fix(fxrt): toggle VST bypass khong reload plugin - cache instance trong bridge, chain giu slot bypassed, bypass state trong chainKey, poll 100ms + fade-in 512 samples che gap latency

This commit is contained in:
2026-08-24 16:56:10 +07:00
parent 3a3f46b7d3
commit 59e4951912
6 changed files with 76 additions and 51 deletions
+14 -14
View File
@@ -1668,7 +1668,7 @@ const FXRT_BURST_MS = 21;
// 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('|');
return (chain || []).map(s => (s.type || '') + ':' + (s.type === 'vst3' ? (s.path || '') + '#' + ((s.preset_b64 || '').slice(0, 16)) : (s.type || '')) + '@' + (s.bypass ? '1' : '0')).join('|') + '|vb:' + (window.currentMasteringSettings && window.currentMasteringSettings.vstBypass ? '1' : '0');
}
function fxRtChain() {
const ms = window.currentMasteringSettings;
@@ -1678,7 +1678,7 @@ function fxRtChain() {
// s.preset_b64 thì preset vừa chỉnh trong GUI chưa kịp persist → bridge load
// default (passthrough) → power ON nghe như OFF. Merge ở đây làm chainKey đổi
// → fxRtStart tự restart session với preset thật.
return (ms.chain || []).filter(s => s && s.active !== false && (s.type !== 'vst3' || (!ms.vstBypass && s.path && !s.bypass)))
return (ms.chain || []).filter(s => s && s.active !== false && (s.type !== 'vst3' || s.path))
.map(s => s.type === 'vst3' && _capturedPresetByPath[s.path] ? { ...s, preset_b64: _capturedPresetByPath[s.path] } : s);
}
// Param master chain entry → C++ BuiltinFx key (mirror Python _BUILTIN_PARAM_MAP).
@@ -1724,8 +1724,8 @@ async function fxRtSetChain(chain) {
const sid = fxRt.sessionId;
if (!sid || !fxRt.ws || fxRt.ws.readyState !== 1) 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 });
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: !!s.bypass || !!((window.currentMasteringSettings || {}).vstBypass) }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: !!s.bypass });
try {
await window.SonicAPI.fxRealtimeSetChain({ session_id: sid, fx_chain: chainPayload });
try { fxRtPushAllParams(); } catch (e) {}
@@ -1756,8 +1756,8 @@ async function fxRtStart() {
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 });
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: !!s.bypass || !!((window.currentMasteringSettings || {}).vstBypass) }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: !!s.bypass });
let st;
try {
st = await window.SonicAPI.fxRealtimeStart({ fx_chain: chainPayload, sample_rate: ctx.sampleRate || 44100 });
@@ -2016,7 +2016,7 @@ function fxRtGuiPushShm() {
}
function fxRtTrackChain(track) {
if (!track) return [];
let chain = (track.fxChain || []).filter(s => s && s.active !== false && (s.type !== 'vst3' || (!track.vstFxBypass && s.path && !s.bypass)));
let chain = (track.fxChain || []).filter(s => s && s.active !== false && (s.type !== 'vst3' || s.path));
// Legacy: project cũ chỉ có vstFxChain riêng — append sau builtin.
if (!chain.some(s => s && s.type === 'vst3') && !track.vstFxBypass) {
chain = chain.concat(activeVstChain(track.vstFxChain || []));
@@ -2027,7 +2027,7 @@ function fxRtTrackChain(track) {
return chain.map(s => s.type === 'vst3' && _capturedPresetByPath[s.path] ? { ...s, preset_b64: _capturedPresetByPath[s.path] } : s);
}
function fxRtTrackChainKey(chain) {
return (chain || []).map(s => (s.type || '') + ':' + (s.type === 'vst3' ? (s.path || '') + '#' + ((s.preset_b64 || '').slice(0, 16)) : (s.type || ''))).join('|');
return (chain || []).map(s => (s.type || '') + ':' + (s.type === 'vst3' ? (s.path || '') + '#' + ((s.preset_b64 || '').slice(0, 16)) : (s.type || '')) + '@' + (s.bypass ? '1' : '0')).join('|');
}
function fxRtTrackPaths(track) {
const paths = [];
@@ -23890,8 +23890,8 @@ const App = () => {
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 });
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: !!s.bypass || !!(track && track.vstFxBypass) }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: !!s.bypass });
let st;
try {
st = await window.SonicAPI.fxRealtimeStart({ fx_chain: chainPayload, sample_rate: ctx.sampleRate || 44100 });
@@ -23899,7 +23899,7 @@ const App = () => {
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 };
const entry = { sessionId: st.session_id, ws: null, node: null, ready: false, path, spliced: null, chainKey: fxRtTrackChainKey(chain) + '|vb:' + (track && track.vstFxBypass ? '1' : '0'), paramsBySlot: chain.map(s => fxRtEntryParams(s, null)), inQueue: [], pumpTimer: null, lastInSent: 0 };
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
@@ -23993,8 +23993,8 @@ const App = () => {
const chain = fxRtTrackChain(track);
if (!chain.length) 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 });
? { type: 'vst3', path: s.path, name: s.name || s.path, preset_b64: s.preset_b64 || '', bypass: !!s.bypass || !!(track && track.vstFxBypass) }
: { type: 'builtin', id: s.type, params: (s.params && Object.keys(s.params).length ? s.params : {}), bypass: !!s.bypass });
entry.chainKey = chainKey;
window.SonicAPI.fxRealtimeSetChain({ session_id: entry.sessionId, fx_chain: chainPayload }).then(() => {
try { (entry.paramsBySlot || []).forEach((pl, slot) => pl.forEach(p => { if (entry.ws && entry.ws.readyState === 1) entry.ws.send(JSON.stringify({ cmd: 'set_param', slot, key: p.key, value: p.value })); })); } catch (e) {}
@@ -24037,7 +24037,7 @@ const App = () => {
const fxRtTrackReconcile = (trackId, node, track) => {
try {
const chain = fxRtTrackChain(track);
const chainKey = fxRtTrackChainKey(chain);
const chainKey = fxRtTrackChainKey(chain) + '|vb:' + (track && track.vstFxBypass ? '1' : '0');
const wantPaths = chain.length ? fxRtTrackPaths(track) : [];
const prefix = trackId + ':';
Object.keys(fxRtTracks).forEach(k => {
+8 -24
View File
@@ -296,13 +296,13 @@ let nativeGuiOpenUntilTs=0;// Pace input realtime: worklet sản xuất đúng r
const FXRT_BURST_MS=21;// 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.
function fxRtChainKey(chain){return(chain||[]).map(s=>(s.type||'')+':'+(s.type==='vst3'?(s.path||'')+'#'+(s.preset_b64||'').slice(0,16):s.type||'')+'@'+(s.bypass?'1':'0')).join('|')+'|vb:'+(window.currentMasteringSettings&&window.currentMasteringSettings.vstBypass?'1':'0');}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.
// preset_b64: merge preset GUI mới nhất (_capturedPresetByPath) — nếu chỉ dùng
// s.preset_b64 thì preset vừa chỉnh trong GUI chưa kịp persist → bridge load
// default (passthrough) → power ON nghe như OFF. Merge ở đây làm chainKey đổi
// → fxRtStart tự restart session với preset thật.
return(ms.chain||[]).filter(s=>s&&s.active!==false&&(s.type!=='vst3'||!ms.vstBypass&&s.path&&!s.bypass)).map(s=>s.type==='vst3'&&_capturedPresetByPath[s.path]?{...s,preset_b64:_capturedPresetByPath[s.path]}:s);}// Param master chain entry → C++ BuiltinFx key (mirror Python _BUILTIN_PARAM_MAP).
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 fxRtSetChain(chain){const sid=fxRt.sessionId;if(!sid||!fxRt.ws||fxRt.ws.readyState!==1)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});try{await window.SonicAPI.fxRealtimeSetChain({session_id:sid,fx_chain:chainPayload});try{fxRtPushAllParams();}catch(e){}}catch(e){console.warn('[FX RT] set-chain fail:',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.
return(ms.chain||[]).filter(s=>s&&s.active!==false&&(s.type!=='vst3'||s.path)).map(s=>s.type==='vst3'&&_capturedPresetByPath[s.path]?{...s,preset_b64:_capturedPresetByPath[s.path]}:s);}// Param master chain entry → C++ BuiltinFx key (mirror Python _BUILTIN_PARAM_MAP).
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 fxRtSetChain(chain){const sid=fxRt.sessionId;if(!sid||!fxRt.ws||fxRt.ws.readyState!==1)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:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});try{await window.SonicAPI.fxRealtimeSetChain({session_id:sid,fx_chain:chainPayload});try{fxRtPushAllParams();}catch(e){}}catch(e){console.warn('[FX RT] set-chain fail:',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 thay đổi (insert/remove/bypass/reorder/preset GUI) — KHÔNG restart
// (ngắt âm + crackle): set_chain lên session ĐANG CHẠY → bridge worker
// load chain mới (audio giữ chain cũ tới khi swap) → swap atomic mượt.
@@ -311,7 +311,7 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re
// `else if (fxRt.active) fxRtStop()` KHÔNG chạy → start in-flight vẫn nối
// 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.
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:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});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=202608242000`);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).
@@ -375,11 +375,11 @@ const fxRtTracks={};// 'trackId:audio|sf' → { sessionId, ws, node, ready, path
// Embedded VST GUI → live SHM session feed (open_fx_gui shm + bridge /shm route).
// url → trackId|null (null = master GUI). The GUI process peeks the session's
// input ring so its meters move with real audio; push on every start/stop.
const _fxGuiEmbedSources={};function _fxGuiRegisterSource(url,trackId){if(!url)return;_fxGuiEmbedSources[url]=trackId===undefined?null:trackId;}function _fxGuiUnregisterSource(url){if(url)delete _fxGuiEmbedSources[url];}function fxRtTrackShmName(trackId){const key=trackId?Object.keys(fxRtTracks).find(k=>k.startsWith(trackId+':')):'';return key?fxRtTracks[key].sessionId||'':'';}function fxRtGuiPushShm(){Object.keys(_fxGuiEmbedSources).forEach(url=>{const trackId=_fxGuiEmbedSources[url];const name=trackId===null?fxRt.sessionId||'':fxRtTrackShmName(trackId);try{fetch(url+'/shm',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({shm:name})}).catch(()=>{});}catch(e){}});}function fxRtTrackChain(track){if(!track)return[];let chain=(track.fxChain||[]).filter(s=>s&&s.active!==false&&(s.type!=='vst3'||!track.vstFxBypass&&s.path&&!s.bypass));// Legacy: project cũ chỉ có vstFxChain riêng — append sau builtin.
const _fxGuiEmbedSources={};function _fxGuiRegisterSource(url,trackId){if(!url)return;_fxGuiEmbedSources[url]=trackId===undefined?null:trackId;}function _fxGuiUnregisterSource(url){if(url)delete _fxGuiEmbedSources[url];}function fxRtTrackShmName(trackId){const key=trackId?Object.keys(fxRtTracks).find(k=>k.startsWith(trackId+':')):'';return key?fxRtTracks[key].sessionId||'':'';}function fxRtGuiPushShm(){Object.keys(_fxGuiEmbedSources).forEach(url=>{const trackId=_fxGuiEmbedSources[url];const name=trackId===null?fxRt.sessionId||'':fxRtTrackShmName(trackId);try{fetch(url+'/shm',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({shm:name})}).catch(()=>{});}catch(e){}});}function fxRtTrackChain(track){if(!track)return[];let chain=(track.fxChain||[]).filter(s=>s&&s.active!==false&&(s.type!=='vst3'||s.path));// Legacy: project cũ chỉ có vstFxChain riêng — append sau builtin.
if(!chain.some(s=>s&&s.type==='vst3')&&!track.vstFxBypass){chain=chain.concat(activeVstChain(track.vstFxChain||[]));}// preset_b64: merge preset GUI mới nhất (giống fxRtChain master) — nếu chỉ
// dùng s.preset_b64 thì preset chỉnh trong GUI chưa kịp persist → bridge load
// default (passthrough). chainKey đổi → fxRtTrackStart tự restart session.
return chain.map(s=>s.type==='vst3'&&_capturedPresetByPath[s.path]?{...s,preset_b64:_capturedPresetByPath[s.path]}:s);}function fxRtTrackChainKey(chain){return(chain||[]).map(s=>(s.type||'')+':'+(s.type==='vst3'?(s.path||'')+'#'+(s.preset_b64||'').slice(0,16):s.type||'')).join('|');}function fxRtTrackPaths(track){const paths=[];if(track&&(track.clips&&track.clips.length>0||!!track.buffer||track.sections&&track.sections.length>0))paths.push('audio');if(track&&(track.midiItems&&track.midiItems.length>0||track.type==='MIDI'||!!track.instrumentId))paths.push('sf');return paths;}// Track-EQ preset library (unified_fx_rack_panel.md §III.1) — band gains only.
return chain.map(s=>s.type==='vst3'&&_capturedPresetByPath[s.path]?{...s,preset_b64:_capturedPresetByPath[s.path]}:s);}function fxRtTrackChainKey(chain){return(chain||[]).map(s=>(s.type||'')+':'+(s.type==='vst3'?(s.path||'')+'#'+(s.preset_b64||'').slice(0,16):s.type||'')+'@'+(s.bypass?'1':'0')).join('|');}function fxRtTrackPaths(track){const paths=[];if(track&&(track.clips&&track.clips.length>0||!!track.buffer||track.sections&&track.sections.length>0))paths.push('audio');if(track&&(track.midiItems&&track.midiItems.length>0||track.type==='MIDI'||!!track.instrumentId))paths.push('sf');return paths;}// Track-EQ preset library (unified_fx_rack_panel.md §III.1) — band gains only.
const TRACK_EQ_PRESETS={flat:{name:'Flat / Reset',g:[0,0,0,0]},vocal_clarity:{name:'Vocal Unmask & Clarity',g:[-2.5,-1.8,3.2,2.0]},bass_punch:{name:'EDM Low-End Punch',g:[4.0,-3.0,1.5,1.0]},warm_tape:{name:'Warm Vintage Analog',g:[2.0,1.0,-2.0,-3.0]},guitar_edge:{name:'Guitar Cut & Presence',g:[-3.0,-2.0,3.5,1.5]}};// Dynamic signal-chain reconstruction (mastering_expand.md §II.3):
// disconnect every module boundary, then wire the ACTIVE modules in series
// between inputAnalyser (chain input) and outputAnalyser (chain output).
@@ -1735,13 +1735,13 @@ 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=202608242000`);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:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});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)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),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=202608242000`);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
// atomic khi worker load xong) → load VST FX / đổi preset GUI không ngắt
// âm/crackle. entry.chainKey cập nhật ngay — reconcile sau đó thấy khớp.
const fxRtTrackSetChain=(trackId,path,track,chainKey)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];if(!entry||!entry.ws||entry.ws.readyState!==1)return;const chain=fxRtTrackChain(track);if(!chain.length)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});entry.chainKey=chainKey;window.SonicAPI.fxRealtimeSetChain({session_id:entry.sessionId,fx_chain:chainPayload}).then(()=>{try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>{if(entry.ws&&entry.ws.readyState===1)entry.ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}));}));}catch(e){}}).catch(e=>console.warn('[FX RT track] set-chain fail',trackId,path,e));};const fxRtTrackSplice=(trackId,path)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];const node=activeTrackNodesRef.current[trackId];if(!entry||!entry.ready||!entry.node||!node||entry.spliced)return;const tail=path==='audio'?node.fxEntry:node.sfEntry;const dest=path==='audio'?node.fxLegacyIn:node.sfOut;if(!tail||!dest)return;try{tail.disconnect(dest);entry.node.connect(dest);tail.connect(entry.node);entry.spliced={tail,dest};console.log('[FX RT track] spliced',trackId,path);}catch(e){console.warn('[FX RT track] splice fail',trackId,path,e);}};const fxRtTrackStop=(trackId,path,silent)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];if(!entry)return;if(entry.pumpTimer){clearInterval(entry.pumpTimer);entry.pumpTimer=null;}entry.inQueue=[];try{fxRtTrackUnsplice(trackId,path);if(entry.node){entry.node.port.onmessage=null;try{entry.node.disconnect();}catch(e){}entry.node=null;}}catch(e){}try{if(entry.ws){entry.ws.onclose=null;entry.ws.onmessage=null;try{entry.ws.close();}catch(e){}entry.ws=null;}}catch(e){}const sid=entry.sessionId;delete fxRtTracks[key];fxRtGuiPushShm();if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}};const fxRtTrackReconcile=(trackId,node,track)=>{try{const chain=fxRtTrackChain(track);const chainKey=fxRtTrackChainKey(chain);const wantPaths=chain.length?fxRtTrackPaths(track):[];const prefix=trackId+':';Object.keys(fxRtTracks).forEach(k=>{if(!k.startsWith(prefix))return;const p=k.slice(prefix.length);if(wantPaths.indexOf(p)<0)fxRtTrackStop(trackId,p,true);});if(!isPlayingRef.current)return;wantPaths.forEach(p=>{const key=trackId+':'+p;const entry=fxRtTracks[key];if(!entry)fxRtTrackStart(trackId,track,p);else if(entry.chainKey!==chainKey){// Chain đổi (load/đổi VST FX, preset GUI, bypass slot) — session
const fxRtTrackSetChain=(trackId,path,track,chainKey)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];if(!entry||!entry.ws||entry.ws.readyState!==1)return;const chain=fxRtTrackChain(track);if(!chain.length)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:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});entry.chainKey=chainKey;window.SonicAPI.fxRealtimeSetChain({session_id:entry.sessionId,fx_chain:chainPayload}).then(()=>{try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>{if(entry.ws&&entry.ws.readyState===1)entry.ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}));}));}catch(e){}}).catch(e=>console.warn('[FX RT track] set-chain fail',trackId,path,e));};const fxRtTrackSplice=(trackId,path)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];const node=activeTrackNodesRef.current[trackId];if(!entry||!entry.ready||!entry.node||!node||entry.spliced)return;const tail=path==='audio'?node.fxEntry:node.sfEntry;const dest=path==='audio'?node.fxLegacyIn:node.sfOut;if(!tail||!dest)return;try{tail.disconnect(dest);entry.node.connect(dest);tail.connect(entry.node);entry.spliced={tail,dest};console.log('[FX RT track] spliced',trackId,path);}catch(e){console.warn('[FX RT track] splice fail',trackId,path,e);}};const fxRtTrackStop=(trackId,path,silent)=>{const key=trackId+':'+path;const entry=fxRtTracks[key];if(!entry)return;if(entry.pumpTimer){clearInterval(entry.pumpTimer);entry.pumpTimer=null;}entry.inQueue=[];try{fxRtTrackUnsplice(trackId,path);if(entry.node){entry.node.port.onmessage=null;try{entry.node.disconnect();}catch(e){}entry.node=null;}}catch(e){}try{if(entry.ws){entry.ws.onclose=null;entry.ws.onmessage=null;try{entry.ws.close();}catch(e){}entry.ws=null;}}catch(e){}const sid=entry.sessionId;delete fxRtTracks[key];fxRtGuiPushShm();if(sid){try{window.SonicAPI.fxRealtimeStop({session_id:sid}).catch(()=>{});}catch(e){}}};const fxRtTrackReconcile=(trackId,node,track)=>{try{const chain=fxRtTrackChain(track);const chainKey=fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0');const wantPaths=chain.length?fxRtTrackPaths(track):[];const prefix=trackId+':';Object.keys(fxRtTracks).forEach(k=>{if(!k.startsWith(prefix))return;const p=k.slice(prefix.length);if(wantPaths.indexOf(p)<0)fxRtTrackStop(trackId,p,true);});if(!isPlayingRef.current)return;wantPaths.forEach(p=>{const key=trackId+':'+p;const entry=fxRtTracks[key];if(!entry)fxRtTrackStart(trackId,track,p);else if(entry.chainKey!==chainKey){// Chain đổi (load/đổi VST FX, preset GUI, bypass slot) — session
// đang chạy → set_chain KHÔNG restart (bridge swap atomic khi load
// xong) → hết crackle khi load VST / chỉnh thông số. Session chưa
// ready (start in-flight) → stop+start lại cho đúng chain.
@@ -2343,19 +2343,3 @@ instrumentSelectorData?.vst_instruments?.length>0&&/*#__PURE__*/React.createElem
// ruler slips away from the cursor mid-scroll. Native capture listener (non-passive)
// blocks scrolling whenever the wheel starts over a [data-sf-wheel] container.
document.addEventListener('wheel',e=>{const t=e.target;if(t&&t.closest&&t.closest('[data-sf-wheel]'))e.preventDefault();},{passive:false,capture:true});const root=ReactDOM.createRoot(document.getElementById('root'));root.render(/*#__PURE__*/React.createElement(App,null));setTimeout(()=>lucide.createIcons(),300);
(function(){
if (document.getElementById('fxrt-stats-ui')) return;
var iv = setInterval(function(){
if (document.getElementById('fxrt-stats-ui')) return;
var s = fxRt && fxRt.stats;
var el = document.getElementById('fxrt-stats-overlay');
if (!el) {
el = document.createElement('div');
el.id = 'fxrt-stats-overlay';
el.style.cssText = 'position:fixed;top:0;left:50%;transform:translateX(-50%);z-index:99999;font:10px Consolas,monospace;background:#0f172a;border:1px solid #334155;color:#34d399;padding:2px 10px;border-radius:0 0 8px 8px;white-space:nowrap;pointer-events:none;';
document.body.appendChild(el);
}
el.textContent = s ? ('FXRT u='+(s.underruns||0)+' h='+(s.holds||0)+' f='+(s.fades||0)+' s='+(s.silences||0)+' q='+(s.queueLen||0)+' cap='+(s.capDrops||0)+' fadeIn='+(s.fadeIns||0)+(((s.underruns||0)>0||(s.silences||0)>0)?' WARN':' ok')) : 'FXRT off';
}, 1000);
})();
+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=202608241630" defer></script>
<script src="/static/js/app.precompiled.js?v=202608241700" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+15 -2
View File
@@ -198,7 +198,7 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
});
// Chain update (set_chain): engine ghi lại job file seq++ — poll mỗi
// 500ms; seq đổi → setChain ASYNC (worker load chain mới, swap atomic
// 100ms; seq đổi → setChain ASYNC (worker load chain mới, swap atomic
// dưới mutex — audio thread copy shared_ptr, không dừng) → load/đổi VST
// FX + đổi preset GUI không ngắt âm/crackle.
uint64_t lastSeq = (uint64_t)jsonNumber(job, "seq", 0);
@@ -209,6 +209,8 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
uint32_t outMask = ipc->h.outSlots - 1;
uint64_t processed = 0;
uint64_t lastGen = 0; // chain generation đã báo latency
uint32_t fadeLeft = 0; // output fade-in còn lại sau chain swap (che gap latency jump)
const uint32_t fadeTotal = 512; // ~11.6ms @44.1k
// ---- FXRT_PERF instrumentation (tạm, đo crackle) ----
uint64_t perfIter = 0, perfProc = 0, perfIdle = 0, perfOutFull = 0;
double perfProcSum = 0.0, perfProcMax = 0.0, perfProcMin = 1e9;
@@ -224,7 +226,7 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
}
{
const auto nowT = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowT - lastPoll).count() >= 500) {
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowT - lastPoll).count() >= 100) {
lastPoll = nowT;
const std::string j2 = readFile(jobPath);
if (!j2.empty()) {
@@ -266,6 +268,7 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
ipc->h.latWrite++;
}
lastGen = gen;
fadeLeft = fadeTotal;
std::cerr << "[RealtimeFxLoop] latency reported: " << lats.size()
<< " slot(s)" << std::endl;
}
@@ -314,6 +317,16 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
if (perfProcMs > perfProcMax) perfProcMax = perfProcMs;
if (perfProcMs < perfProcMin) perfProcMin = perfProcMs;
for (uint32_t i = 0, o = 0; i < take; ++i, o += n) {
if (fadeLeft > 0) {
const uint32_t f = std::min<uint32_t>(fadeLeft, n);
const float start = (float)(fadeTotal - fadeLeft);
for (uint32_t k = 0; k < f; ++k) {
const float g = (start + (float)k) / (float)fadeTotal;
L[o + k] *= g;
R[o + k] *= g;
}
fadeLeft -= f;
}
const uint32_t oslot = ipc->h.outWrite & outMask;
std::memcpy(ipc->outL[oslot], L + o, n * sizeof(float));
std::memcpy(ipc->outR[oslot], R + o, n * sizeof(float));
+38 -10
View File
@@ -1244,6 +1244,12 @@ struct RealtimeFxChain::Impl {
double sampleRate_ = 44100.0;
int32_t blockSize_ = 256;
// VST3 instance cache: giữ plugin đã load qua các lần buildChain để toggle
// bypass không phải load lại từ disk (tránh delay dry->wet). Key = path + #occ.
std::map<std::string, std::shared_ptr<Vst3Fx>> cache_;
std::map<std::string, std::string> lastPreset_; // preset b64 da ap (tranh ap lai)
std::map<std::string, int> occ_; // so lan xuat hien cua path trong chain
std::shared_ptr<Chain> buildChain(const std::string& json);
void workerLoop();
};
@@ -1332,12 +1338,13 @@ std::vector<uint32_t> RealtimeFxChain::entryLatencies() {
if (!c) return out;
out.reserve(c->entries.size());
for (const auto& e : c->entries)
out.push_back(e.fx ? (uint32_t)e.fx->latencySamples() : 0u);
out.push_back((!e.bypass && e.fx) ? (uint32_t)e.fx->latencySamples() : 0u);
return out;
}
std::shared_ptr<RealtimeFxChain::Impl::Chain> RealtimeFxChain::Impl::buildChain(const std::string& json) {
auto chain = std::make_shared<Chain>();
occ_.clear(); // key theo vị trí slot trong CHAIN NÀY — reset mỗi build
json_parse_result_s pres = {};
json_value_s* root = json_parse_ex(json.data(), json.size(), json_parse_flags_default,
nullptr, nullptr, &pres);
@@ -1373,21 +1380,42 @@ std::shared_ptr<RealtimeFxChain::Impl::Chain> RealtimeFxChain::Impl::buildChain(
if (path.empty()) continue;
Entry ent;
ent.bypass = memberBool(o, "bypass", false);
const std::string preset = memberString(o, "preset_b64", "");
// Key ổn định theo vị trí slot: đếm MỌI lần path xuất hiện (kể cả
// bypassed) — toggle bypass không đổi key → không reload nhầm instance.
const std::string ckey = path + "#" + std::to_string(occ_[path]++);
if (!ent.bypass) {
auto fx = std::make_shared<Vst3Fx>();
if (fx->load(path, sampleRate_, blockSize_)) {
const std::string preset = memberString(o, "preset_b64", "");
if (!preset.empty()) {
if (fx->applyPreset(preset))
std::cerr << "[RealtimeFx] preset applied: " << path << std::endl;
auto it = cache_.find(ckey);
if (it != cache_.end()) {
ent.fx = it->second; // đã load: reuse ngay, khong reload
const std::string last = lastPreset_[ckey];
if (!preset.empty() && preset != last) {
if (ent.fx->applyPreset(preset))
std::cerr << "[RealtimeFx] preset applied (cached): " << path << std::endl;
else
std::cerr << "[RealtimeFx] preset apply FAILED (default kept): " << path << std::endl;
lastPreset_[ckey] = preset;
}
ent.fx = std::move(fx);
if (preset.empty()) lastPreset_[ckey] = "";
}
else {
std::cerr << "[RealtimeFx] load failed — slot bypassed: " << path << std::endl;
ent.bypass = true;
auto fx = std::make_shared<Vst3Fx>();
if (fx->load(path, sampleRate_, blockSize_)) {
if (!preset.empty()) {
if (fx->applyPreset(preset))
std::cerr << "[RealtimeFx] preset applied: " << path << std::endl;
else
std::cerr << "[RealtimeFx] preset apply FAILED (default kept): " << path << std::endl;
}
ent.fx = fx;
cache_[ckey] = fx;
lastPreset_[ckey] = preset;
}
else {
std::cerr << "[RealtimeFx] load failed — slot bypassed: " << path << std::endl;
ent.bypass = true;
occ_[path]--; // khong dem lan that bai
}
}
}
chain->entries.push_back(std::move(ent));
Binary file not shown.