From 71278f2abaccea8845b50ef42ed8e3d57d31ede2 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 3 Aug 2026 11:44:13 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20m=E1=BA=A5t=20soundfont=20khi=20ARM=20-?= =?UTF-8?q?=20clamp=20tham=20s=E1=BB=91=20master=20chain=20ch=E1=BA=B7n=20?= =?UTF-8?q?NaN=20l=C3=A0m=20biquad=20state=20bad,=20toggleMasteringOnMaste?= =?UTF-8?q?r=20idempotent,=20loadSoundFont=20retry=20khi=20l=E1=BB=97i=20t?= =?UTF-8?q?ho=C3=A1ng=20qua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 58 +++++++++++++++-------- app/static/js/app.precompiled.js | 25 ++++++---- app/static/js/services/soundfontPlayer.js | 8 +++- app/templates/index.html | 4 +- wiki.md | 5 ++ 5 files changed, 67 insertions(+), 33 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 92f479b..3e7c8c0 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -75,50 +75,58 @@ function makeDistortionCurve(k) { } function applyMasteringSettings(s) { - if (!masterBus || !audioCtx) return; + if (!masterBus || !audioCtx || !s) return; 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 + // 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 ? s.eqLowGain : 0, now, 0.01); - masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive ? s.eqMid1Gain : 0, now, 0.01); - masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive ? s.eqMid2Gain : 0, now, 0.01); - masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive ? s.eqHighGain : 0, now, 0.01); - + 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) const updateImagerBand = (w, active, gainLL, gainRL, gainLR, gainRR) => { - const widthVal = (s.imagerActive && active) ? w : 0; + 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, s.maxGain / 20) : 1.0; + 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 + (s.maxSoftClip / 100) * 10; + const k = 1 + (clamp(s.maxSoftClip, 0, 100) / 100) * 10; masterBus.maximizerSoftClipper.curve = makeDistortionCurve(k); } else { masterBus.maximizerSoftClipper.curve = null; } - + // Upward Compressor - const upwardGainLinear = (s.maximizerActive && s.maxUpward > 0) ? (Math.pow(10, s.maxUpward / 20) - 1.0) : 0.0; + const upwardGainLinear = (s.maximizerActive && s.maxUpward > 0) ? (Math.pow(10, clamp(s.maxUpward, 0, 30) / 20) - 1.0) : 0.0; masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear, now, 0.01); - + // Limiter Threshold - const ceilingVal = s.maximizerActive ? s.ceiling : -0.1; + const ceilingVal = s.maximizerActive ? clamp(s.ceiling, -60, 0) : -0.1; masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal, now, 0.01); } @@ -320,14 +328,22 @@ 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 + // (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 (activate && !isBypassed) { + + if (active) { // Active routing: inputAnalyser -> EQ -> Imager -> Maximizer -> outputAnalyser masterBus.inputAnalyser.connect(masterBus.eqLowFilter); masterBus.maximizerCompressor.connect(masterBus.outputAnalyser); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 2ce2c60..2c0dd58 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -10,13 +10,17 @@ 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 widthVal=s.imagerActive&&active?w: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,s.maxGain/20):1.0;masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear,now,0.01);// Soft Clipper -if(s.maximizerActive&&s.maxSoftClip>0){const k=1+s.maxSoftClip/100*10;masterBus.maximizerSoftClipper.curve=makeDistortionCurve(k);}else{masterBus.maximizerSoftClipper.curve=null;}// Upward Compressor -const upwardGainLinear=s.maximizerActive&&s.maxUpward>0?Math.pow(10,s.maxUpward/20)-1.0:0.0;masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear,now,0.01);// Limiter Threshold -const ceilingVal=s.maximizerActive?s.ceiling:-0.1;masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal,now,0.01);}function initMasterBus(ctx){if(masterBus)return masterBus;// Create EQ filters +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 +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) +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 +const upwardGainLinear=s.maximizerActive&&s.maxUpward>0?Math.pow(10,clamp(s.maxUpward,0,30)/20)-1.0:0.0;masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear,now,0.01);// Limiter Threshold +const ceilingVal=s.maximizerActive?clamp(s.ceiling,-60,0):-0.1;masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal,now,0.01);}function initMasterBus(ctx){if(masterBus)return masterBus;// Create EQ filters const eqLowFilter=ctx.createBiquadFilter();eqLowFilter.type='lowshelf';eqLowFilter.frequency.value=100;const eqMid1Filter=ctx.createBiquadFilter();eqMid1Filter.type='peaking';eqMid1Filter.frequency.value=822;eqMid1Filter.Q.value=0.7;const eqMid2Filter=ctx.createBiquadFilter();eqMid2Filter.type='peaking';eqMid2Filter.frequency.value=3200;eqMid2Filter.Q.value=1.2;const eqHighFilter=ctx.createBiquadFilter();eqHighFilter.type='highshelf';eqHighFilter.frequency.value=10000;// Create Stereo Imager nodes const imagerInput=ctx.createGain();const imagerOutput=ctx.createGain();// Imager Crossover Filters const f1_lp=ctx.createBiquadFilter();f1_lp.type='lowpass';f1_lp.frequency.value=100;const f2_hp=ctx.createBiquadFilter();f2_hp.type='highpass';f2_hp.frequency.value=100;const f2_lp=ctx.createBiquadFilter();f2_lp.type='lowpass';f2_lp.frequency.value=1000;const f3_hp=ctx.createBiquadFilter();f3_hp.type='highpass';f3_hp.frequency.value=1000;const f3_lp=ctx.createBiquadFilter();f3_lp.type='lowpass';f3_lp.frequency.value=6000;const f4_hp=ctx.createBiquadFilter();f4_hp.type='highpass';f4_hp.frequency.value=6000;const split1=ctx.createChannelSplitter(2);const split2=ctx.createChannelSplitter(2);const split3=ctx.createChannelSplitter(2);const split4=ctx.createChannelSplitter(2);const merge1=ctx.createChannelMerger(2);const merge2=ctx.createChannelMerger(2);const merge3=ctx.createChannelMerger(2);const merge4=ctx.createChannelMerger(2);const gainLL1=ctx.createGain();const gainRL1=ctx.createGain();const gainLR1=ctx.createGain();const gainRR1=ctx.createGain();const gainLL2=ctx.createGain();const gainRL2=ctx.createGain();const gainLR2=ctx.createGain();const gainRR2=ctx.createGain();const gainLL3=ctx.createGain();const gainRL3=ctx.createGain();const gainLR3=ctx.createGain();const gainRR3=ctx.createGain();const gainLL4=ctx.createGain();const gainRL4=ctx.createGain();const gainLR4=ctx.createGain();const gainRR4=ctx.createGain();// Connections for Imager DSP @@ -34,8 +38,11 @@ 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);}function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;// Disconnect the dynamic junction -masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnect();if(activate&&!isBypassed){// Active routing: inputAnalyser -> EQ -> Imager -> Maximizer -> outputAnalyser +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 +// (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 } this.recordedNotes=[];this.recStartAudioTime=0.0;this.recStartBar=0.0;this.selectedMidiInputId=null;// Compute round-trip browser latency diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 563b048..a95fa44 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -233,7 +233,13 @@ // times (handles 1,2,3,4…) — wasting the 256MB WASM heap and stalling // notes until each load finishes (audible lag, then silence). if (!_loadPromises[sfId]) { - _loadPromises[sfId] = this._doLoadSoundFont(sfId); + _loadPromises[sfId] = this._doLoadSoundFont(sfId).then(function (ok) { + // Do NOT cache failures: a transient error (network hiccup, + // memory pressure) must not permanently kill the instrument — + // the next note retries the load and recovers. + if (!ok) delete _loadPromises[sfId]; + return ok; + }); } return _loadPromises[sfId]; }, diff --git a/app/templates/index.html b/app/templates/index.html index 3440a4d..94c83fd 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - +