FIX: Piano roll tab không xuất âm thanh qua mastering chain được recovery nhưng âm thanh rất nhỏ
This commit is contained in:
+64
-28
@@ -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;
|
||||
|
||||
@@ -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;i<d.length;i++){const v=Math.abs(d[i]-128)/128;if(v>pk)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;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
|
||||
// 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<audioCtx.currentTime+lookahead){const isDownbeat=beatNum%4===0;playMetronomeClick(beatTime,isDownbeat);nextMetronomeBeatRef.current++;}else{break;}}// Throttled Live Preview compilation (every 200ms)
|
||||
|
||||
Reference in New Issue
Block a user