fix(fx): realtime VST ngắt quãng khi mở MASTERING PANEL + PWR master không ảnh hưởng âm; pacing WS input/output, gate fxRt theo masterConnected, worklet cap 32
This commit is contained in:
+536
-61
@@ -1541,9 +1541,10 @@ function createTrackFxModule(type, ctx, params) {
|
||||
nodes = { gLL, gRL, gLR, gRR };
|
||||
} else if (type === 'eqpro') {
|
||||
return createEqProModule(ctx, params);
|
||||
} else if (type === 'carla') {
|
||||
// Carla Bridge = VST FX chạy NGOÀI (ứng dụng ngoài, user tự cài) — trong
|
||||
// WebAudio graph chỉ là pass-through (không thêm DSP): âm track đi thẳng.
|
||||
} 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.
|
||||
input.connect(output);
|
||||
nodes = { passthrough: input };
|
||||
} else {
|
||||
@@ -1567,8 +1568,12 @@ function createTrackFxModule(type, ctx, params) {
|
||||
// keyed in a LOCAL map so the live graph is never touched. Audio clips are then
|
||||
// scheduled through node.gainNode, and the offline master bus applies the
|
||||
// mastering chain — the exported file therefore matches what you hear.
|
||||
function buildOfflineTrackNode(track, ctx, nodeMap) {
|
||||
function buildOfflineTrackNode(track, ctx, nodeMap, opts) {
|
||||
if (!track || nodeMap[track.id]) return nodeMap[track.id] || null;
|
||||
// directOut: render track RIÊNG (track FX chain, chưa qua mastering bus) —
|
||||
// dùng cho export có VST FX: mỗi track render solo → áp VST chain qua
|
||||
// fx_vst_bridge → cộng mix → mastering 1 lần trên tổng.
|
||||
const directOut = !!(opts && opts.directOut);
|
||||
const gainNode = ctx.createGain();
|
||||
const volDb = track.volumeDb ?? 0;
|
||||
gainNode.gain.setValueAtTime(volDb <= -50 ? 0 : Math.pow(10, volDb / 20), 0);
|
||||
@@ -1577,10 +1582,16 @@ function buildOfflineTrackNode(track, ctx, nodeMap) {
|
||||
const analyserNode = ctx.createAnalyser();
|
||||
analyserNode.fftSize = 2048;
|
||||
pannerNode.connect(analyserNode);
|
||||
if (!masterBus) initMasterBus(ctx);
|
||||
const route = createMasteringRoute(ctx, track, masterBus);
|
||||
analyserNode.connect(route.routeGain);
|
||||
analyserNode.connect(route.dryGain);
|
||||
const node = { gainNode, pannerNode, analyserNode, route: null, fxStopFn: null, sfEntry: null, sfOut: null, sfRouteGain: null, sfDryGain: null };
|
||||
if (directOut) {
|
||||
analyserNode.connect(ctx.destination);
|
||||
} else {
|
||||
if (!masterBus) initMasterBus(ctx);
|
||||
const route = createMasteringRoute(ctx, track, masterBus);
|
||||
analyserNode.connect(route.routeGain);
|
||||
analyserNode.connect(route.dryGain);
|
||||
node.route = route;
|
||||
}
|
||||
let fxStopFn;
|
||||
const fxEntry = ctx.createGain();
|
||||
const fxLegacyIn = ctx.createGain();
|
||||
@@ -1605,7 +1616,7 @@ function buildOfflineTrackNode(track, ctx, nodeMap) {
|
||||
} else {
|
||||
fxLegacyIn.connect(pannerNode);
|
||||
}
|
||||
const node = { gainNode, pannerNode, analyserNode, route, fxStopFn, sfEntry: null, sfOut: null, sfRouteGain: null, sfDryGain: null };
|
||||
node.fxStopFn = fxStopFn || null;
|
||||
// Soundfont (MIDI cache) chain — mirrors the live node so cached MIDI buffers
|
||||
// flow through the track FX modules + mastering exactly like playback.
|
||||
if ((track.midiItems && track.midiItems.length > 0)) {
|
||||
@@ -1622,8 +1633,12 @@ function buildOfflineTrackNode(track, ctx, nodeMap) {
|
||||
sfDryGain.gain.value = sfBypass ? 1 : 0;
|
||||
sfPan.connect(sfRouteGain);
|
||||
sfPan.connect(sfDryGain);
|
||||
sfRouteGain.connect(masterBus.input);
|
||||
sfDryGain.connect(masterBus.dryInput);
|
||||
if (directOut) {
|
||||
sfPan.connect(ctx.destination);
|
||||
} else {
|
||||
sfRouteGain.connect(masterBus.input);
|
||||
sfDryGain.connect(masterBus.dryInput);
|
||||
}
|
||||
const sfMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
|
||||
try { return createTrackFxModule(m.type || m, ctx, m.params); } catch (e) { return null; }
|
||||
}).filter(Boolean) : [];
|
||||
@@ -1639,6 +1654,187 @@ function buildOfflineTrackNode(track, ctx, nodeMap) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── VST FX export helpers (fx_vst_bridge --render-fx qua engine /fx-render) ──
|
||||
// Track + master VST chains là pass-through trong WebAudio (live + offline
|
||||
// graph); xử lý THẬT chỉ chạy được ở export OFFLINE qua fx_vst_bridge (one-shot
|
||||
// render, không SHM realtime — xem native_bridge/src/main_fx.cpp).
|
||||
function audioBufferToInterleaved(buf) {
|
||||
const ch = buf.numberOfChannels;
|
||||
const len = buf.length;
|
||||
const mono = ch < 2;
|
||||
const out = new Float32Array(len * (mono ? 1 : 2));
|
||||
const L = buf.getChannelData(0);
|
||||
if (mono) {
|
||||
out.set(L);
|
||||
} else {
|
||||
const R = buf.getChannelData(1);
|
||||
for (let i = 0; i < len; i++) { out[i * 2] = L[i]; out[i * 2 + 1] = R[i]; }
|
||||
}
|
||||
return { pcm: out, channels: mono ? 1 : 2, sampleRate: buf.sampleRate };
|
||||
}
|
||||
function interleavedToAudioBuffer(pcm, sampleRate, channels, ctx) {
|
||||
const n = Math.floor(pcm.length / channels);
|
||||
const buf = ctx.createBuffer(channels, n, sampleRate);
|
||||
for (let ch = 0; ch < channels; ch++) {
|
||||
const d = buf.getChannelData(ch);
|
||||
for (let i = 0; i < n; i++) d[i] = pcm[i * channels + ch];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
function mixInterleaved(a, b) {
|
||||
const n = Math.max(a.length, b.length);
|
||||
const out = new Float32Array(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
out[i] = (i < a.length ? a[i] : 0) + (i < b.length ? b[i] : 0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function activeVstChain(chain) {
|
||||
return (chain || []).filter(s => s && s.path && !s.bypass);
|
||||
}
|
||||
function encodeFloatWavBlob(pcm, sampleRate, channels) {
|
||||
// IEEE float32 WAV — bit-exact input cho fx_vst_bridge (không dither/quantize
|
||||
// ở đây; final export encode giữ nguyên chính sách hiện có).
|
||||
const len = Math.floor(pcm.length / channels);
|
||||
const bps = 4;
|
||||
const dataBytes = len * bps * channels;
|
||||
const buf = new ArrayBuffer(44 + dataBytes);
|
||||
const v = new DataView(buf);
|
||||
const ws = (o, s) => { for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i)); };
|
||||
ws(0, 'RIFF'); v.setUint32(4, 36 + dataBytes, true); ws(8, 'WAVE');
|
||||
ws(12, 'fmt '); v.setUint32(16, 16, true); v.setUint16(20, 3, true);
|
||||
v.setUint16(22, channels, true); v.setUint32(24, sampleRate, true);
|
||||
v.setUint32(28, sampleRate * bps * channels, true);
|
||||
v.setUint16(32, bps, true); v.setUint16(34, 32, true);
|
||||
ws(36, 'data'); v.setUint32(40, dataBytes, true);
|
||||
let o = 44;
|
||||
for (let i = 0; i < pcm.length; i++) v.setFloat32(o + i * 4, pcm[i], true);
|
||||
return new Blob([buf], { type: 'audio/wav' });
|
||||
}
|
||||
|
||||
// ── Realtime VST FX (SHM bridge) — master chain LIVE qua fx_vst_bridge ─────
|
||||
// --realtime-fx: engine giữ SHM ring FxRealtimeIPC, spawn bridge; browser gửi
|
||||
// block 256 stereo float32 qua WS → engine SHM → bridge xử lý → SHM → engine →
|
||||
// WS → AudioWorklet (sf-fx-realtime.js) phát. Master: masterBus.output →
|
||||
// worklet → analyser (sau master fader, trước metering/destination — khớp thứ
|
||||
// tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường
|
||||
// chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out.
|
||||
const fxRt = { active: false, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null };
|
||||
function fxRtChain() {
|
||||
const ms = window.currentMasteringSettings;
|
||||
if (!ms || !ms.masterConnected || ms.isBypassed) return [];
|
||||
const chain = activeVstChain((ms && ms.vstFxChain) || []);
|
||||
return (chain.length > 0 && !(ms && ms.vstBypass)) ? chain : [];
|
||||
}
|
||||
async function fxRtStart() {
|
||||
if (fxRt.active) {
|
||||
// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
|
||||
if (!fxRt.node || fxRt.node.context !== getAudioContext()) fxRtStop(true);
|
||||
else return;
|
||||
}
|
||||
const chain = fxRtChain();
|
||||
if (!chain.length) return;
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx || !ctx.audioWorklet || !masterBus || !masterBus.output || !masterBus.analyser) return;
|
||||
const chainPayload = chain.map(s => ({ type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }));
|
||||
let st;
|
||||
try {
|
||||
st = await window.SonicAPI.fxRealtimeStart({ fx_chain: chainPayload, sample_rate: ctx.sampleRate || 44100 });
|
||||
} catch (e) {
|
||||
console.warn('[FX RT] start fail:', e);
|
||||
showToast('Không khởi động VST FX realtime: ' + (e.message || e), 'warning');
|
||||
return;
|
||||
}
|
||||
fxRt.sessionId = st.session_id;
|
||||
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] });
|
||||
const u = new URL(API_BASE_URL);
|
||||
const wsProto = u.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const tok = localStorage.getItem('sonic_token') || '';
|
||||
const wsUrl = `${wsProto}${u.host}${st.ws_url}${tok ? '?token=' + encodeURIComponent(tok) : ''}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
fxRt.ws = ws;
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = () => { fxRt.active = true; };
|
||||
ws.onmessage = (ev) => {
|
||||
if (!(ev.data instanceof ArrayBuffer) || !fxRt.node) return;
|
||||
const f = new Float32Array(ev.data);
|
||||
const half = f.length / 2;
|
||||
const l = new Float32Array(half);
|
||||
const r = new Float32Array(half);
|
||||
for (let i = 0; i < half; i++) { l[i] = f[i * 2]; r[i] = f[i * 2 + 1]; }
|
||||
fxRt.node.port.postMessage({ type: 'out', l, r });
|
||||
};
|
||||
ws.onclose = () => fxRtStop(true);
|
||||
ws.onerror = () => { try { ws.close(); } catch (e) {} };
|
||||
fxRt.node.port.onmessage = (ev) => {
|
||||
const d = ev.data;
|
||||
if (!d || d.type !== 'in' || !fxRt.ws) return;
|
||||
if (fxRt.inQueue.length > 64) fxRt.inQueue.shift();
|
||||
fxRt.inQueue.push(d.data);
|
||||
};
|
||||
// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn theo burst (main thread bị
|
||||
// rAF renderFrame block khi mở MASTERING PANEL) → SHM ring 4-slot tràn → mất
|
||||
// block → ngắt quãng.
|
||||
const blockMs = Math.max(4, Math.round((256 / (ctx.sampleRate || 44100)) * 1000));
|
||||
if (fxRt.pumpTimer) clearInterval(fxRt.pumpTimer);
|
||||
fxRt.pumpTimer = setInterval(() => {
|
||||
const ws = fxRt.ws;
|
||||
if (!ws || ws.readyState !== 1 || !fxRt.inQueue.length) return;
|
||||
const d = fxRt.inQueue.shift();
|
||||
try { ws.send(d); } catch (e) { fxRt.inQueue.unshift(d); }
|
||||
}, blockMs);
|
||||
try { masterBus.output.disconnect(masterBus.analyser); } catch (e) {}
|
||||
masterBus.output.connect(fxRt.node);
|
||||
fxRt.node.connect(masterBus.analyser);
|
||||
} catch (e) {
|
||||
console.warn('[FX RT] setup fail:', e);
|
||||
fxRtStop(true);
|
||||
}
|
||||
}
|
||||
function fxRtStop(silent) {
|
||||
if (fxRt.pumpTimer) { clearInterval(fxRt.pumpTimer); fxRt.pumpTimer = null; }
|
||||
fxRt.inQueue = [];
|
||||
if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return;
|
||||
try {
|
||||
if (fxRt.node) {
|
||||
fxRt.node.disconnect();
|
||||
if (masterBus && masterBus.output && masterBus.analyser) {
|
||||
try { masterBus.output.connect(masterBus.analyser); } catch (e) {}
|
||||
}
|
||||
fxRt.node.port.onmessage = null;
|
||||
fxRt.node = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
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;
|
||||
fxRt.active = false;
|
||||
if (sid) {
|
||||
try { window.SonicAPI.fxRealtimeStop({ session_id: sid }).catch(() => {}); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Realtime VST FX per-track (SHM bridge) — mọi luồng track qua bridge ─────
|
||||
// Mỗi track: session riêng (1 WS/SHM) cho TỪNG đường chain có âm — audio-clips
|
||||
// (path 'audio', splice fxEntry→mods→worklet→fxLegacyIn) và MIDI/SF (path 'sf',
|
||||
// splice sfEntry→mods→worklet→sfOut). Track vừa clips vừa MIDI → 2 session
|
||||
// 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 }
|
||||
function fxRtTrackChain(track) {
|
||||
if (!track) return [];
|
||||
const chain = activeVstChain(track.vstFxChain || []);
|
||||
return (chain.length > 0 && !track.vstFxBypass) ? chain : [];
|
||||
}
|
||||
function fxRtTrackPaths(track) {
|
||||
const paths = [];
|
||||
if (track && ((track.clips && track.clips.length > 0) || !!track.buffer || (track.sections && track.sections.length > 0))) paths.push('audio');
|
||||
if (track && ((track.midiItems && track.midiItems.length > 0) || track.type === 'MIDI' || !!track.instrumentId)) paths.push('sf');
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Track-EQ preset library (unified_fx_rack_panel.md §III.1) — band gains only.
|
||||
const TRACK_EQ_PRESETS = {
|
||||
flat: { name: 'Flat / Reset', g: [0, 0, 0, 0] },
|
||||
@@ -2268,7 +2464,6 @@ const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
|
||||
// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ──
|
||||
const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
|
||||
const [isFxActive, setIsFxActive] = React.useState(true);
|
||||
const [isTestPlaying, setIsTestPlaying] = React.useState(false);
|
||||
const [isMuted, setIsMuted] = React.useState(false);
|
||||
const [isMono, setIsMono] = React.useState(false);
|
||||
@@ -2535,7 +2730,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
|
||||
}
|
||||
},
|
||||
title: "Bật/Tắt MASTER FX CHAIN Bypass"
|
||||
}, [React.createElement("i", { className: "fa-solid fa-power-off" + (isFxActive ? " text-emerald-400" : " text-slate-500"), key: "ico" }), React.createElement("span", { key: "lbl", className: "text-[7px] font-bold" + (isFxActive ? " text-emerald-300" : " text-slate-400") }, "PWR")]),
|
||||
}, [React.createElement("i", { className: "fa-solid fa-power-off" + (isMasterActive ? " text-emerald-400" : " text-slate-500"), key: "ico" }), React.createElement("span", { key: "lbl", className: "text-[7px] font-bold" + (isMasterActive ? " text-emerald-300" : " text-slate-400") }, "PWR")]),
|
||||
React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 text-[8px]", title: "Trim Envelope" }, "TRIM"),
|
||||
React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 text-[9px]", title: "Session Info" },
|
||||
React.createElement("i", { className: "fa-solid fa-info" })
|
||||
@@ -2742,7 +2937,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs, style })
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { fxActive: next });
|
||||
// Live re-route: rebuild both FX chains (audio + SF) so EVERY item
|
||||
// (midi/section/audioclip) immediately follows the FX power state.
|
||||
if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id);
|
||||
if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id, { fxActive: next });
|
||||
},
|
||||
className: "btn-daw h-[24px] rounded flex items-center justify-center text-[8px]" + (track.fxActive !== false ? " text-emerald-400" : " text-slate-500"),
|
||||
title: "Toggle FX Power: ON (sáng) = MỌI items (MIDI/section/audioclip) qua FX Rack Panel (nút A/♪ chỉ ảnh hưởng Mastering FX Chain); OFF (tối) = bỏ toàn bộ track FX"
|
||||
@@ -11296,7 +11491,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
||||
fx_chain: masteringSettings.vstFxChain.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
volume_db: (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0,
|
||||
mute: false,
|
||||
bypass: !!masteringSettings.vstBypass
|
||||
bypass: !masteringSettings.masterConnected || !!masteringSettings.vstBypass
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
@@ -12192,7 +12387,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
...chain.map((m, i) => ({ kind: 'builtin', m, i })),
|
||||
...vstChain.map((s, i) => ({ kind: 'vst', s, i })),
|
||||
];
|
||||
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
|
||||
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); };
|
||||
@@ -12262,7 +12457,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
|
||||
const setChain = (next) => {
|
||||
if (onUpdateTrack) onUpdateTrack(track.id, { fxChain: next });
|
||||
if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id);
|
||||
if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id, { fxChain: next });
|
||||
};
|
||||
const paramsOf = (m) => ({ ...(TRACK_FX_DEFAULTS[m.type] || {}), ...(m.params || {}) });
|
||||
const setParams = (idx, patch) => {
|
||||
@@ -12564,13 +12759,13 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
|
||||
{/* VST FX toolbar — VST3 (Ozone...) áp dụng lúc Export/Offline render (preview = âm export) */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-2 shrink-0">
|
||||
<span className="text-[9px] text-slate-500 font-mono truncate">VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render</span>
|
||||
<span className="text-[9px] text-slate-500 font-mono truncate">VST3 — realtime qua bridge (SHM) khi play + áp dụng lúc Export/Offline render</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={previewFx} className="px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1">
|
||||
<i data-lucide="play" className="w-3 h-3"></i> Nghe thử (render offline)
|
||||
</button>
|
||||
<label className="flex items-center gap-1 text-[10px] text-slate-400 font-mono cursor-pointer">
|
||||
<input type="checkbox" checked={vstBypass} onChange={e => { const val = e.target.checked; setVstBypass(val); if (onUpdateTrack) onUpdateTrack(track.id, { vstFxBypass: val }); }} className="w-3 h-3" /> Bypass
|
||||
<input type="checkbox" checked={vstBypass} onChange={e => { const val = e.target.checked; setVstBypass(val); if (onUpdateTrack) onUpdateTrack(track.id, { vstFxBypass: val }); if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id, { vstFxBypass: val }); }} className="w-3 h-3" /> Bypass
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18552,6 +18747,12 @@ const App = () => {
|
||||
if (audioCtx && masterBus) {
|
||||
toggleMasteringOnMaster(masteringSettings.masterConnected, masteringSettings.isBypassed);
|
||||
applyMasteringSettings(masteringSettings);
|
||||
// Đồng bộ realtime VST master với PWR/Bypass: WebAudio graph đã đổi theo
|
||||
// masterConnected nhưng fxRt không tự dừng → tắt PWR mà âm không đổi.
|
||||
try {
|
||||
if (fxRtChain().length) { if (!fxRt.active) fxRtStart(); }
|
||||
else if (fxRt.active) { fxRtStop(); }
|
||||
} catch (e) { console.warn('[FX RT] resync fail:', e); }
|
||||
// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
|
||||
// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
|
||||
// ngay lên node đang phát (không chờ tracks effect).
|
||||
@@ -23094,13 +23295,14 @@ const App = () => {
|
||||
// re-routes ONLY this track's fx chain (fxEntry → active modules → fxLegacyIn),
|
||||
// leaving other tracks and the master bus untouched. Called by the FX Rack
|
||||
// panel on add/remove/reorder/toggle/param change — live while playing.
|
||||
const rebuildTrackFxGraph = (trackId) => {
|
||||
const rebuildTrackFxGraph = (trackId, patch) => {
|
||||
const node = activeTrackNodesRef.current[trackId];
|
||||
if (!node || !node.fxEntry || !node.fxLegacyIn) return;
|
||||
try {
|
||||
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
|
||||
const track = list.find(t => t.id === trackId);
|
||||
let track = list.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
if (patch) track = { ...track, ...patch };
|
||||
node.fxEntry.disconnect();
|
||||
const fxEnabled = track.fxActive !== false;
|
||||
const chainMods = fxEnabled ? (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
|
||||
@@ -23130,6 +23332,10 @@ const App = () => {
|
||||
if (node.sfPan && getAudioContext()) {
|
||||
node.sfPan.pan.setTargetAtTime((track.pan ?? 0) / 100, getAudioContext().currentTime, 0.02);
|
||||
}
|
||||
// Realtime VST FX per-track: chain mới → reconcile session/worklet
|
||||
// (start nếu chain active khi play / stop nếu chain bị bypass / re-splice
|
||||
// nếu mods đổi). VST áp SAU builtin FX — khớp offline export.
|
||||
fxRtTrackReconcile(trackId, node, track);
|
||||
} catch (e) {
|
||||
console.warn('rebuildTrackFxGraph error:', e);
|
||||
}
|
||||
@@ -23138,6 +23344,143 @@ const App = () => {
|
||||
rebuildTrackFxGraphRef.current = rebuildTrackFxGraph;
|
||||
window.__rebuildTrackFxGraph = rebuildTrackFxGraph;
|
||||
|
||||
// ── Realtime VST FX per-track (SHM bridge) — component scope ──────────────
|
||||
// Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart):
|
||||
// fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice
|
||||
// worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild).
|
||||
const fxRtTrackStart = async (trackId, track, path) => {
|
||||
const key = trackId + ':' + path;
|
||||
const chain = fxRtTrackChain(track);
|
||||
if (!chain.length) return;
|
||||
if (fxRtTracks[key]) {
|
||||
if (!fxRtTracks[key].node || fxRtTracks[key].node.context !== getAudioContext()) fxRtTrackStop(trackId, path, true);
|
||||
else return;
|
||||
}
|
||||
const ctx = getAudioContext();
|
||||
if (!ctx || !ctx.audioWorklet) return;
|
||||
const chainPayload = chain.map(s => ({ type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }));
|
||||
let st;
|
||||
try {
|
||||
st = await window.SonicAPI.fxRealtimeStart({ fx_chain: chainPayload, sample_rate: ctx.sampleRate || 44100 });
|
||||
} catch (e) {
|
||||
console.warn('[FX RT track] start fail', trackId, path, e);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
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] });
|
||||
entry.node = node;
|
||||
const u = new URL(API_BASE_URL);
|
||||
const wsProto = u.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const tok = localStorage.getItem('sonic_token') || '';
|
||||
const ws = new WebSocket(`${wsProto}${u.host}${st.ws_url}${tok ? '?token=' + encodeURIComponent(tok) : ''}`);
|
||||
entry.ws = ws;
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = () => { entry.ready = true; fxRtTrackSplice(trackId, path); };
|
||||
ws.onmessage = (ev) => {
|
||||
if (!(ev.data instanceof ArrayBuffer) || !entry.node) return;
|
||||
const f = new Float32Array(ev.data);
|
||||
const half = f.length / 2;
|
||||
const l = new Float32Array(half);
|
||||
const r = new Float32Array(half);
|
||||
for (let i = 0; i < half; i++) { l[i] = f[i * 2]; r[i] = f[i * 2 + 1]; }
|
||||
entry.node.port.postMessage({ type: 'out', l, r });
|
||||
};
|
||||
ws.onclose = () => fxRtTrackStop(trackId, path, true);
|
||||
ws.onerror = () => { try { ws.close(); } catch (e) {} };
|
||||
entry.node.port.onmessage = (ev) => {
|
||||
const d = ev.data;
|
||||
if (!d || d.type !== 'in' || !entry.ws) return;
|
||||
if (entry.inQueue.length > 64) entry.inQueue.shift();
|
||||
entry.inQueue.push(d.data);
|
||||
};
|
||||
const blockMs = Math.max(4, Math.round((256 / (ctx.sampleRate || 44100)) * 1000));
|
||||
entry.pumpTimer = setInterval(() => {
|
||||
const ws = entry.ws;
|
||||
if (!ws || ws.readyState !== 1 || !entry.inQueue.length) return;
|
||||
const d = entry.inQueue.shift();
|
||||
try { ws.send(d); } catch (e) { entry.inQueue.unshift(d); }
|
||||
}, blockMs);
|
||||
} catch (e) {
|
||||
console.warn('[FX RT track] setup fail', trackId, path, e);
|
||||
fxRtTrackStop(trackId, path, true);
|
||||
}
|
||||
};
|
||||
const fxRtTrackUnsplice = (trackId, path) => {
|
||||
const entry = fxRtTracks[trackId + ':' + path];
|
||||
if (!entry || !entry.spliced || !entry.node) return;
|
||||
try {
|
||||
entry.node.disconnect();
|
||||
try { entry.spliced.tail.connect(entry.spliced.dest); } catch (e) {}
|
||||
entry.spliced = null;
|
||||
} catch (e) { console.warn('[FX RT track] unsplice fail', trackId, path, e); }
|
||||
};
|
||||
const fxRtTrackSplice = (trackId, path) => {
|
||||
const key = trackId + ':' + path;
|
||||
const entry = fxRtTracks[key];
|
||||
const node = activeTrackNodesRef.current[trackId];
|
||||
if (!entry || !entry.ready || !entry.node || !node || entry.spliced) return;
|
||||
const tail = path === 'audio'
|
||||
? (node.fxMods && node.fxMods.length ? node.fxMods[node.fxMods.length - 1].output : node.fxEntry)
|
||||
: (node.sfMods && node.sfMods.length ? node.sfMods[node.sfMods.length - 1].output : node.sfEntry);
|
||||
const dest = path === 'audio' ? node.fxLegacyIn : node.sfOut;
|
||||
if (!tail || !dest) return;
|
||||
try {
|
||||
tail.disconnect(dest);
|
||||
entry.node.connect(dest);
|
||||
tail.connect(entry.node);
|
||||
entry.spliced = { tail, dest };
|
||||
console.log('[FX RT track] spliced', trackId, path);
|
||||
} catch (e) { console.warn('[FX RT track] splice fail', trackId, path, e); }
|
||||
};
|
||||
const fxRtTrackStop = (trackId, path, silent) => {
|
||||
const key = trackId + ':' + path;
|
||||
const entry = fxRtTracks[key];
|
||||
if (!entry) return;
|
||||
if (entry.pumpTimer) { clearInterval(entry.pumpTimer); entry.pumpTimer = null; }
|
||||
entry.inQueue = [];
|
||||
try {
|
||||
fxRtTrackUnsplice(trackId, path);
|
||||
if (entry.node) { entry.node.port.onmessage = null; try { entry.node.disconnect(); } catch (e) {} entry.node = null; }
|
||||
} catch (e) {}
|
||||
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];
|
||||
if (sid) {
|
||||
try { window.SonicAPI.fxRealtimeStop({ session_id: sid }).catch(() => {}); } catch (e) {}
|
||||
}
|
||||
};
|
||||
const fxRtTrackReconcile = (trackId, node, track) => {
|
||||
try {
|
||||
const chain = fxRtTrackChain(track);
|
||||
const wantPaths = chain.length ? fxRtTrackPaths(track) : [];
|
||||
const prefix = trackId + ':';
|
||||
Object.keys(fxRtTracks).forEach(k => {
|
||||
if (!k.startsWith(prefix)) return;
|
||||
const p = k.slice(prefix.length);
|
||||
if (wantPaths.indexOf(p) < 0) fxRtTrackStop(trackId, p, true);
|
||||
});
|
||||
if (!isPlayingRef.current) return;
|
||||
wantPaths.forEach(p => {
|
||||
const key = trackId + ':' + p;
|
||||
const entry = fxRtTracks[key];
|
||||
if (!entry) fxRtTrackStart(trackId, track, p);
|
||||
else if (entry.ready && node) {
|
||||
fxRtTrackUnsplice(trackId, p); // chain mới (sau rebuild) — splice lại đúng mods
|
||||
fxRtTrackSplice(trackId, p);
|
||||
}
|
||||
});
|
||||
} catch (e) { console.warn('fxRtTrackReconcile error:', e); }
|
||||
};
|
||||
const fxRtTrackStopAll = () => {
|
||||
Object.keys(fxRtTracks).forEach(k => {
|
||||
const i = k.lastIndexOf(':');
|
||||
fxRtTrackStop(k.slice(0, i), k.slice(i + 1), true);
|
||||
});
|
||||
};
|
||||
|
||||
// Route the SHARED FluidSynth output through the gainNode of the single
|
||||
// audible MIDI track so its FX Rack chain / fader / pan affect the soundfont
|
||||
// instrument (unified_fx_rack_panel.md + user request). With more than one
|
||||
@@ -23546,6 +23889,8 @@ const App = () => {
|
||||
const context = getAudioContext();
|
||||
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms)
|
||||
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
|
||||
// Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active.
|
||||
try { fxRtStart(); } catch (e) { console.warn('[FX RT] start error:', e); }
|
||||
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
|
||||
// bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
|
||||
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
|
||||
@@ -23578,6 +23923,14 @@ const App = () => {
|
||||
if (!isPlayable) return;
|
||||
|
||||
const gainNode = getOrCreateTrackNode(track, context);
|
||||
// Realtime VST FX per-track (SHM bridge): mọi luồng track qua bridge —
|
||||
// session/worklet cho TỪNG path có âm (audio-clips + MIDI/SF); splice
|
||||
// vào graph khi WS ready.
|
||||
try {
|
||||
const rtPaths = fxRtTrackPaths(track);
|
||||
if (rtPaths.indexOf('audio') >= 0) fxRtTrackStart(track.id, track, 'audio');
|
||||
if (rtPaths.indexOf('sf') >= 0) fxRtTrackStart(track.id, track, 'sf');
|
||||
} catch (e) { console.warn('[FX RT track] start error:', e); }
|
||||
const pannerNode = activeTrackNodesRef.current[track.id].pannerNode;
|
||||
// Re-apply route gains theo content-type NGAY tại thời điểm play — node
|
||||
// có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ)
|
||||
@@ -24203,6 +24556,10 @@ const App = () => {
|
||||
const stopAllPlayback = (fade) => {
|
||||
try {
|
||||
isPlayingRef.current = false;
|
||||
// Realtime VST FX: dừng session + khôi phục output→analyser.
|
||||
try { fxRtStop(); } catch (e) { console.warn('[FX RT] stop error:', e); }
|
||||
// Realtime VST FX per-track: dừng mọi session track + restore chain trực tiếp.
|
||||
try { fxRtTrackStopAll(); } catch (e) { console.warn('[FX RT track] stop error:', e); }
|
||||
_nativePlayEpoch++; // hủy mọi render/decode pending của lần play cũ
|
||||
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
|
||||
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
|
||||
@@ -26936,9 +27293,24 @@ const App = () => {
|
||||
else if (m.startTime) needDur = Math.max(needDur, m.startTime + (m.duration || 8));
|
||||
});
|
||||
});
|
||||
// Cache có thể "đủ" (chunks + duration) nhưng RỖNG — khi bridge VSTi active,
|
||||
// âm đi qua BridgeAudioNode, KHÔNG qua track.sfEntry nơi ensureMidiCapture
|
||||
// tap → chunks toàn zero → export offline ra file câm tuyệt đối. Kiểm tra
|
||||
// năng lượng cache: nếu không có tín hiệu → bounce realtime (bắt đúng âm bridge).
|
||||
const cacheHasEnergy = (c) => {
|
||||
if (!c) return false;
|
||||
const chunks = c.chunks || [];
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const ch = chunks[i];
|
||||
for (let j = 0; j < ch.length; j++) {
|
||||
if (Math.abs(ch[j]) > 0.0005) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const cacheOK = midiTracks.length > 0 && midiTracks.every(t => {
|
||||
const c = midiCacheRef.current[t.id];
|
||||
return c && c.chunks.length > 0 && c.duration >= needDur - 0.3;
|
||||
return c && c.chunks.length > 0 && c.duration >= needDur - 0.3 && cacheHasEnergy(c);
|
||||
});
|
||||
if (cacheOK) {
|
||||
showToast("Dùng MIDI cache từ preview — export OFFLINE NHANH (vẫn đủ FX Rack + Mastering Chain).", "info");
|
||||
@@ -27417,6 +27789,75 @@ const App = () => {
|
||||
if (c && !loopActive && c.duration > durationLimit) durationLimit = c.duration;
|
||||
});
|
||||
const outChannels = exportSettings.channels === 'mono' ? 1 : 2;
|
||||
const scheduleTrackInto = (t, ctx, nodes, directOut) => {
|
||||
const node = buildOfflineTrackNode(t, ctx, nodes, { directOut });
|
||||
if (!node) return null;
|
||||
const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
|
||||
id: 'default',
|
||||
buffer: t.buffer,
|
||||
startTime: t.startTime || 0,
|
||||
name: t.name,
|
||||
speed: t.speed || 1.0
|
||||
}] : [];
|
||||
clips.forEach(clip => {
|
||||
if (!clip.buffer) return;
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = clip.buffer;
|
||||
source.playbackRate.value = clip.speed || 1.0;
|
||||
source.connect(node.gainNode);
|
||||
const clipStart = clip.startTime || 0;
|
||||
source.start(clipStart);
|
||||
});
|
||||
// MIDI preview cache → schedule through the SF chain (track FX + mastering)
|
||||
if (node.sfEntry) {
|
||||
const midiBuf = midiCacheToBuffer(t.id);
|
||||
if (midiBuf) {
|
||||
let mSrcBuf = midiBuf;
|
||||
if (loopInfo && loopInfo.loopDur) {
|
||||
const ln = Math.min(midiBuf.length, Math.max(1, Math.ceil(loopInfo.loopDur * midiBuf.sampleRate)));
|
||||
mSrcBuf = ctx.createBuffer(midiBuf.numberOfChannels, ln, midiBuf.sampleRate);
|
||||
for (let mc = 0; mc < midiBuf.numberOfChannels; mc++) mSrcBuf.copyToChannel(midiBuf.getChannelData(mc).subarray(0, ln), mc);
|
||||
}
|
||||
const mSrc = ctx.createBufferSource();
|
||||
mSrc.buffer = mSrcBuf;
|
||||
mSrc.connect(node.sfEntry);
|
||||
mSrc.start(0);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
// VST FX THẬT qua fx_vst_bridge (engine /fx-render): encode WAV float32 →
|
||||
// upload → bridge render → download → decode. Lỗi → giữ nguyên PCM (export
|
||||
// vẫn chạy, toast cảnh báo) — không chặn export.
|
||||
const applyVstChainToPcm = async (pcm, rate, chain, label) => {
|
||||
const active = activeVstChain(chain);
|
||||
if (!active.length) return pcm;
|
||||
try {
|
||||
const blob = encodeFloatWavBlob(pcm, rate, 2);
|
||||
const fd = new FormData();
|
||||
fd.append('file', blob, 'vst_fx_input.wav');
|
||||
const up = await fetch(`${API_AUDIO}/upload`, { method: 'POST', body: fd });
|
||||
if (!up.ok) throw new Error('upload VST input thất bại (HTTP ' + up.status + ')');
|
||||
const upData = await up.json();
|
||||
const fxChain = active.map(s => ({ type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }));
|
||||
const r = await window.SonicAPI.fxRender({ input_file_id: upData.file_id, fx_chain: fxChain, sample_rate: rate });
|
||||
if (!r || !r.url) throw new Error('fx-render không trả url');
|
||||
const resp = await fetch(`${API_BASE_URL}${r.url}`);
|
||||
if (!resp.ok) throw new Error('download VST output thất bại (HTTP ' + resp.status + ')');
|
||||
const ab = await resp.arrayBuffer();
|
||||
const dctx = new OfflineAudioContext(2, 1, rate);
|
||||
const audioBuf = await dctx.decodeAudioData(ab);
|
||||
try { if (window.SonicAPI.deleteMyFile) window.SonicAPI.deleteMyFile(upData.file_id); } catch (e) { }
|
||||
return audioBufferToInterleaved(audioBuf).pcm;
|
||||
} catch (e) {
|
||||
console.warn('[VST FX]', label, 'không áp dụng:', e);
|
||||
showToast(`VST FX "${label}" không áp dụng được: ${e.message || e} — export không có VST này.`, 'warning');
|
||||
return pcm;
|
||||
}
|
||||
};
|
||||
const trackVstChains = activeTracks.filter(t => activeVstChain(t.vstFxChain || []).length > 0);
|
||||
const masterVstChain = activeVstChain((masteringSettings && masteringSettings.vstFxChain) || []);
|
||||
const masterVstOn = masterVstChain.length > 0 && !(masteringSettings && masteringSettings.vstBypass);
|
||||
const offlineCtx = new OfflineAudioContext(outChannels, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate);
|
||||
// Build the FULL graph (per-track FX Rack chains + mastering chain — the
|
||||
// same DSP as playback), so the exported file matches what you hear.
|
||||
@@ -27439,43 +27880,60 @@ const App = () => {
|
||||
masterBus.output.gain.setValueAtTime(mv <= -50 ? 0 : Math.pow(10, mv / 20), 0);
|
||||
}
|
||||
} catch (e) { console.warn('offline main-out volume error:', e); }
|
||||
activeTracks.forEach(t => {
|
||||
const node = buildOfflineTrackNode(t, offlineCtx, offlineNodes);
|
||||
if (!node) return;
|
||||
const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
|
||||
id: 'default',
|
||||
buffer: t.buffer,
|
||||
startTime: t.startTime || 0,
|
||||
name: t.name,
|
||||
speed: t.speed || 1.0
|
||||
}] : [];
|
||||
clips.forEach(clip => {
|
||||
if (!clip.buffer) return;
|
||||
const source = offlineCtx.createBufferSource();
|
||||
source.buffer = clip.buffer;
|
||||
source.playbackRate.value = clip.speed || 1.0;
|
||||
source.connect(node.gainNode);
|
||||
const clipStart = clip.startTime || 0;
|
||||
source.start(clipStart);
|
||||
});
|
||||
// MIDI preview cache → schedule through the SF chain (track FX + mastering)
|
||||
if (node.sfEntry) {
|
||||
const midiBuf = midiCacheToBuffer(t.id);
|
||||
if (midiBuf) {
|
||||
let mSrcBuf = midiBuf;
|
||||
if (loopInfo && loopInfo.loopDur) {
|
||||
const ln = Math.min(midiBuf.length, Math.max(1, Math.ceil(loopInfo.loopDur * midiBuf.sampleRate)));
|
||||
mSrcBuf = offlineCtx.createBuffer(midiBuf.numberOfChannels, ln, midiBuf.sampleRate);
|
||||
for (let mc = 0; mc < midiBuf.numberOfChannels; mc++) mSrcBuf.copyToChannel(midiBuf.getChannelData(mc).subarray(0, ln), mc);
|
||||
}
|
||||
const mSrc = offlineCtx.createBufferSource();
|
||||
mSrc.buffer = mSrcBuf;
|
||||
mSrc.connect(node.sfEntry);
|
||||
mSrc.start(0);
|
||||
}
|
||||
let renderedBuffer;
|
||||
if (trackVstChains.length > 0 || masterVstOn) {
|
||||
// Export có VST FX → render TRACK RIÊNG (không mastering) → áp track
|
||||
// VST chain qua fx_vst_bridge → cộng mix → mastering built-in 1 lần
|
||||
// trên tổng → master VST chain cuối chain. Đúng thứ tự live: track fx
|
||||
// → (sum) → master fx → out.
|
||||
let mix = null;
|
||||
for (const t of activeTracks) {
|
||||
const chain = activeVstChain(t.vstFxChain || []);
|
||||
const tctx = new OfflineAudioContext(2, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate);
|
||||
const tNodes = {};
|
||||
const node = scheduleTrackInto(t, tctx, tNodes, true);
|
||||
if (!node) continue;
|
||||
const tb = await tctx.startRendering();
|
||||
try { if (node.fxStopFn) node.fxStopFn(); } catch (e) { }
|
||||
let pcm = audioBufferToInterleaved(tb).pcm;
|
||||
if (chain.length) pcm = await applyVstChainToPcm(pcm, targetRate, chain, t.name || 'track');
|
||||
mix = mix ? mixInterleaved(mix, pcm) : pcm;
|
||||
}
|
||||
});
|
||||
const renderedBuffer = await offlineCtx.startRendering();
|
||||
if (!mix) mix = new Float32Array(Math.ceil(targetRate * Math.max(0.1, durationLimit)) * 2);
|
||||
const mctx = new OfflineAudioContext(2, Math.floor(mix.length / 2), targetRate);
|
||||
masterBus = null;
|
||||
initMasterBus(mctx);
|
||||
try {
|
||||
toggleMasteringOnMaster(!!(masteringSettings && masteringSettings.masterConnected), !!(masteringSettings && masteringSettings.isBypassed));
|
||||
} catch (e) { console.warn('offline mastering toggle error (vst):', e); }
|
||||
try {
|
||||
if (masterBus && masterBus.output) {
|
||||
const mv = (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0;
|
||||
masterBus.output.gain.setValueAtTime(mv <= -50 ? 0 : Math.pow(10, mv / 20), 0);
|
||||
}
|
||||
} catch (e) { console.warn('offline main-out volume error (vst):', e); }
|
||||
const src = mctx.createBufferSource();
|
||||
src.buffer = interleavedToAudioBuffer(mix, targetRate, 2, mctx);
|
||||
src.connect(masterBus.input);
|
||||
src.start(0);
|
||||
renderedBuffer = await mctx.startRendering();
|
||||
if (masterVstOn) {
|
||||
const conv = audioBufferToInterleaved(renderedBuffer);
|
||||
let outPcm = await applyVstChainToPcm(conv.pcm, targetRate, masterVstChain, 'master');
|
||||
if (outChannels === 1) {
|
||||
const n = Math.floor(outPcm.length / 2);
|
||||
const mono = new Float32Array(n);
|
||||
for (let i = 0; i < n; i++) mono[i] = (outPcm[i * 2] + outPcm[i * 2 + 1]) * 0.5;
|
||||
outPcm = mono;
|
||||
}
|
||||
renderedBuffer = interleavedToAudioBuffer(outPcm, targetRate, outChannels, mctx);
|
||||
}
|
||||
} else {
|
||||
activeTracks.forEach(t => {
|
||||
scheduleTrackInto(t, offlineCtx, offlineNodes, false);
|
||||
});
|
||||
renderedBuffer = await offlineCtx.startRendering();
|
||||
}
|
||||
const numExportCh = renderedBuffer.numberOfChannels;
|
||||
const exportLength = renderedBuffer.length;
|
||||
const bytesPerSample = bitDepth / 8;
|
||||
@@ -27590,9 +28048,26 @@ const App = () => {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Restore the live master bus + live mastering state after offline render.
|
||||
masterBus = savedMasterBus;
|
||||
try { if (savedMasterBus) toggleMasteringOnMaster(!!(masteringSettings && masteringSettings.masterConnected), !!(masteringSettings && masteringSettings.isBypassed)); } catch (e) { }
|
||||
// Restore the LIVE master bus after offline render: initMasterBus(offlineCtx)
|
||||
// ghi đè cả `masterBus` lẫn `window.masterBus` bằng graph OfflineAudioContext
|
||||
// (state closed) → track route (window.masterBus.input) đổ vào context câm →
|
||||
// play/bounce sau export CÂM. Khôi phục bus live (giữ wiring cũ nếu còn sống,
|
||||
// ngược lại rebuild graph trên audioCtx thật) và luôn trả lại window.masterBus.
|
||||
try {
|
||||
const liveCtx = audioCtx;
|
||||
let restored = savedMasterBus;
|
||||
if (!(restored && liveCtx && restored.output && restored.output.context === liveCtx && liveCtx.state !== 'closed')) {
|
||||
masterBus = null;
|
||||
initMasterBus(liveCtx || getAudioContext());
|
||||
restored = masterBus;
|
||||
}
|
||||
masterBus = restored;
|
||||
window.masterBus = restored;
|
||||
} catch (e) {
|
||||
masterBus = savedMasterBus;
|
||||
window.masterBus = savedMasterBus;
|
||||
}
|
||||
try { toggleMasteringOnMaster(!!(masteringSettings && masteringSettings.masterConnected), !!(masteringSettings && masteringSettings.isBypassed)); } catch (e) { }
|
||||
Object.keys(offlineNodes).forEach(k => { try { if (offlineNodes[k].fxStopFn) offlineNodes[k].fxStopFn(); } catch (e) { } });
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -92,6 +92,10 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
listFx: () => apiRequest('/api/v1/plugins/fx', { method: 'GET' }),
|
||||
fxRender: (payload) => apiRequest('/api/v1/plugins/fx-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Realtime VST FX qua SHM bridge (master chain): start session → WS
|
||||
// /ws/fx-realtime/{id} đẩy/kéo block audio; stop → tắt bridge.
|
||||
fxRealtimeStart: (payload) => apiRequest('/api/v1/plugins/fx-realtime/start', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
fxRealtimeStop: (payload) => apiRequest('/api/v1/plugins/fx-realtime/stop', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// 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) }),
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// sf-fx-realtime.js — AudioWorkletProcessor nối WebAudio → SHM FX engine.
|
||||
// process() gom 128-sample quantum thành block 256 stereo float32 (khớp
|
||||
// FXRT_BLOCK trong FxRealtimeIPC.h) → postMessage cho main thread gửi WS;
|
||||
// nhận block đã xử lý từ main thread → queue → phát ra output. Queue rỗng →
|
||||
// zero fill (WS chưa kịp round-trip).
|
||||
//
|
||||
// Latency ~2 block; backlog > 32 block (~190ms) → drop block cũ nhất. Cap cao
|
||||
// hấp thụ jitter main thread (rAF renderFrame nặng khi mở MASTERING PANEL) mà
|
||||
// không drop. ponytail: thay drop bằng adaptive stretch khi cần chất lượng cao.
|
||||
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.BLOCK = 256; // FXRT_BLOCK
|
||||
this.inL = new Float32Array(this.BLOCK);
|
||||
this.inR = new Float32Array(this.BLOCK);
|
||||
this.inCount = 0;
|
||||
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
|
||||
this.outPos = 0;
|
||||
this.port.onmessage = (e) => {
|
||||
const d = e.data;
|
||||
if (!d || d.type !== 'out' || !d.l || !d.r) return;
|
||||
this.outQueue.push({ l: d.l, r: d.r });
|
||||
while (this.outQueue.length > 32) this.outQueue.shift();
|
||||
};
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
const input = inputs[0];
|
||||
const output = outputs[0];
|
||||
const n = output[0] ? output[0].length : 128;
|
||||
const outL = output[0];
|
||||
const outR = output[1];
|
||||
|
||||
// Góp input thành block 256 → gửi main thread (transferable buffer).
|
||||
if (input && input[0]) {
|
||||
const inL = input[0];
|
||||
const inR = input[1] || input[0];
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (this.inCount < this.BLOCK) {
|
||||
this.inL[this.inCount] = inL[i];
|
||||
this.inR[this.inCount] = inR[i];
|
||||
}
|
||||
this.inCount++;
|
||||
if (this.inCount >= this.BLOCK) {
|
||||
const inter = new Float32Array(this.BLOCK * 2);
|
||||
for (let j = 0; j < this.BLOCK; j++) {
|
||||
inter[j * 2] = this.inL[j];
|
||||
inter[j * 2 + 1] = this.inR[j];
|
||||
}
|
||||
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
|
||||
this.inCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phát output từ queue; rỗng → zero.
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (this.outQueue.length === 0) {
|
||||
outL[i] = 0;
|
||||
if (outR) outR[i] = 0;
|
||||
continue;
|
||||
}
|
||||
const blk = this.outQueue[0];
|
||||
outL[i] = blk.l[this.outPos];
|
||||
if (outR) outR[i] = blk.r[this.outPos];
|
||||
this.outPos++;
|
||||
if (this.outPos >= this.BLOCK) {
|
||||
this.outQueue.shift();
|
||||
this.outPos = 0;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);
|
||||
Reference in New Issue
Block a user