|
|
|
@@ -193,10 +193,13 @@ 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);splitter.connect(leftAnalyser,0);splitter.connect(rightAnalyser,1);// Master L/R volume + MUTE stage (spec §2: L/R fader + LINK + MUTE master).
|
|
|
|
|
const dryInput=ctx.createGain();const dryOutput=ctx.createGain();dryInput.connect(dryOutput);// Gap 8 (PDC realtime): dry path bu delay master chain realtime — worklet
|
|
|
|
|
// round-trip ~2 block + latency VST3 (bridge bao qua WS {cmd:'latency'}).
|
|
|
|
|
// Bypassed tracks align voi mastered (fxRtApplyPdc cap nhat delayTime).
|
|
|
|
|
const pdcDelay=ctx.createDelay(5.0);pdcDelay.delayTime.value=0;dryOutput.connect(pdcDelay);pdcDelay.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);splitter.connect(leftAnalyser,0);splitter.connect(rightAnalyser,1);// Master L/R volume + MUTE stage (spec §2: L/R fader + LINK + MUTE master).
|
|
|
|
|
// output -> volSplit -> volLGain/volRGain -> volMerge -> muteGain -> analyser;
|
|
|
|
|
// meters (splitter) lay sau muteGain -> MUTE that (VU ve 0).
|
|
|
|
|
const volSplit=ctx.createChannelSplitter(2);const volLGain=ctx.createGain();volLGain.gain.value=1.0;const volRGain=ctx.createGain();volRGain.gain.value=1.0;const volMerge=ctx.createChannelMerger(2);const muteGain=ctx.createGain();muteGain.gain.value=1.0;output.connect(volSplit);volSplit.connect(volLGain,0);volSplit.connect(volRGain,1);volLGain.connect(volMerge,0,0);volRGain.connect(volMerge,0,1);volMerge.connect(muteGain);masterBus={input:ctx.createGain(),compressor:ctx.createDynamicsCompressor(),analyser,output,masteringActive:false,dryInput,dryOutput,splitter,volSplit,volLGain,volRGain,volMerge,muteGain,// Analysers for metering
|
|
|
|
|
const volSplit=ctx.createChannelSplitter(2);const volLGain=ctx.createGain();volLGain.gain.value=1.0;const volRGain=ctx.createGain();volRGain.gain.value=1.0;const volMerge=ctx.createChannelMerger(2);const muteGain=ctx.createGain();muteGain.gain.value=1.0;output.connect(volSplit);volSplit.connect(volLGain,0);volSplit.connect(volRGain,1);volLGain.connect(volMerge,0,0);volRGain.connect(volMerge,0,1);volMerge.connect(muteGain);masterBus={input:ctx.createGain(),compressor:ctx.createDynamicsCompressor(),analyser,output,masteringActive:false,dryInput,dryOutput,pdcDelay,splitter,volSplit,volLGain,volRGain,volMerge,muteGain,// 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
|
|
|
|
@@ -266,7 +269,7 @@ 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,chainKey:null};// Signature của chain VST realtime — phát hiện chain ĐỔI (insert/remove/bypass/
|
|
|
|
|
const fxRt={active:false,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,chainKey:null,latencyTotal:0};// 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.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[];// Chain hợp nhất (builtin + vst3 theo UI order) — mọi slot active gửi bridge.
|
|
|
|
@@ -274,13 +277,21 @@ return(ms.chain||[]).filter(s=>s&&s.active!==false&&(s.type!=='vst3'||!ms.vstByp
|
|
|
|
|
const FX_RT_PARAM_MAP={eq:[['g1','eqLowGain'],['g2','eqMid1Gain'],['g3','eqMid2Gain'],['g4','eqHighGain']],compressor:[['threshold','compThreshold'],['ratio','compRatio'],['makeup','compMakeup']],limiter:[['ceiling','limThreshold'],['mode','limMode'],['lookahead_ms','limLookahead']],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();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=>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});}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;fxRtGuiPushShm();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;try{fxRtPushAllParams();}catch(e){}};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||!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);};// Pace input: tối đa 1 block/kỳ (~5.8ms). Gửi dồn theo burst (main thread bị
|
|
|
|
|
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=>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});}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;fxRtGuiPushShm();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;try{fxRtPushAllParams();}catch(e){}};ws.onmessage=ev=>{// Gap 8 (PDC realtime): text frame {cmd:'latency', total} tu server ->
|
|
|
|
|
// cap nhat delay dry-bypass path (bu latency chain VST3 realtime).
|
|
|
|
|
if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='latency'&&typeof d.total==='number'){fxRt.latencyTotal=d.total;fxRtApplyPdc();}}catch(e){}return;}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||!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);};// 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);// 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);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.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;}}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;fxRtGuiPushShm();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 ─────
|
|
|
|
|
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);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.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;}}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;fxRtGuiPushShm();fxRt.active=false;fxRt.chainKey=null;fxRt.latencyTotal=0;fxRtApplyPdc();if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}}// ── Gap 8: PDC realtime — bù delay dry-bypass path (master chain) ──────────
|
|
|
|
|
// Master chain (worklet → bridge VST3) trễ ~2 block round-trip + latency
|
|
|
|
|
// plugin. Track bypass mastering đi dry path (dryInput→dryOutput→output) KHÔNG
|
|
|
|
|
// qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng
|
|
|
|
|
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng (~2 block 256 @44.1k) —
|
|
|
|
|
// ponytail: đo RTT thật (tag seq trên block) khi cần sample-accurate.
|
|
|
|
|
const FX_RT_ROUNDTRIP_SAMPLES=512;function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;const l=fxRt&&fxRt.active?FX_RT_ROUNDTRIP_SAMPLES+(fxRt.latencyTotal||0):0;try{bus.pdcDelay.delayTime.setTargetAtTime(l/(ctx.sampleRate||44100),ctx.currentTime,0.05);}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
|
|
|
|
|