fix(playback): stop fade-out 80ms khi nhan Stop/Pause - ramp gain track + bridge gain xuong 0 truoc hard-stop, guard play-gen tranh STOP tre giet play moi; sync 5 ban JS + index.html ?v=202608181818

This commit is contained in:
2026-08-18 18:23:07 +07:00
parent 45a15da55e
commit 1e16189da8
3 changed files with 121 additions and 51 deletions
+92 -35
View File
@@ -21486,6 +21486,7 @@ const App = () => {
// ── Update Playhead ──
const startSubTabPlayback = (st, offsetWallTime) => {
const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
if (!st.buffer) {
st.buffer = context.createBuffer(1, 128, context.sampleRate);
}
@@ -22677,6 +22678,7 @@ const App = () => {
const startTrackPlayback = offsetTime => {
const context = getAudioContext();
_nativePlayEpoch++; // 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) {
@@ -23126,6 +23128,7 @@ const App = () => {
const schedulePianoRollMidi = (st, offsetSeconds, notesOverride) => {
if (st.type !== 'PIANO_ROLL') return;
const context = getAudioContext();
_nativePlayEpoch++; // 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).
@@ -23236,13 +23239,13 @@ const App = () => {
// tab): Space phải STOP trước — không được play sub-tab (user: trong
// PIANO ROLL không phát hiện MIDI đang play → space 2 lần mới stop).
if (isPlaying) {
stopAllPlayback();
stopAllPlayback(true); // user stop — fade out
setSubTabs(prev => prev.map(s => s.isPlaying ? { ...s, isPlaying: false } : s));
setIsPlaying(false);
return;
}
if (st.isPlaying) {
stopAllPlayback();
stopAllPlayback(true); // user stop — fade out
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s,
isPlaying: false
@@ -23275,7 +23278,7 @@ const App = () => {
}
const context = getAudioContext();
if (isPlaying) {
stopAllPlayback();
stopAllPlayback(true); // user stop — fade out
} else {
startOffsetTimeRef.current = currentTime;
startAudioTimeRef.current = context.currentTime;
@@ -23327,9 +23330,9 @@ const App = () => {
}, 60);
};
const handlePause = () => {
if (isPlaying || subTabs.some(s => s.isPlaying)) stopAllPlayback();
if (isPlaying || subTabs.some(s => s.isPlaying)) stopAllPlayback(true);
};
const stopAllPlayback = () => {
const stopAllPlayback = (fade) => {
try {
isPlayingRef.current = false;
_nativePlayEpoch++; // hủy mọi render/decode pending của lần play cũ
@@ -23338,16 +23341,91 @@ const App = () => {
bridgeNoteEpochRef.current++;
try { clearAllPendingNoteOffs(); } catch (e) {}
try { if (subTabsRef.current) subTabsRef.current = subTabsRef.current.map(s => ({ ...s, isPlaying: false })); } catch (e) {}
activeSourcesRef.current.forEach(src => {
try {
src.stop();
} 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.
// Loop restart/seek (fade=false) giữ stop tức thì như cũ (không fade).
// Lưu ref trước khi clear để doHardStop trễ không đụng playback mới.
const ctx = getAudioContext();
const stopEpoch = _nativePlayEpoch;
const FADE_MS = 80;
const capturedSources = activeSourcesRef.current.slice();
const capturedNodes = Object.values(activeTrackNodesRef.current);
activeSourcesRef.current = [];
Object.values(activeTrackNodesRef.current).forEach(n => {
if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} }
});
activeTrackNodesRef.current = {};
const fadedGains = [];
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ó
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
// qua React effect (cham) van bay toi bridge sau STOP → retrigger am
// 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ừ
// â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).
@@ -23355,27 +23433,6 @@ const App = () => {
// Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng)
// tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
try { heldMidiNotesRef.current = {}; } catch (e) { }
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 qua
// React effect (cham) van bay toi bridge sau STOP → retrigger am 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);
}
@@ -30990,7 +31047,7 @@ STRICT CONSTRAINTS:
onSaveNotes: handleSaveMidiNotes,
setSubTabs: setSubTabs,
onPlayPause: handlePlayPause,
onStop: stopAllPlayback,
onStop: () => stopAllPlayback(true),
isPlaying: isPlaying,
playPreviewNote: playMidiPreviewNote,
showToast: showToast,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -50,7 +50,7 @@
<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/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608181807" defer></script>
<script src="/static/js/app.precompiled.js?v=202608181818" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {