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);
+2 -2
View File
@@ -39,7 +39,7 @@
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608112200"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608181735"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608152200"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
@@ -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=202608181642" defer></script>
<script src="/static/js/app.precompiled.js?v=202608181807" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+40 -1
View File
@@ -754,6 +754,41 @@ struct BridgeStatus {
shm_size_bytes: usize,
sample_rate: u32,
block_size: u32,
// V10: channel -> path mà C++ đã load/restore (bridge_state.json). JS dùng
// làm nguồn sự thật khi webview không load được qua HTTP (token hết hạn /
// listPlugins thiếu path) — C++ restore độc lập nên GUI vẫn mở được.
loaded_channels: Vec<BridgeLoadedChannel>,
}
#[derive(serde::Serialize)]
struct BridgeLoadedChannel {
channel: u32,
path: String,
}
#[derive(serde::Deserialize)]
struct BridgeStateFile {
#[serde(default)]
instruments: Vec<BridgeStateInstrument>,
}
#[derive(serde::Deserialize)]
struct BridgeStateInstrument {
channel: u32,
path: String,
}
/// %APPDATA%/SonicForgeDAW/state/bridge_state.json — C++ StateStore::save ghi
/// mỗi ~2s khi có instrument + khi DUMP_STATE lúc pre-kill (main.cpp G2.1).
fn read_bridge_loaded_channels() -> Vec<BridgeLoadedChannel> {
let Some(appdata) = std::env::var("APPDATA").ok() else { return Vec::new() };
let path = Path::new(&appdata).join("SonicForgeDAW").join("state").join("bridge_state.json");
let Ok(raw) = std::fs::read_to_string(&path) else { return Vec::new() };
let Ok(state): Result<BridgeStateFile, _> = serde_json::from_str(&raw) else { return Vec::new() };
state.instruments
.iter()
.map(|i| BridgeLoadedChannel { channel: i.channel, path: i.path.clone() })
.collect()
}
/// %APPDATA%/SonicForgeDAW/ipc — cùng dir engine (plugins.py) đọc cho
@@ -767,7 +802,7 @@ fn bridge_ipc_dir() -> Option<PathBuf> {
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?;
let status = match guard.as_ref() {
let mut status = match guard.as_ref() {
Some(shm) => BridgeStatus {
connected: true,
shm_name: shm::shm_name(),
@@ -780,6 +815,7 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
*g
},
block_size: shm::AUDIO_BLOCK_SIZE as u32,
loaded_channels: Vec::new(),
},
None => BridgeStatus {
connected: false,
@@ -789,8 +825,11 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
shm_size_bytes: 0,
sample_rate: 0,
block_size: 0,
loaded_channels: Vec::new(),
},
};
// V10: C++ loaded channels (state file) — không phụ thuộc HTTP/auth.
status.loaded_channels = read_bridge_loaded_channels();
// E1: ghi JSON → engine /api/v1/bridge/status (badge Plugin Manager phải
// phản ánh đúng bridge thật — trước đây file không tồn tại → luôn OFF).
if let Some(dir) = bridge_ipc_dir() {