Gap 8: PDC realtime — WS latency push + dry-path delay compensation

Backend: /ws/fx-realtime pump drain get_latencies (SHM ring, bridge da bao REPORT_LATENCY) -> push {cmd:'latency',total,latencies} khi tong doi. Frontend: pdcDelay (DelayNode) giua dryOutput->output bu delay master chain (round-trip 2 block + latency VST3), fxRtApplyPdc cap nhat tu WS frame, reset khi chain off. Test test_63 fake session 1024 -> frame total 1024; full 175 passed.
This commit is contained in:
2026-08-23 08:44:54 +07:00
parent db12698ba2
commit 6b2c9a039b
5 changed files with 110 additions and 8 deletions
+16
View File
@@ -1064,8 +1064,24 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
# output theo burst khi nhận input dồn; bắn toàn bộ cùng lúc → worklet
# overflow drop block cũ nhất → âm ngắt quãng.
block_sec = (sess.block_size or 256) / max(int(sess.sample_rate or 44100), 1)
# Gap 8 (PDC realtime): drain latency chain tu SHM ring (bridge bao qua
# REPORT_LATENCY per-slot) -> push text frame khi tong doi. Client dung
# de bu delay dry-bypass path (app.jsx fxRtApplyPdc).
_last_lat = None
try:
while True:
try:
_lats = sess.get_latencies()
_total = sum(_lats.values()) if _lats else 0
if _total != _last_lat:
_last_lat = _total
await websocket.send_text(json.dumps({
"cmd": "latency",
"total": _total,
"latencies": _lats,
}))
except Exception:
pass
blks = sess.read_output()
if not blks:
await asyncio.sleep(0.002)
+38 -2
View File
@@ -1061,7 +1061,13 @@ function initMasterBus(ctx) {
const dryInput = ctx.createGain();
const dryOutput = ctx.createGain();
dryInput.connect(dryOutput);
dryOutput.connect(output);
// 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;
@@ -1101,6 +1107,7 @@ function initMasterBus(ctx) {
masteringActive: false,
dryInput,
dryOutput,
pdcDelay,
splitter,
volSplit,
volLGain,
@@ -1593,7 +1600,7 @@ 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, chainKey: null };
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).
@@ -1684,6 +1691,18 @@ async function fxRtStart() {
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;
@@ -1749,11 +1768,28 @@ function fxRtStop(silent) {
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',
+16 -5
View File
@@ -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
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608212300" defer></script>
<script src="/static/js/app.precompiled.js?v=202608212310" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+39
View File
@@ -702,3 +702,42 @@ def test_62_multiband_deesser(fx_bridge):
out = render(de, 500.0, 0.5)
g = peak_db(out)
assert -6.5 <= g <= -5.5, f"deesser 500Hz changed: {g:.2f}dBFS (expect ~-6.02)"
# ── 5.63 Gap 8: PDC realtime — WS latency push ───────────────────────────
def test_63_fxrt_ws_latency_push(monkeypatch):
"""Gap 8: WS /ws/fx-realtime/{id} day text frame {cmd:'latency', total,
latencies} khi chain bao latency (SHM ring drain trong output pump) —
client bu delay dry-bypass path. Fake session bao {0:1024,1:0} → total 1024."""
from fastapi.testclient import TestClient
from app.core import fx_realtime
from app.main import app as main_app
class _FakeSess:
block_size = 256
sample_rate = 44100
def alive(self):
return True
def read_output(self):
return []
def get_latencies(self):
return {0: 1024, 1: 0}
def set_param(self, *a):
pass
def report_latency(self, *a):
pass
def write_input(self, b):
pass
monkeypatch.setattr(fx_realtime, "get_session", lambda sid: _FakeSess())
client = TestClient(main_app)
with client.websocket_connect("/api/v1/plugins/ws/fx-realtime/fake") as ws:
d = ws.receive_json()
assert d.get("cmd") == "latency", d
assert d.get("total") == 1024, d
assert d.get("latencies", {}).get("0") == 1024, d