instrument: deliver continuous playback params live to sounding voices via a seqlock block, holding normalized stage position across time edits

This commit is contained in:
2026-07-30 21:03:05 -04:00
parent 7bd911d58b
commit 1dade0bfcf
25 changed files with 1352 additions and 66 deletions
+31 -6
View File
@@ -33,6 +33,32 @@ VoiceEngine::VoiceEngine(std::size_t maxVoices, const SampleData& sample,
}
}
bool VoiceEngine::refreshLive() {
const instrument::engine::LiveParams* block = sample_.live;
if (block == nullptr) return false; // bare engine: the latched note-on values stand
instrument::engine::LiveValues observed;
const std::uint32_t generation = block->read(observed);
if (generation == 0 || generation == liveGeneration_) return false;
liveGeneration_ = generation;
live_ = observed;
haveLive_ = true;
return true;
}
void VoiceEngine::applyLiveToActive() {
if (!refreshLive()) return;
for (Voice& voice : voices_) {
if (voice.active()) voice.applyLive(live_, /*snap=*/false);
}
}
void VoiceEngine::startVoice(Voice& voice, int note, int velocity) {
refreshLive();
voice.start(note, velocity, sample_, /*declickTakeover=*/takeoverDeclick_);
if (haveLive_) voice.applyLive(live_, /*snap=*/true);
voice.setStartOrder(nextStartOrder_++);
}
std::size_t VoiceEngine::activePreserveVoices() const {
// Count only voices that are SOUNDING A NOTE (playable span still running), not voices
// that have finished their note but are still ringing out a declick tail. A ramp-only
@@ -122,8 +148,7 @@ std::size_t VoiceEngine::monoNoteOn(int note, int velocity) {
// RETRIGGER takeover / first note of a phrase: (re)start the voice. The declick opt-in
// rides every mono restart; start() self-gates it on the voice being ACTIVE, so a
// first-note fresh start never ramps — only a hard cut of a sounding tone.
v.start(note, velocity, sample_, /*declickTakeover=*/takeoverDeclick_);
v.setStartOrder(nextStartOrder_++);
startVoice(v, note, velocity);
return 0;
}
@@ -150,8 +175,7 @@ void VoiceEngine::monoNoteOff(int note) {
// Retrigger fallback: re-strike the fallen-back-to note at its own original velocity.
// Peer restart site of monoNoteOn's takeover — same declick opt-in (the fallback also
// hard-cuts the sounding tone).
v.start(fb.note, fb.velocity, sample_, /*declickTakeover=*/takeoverDeclick_);
v.setStartOrder(nextStartOrder_++);
startVoice(v, fb.note, fb.velocity);
}
std::size_t VoiceEngine::noteOn(int note, int velocity) {
@@ -174,8 +198,7 @@ std::size_t VoiceEngine::noteOn(int note, int velocity) {
// active, so a free-voice start never ramps — only an at-cap steal, which is the same hard
// cut of a sounding tone as the mono retrig takeover.
const std::size_t v = allocateVoice();
voices_[v].start(note, velocity, sample_, /*declickTakeover=*/takeoverDeclick_);
voices_[v].setStartOrder(nextStartOrder_++);
startVoice(voices_[v], note, velocity);
return v;
}
@@ -225,6 +248,7 @@ void VoiceEngine::render(AudioSample* out, std::size_t frameCount) {
// The VST3 process callback hands us the host's output channel buffer here, so the
// audio thread never touches the heap.
if (out == nullptr || frameCount == 0) return;
applyLiveToActive(); // block boundary, once — never inside the frame loop
for (Voice& voice : voices_) {
if (!voice.active()) continue;
for (std::size_t f = 0; f < frameCount; ++f) {
@@ -240,6 +264,7 @@ void VoiceEngine::render(AudioSample* left, AudioSample* right, std::size_t fram
// iteration, same mid-block idle short-circuit) so stereo and mono share one stealing/idle
// discipline; only the per-frame call differs (renderFrameStereo vs renderFrame).
if (left == nullptr || right == nullptr || frameCount == 0) return;
applyLiveToActive(); // block boundary, once — never inside the frame loop
for (Voice& voice : voices_) {
if (!voice.active()) continue;
for (std::size_t f = 0; f < frameCount; ++f) {