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:
+63
-8
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user