feat(fx-rt): VST master chain realtime khi insert/bypass/reorder + VU meter đo sau VST node

- fxRtStart: so chainKey signature — insert/remove/bypass/reorder VST trong
  MASTER FX CHAIN giờ restart session SHM với chuỗi FX mới (trước đây
  early-return khi active → VST mới không bao giờ được áp dụng realtime)
- masterBus.splitter (leftAnalyser/rightAnalyser) chuyển sang sau fxRt.node
  khi realtime active → VU meter master strip + peak graph hiển thị audio
  data ĐÃ QUA VST; khôi phục output khi stop
- MasteringModal outPeak/outRms/LUFS đọc masterBus.analyser (post-VST) thay
  vì outputAnalyser (pre-VST)
This commit is contained in:
2026-08-21 15:39:23 +00:00
parent bb6658e03d
commit 1a8b210063
2 changed files with 104 additions and 22 deletions
+63 -8
View File
@@ -620,7 +620,11 @@ const masteringChainOn = () => !!(window.currentMasteringSettings && window.curr
// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải
// qua chain để đủ lớn). Chain OFF → theo ♪ maps như cũ.
const effMidiBypass = (track) => {
// GỠ override mastering-ON (17:00) — nút ♪ quyết định bypass (user rules).
// PWR master ON → MỌI track PHẢI qua mastering chain (fx_chain_architecture.md
// §1: track → summing → MASTER FX CHAIN → main out). Nút ♪ chỉ có hiệu lực
// khi chain TẮT (masterConnected=false) — khi bật, âm MIDI/SF bắt buộc qua
// chain (user requirement: âm phải qua chain để đủ lớn).
if (masteringChainOn()) return false;
// ƯU TIÊN track object (field midiBypass): track object phân biệt đúng
// context (main track vs section clone — id TRÙNG nhau); map keyed theo id
// gây collision (clone section ghi đè map của main track cùng id → MIDI
@@ -630,7 +634,10 @@ const effMidiBypass = (track) => {
return false;
};
const effAudioBypass = (track) => {
// GỠ override mastering-ON (17:00) — nút A quyết định bypass (user rules)
// PWR master ON → MỌI track PHẢI qua mastering chain (fx_chain_architecture.md
// §1). Nút A chỉ có hiệu lực khi chain TẮT — khi bật, audio-clip bắt buộc
// qua chain.
if (masteringChainOn()) return false;
if (track && track.audioBypass !== undefined) return !!track.audioBypass;
if (track && track.id && trackAudioBypassMap[track.id] !== undefined) return !!trackAudioBypassMap[track.id];
return false;
@@ -660,6 +667,10 @@ function createMasteringRoute(ctx, track, bus) {
// ⚠️ GỠ 17:00 — override nuốt nút A/♪ (log: audioBypass=true nhưng
// routeGain=1) — rules hiện tại: NÚT A/♪ quyết định bypass, không phụ thuộc
// mastering ON.
// ✅ KHÔI PHỤC override (fx_chain_architecture.md §1): khi PWR master bật,
// mọi luồng track (audio-clip + MIDI/SF) BẮT BUỘC qua MASTER FX CHAIN —
// nút A/♪ track chỉ có hiệu lực khi chain TẮT (bypass khi tắt).
if (masteringChainOn()) bypass = false;
const routeGain = ctx.createGain();
const dryGain = ctx.createGain();
const masterDest = bus ? bus.input : ctx.destination;
@@ -679,6 +690,9 @@ function createMasteringRoute(ctx, track, bus) {
// nút A. Section track default audioBypass=true — nếu route đọc audioBypass
// cho track MIDI → midi bị kéo vào dry (bị bypass) dù ♪ tắt.
function routeBypassFor(track) {
// PWR master ON → mọi track qua mastering chain (fx_chain_architecture.md §1);
// nút A/♪ chỉ có hiệu lực khi chain TẮT.
if (masteringChainOn()) { routeBypassFor._lastType = 'master-on'; return false; }
const hasC = track && ((track.clips && track.clips.length > 0) || !!track.buffer);
const hasM = track && track.midiItems && track.midiItems.length > 0;
if (hasM && !hasC) {
@@ -1234,6 +1248,7 @@ function initMasterBus(ctx) {
masteringActive: false,
dryInput,
dryOutput,
splitter,
// Analysers for metering
inputAnalyser,
@@ -1719,7 +1734,13 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) {
// worklet → analyser (sau master fader, trước metering/destination — khớp thứ
// tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường
// chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out.
const fxRt = { active: false, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null };
const fxRt = { active: false, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, chainKey: null };
// Signature của chain VST realtime — phát hiện chain ĐỔI (insert/remove/bypass/
// 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('|');
}
function fxRtChain() {
const ms = window.currentMasteringSettings;
if (!ms || !ms.masterConnected || ms.isBypassed) return [];
@@ -1727,12 +1748,16 @@ function fxRtChain() {
return (chain.length > 0 && !(ms && ms.vstBypass)) ? chain : [];
}
async function fxRtStart() {
const chain = fxRtChain();
const ck = fxRtChainKey(chain);
if (fxRt.active) {
// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
if (!fxRt.node || fxRt.node.context !== getAudioContext()) fxRtStop(true);
// Chain VST thay đổi (insert/remove/bypass/reorder trong MASTER FX CHAIN)
// → stop + start lại để bridge load đúng chuỗi FX mới.
else if (fxRt.chainKey !== ck) fxRtStop(true);
else return;
}
const chain = fxRtChain();
if (!chain.length) return;
const ctx = getAudioContext();
if (!ctx || !ctx.audioWorklet || !masterBus || !masterBus.output || !masterBus.analyser) return;
@@ -1786,8 +1811,16 @@ async function fxRtStart() {
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);
fxRt.chainKey = ck;
} catch (e) {
console.warn('[FX RT] setup fail:', e);
fxRtStop(true);
@@ -1802,6 +1835,10 @@ function fxRtStop(silent) {
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) {}
}
}
fxRt.node.port.onmessage = null;
fxRt.node = null;
@@ -1811,6 +1848,7 @@ function fxRtStop(silent) {
const sid = fxRt.sessionId;
fxRt.sessionId = null;
fxRt.active = false;
fxRt.chainKey = null;
if (sid) {
try { window.SonicAPI.fxRealtimeStop({ session_id: sid }).catch(() => {}); } catch (e) {}
}
@@ -13325,12 +13363,15 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
}
};
renderMeter(masterBus && masterBus.inputAnalyser, inMeterCanvasRef, 'inPeakText');
renderMeter(masterBus && masterBus.outputAnalyser, outMeterCanvasRef, 'outPeakText');
// OUT meter: đọc masterBus.analyser (SAU fxRt VST node khi realtime active)
// → peak/RMS/LUFS hiển thị audio data ĐÃ QUA chuỗi VST master chain
// (không phải outputAnalyser — node đó nằm TRƯỚC VST realtime).
renderMeter(masterBus && masterBus.analyser, outMeterCanvasRef, 'outPeakText');
// Loudness measurement (post-mastering output): RMS dBFS + LUFS approx
// (ITU-R BS.1770 mean-square blocks, ~400ms, without K-weighting — close
// enough to gauge perceived loudness of the processed master).
const outAn = masterBus && masterBus.outputAnalyser;
const outAn = masterBus && masterBus.analyser;
if (outAn) {
const outBuf = new Float32Array(outAn.fftSize);
outAn.getFloatTimeDomainData(outBuf);
@@ -18773,6 +18814,18 @@ const App = () => {
}
}
});
// Sub-nodes (section tabs, key '<parent>_sub_<id>'): PWR master ON →
// ép qua master chain; OFF → khôi phục theo nút A/♪ riêng của section
// (route._bypass lưu trạng thái gốc lúc tạo/toggle).
Object.keys(activeTrackNodesRef.current).forEach(_k => {
if (!_k.includes('_sub_')) return;
const _sn = activeTrackNodesRef.current[_k];
if (_sn && _sn.route && _sn.route.routeGain && _sn.route.dryGain) {
const _sab = masteringChainOn() ? false : !!_sn.route._bypass;
_sn.route.routeGain.gain.value = _sab ? 0 : 1;
_sn.route.dryGain.gain.value = _sab ? 1 : 0;
}
});
updateSfRouting();
} catch (e) {}
}
@@ -23766,9 +23819,11 @@ const App = () => {
// lúc tạo node → nút A phải bấm 2 lần; stop+play → node tái tạo → mất).
// RULES: sub-track có CLIPS → nút A (audioBypass); có MIDI → nút ♪ (midiBypass).
const subHasClips = (subTrack.clips && subTrack.clips.length > 0) || !!subTrack.buffer;
const subBypass = subHasClips
// PWR master ON → sub-track cũng bắt buộc qua master chain (nút A/♪ của
// section chỉ có hiệu lực khi chain TẮT).
const subBypass = masteringChainOn() ? false : (subHasClips
? (subTrack.audioBypass !== undefined ? !!subTrack.audioBypass : (trackAudioBypassMap[subTrack.id] !== undefined ? !!trackAudioBypassMap[subTrack.id] : false))
: (subTrack.midiBypass !== undefined ? !!subTrack.midiBypass : (trackMidiBypassMap[subTrack.id] !== undefined ? !!trackMidiBypassMap[subTrack.id] : false));
: (subTrack.midiBypass !== undefined ? !!subTrack.midiBypass : (trackMidiBypassMap[subTrack.id] !== undefined ? !!trackMidiBypassMap[subTrack.id] : false)));
const routeGain = context.createGain();
const dryGain = context.createGain();
routeGain.gain.value = subBypass ? 0 : 1;