fix(midi): playback khong cam loop pass 2+ va note cung pitch khong bi cat (bugs 1-2) - set isPlayingRef truoc bridge PLAY (startTrackPlayback/local/piano-roll), cancel STOP#2 retry khi loop restart; dedupe NOTE_OFF theo channel:pitch (pendingNoteOffMap + scheduleNoteOff) de NOTE_OFF tre (dur+30ms) khong cat note-on cung pitch ke tiep (bar lien nhau, piano roll note chong), clear map khi stop; sync 5 ban JS + index.html ?v=202608181807, rebuild exe release

This commit is contained in:
2026-08-18 18:13:36 +07:00
parent 6b6328044c
commit 45a15da55e
5 changed files with 2157 additions and 2021 deletions
+69 -5
View File
@@ -113,9 +113,30 @@ const ensureSonicInstrument = (ctx) => {
// track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback
// (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI play
// qua Carla bridge khi VSTi được loaded.
// FIX (Bug 2): NOTE_OFF trễ (dur+30ms) của note cùng pitch bắn SAU note-on kế
// tiếp (bar liền nhau cùng hợp âm, piano roll note chồng) -> cắt note mới →
// câm. Map pending theo key channel:pitch — schedule NOTE_OFF mới cùng key →
// hủy NOTE_OFF cũ (retrigger đúng, note mới ngân đủ duration).
const pendingNoteOffMap = {};
const scheduleNoteOff = (key, fn, delayMs) => {
const prev = pendingNoteOffMap[key];
if (prev) { try { clearTimeout(prev); } catch (e) {} }
const timer = setTimeout(function () {
if (pendingNoteOffMap[key] === timer) delete pendingNoteOffMap[key];
fn();
}, delayMs);
pendingNoteOffMap[key] = timer;
return timer;
};
const clearAllPendingNoteOffs = () => {
Object.keys(pendingNoteOffMap).forEach(function (k) {
try { clearTimeout(pendingNoteOffMap[k]); } catch (e) {}
delete pendingNoteOffMap[k];
});
};
const fireCarlaNote = (ch, pitch, vel, delay, durMs) => {
setTimeout(function () { window.SonicCarlaMidi.noteOn(ch, pitch, vel); }, delay);
setTimeout(function () { window.SonicCarlaMidi.noteOff(ch, pitch); }, delay + durMs + 30);
scheduleNoteOff('C:' + ch + ':' + pitch, function () { window.SonicCarlaMidi.noteOff(ch, pitch); }, delay + durMs + 30);
};
// ⚠️ FIX (Bug 2): Carla mở ASYNC lúc play (ensureCarlaForPlayback) — nốt gửi
// trước khi OSC engine bind cổng bị mất → item câm dù keybed kêu. Nốt khi
@@ -6488,13 +6509,20 @@ const loadVstToBridge = async (track) => {
}
// V9: path sai (plugin_path cũ / cache của plugin khác) → bỏ qua, resolve theo tên.
knownPath = _bridgeSafeVstPath(knownPath, instrumentId);
// V10: C++ đã load/restore ĐÚNG instrument trên channel (bridge_state.json,
// Rust bridge_status trả qua Tauri invoke — không cần HTTP/auth) → dùng path
// đó load thẳng; C++ double-load guard skip nên không LOAD trùng.
if (!knownPath && window.__bridgeLoadedState) {
const lc = window.__bridgeLoadedState.find(x => x.channel === bch && _bridgeSafeVstPath(x.path, instrumentId));
if (lc) knownPath = lc.path;
}
// Đã 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 (window.__bridgeLoadedPaths[bch] === knownPath) 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 (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = knownPath; if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {}; window.__bridgePluginPaths[instrumentId] = knownPath; }
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', knownPath);
return !!ok;
}
@@ -6546,7 +6574,22 @@ const ensureAndOpenVstGui = async (track) => {
// guard skip → ok=true → GUI vẫn mở. Không có instrument → OPEN_GUI defer.
let loadOk = false;
try { loadOk = await loadVstToBridge(track); } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', trackId, e); }
if (!loadOk) return;
// V9 regression: loadVstToBridge tra false khi KHONG resolve lai duoc path
// (synth_engine.plugin_path khong luu / instrumentSelectorData chua load /
// POST /bridge/load auth-CORS fail / _bridgeSafeVstPath null path cu sai)
// DU track da co DUNG instrument tren channel tu truoc (flag) → GUI chet
// im lang. Chi chan khi channel THUC SU chua co instrument — mo GUI khi do
// se mo cua plugin KHAC (V9 guard WaveObserver vs Nexus, giu nguyen).
if (!loadOk && !window.__bridgeLoadedChannels[bch]) {
// V10: loadVstToBridge fail (token hết hạn / path không resolve được) nhưng
// C++ ĐÃ restore ĐÚNG instrument từ state file → mở GUI thẳng, không cần
// LOAD. Chỉ mở khi path khớp instrumentId (giữ V9 guard: không mở GUI của
// plugin KHÁC — VD WaveObserver thay vì Nexus).
const lc = (window.__bridgeLoadedState || []).find(x => x.channel === bch && _bridgeSafeVstPath(x.path, instrumentId));
if (!lc) return;
window.__bridgeLoadedChannels[bch] = true;
window.__bridgeLoadedPaths[bch] = lc.path;
}
try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); }
};
// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) — OPEN_GUI
@@ -22616,7 +22659,7 @@ const App = () => {
if (!guardPlay()) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) {}
}, delayMs);
setTimeout(function () {
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) {}
@@ -22646,6 +22689,11 @@ const App = () => {
}
try { window.NativeBridgeService.transport('play'); } catch (e) {}
}
// V12: guardPlay (scheduleMidiNoteDispatch) đọc isPlayingRef.current — nếu
// để React render set (setIsPlaying(true) sau) thì note-on delay=0 của
// loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop
// → loop pass 2+ câm. Set NGAY tại scheduler.
isPlayingRef.current = true;
// ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
// qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
@@ -22960,6 +23008,18 @@ const App = () => {
// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
const startLocalTrackPlayback = (trackId, offsetTime) => {
const context = getAudioContext();
// V12: parity với startTrackPlayback — loop restart (solo/local) sau
// stopAllPlayback(): hủy STOP#2 + báo PLAY (nếu không C++ transportStopped
// giữ true → mọi note-on bị drop → solo/local loop pass 2+ câm) + set
// isPlayingRef NGAY (guardPlay race — xem startTrackPlayback).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
if (bridgeStopRetryTimerRef.current) {
clearTimeout(bridgeStopRetryTimerRef.current);
bridgeStopRetryTimerRef.current = null;
}
try { window.NativeBridgeService.transport('play'); } catch (e) {}
}
isPlayingRef.current = true;
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const track = tracks.find(t => t.id === trackId);
@@ -23076,6 +23136,9 @@ const App = () => {
}
try { window.NativeBridgeService.transport('play'); } catch (e) {}
}
// V12: guardPlay race tương tự startTrackPlayback — set NGAY để note-on
// delay=0 của sub-tab loop restart không bị drop.
isPlayingRef.current = true;
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
refreshCarlaStatus();
const midiNotes = notesOverride || st.notes || [];
@@ -23130,7 +23193,7 @@ const App = () => {
const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : mainCh;
const carlaPitch = note.pitch || 60;
setTimeout(function () { window.SonicCarlaMidi.noteOn(carlaCh, carlaPitch, carlaVel); }, carlaDelay);
setTimeout(function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay + durMs + 30);
scheduleNoteOff('C:' + carlaCh + ':' + carlaPitch, function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay + durMs + 30);
}
}
});
@@ -23273,6 +23336,7 @@ const App = () => {
// 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 { if (subTabsRef.current) subTabsRef.current = subTabsRef.current.map(s => ({ ...s, isPlaying: false })); } catch (e) {}
activeSourcesRef.current.forEach(src => {
try {
File diff suppressed because one or more lines are too long
@@ -75,6 +75,12 @@
try {
var s = await window.__TAURI__.core.invoke('bridge_status');
this._notifyStatus(!!s.connected);
// V10: C++ loaded channels (bridge_state.json) — nguồn path thật không
// cần HTTP/auth. loadVstToBridge/ensureAndOpenVstGui dùng để mở GUI khi
// JS không resolve được path (token hết hạn / listPlugins thiếu path).
if (s && Array.isArray(s.loaded_channels)) {
window.__bridgeLoadedState = s.loaded_channels;
}
return s;
} catch (e) {
this._notifyStatus(false);