Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
leslie_rotary.hpp
Go to the documentation of this file.
1#pragma once
2
6#include <pulp/signal/detail/leslie_common.hpp>
8#include <pulp/signal/lfo.hpp>
10#include <pulp/signal/rng.hpp>
13#include <pulp/signal/units.hpp>
14
15#include <algorithm>
16#include <cmath>
17#include <cstdint>
18
19namespace pulp::signal {
20
22enum class LeslieSpeed : std::uint8_t {
26};
27
28// ═══════════════════════════════════════════════════════════════════════════
29// Leslie rotary speaker
30// ═══════════════════════════════════════════════════════════════════════════
31
34template <typename SampleType = float>
36public:
38
39 // ── Rotor rates ───────────────────────────────────────────────────────
40 // Honest-gap note, applying to this whole block: that the horn runs faster
41 // than the drum, and that both have a slow and a fast setting, is
42 // documented Leslie behaviour. The exact rates are engineering defaults
43 // calibrated to the documented RPM ranges, not values lifted from a
44 // verified primary source — which is why every one of them is a parameter.
45
47 static constexpr double kHornSlowHz = 0.80;
49 static constexpr double kHornFastHz = 6.70;
51 static constexpr double kDrumSlowHz = 0.60;
53 static constexpr double kDrumFastHz = 5.70;
54
55 // ── Inertia ───────────────────────────────────────────────────────────
56
58 static constexpr double kHornAccelS = 1.0;
60 static constexpr double kHornDecelS = 1.0;
62 static constexpr double kDrumAccelS = 3.0;
65 static constexpr double kDrumDecelS = 5.0;
66
82 static constexpr double kSettleFraction = 0.95;
83
84 // ── Geometry ──────────────────────────────────────────────────────────
85
87 static constexpr double kHornRadiusM = 0.18;
91 static constexpr double kDrumRadiusM = 0.12;
94 static constexpr double kMicDistanceM = 1.0;
97 static constexpr double kMicAngleDeg = 45.0;
98
103 static constexpr double kDBiasMs = 0.5;
104
105 // ── Band split, beam and brightness ───────────────────────────────────
106
110 static constexpr double kCrossoverHz = 800.0;
111
115 static constexpr double kAmDepth = 0.5;
116
118 static constexpr double kDirDepthDb = 6.0;
121 static constexpr double kDrumDirDepthDb = 2.0;
123 static constexpr double kDirCornerHz = 2000.0;
124
125 // ── Cabinet ───────────────────────────────────────────────────────────
126
128 static constexpr int kNumReflections = 2;
130 static constexpr double kReflDelayMs = 3.5;
132 static constexpr double kReflSpacingMs = 1.5;
134 static constexpr double kReflCornerHz = 3000.0;
136 static constexpr double kReflLevelDb = -12.0;
141 static constexpr double kReflectionOffDb = -60.0;
142
143 // ── Drift ─────────────────────────────────────────────────────────────
144
149 static constexpr double kDriftCents = 3.0;
152 static constexpr double kDriftCorrelationS = 4.0;
155 static constexpr std::uint32_t kDefaultSeed = 0x1e511e00u;
156
157 // ── Parameter-range maxima, used for buffer sizing ────────────────────
158 // `prepare` sizes from these rather than from the current settings, so no
159 // later `set_*` can ask for a read the buffer cannot serve (series law 6).
160
161 static constexpr double kMaxRadiusM = 0.25;
162 static constexpr double kMaxDBiasMs = 1.0;
163 static constexpr double kMaxReflDelayMs = 6.0;
164 static constexpr double kMaxReflSpacingMs = 3.0;
165 static constexpr int kMaxReflections = 4;
166
182 static constexpr double kWorstCaseGain = 6.0;
183
185 // The rotor phase accumulators are shared `LfoT`s; their SHAPE output is
186 // discarded (see `run`), so the cheapest shape is chosen to be thrown
187 // away rather than the default sine's transcendental.
188 horn_phase_.set_wave(LfoWave::saw_up);
189 drum_phase_.set_wave(LfoWave::saw_up);
190 update();
191 }
192
194 void prepare(double sample_rate) {
195 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
196 const int samples = worst_case_delay_samples(sample_rate_);
197 horn_line_.prepare(samples);
198 drum_line_.prepare(samples);
199 horn_phase_.prepare(sample_rate_);
200 drum_phase_.prepare(sample_rate_);
201 horn_inertia_.prepare(sample_rate_);
202 drum_inertia_.prepare(sample_rate_);
203 horn_drift_.prepare(sample_rate_);
204 drum_drift_.prepare(sample_rate_);
205 for (auto& f : horn_shelf_) f.prepare(static_cast<SampleType>(sample_rate_));
206 for (auto& f : drum_shelf_) f.prepare(static_cast<SampleType>(sample_rate_));
207 horn_refl_lp_.prepare(static_cast<SampleType>(sample_rate_));
208 drum_refl_lp_.prepare(static_cast<SampleType>(sample_rate_));
209 // The blocker only has to clear subsonic offset; its corner sits below
210 // the lowest rotor rate so it cannot eat the modulation itself.
211 blocker_.set_pole(static_cast<SampleType>(
212 1.0 - kDcCornerHz * leslie_detail::kTwoPi / sample_rate_));
213 update();
214 reset();
215 }
216
225 void reset() {
226 horn_line_.reset();
227 drum_line_.reset();
228 horn_phase_.reset();
229 drum_phase_.reset();
230 horn_inertia_.set_immediate(target_horn_hz_);
231 drum_inertia_.set_immediate(target_drum_hz_);
232 horn_drift_.set_seed(kDefaultSeed);
233 drum_drift_.set_seed(kDefaultSeed ^ 0x9e3779b9u);
234 horn_drift_.reset();
235 drum_drift_.reset();
236 for (auto& f : horn_shelf_) f.reset();
237 for (auto& f : drum_shelf_) f.reset();
238 horn_refl_lp_.reset();
239 drum_refl_lp_.reset();
240 crossover_.reset();
241 blocker_.reset();
242 }
243
246 void discard_history() noexcept {
247 horn_line_.discard_history();
248 drum_line_.discard_history();
249 horn_phase_.reset();
250 drum_phase_.reset();
251 horn_inertia_.set_immediate(target_horn_hz_);
252 drum_inertia_.set_immediate(target_drum_hz_);
253 horn_drift_.set_seed(kDefaultSeed);
254 drum_drift_.set_seed(kDefaultSeed ^ 0x9e3779b9u);
255 horn_drift_.reset();
256 drum_drift_.reset();
257 for (auto& f : horn_shelf_) f.reset();
258 for (auto& f : drum_shelf_) f.reset();
259 horn_refl_lp_.reset();
260 drum_refl_lp_.reset();
261 crossover_.reset();
262 blocker_.reset();
263 }
264
265 // ── Controls ──────────────────────────────────────────────────────────
266
276 speed_ = speed;
277 update();
278 }
279
280 Speed speed() const { return speed_; }
281
282 void set_horn_fast_hz(double hz) {
283 if (!std::isfinite(hz)) return;
284 horn_fast_hz_ = std::clamp(hz, 0.0, 20.0);
285 update();
286 }
287 void set_horn_slow_hz(double hz) {
288 if (!std::isfinite(hz)) return;
289 horn_slow_hz_ = std::clamp(hz, 0.0, 20.0);
290 update();
291 }
292 void set_drum_fast_hz(double hz) {
293 if (!std::isfinite(hz)) return;
294 drum_fast_hz_ = std::clamp(hz, 0.0, 20.0);
295 update();
296 }
297 void set_drum_slow_hz(double hz) {
298 if (!std::isfinite(hz)) return;
299 drum_slow_hz_ = std::clamp(hz, 0.0, 20.0);
300 update();
301 }
302
304 void set_horn_accel_s(double seconds) {
305 if (!std::isfinite(seconds)) return;
306 horn_accel_s_ = std::max(seconds, 0.0);
307 update();
308 }
309 void set_horn_decel_s(double seconds) {
310 if (!std::isfinite(seconds)) return;
311 horn_decel_s_ = std::max(seconds, 0.0);
312 update();
313 }
314 void set_drum_accel_s(double seconds) {
315 if (!std::isfinite(seconds)) return;
316 drum_accel_s_ = std::max(seconds, 0.0);
317 update();
318 }
319 void set_drum_decel_s(double seconds) {
320 if (!std::isfinite(seconds)) return;
321 drum_decel_s_ = std::max(seconds, 0.0);
322 update();
323 }
324
325 void set_crossover_hz(double hz) {
326 if (!std::isfinite(hz)) return;
327 crossover_hz_ = std::clamp(hz, 20.0, 20000.0);
328 update();
329 }
330
337 void set_horn_radius_m(double metres) {
338 if (!std::isfinite(metres)) return;
339 horn_radius_m_ = std::clamp(metres, 0.0, kMaxRadiusM);
340 }
341 void set_drum_radius_m(double metres) {
342 if (!std::isfinite(metres)) return;
343 drum_radius_m_ = std::clamp(metres, 0.0, kMaxRadiusM);
344 }
345
350 void set_mic_distance_m(double metres) {
351 if (!std::isfinite(metres)) return;
352 mic_distance_m_ = std::clamp(metres, kMinMicDistanceM, 10.0);
353 }
354
359 void set_mic_angle_deg(double degrees) {
360 if (!std::isfinite(degrees)) return;
361 mic_angle_deg_ = std::clamp(degrees, 0.0, 180.0);
362 update();
363 }
364
365 void set_am_depth(double depth) {
366 if (!std::isfinite(depth)) return;
367 am_depth_ = std::clamp(depth, 0.0, 0.9);
368 }
369
370 void set_dir_depth_db(double db) {
371 if (!std::isfinite(db)) return;
372 dir_depth_db_ = std::clamp(db, 0.0, 24.0);
373 }
374 void set_drum_dir_depth_db(double db) {
375 if (!std::isfinite(db)) return;
376 drum_dir_depth_db_ = std::clamp(db, 0.0, 24.0);
377 }
378
379 void set_dir_corner_hz(double hz) {
380 if (!std::isfinite(hz)) return;
381 dir_corner_hz_ = std::clamp(hz, 20.0, 20000.0);
382 update();
383 }
384
385 void set_d_bias_ms(double ms) {
386 if (!std::isfinite(ms)) return;
387 d_bias_ms_ = std::clamp(ms, 0.0, kMaxDBiasMs);
388 }
389
395 void set_reflection_db(double db) {
396 if (!std::isfinite(db)) return;
397 reflection_db_ = std::clamp(db, -120.0, 0.0);
398 update();
399 }
400
401 void set_num_reflections(int taps) {
402 num_reflections_ = std::clamp(taps, 0, kMaxReflections);
403 update();
404 }
405 void set_refl_delay_ms(double ms) {
406 if (!std::isfinite(ms)) return;
407 refl_delay_ms_ = std::clamp(ms, 0.0, kMaxReflDelayMs);
408 update();
409 }
410 void set_refl_spacing_ms(double ms) {
411 if (!std::isfinite(ms)) return;
412 refl_spacing_ms_ = std::clamp(ms, 0.0, kMaxReflSpacingMs);
413 update();
414 }
415 void set_refl_corner_hz(double hz) {
416 if (!std::isfinite(hz)) return;
417 refl_corner_hz_ = std::clamp(hz, 20.0, 20000.0);
418 update();
419 }
420
422 void set_drift_cents(double cents) {
423 if (!std::isfinite(cents)) return;
424 drift_cents_ = std::clamp(cents, 0.0, 50.0);
425 update();
426 }
427
429 void set_seed(std::uint32_t seed) {
430 horn_drift_.set_seed(seed);
431 drum_drift_.set_seed(seed ^ 0x9e3779b9u);
432 }
433
434 void set_mix(double mix01) {
435 if (!std::isfinite(mix01)) return;
436 mix_ = std::clamp(mix01, 0.0, 1.0);
437 }
438
439 // ── Observables ───────────────────────────────────────────────────────
440
444 double horn_rate_hz() const { return horn_inertia_.value(); }
445 double drum_rate_hz() const { return drum_inertia_.value(); }
446
448 double target_horn_hz() const { return target_horn_hz_; }
449 double target_drum_hz() const { return target_drum_hz_; }
450
452 double horn_phase() const { return horn_phase_.phase(); }
453 double drum_phase() const { return drum_phase_.phase(); }
454
456 double mic_face_offset() const { return mic_face_offset_; }
457
461 double horn_delay_seconds(double phase_cycles, bool right_mic) const {
462 return rotor_delay_seconds(phase_cycles, horn_radius_m_, right_mic);
463 }
464 double drum_delay_seconds(double phase_cycles, bool right_mic) const {
465 return rotor_delay_seconds(phase_cycles, drum_radius_m_, right_mic);
466 }
467
472 static constexpr int latency_samples() { return 0; }
473
474 // ── Processing ────────────────────────────────────────────────────────
475
476 void process(SampleType in_left, SampleType in_right, SampleType& out_left,
477 SampleType& out_right) {
478 if (!std::isfinite(static_cast<double>(in_left)) ||
479 !std::isfinite(static_cast<double>(in_right))) {
481 out_left = out_right = SampleType{0};
482 return;
483 }
484 // One cabinet has one input. The dry path stays stereo so `mix` blends
485 // against what actually arrived rather than against a folded-down copy.
486 const double mono = 0.5 * (static_cast<double>(in_left) + static_cast<double>(in_right));
487 double wet_l = 0.0;
488 double wet_r = 0.0;
489 run(mono, wet_l, wet_r);
490 out_left = static_cast<SampleType>(snap_to_zero(
491 mix_ * wet_l + (1.0 - mix_) * static_cast<double>(in_left)));
492 out_right = static_cast<SampleType>(snap_to_zero(
493 mix_ * wet_r + (1.0 - mix_) * static_cast<double>(in_right)));
494 }
495
496 void process(SampleType mono_in, SampleType& out_left, SampleType& out_right) {
497 process(mono_in, mono_in, out_left, out_right);
498 }
499
500 void process_block(const SampleType* in_left, const SampleType* in_right,
501 SampleType* out_left, SampleType* out_right, int n) {
502 for (int i = 0; i < n; ++i) process(in_left[i], in_right[i], out_left[i], out_right[i]);
503 }
504
505 void process_block(const SampleType* mono_in, SampleType* out_left, SampleType* out_right,
506 int n) {
507 for (int i = 0; i < n; ++i) process(mono_in[i], out_left[i], out_right[i]);
508 }
509
513 static int worst_case_delay_samples(double sample_rate) {
514 // The rotor path spans `L_max − L_min = (d+r) − (d−r) = 2r` — the mic
515 // distance cancels, so the worst case is at MAXIMUM radius and is
516 // independent of how far away the mic is.
517 const double doppler_ms =
518 2.0 * kMaxRadiusM / leslie_detail::kSpeedOfSound * 1000.0 + kMaxDBiasMs;
519 const double reflection_ms =
520 kMaxReflDelayMs + static_cast<double>(kMaxReflections - 1) * kMaxReflSpacingMs;
521 const double longest_ms = std::max(doppler_ms, reflection_ms);
522 return static_cast<int>(std::ceil(longest_ms * 0.001 * sample_rate)) +
523 leslie_detail::kInterpolatorMargin;
524 }
525
526private:
530 static constexpr double kDcCornerHz = 5.0;
531
536 static constexpr double kMinMicDistanceM = 0.3;
537
545 double rotor_delay_seconds(double phase_cycles, double radius_m, bool right_mic) const {
546 const double face = right_mic ? mic_face_offset_ : -mic_face_offset_;
547 const double c = std::cos(leslie_detail::kTwoPi * (phase_cycles - face));
548 return path_delay_seconds(c, radius_m);
549 }
550
551 double path_delay_seconds(double cos_face, double radius_m) const {
552 const double d = mic_distance_m_;
553 const double length =
554 std::sqrt(d * d + radius_m * radius_m - 2.0 * d * radius_m * cos_face);
555 return (length - (d - radius_m)) / leslie_detail::kSpeedOfSound + d_bias_ms_ * 0.001;
556 }
557
567 static double read_cubic(const DelayLineT<SampleType>& line, double delay_samples) {
568 const int index = static_cast<int>(std::floor(delay_samples));
569 const double frac = delay_samples - static_cast<double>(index);
570 return Interpolator::lagrange(frac,
571 static_cast<double>(line.read(index - 1)),
572 static_cast<double>(line.read(index)),
573 static_cast<double>(line.read(index + 1)),
574 static_cast<double>(line.read(index + 2)));
575 }
576
585 double rotor_into_mic(const DelayLineT<SampleType>& line, double phase_cycles,
586 double radius_m, double face, double dir_depth_db,
587 TptFilterT<SampleType>& shelf) const {
588 const double cos_face = std::cos(leslie_detail::kTwoPi * (phase_cycles - face));
589
590 const double delay_samples = path_delay_seconds(cos_face, radius_m) * sample_rate_;
591 double value = read_cubic(line, delay_samples);
592
593 // Beam gain: unity when the horn faces this mic, `1 − depth` when it
594 // faces away. Bounded at or below 1 and outside any loop, so there is
595 // nothing for it to build into.
596 value *= 1.0 - am_depth_ * 0.5 * (1.0 - cos_face);
597
598 // Brightness: a high shelf whose boost tracks the same facing angle,
599 // full on-axis and flat off-axis.
600 const double shelf_gain =
601 units::db_to_linear(dir_depth_db * 0.5 * (1.0 + cos_face));
602 const auto bands = shelf.process(static_cast<SampleType>(value));
603 return static_cast<double>(bands.lowpass) +
604 shelf_gain * static_cast<double>(bands.highpass);
605 }
606
613 double reflections(const DelayLineT<SampleType>& line, TptFilterT<SampleType>& lp) const {
614 if (reflection_gain_ <= 0.0 || num_reflections_ <= 0) return 0.0;
615 double sum = 0.0;
616 for (int k = 0; k < num_reflections_; ++k) {
617 sum += static_cast<double>(line.read(reflection_taps_[k]));
618 }
619 return reflection_gain_ * static_cast<double>(
620 lp.process_lowpass(static_cast<SampleType>(sum)));
621 }
622
623 void run(double mono_in, double& out_left, double& out_right) {
624 const double x = static_cast<double>(blocker_.process(static_cast<SampleType>(mono_in)));
625
626 // ── Control rate: inertia, then drift, then phase ──
627 double horn_hz = horn_inertia_.process(target_horn_hz_);
628 double drum_hz = drum_inertia_.process(target_drum_hz_);
629 if (drift_cents_ > 0.0) {
630 horn_hz *= static_cast<double>(horn_drift_.next());
631 drum_hz *= static_cast<double>(drum_drift_.next());
632 }
633 // The phase accumulator is the shared LFO, driven at the inertia-ramped
634 // rate. Its shape output is discarded — what is wanted is the phase,
635 // which two mics then read at two different facing angles, so no single
636 // shape value could serve. `saw_up` is chosen because it is the
637 // cheapest shape to evaluate and throw away.
638 horn_phase_.set_rate_hz(horn_hz);
639 drum_phase_.set_rate_hz(drum_hz);
640 (void)horn_phase_.next();
641 (void)drum_phase_.next();
642 const double theta_h = horn_phase_.phase();
643 const double theta_d = drum_phase_.phase();
644
645 // ── Band split ──
646 const auto split = crossover_.process(static_cast<SampleType>(x));
647 horn_line_.push(split.high);
648 drum_line_.push(split.low);
649
650 // ── Direct paths, two mics ──
651 const double horn_l = rotor_into_mic(horn_line_, theta_h, horn_radius_m_,
652 -mic_face_offset_, dir_depth_db_, horn_shelf_[0]);
653 const double horn_r = rotor_into_mic(horn_line_, theta_h, horn_radius_m_,
654 +mic_face_offset_, dir_depth_db_, horn_shelf_[1]);
655 const double drum_l = rotor_into_mic(drum_line_, theta_d, drum_radius_m_,
656 -mic_face_offset_, drum_dir_depth_db_,
657 drum_shelf_[0]);
658 const double drum_r = rotor_into_mic(drum_line_, theta_d, drum_radius_m_,
659 +mic_face_offset_, drum_dir_depth_db_,
660 drum_shelf_[1]);
661
662 // ── Cabinet ──
663 // The reflection taps are fixed, so both mics receive the same
664 // reflected component — which is physically right (the box radiates the
665 // same reflections in both directions) and is also what keeps the
666 // stereo image coming from the rotor geometry rather than from a
667 // decorrelated reverb.
668 const double refl = reflections(horn_line_, horn_refl_lp_) +
669 reflections(drum_line_, drum_refl_lp_);
670
671 out_left = snap_to_zero(horn_l + drum_l + refl);
672 out_right = snap_to_zero(horn_r + drum_r + refl);
673 }
674
675 void update() {
676 switch (speed_) {
677 case Speed::stop:
678 target_horn_hz_ = 0.0;
679 target_drum_hz_ = 0.0;
680 break;
681 case Speed::chorale:
682 target_horn_hz_ = horn_slow_hz_;
683 target_drum_hz_ = drum_slow_hz_;
684 break;
685 case Speed::tremolo:
686 target_horn_hz_ = horn_fast_hz_;
687 target_drum_hz_ = drum_fast_hz_;
688 break;
689 }
690
691 // Exponential mode is the one mechanical inertia actually has: a
692 // constant TIME CONSTANT, so a bigger speed change takes proportionally
693 // longer. Linear mode would cover any distance in the same wall-clock
694 // time, which is a portamento law, not a flywheel.
695 horn_inertia_.set_mode(SlewMode::exponential);
696 drum_inertia_.set_mode(SlewMode::exponential);
697 horn_inertia_.set_rise_ms(tau_ms(horn_accel_s_));
698 horn_inertia_.set_fall_ms(tau_ms(horn_decel_s_));
699 drum_inertia_.set_rise_ms(tau_ms(drum_accel_s_));
700 drum_inertia_.set_fall_ms(tau_ms(drum_decel_s_));
701
702 crossover_.set_frequency(static_cast<SampleType>(crossover_hz_),
703 static_cast<SampleType>(sample_rate_));
704
705 for (auto& f : horn_shelf_) f.set_cutoff(static_cast<SampleType>(dir_corner_hz_));
706 for (auto& f : drum_shelf_) f.set_cutoff(static_cast<SampleType>(dir_corner_hz_));
707 horn_refl_lp_.set_cutoff(static_cast<SampleType>(refl_corner_hz_));
708 drum_refl_lp_.set_cutoff(static_cast<SampleType>(refl_corner_hz_));
709
710 // Half the included angle either side of the axis: the two mics are
711 // `mic_angle_deg` apart in facing phase, which is the whole of the
712 // stereo image.
713 mic_face_offset_ = mic_angle_deg_ / 720.0;
714
715 reflection_gain_ = reflection_db_ <= kReflectionOffDb
716 ? 0.0
717 : units::db_to_linear(reflection_db_);
718 for (int k = 0; k < kMaxReflections; ++k) {
719 const double ms = refl_delay_ms_ + static_cast<double>(k) * refl_spacing_ms_;
720 reflection_taps_[k] = static_cast<int>(std::lround(ms * 0.001 * sample_rate_));
721 }
722
723 // Cents of rate wander converted to the fractional depth `DriftT`
724 // speaks: a ratio of 1 + depth, so the percentage is the ratio's excess
725 // over unity.
726 const double ratio = units::cents_to_ratio(drift_cents_);
727 horn_drift_.set_depth_percent((ratio - 1.0) * 100.0);
728 drum_drift_.set_depth_percent((ratio - 1.0) * 100.0);
729 horn_drift_.set_correlation_time(kDriftCorrelationS);
730 drum_drift_.set_correlation_time(kDriftCorrelationS);
731 }
732
735 static double tau_ms(double seconds) {
736 constexpr double divisor_guard = 1e-9;
737 const double divisor = std::max(-std::log(1.0 - kSettleFraction), divisor_guard);
738 return seconds * 1000.0 / divisor;
739 }
740
741 double sample_rate_ = 48000.0;
742
743 Speed speed_ = Speed::tremolo;
744 double horn_fast_hz_ = kHornFastHz;
745 double horn_slow_hz_ = kHornSlowHz;
746 double drum_fast_hz_ = kDrumFastHz;
747 double drum_slow_hz_ = kDrumSlowHz;
748 double horn_accel_s_ = kHornAccelS;
749 double horn_decel_s_ = kHornDecelS;
750 double drum_accel_s_ = kDrumAccelS;
751 double drum_decel_s_ = kDrumDecelS;
752 double target_horn_hz_ = kHornFastHz;
753 double target_drum_hz_ = kDrumFastHz;
754
755 double crossover_hz_ = kCrossoverHz;
756 double horn_radius_m_ = kHornRadiusM;
757 double drum_radius_m_ = kDrumRadiusM;
758 double mic_distance_m_ = kMicDistanceM;
759 double mic_angle_deg_ = kMicAngleDeg;
760 double mic_face_offset_ = kMicAngleDeg / 720.0;
761 double d_bias_ms_ = kDBiasMs;
762
763 double am_depth_ = kAmDepth;
764 double dir_depth_db_ = kDirDepthDb;
765 double drum_dir_depth_db_ = kDrumDirDepthDb;
766 double dir_corner_hz_ = kDirCornerHz;
767
768 int num_reflections_ = kNumReflections;
769 double refl_delay_ms_ = kReflDelayMs;
770 double refl_spacing_ms_ = kReflSpacingMs;
771 double refl_corner_hz_ = kReflCornerHz;
772 double reflection_db_ = kReflLevelDb;
773 double reflection_gain_ = 0.0;
774 int reflection_taps_[kMaxReflections] = {0, 0, 0, 0};
775
776 double drift_cents_ = 0.0;
777 double mix_ = 1.0;
778
779 DelayLineT<SampleType> horn_line_{};
780 DelayLineT<SampleType> drum_line_{};
781 EffectLfoT<double> horn_phase_{};
782 EffectLfoT<double> drum_phase_{};
783 SlewLimiterT<double> horn_inertia_{};
784 SlewLimiterT<double> drum_inertia_{};
785 DriftT<double> horn_drift_{};
786 DriftT<double> drum_drift_{};
787 LinkwitzRileyT<SampleType> crossover_{};
788 TptFilterT<SampleType> horn_shelf_[2]{};
789 TptFilterT<SampleType> drum_shelf_[2]{};
790 TptFilterT<SampleType> horn_refl_lp_{};
791 TptFilterT<SampleType> drum_refl_lp_{};
792 DcBlocker<SampleType> blocker_{};
793};
794
797
798} // namespace pulp::signal
A rotary speaker cabinet: two rotors, a crossover, a stereo mic pair and a reflective box.
Definition leslie_rotary.hpp:35
double drum_rate_hz() const
Definition leslie_rotary.hpp:445
void set_num_reflections(int taps)
Definition leslie_rotary.hpp:401
static constexpr double kMaxDBiasMs
Definition leslie_rotary.hpp:162
double drum_delay_seconds(double phase_cycles, bool right_mic) const
Definition leslie_rotary.hpp:464
static constexpr double kDrumSlowHz
Definition leslie_rotary.hpp:51
void set_dir_depth_db(double db)
Definition leslie_rotary.hpp:370
static constexpr double kDirDepthDb
Definition leslie_rotary.hpp:118
static constexpr double kDrumFastHz
Definition leslie_rotary.hpp:53
void set_horn_decel_s(double seconds)
Definition leslie_rotary.hpp:309
void set_speed(Speed speed)
The switch, and the most important control on the cabinet.
Definition leslie_rotary.hpp:275
static constexpr double kReflSpacingMs
Definition leslie_rotary.hpp:132
void set_drum_dir_depth_db(double db)
Definition leslie_rotary.hpp:374
static constexpr double kDBiasMs
Definition leslie_rotary.hpp:103
double horn_delay_seconds(double phase_cycles, bool right_mic) const
The delay a rotor presents at a given phase and mic, in seconds — the modelled acoustic path,...
Definition leslie_rotary.hpp:461
Speed speed() const
Definition leslie_rotary.hpp:280
static constexpr double kDriftCents
Definition leslie_rotary.hpp:149
static constexpr double kWorstCaseGain
Definition leslie_rotary.hpp:182
void set_refl_corner_hz(double hz)
Definition leslie_rotary.hpp:415
static constexpr double kCrossoverHz
Definition leslie_rotary.hpp:110
void set_mic_angle_deg(double degrees)
Stereo width IS the mic angle: 0° puts both mics on the same facing phase and collapses the image,...
Definition leslie_rotary.hpp:359
void set_horn_slow_hz(double hz)
Definition leslie_rotary.hpp:287
double horn_rate_hz() const
The rotors' CURRENT rates, mid-ramp included.
Definition leslie_rotary.hpp:444
double drum_phase() const
Definition leslie_rotary.hpp:453
static constexpr double kHornFastHz
Definition leslie_rotary.hpp:49
static constexpr double kReflLevelDb
Definition leslie_rotary.hpp:136
static constexpr double kDrumRadiusM
Definition leslie_rotary.hpp:91
static constexpr double kMaxReflSpacingMs
Definition leslie_rotary.hpp:164
void set_reflection_db(double db)
The "box".
Definition leslie_rotary.hpp:395
void set_drum_accel_s(double seconds)
Definition leslie_rotary.hpp:314
double horn_phase() const
Rotor phases in cycles, [0, 1).
Definition leslie_rotary.hpp:452
void process_block(const SampleType *in_left, const SampleType *in_right, SampleType *out_left, SampleType *out_right, int n)
Definition leslie_rotary.hpp:500
void set_mic_distance_m(double metres)
Mic distance trades Doppler against tremolo.
Definition leslie_rotary.hpp:350
static constexpr double kReflDelayMs
Definition leslie_rotary.hpp:130
void set_drift_cents(double cents)
Motor wow, in cents of rate (1σ). 0 disables the walk.
Definition leslie_rotary.hpp:422
void set_refl_spacing_ms(double ms)
Definition leslie_rotary.hpp:410
void set_drum_decel_s(double seconds)
Definition leslie_rotary.hpp:319
double mic_face_offset() const
The facing phase of each mic, in cycles either side of the cabinet axis.
Definition leslie_rotary.hpp:456
static constexpr double kAmDepth
Definition leslie_rotary.hpp:115
void set_horn_fast_hz(double hz)
Definition leslie_rotary.hpp:282
static constexpr double kHornSlowHz
Definition leslie_rotary.hpp:47
static constexpr double kReflCornerHz
Definition leslie_rotary.hpp:134
static constexpr int latency_samples()
Zero, and honestly so: every delay in this model is a modelled acoustic path, not processing latency ...
Definition leslie_rotary.hpp:472
void set_refl_delay_ms(double ms)
Definition leslie_rotary.hpp:405
double target_horn_hz() const
The rates the switch is currently asking for.
Definition leslie_rotary.hpp:448
double target_drum_hz() const
Definition leslie_rotary.hpp:449
static constexpr double kMaxRadiusM
Definition leslie_rotary.hpp:161
static constexpr double kDrumDirDepthDb
Definition leslie_rotary.hpp:121
void set_am_depth(double depth)
Definition leslie_rotary.hpp:365
static constexpr double kHornAccelS
Definition leslie_rotary.hpp:58
LeslieSpeed Speed
Definition leslie_rotary.hpp:37
void set_horn_accel_s(double seconds)
Seconds for the horn to close kSettleFraction of a speed change.
Definition leslie_rotary.hpp:304
void set_mix(double mix01)
Definition leslie_rotary.hpp:434
static constexpr int kMaxReflections
Definition leslie_rotary.hpp:165
void process(SampleType in_left, SampleType in_right, SampleType &out_left, SampleType &out_right)
Definition leslie_rotary.hpp:476
static constexpr std::uint32_t kDefaultSeed
Definition leslie_rotary.hpp:155
LeslieRotaryT()
Definition leslie_rotary.hpp:184
static constexpr int kNumReflections
Definition leslie_rotary.hpp:128
static constexpr double kDirCornerHz
Definition leslie_rotary.hpp:123
void process_block(const SampleType *mono_in, SampleType *out_left, SampleType *out_right, int n)
Definition leslie_rotary.hpp:505
void reset()
Returns to a fresh, silent cabinet already turning at the selected speed.
Definition leslie_rotary.hpp:225
static int worst_case_delay_samples(double sample_rate)
The longest read any legal parameter set can produce, in samples.
Definition leslie_rotary.hpp:513
void set_seed(std::uint32_t seed)
Construction or preset choice only, never automated (series law 2).
Definition leslie_rotary.hpp:429
static constexpr double kMicDistanceM
Definition leslie_rotary.hpp:94
void set_horn_radius_m(double metres)
Doppler depth lives here, not in a depth knob: the peak fractional pitch shift is r·ω/c,...
Definition leslie_rotary.hpp:337
void set_drum_radius_m(double metres)
Definition leslie_rotary.hpp:341
static constexpr double kHornDecelS
Definition leslie_rotary.hpp:60
void prepare(double sample_rate)
Sizes the delay lines and filter state. May allocate.
Definition leslie_rotary.hpp:194
void set_drum_fast_hz(double hz)
Definition leslie_rotary.hpp:292
static constexpr double kMaxReflDelayMs
Definition leslie_rotary.hpp:163
static constexpr double kMicAngleDeg
Definition leslie_rotary.hpp:97
void set_d_bias_ms(double ms)
Definition leslie_rotary.hpp:385
static constexpr double kDriftCorrelationS
Definition leslie_rotary.hpp:152
void set_dir_corner_hz(double hz)
Definition leslie_rotary.hpp:379
void discard_history() noexcept
Constant-time audio fault recovery; retains controls and makes all old delay samples unreachable with...
Definition leslie_rotary.hpp:246
static constexpr double kReflectionOffDb
Definition leslie_rotary.hpp:141
void process(SampleType mono_in, SampleType &out_left, SampleType &out_right)
Definition leslie_rotary.hpp:496
static constexpr double kHornRadiusM
Definition leslie_rotary.hpp:87
void set_crossover_hz(double hz)
Definition leslie_rotary.hpp:325
static constexpr double kSettleFraction
Definition leslie_rotary.hpp:82
static constexpr double kDrumDecelS
Definition leslie_rotary.hpp:65
void set_drum_slow_hz(double hz)
Definition leslie_rotary.hpp:297
static constexpr double kDrumAccelS
Definition leslie_rotary.hpp:62
High-quality sample interpolation for delay lines and resampling.
The house low-frequency oscillator: seven waveforms plus a continuous shape morph,...
@ f
Definition pitch.hpp:25
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
@ exponential
Definition mod_tools.hpp:106
@ c
Definition spectrum_trace.hpp:35
LeslieSpeed
The three positions of the cabinet's speed switch.
Definition leslie_rotary.hpp:22
@ chorale
Slow. A wide, gentle chorus.
Definition leslie_rotary.hpp:24
@ tremolo
Fast. The shimmering swirl.
Definition leslie_rotary.hpp:25
@ stop
Brake. Both rotors ramp to rest and the modulation freezes.
Definition leslie_rotary.hpp:23
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
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
LeslieRotaryT< float > LeslieRotary
Definition leslie_rotary.hpp:795
@ saw_up
Definition lfo_sampling.hpp:56
LeslieRotaryT< double > LeslieRotary64
Definition leslie_rotary.hpp:796
float sum(const float *data, std::size_t count) noexcept
@ line
Definition web_event_translate.hpp:116
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
Compatibility include for the canonical modulation-toolkit slew limiter and sample-and-hold implement...
static SampleType lagrange(SampleType frac, SampleType ym1, SampleType y0, SampleType y1, SampleType y2)
4-point Lagrange interpolation.
Definition interpolator.hpp:60
First-order topology-preserving transform (TPT) filter.
The real-unit vocabulary shared by every DSP block in the catalog.