diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 1bac872..a76e21a 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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 '_sub_'): 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; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index c0533ef..95af81d 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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;ifxRtStop(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;ifxRtStop(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=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 '_sub_'): 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 →