V45: D17 seamless loop wrap + D18 stop solo note-off (Qin) + D18b/c guard + engine VST load fixes

- D17: wrap_info/wrap-skip, fx wrap xfade, playhead-derived scheduling
- D18: solo stop -> NOTE_OFF all sounding timeline pitches + CC123/CC120
- D18b: track audible gain guard, tracksRef fallback
- D18c: timelinePitchesRef per-track Set, kill at applyAllTrackMuteSolo
- bridge ring _MAX 16, index v=202608290928
- plugins.py/vst_engine.py/fx_realtime.py/JuceFxEngine.cpp: VSTi load + Qin fixes (uncommitted 12:18)
This commit is contained in:
2026-08-29 16:39:02 +07:00
parent 2afa828c8f
commit c084ac8ba7
9 changed files with 434 additions and 104 deletions
+196 -46
View File
@@ -7307,6 +7307,7 @@ const loadVstToBridge = async (track) => {
if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {};
const tId = track.id || track.trackId;
const bch = window.SonicMidiRouter.allocateChannel(tId, false);
markTrackLoad(tId, { state: 'loading', channel: bch });
try {
// V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename —
// KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu
@@ -7331,11 +7332,12 @@ const loadVstToBridge = async (track) => {
}
// Đã load ĐÚNG path vào channel này rồi → skip (không LOAD trùng). Bỏ
// early-return mù theo flag — flag sai khi C++ restore instrument khác.
if (knownPath && window.__bridgeLoadedPaths[bch] === knownPath) return true;
if (knownPath && window.__bridgeLoadedPaths[bch] === knownPath) { markTrackLoad(tId, { state: 'done' }); return true; }
// Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS).
if (knownPath) {
const ok = await window.NativeBridgeService.loadInstrument(knownPath, btype, bch);
if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = knownPath; if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {}; window.__bridgePluginPaths[instrumentId] = knownPath; }
if (!ok) markTrackLoad(tId, { state: 'failed' });
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', knownPath);
return !!ok;
}
@@ -7355,12 +7357,13 @@ const loadVstToBridge = async (track) => {
}
} catch (e2) { console.warn('[Bridge] listPlugins fallback fail:', e2); }
}
if (!p) return false;
if (!p) { markTrackLoad(tId, { state: 'failed' }); return false; }
const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = p; }
if (!ok) markTrackLoad(tId, { state: 'failed' });
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
return !!ok;
} catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; }
} catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); markTrackLoad(tId, { state: 'failed' }); return false; }
};
// GM font dự phòng khi soundfont track không có trong thư viện (VD id
// sf_generaluser_gs do AI sinh nhưng user chưa tải font) — ưu tiên font GM
@@ -7385,6 +7388,7 @@ const loadAnyTrackToBridge = async (track) => {
const se = track.synth_engine || {};
const bank = se.soundfont_bank !== undefined ? se.soundfont_bank : (track.soundfont_bank !== undefined ? track.soundfont_bank : 0);
const bch = window.SonicMidiRouter.allocateChannel(track.id || track.trackId, bank === 128);
markTrackLoad(track.id || track.trackId, { state: 'loading', channel: bch });
let pl = null;
try {
const sfId = inst.replace('sf_', '');
@@ -7408,15 +7412,37 @@ const loadAnyTrackToBridge = async (track) => {
p = r2 && r2.path ? r2.path : null;
}
}
if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); return false; }
if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); markTrackLoad(track.id || track.trackId, { state: 'failed' }); return false; }
const ok = await window.NativeBridgeService.loadInstrument(p, 'SF2', bch);
if (ok) { if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {}; window.__bridgeLoadedChannels[bch] = true; if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {}; window.__bridgeLoadedPaths[bch] = p; }
if (!ok) markTrackLoad(track.id || track.trackId, { state: 'failed' });
console.log('[Bridge] preload SF2 track', track.id, '-> ch', bch, ok ? 'OK' : 'FAIL', p);
return !!ok;
} catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); return false; }
} catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); markTrackLoad(track.id || track.trackId, { state: 'failed' }); return false; }
}
try { return await loadVstToBridge(track); } catch (e) { return false; }
};
// D16: per-track loading ruler (TCP). window.__trackLoadState[trackId] =
// { state:'pending'|'loading'|'done'|'failed', channel, startedTs, doneTs }.
// Poller trong App (loadTick) chuyển loading→done khi channel xuất hiện trong
// loaded_channels (queryStatus) — đúng cho cả C++ restore (bridge_state.json,
// chạy từ spawn) lẫn JS load — không đánh dấu done sớm khi C++ guard skip.
const markTrackLoad = (trackId, patch) => {
if (!trackId) return;
if (!window.__trackLoadState) window.__trackLoadState = {};
const cur = window.__trackLoadState[trackId] || { state: 'pending', channel: null, startedTs: 0, doneTs: 0 };
const next = Object.assign({}, cur, patch);
if (patch.state === 'loading' && cur.state !== 'loading') next.startedTs = Date.now();
if (patch.state === 'done' && !patch.doneTs) next.doneTs = Date.now();
window.__trackLoadState[trackId] = next;
if (window.__onTrackLoadState) { try { window.__onTrackLoadState(trackId); } catch (e) {} }
};
if (typeof document !== 'undefined' && !document.getElementById('sf-ruler-style')) {
const _st = document.createElement('style');
_st.id = 'sf-ruler-style';
_st.textContent = '@keyframes sf-ruler { 0% { transform: translateX(-110%); } 100% { transform: translateX(340%); } } .sf-ruler-bar { animation: sf-ruler 1.1s ease-in-out infinite; } @keyframes sf-ruler-done { 0% { opacity: 1; } 100% { opacity: 0; } } .sf-ruler-done { animation: sf-ruler-done 1.4s ease-out forwards; }';
document.head.appendChild(_st);
}
const ensureAndOpenVstGui = async (track) => {
const trackId = track.trackId || track.id;
const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id);
@@ -7522,6 +7548,28 @@ const wheelSteps = (key, deltaY, deltaMode, pxPerStep = 100) => {
// the soundfonts; the per-track channel re-selection happens at note time.
const preloadTrackInstruments = async (tracks) => {
const list = tracks || [];
// D13: bridge load TRƯỚC vòng WASM — vòng selectInstrument (parse font WASM
// từng track, có thể ~20s) trước đây chặn vòng bridge → user bấm play khi
// app vừa load: NOTE_ON vào channel bridge chưa có instrument → im lặng tới
// khi bridge load xong (~27s từ bootstrap — dbg.log play_first_audible).
if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
// D16b: pre-mark TẤT CẢ track có instrument thành loading TRƯỚC loop — loop
// nối tiếp (C++ g_loadMutex) khiến track 2-9 không có ruler tới khi track 1
// xong → UI lâu không có loading item. markTrackLoad channel=null hiện ruler
// ngay; loadAnyTrackToBridge set channel khi tới lượt; poller chuyển done.
for (let i = 0; i < list.length; i++) {
const t = list[i];
const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
if (!inst) continue;
markTrackLoad(t.id || t.trackId, { state: 'loading', channel: null });
}
for (let i = 0; i < list.length; i++) {
const t = list[i];
const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
if (!inst) continue;
try { await loadAnyTrackToBridge(t); } catch (e) {}
}
}
if (window.SonicSF && window.SonicSF.selectInstrument) {
for (let i = 0; i < list.length; i++) {
const t = list[i];
@@ -7536,17 +7584,7 @@ const preloadTrackInstruments = async (tracks) => {
} catch (e) {}
}
}
// Lỗi 3: mở lại project → VST3 đã lưu phải load lại vào bridge (không mở GUI).
if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
for (let i = 0; i < list.length; i++) {
const t = list[i];
const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
if (!inst) continue;
try { await loadAnyTrackToBridge(t); } catch (e) {}
}
}
};
const ProfileModal = ({
isOpen,
onClose,
@@ -18741,6 +18779,8 @@ const App = () => {
const activeAudioRecordersRef = useRef({});
const pianoRollRecorderRef = useRef(null);
const activeMidiPitchesRef = useRef(new Set());
const timelinePitchesRef = useRef({}); // D18c: per-track Set timeline MIDI pitches dang keu (NOTE_ON fired)
const tracksRef = useRef([]); // D18c: tracks state moi nhat (guard mute/solo fire-time)
const [activeMidiPitches, setActiveMidiPitches] = useState(new Set());
const recordingPCMDataRef = useRef({});
const recordingStartTimeRef = useRef(0);
@@ -18922,6 +18962,32 @@ const App = () => {
if (isMidiSoundTrack(t)) {
try { sendMidiVolumeCC7(t, effective, audible); } catch (e) {}
}
// D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0
// (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume)
// → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang
// giữ + CC123/CC120 all-notes-off trên channel track đó.
if (!audible && wasAudible && isMidiSoundTrack(t)) {
try {
const _isPerc = !!(t.synth_engine && t.synth_engine.soundfont_bank === 128) || !!t.is_percussion;
const _held = activeMidiPitchesRef.current || new Set();
// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch;
// truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note
// timeline (Qin VST2) cu ngan/loop (tremolo) khi solo track khac.
const _tSet = (timelinePitchesRef.current || {})[t.id];
if (_tSet && _tSet.size) {
_tSet.forEach(function (_p) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: t.id, pitch: _p, velocity: 0, percussion: _isPerc, live: true });
});
}
_held.forEach(function (_p) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: t.id, pitch: _p, velocity: 0, percussion: _isPerc, live: true });
});
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 123, velocity: 0, data2: 0, percussion: _isPerc });
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 120, velocity: 0, data2: 0, percussion: _isPerc });
if (heldMidiNotesRef.current) delete heldMidiNotesRef.current[t.id];
if (timelinePitchesRef.current) delete timelinePitchesRef.current[t.id];
} catch (e) {}
}
if (audible && !wasAudible) becameAudible = true;
trackAudibleRef.current[t.id] = audible;
});
@@ -19114,6 +19180,31 @@ const App = () => {
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
const [editNameInput, setEditNameInput] = useState('');
// D16: per-track loading ruler — ticker buộc re-render khi có track đang
// loading; poller queryStatus chuyển loading→done khi channel sẵn sàng
// (đúng cho cả C++ restore từ spawn lẫn JS load — không phụ thuộc thứ tự).
const [loadTick, setLoadTick] = useState(0);
useEffect(() => {
window.__onTrackLoadState = () => setLoadTick(t => t + 1);
const iv = setInterval(async () => {
try {
if (!window.NativeBridgeService || !window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return;
const m = window.__trackLoadState || {};
const loadingKeys = Object.keys(m).filter(k => m[k].state === 'loading');
if (!loadingKeys.length) return;
const st = await window.NativeBridgeService.queryStatus();
const lc = (st && Array.isArray(st.loaded_channels)) ? st.loaded_channels : null;
let changed = false;
loadingKeys.forEach(k => {
const s = m[k];
if (lc && s.channel != null && lc.some(x => x.channel === s.channel)) { m[k] = Object.assign({}, s, { state: 'done', doneTs: Date.now() }); changed = true; }
else if (!s.startedTs || Date.now() - s.startedTs > 90000) { m[k] = Object.assign({}, s, { state: 'failed' }); changed = true; }
});
if (changed) setLoadTick(t => t + 1);
} catch (e) {}
}, 400);
return () => { delete window.__onTrackLoadState; clearInterval(iv); };
}, []);
// ── Context Menu & Clipboard ──
const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
@@ -19582,6 +19673,7 @@ const App = () => {
}, [activeTab, sessionTabs, subTabs, tracks]);
const activeTracksRef = useRef([]);
activeTracksRef.current = activeTracks;
tracksRef.current = tracks; // D18c: sync tracks state moi nhat (guard fire-time)
const sessionTabsRef = useRef(sessionTabs);
sessionTabsRef.current = sessionTabs;
@@ -23883,12 +23975,14 @@ const App = () => {
// để master FX bị teardown giữa lúc section đang phát).
if (fxRtWarmIdleTimer) { clearTimeout(fxRtWarmIdleTimer); fxRtWarmIdleTimer = null; }
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua
// tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap
// trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu.
const _wrapRestart = fxRtWrapRestart;
const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (!_wrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
if (!st.buffer) {
st.buffer = context.createBuffer(1, 128, context.sampleRate);
@@ -24349,6 +24443,7 @@ const App = () => {
const end = Math.max(st.selectionStart, st.selectionEnd);
if (bufferPos >= end) {
fxRtWrapRestart = true; // V38: loop wrap restart
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
@@ -24377,6 +24472,7 @@ const App = () => {
})() : st.buffer.duration;
if (bufferPos >= effectiveDuration) {
fxRtWrapRestart = true; // V38: loop wrap restart
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
if (st.isLooping) {
@@ -24423,6 +24519,7 @@ const App = () => {
if (selRight > selLeft && updatedTime >= selRight) {
if (hasAnySolo || selectionMode === 'local') {
fxRtWrapRestart = true; // V38: loop wrap restart
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
@@ -24437,6 +24534,7 @@ const App = () => {
setIsPlaying(true);
} else {
fxRtWrapRestart = true; // V38: loop wrap restart
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = selLeft;
@@ -24460,6 +24558,7 @@ const App = () => {
}
if (isLoopingSelection) {
fxRtWrapRestart = true; // V38: loop wrap restart
try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {}
fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime
stopAllPlayback();
startOffsetTimeRef.current = 0;
@@ -25321,6 +25420,14 @@ const App = () => {
setTimeout(function () {
if (epoch !== bridgeNoteEpochRef.current) return;
if (!guardPlay()) return;
// D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track
// vua tro nen inaudible (solo track khac / mute) khong duoc phat ra.
const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : (tracksRef.current && tracksRef.current.length ? tracksRef.current : activeTracks);
const _trk = _list.find(t => t.id === trkId) || track;
if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) return;
// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill
// dung note khi track thanh inaudible giua chung (Qin VST2 tremolo).
try { var _tps = timelinePitchesRef.current; (_tps[trkId] || (_tps[trkId] = new Set())).add(pitch || 60); } catch (e) {}
var _rt = window.SonicMidiRouter;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) { console.warn('[Midi] pushEvent error:', e && e.message); }
}, delayMs);
@@ -25328,6 +25435,8 @@ const App = () => {
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) {}
// D18c: xoa khoi per-track set (note da tat - khong con can kill).
try { var _tps2 = timelinePitchesRef.current; if (_tps2[trkId]) _tps2[trkId].delete(pitch || 60); } catch (e) {}
}, delayMs, durMs || 1000);
return;
}
@@ -25344,12 +25453,14 @@ const App = () => {
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
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua
// tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap
// trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu.
const _wrapRestart = fxRtWrapRestart;
const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (!_wrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {}
_dbgPlayStartMs = performance.now();
_dbgFirstAudioPending = true;
@@ -25852,12 +25963,12 @@ const App = () => {
const context = getAudioContext();
_nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade
restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain
// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc
// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu).
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong
// cho phia sau; flush ring bridge (V30) khong chaym queue nay.
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: 'play' }); } catch (e) {}
// D18: piano-roll wrap (fxRtWrapRestart set o loop restart) — KHONG flush
// ring + clear_queue mode 'loop' (giu queue + wrapFade) nhu main path; truoc
// day luon mode 'play' → wipe + re-buffer moi pass = giat. KHONG reset flag
// (startSubTabPlayback goi sau van can doc de clear dung mode).
try { if (!fxRtWrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {}
try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: fxRtWrapRestart ? 'loop' : 'play' }); } catch (e) {}
// 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).
@@ -26057,18 +26168,11 @@ const App = () => {
};
const stopAllPlayback = (fade) => {
try {
if (fxRtWrapRestart && fxRtChain().length === 0 && nativeLoopSrcsRef.current.size === 0 && masterBus && masterBus.muteGain && masterBus.muteGain.gain) {
// V41: dip CHỈ khi bypass (chain rỗng). FX active → seam thật xảy ra sau
// latency (~80ms) tại _now+latency; dip tại _now chỉ tạo dropout 14ms giữa
// đuôi pass cũ (audible) mà không che được seam click.
// V42: rút ngắn dip 14ms → 5ms (2ms xuống + 3ms lên) — đủ che click seam
// nhưng dropout quá ngắn để nghe thấy (V39 14ms tạo dropout rõ).
const _actx = getAudioContext(); const _now = _actx.currentTime;
masterBus.muteGain.gain.cancelScheduledValues(_now);
masterBus.muteGain.gain.setValueAtTime(masterBus.muteGain.gain.value || 1, _now);
masterBus.muteGain.gain.linearRampToValueAtTime(0.0001, _now + 0.002);
masterBus.muteGain.gain.linearRampToValueAtTime(1, _now + 0.005);
} // V39: dip input truoc hard-stop
// D14: bỏ dip muteGain tại wrap (loop) — 5ms master→0.0001 mỗi vòng =
// âm beat 0:0 nhỏ hơn hẳn âm cuối pass (bug seam volume). Click seam giờ
// xử bằng release mượt (CC123, không CC120 hard-cut — D15) + worklet
// WRAP_FADE 256 (FX path) → không cần che bằng dropout master.
_dbgLog('stop_start', 'fade=' + !!fade + ' ' + _dbgBridgeGain());
isPlayingRef.current = false;
// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng).
@@ -26100,6 +26204,7 @@ 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
_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) {}
@@ -26189,22 +26294,36 @@ const App = () => {
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).
// D17: wrap (loop restart) — BỎ transport STOP. C++ STOP làm allNotesOff
// + CC64=0 trên cả 16 channel → cắt mọi note (kể cả note ngân qua seam)
// + có thể stall audio loop ~20ms (dbg.log: wrap play_start→first_audio
// δ17-32ms vs restart thường δ3-5ms) = "reload" tại beat 0:0. CC123 (D15)
// đã lo release JS-side; PLAY vẫn reset playhead (playheadSamples chỉ đổi
// qua transport); transportStopped giữ false → note-on pass mới không bị
// drop (V8 guard). Stop/seek thật vẫn gửi STOP.
const _wrapNoStop = !!(fxRtWrapRestart && !stale);
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
try { window.NativeBridgeService.transport('stop'); } catch (e) {}
_dbgLog('stop_transport', 'epoch=' + stopEpoch);
if (!_wrapNoStop) { try { window.NativeBridgeService.transport('stop'); } catch (e) {} }
_dbgLog('stop_transport', 'epoch=' + stopEpoch + (_wrapNoStop ? ' wrap-skip' : ''));
// V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay.
// D15: wrap (loop) → chỉ CC123 (all-notes-off, release mượt); bỏ CC120
// (all-sound-off, hard cut → click + âm beat 0:0 tụt). Release chồng
// lên re-noteon pass mới → seam như cùng khuôn nhạc. Stop thật giữ CC120.
try {
const _wrapRelease = !!(fxRtWrapRestart && !stale);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive && window.SonicMidiRouter.isBridgeActive()) {
Object.keys(trackMidiChannelsRef.current || {}).forEach(function (_tid) {
var _ch = trackMidiChannelsRef.current[_tid];
window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 123, 0, 0, 0, 0, false);
window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 120, 0, 0, 0, 0, false);
if (!_wrapRelease) window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 120, 0, 0, 0, 0, false);
});
}
} catch (e) { console.warn('[Stop] midi panic error:', e); }
// V27: xóa audio còn buffer trong pipeline (preBuffer/chunks + worklet
// ring) — nếu không, âm trước stop còn trong ring phát tiếp sau fade.
try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) { console.warn('[Stop] bridge flush error:', e); }
// D18: wrap (_wrapNoStop) — KHÔNG flush (giữ dòng liên tục qua seam);
// flush ở đây + ở startTrackPlayback = ring rỗng → gap giật beat 0:0.
if (!_wrapNoStop) { try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) { console.warn('[Stop] bridge flush error:', 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
@@ -34001,7 +34120,38 @@ STRICT CONSTRAINTS:
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "trash-2",
className: "w-3 h-3"
}))))), /*#__PURE__*/React.createElement("div", {
}))))), (function () {
const _ls = window.__trackLoadState && window.__trackLoadState[track.id];
if (_ls && _ls.state === 'loading') {
return /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1.5 mt-0.5",
title: "Đang nạp instrument vào bridge — track sẽ phát sau khi load xong"
}, /*#__PURE__*/React.createElement("div", {
className: "flex-1 h-1 rounded bg-zinc-800 overflow-hidden"
}, /*#__PURE__*/React.createElement("div", {
className: "h-full rounded sf-ruler-bar",
style: {
background: '#06b6d4',
width: '45%'
}
})), /*#__PURE__*/React.createElement("span", {
className: "text-[9px] text-cyan-400 font-mono shrink-0"
}, "Loading…"));
}
if (_ls && _ls.state === 'failed') {
return /*#__PURE__*/React.createElement("div", {
className: "mt-0.5 text-[9px] text-red-500 font-mono"
}, "Load failed");
}
if (_ls && _ls.state === 'done' && _ls.doneTs && Date.now() - _ls.doneTs < 2500) {
return /*#__PURE__*/React.createElement("div", {
className: "mt-0.5 h-1 rounded bg-green-600/30 overflow-hidden"
}, /*#__PURE__*/React.createElement("div", {
className: "h-full bg-green-500 sf-ruler-done"
}));
}
return null;
})(), /*#__PURE__*/React.createElement("div", {
className: "flex flex-col gap-0.5 text-xs",
onClick: e => e.stopPropagation()
}, /*#__PURE__*/React.createElement("div", {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -31,7 +31,7 @@
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301';
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608291200';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
+3 -2
View File
@@ -1,6 +1,7 @@
// app/static/js/services/bridgeAudioWorklet.js
// VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread).
// - Ring block 256 mau, toi da 8 block (~42ms @48k) — hap thu jitter pump
// - Ring block 256 mau, toi da 16 block (~85ms @48k) — hap thu jitter pump
// (option 2: 8->16 de dung tremolo crackle; backlog lon hon ~85ms)
// nhung gioi han backlog: 64 block (~371ms) tao tre ~0.3-0.4s khi play/
// keyboard preview (audio moi phai cho ring cu chay het).
// - Underrun: fade ve 0 thay vi click (hard silence).
@@ -10,7 +11,7 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._ring = [];
this._MAX = 8;
this._MAX = 16;
this._gain = 1.0;
this._lastOutL = 0.0;
this._lastOutR = 0.0;