V45.3 fix Qin tremolo khi solo track khac: activeTracksRef luon ghi state da patch (bo restore STALE) - D18b guard/scheduler thay solo-out ngay, v=202608291749

This commit is contained in:
2026-08-29 17:50:12 +07:00
parent f3fd0109a7
commit 788a371f72
7 changed files with 3517 additions and 14 deletions
+10 -9
View File
@@ -1907,7 +1907,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=202608291721`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608291749`);
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);
@@ -18937,6 +18937,12 @@ const App = () => {
const applyAllTrackMuteSolo = (trackId, patch) => {
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
const effective = (trackId && patch) ? list.map(t => t.id === trackId ? { ...t, ...patch } : t) : list;
// V45.3: LUON ghi ref voi state da patch (mute/solo). Truoc day chi ghi trong
// nhanh becameAudible roi restore STALE -> solo-out khong co track became
// audible -> D18b guard (25423) + scheduler (25498) + toggleTrackSoloEvaluate
// (setTimeout 60ms restart) thay trang thai PRE-solo -> Qin note van fire
// (tremolo). Render ke tiep tu dong dong bo lai ref.
activeTracksRef.current = effective;
let becameAudible = false;
effective.forEach(t => {
const audible = computeTrackAudibleGain(effective, t) > 0;
@@ -19011,13 +19017,8 @@ const App = () => {
// startTrackPlayback (isPlayable) → câm + CC11 không chạy. Dùng
// `effective` (đã patch mute/solo) làm nguồn restart; ref tự phục hồi
// ở render kế tiếp (activeTracksRef.current = activeTracks).
const _prevActiveTracks = activeTracksRef.current;
activeTracksRef.current = effective;
try {
startTrackPlayback(resumeAt);
} finally {
activeTracksRef.current = _prevActiveTracks;
}
// V45.3: ref da = effective (dau ham) -> restart thay trang thai moi ngay.
startTrackPlayback(resumeAt);
} catch (e) {
console.warn('mute/solo resume error:', e);
}
@@ -24820,7 +24821,7 @@ const App = () => {
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608291721`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608291749`);
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
entry.node = node;
const u = new URL(API_BASE_URL);
+10 -4
View File
@@ -339,7 +339,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:!!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=202608291721`);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://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
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=45.3`);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://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong
// co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung
// thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback.
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${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.flushing=false;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
@@ -1321,7 +1321,12 @@ window.__setTrackMasteringBypass=function(trackId,bypass){trackAudioBypassMap[tr
// track node in the current context. Called from the M/S buttons of the
// mixer strips + track strips so toggling muting/soloing affects the items
// already playing on that track immediately (20ms crossfade, no click).
const applyAllTrackMuteSolo=(trackId,patch)=>{const list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;const effective=trackId&&patch?list.map(t=>t.id===trackId?{...t,...patch}:t):list;let becameAudible=false;effective.forEach(t=>{const audible=computeTrackAudibleGain(effective,t)>0;const wasAudible=trackAudibleRef.current[t.id]!==false;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(effective,t);if(node)setTrackNodeGain(node,audibleGain);// Section sub-nodes (key: <parent>_sub_<trackId>) — mute/solo của
const applyAllTrackMuteSolo=(trackId,patch)=>{const list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;const effective=trackId&&patch?list.map(t=>t.id===trackId?{...t,...patch}:t):list;// V45.3: LUON ghi ref voi state da patch (mute/solo). Truoc day chi ghi trong
// nhanh becameAudible roi restore STALE -> solo-out khong co track became
// audible -> D18b guard (25423) + scheduler (25498) + toggleTrackSoloEvaluate
// (setTimeout 60ms restart) thay trang thai PRE-solo -> Qin note van fire
// (tremolo). Render ke tiep tu dong dong bo lai ref.
activeTracksRef.current=effective;let becameAudible=false;effective.forEach(t=>{const audible=computeTrackAudibleGain(effective,t)>0;const wasAudible=trackAudibleRef.current[t.id]!==false;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(effective,t);if(node)setTrackNodeGain(node,audibleGain);// Section sub-nodes (key: <parent>_sub_<trackId>) — mute/solo của
// SECTION-TAB phải áp gain cho node play section — CHỈ trong section
// (activeTracksRef = tracks section), KHÔNG đụng track MAIN.
Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+t.id)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)setTrackNodeGain(sn,audibleGain);}});if(node&&node.sfEntry){try{node.sfEntry.gain.setTargetAtTime(audibleGain,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: CC7 (channel volume) realtime — even for notes
@@ -1345,7 +1350,8 @@ stopAllPlayback();const ctx=getAudioContext();const resumeAt=currentTimeRef.curr
// startTrackPlayback (isPlayable) → câm + CC11 không chạy. Dùng
// `effective` (đã patch mute/solo) làm nguồn restart; ref tự phục hồi
// ở render kế tiếp (activeTracksRef.current = activeTracks).
const _prevActiveTracks=activeTracksRef.current;activeTracksRef.current=effective;try{startTrackPlayback(resumeAt);}finally{activeTracksRef.current=_prevActiveTracks;}}catch(e){console.warn('mute/solo resume error:',e);}}updateSfRouting();};window.__applyTrackMuteSolo=applyAllTrackMuteSolo;window.__toggleMediaExplorerRef=function(){setShowMediaExplorer(function(p){const next=!p;if(next)setShowMixerRef.current(false);return next;});};const[mixerHeight,setMixerHeight]=useState(function(){var saved=localStorage.getItem('studio_mixer_height');return saved?parseInt(saved):200;}());const[mediaExplorerPanelHeight,setMediaExplorerPanelHeight]=useState(function(){var saved=localStorage.getItem('studio_media_explorer_height');return saved?parseInt(saved):240;}());const[masterVolume,setMasterVolume]=useState(0);// dB (legacy single fader)
// V45.3: ref da = effective (dau ham) -> restart thay trang thai moi ngay.
startTrackPlayback(resumeAt);}catch(e){console.warn('mute/solo resume error:',e);}}updateSfRouting();};window.__applyTrackMuteSolo=applyAllTrackMuteSolo;window.__toggleMediaExplorerRef=function(){setShowMediaExplorer(function(p){const next=!p;if(next)setShowMixerRef.current(false);return next;});};const[mixerHeight,setMixerHeight]=useState(function(){var saved=localStorage.getItem('studio_mixer_height');return saved?parseInt(saved):200;}());const[mediaExplorerPanelHeight,setMediaExplorerPanelHeight]=useState(function(){var saved=localStorage.getItem('studio_media_explorer_height');return saved?parseInt(saved):240;}());const[masterVolume,setMasterVolume]=useState(0);// dB (legacy single fader)
const[masterVolL,setMasterVolL]=useState(0);// dB - master L fader
const[masterVolR,setMasterVolR]=useState(0);// dB - master R fader
const[masterLink,setMasterLink]=useState(true);// LINK L/R
@@ -1930,7 +1936,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:!!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,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608291721`);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://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
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,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=45.3`);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://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1
const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${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(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='flush_done'){entry.flushing=false;if(entry.flushTimer){clearTimeout(entry.flushTimer);entry.flushTimer=null;}if(entry.node)entry.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;if(entry.flushing)return;// V24: output stale trong luc flush — drop
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==='flush'){// V24: nhu master — worklet underrun-resume → flush engine ring