T6: scheduler items qua router - note-on/off TIMELINE_SCHEDULER (startTime audio clock support trong router+TrackInstrument); 8 site scheduler (timeline/section/local/piano-roll/ghost) route UnifiedMidiRouter, fallback SonicSF, tracker doi xung qua note-off setTimeout

This commit is contained in:
2026-08-11 15:48:21 +07:00
parent 2001d1c601
commit 9dcc37e037
4 changed files with 112 additions and 87 deletions
+97 -74
View File
@@ -226,7 +226,7 @@ const _makeCtx = (ch, program, synthEngine, dest) => ({
}); });
// Note-on qua router. Tr true nếu router x lý, false nếu caller phi fallback // Note-on qua router. Tr true nếu router x lý, false nếu caller phi fallback
// SonicSF trc tiếp. ctx: TrackInstrumentCtx (ch/program/synthEngine/dest). // SonicSF trc tiếp. ctx: TrackInstrumentCtx (ch/program/synthEngine/dest).
const _routeNoteOn = (trackId, ctx, pitch, velocity, durationMs, sourceType) => { const _routeNoteOn = (trackId, ctx, pitch, velocity, durationMs, sourceType, startTime) => {
if (trackId === undefined || trackId === null || !ctx) return false; if (trackId === undefined || trackId === null || !ctx) return false;
const router = _getMidiRouter(); const router = _getMidiRouter();
if (!router) return false; if (!router) return false;
@@ -240,6 +240,7 @@ const _routeNoteOn = (trackId, ctx, pitch, velocity, durationMs, sourceType) =>
pitch, pitch,
velocity: velocity != null ? velocity : 0.8, velocity: velocity != null ? velocity : 0.8,
durationMs, durationMs,
startTime,
source: sourceType || 'PREVIEW' source: sourceType || 'PREVIEW'
}); });
return true; return true;
@@ -262,10 +263,16 @@ const _routeNoteOff = (trackId, ctx, pitch, sourceType) => {
}; };
// Fire-and-forget: note-on + note-off sau durationMs+60 (gi tracker đi xng // Fire-and-forget: note-on + note-off sau durationMs+60 (gi tracker đi xng
// cho click/draw/playing-tick không có note-off riêng). // cho click/draw/playing-tick không có note-off riêng).
const _routePreviewNote = (trackId, ctx, pitch, velocity, durationMs, sourceType) => { const _routePreviewNote = (trackId, ctx, pitch, velocity, durationMs, sourceType, startTime) => {
if (!_routeNoteOn(trackId, ctx, pitch, velocity, durationMs, sourceType)) return false; if (!_routeNoteOn(trackId, ctx, pitch, velocity, durationMs, sourceType, startTime)) return false;
const dur = Math.max(50, durationMs || 500); const dur = Math.max(50, durationMs || 500);
setTimeout(() => { _routeNoteOff(trackId, ctx, pitch, sourceType); }, dur + 60); let delayMs = dur + 60;
// Scheduler (TIMELINE_SCHEDULER) schedule theo audio clock note-off đi
// sau khi note bt đu + duration (gi tracker đi xng cho scheduled note).
if (startTime) {
try { delayMs = Math.max(0, (startTime - getAudioContext().currentTime) * 1000) + dur + 60; } catch (e) {}
}
setTimeout(() => { _routeNoteOff(trackId, ctx, pitch, sourceType); }, delayMs);
return true; return true;
}; };
const _stopMidiFilePreview = () => { const _stopMidiFilePreview = () => {
@@ -21803,17 +21810,19 @@ const App = () => {
if (offsetTime < noteStartSec) { if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime; const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla) {
window.SonicSF.playNote( if (!_routePreviewNote(track.id, _makeCtx(trkCh, program, track.synth_engine, gainNode), note.pitch || 60, note.velocity || 0.8, durationMs, 'TIMELINE_SCHEDULER', startTime) && 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.
@@ -21831,17 +21840,19 @@ const App = () => {
} else { } else {
const playOffset = offsetTime - noteStartSec; const playOffset = offsetTime - noteStartSec;
const remainingDurMs = (noteEndSec - offsetTime) * 1000; const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla) {
window.SonicSF.playNote( if (!_routePreviewNote(track.id, _makeCtx(trkCh, program, track.synth_engine, gainNode), note.pitch || 60, note.velocity || 0.8, remainingDurMs, 'TIMELINE_SCHEDULER', context.currentTime) && 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)
if (routeToCarla) 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);
@@ -21960,17 +21971,19 @@ const App = () => {
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;
if (!subRouteCarla && window.SonicSF) { if (!subRouteCarla) {
window.SonicSF.playNote( if (!_routePreviewNote(subTrack.id, _makeCtx(subCh, program, subTrack.synth_engine, subNode), note.pitch || 60, note.velocity || 0.8, playDurMs, 'TIMELINE_SCHEDULER', startTime) && 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); 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
@@ -21990,17 +22003,19 @@ const App = () => {
}, delay * 1000); }, delay * 1000);
} else { } else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
if (!subRouteCarla && window.SonicSF) { if (!subRouteCarla) {
window.SonicSF.playNote( if (!_routePreviewNote(subTrack.id, _makeCtx(subCh, program, subTrack.synth_engine, subNode), note.pitch || 60, note.velocity || 0.8, remainingDurMs, 'TIMELINE_SCHEDULER', context.currentTime) && 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 (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
@@ -22089,33 +22104,37 @@ const App = () => {
if (offsetTime < noteStartSec) { if (offsetTime < noteStartSec) {
const delay = noteStartSec - offsetTime; const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay; const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla) {
window.SonicSF.playNote( if (!_routePreviewNote(track.id, _makeCtx(lcCh, program, track.synth_engine, gainNode), note.pitch || 60, note.velocity || 0.8, durationMs, 'TIMELINE_SCHEDULER', startTime) && 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)
if (routeToCarla) 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;
if (!routeToCarla && window.SonicSF) { if (!routeToCarla) {
window.SonicSF.playNote( if (!_routePreviewNote(track.id, _makeCtx(lcCh, program, track.synth_engine, gainNode), note.pitch || 60, note.velocity || 0.8, remainingDurMs, 'TIMELINE_SCHEDULER', context.currentTime) && 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)
if (routeToCarla) 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);
@@ -22171,8 +22190,10 @@ const App = () => {
// Track VSTi + Carla local note PHI play qua Carla bridge (ch route // Track VSTi + Carla local note PHI play qua Carla bridge (ch route
// Carla không play FluidSynth GM sai âm chng lên) // Carla không play FluidSynth GM sai âm chng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)); const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
if (!routeToCarla && window.SonicSF) { if (!routeToCarla) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine); if (!_routePreviewNote(track.id, _makeCtx(mainCh, instrumentProgram, synthEngine, destNode), note.pitch || 60, note.velocity || 0.8, durMs, 'TIMELINE_SCHEDULER', scheduledTime) && 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 // 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).
@@ -22207,8 +22228,10 @@ const App = () => {
var durMs = effDur * 1000; var durMs = effDur * 1000;
// Ghost note Carla bridge khi ghost track VSTi (ch route Carla) // Ghost note Carla bridge khi ghost track VSTi (ch route Carla)
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth)); var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
if (!gRouteCarla && window.SonicSF) { if (!gRouteCarla) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth); if (!_routePreviewNote(ghostTrack && ghostTrack.id, _makeCtx(ghostCh, ghostProg, ghostSynth, ghostDest), note.pitch || 60, note.velocity || 0.8, durMs, 'TIMELINE_SCHEDULER', schedTime) && 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); if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
} }
+12 -10
View File
@@ -47,9 +47,11 @@ const shouldRouteCarla=()=>false;// live playback 100% client (SF2 autosampled W
// mọi voice khi stop. Fallback SonicSF trực tiếp nếu router chưa sẵn sàng. // mọi voice khi stop. Fallback SonicSF trực tiếp nếu router chưa sẵn sàng.
let _midiRouter=null;let _midiFilePreviewAudio=null;const _getMidiRouter=()=>{try{if(!_midiRouter&&window.SonicUnifiedMidiRouter&&window.SonicUnifiedMidiRouter.UnifiedMidiRouter&&window.TrackInstrument){_midiRouter=new window.SonicUnifiedMidiRouter.UnifiedMidiRouter();}}catch(e){_midiRouter=null;}return _midiRouter;};const _makeCtx=(ch,program,synthEngine,dest)=>({ch:ch!=null?ch:0,program,synthEngine,sfId:synthEngine&&synthEngine.soundfont_id||undefined,bank:synthEngine&&synthEngine.soundfont_bank||0,prog:synthEngine&&synthEngine.soundfont_program||(program!=null?program:0),dest:dest||null});// Note-on qua router. Trả true nếu router xử lý, false nếu caller phải fallback let _midiRouter=null;let _midiFilePreviewAudio=null;const _getMidiRouter=()=>{try{if(!_midiRouter&&window.SonicUnifiedMidiRouter&&window.SonicUnifiedMidiRouter.UnifiedMidiRouter&&window.TrackInstrument){_midiRouter=new window.SonicUnifiedMidiRouter.UnifiedMidiRouter();}}catch(e){_midiRouter=null;}return _midiRouter;};const _makeCtx=(ch,program,synthEngine,dest)=>({ch:ch!=null?ch:0,program,synthEngine,sfId:synthEngine&&synthEngine.soundfont_id||undefined,bank:synthEngine&&synthEngine.soundfont_bank||0,prog:synthEngine&&synthEngine.soundfont_program||(program!=null?program:0),dest:dest||null});// Note-on qua router. Trả true nếu router xử lý, false nếu caller phải fallback
// SonicSF trực tiếp. ctx: TrackInstrumentCtx (ch/program/synthEngine/dest). // SonicSF trực tiếp. ctx: TrackInstrumentCtx (ch/program/synthEngine/dest).
const _routeNoteOn=(trackId,ctx,pitch,velocity,durationMs,sourceType)=>{if(trackId===undefined||trackId===null||!ctx)return false;const router=_getMidiRouter();if(!router)return false;try{const inst=new window.TrackInstrument(trackId,ctx);router.registerEngine(trackId,inst);router.dispatchMidiEvent({trackId,command:0x90,channel:ctx.ch,pitch,velocity:velocity!=null?velocity:0.8,durationMs,source:sourceType||'PREVIEW'});return true;}catch(e){console.warn('[MidiRouter] note-on error:',e);return false;}};const _routeNoteOff=(trackId,ctx,pitch,sourceType)=>{if(trackId===undefined||trackId===null||!ctx)return false;const router=_getMidiRouter();if(!router)return false;try{router.dispatchMidiEvent({trackId,command:0x80,channel:ctx.ch,pitch,source:sourceType||'PREVIEW'});return true;}catch(e){console.warn('[MidiRouter] note-off error:',e);return false;}};// Fire-and-forget: note-on + note-off sau durationMs+60 (giữ tracker đối xứng const _routeNoteOn=(trackId,ctx,pitch,velocity,durationMs,sourceType,startTime)=>{if(trackId===undefined||trackId===null||!ctx)return false;const router=_getMidiRouter();if(!router)return false;try{const inst=new window.TrackInstrument(trackId,ctx);router.registerEngine(trackId,inst);router.dispatchMidiEvent({trackId,command:0x90,channel:ctx.ch,pitch,velocity:velocity!=null?velocity:0.8,durationMs,startTime,source:sourceType||'PREVIEW'});return true;}catch(e){console.warn('[MidiRouter] note-on error:',e);return false;}};const _routeNoteOff=(trackId,ctx,pitch,sourceType)=>{if(trackId===undefined||trackId===null||!ctx)return false;const router=_getMidiRouter();if(!router)return false;try{router.dispatchMidiEvent({trackId,command:0x80,channel:ctx.ch,pitch,source:sourceType||'PREVIEW'});return true;}catch(e){console.warn('[MidiRouter] note-off error:',e);return false;}};// Fire-and-forget: note-on + note-off sau durationMs+60 (giữ tracker đối xứng
// cho click/draw/playing-tick không có note-off riêng). // cho click/draw/playing-tick không có note-off riêng).
const _routePreviewNote=(trackId,ctx,pitch,velocity,durationMs,sourceType)=>{if(!_routeNoteOn(trackId,ctx,pitch,velocity,durationMs,sourceType))return false;const dur=Math.max(50,durationMs||500);setTimeout(()=>{_routeNoteOff(trackId,ctx,pitch,sourceType);},dur+60);return true;};const _stopMidiFilePreview=()=>{if(_midiFilePreviewAudio){try{_midiFilePreviewAudio.pause();_midiFilePreviewAudio.currentTime=0;}catch(e){}_midiFilePreviewAudio=null;}};const _panicMidi=()=>{_stopMidiFilePreview();const router=_getMidiRouter();if(router){try{router.panicAllNotesOff();}catch(e){}}};// ── Native soundfont item render (standalone, transport) ────────────────── const _routePreviewNote=(trackId,ctx,pitch,velocity,durationMs,sourceType,startTime)=>{if(!_routeNoteOn(trackId,ctx,pitch,velocity,durationMs,sourceType,startTime))return false;const dur=Math.max(50,durationMs||500);let delayMs=dur+60;// Scheduler (TIMELINE_SCHEDULER) schedule theo audio clock → note-off đợi
// sau khi note bắt đầu + duration (giữ tracker đối xứng cho scheduled note).
if(startTime){try{delayMs=Math.max(0,(startTime-getAudioContext().currentTime)*1000)+dur+60;}catch(e){}}setTimeout(()=>{_routeNoteOff(trackId,ctx,pitch,sourceType);},delayMs);return true;};const _stopMidiFilePreview=()=>{if(_midiFilePreviewAudio){try{_midiFilePreviewAudio.pause();_midiFilePreviewAudio.currentTime=0;}catch(e){}_midiFilePreviewAudio=null;}};const _panicMidi=()=>{_stopMidiFilePreview();const router=_getMidiRouter();if(router){try{router.panicAllNotesOff();}catch(e){}}};// ── Native soundfont item render (standalone, transport) ──────────────────
// Render TOÀN BỘ MIDI item bằng native FluidSynth → decode AudioBuffer → // Render TOÀN BỘ MIDI item bằng native FluidSynth → decode AudioBuffer →
// schedule nguồn audio đúng vị trí item (giống audio clip). opts: // schedule nguồn audio đúng vị trí item (giống audio clip). opts:
// baseOffsetSec: offset thêm (section: secStart) | limitSec: chặn tại secEnd // baseOffsetSec: offset thêm (section: secStart) | limitSec: chặn tại secEnd
@@ -1433,13 +1435,13 @@ const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteSt
// sai instrument chồng lên). Không có Carla → FluidSynth như cũ. // sai instrument chồng lên). Không có Carla → FluidSynth như cũ.
// ⚠️ FIX: item route giống hệt keybed (không gate __carlaRunning — Carla mở // ⚠️ FIX: item route giống hệt keybed (không gate __carlaRunning — Carla mở
// thủ công/cũ vẫn phải route; gate cũ làm "keybed kêu, item câm"). // thủ công/cũ vẫn phải route; gate cũ làm "keybed kêu, item câm").
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 const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla){if(!_routePreviewNote(track.id,_makeCtx(trkCh,program,track.synth_engine,gainNode),note.pitch||60,note.velocity||0.8,durationMs,'TIMELINE_SCHEDULER',startTime)&&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.
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 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;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) 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){if(!_routePreviewNote(track.id,_makeCtx(trkCh,program,track.synth_engine,gainNode),note.pitch||60,note.velocity||0.8,remainingDurMs,'TIMELINE_SCHEDULER',context.currentTime)&&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)
if(routeToCarla)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)
@@ -1455,7 +1457,7 @@ const subMidiItems=subTrack.midiItems||[];const subRouteCarla=shouldRouteCarla(s
}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 }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);// MIDI item trong SECTION → Carla bridge khi VSTi loaded 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) // (chỉ route Carla — không play FluidSynth GM sai âm)
const subRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));if(subRouteCarla||window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine))ensureCarlaForPlayback(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 const subRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));if(subRouteCarla||window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine))ensureCarlaForPlayback(subTrack.synth_engine);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;if(!subRouteCarla){if(!_routePreviewNote(subTrack.id,_makeCtx(subCh,program,subTrack.synth_engine,subNode),note.pitch||60,note.velocity||0.8,playDurMs,'TIMELINE_SCHEDULER',startTime)&&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
@@ -1463,7 +1465,7 @@ const subRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRouteP
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;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);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;if(!subRouteCarla){if(!_routePreviewNote(subTrack.id,_makeCtx(subCh,program,subTrack.synth_engine,subNode),note.pitch||60,note.velocity||0.8,remainingDurMs,'TIMELINE_SCHEDULER',context.currentTime)&&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();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();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();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();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
@@ -1474,8 +1476,8 @@ const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.
const midiItems=track.midiItems||[];const routeCarla=shouldRouteCarla(track.synth_engine);const nativeSf=isStandaloneSf()&&!routeCarla&&isSfTrackEngine(track.synth_engine);if((track.type==='MIDI'||midiItems.length>0)&&(window.SonicSF||routeCarla||nativeSf)){if(nativeSf){midiItems.forEach(item=>scheduleNativeSfItem(track,item,offsetTime,context,gainNode,bpm,{sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current}));return;// native render → audio buffer (không qua WASM/Carla per-note) const midiItems=track.midiItems||[];const routeCarla=shouldRouteCarla(track.synth_engine);const nativeSf=isStandaloneSf()&&!routeCarla&&isSfTrackEngine(track.synth_engine);if((track.type==='MIDI'||midiItems.length>0)&&(window.SonicSF||routeCarla||nativeSf)){if(nativeSf){midiItems.forEach(item=>scheduleNativeSfItem(track,item,offsetTime,context,gainNode,bpm,{sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current}));return;// native render → audio buffer (không qua WASM/Carla per-note)
}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 }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
// play FluidSynth GM sai âm chồng lên) // play FluidSynth GM sai âm chồng lên)
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) const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla){if(!_routePreviewNote(track.id,_makeCtx(lcCh,program,track.synth_engine,gainNode),note.pitch||60,note.velocity||0.8,durationMs,'TIMELINE_SCHEDULER',startTime)&&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,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla){if(!_routePreviewNote(track.id,_makeCtx(lcCh,program,track.synth_engine,gainNode),note.pitch||60,note.velocity||0.8,remainingDurMs,'TIMELINE_SCHEDULER',context.currentTime)&&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();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();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 "đã 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();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();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
@@ -1488,11 +1490,11 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const
const routeCarla=shouldRouteCarla(synthEngine);if(isStandaloneSf()&&!routeCarla&&isSfTrackEngine(synthEngine)){scheduleNativeSfItem(track,{startTime:0,notes:midiNotes},offsetSeconds,context,destNode,bpm,{sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current});return;// native render cả tab → audio buffer (không per-note WASM/Carla) const routeCarla=shouldRouteCarla(synthEngine);if(isStandaloneSf()&&!routeCarla&&isSfTrackEngine(synthEngine)){scheduleNativeSfItem(track,{startTime:0,notes:midiNotes},offsetSeconds,context,destNode,bpm,{sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current});return;// native render cả tab → audio buffer (không per-note WASM/Carla)
}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 }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) // 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 const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));if(!routeToCarla){if(!_routePreviewNote(track.id,_makeCtx(mainCh,instrumentProgram,synthEngine,destNode),note.pitch||60,note.velocity||0.8,durMs,'TIMELINE_SCHEDULER',scheduledTime)&&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(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 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;// Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla) 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)
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 var gRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));if(!gRouteCarla){if(!_routePreviewNote(ghostTrack&&ghostTrack.id,_makeCtx(ghostCh,ghostProg,ghostSynth,ghostDest),note.pitch||60,note.velocity||0.8,durMs,'TIMELINE_SCHEDULER',schedTime)&&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).
+2 -2
View File
@@ -33,12 +33,12 @@ window.TrackInstrument = window.TrackInstrument || {};
} }
}; };
TrackInstrument.prototype.playNote = function (pitch, velocity, durationMs) { TrackInstrument.prototype.playNote = function (pitch, velocity, durationMs, startTime) {
try { try {
if (!window.SonicSF || !window.SonicSF.playNote) return; if (!window.SonicSF || !window.SonicSF.playNote) return;
this._ensure(); this._ensure();
var dur = (durationMs != null ? durationMs : 500) || 500; var dur = (durationMs != null ? durationMs : 500) || 500;
window.SonicSF.playNote(pitch, velocity != null ? velocity : 0.8, dur, undefined, this.program, this.dest, this.ch, this.synthEngine); window.SonicSF.playNote(pitch, velocity != null ? velocity : 0.8, dur, startTime, this.program, this.dest, this.ch, this.synthEngine);
} catch (e) { } catch (e) {
console.warn('[TrackInstrument] playNote error:', e); console.warn('[TrackInstrument] playNote error:', e);
} }
+1 -1
View File
@@ -76,7 +76,7 @@ window.SonicUnifiedMidiRouter = window.SonicUnifiedMidiRouter || {};
// Chỉ trigger âm ở note-on đầu tiên; các note-on trùng key chỉ tăng // Chỉ trigger âm ở note-on đầu tiên; các note-on trùng key chỉ tăng
// count (sustain lặp) — note-off cuối cùng mới tắt voice. // count (sustain lặp) — note-off cuối cùng mới tắt voice.
if (cur.count === 1 && engine && engine.playNote) { if (cur.count === 1 && engine && engine.playNote) {
try { engine.playNote(pitch, vel, evt.durationMs || 500); } catch (e) {} try { engine.playNote(pitch, vel, evt.durationMs || 500, evt.startTime); } catch (e) {}
} }
return true; return true;
} }