d8f8506077
- 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
179 lines
5.2 KiB
Rust
179 lines
5.2 KiB
Rust
// 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::Memory::{
|
|
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.Value.is_null() {
|
|
unsafe { CloseHandle(handle) };
|
|
return None;
|
|
}
|
|
Some(Shm {
|
|
handle,
|
|
view: view.Value 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(windows_sys::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS { Value: self.view as *mut c_void }) };
|
|
}
|
|
unsafe { CloseHandle(self.handle) };
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|