From 94b2d2ef419dc245494d4298ef68ec210e10b036 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 3 Aug 2026 12:26:03 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20c=C3=A2m=20to=C3=A0n=20c=E1=BB=A5c=20+?= =?UTF-8?q?=20BiquadFilterNode=20state=20is=20bad=20-=20g=E1=BB=A1=20maste?= =?UTF-8?q?ring=20kh=E1=BB=8Fi=20getAudioContext=20(ch=E1=BB=89=20=C3=A1p?= =?UTF-8?q?=20d=E1=BB=A5ng=20=E1=BB=9F=20initMasterBus/effect),=20time=20c?= =?UTF-8?q?onstant=20ch=E1=BA=ADm=20h=C6=A1n=20+=20cancelScheduledValues,?= =?UTF-8?q?=20toggleMasteringOnMaster=20lu=C3=B4n=20reconnect=20d=C3=B9=20?= =?UTF-8?q?l=E1=BB=97i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 75 ++++++++++++++++++++++---------- app/static/js/app.precompiled.js | 26 +++++++---- app/templates/index.html | 2 +- wiki.md | 5 +++ 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index b8cc705..a6e28ac 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -95,10 +95,17 @@ function applyMasteringSettings(s) { }; // 1. EQ Settings - masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqLowGain, -24, 24) : 0, now, 0.01); - masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqMid1Gain, -24, 24) : 0, now, 0.01); - masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqMid2Gain, -24, 24) : 0, now, 0.01); - masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqHighGain, -24, 24) : 0, now, 0.01); + // cancelScheduledValues + a slower time constant keeps rapid slider drags + // from piling up automation events on the biquad filters (the trigger for + // Chromium's "BiquadFilterNode: state is bad"). + masterBus.eqLowFilter.gain.cancelScheduledValues(now); + masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqLowGain, -24, 24) : 0, now, 0.05); + masterBus.eqMid1Filter.gain.cancelScheduledValues(now); + masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqMid1Gain, -24, 24) : 0, now, 0.05); + masterBus.eqMid2Filter.gain.cancelScheduledValues(now); + masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqMid2Gain, -24, 24) : 0, now, 0.05); + masterBus.eqHighFilter.gain.cancelScheduledValues(now); + masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive ? clamp(s.eqHighGain, -24, 24) : 0, now, 0.05); // 2. Imager Settings (Mid/Side matrix width control for each band) const updateImagerBand = (w, active, gainLL, gainRL, gainLR, gainRR) => { @@ -329,6 +336,16 @@ function initMasterBus(ctx) { masterBus.analyser.connect(ctx.destination); window.masterBus = masterBus; + // Apply mastering once when the chain is first created (and on the + // masteringSettings effect for subsequent changes — see useEffect). + if (window.currentMasteringSettings) { + try { + toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected, window.currentMasteringSettings.isBypassed); + applyMasteringSettings(window.currentMasteringSettings); + } catch (e) { + console.warn('initMasterBus apply mastering error:', e); + } + } return masterBus; } @@ -342,25 +359,33 @@ 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". + // Idempotent: don't disconnect/reconnect the mastering chain on every call. if (_lastMasteringActive === active && masterBus.masteringActive === active) return; _lastMasteringActive = active; - // Disconnect the dynamic junction - masterBus.inputAnalyser.disconnect(); - masterBus.maximizerCompressor.disconnect(); - - if (active) { - // Active routing: inputAnalyser -> EQ -> Imager -> Maximizer -> outputAnalyser - masterBus.inputAnalyser.connect(masterBus.eqLowFilter); - masterBus.maximizerCompressor.connect(masterBus.outputAnalyser); - masterBus.masteringActive = true; - } else { - // Bypassed routing: inputAnalyser -> outputAnalyser - masterBus.inputAnalyser.connect(masterBus.outputAnalyser); - masterBus.masteringActive = false; + // Disconnect + immediately reconnect in one synchronous block so the master + // routing can NEVER be left broken (a mid-swap exception would otherwise + // disconnect inputAnalyser and silence ALL audio globally). + try { + masterBus.inputAnalyser.disconnect(); + masterBus.maximizerCompressor.disconnect(); + if (active) { + masterBus.inputAnalyser.connect(masterBus.eqLowFilter); + masterBus.maximizerCompressor.connect(masterBus.outputAnalyser); + masterBus.masteringActive = true; + } else { + masterBus.inputAnalyser.connect(masterBus.outputAnalyser); + masterBus.masteringActive = false; + } + } catch (e) { + console.warn('toggleMasteringOnMaster error:', e); + // Restore a guaranteed-valid default routing regardless of the failure. + try { + masterBus.inputAnalyser.disconnect(); + masterBus.maximizerCompressor.disconnect(); + masterBus.inputAnalyser.connect(masterBus.outputAnalyser); + masterBus.masteringActive = false; + } catch (e2) {} } } @@ -377,10 +402,12 @@ function getAudioContext() { if (!masterBus) { initMasterBus(audioCtx); } - if (window.currentMasteringSettings) { - toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected, window.currentMasteringSettings.isBypassed); - applyMasteringSettings(window.currentMasteringSettings); - } + // The mastering chain is ONLY managed by the masteringSettings effect — NOT + // here. getAudioContext runs constantly (play, stopAll, VU, double-click…); + // re-toggling/re-automating the biquad EQ here in bursts is "fast parameter + // automation" that makes Chromium flag the filters as unstable + // ("BiquadFilterNode: state is bad") and can leave the master routing broken + // → global silence. The React effect applies it once per settings change. if (window.SonicSF && window.SonicSF.init) { window.SonicSF.init(audioCtx); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 781ad59..7173325 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -19,7 +19,10 @@ const sig=[s.eqActive,s.eqLowGain,s.eqMid1Gain,s.eqMid2Gain,s.eqHighGain,s.image // chain into a bad state and silences ALL audio (the "mất soundfont" symptom). const clamp=(v,lo,hi)=>{const n=Number(v);if(!isFinite(n))return 0;// missing/NaN → neutral, never a filter-breaking value return Math.min(hi,Math.max(lo,n));};// 1. EQ Settings -masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqLowGain,-24,24):0,now,0.01);masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid1Gain,-24,24):0,now,0.01);masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid2Gain,-24,24):0,now,0.01);masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqHighGain,-24,24):0,now,0.01);// 2. Imager Settings (Mid/Side matrix width control for each band) +// cancelScheduledValues + a slower time constant keeps rapid slider drags +// from piling up automation events on the biquad filters (the trigger for +// Chromium's "BiquadFilterNode: state is bad"). +masterBus.eqLowFilter.gain.cancelScheduledValues(now);masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqLowGain,-24,24):0,now,0.05);masterBus.eqMid1Filter.gain.cancelScheduledValues(now);masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid1Gain,-24,24):0,now,0.05);masterBus.eqMid2Filter.gain.cancelScheduledValues(now);masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid2Gain,-24,24):0,now,0.05);masterBus.eqHighFilter.gain.cancelScheduledValues(now);masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqHighGain,-24,24):0,now,0.05);// 2. Imager Settings (Mid/Side matrix width control for each band) const updateImagerBand=(w,active,gainLL,gainRL,gainLR,gainRR)=>{const widthVal=s.imagerActive&&active?clamp(w,-100,100):0;const g1=1+widthVal/200;const g2=-widthVal/200;gainLL.gain.setTargetAtTime(g1,now,0.01);gainRR.gain.setTargetAtTime(g1,now,0.01);gainRL.gain.setTargetAtTime(g2,now,0.01);gainLR.gain.setTargetAtTime(g2,now,0.01);};updateImagerBand(s.w1,true,masterBus.gainLL1,masterBus.gainRL1,masterBus.gainLR1,masterBus.gainRR1);updateImagerBand(s.w2,true,masterBus.gainLL2,masterBus.gainRL2,masterBus.gainLR2,masterBus.gainRR2);updateImagerBand(s.w3,true,masterBus.gainLL3,masterBus.gainRL3,masterBus.gainLR3,masterBus.gainRR3);updateImagerBand(s.w4,true,masterBus.gainLL4,masterBus.gainRL4,masterBus.gainLR4,masterBus.gainRR4);// 3. Maximizer Settings const boostLinear=s.maximizerActive?Math.pow(10,clamp(s.maxGain,-60,30)/20):1.0;masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear,now,0.01);// Soft Clipper if(s.maximizerActive&&s.maxSoftClip>0){const k=1+clamp(s.maxSoftClip,0,100)/100*10;masterBus.maximizerSoftClipper.curve=makeDistortionCurve(k);}else{masterBus.maximizerSoftClipper.curve=null;}// Upward Compressor @@ -42,13 +45,20 @@ 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;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 -masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnect();if(active){// Active routing: inputAnalyser -> EQ -> Imager -> Maximizer -> outputAnalyser -masterBus.inputAnalyser.connect(masterBus.eqLowFilter);masterBus.maximizerCompressor.connect(masterBus.outputAnalyser);masterBus.masteringActive=true;}else{// Bypassed routing: inputAnalyser -> outputAnalyser -masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.masteringActive=false;}}function getAudioContext(){if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)();if(window.SonicAudio&&window.SonicAudio.initAudioWorklet){window.SonicAudio.initAudioWorklet();}}if(audioCtx.state==='suspended'){audioCtx.resume();}if(!masterBus){initMasterBus(audioCtx);}if(window.currentMasteringSettings){toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}return audioCtx;}const formatTime=secs=>{if(isNaN(secs)||secs<0)return"0:00.000";const m=Math.floor(secs/60);const s=Math.floor(secs%60);const ms=Math.floor(secs%1*1000).toString().padStart(3,'0');return`${m}:${s.toString().padStart(2,'0')}.${ms}`;};const formatTimeSimple=secs=>{if(isNaN(secs)||secs<0)return"0.00s";return`${secs.toFixed(2)}s`;};const formatBeat=(secs,bpmVal)=>{if(isNaN(secs)||secs<0)return"0.1.1";const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const bar=Math.floor(secs/barDuration);const beat=Math.floor(secs%barDuration/beatDuration)+1;const sub=Math.floor(secs%beatDuration/(beatDuration/4))+1;return`${bar}.${beat}.${sub}`;};const midiPitchToName=pitch=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;return names[pitch%12]+octave;};const getBeatMarkers=(maxDur,bpmVal)=>{const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const markers=[];for(let t=0;t<=maxDur;t+=beatDuration){const isBar=Math.abs(t%barDuration)<0.001||Math.abs(t%barDuration-barDuration)<0.001;markers.push({time:t,isBar,beatNum:Math.floor(t/beatDuration)+1});}return markers;};const findZeroCrossing=(buffer,targetTime)=>{if(!buffer)return targetTime;const sampleRate=buffer.sampleRate;const data=buffer.getChannelData(0);const targetSample=Math.floor(targetTime*sampleRate);const windowSize=Math.floor(0.04*sampleRate);const start=Math.max(0,targetSample-windowSize);const end=Math.min(data.length-2,targetSample+windowSize);let bestSample=targetSample;let minDistance=Infinity;for(let i=start;i<=end;i++){if(data[i]>=0&&data[i+1]<=0||data[i]<=0&&data[i+1]>=0){const dist=Math.abs(i-targetSample);if(dist { noteId, startBeat, velocity } +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;// Apply mastering once when the chain is first created (and on the +// masteringSettings effect for subsequent changes — see useEffect). +if(window.currentMasteringSettings){try{toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}catch(e){console.warn('initMasterBus apply mastering error:',e);}}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. +if(_lastMasteringActive===active&&masterBus.masteringActive===active)return;_lastMasteringActive=active;// Disconnect + immediately reconnect in one synchronous block so the master +// routing can NEVER be left broken (a mid-swap exception would otherwise +// disconnect inputAnalyser and silence ALL audio globally). +try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnect();if(active){masterBus.inputAnalyser.connect(masterBus.eqLowFilter);masterBus.maximizerCompressor.connect(masterBus.outputAnalyser);masterBus.masteringActive=true;}else{masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.masteringActive=false;}}catch(e){console.warn('toggleMasteringOnMaster error:',e);// Restore a guaranteed-valid default routing regardless of the failure. +try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnect();masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.masteringActive=false;}catch(e2){}}}function getAudioContext(){if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)();if(window.SonicAudio&&window.SonicAudio.initAudioWorklet){window.SonicAudio.initAudioWorklet();}}if(audioCtx.state==='suspended'){audioCtx.resume();}if(!masterBus){initMasterBus(audioCtx);}// The mastering chain is ONLY managed by the masteringSettings effect — NOT +// here. getAudioContext runs constantly (play, stopAll, VU, double-click…); +// re-toggling/re-automating the biquad EQ here in bursts is "fast parameter +// automation" that makes Chromium flag the filters as unstable +// ("BiquadFilterNode: state is bad") and can leave the master routing broken +// → global silence. The React effect applies it once per settings change. +if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}return audioCtx;}const formatTime=secs=>{if(isNaN(secs)||secs<0)return"0:00.000";const m=Math.floor(secs/60);const s=Math.floor(secs%60);const ms=Math.floor(secs%1*1000).toString().padStart(3,'0');return`${m}:${s.toString().padStart(2,'0')}.${ms}`;};const formatTimeSimple=secs=>{if(isNaN(secs)||secs<0)return"0.00s";return`${secs.toFixed(2)}s`;};const formatBeat=(secs,bpmVal)=>{if(isNaN(secs)||secs<0)return"0.1.1";const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const bar=Math.floor(secs/barDuration);const beat=Math.floor(secs%barDuration/beatDuration)+1;const sub=Math.floor(secs%beatDuration/(beatDuration/4))+1;return`${bar}.${beat}.${sub}`;};const midiPitchToName=pitch=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;return names[pitch%12]+octave;};const getBeatMarkers=(maxDur,bpmVal)=>{const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const markers=[];for(let t=0;t<=maxDur;t+=beatDuration){const isBar=Math.abs(t%barDuration)<0.001||Math.abs(t%barDuration-barDuration)<0.001;markers.push({time:t,isBar,beatNum:Math.floor(t/beatDuration)+1});}return markers;};const findZeroCrossing=(buffer,targetTime)=>{if(!buffer)return targetTime;const sampleRate=buffer.sampleRate;const data=buffer.getChannelData(0);const targetSample=Math.floor(targetTime*sampleRate);const windowSize=Math.floor(0.04*sampleRate);const start=Math.max(0,targetSample-windowSize);const end=Math.min(data.length-2,targetSample+windowSize);let bestSample=targetSample;let minDistance=Infinity;for(let i=start;i<=end;i++){if(data[i]>=0&&data[i+1]<=0||data[i]<=0&&data[i+1]>=0){const dist=Math.abs(i-targetSample);if(dist { noteId, startBeat, velocity } this.recordedNotes=[];this.recStartAudioTime=0.0;this.recStartBar=0.0;this.selectedMidiInputId=null;// Compute round-trip browser latency this.latencyCompSec=(this.audioCtx.baseLatency||0)+(this.audioCtx.outputLatency||0);}start(startBar=0.0,selectedMidiInputId=null){this.isRecording=true;this.recordedNotes=[];this.activeNotes.clear();this.recStartBar=startBar;this.recStartAudioTime=this.audioCtx.currentTime;this.selectedMidiInputId=selectedMidiInputId;}handleMIDIMessage(event,sourceInputId=null){if(!this.isRecording)return;if(this.selectedMidiInputId&&this.selectedMidiInputId!=='ALL'&&sourceInputId&&sourceInputId!==this.selectedMidiInputId){console.log(`[DevLog] [MIDI Rec] Ignoring input message from "${sourceInputId}" (Selected: "${this.selectedMidiInputId}")`);return;}const[status,pitch,velocity]=event.data;const command=status>>4;// Apply latency compensation formula const currentTimeSec=Math.max(0,this.audioCtx.currentTime-this.recStartAudioTime-this.latencyCompSec);const secondsPerBeat=60.0/this.bpm;const currentBeat=currentTimeSec/secondsPerBeat;// Command 0x9: Note On diff --git a/app/templates/index.html b/app/templates/index.html index 276c5ff..7b69ea7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +