S4 Tier 0: the bank plays — VST3 marshals MIDI to the S3 core, reads the live bank + resolves WAV the M4 way, mono downmix, lock-free load handoff, LICE sample-pick

This commit is contained in:
2026-07-26 16:43:04 -04:00
parent b0fc052113
commit 0cde457224
24 changed files with 1239 additions and 302 deletions
+15 -4
View File
@@ -289,18 +289,29 @@ void VoiceEngine::noteOff(int note) {
if (target != kNoVoice) voices_[target].release();
}
void VoiceEngine::render(std::vector<AudioSample>& out, std::size_t frameCount) {
const std::size_t base = out.size();
out.resize(base + frameCount, 0.0f); // S4: caller must pre-reserve — no allocation allowed under the VST3 process callback.
void VoiceEngine::render(AudioSample* out, std::size_t frameCount) {
// Real-time safe: no allocation, no resize — mix straight into the caller's buffer.
// The VST3 process callback hands us the host's output channel buffer here, so the
// audio thread never touches the heap (S4 real-time discipline).
if (out == nullptr || frameCount == 0) return;
for (Voice& voice : voices_) {
if (!voice.active()) continue;
for (std::size_t f = 0; f < frameCount; ++f) {
if (!voice.active()) break;
out[base + f] += voice.renderFrame();
out[f] += voice.renderFrame();
}
}
}
void VoiceEngine::render(std::vector<AudioSample>& out, std::size_t frameCount) {
// Off-thread / test path: grow the buffer (this allocates — never call under
// process), zero-fill the appended span, then delegate to the RT mix loop so both
// overloads share exactly one summation path.
const std::size_t base = out.size();
out.resize(base + frameCount, 0.0f);
render(out.data() + base, frameCount);
}
std::size_t VoiceEngine::activeVoiceCount() const {
std::size_t n = 0;
for (const Voice& v : voices_) {