feat(fx-gui): đưa audio vào process GUI — meter VST nhảy theo âm thật qua SHM peek

- GUI process peek ring SHM đầu vào (không consume, không tranh chấp RealtimeFxLoop)
- gọi fx->processAudio() trên instance GUI để meter/UI plugin chuyển động theo âm thật
- performEdit forward sang controller -> meter điều khiển bởi param cũng cập nhật
- bridge /shm: frontend đẩy tên session live vào mỗi GUI đang mở (start/stop/open)
- tests: 127 passed
This commit is contained in:
2026-08-22 17:06:37 +07:00
parent 6ba38286ad
commit 6dfe390bda
7 changed files with 219 additions and 24 deletions
+27 -6
View File
@@ -1765,6 +1765,7 @@ async function fxRtStart() {
return;
}
fxRt.sessionId = st.session_id;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212200`);
fxRt.node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
@@ -1841,6 +1842,7 @@ function fxRtStop(silent) {
try { if (fxRt.ws) { fxRt.ws.onclose = null; fxRt.ws.onmessage = null; try { fxRt.ws.close(); } catch (e) {} fxRt.ws = null; } } catch (e) {}
const sid = fxRt.sessionId;
fxRt.sessionId = null;
fxRtGuiPushShm();
fxRt.active = false;
fxRt.chainKey = null;
if (sid) {
@@ -1855,6 +1857,23 @@ function fxRtStop(silent) {
// cùng chain (khớp app: builtin FX cũng chạy instance riêng mỗi path; offline
// export áp VST lên track mix tổng). Thứ tự: builtin FX → VST (khớp offline).
const fxRtTracks = {}; // 'trackId:audio|sf' → { sessionId, ws, node, ready, path, spliced, chainKey }
// Embedded VST GUI → live SHM session feed (open_fx_gui shm + bridge /shm route).
// url → trackId|null (null = master GUI). The GUI process peeks the session's
// input ring so its meters move with real audio; push on every start/stop.
const _fxGuiEmbedSources = {};
function _fxGuiRegisterSource(url, trackId) { if (!url) return; _fxGuiEmbedSources[url] = (trackId === undefined ? null : trackId); }
function _fxGuiUnregisterSource(url) { if (url) delete _fxGuiEmbedSources[url]; }
function fxRtTrackShmName(trackId) {
const key = trackId ? Object.keys(fxRtTracks).find(k => k.startsWith(trackId + ':')) : '';
return key ? (fxRtTracks[key].sessionId || '') : '';
}
function fxRtGuiPushShm() {
Object.keys(_fxGuiEmbedSources).forEach(url => {
const trackId = _fxGuiEmbedSources[url];
const name = trackId === null ? (fxRt.sessionId || '') : fxRtTrackShmName(trackId);
try { fetch(url + '/shm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shm: name }) }).catch(() => {}); } catch (e) {}
});
}
function fxRtTrackChain(track) {
if (!track) return [];
const chain = activeVstChain(track.vstFxChain || []);
@@ -12430,7 +12449,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3)
const [fxGuiOpen, setFxGuiOpenState] = React.useState(_persistFxGuiOpen); // { path, url } | null
const setFxGuiOpen = (v) => { _persistFxGuiOpen = v; _persistFxGuiTrackId = v ? (track && track.id) : null; setFxGuiOpenState(v); };
const setFxGuiOpen = (v) => { if (_persistFxGuiOpen && _persistFxGuiOpen.url) _fxGuiUnregisterSource(_persistFxGuiOpen.url); _persistFxGuiOpen = v; _persistFxGuiTrackId = v ? (track && track.id) : null; if (v && v.url) _fxGuiRegisterSource(v.url, track && track.id); setFxGuiOpenState(v); };
const fxGuiOpenRef = React.useRef(null);
fxGuiOpenRef.current = fxGuiOpen;
const closeFxGui = () => {
@@ -12454,7 +12473,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
if (!track || !track.vstGuiOpenPath || fxGuiOpen) return;
const s = (track.vstFxChain || []).find(x => x.path === track.vstGuiOpenPath);
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => {
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id) }).then(r => {
if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url });
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -12462,7 +12481,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (rem) delete _capturedPresetByPath[rem.path]; if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); };
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRtTrackShmName(track && track.id) }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; const next = [...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]; setVstChain(next); setActiveVstIdx(next.length - 1); };
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
const uploadVstPreset = (idx) => {
@@ -13023,7 +13042,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const masterVstChain = (ozState.vstFxChain || []);
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 2)
const [masterGuiOpen, setMasterGuiOpenState] = React.useState(_persistMasterGuiOpen); // { path, url } | null
const setMasterGuiOpen = (v) => { _persistMasterGuiOpen = v; setMasterGuiOpenState(v); };
const setMasterGuiOpen = (v) => { if (_persistMasterGuiOpen && _persistMasterGuiOpen.url) _fxGuiUnregisterSource(_persistMasterGuiOpen.url); _persistMasterGuiOpen = v; if (v && v.url) _fxGuiRegisterSource(v.url, null); setMasterGuiOpenState(v); };
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
const masterGuiOpenRef = React.useRef(null);
masterGuiOpenRef.current = masterGuiOpen;
@@ -13049,7 +13068,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen) return;
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true }).then(r => {
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true, shm: fxRt.sessionId || '' }).then(r => {
if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url });
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -13062,7 +13081,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; setMasterVstChain(masterVstChain.filter((_, i) => i !== idx)); };
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRt.sessionId || '' }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
@@ -23420,6 +23439,7 @@ const App = () => {
}
const entry = { sessionId: st.session_id, ws: null, node: null, ready: false, path, spliced: null, chainKey: JSON.stringify(chainPayload), inQueue: [], pumpTimer: null };
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212200`);
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
@@ -23500,6 +23520,7 @@ const App = () => {
try { if (entry.ws) { entry.ws.onclose = null; entry.ws.onmessage = null; try { entry.ws.close(); } catch (e) {} entry.ws = null; } } catch (e) {}
const sid = entry.sessionId;
delete fxRtTracks[key];
fxRtGuiPushShm();
if (sid) {
try { window.SonicAPI.fxRealtimeStop({ session_id: sid }).catch(() => {}); } catch (e) {}
}