DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Sampler.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
71#include "DspMath.h"
72#include "AudioBuffer.h"
73#include "AudioSpec.h"
74#include "EnvelopeGenerator.h"
75
76#include <algorithm>
77#include <array>
78#include <atomic>
79#include <cmath>
80#include <cstdint>
81#include <memory>
82#include <new>
83#include <span>
84#include <vector>
85
86namespace dspark {
87
103template <FloatType T>
105{
106public:
108 enum class LoopMode
109 {
110 None,
111 Continuous,
112 Sustain
113 };
114
116 struct ZoneSpec
117 {
119 double sampleRate = 48000.0;
120 int rootKey = 60;
121 double tuneCents = 0.0;
122 int keyLow = 0;
123 int keyHigh = 127;
124 double velocityLow = 0.0;
125 double velocityHigh = 1.0;
126 double gainDb = 0.0;
127 double pan = 0.0;
128 int64_t startOffset = 0;
130 int64_t loopStart = 0;
131 int64_t loopEnd = 0;
132 int64_t loopCrossfade = 0;
135 };
136
137 Sampler() = default;
139 {
140 delete pending_.exchange(nullptr, std::memory_order_acq_rel);
141 delete retired_.exchange(nullptr, std::memory_order_acq_rel);
142 delete fading_;
143 delete active_;
144 }
145 Sampler(const Sampler&) = delete;
146 Sampler& operator=(const Sampler&) = delete;
147
148 // -- Lifecycle ---------------------------------------------------------------
149
156 void prepare(const AudioSpec& spec, int maxVoices = 32)
157 {
158 if (!(spec.sampleRate > 0.0) || !std::isfinite(spec.sampleRate) || spec.numChannels < 1)
159 return;
160 sampleRate_ = spec.sampleRate;
161 polyphony_ = std::clamp(maxVoices, 1, 256);
162 voices_.assign(static_cast<size_t>(polyphony_ + polyphony_ / 4 + 2), Voice {});
163 for (auto& v : voices_)
164 {
165 v.env.prepare(sampleRate_);
166 for (auto& a : v.acc) a.assign(static_cast<size_t>(kAccLength), T(0));
167 }
168 weights_.assign(static_cast<size_t>(2 * kMaxHalfTaps + 2), T(0));
169 (void)polyphaseTable(); // build the shared tables here, never on the audio thread
170 stealFadeSamples_ = std::max(1, static_cast<int>(std::lround(0.003 * sampleRate_)));
171 reset();
172 prepared_ = true;
173 }
174
176 void reset() noexcept
177 {
178 for (auto& v : voices_) { v.active = false; v.env.reset(); }
179 eventCount_ = 0;
180 sustainPedal_ = false;
181 pitchBend_ = 0.0;
182 activeVoices_.store(0, std::memory_order_relaxed);
183 }
184
185 // -- Zones (control thread) --------------------------------------------------
186
197 bool setZones(std::span<const ZoneSpec> zones)
198 {
199 reclaim();
200 auto set = std::make_unique<ZoneSet>();
201 set->zones.reserve(zones.size());
202 for (const ZoneSpec& z : zones)
203 {
204 ZoneData d;
205 if (!buildZone(z, d)) return false;
206 set->zones.push_back(std::move(d));
207 }
208 publish(set.release());
209 return true;
210 }
211
213 bool loadSample(AudioBufferView<const T> audio, double sampleRate, int rootKey = 60)
214 {
215 ZoneSpec z;
216 z.audio = audio;
217 z.sampleRate = sampleRate;
218 z.rootKey = rootKey;
219 return setZones(std::span<const ZoneSpec>(&z, 1));
220 }
221
223 void clearZones() { reclaim(); publish(new ZoneSet {}); }
224
225 // -- Parameters (any thread) --------------------------------------------------
226
229 void setEnvelope(T attackMs, T decayMs, T sustain, T releaseMs) noexcept
230 {
231 if (!std::isfinite(attackMs) || !std::isfinite(decayMs)
232 || !std::isfinite(sustain) || !std::isfinite(releaseMs)) return;
233 attackMs_.store(std::max(0.01, static_cast<double>(attackMs)), std::memory_order_relaxed);
234 decayMs_.store(std::max(0.01, static_cast<double>(decayMs)), std::memory_order_relaxed);
235 sustain_.store(std::clamp(static_cast<double>(sustain), 0.0, 1.0), std::memory_order_relaxed);
236 releaseMs_.store(std::max(0.01, static_cast<double>(releaseMs)), std::memory_order_relaxed);
237 }
238
241 void setVelocityRange(T rangeDb) noexcept
242 {
243 if (!std::isfinite(rangeDb)) return;
244 velocityRangeDb_.store(std::clamp(static_cast<double>(rangeDb), 0.0, 96.0),
245 std::memory_order_relaxed);
246 }
247
249 void setGain(T dB) noexcept
250 {
251 if (!std::isfinite(dB)) return;
252 outputGain_.store(std::pow(10.0, std::clamp(static_cast<double>(dB), -120.0, 24.0) / 20.0),
253 std::memory_order_relaxed);
254 }
255
256 // -- Events (audio thread) ----------------------------------------------------
257
260 void noteOn(int key, T velocity, int offset = 0) noexcept
261 {
262 if (!std::isfinite(velocity)) return;
263 if (velocity <= T(0)) { noteOff(key, offset); return; }
264 push({ EventType::NoteOn, std::clamp(key, 0, 127),
265 std::clamp(static_cast<double>(velocity), 0.0, 1.0), offset });
266 }
267
269 void noteOff(int key, int offset = 0) noexcept
270 {
271 push({ EventType::NoteOff, std::clamp(key, 0, 127), 0.0, offset });
272 }
273
275 void setSustainPedal(bool down, int offset = 0) noexcept
276 {
277 push({ EventType::Pedal, 0, down ? 1.0 : 0.0, offset });
278 }
279
281 void setPitchBend(T semitones, int offset = 0) noexcept
282 {
283 if (!std::isfinite(semitones)) return;
284 push({ EventType::Bend, 0, std::clamp(static_cast<double>(semitones), -48.0, 48.0), offset });
285 }
286
288 void allNotesOff(int offset = 0) noexcept
289 {
290 push({ EventType::AllOff, 0, 0.0, offset });
291 }
292
293 // -- Processing (audio thread) -------------------------------------------------
294
305 {
306 const int n = out.getNumSamples();
307 const int nCh = out.getNumChannels();
308 for (int c = 0; c < nCh; ++c)
309 std::fill_n(out.getChannel(c), n, T(0));
310 if (!prepared_ || n <= 0) { eventCount_ = 0; return; }
311
312 adoptPending();
313
314 int cursor = 0;
315 for (int e = 0; e < eventCount_; ++e)
316 {
317 const int at = std::clamp(events_[static_cast<size_t>(e)].offset, 0, n);
318 if (at > cursor) { render(out, cursor, at); cursor = at; }
319 apply(events_[static_cast<size_t>(e)]);
320 }
321 eventCount_ = 0;
322 if (cursor < n) render(out, cursor, n);
323
324 const double g = outputGain_.load(std::memory_order_relaxed);
325 if (g != 1.0)
326 for (int c = 0; c < std::min(nCh, 2); ++c)
327 {
328 T* d = out.getChannel(c);
329 for (int i = 0; i < n; ++i) d[i] = static_cast<T>(d[i] * g);
330 }
331
332 retireFadedSet();
333 int count = 0;
334 for (const auto& v : voices_) count += v.active ? 1 : 0;
335 activeVoices_.store(count, std::memory_order_relaxed);
336 }
337
339 [[nodiscard]] int getActiveVoiceCount() const noexcept
340 {
341 return activeVoices_.load(std::memory_order_relaxed);
342 }
343
345 [[nodiscard]] int getPolyphony() const noexcept { return polyphony_; }
346
347private:
348 // -- Interpolation kernel -------------------------------------------------------
349
351 static constexpr int kHalfTaps = 32;
353 static constexpr int kTablePerUnit = 2048;
355 static constexpr double kMaxRate = 8.0;
356 static constexpr int kMaxHalfTaps = static_cast<int>(kHalfTaps * kMaxRate);
359 static constexpr double kCutoff = 0.9;
360 static constexpr double kBeta = 10.06;
361
363 static const std::vector<double>& kernelTable()
364 {
365 static const std::vector<double> table = [] {
366 std::vector<double> t(static_cast<size_t>(kHalfTaps * kTablePerUnit + 2), 0.0);
367 auto i0 = [](double x) {
368 double sum = 1.0, term = 1.0;
369 for (int k = 1; k < 64; ++k)
370 {
371 term *= (x / (2.0 * k)) * (x / (2.0 * k));
372 sum += term;
373 if (term < sum * 1e-17) break;
374 }
375 return sum;
376 };
377 const double norm = i0(kBeta);
378 for (size_t j = 0; j + 1 < t.size(); ++j)
379 {
380 const double x = static_cast<double>(j) / kTablePerUnit;
381 if (x >= kHalfTaps) break;
382 const double arg = pi<double> * kCutoff * x;
383 const double sinc = (x == 0.0) ? 1.0 : std::sin(arg) / arg;
384 const double r = x / kHalfTaps;
385 t[j] = kCutoff * sinc * i0(kBeta * std::sqrt(std::max(0.0, 1.0 - r * r))) / norm;
386 }
387 return t;
388 }();
389 return table;
390 }
391
393 static constexpr int kPhases = 2048;
394 static constexpr int kTaps = 2 * kHalfTaps;
395
403 static const std::vector<T>& polyphaseTable()
404 {
405 static const std::vector<T> rows = [] {
406 const auto& h = kernelTable();
407 std::vector<T> r(static_cast<size_t>((kPhases + 1) * kTaps), T(0));
408 for (int q = 0; q <= kPhases; ++q)
409 for (int t = 0; t < kTaps; ++t)
410 {
411 // |k - frac| in table units, exactly: (t - 31) * P - q.
412 const long u = std::labs(static_cast<long>(t - (kHalfTaps - 1)) * kTablePerUnit - q);
413 r[static_cast<size_t>(q * kTaps + t)] =
414 (static_cast<size_t>(u) < h.size()) ? static_cast<T>(h[static_cast<size_t>(u)]) : T(0);
415 }
416 return r;
417 }();
418 return rows;
419 }
420
421 // -- Zones -----------------------------------------------------------------------
422
423 struct ZoneData
424 {
425 std::vector<T> audio;
426 std::vector<T> baked;
427 int channels = 0;
428 int64_t length = 0;
429 double sampleRate = 48000.0;
430 int rootKey = 60;
431 double tuneCents = 0.0;
432 int keyLow = 0, keyHigh = 127;
433 double velLow = 0.0, velHigh = 1.0;
434 double gain = 1.0;
435 double pan = 0.0;
436 int64_t start = 0;
438 int64_t loopStart = 0, loopEnd = 0, xfade = 0;
439 };
440
441 struct ZoneSet { std::vector<ZoneData> zones; };
442
443 static bool buildZone(const ZoneSpec& z, ZoneData& d)
444 {
445 const int ch = z.audio.getNumChannels();
446 const int64_t len = z.audio.getNumSamples();
447 if (ch < 1 || len < 1 || !(z.sampleRate > 0.0) || !std::isfinite(z.sampleRate))
448 return false;
449 for (int c = 0; c < ch; ++c)
450 if (z.audio.getChannel(c) == nullptr) return false;
451 if (z.keyLow > z.keyHigh || z.velocityLow > z.velocityHigh
452 || !std::isfinite(z.tuneCents) || !std::isfinite(z.gainDb) || !std::isfinite(z.pan))
453 return false;
454
455 d.channels = ch;
456 d.length = len;
457 d.sampleRate = z.sampleRate;
458 d.rootKey = z.rootKey;
459 d.tuneCents = z.tuneCents;
460 d.keyLow = std::clamp(z.keyLow, 0, 127);
461 d.keyHigh = std::clamp(z.keyHigh, 0, 127);
462 d.velLow = std::clamp(z.velocityLow, 0.0, 1.0);
463 d.velHigh = std::clamp(z.velocityHigh, 0.0, 1.0);
464 d.gain = std::pow(10.0, std::clamp(z.gainDb, -120.0, 24.0) / 20.0);
465 d.pan = std::clamp(z.pan, -1.0, 1.0);
466 d.start = std::clamp<int64_t>(z.startOffset, 0, len - 1);
467 d.audio.resize(static_cast<size_t>(ch) * static_cast<size_t>(len));
468 for (int c = 0; c < ch; ++c)
469 {
470 const T* src = z.audio.getChannel(c);
471 T* dst = d.audio.data() + static_cast<size_t>(c) * static_cast<size_t>(len);
472 for (int64_t i = 0; i < len; ++i)
473 dst[i] = std::isfinite(src[i]) ? src[i] : T(0);
474 }
475
476 d.loop = z.loopMode;
477 d.loopStart = std::clamp<int64_t>(z.loopStart, 0, len);
478 d.loopEnd = std::clamp<int64_t>(z.loopEnd, 0, len);
479 if (d.loop != LoopMode::None && d.loopEnd - d.loopStart < 2)
480 d.loop = LoopMode::None;
481 if (d.loop == LoopMode::None) return true;
482
483 d.xfade = std::clamp<int64_t>(z.loopCrossfade, 0,
484 std::min(d.loopStart, d.loopEnd - d.loopStart));
485 if (d.xfade > 0)
486 {
487 d.baked.resize(static_cast<size_t>(ch) * static_cast<size_t>(d.xfade));
488 for (int c = 0; c < ch; ++c)
489 {
490 const T* src = d.audio.data() + static_cast<size_t>(c) * static_cast<size_t>(len);
491 T* dst = d.baked.data() + static_cast<size_t>(c) * static_cast<size_t>(d.xfade);
492 for (int64_t i = 0; i < d.xfade; ++i)
493 {
494 // Fade the loop's tail out into the audio that precedes the
495 // loop start, so the jump lands on continuous material.
496 const double x = (static_cast<double>(i) + 0.5) / static_cast<double>(d.xfade);
497 const double fin = z.equalPowerCrossfade ? std::sin(0.5 * pi<double> * x) : x;
498 const double fout = z.equalPowerCrossfade ? std::cos(0.5 * pi<double> * x) : 1.0 - x;
499 const double tail = static_cast<double>(src[d.loopEnd - d.xfade + i]);
500 const double lead = static_cast<double>(src[d.loopStart - d.xfade + i]);
501 dst[i] = static_cast<T>(fout * tail + fin * lead);
502 }
503 }
504 }
505 return true;
506 }
507
508 // -- Publication -------------------------------------------------------------------
509
511 void reclaim() noexcept
512 {
513 delete retired_.exchange(nullptr, std::memory_order_acq_rel);
514 }
515
517 void publish(ZoneSet* set) noexcept
518 {
519 delete pending_.exchange(set, std::memory_order_acq_rel);
520 }
521
523 void adoptPending() noexcept
524 {
525 if (fading_ != nullptr) return; // one handover at a time
526 ZoneSet* next = pending_.exchange(nullptr, std::memory_order_acq_rel);
527 if (next == nullptr) return;
528 fading_ = active_;
529 active_ = next;
530 }
531
533 void retireFadedSet() noexcept
534 {
535 if (fading_ == nullptr) return;
536 for (const auto& v : voices_)
537 if (v.active && v.set == fading_) return;
538 if (retired_.load(std::memory_order_acquire) != nullptr) return;
539 retired_.store(fading_, std::memory_order_release);
540 fading_ = nullptr;
541 }
542
543 // -- Voices --------------------------------------------------------------------------
544
545 struct Voice
546 {
547 bool active = false;
548 const ZoneSet* set = nullptr;
549 const ZoneData* zone = nullptr;
550 int key = 0;
551 double pos = 0.0;
552 double baseRatio = 1.0;
553 bool loopEngaged = false;
554 bool releaseLoop = false;
555 bool held = true;
556 bool sustained = false;
557 double gainL = 1.0, gainR = 1.0, gainMono = 1.0;
558 int stealLeft = -1;
559 uint64_t age = 0;
560 ADSREnvelope<double> env;
561 // Scatter form: the next recording sample to spread, the output time
562 // it falls on (relative to the next output sample), and the output
563 // accumulators it is spread into.
564 bool scatter = false;
565 bool inputDone = false;
566 int drainLeft = 0;
567 int64_t jIn = 0;
568 double tNext = 0.0;
569 int accPos = 0;
570 std::array<std::vector<T>, 2> acc;
571 };
572
573 enum class EventType { NoteOn, NoteOff, Pedal, Bend, AllOff };
574 struct Event { EventType type; int key; double value; int offset; };
575
576 void push(const Event& e) noexcept
577 {
578 if (!prepared_ || eventCount_ >= kMaxEvents) return;
579 // Keep the queue ordered by offset; events with equal offsets keep
580 // their call order.
581 int i = eventCount_++;
582 while (i > 0 && events_[static_cast<size_t>(i - 1)].offset > e.offset)
583 {
584 events_[static_cast<size_t>(i)] = events_[static_cast<size_t>(i - 1)];
585 --i;
586 }
587 events_[static_cast<size_t>(i)] = e;
588 }
589
590 void apply(const Event& e) noexcept
591 {
592 switch (e.type)
593 {
594 case EventType::NoteOn: startNote(e.key, e.value); break;
595 case EventType::NoteOff: releaseKey(e.key); break;
596 case EventType::Bend: pitchBend_ = e.value; break;
597 case EventType::Pedal:
598 sustainPedal_ = e.value > 0.5;
599 if (!sustainPedal_)
600 for (auto& v : voices_)
601 if (v.active && v.sustained) release(v);
602 break;
603 case EventType::AllOff:
604 sustainPedal_ = false;
605 for (auto& v : voices_)
606 if (v.active && v.stealLeft < 0) release(v);
607 break;
608 }
609 }
610
611 void release(Voice& v) noexcept
612 {
613 v.held = false;
614 v.sustained = false;
615 v.env.setRelease(static_cast<double>(releaseMs_.load(std::memory_order_relaxed)));
616 v.env.noteOff();
617 if (v.zone->loop == LoopMode::Sustain) v.releaseLoop = true;
618 }
619
620 void releaseKey(int key) noexcept
621 {
622 for (auto& v : voices_)
623 if (v.active && v.held && v.key == key && v.stealLeft < 0)
624 {
625 if (sustainPedal_) { v.held = false; v.sustained = true; }
626 else release(v);
627 }
628 }
629
630 void startNote(int key, double velocity) noexcept
631 {
632 if (active_ == nullptr) return;
633 // A retriggered key releases what it was playing.
634 for (auto& v : voices_)
635 if (v.active && v.key == key && v.stealLeft < 0 && (v.held || v.sustained))
636 release(v);
637
638 const double velGain = std::pow(10.0, velocityRangeDb_.load(std::memory_order_relaxed)
639 * (velocity - 1.0) / 20.0);
640 for (const ZoneData& z : active_->zones)
641 {
642 if (key < z.keyLow || key > z.keyHigh || velocity < z.velLow || velocity > z.velHigh)
643 continue;
644 Voice* v = freeVoice();
645 if (v == nullptr) return;
646 v->active = true;
647 v->set = active_;
648 v->zone = &z;
649 v->key = key;
650 v->pos = static_cast<double>(z.start);
651 v->baseRatio = z.sampleRate / sampleRate_
652 * std::pow(2.0, (key - z.rootKey + z.tuneCents / 100.0) / 12.0);
653 v->scatter = v->baseRatio * std::pow(2.0, pitchBend_ / 12.0) > 1.0;
654 v->inputDone = false;
655 v->drainLeft = 0;
656 v->jIn = z.start;
657 v->tNext = 0.0;
658 v->accPos = 0;
659 if (v->scatter)
660 for (auto& a : v->acc) std::fill(a.begin(), a.end(), T(0));
661 v->loopEngaged = z.loop != LoopMode::None;
662 v->releaseLoop = false;
663 v->held = true;
664 v->sustained = false;
665 v->stealLeft = -1;
666 v->age = ++clock_;
667 const double g = z.gain * velGain;
668 const double theta = 0.25 * pi<double> * (z.pan + 1.0);
669 v->gainMono = g;
670 if (z.channels == 1)
671 {
672 // Constant power, unity at centre.
673 v->gainL = g * std::cos(theta) * sqrt2<double>;
674 v->gainR = g * std::sin(theta) * sqrt2<double>;
675 }
676 else
677 {
678 v->gainL = g * std::min(1.0, 1.0 - z.pan);
679 v->gainR = g * std::min(1.0, 1.0 + z.pan);
680 }
681 v->env.setParameters(attackMs_.load(std::memory_order_relaxed),
682 decayMs_.load(std::memory_order_relaxed),
683 sustain_.load(std::memory_order_relaxed),
684 releaseMs_.load(std::memory_order_relaxed));
685 v->env.reset();
686 v->env.noteOn();
687 }
688 }
689
692 Voice* freeVoice() noexcept
693 {
694 int sounding = 0;
695 Voice* idle = nullptr;
696 for (auto& v : voices_)
697 {
698 if (!v.active) { if (idle == nullptr) idle = &v; }
699 else if (v.stealLeft < 0) ++sounding;
700 }
701 if (sounding < polyphony_ && idle != nullptr) return idle;
702
703 // Steal: the oldest released voice, else the quietest, else the oldest.
704 Voice* victim = nullptr;
705 for (auto& v : voices_)
706 {
707 if (!v.active || v.stealLeft >= 0) continue;
708 if (victim == nullptr) { victim = &v; continue; }
709 const bool vr = !v.held && !v.sustained, cr = !victim->held && !victim->sustained;
710 if (vr != cr) { if (vr) victim = &v; continue; }
711 const double va = v.env.getCurrentValue(), ca = victim->env.getCurrentValue();
712 if (va < ca - 1e-9 || (std::abs(va - ca) <= 1e-9 && v.age < victim->age)) victim = &v;
713 }
714 if (victim != nullptr) victim->stealLeft = stealFadeSamples_;
715 if (idle != nullptr) return idle;
716 // No spare slot left: the oldest stealing fade is cut short.
717 Voice* oldest = nullptr;
718 for (auto& v : voices_)
719 if (v.active && v.stealLeft >= 0 && (oldest == nullptr || v.age < oldest->age)) oldest = &v;
720 if (oldest != nullptr) oldest->active = false;
721 return oldest;
722 }
723
724 // -- Rendering --------------------------------------------------------------------------
725
728 [[nodiscard]] static double dot(const T* w, const T* x, int n) noexcept
729 {
730 T a[8] = { T(0), T(0), T(0), T(0), T(0), T(0), T(0), T(0) };
731 int t = 0;
732 for (; t + 8 <= n; t += 8)
733 for (int l = 0; l < 8; ++l)
734 a[l] += w[t + l] * x[t + l];
735 T sum = ((a[0] + a[4]) + (a[1] + a[5])) + ((a[2] + a[6]) + (a[3] + a[7]));
736 for (; t < n; ++t) sum += w[t] * x[t];
737 return static_cast<double>(sum);
738 }
739
741 [[nodiscard]] static double fetch(const ZoneData& z, const T* chan, const T* baked,
742 int64_t j, bool loopEngaged) noexcept
743 {
744 if (loopEngaged && j >= z.loopEnd)
745 j = z.loopStart + (j - z.loopEnd) % (z.loopEnd - z.loopStart);
746 if (j < 0 || j >= z.length) return 0.0;
747 if (loopEngaged && baked != nullptr && j >= z.loopEnd - z.xfade && j < z.loopEnd)
748 return static_cast<double>(baked[j - (z.loopEnd - z.xfade)]);
749 return static_cast<double>(chan[j]);
750 }
751
754 int kernelWeights(double frac, double fc, int half) noexcept
755 {
756 if (fc >= 1.0)
757 {
758 const T* rows = polyphaseTable().data();
759 const double qf = frac * kPhases;
760 const int q = std::min(static_cast<int>(qf), kPhases - 1);
761 const T g = static_cast<T>(qf - static_cast<double>(q));
762 const T* r0 = rows + static_cast<size_t>(q) * kTaps;
763 const T* r1 = r0 + kTaps;
764 T* w = weights_.data();
765 for (int t = 0; t < kTaps; ++t)
766 w[t] = r0[t] + g * (r1[t] - r0[t]);
767 return kTaps;
768 }
769 // Stretched kernel: |k - frac| * fc in table units, one step of fc per tap.
770 const auto& table = kernelTable();
771 const double step = fc * kTablePerUnit;
772 double x = (static_cast<double>(-half + 1) - frac) * step;
773 const size_t last = table.size() - 1;
774 for (int t = 0; t < 2 * half; ++t, x += step)
775 {
776 const double u = std::abs(x);
777 const size_t iu = static_cast<size_t>(u);
778 double w = 0.0;
779 if (iu < last)
780 {
781 const double f = u - static_cast<double>(iu);
782 w = table[iu] + f * (table[iu + 1] - table[iu]);
783 }
784 weights_[static_cast<size_t>(t)] = static_cast<T>(w * fc);
785 }
786 return 2 * half;
787 }
788
791 static bool advanceGain(Voice& v, int stealFade, double& gain) noexcept
792 {
793 gain = v.env.getNextValue();
794 if (v.stealLeft >= 0)
795 {
796 gain *= static_cast<double>(v.stealLeft) / static_cast<double>(stealFade);
797 if (--v.stealLeft < 0) { v.active = false; return false; }
798 }
799 if (!v.env.isActive()) { v.active = false; return false; }
800 return true;
801 }
802
803 void emit(AudioBufferView<T> out, int s, const Voice& v, double a0, double a1, double gain) noexcept
804 {
805 const int nCh = out.getNumChannels();
806 if (nCh == 1)
807 out.getChannel(0)[s] += static_cast<T>(a0 * v.gainMono * gain);
808 else
809 {
810 out.getChannel(0)[s] += static_cast<T>(a0 * v.gainL * gain);
811 out.getChannel(1)[s] += static_cast<T>((v.zone->channels == 1 ? a0 : a1) * v.gainR * gain);
812 }
813 }
814
815 void render(AudioBufferView<T> out, int from, int to) noexcept
816 {
817 const double bendRatio = std::pow(2.0, pitchBend_ / 12.0);
818 for (auto& v : voices_)
819 {
820 if (!v.active) continue;
821 const double ratio = std::clamp(v.baseRatio * bendRatio, 1.0 / kMaxRate, kMaxRate);
822 if (v.scatter) renderScatter(v, out, from, to, ratio);
823 else renderGather(v, out, from, to, ratio);
824 }
825 }
826
833 void renderGather(Voice& v, AudioBufferView<T> out, int from, int to, double ratio) noexcept
834 {
835 const ZoneData& z = *v.zone;
836 const int nCh = out.getNumChannels();
837 const double fc = std::min(1.0, 1.0 / ratio);
838 const int half = static_cast<int>(std::ceil(kHalfTaps / fc));
839 const double loopLen = static_cast<double>(z.loopEnd - z.loopStart);
840 const int zc = z.channels;
841 const int used = (nCh == 1) ? zc : std::min(zc, 2);
842
843 for (int s = from; s < to; ++s)
844 {
845 // Sustain loop released: leave the loop once the kernel is clear
846 // of the crossfade region, so no read jumps sources.
847 if (v.releaseLoop && v.pos + half < static_cast<double>(z.loopEnd - z.xfade))
848 {
849 v.loopEngaged = false;
850 v.releaseLoop = false;
851 }
852 if (!v.loopEngaged && v.pos >= static_cast<double>(z.length)) { v.active = false; return; }
853
854 const double fl = std::floor(v.pos);
855 const int64_t ip = static_cast<int64_t>(fl);
856 const int taps = kernelWeights(v.pos - fl, fc, half);
857
858 double gain = 0.0;
859 if (!advanceGain(v, stealFadeSamples_, gain)) return;
860
861 const int64_t first = ip - half + 1;
862 const bool direct = first >= 0 && first + taps <= z.length
863 && (!v.loopEngaged || first + taps <= z.loopEnd - z.xfade);
864 double acc[2] = { 0.0, 0.0 };
865 for (int c = 0; c < used; ++c)
866 {
867 const T* chan = z.audio.data() + static_cast<size_t>(c) * static_cast<size_t>(z.length);
868 const T* baked = z.baked.empty() ? nullptr
869 : z.baked.data() + static_cast<size_t>(c) * static_cast<size_t>(z.xfade);
870 double sum = 0.0;
871 if (direct)
872 sum = dot(weights_.data(), chan + first, taps);
873 else
874 for (int t = 0; t < taps; ++t)
875 sum += static_cast<double>(weights_[static_cast<size_t>(t)])
876 * fetch(z, chan, baked, first + t, v.loopEngaged);
877 if (nCh == 1) acc[0] += sum / zc;
878 else acc[c] = sum;
879 }
880 emit(out, s, v, acc[0], acc[1], gain);
881
882 v.pos += ratio;
883 if (v.loopEngaged && v.pos >= static_cast<double>(z.loopEnd))
884 v.pos -= loopLen;
885 }
886 }
887
900 void renderScatter(Voice& v, AudioBufferView<T> out, int from, int to, double ratio) noexcept
901 {
902 const ZoneData& z = *v.zone;
903 const int nCh = out.getNumChannels();
904 const double fc = std::min(1.0, ratio); // output-time kernel scale
905 const int half = static_cast<int>(std::ceil(kHalfTaps / fc));
906 const double spacing = 1.0 / ratio; // output samples per input sample
907 const int zc = z.channels;
908 const int used = (nCh == 1) ? zc : std::min(zc, 2);
909 const int rings = (nCh == 1 || zc == 1) ? 1 : 2;
910
911 for (int s = from; s < to; ++s)
912 {
913 // Spread every input whose support reaches this output sample.
914 while (!v.inputDone && v.tNext < static_cast<double>(half))
915 {
916 if (v.releaseLoop && v.jIn < z.loopEnd - z.xfade)
917 {
918 v.loopEngaged = false;
919 v.releaseLoop = false;
920 }
921 double x[2] = { 0.0, 0.0 };
922 for (int c = 0; c < used; ++c)
923 {
924 const T* chan = z.audio.data() + static_cast<size_t>(c) * static_cast<size_t>(z.length);
925 const T* baked = z.baked.empty() ? nullptr
926 : z.baked.data() + static_cast<size_t>(c) * static_cast<size_t>(z.xfade);
927 const double sample = fetch(z, chan, baked, v.jIn, v.loopEngaged);
928 if (rings == 1) x[0] += sample / used;
929 else x[c] = sample;
930 }
931 const double fl = std::floor(v.tNext);
932 const int taps = kernelWeights(v.tNext - fl, fc, half);
933 const int base = v.accPos + static_cast<int>(fl) - half + 1;
934 for (int r = 0; r < rings; ++r)
935 {
936 const T xs = static_cast<T>(x[r] * spacing);
937 T* acc = v.acc[static_cast<size_t>(r)].data();
938 const T* w = weights_.data();
939 for (int t = 0; t < taps; ++t)
940 {
941 const int idx = base + t;
942 if (idx >= v.accPos) acc[idx] += w[t] * xs; // earlier outputs are gone
943 }
944 }
945 ++v.jIn;
946 if (v.loopEngaged && v.jIn >= z.loopEnd) v.jIn = z.loopStart;
947 if (!v.loopEngaged && v.jIn >= z.length) { v.inputDone = true; v.drainLeft = 2 * half; }
948 v.tNext += spacing;
949 }
950
951 double gain = 0.0;
952 if (!advanceGain(v, stealFadeSamples_, gain)) return;
953 const double a0 = static_cast<double>(v.acc[0][static_cast<size_t>(v.accPos)]);
954 const double a1 = (rings == 2) ? static_cast<double>(v.acc[1][static_cast<size_t>(v.accPos)]) : a0;
955 emit(out, s, v, a0, a1, gain);
956
957 // Advance the output: the accumulator slides once it nears its end.
958 for (int r = 0; r < rings; ++r) v.acc[static_cast<size_t>(r)][static_cast<size_t>(v.accPos)] = T(0);
959 ++v.accPos;
960 v.tNext -= 1.0;
961 if (v.accPos + 2 * kMaxHalfTaps + 2 >= kAccLength)
962 {
963 const int keep = kAccLength - v.accPos;
964 for (int r = 0; r < rings; ++r)
965 {
966 T* acc = v.acc[static_cast<size_t>(r)].data();
967 std::copy(acc + v.accPos, acc + kAccLength, acc);
968 std::fill(acc + keep, acc + kAccLength, T(0));
969 }
970 v.accPos = 0;
971 }
972 if (v.inputDone && --v.drainLeft <= 0) { v.active = false; return; }
973 }
974 }
975
976 // -- Members ------------------------------------------------------------------------------
977
978 static constexpr int kMaxEvents = 1024;
981 static constexpr int kAccLength = 4 * kMaxHalfTaps + 256;
982
983 double sampleRate_ = 48000.0;
984 int polyphony_ = 0;
985 bool prepared_ = false;
986 int stealFadeSamples_ = 144;
987 uint64_t clock_ = 0;
988
989 std::vector<Voice> voices_;
990 std::vector<T> weights_;
991 std::array<Event, kMaxEvents> events_ {};
992 int eventCount_ = 0;
993 bool sustainPedal_ = false;
994 double pitchBend_ = 0.0;
995
996 ZoneSet* active_ = nullptr;
997 ZoneSet* fading_ = nullptr;
998 std::atomic<ZoneSet*> pending_ { nullptr };
999 std::atomic<ZoneSet*> retired_ { nullptr };
1000
1001 std::atomic<double> attackMs_ { 1.0 };
1002 std::atomic<double> decayMs_ { 100.0 };
1003 std::atomic<double> sustain_ { 1.0 };
1004 std::atomic<double> releaseMs_ { 100.0 };
1005 std::atomic<double> velocityRangeDb_ { 24.0 };
1006 std::atomic<double> outputGain_ { 1.0 };
1007 std::atomic<int> activeVoices_ { 0 };
1008};
1009
1010} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Polyphonic multi-zone sample player.
Definition Sampler.h:105
Sampler()=default
void allNotesOff(int offset=0) noexcept
Releases every sounding note (and lifts the pedal).
Definition Sampler.h:288
int getActiveVoiceCount() const noexcept
Voices sounding after the last block, stealing fades included.
Definition Sampler.h:339
int getPolyphony() const noexcept
Polyphony set by prepare().
Definition Sampler.h:345
void clearZones()
Publishes an empty zone set: sounding voices finish, new notes are silent.
Definition Sampler.h:223
void processBlock(AudioBufferView< T > out) noexcept
Renders the block, overwriting out, and applies the queued events at their offsets....
Definition Sampler.h:304
void reset() noexcept
Silences every voice and drops queued events. Audio thread.
Definition Sampler.h:176
void setVelocityRange(T rangeDb) noexcept
Velocity sensitivity: velocity 0 plays rangeDb below velocity 1 (0..96 dB, default 24; 0 ignores velo...
Definition Sampler.h:241
void setSustainPedal(bool down, int offset=0) noexcept
Sustain pedal: while down, released keys keep sounding.
Definition Sampler.h:275
Sampler & operator=(const Sampler &)=delete
Sampler(const Sampler &)=delete
void noteOff(int key, int offset=0) noexcept
Releases a note at offset samples into the next block.
Definition Sampler.h:269
void setGain(T dB) noexcept
Output gain in dB.
Definition Sampler.h:249
void noteOn(int key, T velocity, int offset=0) noexcept
Starts a note at offset samples into the next block. Velocity 0 is a note-off, as in MIDI.
Definition Sampler.h:260
bool loadSample(AudioBufferView< const T > audio, double sampleRate, int rootKey=60)
One zone over the whole keyboard and velocity range.
Definition Sampler.h:213
void setPitchBend(T semitones, int offset=0) noexcept
Pitch bend in semitones (-48..48), applied to every voice.
Definition Sampler.h:281
LoopMode
How a zone loops.
Definition Sampler.h:109
@ Continuous
Loop for as long as the voice sounds, release included.
@ None
Play once to the end of the recording.
@ Sustain
Loop while the key is held, then play on past the loop end.
bool setZones(std::span< const ZoneSpec > zones)
Replaces the zone set.
Definition Sampler.h:197
void prepare(const AudioSpec &spec, int maxVoices=32)
Allocates the voices and the event queue.
Definition Sampler.h:156
void setEnvelope(T attackMs, T decayMs, T sustain, T releaseMs) noexcept
ADSR for notes that start from now on (release: also for notes released from now on)....
Definition Sampler.h:229
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
One zone, as the caller describes it. The audio is copied.
Definition Sampler.h:117
int64_t loopEnd
One past the last sample of the loop.
Definition Sampler.h:131
double sampleRate
Its sample rate.
Definition Sampler.h:119
int64_t loopCrossfade
Crossfade length in samples (0 = hard loop).
Definition Sampler.h:132
double velocityHigh
Highest velocity it answers, 0..1 (inclusive).
Definition Sampler.h:125
int rootKey
MIDI key that plays it untransposed.
Definition Sampler.h:120
int64_t startOffset
First sample played.
Definition Sampler.h:128
int keyLow
Lowest key it answers (inclusive).
Definition Sampler.h:122
AudioBufferView< const T > audio
The recording (1 or more channels).
Definition Sampler.h:118
double gainDb
Zone gain.
Definition Sampler.h:126
double tuneCents
Fine tuning added to every note.
Definition Sampler.h:121
int64_t loopStart
First sample of the loop.
Definition Sampler.h:130
double pan
-1 left .. +1 right (balance for multichannel).
Definition Sampler.h:127
int keyHigh
Highest key it answers (inclusive).
Definition Sampler.h:123
double velocityLow
Lowest velocity it answers, 0..1 (inclusive).
Definition Sampler.h:124