Files
SonicForgeStudio/src-tauri/src/lib.rs
T

816 lines
36 KiB
Rust

// SonicForge DAW — desktop shell: spawn/terminate daw_engine sidecar.
//
// QUAN TRONG (fix bản 1.1.1 — engine không chạy trên Windows):
// tauri.conf.json bundle.resources giờ dùng dạng MAP:
// { "resources/daw_engine": "daw_engine/" }
// vì dạng ARRAY ("resources/daw_engine") copy file tới
// $RESOURCE_DIR/resources/daw_engine/... (giữ tiền tố "resources/"),
// trong khi code cũ tìm ở $RESOURCE_DIR/daw_engine/... -> exists=false.
// Dạng map (Walk mode) giữ nguyên cây thư mục (_internal) dưới đích
// "daw_engine/" — đúng layout lib.rs chờ.
// Ngoài ra lib.rs còn dò THÊM các vị trí fallback (legacy/portable/dev)
// 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, CommandEvent};
use tauri_plugin_shell::ShellExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
mod shm;
/// App đang tắt (cửa sổ chính đã Destroyed) — pump thread phải dừng ngay,
/// không respawn bridge mới sau khi sidecar đã bị giết (fix: app còn sống
/// sau khi đóng window → pump thấy stall 3s → spawn bridge mới → orphan).
static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
// F-PROC-2 (audit 5.2): serialize kill_bridge -> spawn_bridge across the pump
// watchdog thread and the sample-rate restart command. Without it both threads
// can kill+spawn concurrently and leave two daw_vst_bridge.exe on the same SHM.
static BRIDGE_RESTART_LOCK: Mutex<()> = Mutex::new(());
use shm::{Shm, ShmState};
struct EngineProcess(Mutex<Option<CommandChild>>);
struct BridgeProcess(Mutex<Option<CommandChild>>);
struct BridgeSampleRate(Mutex<u32>);
/// Overwrite the managed bridge child. `app.manage` is a NO-OP once the state
/// exists (StateManager::set keeps the first value), so restarts would leave
/// the state at None and the Destroyed handler would never kill the new
/// bridge — orphan daw_vst_bridge.exe. First call still uses manage().
fn set_bridge_child(app: &AppHandle, child: Option<CommandChild>) {
if let Some(state) = app.try_state::<BridgeProcess>() {
if let Ok(mut g) = state.0.lock() {
*g = child;
}
} else {
app.manage(BridgeProcess(Mutex::new(child)));
}
}
/// G2.2: ask the bridge to flush its state file NOW (control type=5
/// DUMP_STATE), then give the UiThread save job ~300ms to finish before
/// the process is killed. Harmless when the bridge is already hung (SHM
/// is owned by Rust; the control is simply never consumed).
fn flush_bridge_state(app: &AppHandle) {
if let Some(state) = app.try_state::<ShmState>() {
if let Ok(guard) = state.0.lock() {
if let Some(shm) = guard.as_ref() {
let _ = shm.push_control(5, 0, 0, 0, "");
}
}
}
std::thread::sleep(std::time::Duration::from_millis(300));
}
/// 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 {
flush_bridge_state(app);
let state = app.state::<BridgeProcess>();
let mut guard = match state.0.lock() {
Ok(g) => g,
Err(_) => return false,
};
match guard.take() {
Some(child) => {
let ok = kill_process_tree(&child) || child.kill().is_ok();
if ok {
println!("Native Host Bridge killed (restart)");
} else {
println!("Bridge kill failed");
}
ok
}
None => false,
}
}
/// Kill a child plus its whole process tree via taskkill /T /F. The engine
/// (PyInstaller ONEDIR) and bridge are direct children, but /T guarantees no
/// orphan subprocess stays behind when the DAW window closes.
fn kill_process_tree(child: &CommandChild) -> bool {
let pid = child.pid();
std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/T", "/F"])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
/// G3.2: true if a process with `pid` still exists (OpenProcess + exit code).
/// Used to classify a stalled heartbeat: pid alive = HANG, pid gone = CRASH.
fn process_alive(pid: u32) -> bool {
use windows_sys::Win32::Foundation::STILL_ACTIVE;
use windows_sys::Win32::System::Threading::{GetExitCodeProcess, OpenProcess};
let handle = unsafe { OpenProcess(windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return false; // not found or no access -> treat as gone
}
let mut code: u32 = 0;
let ok = unsafe { GetExitCodeProcess(handle, &mut code) } != 0;
unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) };
ok && code == STILL_ACTIVE as u32
}
/// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event).
#[derive(Clone, serde::Serialize)]
struct AudioFrame {
l: Vec<f32>,
r: Vec<f32>,
}
/// Các vị trí có thể chứa daw_engine, theo thứ tự ưu tiên.
fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
let exe_name = if cfg!(windows) { "daw_engine.exe" } else { "daw_engine" };
vec![
// 1. Layout chuẩn (resources map): $RESOURCE/daw_engine/daw_engine.exe
(
res_dir.join("daw_engine").join(exe_name),
"resource_dir/daw_engine (map layout)".into(),
),
// 2. Legacy (resources array cũ giữ tiền tố resources/)
(
res_dir.join("resources").join("daw_engine").join(exe_name),
"resource_dir/resources/daw_engine (legacy array)".into(),
),
// 3. Portable: engine đặt cạnh exe
(
exe_dir.join("daw_engine").join(exe_name),
"exe_dir/daw_engine (portable)".into(),
),
// 4. Dev mode: target/{debug,release} -> src-tauri/resources
(
exe_dir.join("..").join("resources").join("daw_engine").join(exe_name),
"exe_dir/../resources/daw_engine (dev)".into(),
),
]
}
/// Các vị trí có thể chứa daw_vst_bridge (Native Host Bridge sidecar).
fn bridge_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
let exe_name = if cfg!(windows) { "daw_vst_bridge.exe" } else { "daw_vst_bridge" };
let triple_name = if cfg!(windows) { "daw_vst_bridge-x86_64-pc-windows-msvc.exe" } else { exe_name };
vec![
(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()),
]
}
/// Spawn daw_vst_bridge.exe sidecar (B3/B8) with SHM + watchdog + audio env.
/// Appends candidate diagnostics to spawn.log; manages `BridgeProcess`.
/// Returns true when the child started.
fn spawn_bridge(
app: &AppHandle,
res_dir: &Path,
exe_dir: &Path,
log_line: &mut String,
spawn_log_path: &Path,
) -> bool {
let candidates = bridge_candidates(res_dir, exe_dir);
let mut found: Option<(PathBuf, String)> = None;
for (path, label) in &candidates {
let exists = path.exists();
log_line.push_str(&format!(" [bridge {label}] {} exists={}\n", path.display(), exists));
if found.is_none() && exists {
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)
.open(spawn_log_path)
{
use std::io::Write;
let _ = f.write_all(log_line.as_bytes());
}
match found {
Some((bridge_exe, label)) => {
let sample_rate = {
let rate_state = app.state::<BridgeSampleRate>();
let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
*g
};
match app
.shell()
.command(&bridge_exe)
.env("SF_SHM_NAME", shm::shm_name())
.env("SF_PARENT_PID", std::process::id().to_string())
.env("SF_SAMPLE_RATE", sample_rate.to_string())
.env("SF_BLOCK_SIZE", "256")
// G2.3: point the bridge at the persisted state file so a
// respawn can auto-reload instruments (StateStore::defaultPath).
.env("SF_STATE_FILE", std::env::var("APPDATA")
.unwrap_or_else(|_| ".".into())
+ "/SonicForgeDAW/state/bridge_state.json")
.spawn()
{
Ok((mut rx, child)) => {
set_bridge_child(app, 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
}
Err(e) => {
println!("Failed to spawn daw_vst_bridge {bridge_exe:?}: {e}");
set_bridge_child(app, None);
false
}
}
}
None => {
println!("daw_vst_bridge binary not found — app will use WASM fallback");
set_bridge_child(app, None);
false
}
}
}
fn list_dir_snippet(dir: &Path) -> String {
let mut s = String::new();
match std::fs::read_dir(dir) {
Ok(rd) => {
let mut n = 0;
for e in rd.flatten() {
if n > 0 {
s.push_str(", ");
}
s.push_str(&e.file_name().to_string_lossy());
n += 1;
if n >= 15 {
s.push_str(", ...");
break;
}
}
}
Err(_) => s.push_str("<khong doc duoc dir>"),
}
s
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
push_midi_event,
load_native_instrument,
open_vst_gui,
bridge_status,
transport_control,
restart_bridge_with_sample_rate
])
.setup(|app| {
let res_dir = app
.path()
.resource_dir()
.expect("resource dir not found");
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
.unwrap_or_default();
let candidates = engine_candidates(&res_dir, &exe_dir);
// Ghi log spawn de chan doan — vao %APPDATA%/SonicForgeDAW/logs/
// spawn.log (thu muc luon ton tai), KHONG ghi vao engine_dir.
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let spawn_log_path = std::path::Path::new(&log_dir)
.join("SonicForgeDAW")
.join("logs")
.join("spawn.log");
if let Some(parent) = spawn_log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let mut log_line = format!(
"resource_dir={}\nexe_dir={}\n",
res_dir.display(),
exe_dir.display()
);
let mut found: Option<(PathBuf, String)> = None;
for (path, label) in &candidates {
let exists = path.exists();
log_line.push_str(&format!(
" [{label}] {} exists={}\n",
path.display(),
exists
));
if found.is_none() && exists {
found = Some((path.clone(), label.clone()));
}
}
if found.is_none() {
log_line.push_str(&format!(
" NOT FOUND — resource_dir contents: {}\n exe_dir contents: {}\n",
list_dir_snippet(&res_dir),
list_dir_snippet(&exe_dir)
));
}
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&spawn_log_path)
{
use std::io::Write;
let _ = f.write_all(log_line.as_bytes());
}
match found {
Some((engine_exe, label)) => {
match app
.shell()
.command(&engine_exe)
.env("SF_PARENT_PID", std::process::id().to_string())
.spawn()
{
Ok((_rx, child)) => {
app.manage(EngineProcess(Mutex::new(Some(child))));
println!(
"Python Background Engine started ({label}): {}",
engine_exe.display()
);
}
Err(e) => {
println!("Failed to spawn daw_engine {engine_exe:?}: {e}");
app.manage(EngineProcess(Mutex::new(None)));
}
}
}
None => {
println!(
"daw_engine binary not found in any candidate path — xem spawn.log de biet chi tiet"
);
app.manage(EngineProcess(Mutex::new(None)));
}
}
// ── Native Host Bridge (daw_vst_bridge.exe): SHM + sidecar spawn ──
let shm_created = Shm::create();
println!(
"SharedMemory {}: {}",
shm::shm_name(),
if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" }
);
app.manage(ShmState(Mutex::new(shm_created)));
app.manage(BridgeSampleRate(Mutex::new(48000)));
// Append bridge diagnostics to spawn.log and spawn the sidecar (B3).
let app_handle = app.handle().clone();
let _ = spawn_bridge(&app_handle, &res_dir, &exe_dir, &mut log_line, &spawn_log_path);
// ── Audio pump: bridge SHM -> WebView `bridge-audio` events ──
// G3.1: bridge liveness keys on the DEDICATED heartbeat (a bridge
// thread, independent of the audio loop). A slow-but-alive audio
// loop no longer looks dead. writeIndex is now only for audio
// emission; a stalled writeIndex with a live heartbeat is logged
// as an audio stall, never a restart.
let pump_handle = app.handle().clone();
std::thread::spawn(move || {
let mut last_index: u32 = 0;
let mut last_hb: u32 = 0;
let mut last_hb_change = std::time::Instant::now();
let mut last_audio_change = std::time::Instant::now();
let mut down_emitted = false;
let mut stall_logged = false;
let mut restart_attempts = 0u32;
loop {
// App đang tắt (window Destroyed) — dừng pump, không respawn bridge.
if SHUTTING_DOWN.load(Ordering::Relaxed) {
break;
}
let state = pump_handle.state::<ShmState>();
let guard = match state.0.lock() {
Ok(g) => g,
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(10));
continue;
}
};
// Liveness = heartbeat advancing (~10Hz from bridge thread).
let hb_advanced = guard.as_ref().map(|shm| {
let hb = shm.heartbeat();
if hb != last_hb {
last_hb = hb;
true
} else {
false
}
});
if hb_advanced == Some(true) {
last_hb_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;
}
// Audio emission = writeIndex advancing (audio loop alive).
let audio_advanced = guard.as_ref().map(|shm| {
let idx = shm.write_index();
if idx != last_index {
last_index = idx;
true
} else {
false
}
});
if audio_advanced == Some(true) {
last_audio_change = std::time::Instant::now();
stall_logged = false;
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",
AudioFrame { l: l.to_vec(), r: r.to_vec() },
);
}
// G3.1 FIX: nhả guard TRƯỚC stall log / watchdog kill+spawn.
// kill_bridge -> flush_bridge_state khóa chính ShmState mutex
// mà pump đang giữ -> deadlock khi watchdog trigger (không
// restart, không log, bridge chết vĩnh viễn).
drop(guard);
if audio_advanced != Some(true)
&& hb_advanced == Some(true)
&& last_audio_change.elapsed() >= std::time::Duration::from_secs(3)
&& !stall_logged
{
// Heartbeat OK nhưng audio loop không push block — stall
// thực sự (không phải bridge chết). Log, không restart.
stall_logged = true;
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 Ok(mut f) = std::fs::OpenOptions::new()
.create(true).append(true).open(&bridge_log)
{
use std::io::Write;
let _ = f.write_all(b"[tauri] audio stall (heartbeat OK)\n");
}
}
if hb_advanced == Some(false) && !down_emitted
&& last_hb_change.elapsed() >= std::time::Duration::from_secs(3)
{
// Check lai truoc khi respawn — Destroyed co the vua set flag.
if SHUTTING_DOWN.load(Ordering::Relaxed) {
break;
}
down_emitted = true;
if restart_attempts == 0 {
restart_attempts += 1;
let res_dir = pump_handle.path().resource_dir().unwrap_or_default();
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
.unwrap_or_default();
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let log_path = std::path::Path::new(&log_dir)
.join("SonicForgeDAW").join("logs").join("spawn.log");
// G3.2: phân loại stall — process còn sống = HANG
// (heartbeat thread kẹt), pid biến mất = CRASH.
let pid = pump_handle
.state::<BridgeProcess>()
.0
.lock()
.ok()
.and_then(|g| g.as_ref().map(|c| c.pid()));
let kind = match pid {
Some(p) if process_alive(p) => {
format!("HUNG (pid {p} alive, heartbeat stalled)")
}
Some(p) => format!("CRASHED (pid {p} gone)"),
None => "UNKNOWN (no tracked pid)".into(),
};
let mut line = format!(
"[tauri] bridge heartbeat stalled 3s — {kind} — restart attempt #1\n"
);
// FIX: kill bridge cũ trước — không được để 2 bridge
// cùng map SHM (race control queue / double load).
// F-PROC-2: serialize with the sample-rate restart
// command (same BRIDGE_RESTART_LOCK).
let _restart_guard = match BRIDGE_RESTART_LOCK.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
// F-PROC-3: re-check after acquiring the lock AND
// after kill — Destroyed may have been set while we
// waited/killed; never respawn during app shutdown.
if SHUTTING_DOWN.load(Ordering::Relaxed) {
break;
}
kill_bridge(&pump_handle);
if SHUTTING_DOWN.load(Ordering::Relaxed) {
break;
}
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)
.join("SonicForgeDAW").join("logs").join("bridge.log");
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true).append(true).open(&bridge_log)
{
use std::io::Write;
let _ = f.write_all(b"[tauri] bridge restarted (attempt 1)
");
}
last_hb_change = std::time::Instant::now();
down_emitted = false;
} else {
let _ = pump_handle.emit("bridge-down", ());
}
} else {
let _ = pump_handle.emit("bridge-down", ());
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
});
// ── Native folder picker bridge (folder picker cho Plugin Manager) ──
// UI chay tren http://127.0.0.1:8000 (engine) — KHONG co __TAURI__
// (WebView2 cung khong ho tro window.prompt) → engine goi qua file
// IPC: engine ghi ipc/pick_dir.request → thread nay mo NATIVE dialog
// (IFileDialog/Explorer — tauri-plugin-dialog) → ghi ket qua vao
// ipc/pick_dir.response → engine tra ve cho frontend.
let data_root = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let ipc_dir = std::path::Path::new(&data_root)
.join("SonicForgeDAW")
.join("ipc");
if let Ok(()) = std::fs::create_dir_all(&ipc_dir) {
// Marker: engine biet bridge ton tai (khong phai chay standalone)
let _ = std::fs::write(ipc_dir.join("tauri_bridge_ready"), "1");
}
let ipc_dir_for_thread = ipc_dir.clone();
let app_handle = app.handle().clone();
std::thread::spawn(move || {
loop {
let req = ipc_dir_for_thread.join("pick_dir.request");
if req.exists() {
let _ = std::fs::remove_file(&req);
let resp = ipc_dir_for_thread.join("pick_dir.response");
let _ = std::fs::remove_file(&resp);
// Dialog phai chay tren main thread (GTK/Windows message loop)
let handle = app_handle.clone();
let resp_for_main = resp.clone();
// Clone rieng cho closure: run_on_main_thread(&self, F)
// borrow `handle`, closure (move) phai dung ban clone.
let handle_for_closure = handle.clone();
let _ = handle.run_on_main_thread(move || {
// tauri-plugin-dialog v2: blocking_pick_folder() tra
// Option<FilePath> (enum Path(PathBuf) | Url(Url))
let picked: Option<FilePath> = handle_for_closure
.dialog()
.file()
.blocking_pick_folder();
let val = picked
.map(|p| match p {
FilePath::Path(pb) => pb.to_string_lossy().to_string(),
FilePath::Url(u) => u.to_string(),
})
.unwrap_or_default();
let _ = std::fs::write(&resp_for_main, val);
});
// Cho den khi co response (user co the de dialog mo lau)
let mut waited_ms = 0u32;
while !resp.exists() && waited_ms < 300_000 {
std::thread::sleep(std::time::Duration::from_millis(100));
waited_ms += 100;
}
}
std::thread::sleep(std::time::Duration::from_millis(150));
}
});
Ok(())
})
.on_window_event(|window, event| {
// Terminate sidecar khi DAW window dong — tranh orphan process
if let tauri::WindowEvent::Destroyed = event {
// Dung pump thread: khong respawn bridge sau khi sidecar da bi giet.
SHUTTING_DOWN.store(true, Ordering::Relaxed);
let child = window
.state::<EngineProcess>()
.0
.lock()
.ok()
.and_then(|mut lock| lock.take());
if let Some(child) = child {
kill_process_tree(&child);
let _ = child.kill();
println!("daw_engine sidecar terminated.");
}
// G2.2: flush state to disk before killing the bridge
flush_bridge_state(window.app_handle());
// Terminate Native Host Bridge khi DAW window dong
let bridge_child = window
.state::<BridgeProcess>()
.0
.lock()
.ok()
.and_then(|mut lock| lock.take());
if let Some(child) = bridge_child {
kill_process_tree(&child);
let _ = child.kill();
println!("daw_vst_bridge sidecar terminated.");
}
// Cua so chinh da dong — ep app thoat han. Neu con window
// phu/an giu event loop song, Tauri khong tu exit; exit(0)
// ket thuc dut diem (pump da dung, khong con respawn bridge).
if window.label() == "main" {
window.app_handle().exit(0);
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// ── Native Host Bridge Tauri commands ──────────────────────────────────────
#[tauri::command]
fn push_midi_event(
app: AppHandle,
command: u8,
channel: u8,
pitch: u8,
velocity: u8,
data2: u8,
data3: u8,
sample_offset: u32,
live: bool,
) -> Result<(), String> {
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")?;
shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset, live as u8)
.then_some(())
.ok_or_else(|| "midi queue full".to_string())
}
#[tauri::command]
fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, channel: u8) -> Result<(), String> {
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")?;
shm.push_control(2, instrument_type as u32, 0, channel as u32, &path)
.then_some(())
.ok_or_else(|| "control queue full".to_string())
}
#[tauri::command]
fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), 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.
// F5 (audit 5.2): trả Err khi lệnh không tới được bridge (SHM unavailable /
// lock poisoned / queue full) — frontend openNativeGUI đã try/catch sẵn.
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")?;
let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id);
if !ok {
eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} failed", channel);
return Err("control queue full".into());
}
Ok(())
}
#[derive(serde::Serialize)]
struct BridgeStatus {
connected: bool,
shm_name: String,
write_index: u32,
block_timestamp: u64,
shm_size_bytes: usize,
sample_rate: u32,
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())?;
let status = match guard.as_ref() {
Some(shm) => BridgeStatus {
connected: true,
shm_name: shm::shm_name(),
write_index: shm.write_index(),
block_timestamp: shm.block_timestamp(),
shm_size_bytes: std::mem::size_of::<shm::SharedAudioBufferIPC>(),
sample_rate: {
let rate_state = app.state::<BridgeSampleRate>();
let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
*g
},
block_size: shm::AUDIO_BLOCK_SIZE as u32,
},
None => BridgeStatus {
connected: false,
shm_name: shm::shm_name(),
write_index: 0,
block_timestamp: 0,
shm_size_bytes: 0,
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]
fn transport_control(app: AppHandle, kind: String, playhead: Option<u32>) -> Result<(), String> {
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")?;
// arg0: 0=STOP (flush), 1=PLAY, 2=SET_POSITION (seek, no flush)
let (arg0, arg1) = match kind.as_str() {
"play" => (1, playhead.unwrap_or(0)),
"set_position" => (2, playhead.unwrap_or(0)),
_ => (0, playhead.unwrap_or(0)), // stop / panic
};
shm.push_control(3, arg0, arg1, 0, "")
.then_some(())
.ok_or_else(|| "control queue full".to_string())
}
#[tauri::command]
fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(), String> {
println!("restart_bridge_with_sample_rate: {}", sample_rate);
{
let rate_state = app.state::<BridgeSampleRate>();
{
let mut g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
*g = sample_rate;
}
}
// F-PROC-2: serialize with the pump watchdog restart so two threads can
// never both kill+spawn (two bridges on the same SHM).
let _restart_guard = match BRIDGE_RESTART_LOCK.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
kill_bridge(&app);
let res_dir = app.path().resource_dir().unwrap_or_default();
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
.unwrap_or_default();
let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let spawn_log_path = std::path::Path::new(&log_dir)
.join("SonicForgeDAW")
.join("logs")
.join("spawn.log");
let mut log_line = format!("[tauri] restarting bridge with sample rate {}\n", sample_rate);
let ok = spawn_bridge(&app, &res_dir, &exe_dir, &mut log_line, &spawn_log_path);
if ok {
Ok(())
} else {
Err("Failed to spawn bridge with new sample rate".into())
}
}