From 8a9d6b3c27b8a465ac15ee3a6706f0551f45b595 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 5 Aug 2026 12:16:00 +0700 Subject: [PATCH] =?UTF-8?q?FIX:=20Piano=20roll=20tab=20kh=C3=B4ng=20xu?= =?UTF-8?q?=E1=BA=A5t=20=C3=A2m=20thanh=20qua=20mastering=20chain=20=C4=91?= =?UTF-8?q?=C6=B0=E1=BB=A3c=20recovery=20nh=C6=B0ng=20=C3=A2m=20thanh=20r?= =?UTF-8?q?=E1=BA=A5t=20nh=E1=BB=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 92 ++++++++++++++++++++++---------- app/static/js/app.precompiled.js | 14 ++++- app/templates/index.html | 2 +- wiki.md | 10 ++++ 4 files changed, 87 insertions(+), 31 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 4b35cdf..56c4278 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -336,9 +336,9 @@ function applyMasteringSettings(s) { 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 + // Limiter Threshold (WaveShaper ceiling — _setCeiling rebuild curve) const ceilingVal = s.maximizerActive ? clamp(s.ceiling, -60, 0) : -0.1; - masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal, now, 0.01); + if (masterBus.maximizerCompressor._setCeiling) masterBus.maximizerCompressor._setCeiling(ceilingVal); // 4. Bus Compressor module (mastering_expand.md §II.2) if (masterBus.compNode) { @@ -348,11 +348,14 @@ function applyMasteringSettings(s) { 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) + // 5. Brickwall Limiter module (WaveShaper tanh — threshold = mức clip; OFF = identity) 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); + 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) @@ -501,12 +504,21 @@ function initMasterBus(ctx) { 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; + // 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); @@ -525,14 +537,23 @@ function initMasterBus(ctx) { compNode.connect(compMakeup); compMakeup.connect(compOutput); - // ── Brickwall Limiter module (ratio 20:1, knee 0) ── + // ── Brickwall Limiter module (WaveShaper tanh soft-clip — KHÔNG + // DynamicsCompressor: NaN trên bass transient → chain stuck) ── 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 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); @@ -643,10 +664,12 @@ function initMasterBus(ctx) { 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); + // 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); @@ -867,12 +890,25 @@ function createTrackFxModule(type, ctx, params) { 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 }; + // 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; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 59494d1..5b5d63d 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -742,9 +742,19 @@ const curTracks=activeTracksRef.current||activeTracks;const soloed=curTracks.som // sẽ rebuild loop + panic hủy notes chờ → CÂM TOÀN CỤC → exempt hoàn toàn // (các fix setValueAtTime/NaN guard đã hết "state is bad" — watchdog chỉ // còn là lớp cứu cuối cho main/audio-tab). -const _anySubPlaying=subTabsRef.current.some(s=>s.isPlaying);const activeSub=subTabsRef.current.find(s=>s.id===activeTabRef.current);const isPianoRoll=activeSub&&activeSub.type==='PIANO_ROLL';if(!isPianoRoll&&(isPlaying||_anySubPlaying)&&masterBus&&masterBus.analyser&&activeSourcesRef.current.length>0){try{// "Đáng lẽ đang có âm" — quyết định watchdog có được rebuild không: +// PIANO_ROLL: watchdog CHỈ rebuild khi output chứa NaN (chain chết — +// state-bad). KHÔNG rebuild khi im lặng thường (rests tự nhiên giữa các +// note — false-positive = stopAllPlayback + reschedule = glitch). +const _anySubPlaying=subTabsRef.current.some(s=>s.isPlaying);const activeSub=subTabsRef.current.find(s=>s.id===activeTabRef.current);const isPianoRoll=activeSub&&activeSub.type==='PIANO_ROLL';if((isPlaying||_anySubPlaying)&&masterBus&&masterBus.analyser&&(isPianoRoll||activeSourcesRef.current.length>0)){try{// "Đáng lẽ đang có âm" — quyết định watchdog có được rebuild không: // - Main / sub-tab audio: source thật đang trong khoảng phát. -const ctxNow=getAudioContext().currentTime;let anyPlaying=false;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;ipk)pk=v;}if(pk<0.001){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={};try{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) +// - PIANO_ROLL: tab đang play (notes đã schedule) = đáng lẽ có âm. +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;ipk)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;i45&&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) const curTab=activeTabRef.current;const subSt=subTabsRef.current.find(s=>s.id===curTab);if(subSt&&(subSt.type==='PIANO_ROLL'||subSt.buffer)){const resumeAt=subSt.currentTime||0;if(subSt.type==='PIANO_ROLL')schedulePianoRollMidi(subSt,resumeAt);startSubTabPlayback(subSt,resumeAt);setSubTabs(prev=>prev.map(s=>s.id===curTab?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),isPlaying:true,currentTime:resumeAt}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setIsPlaying(true);startTrackPlayback(pt);}}catch(e){console.warn('[Recovery] rebuild error:',e);}}}else{masterSilenceFramesRef.current=0;}}else{masterSilenceFramesRef.current=0;}}catch(e){masterSilenceFramesRef.current=0;}}else{masterSilenceFramesRef.current=0;}if(recordingStateRef.current==='RECORDING'){const audioCtx=getAudioContext();const lookahead=0.1;// 100ms const secondsPerBeat=60.0/(parseInt(bpmRef.current)||120);// Metronome Click Scheduler while(true){const beatNum=nextMetronomeBeatRef.current;const elapsedBeats=beatNum-recordingStartTimeRef.current/secondsPerBeat;const beatTime=startAudioTimeRef.current+elapsedBeats*secondsPerBeat;if(beatTime - +