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;
|
||||
|
||||
@@ -103,13 +103,18 @@ const trackMasteringBypassMap={};const trackAudioBypassMap={};const trackMidiByp
|
||||
const masteringChainOn=()=>!!(window.currentMasteringSettings&&window.currentMasteringSettings.masterConnected&&!window.currentMasteringSettings.isBypassed);// ♪ bypass hiệu lực CHỈ khi mastering chain TẮT — khi chain ON, MỌI track
|
||||
// (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).
|
||||
// ƯU TIÊN track object (field midiBypass): track object phân biệt đúng
|
||||
const effMidiBypass=track=>{// 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
|
||||
// section bị route theo ♪ main = bypass sai). Map chỉ là fallback realtime.
|
||||
if(track&&track.midiBypass!==undefined)return!!track.midiBypass;if(track&&track.id&&trackMidiBypassMap[track.id]!==undefined)return!!trackMidiBypassMap[track.id];return false;};const effAudioBypass=track=>{// GỠ override mastering-ON (17:00) — nút A quyết định bypass (user rules)
|
||||
if(track&&track.audioBypass!==undefined)return!!track.audioBypass;if(track&&track.id&&trackAudioBypassMap[track.id]!==undefined)return!!trackAudioBypassMap[track.id];return false;};// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
||||
if(track&&track.midiBypass!==undefined)return!!track.midiBypass;if(track&&track.id&&trackMidiBypassMap[track.id]!==undefined)return!!trackMidiBypassMap[track.id];return false;};const effAudioBypass=track=>{// 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;};// Build the dual routing for one track: routeGain -> mastering chain (normal),
|
||||
// dryGain -> dry bus (bypass). Gains start at complementary 1/0 values.
|
||||
function createMasteringRoute(ctx,track,bus){// Prefer the live bypass map (synced from the tracks state on every render),
|
||||
// falling back to the track object — this guarantees the A-button toggle is
|
||||
@@ -124,11 +129,16 @@ bypass=track.midiBypass!==undefined?!!track.midiBypass:track.id&&trackMidiBypass
|
||||
// ⚠️ 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.
|
||||
const routeGain=ctx.createGain();const dryGain=ctx.createGain();const masterDest=bus?bus.input:ctx.destination;const dryDest=bus&&bus.dryInput?bus.dryInput:ctx.destination;routeGain.gain.value=bypass?0:1;dryGain.gain.value=bypass?1:0;routeGain.connect(masterDest);dryGain.connect(dryDest);const routeObj={routeGain,dryGain};routeObj._trackId=track&&track.id;routeObj._bypass=bypass;return routeObj;}// Bypass theo LOẠI nội dung track (dùng chung: createMasteringRoute, sync
|
||||
// ✅ 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;const dryDest=bus&&bus.dryInput?bus.dryInput:ctx.destination;routeGain.gain.value=bypass?0:1;dryGain.gain.value=bypass?1:0;routeGain.connect(masterDest);dryGain.connect(dryDest);const routeObj={routeGain,dryGain};routeObj._trackId=track&&track.id;routeObj._bypass=bypass;return routeObj;}// Bypass theo LOẠI nội dung track (dùng chung: createMasteringRoute, sync
|
||||
// effect, startTrackPlayback re-apply) — MIDI-only track → nút ♪; có clips →
|
||||
// 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){const hasC=track&&(track.clips&&track.clips.length>0||!!track.buffer);const hasM=track&&track.midiItems&&track.midiItems.length>0;if(hasM&&!hasC){// MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
|
||||
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){// MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
|
||||
// clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
|
||||
routeBypassFor._lastType='midi';return track.midiBypass!==undefined?!!track.midiBypass:track.id&&trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:false;}routeBypassFor._lastType='audio';return track.audioBypass!==undefined?!!track.audioBypass:track.id&&trackAudioBypassMap[track.id]!==undefined?!!trackAudioBypassMap[track.id]:false;}// Live-toggle a route. HARD switch: cancel any pending automation and assign
|
||||
// .value directly (instant, cannot be delayed by the automation queue).
|
||||
@@ -224,7 +234,7 @@ const output=ctx.createGain();output.gain.value=1.0;// Per-track mastering-bypas
|
||||
// dryInput -> dryOutput -> output, skipping the mastering modules
|
||||
// (EQ / Imager / Maximizer) while still passing the master volume fader
|
||||
// and the master output metering.
|
||||
const dryInput=ctx.createGain();const dryOutput=ctx.createGain();dryInput.connect(dryOutput);dryOutput.connect(output);const analyser=ctx.createAnalyser();analyser.fftSize=256;const leftAnalyser=ctx.createAnalyser();leftAnalyser.fftSize=2048;const rightAnalyser=ctx.createAnalyser();rightAnalyser.fftSize=2048;const splitter=ctx.createChannelSplitter(2);output.connect(splitter);splitter.connect(leftAnalyser,0);splitter.connect(rightAnalyser,1);masterBus={input:ctx.createGain(),compressor:ctx.createDynamicsCompressor(),analyser,output,masteringActive:false,dryInput,dryOutput,// Analysers for metering
|
||||
const dryInput=ctx.createGain();const dryOutput=ctx.createGain();dryInput.connect(dryOutput);dryOutput.connect(output);const analyser=ctx.createAnalyser();analyser.fftSize=256;const leftAnalyser=ctx.createAnalyser();leftAnalyser.fftSize=2048;const rightAnalyser=ctx.createAnalyser();rightAnalyser.fftSize=2048;const splitter=ctx.createChannelSplitter(2);output.connect(splitter);splitter.connect(leftAnalyser,0);splitter.connect(rightAnalyser,1);masterBus={input:ctx.createGain(),compressor:ctx.createDynamicsCompressor(),analyser,output,masteringActive:false,dryInput,dryOutput,splitter,// Analysers for metering
|
||||
inputAnalyser,outputAnalyser,leftAnalyser,rightAnalyser,// Mastering nodes
|
||||
eqLowFilter,eqMid1Filter,eqMid2Filter,eqHighFilter,imagerInput,imagerOutput,gainLL1,gainRL1,gainLR1,gainRR1,gainLL2,gainRL2,gainLR2,gainRR2,gainLL3,gainRL3,gainLR3,gainRR3,gainLL4,gainRL4,gainLR4,gainRR4,maximizerBoostGain,maximizerSoftClipper,upwardCompressor,upwardGain,upwardSummingGain,maximizerCompressor,// Extension modules (mastering_expand.md §II.2)
|
||||
compInput,compNode,compMakeup,compOutput,limInput,limNode,limOutput,excInput,excHp,excShaper,excDry,excWet,excOutput,rebalInput,rebalSplit,rebalMerge,gLLr,gRLr,gLRr,gRRr,rebalOutput};// Connect EQ chain (internal — the module BOUNDARIES are wired dynamically
|
||||
@@ -294,11 +304,20 @@ const len=Math.floor(pcm.length/channels);const bps=4;const dataBytes=len*bps*ch
|
||||
// worklet → analyser (sau master fader, trước metering/destination — khớp thứ
|
||||
// tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường
|
||||
// chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out.
|
||||
const fxRt={active:false,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null};function fxRtChain(){const ms=window.currentMasteringSettings;if(!ms||!ms.masterConnected||ms.isBypassed)return[];const chain=activeVstChain(ms&&ms.vstFxChain||[]);return chain.length>0&&!(ms&&ms.vstBypass)?chain:[];}async function fxRtStart(){if(fxRt.active){// masterBus bị rebuild (recovery/export) → node thuộc graph cũ → restart.
|
||||
if(!fxRt.node||fxRt.node.context!==getAudioContext())fxRtStop(true);else return;}const chain=fxRtChain();if(!chain.length)return;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser)return;const chainPayload=chain.map(s=>({type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:false}));let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');return;}fxRt.sessionId=st.session_id;try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212200`);fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{fxRt.active=true;};ws.onmessage=ev=>{if(!(ev.data instanceof ArrayBuffer)||!fxRt.node)return;const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}fxRt.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtStop(true);ws.onerror=()=>{try{ws.close();}catch(e){}};fxRt.node.port.onmessage=ev=>{const d=ev.data;if(!d||d.type!=='in'||!fxRt.ws)return;if(fxRt.inQueue.length>64)fxRt.inQueue.shift();fxRt.inQueue.push(d.data);};// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn theo burst (main thread bị
|
||||
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[];const chain=activeVstChain(ms&&ms.vstFxChain||[]);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;}if(!chain.length)return;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser)return;const chainPayload=chain.map(s=>({type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:false}));let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');return;}fxRt.sessionId=st.session_id;try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212200`);fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{fxRt.active=true;};ws.onmessage=ev=>{if(!(ev.data instanceof ArrayBuffer)||!fxRt.node)return;const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;i<half;i++){l[i]=f[i*2];r[i]=f[i*2+1];}fxRt.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtStop(true);ws.onerror=()=>{try{ws.close();}catch(e){}};fxRt.node.port.onmessage=ev=>{const d=ev.data;if(!d||d.type!=='in'||!fxRt.ws)return;if(fxRt.inQueue.length>64)fxRt.inQueue.shift();fxRt.inQueue.push(d.data);};// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn theo burst (main thread bị
|
||||
// rAF renderFrame block khi mở MASTERING PANEL) → SHM ring 4-slot tràn → mất
|
||||
// block → ngắt quãng.
|
||||
const blockMs=Math.max(4,Math.round(256/(ctx.sampleRate||44100)*1000));if(fxRt.pumpTimer)clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=setInterval(()=>{const ws=fxRt.ws;if(!ws||ws.readyState!==1||!fxRt.inQueue.length)return;const d=fxRt.inQueue.shift();try{ws.send(d);}catch(e){fxRt.inQueue.unshift(d);}},blockMs);try{masterBus.output.disconnect(masterBus.analyser);}catch(e){}masterBus.output.connect(fxRt.node);fxRt.node.connect(masterBus.analyser);}catch(e){console.warn('[FX RT] setup fail:',e);fxRtStop(true);}}function fxRtStop(silent){if(fxRt.pumpTimer){clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=null;}fxRt.inQueue=[];if(!fxRt.active&&!fxRt.ws&&!fxRt.node&&!fxRt.sessionId)return;try{if(fxRt.node){fxRt.node.disconnect();if(masterBus&&masterBus.output&&masterBus.analyser){try{masterBus.output.connect(masterBus.analyser);}catch(e){}}fxRt.node.port.onmessage=null;fxRt.node=null;}}catch(e){}try{if(fxRt.ws){fxRt.ws.onclose=null;fxRt.ws.onmessage=null;try{fxRt.ws.close();}catch(e){}fxRt.ws=null;}}catch(e){}const sid=fxRt.sessionId;fxRt.sessionId=null;fxRt.active=false;if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}}// ── Realtime VST FX per-track (SHM bridge) — mọi luồng track qua bridge ─────
|
||||
const blockMs=Math.max(4,Math.round(256/(ctx.sampleRate||44100)*1000));if(fxRt.pumpTimer)clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=setInterval(()=>{const ws=fxRt.ws;if(!ws||ws.readyState!==1||!fxRt.inQueue.length)return;const d=fxRt.inQueue.shift();try{ws.send(d);}catch(e){fxRt.inQueue.unshift(d);}},blockMs);try{masterBus.output.disconnect(masterBus.analyser);}catch(e){}// 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);}}function fxRtStop(silent){if(fxRt.pumpTimer){clearInterval(fxRt.pumpTimer);fxRt.pumpTimer=null;}fxRt.inQueue=[];if(!fxRt.active&&!fxRt.ws&&!fxRt.node&&!fxRt.sessionId)return;try{if(fxRt.node){fxRt.node.disconnect();if(masterBus&&masterBus.output&&masterBus.analyser){try{masterBus.output.connect(masterBus.analyser);}catch(e){}// 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;}}catch(e){}try{if(fxRt.ws){fxRt.ws.onclose=null;fxRt.ws.onmessage=null;try{fxRt.ws.close();}catch(e){}fxRt.ws=null;}}catch(e){}const sid=fxRt.sessionId;fxRt.sessionId=null;fxRt.active=false;fxRt.chainKey=null;if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}}// ── Realtime VST FX per-track (SHM bridge) — mọi luồng track qua bridge ─────
|
||||
// Mỗi track: session riêng (1 WS/SHM) cho TỪNG đường chain có âm — audio-clips
|
||||
// (path 'audio', splice fxEntry→mods→worklet→fxLegacyIn) và MIDI/SF (path 'sf',
|
||||
// splice sfEntry→mods→worklet→sfOut). Track vừa clips vừa MIDI → 2 session
|
||||
@@ -854,10 +873,13 @@ ic.fillStyle='rgba(30, 41, 59, 0.9)';ic.fillRect(20,gaugeY,gaugeW,7);// zones: d
|
||||
const zx=v=>20+(v+1)/2*gaugeW;ic.fillStyle='rgba(239, 68, 68, 0.55)';ic.fillRect(zx(-1),gaugeY,zx(0)-zx(-1),7);ic.fillStyle='rgba(245, 158, 11, 0.55)';ic.fillRect(zx(0),gaugeY,zx(0.5)-zx(0),7);ic.fillStyle='rgba(52, 211, 153, 0.55)';ic.fillRect(zx(0.5),gaugeY,zx(1)-zx(0.5),7);// marker
|
||||
ic.fillStyle='#f8fafc';ic.fillRect(zx(corr)-1,gaugeY-2,2,11);// labels
|
||||
ic.fillStyle='rgba(148, 163, 184, 0.8)';ic.font='9px monospace';ic.fillText('−1',20,gaugeY-4);ic.fillText('0',iw/2-3,gaugeY-4);ic.fillText('+1',iw-26,gaugeY-4);const corrEl=document.getElementById('corrText');if(corrEl){corrEl.innerText=corr.toFixed(2);corrEl.style.color=corr>=0.5?'#34d399':corr>=0?'#fbbf24':'#ef4444';}}// I/O Meters
|
||||
const renderMeter=(analyser,ctxRef,textId)=>{const canvas=ctxRef.current;if(!canvas)return;const w=canvas.width,h=canvas.height;const ctx=canvas.getContext('2d');ctx.clearRect(0,0,w,h);const peak=getPeakLevel(analyser);const barH=Math.min(1.0,peak)*h;const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#38bdf8');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(1,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(2,h-barH,w-4,barH);const el=document.getElementById(textId);if(el){if(peak>0){const dbVal=20*Math.log10(peak);el.innerText=dbVal<-90?'-inf dB':`${dbVal.toFixed(1)} dB`;}else{el.innerText='-inf dB';}}};renderMeter(masterBus&&masterBus.inputAnalyser,inMeterCanvasRef,'inPeakText');renderMeter(masterBus&&masterBus.outputAnalyser,outMeterCanvasRef,'outPeakText');// Loudness measurement (post-mastering output): RMS dBFS + LUFS approx
|
||||
const renderMeter=(analyser,ctxRef,textId)=>{const canvas=ctxRef.current;if(!canvas)return;const w=canvas.width,h=canvas.height;const ctx=canvas.getContext('2d');ctx.clearRect(0,0,w,h);const peak=getPeakLevel(analyser);const barH=Math.min(1.0,peak)*h;const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#38bdf8');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(1,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(2,h-barH,w-4,barH);const el=document.getElementById(textId);if(el){if(peak>0){const dbVal=20*Math.log10(peak);el.innerText=dbVal<-90?'-inf dB':`${dbVal.toFixed(1)} dB`;}else{el.innerText='-inf dB';}}};renderMeter(masterBus&&masterBus.inputAnalyser,inMeterCanvasRef,'inPeakText');// 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;if(outAn){const outBuf=new Float32Array(outAn.fftSize);outAn.getFloatTimeDomainData(outBuf);let sq=0;for(let i=0;i<outBuf.length;i++)sq+=outBuf[i]*outBuf[i];const rmsDb=20*Math.log10(Math.sqrt(sq/outBuf.length)+1e-12);const rmsEl=document.getElementById('outRmsText');if(rmsEl)rmsEl.innerText=rmsDb<-90?'-inf':rmsDb.toFixed(1)+' dB';if(!lufsAccRef.current)lufsAccRef.current=[];lufsAccRef.current.push(sq/outBuf.length);const accLen=24;// ~0.4s at 60fps rAF
|
||||
const outAn=masterBus&&masterBus.analyser;if(outAn){const outBuf=new Float32Array(outAn.fftSize);outAn.getFloatTimeDomainData(outBuf);let sq=0;for(let i=0;i<outBuf.length;i++)sq+=outBuf[i]*outBuf[i];const rmsDb=20*Math.log10(Math.sqrt(sq/outBuf.length)+1e-12);const rmsEl=document.getElementById('outRmsText');if(rmsEl)rmsEl.innerText=rmsDb<-90?'-inf':rmsDb.toFixed(1)+' dB';if(!lufsAccRef.current)lufsAccRef.current=[];lufsAccRef.current.push(sq/outBuf.length);const accLen=24;// ~0.4s at 60fps rAF
|
||||
if(lufsAccRef.current.length>=accLen){const blocks=lufsAccRef.current.splice(0,accLen);const m=blocks.reduce((a,b)=>a+b,0)/blocks.length;const lufs=-0.691+10*Math.log10(m+1e-12);const lufsEl=document.getElementById('lufsText');if(lufsEl)lufsEl.innerText=lufs.toFixed(1)+' LUFS';}}// Safety watchdog: if the mastering chain is broken (signal in, silence
|
||||
// out — e.g. a biquad in a bad state), fall back to the direct routing so
|
||||
// audio is NEVER globally silent. The user can re-enable mastering after.
|
||||
@@ -1219,7 +1241,10 @@ activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain
|
||||
try{if(fxRtChain().length){if(!fxRt.active)fxRtStart();}else if(fxRt.active){fxRtStop();}}catch(e){console.warn('[FX RT] resync fail:',e);}// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
|
||||
// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
|
||||
// ngay lên node đang phát (không chờ tracks effect).
|
||||
try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});updateSfRouting();}catch(e){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);// D10: bridge indicator + "Ép dùng WASM" debug toggle.
|
||||
try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});// 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){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);// D10: bridge indicator + "Ép dùng WASM" debug toggle.
|
||||
const[bridgeUi,setBridgeUi]=useState({connected:false,forceWasm:false});React.useEffect(()=>{if(!window.NativeBridgeService)return;setBridgeUi(s=>({...s,connected:!!window.NativeBridgeService.isBridgeConnected}));window.NativeBridgeService.onStatusChange(function(st){// V8 bug 4: reset map NGAY DAU callback (ca connected lan !connected) —
|
||||
// Rust auto-restart KHONG emit bridge-down khi thanh cong → map cu true →
|
||||
// GUI click skip load → attach null → nen trang.
|
||||
@@ -1713,7 +1738,9 @@ window.__getTrackSfFxModule=(trackId,type)=>{const node=activeTrackNodesRef.curr
|
||||
// effAudioBypass (override mastering-ON ép false → route luôn no-bypass
|
||||
// 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?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;const routeGain=context.createGain();const dryGain=context.createGain();routeGain.gain.value=subBypass?0:1;dryGain.gain.value=subBypass?1:0;// analyserNode cho VU meter của SECTION-TAB track strip (VU tick đọc
|
||||
const subHasClips=subTrack.clips&&subTrack.clips.length>0||!!subTrack.buffer;// 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;const routeGain=context.createGain();const dryGain=context.createGain();routeGain.gain.value=subBypass?0:1;dryGain.gain.value=subBypass?1:0;// analyserNode cho VU meter của SECTION-TAB track strip (VU tick đọc
|
||||
// node.analyserNode — sub-node trước đây không có → VU không animation).
|
||||
const subAnalyser=context.createAnalyser();subAnalyser.fftSize=256;subAnalyser.smoothingTimeConstant=0.4;gainNode.connect(subAnalyser);subAnalyser.connect(routeGain);subAnalyser.connect(dryGain);// Mastering path → masterBus.input TRỰC TIẾP (không qua parent node:
|
||||
// parent có route riêng với bypass riêng — parent audio_bypass=true →
|
||||
|
||||
Reference in New Issue
Block a user