constboostLinear=s.maximizerActive?Math.pow(10,clamp(s.maxGain,-60,30)/20):1.0;masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear,now,0.01);// Soft Clipper (identity passthrough when off — never null curve)
constboostLinear=s.maximizerActive?Math.pow(10,clamp(s.maxGain,-60,30)/20):1.0;masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear,now,0.01);// Soft Clipper (identity passthrough when off — never null curve)
if(masterBus.excWet){constexcOn=!!s.excActive;constwetAmt=excOn?clamp(s.excDrive,0,100)/100:0;masterBus.excWet.gain.setTargetAtTime(wetAmt*0.6,now,0.02);masterBus.excDry.gain.setTargetAtTime(1.0,now,0.02);}// 7. Master Rebalance module (M/S gains via L/R crossfeed)
if(masterBus.excWet){constexcOn=!!s.excActive;constwetAmt=excOn?clamp(s.excDrive,0,100)/100:0;masterBus.excWet.gain.setTargetAtTime(wetAmt*0.6,now,0.02);masterBus.excDry.gain.setTargetAtTime(1.0,now,0.02);}// 7. Master Rebalance module (M/S gains via L/R crossfeed)
// L' = a·L + b·R, R' = b·L + a·R with a=(mid+side)/2, b=(mid−side)/2
// L' = a·L + b·R, R' = b·L + a·R with a=(mid+side)/2, b=(mid−side)/2
if(masterBus.gLLr){constrebOn=!!s.rebalActive;constmidLin=rebOn?Math.pow(10,clamp(s.rebalMid,-24,24)/20):1.0;constsideLin=rebOn?Math.pow(10,clamp(s.rebalSide,-24,24)/20):1.0;consta=(midLin+sideLin)/2;constb=(midLin-sideLin)/2;masterBus.gLLr.gain.setTargetAtTime(a,now,0.01);masterBus.gRRr.gain.setTargetAtTime(a,now,0.01);masterBus.gRLr.gain.setTargetAtTime(b,now,0.01);masterBus.gLRr.gain.setTargetAtTime(b,now,0.01);}}functioninitMasterBus(ctx){if(masterBus)returnmasterBus;// Clamp every filter frequency below Nyquist (0.45 * sampleRate). A biquad
if(masterBus.gLLr){constrebOn=!!s.rebalActive;constmidLin=rebOn?Math.pow(10,clamp(s.rebalMid,-24,24)/20):1.0;constsideLin=rebOn?Math.pow(10,clamp(s.rebalSide,-24,24)/20):1.0;consta=(midLin+sideLin)/2;constb=(midLin-sideLin)/2;masterBus.gLLr.gain.setTargetAtTime(a,now,0.01);masterBus.gRRr.gain.setTargetAtTime(a,now,0.01);masterBus.gRLr.gain.setTargetAtTime(b,now,0.01);masterBus.gLRr.gain.setTargetAtTime(b,now,0.01);}}functioninitMasterBus(ctx){if(masterBus)returnmasterBus;// Clamp every filter frequency below Nyquist (0.45 * sampleRate). A biquad
constmaximizerBoostGain=ctx.createGain();constmaximizerSoftClipper=ctx.createWaveShaper();// NEVER leave the curve null: a WaveShaper with a null/identity curve can
constmaximizerBoostGain=ctx.createGain();constmaximizerSoftClipper=ctx.createWaveShaper();// NEVER leave the curve null: a WaveShaper with a null/identity curve can
// output silence in some engines, which would kill the whole mastering path.
// output silence in some engines, which would kill the whole mastering path.
// Use an explicit linear identity table for passthrough.
// Use an explicit linear identity table for passthrough.
maximizerSoftClipper.curve=newFloat32Array([-1,1]);maximizerSoftClipper.oversample='4x';constupwardCompressor=ctx.createDynamicsCompressor();upwardCompressor.threshold.value=-30;upwardCompressor.knee.value=10;upwardCompressor.ratio.value=4;upwardCompressor.attack.value=0.01;upwardCompressor.release.value=0.1;constupwardGain=ctx.createGain();upwardGain.gain.value=0.0;constupwardSummingGain=ctx.createGain();maximizerBoostGain.connect(maximizerSoftClipper);maximizerSoftClipper.connect(upwardSummingGain);maximizerBoostGain.connect(upwardCompressor);upwardCompressor.connect(upwardGain);upwardGain.connect(upwardSummingGain);constmaximizerCompressor=ctx.createDynamicsCompressor();maximizerCompressor.threshold.value=-0.1;maximizerCompressor.knee.value=0.0;maximizerCompressor.ratio.value=20.0;maximizerCompressor.attack.value=0.001;maximizerCompressor.release.value=0.05;upwardSummingGain.connect(maximizerCompressor);// ── Bus Compressor module (mastering_expand.md §II.2) ──
maximizerSoftClipper.curve=newFloat32Array([-1,1]);maximizerSoftClipper.oversample='4x';constupwardCompressor=ctx.createDynamicsCompressor();upwardCompressor.threshold.value=-30;upwardCompressor.knee.value=10;upwardCompressor.ratio.value=4;upwardCompressor.attack.value=0.01;upwardCompressor.release.value=0.1;constupwardGain=ctx.createGain();upwardGain.gain.value=0.0;constupwardSummingGain=ctx.createGain();maximizerBoostGain.connect(maximizerSoftClipper);maximizerSoftClipper.connect(upwardSummingGain);maximizerBoostGain.connect(upwardCompressor);upwardCompressor.connect(upwardGain);upwardGain.connect(upwardSummingGain);// Brickwall Limiter tại ceiling: WaveShaper HARD CLIP (slope 1 — không boost,
constinputAnalyser=ctx.createAnalyser();inputAnalyser.fftSize=2048;constoutputAnalyser=ctx.createAnalyser();outputAnalyser.fftSize=2048;// Global fader / output
constinputAnalyser=ctx.createAnalyser();inputAnalyser.fftSize=2048;constoutputAnalyser=ctx.createAnalyser();outputAnalyser.fftSize=2048;// Global fader / output
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
// tín hiệu → âm nhỏ/méo, vừa (b) phát NaN khi gặp bass transient → 11 biquad
// "state is bad" → CÂM + stuck. Mastering chain có comp/lim module riêng khi bật.
masterBus.input.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).
// masteringSettings effect for subsequent changes — see useEffect).
// output), + post-module analyser for the realtime FFT spectrum overlay.
// output), + post-module analyser for the realtime FFT spectrum overlay.
functioncreateEqProModule(ctx,params){constinput=ctx.createGain();constoutput=ctx.createGain();constanalyser=ctx.createAnalyser();analyser.fftSize=2048;output.connect(analyser);constbands=params&&Array.isArray(params.bands)?JSON.parse(JSON.stringify(params.bands)):JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));constfilters=[];letamount=params&¶ms.amount!==undefined?params.amount:100;constrebuild=()=>{try{input.disconnect();}catch(e){}filters.forEach(f=>{try{f.disconnect();}catch(e){}});filters.length=0;lettail=input;constmaxF=(ctx.sampleRate||44100)*0.45;bands.forEach(b=>{constf=ctx.createBiquadFilter();f.type=b.type||'peaking';f.frequency.value=eqproClamp(b.freq!==undefined?b.freq:1000,EQPRO_F_MIN,Math.min(EQPRO_F_MAX,maxF));f.Q.value=eqproClamp(b.q!==undefined?b.q:1,0.1,18);f.gain.value=b.active!==false?(b.gain||0)*amount/100:0;tail.connect(f);tail=f;filters.push(f);});tail.connect(output);};rebuild();constsetBand=(i,patch)=>{constb=bands[i];if(!b)return;Object.assign(b,patch);constf=filters[i];if(!f)return;constnow=ctx.currentTime;constmaxF=(ctx.sampleRate||44100)*0.45;if(patch.type!==undefined)f.type=patch.type;if(patch.freq!==undefined)f.frequency.setValueAtTime(eqproClamp(b.freq,EQPRO_F_MIN,Math.min(EQPRO_F_MAX,maxF)),now);if(patch.q!==undefined)f.Q.setValueAtTime(eqproClamp(b.q,0.1,18),now);if(patch.gain!==undefined||patch.active!==undefined)f.gain.setValueAtTime(b.active!==false?(b.gain||0)*amount/100:0,now);};constsetAmount=a=>{amount=eqproClamp(a,0,200);constnow=ctx.currentTime;filters.forEach((f,i)=>{constb=bands[i];if(b)f.gain.setValueAtTime(b.active!==false?(b.gain||0)*amount/100:0,now);});};// Replace the internal band model + rebuild DSP immediately (add/delete/reset
functioncreateEqProModule(ctx,params){constinput=ctx.createGain();constoutput=ctx.createGain();constanalyser=ctx.createAnalyser();analyser.fftSize=2048;output.connect(analyser);constbands=params&&Array.isArray(params.bands)?JSON.parse(JSON.stringify(params.bands)):JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));constfilters=[];letamount=params&¶ms.amount!==undefined?params.amount:100;constrebuild=()=>{try{input.disconnect();}catch(e){}filters.forEach(f=>{try{f.disconnect();}catch(e){}});filters.length=0;lettail=input;constmaxF=(ctx.sampleRate||44100)*0.45;bands.forEach(b=>{constf=ctx.createBiquadFilter();f.type=b.type||'peaking';f.frequency.value=eqproClamp(b.freq!==undefined?b.freq:1000,EQPRO_F_MIN,Math.min(EQPRO_F_MAX,maxF));f.Q.value=eqproClamp(b.q!==undefined?b.q:1,0.1,18);f.gain.value=b.active!==false?(b.gain||0)*amount/100:0;tail.connect(f);tail=f;filters.push(f);});tail.connect(output);};rebuild();constsetBand=(i,patch)=>{constb=bands[i];if(!b)return;Object.assign(b,patch);constf=filters[i];if(!f)return;constnow=ctx.currentTime;constmaxF=(ctx.sampleRate||44100)*0.45;if(patch.type!==undefined)f.type=patch.type;if(patch.freq!==undefined)f.frequency.setValueAtTime(eqproClamp(b.freq,EQPRO_F_MIN,Math.min(EQPRO_F_MAX,maxF)),now);if(patch.q!==undefined)f.Q.setValueAtTime(eqproClamp(b.q,0.1,18),now);if(patch.gain!==undefined||patch.active!==undefined)f.gain.setValueAtTime(b.active!==false?(b.gain||0)*amount/100:0,now);};constsetAmount=a=>{amount=eqproClamp(a,0,200);constnow=ctx.currentTime;filters.forEach((f,i)=>{constb=bands[i];if(b)f.gain.setValueAtTime(b.active!==false?(b.gain||0)*amount/100:0,now);});};// Replace the internal band model + rebuild DSP immediately (add/delete/reset
// from the UI) — guarantees the audible result matches the added bands at once.
// from the UI) — guarantees the audible result matches the added bands at once.
constsyncBands=newBands=>{bands.length=0;(newBands||[]).forEach(b=>bands.push({...b}));rebuild();};return{type:'eqpro',input,output,analyser,getbands(){returnbands;},getfilters(){returnfilters;},getamount(){returnamount;},setBand,setAmount,rebuild,syncBands,destroy(){try{input.disconnect();output.disconnect();analyser.disconnect();}catch(e){}}};}functioncreateTrackFxModule(type,ctx,params){constnum=(v,def)=>{constn=Number(v);returnisFinite(n)&&!isNaN(n)?n:def;};constmaxF=(ctx.sampleRate||44100)*0.45;constclampF=v=>Math.max(20,Math.min(v,maxF));constinput=ctx.createGain();constoutput=ctx.createGain();constp=params||{};letnodes={};if(type==='compressor'){constcomp=ctx.createDynamicsCompressor();comp.threshold.value=num(p.threshold,-16);comp.knee.value=8;comp.ratio.value=num(p.ratio,3);comp.attack.value=0.02;comp.release.value=0.25;constmakeup=ctx.createGain();makeup.gain.value=Math.pow(10,num(p.makeup,0)/20);input.connect(comp);comp.connect(makeup);makeup.connect(output);nodes={comp,makeup};}elseif(type==='limiter'){constlim=ctx.createDynamicsCompressor();lim.threshold.value=num(p.ceiling,-1.0);lim.knee.value=0;lim.ratio.value=20;lim.attack.value=0.001;lim.release.value=0.05;input.connect(lim);lim.connect(output);nodes={lim};}elseif(type==='exciter'){consthp=ctx.createBiquadFilter();hp.type='highpass';hp.frequency.value=clampF(2000);hp.Q.value=0.7;constshaper=ctx.createWaveShaper();shaper.curve=makeDistortionCurve(3);shaper.oversample='4x';constdry=ctx.createGain();dry.gain.value=1.0;constwet=ctx.createGain();wet.gain.value=num(p.drive,40)/100*0.6;input.connect(dry);dry.connect(output);input.connect(hp);hp.connect(shaper);shaper.connect(wet);wet.connect(output);nodes={hp,shaper,dry,wet};}elseif(type==='rebalance'){constsplit=ctx.createChannelSplitter(2);constmerge=ctx.createChannelMerger(2);constgLL=ctx.createGain(),gRL=ctx.createGain(),gLR=ctx.createGain(),gRR=ctx.createGain();constmidLin=Math.pow(10,num(p.mid,0)/20);constsideLin=Math.pow(10,num(p.side,0)/20);consta=(midLin+sideLin)/2,b=(midLin-sideLin)/2;gLL.gain.value=a;gRR.gain.value=a;gRL.gain.value=b;gLR.gain.value=b;input.connect(split);split.connect(gLL,0);split.connect(gRL,0);split.connect(gLR,1);split.connect(gRR,1);gLL.connect(merge,0,0);gRL.connect(merge,0,0);gLR.connect(merge,0,1);gRR.connect(merge,0,1);merge.connect(output);nodes={gLL,gRL,gLR,gRR};}elseif(type==='eqpro'){returncreateEqProModule(ctx,params);}else{// 'eq' or default: 4-band EQ (params.g1..g4 = band gains in dB)
constsyncBands=newBands=>{bands.length=0;(newBands||[]).forEach(b=>bands.push({...b}));rebuild();};return{type:'eqpro',input,output,analyser,getbands(){returnbands;},getfilters(){returnfilters;},getamount(){returnamount;},setBand,setAmount,rebuild,syncBands,destroy(){try{input.disconnect();output.disconnect();analyser.disconnect();}catch(e){}}};}functioncreateTrackFxModule(type,ctx,params){constnum=(v,def)=>{constn=Number(v);returnisFinite(n)&&!isNaN(n)?n:def;};constmaxF=(ctx.sampleRate||44100)*0.45;constclampF=v=>Math.max(20,Math.min(v,maxF));constinput=ctx.createGain();constoutput=ctx.createGain();constp=params||{};letnodes={};if(type==='compressor'){constcomp=ctx.createDynamicsCompressor();comp.threshold.value=num(p.threshold,-16);comp.knee.value=8;comp.ratio.value=num(p.ratio,3);comp.attack.value=0.02;comp.release.value=0.25;constmakeup=ctx.createGain();makeup.gain.value=Math.pow(10,num(p.makeup,0)/20);input.connect(comp);comp.connect(makeup);makeup.connect(output);nodes={comp,makeup};}elseif(type==='limiter'){// Brickwall Limiter bằng WaveShaper tanh soft-clip — KHÔNG DynamicsCompressor:
// Chromium compressor phát NaN với bass transient mạnh (pitch thấp + vel cao
// đồng loạt) → NaN vào master chain → 11 biquad "state is bad" → CÂM + stuck.
constshaper=ctx.createWaveShaper();shaper.oversample='2x';constceilingDb=Math.min(0,num(p.ceiling,-1.0));constthreshLin=Math.pow(10,ceilingDb/20);constk=1/Math.max(0.02,threshLin);const_curve=newFloat32Array(4096);const_tanhK=Math.tanh(k);for(let_i=0;_i<4096;_i++){const_x=_i/4095*2-1;_curve[_i]=Math.tanh(_x*k)/_tanhK;}shaper.curve=_curve;constmakeup=ctx.createGain();makeup.gain.value=1.0;input.connect(shaper);shaper.connect(makeup);makeup.connect(output);nodes={shaper,makeup};}elseif(type==='exciter'){consthp=ctx.createBiquadFilter();hp.type='highpass';hp.frequency.value=clampF(2000);hp.Q.value=0.7;constshaper=ctx.createWaveShaper();shaper.curve=makeDistortionCurve(3);shaper.oversample='4x';constdry=ctx.createGain();dry.gain.value=1.0;constwet=ctx.createGain();wet.gain.value=num(p.drive,40)/100*0.6;input.connect(dry);dry.connect(output);input.connect(hp);hp.connect(shaper);shaper.connect(wet);wet.connect(output);nodes={hp,shaper,dry,wet};}elseif(type==='rebalance'){constsplit=ctx.createChannelSplitter(2);constmerge=ctx.createChannelMerger(2);constgLL=ctx.createGain(),gRL=ctx.createGain(),gLR=ctx.createGain(),gRR=ctx.createGain();constmidLin=Math.pow(10,num(p.mid,0)/20);constsideLin=Math.pow(10,num(p.side,0)/20);consta=(midLin+sideLin)/2,b=(midLin-sideLin)/2;gLL.gain.value=a;gRR.gain.value=a;gRL.gain.value=b;gLR.gain.value=b;input.connect(split);split.connect(gLL,0);split.connect(gRL,0);split.connect(gLR,1);split.connect(gRR,1);gLL.connect(merge,0,0);gRL.connect(merge,0,0);gLR.connect(merge,0,1);gRR.connect(merge,0,1);merge.connect(output);nodes={gLL,gRL,gLR,gRR};}elseif(type==='eqpro'){returncreateEqProModule(ctx,params);}else{// 'eq' or default: 4-band EQ (params.g1..g4 = band gains in dB)
constf1=ctx.createBiquadFilter();f1.type='lowshelf';f1.frequency.value=clampF(100);constf2=ctx.createBiquadFilter();f2.type='peaking';f2.frequency.value=clampF(800);f2.Q.value=0.7;constf3=ctx.createBiquadFilter();f3.type='peaking';f3.frequency.value=clampF(3200);f3.Q.value=1.2;constf4=ctx.createBiquadFilter();f4.type='highshelf';f4.frequency.value=clampF(10000);f1.gain.value=num(p.g1,0);f2.gain.value=num(p.g2,0);f3.gain.value=num(p.g3,0);f4.gain.value=num(p.g4,0);input.connect(f1);f1.connect(f2);f2.connect(f3);f3.connect(f4);f4.connect(output);nodes={f1,f2,f3,f4};}return{input,output,nodes,type};}// Offline-export helper: builds a track node IDENTICAL to playback (track FX
constf1=ctx.createBiquadFilter();f1.type='lowshelf';f1.frequency.value=clampF(100);constf2=ctx.createBiquadFilter();f2.type='peaking';f2.frequency.value=clampF(800);f2.Q.value=0.7;constf3=ctx.createBiquadFilter();f3.type='peaking';f3.frequency.value=clampF(3200);f3.Q.value=1.2;constf4=ctx.createBiquadFilter();f4.type='highshelf';f4.frequency.value=clampF(10000);f1.gain.value=num(p.g1,0);f2.gain.value=num(p.g2,0);f3.gain.value=num(p.g3,0);f4.gain.value=num(p.g4,0);input.connect(f1);f1.connect(f2);f2.connect(f3);f3.connect(f4);f4.connect(output);nodes={f1,f2,f3,f4};}return{input,output,nodes,type};}// Offline-export helper: builds a track node IDENTICAL to playback (track FX
// chain + mastering route + legacy chorus/reverb) but inside an OfflineAudioContext,
// chain + mastering route + legacy chorus/reverb) but inside an OfflineAudioContext,
// keyed in a LOCAL map so the live graph is never touched. Audio clips are then
// keyed in a LOCAL map so the live graph is never touched. Audio clips are then
constctxNow=getAudioContext().currentTime;letanyPlaying=false;if(isPianoRoll){anyPlaying=_anySubPlaying;}elseif(_anySubPlaying){const_subs=subTabsRef.current||[];for(let_si=0;_si<_subs.length;_si++){consts=_subs[_si];if(!s.isPlaying)continue;if(s.buffer){if(activeSourcesRef.current.some(src=>typeofsrc.startTime==='number'&&ctxNow>=src.startTime&&ctxNow<=src.startTime+(src.buffer?src.buffer.duration:0)+0.1)){anyPlaying=true;break;}}}}else{anyPlaying=activeSourcesRef.current.some(s=>typeofs.startTime==='number'&&ctxNow>=s.startTime&&ctxNow<=s.startTime+(s.buffer?s.buffer.duration:0)+0.1);}if(anyPlaying){constd=newUint8Array(128);masterBus.analyser.getByteTimeDomainData(d);letpk=0;for(leti=0;i<d.length;i++){constv=Math.abs(d[i]-128)/128;if(v>pk)pk=v;}// PIANO_ROLL: chain chết xuất NaN → getByteTimeDomainData đọc NaN →
constctxNow=getAudioContext().currentTime;letanyPlaying=false;if(isPianoRoll){anyPlaying=_anySubPlaying;}elseif(_anySubPlaying){const_subs=subTabsRef.current||[];for(let_si=0;_si<_subs.length;_si++){consts=_subs[_si];if(!s.isPlaying)continue;if(s.buffer){if(activeSourcesRef.current.some(src=>typeofsrc.startTime==='number'&&ctxNow>=src.startTime&&ctxNow<=src.startTime+(src.buffer?src.buffer.duration:0)+0.1)){anyPlaying=true;break;}}}}else{anyPlaying=activeSourcesRef.current.some(s=>typeofs.startTime==='number'&&ctxNow>=s.startTime&&ctxNow<=s.startTime+(s.buffer?s.buffer.duration:0)+0.1);}if(anyPlaying){constd=newUint8Array(128);masterBus.analyser.getByteTimeDomainData(d);letpk=0;for(leti=0;i<d.length;i++){constv=Math.abs(d[i]-128)/128;if(v>pk)pk=v;}// PIANO_ROLL: chain chết xuất NaN → getByteTimeDomainData đọc NaN →
// byte 0/128 → pk CAO → mù. Check float data (chỉ piano roll — rests
// byte 0/128 → pk CAO → mù. Check float data (chỉ piano roll — rests
// tự nhiên thì KHÔNG rebuild).
// tự nhiên thì KHÔNG rebuild).
letnanOut=false;if(isPianoRoll){try{constf=newFloat32Array(128);masterBus.analyser.getFloatTimeDomainData(f);for(leti=0;i<f.length;i++){if(!isFinite(f[i])){nanOut=true;break;}}}catch(e){}}if(pk<0.001||isPianoRoll&&nanOut){masterSilenceFramesRef.current++;constsinceRebuild=performance.now()-(lastMasterRebuildTimeRef.current||0);if(masterSilenceFramesRef.current>45&&sinceRebuild>3000){masterSilenceFramesRef.current=0;lastMasterRebuildTimeRef.current=performance.now();console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');try{constpt=currentTimeRef.current;stopAllPlayback();Object.keys(activeTrackNodesRef.current).forEach(k=>{constn=activeTrackNodesRef.current[k];try{if(n&&n.gainNode&&n.gainNode.disconnect)n.gainNode.disconnect();}catch(e){}});activeTrackNodesRef.current={};// Tháo chain cũ khỏi destination rồi rebuild THẬT — initMasterBus
letnanOut=false;if(isPianoRoll){try{constf=newFloat32Array(128);masterBus.analyser.getFloatTimeDomainData(f);for(leti=0;i<f.length;i++){if(!isFinite(f[i])){nanOut=true;break;}}}catch(e){}}// PIANO_ROLL: CHỈ rebuild khi output NaN (chain chết — state-bad).
// pk<0.001 (im lặng) KHÔNG trigger cho piano roll — rests tự nhiên
// giữa các note > 750ms là BÌNH THƯỜNG → false-positive = recovery
if(isPianoRoll?nanOut:pk<0.001||nanOut){masterSilenceFramesRef.current++;constsinceRebuild=performance.now()-(lastMasterRebuildTimeRef.current||0);if(masterSilenceFramesRef.current>45&&sinceRebuild>3000){masterSilenceFramesRef.current=0;lastMasterRebuildTimeRef.current=performance.now();console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');try{constpt=currentTimeRef.current;stopAllPlayback();Object.keys(activeTrackNodesRef.current).forEach(k=>{constn=activeTrackNodesRef.current[k];try{if(n&&n.gainNode&&n.gainNode.disconnect)n.gainNode.disconnect();}catch(e){}});activeTrackNodesRef.current={};// Tháo chain cũ khỏi destination rồi rebuild THẬT — initMasterBus
// early-return khi masterBus còn tồn tại → recovery trước đây là
// early-return khi masterBus còn tồn tại → recovery trước đây là
// no-op → chain "state is bad" bị STUCK vĩnh viễn.
// no-op → chain "state is bad" bị STUCK vĩnh viễn.
try{if(masterBus){try{if(masterBus.analyser)masterBus.analyser.disconnect();}catch(e){}try{if(masterBus.output)masterBus.output.disconnect();}catch(e){}try{if(masterBus.dryOutput)masterBus.dryOutput.disconnect();}catch(e){}}masterBus=null;initMasterBus(getAudioContext());}catch(e){console.warn('[Recovery] initMasterBus error:',e);}// Resume ĐÚNG chế độ play hiện tại (main hoặc sub-tab piano roll)
try{if(masterBus){try{if(masterBus.analyser)masterBus.analyser.disconnect();}catch(e){}try{if(masterBus.output)masterBus.output.disconnect();}catch(e){}try{if(masterBus.dryOutput)masterBus.dryOutput.disconnect();}catch(e){}}masterBus=null;initMasterBus(getAudioContext());}catch(e){console.warn('[Recovery] initMasterBus error:',e);}// Resume ĐÚNG chế độ play hiện tại (main hoặc sub-tab piano roll)
- **Ghi chú/Test:**`npm run build` → hard refresh → play → BACK → play từ đầu → KỲ VỌNG: CÓ ÂM, âm lượng đủ (hết compressor pump-down), qua mastering.
- **Ghi chú/Test:**`npm run build` → hard refresh → play → BACK → play từ đầu → KỲ VỌNG: CÓ ÂM, âm lượng đủ (hết compressor pump-down), qua mastering.
### [2026-08-05 23:50] Task: Watchdog piano-roll FALSE POSITIVE — recovery trên rests tự nhiên (pk trigger) → NaN-ONLY
- **Log v2330 (compressor fix ĐÃ chạy):** recovery fire → rebuild → applyMasteringSettings → node mới → sfEntry → **noteon 23 notes từ 0:0 (bass 56/49/37 vel 127) — KHÔNG state-bad!!** ⇒ compressor fix HOẠT ĐỘNG (bass không còn NaN → chain không chết).
- **NHƯNG [Recovery] vẫn fire** — lỗi watchdog của agent: điều kiện `pk < 0.001 || (isPianoRoll && nanOut)` — **piano roll vẫn trigger bởi pk<0.001** — rest tự nhiên > 750ms trong pattern = FALSE POSITIVE → recovery hủy play + restart notes (glitch + "âm nhỏ" do restart mất bối cảnh).
- **FIX:**`(isPianoRoll ? nanOut : (pk < 0.001 || nanOut))` — piano roll CHỈ rebuild khi output NaN (chain chết). Main giữ nguyên (pk || nanOut).
- **Ghi chú/Test:**`npm run build` → hard refresh → play piano roll từ 0:0 → KỲ VỌNG: CÓ ÂM liên tục, KHÔNG [Recovery] (trừ khi chain thực sự chết — NaN), không restart glitch.
### [2026-08-06 00:00] Task: TRIGGER THẬT cuối — routing swap DƯ giữa stream (dedupe setOutputDestination) + state-bad trên mastering PWR
- **Log v2350:** state-bad TRỞ LẠI — nổ NGAY SAU `setOutputDestination to: track node (sfEntry)` LẦN 2 (sau 6 noteon đầu — từ applyAllTrackMuteSolo cuối startSubTabPlayback) — dù compressor đã hết (bass play sạch ở các log khác). ⇒ **Graph mutation (disconnect/reconnect _gainNode GIỮA stream) làm ScriptProcessor xuất buffer uninitialized → NaN → 11 biquad sụp.** Compressor vô can. Khớp mọi log từ đầu: state-bad LUÔN sau swap lần 2; play 0:1 (swap xong trước notes) không nổ.
- **Các file ảnh hưởng:** soundfontPlayer.js (?v=202608052355 — chỉ hard refresh, không cần build precompiled), wiki.md.
- **Ghi chú/Test:** hard refresh → bật mastering PWR → play piano roll từ 0:0 → KỲ VỌNG: KHÔNG state-bad, âm qua mastering (EQ/imager/maximizer nghe rõ), không recovery giả.
### [2026-08-06 00:00] Task: Chain FLAT sau recreation — stale _lastMasteringSig cache (spectrum hiển thị nhưng không xử lí)
- **Báo cáo user:** mastering chain spectrum hiển thị trong các module NHƯNG âm thanh không được xử lí (không tăng gain, không thay đổi).
- **Cơ chế:**`applyMasteringSettings` early-return qua `_lastMasteringSig` (module-level, PERSIST qua recreation masterBus). Sau recovery (watchdog rebuild: masterBus=null → initMasterBus) chain MỚI giữ giá trị INIT (EQ gain 0, imager width 100, maximizer boost 0) → **FLAT** — tín hiệu chảy qua modules (spectrum hiển thị) nhưng output = input. Cũng giải thích "âm rất nhỏ" sau recovery ở vòng trước (mất maximizer gain 5.4dB + EQ).
- **FIX (app.jsx initMasterBus):**`_lastMasteringSig = null;` trước `applyMasteringSettings` — chain mới được cấu hình lại đầy đủ.
- **Kết hợp với v23:55 (dedupe swap):** dedupe ngăn state-bad (không recovery → không flat); sig-reset đảm bảo recovery (nếu có) cấu hình lại chain — 2 fix bổ trợ.
- **Ghi chú/Test:**`npm run build` → hard refresh → bật mastering PWR → play piano roll 0:0 → KỲ VỌNG: âm qua mastering ĐẦY ĐỦ (EQ gain nghe rõ, maximizer boost, imager width) — chỉnh slider module → âm thay đổi ngay.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.