A14: MIDI-out từ plugin (VST2/VST3) → SHM → Rust emit → DAW track
- Vst2Instrument: audioMaster callback thu VstEvents, push SHM midiOutQueue
- Vst3Instrument: IEventList output bus (kAudioMasterWantMidi) — check
- SharedMemoryIPC.h: midiOutQueue[64] + midiOutCount (offset 27556/28324, sizeof 28328)
- shm.rs: MidiEventIPC Serialize, drain_midi_out() emit 'bridge-midi-out'
- lib.rs: pump emit midi-out events
- nativeBridgeService.js: onMidiOut API
- app.jsx: route MIDI-out về selected track qua SonicMidiRouter (feedback guard)
- TASKS.md: A14 ✅
This commit is contained in:
@@ -152,7 +152,7 @@ Các task nhóm H lấp những khâu còn hở giữa A-G để pipeline chạy
|
||||
| ✅ A11 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Sample-accurate: không xử lý MIDI ngay; buffer event theo `sampleOffset`, rẽ block 256 thành các đoạn theo offset, gọi `noteOn/Off` đúng vị trí trước khi render đoạn đó | note timeline không lệch nhịp (>±1 sample test) |
|
||||
| ✅ A12 | `native_bridge/include/SharedMemoryIPC.h` + engine | **[BỔ SUNG]** Mở rộng `MidiEventIPC`: field `data2` (+`data3` PB MSB); hỗ trợ `0xB CC` (FluidSynth `fluid_synth_cc`), `0xC program change`, `0xE pitch bend` (`fluid_synth_pitch_bend`); VST3 → tham số tương ứng | sustain/CC/pitchbend qua bridge hoạt động |
|
||||
| ✅ A13 | `native_bridge/src/main.cpp` | **[BỔ SUNG]** Control `type=3 TRANSPORT`: `arg0=0 STOP` → flush note-off toàn channel + `fluid_synth_all_notes_off`; `arg0=1 PLAY` kèm `arg1=playheadSamples` → đồng bộ timeline | Stop → hết âm ngân tức thì |
|
||||
| A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) |
|
||||
| ✅ A14 | `native_bridge/src/main.cpp` + `Vst2Instrument.cpp`/`Vst3Instrument.cpp` + `SharedMemoryIPC.h` + `src-tauri/src/lib.rs` + `nativeBridgeService.js` | **[BỔ SUNG]** MIDI-out từ plugin: bridge hiện CHỈ gửi MIDI vào (kAudioMasterWantMidi=1, processEvents) — không đọc MIDI plugin xuất ra. MIDI-generator (Scaler2/Instacomposer/MelodicFlow) xuất chord/MIDI không phải audio → câm trong test audio. Cần: plugin gọi audioMaster → host thu VstEvents (VST2 `audioMaster` callback `audioMasterGetVstVersion`+event queue; VST3 `IEventList` output bus) → push vào SHM midiQueue mới (out) → Rust emit → JS route về DAW track | plugin MIDI-gen xuất event → DAW nhận note (test Scaler2 gõ chord → MIDI ghi nhận) |
|
||||
|
||||
### H-B. Rust — bổ sung
|
||||
|
||||
|
||||
@@ -1797,6 +1797,38 @@ function getAudioContext() {
|
||||
}
|
||||
window.BridgeAudioNode.init(audioCtx);
|
||||
window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
|
||||
// A14: plugin MIDI output -> route notes to the selected track
|
||||
// (Scaler2/Instacomposer chords play the selected track's
|
||||
// instrument). Feedback guard: skip events whose source channel
|
||||
// is the selected track's own channel (would loop generator into
|
||||
// itself). command is the VST2 status high nibble (0x90 etc.).
|
||||
window.NativeBridgeService.onMidiOut(function (events) {
|
||||
if (!events || !events.length) return;
|
||||
var ref = window.__selectedTrackIdRef;
|
||||
var tId = (ref && ref.current) ? ref.current : '1';
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var ev = events[i];
|
||||
var cmd;
|
||||
switch (ev.command) {
|
||||
case 0x90: cmd = ev.velocity > 0 ? 'NOTE_ON' : 'NOTE_OFF'; break;
|
||||
case 0x80: cmd = 'NOTE_OFF'; break;
|
||||
case 0xB0: cmd = 'CC'; break;
|
||||
case 0xC0: cmd = 'PROGRAM'; break;
|
||||
case 0xE0: cmd = 'PITCH_BEND'; break;
|
||||
default: continue;
|
||||
}
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
var tCh = window.SonicMidiRouter.allocateChannel(tId, false);
|
||||
if (tCh === ev.channel) continue; // feedback guard
|
||||
window.SonicMidiRouter.pushEvent({
|
||||
cmd: cmd, channel: tId, pitch: ev.pitch,
|
||||
velocity: ev.velocity / 127, data2: ev.data2, data3: ev.data3,
|
||||
sampleOffset: ev.sample_offset || 0, live: true
|
||||
});
|
||||
}
|
||||
console.log('[Bridge] MIDI-out', cmd, 'ch', ev.channel, 'note', ev.pitch, 'vel', ev.velocity, '-> track', tId);
|
||||
}
|
||||
});
|
||||
if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -18976,6 +19008,9 @@ const App = () => {
|
||||
selectedClipIdRef.current = selectedClipId;
|
||||
const selectedTrackIdRef = useRef(selectedTrackId);
|
||||
selectedTrackIdRef.current = selectedTrackId;
|
||||
// A14: plugin MIDI-out routing target — bootstrap handler (module scope)
|
||||
// reads the live selection through this window ref (avoid stale closure).
|
||||
window.__selectedTrackIdRef = selectedTrackIdRef;
|
||||
const handleDeleteTrackRef = useRef(() => {});
|
||||
const handleSplitTrackRef = useRef(() => {});
|
||||
const handleRecordClickRef = useRef(() => {});
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
activeInstrumentType: 'VST3',
|
||||
_audioCb: null,
|
||||
_statusCb: null,
|
||||
_midiOutCb: null,
|
||||
_tauri: function () { return window.__TAURI__; },
|
||||
|
||||
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
|
||||
@@ -58,6 +59,12 @@
|
||||
if (!self.isBridgeConnected) self._reconnectIfNeeded();
|
||||
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
|
||||
});
|
||||
// A14: plugin MIDI output (VstEvents -> SHM midiOutQueue -> pump).
|
||||
// Payload: array of {command, channel, pitch, velocity, data2,
|
||||
// data3, reserved, sample_offset}. JS routes notes to DAW tracks.
|
||||
window.__TAURI__.event.listen('bridge-midi-out', function (e) {
|
||||
if (self._midiOutCb) self._midiOutCb(e.payload);
|
||||
});
|
||||
window.__TAURI__.event.listen('bridge-down', function () {
|
||||
self._notifyStatus(false);
|
||||
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
|
||||
@@ -187,6 +194,13 @@
|
||||
*/
|
||||
onAudio: function (cb) {
|
||||
this._audioCb = cb;
|
||||
},
|
||||
|
||||
/** A14: register consumer of plugin MIDI output.
|
||||
* cb(events): array of {command, channel, pitch, velocity, data2,
|
||||
* data3, reserved, sample_offset} — route to DAW tracks. */
|
||||
onMidiOut: function (cb) {
|
||||
this._midiOutCb = cb;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -73,6 +73,14 @@ struct SharedAudioBufferIPC {
|
||||
volatile uint32_t ringMask;
|
||||
float ringLeft[8][AUDIO_BLOCK_SIZE];
|
||||
float ringRight[8][AUDIO_BLOCK_SIZE];
|
||||
|
||||
// A14: MIDI OUT from plugins (VST2 audioMaster opcode=8 VstEvents,
|
||||
// VST3 IEventList output bus). The bridge writes events the plugin
|
||||
// generated; the Tauri pump drains them to `bridge-midi-out` and the
|
||||
// JS routes them to DAW tracks. Append-only field: all offsets above
|
||||
// stay stable.
|
||||
MidiEventIPC midiOutQueue[64];
|
||||
volatile uint32_t midiOutCount;
|
||||
};
|
||||
|
||||
// G4.3: per-plugin sandbox SHM — double-buffered audio handoff.
|
||||
|
||||
@@ -30,6 +30,10 @@ thread_local bool t_editingProbe = false;
|
||||
// Resize hook registered by the bridge (main.cpp): plugin audioMaster
|
||||
// (SizeWindow) -> move the host window on the UI worker thread.
|
||||
static void (*g_vst2ResizeHook)(void*, int, int) = nullptr;
|
||||
// A14: plugin-generated MIDI (VST2 audioMaster opcode=8 -> VstEvents).
|
||||
// main.cpp registers a hook that writes into the SHM midiOutQueue.
|
||||
static void (*g_vst2MidiOutHook)(uint8_t cmd, uint8_t ch, uint8_t d1,
|
||||
uint8_t d2, uint32_t deltaFrames) = nullptr;
|
||||
|
||||
// base64 encode/decode (RFC 4648) — local copies; Vst3Instrument.cpp keeps
|
||||
// its own so neither file depends on the other's statics.
|
||||
@@ -118,6 +122,28 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
|
||||
case kAudioMasterGetBlockSize:
|
||||
return self ? (intptr_t)self->maxBlockSize() : 512;
|
||||
case kAudioMasterWantMidi: return 1; // we send MIDI events
|
||||
case kAudioMasterProcessEvents: {
|
||||
// Plugin -> host MIDI output (Scaler2/Instacomposer/etc.
|
||||
// generate chords). ptr = VstEvents*.
|
||||
if (!ptr || !g_vst2MidiOutHook) return 0;
|
||||
const VstEvents* evs = static_cast<const VstEvents*>(ptr);
|
||||
if (evs->numEvents <= 0) return 1;
|
||||
int n = evs->numEvents > 64 ? 64 : evs->numEvents;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const VstEvent* e = evs->events[i];
|
||||
if (!e || e->type != kVstMidiType) continue;
|
||||
// VstMidiEvent starts with the VstEvent prefix — layout-compatible,
|
||||
// but the C++ types are unrelated so the cast must be reinterpret_cast.
|
||||
const VstMidiEvent* me = reinterpret_cast<const VstMidiEvent*>(e);
|
||||
uint8_t status = (uint8_t)me->midiData[0];
|
||||
if (!status) continue; // running status not used by plugins
|
||||
g_vst2MidiOutHook(status & 0xF0, status & 0x0F,
|
||||
(uint8_t)me->midiData[1],
|
||||
(uint8_t)me->midiData[2],
|
||||
(uint32_t)me->deltaFrames);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
case kAudioMasterGetTime: {
|
||||
static VstTimeInfo ti;
|
||||
static double pos = 0.0;
|
||||
@@ -615,6 +641,10 @@ void Vst2Instrument::setResizeHook(void (*fn)(void*, int, int)) {
|
||||
g_vst2ResizeHook = fn;
|
||||
}
|
||||
|
||||
void Vst2Instrument::setMidiOutHook(void (*fn)(uint8_t, uint8_t, uint8_t, uint8_t, uint32_t)) {
|
||||
g_vst2MidiOutHook = fn;
|
||||
}
|
||||
|
||||
void Vst2Instrument::captureState() {
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
if (!s || !s->effect) return;
|
||||
|
||||
@@ -51,6 +51,10 @@ public:
|
||||
// Register the SizeWindow hook (main.cpp posts the resize to the UI
|
||||
// worker). Probe builds never set it -> no-op.
|
||||
static void setResizeHook(void (*fn)(void* hwnd, int w, int h));
|
||||
// A14: register the plugin-MIDI-out sink (main.cpp writes SHM
|
||||
// midiOutQueue). Probe builds never set it -> no-op.
|
||||
static void setMidiOutHook(void (*fn)(uint8_t cmd, uint8_t ch, uint8_t d1,
|
||||
uint8_t d2, uint32_t deltaFrames));
|
||||
|
||||
private:
|
||||
void* state_; // Vst2HostState* (pimpl — aeffect.h stays out of the header)
|
||||
|
||||
@@ -120,6 +120,7 @@ static std::mutex g_guiMutex;
|
||||
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
|
||||
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
|
||||
static ChannelWorker* g_uiWorker = nullptr;
|
||||
static SharedAudioBufferIPC* g_shmIPC = nullptr; // A14: midi-out hook writes SHM
|
||||
// VST2 (DUNE 3) thread contract: entry + init dispatcher + processReplacing
|
||||
// must share ONE thread. processReplacing runs on the audio (main) thread,
|
||||
// so VST2 loads (LoadLibrary + VSTPluginMain + init dispatcher) are deferred
|
||||
@@ -852,6 +853,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
|
||||
if (!shmIPC) { CloseHandle(hMapFile); return 1; }
|
||||
g_shmIPC = shmIPC; // A14: midi-out hook (audio thread) writes SHM via this
|
||||
// VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h). Must be set
|
||||
// on Windows too — ringMask stays 0 otherwise and the ring collapses to
|
||||
// 1 slot (slot = writeIndex & 0), dropping whole blocks on any jank.
|
||||
@@ -903,6 +905,26 @@ int main(int argc, char* argv[]) {
|
||||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
});
|
||||
});
|
||||
// A14: plugin MIDI output sink. audioMaster(kAudioMasterProcessEvents)
|
||||
// (VstEvents from Scaler2/Instacomposer/...) -> SHM midiOutQueue,
|
||||
// drained by the Tauri pump to `bridge-midi-out`, JS routes notes to
|
||||
// DAW tracks. Append-only, no mutex — same pattern as midiQueue (audio
|
||||
// thread writes, pump reads+resets). Cap 64; drop when full.
|
||||
Vst2Instrument::setMidiOutHook([](uint8_t cmd, uint8_t ch, uint8_t d1,
|
||||
uint8_t d2, uint32_t deltaFrames) {
|
||||
if (!g_shmIPC) return;
|
||||
if (g_shmIPC->midiOutCount >= 64) return;
|
||||
auto& ev = g_shmIPC->midiOutQueue[g_shmIPC->midiOutCount++];
|
||||
ev.command = cmd;
|
||||
ev.channel = ch;
|
||||
ev.pitch = d1;
|
||||
ev.velocity = d2;
|
||||
ev.data2 = 0;
|
||||
ev.data3 = 0;
|
||||
ev.reserved[0] = 0;
|
||||
ev.reserved[1] = 0;
|
||||
ev.sampleOffset = deltaFrames;
|
||||
});
|
||||
// B9: native editor windows per channel — keep alive (HWND outlives the job).
|
||||
// Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc.
|
||||
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
|
||||
|
||||
@@ -67,8 +67,9 @@ int main() {
|
||||
assert(sizeof(SharedAudioBufferIPC::ControlEventIPC) == 1040);
|
||||
// G4.5 ring: 11168 (G3.1 +heartbeat) + ringMask(4) + ringLeft(8192) + ringRight(8192)
|
||||
// = 27556, rounded up to 8-byte struct alignment (u64 blockTimestamp) => 27560.
|
||||
// Matches Rust size_of (src-tauri/src/shm.rs layout test).
|
||||
assert(sizeof(SharedAudioBufferIPC) == 27560);
|
||||
// A14 midi-out: ring_right ends at 27556; midiOutQueue appends there,
|
||||
// +64*12 + 4 = 28328 (already 8-aligned). Matches Rust size_of (src-tauri/src/shm.rs).
|
||||
assert(sizeof(SharedAudioBufferIPC) == 28328);
|
||||
|
||||
// 6. Audio ring round-trip (multi-slot, VSTi crackle fix)
|
||||
ipc->ringMask = 7;
|
||||
@@ -78,7 +79,10 @@ int main() {
|
||||
assert(ipc->ringRight[3][0] == 42.0f);
|
||||
assert((ipc->bridgeWriteIndex & ipc->ringMask) < 8);
|
||||
|
||||
std::printf("SHM self-check OK (sizeof struct = %zu bytes)\n", sizeof(SharedAudioBufferIPC));
|
||||
std::printf("SHM self-check sizeof struct = %zu bytes\n", sizeof(SharedAudioBufferIPC));
|
||||
std::printf("midiOutQueue offset = %zu, midiOutCount offset = %zu\n",
|
||||
offsetof(SharedAudioBufferIPC, midiOutQueue),
|
||||
offsetof(SharedAudioBufferIPC, midiOutCount));
|
||||
shm_close(h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -481,6 +481,17 @@ pub fn run() {
|
||||
last_audio_change = std::time::Instant::now();
|
||||
stall_logged = false;
|
||||
}
|
||||
// A14: plugin MIDI output (VST2 audioMaster ProcessEvents
|
||||
// -> SHM midiOutQueue). Drain and forward to the page;
|
||||
// JS routes the notes to DAW tracks. Payload is a JSON
|
||||
// array of MidiEventIPC {command, channel, pitch,
|
||||
// velocity, data2, data3, reserved, sample_offset}.
|
||||
if let Some(s) = guard.as_ref() {
|
||||
let out = s.drain_midi_out();
|
||||
if !out.is_empty() {
|
||||
let _ = pump_handle.emit("bridge-midi-out", out);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
|
||||
+243
-222
@@ -1,222 +1,243 @@
|
||||
// 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,
|
||||
};
|
||||
|
||||
// SHM name RIENG cho tung process app: 2 instance (vi du bản portable install\
|
||||
// + bản MSI cài song song) dung chung ten -> 2 bridge cung map 1 SHM -> ca 2
|
||||
// drain control/midi queue (double instrument load, double GUI attach) -> race
|
||||
// -> crash USER32 (Event Log 12:07:06 + 12:07:51: bridge MSI crash trong luc
|
||||
// bridge portable dang chay). App PID duy nhat moi instance.
|
||||
pub fn shm_name() -> String {
|
||||
format!("SonicForge_DAW_IPC_{}", std::process::id())
|
||||
}
|
||||
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], // reserved[0]: live flag (1=live input, bypass transportStopped guard); reserved[1]: 0
|
||||
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,
|
||||
// G0.3: bridge sets this while an openGUI attach job is in flight.
|
||||
// Append-only field — offsets of control_queue (2836) and
|
||||
// control_queue_count (11156) are unchanged.
|
||||
pub attach_in_flight: u32,
|
||||
// G3.1: dedicated heartbeat — incremented ~10Hz by a bridge thread
|
||||
// independent of the audio loop. Watchdog liveness keys on this.
|
||||
// Append-only field: offsets above are unchanged.
|
||||
pub heartbeat: u32,
|
||||
// VSTi crackle fix: multi-slot audio ring (appended after heartbeat —
|
||||
// offsets above are unchanged). Bridge renders block N into slot
|
||||
// (bridge_write_index & ring_mask), then increments bridge_write_index.
|
||||
// Pump drains EVERY slot between its last index and write_index, in order.
|
||||
pub ring_mask: u32,
|
||||
pub ring_left: [[f32; AUDIO_BLOCK_SIZE]; 8],
|
||||
pub ring_right: [[f32; AUDIO_BLOCK_SIZE]; 8],
|
||||
}
|
||||
|
||||
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, live: u8) -> 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: [live, 0u8],
|
||||
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)
|
||||
}
|
||||
|
||||
/// Read ring slot for block index `idx` (slot = idx & ring_mask).
|
||||
pub fn read_ring_audio(&self, idx: u32) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) {
|
||||
let ipc = self.ipc();
|
||||
let slot = (idx & ipc.ring_mask) as usize;
|
||||
(ipc.ring_left[slot], ipc.ring_right[slot])
|
||||
}
|
||||
|
||||
pub fn ring_mask(&self) -> u32 {
|
||||
self.ipc().ring_mask
|
||||
}
|
||||
|
||||
pub fn write_index(&self) -> u32 {
|
||||
self.ipc().bridge_write_index
|
||||
}
|
||||
|
||||
pub fn block_timestamp(&self) -> u64 {
|
||||
self.ipc().block_timestamp
|
||||
}
|
||||
|
||||
pub fn attach_in_flight(&self) -> bool {
|
||||
self.ipc().attach_in_flight != 0
|
||||
}
|
||||
|
||||
pub fn heartbeat(&self) -> u32 {
|
||||
self.ipc().heartbeat
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// 11168 + ring_mask(4) + ring_left(8*256*4) + ring_right(8*256*4)
|
||||
// = 27556, rounded up to 8-byte struct alignment (u64 block_timestamp)
|
||||
// => 27560. Matches MSVC sizeof (verified against the C header).
|
||||
assert_eq!(std::mem::size_of::<SharedAudioBufferIPC>(), 27560);
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
};
|
||||
|
||||
// SHM name RIENG cho tung process app: 2 instance (vi du bản portable install\
|
||||
// + bản MSI cài song song) dung chung ten -> 2 bridge cung map 1 SHM -> ca 2
|
||||
// drain control/midi queue (double instrument load, double GUI attach) -> race
|
||||
// -> crash USER32 (Event Log 12:07:06 + 12:07:51: bridge MSI crash trong luc
|
||||
// bridge portable dang chay). App PID duy nhat moi instance.
|
||||
pub fn shm_name() -> String {
|
||||
format!("SonicForge_DAW_IPC_{}", std::process::id())
|
||||
}
|
||||
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, serde::Serialize)]
|
||||
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], // reserved[0]: live flag (1=live input, bypass transportStopped guard); reserved[1]: 0
|
||||
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,
|
||||
// G0.3: bridge sets this while an openGUI attach job is in flight.
|
||||
// Append-only field — offsets of control_queue (2836) and
|
||||
// control_queue_count (11156) are unchanged.
|
||||
pub attach_in_flight: u32,
|
||||
// G3.1: dedicated heartbeat — incremented ~10Hz by a bridge thread
|
||||
// independent of the audio loop. Watchdog liveness keys on this.
|
||||
// Append-only field: offsets above are unchanged.
|
||||
pub heartbeat: u32,
|
||||
// VSTi crackle fix: multi-slot audio ring (appended after heartbeat —
|
||||
// offsets above are unchanged). Bridge renders block N into slot
|
||||
// (bridge_write_index & ring_mask), then increments bridge_write_index.
|
||||
// Pump drains EVERY slot between its last index and write_index, in order.
|
||||
pub ring_mask: u32,
|
||||
pub ring_left: [[f32; AUDIO_BLOCK_SIZE]; 8],
|
||||
pub ring_right: [[f32; AUDIO_BLOCK_SIZE]; 8],
|
||||
// A14: MIDI OUT from plugins (VST2 audioMaster opcode=8 VstEvents / VST3
|
||||
// IEventList). Bridge writes, pump drains -> `bridge-midi-out` -> JS.
|
||||
// Append-only field: offsets above are unchanged.
|
||||
pub midi_out_queue: [MidiEventIPC; MIDI_QUEUE_CAP],
|
||||
pub midi_out_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, live: u8) -> 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: [live, 0u8],
|
||||
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)
|
||||
}
|
||||
|
||||
/// Read ring slot for block index `idx` (slot = idx & ring_mask).
|
||||
pub fn read_ring_audio(&self, idx: u32) -> ([f32; AUDIO_BLOCK_SIZE], [f32; AUDIO_BLOCK_SIZE]) {
|
||||
let ipc = self.ipc();
|
||||
let slot = (idx & ipc.ring_mask) as usize;
|
||||
(ipc.ring_left[slot], ipc.ring_right[slot])
|
||||
}
|
||||
|
||||
pub fn ring_mask(&self) -> u32 {
|
||||
self.ipc().ring_mask
|
||||
}
|
||||
|
||||
pub fn write_index(&self) -> u32 {
|
||||
self.ipc().bridge_write_index
|
||||
}
|
||||
|
||||
pub fn block_timestamp(&self) -> u64 {
|
||||
self.ipc().block_timestamp
|
||||
}
|
||||
|
||||
pub fn attach_in_flight(&self) -> bool {
|
||||
self.ipc().attach_in_flight != 0
|
||||
}
|
||||
|
||||
pub fn heartbeat(&self) -> u32 {
|
||||
self.ipc().heartbeat
|
||||
}
|
||||
|
||||
/// Drain all pending MIDI-out events (plugin-generated). Returns them and
|
||||
/// resets the count; empty vec when none.
|
||||
pub fn drain_midi_out(&self) -> Vec<MidiEventIPC> {
|
||||
let ipc = self.ipc();
|
||||
let n = (ipc.midi_out_count as usize).min(MIDI_QUEUE_CAP);
|
||||
if n == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
out.push(ipc.midi_out_queue[i]);
|
||||
}
|
||||
ipc.midi_out_count = 0;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// Last field (ring_right) ends at 27556; midi_out_queue appends
|
||||
// at 27556, +64*12 + 4 = 28328 (already 8-aligned — no tail pad).
|
||||
// Matches MSVC sizeof (verified against the C header).
|
||||
assert_eq!(std::mem::size_of::<SharedAudioBufferIPC>(), 28328);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user