T5: unified MIDI routing - thay native preview bang UnifiedMidiRouter + TrackInstrument (per-track FluidSynth channel); keybed/click/draw/hardware MIDI note qua router, fallback SonicSF; _panicMidi stop moi voice; bo _nativeSfPreviews/playNativeSfNote/stopNativeSfNote/stopAllNativeSfNotes/_sfWasmFallbackNote
This commit is contained in:
+127
-116
@@ -200,101 +200,86 @@ const isVstTrackEngine = (se) => !!se && String(se.type || '').indexOf('vst') !=
|
|||||||
// Track dùng VSTi + Carla local → route Carla (native GUI, realtime).
|
// Track dùng VSTi + Carla local → route Carla (native GUI, realtime).
|
||||||
const shouldRouteCarla = () => false; // live playback 100% client (SF2 autosampled WASM) — không route Carla
|
const shouldRouteCarla = () => false; // live playback 100% client (SF2 autosampled WASM) — không route Carla
|
||||||
|
|
||||||
// ── Native soundfont preview (standalone) ─────────────────────────────────
|
// ── Unified MIDI routing (preview mọi nguồn) ───────────────────────────────
|
||||||
// Render 1 note bằng backend native FluidSynth (pyfluidsynth) → play WAV.
|
// Mọi note preview (keybed, piano roll, draw, hardware MIDI, click) đi qua
|
||||||
// Mỗi key (thường = track.id) một Audio element — note mới stop note cũ;
|
// UnifiedMidiRouter + TrackInstrument (FluidSynth channel per-track). Router
|
||||||
// token chống stale (response cũ không đè response mới).
|
// chống re-trigger (note-on trùng key chỉ tăng count), panicAllNotesOff dừng
|
||||||
const _nativeSfPreviews = {};
|
// mọi voice khi stop. Fallback SonicSF trực tiếp nếu router chưa sẵn sàng.
|
||||||
// WASM fallback: backend pyfluidsynth render HONG tren ban standalone
|
let _midiRouter = null;
|
||||||
// (thieu libfluidsynth DLL) -> /soundfont-render 501 -> apiRequest throw ->
|
let _midiFilePreviewAudio = null;
|
||||||
// native path cam lang. Fallback choi note TRUC TIEP qua SonicSF (Web Audio
|
const _getMidiRouter = () => {
|
||||||
// + libfluidsynth WASM da bundle) - am ra _gainNode -> masterBus.input.
|
|
||||||
// window.__nativeSfOk: undefined (chua biet) | true (native OK) | false (dung WASM).
|
|
||||||
const _sfWasmFallbackNote = (track, pitch, velocity, durationMs, startTime, key, token) => {
|
|
||||||
try {
|
try {
|
||||||
const eng = track && track.synth_engine;
|
if (!_midiRouter && window.SonicUnifiedMidiRouter && window.SonicUnifiedMidiRouter.UnifiedMidiRouter && window.TrackInstrument) {
|
||||||
const sfId = (eng && eng.soundfont_id) || (track && track.instrumentId && String(track.instrumentId).startsWith('sf_') ? track.instrumentId : null);
|
_midiRouter = new window.SonicUnifiedMidiRouter.UnifiedMidiRouter();
|
||||||
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');
|
|
||||||
if (_nativeSfPreviews[k] && token != null && _nativeSfPreviews[k].token > token) return; // stale (claim-then-compare: chi bo neu co note MOI HON)
|
|
||||||
const S = window.SonicSF;
|
|
||||||
if (!S) return;
|
|
||||||
const ch = (track && track.midiChannel != null) ? track.midiChannel : 0;
|
|
||||||
const durMs = Math.max(200, durationMs || 500);
|
|
||||||
const ctx = getAudioContext();
|
|
||||||
const delaySec = startTime ? Math.max(0, (startTime - ctx.currentTime)) : 0;
|
|
||||||
_nativeSfPreviews[k] = { wasm: { ch: ch, pitch: pitch }, token: token };
|
|
||||||
const fire = function () {
|
|
||||||
Promise.resolve(S.selectInstrument(ch, bank, program, sfId)).then(function () {
|
|
||||||
S.playNote(pitch, velocity != null ? velocity : 0.8, durMs, undefined, eng ? undefined : program, null, ch, eng || undefined);
|
|
||||||
}).catch(function () {
|
|
||||||
try { S.playNote(pitch, velocity != null ? velocity : 0.8, durMs, undefined, eng ? undefined : program, null, ch, eng || undefined); } catch (e2) {}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (delaySec > 0) { setTimeout(fire, delaySec * 1000); } else { fire(); }
|
|
||||||
} catch (e) { console.warn('[NativeSF] wasm fallback note error:', e); }
|
|
||||||
};
|
|
||||||
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;
|
|
||||||
// Native hong da biet (501) -> di thang WASM fallback, khong goi API.
|
|
||||||
if (window.__nativeSfOk === false) {
|
|
||||||
_sfWasmFallbackNote(track, pitch, velocity, durationMs, startTime, k, token);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const durationSec = Math.max(0.2, (durationMs || 500) / 1000);
|
} catch (e) { _midiRouter = null; }
|
||||||
window.SonicAPI.soundfontRender({
|
return _midiRouter;
|
||||||
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) {
|
|
||||||
window.__nativeSfOk = false;
|
|
||||||
_sfWasmFallbackNote(track, pitch, velocity, durationMs, startTime, k, token);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_nativeSfPreviews[k] && _nativeSfPreviews[k].token > token) return; // stale (claim-then-compare: chi bo neu co note MOI HON)
|
|
||||||
window.__nativeSfOk = true;
|
|
||||||
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 () {
|
|
||||||
window.__nativeSfOk = false;
|
|
||||||
_sfWasmFallbackNote(track, pitch, velocity, durationMs, startTime, k, token);
|
|
||||||
});
|
|
||||||
} catch (e) { console.warn('[NativeSF] playNativeSfNote error:', e); }
|
|
||||||
};
|
};
|
||||||
const stopNativeSfNote = (key) => {
|
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).
|
||||||
|
const _routeNoteOn = (trackId, ctx, pitch, velocity, durationMs, sourceType) => {
|
||||||
|
if (trackId === undefined || trackId === null || !ctx) return false;
|
||||||
|
const router = _getMidiRouter();
|
||||||
|
if (!router) return false;
|
||||||
try {
|
try {
|
||||||
const k = key || 'global';
|
const inst = new window.TrackInstrument(trackId, ctx);
|
||||||
const prev = _nativeSfPreviews[k];
|
router.registerEngine(trackId, inst);
|
||||||
if (!prev) return;
|
router.dispatchMidiEvent({
|
||||||
prev.token++;
|
trackId,
|
||||||
try { if (prev.audio) { prev.audio.pause(); prev.audio.currentTime = 0; } } catch (e) {}
|
command: 0x90,
|
||||||
try { if (prev.wasm && window.SonicSF) window.SonicSF.stopNote(prev.wasm.ch, prev.wasm.pitch); } catch (e) {}
|
channel: ctx.ch,
|
||||||
} catch (e) {}
|
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 stopAllNativeSfNotes = () => {
|
const _routeNoteOff = (trackId, ctx, pitch, sourceType) => {
|
||||||
try { Object.keys(_nativeSfPreviews).forEach(function (k) { stopNativeSfNote(k); }); } catch (e) {}
|
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).
|
||||||
|
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) ──────────────────
|
// ── 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 →
|
||||||
@@ -8291,7 +8276,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
ensureSonicInstrument(pvCtx);
|
ensureSonicInstrument(pvCtx);
|
||||||
playing.forEach(n => {
|
playing.forEach(n => {
|
||||||
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
|
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
|
||||||
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
|
if (!_routePreviewNote(pvTrk && pvTrk.id, pvCtx, n.pitch, n.velocity || 0.8, 200, 'PLAYING_TICK')) {
|
||||||
|
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
|
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
|
||||||
@@ -8661,7 +8648,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
|
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
|
||||||
ensureSonicInstrument(clCtx);
|
ensureSonicInstrument(clCtx);
|
||||||
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(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);
|
if (!_routePreviewNote(clTrk && clTrk.id, clCtx, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, 'CLICK')) {
|
||||||
|
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||||
}
|
}
|
||||||
@@ -8921,11 +8910,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
previewNodesRef.current = null;
|
previewNodesRef.current = null;
|
||||||
}
|
}
|
||||||
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
|
var dwCtx = resolveTrackInstrumentCtx(dwTrk, activeTracks);
|
||||||
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
||||||
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
||||||
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
||||||
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
|
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
|
||||||
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
|
if (!_routePreviewNote(dwTrk && dwTrk.id, dwCtx, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, 'DRAW')) {
|
||||||
|
if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
||||||
@@ -9026,7 +9021,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
||||||
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
|
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
|
||||||
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
|
if (!_routePreviewNote(pvTrk && pvTrk.id, pvCtxInst, p, brushVelocityRef.current || 0.8, durMs, 'DRAW')) {
|
||||||
|
if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
|
var pvCtx = getAudioContext();
|
||||||
|
var pvVel = Math.round(brushVelocityRef.current * 127);
|
||||||
|
window.SonicSF.playNote(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
|
||||||
|
previewPitchRef.current = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
var pvCtx = getAudioContext();
|
var pvCtx = getAudioContext();
|
||||||
var pvVel = Math.round(brushVelocityRef.current * 127);
|
var pvVel = Math.round(brushVelocityRef.current * 127);
|
||||||
@@ -9420,9 +9422,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
}
|
}
|
||||||
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine);
|
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine);
|
||||||
if (kbNative) {
|
if (kbNative) {
|
||||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
if (!_routeNoteOn(kbTrk && kbTrk.id, kbCtx, pitch, 100 / 127, 5000, 'KEYBED')) {
|
||||||
}
|
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
||||||
if (window.SonicSF && !kbNative) {
|
}
|
||||||
|
} else if (window.SonicSF) {
|
||||||
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
|
// ⚠️ 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
|
// 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).
|
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
|
||||||
@@ -9445,9 +9448,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
|
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
|
||||||
}
|
}
|
||||||
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
|
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
|
||||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
if (!_routeNoteOn(kbTrk && kbTrk.id, kbCtx, pitch, 100 / 127, 5000, 'KEYBED')) {
|
||||||
}
|
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
||||||
if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
|
}
|
||||||
|
} else if (window.SonicSF) {
|
||||||
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
|
// 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);
|
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
||||||
}
|
}
|
||||||
@@ -9463,7 +9467,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
keybedMouseDownRef.current = false;
|
keybedMouseDownRef.current = false;
|
||||||
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
|
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
|
||||||
try {
|
try {
|
||||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
if (isStandaloneSf()) _routeNoteOff(kbTrk && kbTrk.id, kbCtx, pitch, 'KEYBED');
|
||||||
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
||||||
@@ -9473,7 +9477,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
if (!keybedMouseDownRef.current) return;
|
if (!keybedMouseDownRef.current) return;
|
||||||
// Kéo chuột ra khỏi phím → dừng note của phím đó
|
// Kéo chuột ra khỏi phím → dừng note của phím đó
|
||||||
try {
|
try {
|
||||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
if (isStandaloneSf()) _routeNoteOff(kbTrk && kbTrk.id, kbCtx, pitch, 'KEYBED');
|
||||||
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
||||||
@@ -14167,7 +14171,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
|||||||
selectTokenRef.current++;
|
selectTokenRef.current++;
|
||||||
const token = selectTokenRef.current;
|
const token = selectTokenRef.current;
|
||||||
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
playMidiPreview(cur, token);
|
playMidiPreview(cur, token);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -14264,7 +14268,8 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
|||||||
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
|
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
|
||||||
const audio = new Audio(API_BASE_URL + res.url);
|
const audio = new Audio(API_BASE_URL + res.url);
|
||||||
audio.loop = !!isLoopingRef.current;
|
audio.loop = !!isLoopingRef.current;
|
||||||
_nativeSfPreviews['midifile'] = { audio: audio, token: selectTokenRef.current };
|
if (_midiFilePreviewAudio) { try { _midiFilePreviewAudio.pause(); } catch (e) {} }
|
||||||
|
_midiFilePreviewAudio = audio;
|
||||||
audio.play().catch(function () {});
|
audio.play().catch(function () {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -14328,7 +14333,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
|||||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||||
rafRef.current = null;
|
rafRef.current = null;
|
||||||
if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; }
|
if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; }
|
||||||
stopNativeSfNote('midifile');
|
_stopMidiFilePreview();
|
||||||
if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
|
if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
|
||||||
try { window.SonicSF.stopAll(); } catch (e) {}
|
try { window.SonicSF.stopAll(); } catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -14643,7 +14648,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
|||||||
selectTokenRef.current++;
|
selectTokenRef.current++;
|
||||||
const token = selectTokenRef.current;
|
const token = selectTokenRef.current;
|
||||||
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
playMidiPreview(cur, token);
|
playMidiPreview(cur, token);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -14814,7 +14819,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
|||||||
selectTokenRef.current++;
|
selectTokenRef.current++;
|
||||||
const token = selectTokenRef.current;
|
const token = selectTokenRef.current;
|
||||||
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
playMidiPreview(cur, token);
|
playMidiPreview(cur, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15472,7 +15477,7 @@ const App = () => {
|
|||||||
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
|
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
|
||||||
window.SonicCarlaMidi.stopBridge();
|
window.SonicCarlaMidi.stopBridge();
|
||||||
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
|
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
|
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); }
|
} catch (e) { console.warn('[Instrument] carla-stop on instrument switch error:', e); }
|
||||||
@@ -15742,7 +15747,9 @@ const App = () => {
|
|||||||
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
|
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
|
if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
|
||||||
playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
|
if (!_routeNoteOn(asTrk && asTrk.id, _makeCtx(asCh, asProg, asSe, null), pitch, scaledVel / 127, 60000, 'HARDWARE_KEYBOARD')) {
|
||||||
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
|
||||||
}
|
}
|
||||||
@@ -15767,7 +15774,9 @@ const App = () => {
|
|||||||
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
|
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
|
if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
|
||||||
playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
|
if (!_routeNoteOn(at && at.id, _makeCtx(atCh, atProg, atSe, atDest), pitch, scaledVel / 127, 60000, 'HARDWARE_KEYBOARD')) {
|
||||||
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
|
window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
|
||||||
}
|
}
|
||||||
@@ -15799,7 +15808,7 @@ const App = () => {
|
|||||||
// channel and kill its sound.
|
// channel and kill its sound.
|
||||||
if (!st.synth_engine && st.midiChannel === undefined) return;
|
if (!st.synth_engine && st.midiChannel === undefined) return;
|
||||||
var stCh = assignTrackMidiChannel(st, stopTracks);
|
var stCh = assignTrackMidiChannel(st, stopTracks);
|
||||||
if (isStandaloneSf()) stopNativeSfNote('midi_' + st.id + '_' + pitch);
|
if (isStandaloneSf()) _routeNoteOff(st.id, { ch: stCh }, pitch, 'HARDWARE_KEYBOARD');
|
||||||
window.SonicSF.stopNote(stCh, pitch);
|
window.SonicSF.stopNote(stCh, pitch);
|
||||||
// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh
|
// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh
|
||||||
// kẹt âm VSTi (giống keybed ảo).
|
// kẹt âm VSTi (giống keybed ảo).
|
||||||
@@ -21681,7 +21690,9 @@ const App = () => {
|
|||||||
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
|
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
|
||||||
const routeCarla = shouldRouteCarla(track && track.synth_engine);
|
const routeCarla = shouldRouteCarla(track && track.synth_engine);
|
||||||
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
|
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
|
||||||
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
|
if (!_routePreviewNote(track && track.id, resolveTrackInstrumentCtx(track, activeTracks), pitch, velocity, durationMs, 'PREVIEW')) {
|
||||||
|
window.SonicSF.playNote(pitch, velocity, durationMs, context.currentTime, program, destNode, prevCh, track ? track.synth_engine : undefined);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.SonicSF.playNote(
|
window.SonicSF.playNote(
|
||||||
@@ -22287,7 +22298,7 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.SonicSF.stopAll();
|
window.SonicSF.stopAll();
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
if (prTNode && prTNode.gainNode) {
|
if (prTNode && prTNode.gainNode) {
|
||||||
const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null;
|
const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null;
|
||||||
const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0;
|
const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0;
|
||||||
@@ -22327,7 +22338,7 @@ const App = () => {
|
|||||||
// tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
|
// tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
|
||||||
try { heldMidiNotesRef.current = {}; } catch (e) { }
|
try { heldMidiNotesRef.current = {}; } catch (e) { }
|
||||||
stopMidiCapture();
|
stopMidiCapture();
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
if (window.SonicSF) {
|
if (window.SonicSF) {
|
||||||
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
|
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
|
||||||
// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
|
// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
|
||||||
@@ -22351,7 +22362,7 @@ const App = () => {
|
|||||||
if (st.isPlaying) {
|
if (st.isPlaying) {
|
||||||
stopAllPlayback();
|
stopAllPlayback();
|
||||||
window.SonicSF.stopAll();
|
window.SonicSF.stopAll();
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
|
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
|
||||||
...s,
|
...s,
|
||||||
currentTime: time,
|
currentTime: time,
|
||||||
@@ -29919,7 +29930,7 @@ STRICT CONSTRAINTS:
|
|||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.SonicSF.stopAll();
|
window.SonicSF.stopAll();
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
if (tNode && tNode.gainNode) {
|
if (tNode && tNode.gainNode) {
|
||||||
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
|
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
|
||||||
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
|
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
|
||||||
@@ -29940,7 +29951,7 @@ STRICT CONSTRAINTS:
|
|||||||
if (seekSt.isPlaying) {
|
if (seekSt.isPlaying) {
|
||||||
stopAllPlayback();
|
stopAllPlayback();
|
||||||
window.SonicSF.stopAll();
|
window.SonicSF.stopAll();
|
||||||
stopAllNativeSfNotes();
|
_panicMidi();
|
||||||
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
|
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
startOffsetTimeRef.current = clickTime;
|
startOffsetTimeRef.current = clickTime;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
|
|||||||
|
// SonicForge TrackInstrument Service
|
||||||
|
// Bọc FluidSynth channel per-track: playNote/noteOff theo TrackInstrumentCtx
|
||||||
|
// (ch/program/synthEngine/sfId/bank/prog/dest) — dùng cho preview mọi nguồn
|
||||||
|
// (keybed, piano roll, draw, hardware MIDI, click) qua UnifiedMidiRouter.
|
||||||
|
// Tạo mới mỗi note-on (overwrite registry) — voice cũ giữ engine cũ trong
|
||||||
|
// tracker nên note-off luôn stop đúng channel.
|
||||||
|
window.TrackInstrument = window.TrackInstrument || {};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
function TrackInstrument(trackId, ctx) {
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.ch = ctx ? (ctx.ch != null ? ctx.ch : 0) : 0;
|
||||||
|
this.program = ctx ? ctx.program : undefined;
|
||||||
|
this.synthEngine = ctx ? ctx.synthEngine : undefined;
|
||||||
|
this.sfId = ctx ? ctx.sfId : undefined;
|
||||||
|
this.bank = ctx ? (ctx.bank || 0) : 0;
|
||||||
|
this.prog = ctx ? (ctx.prog || 0) : 0;
|
||||||
|
this.dest = ctx ? (ctx.dest || null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đảm bảo channel đã select đúng instrument trước khi play (fire-and-forget:
|
||||||
|
// playNote tự load + retry nếu SF chưa load xong — dedup trong loadSoundFont).
|
||||||
|
TrackInstrument.prototype._ensure = function () {
|
||||||
|
try {
|
||||||
|
if (!window.SonicSF || !window.SonicSF.selectInstrument) return;
|
||||||
|
if (this.sfId) {
|
||||||
|
window.SonicSF.selectInstrument(this.ch, this.bank, this.prog, this.sfId);
|
||||||
|
} else if (this.program !== undefined) {
|
||||||
|
window.SonicSF.selectInstrument(this.ch, 0, this.program, null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TrackInstrument] selectInstrument error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TrackInstrument.prototype.playNote = function (pitch, velocity, durationMs) {
|
||||||
|
try {
|
||||||
|
if (!window.SonicSF || !window.SonicSF.playNote) return;
|
||||||
|
this._ensure();
|
||||||
|
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);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TrackInstrument] playNote error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TrackInstrument.prototype.noteOff = function (pitch) {
|
||||||
|
try {
|
||||||
|
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(this.ch, pitch);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.TrackInstrument = TrackInstrument;
|
||||||
|
})();
|
||||||
@@ -36,6 +36,8 @@
|
|||||||
<script src="/static/js/services/runtime.js?v=202608111230"></script>
|
<script src="/static/js/services/runtime.js?v=202608111230"></script>
|
||||||
<script src="/static/js/services/api.js?v=202608111230"></script>
|
<script src="/static/js/services/api.js?v=202608111230"></script>
|
||||||
<script src="/static/js/services/vstiAutosample.js?v=202608111230"></script>
|
<script src="/static/js/services/vstiAutosample.js?v=202608111230"></script>
|
||||||
|
<script src="/static/js/services/unifiedMidiRouter.js?v=202608111230"></script>
|
||||||
|
<script src="/static/js/services/trackInstrument.js?v=202608111230"></script>
|
||||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user