Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
speaker_cabinet.hpp
Go to the documentation of this file.
1#pragma once
2
119
120#include <pulp/signal/biquad.hpp>
125#include <pulp/signal/units.hpp>
126
127#include <algorithm>
128#include <cmath>
129#include <complex>
130
131namespace pulp::signal {
132
137
146 const char* name;
147 double fs_hz;
148 double qts;
149 double vas_litres;
152};
153
154namespace detail {
155
167template <typename SampleType>
169public:
170 struct Taps {
171 SampleType highpass;
172 SampleType lowpass;
173 };
174
175 void set_coefficients(double cutoff_hz, double q, double sample_rate) {
176 constexpr double pi = 3.14159265358979323846;
177 const double nyquist_limit = 0.49 * sample_rate;
178 const double fc = std::clamp(cutoff_hz, 1.0, nyquist_limit);
179 const double safe_q = std::max(q, 1.0e-3);
180 const double g = std::tan(pi * fc / sample_rate);
181 const double k = 1.0 / safe_q;
182 const double a1 = 1.0 / (1.0 + g * (g + k));
183 g_ = static_cast<SampleType>(g);
184 k_ = static_cast<SampleType>(k);
185 a1_ = static_cast<SampleType>(a1);
186 a2_ = static_cast<SampleType>(g * a1);
187 a3_ = static_cast<SampleType>(g * g * a1);
188 }
189
190 Taps tick(SampleType input) {
191 const SampleType v3 = input - ic2_;
192 const SampleType v1 = a1_ * ic1_ + a2_ * v3;
193 const SampleType v2 = ic2_ + a2_ * ic1_ + a3_ * v3;
194 // Snap both integrator states: the TPT feedback path has no FTZ guard
195 // of its own, exactly as SvfT documents.
196 ic1_ = snap_to_zero(SampleType{2} * v1 - ic1_);
197 ic2_ = snap_to_zero(SampleType{2} * v2 - ic2_);
198 return {input - k_ * v1 - v2, v2};
199 }
200
201 void reset() {
202 ic1_ = SampleType{0};
203 ic2_ = SampleType{0};
204 }
205
206private:
207 SampleType g_ = SampleType{0};
208 SampleType k_ = SampleType{1};
209 SampleType a1_ = SampleType{1};
210 SampleType a2_ = SampleType{0};
211 SampleType a3_ = SampleType{0};
212 SampleType ic1_ = SampleType{0};
213 SampleType ic2_ = SampleType{0};
214};
215
216} // namespace detail
217
227template <typename SampleType = float>
229public:
230 // ── Driver archetypes ────────────────────────────────────────────────────
231
232 static constexpr int kArchetypeCount = 5;
233
237 static const SpeakerDriverArchetype& archetype(int index) {
238 static constexpr SpeakerDriverArchetype kTable[kArchetypeCount] = {
239 // name fs Qts Vas(L) f_breakup baffle W(m)
240 {"Brit-12 ceramic", 75.0, 0.50, 200.0, 1900.0, 0.50},
241 {"Amer-12 ceramic", 95.0, 0.60, 150.0, 2200.0, 0.50},
242 {"Alnico-12", 85.0, 0.45, 170.0, 2000.0, 0.48},
243 {"Brit-10", 100.0, 0.55, 90.0, 2600.0, 0.42},
244 {"Bass-15", 45.0, 0.35, 300.0, 1200.0, 0.60},
245 };
246 return kTable[std::clamp(index, 0, kArchetypeCount - 1)];
247 }
248
249 // ── Design parameters ────────────────────────────────────────────────────
250 // Every value here is ours to tune, declared with default + range.
251
255 static constexpr double kSpeedOfSoundMs = 343.0;
256
258 static constexpr double kBoxVolumeLDefault = 28.0;
259 static constexpr double kBoxVolumeLMin = 5.0;
260 static constexpr double kBoxVolumeLMax = 150.0;
261
263 static constexpr double kResonanceTrimSemitonesMin = -12.0;
264 static constexpr double kResonanceTrimSemitonesMax = 12.0;
265
268 static constexpr double kQResonanceMin = 0.4;
269 static constexpr double kQResonanceMax = 2.0;
270
272 static constexpr double kConeBreakupAmountDefault = 100.0;
273
275 static constexpr double kTrebleRolloffHzDefault = 4000.0;
276 static constexpr double kTrebleRolloffHzMin = 1500.0;
277 static constexpr double kTrebleRolloffHzMax = 8000.0;
278
283 static constexpr double kSemiInductanceDefault = 0.25;
284 static constexpr double kSemiInductanceMin = 0.0;
285 static constexpr double kSemiInductanceMax = 1.0;
286 static constexpr double kSemiInductanceShelfDb = -12.0;
287 static constexpr double kSemiInductanceShelfDbMin = -18.0;
288 static constexpr double kSemiInductanceShelfDbMax = -6.0;
289
291 static constexpr double kDriveDbDefault = 0.0;
292 static constexpr double kDriveDbMin = -12.0;
293 static constexpr double kDriveDbMax = 24.0;
294
296 static constexpr double kCompressionAmountDefault = 60.0;
297
301 static constexpr double kExcursionReference = 0.5;
302 static constexpr double kExcursionReferenceMin = 0.1;
303 static constexpr double kExcursionReferenceMax = 2.0;
304
307 static constexpr double kBlFalloff = 1.5;
308 static constexpr double kBlFalloffMin = 0.0;
309 static constexpr double kBlFalloffMax = 4.0;
310
313 static constexpr double kCmsStiffening = 0.35;
314 static constexpr double kCmsStiffeningMin = 0.0;
315 static constexpr double kCmsStiffeningMax = 1.0;
316
318 static constexpr double kMicDistanceCmDefault = 3.0;
319 static constexpr double kMicDistanceCmMin = 1.0;
320 static constexpr double kMicDistanceCmMax = 60.0;
321
328 static constexpr double kProximityGainK = 12.0;
329 static constexpr double kProximityGainKMin = 0.0;
330 static constexpr double kProximityGainKMax = 24.0;
331 static constexpr double kProximityReferenceCm = 30.0;
332 static constexpr double kProximityReferenceCmMin = 20.0;
333 static constexpr double kProximityReferenceCmMax = 50.0;
334 static constexpr double kProximityCeilingDb = 8.0;
335 static constexpr double kProximityCeilingDbMin = 6.0;
336 static constexpr double kProximityCeilingDbMax = 12.0;
337 static constexpr double kProximityCornerHz = 250.0;
338 static constexpr double kProximityCornerHzMin = 150.0;
339 static constexpr double kProximityCornerHzMax = 400.0;
340
343 static constexpr double kAirLossDbPerCm = 0.05;
344 static constexpr double kAirLossDbPerCmMin = 0.02;
345 static constexpr double kAirLossDbPerCmMax = 0.10;
346 static constexpr double kAirLossCornerHz = 6000.0;
347 static constexpr double kAirLossCornerHzMin = 4000.0;
348 static constexpr double kAirLossCornerHzMax = 8000.0;
349
353 static constexpr double kPresenceCapDb = 3.0;
354 static constexpr double kPresenceEdgeDb = -8.0;
355 static constexpr double kPresenceDbMax = 6.0;
356 static constexpr double kPresenceDbMin = -12.0;
357 static constexpr double kPresenceCornerHz = 2500.0;
358 static constexpr double kPresenceCornerHzMin = 1500.0;
359 static constexpr double kPresenceCornerHzMax = 3500.0;
360 static constexpr double kMicPositionPctDefault = 30.0;
361
364 static constexpr double kOffAxisOnAxisHz = 8000.0;
365 static constexpr double kOffAxisOnAxisHzMin = 5000.0;
366 static constexpr double kOffAxisOnAxisHzMax = 12000.0;
367 static constexpr double kOffAxisFactor = 0.7;
368 static constexpr double kOffAxisFactorMin = 0.0;
369 static constexpr double kOffAxisFactorMax = 0.95;
370 static constexpr double kMicAxisDegMin = 0.0;
371 static constexpr double kMicAxisDegMax = 90.0;
372
376 static constexpr double kBaffleStepDb = 6.0;
377
381 static constexpr double kEdgeDistanceM = 0.15;
382 static constexpr double kEdgeDistanceMMin = 0.08;
383 static constexpr double kEdgeDistanceMMax = 0.30;
384 static constexpr double kRipple1Q = 2.0;
385 static constexpr double kRipple1Db = 2.5;
386 static constexpr double kRipple2Q = 2.5;
387 static constexpr double kRipple2Db = -2.0;
388 static constexpr double kRippleQMin = 1.5;
389 static constexpr double kRippleQMax = 4.0;
390 static constexpr double kRippleDbMax = 4.0;
391 static constexpr double kDiffractionAmountDefault = 50.0;
392
396 static constexpr double kDipolePathM = 0.30;
397 static constexpr double kDipolePathMMin = 0.15;
398 static constexpr double kDipolePathMMax = 0.50;
399 static constexpr double kDipoleNotchRatio = 1.8;
400 static constexpr double kDipoleNotchRatioMin = 1.4;
401 static constexpr double kDipoleNotchRatioMax = 2.2;
402 static constexpr double kDipoleNotchQ = 1.5;
403 static constexpr double kDipoleNotchQMin = 1.0;
404 static constexpr double kDipoleNotchQMax = 3.0;
405 static constexpr double kDipoleNotchDb = -4.0;
406 static constexpr double kDipoleNotchDbMin = -8.0;
407 static constexpr double kDipoleNotchDbMax = -2.0;
408
410 static constexpr double kOutputTrimDbMin = -24.0;
411 static constexpr double kOutputTrimDbMax = 24.0;
412
422 static constexpr double kDcBlockerHz = 5.0;
423 static constexpr double kDcBlockerHzMin = 2.0;
424 static constexpr double kDcBlockerHzMax = 20.0;
425
427 static constexpr double kSmoothingMs = 15.0;
428 static constexpr double kSmoothingMsMin = 5.0;
429 static constexpr double kSmoothingMsMax = 30.0;
430
434 static constexpr int kControlBlock = 32;
435 static constexpr int kControlBlockMin = 16;
436 static constexpr int kControlBlockMax = 64;
437
441 static constexpr double kWorstCaseGain = 10.0;
442
443 // ── Cone-breakup mode shape ─────────────────────────────────────────────
444
445 static constexpr int kBreakupModeCount = 4;
446
452 struct BreakupMode {
453 double ratio;
454 double q;
455 double gain_db;
456 };
457
462 static const BreakupMode& breakup_mode(int index) {
463 static constexpr BreakupMode kShape[kBreakupModeCount] = {
464 {1.00, 3.0, 4.0},
465 {1.55, 4.0, 3.0},
466 {2.20, 5.0, -4.0},
467 {3.10, 6.0, 2.5},
468 };
469 return kShape[std::clamp(index, 0, kBreakupModeCount - 1)];
470 }
471 static constexpr double kBreakupRatioMin = 1.0;
472 static constexpr double kBreakupRatioMax = 4.0;
473 static constexpr double kBreakupQMin = 1.5;
474 static constexpr double kBreakupQMax = 8.0;
475 static constexpr double kBreakupGainDbMax = 6.0;
476
477 // ── Lifecycle ────────────────────────────────────────────────────────────
478
480 void prepare(double sample_rate) {
481 if (std::isfinite(sample_rate) && sample_rate > 0.0) sample_rate_ = sample_rate;
482 const auto smoothing = static_cast<SampleType>(kSmoothingMs / 1000.0);
483 const auto fs = static_cast<SampleType>(sample_rate_);
484 drive_lin_.set_ramp_time(smoothing, fs);
485 out_trim_lin_.set_ramp_time(smoothing, fs);
486 treble_hz_.set_ramp_time(smoothing, fs);
487 q_smoothed_.set_ramp_time(smoothing, fs);
488 presence_db_.set_ramp_time(smoothing, fs);
489 proximity_db_.set_ramp_time(smoothing, fs);
490 offaxis_hz_.set_ramp_time(smoothing, fs);
491 inductance_lp_.prepare(fs);
492 offaxis_lp_.prepare(fs);
493 dipole_hp_.prepare(fs);
494 // One-pole high-pass pole for the requested corner: p = 1 - 2*pi*fc/fs.
495 constexpr double two_pi = 6.283185307179586476925286766559;
496 dc_.set_pole(static_cast<SampleType>(
497 std::clamp(1.0 - two_pi * kDcBlockerHz / sample_rate_, 0.0, 0.99999)));
498 drive_lin_.set_immediate(static_cast<SampleType>(units::db_to_linear(drive_db_)));
499 out_trim_lin_.set_immediate(static_cast<SampleType>(units::db_to_linear(out_trim_db_)));
500 treble_hz_.set_immediate(static_cast<SampleType>(treble_hz_target_));
501 q_smoothed_.set_immediate(static_cast<SampleType>(resonance_q()));
502 presence_db_.set_immediate(static_cast<SampleType>(presence_shelf_db()));
503 proximity_db_.set_immediate(static_cast<SampleType>(proximity_gain_db()));
504 offaxis_hz_.set_immediate(static_cast<SampleType>(offaxis_corner_hz()));
505 control_counter_ = 0;
506 update_coefficients();
507 reset();
508 }
509
510 // ── Parameters ───────────────────────────────────────────────────────────
511
512 void set_driver_archetype(int index) {
513 archetype_index_ = std::clamp(index, 0, kArchetypeCount - 1);
514 retarget_smoothed();
515 }
517 box_type_ = type;
518 retarget_smoothed();
519 }
520 void set_box_volume_l(double litres) {
521 if (!std::isfinite(litres)) return;
522 box_volume_l_ = std::clamp(litres, kBoxVolumeLMin, kBoxVolumeLMax);
523 retarget_smoothed();
524 }
525 void set_resonance_trim_semitones(double semitones) {
526 if (!std::isfinite(semitones)) return;
527 resonance_trim_ =
529 }
530
532 void set_q_resonance(double q) {
533 if (!std::isfinite(q)) return;
534 q_override_ = q > 0.0 ? std::clamp(q, kQResonanceMin, kQResonanceMax) : 0.0;
535 retarget_smoothed();
536 }
537 void set_cone_breakup_amount(double percent) {
538 if (!std::isfinite(percent)) return;
539 breakup_amount_ = std::clamp(percent, 0.0, 100.0) / 100.0;
540 }
541 void set_treble_rolloff_hz(double hz) {
542 if (!std::isfinite(hz)) return;
543 treble_hz_target_ = std::clamp(hz, kTrebleRolloffHzMin, kTrebleRolloffHzMax);
544 treble_hz_.set_target(static_cast<SampleType>(treble_hz_target_));
545 }
546 void set_drive_db(double db) {
547 if (!std::isfinite(db)) return;
548 drive_db_ = std::clamp(db, kDriveDbMin, kDriveDbMax);
549 drive_lin_.set_target(static_cast<SampleType>(units::db_to_linear(drive_db_)));
550 }
551 void set_compression_amount(double percent) {
552 if (!std::isfinite(percent)) return;
553 compression_amount_ = std::clamp(percent, 0.0, 100.0) / 100.0;
554 }
555 void set_mic_distance_cm(double cm) {
556 if (!std::isfinite(cm)) return;
557 mic_distance_cm_ = std::clamp(cm, kMicDistanceCmMin, kMicDistanceCmMax);
558 proximity_db_.set_target(static_cast<SampleType>(proximity_gain_db()));
559 }
560 void set_mic_position_pct(double percent) {
561 if (!std::isfinite(percent)) return;
562 mic_position_ = std::clamp(percent, 0.0, 100.0) / 100.0;
563 presence_db_.set_target(static_cast<SampleType>(presence_shelf_db()));
564 }
565 void set_mic_axis_deg(double degrees) {
566 if (!std::isfinite(degrees)) return;
567 mic_axis_deg_ = std::clamp(degrees, kMicAxisDegMin, kMicAxisDegMax);
568 offaxis_hz_.set_target(static_cast<SampleType>(offaxis_corner_hz()));
569 }
570 void set_diffraction_amount(double percent) {
571 if (!std::isfinite(percent)) return;
572 diffraction_amount_ = std::clamp(percent, 0.0, 100.0) / 100.0;
573 }
574 void set_output_trim_db(double db) {
575 if (!std::isfinite(db)) return;
576 out_trim_db_ = std::clamp(db, kOutputTrimDbMin, kOutputTrimDbMax);
577 out_trim_lin_.set_target(static_cast<SampleType>(units::db_to_linear(out_trim_db_)));
578 }
579
580 // ── Audio ────────────────────────────────────────────────────────────────
581
582 SampleType process(SampleType input) {
583 if (!std::isfinite(static_cast<double>(input))) {
584 reset();
585 return SampleType{0};
586 }
587 if (control_counter_ == 0) update_coefficients();
588 control_counter_ = (control_counter_ + 1) % kControlBlock;
589
590 const SampleType drive = drive_lin_.next();
591 const SampleType trim = out_trim_lin_.next();
592
593 // Thiele-Small resonance. HP tap is the radiated pressure; LP tap is
594 // proportional to cone displacement and feeds the nonlinear stage.
595 const auto taps = resonance_.tick(dc_.process(input) * drive);
596
597 // BL(x): even, strictly compressive, exactly unity at rest.
598 const SampleType x_norm = taps.lowpass * excursion_scale_;
599 const SampleType g_bl = SampleType{1} / (SampleType{1} + beta_ * x_norm * x_norm);
600 excursion_ = x_norm;
601 // Peak-track within the control block: the Cms stiffening is an
602 // envelope effect, and sampling the instantaneous excursion once per
603 // block would alias the carrier's own phase into the coefficient.
604 const SampleType magnitude = std::abs(x_norm);
605 if (magnitude > excursion_peak_) excursion_peak_ = magnitude;
606
607 SampleType s = taps.highpass * g_bl;
608 s = inductance(s);
609 for (int i = 0; i < kBreakupModeCount; ++i) s = breakup_[i].process(s);
610 s = baffle_.process(s);
611 s = ripple_[0].process(s);
612 s = ripple_[1].process(s);
613 if (box_type_ == SpeakerBoxType::open_back) {
614 s = dipole_hp_.process_highpass(s);
615 s = dipole_notch_.process(s);
616 }
617 s = proximity_.process(s);
618 s = air_loss_.process(s);
619 s = presence_.process(s);
620 s = offaxis_lp_.process_lowpass(s);
621 const SampleType output = s * trim;
622 if (!std::isfinite(static_cast<double>(output))) {
623 reset();
624 return SampleType{0};
625 }
626 return output;
627 }
628
629 void process(const SampleType* in, SampleType* out, int n) {
630 for (int i = 0; i < n; ++i) out[i] = process(in[i]);
631 }
632
635 void reset() {
636 dc_.reset();
637 resonance_.reset();
638 inductance_lp_.reset();
639 semi_shelf_.reset();
640 for (auto& mode : breakup_) mode.reset();
641 baffle_.reset();
642 ripple_[0].reset();
643 ripple_[1].reset();
644 dipole_hp_.reset();
645 dipole_notch_.reset();
646 proximity_.reset();
647 air_loss_.reset();
648 presence_.reset();
649 offaxis_lp_.reset();
650 excursion_ = SampleType{0};
651 excursion_peak_ = SampleType{0};
652 fc_dynamic_ = resonance_fc_hz();
653 control_counter_ = 0;
654 }
655
658 int latency_samples() const { return 0; }
659
662 double worst_case_gain() const { return kWorstCaseGain; }
663
664 // ── Derived physics (tests compute their expected values from these) ─────
665
670 double compliance_ratio() const {
671 if (box_type_ == SpeakerBoxType::open_back) return 0.0;
672 return archetype(archetype_index_).vas_litres / box_volume_l_;
673 }
674
676 double resonance_fc_hz() const {
677 const double base = archetype(archetype_index_).fs_hz *
678 std::sqrt(1.0 + compliance_ratio());
679 return base * std::pow(2.0, resonance_trim_ / 12.0);
680 }
681
683 double resonance_q() const {
684 if (q_override_ > 0.0) return q_override_;
685 return archetype(archetype_index_).qts * std::sqrt(1.0 + compliance_ratio());
686 }
687
691 double resonance_peak_db() const {
692 const double q = resonance_q();
693 const double inner = 1.0 - 1.0 / (4.0 * q * q);
694 if (inner <= 0.0) return 0.0;
695 return units::linear_to_db(q / std::sqrt(inner));
696 }
697
700 double resonance_peak_hz() const {
701 const double q = resonance_q();
702 const double inner = 1.0 - 1.0 / (2.0 * q * q);
703 if (inner <= 0.0) return resonance_fc_hz();
704 return resonance_fc_hz() / std::sqrt(inner);
705 }
706
708 double baffle_step_hz() const {
709 return kSpeedOfSoundMs / (3.14159265358979323846 * archetype(archetype_index_).baffle_width_m);
710 }
711
713 double ripple_hz() const { return kSpeedOfSoundMs / (2.0 * kEdgeDistanceM); }
714
716 double dipole_hz() const { return kSpeedOfSoundMs / (2.0 * kDipolePathM); }
717
719 double breakup_mode_hz(int index) const {
720 return archetype(archetype_index_).f_breakup_hz * breakup_mode(index).ratio;
721 }
722
724 double offaxis_corner_hz() const {
725 constexpr double deg_to_rad = 3.14159265358979323846 / 180.0;
726 const double reduction = kOffAxisFactor * std::sin(mic_axis_deg_ * deg_to_rad);
727 return kOffAxisOnAxisHz * std::max(0.05, 1.0 - reduction);
728 }
729
732 double proximity_gain_db() const {
733 const double raw = kProximityGainK * (1.0 / mic_distance_cm_ - 1.0 / kProximityReferenceCm);
734 return std::clamp(raw, 0.0, kProximityCeilingDb);
735 }
736
738 double presence_shelf_db() const {
739 return kPresenceCapDb + mic_position_ * (kPresenceEdgeDb - kPresenceCapDb);
740 }
741
743 double air_loss_db() const { return -kAirLossDbPerCm * mic_distance_cm_; }
744
754 double inductance_magnitude_db(double hz) const {
755 constexpr double pi = 3.14159265358979323846;
756 const double w = 2.0 * pi * hz / sample_rate_;
757 const std::complex<double> z_inv = std::polar(1.0, -w);
758
759 // TPT one-pole lowpass: H(z) = g(1 + z^-1) / (1 - (1 - 2g) z^-1),
760 // with the same bilinear g the filter itself computes from its corner.
761 const double fc = static_cast<double>(inductance_lp_.cutoff());
762 const double wa = 2.0 * sample_rate_ * std::tan(pi * fc / sample_rate_);
763 const double g = wa / (2.0 * sample_rate_ + wa);
764 const std::complex<double> lp =
765 g * (1.0 + z_inv) / (1.0 - (1.0 - 2.0 * g) * z_inv);
766
767 // The shelf's live coefficients, evaluated as H(e^jw).
768 const auto c = semi_shelf_.coefficients();
769 const std::complex<double> z2 = z_inv * z_inv;
770 const std::complex<double> shelf =
771 (static_cast<double>(c.b0) + static_cast<double>(c.b1) * z_inv +
772 static_cast<double>(c.b2) * z2) /
773 (1.0 + static_cast<double>(c.a1) * z_inv + static_cast<double>(c.a2) * z2);
774
775 const double s = static_cast<double>(semi_);
776 return units::linear_to_db(std::abs((1.0 - s) * lp + s * shelf));
777 }
778
780 double bl_beta() const { return compression_amount_ * kBlFalloff; }
781
783 double cms_gamma() const { return compression_amount_ * kCmsStiffening; }
784
786 double excursion() const { return static_cast<double>(excursion_); }
787
789 double dynamic_fc_hz() const { return fc_dynamic_; }
790
791 int archetype_index() const { return archetype_index_; }
792 SpeakerBoxType box_type() const { return box_type_; }
793 double sample_rate() const { return sample_rate_; }
794
795private:
796 // ── Voice-coil inductance: first-order rolloff blended with a shelf ──────
797 // Leach's semi-inductive motor measures a gentler slope than a clean
798 // -6 dB/oct, because eddy-current losses make the coil's impedance rise
799 // sub-linearly. Blending a fraction of a high-shelved copy of the input
800 // back in floors the decay instead of letting it fall forever.
801 SampleType inductance(SampleType x) {
802 const SampleType rolled = inductance_lp_.process_lowpass(x);
803 const SampleType shelved = semi_shelf_.process(x);
804 return (SampleType{1} - semi_) * rolled + semi_ * shelved;
805 }
806
807 void retarget_smoothed() {
808 q_smoothed_.set_target(static_cast<SampleType>(resonance_q()));
809 presence_db_.set_target(static_cast<SampleType>(presence_shelf_db()));
810 proximity_db_.set_target(static_cast<SampleType>(proximity_gain_db()));
811 offaxis_hz_.set_target(static_cast<SampleType>(offaxis_corner_hz()));
812 }
813
816 void update_coefficients() {
817 const auto fs = static_cast<SampleType>(sample_rate_);
818
819 // Advance the smoothers by one control block so their ramps track
820 // real time even though coefficients only move at this cadence.
821 const double q = static_cast<double>(q_smoothed_.current());
822 const double treble = static_cast<double>(treble_hz_.current());
823 const double presence = static_cast<double>(presence_db_.current());
824 const double proximity = static_cast<double>(proximity_db_.current());
825 const double offaxis = static_cast<double>(offaxis_hz_.current());
826 q_smoothed_.skip(kControlBlock);
827 treble_hz_.skip(kControlBlock);
828 presence_db_.skip(kControlBlock);
829 proximity_db_.skip(kControlBlock);
830 offaxis_hz_.skip(kControlBlock);
831
832 // Cms(x) stiffening from the PREVIOUS block's peak excursion: raising
833 // fc at high excursion reduces the displacement that produced it, so
834 // the loop is compressive negative feedback and cannot run away.
835 const double peak = static_cast<double>(excursion_peak_);
836 fc_dynamic_ = resonance_fc_hz() * (1.0 + cms_gamma() * peak * peak);
837 excursion_peak_ = SampleType{0};
838 resonance_.set_coefficients(fc_dynamic_, q, sample_rate_);
839
840 beta_ = static_cast<SampleType>(bl_beta());
841 excursion_scale_ = static_cast<SampleType>(1.0 / kExcursionReference);
842
843 inductance_lp_.set_cutoff(static_cast<SampleType>(treble));
844 semi_ = static_cast<SampleType>(semi_inductance_);
845 semi_shelf_.set_coefficients(Biquad::Type::high_shelf, static_cast<SampleType>(treble),
846 kShelfQ, fs,
847 static_cast<SampleType>(kSemiInductanceShelfDb));
848
849 for (int i = 0; i < kBreakupModeCount; ++i) {
850 const auto& mode = breakup_mode(i);
851 breakup_[i].set_coefficients(
852 Biquad::Type::peaking, static_cast<SampleType>(breakup_mode_hz(i)),
853 static_cast<SampleType>(mode.q), fs,
854 static_cast<SampleType>(mode.gain_db * breakup_amount_));
855 }
856
857 baffle_.set_coefficients(Biquad::Type::high_shelf,
858 static_cast<SampleType>(baffle_step_hz()), kShelfQ, fs,
859 static_cast<SampleType>(kBaffleStepDb * diffraction_amount_));
860 const double ripple1 = ripple_hz();
861 ripple_[0].set_coefficients(Biquad::Type::peaking, static_cast<SampleType>(ripple1),
862 static_cast<SampleType>(kRipple1Q), fs,
863 static_cast<SampleType>(kRipple1Db * diffraction_amount_));
864 ripple_[1].set_coefficients(Biquad::Type::peaking, static_cast<SampleType>(2.0 * ripple1),
865 static_cast<SampleType>(kRipple2Q), fs,
866 static_cast<SampleType>(kRipple2Db * diffraction_amount_));
867
868 dipole_hp_.set_cutoff(static_cast<SampleType>(dipole_hz()));
869 dipole_notch_.set_coefficients(
870 Biquad::Type::peaking, static_cast<SampleType>(dipole_hz() * kDipoleNotchRatio),
871 static_cast<SampleType>(kDipoleNotchQ), fs,
872 static_cast<SampleType>(kDipoleNotchDb));
873
874 proximity_.set_coefficients(Biquad::Type::low_shelf,
875 static_cast<SampleType>(kProximityCornerHz), kShelfQ, fs,
876 static_cast<SampleType>(proximity));
877 air_loss_.set_coefficients(Biquad::Type::high_shelf,
878 static_cast<SampleType>(kAirLossCornerHz), kShelfQ, fs,
879 static_cast<SampleType>(air_loss_db()));
880 presence_.set_coefficients(Biquad::Type::high_shelf,
881 static_cast<SampleType>(kPresenceCornerHz), kShelfQ, fs,
882 static_cast<SampleType>(presence));
883 offaxis_lp_.set_cutoff(static_cast<SampleType>(offaxis));
884 }
885
889 static constexpr SampleType kShelfQ = static_cast<SampleType>(0.70710678118654752440);
890
891 using Biquad = BiquadT<SampleType>;
892
893 // ── Configuration ────────────────────────────────────────────────────────
894 double sample_rate_ = 48000.0;
895 int archetype_index_ = 0;
897 double box_volume_l_ = kBoxVolumeLDefault;
898 double resonance_trim_ = 0.0;
899 double q_override_ = 0.0;
900 double breakup_amount_ = kConeBreakupAmountDefault / 100.0;
901 double treble_hz_target_ = kTrebleRolloffHzDefault;
902 double semi_inductance_ = kSemiInductanceDefault;
903 double drive_db_ = kDriveDbDefault;
904 double compression_amount_ = kCompressionAmountDefault / 100.0;
905 double mic_distance_cm_ = kMicDistanceCmDefault;
906 double mic_position_ = kMicPositionPctDefault / 100.0;
907 double mic_axis_deg_ = 0.0;
908 double diffraction_amount_ = kDiffractionAmountDefault / 100.0;
909 double out_trim_db_ = 0.0;
910
911 // ── Smoothed control values ──────────────────────────────────────────────
912 SmoothedValue<SampleType> drive_lin_{SampleType{1}};
913 SmoothedValue<SampleType> out_trim_lin_{SampleType{1}};
914 SmoothedValue<SampleType> treble_hz_{static_cast<SampleType>(kTrebleRolloffHzDefault)};
915 SmoothedValue<SampleType> q_smoothed_{SampleType{1}};
916 SmoothedValue<SampleType> presence_db_{SampleType{0}};
917 SmoothedValue<SampleType> proximity_db_{SampleType{0}};
918 SmoothedValue<SampleType> offaxis_hz_{static_cast<SampleType>(kOffAxisOnAxisHz)};
919
920 // ── Audio state ──────────────────────────────────────────────────────────
921 DcBlocker<SampleType> dc_;
922 detail::DualTapResonatorT<SampleType> resonance_;
923 TptFilterT<SampleType> inductance_lp_;
924 Biquad semi_shelf_;
925 Biquad breakup_[kBreakupModeCount];
926 Biquad baffle_;
927 Biquad ripple_[2];
928 TptFilterT<SampleType> dipole_hp_;
929 Biquad dipole_notch_;
930 Biquad proximity_;
931 Biquad air_loss_;
932 Biquad presence_;
933 TptFilterT<SampleType> offaxis_lp_;
934
935 SampleType beta_ = SampleType{0};
936 SampleType semi_ = static_cast<SampleType>(kSemiInductanceDefault);
937 SampleType excursion_scale_ = static_cast<SampleType>(1.0 / kExcursionReference);
938 SampleType excursion_ = SampleType{0};
939 SampleType excursion_peak_ = SampleType{0};
940 double fc_dynamic_ = 0.0;
941 int control_counter_ = 0;
942};
943
946
962
963} // namespace pulp::signal
SampleType process(SampleType input)
Definition biquad.hpp:211
Physics-based speaker + cabinet + microphone emulator.
Definition speaker_cabinet.hpp:228
static constexpr double kOffAxisFactorMax
Definition speaker_cabinet.hpp:369
SampleType process(SampleType input)
Definition speaker_cabinet.hpp:582
static constexpr double kCmsStiffeningMax
Definition speaker_cabinet.hpp:315
static constexpr double kProximityGainKMin
Definition speaker_cabinet.hpp:329
static constexpr double kDipoleNotchDbMin
Definition speaker_cabinet.hpp:406
static constexpr double kProximityReferenceCm
Definition speaker_cabinet.hpp:331
static constexpr double kDipoleNotchDbMax
Definition speaker_cabinet.hpp:407
static constexpr double kProximityGainK
Definition speaker_cabinet.hpp:328
static constexpr double kSmoothingMs
Definition speaker_cabinet.hpp:427
static constexpr double kSmoothingMsMin
Definition speaker_cabinet.hpp:428
static constexpr double kQResonanceMax
Definition speaker_cabinet.hpp:269
static constexpr double kRipple1Q
Definition speaker_cabinet.hpp:384
double air_loss_db() const
Air/distance HF trim, in dB (negative), scaling with distance.
Definition speaker_cabinet.hpp:743
static constexpr double kBlFalloffMax
Definition speaker_cabinet.hpp:309
static constexpr double kConeBreakupAmountDefault
Definition speaker_cabinet.hpp:272
void set_cone_breakup_amount(double percent)
Definition speaker_cabinet.hpp:537
void set_box_volume_l(double litres)
Definition speaker_cabinet.hpp:520
static constexpr double kCmsStiffeningMin
Definition speaker_cabinet.hpp:314
static constexpr double kOffAxisOnAxisHzMin
Definition speaker_cabinet.hpp:365
double ripple_hz() const
First edge-diffraction ripple c / (2 * d_edge).
Definition speaker_cabinet.hpp:713
static constexpr double kDipoleNotchRatioMin
Definition speaker_cabinet.hpp:400
static constexpr double kWorstCaseGain
Definition speaker_cabinet.hpp:441
static constexpr double kResonanceTrimSemitonesMax
Definition speaker_cabinet.hpp:264
void set_resonance_trim_semitones(double semitones)
Definition speaker_cabinet.hpp:525
static constexpr double kEdgeDistanceMMax
Definition speaker_cabinet.hpp:383
static constexpr double kOutputTrimDbMin
Definition speaker_cabinet.hpp:410
static constexpr double kMicDistanceCmDefault
Definition speaker_cabinet.hpp:318
static constexpr double kBreakupGainDbMax
Definition speaker_cabinet.hpp:475
double dynamic_fc_hz() const
The resonance cutoff currently in force, including Cms stiffening.
Definition speaker_cabinet.hpp:789
void set_driver_archetype(int index)
Definition speaker_cabinet.hpp:512
static constexpr double kMicDistanceCmMax
Definition speaker_cabinet.hpp:320
static constexpr double kEdgeDistanceM
Definition speaker_cabinet.hpp:381
static constexpr double kRippleQMax
Definition speaker_cabinet.hpp:389
static constexpr double kDipoleNotchRatio
Definition speaker_cabinet.hpp:399
static constexpr double kDcBlockerHzMin
Definition speaker_cabinet.hpp:423
void set_mic_position_pct(double percent)
Definition speaker_cabinet.hpp:560
static constexpr double kMicAxisDegMax
Definition speaker_cabinet.hpp:371
static constexpr double kDipoleNotchRatioMax
Definition speaker_cabinet.hpp:401
static constexpr double kDriveDbMin
Definition speaker_cabinet.hpp:292
static constexpr double kDipoleNotchQ
Definition speaker_cabinet.hpp:402
static constexpr double kProximityReferenceCmMin
Definition speaker_cabinet.hpp:332
static constexpr double kTrebleRolloffHzMax
Definition speaker_cabinet.hpp:277
static constexpr double kExcursionReferenceMax
Definition speaker_cabinet.hpp:303
static constexpr double kSmoothingMsMax
Definition speaker_cabinet.hpp:429
static constexpr int kControlBlock
Definition speaker_cabinet.hpp:434
static constexpr double kPresenceCornerHz
Definition speaker_cabinet.hpp:357
double resonance_peak_db() const
Peak magnitude of the second-order high-pass at the resonance, in dB: Q / sqrt(1 - 1/(4 Q^2)).
Definition speaker_cabinet.hpp:691
void set_box_type(SpeakerBoxType type)
Definition speaker_cabinet.hpp:516
static constexpr double kSemiInductanceShelfDb
Definition speaker_cabinet.hpp:286
double excursion() const
Normalized excursion produced by the most recent sample.
Definition speaker_cabinet.hpp:786
static constexpr double kCompressionAmountDefault
Definition speaker_cabinet.hpp:296
static constexpr double kDipoleNotchQMax
Definition speaker_cabinet.hpp:404
static constexpr double kRippleQMin
Definition speaker_cabinet.hpp:388
double dipole_hz() const
Open-back dipole corner c / (2 * D).
Definition speaker_cabinet.hpp:716
static constexpr double kPresenceCornerHzMax
Definition speaker_cabinet.hpp:359
static constexpr double kResonanceTrimSemitonesMin
Definition speaker_cabinet.hpp:263
static constexpr double kPresenceCapDb
Definition speaker_cabinet.hpp:353
double breakup_mode_hz(int index) const
Centre of breakup mode index, anchored on the archetype.
Definition speaker_cabinet.hpp:719
static constexpr double kRippleDbMax
Definition speaker_cabinet.hpp:390
static constexpr double kBoxVolumeLMax
Definition speaker_cabinet.hpp:260
static constexpr double kOffAxisFactorMin
Definition speaker_cabinet.hpp:368
static const BreakupMode & breakup_mode(int index)
[design parameter] shape.
Definition speaker_cabinet.hpp:462
static constexpr double kProximityCeilingDbMin
Definition speaker_cabinet.hpp:335
static constexpr double kTrebleRolloffHzMin
Definition speaker_cabinet.hpp:276
static constexpr double kProximityCornerHzMax
Definition speaker_cabinet.hpp:339
void set_diffraction_amount(double percent)
Definition speaker_cabinet.hpp:570
static constexpr double kSemiInductanceMin
Definition speaker_cabinet.hpp:284
static constexpr double kProximityCornerHz
Definition speaker_cabinet.hpp:337
static constexpr double kDiffractionAmountDefault
Definition speaker_cabinet.hpp:391
double resonance_fc_hz() const
Box resonance fc = fs * sqrt(1 + alpha), shifted by the voicing trim.
Definition speaker_cabinet.hpp:676
double cms_gamma() const
Cms(x) stiffening coefficient gamma actually in force.
Definition speaker_cabinet.hpp:783
static constexpr double kBlFalloffMin
Definition speaker_cabinet.hpp:308
static constexpr double kExcursionReference
Definition speaker_cabinet.hpp:301
static constexpr double kPresenceDbMax
Definition speaker_cabinet.hpp:355
static constexpr double kProximityReferenceCmMax
Definition speaker_cabinet.hpp:333
static constexpr double kAirLossCornerHzMax
Definition speaker_cabinet.hpp:348
static constexpr double kPresenceCornerHzMin
Definition speaker_cabinet.hpp:358
static constexpr double kSemiInductanceShelfDbMin
Definition speaker_cabinet.hpp:287
static constexpr double kSemiInductanceShelfDbMax
Definition speaker_cabinet.hpp:288
static constexpr double kSpeedOfSoundMs
Definition speaker_cabinet.hpp:255
static constexpr double kEdgeDistanceMMin
Definition speaker_cabinet.hpp:382
static constexpr double kDcBlockerHzMax
Definition speaker_cabinet.hpp:424
static constexpr double kBaffleStepDb
Definition speaker_cabinet.hpp:376
static constexpr int kControlBlockMax
Definition speaker_cabinet.hpp:436
static constexpr double kPresenceEdgeDb
Definition speaker_cabinet.hpp:354
static constexpr double kDriveDbDefault
Definition speaker_cabinet.hpp:291
SpeakerBoxType box_type() const
Definition speaker_cabinet.hpp:792
static constexpr double kSemiInductanceMax
Definition speaker_cabinet.hpp:285
void set_mic_distance_cm(double cm)
Definition speaker_cabinet.hpp:555
static constexpr double kOffAxisFactor
Definition speaker_cabinet.hpp:367
static constexpr double kBlFalloff
Definition speaker_cabinet.hpp:307
static constexpr double kDcBlockerHz
Definition speaker_cabinet.hpp:422
static constexpr double kMicDistanceCmMin
Definition speaker_cabinet.hpp:319
static constexpr double kAirLossDbPerCm
Definition speaker_cabinet.hpp:343
double resonance_peak_hz() const
Frequency of that peak: fc / sqrt(1 - 1/(2 Q^2)).
Definition speaker_cabinet.hpp:700
static constexpr double kRipple2Q
Definition speaker_cabinet.hpp:386
static constexpr double kOffAxisOnAxisHz
Definition speaker_cabinet.hpp:364
void prepare(double sample_rate)
Recompute every corner coefficient for a new sample rate. No allocation.
Definition speaker_cabinet.hpp:480
static constexpr double kRipple1Db
Definition speaker_cabinet.hpp:385
static constexpr int kControlBlockMin
Definition speaker_cabinet.hpp:435
static constexpr double kOffAxisOnAxisHzMax
Definition speaker_cabinet.hpp:366
double presence_shelf_db() const
Cap-to-cone traverse shelf, lerped bright-to-dark.
Definition speaker_cabinet.hpp:738
void set_drive_db(double db)
Definition speaker_cabinet.hpp:546
static constexpr double kOutputTrimDbMax
Definition speaker_cabinet.hpp:411
double proximity_gain_db() const
Proximity shelf gain, boost-only by construction and clamped at the ceiling: k_prox * (1/d - 1/d_ref)...
Definition speaker_cabinet.hpp:732
static constexpr double kCmsStiffening
Definition speaker_cabinet.hpp:313
static constexpr double kProximityGainKMax
Definition speaker_cabinet.hpp:330
static constexpr double kAirLossCornerHz
Definition speaker_cabinet.hpp:346
double compliance_ratio() const
Compliance ratio alpha = Vas / Vb.
Definition speaker_cabinet.hpp:670
static constexpr double kDipoleNotchQMin
Definition speaker_cabinet.hpp:403
void set_q_resonance(double q)
Override the computed box Qtc.
Definition speaker_cabinet.hpp:532
int archetype_index() const
Definition speaker_cabinet.hpp:791
static constexpr double kDipolePathM
Definition speaker_cabinet.hpp:396
static constexpr double kBoxVolumeLMin
Definition speaker_cabinet.hpp:259
void set_treble_rolloff_hz(double hz)
Definition speaker_cabinet.hpp:541
static constexpr double kDriveDbMax
Definition speaker_cabinet.hpp:293
static constexpr double kMicAxisDegMin
Definition speaker_cabinet.hpp:370
double bl_beta() const
BL(x) falloff coefficient beta actually in force.
Definition speaker_cabinet.hpp:780
static constexpr double kQResonanceMin
Definition speaker_cabinet.hpp:268
static constexpr double kBreakupQMin
Definition speaker_cabinet.hpp:473
double sample_rate() const
Definition speaker_cabinet.hpp:793
static constexpr double kExcursionReferenceMin
Definition speaker_cabinet.hpp:302
static constexpr double kPresenceDbMin
Definition speaker_cabinet.hpp:356
void process(const SampleType *in, SampleType *out, int n)
Definition speaker_cabinet.hpp:629
static constexpr double kProximityCornerHzMin
Definition speaker_cabinet.hpp:338
static constexpr double kBoxVolumeLDefault
Definition speaker_cabinet.hpp:258
static constexpr double kAirLossDbPerCmMin
Definition speaker_cabinet.hpp:344
static constexpr double kDipolePathMMin
Definition speaker_cabinet.hpp:397
double inductance_magnitude_db(double hz) const
Exact magnitude of the voice-coil inductance stage ALONE, in dB, from the coefficients currently in f...
Definition speaker_cabinet.hpp:754
int latency_samples() const
The chain is a cascade of minimum-phase filters with no block buffering and no oversampler,...
Definition speaker_cabinet.hpp:658
static constexpr double kMicPositionPctDefault
Definition speaker_cabinet.hpp:360
static constexpr double kProximityCeilingDb
Definition speaker_cabinet.hpp:334
static constexpr double kBreakupRatioMin
Definition speaker_cabinet.hpp:471
void set_mic_axis_deg(double degrees)
Definition speaker_cabinet.hpp:565
static constexpr double kProximityCeilingDbMax
Definition speaker_cabinet.hpp:336
static constexpr double kTrebleRolloffHzDefault
Definition speaker_cabinet.hpp:275
double baffle_step_hz() const
Baffle-step transition c / (pi * W).
Definition speaker_cabinet.hpp:708
double worst_case_gain() const
Sum of per-stage peak magnitudes across the parameter space.
Definition speaker_cabinet.hpp:662
void set_compression_amount(double percent)
Definition speaker_cabinet.hpp:551
static constexpr double kAirLossDbPerCmMax
Definition speaker_cabinet.hpp:345
static constexpr double kSemiInductanceDefault
Definition speaker_cabinet.hpp:283
double resonance_q() const
Box Qtc = Qts * sqrt(1 + alpha), unless overridden.
Definition speaker_cabinet.hpp:683
static constexpr double kRipple2Db
Definition speaker_cabinet.hpp:387
static constexpr double kDipoleNotchDb
Definition speaker_cabinet.hpp:405
static constexpr double kBreakupQMax
Definition speaker_cabinet.hpp:474
static constexpr double kAirLossCornerHzMin
Definition speaker_cabinet.hpp:347
double offaxis_corner_hz() const
Off-axis corner f_on * (1 - k_ax * sin(angle)).
Definition speaker_cabinet.hpp:724
static constexpr int kBreakupModeCount
Definition speaker_cabinet.hpp:445
static constexpr double kDipolePathMMax
Definition speaker_cabinet.hpp:398
void set_output_trim_db(double db)
Definition speaker_cabinet.hpp:574
static constexpr double kBreakupRatioMax
Definition speaker_cabinet.hpp:472
void reset()
Zero every filter and excursion state.
Definition speaker_cabinet.hpp:635
static constexpr int kArchetypeCount
Definition speaker_cabinet.hpp:232
static const SpeakerDriverArchetype & archetype(int index)
[design parameter] table; see SpeakerDriverArchetype and the model scope above.
Definition speaker_cabinet.hpp:237
Dual-tap TPT state-variable resonator (Zavalishin topology, identical to SvfT) returning the highpass...
Definition speaker_cabinet.hpp:168
void set_coefficients(double cutoff_hz, double q, double sample_rate)
Definition speaker_cabinet.hpp:175
Taps tick(SampleType input)
Definition speaker_cabinet.hpp:190
void reset()
Definition speaker_cabinet.hpp:201
Round-2 effect-wave vocabulary.
Definition analysis_frontends.hpp:50
T linear_to_db(T linear)
Linear gain → amplitude dB, floored at kDbFloorAmplitude so silence maps to a finite −200 dB rather t...
Definition units.hpp:79
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
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
SpeakerModelT< float > SpeakerModel
Definition speaker_cabinet.hpp:944
@ c
Definition spectrum_trace.hpp:35
SpeakerModelT< double > SpeakerModel64
Definition speaker_cabinet.hpp:945
SpeakerBoxType
Cabinet topology.
Definition speaker_cabinet.hpp:136
@ sealed
Definition speaker_cabinet.hpp:136
@ open_back
Definition speaker_cabinet.hpp:136
@ v2
Medium vibrato.
Definition scanner_vibrato.hpp:24
@ v3
Full vibrato.
Definition scanner_vibrato.hpp:25
@ v1
Shallow vibrato — scanner output only.
Definition scanner_vibrato.hpp:23
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
@ q
Definition input_events.hpp:41
One driver's published-form Thiele-Small set plus the two geometry figures the cabinet and breakup st...
Definition speaker_cabinet.hpp:145
double f_breakup_hz
anchor frequency of the dimensionless mode shape
Definition speaker_cabinet.hpp:150
const char * name
Definition speaker_cabinet.hpp:146
double baffle_width_m
sets the baffle-step transition
Definition speaker_cabinet.hpp:151
double fs_hz
free-air resonance
Definition speaker_cabinet.hpp:147
double vas_litres
equivalent suspension compliance volume
Definition speaker_cabinet.hpp:149
double qts
total Q at fs
Definition speaker_cabinet.hpp:148
One DIMENSIONLESS breakup mode: a ratio to the archetype's anchor frequency plus a Q and a gain.
Definition speaker_cabinet.hpp:452
double q
Definition speaker_cabinet.hpp:454
double gain_db
Definition speaker_cabinet.hpp:455
double ratio
Definition speaker_cabinet.hpp:453
Definition speaker_cabinet.hpp:170
SampleType lowpass
Definition speaker_cabinet.hpp:172
SampleType highpass
Definition speaker_cabinet.hpp:171
First-order topology-preserving transform (TPT) filter.
The real-unit vocabulary shared by every DSP block in the catalog.