feat: native host bridge integration — C++ bridge, Rust SHM, JS routing, build scripts, docs
- native_bridge/: InstrumentEngineManager multi-channel, sample-accurate, CC/program/pitchbend, transport, Vst3Instrument stub (HAVE_VST3SDK) - src-tauri: shm.rs, bridge spawn + audio pump + health monitor, open_vst_gui, externalBin, commands - app: UnifiedMidiRouter, NativeBridgeService, bridgeAudioNode, audioRoutingEngine, Plugin Manager UI, Bridge/WASM indicator, set_position sync - build: 3 ps1 (force-added, build/ ignored), verify_bundle --check-bridge, CI workflow - docs: TASKS.md, TEST_NOTES.md (Windows verify checklist), install/report updates
This commit is contained in:
@@ -17,6 +17,11 @@ 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
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_MemoryManagement",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
+305
-1
@@ -10,14 +10,25 @@
|
||||
// "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::Manager;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_dialog::{DialogExt, FilePath};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
mod shm;
|
||||
use shm::{Shm, ShmState};
|
||||
|
||||
struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||
struct BridgeProcess(Mutex<Option<CommandChild>>);
|
||||
|
||||
/// 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)> {
|
||||
@@ -46,6 +57,78 @@ fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> {
|
||||
]
|
||||
}
|
||||
|
||||
/// 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()),
|
||||
(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()));
|
||||
}
|
||||
}
|
||||
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)) => {
|
||||
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", "44100") // B8: JS resampler handles mismatches (C5)
|
||||
.env("SF_BLOCK_SIZE", "256")
|
||||
.spawn()
|
||||
{
|
||||
Ok((_rx, child)) => {
|
||||
app.manage(BridgeProcess(Mutex::new(Some(child))));
|
||||
println!("Native Host Bridge started ({label}): {}", bridge_exe.display());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to spawn daw_vst_bridge {bridge_exe:?}: {e}");
|
||||
app.manage(BridgeProcess(Mutex::new(None)));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("daw_vst_bridge binary not found — app will use WASM fallback");
|
||||
app.manage(BridgeProcess(Mutex::new(None)));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_dir_snippet(dir: &Path) -> String {
|
||||
let mut s = String::new();
|
||||
match std::fs::read_dir(dir) {
|
||||
@@ -73,6 +156,13 @@ 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
|
||||
])
|
||||
.setup(|app| {
|
||||
let res_dir = app
|
||||
.path()
|
||||
@@ -157,6 +247,91 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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)));
|
||||
|
||||
// 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 ──
|
||||
// B10 health: if bridgeWriteIndex stalls for 3s the bridge is dead —
|
||||
// respawn once, then emit `bridge-down` so the UI falls back to WASM.
|
||||
let pump_handle = app.handle().clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut last_index: u32 = 0;
|
||||
let mut last_change = std::time::Instant::now();
|
||||
let mut down_emitted = false;
|
||||
let mut restart_attempts = 0u32;
|
||||
loop {
|
||||
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;
|
||||
}
|
||||
};
|
||||
let advanced = guard.as_ref().map(|shm| {
|
||||
let idx = shm.write_index();
|
||||
if idx != last_index {
|
||||
last_index = idx;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if advanced == Some(true) {
|
||||
last_change = std::time::Instant::now();
|
||||
down_emitted = 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() },
|
||||
);
|
||||
} else if last_change.elapsed() >= std::time::Duration::from_secs(3) && !down_emitted {
|
||||
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");
|
||||
let mut line = String::from("[tauri] bridge stalled 3s — restart attempt #1\n");
|
||||
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)\n");
|
||||
}
|
||||
last_change = std::time::Instant::now();
|
||||
down_emitted = false;
|
||||
} else {
|
||||
let _ = pump_handle.emit("bridge-down", ());
|
||||
}
|
||||
} else {
|
||||
let _ = pump_handle.emit("bridge-down", ());
|
||||
}
|
||||
}
|
||||
drop(guard);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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
|
||||
@@ -226,8 +401,137 @@ pub fn run() {
|
||||
let _ = child.kill();
|
||||
println!("daw_engine sidecar terminated.");
|
||||
}
|
||||
// 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 {
|
||||
let _ = child.kill();
|
||||
println!("daw_vst_bridge sidecar terminated.");
|
||||
}
|
||||
}
|
||||
})
|
||||
.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,
|
||||
) -> 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)
|
||||
.then_some(())
|
||||
.ok_or("midi queue full")
|
||||
}
|
||||
|
||||
#[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("control queue full")
|
||||
}
|
||||
|
||||
#[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())?;
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct BridgeStatus {
|
||||
connected: bool,
|
||||
shm_name: &'static str,
|
||||
write_index: u32,
|
||||
block_timestamp: u64,
|
||||
shm_size_bytes: usize,
|
||||
sample_rate: u32,
|
||||
block_size: u32,
|
||||
}
|
||||
|
||||
#[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() {
|
||||
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: 44100,
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[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("control queue full")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// src-tauri/src/shm.rs
|
||||
// Shared memory `SonicForge_DAW_IPC` — created by the DAW, opened by the
|
||||
// C++ bridge (native_bridge). Layout MUST match native_bridge/include/
|
||||
// SharedMemoryIPC.h (verified by native_bridge/tests/shm_selfcheck.cpp).
|
||||
#![cfg_attr(not(windows), allow(dead_code))]
|
||||
|
||||
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::{
|
||||
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
|
||||
};
|
||||
|
||||
pub const SHM_NAME: &str = "SonicForge_DAW_IPC";
|
||||
pub const AUDIO_BLOCK_SIZE: usize = 256;
|
||||
pub const MIDI_QUEUE_CAP: usize = 64;
|
||||
pub const CONTROL_QUEUE_CAP: usize = 8;
|
||||
pub const CONTROL_PATH_MAX: usize = 1024;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MidiEventIPC {
|
||||
pub command: u8,
|
||||
pub channel: u8,
|
||||
pub pitch: u8,
|
||||
pub velocity: u8,
|
||||
pub data2: u8, // CC value / program / PB LSB
|
||||
pub data3: u8, // PB MSB (0xE only)
|
||||
pub reserved: [u8; 2],
|
||||
pub sample_offset: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ControlEventIPC {
|
||||
pub ctype: u32,
|
||||
pub arg0: u32,
|
||||
pub arg1: u32,
|
||||
pub channel: u32, // LOAD: MIDI channel to assign (A10)
|
||||
pub arg2: [u8; CONTROL_PATH_MAX],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SharedAudioBufferIPC {
|
||||
pub client_read_index: u32,
|
||||
pub bridge_write_index: u32,
|
||||
pub master_left: [f32; AUDIO_BLOCK_SIZE],
|
||||
pub master_right: [f32; AUDIO_BLOCK_SIZE],
|
||||
pub block_timestamp: u64,
|
||||
pub midi_queue: [MidiEventIPC; MIDI_QUEUE_CAP],
|
||||
pub midi_queue_count: u32,
|
||||
pub control_queue: [ControlEventIPC; CONTROL_QUEUE_CAP],
|
||||
pub control_queue_count: u32,
|
||||
}
|
||||
|
||||
pub struct Shm {
|
||||
handle: HANDLE,
|
||||
view: *mut SharedAudioBufferIPC,
|
||||
}
|
||||
|
||||
unsafe impl Send for Shm {}
|
||||
|
||||
impl Shm {
|
||||
/// Create (or open) the mapping. Returns None on non-Windows (dev fallback).
|
||||
pub fn create() -> Option<Self> {
|
||||
if !cfg!(windows) {
|
||||
return None;
|
||||
}
|
||||
let name_wide: Vec<u16> = SHM_NAME.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let size = std::mem::size_of::<SharedAudioBufferIPC>() as u64;
|
||||
let handle = unsafe {
|
||||
CreateFileMappingW(
|
||||
HANDLE::default(),
|
||||
ptr::null(),
|
||||
PAGE_READWRITE,
|
||||
(size >> 32) as u32,
|
||||
size as u32,
|
||||
name_wide.as_ptr(),
|
||||
)
|
||||
};
|
||||
if handle == HANDLE::default() {
|
||||
return None;
|
||||
}
|
||||
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
|
||||
if view.is_null() {
|
||||
unsafe { CloseHandle(handle) };
|
||||
return None;
|
||||
}
|
||||
Some(Shm {
|
||||
handle,
|
||||
view: view as *mut SharedAudioBufferIPC,
|
||||
})
|
||||
}
|
||||
|
||||
fn ipc(&self) -> &mut SharedAudioBufferIPC {
|
||||
unsafe { &mut *self.view }
|
||||
}
|
||||
|
||||
pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32) -> bool {
|
||||
let ipc = self.ipc();
|
||||
if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP {
|
||||
return false;
|
||||
}
|
||||
let i = ipc.midi_queue_count as usize;
|
||||
ipc.midi_queue[i] = MidiEventIPC {
|
||||
command: cmd,
|
||||
channel,
|
||||
pitch,
|
||||
velocity,
|
||||
data2,
|
||||
data3,
|
||||
reserved: [0u8; 2],
|
||||
sample_offset,
|
||||
};
|
||||
ipc.midi_queue_count += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn push_control(&self, ctype: u32, arg0: u32, arg1: u32, channel: u32, path: &str) -> bool {
|
||||
let ipc = self.ipc();
|
||||
if ipc.control_queue_count as usize >= CONTROL_QUEUE_CAP {
|
||||
return false;
|
||||
}
|
||||
let mut ev = ControlEventIPC {
|
||||
ctype,
|
||||
arg0,
|
||||
arg1,
|
||||
channel,
|
||||
arg2: [0u8; CONTROL_PATH_MAX],
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let n = bytes.len().min(CONTROL_PATH_MAX - 1);
|
||||
ev.arg2[..n].copy_from_slice(&bytes[..n]);
|
||||
// LOAD relies on C++ side strnlen() — arg1 is free for TRANSPORT playhead.
|
||||
let i = ipc.control_queue_count as usize;
|
||||
ipc.control_queue[i] = ev;
|
||||
ipc.control_queue_count += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn read_audio(&self) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) {
|
||||
let ipc = self.ipc();
|
||||
(ipc.master_left, ipc.master_right)
|
||||
}
|
||||
|
||||
pub fn write_index(&self) -> u32 {
|
||||
self.ipc().bridge_write_index
|
||||
}
|
||||
|
||||
pub fn block_timestamp(&self) -> u64 {
|
||||
self.ipc().block_timestamp
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Shm {
|
||||
fn drop(&mut self) {
|
||||
if !self.view.is_null() {
|
||||
unsafe { UnmapViewOfFile(self.view as *const c_void) };
|
||||
}
|
||||
unsafe { CloseHandle(self.handle) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Process-wide shared-memory state for Tauri.
|
||||
pub struct ShmState(pub Mutex<Option<Shm>>);
|
||||
@@ -35,7 +35,7 @@
|
||||
"resources": {
|
||||
"resources/daw_engine": "daw_engine/"
|
||||
},
|
||||
"externalBin": [],
|
||||
"externalBin": ["binaries/daw_vst_bridge"],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installerHooks": "hooks.nsh"
|
||||
|
||||
Reference in New Issue
Block a user