da7ae02509
Root cause: child plugin_host render ~50x real-time (9323 blocks/s), bridge đọc slot đang bị child ghi đè (torn read) và mỗi segment của 1 DAW block đọc từ nhiều child block khác nhau -> discontinuity -> tạch thay vì âm thật. - SandboxHostIPC: thêm bridgeConsumed[2] cuối struct (append-only, offsets cũ giữ nguyên) — child đếm publish/slot, bridge đếm consume/slot. - plugin_host_main.cpp: sau khi publish 1 block, chờ bridge consume xong block đó mới render block kế (wait-sau-publish, không phải wait-trước- render — wait-trước vẫn cho phép publish 2 block liên tiếp giữa 2 lần consume -> bridge đọc latest sẽ bỏ block). Child giờ publish đúng 1 block/DAW block = real-time; bridge không bao giờ stall (main loop DAW render + pace mọi iteration) nên không deadlock; nếu bridge chết, parent_alive break wait. - SandboxVst2Host: copy full 256 mẫu vào local buffer khi slot đổi, serve mọi segment của 1 DAW block từ 1 child block nhất quán. Verify: child pace 371 blocks/2s (real-time 375, trước 18647); domFreq của waveform = 257.8Hz đúng nốt C4 (note 60) — âm thật, không phải noise; correlation giữa các block liên tiếp âm đều ~-0.4 (waveform tuần hoàn); GUI open path không deadlock (pace vẫn real-time, jBridge dialog + Qin_VST_ Window hiện đủ); GUI đóng -> child idle delta 0; silence_all master peak 0.000016.
125 lines
6.3 KiB
C++
125 lines
6.3 KiB
C++
// native_bridge/include/SharedMemoryIPC.h
|
|
#pragma once
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#define AUDIO_BLOCK_SIZE 256
|
|
|
|
// WARNING: volatile is NOT a sync primitive. Real impl should use an
|
|
// interlocked/index-flag pair or a Win32 event (SetEvent) signalled by the
|
|
// writer. This struct follows the spec layout so Rust/JS mapping stays in sync.
|
|
struct SharedAudioBufferIPC {
|
|
// Synchronization Flags
|
|
volatile uint32_t clientReadIndex;
|
|
volatile uint32_t bridgeWriteIndex;
|
|
|
|
// PCM Float32 Audio Buffers
|
|
float masterLeft[AUDIO_BLOCK_SIZE];
|
|
float masterRight[AUDIO_BLOCK_SIZE];
|
|
|
|
// Latency probe: bridge stamps every rendered block (QueryPerformanceCounter)
|
|
volatile uint64_t blockTimestamp;
|
|
|
|
// MIDI Event Exchange Queue
|
|
struct MidiEventIPC {
|
|
uint8_t command; // 0x9 note on / 0x8 note off / 0xB CC / 0xC program / 0xE pitch bend
|
|
uint8_t channel;
|
|
uint8_t pitch; // note number (0x9/0x8) or CC number (0xB)
|
|
uint8_t velocity; // 0-127 (0x9 + vel=0 == note off)
|
|
uint8_t data2; // CC value / program number / PB LSB
|
|
uint8_t data3; // PB MSB (0xE only)
|
|
uint8_t reserved[2]; // reserved[0]: live flag (1 = live input, bypass transportStopped guard); reserved[1]: 0 — keeps C/Rust layout stable
|
|
uint32_t sampleOffset;
|
|
} midiQueue[64];
|
|
|
|
volatile uint32_t midiQueueCount;
|
|
|
|
// Transport / Control (added vs spec: Stop/Play flush, instrument switch)
|
|
struct ControlEventIPC {
|
|
uint32_t type; // 0 = NONE, 1 = PANIC/ALL_NOTES_OFF, 2 = LOAD_INSTRUMENT,
|
|
// 3 = TRANSPORT, 4 = OPEN_GUI
|
|
uint32_t arg0; // LOAD: instrument type (InstrumentType);
|
|
// TRANSPORT: 0=STOP, 1=PLAY, 2=SET_POSITION
|
|
uint32_t arg1; // LOAD: string length (bytes) of path;
|
|
// TRANSPORT: playheadSamples (for timeline sync)
|
|
uint32_t channel; // LOAD: MIDI channel to assign instrument to (A10)
|
|
char arg2[1024]; // LOAD: UTF-8 path
|
|
} controlQueue[8];
|
|
volatile uint32_t controlQueueCount;
|
|
|
|
// G0.3: set by the bridge while an openGUI attach job is in flight
|
|
// (reload/attachView on a ChannelWorker). The Tauri watchdog reads this
|
|
// to extend its stall deadline — a long Nexus attach must not look like
|
|
// a dead bridge. Append-only field: existing offsets stay stable.
|
|
volatile uint32_t attachInFlight;
|
|
|
|
// G3.1: dedicated heartbeat, incremented every 100ms by a bridge-side
|
|
// thread INDEPENDENT of the audio loop. The Tauri watchdog keys bridge
|
|
// liveness on this, so a slow (but alive) audio loop no longer looks dead.
|
|
// Append-only field: existing offsets stay stable.
|
|
volatile uint32_t heartbeat;
|
|
|
|
// VSTi crackle fix: multi-slot audio ring (appended after heartbeat — all
|
|
// offsets above stay stable). The bridge renders block N into slot
|
|
// (bridgeWriteIndex & ringMask), then increments bridgeWriteIndex (publish).
|
|
// The reader drains EVERY slot between its last index and writeIndex, in
|
|
// order, so a delayed reader no longer loses a whole block to single-slot
|
|
// overwrite (the old 1-slot gap). ringMask = slot count - 1 (power of 2);
|
|
// 8 slots ~= 42ms of audio @ 48k — covers a pump/WebView jank window.
|
|
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.
|
|
// The child renders into the slot it is NOT publishing (writeSlot flips),
|
|
// then publishes; the bridge reads the published slot. A stale slot is safe
|
|
// to read (bridge keeps last block), so a slow child never tears audio.
|
|
// G4.5: preset blob round-trip for VST2 sandbox children. jBridge romplers
|
|
// like Kong Audio Qin_RV render SILENT from factory state - they need their
|
|
// program/bank chunk restored (bridge_state presetBase64). presetIn is
|
|
// bridge->child (base64 text; presetInLen>0 = pending, child applies and
|
|
// zeroes it). presetOut is child->bridge: the child republishes its chunk
|
|
// after load / preset apply / GUI edit, so the bridge can persist it.
|
|
#define PRESET_BLOB_SIZE 65536
|
|
struct SandboxHostIPC {
|
|
SharedAudioBufferIPC base; // MIDI/control/heartbeat, offsets unchanged
|
|
volatile uint32_t writeSlot; // 0 or 1: slot the child finished writing
|
|
float audioLeft[2][AUDIO_BLOCK_SIZE]; // 2 full blocks
|
|
float audioRight[2][AUDIO_BLOCK_SIZE];
|
|
char presetIn[PRESET_BLOB_SIZE]; // bridge -> child (base64 text)
|
|
volatile uint32_t presetInLen; // >0: pending; child applies + zeroes
|
|
char presetOut[PRESET_BLOB_SIZE]; // child -> bridge (base64 text)
|
|
volatile uint32_t presetOutLen; // 0 = none published yet
|
|
// G4.7: consumer-sync pacing. The unpaced child renders ~50x real-time and
|
|
// overwrites the slot the bridge is reading (torn audio -> "tạch tạch").
|
|
// Child increments a publish count per slot; bridge increments the consume
|
|
// count when it copies a slot. The child waits after each publish until the
|
|
// bridge consumed it, so it publishes exactly one block per DAW block.
|
|
// Append-only: keep at the END, offsets above stay stable.
|
|
volatile uint32_t bridgeConsumed[2]; // bridge -> child: consume count per slot
|
|
};
|
|
|
|
// Shared-memory helpers (impl in SharedMemoryIPC.cpp).
|
|
struct ShmHandle;
|
|
ShmHandle* shm_open(const char* name, size_t size = sizeof(SharedAudioBufferIPC));
|
|
ShmHandle* shm_create(const char* name, size_t size = sizeof(SharedAudioBufferIPC));
|
|
SharedAudioBufferIPC* shm_ptr(ShmHandle* h);
|
|
SandboxHostIPC* shm_sandbox_ptr(ShmHandle* h);
|
|
void shm_close(ShmHandle* h);
|
|
bool shm_write_midi(ShmHandle* h, uint8_t cmd, uint8_t channel, uint8_t pitch,
|
|
uint8_t velocity, uint32_t sampleOffset, uint8_t data2 = 0,
|
|
uint8_t data3 = 0);
|