Phase 4+5: WebAudio I/O shim + verify DAW checklist — live=export, order, fader, PDC, smoke

This commit is contained in:
2026-08-22 21:02:52 +07:00
parent e5c957fc87
commit c3aba40ffe
9 changed files with 756 additions and 388 deletions
+2
View File
@@ -140,6 +140,8 @@ class FxRealtimeSession:
"type": s.get("type", ""),
"bypass": bool(s.get("bypass")),
}
if s.get("id"):
slot["id"] = s["id"]
if s.get("path"):
slot["path"] = s["path"]
if s.get("preset_b64"):
+5 -1
View File
@@ -304,7 +304,11 @@ def _merge_track_fx_chain(track):
if isinstance(slot, dict):
merged.append(slot)
if not track.get("vst_fx_bypass"):
merged += list(track.get("vst_fx_chain") or [])
seen = {s.get("path") for s in merged if s.get("path")}
for slot in (track.get("vst_fx_chain") or []):
if isinstance(slot, dict) and slot.get("path") in seen:
continue # đã có trong fx_chain (migration) — không nhân đôi
merged.append(slot)
return merged
# Param mapping master chain entry → C++ BuiltinFx (Phase 3.2). UI chain entry
+224 -302
View File
@@ -823,114 +823,9 @@ function applyMasteringSettings(s) {
if (sig === _lastMasteringSig) return;
_lastMasteringSig = sig;
const now = audioCtx.currentTime;
// Clamp every parameter so a stale/incomplete settings object can never push
// NaN or an extreme value into the biquad filters — that puts the master
// chain into a bad state and silences ALL audio (the "mất soundfont" symptom).
const clamp = (v, lo, hi) => {
const n = Number(v);
if (!isFinite(n)) return 0; // missing/NaN → neutral, never a filter-breaking value
return Math.min(hi, Math.max(lo, n));
};
// 1. EQ Settings
// cancelScheduledValues + a slower time constant keeps rapid slider drags
// from piling up automation events on the biquad filters (the trigger for
// Chromium's "BiquadFilterNode: state is bad").
// ⚠️ Guard NaN: setValueAtTime(NaN) trên biquad → Chromium "BiquadFilterNode:
// state is bad" + câm. Field settings undefined/NaN → mặc định 0.
const _g = (v, lo, hi) => (typeof v === 'number' && isFinite(v)) ? clamp(v, lo, hi) : 0;
masterBus.eqLowFilter.gain.cancelScheduledValues(now);
masterBus.eqLowFilter.gain.setValueAtTime(s.eqActive ? _g(s.eqLowGain, -24, 24) : 0, now);
masterBus.eqMid1Filter.gain.cancelScheduledValues(now);
masterBus.eqMid1Filter.gain.setValueAtTime(s.eqActive ? _g(s.eqMid1Gain, -24, 24) : 0, now);
masterBus.eqMid2Filter.gain.cancelScheduledValues(now);
masterBus.eqMid2Filter.gain.setValueAtTime(s.eqActive ? _g(s.eqMid2Gain, -24, 24) : 0, now);
masterBus.eqHighFilter.gain.cancelScheduledValues(now);
masterBus.eqHighFilter.gain.setValueAtTime(s.eqActive ? _g(s.eqHighGain, -24, 24) : 0, now);
// 2. Imager Settings (Mid/Side width per band — imager_spec.md)
// Width % semantics per the guide: 0% = MONO (S × 0), 100% = original
// (S × 1), 200% = double width (S × 2). The L/R crossfeed gains below are
// exactly equivalent to M/S scaling: L'=g1·L+g2·R, R'=g1·R+g2·L with
// g1=(w+100)/200, g2=(100−w)/200 → Mid (L+R)/2 untouched, Side (L−R)/2
// scaled by w/100. Bands: 1=20-100Hz, 2=100Hz-1kHz, 3=1k-6kHz, 4=6k-20kHz.
const updateImagerBand = (w, active, gainLL, gainRL, gainLR, gainRR) => {
const widthVal = (s.imagerActive && active) ? clamp(w, 0, 200) : 100;
const g1 = (widthVal + 100) / 200;
const g2 = (100 - widthVal) / 200;
gainLL.gain.setTargetAtTime(g1, now, 0.01);
gainRR.gain.setTargetAtTime(g1, now, 0.01);
gainRL.gain.setTargetAtTime(g2, now, 0.01);
gainLR.gain.setTargetAtTime(g2, now, 0.01);
};
updateImagerBand(s.w1, true, masterBus.gainLL1, masterBus.gainRL1, masterBus.gainLR1, masterBus.gainRR1);
updateImagerBand(s.w2, true, masterBus.gainLL2, masterBus.gainRL2, masterBus.gainLR2, masterBus.gainRR2);
updateImagerBand(s.w3, true, masterBus.gainLL3, masterBus.gainRL3, masterBus.gainLR3, masterBus.gainRR3);
updateImagerBand(s.w4, true, masterBus.gainLL4, masterBus.gainRL4, masterBus.gainLR4, masterBus.gainRR4);
// 3. Maximizer Settings
const boostLinear = (s.maximizerActive) ? Math.pow(10, clamp(s.maxGain, -60, 30) / 20) : 1.0;
masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear, now, 0.01);
// Soft Clipper (identity passthrough when off — never null curve).
// Unity-gain curve: bo gain an cua atan (slope ~2.1x cu cong +6 dB len
// maxGain -> tin hieu >~-12 dBFS bi hard-clip meo "be").
if (s.maximizerActive && s.maxSoftClip > 0) {
masterBus.maximizerSoftClipper.curve = makeSoftClipCurve(s.maxSoftClip);
} else {
masterBus.maximizerSoftClipper.curve = new Float32Array([-1, 1]);
}
// Upward Compressor
const upwardGainLinear = (s.maximizerActive && s.maxUpward > 0) ? (Math.pow(10, clamp(s.maxUpward, 0, 30) / 20) - 1.0) : 0.0;
masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear, now, 0.01);
// Limiter Threshold (WaveShaper ceiling — _setCeiling rebuild curve)
const ceilingVal = s.maximizerActive ? clamp(s.ceiling, -60, 0) : -0.1;
if (masterBus.maximizerCompressor._setCeiling) masterBus.maximizerCompressor._setCeiling(ceilingVal);
// 4. Bus Compressor module (mastering_expand.md §II.2)
if (masterBus.compNode) {
const compOn = !!s.compActive;
masterBus.compNode.threshold.setTargetAtTime(compOn ? clamp(s.compThreshold, -60, 0) : 0, now, 0.02);
masterBus.compNode.ratio.setTargetAtTime(compOn ? clamp(s.compRatio, 1, 20) : 1, now, 0.02);
masterBus.compMakeup.gain.setTargetAtTime(compOn ? Math.pow(10, clamp(s.compMakeup, 0, 12) / 20) : 1.0, now, 0.02);
}
// 5. Brickwall Limiter module (WaveShaper tanh — threshold = mức clip; OFF = identity)
if (masterBus.limNode) {
const limOn = !!s.limActive;
if (limOn) {
if (masterBus.limNode._setThreshold) masterBus.limNode._setThreshold(clamp(s.limThreshold, -24, 0));
} else {
try { masterBus.limNode.curve = new Float32Array([-1, 1]); } catch (e) {}
}
}
// 6. Harmonic Exciter module (dry/wet mix; dry stays 1.0 for subtle warmth)
if (masterBus.excWet) {
const excOn = !!s.excActive;
const wetAmt = excOn ? clamp(s.excDrive, 0, 100) / 100 : 0;
masterBus.excWet.gain.setTargetAtTime(wetAmt * 0.6, now, 0.02);
masterBus.excDry.gain.setTargetAtTime(1.0, now, 0.02);
}
// 7. Master Rebalance module (M/S gains via L/R crossfeed)
// L' = a·L + b·R, R' = b·L + a·R with a=(mid+side)/2, b=(mid−side)/2
if (masterBus.gLLr) {
const rebOn = !!s.rebalActive;
const midLin = rebOn ? Math.pow(10, clamp(s.rebalMid, -24, 24) / 20) : 1.0;
const sideLin = rebOn ? Math.pow(10, clamp(s.rebalSide, -24, 24) / 20) : 1.0;
const a = (midLin + sideLin) / 2;
const b = (midLin - sideLin) / 2;
masterBus.gLLr.gain.setTargetAtTime(a, now, 0.01);
masterBus.gRRr.gain.setTargetAtTime(a, now, 0.01);
masterBus.gRLr.gain.setTargetAtTime(b, now, 0.01);
masterBus.gLRr.gain.setTargetAtTime(b, now, 0.01);
}
// Phase 4.5: master chain DSP chạy trong fx_vst_bridge — WebAudio chỉ
// I/O. Param đổi → đẩy TOÀN chain về engine qua SET_PARAM.
if (fxRt && fxRt.ws && fxRt.ws.readyState === 1) { try { fxRtPushAllParams(); } catch (e) {} }
}
function initMasterBus(ctx) {
@@ -1255,19 +1150,6 @@ let _lastMasteringActive = null;
let _lastMasteringSig = null;
let _lastChainSig = null;
// Module input/output boundary nodes for dynamic chain re-routing
// (mastering_expand.md §II.3 — rebuildAudioGraph). Each module's INTERNAL
// wiring is fixed; only the boundaries get re-connected per chain order.
const MASTER_MODULE_IO = {
eq: { input: 'eqLowFilter', output: 'eqHighFilter' },
imager: { input: 'imagerInput', output: 'imagerOutput' },
maximizer: { input: 'maximizerBoostGain', output: 'maximizerCompressor' },
compressor: { input: 'compInput', output: 'compOutput' },
limiter: { input: 'limInput', output: 'limOutput' },
exciter: { input: 'excInput', output: 'excOutput' },
rebalance: { input: 'rebalInput', output: 'rebalOutput' }
};
function chainSignature(chainArray) {
return (chainArray || []).map(m => (m.type || '') + (m.active ? '1' : '0')).join(',');
}
@@ -1671,13 +1553,52 @@ const fxRt = { active: false, sessionId: null, ws: null, node: null, inQueue: []
// reorder VST) → restart session với chain mới (fxRtStart early-return khi
// active, nếu không so chainKey thì VST mới insert không bao giờ được áp).
function fxRtChainKey(chain) {
return (chain || []).map(s => (s.path || '') + '#' + ((s.preset_b64 || '').slice(0, 16))).join('|');
return (chain || []).map(s => (s.type || '') + ':' + (s.type === 'vst3' ? (s.path || '') + '#' + ((s.preset_b64 || '').slice(0, 16)) : (s.type || ''))).join('|');
}
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 : [];
// Chain hợp nhất (builtin + vst3 theo UI order) — mọi slot active gửi bridge.
return (ms.chain || []).filter(s => s && s.active !== false && (s.type !== 'vst3' || (!ms.vstBypass && s.path && !s.bypass)));
}
// Param master chain entry → C++ BuiltinFx key (mirror Python _BUILTIN_PARAM_MAP).
const FX_RT_PARAM_MAP = {
eq: [['g1', 'eqLowGain'], ['g2', 'eqMid1Gain'], ['g3', 'eqMid2Gain'], ['g4', 'eqHighGain']],
compressor: [['threshold', 'compThreshold'], ['ratio', 'compRatio'], ['makeup', 'compMakeup']],
limiter: [['ceiling', 'limThreshold']],
exciter: [['drive', 'excDrive']],
rebalance: [['mid', 'rebalMid'], ['side', 'rebalSide']],
imager: [['w1', 'w1'], ['w2', 'w2'], ['w3', 'w3'], ['w4', 'w4']],
maximizer: [['boost_db', 'maxGain'], ['soft_clip', 'maxSoftClip'], ['upward', 'maxUpward'], ['ceiling_db', 'ceiling']]
};
function fxRtEntryParams(entry, ms) {
const out = [];
if (!entry || entry.type === 'vst3') return out;
const src = ms || (window.currentMasteringSettings || {});
const push = (k, v) => { const n = Number(v); if (isFinite(n)) out.push({ key: k, value: n }); };
const map = FX_RT_PARAM_MAP[entry.type] || [];
map.forEach(([cppKey, jsKey]) => {
if (entry.params && entry.params[cppKey] !== undefined) push(cppKey, entry.params[cppKey]);
else if (entry.params && entry.params[jsKey] !== undefined) push(jsKey, entry.params[jsKey]);
else if (src[jsKey] !== undefined) push(cppKey, src[jsKey]);
});
if (entry.type === 'eqpro') {
if (entry.params && entry.params.amount !== undefined) push('amount', entry.params.amount);
const bands = (entry.params && entry.params.bands) || [];
bands.forEach((b, i) => {
['freq', 'gain', 'q'].forEach(k => { if (b && b[k] !== undefined) push('band' + i + '_' + k, b[k]); });
});
}
return out;
}
function fxRtPushAllParams() {
if (!fxRt || !fxRt.ws || fxRt.ws.readyState !== 1) return;
const chain = fxRtChain();
chain.forEach((s, i) => {
fxRtEntryParams(s, null).forEach(p => {
try { fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot: i, key: p.key, value: p.value })); } catch (e) {}
});
});
}
async function fxRtStart() {
const chain = fxRtChain();
@@ -1693,7 +1614,9 @@ async function fxRtStart() {
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 }));
const chainPayload = chain.map(s => s.type === 'vst3'
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: false });
let st;
try {
st = await window.SonicAPI.fxRealtimeStart({ fx_chain: chainPayload, sample_rate: ctx.sampleRate || 44100 });
@@ -1714,7 +1637,7 @@ async function fxRtStart() {
const ws = new WebSocket(wsUrl);
fxRt.ws = ws;
ws.binaryType = 'arraybuffer';
ws.onopen = () => { fxRt.active = true; };
ws.onopen = () => { fxRt.active = true; try { fxRtPushAllParams(); } catch (e) {} };
ws.onmessage = (ev) => {
if (!(ev.data instanceof ArrayBuffer) || !fxRt.node) return;
const f = new Float32Array(ev.data);
@@ -1728,7 +1651,12 @@ async function fxRtStart() {
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 (!d || !fxRt.ws) return;
if (d.type === 'set_param') {
try { fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot: d.slot, key: d.key, value: d.value })); } catch (e) {}
return;
}
if (d.type !== 'in') return;
if (fxRt.inQueue.length > 64) fxRt.inQueue.shift();
fxRt.inQueue.push(d.data);
};
@@ -1743,16 +1671,12 @@ async function fxRtStart() {
const d = fxRt.inQueue.shift();
try { ws.send(d); } catch (e) { fxRt.inQueue.unshift(d); }
}, blockMs);
try { masterBus.output.disconnect(masterBus.analyser); } catch (e) {}
// VU meter + peak graph (leftAnalyser/rightAnalyser qua splitter) đo âm
// SAU VST realtime: ngắt splitter khỏi output, nối vào fxRt.node để meter
// hiển thị audio data đã qua chuỗi VST (user requirement — peak/VU realtime).
if (masterBus.splitter) {
try { masterBus.output.disconnect(masterBus.splitter); } catch (e) {}
}
masterBus.output.connect(fxRt.node);
fxRt.node.connect(masterBus.analyser);
if (masterBus.splitter) fxRt.node.connect(masterBus.splitter);
// Phase 4.2: fader SAU worklet — worklet chèn giữa outputAnalyser (chain
// output) và output (fader/destination). output → analyser/splitter giữ
// nguyên (meter đo âm SAU fader).
try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}
masterBus.outputAnalyser.connect(fxRt.node);
fxRt.node.connect(masterBus.output);
fxRt.chainKey = ck;
} catch (e) {
console.warn('[FX RT] setup fail:', e);
@@ -1766,12 +1690,9 @@ function fxRtStop(silent) {
try {
if (fxRt.node) {
fxRt.node.disconnect();
if (masterBus && masterBus.output && masterBus.analyser) {
try { masterBus.output.connect(masterBus.analyser); } catch (e) {}
// Khôi phục VU meter splitter về output (pre-VST) khi tắt realtime.
if (masterBus.splitter) {
try { masterBus.output.connect(masterBus.splitter); } catch (e) {}
}
if (masterBus && masterBus.outputAnalyser && masterBus.output) {
try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}
try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {}
}
fxRt.node.port.onmessage = null;
fxRt.node = null;
@@ -1814,8 +1735,15 @@ function fxRtGuiPushShm() {
}
function fxRtTrackChain(track) {
if (!track) return [];
const chain = activeVstChain(track.vstFxChain || []);
return (chain.length > 0 && !track.vstFxBypass) ? chain : [];
let chain = (track.fxChain || []).filter(s => s && s.active !== false && (s.type !== 'vst3' || (!track.vstFxBypass && s.path && !s.bypass)));
// Legacy: project cũ chỉ có vstFxChain riêng — append sau builtin.
if (!chain.some(s => s && s.type === 'vst3') && !track.vstFxBypass) {
chain = chain.concat(activeVstChain(track.vstFxChain || []));
}
return chain;
}
function fxRtTrackChainKey(chain) {
return (chain || []).map(s => (s.type || '') + ':' + (s.type === 'vst3' ? (s.path || '') + '#' + ((s.preset_b64 || '').slice(0, 16)) : (s.type || ''))).join('|');
}
function fxRtTrackPaths(track) {
const paths = [];
@@ -1838,65 +1766,16 @@ const TRACK_EQ_PRESETS = {
// between inputAnalyser (chain input) and outputAnalyser (chain output).
function rebuildMasteringGraph(activate, chainArray) {
if (!masterBus) return;
const active = !!(activate);
try {
// 1. Disconnect all module boundary outputs (breaks static + previous dynamic links)
// Phase 4.2: master chain xử lý TOÀN bộ trong fx_vst_bridge — WebAudio
// master là pass-through (inputAnalyser → outputAnalyser).
masterBus.inputAnalyser.disconnect();
masterBus.eqHighFilter.disconnect();
masterBus.imagerOutput.disconnect();
masterBus.maximizerCompressor.disconnect();
masterBus.compOutput.disconnect();
masterBus.limOutput.disconnect();
masterBus.excOutput.disconnect();
masterBus.rebalOutput.disconnect();
if (!active) {
// 2a. No mastering → straight chain: inputAnalyser → outputAnalyser
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.masteringActive = false;
return;
}
// 2b. Filter active modules
const activeMods = (chainArray || []).filter(m => m && m.active);
if (activeMods.length === 0) {
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.masteringActive = true;
return;
}
// 3. Wire in series: Input → mod[0].input → mod[0].output → mod[1].input → … → Output
let prev = masterBus.inputAnalyser;
// Dynamic modules (eqpro): rebuilt on every graph rebuild; instances kept
// in masterBus.eqProInstances so the Mastering panel can patch them live.
const eqProStore = masterBus.eqProInstances = masterBus.eqProInstances || {};
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 === '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') {
const m = createEqProModule(getAudioContext(), mod.params || {});
eqProStore[mod.id] = m;
prev.connect(m.input);
prev = m.output;
} else {
const io = MASTER_MODULE_IO[mod.type] || MASTER_MODULE_IO.eq;
const inNode = masterBus[io.input];
if (inNode) prev.connect(inNode);
prev = masterBus[io.output] || prev;
}
});
prev.connect(masterBus.outputAnalyser);
masterBus.masteringActive = true;
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
masterBus.masteringActive = !!(activate);
} catch (e) {
console.warn('rebuildMasteringGraph error:', e);
}
}
function toggleMasteringOnMaster(activate, isBypassed) {
if (!masterBus) return;
@@ -11184,8 +11063,19 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
soundfont_program: t.soundfont_program !== undefined ? t.soundfont_program : (t.synth_engine ? t.synth_engine.soundfont_program : null),
synth_engine: t.synth_engine || undefined,
midi_channel: t.midiChannel !== undefined ? t.midiChannel : null,
fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),
vst_fx_chain: (t.vstFxChain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '', bypass: !!s.bypass })),
fx_chain: (t.fxChain || []).map(m => {
if (typeof m === 'string') return { type: m, active: true };
const o = { type: m.type, active: m.active !== false };
if (m.type === 'vst3') {
o.path = m.path || ''; o.name = m.name || ''; o.preset_b64 = _capturedPresetByPath[m.path] || m.preset_b64 || ''; o.bypass = !!m.bypass;
} else if (m.params && Object.keys(m.params).length) o.params = m.params;
return o;
}),
vst_fx_chain: (() => {
const chainVst = (t.fxChain || []).filter(m => m && m.type === 'vst3');
const vstSrc = chainVst.length ? chainVst : (t.vstFxChain || []);
return vstSrc.map(s => ({ type: 'vst3', path: s.path || '', name: s.name || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '', bypass: !!s.bypass }));
})(),
vst_fx_bypass: !!t.vstFxBypass,
vst_gui_open_path: (t.id === _persistFxGuiTrackId && _persistFxGuiOpen && _persistFxGuiOpen.path) ? _persistFxGuiOpen.path : (t.vstGuiOpenPath || null),
vst_gui_open_name: (t.id === _persistFxGuiTrackId && _persistFxGuiOpen && _persistFxGuiOpen.name) ? _persistFxGuiOpen.name : (t.vstGuiOpenName || null),
@@ -11288,8 +11178,17 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore, visite
soundfont_bank: t.soundfont_bank !== null ? t.soundfont_bank : undefined,
soundfont_program: t.soundfont_program !== null ? t.soundfont_program : undefined,
synth_engine: t.synth_engine || undefined,
fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),
vstFxChain: (t.vst_fx_chain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
fxChain: (() => {
const parsed = (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : ({ type: m.type || m, active: m.active !== false, ...(m.params ? { params: m.params } : {}), ...(m.type === 'vst3' ? { path: m.path || '', name: m.name || '', preset_b64: m.preset_b64 || '', bypass: !!m.bypass } : {}) }));
const seen = new Set(parsed.filter(m => m.type === 'vst3').map(m => m.path));
(t.vst_fx_chain || []).forEach(s => { if (s && s.path && !seen.has(s.path)) { parsed.push({ type: 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass, active: true }); seen.add(s.path); } });
return parsed;
})(),
vstFxChain: (() => {
const fromChain = (t.fx_chain || []).filter(m => m && m.type === 'vst3');
const src = fromChain.length ? fromChain : (t.vst_fx_chain || []);
return src.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass }));
})(),
vstFxBypass: t.vst_fx_bypass !== undefined ? !!t.vst_fx_bypass : false,
vstGuiOpenPath: t.vst_gui_open_path || null,
vstGuiOpenName: t.vst_gui_open_name || null,
@@ -11509,13 +11408,26 @@ const deserializeProjectFromSchema = (schemaObj) => {
tracks: restoredTracks,
sessionTabs: restoredSessionTabs,
subTabs: restoredSubTabs,
masteringSettings: {
..._migratedMastering,
vstFxChain: _masterFx.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
vstBypass: !!_masterBus.bypass,
gui_open_path: (schemaObj.mastering_settings && schemaObj.mastering_settings.gui_open_path) ? schemaObj.mastering_settings.gui_open_path : null,
gui_open_name: (schemaObj.mastering_settings && schemaObj.mastering_settings.gui_open_name) ? schemaObj.mastering_settings.gui_open_name : null
},
masteringSettings: (() => {
const migrated = { ..._migratedMastering };
const _masterVstSources = [];
(migrated.vstFxChain || []).forEach(s => { if (s && s.path && !_masterVstSources.some(x => x.path === s.path)) _masterVstSources.push(s); });
_masterFx.forEach(s => { if (s && s.path && !_masterVstSources.some(x => x.path === s.path)) _masterVstSources.push(s); });
const _finalChain = [...(migrated.chain || [])];
_masterVstSources.forEach(s => {
if (!_finalChain.some(m => m.type === 'vst3' && m.path === s.path)) {
_finalChain.push({ id: 'mod_vst_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8), type: 'vst3', path: s.path, name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass, active: true });
}
});
return {
...migrated,
chain: _finalChain,
vstFxChain: _finalChain.filter(m => m.type === 'vst3').map(s => ({ type: 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
vstBypass: !!_masterBus.bypass,
gui_open_path: (schemaObj.mastering_settings && schemaObj.mastering_settings.gui_open_path) ? schemaObj.mastering_settings.gui_open_path : null,
gui_open_name: (schemaObj.mastering_settings && schemaObj.mastering_settings.gui_open_name) ? schemaObj.mastering_settings.gui_open_name : null
};
})(),
masterVolume: (typeof _masterBus.volume_db === 'number' && isFinite(_masterBus.volume_db)) ? _masterBus.volume_db : 0,
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
};
@@ -12300,14 +12212,16 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const chain = track.fxChain || [];
// ── VST FX chain (native_bridge --render-fx, áp dụng khi Export/Offline render) ──
const vstChain = (track.vstFxChain || []);
// ── VST FX chain (native_bridge --render-fx) — tham chiếu từ unified chain ──
const vstChain = chain.filter(m => m && m.type === 'vst3');
// Unified chain strip: builtin DSP + VST FX chung 1 chain (kéo đổi thứ tự giữa cả 2 loại).
const unifiedChain = [
...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 }); if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
const unifiedChain = chain.map((m, i) => ({ kind: m && m.type === 'vst3' ? 'vst' : 'builtin', m, i }));
const setVstChain = (next) => {
const builtin = chain.filter(m => !m || m.type !== 'vst3');
const merged = [...builtin, ...next.map(s => ({ type: 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass }))];
if (onUpdateTrack) onUpdateTrack(track.id, { fxChain: merged, vstFxChain: next, vstFxBypass: vstBypass });
if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id, { fxChain: merged, 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) => { 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); };
@@ -12332,7 +12246,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
// Reopen the saved GUI when the rack is mounted after a project restore.
React.useEffect(() => {
if (!track || !track.vstGuiOpenPath || fxGuiOpen) return;
const s = (track.vstFxChain || []).find(x => x.path === track.vstGuiOpenPath);
const s = (track.fxChain || []).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, shm: fxRtTrackShmName(track && track.id) }).then(r => {
if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url });
@@ -12627,7 +12541,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
draggable
onDragStart={e => { dragChainIndexRef.current = ui; e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== ui) { const next = [...unifiedChain]; const mv = next.splice(from, 1)[0]; next.splice(ui, 0, mv); const nb = next.filter(x => x.kind === 'builtin').map(x => x.m); const nv = next.filter(x => x.kind === 'vst').map(x => x.s); setChain(nb); setVstChain(nv); if (mv.kind === 'vst') setActiveVstIdx(nv.indexOf(mv.s)); } dragChainIndexRef.current = null; }}
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== ui) { const next = [...unifiedChain]; const mv = next.splice(from, 1)[0]; next.splice(ui, 0, mv); const nb = next.filter(x => x.kind === 'builtin').map(x => x.m); const nv = next.filter(x => x.kind === 'vst').map(x => x.m); setChain(nb); setVstChain(nv); if (mv.kind === 'vst') setActiveVstIdx(nv.indexOf(mv.m)); } dragChainIndexRef.current = null; }}
onClick={() => { closeFxGui(); setActiveType(m.type); }}
className={`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeType === m.type ? 'border-2 border-cyan-400 bg-slate-800' : 'bg-slate-900 border border-slate-800 hover:border-slate-600'}`}>
<div className="flex items-center gap-2 min-w-0">
@@ -12645,17 +12559,17 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
</div>
);
}
const s = u.s;
const s = u.m;
return (
<div key={`v${u.i}`}
draggable
onDragStart={e => { dragChainIndexRef.current = ui; e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== ui) { const next = [...unifiedChain]; const mv = next.splice(from, 1)[0]; next.splice(ui, 0, mv); const nb = next.filter(x => x.kind === 'builtin').map(x => x.m); const nv = next.filter(x => x.kind === 'vst').map(x => x.s); setChain(nb); setVstChain(nv); if (mv.kind === 'vst') setActiveVstIdx(nv.indexOf(mv.s)); } dragChainIndexRef.current = null; }}
onClick={() => { closeFxGui(); setActiveVstIdx(u.i); }}
className={`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeVstIdx === u.i ? 'border-2 border-teal-400 bg-slate-800' : 'bg-slate-900 border border-teal-900/70 hover:border-teal-600'}`}>
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== ui) { const next = [...unifiedChain]; const mv = next.splice(from, 1)[0]; next.splice(ui, 0, mv); const nb = next.filter(x => x.kind === 'builtin').map(x => x.m); const nv = next.filter(x => x.kind === 'vst').map(x => x.m); setChain(nb); setVstChain(nv); if (mv.kind === 'vst') setActiveVstIdx(nv.indexOf(mv.m)); } dragChainIndexRef.current = null; }}
onClick={() => { closeFxGui(); setActiveVstIdx(vstChain.indexOf(s)); }}
className={`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeVstIdx === vstChain.indexOf(s) ? 'border-2 border-teal-400 bg-slate-800' : 'bg-slate-900 border border-teal-900/70 hover:border-teal-600'}`}>
<div className="flex items-center gap-2 min-w-0">
<button onClick={e => { e.stopPropagation(); toggleVst(u.i); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{ backgroundColor: !s.bypass ? '#2dd4bf' : '#334155', color: !s.bypass ? '#0f172a' : '#94a3b8' }}>
<button onClick={e => { e.stopPropagation(); toggleVst(vstChain.indexOf(s)); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{ backgroundColor: !s.bypass ? '#2dd4bf' : '#334155', color: !s.bypass ? '#0f172a' : '#94a3b8' }}>
<i data-lucide="power" className="w-2.5 h-2.5"></i>
</button>
<div className="min-w-0">
@@ -12663,7 +12577,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
<div className="text-[9px] font-mono truncate text-teal-500/80">{u.i + 1}. {s.preset_b64 ? '✓ preset' : 'no preset'}</div>
</div>
</div>
<button onClick={e => { e.stopPropagation(); removeVst(u.i); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0" title="Xóa slot">
<button onClick={e => { e.stopPropagation(); removeVst(vstChain.indexOf(s)); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0" title="Xóa slot">
<i data-lucide="x" className="w-3 h-3"></i>
</button>
</div>
@@ -12923,7 +12837,20 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, ozState && ozState.gui_open_path]);
const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));
const setMasterVstChain = (next) => setOzState(prev => {
const chain = prev.chain || [];
const out = [];
let vi = 0;
const mk = (s, i) => ({ id: 'mod_vst_' + Date.now() + '_' + i + '_' + Math.random().toString(36).slice(2, 8), type: 'vst3', path: s.path, name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass, active: true });
chain.forEach(m => {
if (m.type === 'vst3') {
const s = next[vi];
if (s) { out.push(mk(s, vi)); vi++; }
} else out.push(m);
});
for (; vi < next.length; vi++) out.push(mk(next[vi], vi));
return { ...prev, vstFxChain: next, chain: out };
});
// LIVE master FX xử lý bởi fxRtStart() (SHM loop qua fx_vst_bridge
// --realtime-fx) — KHÔNG push chain xuống native bridge (setFxChain):
// nếu push, MIDI/VSTi instrument bị xử lý 2 lần (native bridge + SHM loop)
@@ -13507,7 +13434,8 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
const chain = [...prev.chain];
const moved = chain.splice(fromIdx, 1)[0];
chain.splice(toIdx, 0, moved);
return { ...prev, chain };
const vstFxChain = chain.filter(m => m && m.type === 'vst3').map(s => ({ type: 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass }));
return { ...prev, chain, vstFxChain };
});
};
const addModuleToChain = (type) => {
@@ -13568,21 +13496,8 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
const applyEQPreset = (presetKey) => {
const preset = EQ_PRESET_LIBRARY[presetKey];
if (!preset) return;
const bus = masterBus;
if (bus && bus.eqLowFilter) {
const now = audioCtx ? audioCtx.currentTime : 0;
const filters = [bus.eqLowFilter, bus.eqMid1Filter, bus.eqMid2Filter, bus.eqHighFilter];
preset.bands.forEach((bd, i) => {
const f = filters[i];
if (!f) return;
try {
// setValueAtTime (không automation) — tránh "BiquadFilterNode: state is bad"
f.frequency.setValueAtTime(bd.freq, now);
f.gain.setValueAtTime(bd.gain, now);
f.Q.setValueAtTime(bd.q, now);
} catch (e) {}
});
}
// Phase 4.5: EQ DSP chạy trong bridge — preset chỉ sync state; engine
// nhận qua fxRtPushAllParams() ở applyMasteringSettings.
// Sync UI knobs + canvas: [eqLowGain, eqMid1Gain, eqMid2Gain, eqHighGain]
setOzState(prev => ({
...prev,
@@ -13771,9 +13686,24 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
track={null}
params={cm.params || {}}
onChange={(next) => updateChainEntryParams(cm.id, next)}
getModule={() => masterBus && masterBus.eqProInstances ? masterBus.eqProInstances[cm.id] : null}
spectrumModules={() => { const m = masterBus && masterBus.eqProInstances ? masterBus.eqProInstances[cm.id] : null; return m ? [m] : []; }}
applyTo={(fn) => { const m = masterBus && masterBus.eqProInstances ? masterBus.eqProInstances[cm.id] : null; if (m) fn(m); }}
getModule={() => null}
spectrumModules={() => []}
applyTo={(fn) => {
const chain = fxRtChain();
const slot = chain.findIndex(m => m && m.type === 'eqpro');
if (slot < 0) return;
fn({
setBand: (idx, patch) => {
Object.keys(patch || {}).forEach(k => { if (fxRt && fxRt.ws && fxRt.ws.readyState === 1) fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot, key: 'band' + idx + '_' + k, value: patch[k] })); });
},
setAmount: (v) => { if (fxRt && fxRt.ws && fxRt.ws.readyState === 1) fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot, key: 'amount', value: v })); },
syncBands: (bands) => {
(bands || []).forEach((b, i) => {
['freq', 'gain', 'q'].forEach(k => { if (fxRt && fxRt.ws && fxRt.ws.readyState === 1) fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot, key: 'band' + i + '_' + k, value: b[k] })); });
});
}
});
}}
/>;
})()}
</div>
@@ -23028,15 +22958,8 @@ const App = () => {
// FX Power (nút PWR): ON = mọi items con (midi/section/audioclip) qua FX
// Rack Panel chain; OFF = bỏ toàn bộ track FX (fxChain + chorus/reverb).
const fxEnabled = track.fxActive !== false;
const chainMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
}).filter(Boolean) : [];
let fxChainTail = fxEntry;
chainMods.forEach(mod => {
fxChainTail.connect(mod.input);
fxChainTail = mod.output;
});
fxChainTail.connect(fxLegacyIn);
// Phase 4.3: track builtin DSP chạy trong bridge — WebAudio pass-through.
fxEntry.connect(fxLegacyIn);
if (fxEnabled && track.fxType === 'chorus') {
const fxInput = context.createGain();
fxLegacyIn.connect(fxInput);
@@ -23090,18 +23013,10 @@ const App = () => {
sfRouteGain.connect(masterBus.input);
sfDryGain.connect(masterBus.dryInput);
sfOut.connect(sfAnalyser);
const sfModsBuilt = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
}).filter(Boolean) : [];
sfMods = sfModsBuilt;
let sfTail = sfEntry;
sfMods.forEach(mod => {
sfTail.connect(mod.input);
sfTail = mod.output;
});
sfTail.connect(sfOut);
sfMods = [];
sfEntry.connect(sfOut);
}
node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR, sfEntry, sfOut, sfPan, sfAnalyser, sfRouteGain, sfDryGain, fxMods: chainMods, sfMods };
node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR, sfEntry, sfOut, sfPan, sfAnalyser, sfRouteGain, sfDryGain, fxMods: [], sfMods: [] };
// Realtime mute/solo: apply the track's current mute/solo/volume state to
// the fresh node so items of muted/soloed tracks start correctly.
const trackList = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : [track];
@@ -23132,30 +23047,13 @@ const App = () => {
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 => {
try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
}).filter(Boolean) : [];
let tail = node.fxEntry;
chainMods.forEach(mod => {
tail.connect(mod.input);
tail = mod.output;
});
tail.connect(node.fxLegacyIn);
node.fxMods = chainMods;
node.fxEntry.connect(node.fxLegacyIn);
node.fxMods = [];
// Rebuild the independent SF chain too (same modules, separate instances)
if (node.sfEntry && node.sfOut) {
node.sfEntry.disconnect();
const sfMods = fxEnabled ? (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
}).filter(Boolean) : [];
node.sfMods = sfMods;
let sfTail = node.sfEntry;
sfMods.forEach(mod => {
sfTail.connect(mod.input);
sfTail = mod.output;
});
sfTail.connect(node.sfOut);
node.sfEntry.connect(node.sfOut);
node.sfMods = [];
}
if (node.sfPan && getAudioContext()) {
node.sfPan.pan.setTargetAtTime((track.pan ?? 0) / 100, getAudioContext().currentTime, 0.02);
@@ -23186,7 +23084,9 @@ const App = () => {
}
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 }));
const chainPayload = chain.map(s => s.type === 'vst3'
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: false }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: false });
let st;
try {
st = await window.SonicAPI.fxRealtimeStart({ fx_chain: chainPayload, sample_rate: ctx.sampleRate || 44100 });
@@ -23194,7 +23094,7 @@ const App = () => {
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 };
const entry = { sessionId: st.session_id, ws: null, node: null, ready: false, path, spliced: null, chainKey: fxRtTrackChainKey(chain), paramsBySlot: chain.map(s => fxRtEntryParams(s, null)), inQueue: [], pumpTimer: null };
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
@@ -23207,7 +23107,13 @@ const App = () => {
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.onopen = () => {
entry.ready = true;
fxRtTrackSplice(trackId, path);
try {
(entry.paramsBySlot || []).forEach((pl, slot) => pl.forEach(p => ws.send(JSON.stringify({ cmd: 'set_param', slot, key: p.key, value: p.value }))));
} catch (e) {}
};
ws.onmessage = (ev) => {
if (!(ev.data instanceof ArrayBuffer) || !entry.node) return;
const f = new Float32Array(ev.data);
@@ -23221,7 +23127,12 @@ const App = () => {
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 (!d || !entry.ws) return;
if (d.type === 'set_param') {
try { entry.ws.send(JSON.stringify({ cmd: 'set_param', slot: d.slot, key: d.key, value: d.value })); } catch (e) {}
return;
}
if (d.type !== 'in') return;
if (entry.inQueue.length > 64) entry.inQueue.shift();
entry.inQueue.push(d.data);
};
@@ -23251,9 +23162,7 @@ const App = () => {
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 tail = path === 'audio' ? node.fxEntry : node.sfEntry;
const dest = path === 'audio' ? node.fxLegacyIn : node.sfOut;
if (!tail || !dest) return;
try {
@@ -23285,6 +23194,7 @@ const App = () => {
const fxRtTrackReconcile = (trackId, node, track) => {
try {
const chain = fxRtTrackChain(track);
const chainKey = fxRtTrackChainKey(chain);
const wantPaths = chain.length ? fxRtTrackPaths(track) : [];
const prefix = trackId + ':';
Object.keys(fxRtTracks).forEach(k => {
@@ -23297,6 +23207,7 @@ const App = () => {
const key = trackId + ':' + p;
const entry = fxRtTracks[key];
if (!entry) fxRtTrackStart(trackId, track, p);
else if (entry.chainKey !== chainKey) { fxRtTrackStop(trackId, p, true); 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);
@@ -23566,19 +23477,30 @@ const App = () => {
// UI helper: fetch a live module instance (e.g. 'eqpro') from the active track
// node so interactive panels can drive its DSP params in realtime.
window.__getTrackFxModule = (trackId, type) => {
const node = activeTrackNodesRef.current[trackId];
if (!node) return null;
const found = (node.fxMods || []).find(m => m && m.type === type);
return found || null;
if (type !== 'eqpro') return null;
const findSlot = () => {
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
const track = list.find(t => t.id === trackId);
return track ? fxRtTrackChain(track).findIndex(m => m && m.type === 'eqpro') : -1;
};
const sendAll = (fn) => {
const slot = findSlot();
if (slot < 0) return;
Object.keys(fxRtTracks).forEach(k => {
if (!k.startsWith(trackId + ':')) return;
const e = fxRtTracks[k];
if (e && e.ws && e.ws.readyState === 1) fn(e.ws, slot);
});
};
return {
setBand: (idx, patch) => sendAll((ws, slot) => Object.keys(patch || {}).forEach(k => { try { ws.send(JSON.stringify({ cmd: 'set_param', slot, key: 'band' + idx + '_' + k, value: patch[k] })); } catch (e) {} })),
setAmount: (v) => sendAll((ws, slot) => { try { ws.send(JSON.stringify({ cmd: 'set_param', slot, key: 'amount', value: v })); } catch (e) {} }),
syncBands: (bands) => sendAll((ws, slot) => (bands || []).forEach((b, i) => ['freq', 'gain', 'q'].forEach(k => { try { ws.send(JSON.stringify({ cmd: 'set_param', slot, key: 'band' + i + '_' + k, value: b[k] })); } catch (e) {} })))
};
};
// Same for the soundfont (MIDI) module instances — lets the EQ PRO panel drive
// BOTH the audio-clip chain and the SF chain in realtime.
window.__getTrackSfFxModule = (trackId, type) => {
const node = activeTrackNodesRef.current[trackId];
if (!node) return null;
const found = (node.sfMods || []).find(m => m && m.type === type);
return found || null;
};
window.__getTrackSfFxModule = (trackId, type) => window.__getTrackFxModule(trackId, type);
const getOrCreateSubTrackNode = (track, subTrack, context) => {
if (!track || !subTrack) return null;
const subKey = track.id + '_sub_' + subTrack.id;
File diff suppressed because one or more lines are too long
+1
View File
@@ -18,6 +18,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.outPos = 0;
this.port.onmessage = (e) => {
const d = e.data;
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
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();