Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
additive_bank.hpp
Go to the documentation of this file.
1#pragma once
2
202
208#include <pulp/signal/rng.hpp>
209#include <pulp/signal/units.hpp>
210
211#include <algorithm>
212#include <array>
213#include <cmath>
214#include <cstdint>
215#include <type_traits>
216#include <vector>
217
218namespace pulp::signal {
219
223 double ratio = 1.0;
224 double amp = 1.0;
225 double phase01 = 0.0;
226 double decay_ms = 0.0;
228};
229
232 static constexpr int kMaxPartials = 256;
233
239 bool harmonic = true;
240 int count = 0;
241 std::array<VoicePartial, kMaxPartials> partials{};
242
243 void clear() { count = 0; }
244 bool add(const VoicePartial& p) {
245 if (count >= kMaxPartials) return false;
246 partials[static_cast<std::size_t>(count++)] = p;
247 return true;
248 }
249};
250
264inline VoiceTable make_organ_voice(int count = 64) {
265 static constexpr double kDrawbarRatio[9] = {0.5, 1.0, 1.5, 2.0, 3.0,
266 4.0, 5.0, 6.0, 8.0};
267 static constexpr double kDrawbarAmp[9] = {0.50, 1.00, 0.35, 0.70, 0.45,
268 0.55, 0.25, 0.20, 0.30};
271 constexpr double kTailLevel = 0.30;
272
273 VoiceTable v;
274 v.harmonic = true;
275 const int n = std::clamp(count, 1, VoiceTable::kMaxPartials);
276 for (int p = 0; p < n; ++p) {
277 VoicePartial row;
278 if (p < 9) {
279 row.ratio = kDrawbarRatio[p];
280 row.amp = kDrawbarAmp[p];
281 } else {
282 // −12 dB/oct is amplitude ∝ 1/ratio², which is what the exponent 2
283 // here means; writing it as a power of the ratio keeps the tail
284 // scale-invariant rather than tied to a partial index.
285 row.ratio = static_cast<double>(p + 1);
286 row.amp = kTailLevel / (row.ratio * row.ratio);
287 }
288 row.phase01 = 0.0;
289 row.decay_ms = 0.0; // sustained
290 v.add(row);
291 }
292 return v;
293}
294
309inline VoiceTable make_bell_voice(int count = 64) {
310 static constexpr double kModeRatio[10] = {0.50, 1.00, 1.19, 1.50, 2.00,
311 2.50, 2.66, 3.00, 4.00, 5.00};
312 static constexpr double kModeAmp[10] = {0.60, 1.00, 0.70, 0.55, 0.80,
313 0.35, 0.30, 0.40, 0.30, 0.20};
314 static constexpr double kModeDecayMs[10] = {8000, 6000, 4000, 3000, 2500,
315 1500, 1400, 1200, 800, 500};
318 constexpr double kClusterRatioStep = 1.14;
322 constexpr double kClusterLevel = 0.20;
323 constexpr double kClusterAmpExponent = 1.5;
324 constexpr double kClusterDecayScaleMs = 3000.0;
325
326 VoiceTable v;
327 v.harmonic = false; // modal: the ratios are the measurement
328 const int n = std::clamp(count, 1, VoiceTable::kMaxPartials);
329 for (int p = 0; p < n; ++p) {
330 VoicePartial row;
331 if (p < 10) {
332 row.ratio = kModeRatio[p];
333 row.amp = kModeAmp[p];
334 row.decay_ms = kModeDecayMs[p];
335 } else {
336 row.ratio = 5.0 * std::pow(kClusterRatioStep,
337 static_cast<double>(p - 9));
338 row.amp = kClusterLevel * std::pow(1.0 / row.ratio,
339 kClusterAmpExponent);
340 row.decay_ms = kClusterDecayScaleMs / row.ratio;
341 }
342 row.phase01 = 0.0;
343 v.add(row);
344 }
345 return v;
346}
347
349template <typename SampleType = float>
351public:
353 enum class RetrigPhase : std::uint8_t {
362 };
363
365 enum class EnvelopeMode : std::uint8_t {
373 };
374
375 // ── Calibration constants ─────────────────────────────────────────────
376
378 static constexpr int kMaxPartialsDefault = 128;
379 static constexpr int kPartialCountDefault = 64;
380
381 static constexpr double kFundamentalMinHz = 20.0;
382 static constexpr double kFundamentalMaxHz = 8000.0;
383 static constexpr double kFundamentalDefaultHz = 220.0;
384
385 static constexpr double kInharmonicityDefault = 0.0;
387 static constexpr double kInharmonicityCitedMax = 0.02;
390 static constexpr double kInharmonicityMax = 0.05;
392 static constexpr double kBPianoTenor = 0.0002;
393 static constexpr double kBPianoMid = 0.0008;
394 static constexpr double kBUpperTreble = 0.004;
395 static constexpr double kBElectricPianoTine = 0.02;
396
397 static constexpr double kSpectralTiltMinDbOct = -24.0;
398 static constexpr double kSpectralTiltMaxDbOct = 6.0;
399 static constexpr double kSpectralTiltDefaultDbOct = -6.0;
400
401 static constexpr double kMasterGainMinDb = -60.0;
402 static constexpr double kMasterGainMaxDb = 6.0;
403 static constexpr double kMasterGainDefaultDb = -6.0;
404
405 static constexpr double kAttackDefaultMs = 5.0;
406 static constexpr double kAttackMinMs = 0.1;
407 static constexpr double kAttackMaxMs = 2000.0;
408 static constexpr double kReleaseDefaultMs = 400.0;
409 static constexpr double kReleaseMinMs = 1.0;
410 static constexpr double kReleaseMaxMs = 20000.0;
411
412 static constexpr double kDetuneDefaultCents = 0.0;
413 static constexpr double kDetuneMaxCents = 50.0;
419 static constexpr double kDoubletJitterSpread = 0.5;
420
424 static constexpr int kAmpUpdateCadenceSamples = 32;
425
428 static constexpr double kNyquistGuardBandHz = 1000.0;
429
432 static constexpr double kEnvelopeCurve = 0.5;
433
434 static constexpr double kTwoPi = 6.283185307179586476925286766559;
435
436 // ── Static laws (shipped, so tests compute expectations from them) ────
437
445 static double partial_frequency_hz(double f0_hz, double ratio,
446 double inharmonicity_b) {
447 return f0_hz * ratio *
448 std::sqrt(1.0 + inharmonicity_b * ratio * ratio);
449 }
450
454 static double nyquist_guard_gain(double f_hz, double sample_rate) {
455 const double nyquist = 0.5 * sample_rate;
456 const double taper_start = nyquist - kNyquistGuardBandHz;
457 if (f_hz <= taper_start) return 1.0;
458 if (f_hz >= nyquist) return 0.0;
459 const double t = (f_hz - taper_start) / kNyquistGuardBandHz;
460 return 0.5 * (1.0 + std::cos(kPi() * t));
461 }
462
467 static double worst_case_gain() {
469 }
470
472 static constexpr int latency_samples() { return 0; }
473
474 // ── Lifecycle ─────────────────────────────────────────────────────────
475
477
481 void prepare(double sample_rate, int max_partials = kMaxPartialsDefault) {
482 if (std::isfinite(sample_rate) && sample_rate > 0.0) sample_rate_ = sample_rate;
483 max_partials_ = std::clamp(max_partials, 1, kMaxPartialsCeiling);
484 partial_count_ = std::min(partial_count_, max_partials_);
485
486 // Two slots per partial: a doublet is a genuine pair of oscillators,
487 // not a modulation trick, so the storage is sized for it up front and
488 // `prepare` stays the only allocation point whether or not the user
489 // later engages the detune.
490 const auto slots = static_cast<std::size_t>(2 * max_partials_);
491 phase_.resize(slots);
492 gain_.assign(slots, 0.0);
493 gain_step_.assign(slots, 0.0);
494 increment_.assign(slots, 0.0);
495 increment_step_.assign(slots, 0.0);
496 decay_env_.assign(slots, 1.0);
497 decay_coef_.assign(slots, 1.0);
498 slot_detune_.assign(slots, 1.0);
499 render_slots_.assign(slots, 0);
500
501 onset_.prepare(sample_rate_);
502 onset_.set_curve(kEnvelopeCurve);
503 refresh_onset();
504 reset();
505 }
506
509 void reset() {
510 rng_.reset();
511 onset_.reset();
512 for (auto& p : phase_) p.reset(0.0);
513 std::fill(decay_env_.begin(), decay_env_.end(), 1.0);
514 std::fill(gain_.begin(), gain_.end(), 0.0);
515 std::fill(gain_step_.begin(), gain_step_.end(), 0.0);
516 std::fill(increment_step_.begin(), increment_step_.end(), 0.0);
517 draw_doublet_jitter();
518 apply_stored_phases();
519 cadence_countdown_ = 0;
520 update_control_block(true);
521 }
522
523 // ── Global voice ──────────────────────────────────────────────────────
524
525 void set_fundamental_hz(double f0) {
526 if (!std::isfinite(f0)) return;
527 fundamental_hz_ = std::clamp(f0, kFundamentalMinHz, kFundamentalMaxHz);
528 }
529 double fundamental_hz() const { return fundamental_hz_; }
530
531 void set_partial_count(int n) {
532 // `std::max(1, ...)` is not defensive padding: an empty voice table
533 // would otherwise hand `std::clamp` a hi below its lo, which is UB.
534 partial_count_ =
535 std::clamp(n, 1, std::max(1, std::min(max_partials_, voice_.count)));
536 }
537 int partial_count() const { return partial_count_; }
538 int max_partials() const { return max_partials_; }
539
542 void set_inharmonicity_b(double b) {
543 if (!std::isfinite(b)) return;
544 inharmonicity_b_ = std::clamp(b, 0.0, kInharmonicityMax);
545 }
546 double inharmonicity_b() const { return inharmonicity_b_; }
547
549 if (!std::isfinite(s)) return;
550 tilt_db_oct_ = std::clamp(s, kSpectralTiltMinDbOct, kSpectralTiltMaxDbOct);
551 }
552 double spectral_tilt_db_oct() const { return tilt_db_oct_; }
553
554 void set_master_gain_db(double g) {
555 if (!std::isfinite(g)) return;
556 master_gain_db_ = std::clamp(g, kMasterGainMinDb, kMasterGainMaxDb);
557 master_linear_ = units::db_to_linear(master_gain_db_);
558 }
559 double master_gain_db() const { return master_gain_db_; }
560
564 bool set_trig_profile(FastTrigProfile profile) noexcept {
565 if (profile != FastTrigProfile::reference &&
567 return false;
568 if constexpr (!std::is_same_v<SampleType, float>) {
569 if (profile != FastTrigProfile::reference) return false;
570 }
571 trig_profile_ = profile;
572 return true;
573 }
574 FastTrigProfile trig_profile() const noexcept { return trig_profile_; }
575
576 static constexpr bool trig_profile_has_vector_path(FastTrigProfile profile) {
577#if defined(__APPLE__) && defined(__clang__) && defined(__aarch64__)
578 return std::is_same_v<SampleType, float> &&
580#else
581 (void)profile;
582 return false;
583#endif
584 }
585
586 // ── The partial table ─────────────────────────────────────────────────
587
588 void load_voice(const VoiceTable& v) {
589 if (v.count < 0 || v.count > VoiceTable::kMaxPartials) return;
590 for (int p = 0; p < v.count; ++p) {
591 const auto& row = v.partials[static_cast<std::size_t>(p)];
592 if (!std::isfinite(row.ratio) || !std::isfinite(row.amp) ||
593 !std::isfinite(row.phase01) || !std::isfinite(row.decay_ms))
594 return;
595 }
596 voice_ = v;
597 partial_count_ = std::clamp(partial_count_, 1,
598 std::max(1, std::min(max_partials_, voice_.count)));
599 }
600 const VoiceTable& voice() const { return voice_; }
601
604 void set_partial(int p, double ratio, double amp_lin, double phase01,
605 double decay_ms) {
606 if (p < 0 || p >= VoiceTable::kMaxPartials) return;
607 if (!std::isfinite(ratio) || !std::isfinite(amp_lin) ||
608 !std::isfinite(phase01) || !std::isfinite(decay_ms))
609 return;
610 voice_.partials[static_cast<std::size_t>(p)] = {ratio, amp_lin, phase01,
611 decay_ms};
612 voice_.count = std::max(voice_.count, p + 1);
613 }
614
615 // ── Spectral envelope and morph ───────────────────────────────────────
616
617 void set_envelope_a(const SpectralEnvelope& e) { envelope_a_ = e; }
618 void set_envelope_b(const SpectralEnvelope& e) { envelope_b_ = e; }
619
624 void set_morph(float m) {
625 if (!std::isfinite(static_cast<double>(m))) return;
626 morph_ = std::clamp(static_cast<double>(m), 0.0, 1.0);
627 }
628 double morph() const { return morph_; }
629
630 void set_spectral_domain(SpectralDomain d) { domain_ = d; }
631 SpectralDomain spectral_domain() const { return domain_; }
632
636 double envelope_db_at(double f_hz) const {
637 const double abscissa = domain_ == SpectralDomain::absolute_hz
638 ? f_hz
639 : f_hz / fundamental_hz_;
640 const double a = envelope_a_.gain_db_at(abscissa);
641 const double b = envelope_b_.gain_db_at(abscissa);
642 return (1.0 - morph_) * a + morph_ * b +
643 tilt_db_oct_ * std::log2(f_hz / fundamental_hz_);
644 }
645
646 // ── Envelope and trajectory ───────────────────────────────────────────
647
648 void set_envelope_mode(EnvelopeMode mode) { envelope_mode_ = mode; }
649 EnvelopeMode envelope_mode() const { return envelope_mode_; }
650
651 void set_attack_ms(double ms) {
652 if (!std::isfinite(ms)) return;
653 // Zero is the explicit instantaneous-envelope mode used by the raw
654 // oscillator/test surface. Non-zero values obey the published catalog
655 // range, including its reachable 0.1 ms lower endpoint.
656 attack_ms_ = ms == 0.0 ? 0.0 : std::clamp(ms, kAttackMinMs, kAttackMaxMs);
657 refresh_onset();
658 }
659 void set_release_ms(double ms) {
660 if (!std::isfinite(ms)) return;
661 // As above, exact zero is a deliberate instantaneous internal mode;
662 // all non-zero requests obey the declared 1 ms minimum.
663 release_ms_ = ms == 0.0 ? 0.0 : std::clamp(ms, kReleaseMinMs, kReleaseMaxMs);
664 refresh_onset();
665 }
666
669 void set_detune_cents(double cents) {
670 if (!std::isfinite(cents)) return;
671 detune_cents_ = std::clamp(cents, 0.0, kDetuneMaxCents);
672 }
673 double detune_cents() const { return detune_cents_; }
674 bool doublet_active() const { return detune_cents_ > 0.0; }
675
679 void set_pitch_glide(double start_cents, double time_ms) {
680 if (!std::isfinite(start_cents) || !std::isfinite(time_ms)) return;
681 glide_start_cents_ = start_cents;
682 glide_time_ms_ = std::max(time_ms, 0.0);
683 }
684
685 void set_retrig_phase(RetrigPhase p) { retrig_phase_ = p; }
686 RetrigPhase retrig_phase() const { return retrig_phase_; }
687
690 void set_seed(std::uint32_t seed) {
691 rng_.set_seed(seed);
692 draw_doublet_jitter();
693 }
694
697 void retrigger() {
698 switch (retrig_phase_) {
700 apply_stored_phases();
701 break;
703 break;
705 for (int p = 0; p < partial_count_; ++p)
706 for (int k = 0; k < 2; ++k)
707 phase_[slot(p, k)].reset(rng_.next_unit<double>());
708 break;
709 }
710 std::fill(decay_env_.begin(), decay_env_.end(), 1.0);
711 glide_position_ = 0.0;
712 onset_.gate_on();
713 update_control_block(true);
714 }
715
719 void release() { onset_.gate_off(); }
720
721 bool active() const { return onset_.active(); }
722
727 double partial_frequency(int p) const {
728 if (p < 0 || p >= partial_count_) return 0.0;
729 return increment_[slot(p, 0)] * sample_rate_;
730 }
731
732 // ── Render ────────────────────────────────────────────────────────────
733
734 SampleType next() {
735 SampleType out{};
736 process(&out, 1);
737 return out;
738 }
739
740 void process(SampleType* out, int n) {
741 switch (trig_profile_) {
743 process_with_trig<FastTrigProfile::realtime_precise>(out, n);
744 return;
746 default:
747 process_with_trig<FastTrigProfile::reference>(out, n);
748 return;
749 }
750 }
751
752private:
753 template <FastTrigProfile Profile>
754 void process_with_trig(SampleType* out, int n) {
755 for (int i = 0; i < n; ++i) {
756 if (cadence_countdown_ <= 0) update_control_block(false);
757 --cadence_countdown_;
758
759 double sum = 0.0;
760#if defined(__APPLE__) && defined(__clang__) && defined(__aarch64__)
761 if constexpr (std::is_same_v<SampleType, float> &&
763 sum = render_precise_simd_sample();
764#endif
765 constexpr bool rendered_simd =
766#if defined(__APPLE__) && defined(__clang__) && defined(__aarch64__)
767 std::is_same_v<SampleType, float> &&
769#else
770 false;
771#endif
772 for (int r = rendered_simd ? render_count_ : 0; r < render_count_; ++r) {
773 const auto s = static_cast<std::size_t>(render_slots_[
774 static_cast<std::size_t>(r)]);
775 // Evaluate at the CURRENT phase, then advance — the catalog's
776 // oscillator convention (`osc::VaOscillator::next` reads its
777 // entry phase). Advancing first would put the first sample one
778 // increment past the stored phase, so a partial stored at 0.25
779 // would start at 0.9983 rather than at 1, and a bank would not
780 // agree sample-for-sample with a catalog sine osc set to the
781 // same phase.
782 const double sine = [&] {
783 if constexpr (Profile == FastTrigProfile::reference)
784 return std::sin(kTwoPi * phase_[s].phase());
785 else
786 return static_cast<double>(
788 static_cast<float>(phase_[s].phase())));
789 }();
790 sum += gain_[s] * decay_env_[s] * sine;
791 phase_[s].advance(increment_[s]);
792 gain_[s] += gain_step_[s];
793 increment_[s] += increment_step_[s];
794 decay_env_[s] *= decay_coef_[s];
795 }
796
797 const double env = static_cast<double>(onset_.next());
798 // The accumulator is the one recursive-looking quantity here; the
799 // per-partial decays are geometric and would otherwise drizzle
800 // subnormals into the sum for minutes after a long tail.
801 out[i] = static_cast<SampleType>(
802 snap_to_zero(sum * env * master_linear_));
803 }
804 }
805 static constexpr double kPi() { return 3.14159265358979323846; }
806
810 static constexpr std::size_t slot(int p, int k) {
811 return static_cast<std::size_t>(2 * p + k);
812 }
813
814#if defined(__APPLE__) && defined(__clang__) && defined(__aarch64__)
816 double render_precise_simd_sample() {
817 simd_float4 accumulated = simd_make_float4(0.0f);
818 int r = 0;
819 for (; r + 3 < render_count_; r += 4) {
820 const auto s0 = static_cast<std::size_t>(render_slots_[r]);
821 const auto s1 = static_cast<std::size_t>(render_slots_[r + 1]);
822 const auto s2 = static_cast<std::size_t>(render_slots_[r + 2]);
823 const auto s3 = static_cast<std::size_t>(render_slots_[r + 3]);
824 const simd_float4 phase = simd_make_float4(
825 static_cast<float>(phase_[s0].phase()),
826 static_cast<float>(phase_[s1].phase()),
827 static_cast<float>(phase_[s2].phase()),
828 static_cast<float>(phase_[s3].phase()));
829 const simd_float4 sine = FastMath::sin_cycles_precise(phase);
830 const simd_float4 amplitude = simd_make_float4(
831 static_cast<float>(gain_[s0] * decay_env_[s0]),
832 static_cast<float>(gain_[s1] * decay_env_[s1]),
833 static_cast<float>(gain_[s2] * decay_env_[s2]),
834 static_cast<float>(gain_[s3] * decay_env_[s3]));
835 accumulated += amplitude * sine;
836
837 const std::array<std::size_t, 4> slots{s0, s1, s2, s3};
838 for (const auto s : slots) {
839 phase_[s].advance(increment_[s]);
840 gain_[s] += gain_step_[s];
841 increment_[s] += increment_step_[s];
842 decay_env_[s] *= decay_coef_[s];
843 }
844 }
845 double sum = static_cast<double>(simd_reduce_add(accumulated));
846 for (; r < render_count_; ++r) {
847 const auto s = static_cast<std::size_t>(render_slots_[r]);
848 sum += gain_[s] * decay_env_[s] *
849 static_cast<double>(FastMath::sin_cycles<
851 static_cast<float>(phase_[s].phase())));
852 phase_[s].advance(increment_[s]);
853 gain_[s] += gain_step_[s];
854 increment_[s] += increment_step_[s];
855 decay_env_[s] *= decay_coef_[s];
856 }
857 return sum;
858 }
859#endif
860
867 void refresh_onset() {
868 onset_.set_attack_ms(attack_ms_);
869 onset_.set_release_ms(release_ms_);
870 }
871
872 void apply_stored_phases() {
873 for (int p = 0; p < std::max(partial_count_, 1); ++p) {
874 const double stored =
875 voice_.partials[static_cast<std::size_t>(p)].phase01;
876 for (int k = 0; k < 2; ++k) {
877 const auto s = slot(p, k);
878 if (s < phase_.size()) phase_[s].reset(stored);
879 }
880 }
881 }
882
886 void draw_doublet_jitter() {
887 for (std::size_t s = 0; s < slot_detune_.size(); ++s)
888 slot_detune_[s] = 1.0 + kDoubletJitterSpread *
889 rng_.next_bipolar<double>();
890 }
891
896 void update_control_block(bool immediate) {
897 cadence_countdown_ = kAmpUpdateCadenceSamples;
898
899 const double glide = glide_ratio();
900 advance_glide();
901
902 const bool pair = doublet_active();
903 const int sub_slots = pair ? 2 : 1;
904 const double pair_scale = pair ? 0.5 : 1.0;
905
906 // Pass one: frequencies, guard gains, envelope gains, and the sum the
907 // normaliser needs. The normaliser has to see every factor that reaches
908 // the output, or the bound it computes is a bound on a different signal.
909 double magnitude_sum = 0.0;
910 render_count_ = 0;
911 for (int p = 0; p < partial_count_; ++p) {
912 const auto& row = voice_.partials[static_cast<std::size_t>(p)];
913 const double base_hz =
914 voice_.harmonic
915 ? partial_frequency_hz(fundamental_hz_, row.ratio,
916 inharmonicity_b_)
917 : fundamental_hz_ * row.ratio;
918
919 for (int k = 0; k < sub_slots; ++k) {
920 const auto s = slot(p, k);
921 // The jitter is indexed by PARTIAL, not by slot, so the pair
922 // straddles its nominal symmetrically. Drawing it per slot
923 // would detune the two members by different amounts, which
924 // shifts the mode's centre frequency instead of splitting it —
925 // a mistuned bell rather than a beating one.
926 const double cents =
927 pair ? (k == 0 ? -0.5 : 0.5) * detune_cents_ *
928 slot_detune_[slot(p, 0)]
929 : 0.0;
930 const double f_hz =
931 base_hz * glide * units::cents_to_ratio(cents);
932
933 const double guard = nyquist_guard_gain(f_hz, sample_rate_);
934 const double env_db = f_hz > 0.0 ? envelope_db_at(f_hz) : 0.0;
935 const double linear = units::db_to_linear(env_db);
936 const double signed_gain =
937 row.amp * pair_scale * linear * guard;
938
939 target_gain_[s] = signed_gain;
940 target_increment_[s] = f_hz / sample_rate_;
941 magnitude_sum += std::abs(signed_gain);
942
943 // `shared_ar` means exactly that: the onset is the whole
944 // amplitude story and the table's per-partial τ is ignored, so
945 // every partial holds while gated. Reading `decay_ms` in this
946 // mode would make the organ table's sustained rows and the
947 // bell's 500 ms rows behave differently under a control the
948 // user thinks selects between them.
949 decay_coef_[s] =
950 envelope_mode_ == EnvelopeMode::per_partial_decay
951 ? decay_coefficient(row.decay_ms)
952 : 1.0;
953 render_slots_[static_cast<std::size_t>(render_count_++)] =
954 static_cast<int>(s);
955 }
956 }
957
958 // Pass two: the normaliser, then the ramps. Applying `g_norm` to the
959 // per-partial gains rather than to the summed output is what makes the
960 // bound survive the ramp — a post-sum normaliser would be a different
961 // number on each side of a control-block edge.
962 normaliser_ = 1.0 / std::max(1.0, magnitude_sum);
963 const double step = 1.0 / static_cast<double>(kAmpUpdateCadenceSamples);
964 for (int r = 0; r < render_count_; ++r) {
965 const auto s = static_cast<std::size_t>(
966 render_slots_[static_cast<std::size_t>(r)]);
967 const double g = target_gain_[s] * normaliser_;
968 if (immediate) {
969 gain_[s] = g;
970 increment_[s] = target_increment_[s];
971 gain_step_[s] = 0.0;
972 increment_step_[s] = 0.0;
973 } else {
974 gain_step_[s] = (g - gain_[s]) * step;
975 increment_step_[s] = (target_increment_[s] - increment_[s]) * step;
976 }
977 }
978 }
979
983 double decay_coefficient(double decay_ms) const {
984 if (!(decay_ms > 0.0)) return 1.0; // sustained — the table's "∞"
985 const double tau_samples = decay_ms * 0.001 * sample_rate_;
986 return tau_samples > 0.0 ? std::exp(-1.0 / tau_samples) : 0.0;
987 }
988
989 double glide_ratio() const {
990 if (glide_time_ms_ <= 0.0 || glide_start_cents_ == 0.0) return 1.0;
991 const double t = std::min(glide_position_, 1.0);
992 return units::cents_to_ratio(glide_start_cents_ * (1.0 - t));
993 }
994
995 void advance_glide() {
996 if (glide_time_ms_ <= 0.0) {
997 glide_position_ = 1.0;
998 return;
999 }
1000 const double block_ms = 1000.0 *
1001 static_cast<double>(kAmpUpdateCadenceSamples) /
1002 sample_rate_;
1003 glide_position_ =
1004 std::min(1.0, glide_position_ + block_ms / glide_time_ms_);
1005 }
1006
1007 double sample_rate_ = 44100.0;
1008 int max_partials_ = kMaxPartialsDefault;
1009 int partial_count_ = kPartialCountDefault;
1010
1011 double fundamental_hz_ = kFundamentalDefaultHz;
1012 double inharmonicity_b_ = kInharmonicityDefault;
1013 double tilt_db_oct_ = kSpectralTiltDefaultDbOct;
1014 double master_gain_db_ = kMasterGainDefaultDb;
1015 double master_linear_ = units::db_to_linear(kMasterGainDefaultDb);
1016 double morph_ = 0.0;
1017 double detune_cents_ = kDetuneDefaultCents;
1018 double attack_ms_ = kAttackDefaultMs;
1019 double release_ms_ = kReleaseDefaultMs;
1020 double glide_start_cents_ = 0.0;
1021 double glide_time_ms_ = 0.0;
1022 double glide_position_ = 1.0;
1023 double normaliser_ = 1.0;
1024
1027 RetrigPhase retrig_phase_ = RetrigPhase::reset_stored;
1029
1030 VoiceTable voice_ = make_organ_voice(kPartialCountDefault);
1031 SpectralEnvelope envelope_a_{};
1032 SpectralEnvelope envelope_b_{};
1033
1034 ArT<double> onset_{};
1035 Xorshift32 rng_{0x9E3779B9u};
1036
1037 std::vector<osc::PhaseAccumulator> phase_{};
1038 std::vector<double> gain_{}, gain_step_{};
1039 std::vector<double> increment_{}, increment_step_{};
1040 std::vector<double> decay_env_{}, decay_coef_{};
1041 std::vector<double> slot_detune_{};
1042 std::vector<int> render_slots_{};
1043 std::array<double, 2 * VoiceTable::kMaxPartials> target_gain_{};
1044 std::array<double, 2 * VoiceTable::kMaxPartials> target_increment_{};
1045
1046 int render_count_ = 0;
1047 int cadence_countdown_ = 0;
1048};
1049
1052
1053} // namespace pulp::signal
Public spectral-envelope value types used by the additive bank.
A sinusoidal-bank additive synthesizer.
Definition additive_bank.hpp:350
double detune_cents() const
Definition additive_bank.hpp:673
static constexpr double kAttackMaxMs
Definition additive_bank.hpp:407
void set_seed(std::uint32_t seed)
Seeds the initial-phase and doublet-jitter generator.
Definition additive_bank.hpp:690
void process(SampleType *out, int n)
Definition additive_bank.hpp:740
AdditiveBankT()
Definition additive_bank.hpp:476
static constexpr double kDoubletJitterSpread
Definition additive_bank.hpp:419
EnvelopeMode
How a partial's amplitude evolves.
Definition additive_bank.hpp:365
@ shared_ar
One onset/release shape for every partial.
Definition additive_bank.hpp:366
@ per_partial_decay
The shared onset times each partial's own exp(−t/τ_p).
Definition additive_bank.hpp:369
static constexpr double kBElectricPianoTine
Definition additive_bank.hpp:395
bool active() const
Definition additive_bank.hpp:721
void set_retrig_phase(RetrigPhase p)
Definition additive_bank.hpp:685
RetrigPhase
Phase treatment on retrigger().
Definition additive_bank.hpp:353
@ seeded_random
Phases drawn from the Xorshift32 seeded at reset().
Definition additive_bank.hpp:359
@ free_run
Phases keep running.
Definition additive_bank.hpp:357
@ reset_stored
Every phase returns to its table phase01.
Definition additive_bank.hpp:354
const VoiceTable & voice() const
Definition additive_bank.hpp:600
static constexpr double kInharmonicityMax
Definition additive_bank.hpp:390
static constexpr double kMasterGainDefaultDb
Definition additive_bank.hpp:403
static constexpr double kInharmonicityDefault
Definition additive_bank.hpp:385
bool set_trig_profile(FastTrigProfile profile) noexcept
Selects the bounded-cycle sine implementation used by the float bank.
Definition additive_bank.hpp:564
static double worst_case_gain()
The worst-case output magnitude over the entire legal parameter space.
Definition additive_bank.hpp:467
void set_fundamental_hz(double f0)
Definition additive_bank.hpp:525
static constexpr double kMasterGainMinDb
Definition additive_bank.hpp:401
static double nyquist_guard_gain(double f_hz, double sample_rate)
The Nyquist guard: unity below fs/2 − kNyquistGuardBandHz, a raised cosine to exactly zero at fs/2,...
Definition additive_bank.hpp:454
static constexpr double kInharmonicityCitedMax
Definition additive_bank.hpp:387
void prepare(double sample_rate, int max_partials=kMaxPartialsDefault)
Sizes the per-partial arrays and returns the bank to a fresh state.
Definition additive_bank.hpp:481
void release()
Note-off.
Definition additive_bank.hpp:719
void set_pitch_glide(double start_cents, double time_ms)
A shared initial pitch glide: every partial is scaled by 2^(cents(t)/1200), starting at start_cents a...
Definition additive_bank.hpp:679
static constexpr double kFundamentalMinHz
Definition additive_bank.hpp:381
void set_master_gain_db(double g)
Definition additive_bank.hpp:554
void set_partial(int p, double ratio, double amp_lin, double phase01, double decay_ms)
Sets one row of the active table.
Definition additive_bank.hpp:604
double envelope_db_at(double f_hz) const
The live morphed envelope in dB at a partial frequency — the exact quantity the render applies,...
Definition additive_bank.hpp:636
static constexpr double kReleaseMinMs
Definition additive_bank.hpp:409
static constexpr double kNyquistGuardBandHz
Definition additive_bank.hpp:428
void set_envelope_a(const SpectralEnvelope &e)
Definition additive_bank.hpp:617
static constexpr double kSpectralTiltMaxDbOct
Definition additive_bank.hpp:398
RetrigPhase retrig_phase() const
Definition additive_bank.hpp:686
void load_voice(const VoiceTable &v)
Definition additive_bank.hpp:588
static constexpr int kAmpUpdateCadenceSamples
Definition additive_bank.hpp:424
SpectralDomain spectral_domain() const
Definition additive_bank.hpp:631
double spectral_tilt_db_oct() const
Definition additive_bank.hpp:552
void set_release_ms(double ms)
Definition additive_bank.hpp:659
static constexpr int kMaxPartialsDefault
Definition additive_bank.hpp:378
static constexpr double kTwoPi
Definition additive_bank.hpp:434
static constexpr double kSpectralTiltMinDbOct
Definition additive_bank.hpp:397
void set_attack_ms(double ms)
Definition additive_bank.hpp:651
double inharmonicity_b() const
Definition additive_bank.hpp:546
static constexpr double kFundamentalMaxHz
Definition additive_bank.hpp:382
static constexpr double kMasterGainMaxDb
Definition additive_bank.hpp:402
static constexpr double kEnvelopeCurve
Definition additive_bank.hpp:432
bool doublet_active() const
Definition additive_bank.hpp:674
void set_partial_count(int n)
Definition additive_bank.hpp:531
static constexpr int latency_samples()
Zero. Direct time-domain oscillators; nothing is buffered.
Definition additive_bank.hpp:472
static constexpr double kSpectralTiltDefaultDbOct
Definition additive_bank.hpp:399
SampleType next()
Definition additive_bank.hpp:734
static constexpr double kAttackMinMs
Definition additive_bank.hpp:406
int max_partials() const
Definition additive_bank.hpp:538
static constexpr double kBUpperTreble
Definition additive_bank.hpp:394
void set_inharmonicity_b(double b)
Stiff-string coefficient.
Definition additive_bank.hpp:542
void set_envelope_b(const SpectralEnvelope &e)
Definition additive_bank.hpp:618
static double partial_frequency_hz(double f0_hz, double ratio, double inharmonicity_b)
The published stiff-string law: f = f0 · ratio · √(1 + B·ratio²).
Definition additive_bank.hpp:445
double partial_frequency(int p) const
The current swept frequency of one partial's primary oscillator, in Hz — the quantity the inharmonici...
Definition additive_bank.hpp:727
EnvelopeMode envelope_mode() const
Definition additive_bank.hpp:649
void reset()
Clears every phase, envelope, and ramp, and rewinds the generator, so a render from here is bit-ident...
Definition additive_bank.hpp:509
static constexpr int kMaxPartialsCeiling
Definition additive_bank.hpp:377
void set_envelope_mode(EnvelopeMode mode)
Definition additive_bank.hpp:648
double master_gain_db() const
Definition additive_bank.hpp:559
static constexpr double kDetuneMaxCents
Definition additive_bank.hpp:413
static constexpr double kFundamentalDefaultHz
Definition additive_bank.hpp:383
static constexpr double kReleaseMaxMs
Definition additive_bank.hpp:410
int partial_count() const
Definition additive_bank.hpp:537
void retrigger()
Note-on.
Definition additive_bank.hpp:697
void set_morph(float m)
Crossfade A↔B.
Definition additive_bank.hpp:624
void set_spectral_domain(SpectralDomain d)
Definition additive_bank.hpp:630
double morph() const
Definition additive_bank.hpp:628
void set_spectral_tilt_db_oct(double s)
Definition additive_bank.hpp:548
static constexpr bool trig_profile_has_vector_path(FastTrigProfile profile)
Definition additive_bank.hpp:576
static constexpr int kPartialCountDefault
Definition additive_bank.hpp:379
double fundamental_hz() const
Definition additive_bank.hpp:529
static constexpr double kDetuneDefaultCents
Definition additive_bank.hpp:412
static constexpr double kBPianoTenor
Definition additive_bank.hpp:392
static constexpr double kBPianoMid
Definition additive_bank.hpp:393
static constexpr double kReleaseDefaultMs
Definition additive_bank.hpp:408
static constexpr double kAttackDefaultMs
Definition additive_bank.hpp:405
void set_detune_cents(double cents)
Doublet spread in cents.
Definition additive_bank.hpp:669
FastTrigProfile trig_profile() const noexcept
Definition additive_bank.hpp:574
SampleType next()
Definition envelope.hpp:404
A break-point spectral envelope: gain in dB as a function of frequency, interpolated linearly in the ...
Definition additive_spectral_envelope.hpp:44
The envelope family — AR, AD, AHD, DAHDSR, and a trigger-driven modulation envelope — plus a level-in...
Scalar math helpers for real-time audio DSP.
@ g
Definition pitch.hpp:27
float simd_reduce_add(const float *data, std::size_t count)
Definition simd.hpp:67
T cents_to_ratio(T cents)
Cents → frequency ratio. +100 → 2^(1/12), +1200 → 2.
Definition units.hpp:118
T db_to_linear(T db)
Amplitude dB → linear gain. 0 dB → 1, −6.0206 dB → 0.5.
Definition units.hpp:71
Definition additive_bank.hpp:218
VoiceTable make_organ_voice(int count=64)
The drawbar organ voice (§10a): nine tonewheel footages over a −12 dB/oct harmonic tail.
Definition additive_bank.hpp:264
FastTrigProfile
Semantic accuracy choices for bounded-cycle real-time trigonometry.
Definition fast_math.hpp:38
@ reference
Platform sine; the compatibility default.
Definition fast_math.hpp:40
@ realtime_precise
Float-precision path with maximum absolute error at most 2.5e-7 over one bounded cycle.
Definition fast_math.hpp:46
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ linear
2-point. Cheapest; audible imaging when pitching up.
Definition granular.hpp:133
@ sine
Definition lfo_sampling.hpp:54
@ immediate
Replace the output with the hold from the adopting frame onward.
Definition freeze_hold.hpp:155
SpectralDomain
This enum exists because "sampling a spectral envelope makes timbrepitch-independent" is only true un...
Definition additive_spectral_envelope.hpp:36
@ absolute_hz
Definition additive_spectral_envelope.hpp:36
AdditiveBankT< float > AdditiveBank
Definition additive_bank.hpp:1050
VoiceTable make_bell_voice(int count=64)
The bell voice (§10b): a modal body whose partials each decay at their own rate.
Definition additive_bank.hpp:309
AdditiveBankT< double > AdditiveBank64
Definition additive_bank.hpp:1051
float sum(const float *data, std::size_t count) noexcept
@ s
Definition input_events.hpp:41
@ r
Definition input_events.hpp:41
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ t
Definition input_events.hpp:41
@ row
Definition design_ir.hpp:53
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
static float sin_cycles(float phase_cycles) noexcept
Computes sine for a normalized phase in [0, 1).
Definition fast_math.hpp:102
Which abscissa a SpectralEnvelope's break-points are measured along.
Definition additive_bank.hpp:222
double ratio
Frequency as a multiple of f0.
Definition additive_bank.hpp:223
double phase01
Stored initial phase in cycles, [0, 1).
Definition additive_bank.hpp:225
double decay_ms
Exponential τ.
Definition additive_bank.hpp:226
double amp
Unit-referenced amplitude, pre-normaliser.
Definition additive_bank.hpp:224
A partial set. Fixed capacity; copying one is a memcpy, never an allocation.
Definition additive_bank.hpp:231
bool add(const VoicePartial &p)
Definition additive_bank.hpp:244
bool harmonic
Whether inharmonicity_B applies.
Definition additive_bank.hpp:239
void clear()
Definition additive_bank.hpp:243
std::array< VoicePartial, kMaxPartials > partials
Definition additive_bank.hpp:241
static constexpr int kMaxPartials
Definition additive_bank.hpp:232
int count
Definition additive_bank.hpp:240
The real-unit vocabulary shared by every DSP block in the catalog.