167 lines
9.4 KiB
C++
167 lines
9.4 KiB
C++
#pragma once
|
|
// voice_engine.h — the COLD half of the sampler engine: note routing, voice allocation and
|
|
// stealing, the mono held-note stack, the two-tier panic, and the block render loops. The
|
|
// per-voice per-sample work it drives is inline in voice.h, so render's inner loop keeps its
|
|
// present inline shape across this seam.
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <vector>
|
|
|
|
#include "core/audio/peaks.h"
|
|
#include "core/instrument/engine/live_params.h"
|
|
#include "core/instrument/engine/play_params.h"
|
|
#include "core/instrument/engine/voice.h"
|
|
|
|
namespace reasampler {
|
|
|
|
using audio::AudioSample;
|
|
|
|
// The polyphonic voice engine: a fixed pool of voices over ONE loaded capture, note-on
|
|
// allocation with bounded voice stealing, note-off routing, and block rendering (sum of
|
|
// voices).
|
|
//
|
|
// Voice-stealing policy (deterministic, documented): when all voices are busy and a new
|
|
// note-on arrives, steal in this priority order:
|
|
// 1. the oldest voice already in release (finishing anyway — cheapest to cut),
|
|
// 2. else the oldest voice overall (longest-held note gives way to the new one).
|
|
// "Oldest" = smallest startOrder (assigned monotonically at note-on) — the standard
|
|
// hardware-sampler policy.
|
|
class VoiceEngine {
|
|
public:
|
|
// Builds an engine with `maxVoices` voices playing `sample` (must outlive the engine —
|
|
// held by reference, never copies PCM). Every playback parameter rides on the sample; the
|
|
// engine holds no parameters of its own beyond the voice-system config below.
|
|
// `preserveVoiceCap` bounds how many Preserve-engine voices may sound at once (the
|
|
// shifter is materially heavier than Varispeed) — a Preserve note-on beyond the cap is
|
|
// dropped rather than glitching; 0 means no separate cap (bounded only by maxVoices).
|
|
// `preserveWindowFrames` is the OLA window every voice's Preserve shifters are pre-sized
|
|
// to at construction (off the audio thread), so note-on never allocates; 0 leaves them
|
|
// pass-through. The processor derives it from the host sample rate.
|
|
//
|
|
// `voiceMode`: POLY is the pool-with-stealing engine above; MONO drives a single voice
|
|
// (voices_[0]) with last-note priority over the held-note stack, per `monoTrigger`
|
|
// (Retrigger restarts the envelopes on every takeover/fallback; Legato retunes without a
|
|
// re-attack). The engine's config is immutable — a mode/count change rebuilds the engine
|
|
// off-thread through the processor's drain-slot reload, so ringing tails survive the swap.
|
|
//
|
|
// `takeoverDeclick`: when true, every restart of a sounding voice (mono retrigger
|
|
// takeover/fallback, poly at-cap steal) seeds the per-voice declick ramp (see
|
|
// kDeclickDecay) so the hard cut doesn't click. start() self-gates on the voice being
|
|
// active, so a fresh start never ramps. Default false keeps the bare core byte-identical
|
|
// to the pre-fix engine; the processor shell opts in.
|
|
VoiceEngine(std::size_t maxVoices, const SampleData& sample,
|
|
std::size_t preserveVoiceCap = 0, std::int64_t preserveWindowFrames = 0,
|
|
VoiceMode voiceMode = VoiceMode::Poly,
|
|
MonoTrigger monoTrigger = MonoTrigger::Retrigger,
|
|
bool takeoverDeclick = false);
|
|
|
|
// 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.
|
|
std::size_t noteOn(int note, int velocity);
|
|
|
|
// MIDI note-off. Releases the most-recently-started active, non-releasing voice
|
|
// playing `note` (so a re-triggered same note releases the newest first, leaving
|
|
// the older tail to ring — matches hardware behavior). No-op if none match.
|
|
void noteOff(int note);
|
|
|
|
// CC 123 (All-Notes-Off): clears the mono held stack and releases every active voice
|
|
// (Gate enters AHDSR release; Trigger ignores release and plays through). The mono
|
|
// stack's only reset path — a phantom entry left by a lost note-off would otherwise be
|
|
// resurrected by the fallback and sustain forever with no key held. RT-safe.
|
|
void allNotesOff();
|
|
|
|
// CC 120 (All-Sounds-Off): hard-stops every voice immediately, clears the mono held
|
|
// stack, silences even Trigger one-shots that would ignore a release. Panic; CC 123 is
|
|
// the softer "let gates release." RT-safe, callable from the audio thread.
|
|
void allSoundsOff();
|
|
|
|
// Sums all active voices into the caller-provided buffer `out[0..frameCount)`, adding
|
|
// to whatever is there — never allocates (the audio-thread entry point; the VST3
|
|
// process callback passes the host's own output buffer). Voices that finish mid-block
|
|
// go idle. `out` must point at least `frameCount` writable samples; null/zero is a no-op.
|
|
void render(AudioSample* out, std::size_t frameCount);
|
|
|
|
// Stereo overload: sums per-channel into `left`/`right`, same RT discipline. A mono
|
|
// sample plays dual-mono (same value both channels); a stereo sample plays its two
|
|
// channels. Mono and stereo render are independent output shapes over the same voice
|
|
// pool — the active channel mode picks which one the process callback drives per block.
|
|
void render(AudioSample* left, AudioSample* right, std::size_t frameCount);
|
|
|
|
// Test/off-thread convenience: appends `frameCount` summed frames to `out` (grows it —
|
|
// do not call on the audio thread). Delegates to the real-time overload after sizing
|
|
// the buffer. Does not clear existing contents — appends.
|
|
void render(std::vector<AudioSample>& out, std::size_t frameCount);
|
|
|
|
// Count of currently active voices (for tests / diagnostics).
|
|
std::size_t activeVoiceCount() const;
|
|
|
|
std::size_t maxVoices() const { return voices_.size(); }
|
|
|
|
static constexpr std::size_t kNoVoice = static_cast<std::size_t>(-1);
|
|
|
|
private:
|
|
// Picks a voice to (re)use for a new note-on: a free voice if any, else a stolen
|
|
// one per the documented policy. Always returns a valid index (maxVoices >= 1).
|
|
std::size_t allocateVoice();
|
|
|
|
// --- Live-parameter observation (live_params.h) ---
|
|
// The ONE place the seqlock is read: at block start and at each note-on, on the audio
|
|
// thread, never per frame. A torn or never-published read leaves the last good snapshot
|
|
// in place rather than spinning. Returns whether a NEW generation landed.
|
|
bool refreshLive();
|
|
// Block-boundary refresh: pushes a newly-observed generation into every sounding voice,
|
|
// which glides toward it. No-op when nothing changed (and when no block is attached).
|
|
void applyLiveToActive();
|
|
// The one restart path: start the voice, hand it the live values outright (it has nothing
|
|
// to glide from), and stamp its age. Shared by the poly steal and both mono restarts so
|
|
// no restart site can miss the live handoff.
|
|
void startVoice(Voice& voice, int note, int velocity);
|
|
|
|
instrument::engine::LiveValues live_{};
|
|
std::uint32_t liveGeneration_ = 0; // last generation observed; 0 = none yet
|
|
bool haveLive_ = 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;
|
|
|
|
// Mono mode: last-note priority over a held-note stack. The stack holds every
|
|
// currently-held, playable note in press order (top = most recent = the sounding note).
|
|
// Re-pressing a held note moves it to the top. Fixed-capacity (128 distinct MIDI notes) —
|
|
// no allocation on the audio thread. Velocity is kept per held note so a retrigger
|
|
// fallback re-strikes at its original velocity.
|
|
struct HeldNote { std::uint8_t note; std::uint8_t velocity; };
|
|
|
|
// Push to the stack and take the voice over (legato retune, else a fresh start). Returns
|
|
// 0 (the mono voice) or kNoVoice for an unplayable/out-of-range note (rejected before the
|
|
// stack, which stores uint8). The Preserve cap is not applied in mono — a single voice
|
|
// runs at most one shifter, inherently within any cap; applying it would wrongly drop a
|
|
// Preserve->Preserve takeover.
|
|
std::size_t monoNoteOn(int note, int velocity);
|
|
// Pop from the stack; if the released note was sounding, fall back to the most-recent
|
|
// still-held note (retrigger or legato per monoTrigger_), else release.
|
|
void monoNoteOff(int note);
|
|
// Drops `note` from the held stack (order of the remaining notes preserved). No-op if absent.
|
|
void removeHeld(int note);
|
|
|
|
std::vector<Voice> voices_;
|
|
const SampleData& sample_;
|
|
std::size_t preserveVoiceCap_ = 0; // max simultaneous Preserve voices (0 = no separate cap)
|
|
std::uint64_t nextStartOrder_ = 1; // monotonic; 0 reserved for "never started"
|
|
VoiceMode voiceMode_ = VoiceMode::Poly;
|
|
MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;
|
|
bool takeoverDeclick_ = false; // declick every restart/steal of a sounding voice
|
|
std::array<HeldNote, 128> heldStack_{}; // mono held notes, press order; top = heldCount_-1
|
|
std::size_t heldCount_ = 0;
|
|
};
|
|
|
|
// The editor's preview trigger is a synthetic note-on at the loaded capture's root note
|
|
// through the same VoiceEngine host MIDI drives, so preview is a real voice: it counts
|
|
// against the voice count, can steal/be stolen, and respects Poly/Mono + Retrigger/Legato.
|
|
// There is no dedicated preview voice isolated from the MIDI pool.
|
|
|
|
} // namespace reasampler
|