diff --git a/native_bridge/include/SharedMemoryIPC.h b/native_bridge/include/SharedMemoryIPC.h index d08296b..cae2539 100644 --- a/native_bridge/include/SharedMemoryIPC.h +++ b/native_bridge/include/SharedMemoryIPC.h @@ -103,6 +103,13 @@ struct SandboxHostIPC { 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). diff --git a/native_bridge/src/SandboxVst2Host.cpp b/native_bridge/src/SandboxVst2Host.cpp index d92903c..b4bcae3 100644 --- a/native_bridge/src/SandboxVst2Host.cpp +++ b/native_bridge/src/SandboxVst2Host.cpp @@ -321,12 +321,26 @@ void SandboxVst2Host::processAudioBlock(float* outputL, float* outputR, uint32_t std::memset(outputR, 0, numSamples * sizeof(float)); return; } - // G4.3: the child publishes the slot it finished rendering (writeSlot). - // Copy that block. A stale slot means the child is still working on the - // other one - we repeat the previous block instead of tearing. + // G4.7: copy the newest child block into a LOCAL buffer once per slot + // change, then serve every segment of this DAW block from it. The old + // direct read tore when the unpaced child (~50x real-time) overwrote + // the slot mid-read, and mixed samples from several child blocks across + // the segments of one DAW block. With consumer-sync pacing the child + // publishes exactly one block per DAW block, so the slot only changes + // at a block boundary. ponytail: a child publish landing between two + // segments of one DAW block (sub-ms race) would still copy mid-block; + // impossible in practice - the child is asleep in Sleep(1) when the + // DAW dispatches segments (<100us). Upgrade path: pass the DAW block + // index into processAudioBlock and copy once per block. uint32_t slot = ipc_->writeSlot & 1u; + if (slot != lastWriteSlot_) { + std::memcpy(lastLeft_, ipc_->audioLeft[slot], sizeof(lastLeft_)); + std::memcpy(lastRight_, ipc_->audioRight[slot], sizeof(lastRight_)); + ipc_->bridgeConsumed[slot]++; + lastWriteSlot_ = slot; + } for (uint32_t i = 0; i < numSamples; ++i) { - outputL[i] = ipc_->audioLeft[slot][i]; - outputR[i] = ipc_->audioRight[slot][i]; + outputL[i] = lastLeft_[i]; + outputR[i] = lastRight_[i]; } } diff --git a/native_bridge/src/SandboxVst2Host.h b/native_bridge/src/SandboxVst2Host.h index e4c6711..c54d347 100644 --- a/native_bridge/src/SandboxVst2Host.h +++ b/native_bridge/src/SandboxVst2Host.h @@ -61,6 +61,14 @@ private: std::string path_; std::string shmName_; std::string lastPreset_; // last preset queued (seed or real patch) — re-queued on watchdog respawn + // G4.7: local copy of the last consumed child block. The DAW may call + // processAudioBlock in segments within one block; serve every segment + // from ONE coherent child block (copied on slot change). lastWriteSlot_ + // starts 0 == zeroed shm slot 0, so the first child publish (slot 1) + // always triggers the copy; before that we serve silence. + float lastLeft_[AUDIO_BLOCK_SIZE] = {}; + float lastRight_[AUDIO_BLOCK_SIZE] = {}; + uint32_t lastWriteSlot_ = 0; double sampleRate_ = 44100.0; uint32_t block_ = AUDIO_BLOCK_SIZE; uint32_t channel_ = 0; diff --git a/native_bridge/src/plugin_host_main.cpp b/native_bridge/src/plugin_host_main.cpp index 9c38f9c..8fa6f64 100644 --- a/native_bridge/src/plugin_host_main.cpp +++ b/native_bridge/src/plugin_host_main.cpp @@ -227,6 +227,10 @@ int main(int argc, char* argv[]) { } }).detach(); + // G4.7: publish counter per slot (how many times this child published + // each slot). Paired with the bridge's bridgeConsumed[2] - see the + // wait after publish below. + uint32_t childPub[2] = {0, 0}; int activeNotes = 0; bool idle = false; bool quit = false; @@ -345,6 +349,7 @@ int main(int argc, char* argv[]) { uint32_t slot = 1u - (ipc->writeSlot & 1u); inst.processAudioBlock(ipc->audioLeft[slot], ipc->audioRight[slot], block); ipc->writeSlot = slot; + childPub[slot]++; ipc->base.bridgeWriteIndex++; if (activeNotes == 0) { float peak = 0.0f; @@ -358,6 +363,18 @@ int main(int argc, char* argv[]) { } if (peak < 0.0005f) idle = true; } + // G4.7: consumer-sync pacing. The bridge (DAW) consumes exactly one + // block per real-time block; the old unpaced child rendered ~50x + // real-time, so the bridge skipped most child blocks -> audio jumps + // ("tạch tạch" instead of the instrument). Wait AFTER publishing + // until the bridge copied THIS block before rendering the next one. + // The bridge never stalls (DAW main loop renders + paces every + // iteration), so this wait is bounded (~5ms). If the bridge dies, + // parentPid points at it - the parent_alive check breaks the wait. + while (ipc->bridgeConsumed[slot] < childPub[slot]) { + if (parentPid && !parent_alive(parentPid)) break; + Sleep(1); + } Sleep(0); } inst.closeGUI();