fix: mất soundfont khi ARM - clamp tham số master chain chặn NaN làm biquad state bad, toggleMasteringOnMaster idempotent, loadSoundFont retry khi lỗi thoáng qua

This commit is contained in:
2026-08-03 11:44:13 +07:00
parent 022ba38a5e
commit 71278f2aba
5 changed files with 67 additions and 33 deletions
+37 -21
View File
@@ -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 "mt 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);
+16 -9
View File
@@ -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<n_samples;++i){const x=i*2/n_samples-1;curve[i]=Math.atan(x*k)/(Math.atan(k)||1);}return curve;}function applyMasteringSettings(s){if(!masterBus||!audioCtx)return;const now=audioCtx.currentTime;// 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);// 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 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<n_samples;++i){const x=i*2/n_samples-1;curve[i]=Math.atan(x*k)/(Math.atan(k)||1);}return curve;}function applyMasteringSettings(s){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?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<minDistance){minDistance=dist;bestSample=i;}}}return bestSample/sampleRate;};class ClientMIDIRecorder{constructor(audioContext,bpm=120,timeSigNumerator=4){this.audioCtx=audioContext;this.bpm=bpm;this.timeSigNum=timeSigNumerator;this.isRecording=false;this.tempMidiItemId=null;this.activeNotes=new Map();// Store pitch -> { noteId, startBeat, velocity }
this.recordedNotes=[];this.recStartAudioTime=0.0;this.recStartBar=0.0;this.selectedMidiInputId=null;// Compute round-trip browser latency
+7 -1
View File
@@ -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];
},
+2 -2
View File
@@ -16,7 +16,7 @@
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608031245"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608031315"></script>
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608031300" defer></script>
<script src="/static/js/app.precompiled.js?v=202608031315" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {