fix(vsti): crackle fix P1-P3 — SHM 8-slot ring + drain-delta pump + AudioWorklet sink

- SharedMemoryIPC.h/main.cpp: render into ring slot (idx & ringMask), 8 slots
- shm.rs: ring fields + read_ring_audio; lib.rs pump drains FULL delta, not just latest
- bridgeAudioNode.js: AudioWorklet primary (bridge-audio-sink), ScriptProcessor fallback
- bridgeAudioWorklet.js: ring 64 blocks, underrun fade, overrun drop + crossfade
- layout tests: sizeof 27560 (8-align via u64)
This commit is contained in:
2026-08-16 20:43:50 +07:00
parent ed26e1706d
commit cead8c8c71
8 changed files with 235 additions and 29 deletions
+11
View File
@@ -62,6 +62,17 @@ struct SharedAudioBufferIPC {
// 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];
};
// G4.3: per-plugin sandbox SHM — double-buffered audio handoff.
+6 -1
View File
@@ -654,6 +654,8 @@ int main(int argc, char* argv[]) {
#else
(void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows
auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC));
// VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h).
shmIPC->ringMask = 7;
if (!shmIPC) return 1;
#endif
@@ -754,9 +756,12 @@ int main(int argc, char* argv[]) {
};
// Render only [from, to) of the block — used by sample-accurate splitting.
// VSTi crackle fix: target the multi-slot ring slot for the block being
// built, so old blocks stay readable until the reader drains them.
auto renderSegment = [&](uint32_t from, uint32_t to) {
if (to <= from) return;
instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from);
uint32_t slot = shmIPC->bridgeWriteIndex & shmIPC->ringMask;
instruments.renderAll(shmIPC->ringLeft[slot] + from, shmIPC->ringRight[slot] + from, to - from);
};
double blockDurationMs = (double)block / sampleRate * 1000.0;
+12 -1
View File
@@ -65,7 +65,18 @@ int main() {
// 5. Layout stability — MUST match Rust src-tauri/src/shm.rs
assert(sizeof(SharedAudioBufferIPC::MidiEventIPC) == 12);
assert(sizeof(SharedAudioBufferIPC::ControlEventIPC) == 1040);
assert(sizeof(SharedAudioBufferIPC) == 11168); // G3.1: +4 heartbeat
// 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);
// 6. Audio ring round-trip (multi-slot, VSTi crackle fix)
ipc->ringMask = 7;
for (uint32_t i = 0; i < AUDIO_BLOCK_SIZE; ++i) ipc->ringLeft[3][i] = (float)i * 2.0f;
ipc->ringRight[3][0] = 42.0f;
assert(ipc->ringLeft[3][255] == 510.0f);
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));
shm_close(h);