// sampler_core — pure sampler engine implementation. See sampler_core.h for the // contract and the design rationale (keymap resolution, pitch ratio, ADSR shape, // voice allocation + stealing policy). NO VST3 / REAPER / SWELL / vendor includes. #include "sampler_core.h" #include namespace reasampler { // --------------------------------------------------------------------------- // pitchRatio // --------------------------------------------------------------------------- double pitchRatio(int note, int rootNote) { // Equal temperament: each semitone is a factor of 2^(1/12). note == root -> 1.0. return std::pow(2.0, static_cast(note - rootNote) / 12.0); } // --------------------------------------------------------------------------- // Keymap // --------------------------------------------------------------------------- ZoneResolution Keymap::resolve(int note, int velocity) const { (void)velocity; // accepted for the Tier-2 seam; does not select at Tier 0-1. for (std::size_t i = 0; i < zones.size(); ++i) { const KeyZone& z = zones[i]; if (note >= z.lowNote && note <= z.highNote) { return ZoneResolution{true, i}; } } return ZoneResolution{false, 0}; } Keymap Keymap::singleSampleChromatic(SampleData sample) { const int root = sample.rootNote; Keymap km; km.samples.push_back(std::move(sample)); KeyZone zone; zone.lowNote = 0; zone.highNote = 127; zone.rootNote = root; zone.sampleIndex = 0; km.zones.push_back(zone); return km; } // --------------------------------------------------------------------------- // AdsrEnvelope // --------------------------------------------------------------------------- void AdsrEnvelope::noteOn() { stage_ = Stage::Attack; level_ = 0.0; framesInStage_ = 0; } void AdsrEnvelope::noteOff() { if (stage_ == Stage::Idle || stage_ == Stage::Finished || stage_ == Stage::Release) { return; // already released / not sounding. } // Release from the CURRENT level — release-before-sustain releases from the // partial attack/decay level, not from sustainLevel. releaseFrom_ = level_; stage_ = Stage::Release; framesInStage_ = 0; } double AdsrEnvelope::tick() { switch (stage_) { case Stage::Idle: case Stage::Finished: level_ = 0.0; return 0.0; case Stage::Attack: { if (params_.attackFrames <= 0) { level_ = 1.0; } else { level_ = static_cast(framesInStage_) / static_cast(params_.attackFrames); if (level_ > 1.0) level_ = 1.0; } const double out = level_; ++framesInStage_; if (framesInStage_ >= params_.attackFrames) { stage_ = Stage::Decay; framesInStage_ = 0; level_ = 1.0; } return out; } case Stage::Decay: { if (params_.decayFrames <= 0) { level_ = params_.sustainLevel; } else { const double t = static_cast(framesInStage_) / static_cast(params_.decayFrames); level_ = 1.0 + (params_.sustainLevel - 1.0) * t; } const double out = level_; ++framesInStage_; if (framesInStage_ >= params_.decayFrames) { stage_ = Stage::Sustain; framesInStage_ = 0; level_ = params_.sustainLevel; } return out; } case Stage::Sustain: level_ = params_.sustainLevel; return level_; case Stage::Release: { if (params_.releaseFrames <= 0) { level_ = 0.0; stage_ = Stage::Finished; return 0.0; } const double t = static_cast(framesInStage_) / static_cast(params_.releaseFrames); level_ = releaseFrom_ * (1.0 - t); if (level_ < 0.0) level_ = 0.0; const double out = level_; ++framesInStage_; if (framesInStage_ >= params_.releaseFrames) { stage_ = Stage::Finished; level_ = 0.0; } return out; } } return 0.0; // unreachable; silences a warning. } // --------------------------------------------------------------------------- // Voice // --------------------------------------------------------------------------- void Voice::start(int note, int velocity, const SampleData& sample, int rootNote, const AdsrParams& adsr) { active_ = true; releasing_ = false; note_ = note; // MIDI velocity 1..127 -> linear gain 0..1. Clamp defensively. int v = velocity; if (v < 0) v = 0; if (v > 127) v = 127; velocityGain_ = static_cast(v) / 127.0; ratio_ = pitchRatio(note, rootNote); readPos_ = 0.0; sample_ = &sample; env_.configure(adsr); env_.noteOn(); } void Voice::release() { if (!active_) return; releasing_ = true; env_.noteOff(); } AudioSample Voice::renderFrame() { if (!active_ || sample_ == nullptr) return 0.0f; const std::vector& pcm = sample_->frames; const std::int64_t frameCount = static_cast(pcm.size()); // Loop-aware sustain: if a valid, non-zero-length loop exists and the read head // has advanced past the loop end, wrap it back into [start, end). A zero-length // loop (start == end) is treated as "no loop" — the note is allowed to run off // the sample end and go silent, rather than spinning on a zero span. const SampleLoop& loop = sample_->loop; const bool loopUsable = loop.hasLoop && loop.end > loop.start && loop.start >= 0 && loop.end <= frameCount; if (loopUsable) { const std::int64_t loopStart = loop.start; const std::int64_t loopEnd = loop.end; const double loopLen = static_cast(loopEnd - loopStart); while (readPos_ >= static_cast(loopEnd)) { readPos_ -= loopLen; // wrap by exactly one loop length, preserving phase. } } // Ran off the end with no usable loop -> voice is done. if (readPos_ >= static_cast(frameCount)) { active_ = false; return 0.0f; } // Linear interpolation between the two bracketing frames. For the loop case, the // second point wraps to loopStart so the seam is continuous. const std::int64_t i0 = static_cast(readPos_); const double frac = readPos_ - static_cast(i0); std::int64_t i1 = i0 + 1; if (loopUsable && i1 >= loop.end) { i1 = loop.start; // seamless wrap for the interpolation partner. } // i0 is always in [0, frameCount) after the early-out above; the guard is purely // defensive. i1 (the interpolation partner) can exceed frameCount when no loop // wraps it — only that partner actually needs the clamp. const double s0 = (i0 >= 0 && i0 < frameCount) ? static_cast(pcm[i0]) : 0.0; const double s1 = (i1 >= 0 && i1 < frameCount) ? static_cast(pcm[i1]) : 0.0; const double interp = s0 + (s1 - s0) * frac; const double amp = env_.tick(); const double out = interp * amp * velocityGain_; readPos_ += ratio_; if (env_.finished()) { active_ = false; } return static_cast(out); } // --------------------------------------------------------------------------- // VoiceEngine // --------------------------------------------------------------------------- VoiceEngine::VoiceEngine(std::size_t maxVoices, const Keymap& keymap, const AdsrParams& adsr) : voices_(maxVoices == 0 ? 1 : maxVoices), keymap_(keymap), adsr_(adsr) { // maxVoices == 0 would mean "no polyphony at all", which cannot service a note-on; // clamp to a single voice so the engine is always usable (documented degenerate). } std::size_t VoiceEngine::allocateVoice() { // 1. A free (idle) voice, lowest index for determinism. for (std::size_t i = 0; i < voices_.size(); ++i) { if (!voices_[i].active()) return i; } // 2. All busy -> steal. Prefer the oldest voice already in release (a dying tail), // else the oldest voice overall. "Oldest" = smallest startOrder. std::size_t bestReleasing = kNoVoice; std::uint64_t bestReleasingOrder = 0; std::size_t bestOverall = kNoVoice; std::uint64_t bestOverallOrder = 0; for (std::size_t i = 0; i < voices_.size(); ++i) { const std::uint64_t order = voices_[i].startOrder(); if (voices_[i].releasing()) { if (bestReleasing == kNoVoice || order < bestReleasingOrder) { bestReleasing = i; bestReleasingOrder = order; } } if (bestOverall == kNoVoice || order < bestOverallOrder) { bestOverall = i; bestOverallOrder = order; } } return bestReleasing != kNoVoice ? bestReleasing : bestOverall; } std::size_t VoiceEngine::noteOn(int note, int velocity) { const ZoneResolution res = keymap_.resolve(note, velocity); if (!res.matched) return kNoVoice; // out-of-zone: defined no-play. const KeyZone& zone = keymap_.zones[res.zoneIndex]; if (zone.sampleIndex >= keymap_.samples.size()) { return kNoVoice; // zone points at a missing sample — refuse rather than UB. } const SampleData& sample = keymap_.samples[zone.sampleIndex]; const std::size_t v = allocateVoice(); voices_[v].start(note, velocity, sample, zone.rootNote, adsr_); voices_[v].setStartOrder(nextStartOrder_++); return v; } void VoiceEngine::noteOff(int note) { // Release the NEWEST active, non-releasing voice on this note (largest startOrder), // so a re-triggered note releases its newest instance first and older tails ring. std::size_t target = kNoVoice; std::uint64_t bestOrder = 0; for (std::size_t i = 0; i < voices_.size(); ++i) { if (voices_[i].active() && !voices_[i].releasing() && voices_[i].note() == note) { const std::uint64_t order = voices_[i].startOrder(); if (target == kNoVoice || order > bestOrder) { target = i; bestOrder = order; } } } if (target != kNoVoice) voices_[target].release(); } void VoiceEngine::render(std::vector& 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. 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(); } } } std::size_t VoiceEngine::activeVoiceCount() const { std::size_t n = 0; for (const Voice& v : voices_) { if (v.active()) ++n; } return n; } } // namespace reasampler