feat: tách luồng âm instrument theo môi trường — docker dùng FluidSynthWASM, standalone dùng native pyfluidsynth (+Carla VSTi)

- runtime: detect()/capabilities() trả environment (docker|standalone)
- vst_engine: render_soundfont_midi_to_audio (pyfluidsynth native, event-stream, release tail)
- plugins: POST /soundfont-render — render MIDI notes -> WAV (auth, fallback default SF)
- frontend: SonicRuntime.environment + dataset.environment; app.jsx route preview/transport:
  standalone+SF -> soundfont-render WAV (playNativeSfNote/scheduleNativeSfItem),
  docker giữ WASM, VSTi standalone giữ Carla bridge
This commit is contained in:
2026-08-10 20:38:54 +07:00
parent e89a594b7c
commit a0d8725541
8 changed files with 447 additions and 48 deletions
+248 -16
View File
@@ -137,6 +137,133 @@ const ensureCarlaForPlayback = (synthEngine) => {
};
// Môi trưng chy: docker vs standalone
// environment: "docker" âm instrument qua FluidSynthWASM (client); "standalone"
// x lí trc tiếp trên OS (backend native pyfluidsynth / Carla). Quy tc:
// KHÔNG phi docker = standalone (Windows/Linux/macOS chy trc tiếp).
const sfEnv = () => {
try {
const r = window.SonicRuntime;
if (r && r.environment) return r.environment;
const c = r && r.capabilities;
return (c && c.docker) ? 'docker' : 'standalone';
} catch (e) { return 'docker'; }
};
const isDockerSf = () => sfEnv() === 'docker';
const isStandaloneSf = () => sfEnv() === 'standalone';
// Track dùng âm soundfont (không phi VSTi) route native khi standalone.
const isSfTrackEngine = (se) => {
if (!se) return false;
return !!(se.soundfont_id || String(se.type || '').indexOf('soundfont') !== -1 || String(se.type || '').indexOf('sf3') !== -1);
};
const isVstTrackEngine = (se) => !!se && String(se.type || '').indexOf('vst') !== -1;
// Track dùng VSTi + Carla local route Carla (native GUI, realtime).
const shouldRouteCarla = (se) => !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(se));
// Native soundfont preview (standalone)
// Render 1 note bng backend native FluidSynth (pyfluidsynth) play WAV.
// Mi key (thưng = track.id) mt Audio element note mi stop note cũ;
// token chng stale (response cũ không đè response mi).
const _nativeSfPreviews = {};
const playNativeSfNote = (track, pitch, velocity, durationMs, startTime, key) => {
try {
const eng = track && track.synth_engine;
const sfId = (eng && eng.soundfont_id) || (track && track.instrumentId && String(track.instrumentId).startsWith('sf_') ? track.instrumentId : null);
if (!sfId) return;
const bank = (eng && eng.soundfont_bank) || 0;
const program = (eng && eng.soundfont_program) || (track && track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
const k = key || (track ? track.id : 'global');
const prev = _nativeSfPreviews[k];
const token = (prev ? prev.token : 0) + 1;
const durationSec = Math.max(0.2, (durationMs || 500) / 1000);
window.SonicAPI.soundfontRender({
soundfont_id: sfId,
bank: bank,
program: program,
bpm: 120,
notes: [{ pitch: pitch, start_beat: 0, duration_beats: durationSec * 2, velocity: velocity != null ? velocity : 0.8 }],
}).then(function (res) {
if (!res || !res.success || !res.url) return;
if (_nativeSfPreviews[k] && _nativeSfPreviews[k].token !== token) return; // stale
const audio = new Audio(API_BASE_URL + res.url);
_nativeSfPreviews[k] = { audio: audio, token: token };
const ctx = getAudioContext();
const delay = startTime ? Math.max(0, (startTime - ctx.currentTime) * 1000) : 0;
setTimeout(function () {
if (!_nativeSfPreviews[k] || _nativeSfPreviews[k].audio !== audio) return;
audio.play().catch(function () {});
audio._sfStopTimer && clearTimeout(audio._sfStopTimer);
audio._sfStopTimer = setTimeout(function () { try { audio.pause(); } catch (e) {} }, durationSec * 1000 + 400);
}, delay);
}).catch(function () {});
} catch (e) { console.warn('[NativeSF] playNativeSfNote error:', e); }
};
const stopNativeSfNote = (key) => {
try {
const k = key || 'global';
const prev = _nativeSfPreviews[k];
if (!prev) return;
prev.token++;
try { if (prev.audio) { prev.audio.pause(); prev.audio.currentTime = 0; } } catch (e) {}
} catch (e) {}
};
const stopAllNativeSfNotes = () => {
try { Object.keys(_nativeSfPreviews).forEach(function (k) { stopNativeSfNote(k); }); } catch (e) {}
};
// Native soundfont item render (standalone, transport)
// Render TOÀN B MIDI item bng native FluidSynth decode AudioBuffer
// schedule ngun audio đúng v trí item (ging audio clip). opts:
// baseOffsetSec: offset thêm (section: secStart) | limitSec: chn ti secEnd
// isActive(): guard stop gia chng | sources: mng ngun đ stop.
const scheduleNativeSfItem = (track, item, offsetTime, context, destNode, bpm, opts) => {
try {
const eng = track && track.synth_engine;
const sfId = eng && eng.soundfont_id;
if (!sfId) return;
const notes = (item && item.notes) || [];
if (!notes.length) return;
const secPerBeat = 60.0 / (parseInt(bpm) || 120);
const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
const itemStartAbs = baseOffsetSec + (item.startTime || 0);
let itemEndAbs = itemStartAbs + 0.05;
notes.forEach(function (n) {
const end = itemStartAbs + ((n.start_beat || 0) + (n.duration_beats || 1)) * secPerBeat;
if (end > itemEndAbs) itemEndAbs = end;
});
window.SonicAPI.soundfontRender({
soundfont_id: sfId,
bank: (eng && eng.soundfont_bank) || 0,
program: (eng && eng.soundfont_program) || 0,
bpm: parseFloat(bpm) || 120,
notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
}).then(function (res) {
if (!res || !res.success || !res.url) return;
fetch(API_BASE_URL + res.url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
context.decodeAudioData(buf, function (audioBuf) {
try {
if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
const src = context.createBufferSource();
src.buffer = audioBuf;
src.connect(destNode);
if (offsetTime < itemStartAbs) {
src.start(context.currentTime + (itemStartAbs - offsetTime), 0);
} else if (offsetTime < itemEndAbs) {
src.start(context.currentTime, Math.min(offsetTime - itemStartAbs, Math.max(0, audioBuf.duration - 0.05)));
} else {
return;
}
if (opts && opts.limitSec && opts.limitSec < itemStartAbs + audioBuf.duration) {
src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(offsetTime, itemStartAbs)));
}
if (opts && opts.sources && opts.sources.push) opts.sources.push(src);
} catch (e) { console.warn('[NativeSF] schedule decode error:', e); }
}, function () {});
}).catch(function () {});
}).catch(function () {});
} catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }
};
// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() {
try {
@@ -8021,6 +8148,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
ensureSonicInstrument(pvCtx);
playing.forEach(n => {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
});
}
@@ -8387,7 +8518,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
ensureSonicInstrument(clCtx);
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else {
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
}
}
}
@@ -8643,12 +8778,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { previewNodesRef.current.gain.disconnect(); } catch(e) {}
previewNodesRef.current = null;
}
if (window.SonicSF && window.SonicSF.playNote) {
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
const ctx = getAudioContext();
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
// playNote (FluidSynth nhc c THT ca track). _playNoteFallback ch
// là oscillator beep (sai âm vi percussion/soundfont user: note v
// mi nghe nhc c track trưc).
@@ -8744,10 +8881,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Đng b mastering + routing SF trưc khi preview draw (âm qua
// mastering FX ca main out khi chain bt)
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
if (window.SonicSF && window.SonicSF.playNote) {
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext();
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
var pvVel = Math.round(brushVelocityRef.current * 127);
// playNote (FluidSynth nhc c THT). _playNoteFallback = oscillator
// beep sai âm (percussion/soundfont).
@@ -9137,7 +9276,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
if (window.SonicSF) {
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine);
if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicSF && !kbNative) {
// FIX: gi note theo thi gian bm phím durationMs ln (5s)
// ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng
// NGAY (trưc đây 500ms note t tt gia chng khi gi phím).
@@ -9159,7 +9302,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
if (window.SonicSF) {
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
// gi note khi kéo qua phím (mouse enter) dng bng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
@@ -9175,6 +9321,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
keybedMouseDownRef.current = false;
// Dng note khi th phím tránh kt âm (loop liên tc) vi soundfont
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
@@ -9184,6 +9331,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (!keybedMouseDownRef.current) return;
// Kéo chut ra khi phím dng note ca phím đó
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
@@ -13873,6 +14021,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token);
}
};
@@ -13948,6 +14097,43 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
allNotes.push(Object.assign({}, note, { trackOffset: track.startTime || 0 }));
});
});
// Standalone + instrument soundfont render C FILE bng native
// FluidSynth (backend) play WAV (loop theo flag/la chn), b per-note
// WASM (WASM không phi lung âm ca standalone).
if (isStandaloneSf() && sfId) {
const nativeNotes = allNotes
.filter(note => !(hasSelection && ((note.start_beat || 0) < loopStartBeats || (note.start_beat || 0) >= loopEndBeats)))
.map(note => {
const shiftedStartBeat = hasSelection ? ((note.start_beat || 0) - loopStartBeats) : (note.start_beat || 0);
return {
pitch: note.pitch || 60,
start_beat: shiftedStartBeat + ((note.trackOffset || 0) / secondsPerBeat),
duration_beats: note.duration_beats || 1,
velocity: note.velocity != null ? note.velocity : 0.8
};
});
if (nativeNotes.length) {
window.SonicAPI.soundfontRender({ soundfont_id: sfId, bank: bank, program: prog !== undefined ? prog : 0, bpm: bpmVal, notes: nativeNotes }).then(function (res) {
if (!res || !res.success || !res.url) return;
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
const audio = new Audio(API_BASE_URL + res.url);
audio.loop = !!isLoopingRef.current;
_nativeSfPreviews['midifile'] = { audio: audio, token: selectTokenRef.current };
audio.play().catch(function () {});
});
}
const startedAtTime = startWallTime - loopStartSec;
playStateRef.current = { source: null, ctx, startedAt: startedAtTime, fakeStart: startedAtTime, midiTotal: totalSec };
setMidiNotes(allNotes);
setMidiTotal(totalSec);
setMidiBars(midiResult[0].bars || 1);
setMidiTotalBeats(midiResult[0].totalBeats || 16);
setMidiFileBpm(midiResult[0].bpm || 120);
setIsPlaying(true);
setIsPaused(false);
startCanvasClock();
return;
}
const schedulePass = (passStartTime) => {
allNotes.forEach(note => {
const noteStartBeat = note.start_beat || 0;
@@ -13996,6 +14182,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = null;
if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; }
stopNativeSfNote('midifile');
if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
try { window.SonicSF.stopAll(); } catch (e) {}
}
@@ -14310,6 +14497,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token);
}
};
@@ -14480,6 +14668,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token);
}
}
@@ -15107,6 +15296,7 @@ const App = () => {
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
window.SonicCarlaMidi.stopBridge();
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
}
} catch (e) { console.warn('[Instrument] carla-stop on instrument switch error:', e); }
@@ -15364,7 +15554,11 @@ const App = () => {
try {
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
} catch (err) { }
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
}
// MIDI Keyboard Carla bridge (piano roll sub-tab ARM):
// track VSTi ca sub-tab phát VSTi realtime ging keybed.
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(asSe, true)) {
@@ -15385,7 +15579,11 @@ const App = () => {
try {
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
} catch (err) { }
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
}
// MIDI Keyboard Carla bridge (track VSTi + ARM + Carla
// local): phát VSTi realtime ging ht keybed o. Trưc
// đây ch keybed gi SonicCarlaMidi; keyboard hardware b
@@ -15414,6 +15612,7 @@ const App = () => {
// channel and kill its sound.
if (!st.synth_engine && st.midiChannel === undefined) return;
var stCh = assignTrackMidiChannel(st, stopTracks);
if (isStandaloneSf()) stopNativeSfNote('midi_' + st.id + '_' + pitch);
window.SonicSF.stopNote(stCh, pitch);
// MIDI Keyboard Carla bridge: note-off khi th phím tránh
// kt âm VSTi (ging keybed o).
@@ -21235,6 +21434,11 @@ const App = () => {
const destNode = getOrCreateTrackNode(track, context);
const program = track ? track.instrumentProgram : undefined;
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
const routeCarla = shouldRouteCarla(track && track.synth_engine);
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
}
window.SonicSF.playNote(
pitch,
velocity,
@@ -21306,7 +21510,12 @@ const App = () => {
// MIDI items playback
const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)))) {
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 }));
} else {
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine);
@@ -21388,6 +21597,7 @@ const App = () => {
}
});
});
}
}
// If track has instrumentId set but no MIDI items, create scheduled oscillators
if (track.instrumentId && midiItems.length === 0 && track.buffer) {
@@ -21464,7 +21674,13 @@ const App = () => {
// 2. Play MIDI items in subTrack
const subMidiItems = subTrack.midiItems || [];
if (subMidiItems.length > 0 && (window.SonicSF || (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)))) {
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine);
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
if (subNativeSf) {
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
return; // native render audio buffer (không qua WASM/Carla per-note)
}
subMidiItems.forEach(item => {
const notes = item.notes || [];
notes.forEach(note => {
@@ -21592,7 +21808,13 @@ const App = () => {
// MIDI items playback
const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)))) {
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;
@@ -21675,6 +21897,11 @@ const App = () => {
// items placed later in the project play `item.startTime` seconds in the
// future (silence when pressing play). Ghost notes are already relative to
// the item window, so no absolute-session offset is applied anywhere here.
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;
@@ -21815,6 +22042,7 @@ const App = () => {
}
setTimeout(() => {
window.SonicSF.stopAll();
stopAllNativeSfNotes();
if (prTNode && prTNode.gainNode) {
const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null;
const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0;
@@ -21854,6 +22082,7 @@ const App = () => {
// tick vn thy heldCnt > 0 gi peak VU dính mãi (user bug 08:08).
try { heldMidiNotesRef.current = {}; } catch (e) { }
stopMidiCapture();
stopAllNativeSfNotes();
if (window.SonicSF) {
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
// Dng trit đ: noteoff tng note + hy scheduled note-on (hết âm stuck)
@@ -21877,6 +22106,7 @@ const App = () => {
if (st.isPlaying) {
stopAllPlayback();
window.SonicSF.stopAll();
stopAllNativeSfNotes();
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s,
currentTime: time,
@@ -29439,6 +29669,7 @@ STRICT CONSTRAINTS:
}
setTimeout(() => {
window.SonicSF.stopAll();
stopAllNativeSfNotes();
if (tNode && tNode.gainNode) {
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
@@ -29459,6 +29690,7 @@ STRICT CONSTRAINTS:
if (seekSt.isPlaying) {
stopAllPlayback();
window.SonicSF.stopAll();
stopAllNativeSfNotes();
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
const ctx = getAudioContext();
startOffsetTimeRef.current = clickTime;