V45.6 fix solo Q3: bo stopAllPlayback+restart 60ms khi solo main-session (track solo giu nguyen tieng, het wobble re-attack); GIU timelinePitchesRef qua stop - kill branch NOTE_OFF tung pitch (Qin im khi solo track khac, truoc day tset=0); C++ allNotesOff sandbox VST2 qua control 7 (child panic local CC64/CC123+128 noteOff) thay 128 note-off flood queue 64-cap (Qin het treo sau STOP); IPC toggleSolo tu applyAllTrackMuteSolo (ref patch + kill); v=202608292138

This commit is contained in:
2026-08-29 21:39:59 +07:00
parent 72ae22f865
commit 945e63918d
12 changed files with 80 additions and 30 deletions
+37 -23
View File
@@ -26208,7 +26208,8 @@ const App = () => {
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
bridgeNoteEpochRef.current++;
try { clearAllPendingNoteOffs(); } catch (e) {}
try { timelinePitchesRef.current = {}; } catch (e) {} // D18c: moi note timeline da tat theo transport stop
// V45.6: GIU timelinePitchesRef qua stop - kill branch (solo/mute) can
// per-pitch set de NOTE_OFF tung note dang keu (Qin VST2 sandbox).
_cc11Epoch++; // CC11 timers lan play cu vo hieu (da clear o tren)
try { clearAllCC11Timers(); } catch (e) {}
try { if (subTabsRef.current) subTabsRef.current = subTabsRef.current.map(s => ({ ...s, isPlaying: false })); } catch (e) {}
@@ -28093,33 +28094,46 @@ const App = () => {
const wasPlaying = isPlaying;
try { if (window.__dbgLog) window.__dbgLog('solo_toggle', 'track=' + trackId + ' wasPlaying=' + (wasPlaying ? 1 : 0) + ' refSolo=' + ((activeTracksRef.current || []).find(function (t) { return t.id === trackId; }) || {}).solo + ' n=' + (activeTracksRef.current || []).length); } catch (e) {}
const playingSubTab = subTabs.find(s => s.isPlaying && s.type === 'PIANO_ROLL');
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
const wasSolo = !!((activeTracksRef.current || []).find(function (t) { return t.id === trackId; }) || {}).solo;
fxRtKeepForRestart = true; // restart/resume - giu session FX realtime
updateActiveTracks(prev => prev.map(t =>
t.id === trackId ? { ...t, solo: !t.solo, muted: false } : t
));
setTimeout(() => {
const soloCtx = getAudioContext();
if (wasPlaying) {
startOffsetTimeRef.current = currentTime;
startAudioTimeRef.current = soloCtx.currentTime;
startBufferOffsetRef.current = currentTime;
setIsPlaying(true);
startTrackPlayback(currentTime);
}
if (playingSubTab) {
const prSt = subTabs.find(s => s.id === playingSubTab.id);
if (prSt) {
const prOffset = prSt.currentTime || 0;
startOffsetTimeRef.current = prOffset;
if (playingSubTab) {
// PIANO_ROLL sub-tab dang play: giu stop+restart cu (scheduler sub-tab
// can restart de ap solo moi; main session khong play song song).
stopAllPlayback();
setTimeout(() => {
const soloCtx = getAudioContext();
if (wasPlaying) {
startOffsetTimeRef.current = currentTime;
startAudioTimeRef.current = soloCtx.currentTime;
startBufferOffsetRef.current = prOffset * (prSt.speed || 1.0);
schedulePianoRollMidi(prSt, prOffset);
startSubTabPlayback(prSt, prOffset);
setSubTabs(prev => prev.map(s => s.id === prSt.id ? Object.assign({}, s, { isPlaying: true, currentTime: prOffset }) : s));
startBufferOffsetRef.current = currentTime;
setIsPlaying(true);
startTrackPlayback(currentTime);
}
}
}, 60);
if (playingSubTab) {
const prSt = subTabs.find(s => s.id === playingSubTab.id);
if (prSt) {
const prOffset = prSt.currentTime || 0;
startOffsetTimeRef.current = prOffset;
startAudioTimeRef.current = soloCtx.currentTime;
startBufferOffsetRef.current = prOffset * (prSt.speed || 1.0);
schedulePianoRollMidi(prSt, prOffset);
startSubTabPlayback(prSt, prOffset);
setSubTabs(prev => prev.map(s => s.id === prSt.id ? Object.assign({}, s, { isPlaying: true, currentTime: prOffset }) : s));
}
}
}, 60);
setTimeout(() => lucide.createIcons(), 50);
return;
}
// V45.6: main-session - KHONG stopAllPlayback nua (track solo dang keu
// giu nguyen, het wobble re-attack 60ms). applyAllTrackMuteSolo tu kill
// track im (NOTE_OFF tung pitch - timelinePitchesRef giu set tu V45.5)
// + restart khi co track became audible. IPC toggleSolo chi goi ham nay
// -> tu apply luon (truoc day khong apply -> ref khong patch -> Qin khong im).
try { applyAllTrackMuteSolo(trackId, { solo: !wasSolo, muted: false }); } catch (e) {}
setTimeout(() => lucide.createIcons(), 50);
};
const toggleTrackMute = trackId => {
+13 -5
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=202608291806`);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=202608292138`);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
@@ -1936,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=202608291806`);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=202608292138`);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
@@ -2214,7 +2214,8 @@ try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}}e
try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
bridgeNoteEpochRef.current++;try{clearAllPendingNoteOffs();}catch(e){}try{timelinePitchesRef.current={};}catch(e){}// D18c: moi note timeline da tat theo transport stop
bridgeNoteEpochRef.current++;try{clearAllPendingNoteOffs();}catch(e){}// V45.6: GIU timelinePitchesRef qua stop - kill branch (solo/mute) can
// per-pitch set de NOTE_OFF tung note dang keu (Qin VST2 sandbox).
_cc11Epoch++;// CC11 timers lan play cu vo hieu (da clear o tren)
try{clearAllCC11Timers();}catch(e){}try{if(subTabsRef.current)subTabsRef.current=subTabsRef.current.map(s=>({...s,isPlaying:false}));}catch(e){}// Fade-out khi user nhấn Stop/Pause (fade=true): ramp gain track + bridge
// xuống 0 trong FADE_MS rồi mới hard-stop — hết click, âm ngân mượt.
@@ -2325,8 +2326,15 @@ useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.curr
if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ──
useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;captureSelectionUndo();if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);if(Math.abs(end-start)<0.02){setSelectedItemIds(new Set());setSweepSelect(null);pushSelectionUndo();return;}const curTracks=activeTracksRef.current||[];const found=new Set();curTracks.forEach(t=>{(t.midiItems||[]).forEach(m=>{if(m.startTime<end&&m.startTime+m.duration>start){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.start<end&&s.start+s.duration>start){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTime<end&&c.startTime+dur>start){const cid=c.id==='default'?'default_'+t.id:c.id;found.add(cid);}});});setSelectedItemIds(prev=>{const next=new Set(prev);found.forEach(id=>{if(next.has(id))next.delete(id);else next.add(id);});return next;});setSweepSelect(null);sweepTrackIdRef.current=null;pushSelectionUndo();}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);const handleSelectRange=(start,end,reset)=>{captureSelectionUndo();const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}setSelectionCleared(false);pushSelectionUndo();};const handleSelectionInputChange=(field,val)=>{captureSelectionUndo();const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){if(field==='start'){setLocalSelectionStart(numericVal);}else{setLocalSelectionEnd(numericVal);}}else{if(field==='start'){setSelectionStart(numericVal);}else{setSelectionEnd(numericVal);}}pushSelectionUndo();};const selectionStats=useMemo(()=>{if(selLeft===null||selRight===null){return{start:0,end:0,length:0};}const s=Math.min(selLeft,selRight);const e=Math.max(selLeft,selRight);return{start:parseFloat(s.toFixed(3)),end:parseFloat(e.toFixed(3)),length:parseFloat((e-s).toFixed(3))};},[selLeft,selRight]);// ── Handle Drag (selection resize) ──
const handleHandleDragStart=(e,side)=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;if(side==='left'){const newLeft=Math.max(0,Math.min(initialRight-0.05,initialLeft+deltaSec));setStart(newLeft);setEnd(initialRight);}else{const newRight=Math.max(initialLeft+0.05,Math.min(maxDuration,initialRight+deltaSec));setStart(initialLeft);setEnd(newRight);}};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};const handleSelectionBodyDragStart=e=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const widthSec=initialRight-initialLeft;const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;let newLeft=initialLeft+deltaSec;let newRight=initialRight+deltaSec;if(newLeft<0){newLeft=0;newRight=widthSec;}if(newRight>maxDuration){newRight=maxDuration;newLeft=maxDuration-widthSec;}setStart(newLeft);setEnd(newRight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Track Controls ──
const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;try{if(window.__dbgLog)window.__dbgLog('solo_toggle','track='+trackId+' wasPlaying='+(wasPlaying?1:0)+' refSolo='+((activeTracksRef.current||[]).find(function(t){return t.id===trackId;})||{}).solo+' n='+(activeTracksRef.current||[]).length);}catch(e){}const playingSubTab=subTabs.find(s=>s.isPlaying&&s.type==='PIANO_ROLL');fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime
stopAllPlayback();updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,solo:!t.solo,muted:false}:t));setTimeout(()=>{const soloCtx=getAudioContext();if(wasPlaying){startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=currentTime;setIsPlaying(true);startTrackPlayback(currentTime);}if(playingSubTab){const prSt=subTabs.find(s=>s.id===playingSubTab.id);if(prSt){const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);schedulePianoRollMidi(prSt,prOffset);startSubTabPlayback(prSt,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{isPlaying:true,currentTime:prOffset}):s));}}},60);setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,muted:!t.muted}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'MUTE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});setTimeout(()=>lucide.createIcons(),50);};const updateTrackProp=(trackId,props)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));// Mixer fader dùng updateTrackProp({volumeDb}) — áp gain realtime cho node
const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;try{if(window.__dbgLog)window.__dbgLog('solo_toggle','track='+trackId+' wasPlaying='+(wasPlaying?1:0)+' refSolo='+((activeTracksRef.current||[]).find(function(t){return t.id===trackId;})||{}).solo+' n='+(activeTracksRef.current||[]).length);}catch(e){}const playingSubTab=subTabs.find(s=>s.isPlaying&&s.type==='PIANO_ROLL');const wasSolo=!!((activeTracksRef.current||[]).find(function(t){return t.id===trackId;})||{}).solo;fxRtKeepForRestart=true;// restart/resume - giu session FX realtime
updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,solo:!t.solo,muted:false}:t));if(playingSubTab){// PIANO_ROLL sub-tab dang play: giu stop+restart cu (scheduler sub-tab
// can restart de ap solo moi; main session khong play song song).
stopAllPlayback();setTimeout(()=>{const soloCtx=getAudioContext();if(wasPlaying){startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=currentTime;setIsPlaying(true);startTrackPlayback(currentTime);}if(playingSubTab){const prSt=subTabs.find(s=>s.id===playingSubTab.id);if(prSt){const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);schedulePianoRollMidi(prSt,prOffset);startSubTabPlayback(prSt,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{isPlaying:true,currentTime:prOffset}):s));}}},60);setTimeout(()=>lucide.createIcons(),50);return;}// V45.6: main-session - KHONG stopAllPlayback nua (track solo dang keu
// giu nguyen, het wobble re-attack 60ms). applyAllTrackMuteSolo tu kill
// track im (NOTE_OFF tung pitch - timelinePitchesRef giu set tu V45.5)
// + restart khi co track became audible. IPC toggleSolo chi goi ham nay
// -> tu apply luon (truoc day khong apply -> ref khong patch -> Qin khong im).
try{applyAllTrackMuteSolo(trackId,{solo:!wasSolo,muted:false});}catch(e){}setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,muted:!t.muted}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'MUTE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});setTimeout(()=>lucide.createIcons(),50);};const updateTrackProp=(trackId,props)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));// Mixer fader dùng updateTrackProp({volumeDb}) — áp gain realtime cho node
// thường + sub-node SECTION (key <parent>_sub_<trackId>)
if(props&&props.volumeDb!==undefined){const volLinear=props.volumeDb<=-50?0:Math.pow(10,props.volumeDb/20);const ctx=getAudioContext();const nodes=activeTrackNodesRef.current[trackId];if(nodes)nodes.gainNode.gain.setValueAtTime(volLinear,ctx.currentTime);Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+trackId)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)sn.gainNode.gain.setValueAtTime(volLinear,ctx.currentTime);}});}setTimeout(()=>lucide.createIcons(),50);};const toggleTrackDrum=trackId=>{var mt=tracks||[];var tidx=mt.findIndex(function(tr){return tr.id===trackId;});if(tidx<0)tidx=0;setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const becomingPercussion=!t.is_percussion;let newBank=becomingPercussion?128:t._saved_sf_bank!==undefined?t._saved_sf_bank:0;let newProgram=becomingPercussion?0:t._saved_sf_program!==undefined?t._saved_sf_program:0;let newCh=becomingPercussion?9:t._saved_sf_channel!==undefined?t._saved_sf_channel:tidx%16;return{...t,is_percussion:becomingPercussion,soundfont_bank:newBank,instrumentProgram:newProgram,midiChannel:newCh,_saved_sf_bank:becomingPercussion?t.soundfont_bank:t._saved_sf_bank,_saved_sf_program:becomingPercussion?t.instrumentProgram:t._saved_sf_program,_saved_sf_channel:becomingPercussion?t.midiChannel:t._saved_sf_channel,synth_engine:t.synth_engine?{...t.synth_engine,soundfont_bank:newBank,soundfont_program:newProgram}:t.synth_engine};}));setTimeout(()=>lucide.createIcons(),50);};const updateTrackVolumeDb=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,volumeDb:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'VOLUME_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback
const volLinear=val<=-50?0:Math.pow(10,val/20);const ctx=getAudioContext();const nodes=activeTrackNodesRef.current[trackId];if(nodes){nodes.gainNode.gain.setValueAtTime(volLinear,ctx.currentTime);}// Section sub-nodes (key: <parent>_sub_<trackId>) — volume slider của
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
<script src="/static/js/app.precompiled.js?v=202608291806" defer></script>
<script src="/static/js/app.precompiled.js?v=202608292138" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {
@@ -31,6 +31,15 @@ public:
virtual void programChange(uint32_t channel, uint32_t program) = 0;
virtual void pitchBend(uint32_t channel, uint32_t bend14) = 0;
// Flush every sounding note on the channel. Default: per-pitch
// note-off sweep (direct engines - no shared queue). Sandbox VST2
// overrides: a 128-note sweep would flood the child's 64-cap MIDI
// queue and drop the tail (Qin keeps ringing after STOP) - it
// sends one control event and the child panics locally instead.
virtual void allNotesOff(uint32_t channel) {
for (uint32_t n = 0; n < 128; ++n) noteOff(channel, n, 0);
}
// Open/close Native GUI window
virtual bool openGUI(void* parentWindowHandle) = 0;
// Headless engines (VST2 without an editor, SF2/SFZ): the bridge must
+1 -1
View File
@@ -483,7 +483,7 @@ void InstrumentEngineManager::allNotesOff() {
std::lock_guard<std::mutex> lock(mu_);
for (auto& [ch, inst] : channels_) {
if (channelQuiet(ch)) continue; // mid-rebuild / editor open: do not touch the instance
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
inst->allNotesOff(ch);
}
}
+6
View File
@@ -228,6 +228,12 @@ void SandboxVst2Host::pitchBend(uint32_t channel, uint32_t bend14) {
(uint8_t)(bend14 & 0x7F), (uint8_t)((bend14 >> 7) & 0x7F));
}
void SandboxVst2Host::allNotesOff(uint32_t /*channel*/) {
if (!alive_.load() || !ipc_) return;
std::cerr << "[Vst2Sandbox] ALL_NOTES_OFF ch=" << channel_ << std::endl;
pushControl(7); // plugin_host panics locally - no 64-cap MIDI flood
}
bool SandboxVst2Host::openGUI(void* parentWindowHandle) {
(void)parentWindowHandle; // child owns its window
if (!alive_.load() || !ipc_) return false;
+4
View File
@@ -33,6 +33,10 @@ public:
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
void programChange(uint32_t channel, uint32_t program) override;
void pitchBend(uint32_t channel, uint32_t bend14) override;
// Control 7 (ALL_NOTES_OFF): the child runs the local panic
// (CC64/CC123 + 128 note-offs) - a 128-note MIDI sweep would
// overflow the child's 64-cap queue and Qin would keep ringing.
void allNotesOff(uint32_t channel) override;
// The child owns its GUI window (plugin_host opens it on OPEN_GUI control).
bool openGUI(void* parentWindowHandle) override;
bool canOpenGUI() const override { return true; }
+9
View File
@@ -269,6 +269,15 @@ int main(int argc, char* argv[]) {
if (g_hostHwnd) ShowWindow(g_hostHwnd, SW_HIDE);
} else if (c.type == 6) {
quit = true;
} else if (c.type == 7) { // ALL_NOTES_OFF: runtime panic (STOP/solo)
panicAfterPreset();
// absorb the panic-mutated chunk so the ~2s periodic
// republish never overwrites presetOut with it
inst.captureState();
std::string postPanic = inst.serializeState();
if (!postPanic.empty()) lastOut = postPanic;
printf("[plugin_host] ALL_NOTES_OFF ch=%u\n", channel);
fflush(stdout);
}
}
ipc->base.controlQueueCount = 0;
Binary file not shown.
Binary file not shown.