Make spline EG drags audible live: held and new notes follow a dragged knot

Contours ride their own seqlock block into a double-buffered engine copy, and the swap step is smoothed. Each drag move publishes contours without a model write; release still commits and reloads.
This commit is contained in:
2026-09-25 13:42:25 -04:00
parent 9ff34b0a9e
commit f7505d0e53
21 changed files with 788 additions and 97 deletions
+22
View File
@@ -10,6 +10,7 @@
#include <vector>
#include "core/audio/peaks.h"
#include "core/instrument/engine/live_contours.h"
#include "core/instrument/engine/live_params.h"
#include "core/instrument/engine/play_params.h"
#include "core/instrument/engine/voice.h"
@@ -57,6 +58,11 @@ public:
MonoTrigger monoTrigger = MonoTrigger::Retrigger,
bool takeoverDeclick = false);
// Voices may hold raw pointers into this engine's own contour buffers, so a copy would
// leave them reading the original's.
VoiceEngine(const VoiceEngine&) = delete;
VoiceEngine& operator=(const VoiceEngine&) = delete;
// MIDI note-on. Allocates a free voice, or steals one per the policy above. Returns the
// index of the voice used, or kNoVoice when nothing is playable (no decoded PCM, an
// out-of-range note, or a Preserve note-on past the cap) — a defined no-play, not an error.
@@ -124,6 +130,22 @@ private:
std::uint32_t liveGeneration_ = 0; // last generation observed; 0 = none yet
bool haveLive_ = false;
// --- Drawn-contour observation (live_contours.h) ---
// Reads a new contour generation, when one landed, into the buffer no voice is bound to and
// rebinds EVERY active voice to it before returning — which is what keeps the other buffer
// free for the next read. That invariant is why this, and never a bare read, is what both
// the block boundary and startVoice call.
void applyContoursToActive();
// Double-buffered because a voice's SplineCursor points INTO the buffer: reading over it
// would change a contour under a sounding voice without the rebind that absorbs the step,
// and a torn read (which the seqlock only reports after the fact) must land somewhere no
// voice reads. ~9 KB each, in the engine, so nothing on the audio thread allocates.
std::array<instrument::engine::LiveContours, 2> contours_{};
std::size_t contourFront_ = 0;
std::uint32_t contourGeneration_ = 0; // last generation observed; 0 = none yet
bool haveContours_ = false;
// Count of active Preserve-engine voices (for the Preserve cap). Rescanned per note-on
// (cheap: bounded by maxVoices) rather than maintained as a running tally.
std::size_t activePreserveVoices() const;