feat(standalone): native VSTi midi-render cho browser client khi bridge inactive

- playNativeVstNote/scheduleNativeVstItem: render qua /api/v1/plugins/midi-render
  (pedalboard VST3 dung instrument_id/preset) -> play WAV, map token rieng
- Them nhanh nativeVst tai moi diem route nhanh cua: startTrackPlayback,
  section/subtrack, playMidiPreviewNote, piano roll preview (alt-scroll/click/
  brush/draw), keybed, MIDI hardware input
- Safety net trong scheduleMidiNoteDispatch fallthrough (ghost notes va caller khac)
- stopAllNativeVstNotes/stopNativeVstNote di kem cac diem stop hien co
This commit is contained in:
2026-08-16 18:23:53 +07:00
parent b2470e1422
commit 1d40cf27b7
2 changed files with 203 additions and 44 deletions
+165 -18
View File
@@ -296,6 +296,107 @@ const scheduleNativeSfItem = (track, item, offsetTime, context, destNode, bpm, o
} catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }
};
// ── Native VSTi preview (standalone, bridge inactive) ─────────────────────
// Render 1 note bằng backend pedalboard (VST3 đúng instrument_id/preset) →
// play WAV — giống hệt nativeSf nhưng qua midiRender; map token riêng.
const _nativeVstPreviews = {};
const playNativeVstNote = (track, pitch, velocity, durationMs, startTime, key) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
if (!instrumentId || !isVstTrackEngine(eng)) return;
const k = key || (track ? track.id : 'global');
const prev = _nativeVstPreviews[k];
const token = (prev ? prev.token : 0) + 1;
const durationSec = Math.max(0.2, (durationMs || 500) / 1000);
window.SonicAPI.midiRender({
instrument_id: instrumentId,
notes: [{ pitch: pitch, start_beat: 0, duration_beats: durationSec * 2, velocity: velocity != null ? velocity : 0.8 }],
bpm: 120,
preset_id: (eng && eng.preset_id) || undefined,
preset_path: (eng && eng.preset_path) || undefined,
preset_data: (eng && eng.preset_data) || undefined,
}).then(function (res) {
if (!res || !res.success || !res.url) return;
if (_nativeVstPreviews[k] && _nativeVstPreviews[k].token !== token) return; // stale
const audio = new Audio(API_BASE_URL + res.url);
_nativeVstPreviews[k] = { audio: audio, token: token };
const ctx = getAudioContext();
const delay = startTime ? Math.max(0, (startTime - ctx.currentTime) * 1000) : 0;
setTimeout(function () {
if (!_nativeVstPreviews[k] || _nativeVstPreviews[k].audio !== audio) return;
audio.play().catch(function () {});
audio._vstStopTimer && clearTimeout(audio._vstStopTimer);
audio._vstStopTimer = setTimeout(function () { try { audio.pause(); } catch (e) {} }, durationSec * 1000 + 400);
}, delay);
}).catch(function () {});
} catch (e) { console.warn('[NativeVST] playNativeVstNote error:', e); }
};
const stopNativeVstNote = (key) => {
try {
const k = key || 'global';
const prev = _nativeVstPreviews[k];
if (!prev) return;
prev.token++;
try { if (prev.audio) { prev.audio.pause(); prev.audio.currentTime = 0; } } catch (e) {}
} catch (e) {}
};
const stopAllNativeVstNotes = () => {
try { Object.keys(_nativeVstPreviews).forEach(function (k) { stopNativeVstNote(k); }); } catch (e) {}
};
// ── Native VSTi item render (standalone, transport) ────────────────────────
// Render TOÀN BỘ MIDI item bằng pedalboard (VST3) → decode AudioBuffer →
// schedule giống scheduleNativeSfItem. opts giống hệt nativeSf.
const scheduleNativeVstItem = (track, item, offsetTime, context, destNode, bpm, opts) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
if (!instrumentId || !isVstTrackEngine(eng)) 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.midiRender({
instrument_id: instrumentId,
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 }; }),
bpm: parseFloat(bpm) || 120,
preset_id: (eng && eng.preset_id) || undefined,
preset_path: (eng && eng.preset_path) || undefined,
preset_data: (eng && eng.preset_data) || undefined,
}).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('[NativeVST] schedule decode error:', e); }
}, function () {});
}).catch(function () {});
}).catch(function () {});
} catch (e) { console.warn('[NativeVST] scheduleNativeVstItem error:', e); }
};
// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() {
try {
@@ -8328,6 +8429,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
if (isStandaloneSf() && isVstTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
});
}
@@ -8696,6 +8801,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ensureSonicInstrument(clCtx);
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else if (isStandaloneSf() && isVstTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else {
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
}
@@ -8960,6 +9067,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (isStandaloneSf() && isVstTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
const ctx = getAudioContext();
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
@@ -9061,6 +9170,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (isStandaloneSf() && isVstTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext();
// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
@@ -9452,13 +9563,16 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
}
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const kbNativeVst = isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} else if (kbNativeVst) {
playNativeVstNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && !kbNative) {
} else if (window.SonicSF && !kbNative && !kbNativeVst) {
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
@@ -9482,11 +9596,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
}
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} else if (isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
} else if (window.SonicSF && !(isStandaloneSf() && (isSfTrackEngine(kbCtx.synthEngine) || isVstTrackEngine(kbCtx.synthEngine)) && !shouldRouteCarla(kbCtx.synthEngine))) {
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
@@ -9502,7 +9618,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
keybedMouseDownRef.current = false;
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (isStandaloneSf()) { stopNativeSfNote('kb_' + st.trackId + '_' + pitch); stopNativeVstNote('kb_' + st.trackId + '_' + pitch); }
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
@@ -9514,7 +9630,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (!keybedMouseDownRef.current) return;
// Kéo chuột ra khỏi phím → dừng note của phím đó
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (isStandaloneSf()) { stopNativeSfNote('kb_' + st.trackId + '_' + pitch); stopNativeVstNote('kb_' + st.trackId + '_' + pitch); }
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
@@ -14216,7 +14332,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
playMidiPreview(cur, token);
}
};
@@ -14445,7 +14561,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');
stopNativeSfNote('midifile'); stopNativeVstNote('midifile');
if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
try { window.SonicSF.stopAll(); } catch (e) {}
}
@@ -14769,7 +14885,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
playMidiPreview(cur, token);
}
};
@@ -14940,7 +15056,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
playMidiPreview(cur, token);
}
}
@@ -15630,7 +15746,7 @@ const App = () => {
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
window.SonicCarlaMidi.stopBridge();
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
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); }
@@ -15906,6 +16022,8 @@ const App = () => {
} catch (err) { }
if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
} else if (isStandaloneSf() && isVstTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
playNativeVstNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
}
@@ -15931,6 +16049,8 @@ const App = () => {
} catch (err) { }
if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
} else if (isStandaloneSf() && isVstTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
playNativeVstNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
} else {
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
}
@@ -15976,7 +16096,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);
if (isStandaloneSf()) stopNativeSfNote('midi_' + st.id + '_' + pitch); stopNativeVstNote('midi_' + st.id + '_' + pitch);
window.SonicSF.stopNote(stCh, pitch);
// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh
// kẹt âm VSTi (giống keybed ảo).
@@ -21943,6 +22063,10 @@ const App = () => {
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
}
if (isStandaloneSf() && !routeCarla && isVstTrackEngine(track && track.synth_engine)) {
playNativeVstNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
return;
}
window.SonicSF.playNote(
pitch,
velocity,
@@ -21988,6 +22112,12 @@ const App = () => {
}, delayMs + (durMs || 1000) + 30);
return;
}
// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì
// FluidSynth GM sai âm — safety net cho mọi caller (ghost notes, per-note).
if (isStandaloneSf() && isVstTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
playNativeVstNote(track, pitch, velocity, durMs, startTime, track ? track.id : 'global');
return;
}
window.SonicSF.playNote(pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine);
};
@@ -22064,9 +22194,12 @@ const App = () => {
const midiItems = track.midiItems || [];
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
const nativeVst = isStandaloneSf() && !routeCarla && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf || nativeVst)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
} else if (nativeVst) {
midiItems.forEach(item => scheduleNativeVstItem(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]);
@@ -22227,11 +22360,16 @@ const App = () => {
const subMidiItems = subTrack.midiItems || [];
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
const subNativeVst = isStandaloneSf() && !subRouteCarla && isVstTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf || subNativeVst)) {
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)
}
if (subNativeVst) {
subMidiItems.forEach(item => scheduleNativeVstItem(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 => {
@@ -22360,11 +22498,16 @@ const App = () => {
const midiItems = track.midiItems || [];
const routeCarla = shouldRouteCarla(track.synth_engine);
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
const nativeVst = isStandaloneSf() && !routeCarla && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf || nativeVst)) {
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)
}
if (nativeVst) {
midiItems.forEach(item => scheduleNativeVstItem(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;
@@ -22462,6 +22605,10 @@ const App = () => {
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)
}
if (isStandaloneSf() && !routeCarla && isVstTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
scheduleNativeVstItem(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;
@@ -22602,7 +22749,7 @@ const App = () => {
}
setTimeout(() => {
window.SonicSF.stopAll();
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
if (prTNode && prTNode.gainNode) {
const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null;
const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0;
@@ -22647,7 +22794,7 @@ const App = () => {
// tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
try { heldMidiNotesRef.current = {}; } catch (e) { }
stopMidiCapture();
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết âm
// ngân tức thì; A13 C++ flush note-off).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
@@ -22685,7 +22832,7 @@ const App = () => {
if (st.isPlaying) {
stopAllPlayback();
window.SonicSF.stopAll();
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s,
currentTime: time,
@@ -30282,7 +30429,7 @@ STRICT CONSTRAINTS:
}
setTimeout(() => {
window.SonicSF.stopAll();
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
if (tNode && tNode.gainNode) {
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
@@ -30303,7 +30450,7 @@ STRICT CONSTRAINTS:
if (seekSt.isPlaying) {
stopAllPlayback();
window.SonicSF.stopAll();
stopAllNativeSfNotes();
stopAllNativeSfNotes(); stopAllNativeVstNotes();
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
const ctx = getAudioContext();
startOffsetTimeRef.current = clickTime;
File diff suppressed because one or more lines are too long