fix: GUI VST native window qua bridge + audio path bridge (SF2 preview/play câm)
- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window) - main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST), tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling) - app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla) - E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset) - docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
This commit is contained in:
@@ -12,15 +12,17 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2", features = ["webview-data-url"] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# Shared memory (SonicForge_DAW_IPC) for the Native Host Bridge
|
||||
raw-window-handle = "0.6"
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_MemoryManagement",
|
||||
"Win32_Security",
|
||||
"Win32_System_Memory",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
PLACEHOLDER - duoc thay boi build_windows.ps1 buoc [4/6] (copy dist\daw_engine)
|
||||
+97
-36
@@ -12,7 +12,7 @@
|
||||
// và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log.
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_dialog::{DialogExt, FilePath};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
@@ -23,6 +23,30 @@ use shm::{Shm, ShmState};
|
||||
struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||
struct BridgeProcess(Mutex<Option<CommandChild>>);
|
||||
|
||||
/// Kill the currently managed bridge child (if any) — a restart MUST never
|
||||
/// leave the old bridge running, else two daw_vst_bridge.exe race on the same
|
||||
/// SHM (double instrument load, double OPEN_GUI, garbled control queue).
|
||||
fn kill_bridge(app: &AppHandle) -> bool {
|
||||
let state = app.state::<BridgeProcess>();
|
||||
let mut guard = match state.0.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return false,
|
||||
};
|
||||
match guard.take() {
|
||||
Some(child) => match child.kill() {
|
||||
Ok(()) => {
|
||||
println!("Native Host Bridge killed (restart)");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Bridge kill failed: {e}");
|
||||
false
|
||||
}
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct AudioFrame {
|
||||
@@ -65,6 +89,8 @@ fn bridge_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
|
||||
(res_dir.join("binaries").join(exe_name), "resource_dir/binaries (bundle)".into()),
|
||||
(res_dir.join(exe_name), "resource_dir (bundle root)".into()),
|
||||
(res_dir.join("binaries").join(triple_name), "resource_dir/binaries (triple name)".into()),
|
||||
(res_dir.join(triple_name), "resource_dir (triple name, bundle)".into()),
|
||||
(exe_dir.join(triple_name), "exe_dir (triple name, bundle)".into()),
|
||||
(exe_dir.join(exe_name), "exe_dir (portable)".into()),
|
||||
(exe_dir.join("..").join("resources").join("binaries").join(exe_name), "exe_dir/../resources/binaries (dev)".into()),
|
||||
(exe_dir.join("..").join("resources").join(exe_name), "exe_dir/../resources (dev)".into()),
|
||||
@@ -90,6 +116,9 @@ fn spawn_bridge(
|
||||
found = Some((path.clone(), label.clone()));
|
||||
}
|
||||
}
|
||||
// B3 DoD: dòng tóm tắt trạng thái bridge trong spawn.log
|
||||
log_line.push_str(&format!("[daw_vst_bridge] exists={}
|
||||
", found.is_some()));
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
@@ -109,8 +138,32 @@ fn spawn_bridge(
|
||||
.env("SF_BLOCK_SIZE", "256")
|
||||
.spawn()
|
||||
{
|
||||
Ok((_rx, child)) => {
|
||||
Ok((mut rx, child)) => {
|
||||
app.manage(BridgeProcess(Mutex::new(Some(child))));
|
||||
// B3: redirect bridge stdout/stderr → %APPDATA%/SonicForgeDAW/logs/bridge.log
|
||||
// (tauri-shell pipe rồi vứt rx; đọc event ghi file — E5 UI tail được).
|
||||
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
|
||||
let bridge_log = std::path::Path::new(&log_dir)
|
||||
.join("SonicForgeDAW").join("logs").join("bridge.log");
|
||||
if let Some(parent) = bridge_log.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
tauri::async_runtime::spawn(async move {
|
||||
use std::io::Write;
|
||||
let mut f = match std::fs::OpenOptions::new().create(true).append(true).open(&bridge_log) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return,
|
||||
};
|
||||
while let Some(ev) = rx.recv().await {
|
||||
let bytes: Vec<u8> = match ev {
|
||||
CommandEvent::Stdout(b) | CommandEvent::Stderr(b) => b,
|
||||
_ => continue,
|
||||
};
|
||||
if let Ok(text) = String::from_utf8(bytes) {
|
||||
let _ = writeln!(f, "{}", text.trim_end());
|
||||
}
|
||||
}
|
||||
});
|
||||
println!("Native Host Bridge started ({label}): {}", bridge_exe.display());
|
||||
true
|
||||
}
|
||||
@@ -290,6 +343,9 @@ pub fn run() {
|
||||
if advanced == Some(true) {
|
||||
last_change = std::time::Instant::now();
|
||||
down_emitted = false;
|
||||
// Bridge đã sống lại sau restart — đóng "sự cố" để lần
|
||||
// stall sau lại được hưởng 1 restart (mỗi incident 1 lần).
|
||||
restart_attempts = 0;
|
||||
let (l, r) = guard.as_ref().map(|s| s.read_audio()).unwrap_or(([0f32; shm::AUDIO_BLOCK_SIZE], [0f32; shm::AUDIO_BLOCK_SIZE]));
|
||||
let _ = pump_handle.emit(
|
||||
"bridge-audio",
|
||||
@@ -308,6 +364,9 @@ pub fn run() {
|
||||
let log_path = std::path::Path::new(&log_dir)
|
||||
.join("SonicForgeDAW").join("logs").join("spawn.log");
|
||||
let mut line = String::from("[tauri] bridge stalled 3s — restart attempt #1\n");
|
||||
// FIX: kill bridge cũ trước — không được để 2 bridge
|
||||
// cùng map SHM (race control queue / double load).
|
||||
kill_bridge(&pump_handle);
|
||||
if spawn_bridge(&pump_handle, &res_dir, &exe_dir, &mut line, &log_path) {
|
||||
// count restarts into bridge.log (same file as stdout redirect)
|
||||
let bridge_log = std::path::Path::new(&log_dir)
|
||||
@@ -436,7 +495,7 @@ fn push_midi_event(
|
||||
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
||||
shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset)
|
||||
.then_some(())
|
||||
.ok_or("midi queue full")
|
||||
.ok_or_else(|| "midi queue full".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -446,41 +505,27 @@ fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, cha
|
||||
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
||||
shm.push_control(2, instrument_type as u32, 0, channel as u32, &path)
|
||||
.then_some(())
|
||||
.ok_or("control queue full")
|
||||
.ok_or_else(|| "control queue full".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_vst_gui(app: AppHandle, plugin_id: String) -> Result<(), String> {
|
||||
// B9: child WebviewWindow acts as the VST GUI surface. The bridge receives
|
||||
// its HWND (control type=4, arg1) so the VST3 editor can be attached (A7).
|
||||
let url = tauri::WebviewUrl::External(
|
||||
tauri::Url::parse("data:text/html,<h2>VST GUI</h2><p>Editor attaches from daw_vst_bridge (A7).</p>")
|
||||
.map_err(|e| e.to_string())?,
|
||||
);
|
||||
let win = tauri::WebviewWindowBuilder::new(&app, format!("vst-{}", plugin_id), url)
|
||||
.title(format!("VST — {}", plugin_id))
|
||||
.inner_size(800.0, 600.0)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// B9: bridge tự tạo native Win32 window cho editor VST3 (không qua
|
||||
// WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0)
|
||||
// với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread.
|
||||
let state = app.state::<ShmState>();
|
||||
let guard = state.0.lock().map_err(|e| e.to_string())?;
|
||||
let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
|
||||
// ponytail: HWND truncated to u32 (HWNDs fit in practice); window-close →
|
||||
// close_gui wiring lands together with A7 on Windows.
|
||||
#[cfg(windows)]
|
||||
let hwnd = {
|
||||
use tauri::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
match win.window_handle().map(|h| h.as_raw()) {
|
||||
Ok(RawWindowHandle::Win32(w)) => w.hwnd as usize as u32,
|
||||
_ => 0u32,
|
||||
}
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
let hwnd = 0u32;
|
||||
shm.push_control(4, 0, hwnd, 0, &plugin_id)
|
||||
.then_some(())
|
||||
.ok_or("control queue full")
|
||||
let lock = state.0.lock();
|
||||
match lock {
|
||||
Ok(guard) => match guard.as_ref() {
|
||||
Some(shm) => {
|
||||
let ok = shm.push_control(4, 0, 0, 0, &plugin_id);
|
||||
eprintln!("open_vst_gui: push_control type=4 hwnd=0 (bridge-native) ok={}", ok);
|
||||
}
|
||||
None => eprintln!("open_vst_gui: bridge shm unavailable"),
|
||||
},
|
||||
Err(e) => eprintln!("open_vst_gui: shm lock poisoned: {}", e),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -494,11 +539,18 @@ struct BridgeStatus {
|
||||
block_size: u32,
|
||||
}
|
||||
|
||||
/// %APPDATA%/SonicForgeDAW/ipc — cùng dir engine (plugins.py) đọc cho
|
||||
/// /api/v1/bridge/status; None khi APPDATA unset (non-Windows).
|
||||
fn bridge_ipc_dir() -> Option<PathBuf> {
|
||||
let appdata = std::env::var("APPDATA").ok()?;
|
||||
Some(Path::new(&appdata).join("SonicForgeDAW").join("ipc"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
||||
let state = app.state::<ShmState>();
|
||||
let guard = state.0.lock().map_err(|e| e.to_string())?;
|
||||
Ok(match guard.as_ref() {
|
||||
let status = match guard.as_ref() {
|
||||
Some(shm) => BridgeStatus {
|
||||
connected: true,
|
||||
shm_name: shm::SHM_NAME,
|
||||
@@ -517,7 +569,16 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
||||
sample_rate: 0,
|
||||
block_size: 0,
|
||||
},
|
||||
})
|
||||
};
|
||||
// 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() {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
if let Ok(json) = serde_json::to_string(&status) {
|
||||
let _ = std::fs::write(dir.join("bridge_status"), json);
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -533,5 +594,5 @@ fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Res
|
||||
};
|
||||
shm.push_control(3, arg0, arg1, 0, "")
|
||||
.then_some(())
|
||||
.ok_or("control queue full")
|
||||
.ok_or_else(|| "control queue full".to_string())
|
||||
}
|
||||
|
||||
+16
-4
@@ -8,7 +8,7 @@ use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use std::sync::Mutex;
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows_sys::Win32::System::MemoryManagement::{
|
||||
use windows_sys::Win32::System::Memory::{
|
||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
|
||||
};
|
||||
|
||||
@@ -83,13 +83,13 @@ impl Shm {
|
||||
return None;
|
||||
}
|
||||
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
|
||||
if view.is_null() {
|
||||
if view.Value.is_null() {
|
||||
unsafe { CloseHandle(handle) };
|
||||
return None;
|
||||
}
|
||||
Some(Shm {
|
||||
handle,
|
||||
view: view as *mut SharedAudioBufferIPC,
|
||||
view: view.Value as *mut SharedAudioBufferIPC,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ impl Shm {
|
||||
impl Drop for Shm {
|
||||
fn drop(&mut self) {
|
||||
if !self.view.is_null() {
|
||||
unsafe { UnmapViewOfFile(self.view as *const c_void) };
|
||||
unsafe { UnmapViewOfFile(windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: self.view as *mut c_void }) };
|
||||
}
|
||||
unsafe { CloseHandle(self.handle) };
|
||||
}
|
||||
@@ -164,3 +164,15 @@ impl Drop for Shm {
|
||||
|
||||
/// Process-wide shared-memory state for Tauri.
|
||||
pub struct ShmState(pub Mutex<Option<Shm>>);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn layout_matches_c_header() {
|
||||
// native_bridge/tests/shm_selfcheck.cpp asserts the same sizes.
|
||||
assert_eq!(std::mem::size_of::<MidiEventIPC>(), 12);
|
||||
assert_eq!(std::mem::size_of::<ControlEventIPC>(), 1040);
|
||||
assert_eq!(std::mem::size_of::<SharedAudioBufferIPC>(), 11160);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"icons/favicon.ico"
|
||||
],
|
||||
"resources": {
|
||||
"resources/daw_engine": "daw_engine/"
|
||||
"resources/daw_engine": "daw_engine/",
|
||||
"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll"
|
||||
},
|
||||
"externalBin": ["binaries/daw_vst_bridge"],
|
||||
"windows": {
|
||||
|
||||
+38
-16
@@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -12,23 +12,45 @@
|
||||
<body>
|
||||
<div id="msg">Đang khởi động SonicForge Engine…</div>
|
||||
<script>
|
||||
// Engine (sidecar) có thể cần vài giây để boot (import librosa/pedalboard).
|
||||
// Thử health-check trên dải port 8000-8010, redirect tới port đầu tiên OK.
|
||||
// Frontend host (Tauri asset page). KHÔNG location.href redirect: chuyển
|
||||
// sang http://127.0.0.1:8000 sẽ rời Tauri context -> mất window.__TAURI__
|
||||
// -> NativeBridgeService vô hiệu (bridge-audio/load_instrument không dùng
|
||||
// được; MIDI rơi về fallback WASM/Carla). Thay vào đó: quét port engine
|
||||
// (8000-8010), nạp toàn bộ UI engine qua document.write trên CÙNG document
|
||||
// (origin tauri:// giữ nguyên -> __TAURI__ sống), base href + API_BASE_URL
|
||||
// trỏ engine HTTP (CORS * đã bật).
|
||||
(async function () {
|
||||
var tries = 0;
|
||||
while (tries < 90) { // ~60s tối đa
|
||||
for (var p = 8000; p <= 8010; p++) {
|
||||
try {
|
||||
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
|
||||
if (r.ok) { location.href = 'http://127.0.0.1:' + p + '/'; return; }
|
||||
} catch (e) { /* port chưa mở */ }
|
||||
}
|
||||
tries++;
|
||||
await new Promise(function (res) { setTimeout(res, 700); });
|
||||
var base = null;
|
||||
for (var p = 8000; p <= 8010 && !base; p++) {
|
||||
try {
|
||||
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
|
||||
if (r.ok) base = 'http://127.0.0.1:' + p + '/';
|
||||
} catch (e) { /* port chưa mở */ }
|
||||
}
|
||||
document.getElementById('msg').textContent =
|
||||
'Không tìm thấy SonicForge Engine (port 8000–8010). Hãy đóng và chạy lại ứng dụng. ' +
|
||||
'Nếu lặp lại, xem log: %APPDATA%\\SonicForgeDAW\\logs\\engine.log';
|
||||
if (!base) {
|
||||
document.getElementById('msg').textContent =
|
||||
'Không tìm thấy SonicForge Engine (port 8000–8010). Hãy đóng và chạy lại ứng dụng. ' +
|
||||
'Nếu lặp lại, xem log: %APPDATA%\\SonicForgeDAW\\logs\\engine.log';
|
||||
return;
|
||||
}
|
||||
var html;
|
||||
try {
|
||||
var res = await fetch(base, { cache: 'no-store' });
|
||||
html = await res.text();
|
||||
} catch (e) {
|
||||
document.getElementById('msg').textContent = 'Engine không phản hồi: ' + e;
|
||||
return;
|
||||
}
|
||||
// base href để /static, /api resolve về engine; API_BASE_URL override
|
||||
// location.origin (origin thật là tauri://). KHÔNG trailing slash trong
|
||||
// API_BASE_URL: api.js nối `${API_BASE_URL}/api/...` -> double slash
|
||||
// `//api/...` -> FastAPI 404 "Not Found" (login báo not found).
|
||||
html = html.replace('<head>',
|
||||
'<head>\n<base href="' + base + '">\n' +
|
||||
'<script>window.API_BASE_URL = "' + base.replace(/\/$/, '') + '";<\/script>');
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user