fix: VST2 qua jbridge bị tạch tạch — pace child theo consumer-sync + bridge copy full block
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.
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user