Remove Carla native VSTi bridge entirely
Drop Carla integration across frontend and backend: - plugins.py: remove /carla-midi, /carla-status, /carla-stop, /open-in-carla, /carla-play-notes endpoints and Carla process management - runtime.py: remove find_carla / _carla_* helpers, detect and capabilities - app.jsx: remove Carla bridge UI, SonicCarlaMidi routing, shouldRouteCarla guards, VST row Carla button and preset fallback - system.py: remove /carla-path - Keep audio path unchanged (tracks -> masterBus -> output -> analyser), directOut only for offline render Build frontend, tests pass (124 passed).
This commit is contained in:
+69
-359
@@ -108,11 +108,7 @@ const ensureSonicInstrument = (ctx) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ── Carla bridge MIDI scheduling helper ────────────────────────────────────
|
||||
// Schedule note_on/note_off tới Carla (OSC qua backend) cho MIDI items khi
|
||||
// track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback
|
||||
// (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI play
|
||||
// qua Carla bridge khi VSTi được loaded.
|
||||
// ── MIDI note-off scheduling helper (pending map chống note chồng) ─────────
|
||||
// FIX (Bug 2): NOTE_OFF trễ (dur+30ms) của note cùng pitch bắn SAU note-on kế
|
||||
// tiếp (bar liền nhau cùng hợp âm, piano roll note chồng) -> cắt note mới →
|
||||
// câm. Map pending theo key channel:pitch — schedule NOTE_OFF mới cùng key →
|
||||
@@ -147,65 +143,10 @@ const clearAllPendingNoteOffs = () => {
|
||||
delete pendingNoteOffMap[k];
|
||||
});
|
||||
};
|
||||
const fireCarlaNote = (ch, pitch, vel, delay, durMs) => {
|
||||
setTimeout(function () { window.SonicCarlaMidi.noteOn(ch, pitch, vel); }, delay);
|
||||
scheduleNoteOff('C:' + ch + ':' + pitch, function () { window.SonicCarlaMidi.noteOff(ch, pitch); }, delay, durMs);
|
||||
};
|
||||
// ⚠️ FIX (Bug 2): Carla mở ASYNC lúc play (ensureCarlaForPlayback) — nốt gửi
|
||||
// trước khi OSC engine bind cổng bị mất → item câm dù keybed kêu. Nốt khi
|
||||
// chưa ready được nhét queue, flush khi bridge OSC ready (giữ đúng thứ tự).
|
||||
const flushCarlaNoteQueue = () => {
|
||||
const q = window.__carlaNoteQueue || [];
|
||||
if (!q.length) return;
|
||||
window.__carlaNoteQueue = [];
|
||||
q.forEach(item => {
|
||||
const remaining = item.delay - (Date.now() - item.scheduledAt);
|
||||
if (remaining <= 0) fireCarlaNote(item.ch, item.pitch, item.vel, 0, item.durMs);
|
||||
else fireCarlaNote(item.ch, item.pitch, item.vel, remaining, item.durMs);
|
||||
});
|
||||
};
|
||||
const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime, durMs) => {
|
||||
try {
|
||||
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
|
||||
const ctx = getAudioContext();
|
||||
const delay = Math.max(0, (startWallTime - ctx.currentTime) * 1000);
|
||||
const carlaVel = Math.round((velocity || 0.8) * 127);
|
||||
const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : (channel || 0);
|
||||
// Chẩn đoán: note MIDI item → Carla (bật console.log để verify route)
|
||||
if (window.__carlaNoteLog === undefined) window.__carlaNoteLog = (window.__carlaNoteLog || 0) + 1;
|
||||
console.log('[Carla] item note ch=' + carlaCh + ' pitch=' + pitch + ' vel=' + carlaVel + ' delay=' + delay.toFixed(0) + 'ms dur=' + (durMs || 300) + 'ms');
|
||||
const dur = durMs || 300;
|
||||
if (window.__carlaRunning !== true) {
|
||||
window.__carlaNoteQueue = window.__carlaNoteQueue || [];
|
||||
window.__carlaNoteQueue.push({ ch: carlaCh, pitch, vel: carlaVel, scheduledAt: Date.now(), delay, durMs: dur });
|
||||
return;
|
||||
}
|
||||
fireCarlaNote(carlaCh, pitch, carlaVel, delay, dur);
|
||||
} catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); }
|
||||
};
|
||||
|
||||
// ── Carla bridge alive tracking + auto-open ────────────────────────────────
|
||||
// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết.
|
||||
// window.__carlaNoteQueue: nốt chờ flush khi Carla chưa ready (cold start).
|
||||
// Quyết định route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth —
|
||||
// tránh "câm toàn phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng).
|
||||
const refreshCarlaStatus = () => {
|
||||
try {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
|
||||
window.SonicAPI.carlaStatus().then(st => {
|
||||
window.__carlaRunning = !!(st && st.running);
|
||||
if (window.__carlaRunning) flushCarlaNoteQueue();
|
||||
}).catch(() => { window.__carlaRunning = false; });
|
||||
} catch (e) {}
|
||||
};
|
||||
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
|
||||
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
|
||||
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).
|
||||
|
||||
|
||||
// ── Môi trường chạy: docker vs standalone ─────────────────────────────────
|
||||
// environment: "docker" → âm instrument qua FluidSynthWASM (client); "standalone"
|
||||
// → xử lí trực tiếp trên OS (backend native pyfluidsynth / Carla). Quy tắc:
|
||||
// → xử lí trực tiếp trên OS (backend native pyfluidsynth). Quy tắc:
|
||||
// KHÔNG phải docker = standalone (Windows/Linux/macOS chạy trực tiếp).
|
||||
const sfEnv = () => {
|
||||
try {
|
||||
@@ -223,9 +164,6 @@ const isSfTrackEngine = (se) => {
|
||||
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 bằng backend native FluidSynth (pyfluidsynth) → play WAV.
|
||||
// Mỗi key (thường = track.id) một Audio element — note mới stop note cũ;
|
||||
@@ -1550,10 +1488,10 @@ function createTrackFxModule(type, ctx, params) {
|
||||
nodes = { gLL, gRL, gLR, gRR };
|
||||
} else if (type === 'eqpro') {
|
||||
return createEqProModule(ctx, params);
|
||||
} else if (type === 'carla' || type === 'vst3') {
|
||||
// Carla Bridge / VST FX = xử lý NGOÀI (Carla standalone / fx_vst_bridge
|
||||
// --render-fx khi export) — trong WebAudio graph chỉ là pass-through
|
||||
// (không thêm DSP): âm track đi thẳng, đúng vị trí chain.
|
||||
} else if (type === 'vst3') {
|
||||
// VST FX = xử lý NGOÀI (fx_vst_bridge --render-fx khi export) — trong
|
||||
// WebAudio graph chỉ là pass-through (không thêm DSP): âm track đi thẳng,
|
||||
// đúng vị trí chain.
|
||||
input.connect(output);
|
||||
nodes = { passthrough: input };
|
||||
} else {
|
||||
@@ -1935,9 +1873,9 @@ function rebuildMasteringGraph(activate, chainArray) {
|
||||
Object.keys(eqProStore).forEach(k => { try { eqProStore[k].destroy && eqProStore[k].destroy(); } catch (e) { } });
|
||||
Object.keys(eqProStore).forEach(k => delete eqProStore[k]);
|
||||
activeMods.forEach(mod => {
|
||||
if (mod.type === 'carla' || mod.type === 'vst3') {
|
||||
// Carla Bridge = VST FX chạy NGOÀI (Carla standalone) — không có node
|
||||
// WebAudio trong master chain: pass-through, prev giữ nguyên.
|
||||
if (mod.type === 'vst3') {
|
||||
// VST FX chạy NGOÀI (fx_vst_bridge) — không có node WebAudio trong
|
||||
// master chain: pass-through, prev giữ nguyên.
|
||||
return;
|
||||
}
|
||||
if (mod.type === 'eqpro') {
|
||||
@@ -6172,10 +6110,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
const [pmSfExpanded, setPmSfExpanded] = React.useState({});
|
||||
const [pmSfInstruments, setPmSfInstruments] = React.useState({});
|
||||
const [pmSfLoading, setPmSfLoading] = React.useState({});
|
||||
// Force re-render sau khi định vị Carla (capabilities đổi)
|
||||
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
|
||||
// Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker)
|
||||
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
|
||||
// D7: trạng thái Native Bridge (query khi mở modal) — badge header.
|
||||
const [bridgeStatus, setBridgeStatus] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
@@ -6287,52 +6221,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
}
|
||||
}
|
||||
};
|
||||
// Định vị Carla.exe — bản Windows là zip portable: KHÔNG cài đặt, KHÔNG dùng
|
||||
// biến môi trường PATH nên heuristic không tìm thấy → user tự chọn thư mục
|
||||
// chứa carla.exe (folder picker native) → lưu config phía server.
|
||||
const locateCarla = async () => {
|
||||
try {
|
||||
let picked = null;
|
||||
try {
|
||||
const d = await window.SonicAPI.pickPluginDir();
|
||||
if (d && typeof d.path === 'string' && d.path) picked = d.path;
|
||||
} catch (e) { /* fallthrough */ }
|
||||
if (!picked && window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
|
||||
try {
|
||||
const sel = await window.__TAURI__.core.invoke('plugin:dialog|open', { options: { directory: true, multiple: false } });
|
||||
if (typeof sel === 'string' && sel) picked = sel;
|
||||
} catch (e) { /* fallthrough */ }
|
||||
}
|
||||
if (!picked) { showToast('Không mở được hộp thoại chọn thư mục', 'error'); return; }
|
||||
const r = await window.SonicAPI.setCarlaPath(picked);
|
||||
if (r && r.success && r.carla_path) {
|
||||
window.SonicRuntime.capabilities = r;
|
||||
document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
|
||||
setPmCarlaVersion(v => v + 1);
|
||||
showToast('Đã định vị Carla: ' + r.carla_path, 'success');
|
||||
} else {
|
||||
showToast('Không tìm thấy carla.exe trong thư mục đã chọn', 'error');
|
||||
}
|
||||
} catch (err) { showToast('Lỗi định vị Carla: ' + (err.message || err), 'error'); }
|
||||
};
|
||||
// Khai báo thư mục Carla trực tiếp (nhập tay) — tương tự Định vị nhưng
|
||||
// không cần hộp thoại chọn thư mục.
|
||||
const saveCarlaInput = async () => {
|
||||
const p = (pmCarlaPathInput || '').trim();
|
||||
if (!p) { showToast('Nhập đường dẫn thư mục chứa carla.exe', 'warning'); return; }
|
||||
try {
|
||||
const r = await window.SonicAPI.setCarlaPath(p);
|
||||
if (r && r.success && r.carla_path) {
|
||||
window.SonicRuntime.capabilities = r;
|
||||
document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
|
||||
setPmCarlaVersion(v => v + 1);
|
||||
setPmCarlaPathInput('');
|
||||
showToast('Đã lưu Carla: ' + r.carla_path, 'success');
|
||||
} else {
|
||||
showToast('Không tìm thấy carla.exe trong đường dẫn đã nhập', 'error');
|
||||
}
|
||||
} catch (err) { showToast('Lỗi: ' + (err.message || err), 'error'); }
|
||||
};
|
||||
// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog.
|
||||
// dirsOverride: mảng dirs mới nhất (fix stale-closure: setTimeout giữ
|
||||
// pmDirs cũ nên add/remove xong tự scan lại vẫn lưu dir cũ).
|
||||
@@ -9055,11 +8943,11 @@ 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) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
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())) {
|
||||
if (isStandaloneSf() && isVstTrackEngine(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeVstNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
|
||||
return;
|
||||
}
|
||||
@@ -9429,9 +9317,9 @@ 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);
|
||||
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(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())) {
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(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);
|
||||
@@ -9695,9 +9583,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
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) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(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())) {
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(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();
|
||||
@@ -9798,9 +9686,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
||||
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(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())) {
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(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();
|
||||
@@ -9809,11 +9697,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
|
||||
previewPitchRef.current = p;
|
||||
}
|
||||
// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(pvTrk && pvTrk.synth_engine, st.isArmed)) {
|
||||
var pvVel2 = Math.round((brushVelocityRef.current || 0.8) * 127);
|
||||
window.SonicCarlaMidi.playNote(pvTrk && pvTrk.synth_engine ? (pvTrk.synth_engine.midi_channel || 0) : 0, p, pvVel2, durMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (pitchChanged) {
|
||||
@@ -10192,8 +10075,8 @@ 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);
|
||||
}
|
||||
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());
|
||||
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
const kbNativeVst = isStandaloneSf() && isVstTrackEngine(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if (kbNative) {
|
||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||
} else if (kbNativeVst) {
|
||||
@@ -10208,12 +10091,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
|
||||
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
||||
}
|
||||
// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
|
||||
window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
|
||||
if (window.__carlaKeybedTimer) clearTimeout(window.__carlaKeybedTimer);
|
||||
window.__carlaKeybedTimer = setTimeout(function () { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); }, 650);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('playNote error:', err);
|
||||
}
|
||||
@@ -10224,23 +10101,18 @@ 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 (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(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())) {
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(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) || isVstTrackEngine(kbCtx.synthEngine)) && !shouldRouteCarla(kbCtx.synthEngine))) {
|
||||
} else if (window.SonicSF && !(isStandaloneSf() && (isSfTrackEngine(kbCtx.synthEngine) || isVstTrackEngine(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);
|
||||
}
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
|
||||
window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
|
||||
if (window.__carlaKeybedTimer) clearTimeout(window.__carlaKeybedTimer);
|
||||
window.__carlaKeybedTimer = setTimeout(function () { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); }, 650);
|
||||
}
|
||||
} catch (err) { console.error('playNote error:', err); }
|
||||
}
|
||||
},
|
||||
@@ -10253,8 +10125,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
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);
|
||||
} catch (e) {}
|
||||
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
||||
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
if (!keybedMouseDownRef.current) return;
|
||||
@@ -10265,7 +10135,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
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);
|
||||
} catch (e) {}
|
||||
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
||||
}
|
||||
}, showLabel && label)
|
||||
);
|
||||
@@ -10763,8 +10632,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
}, React.createElement("i", { "data-lucide": "file-down", className: "w-3 h-3" }), "Export MIDI"), React.createElement("button", {
|
||||
onClick: async () => {
|
||||
// Preview/export MIDI notes với âm VSTi: midi-render (pedalboard) →
|
||||
// play wav ngay; nếu pedalboard không render được plugin (VST2/state
|
||||
// GUI) fallback carla-play-notes (realtime qua Carla bridge, OSC).
|
||||
// play wav ngay.
|
||||
const se = st.synth_engine || {};
|
||||
const instrumentId = se.plugin_id || st.instrumentId || null;
|
||||
if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
|
||||
@@ -10788,17 +10656,11 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
audio.play().catch(() => {});
|
||||
showToast(`Preview VSTi (${(res.duration_sec || 0).toFixed(1)}s) — ${se.plugin_id || instrumentId}`, 'success');
|
||||
} catch (err) {
|
||||
// midi-render fail (404 plugin không load / 501 pedalboard) → Carla
|
||||
try {
|
||||
await window.SonicAPI.carlaPlayNotes({ notes: payload.notes, bpm: payload.bpm, channel: se.midi_channel || 0 });
|
||||
showToast('Preview qua Carla (realtime) — nghe loa hệ thống', 'success');
|
||||
} catch (err2) {
|
||||
showToast('Preview VSTi thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
|
||||
}
|
||||
showToast('Preview VSTi thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
|
||||
}
|
||||
},
|
||||
className: "px-2.5 py-1 bg-violet-700 hover:bg-violet-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold",
|
||||
title: "Render MIDI notes bằng VSTi (pedalboard) và phát — fallback qua Carla realtime"
|
||||
title: "Render MIDI notes bằng VSTi (pedalboard) và phát"
|
||||
}, React.createElement("i", { "data-lucide": "play", className: "w-3 h-3" }), "Preview VSTi"), React.createElement("button", {
|
||||
onClick: () => {
|
||||
const se = st.synth_engine || {};
|
||||
@@ -12335,7 +12197,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
);
|
||||
};
|
||||
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, showCarla = true, activeIdx = -1, onSelect = null) => (
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, activeIdx = -1, onSelect = null) => (
|
||||
<div key={idx} onClick={() => onSelect && onSelect(s, idx)} className={`flex items-center gap-2 bg-slate-900/60 border rounded-lg px-2 py-1.5 flex-wrap cursor-pointer transition-all ${activeIdx === idx ? 'border-teal-400 bg-slate-800/80' : 'border-slate-800 hover:border-slate-600'}`}>
|
||||
<button onClick={e => { e.stopPropagation(); toggleV(idx); }} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>
|
||||
<i data-lucide="power" className="w-2.5 h-2.5"></i>
|
||||
@@ -12348,7 +12210,6 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
<span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>
|
||||
<button onClick={e => { e.stopPropagation(); uploadV(idx); }} className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition" title="Upload .vstpreset (Save preset)">Preset</button>
|
||||
{openGui && <button onClick={e => { e.stopPropagation(); openGui(s, idx); }} disabled={!s.path} className="px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition" title="Mở GUI của VST (chỉnh thông số trực tiếp)"><i data-lucide="monitor" className="w-3 h-3"></i> GUI</button>}
|
||||
{showCarla && <button onClick={e => { e.stopPropagation(); if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className="px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition" title="Mở native GUI trong Carla">Carla</button>}
|
||||
<button onClick={e => { e.stopPropagation(); moveV(idx, -1); }} disabled={idx === 0} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Lên"><i data-lucide="chevron-up" className="w-3 h-3"></i></button>
|
||||
<button onClick={e => { e.stopPropagation(); moveV(idx, 1); }} disabled={idx === chainLen - 1} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Xuống"><i data-lucide="chevron-down" className="w-3 h-3"></i></button>
|
||||
<button onClick={e => { e.stopPropagation(); removeV(idx); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5" title="Xóa slot"><i data-lucide="x" className="w-3 h-3"></i></button>
|
||||
@@ -12843,7 +12704,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
<span className="text-slate-400">Active VST: <strong className="text-teal-400">{vstChain[activeVstIdx].name || vstChain[activeVstIdx].path}</strong></span>
|
||||
<span className="text-[9px] text-slate-500">Slot {activeVstIdx + 1}/{vstChain.length} — bấm card DSP trong CHAIN để chỉnh module builtin</span>
|
||||
</div>
|
||||
{vstRow(vstChain[activeVstIdx], activeVstIdx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, true, activeVstIdx, null)}
|
||||
{vstRow(vstChain[activeVstIdx], activeVstIdx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, activeVstIdx, null)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-4 flex flex-col gap-4 min-h-[240px] overflow-y-auto oz-scrollbar">
|
||||
@@ -12928,17 +12789,6 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeMod && activeMod.type === 'carla' && activeIdx >= 0 && (
|
||||
<div className="bg-amber-950/40 border border-amber-800/60 rounded-lg p-4 space-y-3">
|
||||
<div className="text-[10px] text-amber-300 font-mono">Carla Bridge đã bị gỡ khỏi FX Rack — track cũ vẫn giữ module để không mất cấu hình.</div>
|
||||
<button
|
||||
onClick={() => removeMod(activeIdx)}
|
||||
className="px-3 py-1.5 bg-amber-800 hover:bg-amber-700 text-amber-50 text-xs font-semibold rounded transition"
|
||||
>
|
||||
Gỡ module Carla khỏi track
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13597,14 +13447,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
// stable hook count across renders (error #310 otherwise).
|
||||
const dragChainIndexRef = React.useRef(null);
|
||||
const [addModuleOpen, setAddModuleOpen] = React.useState(false);
|
||||
// Carla Bridge (VST FX) — danh sách VST để load vào Carla (master chain)
|
||||
const [masterCarlaVsts, setMasterCarlaVsts] = React.useState(null); // null = chưa load
|
||||
React.useEffect(() => {
|
||||
if (isOpen && window.SonicAPI && window.SonicAPI.listPlugins) {
|
||||
window.SonicAPI.listPlugins().then(d => setMasterCarlaVsts((d && d.vst_instruments) || [])).catch(() => setMasterCarlaVsts([]));
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const switchModule = (name) => setOzState(prev => ({ ...prev, activeModule: name }));
|
||||
@@ -13619,10 +13461,9 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
limiter: { name: 'Brickwall Limiter', sub: 'True-Peak 20:1', icon: 'shield-half', color: '#f43f5e' },
|
||||
exciter: { name: 'Harmonic Exciter', sub: 'Saturation & Air', icon: 'wand-2', color: '#c084fc' },
|
||||
rebalance: { name: 'Master Rebalance', sub: 'M/S Balance', icon: 'sliders-horizontal', color: '#38bdf8' },
|
||||
carla: { name: 'Carla Bridge (VST FX)', sub: 'Native VST audio processing', icon: 'sliders', color: '#14b8a6' },
|
||||
vst3: { name: 'VST FX', sub: 'VST3 plugin', icon: 'plug', color: '#2dd4bf' }
|
||||
};
|
||||
const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'carla' ? 'carlaBridgeActive' : type === 'vst3' ? 'vstFxActive' : 'rebalActive';
|
||||
const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'vst3' ? 'vstFxActive' : 'rebalActive';
|
||||
const chainActive = (type) => !!ozState[chainFlag(type)];
|
||||
|
||||
const toggleChainModule = (modId) => {
|
||||
@@ -13674,7 +13515,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
const id = 'mod_' + type + '_' + Date.now();
|
||||
const entry = { id, type, name: meta.name, active: true };
|
||||
if (type === 'eqpro') entry.params = { amount: 100, bands: JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS)) };
|
||||
if (type === 'carla') entry.params = { plugin: '', plugin_path: '' };
|
||||
setOzState(prev => ({
|
||||
...prev,
|
||||
chain: [...(prev.chain || []), entry],
|
||||
@@ -14147,62 +13987,6 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VIEW: CARLA BRIDGE (VST FX) — load carla bridge để dùng VST chỉnh sửa âm thanh */}
|
||||
<div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'carla' ? '' : 'hidden'}`}>
|
||||
<div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
|
||||
<div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
|
||||
<span className="text-xs font-bold text-teal-400 uppercase oz-font-mono mb-3">Carla Bridge (VST FX)</span>
|
||||
<button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'carla')?.id)}
|
||||
className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.carlaBridgeActive ? 'bg-teal-700 border-teal-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
|
||||
{ozState.carlaBridgeActive ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-span-8 space-y-3">
|
||||
<div className="flex items-end gap-2 flex-wrap">
|
||||
<div className="flex-1 min-w-[220px]">
|
||||
<div className="text-[9px] text-slate-500 oz-font-mono mb-1">CHỌN VST FX (đã scan trên máy)</div>
|
||||
<select
|
||||
value={(ozState.carlaBridge && ozState.carlaBridge.plugin) || ''}
|
||||
onChange={e => setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), plugin: e.target.value, plugin_path: '' } }))}
|
||||
className="w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1.5 text-xs outline-none focus:border-teal-500"
|
||||
>
|
||||
<option value="">— Chọn VST FX —</option>
|
||||
{(masterCarlaVsts || []).map(v => <option key={v.id || v.name} value={v.id || v.name}>{v.name || v.id}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const plugin = (ozState.carlaBridge && ozState.carlaBridge.plugin) || '';
|
||||
if (!plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
|
||||
window.SonicAPI.openInCarla(plugin, ozState.carlaBridge.plugin_path).then(r => {
|
||||
if (r && r.success) {
|
||||
setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: true } }));
|
||||
window.showToast && window.showToast('Đã mở Carla với ' + plugin + ' (VST FX) — chỉnh âm thanh trong Carla', 'success');
|
||||
} else { window.showToast && window.showToast('Không mở được Carla', 'error'); }
|
||||
}).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error'));
|
||||
}}
|
||||
className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
|
||||
>
|
||||
<i data-lucide="play" className="w-3 h-3"></i> Load Carla Bridge
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) window.SonicCarlaMidi.stopBridge();
|
||||
setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: false } }));
|
||||
window.showToast && window.showToast('Đã ngắt kết nối Carla Bridge', 'info');
|
||||
}}
|
||||
className="px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-red-300 text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
|
||||
>
|
||||
<i data-lucide="square" className="w-3 h-3"></i> Stop / Unload
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 oz-font-mono leading-relaxed">
|
||||
Mở Carla với VST FX để chỉnh sửa âm thanh master (native GUI). Module này là pass-through trong master chain (không thêm DSP WebAudio).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* WAVE OBSERVER INTEGRATION */}
|
||||
<div className="border-t border-slate-800 bg-slate-900/60 p-3 flex flex-col shrink-0">
|
||||
{/* Header */}
|
||||
@@ -17181,20 +16965,17 @@ const App = () => {
|
||||
var mt = activeTracksRef.current || tracks;
|
||||
var curTrk = null;
|
||||
for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { curTrk = mt[ci]; break; } }
|
||||
// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
|
||||
// (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn
|
||||
// play qua Carla bridge (âm sai instrument + âm kẹt không dừng được).
|
||||
// ── Panic khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
|
||||
// (soundfont/GM/default) — dừng mọi note đang vang từ VSTi cũ.
|
||||
try {
|
||||
const _hasInst = !!instrumentId;
|
||||
const _wasVst = curTrk && curTrk.synth_engine && String(curTrk.synth_engine.type || '').indexOf('vst') !== -1;
|
||||
const _nowVst = _hasInst && !isSfInstrument;
|
||||
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
|
||||
window.SonicCarlaMidi.stopBridge();
|
||||
if (_wasVst && !_nowVst) {
|
||||
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
|
||||
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); }
|
||||
} catch (e) {}
|
||||
var mch = curTrk ? assignTrackMidiChannel(curTrk, mt) : (sfBank === 128 ? 9 : 0);
|
||||
updateActiveTracks(prev => prev.map(t => {
|
||||
if (t.id !== trackId) return t;
|
||||
@@ -17434,7 +17215,7 @@ const App = () => {
|
||||
// Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
// D1: bridge active -> 1 cổng router (per-track channel alloc) →
|
||||
// Rust SHM → C++ bridge; giữ SonicSF/Carla fallback ở nhánh else.
|
||||
// Rust SHM → C++ bridge; giữ SonicSF fallback ở nhánh else.
|
||||
var bTracks = activeTracksRef.current || [];
|
||||
bTracks.forEach(function (bt) {
|
||||
if (!bt.isArmed) return;
|
||||
@@ -17465,18 +17246,13 @@ const App = () => {
|
||||
try {
|
||||
heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
|
||||
} catch (err) { }
|
||||
if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(asSe)) {
|
||||
playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(asSe)) {
|
||||
playNativeVstNote(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 của sub-tab phát VSTi realtime giống keybed.
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(asSe, true)) {
|
||||
try { window.SonicCarlaMidi.noteOn(asCh, pitch, scaledVel); } catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Route to ALL armed tracks (not just the first one)
|
||||
@@ -17492,20 +17268,13 @@ const App = () => {
|
||||
try {
|
||||
heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
|
||||
} catch (err) { }
|
||||
if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(atSe)) {
|
||||
playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
|
||||
} else if (isStandaloneSf() && isVstTrackEngine(atSe)) {
|
||||
playNativeVstNote(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 — giống hệt keybed ảo. Trước
|
||||
// đây chỉ keybed gọi SonicCarlaMidi; keyboard hardware bị
|
||||
// bỏ sót → track VSTi không kêu khi bấm đàn thật.
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(atSe, at.isArmed)) {
|
||||
try { window.SonicCarlaMidi.noteOn(atCh, pitch, scaledVel); } catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)) {
|
||||
@@ -17543,11 +17312,6 @@ const App = () => {
|
||||
var stCh = assignTrackMidiChannel(st, stopTracks);
|
||||
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).
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(st.synth_engine, st.isArmed)) {
|
||||
try { window.SonicCarlaMidi.noteOff(stCh, pitch); } catch (e) {}
|
||||
}
|
||||
// Giảm counter note đang giữ — hết note → VU được phép decay/tắt
|
||||
// (tick sẽ thấy counter = 0 và không còn giữ peak nữa)
|
||||
try {
|
||||
@@ -18195,9 +17959,9 @@ const App = () => {
|
||||
setAiPrompt(newText);
|
||||
};
|
||||
|
||||
// Gán preset VST3 (.vstpreset từ thư viện storage/presets — cầu nối
|
||||
// Carla → pedalboard) vào track: preset_id nằm trong synth_engine → render
|
||||
// engine tải qua load_preset khi render → âm render = âm đã chỉnh trong Carla.
|
||||
// Gán preset VST3 (.vstpreset từ thư viện storage/presets) vào track:
|
||||
// preset_id nằm trong synth_engine → render engine tải qua load_preset khi
|
||||
// render → âm render = âm đã chỉnh trong preset.
|
||||
const setTrackPreset = (trackId, presetId) => {
|
||||
if (!trackId || !presetId) return;
|
||||
var mt = activeTracksRef.current || tracks;
|
||||
@@ -19224,7 +18988,6 @@ const App = () => {
|
||||
limActive: false, limThreshold: -1.0,
|
||||
excActive: false, excDrive: 40,
|
||||
rebalActive: false, rebalMid: 0, rebalSide: 0,
|
||||
carlaBridgeActive: false, carlaBridge: { plugin: '', plugin_path: '', connected: false },
|
||||
});
|
||||
}
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
@@ -22425,7 +22188,7 @@ const App = () => {
|
||||
|
||||
// ── Virtual MIDI Keyboard note routing (Phase 5) ────────────────────────
|
||||
// NOTE_ON/NOTE_OFF theo pattern attachMidiHandler (hardware MIDI): bridge →
|
||||
// per-track channel alloc; native SF/VST; SonicSF fallback; Carla; VU + giữ
|
||||
// per-track channel alloc; native SF/VST; SonicSF fallback; VU + giữ
|
||||
// counter (heldMidiNotesRef); forward recorder (live record vào midiItems).
|
||||
const vkbNoteOn = (pitch, vel) => {
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||
@@ -22448,16 +22211,13 @@ const App = () => {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(t.id, ctx.program, ctx.synthEngine); } catch (er) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: t.id, pitch: pitch, velocity: vel, percussion: percussion, live: true }); } catch (e) {}
|
||||
} else if (isStandaloneSf() && (isSfTrackEngine(ctx.synthEngine) || isVstTrackEngine(ctx.synthEngine)) && !shouldRouteCarla(ctx.synthEngine)) {
|
||||
} else if (isStandaloneSf() && (isSfTrackEngine(ctx.synthEngine) || isVstTrackEngine(ctx.synthEngine))) {
|
||||
if (isSfTrackEngine(ctx.synthEngine)) playNativeSfNote(t, pitch, vel, 60000, undefined, 'vkb_' + t.id + '_' + pitch);
|
||||
else playNativeVstNote(t, pitch, vel, 60000, undefined, 'vkb_' + t.id + '_' + pitch);
|
||||
} else if (window.SonicSF) {
|
||||
const dest = (activeTrackNodesRef.current[t.id] && activeTrackNodesRef.current[t.id].gainNode) || null;
|
||||
window.SonicSF.playNote(pitch, Math.max(1, Math.round(vel * 127)), 60000, undefined, ctx.program, dest, ctx.ch, ctx.synthEngine);
|
||||
}
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(ctx.synthEngine, t.isArmed)) {
|
||||
try { window.SonicCarlaMidi.noteOn(ctx.ch, pitch, Math.max(1, Math.round(vel * 127))); } catch (e) {}
|
||||
}
|
||||
});
|
||||
// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll)
|
||||
if (activeMIDIRecordersRef.current) {
|
||||
@@ -22481,9 +22241,6 @@ const App = () => {
|
||||
} else if (window.SonicSF && window.SonicSF.stopNote) {
|
||||
if (isStandaloneSf()) { stopNativeSfNote('vkb_' + t.id + '_' + pitch); stopNativeVstNote('vkb_' + t.id + '_' + pitch); }
|
||||
window.SonicSF.stopNote(ctx.ch, pitch);
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(t.synth_engine, t.isArmed)) {
|
||||
try { window.SonicCarlaMidi.noteOff(ctx.ch, pitch); } catch (e) {}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (heldMidiNotesRef.current[t.id] !== undefined) {
|
||||
@@ -23885,7 +23642,6 @@ 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);
|
||||
// D7: bridge active → preview qua bridge (channel router allocate — đúng
|
||||
// instrument đã load khi chèn vào track).
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
@@ -23898,11 +23654,11 @@ const App = () => {
|
||||
}, (durationMs || 500) + 30);
|
||||
return;
|
||||
}
|
||||
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(track && track.synth_engine)) {
|
||||
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
|
||||
return;
|
||||
}
|
||||
if (isStandaloneSf() && !routeCarla && isVstTrackEngine(track && track.synth_engine)) {
|
||||
if (isStandaloneSf() && isVstTrackEngine(track && track.synth_engine)) {
|
||||
playNativeVstNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
|
||||
return;
|
||||
}
|
||||
@@ -23983,10 +23739,9 @@ const App = () => {
|
||||
// loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop
|
||||
// → loop pass 2+ câm. Set NGAY tại scheduler.
|
||||
isPlayingRef.current = true;
|
||||
// ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
|
||||
// qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
|
||||
// ⚠️ FIX: đồng bộ mastering NGAY khi play — MIDI item phải qua mastering
|
||||
// FX (khi bật).
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
|
||||
refreshCarlaStatus();
|
||||
const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks;
|
||||
// Capture items signature at schedule time — updatePlayhead so sánh với
|
||||
// signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
|
||||
@@ -24048,10 +23803,9 @@ const App = () => {
|
||||
|
||||
// MIDI items playback
|
||||
const midiItems = track.midiItems || [];
|
||||
const routeCarla = shouldRouteCarla(track.synth_engine);
|
||||
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
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)) {
|
||||
const nativeSf = isStandaloneSf() && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
const nativeVst = isStandaloneSf() && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || nativeSf || nativeVst)) {
|
||||
if (nativeSf || nativeVst) {
|
||||
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
} else {
|
||||
@@ -24076,16 +23830,10 @@ const App = () => {
|
||||
if (offsetTime < noteEndSec) {
|
||||
const durationMs = noteDurSec * 1000;
|
||||
const program = track._isSectionClone ? track.instrumentProgram : (track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
|
||||
// Track VSTi + Carla local → MIDI item PHẢI play qua Carla bridge
|
||||
// (chỉ route Carla — KHÔNG play qua FluidSynth GM nữa, tránh âm
|
||||
// sai instrument chồng lên). Không có Carla → FluidSynth như cũ.
|
||||
// ⚠️ 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").
|
||||
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) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
@@ -24097,9 +23845,6 @@ const App = () => {
|
||||
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.
|
||||
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
|
||||
@@ -24113,7 +23858,7 @@ const App = () => {
|
||||
} else {
|
||||
const playOffset = offsetTime - noteStartSec;
|
||||
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
@@ -24125,8 +23870,6 @@ const App = () => {
|
||||
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 (window.triggerMidiVuActivity) {
|
||||
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
|
||||
@@ -24212,13 +23955,12 @@ const App = () => {
|
||||
|
||||
// 2. Play MIDI items in subTrack
|
||||
const subMidiItems = subTrack.midiItems || [];
|
||||
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
|
||||
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
const subNativeVst = isStandaloneSf() && !subRouteCarla && isVstTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf || subNativeVst)) {
|
||||
const subNativeSf = isStandaloneSf() && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
const subNativeVst = isStandaloneSf() && isVstTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if (subMidiItems.length > 0 && (window.SonicSF || subNativeSf || subNativeVst)) {
|
||||
if (subNativeSf || subNativeVst) {
|
||||
scheduleNativeTrack(subTrack, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render → audio buffer (không qua WASM/Carla per-note)
|
||||
return; // native render → audio buffer (không qua WASM per-note)
|
||||
}
|
||||
subMidiItems.forEach(item => {
|
||||
const notes = item.notes || [];
|
||||
@@ -24234,15 +23976,12 @@ const App = () => {
|
||||
const notePlayEndMain = Math.min(noteEndMain, secEnd);
|
||||
const program = subTrack.instrumentProgram;
|
||||
var subCh = assignTrackMidiChannel(subTrack, secTracks);
|
||||
// MIDI item trong SECTION → Carla bridge khi VSTi loaded
|
||||
// (chỉ route Carla — không play FluidSynth GM sai âm)
|
||||
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
|
||||
|
||||
if (offsetTime < noteStartMain) {
|
||||
const delay = noteStartMain - offsetTime;
|
||||
const startTime = context.currentTime + delay;
|
||||
const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
|
||||
if (!subRouteCarla && window.SonicSF) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(
|
||||
subTrack, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
@@ -24254,7 +23993,6 @@ const App = () => {
|
||||
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
|
||||
// SF → masterBus.input (không qua sub-node analyser) →
|
||||
@@ -24272,7 +24010,7 @@ const App = () => {
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
|
||||
if (!subRouteCarla && window.SonicSF) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(
|
||||
subTrack, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
@@ -24284,7 +24022,6 @@ const App = () => {
|
||||
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);
|
||||
@@ -24318,7 +24055,6 @@ const App = () => {
|
||||
}
|
||||
isPlayingRef.current = true;
|
||||
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 ? [{
|
||||
@@ -24358,13 +24094,12 @@ const App = () => {
|
||||
|
||||
// MIDI items playback
|
||||
const midiItems = track.midiItems || [];
|
||||
const routeCarla = shouldRouteCarla(track.synth_engine);
|
||||
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
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)) {
|
||||
const nativeSf = isStandaloneSf() && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
const nativeVst = isStandaloneSf() && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || nativeSf || nativeVst)) {
|
||||
if (nativeSf || nativeVst) {
|
||||
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render → audio buffer (không qua WASM/Carla per-note)
|
||||
return; // native render → audio buffer (không qua WASM per-note)
|
||||
}
|
||||
var lcCh = assignTrackMidiChannel(track, tracks);
|
||||
const bpmVal = parseInt(bpm) || 120;
|
||||
@@ -24378,13 +24113,10 @@ const App = () => {
|
||||
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)
|
||||
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) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
@@ -24396,11 +24128,9 @@ const App = () => {
|
||||
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) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
@@ -24412,8 +24142,6 @@ const App = () => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -24439,7 +24167,6 @@ const App = () => {
|
||||
// delay=0 của sub-tab loop restart không bị drop.
|
||||
isPlayingRef.current = true;
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
|
||||
refreshCarlaStatus();
|
||||
const midiNotes = notesOverride || st.notes || [];
|
||||
const bpmVal = parseInt(bpm) || 120;
|
||||
const secondsPerBeat = 60.0 / bpmVal;
|
||||
@@ -24463,10 +24190,9 @@ 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) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
if (isStandaloneSf() && (isSfTrackEngine(synthEngine) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
scheduleNativeTrack(track, offsetSeconds, context, destNode, bpm, { notesOverride: midiNotes, playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render cả tab → audio buffer (không per-note WASM/Carla)
|
||||
return; // native render cả tab → audio buffer (không per-note WASM)
|
||||
}
|
||||
midiNotes.forEach(note => {
|
||||
const noteOnBeat = note.start_beat || 0;
|
||||
@@ -24478,22 +24204,9 @@ const App = () => {
|
||||
const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec);
|
||||
const scheduledTime = startWallTime + effectiveStart;
|
||||
const durMs = effectiveDur * 1000;
|
||||
// Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route
|
||||
// Carla — không play FluidSynth GM sai âm chồng lên)
|
||||
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(track, 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).
|
||||
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);
|
||||
scheduleNoteOff('C:' + carlaCh + ':' + carlaPitch, function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay, durMs);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Play ghost notes from active tracks
|
||||
@@ -24515,12 +24228,9 @@ const App = () => {
|
||||
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) {
|
||||
if (window.SonicSF) {
|
||||
scheduleMidiNoteDispatch(ghostTrack, 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -34084,8 +33794,8 @@ STRICT CONSTRAINTS:
|
||||
inp.click();
|
||||
},
|
||||
className: "w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",
|
||||
title: "Upload preset .vstpreset xuất từ Carla (native GUI)"
|
||||
}, "\u2B06 Upload preset (t\u1EEB Carla...)"),
|
||||
title: "Upload preset .vstpreset xuất từ native GUI"
|
||||
}, "\u2B06 Upload preset (t\u1EEB native GUI...)"),
|
||||
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o")
|
||||
)));
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -73,18 +73,6 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
// Runtime capabilities — frontend gọi lúc boot để biết môi trường
|
||||
// (desktop Windows / docker headless) và bật/tắt tính năng
|
||||
getCapabilities: () => apiRequest('/api/v1/system/capabilities', { method: 'GET' }),
|
||||
// Định vị Carla.exe (bản portable zip không cài đặt/PATH) — lưu config
|
||||
setCarlaPath: (path) => apiRequest('/api/v1/system/carla-path', { method: 'POST', body: JSON.stringify({ carla_path: path }) }),
|
||||
// Mở native GUI VSTi trong Carla (chỉ khi runtime=desktop + có Carla local)
|
||||
openInCarla: (pluginName, pluginPath) => apiRequest('/api/v1/plugins/open-in-carla', { method: 'POST', body: JSON.stringify({ plugin_name: pluginName, plugin_path: pluginPath }) }),
|
||||
// Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla
|
||||
// do app spawn (gọi khi track chuyển từ VSTi sang instrument soundfont)
|
||||
stopCarla: () => apiRequest('/api/v1/plugins/carla-stop', { method: 'POST', body: JSON.stringify({}) }),
|
||||
// Kiểm tra Carla còn sống không (app-spawn) + cổng OSC — quyết định
|
||||
// route MIDI item exclusive qua Carla hay fallback FluidSynth
|
||||
carlaStatus: () => apiRequest('/api/v1/plugins/carla-status', { method: 'GET' }),
|
||||
// Gửi MIDI note (track ARM → Carla OSC) để preview VSTi realtime
|
||||
carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
|
||||
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Render MIDI notes → WAV bằng VSTi (pedalboard, âm thật) — trả file_id
|
||||
@@ -99,10 +87,7 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
// Mở native GUI của VST FX (mastering chain / FX rack) qua bridge
|
||||
openFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
|
||||
// pedalboard không render được plugin (VD VST2)
|
||||
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
|
||||
// Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard
|
||||
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
|
||||
uploadPreset: (file, pluginHint) => {
|
||||
const fd = new FormData();
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// SonicForge Runtime service — phát hiện môi trường chạy (desktop Windows /
|
||||
// docker headless) qua /api/v1/system/capabilities, bật/tắt tính năng theo đó.
|
||||
// - data-runtime trên <html>: "desktop" | "headless"
|
||||
// - data-carla="1": có Carla local (hiện nút "Mở trong Carla")
|
||||
// - Phần tử có thuộc tính data-carla-only sẽ bị ẩn khi không có Carla local.
|
||||
// - Thư viện preset (.vstpreset) cache trong SonicRuntime.presets — dùng cho
|
||||
// dropdown gán preset vào track (Carla → pedalboard bridge).
|
||||
// dropdown gán preset vào track (preset → pedalboard bridge).
|
||||
window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null, presets: null };
|
||||
|
||||
(function () {
|
||||
@@ -22,19 +20,13 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
|
||||
window.SonicRuntime.loaded = true;
|
||||
// environment: "docker" | "standalone" — quyết định luồng âm
|
||||
// instrument: docker → FluidSynthWASM (client), standalone →
|
||||
// native (backend pyfluidsynth / Carla). Không phải docker =
|
||||
// standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
|
||||
// native (backend pyfluidsynth / native_bridge). Không phải
|
||||
// docker = standalone (Windows/Linux/macOS chạy trực tiếp trên OS).
|
||||
window.SonicRuntime.environment = c.environment || (c.docker ? 'docker' : 'standalone');
|
||||
var html = document.documentElement;
|
||||
html.dataset.runtime = c.runtime || 'unknown';
|
||||
html.dataset.platform = c.platform || '';
|
||||
html.dataset.environment = window.SonicRuntime.environment;
|
||||
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
|
||||
if (c.features && c.features.carla_local === false) {
|
||||
document.querySelectorAll('[data-carla-only]').forEach(function (el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
// Cache sẵn danh sách preset (static, ít thay đổi)
|
||||
listPresets().catch(function () {});
|
||||
return c;
|
||||
@@ -66,54 +58,3 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
|
||||
load();
|
||||
}
|
||||
})();
|
||||
|
||||
// ── SonicCarlaMidi: cầu nối MIDI từ track ARM → Carla (OSC /Carla/0/note_*) ──
|
||||
// Khi track dùng VSTi (synth_engine.type chứa 'vst') + được ARM + máy có Carla
|
||||
// local → phím bấm trên piano roll / keybed được gửi tới Carla để phát realtime
|
||||
// (Carla standalone bật OSC UDP mặc định cổng 22752).
|
||||
window.SonicCarlaMidi = window.SonicCarlaMidi || {
|
||||
shouldRoute: function (synthEngine, isArmed) {
|
||||
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||
return false;
|
||||
},
|
||||
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
|
||||
// user đã chủ động bấm Play trên item đó).
|
||||
shouldRoutePlayback: function (synthEngine) {
|
||||
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||
return false;
|
||||
},
|
||||
noteOn: function (channel, note, velocity) {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
window.SonicAPI.carlaMidi({ event: 'note_on', note: note, velocity: velocity || 100, channel: channel || 0 }).catch(function () {});
|
||||
},
|
||||
noteOff: function (channel, note) {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
window.SonicAPI.carlaMidi({ event: 'note_off', note: note, channel: channel || 0 }).catch(function () {});
|
||||
},
|
||||
// Bật note rồi tự tắt sau durMs (preview ngắn)
|
||||
playNote: function (channel, note, velocity, durMs) {
|
||||
this.noteOn(channel, note, velocity);
|
||||
var self = this;
|
||||
setTimeout(function () { self.noteOff(channel, note); }, (durMs || 300) + 50);
|
||||
},
|
||||
// Tắt mọi note đang ngân trong Carla (note_off toàn pitch — dùng khi stop)
|
||||
allNotesOff: function () {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
for (var ch = 0; ch < 16; ch++) {
|
||||
for (var n = 0; n < 128; n++) {
|
||||
window.SonicAPI.carlaMidi({ event: 'note_off', note: n, channel: ch }).catch(function () {});
|
||||
}
|
||||
}
|
||||
},
|
||||
// Unload Carla bridge: tắt âm + terminate tiến trình Carla (app spawn).
|
||||
// Gọi khi track chuyển từ VSTi → instrument soundfont để âm KHÔNG còn
|
||||
// play qua Carla bridge nữa.
|
||||
stopBridge: function () {
|
||||
var self = this;
|
||||
try { self.allNotesOff(); } catch (e) {}
|
||||
if (window.SonicAPI && window.SonicAPI.stopCarla) {
|
||||
return window.SonicAPI.stopCarla().catch(function () {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// app/static/js/services/unifiedMidiRouter.js
|
||||
// Single MIDI entry point for Web MIDI keyboard + timeline playback.
|
||||
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
|
||||
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
|
||||
// bridge down -> onFallback callback (app.jsx wires SonicSF/native path).
|
||||
(function () {
|
||||
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
|
||||
|
||||
|
||||
Reference in New Issue
Block a user