6#include <pulp/signal/detail/leslie_common.hpp>
34template <
typename SampleType =
float>
195 sample_rate_ = sample_rate > 0.0 ? sample_rate : 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_));
211 blocker_.set_pole(
static_cast<SampleType
>(
212 1.0 - kDcCornerHz * leslie_detail::kTwoPi / sample_rate_));
230 horn_inertia_.set_immediate(target_horn_hz_);
231 drum_inertia_.set_immediate(target_drum_hz_);
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();
247 horn_line_.discard_history();
248 drum_line_.discard_history();
251 horn_inertia_.set_immediate(target_horn_hz_);
252 drum_inertia_.set_immediate(target_drum_hz_);
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();
283 if (!std::isfinite(hz))
return;
284 horn_fast_hz_ = std::clamp(hz, 0.0, 20.0);
288 if (!std::isfinite(hz))
return;
289 horn_slow_hz_ = std::clamp(hz, 0.0, 20.0);
293 if (!std::isfinite(hz))
return;
294 drum_fast_hz_ = std::clamp(hz, 0.0, 20.0);
298 if (!std::isfinite(hz))
return;
299 drum_slow_hz_ = std::clamp(hz, 0.0, 20.0);
305 if (!std::isfinite(seconds))
return;
306 horn_accel_s_ = std::max(seconds, 0.0);
310 if (!std::isfinite(seconds))
return;
311 horn_decel_s_ = std::max(seconds, 0.0);
315 if (!std::isfinite(seconds))
return;
316 drum_accel_s_ = std::max(seconds, 0.0);
320 if (!std::isfinite(seconds))
return;
321 drum_decel_s_ = std::max(seconds, 0.0);
326 if (!std::isfinite(hz))
return;
327 crossover_hz_ = std::clamp(hz, 20.0, 20000.0);
338 if (!std::isfinite(metres))
return;
339 horn_radius_m_ = std::clamp(metres, 0.0,
kMaxRadiusM);
342 if (!std::isfinite(metres))
return;
343 drum_radius_m_ = std::clamp(metres, 0.0,
kMaxRadiusM);
351 if (!std::isfinite(metres))
return;
352 mic_distance_m_ = std::clamp(metres, kMinMicDistanceM, 10.0);
360 if (!std::isfinite(degrees))
return;
361 mic_angle_deg_ = std::clamp(degrees, 0.0, 180.0);
366 if (!std::isfinite(depth))
return;
367 am_depth_ = std::clamp(depth, 0.0, 0.9);
371 if (!std::isfinite(db))
return;
372 dir_depth_db_ = std::clamp(db, 0.0, 24.0);
375 if (!std::isfinite(db))
return;
376 drum_dir_depth_db_ = std::clamp(db, 0.0, 24.0);
380 if (!std::isfinite(hz))
return;
381 dir_corner_hz_ = std::clamp(hz, 20.0, 20000.0);
386 if (!std::isfinite(ms))
return;
396 if (!std::isfinite(db))
return;
397 reflection_db_ = std::clamp(db, -120.0, 0.0);
406 if (!std::isfinite(ms))
return;
411 if (!std::isfinite(ms))
return;
416 if (!std::isfinite(hz))
return;
417 refl_corner_hz_ = std::clamp(hz, 20.0, 20000.0);
423 if (!std::isfinite(cents))
return;
424 drift_cents_ = std::clamp(cents, 0.0, 50.0);
430 horn_drift_.set_seed(seed);
431 drum_drift_.set_seed(seed ^ 0x9e3779b9u);
435 if (!std::isfinite(mix01))
return;
436 mix_ = std::clamp(mix01, 0.0, 1.0);
462 return rotor_delay_seconds(phase_cycles, horn_radius_m_, right_mic);
465 return rotor_delay_seconds(phase_cycles, drum_radius_m_, right_mic);
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};
486 const double mono = 0.5 * (
static_cast<double>(in_left) +
static_cast<double>(in_right));
489 run(mono, wet_l, wet_r);
491 mix_ * wet_l + (1.0 - mix_) *
static_cast<double>(in_left)));
493 mix_ * wet_r + (1.0 - mix_) *
static_cast<double>(in_right)));
496 void process(SampleType mono_in, SampleType& out_left, SampleType& out_right) {
497 process(mono_in, mono_in, out_left, out_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]);
505 void process_block(
const SampleType* mono_in, SampleType* out_left, SampleType* out_right,
507 for (
int i = 0;
i < n; ++
i)
process(mono_in[
i], out_left[
i], out_right[
i]);
517 const double doppler_ms =
519 const double reflection_ms =
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;
530 static constexpr double kDcCornerHz = 5.0;
536 static constexpr double kMinMicDistanceM = 0.3;
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);
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;
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);
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)));
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));
590 const double delay_samples = path_delay_seconds(cos_face, radius_m) * sample_rate_;
591 double value = read_cubic(line, delay_samples);
596 value *= 1.0 - am_depth_ * 0.5 * (1.0 - cos_face);
600 const double shelf_gain =
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);
613 double reflections(
const DelayLineT<SampleType>& line, TptFilterT<SampleType>& lp)
const {
614 if (reflection_gain_ <= 0.0 || num_reflections_ <= 0)
return 0.0;
616 for (
int k = 0;
k < num_reflections_; ++
k) {
617 sum +=
static_cast<double>(
line.read(reflection_taps_[k]));
619 return reflection_gain_ *
static_cast<double>(
620 lp.process_lowpass(
static_cast<SampleType
>(sum)));
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)));
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());
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();
646 const auto split = crossover_.process(
static_cast<SampleType
>(x));
647 horn_line_.push(split.high);
648 drum_line_.push(split.low);
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_,
658 const double drum_r = rotor_into_mic(drum_line_, theta_d, drum_radius_m_,
659 +mic_face_offset_, drum_dir_depth_db_,
668 const double refl = reflections(horn_line_, horn_refl_lp_) +
669 reflections(drum_line_, drum_refl_lp_);
678 target_horn_hz_ = 0.0;
679 target_drum_hz_ = 0.0;
682 target_horn_hz_ = horn_slow_hz_;
683 target_drum_hz_ = drum_slow_hz_;
686 target_horn_hz_ = horn_fast_hz_;
687 target_drum_hz_ = drum_fast_hz_;
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_));
702 crossover_.set_frequency(
static_cast<SampleType
>(crossover_hz_),
703 static_cast<SampleType
>(sample_rate_));
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_));
713 mic_face_offset_ = mic_angle_deg_ / 720.0;
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_));
727 horn_drift_.set_depth_percent((
ratio - 1.0) * 100.0);
728 drum_drift_.set_depth_percent((
ratio - 1.0) * 100.0);
735 static double tau_ms(
double seconds) {
736 constexpr double divisor_guard = 1
e-9;
737 const double divisor = std::max(-std::log(1.0 -
kSettleFraction), divisor_guard);
738 return seconds * 1000.0 / divisor;
741 double sample_rate_ = 48000.0;
743 Speed speed_ = Speed::tremolo;
773 double reflection_gain_ = 0.0;
776 double drift_cents_ = 0.0;
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_{};
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.