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:
@@ -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>>);
|
||||
Reference in New Issue
Block a user