fix(playback): het fade-out dai dang khi stop - restore gain LUON (ke ca stale/play moi trong cua so fade) qua pending fade session, huy fade ngay khi play moi (het dip 200ms); sync 6 ban JS + index.html ?v=202608181840

This commit is contained in:
2026-08-18 19:53:59 +07:00
parent 4ddaf1f26a
commit be8af7a6ee
3 changed files with 38 additions and 19 deletions
+30 -10
View File
@@ -279,6 +279,22 @@ const stopAllNativeSfNotes = () => {
// Epoch guard: stop/rebuild tăng _nativePlayEpoch → mọi render pending của // Epoch guard: stop/rebuild tăng _nativePlayEpoch → mọi render pending của
// lần play cũ resolve nhưng bị skip (không duplicate schedule). // lần play cũ resolve nhưng bị skip (không duplicate schedule).
let _nativePlayEpoch = 0; let _nativePlayEpoch = 0;
// V17: fade session dang treo (gain ramp FADE_MS) — restore khi play moi /
// stop moi / doHardStop de gain KHONG bi ket ~0.0001 (root cause "am bi fade out").
let _pendingFadeSession = null;
const restorePendingFadeSession = () => {
if (!_pendingFadeSession) return;
try {
const t = getAudioContext().currentTime;
_pendingFadeSession.fadedGains.forEach(g => {
try { g.node.gain.cancelScheduledValues(t); g.node.gain.setValueAtTime(g.prev, t); } catch (e) {}
});
if (_pendingFadeSession.bridgeGainNode) {
try { _pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t); _pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0, t); } catch (e) {}
}
} catch (e) {}
_pendingFadeSession = null;
};
const _nativeTrackUrls = {}; const _nativeTrackUrls = {};
const _nativeTrackPending = {}; const _nativeTrackPending = {};
// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─ // ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─
@@ -21487,6 +21503,7 @@ const App = () => {
const startSubTabPlayback = (st, offsetWallTime) => { const startSubTabPlayback = (st, offsetWallTime) => {
const context = getAudioContext(); const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
if (!st.buffer) { if (!st.buffer) {
st.buffer = context.createBuffer(1, 128, context.sampleRate); st.buffer = context.createBuffer(1, 128, context.sampleRate);
} }
@@ -22679,6 +22696,7 @@ const App = () => {
const startTrackPlayback = offsetTime => { const startTrackPlayback = offsetTime => {
const context = getAudioContext(); const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms)
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng // D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
// bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này). // bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) { if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
@@ -23129,6 +23147,7 @@ const App = () => {
if (st.type !== 'PIANO_ROLL') return; if (st.type !== 'PIANO_ROLL') return;
const context = getAudioContext(); const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 → // V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 →
// bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại. // bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại.
// Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục). // Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục).
@@ -23363,6 +23382,10 @@ const App = () => {
} catch (e) {} } catch (e) {}
let bridgeGainNode = null; let bridgeGainNode = null;
if (fade && ctx) { if (fade && ctx) {
// V17: restore fade session cu TRUOC khi capture prev — double-stop trong
// cua so fade: neu khong, session cu stale -> bo restore, session moi capture
// prev giua ramp (vd 0.5) -> restore sai -> gain ket.
restorePendingFadeSession();
try { try {
const now = ctx.currentTime; const now = ctx.currentTime;
capturedNodes.forEach(n => { capturedNodes.forEach(n => {
@@ -23383,6 +23406,7 @@ const App = () => {
} }
} }
} catch (e) { console.warn('[Stop] fade setup error:', e); } } catch (e) { console.warn('[Stop] fade setup error:', e); }
_pendingFadeSession = { fadedGains, bridgeGainNode, stopEpoch };
} }
const doHardStop = () => { const doHardStop = () => {
try { try {
@@ -23393,6 +23417,12 @@ const App = () => {
capturedNodes.forEach(n => { capturedNodes.forEach(n => {
if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} } if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} }
}); });
// V17: restore gain LUON — play moi trong cua so fade (stale) truoc day bo qua
// restore -> track/bridge gain ket 0.0001 -> moi am sau bi fade-out dai dang.
// Chi skip khi co fade session MOI hon (session nay het pending — session moi tu restore).
if (_pendingFadeSession && _pendingFadeSession.stopEpoch === stopEpoch) {
restorePendingFadeSession();
}
if (stale) return; // play mới đang chạy — không dừng bridge/SF của nó if (stale) return; // play mới đang chạy — không dừng bridge/SF của nó
stopMidiCapture(); stopMidiCapture();
stopAllNativeSfNotes(); stopAllNativeVstNotes(); stopAllNativeSfNotes(); stopAllNativeVstNotes();
@@ -23418,16 +23448,6 @@ const App = () => {
} catch (e) { } catch (e) {
console.warn('[Stop] stopAllPlayback error:', e); console.warn('[Stop] stopAllPlayback error:', e);
} }
// Restore gain về giá trị cũ — play lần sau không bị câm/đổi volume.
try {
const t = getAudioContext().currentTime;
fadedGains.forEach(g => {
try { g.node.gain.cancelScheduledValues(t); g.node.gain.setValueAtTime(g.prev, t); } catch (e) {}
});
if (bridgeGainNode) {
try { bridgeGainNode.gain.cancelScheduledValues(t); bridgeGainNode.gain.setValueAtTime(1.0, t); } catch (e) {}
}
} catch (e) {}
}; };
if (fade && ctx) { if (fade && ctx) {
try { setTimeout(doHardStop, FADE_MS); } catch (e) { doHardStop(); } try { setTimeout(doHardStop, FADE_MS); } catch (e) { doHardStop(); }
+7 -8
View File
@@ -66,7 +66,7 @@ const audio=new Audio(API_BASE_URL+res.url);_nativeSfPreviews[k]={audio:audio,to
// notesOverride: dùng thay track.midiItems (piano roll tab). // notesOverride: dùng thay track.midiItems (piano roll tab).
// Epoch guard: stop/rebuild tăng _nativePlayEpoch → mọi render pending của // Epoch guard: stop/rebuild tăng _nativePlayEpoch → mọi render pending của
// lần play cũ resolve nhưng bị skip (không duplicate schedule). // lần play cũ resolve nhưng bị skip (không duplicate schedule).
let _nativePlayEpoch=0;const _nativeTrackUrls={};const _nativeTrackPending={};// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─ let _nativePlayEpoch=0;let _pendingFadeSession=null;const restorePendingFadeSession=()=>{if(!_pendingFadeSession)return;try{const t=getAudioContext().currentTime;_pendingFadeSession.fadedGains.forEach(g=>{try{g.node.gain.cancelScheduledValues(t);g.node.gain.setValueAtTime(g.prev,t);}catch(e){}});if(_pendingFadeSession.bridgeGainNode){try{_pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t);_pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0,t);}catch(e){}}}catch(e){}_pendingFadeSession=null;};const _nativeTrackUrls={};const _nativeTrackPending={};// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─
// Dedup: note cùng pitch + cùng start_beat → gộp 1, giữ velocity cao hơn // Dedup: note cùng pitch + cùng start_beat → gộp 1, giữ velocity cao hơn
// (tránh synth instantiate nhiều voice cùng pitch — spec §II.5). // (tránh synth instantiate nhiều voice cùng pitch — spec §II.5).
// Legato cleanup: note trước cùng pitch kéo dài qua start_beat note sau → cắt // Legato cleanup: note trước cùng pitch kéo dài qua start_beat note sau → cắt
@@ -1370,7 +1370,7 @@ view.setUint16(22,numCh,true);view.setUint32(24,sr,true);view.setUint32(28,sr*nu
const loadServerWaveform=async fileId=>{try{const resp=await fetch(`${API_AUDIO}/waveform/${fileId}?num_peaks=800`);if(!resp.ok)return null;return await resp.json();}catch{return null;}};// ── Check Celery task result ── const loadServerWaveform=async fileId=>{try{const resp=await fetch(`${API_AUDIO}/waveform/${fileId}?num_peaks=800`);if(!resp.ok)return null;return await resp.json();}catch{return null;}};// ── Check Celery task result ──
const pollTaskResult=async(taskId,maxPoll=10)=>{for(let i=0;i<maxPoll;i++){await new Promise(r=>setTimeout(r,1500));try{const resp=await fetch(`${API_TASKS}/${taskId}`);if(!resp.ok)continue;const data=await resp.json();if(data.status==='SUCCESS')return data.result;if(data.status==='FAILURE')throw new Error(data.error||'Task failed');}catch(err){throw err;}}throw new Error('Task polling timeout');};// ── Wheel Zoom ── const pollTaskResult=async(taskId,maxPoll=10)=>{for(let i=0;i<maxPoll;i++){await new Promise(r=>setTimeout(r,1500));try{const resp=await fetch(`${API_TASKS}/${taskId}`);if(!resp.ok)continue;const data=await resp.json();if(data.status==='SUCCESS')return data.result;if(data.status==='FAILURE')throw new Error(data.error||'Task failed');}catch(err){throw err;}}throw new Error('Task polling timeout');};// ── Wheel Zoom ──
useEffect(()=>{const timeline=timelineWrapperRef.current;if(!timeline)return;const handleWheel=e=>{if(e.ctrlKey||e.metaKey){e.preventDefault();const zoomFactor=e.deltaY>0?0.9:1.1;setZoom(prevZoom=>{let newZoom=prevZoom*zoomFactor;if(newZoom<minZoom)newZoom=minZoom;if(newZoom>50000)newZoom=50000;requestAnimationFrame(()=>{const centerTime=currentTimeRef.current;const newCenterX=centerTime*newZoom;const viewportWidth=timeline.clientWidth;timeline.scrollLeft=newCenterX-viewportWidth/2;});return newZoom;});}else if(e.shiftKey){e.preventDefault();timeline.scrollLeft+=e.deltaY;}};timeline.addEventListener('wheel',handleWheel,{passive:false});return()=>timeline.removeEventListener('wheel',handleWheel);},[minZoom]);// ── Update Playhead ── useEffect(()=>{const timeline=timelineWrapperRef.current;if(!timeline)return;const handleWheel=e=>{if(e.ctrlKey||e.metaKey){e.preventDefault();const zoomFactor=e.deltaY>0?0.9:1.1;setZoom(prevZoom=>{let newZoom=prevZoom*zoomFactor;if(newZoom<minZoom)newZoom=minZoom;if(newZoom>50000)newZoom=50000;requestAnimationFrame(()=>{const centerTime=currentTimeRef.current;const newCenterX=centerTime*newZoom;const viewportWidth=timeline.clientWidth;timeline.scrollLeft=newCenterX-viewportWidth/2;});return newZoom;});}else if(e.shiftKey){e.preventDefault();timeline.scrollLeft+=e.deltaY;}};timeline.addEventListener('wheel',handleWheel,{passive:false});return()=>timeline.removeEventListener('wheel',handleWheel);},[minZoom]);// ── Update Playhead ──
const startSubTabPlayback=(st,offsetWallTime)=>{const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade const startSubTabPlayback=(st,offsetWallTime)=>{const context=getAudioContext();_nativePlayEpoch++;restorePendingFadeSession();// new play — vô hiệu doHardStop đang fade
if(!st.buffer){st.buffer=context.createBuffer(1,128,context.sampleRate);}const speed=st.speed||1.0;const offsetBuffer=offsetWallTime*speed;const eff=st.buffer.getChannelData(0);const edBuffer=context.createBuffer(1,eff.length,st.buffer.sampleRate);const edData=edBuffer.getChannelData(0);edData.set(eff);const fx=st.effects||{};if(fx.reverse){const reversed=new Float32Array(edData);for(let i=0;i<edData.length;i++)reversed[i]=edData[edData.length-1-i];edBuffer.copyToChannel(reversed,0);}if(fx.gainDb!==0){const gain=Math.pow(10,fx.gainDb/20);for(let i=0;i<edData.length;i++)edData[i]=Math.max(-1,Math.min(1,edData[i]*gain));}if(fx.fadeInMs>0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i<fadeSamples;i++)edData[i]=edData[i]*(i/fadeSamples);}if(fx.fadeOutMs>0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i<edData.length;i++){edData[i]=edData[i]*((edData.length-1-i)/fadeSamples);}}const source=context.createBufferSource();source.buffer=edBuffer;source.playbackRate.value=speed;activePlaybackSpeedRef.current=speed;// Graph Editor Automation Node Chain (15_GRAPH_EDIT.md §3) if(!st.buffer){st.buffer=context.createBuffer(1,128,context.sampleRate);}const speed=st.speed||1.0;const offsetBuffer=offsetWallTime*speed;const eff=st.buffer.getChannelData(0);const edBuffer=context.createBuffer(1,eff.length,st.buffer.sampleRate);const edData=edBuffer.getChannelData(0);edData.set(eff);const fx=st.effects||{};if(fx.reverse){const reversed=new Float32Array(edData);for(let i=0;i<edData.length;i++)reversed[i]=edData[edData.length-1-i];edBuffer.copyToChannel(reversed,0);}if(fx.gainDb!==0){const gain=Math.pow(10,fx.gainDb/20);for(let i=0;i<edData.length;i++)edData[i]=Math.max(-1,Math.min(1,edData[i]*gain));}if(fx.fadeInMs>0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i<fadeSamples;i++)edData[i]=edData[i]*(i/fadeSamples);}if(fx.fadeOutMs>0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i<edData.length;i++){edData[i]=edData[i]*((edData.length-1-i)/fadeSamples);}}const source=context.createBufferSource();source.buffer=edBuffer;source.playbackRate.value=speed;activePlaybackSpeedRef.current=speed;// Graph Editor Automation Node Chain (15_GRAPH_EDIT.md §3)
// source → volumeGainNode → pannerNode → fadeGainNode → destination // source → volumeGainNode → pannerNode → fadeGainNode → destination
const volumeGainNode=context.createGain();volumeGainNode.gain.setValueAtTime(1.0,context.currentTime);const pannerNode=context.createStereoPanner();pannerNode.pan.setValueAtTime(0.0,context.currentTime);const fadeGainNode=context.createGain();fadeGainNode.gain.setValueAtTime(1.0,context.currentTime);// Schedule volume automation nodes const volumeGainNode=context.createGain();volumeGainNode.gain.setValueAtTime(1.0,context.currentTime);const pannerNode=context.createStereoPanner();pannerNode.pan.setValueAtTime(0.0,context.currentTime);const fadeGainNode=context.createGain();fadeGainNode.gain.setValueAtTime(1.0,context.currentTime);// Schedule volume automation nodes
@@ -1609,7 +1609,7 @@ const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đ
if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}// V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref). if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}// V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref).
const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}},delayMs+(durMs||1000)+30);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}},delayMs+(durMs||1000)+30);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì
// FluidSynth GM sai âm — safety net cho mọi caller (ghost notes, per-note). // FluidSynth GM sai âm — safety net cho mọi caller (ghost notes, per-note).
if(isStandaloneSf()&&isVstTrackEngine(synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(track,pitch,velocity,durMs,startTime,track?track.id:'global');return;}window.SonicSF.playNote(pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) if(isStandaloneSf()&&isVstTrackEngine(synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(track,pitch,velocity,durMs,startTime,track?track.id:'global');return;}window.SonicSF.playNote(pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();_nativePlayEpoch++;restorePendingFadeSession();// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms)
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng // D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
// bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này). // bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){// V10: loop restart — stopAllPlayback vừa schedule STOP#2 (150ms); hủy if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){// V10: loop restart — stopAllPlayback vừa schedule STOP#2 (150ms); hủy
@@ -1684,7 +1684,7 @@ const midiItems=track.midiItems||[];const routeCarla=shouldRouteCarla(track.synt
// play FluidSynth GM sai âm chồng lên) // play FluidSynth GM sai âm chồng lên)
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local) const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;if(!routeToCarla&&window.SonicSF){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local) if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,startTime,durationMs);}else{const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,lcCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local)
if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();_nativePlayEpoch++;restorePendingFadeSession();// new play — vô hiệu doHardStop đang fade
// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 → // V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 →
// bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại. // bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại.
// Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục). // Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục).
@@ -1725,8 +1725,8 @@ bridgeNoteEpochRef.current++;try{clearAllPendingNoteOffs();}catch(e){}try{if(sub
const ctx=getAudioContext();const stopEpoch=_nativePlayEpoch;const FADE_MS=200;const capturedSources=activeSourcesRef.current.slice();const capturedNodes=Object.values(activeTrackNodesRef.current);activeSourcesRef.current=[];activeTrackNodesRef.current={};const fadedGains=[];// V16: stop NGAY clip source CHƯA phát (playbackState='scheduled') — nếu const ctx=getAudioContext();const stopEpoch=_nativePlayEpoch;const FADE_MS=200;const capturedSources=activeSourcesRef.current.slice();const capturedNodes=Object.values(activeTrackNodesRef.current);activeSourcesRef.current=[];activeTrackNodesRef.current={};const fadedGains=[];// V16: stop NGAY clip source CHƯA phát (playbackState='scheduled') — nếu
// chờ tới doHardStop (sau FADE_MS) chúng đã bắt đầu phát trong lúc fade → // chờ tới doHardStop (sau FADE_MS) chúng đã bắt đầu phát trong lúc fade →
// nghe như "play lại rồi tắt". Source đang kêu ('playing') giữ lại cho ramp. // nghe như "play lại rồi tắt". Source đang kêu ('playing') giữ lại cho ramp.
try{capturedSources.forEach(src=>{try{if(src.playbackState==='scheduled')src.stop();}catch(e){}});}catch(e){}let bridgeGainNode=null;if(fade&&ctx){try{const now=ctx.currentTime;capturedNodes.forEach(n=>{if(n&&n.gainNode&&n.gainNode.gain){fadedGains.push({node:n.gainNode,prev:n.gainNode.gain.value});n.gainNode.gain.cancelScheduledValues(now);n.gainNode.gain.setValueAtTime(n.gainNode.gain.value,now);n.gainNode.gain.linearRampToValueAtTime(0.0001,now+FADE_MS/1000);}});if(window.BridgeAudioNode&&window.BridgeAudioNode.getOutputNode){const bg=window.BridgeAudioNode.getOutputNode();if(bg&&bg.gain){bridgeGainNode=bg;bg.gain.cancelScheduledValues(now);bg.gain.setValueAtTime(bg.gain.value,now);bg.gain.linearRampToValueAtTime(0.0001,now+FADE_MS/1000);}}}catch(e){console.warn('[Stop] fade setup error:',e);}}const doHardStop=()=>{try{const stale=_nativePlayEpoch!==stopEpoch;// play mới bắt đầu trong lúc fade try{capturedSources.forEach(src=>{try{if(src.playbackState==='scheduled')src.stop();}catch(e){}});}catch(e){}let bridgeGainNode=null;if(fade&&ctx){restorePendingFadeSession();try{const now=ctx.currentTime;capturedNodes.forEach(n=>{if(n&&n.gainNode&&n.gainNode.gain){fadedGains.push({node:n.gainNode,prev:n.gainNode.gain.value});n.gainNode.gain.cancelScheduledValues(now);n.gainNode.gain.setValueAtTime(n.gainNode.gain.value,now);n.gainNode.gain.linearRampToValueAtTime(0.0001,now+FADE_MS/1000);}});if(window.BridgeAudioNode&&window.BridgeAudioNode.getOutputNode){const bg=window.BridgeAudioNode.getOutputNode();if(bg&&bg.gain){bridgeGainNode=bg;bg.gain.cancelScheduledValues(now);bg.gain.setValueAtTime(bg.gain.value,now);bg.gain.linearRampToValueAtTime(0.0001,now+FADE_MS/1000);}}}catch(e){console.warn('[Stop] fade setup error:',e);}_pendingFadeSession={fadedGains,bridgeGainNode,stopEpoch};}const doHardStop=()=>{try{const stale=_nativePlayEpoch!==stopEpoch;// play mới bắt đầu trong lúc fade
capturedSources.forEach(src=>{try{src.stop();}catch(e){}});capturedNodes.forEach(n=>{if(n.fxStopFn){try{n.fxStopFn();}catch(e){}}});if(stale)return;// play mới đang chạy — không dừng bridge/SF của nó capturedSources.forEach(src=>{try{src.stop();}catch(e){}});capturedNodes.forEach(n=>{if(n.fxStopFn){try{n.fxStopFn();}catch(e){}}});if(_pendingFadeSession&&_pendingFadeSession.stopEpoch===stopEpoch)restorePendingFadeSession();if(stale)return;// play mới đang chạy — không dừng bridge/SF của nó
stopMidiCapture();stopAllNativeSfNotes();stopAllNativeVstNotes();// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết stopMidiCapture();stopAllNativeSfNotes();stopAllNativeVstNotes();// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết
// âm ngân tức thì; A13 C++ flush note-off). // âm ngân tức thì; A13 C++ flush note-off).
if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try{window.NativeBridgeService.transport('stop');}catch(e){}// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try{window.NativeBridgeService.transport('stop');}catch(e){}// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync
@@ -1734,8 +1734,7 @@ if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try
// treo. V10: timer lưu ref — startTrackPlayback (loop restart) hủy // treo. V10: timer lưu ref — startTrackPlayback (loop restart) hủy
// khi PLAY, tránh STOP#2 giết loop tiếp theo (VSTi chỉ play 1 vòng). // khi PLAY, tránh STOP#2 giết loop tiếp theo (VSTi chỉ play 1 vòng).
if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);}bridgeStopRetryTimerRef.current=setTimeout(()=>{bridgeStopRetryTimerRef.current=null;try{window.NativeBridgeService.transport('stop');}catch(e){}},150);}if(window.SonicSF){try{window.SonicSF.stopAll();}catch(e){console.warn('[Stop] stopAll error:',e);}// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck) if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);}bridgeStopRetryTimerRef.current=setTimeout(()=>{bridgeStopRetryTimerRef.current=null;try{window.NativeBridgeService.transport('stop');}catch(e){}},150);}if(window.SonicSF){try{window.SonicSF.stopAll();}catch(e){console.warn('[Stop] stopAll error:',e);}// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
if(window.SonicSF.panic){try{window.SonicSF.panic();}catch(e){console.warn('[Stop] panic error:',e);}}}}catch(e){console.warn('[Stop] stopAllPlayback error:',e);}// Restore gain về giá trị cũ — play lần sau không bị câm/đổi volume. if(window.SonicSF.panic){try{window.SonicSF.panic();}catch(e){console.warn('[Stop] panic error:',e);}}}}catch(e){console.warn('[Stop] stopAllPlayback error:',e);}};if(fade&&ctx){try{setTimeout(doHardStop,FADE_MS);}catch(e){doHardStop();}}else{doHardStop();}// Clear VU activity NGAY: hết âm → VU tắt tức thì (không decay 0.35s từ
try{const t=getAudioContext().currentTime;fadedGains.forEach(g=>{try{g.node.gain.cancelScheduledValues(t);g.node.gain.setValueAtTime(g.prev,t);}catch(e){}});if(bridgeGainNode){try{bridgeGainNode.gain.cancelScheduledValues(t);bridgeGainNode.gain.setValueAtTime(1.0,t);}catch(e){}}}catch(e){}};if(fade&&ctx){try{setTimeout(doHardStop,FADE_MS);}catch(e){doHardStop();}}else{doHardStop();}// Clear VU activity NGAY: hết âm → VU tắt tức thì (không decay 0.35s từ
// âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main // âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main
// items phải tắt theo, không "diễn" tiếp). // items phải tắt theo, không "diễn" tiếp).
try{midiVuActivityRef.current={};}catch(e){}// Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng) try{midiVuActivityRef.current={};}catch(e){}// Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng)
+1 -1
View File
@@ -50,7 +50,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script> <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/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608181835" defer></script> <script src="/static/js/app.precompiled.js?v=202608181840" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {