diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 6c055f2..e02751c 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -279,6 +279,22 @@ const stopAllNativeSfNotes = () => { // 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). 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 _nativeTrackPending = {}; // ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─ @@ -21487,6 +21503,7 @@ const App = () => { const startSubTabPlayback = (st, offsetWallTime) => { const context = getAudioContext(); _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade + restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain if (!st.buffer) { st.buffer = context.createBuffer(1, 128, context.sampleRate); } @@ -22679,6 +22696,7 @@ const App = () => { const startTrackPlayback = offsetTime => { const context = getAudioContext(); _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 // bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này). if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) { @@ -23129,6 +23147,7 @@ const App = () => { if (st.type !== 'PIANO_ROLL') return; const context = getAudioContext(); _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 → // 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). @@ -23363,6 +23382,10 @@ const App = () => { } catch (e) {} let bridgeGainNode = null; 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 { const now = ctx.currentTime; capturedNodes.forEach(n => { @@ -23383,6 +23406,7 @@ const App = () => { } } } catch (e) { console.warn('[Stop] fade setup error:', e); } + _pendingFadeSession = { fadedGains, bridgeGainNode, stopEpoch }; } const doHardStop = () => { try { @@ -23393,6 +23417,12 @@ const App = () => { capturedNodes.forEach(n => { 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ó stopMidiCapture(); stopAllNativeSfNotes(); stopAllNativeVstNotes(); @@ -23418,16 +23448,6 @@ const App = () => { } 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. - 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(); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 6501343..1f9caec 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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). // 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). -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 // (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 @@ -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 pollTaskResult=async(taskId,maxPoll=10)=>{for(let i=0;isetTimeout(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(newZoom50000)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;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i{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 // 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 @@ -1684,7 +1684,7 @@ const midiItems=track.midiItems||[];const routeCarla=shouldRouteCarla(track.synt // play FluidSynth GM sai âm chồng lên) const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));if(offsetTime{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 → // 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). @@ -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 // 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. -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 -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ó +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(_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 // â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 @@ -1734,8 +1734,7 @@ if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try // 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). 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. -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ừ +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ừ // â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). try{midiVuActivityRef.current={};}catch(e){}// Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng) diff --git a/app/templates/index.html b/app/templates/index.html index 3b7cef6..ec81d18 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -50,7 +50,7 @@ - +