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:
+69
-5
@@ -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 {
|
||||
|
||||
+2040
-2013
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);
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user