FIX: Piano roll tab không xuất âm thanh qua mastering chain được recovery

This commit is contained in:
2026-08-05 12:30:42 +07:00
parent 8a9d6b3c27
commit d37f4e7557
5 changed files with 82 additions and 21 deletions
+10 -1
View File
@@ -680,6 +680,11 @@ function initMasterBus(ctx) {
// masteringSettings effect for subsequent changes see useEffect).
if (window.currentMasteringSettings) {
try {
// Reset sig-cache: chain MI (biquad/maximizer node mi) gi giá tr
// INIT (gain 0, width 100) applyMasteringSettings early-return vì
// _lastMasteringSig không đi (module-level, persist qua recreation)
// chain FLAT ("spectrum hin th nhưng không x lí âm thanh").
_lastMasteringSig = null;
toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected, window.currentMasteringSettings.isBypassed);
applyMasteringSettings(window.currentMasteringSettings);
} catch (e) {
@@ -17813,7 +17818,11 @@ const App = () => {
for (let i = 0; i < f.length; i++) { if (!isFinite(f[i])) { nanOut = true; break; } }
} catch (e) {}
}
if (pk < 0.001 || (isPianoRoll && nanOut)) {
// PIANO_ROLL: CH rebuild khi output NaN (chain chết state-bad).
// pk<0.001 (im lng) KHÔNG trigger cho piano roll rests t nhiên
// gia các note > 750ms là BÌNH THƯNG false-positive = recovery
// hy play + restart notes (glitch) đúng chui log recovery trưc.
if ((isPianoRoll ? nanOut : (pk < 0.001 || nanOut))) {
masterSilenceFramesRef.current++;
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
if (masterSilenceFramesRef.current > 45 && sinceRebuild > 3000) {
+31 -13
View File
@@ -74,10 +74,10 @@ const _g=(v,lo,hi)=>typeof v==='number'&&isFinite(v)?clamp(v,lo,hi):0;masterBus.
const updateImagerBand=(w,active,gainLL,gainRL,gainLR,gainRR)=>{const widthVal=s.imagerActive&&active?clamp(w,0,200):100;const g1=(widthVal+100)/200;const g2=(100-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 (identity passthrough when off — never null curve)
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=new Float32Array([-1,1]);}// 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);// 4. Bus Compressor module (mastering_expand.md §II.2)
if(masterBus.compNode){const compOn=!!s.compActive;masterBus.compNode.threshold.setTargetAtTime(compOn?clamp(s.compThreshold,-60,0):0,now,0.02);masterBus.compNode.ratio.setTargetAtTime(compOn?clamp(s.compRatio,1,20):1,now,0.02);masterBus.compMakeup.gain.setTargetAtTime(compOn?Math.pow(10,clamp(s.compMakeup,0,12)/20):1.0,now,0.02);}// 5. Brickwall Limiter module (ratio 20:1, knee 0)
if(masterBus.limNode){const limOn=!!s.limActive;masterBus.limNode.threshold.setTargetAtTime(limOn?clamp(s.limThreshold,-24,0):0,now,0.02);masterBus.limNode.ratio.setTargetAtTime(limOn?20:1,now,0.02);}// 6. Harmonic Exciter module (dry/wet mix; dry stays 1.0 for subtle warmth)
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 (WaveShaper ceiling — _setCeiling rebuild curve)
const ceilingVal=s.maximizerActive?clamp(s.ceiling,-60,0):-0.1;if(masterBus.maximizerCompressor._setCeiling)masterBus.maximizerCompressor._setCeiling(ceilingVal);// 4. Bus Compressor module (mastering_expand.md §II.2)
if(masterBus.compNode){const compOn=!!s.compActive;masterBus.compNode.threshold.setTargetAtTime(compOn?clamp(s.compThreshold,-60,0):0,now,0.02);masterBus.compNode.ratio.setTargetAtTime(compOn?clamp(s.compRatio,1,20):1,now,0.02);masterBus.compMakeup.gain.setTargetAtTime(compOn?Math.pow(10,clamp(s.compMakeup,0,12)/20):1.0,now,0.02);}// 5. Brickwall Limiter module (WaveShaper tanh — threshold = mức clip; OFF = identity)
if(masterBus.limNode){const limOn=!!s.limActive;if(limOn){if(masterBus.limNode._setThreshold)masterBus.limNode._setThreshold(clamp(s.limThreshold,-24,0));}else{try{masterBus.limNode.curve=new Float32Array([-1,1]);}catch(e){}}}// 6. Harmonic Exciter module (dry/wet mix; dry stays 1.0 for subtle warmth)
if(masterBus.excWet){const excOn=!!s.excActive;const wetAmt=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=(midside)/2
if(masterBus.gLLr){const rebOn=!!s.rebalActive;const midLin=rebOn?Math.pow(10,clamp(s.rebalMid,-24,24)/20):1.0;const sideLin=rebOn?Math.pow(10,clamp(s.rebalSide,-24,24)/20):1.0;const a=(midLin+sideLin)/2;const b=(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);}}function initMasterBus(ctx){if(masterBus)return masterBus;// Clamp every filter frequency below Nyquist (0.45 * sampleRate). A biquad
@@ -97,9 +97,13 @@ f4_hp.connect(split4);split4.connect(gainLL4,0);split4.connect(gainLR4,0);split4
const maximizerBoostGain=ctx.createGain();const maximizerSoftClipper=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.
// Use an explicit linear identity table for passthrough.
maximizerSoftClipper.curve=new Float32Array([-1,1]);maximizerSoftClipper.oversample='4x';const upwardCompressor=ctx.createDynamicsCompressor();upwardCompressor.threshold.value=-30;upwardCompressor.knee.value=10;upwardCompressor.ratio.value=4;upwardCompressor.attack.value=0.01;upwardCompressor.release.value=0.1;const upwardGain=ctx.createGain();upwardGain.gain.value=0.0;const upwardSummingGain=ctx.createGain();maximizerBoostGain.connect(maximizerSoftClipper);maximizerSoftClipper.connect(upwardSummingGain);maximizerBoostGain.connect(upwardCompressor);upwardCompressor.connect(upwardGain);upwardGain.connect(upwardSummingGain);const maximizerCompressor=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) ──
const compInput=ctx.createGain();const compNode=ctx.createDynamicsCompressor();compNode.threshold.value=-16;compNode.knee.value=8;compNode.ratio.value=3;compNode.attack.value=0.02;compNode.release.value=0.25;const compMakeup=ctx.createGain();compMakeup.gain.value=1.0;const compOutput=ctx.createGain();compInput.connect(compNode);compNode.connect(compMakeup);compMakeup.connect(compOutput);// ── Brickwall Limiter module (ratio 20:1, knee 0) ──
const limInput=ctx.createGain();const limNode=ctx.createDynamicsCompressor();limNode.threshold.value=-1.0;limNode.knee.value=0;limNode.ratio.value=20;limNode.attack.value=0.001;limNode.release.value=0.05;const limOutput=ctx.createGain();limInput.connect(limNode);limNode.connect(limOutput);// ── Harmonic Exciter module (WaveShaper saturator + high-pass, dry/wet) ──
maximizerSoftClipper.curve=new Float32Array([-1,1]);maximizerSoftClipper.oversample='4x';const upwardCompressor=ctx.createDynamicsCompressor();upwardCompressor.threshold.value=-30;upwardCompressor.knee.value=10;upwardCompressor.ratio.value=4;upwardCompressor.attack.value=0.01;upwardCompressor.release.value=0.1;const upwardGain=ctx.createGain();upwardGain.gain.value=0.0;const upwardSummingGain=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,
// clip chính xác tại ceiling) — KHÔNG DynamicsCompressor (NaN trên bass
// transient → chain state-bad → CÂM + stuck).
const maximizerCompressor=ctx.createWaveShaper();maximizerCompressor.oversample='2x';let _maxCeil=-0.1;const _buildMaxCurve=db=>{const c=Math.pow(10,Math.max(-60,Math.min(0,db))/20);const _c=new Float32Array(4096);for(let _i=0;_i<4096;_i++){const _x=_i/4095*2-1;_c[_i]=Math.max(-c,Math.min(c,_x));}maximizerCompressor.curve=_c;_maxCeil=db;};_buildMaxCurve(-0.1);maximizerCompressor._setCeiling=db=>{if(db!==_maxCeil)_buildMaxCurve(db);};upwardSummingGain.connect(maximizerCompressor);// ── Bus Compressor module (mastering_expand.md §II.2) ──
const compInput=ctx.createGain();const compNode=ctx.createDynamicsCompressor();compNode.threshold.value=-16;compNode.knee.value=8;compNode.ratio.value=3;compNode.attack.value=0.02;compNode.release.value=0.25;const compMakeup=ctx.createGain();compMakeup.gain.value=1.0;const compOutput=ctx.createGain();compInput.connect(compNode);compNode.connect(compMakeup);compMakeup.connect(compOutput);// ── Brickwall Limiter module (WaveShaper tanh soft-clip — KHÔNG
// DynamicsCompressor: NaN trên bass transient → chain stuck) ──
const limInput=ctx.createGain();const limNode=ctx.createWaveShaper();limNode.oversample='2x';let _limLastThresh=null;const _buildLimCurve=db=>{const tLin=Math.pow(10,Math.max(-24,Math.min(0,db))/20);const k=1/Math.max(0.02,tLin);const _c=new Float32Array(4096);const _tk=Math.tanh(k);for(let _i=0;_i<4096;_i++){const _x=_i/4095*2-1;_c[_i]=Math.tanh(_x*k)/_tk;}limNode.curve=_c;_limLastThresh=db;};_buildLimCurve(-1.0);limNode._setThreshold=db=>{if(db!==_limLastThresh)_buildLimCurve(db);};const limOutput=ctx.createGain();limInput.connect(limNode);limNode.connect(limOutput);// ── Harmonic Exciter module (WaveShaper saturator + high-pass, dry/wet) ──
const excInput=ctx.createGain();const excHp=ctx.createBiquadFilter();excHp.type='highpass';excHp.frequency.value=clampF(2000);excHp.Q.value=0.7;const excShaper=ctx.createWaveShaper();excShaper.curve=makeDistortionCurve(3);excShaper.oversample='4x';const excDry=ctx.createGain();excDry.gain.value=1.0;const excWet=ctx.createGain();excWet.gain.value=0.0;const excOutput=ctx.createGain();excInput.connect(excDry);excDry.connect(excOutput);excInput.connect(excHp);excHp.connect(excShaper);excShaper.connect(excWet);excWet.connect(excOutput);// ── Master Rebalance module (M/S gains via L/R crossfeed) ──
const rebalInput=ctx.createGain();const rebalSplit=ctx.createChannelSplitter(2);const rebalMerge=ctx.createChannelMerger(2);const rebalOutput=ctx.createGain();const gLLr=ctx.createGain();const gRLr=ctx.createGain();const gLRr=ctx.createGain();const gRRr=ctx.createGain();rebalInput.connect(rebalSplit);rebalSplit.connect(gLLr,0);rebalSplit.connect(gRLr,0);rebalSplit.connect(gLRr,1);rebalSplit.connect(gRRr,1);gLLr.connect(rebalMerge,0,0);gRLr.connect(rebalMerge,0,0);gLRr.connect(rebalMerge,0,1);gRRr.connect(rebalMerge,0,1);rebalMerge.connect(rebalOutput);// Setup Analysers
const inputAnalyser=ctx.createAnalyser();inputAnalyser.fftSize=2048;const outputAnalyser=ctx.createAnalyser();outputAnalyser.fftSize=2048;// Global fader / output
@@ -112,11 +116,18 @@ inputAnalyser,outputAnalyser,leftAnalyser,rightAnalyser,// Mastering nodes
eqLowFilter,eqMid1Filter,eqMid2Filter,eqHighFilter,imagerInput,imagerOutput,gainLL1,gainRL1,gainLR1,gainRR1,gainLL2,gainRL2,gainLR2,gainRR2,gainLL3,gainRL3,gainLR3,gainRR3,gainLL4,gainRL4,gainLR4,gainRR4,maximizerBoostGain,maximizerSoftClipper,upwardCompressor,upwardGain,upwardSummingGain,maximizerCompressor,// Extension modules (mastering_expand.md §II.2)
compInput,compNode,compMakeup,compOutput,limInput,limNode,limOutput,excInput,excHp,excShaper,excDry,excWet,excOutput,rebalInput,rebalSplit,rebalMerge,gLLr,gRLr,gLRr,gRRr,rebalOutput};// Connect EQ chain (internal — the module BOUNDARIES are wired dynamically
// by rebuildMasteringGraph so modules can be reordered on the CHAIN bar)
eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filter.connect(eqHighFilter);// 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;// Apply mastering once when the chain is first created (and on the
eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filter.connect(eqHighFilter);// Setup default non-mastered routing (KHÔNG compressor mặc định trong path):
// input -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
// Compressor mặc định (ratio 12, threshold -24 — LUÔN-ON) vừa (a) pump-down
// 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).
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;let _lastChainSig=null;// Module input/output boundary nodes for dynamic chain re-routing
if(window.currentMasteringSettings){try{// Reset sig-cache: chain MỚI (biquad/maximizer node mới) giữ giá trị
// INIT (gain 0, width 100…) — applyMasteringSettings early-return vì
// _lastMasteringSig không đổi (module-level, persist qua recreation) →
// chain FLAT ("spectrum hiển thị nhưng không xử lí âm thanh").
_lastMasteringSig=null;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;let _lastChainSig=null;// Module input/output boundary nodes for dynamic chain re-routing
// (mastering_expand.md §II.3 — rebuildAudioGraph). Each module's INTERNAL
// wiring is fixed; only the boundaries get re-connected per chain order.
const MASTER_MODULE_IO={eq:{input:'eqLowFilter',output:'eqHighFilter'},imager:{input:'imagerInput',output:'imagerOutput'},maximizer:{input:'maximizerBoostGain',output:'maximizerCompressor'},compressor:{input:'compInput',output:'compOutput'},limiter:{input:'limInput',output:'limOutput'},exciter:{input:'excInput',output:'excOutput'},rebalance:{input:'rebalInput',output:'rebalOutput'}};const DEFAULT_MASTER_CHAIN=[{id:'mod_eq',type:'eq',name:'Dynamic EQ',active:true},{id:'mod_imager',type:'imager',name:'Imager',active:true},{id:'mod_maximizer',type:'maximizer',name:'Maximizer',active:true}];function chainSignature(chainArray){return(chainArray||[]).map(m=>(m.type||'')+(m.active?'1':'0')).join(',');}// ── Track FX module factory (mastering_expand.md §II.4) ──
@@ -140,7 +151,10 @@ b0=alpha;b1=0;b2=-alpha;a0=1+alpha;a1=-2*cw;a2=1-alpha;}const w=2*Math.PI*f/fs,c
// output), + post-module analyser for the realtime FFT spectrum overlay.
function createEqProModule(ctx,params){const input=ctx.createGain();const output=ctx.createGain();const analyser=ctx.createAnalyser();analyser.fftSize=2048;output.connect(analyser);const bands=params&&Array.isArray(params.bands)?JSON.parse(JSON.stringify(params.bands)):JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));const filters=[];let amount=params&&params.amount!==undefined?params.amount:100;const rebuild=()=>{try{input.disconnect();}catch(e){}filters.forEach(f=>{try{f.disconnect();}catch(e){}});filters.length=0;let tail=input;const maxF=(ctx.sampleRate||44100)*0.45;bands.forEach(b=>{const f=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();const setBand=(i,patch)=>{const b=bands[i];if(!b)return;Object.assign(b,patch);const f=filters[i];if(!f)return;const now=ctx.currentTime;const maxF=(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);};const setAmount=a=>{amount=eqproClamp(a,0,200);const now=ctx.currentTime;filters.forEach((f,i)=>{const b=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.
const syncBands=newBands=>{bands.length=0;(newBands||[]).forEach(b=>bands.push({...b}));rebuild();};return{type:'eqpro',input,output,analyser,get bands(){return bands;},get filters(){return filters;},get amount(){return amount;},setBand,setAmount,rebuild,syncBands,destroy(){try{input.disconnect();output.disconnect();analyser.disconnect();}catch(e){}}};}function createTrackFxModule(type,ctx,params){const num=(v,def)=>{const n=Number(v);return isFinite(n)&&!isNaN(n)?n:def;};const maxF=(ctx.sampleRate||44100)*0.45;const clampF=v=>Math.max(20,Math.min(v,maxF));const input=ctx.createGain();const output=ctx.createGain();const p=params||{};let nodes={};if(type==='compressor'){const comp=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;const makeup=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};}else if(type==='limiter'){const lim=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};}else if(type==='exciter'){const hp=ctx.createBiquadFilter();hp.type='highpass';hp.frequency.value=clampF(2000);hp.Q.value=0.7;const shaper=ctx.createWaveShaper();shaper.curve=makeDistortionCurve(3);shaper.oversample='4x';const dry=ctx.createGain();dry.gain.value=1.0;const wet=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};}else if(type==='rebalance'){const split=ctx.createChannelSplitter(2);const merge=ctx.createChannelMerger(2);const gLL=ctx.createGain(),gRL=ctx.createGain(),gLR=ctx.createGain(),gRR=ctx.createGain();const midLin=Math.pow(10,num(p.mid,0)/20);const sideLin=Math.pow(10,num(p.side,0)/20);const a=(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};}else if(type==='eqpro'){return createEqProModule(ctx,params);}else{// 'eq' or default: 4-band EQ (params.g1..g4 = band gains in dB)
const syncBands=newBands=>{bands.length=0;(newBands||[]).forEach(b=>bands.push({...b}));rebuild();};return{type:'eqpro',input,output,analyser,get bands(){return bands;},get filters(){return filters;},get amount(){return amount;},setBand,setAmount,rebuild,syncBands,destroy(){try{input.disconnect();output.disconnect();analyser.disconnect();}catch(e){}}};}function createTrackFxModule(type,ctx,params){const num=(v,def)=>{const n=Number(v);return isFinite(n)&&!isNaN(n)?n:def;};const maxF=(ctx.sampleRate||44100)*0.45;const clampF=v=>Math.max(20,Math.min(v,maxF));const input=ctx.createGain();const output=ctx.createGain();const p=params||{};let nodes={};if(type==='compressor'){const comp=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;const makeup=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};}else if(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.
const shaper=ctx.createWaveShaper();shaper.oversample='2x';const ceilingDb=Math.min(0,num(p.ceiling,-1.0));const threshLin=Math.pow(10,ceilingDb/20);const k=1/Math.max(0.02,threshLin);const _curve=new Float32Array(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;const makeup=ctx.createGain();makeup.gain.value=1.0;input.connect(shaper);shaper.connect(makeup);makeup.connect(output);nodes={shaper,makeup};}else if(type==='exciter'){const hp=ctx.createBiquadFilter();hp.type='highpass';hp.frequency.value=clampF(2000);hp.Q.value=0.7;const shaper=ctx.createWaveShaper();shaper.curve=makeDistortionCurve(3);shaper.oversample='4x';const dry=ctx.createGain();dry.gain.value=1.0;const wet=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};}else if(type==='rebalance'){const split=ctx.createChannelSplitter(2);const merge=ctx.createChannelMerger(2);const gLL=ctx.createGain(),gRL=ctx.createGain(),gLR=ctx.createGain(),gRR=ctx.createGain();const midLin=Math.pow(10,num(p.mid,0)/20);const sideLin=Math.pow(10,num(p.side,0)/20);const a=(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};}else if(type==='eqpro'){return createEqProModule(ctx,params);}else{// 'eq' or default: 4-band EQ (params.g1..g4 = band gains in dB)
const f1=ctx.createBiquadFilter();f1.type='lowshelf';f1.frequency.value=clampF(100);const f2=ctx.createBiquadFilter();f2.type='peaking';f2.frequency.value=clampF(800);f2.Q.value=0.7;const f3=ctx.createBiquadFilter();f3.type='peaking';f3.frequency.value=clampF(3200);f3.Q.value=1.2;const f4=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,
// keyed in a LOCAL map so the live graph is never touched. Audio clips are then
@@ -751,7 +765,11 @@ const _anySubPlaying=subTabsRef.current.some(s=>s.isPlaying);const activeSub=sub
const ctxNow=getAudioContext().currentTime;let anyPlaying=false;if(isPianoRoll){anyPlaying=_anySubPlaying;}else if(_anySubPlaying){const _subs=subTabsRef.current||[];for(let _si=0;_si<_subs.length;_si++){const s=_subs[_si];if(!s.isPlaying)continue;if(s.buffer){if(activeSourcesRef.current.some(src=>typeof src.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=>typeof s.startTime==='number'&&ctxNow>=s.startTime&&ctxNow<=s.startTime+(s.buffer?s.buffer.duration:0)+0.1);}if(anyPlaying){const d=new Uint8Array(128);masterBus.analyser.getByteTimeDomainData(d);let pk=0;for(let i=0;i<d.length;i++){const v=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
// tự nhiên thì KHÔNG rebuild).
let nanOut=false;if(isPianoRoll){try{const f=new Float32Array(128);masterBus.analyser.getFloatTimeDomainData(f);for(let i=0;i<f.length;i++){if(!isFinite(f[i])){nanOut=true;break;}}}catch(e){}}if(pk<0.001||isPianoRoll&&nanOut){masterSilenceFramesRef.current++;const sinceRebuild=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{const pt=currentTimeRef.current;stopAllPlayback();Object.keys(activeTrackNodesRef.current).forEach(k=>{const n=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
let nanOut=false;if(isPianoRoll){try{const f=new Float32Array(128);masterBus.analyser.getFloatTimeDomainData(f);for(let i=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
// hủy play + restart notes (glitch) — đúng chuỗi log recovery trước.
if(isPianoRoll?nanOut:pk<0.001||nanOut){masterSilenceFramesRef.current++;const sinceRebuild=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{const pt=currentTimeRef.current;stopAllPlayback();Object.keys(activeTrackNodesRef.current).forEach(k=>{const n=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à
// 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)
+18 -5
View File
@@ -23,6 +23,7 @@
let _activeOscillators = {};
let _gainNode = null;
let _pendingOutputDestination = null;
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
let _scheduledNotes = [];
let _loadPromises = {};
let _sfloadSeq = 0;
@@ -32,7 +33,8 @@
if (!_gainNode) {
_gainNode = _audioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
_gainNode.connect(_outputDestination);
}
return _audioCtx;
}
@@ -41,7 +43,8 @@
if (!_gainNode) {
_gainNode = ctx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination));
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination);
_gainNode.connect(_outputDestination);
}
return ctx;
}
@@ -54,7 +57,8 @@
if (!_gainNode) {
_gainNode = window.__sharedAudioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || window.__sharedAudioCtx.destination);
_outputDestination = _pendingOutputDestination || window.__sharedAudioCtx.destination;
_gainNode.connect(_outputDestination);
}
return window.__sharedAudioCtx;
};
@@ -68,9 +72,17 @@
setOutputDestination: function (node) {
try {
if (_gainNode) {
_gainNode.disconnect();
const dest = node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination));
// DEDUPE: đích không đổi → KHÔNG disconnect/reconnect.
// Swap dư giữa dòng notes đang phát (applyAllTrackMuteSolo →
// updateSfRouting gọi lại cùng đích sfEntry sau noteon đầu)
// làm ScriptProcessor xuất buffer uninitialized → NaN →
// 11 biquad "state is bad" → CÂM (mọi log: state-bad nổ
// ngay sau setOutputDestination lần 2).
if (dest === _outputDestination) return;
_gainNode.disconnect();
_gainNode.connect(dest);
_outputDestination = dest;
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
} else {
_pendingOutputDestination = node || null;
@@ -104,7 +116,8 @@
if (!_gainNode) {
_gainNode = _audioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
_outputDestination = _pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination);
_gainNode.connect(_outputDestination);
}
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
+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=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608052230"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608052355"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></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=202608052330" defer></script>
<script src="/static/js/app.precompiled.js?v=202608060000" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {