Phase 4+5: WebAudio I/O shim + verify DAW checklist — live=export, order, fader, PDC, smoke
This commit is contained in:
@@ -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"):
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user