fix(fxrt): underrun xu ly o worklet (hold+fade), bo fade engine, fix wet bus
- sf-fx-realtime.js: underrun -> hold block cuoi (HOLD) roi fade-out 4 block ~21ms gain 1->0 truoc khi im lang (het click cat cung khi main-thread stall chan ca 2 huong - engine khong the gui fade toi kip); resume giua fade -> fade-in nhanh - plugins.py: bo fade-out/fade-in engine (den tre vo dung + fade-in sau stall tao dip dinh ky = vibrato/wow&flutter), giu to_thread(time.sleep) poll 2ms - app.jsx/precompiled/index.html: fix master wet bus (no dry lai) + latency const FX_RT_ROUNDTRIP_SAMPLES + bump worklet cache-buster v=202608241800 - fxrt_ws_probe.py: consumer stall (mo hinh dung app that), WorkletSim holdfade mode Probe consumer-stall (app that): light 5/5 hard click 0.245-0.285 -> hard=0; heavy: recv=sent (het fade thua), hard=0
This commit is contained in:
+7
-63
@@ -1097,38 +1097,16 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
# 4 → gửi hết ngay → CLIENT tích lũy buffer (worklet FILL) hấp thụ
|
||||
# jitter bridge/WS. Pace đúng realtime trước đây làm client queue ≈1 →
|
||||
# bất kỳ jitter (bridge process VST biến thiên, GC, WS) → queue cạn →
|
||||
# zero-fill crackle. Worklet cap 32 block chặn overflow khi bridge
|
||||
# zero-fill crackle. Worklet cap 48 block chặn overflow khi bridge
|
||||
# nhanh hơn.
|
||||
# StALL-FADE (fix crackle nặng khi load VST FX): transport stall (browser
|
||||
# main-thread block GUI/rAF/React) làm bridge ngừng sản xuất output →
|
||||
# worklet hết buffer (FILL=24 ~128ms) → HOLD 2 block → CẮT cứng xuống
|
||||
# silence → click. Khi nhận ra output ngừng > ngưỡng → gửi fade-out:
|
||||
# lặp block real cuối với gain 1→0 qua 8 block (~42ms, pace realtime).
|
||||
# Worklet hết buffer nhưng nội dung đã về ~0 → hold/silence không click.
|
||||
# Output trở lại → fade-in 8 block đầu 0→1 (hết click resume).
|
||||
import struct as _struct
|
||||
block_sec = (sess.block_size or 256) / max(int(sess.sample_rate or 44100), 1)
|
||||
# Ngưỡng stall: > batch period (4*block_sec) + jitter; 5.5*block_sec ≈ 29.3ms
|
||||
# (batch gap thường ~21.3ms + jitter bridge ~2ms sau fix timer 1ms + wakeup
|
||||
# pump hiếm ~8ms). 6x (32ms) bỏ sót case queue worklet thấp lúc recovery;
|
||||
# 5x false-fade khi gap 29ms không phải stall thật. 5.5x dung hòa.
|
||||
_stall_s = 5.5 * block_sec
|
||||
_fade_blocks = 8
|
||||
_fade_fmt = '<%df' % ((sess.block_size or 256) * 2)
|
||||
def _gain(blk, g):
|
||||
if g >= 1.0: return blk
|
||||
if g <= 0.0: return b'\x00' * len(blk)
|
||||
vals = _struct.unpack(_fade_fmt, blk)
|
||||
return _struct.pack(_fade_fmt, *[v * g for v in vals])
|
||||
# UNDERRUN do stall (main-thread browser block GUI/rAF/React) xử lí Ở
|
||||
# CLIENT: worklet lặp block cuối (HOLD) rồi fade-out ~21ms
|
||||
# (sf-fx-realtime.js). Engine KHÔNG fade — fade gửi từ engine không thể
|
||||
# tới worklet kịp khi main-thread stall chặn cả 2 hướng (fade nằm trong
|
||||
# browser queue, tới sau khi worklet đã cạn buffer → click vẫn còn), và
|
||||
# fade-in sau stall tạo dip âm lượng định kỳ = vibrato/wow&flutter.
|
||||
_last_lat = None
|
||||
_last_blk = None
|
||||
_last_out = 0.0
|
||||
_had_out = False
|
||||
_fading = False
|
||||
_fade_i = 0
|
||||
_fade_in = 0
|
||||
try:
|
||||
_loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
try:
|
||||
sess.flush_input_stale() # input lẻ kẹt trong pending → publish
|
||||
@@ -1145,47 +1123,13 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
pass
|
||||
blks = sess.read_output()
|
||||
if not blks:
|
||||
_now = _loop.time()
|
||||
_gap = _now - _last_out if _had_out else 0.0
|
||||
if (_had_out and not _fading and _fade_in <= 0 and
|
||||
_last_blk is not None and _fade_i == 0 and
|
||||
_gap > _stall_s):
|
||||
_fading = True
|
||||
_fade_i = 0
|
||||
if _fading and _last_blk is not None and _fade_i < _fade_blocks:
|
||||
# Gửi BURST toàn bộ fade-out (không pace realtime): worklet
|
||||
# queue (FILL 24) hấp thụ 8 block; fade frames xếp sau buffer
|
||||
# real → worklet phát dốc 1→0 khi hết buffer → silence ~0.
|
||||
# Burst cũng phủ case queue worklet THẤP lúc recovery (fade
|
||||
# đến trước khi queue cạn, thay vì chậm 42ms như pace thật).
|
||||
while _fade_i < _fade_blocks:
|
||||
# gain (8-i-1)/7: block cuối đúng 0.0 → hold/silence ~0
|
||||
await websocket.send_bytes(_gain(
|
||||
_last_blk,
|
||||
(_fade_blocks - 1 - _fade_i) / (_fade_blocks - 1)))
|
||||
_fade_i += 1
|
||||
_last_out = _loop.time()
|
||||
_fading = False # đã về ~0 → worklet tự silence
|
||||
continue
|
||||
# to_thread(time.sleep): asyncio.sleep trên Windows bị cap
|
||||
# 15.6ms (GQCS/IOCP, Win11 cap nền) dù timeBeginPeriod;
|
||||
# time.sleep dùng high-res waitable timer -> poll đúng 2ms.
|
||||
await asyncio.to_thread(_time.sleep, 0.002)
|
||||
continue
|
||||
for _blk in blks:
|
||||
if _fading or _fade_i >= _fade_blocks:
|
||||
# resume (giữa fade hoặc sau fade xong) → reset + fade-in
|
||||
_fading = False
|
||||
_fade_i = 0
|
||||
_fade_in = _fade_blocks
|
||||
if _fade_in > 0:
|
||||
# gain (8-i-1)/7 dốc 0→1: block cuối full
|
||||
_blk = _gain(_blk, 1.0 - (_fade_in - 1) / _fade_blocks)
|
||||
_fade_in -= 1
|
||||
await websocket.send_bytes(_blk)
|
||||
_last_blk = _blk
|
||||
_last_out = _loop.time()
|
||||
_had_out = True
|
||||
except Exception:
|
||||
pass
|
||||
pump_task = asyncio.create_task(_pump_output())
|
||||
|
||||
@@ -1165,7 +1165,8 @@ function initMasterBus(ctx) {
|
||||
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
|
||||
masterBus.outputAnalyser.connect(masterBus.masterFxDryGain);
|
||||
masterBus.masterFxDryGain.connect(masterBus.pdcDelay);
|
||||
masterBus.outputAnalyser.connect(masterBus.masterFxGain);
|
||||
// Wet bus: CHI nhan node fxRt (fxRtStart noi) — khong noi dry truc tiep vao
|
||||
// masterFxGain (no = dry + processed = play double khi PWR bat).
|
||||
masterBus.masterFxGain.connect(masterBus.output);
|
||||
masterBus.muteGain.connect(masterBus.analyser);
|
||||
masterBus.muteGain.connect(masterBus.splitter);
|
||||
@@ -1766,7 +1767,7 @@ async function fxRtStart() {
|
||||
fxRt.sessionId = st.session_id;
|
||||
fxRtGuiPushShm();
|
||||
try {
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241200`);
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241800`);
|
||||
if (token !== fxRt.startToken) { fxRt.starting = false; return; }
|
||||
fxRt.node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
|
||||
const u = new URL(API_BASE_URL);
|
||||
@@ -1941,9 +1942,10 @@ function fxRtStop(silent) {
|
||||
// qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng
|
||||
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật
|
||||
// (tag seq trên block) khi cần sample-accurate.
|
||||
// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 8 block (2048)
|
||||
// — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 15 block.
|
||||
const FX_RT_ROUNDTRIP_SAMPLES = 3840;
|
||||
// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 24 block
|
||||
// (6144) — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 31
|
||||
// block ≈ 7936. (FILL 16 trước đây = 5888; giữ đồng bộ với sf-fx-realtime.js)
|
||||
const FX_RT_ROUNDTRIP_SAMPLES = 7936;
|
||||
function fxRtApplyPdc() {
|
||||
const ctx = getAudioContext();
|
||||
const bus = masterBus;
|
||||
@@ -23837,7 +23839,7 @@ const App = () => {
|
||||
fxRtTracks[key] = entry;
|
||||
fxRtGuiPushShm();
|
||||
try {
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241200`);
|
||||
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241800`);
|
||||
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
|
||||
entry.node = node;
|
||||
const u = new URL(API_BASE_URL);
|
||||
|
||||
@@ -214,7 +214,9 @@ eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filte
|
||||
// Compressor mặc định (ratio 12, threshold -24 — LUÔN-ON) vừa (a) pump-down
|
||||
// tín hiệu → âm nhỏ/méo, vừa (b) phát NaN khi gặp bass transient → 11 biquad
|
||||
// "state is bad" → CÂM + stuck. Mastering chain có comp/lim module riêng khi bật.
|
||||
masterBus.input.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.masterFxDryGain);masterBus.masterFxDryGain.connect(masterBus.pdcDelay);masterBus.outputAnalyser.connect(masterBus.masterFxGain);masterBus.masterFxGain.connect(masterBus.output);masterBus.muteGain.connect(masterBus.analyser);masterBus.muteGain.connect(masterBus.splitter);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;// Apply mastering once when the chain is first created (and on the
|
||||
masterBus.input.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.masterFxDryGain);masterBus.masterFxDryGain.connect(masterBus.pdcDelay);// Wet bus: CHI nhan node fxRt (fxRtStart noi) — khong noi dry truc tiep vao
|
||||
// masterFxGain (no = dry + processed = play double khi PWR bat).
|
||||
masterBus.masterFxGain.connect(masterBus.output);masterBus.muteGain.connect(masterBus.analyser);masterBus.muteGain.connect(masterBus.splitter);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;// Apply mastering once when the chain is first created (and on the
|
||||
// masteringSettings effect for subsequent changes — see useEffect).
|
||||
if(window.currentMasteringSettings){try{// Reset sig-cache: chain MỚI (biquad/maximizer node mới) giữ giá trị
|
||||
// INIT (gain 0, width 100…) — applyMasteringSettings early-return vì
|
||||
@@ -302,7 +304,7 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re
|
||||
// node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint
|
||||
// dưới đây thấy mismatch → hủy session vừa tạo, không nối node.
|
||||
const token=++fxRt.startToken;fxRt.starting=true;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser||!masterBus.masterFxGain||!masterBus.masterFxDryGain)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');fxRt.starting=false;return;}if(token!==fxRt.startToken){// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo.
|
||||
console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608240831`);if(token!==fxRt.startToken){fxRt.starting=false;return;}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=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu
|
||||
console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241800`);if(token!==fxRt.startToken){fxRt.starting=false;return;}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=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu
|
||||
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;// Chi delay dry path khi crossfade da xong (dry silent) — ap som
|
||||
@@ -342,9 +344,10 @@ if(!_fxStopNode)fxRtApplyPdc();if(sid){try{window.SonicAPI.fxRealtimeStop({sessi
|
||||
// qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng
|
||||
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật
|
||||
// (tag seq trên block) khi cần sample-accurate.
|
||||
// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 8 block (2048)
|
||||
// — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 15 block.
|
||||
const FX_RT_ROUNDTRIP_SAMPLES=3840;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){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi
|
||||
// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 24 block
|
||||
// (6144) — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 31
|
||||
// block ≈ 7936. (FILL 16 trước đây = 5888; giữ đồng bộ với sf-fx-realtime.js)
|
||||
const FX_RT_ROUNDTRIP_SAMPLES=7936;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){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi
|
||||
// hard-rewire (click). Hai gain nay duoc tao trong initMasterBus.
|
||||
function fxRtCrossfade(toFx,token){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.masterFxGain||!bus.masterFxDryGain)return;if(token!==undefined&&token!==fxRt.startToken)return;const tc=0.025;try{bus.masterFxGain.gain.setTargetAtTime(toFx?1:0,ctx.currentTime,tc);bus.masterFxDryGain.gain.setTargetAtTime(toFx?0:1,ctx.currentTime,tc);}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
|
||||
@@ -1696,7 +1699,7 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra
|
||||
// Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart):
|
||||
// fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice
|
||||
// worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild).
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)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 track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608240831`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu
|
||||
const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)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 track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608241800`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${u.host}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu
|
||||
fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(!(ev.data instanceof ArrayBuffer)||!entry.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];}entry.node.port.postMessage({type:'out',l,r});};ws.onclose=()=>fxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)return;if(d.type==='set_param'){try{entry.ws.send(JSON.stringify({cmd:'set_param',slot:d.slot,key:d.key,value:d.value}));}catch(e){}return;}if(d.type!=='in')return;// Burst 4 block (1024 mẫu) → tách 4 WS frame 512 floats → bridge batch 4.
|
||||
// Pace 1 burst/21ms (giống master) — tránh dồn frame sau stall làm tràn ring.
|
||||
entry.inQueue.push(d);const pump=()=>{if(!entry.inQueue.length)return;const now=Date.now();while(entry.inQueue.length&&now-entry.lastInSent>=FXRT_BURST_MS){entry.lastInSent=now;const dd=entry.inQueue.shift();if(entry.ws&&entry.ws.readyState===1){try{const f=new Float32Array(dd.data);if(f.length>512){for(let i=0;i<f.length;i+=512)entry.ws.send(f.slice(i,i+512).buffer);}else{entry.ws.send(dd.data);}}catch(e){}}}if(entry.inQueue.length){if(entry.pumpTimer)clearTimeout(entry.pumpTimer);entry.pumpTimer=setTimeout(pump,Math.max(1,FXRT_BURST_MS-(now-entry.lastInSent)));}else if(entry.pumpTimer){clearTimeout(entry.pumpTimer);entry.pumpTimer=null;}};pump();};}catch(e){console.warn('[FX RT track] setup fail',trackId,path,e);fxRtTrackStop(trackId,path,true);}};const fxRtTrackUnsplice=(trackId,path)=>{const entry=fxRtTracks[trackId+':'+path];if(!entry||!entry.spliced||!entry.node)return;try{entry.node.disconnect();try{entry.spliced.tail.connect(entry.spliced.dest);}catch(e){}entry.spliced=null;}catch(e){console.warn('[FX RT track] unsplice fail',trackId,path,e);}};// Chain đổi trên session ĐANG CHẠY — set_chain KHÔNG restart (bridge swap
|
||||
|
||||
@@ -12,21 +12,32 @@
|
||||
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
|
||||
//
|
||||
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
|
||||
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
|
||||
// ≥ FILL (24 block ~128ms @48k) rồi mới phát. Engine gửi ASAP (không pace
|
||||
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
|
||||
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
|
||||
// → cạn → zero-fill. Underrun (queue cạn khi đang phát) → re-buffer: im lặng
|
||||
// chờ đủ FILL rồi phát tiếp (1 lần gián đoạn thay vì zero-fill liên tục).
|
||||
// Cap 32 block (~170ms) chặn overflow khi bridge nhanh hơn realtime.
|
||||
// ponytail: thay drop/rebuffer bằng adaptive stretch khi cần chất lượng cao.
|
||||
// → cạn → ngắt quãng.
|
||||
// UNDERRUN (queue cạn khi đang phát): KHÔNG zero-fill/re-buffer im lặng — lặp
|
||||
// block cuối tối đa HOLD block (~11ms) giữ âm liên tục (hết click/ngắt), rồi
|
||||
// FADE-OUT HOLD_FADE block (~21ms) gain 1→0 trước khi im lặng nếu stall kéo
|
||||
// dài (hết click cắt cứng — engine không thể gửi fade tới kịp khi main-thread
|
||||
// stall chặn cả 2 hướng); resume NGAY khi có block mới (không đợi đủ FILL),
|
||||
// nếu đang giữa fade-out → fade-in nhanh (hết jump ~0→1) + không re-fade sau
|
||||
// lần đầu (tránh tụt volume).
|
||||
// Cap 48 block (~256ms) chặn overflow khi bridge nhanh hơn realtime.
|
||||
// ponytail: thay hold bằng adaptive stretch khi cần chất lượng cao.
|
||||
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.BLOCK = 256; // FXRT_BLOCK
|
||||
this.BURST = 4; // số block gom 1 frame (bridge batch)
|
||||
this.FILL = 16; // 16 block ~86ms: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, solo restart)
|
||||
this.REBUFFER = 2; // 2 block ~11ms: sau underrun chỉ cần bấy nhiêu để phát lại — gap ngắn, không còn 86ms im lặng
|
||||
this.CAP = 32; // cap queue chặn overflow
|
||||
this.FILL = 24; // 24 block ~128ms: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, solo restart)
|
||||
this.CAP = 48; // cap queue chặn overflow (~256ms)
|
||||
this.HOLD = 2; // 2 block ~11ms: lặp block cuối khi underrun (hết click) trước khi im lặng
|
||||
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
|
||||
this.lastBlock = null; // block cuối đã phát xong (hold khi underrun)
|
||||
this.holdPos = 0;
|
||||
this.holdLeft = 0;
|
||||
this.holdFadeLeft = 0; // mẫu còn lại của fade-out sau HOLD
|
||||
this.bufLen = this.BLOCK * this.BURST;
|
||||
this.inL = new Float32Array(this.bufLen);
|
||||
this.inR = new Float32Array(this.bufLen);
|
||||
@@ -34,7 +45,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
|
||||
this.outPos = 0;
|
||||
this.playing = false; // false → im lặng (đang chờ đủ FILL)
|
||||
this.hasStarted = false; // đã phát block đầu chưa — phân biệt FILL khởi đầu vs REBUFFER sau underrun
|
||||
this.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu
|
||||
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
|
||||
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
|
||||
this.fadeLeft = 0; // số mẫu còn lại của fade
|
||||
@@ -76,40 +87,77 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// Phát output: im lặng tới khi queue ≥ FILL; queue cạn khi đang phát
|
||||
// (underrun) → im lặng chờ đủ FILL (re-buffer) thay vì zero-fill liên tục.
|
||||
// Phát output: chờ queue ≥ FILL trước khi phát lần đầu (hấp thụ jitter
|
||||
// engine/bridge/WS); underrun GIỮA stream → lặp block cuối tối đa HOLD
|
||||
// block (không zero-fill → hết click/ngắt) rồi mới im lặng; resume NGAY
|
||||
// khi có block mới (không đợi re-buffer FILL) + không re-fade sau lần đầu.
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (this.playing && this.outQueue.length === 0) this.playing = false;
|
||||
if (!this.playing) {
|
||||
// Khởi đầu: chờ đủ FILL (hấp thụ jitter). Sau underrun: chỉ cần
|
||||
// REBUFFER (2 block ~11ms) — gap ngắn thay vì 86ms im lặng.
|
||||
const need = this.hasStarted ? this.REBUFFER : this.FILL;
|
||||
if (this.outQueue.length >= need) {
|
||||
this.playing = true;
|
||||
this.hasStarted = true;
|
||||
// Fade-in mượt (hết click lúc FILL xong / resume sau underrun); báo
|
||||
// main thread để crossfade dry→fx đúng thời điểm.
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
this.port.postMessage({ type: 'started' });
|
||||
} else {
|
||||
outL[i] = 0;
|
||||
if (outR) outR[i] = 0;
|
||||
if (this.playing) {
|
||||
const blk = this.outQueue[0];
|
||||
if (blk) {
|
||||
if (this.fadeLeft > 0) {
|
||||
this.outGain = Math.min(1, this.outGain + 1 / this.FADE);
|
||||
this.fadeLeft--;
|
||||
}
|
||||
outL[i] = blk.l[this.outPos] * this.outGain;
|
||||
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
|
||||
this.outPos++;
|
||||
if (this.outPos >= this.BLOCK) {
|
||||
this.lastBlock = blk;
|
||||
this.outQueue.shift();
|
||||
this.outPos = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Underrun giữa stream — chuyển hold (không cắt zero ngay → hết click).
|
||||
this.playing = false;
|
||||
this.holdPos = 0;
|
||||
this.holdLeft = this.HOLD * this.BLOCK;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
}
|
||||
const blk = this.outQueue[0];
|
||||
if (this.fadeLeft > 0) {
|
||||
this.outGain = Math.min(1, this.outGain + 1 / this.FADE);
|
||||
this.fadeLeft--;
|
||||
}
|
||||
outL[i] = blk.l[this.outPos] * this.outGain;
|
||||
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
|
||||
this.outPos++;
|
||||
if (this.outPos >= this.BLOCK) {
|
||||
this.outQueue.shift();
|
||||
// Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1
|
||||
// block — hold che khoảng chờ).
|
||||
if (this.outQueue.length >= (this.hasStarted ? 1 : this.FILL)) {
|
||||
this.playing = true;
|
||||
this.outPos = 0;
|
||||
const wasFading = this.holdFadeLeft > 0 && this.holdLeft <= 0;
|
||||
this.holdLeft = 0;
|
||||
this.holdFadeLeft = 0;
|
||||
if (wasFading && this.hasStarted) {
|
||||
// Resume giữa fade-out: gain đang <1 → fade-in nhanh tránh jump ~0→1
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
}
|
||||
if (!this.hasStarted) {
|
||||
this.hasStarted = true;
|
||||
this.outGain = 0;
|
||||
this.fadeLeft = this.FADE;
|
||||
// Báo main thread 1 lần để crossfade dry→fx đúng thời điểm.
|
||||
this.port.postMessage({ type: 'started' });
|
||||
}
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (this.holdLeft > 0 && this.lastBlock) {
|
||||
outL[i] = this.lastBlock.l[this.holdPos] * this.outGain;
|
||||
if (outR) outR[i] = this.lastBlock.r[this.holdPos] * this.outGain;
|
||||
this.holdPos++;
|
||||
if (this.holdPos >= this.BLOCK) this.holdPos = 0;
|
||||
this.holdLeft--;
|
||||
continue;
|
||||
}
|
||||
if (this.holdFadeLeft > 0 && this.lastBlock) {
|
||||
// Fade-out 1→0 sau HOLD: im lặng êm, hết click cắt cứng.
|
||||
const g = this.outGain * (this.holdFadeLeft / (this.HOLD_FADE * this.BLOCK));
|
||||
outL[i] = this.lastBlock.l[this.holdPos] * g;
|
||||
if (outR) outR[i] = this.lastBlock.r[this.holdPos] * g;
|
||||
this.holdPos++;
|
||||
if (this.holdPos >= this.BLOCK) this.holdPos = 0;
|
||||
this.holdFadeLeft--;
|
||||
continue;
|
||||
}
|
||||
outL[i] = 0;
|
||||
if (outR) outR[i] = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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=202608240831" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608241041" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
+60
-38
@@ -54,14 +54,25 @@ def stall_model(name):
|
||||
|
||||
|
||||
class WorkletSim:
|
||||
"""Mô phỏng chính xác SfFxRealtimeProcessor outQueue từ arrival times."""
|
||||
"""Mô phỏng chính xác SfFxRealtimeProcessor outQueue từ arrival times.
|
||||
|
||||
def __init__(self):
|
||||
mode='cut' — hiện tại: underrun → hold HOLD block rồi CẮT cứng silence
|
||||
(click nếu nội dung khác 0).
|
||||
mode='holdfade' — đề xuất (fix worklet): underrun → giữ block cuối + fade
|
||||
gain 1→0 qua FADE_HOLD block (~21ms) rồi silence ~0
|
||||
(hết click); giữa hold, block mới tới → resume ngay.
|
||||
"""
|
||||
|
||||
FADE_HOLD = 4 # block fade khi underrun (proposal)
|
||||
|
||||
def __init__(self, mode="cut"):
|
||||
self.mode = mode
|
||||
self.arrivals = [] # (block_seq, time, peak)
|
||||
self.underruns = 0
|
||||
self.silences = 0
|
||||
self.silent_ms = 0.0
|
||||
self.hold_events = 0
|
||||
self.hold_fades = 0
|
||||
self.silence_peaks = [] # peak block cuối trước mỗi silence (fade -> ~0)
|
||||
|
||||
def run(self, duration):
|
||||
@@ -70,6 +81,7 @@ class WorkletSim:
|
||||
started = False
|
||||
idx = 0
|
||||
last_peak = 1.0
|
||||
hold_fade = 0 # còn N block fade đang phát (mode holdfade)
|
||||
for item in self.arrivals:
|
||||
bi, t = item[0], item[1]
|
||||
peak = item[2] if len(item) > 2 else 1.0
|
||||
@@ -85,29 +97,50 @@ class WorkletSim:
|
||||
# consume các block đến hạn trước arrival này
|
||||
while play is not None and play <= t:
|
||||
if queue:
|
||||
if hold_fade > 0 and self.mode == "holdfade":
|
||||
# có block mới giữa hold-fade → resume ngay (không đợi
|
||||
# hết fade, không cắt): fade chỉ còn là chỗi ~0.
|
||||
hold_fade = 0
|
||||
last_peak = queue.pop(0)[1]
|
||||
play += BLOCK_PERIOD
|
||||
else:
|
||||
# underrun tại play — hold HOLD block rồi silence
|
||||
# underrun tại play
|
||||
self.underruns += 1
|
||||
hold_end = play + HOLD * BLOCK_PERIOD
|
||||
if t >= hold_end:
|
||||
# silence từ hold_end tới khi có block mới (t)
|
||||
self.silences += 1
|
||||
self.silent_ms += (t - hold_end) * 1e3
|
||||
self.silence_peaks.append(last_peak)
|
||||
if self.mode == "holdfade":
|
||||
# giữ block cuối + fade 1→0 qua FADE_HOLD block
|
||||
hold_fade = self.FADE_HOLD
|
||||
self.hold_fades += 1
|
||||
while play <= t and hold_fade > 0:
|
||||
last_peak *= (hold_fade - 1) / max(self.FADE_HOLD - 1, 1)
|
||||
hold_fade -= 1
|
||||
play += BLOCK_PERIOD
|
||||
if hold_fade == 0 and play <= t:
|
||||
# đã fade về ~0 → silence êm (không click)
|
||||
self.silences += 1
|
||||
self.silent_ms += (t - play) * 1e3
|
||||
self.silence_peaks.append(last_peak)
|
||||
play = None
|
||||
idx += 1
|
||||
else:
|
||||
self.hold_events += 1
|
||||
play = None
|
||||
idx += 1
|
||||
hold_end = play + HOLD * BLOCK_PERIOD
|
||||
if t >= hold_end:
|
||||
# silence từ hold_end tới khi có block mới (t)
|
||||
self.silences += 1
|
||||
self.silent_ms += (t - hold_end) * 1e3
|
||||
self.silence_peaks.append(last_peak)
|
||||
else:
|
||||
self.hold_events += 1
|
||||
play = None
|
||||
idx += 1
|
||||
if play is None and queue:
|
||||
play = t + BLOCK_PERIOD
|
||||
last_peak = queue.pop(0)[1]
|
||||
return self
|
||||
|
||||
def report(self):
|
||||
r = ("worklet sim: underruns=%d hold_only=%d silences=%d silent_ms=%.1f"
|
||||
% (self.underruns, self.hold_events, self.silences, self.silent_ms))
|
||||
r = ("worklet sim[%s]: underruns=%d hold_only=%d hold_fades=%d silences=%d silent_ms=%.1f"
|
||||
% (self.mode, self.underruns, self.hold_events, self.hold_fades,
|
||||
self.silences, self.silent_ms))
|
||||
if self.silence_peaks:
|
||||
arr = np.array(self.silence_peaks)
|
||||
hard = int((arr > 0.05).sum())
|
||||
@@ -179,12 +212,13 @@ async def run_ws_probe(port, chain, secs, stall):
|
||||
produce_count = n
|
||||
|
||||
async def stall_scheduler():
|
||||
# main-thread busy: báo stall_until cho pump qua queue
|
||||
# main-thread busy: set stall_until — pump VÀ consumer đều đọc (cùng
|
||||
# main thread bị block → cả 2 hướng đều đứng; app thật đúng vậy).
|
||||
while not stop.is_set():
|
||||
if s_prob:
|
||||
await asyncio.sleep(random.expovariate(s_prob))
|
||||
dur = random.uniform(s_min, s_max) / 1000.0
|
||||
await stall_q.put(time.perf_counter() + dur)
|
||||
stall_state["until"] = time.perf_counter() + dur
|
||||
else:
|
||||
await asyncio.sleep(60)
|
||||
|
||||
@@ -192,13 +226,12 @@ async def run_ws_probe(port, chain, secs, stall):
|
||||
# fxRtInPump JS: tối đa 1 burst/21ms; stall → không gửi
|
||||
nonlocal backlog
|
||||
last_sent = 0.0
|
||||
stall_until = 0.0
|
||||
send_times = []
|
||||
iter_times = []
|
||||
while not stop.is_set():
|
||||
now = time.perf_counter()
|
||||
iter_times.append(now)
|
||||
if now < stall_until:
|
||||
if now < stall_state["until"]:
|
||||
await asyncio.to_thread(time.sleep, 0.002)
|
||||
continue
|
||||
while not produced.empty():
|
||||
@@ -212,33 +245,22 @@ async def run_ws_probe(port, chain, secs, stall):
|
||||
send_times.append(time.perf_counter() - t0)
|
||||
last_sent = now
|
||||
await asyncio.to_thread(time.sleep, 0.001)
|
||||
# cập nhật stall_until từ scheduler
|
||||
if not stall_q.empty():
|
||||
stall_until = stall_q.get_nowait()
|
||||
nonlocal send_time_stats, pump_iter_stats
|
||||
send_time_stats = send_times
|
||||
pump_iter_stats = iter_times
|
||||
|
||||
stall_q = asyncio.Queue()
|
||||
stall_state = {"until": 0.0}
|
||||
|
||||
async def consumer():
|
||||
if os.environ.get("FXRT_PROBE_PLAIN_RECV"):
|
||||
while not stop.is_set():
|
||||
try:
|
||||
msg = await ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if isinstance(msg, bytes) and len(msg) == BLOCK * 2 * 4:
|
||||
now = time.perf_counter()
|
||||
pk = float(np.frombuffer(msg, dtype=np.float32)[::2].max())
|
||||
async with recv_lock:
|
||||
recv_times.append((len(recv_times), now, pk))
|
||||
return
|
||||
while not stop.is_set():
|
||||
try:
|
||||
msg = await asyncio.wait_for(ws.recv(), timeout=1.0)
|
||||
except asyncio.TimeoutError:
|
||||
if os.environ.get("FXRT_PROBE_CONSUMER_STALL") and \
|
||||
time.perf_counter() < stall_state["until"]:
|
||||
# main-thread busy: WS message nằm trong browser queue, main
|
||||
# không decode/postMessage → worklet không nhận gì.
|
||||
await asyncio.to_thread(time.sleep, 0.002)
|
||||
continue
|
||||
try:
|
||||
msg = await ws.recv()
|
||||
except Exception:
|
||||
break
|
||||
if isinstance(msg, bytes) and len(msg) == BLOCK * 2 * 4:
|
||||
@@ -308,7 +330,7 @@ async def run_ws_probe(port, chain, secs, stall):
|
||||
over = g[g > 1.35 * BLOCK_PERIOD * 1e3]
|
||||
print(" gaps>%.1fms: %d (%.2f%%)" % (1.35 * BLOCK_PERIOD * 1e3, len(over),
|
||||
100.0 * len(over) / len(g) if len(g) else 0))
|
||||
sim = WorkletSim()
|
||||
sim = WorkletSim(mode=os.environ.get("FXRT_PROBE_WORKLET", "cut"))
|
||||
sim.arrivals = times
|
||||
sim.run(dur)
|
||||
print(" " + sim.report())
|
||||
|
||||
Reference in New Issue
Block a user