FIX: (1) MIDI item route CHỈ qua Carla bridge khi VSTi loaded (bỏ FluidSynth GM sai âm) ở mọi đường playback + ghosts; (2) updateSfRouting sync route gains nhánh gainNode + ensureMasteringRouting force-sync mọi node — preview MIDI keyboard qua mastering không cần bật/tắt power; (3) stopNote/panic dừng fallback oscillator theo channel:pitch — hết âm kêu liên tục sau khi thả phím

This commit is contained in:
2026-08-10 09:07:42 +00:00
parent 75795653c5
commit f492104869
5 changed files with 229 additions and 90 deletions
+135 -69
View File
@@ -20805,6 +20805,16 @@ const App = () => {
} }
if (_prNode.gainNode) { if (_prNode.gainNode) {
window.SonicSF.setOutputDestination(_prNode.gainNode); window.SonicSF.setOutputDestination(_prNode.gainNode);
// FIX: sync route gains NGAY ti thi đim routing trưc đây
// b qua node gi bypass cũ (to khi mastering OFF / audioBypass
// true) preview âm soundfont KHÔNG qua mastering cho ti khi bt/
// tt power (effect re-sync). MIDI preview route theo (midiBypass).
if (_prNode.route && _prNode.route.routeGain && _prNode.route.dryGain) {
const _prTrk2 = list.find(t => t.id === _activeSub.trackId);
const _b2 = effMidiBypass(_prTrk2 || { id: _activeSub.trackId });
_prNode.route.routeGain.gain.value = _b2 ? 0 : 1;
_prNode.route.dryGain.gain.value = _b2 ? 1 : 0;
}
return; return;
} }
} }
@@ -20845,6 +20855,13 @@ const App = () => {
if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) { if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
console.log('[SFRoute] dest=gainNode node', t.id, '(no sfEntry) — route.routeGain=' + (node.route ? node.route.routeGain.gain.value : 'MISSING'), 'route.dryGain=' + (node.route ? node.route.dryGain.gain.value : 'MISSING')); console.log('[SFRoute] dest=gainNode node', t.id, '(no sfEntry) — route.routeGain=' + (node.route ? node.route.routeGain.gain.value : 'MISSING'), 'route.dryGain=' + (node.route ? node.route.dryGain.gain.value : 'MISSING'));
window.SonicSF.setOutputDestination(node.gainNode); window.SonicSF.setOutputDestination(node.gainNode);
// FIX: sync route gains node gi bypass cũ preview không qua
// mastering cho ti khi bt/tt power. MIDI preview theo .
if (node.route && node.route.routeGain && node.route.dryGain) {
const _b3 = effMidiBypass(t);
node.route.routeGain.gain.value = _b3 ? 0 : 1;
node.route.dryGain.gain.value = _b3 ? 1 : 0;
}
return; return;
} }
} }
@@ -20875,6 +20892,27 @@ const App = () => {
toggleMasteringOnMaster(!!_s.masterConnected, !!_s.isBypassed); toggleMasteringOnMaster(!!_s.masterConnected, !!_s.isBypassed);
applyMasteringSettings(_s); applyMasteringSettings(_s);
updateSfRouting(); updateSfRouting();
// Force-sync MI node đang tn ti (route + sf route) theo trng thái
// hin ti phòng node to t trưc vi bypass cũ (mastering OFF lúc
// to node / audioBypass default true ca section) preview LUÔN qua
// mastering khi chain bt, không cn bt/tt power.
try {
const _list2 = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
_list2.forEach(_t => {
const _n = activeTrackNodesRef.current[_t.id];
if (!_n) return;
if (_n.sfRouteGain && _n.sfDryGain) {
const _mb = effMidiBypass(_t);
_n.sfRouteGain.gain.value = _mb ? 0 : 1;
_n.sfDryGain.gain.value = _mb ? 1 : 0;
}
if (_n.route && _n.route.routeGain && _n.route.dryGain) {
const _ab = routeBypassFor(_t);
_n.route.routeGain.gain.value = _ab ? 0 : 1;
_n.route.dryGain.gain.value = _ab ? 1 : 0;
}
});
} catch (e2) {}
} catch (e) { } catch (e) {
console.warn('ensureMasteringRouting error:', e); console.warn('ensureMasteringRouting error:', e);
} }
@@ -21115,22 +21153,28 @@ const App = () => {
if (offsetTime < noteEndSec) { if (offsetTime < noteEndSec) {
const durationMs = noteDurSec * 1000; const durationMs = noteDurSec * 1000;
const program = track._isSectionClone ? track.instrumentProgram : (track.instrumentProgram !== undefined ? track.instrumentProgram : 0); const program = track._isSectionClone ? track.instrumentProgram : (track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
// Track VSTi + Carla local MIDI item PHI play qua Carla bridge
// (ch route Carla KHÔNG play qua FluidSynth GM na, tránh âm
// sai instrument chng lên). Không có Carla FluidSynth như cũ.
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
if (offsetTime < noteStartSec) { if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime; const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
window.SonicSF.playNote( if (!routeToCarla && window.SonicSF) {
note.pitch || 60, window.SonicSF.playNote(
note.velocity || 0.8, note.pitch || 60,
durationMs, note.velocity || 0.8,
startTime, durationMs,
program, startTime,
gainNode, program,
trkCh, gainNode,
track.synth_engine trkCh,
); track.synth_engine
);
}
// MIDI items Carla bridge (track VSTi + Carla local): phát VSTi // MIDI items Carla bridge (track VSTi + Carla local): phát VSTi
// realtime yêu cu: MIDI item PHI play qua Carla khi VSTi loaded. // realtime yêu cu: MIDI item PHI play qua Carla khi VSTi loaded.
scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs); if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
// Trigger VU meter flash when the note starts playing // Trigger VU meter flash when the note starts playing
setTimeout(() => { setTimeout(() => {
// Guard: stop setTimeout sót không đưc fire (VU nhy // Guard: stop setTimeout sót không đưc fire (VU nhy
@@ -21144,18 +21188,20 @@ const App = () => {
} else { } else {
const playOffset = offsetTime - noteStartSec; const playOffset = offsetTime - noteStartSec;
const remainingDurMs = (noteEndSec - offsetTime) * 1000; const remainingDurMs = (noteEndSec - offsetTime) * 1000;
window.SonicSF.playNote( if (!routeToCarla && window.SonicSF) {
note.pitch || 60, window.SonicSF.playNote(
note.velocity || 0.8, note.pitch || 60,
remainingDurMs, note.velocity || 0.8,
context.currentTime, remainingDurMs,
program, context.currentTime,
gainNode, program,
trkCh, gainNode,
track.synth_engine trkCh,
); track.synth_engine
);
}
// MIDI items Carla bridge (track VSTi + Carla local) // MIDI items Carla bridge (track VSTi + Carla local)
scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs); if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
// Trigger VU meter flash instantly // Trigger VU meter flash instantly
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8); window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
@@ -21255,21 +21301,27 @@ const App = () => {
const notePlayEndMain = Math.min(noteEndMain, secEnd); const notePlayEndMain = Math.min(noteEndMain, secEnd);
const program = subTrack.instrumentProgram; const program = subTrack.instrumentProgram;
var subCh = assignTrackMidiChannel(subTrack, secTracks); var subCh = assignTrackMidiChannel(subTrack, secTracks);
// MIDI item trong SECTION Carla bridge khi VSTi loaded
// (ch route Carla không play FluidSynth GM sai âm)
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
if (offsetTime < noteStartMain) { if (offsetTime < noteStartMain) {
const delay = noteStartMain - offsetTime; const delay = noteStartMain - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
const playDurMs = (notePlayEndMain - noteStartMain) * 1000; const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
window.SonicSF.playNote( if (!subRouteCarla && window.SonicSF) {
note.pitch || 60, window.SonicSF.playNote(
note.velocity || 0.8, note.pitch || 60,
playDurMs, note.velocity || 0.8,
startTime, playDurMs,
program, startTime,
subNode, program,
subCh, subNode,
subTrack.synth_engine subCh,
); subTrack.synth_engine
);
}
if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, startTime, playDurMs);
// Trigger VU meter flash đúng nhp (c subTrack VU // Trigger VU meter flash đúng nhp (c subTrack VU
// section tab + parent track VU MAIN): MIDI section đi // section tab + parent track VU MAIN): MIDI section đi
// SF masterBus.input (không qua sub-node analyser) // SF masterBus.input (không qua sub-node analyser)
@@ -21287,16 +21339,19 @@ const App = () => {
}, delay * 1000); }, delay * 1000);
} else { } else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
window.SonicSF.playNote( if (!subRouteCarla && window.SonicSF) {
note.pitch || 60, window.SonicSF.playNote(
note.velocity || 0.8, note.pitch || 60,
remainingDurMs, note.velocity || 0.8,
context.currentTime, remainingDurMs,
program, context.currentTime,
subNode, program,
subCh, subNode,
subTrack.synth_engine subCh,
); subTrack.synth_engine
);
}
if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
// VU key theo CONTEXT (tách MAIN vs SECTION 07:20) // VU key theo CONTEXT (tách MAIN vs SECTION 07:20)
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8); window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8);
@@ -21369,35 +21424,42 @@ const App = () => {
if (offsetTime < noteEndSec) { if (offsetTime < noteEndSec) {
const durationMs = noteDurSec * 1000; const durationMs = noteDurSec * 1000;
const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0; const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0;
// MIDI item Carla bridge khi VSTi loaded (ch route Carla không
// play FluidSynth GM sai âm chng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
if (offsetTime < noteStartSec) { if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime; const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
window.SonicSF.playNote( if (!routeToCarla && window.SonicSF) {
note.pitch || 60, window.SonicSF.playNote(
note.velocity || 0.8, note.pitch || 60,
durationMs, note.velocity || 0.8,
startTime, durationMs,
program, startTime,
gainNode, program,
lcCh, gainNode,
track.synth_engine lcCh,
); track.synth_engine
);
}
// MIDI items Carla bridge (track VSTi + Carla local) // MIDI items Carla bridge (track VSTi + Carla local)
scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs); if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
} else { } else {
const remainingDurMs = (noteEndSec - offsetTime) * 1000; const remainingDurMs = (noteEndSec - offsetTime) * 1000;
window.SonicSF.playNote( if (!routeToCarla && window.SonicSF) {
note.pitch || 60, window.SonicSF.playNote(
note.velocity || 0.8, note.pitch || 60,
remainingDurMs, note.velocity || 0.8,
context.currentTime, remainingDurMs,
program, context.currentTime,
gainNode, program,
lcCh, gainNode,
track.synth_engine lcCh,
); track.synth_engine
);
}
// MIDI items Carla bridge (track VSTi + Carla local) // MIDI items Carla bridge (track VSTi + Carla local)
scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs); if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
} }
} }
}); });
@@ -21440,12 +21502,15 @@ const App = () => {
const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec); const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec);
const scheduledTime = startWallTime + effectiveStart; const scheduledTime = startWallTime + effectiveStart;
const durMs = effectiveDur * 1000; const durMs = effectiveDur * 1000;
if (window.SonicSF) { // Track VSTi + Carla local note PHI play qua Carla bridge (ch route
// Carla không play FluidSynth GM sai âm chng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine); window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
} }
// MIDI items Carla (track VSTi + Carla local): phát VSTi realtime // MIDI items Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bng setTimeout preview, timing gn đúng). // (schedule theo audio clock bng setTimeout preview, timing gn đúng).
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) { if (routeToCarla) {
const carlaDelay = Math.max(0, (scheduledTime - context.currentTime) * 1000); const carlaDelay = Math.max(0, (scheduledTime - context.currentTime) * 1000);
const carlaVel = Math.round((note.velocity || 0.8) * 127); const carlaVel = Math.round((note.velocity || 0.8) * 127);
const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : mainCh; const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : mainCh;
@@ -21474,11 +21539,12 @@ const App = () => {
var effDur = durSec - Math.max(0, offsetSeconds - startSec); var effDur = durSec - Math.max(0, offsetSeconds - startSec);
var schedTime = startWallTime + effStart; var schedTime = startWallTime + effStart;
var durMs = effDur * 1000; var durMs = effDur * 1000;
if (window.SonicSF) { // Ghost note Carla bridge khi ghost track VSTi (ch route Carla)
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
if (!gRouteCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth); window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
} }
// Ghost notes Carla bridge (ghost track VSTi + Carla local) if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
} }
}); });
}); });
+35 -16
View File
@@ -1269,7 +1269,11 @@ if(_activeSub&&_activeSub.type==='PIANO_ROLL'&&!isPlayingRef.current){// PIANO R
// không có sub-node → null (masterBus.input — qua mastering). // không có sub-node → null (masterBus.input — qua mastering).
let _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_activeSub.parent_tab_id&&_activeSub.parent_tab_id.startsWith('session_')){const _prSubKeys=Object.keys(activeTrackNodesRef.current).filter(k=>k.endsWith('_sub_'+_activeSub.trackId));_prNode=_prSubKeys.length?activeTrackNodesRef.current[_prSubKeys[0]]:null;}if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có let _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_activeSub.parent_tab_id&&_activeSub.parent_tab_id.startsWith('session_')){const _prSubKeys=Object.keys(activeTrackNodesRef.current).filter(k=>k.endsWith('_sub_'+_activeSub.trackId));_prNode=_prSubKeys.length?activeTrackNodesRef.current[_prSubKeys[0]]:null;}if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có
// thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON). // thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON).
if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);// ⚠️ FIX: sync route gains NGAY tại thời điểm routing — trước đây
// bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass
// true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/
// tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass).
if(_prNode.route&&_prNode.route.routeGain&&_prNode.route.dryGain){const _prTrk2=list.find(t=>t.id===_activeSub.trackId);const _b2=effMidiBypass(_prTrk2||{id:_activeSub.trackId});_prNode.route.routeGain.gain.value=_b2?0:1;_prNode.route.dryGain.gain.value=_b2?1:0;}return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node
// chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI // chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI
// section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> — // section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> —
// route theo ♪ section + thẳng masterBus.input) — không có sub-node → // route theo ♪ section + thẳng masterBus.input) — không có sub-node →
@@ -1282,7 +1286,9 @@ if(node.sfRouteGain&&node.sfDryGain){// ⚠️ FIX: truyền track OBJECT (t)
// → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn // → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn
// false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua // false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua
// đường này. // đường này.
const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ── const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);// ⚠️ FIX: sync route gains — node giữ bypass cũ → preview không qua
// mastering cho tới khi bật/tắt power. MIDI preview theo ♪.
if(node.route&&node.route.routeGain&&node.route.dryGain){const _b3=effMidiBypass(t);node.route.routeGain.gain.value=_b3?0:1;node.route.dryGain.gain.value=_b3?1:0;}return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ──
// Bug: preview instrument soundfont với MIDI keyboard KHÔNG tự động đi qua // Bug: preview instrument soundfont với MIDI keyboard KHÔNG tự động đi qua
// mastering FX của main out — phải bật/tắt nút power mới có tác dụng. Nguyên // mastering FX của main out — phải bật/tắt nút power mới có tác dụng. Nguyên
// nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph // nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph
@@ -1292,7 +1298,11 @@ const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain
// gains) — gọi tại mọi điểm preview → âm preview LUÔN qua mastering khi bật. // gains) — gọi tại mọi điểm preview → âm preview LUÔN qua mastering khi bật.
const ensureMasteringRouting=()=>{try{const _s=window.currentMasteringSettings;if(!_s)return;// Đảm bảo audioCtx + masterBus đã tồn tại trước khi sync (lần preview const ensureMasteringRouting=()=>{try{const _s=window.currentMasteringSettings;if(!_s)return;// Đảm bảo audioCtx + masterBus đã tồn tại trước khi sync (lần preview
// đầu tiên audio chưa init → getAudioContext tạo ngay) // đầu tiên audio chưa init → getAudioContext tạo ngay)
getAudioContext();if(!masterBus||!audioCtx)return;toggleMasteringOnMaster(!!_s.masterConnected,!!_s.isBypassed);applyMasteringSettings(_s);updateSfRouting();}catch(e){console.warn('ensureMasteringRouting error:',e);}};const ensureMasteringRoutingRef=useRef(null);ensureMasteringRoutingRef.current=ensureMasteringRouting;window.__ensureMasteringRouting=ensureMasteringRouting;// ── MIDI preview cache (bounce nhanh offline) ── getAudioContext();if(!masterBus||!audioCtx)return;toggleMasteringOnMaster(!!_s.masterConnected,!!_s.isBypassed);applyMasteringSettings(_s);updateSfRouting();// Force-sync MỌI node đang tồn tại (route + sf route) theo trạng thái
// hiện tại — phòng node tạo từ trước với bypass cũ (mastering OFF lúc
// tạo node / audioBypass default true của section) → preview LUÔN qua
// mastering khi chain bật, không cần bật/tắt power.
try{const _list2=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list2.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(!_n)return;if(_n.sfRouteGain&&_n.sfDryGain){const _mb=effMidiBypass(_t);_n.sfRouteGain.gain.value=_mb?0:1;_n.sfDryGain.gain.value=_mb?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}});}catch(e2){}}catch(e){console.warn('ensureMasteringRouting error:',e);}};const ensureMasteringRoutingRef=useRef(null);ensureMasteringRoutingRef.current=ensureMasteringRouting;window.__ensureMasteringRouting=ensureMasteringRouting;// ── MIDI preview cache (bounce nhanh offline) ──
// Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo // Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo
// track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache // track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache
// (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime. // (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime.
@@ -1330,14 +1340,17 @@ const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)
ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth
if(track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items) if(track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items)
const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;if(noteStartSec>=itemEndSec)return;// skip notes outside item's visual duration const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;if(noteStartSec>=itemEndSec)return;// skip notes outside item's visual duration
const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track._isSectionClone?track.instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:0;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,trkCh,track.synth_engine);// MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track._isSectionClone?track.instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:0;// Track VSTi + Carla local → MIDI item PHẢI play qua Carla bridge
// (chỉ route Carla — KHÔNG play qua FluidSynth GM nữa, tránh âm
// sai instrument chồng lên). Không có Carla → FluidSynth như cũ.
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,trkCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi
// realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded. // realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded.
scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);// Trigger VU meter flash when the note starts playing if(routeToCarla)scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);// Trigger VU meter flash when the note starts playing
setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy
// sau khi âm đã dừng — user bug 05:45). // sau khi âm đã dừng — user bug 05:45).
if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — user 07:20) if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — user 07:20)
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);// MIDI items → Carla bridge (track VSTi + Carla local) window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);// Trigger VU meter flash instantly if(routeToCarla)scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);// Trigger VU meter flash instantly
if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}// If track has instrumentId set but no MIDI items, create scheduled oscillators if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}// If track has instrumentId set but no MIDI items, create scheduled oscillators
if(track.instrumentId&&midiItems.length===0&&track.buffer){// Play audio normally (the buffer has the audio data) if(track.instrumentId&&midiItems.length===0&&track.buffer){// Play audio normally (the buffer has the audio data)
// This block is intentionally empty - audio clips already play above // This block is intentionally empty - audio clips already play above
@@ -1349,7 +1362,9 @@ const subNodeObj=activeTrackNodesRef.current[track.id+'_sub_'+subTrack.id];if(su
const subClips=subTrack.clips||[];subClips.forEach(clip=>{if(!clip.buffer)return;const clipStartLocal=clip.startTime||0;const clipDurLocal=clip.buffer.duration/(clip.speed||1.0);const clipStartMain=secStart+clipStartLocal;const clipEndMain=clipStartMain+clipDurLocal;// Only play within the bounds of [secStart, secEnd] const subClips=subTrack.clips||[];subClips.forEach(clip=>{if(!clip.buffer)return;const clipStartLocal=clip.startTime||0;const clipDurLocal=clip.buffer.duration/(clip.speed||1.0);const clipStartMain=secStart+clipStartLocal;const clipEndMain=clipStartMain+clipDurLocal;// Only play within the bounds of [secStart, secEnd]
const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart<activeEnd&&offsetTime<activeEnd){const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(subNode);if(offsetTime<activeStart){const delay=activeStart-offsetTime;const playOffset=activeStart-clipStartMain;const playDuration=activeEnd-activeStart;source.start(context.currentTime+delay,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}else{const playOffset=offsetTime-clipStartMain;const playDuration=activeEnd-offsetTime;source.start(context.currentTime,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}}});// 2. Play MIDI items in subTrack const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart<activeEnd&&offsetTime<activeEnd){const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(subNode);if(offsetTime<activeStart){const delay=activeStart-offsetTime;const playOffset=activeStart-clipStartMain;const playDuration=activeEnd-activeStart;source.start(context.currentTime+delay,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}else{const playOffset=offsetTime-clipStartMain;const playDuration=activeEnd-offsetTime;source.start(context.currentTime,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}}});// 2. Play MIDI items in subTrack
const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){subMidiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSecLocal=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteStartMain=secStart+noteStartSecLocal;const noteEndMain=noteStartMain+noteDurSec;// Only play if the note starts inside the Section item bounds and hasn't finished yet const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){subMidiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSecLocal=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteStartMain=secStart+noteStartSecLocal;const noteEndMain=noteStartMain+noteDurSec;// Only play if the note starts inside the Section item bounds and hasn't finished yet
if(noteStartMain>=secStart&&noteStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);// Trigger VU meter flash đúng nhịp (cả subTrack — VU if(noteStartMain>=secStart&&noteStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);// MIDI item trong SECTION → Carla bridge khi VSTi loaded
// (chỉ route Carla — không play FluidSynth GM sai âm)
const subRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;if(!subRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);}if(subRouteCarla)scheduleCarlaNote(subTrack.synth_engine,subCh,note.pitch||60,note.velocity||0.8,startTime,playDurMs);// Trigger VU meter flash đúng nhịp (cả subTrack — VU
// section tab + parent track — VU MAIN): MIDI section đi // section tab + parent track — VU MAIN): MIDI section đi
// SF → masterBus.input (không qua sub-node analyser) → // SF → masterBus.input (không qua sub-node analyser) →
// VU track chỉ nhảy qua midiVuActivity. (Bị mất khi // VU track chỉ nhảy qua midiVuActivity. (Bị mất khi
@@ -1357,7 +1372,7 @@ if(noteStartMain>=secStart&&noteStartMain<secEnd&&offsetTime<noteEndMain){const
setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU
// nhảy sau khi âm đã dừng — user bug 05:45). // nhảy sau khi âm đã dừng — user bug 05:45).
if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20) if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20) window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;if(!subRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);}if(subRouteCarla)scheduleCarlaNote(subTrack.synth_engine,subCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control
const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);// NOTE: do NOT connect gainNode → pannerNode directly here — the track const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);// NOTE: do NOT connect gainNode → pannerNode directly here — the track
@@ -1365,9 +1380,11 @@ const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackN
// chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack // chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack
// chain for audio clips (double path: with + without FX). // chain for audio clips (double path: with + without FX).
const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime<clipStart){const delay=clipStart-offsetTime;source.start(context.currentTime+delay,0);activeSourcesRef.current.push(source);}else if(offsetTime<clipEnd){const playOffset=offsetTime-clipStart;source.start(context.currentTime,playOffset*(clip.speed||1.0));activeSourcesRef.current.push(source);}});// MIDI items playback const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime<clipStart){const delay=clipStart-offsetTime;source.start(context.currentTime+delay,0);activeSourcesRef.current.push(source);}else if(offsetTime<clipEnd){const playOffset=offsetTime-clipStart;source.start(context.currentTime,playOffset*(clip.speed||1.0));activeSourcesRef.current.push(source);}});// MIDI items playback
const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&window.SonicSF){var lcCh=assignTrackMidiChannel(track,tracks);const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track.instrumentProgram!==undefined?track.instrumentProgram:0;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,lcCh,track.synth_engine);// MIDI items → Carla bridge (track VSTi + Carla local) const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&window.SonicSF){var lcCh=assignTrackMidiChannel(track,tracks);const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track.instrumentProgram!==undefined?track.instrumentProgram:0;// MIDI item → Carla bridge khi VSTi loaded (chỉ route Carla — không
scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,lcCh,track.synth_engine);// MIDI items → Carla bridge (track VSTi + Carla local) // play FluidSynth GM sai âm chồng lên)
scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;if(!track){console.warn('[Play] Piano Roll track not found:',st.trackId);return;}const destNode=getOrCreateTrackNode(track,context);// Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;if(!track){console.warn('[Play] Piano Roll track not found:',st.trackId);return;}const destNode=getOrCreateTrackNode(track,context);// Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã
// loaded instrument" (instrumentProgram GM hoặc synth_engine soundfont) // loaded instrument" (instrumentProgram GM hoặc synth_engine soundfont)
// phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng // phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
// trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF // trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF
@@ -1376,11 +1393,13 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const
// items placed later in the project play `item.startTime` seconds in the // items placed later in the project play `item.startTime` seconds in the
// future (silence when pressing play). Ghost notes are already relative to // future (silence when pressing play). Ghost notes are already relative to
// the item window, so no absolute-session offset is applied anywhere here. // the item window, so no absolute-session offset is applied anywhere here.
midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;// Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route
// Carla — không play FluidSynth GM sai âm chồng lên)
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng). // (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)){const carlaDelay=Math.max(0,(scheduledTime-context.currentTime)*1000);const carlaVel=Math.round((note.velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:mainCh;const carlaPitch=note.pitch||60;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,carlaPitch,carlaVel);},carlaDelay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay+durMs+30);}}});// Play ghost notes from active tracks if(routeToCarla){const carlaDelay=Math.max(0,(scheduledTime-context.currentTime)*1000);const carlaVel=Math.round((note.velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:mainCh;const carlaPitch=note.pitch||60;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,carlaPitch,carlaVel);},carlaDelay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay+durMs+30);}}});// Play ghost notes from active tracks
var ghostLayers=st.ghostPlayLayers||[];ghostLayers.forEach(function(layer){var ghostTrack=allTracks.find(function(t){return t.id===layer.trackId;});var ghostProg=layer.instrumentProgram!==undefined?layer.instrumentProgram:ghostTrack?ghostTrack.instrumentProgram:undefined;var ghostSynth=layer.synthEngine||(ghostTrack?ghostTrack.synth_engine:undefined);if(ghostProg===undefined&&!ghostSynth)return;var ghostDest=getOrCreateTrackNode(ghostTrack,context);var ghostCh=ghostTrack?assignTrackMidiChannel(ghostTrack,allTracks):0;layer.notes.forEach(function(note){var beat=note.start_beat||0;var dur=note.duration_beats||1;var startSec=beat*secondsPerBeat;var durSec=dur*secondsPerBeat;if(startSec+durSec>offsetSeconds){var effStart=Math.max(0,startSec-offsetSeconds);var effDur=durSec-Math.max(0,offsetSeconds-startSec);var schedTime=startWallTime+effStart;var durMs=effDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}// Ghost notes → Carla bridge (ghost track VSTi + Carla local) var ghostLayers=st.ghostPlayLayers||[];ghostLayers.forEach(function(layer){var ghostTrack=allTracks.find(function(t){return t.id===layer.trackId;});var ghostProg=layer.instrumentProgram!==undefined?layer.instrumentProgram:ghostTrack?ghostTrack.instrumentProgram:undefined;var ghostSynth=layer.synthEngine||(ghostTrack?ghostTrack.synth_engine:undefined);if(ghostProg===undefined&&!ghostSynth)return;var ghostDest=getOrCreateTrackNode(ghostTrack,context);var ghostCh=ghostTrack?assignTrackMidiChannel(ghostTrack,allTracks):0;layer.notes.forEach(function(note){var beat=note.start_beat||0;var dur=note.duration_beats||1;var startSec=beat*secondsPerBeat;var durSec=dur*secondsPerBeat;if(startSec+durSec>offsetSeconds){var effStart=Math.max(0,startSec-offsetSeconds);var effDur=durSec-Math.max(0,offsetSeconds-startSec);var schedTime=startWallTime+effStart;var durMs=effDur*1000;// Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla)
scheduleCarlaNote(ghostSynth,ghostCh,note.pitch||60,note.velocity||0.8,schedTime,durMs);}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport var gRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));if(!gRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}if(gRouteCarla)scheduleCarlaNote(ghostSynth,ghostCh,note.pitch||60,note.velocity||0.8,schedTime,durMs);}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport
const st=subTabs.find(s=>s.id===activeTab);if(!st||!st.buffer&&st.type!=='PIANO_ROLL')return;// MAIN/SECTION đang play (ngầm — MIDI section/main còn kêu sau khi mở const st=subTabs.find(s=>s.id===activeTab);if(!st||!st.buffer&&st.type!=='PIANO_ROLL')return;// MAIN/SECTION đang play (ngầm — MIDI section/main còn kêu sau khi mở
// tab): Space phải STOP trước — không được play sub-tab (user: trong // tab): Space phải STOP trước — không được play sub-tab (user: trong
// PIANO ROLL không phát hiện MIDI đang play → space 2 lần mới stop). // PIANO ROLL không phát hiện MIDI đang play → space 2 lần mới stop).
+47 -1
View File
@@ -21,6 +21,10 @@
let _pendingNoteTimers = []; let _pendingNoteTimers = [];
let _loadedFonts = {}; let _loadedFonts = {};
let _activeOscillators = {}; let _activeOscillators = {};
// Fallback oscillator theo 'channel:pitch' — để stopNote/stopAll dừng được
// note khi thả phím (trước đây stopNote không dừng oscillator → âm kêu
// liên tục dù đã thả phím khi FluidSynth chưa sẵn sàng / font load fail).
let _activeOscillatorsByKey = {};
let _gainNode = null; let _gainNode = null;
let _pendingOutputDestination = null; let _pendingOutputDestination = null;
let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream
@@ -479,6 +483,23 @@
// Hủy note đang CHỜ LOAD soundfont (chưa noteon) — trước đây note // Hủy note đang CHỜ LOAD soundfont (chưa noteon) — trước đây note
// này vẫn bắn TRỄ sau khi thả phím → âm loop không dừng được. // này vẫn bắn TRỄ sau khi thả phím → âm loop không dừng được.
if (_pendingNoteOns[key]) delete _pendingNoteOns[key]; if (_pendingNoteOns[key]) delete _pendingNoteOns[key];
// ⚠️ FIX: dừng fallback oscillator theo 'channel:pitch' — trước đây
// stopNote chỉ noteoff FluidSynth, KHÔNG dừng oscillator (khi WASM
// chưa sẵn sàng / font load fail → _playNoteFallback) → âm kêu liên
// tục mặc dù đã thả phím (không xử lí noteoff).
var _oscIds = _activeOscillatorsByKey[key];
if (_oscIds && _oscIds.length) {
var _nowT = getCtx().currentTime;
_oscIds.slice().forEach(function (oid) {
var entry = _activeOscillators[oid];
if (entry) {
try { entry.gain.gain.cancelScheduledValues(_nowT); entry.gain.gain.setValueAtTime(0, _nowT); } catch (e) {}
try { entry.osc.stop(_nowT); } catch (e) {}
delete _activeOscillators[oid];
}
});
delete _activeOscillatorsByKey[key];
}
if (_initialized && _fluidModule) { if (_initialized && _fluidModule) {
var mappedChs = _activeNotes[key]; var mappedChs = _activeNotes[key];
if (mappedChs === undefined) mappedChs = [channel]; if (mappedChs === undefined) mappedChs = [channel];
@@ -711,6 +732,17 @@
for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c); for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c);
} catch (e) {} } catch (e) {}
} }
// Dừng fallback oscillator (FluidSynth chưa sẵn sàng / font fail)
try {
var _pctx = getCtx();
var _pnow = _pctx.currentTime;
Object.values(_activeOscillators).forEach(function (entry) {
try { if (entry.gain) { entry.gain.gain.cancelScheduledValues(_pnow); entry.gain.gain.setValueAtTime(0, _pnow); } } catch (e) {}
try { if (entry.osc) entry.osc.stop(_pnow); } catch (e) {}
});
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
_activeOscillatorsByKey = {};
} catch (e) {}
_activeNotes = {}; _activeNotes = {};
}, },
@@ -768,7 +800,20 @@
osc.stop(stopAt); osc.stop(stopAt);
var oscId = note + '_' + Date.now() + '_' + Math.random(); var oscId = note + '_' + Date.now() + '_' + Math.random();
_activeOscillators[oscId] = { osc: osc, gain: noteGain }; _activeOscillators[oscId] = { osc: osc, gain: noteGain };
setTimeout(function () { delete _activeOscillators[oscId]; }, (stopAt - ctx.currentTime) * 1000 + 100); // Đăng ký theo 'channel:pitch' để stopNote dừng được khi thả phím
var oscKey = (channel !== undefined && channel >= 0 && channel < 16) ? (channel + ':' + note) : null;
if (oscKey) {
if (!_activeOscillatorsByKey[oscKey]) _activeOscillatorsByKey[oscKey] = [];
_activeOscillatorsByKey[oscKey].push(oscId);
}
setTimeout(function () {
delete _activeOscillators[oscId];
if (oscKey && _activeOscillatorsByKey[oscKey]) {
var _oi = _activeOscillatorsByKey[oscKey].indexOf(oscId);
if (_oi >= 0) _activeOscillatorsByKey[oscKey].splice(_oi, 1);
if (_activeOscillatorsByKey[oscKey].length === 0) delete _activeOscillatorsByKey[oscKey];
}
}, (stopAt - ctx.currentTime) * 1000 + 100);
return osc; return osc;
}, },
@@ -805,6 +850,7 @@
} catch (e) {} } catch (e) {}
}); });
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; }); Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
_activeOscillatorsByKey = {};
_activeNotes = {}; _activeNotes = {};
}, },
+4 -4
View File
@@ -31,12 +31,12 @@
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script> <script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script> <script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
<script src="/static/js/services/runtime.js?v=202608101500"></script> <script src="/static/js/services/runtime.js?v=202608101600"></script>
<script src="/static/js/services/api.js?v=202608101500"></script> <script src="/static/js/services/api.js?v=202608101600"></script>
<script src="/static/js/services/audioEngine.js?v=202607271016"></script> <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/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script> <script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608101500"></script> <script src="/static/js/services/soundfontPlayer.js?v=202608101600"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></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/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script> <script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
@@ -44,7 +44,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <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/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608101500" defer></script> <script src="/static/js/app.precompiled.js?v=202608101600" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {
+8
View File
@@ -1,3 +1,11 @@
### [2026-08-10] Task: Fix (KHÔNG bỏ qua) — MIDI keyboard preview không qua mastering + noteoff không xử lí + MIDI item không play qua Carla bridge
- **Tóm tắt thay đổi:**
(1) **MIDI item PHẢI play qua Carla bridge khi VSTi loaded** (`app.jsx`): trước đây các hàm playback gọi `SonicSF.playNote` (FluidSynth GM — âm SAI) SONG SONG với route Carla → âm nghe được là GM, không phải VSTi. Giờ: mọi đường playback (startTrackPlayback main, MIDI section subTrack, startLocalTrackPlayback, schedulePianoRollMidi + ghost notes) tính `routeToCarla = shouldRoutePlayback(synth_engine)` — nếu TRUE → CHỈ route Carla (bỏ hẳn FluidSynth); FALSE → FluidSynth như cũ.
(2) **MIDI keyboard preview không route qua mastering FX chain** (`app.jsx`): `updateSfRouting` nhánh gainNode (piano roll + midiAudible) THIẾU sync route gains → node giữ bypass cũ (tạo khi mastering OFF / audioBypass true) → preview đi dry cho tới khi bật/tắt power. Fix: sync `route.routeGain/dryGain` theo `effMidiBypass` NGAY tại thời điểm routing; `ensureMasteringRouting` giờ force-sync MỌI node (sfRouteGain/sfDryGain + routeGain/dryGain) theo trạng thái hiện tại.
(3) **Âm kêu liên tục dù đã thả phím (không xử lí noteoff)** (`soundfontPlayer.js`): khi FluidSynth WASM chưa sẵn sàng / font load fail → `_playNoteFallback` tạo oscillator — `stopNote` trước đây CHỈ noteoff FluidSynth, KHÔNG dừng oscillator → âm kêu tới khi auto-stop 2s. Fix: `_activeOscillatorsByKey` (key 'channel:pitch'); `stopNote` dừng oscillator đúng key ngay (osc.stop + gain 0); `panic`/`stopAll` dừng toàn bộ + clear map.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild babel), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html` (?v= 202608101600), `dist/daw_engine/_internal/app/static/**` + `templates/index.html`
- **Ghi chú/Test (nếu có):** pytest 86 passed, 7 skipped. Test jsdom noteoff: 6/6 pass (stopNote gọi osc.stop ngay t=1.0 + gain 0; đúng channel:pitch — không đụng note/osc channel khác; panic dừng tất cả). Bundle: routeToCarla ở cả 4 đường + ghosts. ⚠️ Rebuild PyInstaller exe trên Windows để nhận backend fix trước đó (plugins.py/projects.py — PYZ).
### [2026-08-10] Task: Preview soundfont + MIDI keyboard không đi qua mastering FX main out (phải bật/tắt power mới có tác dụng) ### [2026-08-10] Task: Preview soundfont + MIDI keyboard không đi qua mastering FX main out (phải bật/tắt power mới có tác dụng)
- **Tóm tắt thay đổi:** Routing âm thanh SF preview (keybed/draw/MIDI file) chỉ được đồng bộ khi masteringSettings ĐỔI (toggle power) — bắt đầu preview KHÔNG chạy lại `updateSfRouting` + đồng bộ route gains → âm preview không tự động qua mastering chain cho tới khi bật/tắt nút power. Fix: (1) thêm `ensureMasteringRouting()` trong App (toggleMasteringOnMaster theo trạng thái hiện tại — early-return nếu đúng; applyMasteringSettings; updateSfRouting) — gọi tại: keybed onMouseDown, playDrawPreview, playMidiPreview (Media Explorer), mở piano roll tab (setTimeout 60ms sau setActiveTab); (2) thêm `masterConnected`/`isBypassed` vào sig của `applyMasteringSettings` — toggle power luôn re-apply tham số module (hết blind-spot early-return); (3) expose `window.__ensureMasteringRouting`. Đã kiểm chứng bằng test jsdom (AudioContext mock theo dõi connect): khi mastering ON, path SF_ROUTE → masterBus.input → inputAnalyser → eqLowFilter (mastering chain) → destination là ĐÚNG — vấn đề nằm ở chỗ không re-sync routing tại thời điểm preview. - **Tóm tắt thay đổi:** Routing âm thanh SF preview (keybed/draw/MIDI file) chỉ được đồng bộ khi masteringSettings ĐỔI (toggle power) — bắt đầu preview KHÔNG chạy lại `updateSfRouting` + đồng bộ route gains → âm preview không tự động qua mastering chain cho tới khi bật/tắt nút power. Fix: (1) thêm `ensureMasteringRouting()` trong App (toggleMasteringOnMaster theo trạng thái hiện tại — early-return nếu đúng; applyMasteringSettings; updateSfRouting) — gọi tại: keybed onMouseDown, playDrawPreview, playMidiPreview (Media Explorer), mở piano roll tab (setTimeout 60ms sau setActiveTab); (2) thêm `masterConnected`/`isBypassed` vào sig của `applyMasteringSettings` — toggle power luôn re-apply tham số module (hết blind-spot early-return); (3) expose `window.__ensureMasteringRouting`. Đã kiểm chứng bằng test jsdom (AudioContext mock theo dõi connect): khi mastering ON, path SF_ROUTE → masterBus.input → inputAnalyser → eqLowFilter (mastering chain) → destination là ĐÚNG — vấn đề nằm ở chỗ không re-sync routing tại thời điểm preview.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild babel), `app/templates/index.html` (?v= bump 202608101400), `dist/daw_engine/_internal/app/static/**` + `templates/index.html` - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild babel), `app/templates/index.html` (?v= bump 202608101400), `dist/daw_engine/_internal/app/static/**` + `templates/index.html`