diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 3e7c8c0..d8aea87 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -76,6 +76,14 @@ function makeDistortionCurve(k) { function applyMasteringSettings(s) { if (!masterBus || !audioCtx || !s) return; + // getAudioContext() invokes this on EVERY call (stopAll, play, VU, …). + // Re-applying identical values in rapid bursts is "fast parameter automation" + // and makes Chromium flag the biquad EQ filters as unstable ("state is bad"). + // Only touch the graph when a value actually changed. + const sig = [s.eqActive, s.eqLowGain, s.eqMid1Gain, s.eqMid2Gain, s.eqHighGain, s.imagerActive, s.w1, s.w2, s.w3, s.w4, s.maximizerActive, s.maxGain, s.maxSoftClip, s.maxUpward, s.ceiling].join('|'); + if (sig === _lastMasteringSig) return; + _lastMasteringSig = sig; + const now = audioCtx.currentTime; // Clamp every parameter so a stale/incomplete settings object can never push // NaN or an extreme value into the biquad filters — that puts the master @@ -329,6 +337,7 @@ function setMasterVolume(linear) { } let _lastMasteringActive = null; +let _lastMasteringSig = null; function toggleMasteringOnMaster(activate, isBypassed) { if (!masterBus) return; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 2c0dd58..c037ef0 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -10,7 +10,11 @@ const trackMidiChannelsRef={current:{}};const ensureTrackMidiChannel=(track,trac (function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive } -function makeDistortionCurve(k){const n_samples=44100;const curve=new Float32Array(n_samples);for(let i=0;i{const n=Number(v);if(!isFinite(n))return 0;// missing/NaN → neutral, never a filter-breaking value @@ -38,7 +42,7 @@ eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filte eqHighFilter.connect(imagerInput);// Connect Imager to Maximizer imagerOutput.connect(maximizerBoostGain);// Setup default non-mastered routing: // input -> compressor -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination -masterBus.input.connect(masterBus.compressor);masterBus.compressor.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.output);masterBus.output.connect(masterBus.analyser);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}let _lastMasteringActive=null;function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;const active=!!(activate&&!isBypassed);// Idempotent: don't disconnect/reconnect the mastering chain on every call +masterBus.input.connect(masterBus.compressor);masterBus.compressor.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.output);masterBus.output.connect(masterBus.analyser);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}let _lastMasteringActive=null;let _lastMasteringSig=null;function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;const active=!!(activate&&!isBypassed);// Idempotent: don't disconnect/reconnect the mastering chain on every call // (getAudioContext invokes this constantly). Rapid re-connection while audio // flows destabilizes the biquad filters → "BiquadFilterNode: state is bad". if(_lastMasteringActive===active&&masterBus.masteringActive===active)return;_lastMasteringActive=active;// Disconnect the dynamic junction diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index a95fa44..ce9b7a2 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -86,14 +86,15 @@ console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate); - // ScriptProcessor is the proven-stable renderer in this app. - // The AudioWorklet path (fluidsynth-bridge) is implemented and - // mono-safe, but it caused device-specific audio failures, so - // keep it opt-in (flip to false to try it) — the deprecation - // console warning is cosmetic. - var _useScriptNode = true; + // Use the AudioWorklet (fluidsynth-bridge) — ScriptProcessor + // is deprecated and logs a console warning. The worklet is + // now mono-safe (handles 1/2/N channel outputs) and the node + // is forced stereo, and the URL is cache-busted so the fixed + // module always loads. Falls back to ScriptProcessor if the + // worklet cannot be created (older browsers). + var _useScriptNode = false; try { - await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js?v=202608031240'); + await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js?v=202608031315'); console.log("[SonicSF] Worklet registered OK"); } catch (e) { console.warn("[SonicSF] Worklet reg failed:", e); diff --git a/app/templates/index.html b/app/templates/index.html index 94c83fd..fbe7a10 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - +