A rotary speaker cabinet: two rotors, a crossover, a stereo mic pair and a reflective box. More...
#include <leslie_rotary.hpp>
Public Types | |
| using | Speed = LeslieSpeed |
Public Member Functions | |
| LeslieRotaryT () | |
| void | prepare (double sample_rate) |
| Sizes the delay lines and filter state. May allocate. | |
| void | reset () |
| Returns to a fresh, silent cabinet already turning at the selected speed. | |
| void | discard_history () noexcept |
| Constant-time audio fault recovery; retains controls and makes all old delay samples unreachable without walking the prepared allocations. | |
| void | set_speed (Speed speed) |
| The switch, and the most important control on the cabinet. | |
| Speed | speed () const |
| void | set_horn_fast_hz (double hz) |
| void | set_horn_slow_hz (double hz) |
| void | set_drum_fast_hz (double hz) |
| void | set_drum_slow_hz (double hz) |
| void | set_horn_accel_s (double seconds) |
| Seconds for the horn to close kSettleFraction of a speed change. | |
| void | set_horn_decel_s (double seconds) |
| void | set_drum_accel_s (double seconds) |
| void | set_drum_decel_s (double seconds) |
| void | set_crossover_hz (double hz) |
| void | set_horn_radius_m (double metres) |
| Doppler depth lives here, not in a depth knob: the peak fractional pitch shift is r·ω/c, so the SAME radius gives an order of magnitude different depth at chorale and at tremolo purely from the rate. | |
| void | set_drum_radius_m (double metres) |
| void | set_mic_distance_m (double metres) |
| Mic distance trades Doppler against tremolo. | |
| 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, 90° puts them in quadrature for the widest ping-pong. | |
| void | set_am_depth (double depth) |
| void | set_dir_depth_db (double db) |
| void | set_drum_dir_depth_db (double db) |
| void | set_dir_corner_hz (double hz) |
| void | set_d_bias_ms (double ms) |
| void | set_reflection_db (double db) |
| The "box". | |
| void | set_num_reflections (int taps) |
| void | set_refl_delay_ms (double ms) |
| void | set_refl_spacing_ms (double ms) |
| void | set_refl_corner_hz (double hz) |
| void | set_drift_cents (double cents) |
| Motor wow, in cents of rate (1σ). 0 disables the walk. | |
| void | set_seed (std::uint32_t seed) |
| Construction or preset choice only, never automated (series law 2). | |
| void | set_mix (double mix01) |
| double | horn_rate_hz () const |
| The rotors' CURRENT rates, mid-ramp included. | |
| double | drum_rate_hz () const |
| double | target_horn_hz () const |
| The rates the switch is currently asking for. | |
| double | target_drum_hz () const |
| double | horn_phase () const |
| Rotor phases in cycles, [0, 1). | |
| double | drum_phase () const |
| double | mic_face_offset () const |
| The facing phase of each mic, in cycles either side of the cabinet axis. | |
| 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, so a test can predict the Doppler rather than only observe it. | |
| double | drum_delay_seconds (double phase_cycles, bool right_mic) const |
| void | process (SampleType in_left, SampleType in_right, SampleType &out_left, SampleType &out_right) |
| void | process (SampleType mono_in, SampleType &out_left, SampleType &out_right) |
| void | process_block (const SampleType *in_left, const SampleType *in_right, SampleType *out_left, SampleType *out_right, int n) |
| void | process_block (const SampleType *mono_in, SampleType *out_left, SampleType *out_right, int n) |
Static Public Member Functions | |
| static constexpr int | latency_samples () |
| Zero, and honestly so: every delay in this model is a modelled acoustic path, not processing latency to be compensated. | |
| static int | worst_case_delay_samples (double sample_rate) |
| The longest read any legal parameter set can produce, in samples. | |
Static Public Attributes | |
| static constexpr double | kHornSlowHz = 0.80 |
| [design parameter] horn chorale rate 0.80 Hz (≈48 RPM), range 0.6 .. 1.0. | |
| static constexpr double | kHornFastHz = 6.70 |
| [design parameter] horn tremolo rate 6.70 Hz (≈400 RPM), range 5.5 .. 7.5. | |
| static constexpr double | kDrumSlowHz = 0.60 |
| [design parameter] drum chorale rate 0.60 Hz (≈36 RPM), range 0.5 .. 0.9. | |
| static constexpr double | kDrumFastHz = 5.70 |
| [design parameter] drum tremolo rate 5.70 Hz (≈342 RPM), range 4.5 .. 6.5. | |
| static constexpr double | kHornAccelS = 1.0 |
| [design parameter] horn spin-up 1.0 s, range 0.3 .. 3.0 s. | |
| static constexpr double | kHornDecelS = 1.0 |
| [design parameter] horn spin-down 1.0 s, range 0.3 .. 3.0 s. | |
| static constexpr double | kDrumAccelS = 3.0 |
| [design parameter] drum spin-up 3.0 s, range 1.0 .. 8.0 s. | |
| static constexpr double | kDrumDecelS = 5.0 |
| [design parameter] drum spin-down 5.0 s, range 1.0 . | |
| static constexpr double | kSettleFraction = 0.95 |
| How much of a speed change the quoted accel/decel time covers. | |
| static constexpr double | kHornRadiusM = 0.18 |
| [design parameter] horn rotation radius 0.18 m, range 0.10 .. 0.25 m. | |
| static constexpr double | kDrumRadiusM = 0.12 |
| [design parameter] drum rotation radius 0.12 m, range 0.08 . | |
| static constexpr double | kMicDistanceM = 1.0 |
| [design parameter] mic distance 1.0 m, range 0.3 . | |
| static constexpr double | kMicAngleDeg = 45.0 |
| [design parameter] mic pair included angle 45°, range 0 . | |
| static constexpr double | kDBiasMs = 0.5 |
| Constant offset added to every modelled delay so the read pointer stays strictly inside the buffer even at the rotor phase where the path length is at its minimum. | |
| static constexpr double | kCrossoverHz = 800.0 |
| [design parameter] crossover 800 Hz, range 700 . | |
| static constexpr double | kAmDepth = 0.5 |
| [design parameter] tremolo depth 0.5, range 0 . | |
| static constexpr double | kDirDepthDb = 6.0 |
| [design parameter] horn brightness sweep 6 dB, range 0 .. 12 dB. | |
| static constexpr double | kDrumDirDepthDb = 2.0 |
| [design parameter] drum brightness sweep 2 dB, range 0 . | |
| static constexpr double | kDirCornerHz = 2000.0 |
| [design parameter] directivity shelf corner 2 kHz, range 1 .. 4 kHz. | |
| static constexpr int | kNumReflections = 2 |
| [design parameter] 2 reflection taps, range 1 .. 4. | |
| static constexpr double | kReflDelayMs = 3.5 |
| [design parameter] first reflection 3.5 ms, range 2.5 .. 6.0 ms. | |
| static constexpr double | kReflSpacingMs = 1.5 |
| [design parameter] inter-tap spacing 1.5 ms, range 1.0 .. 3.0 ms. | |
| static constexpr double | kReflCornerHz = 3000.0 |
| [design parameter] reflection low-pass 3 kHz, range 1 .. 8 kHz. | |
| static constexpr double | kReflLevelDb = -12.0 |
| [design parameter] reflection level −12 dB, range −60 .. −6 dB. | |
| static constexpr double | kReflectionOffDb = -60.0 |
| At or below this the reflections are switched off EXACTLY rather than left at a vanishing gain, so "reflections off" is a state a test can assert against rather than a very small number. | |
| static constexpr double | kDriftCents = 3.0 |
| [design parameter] motor wow 3 cents (1σ) of rate, range 0 . | |
| static constexpr double | kDriftCorrelationS = 4.0 |
| [design parameter] drift correlation time 4 s, range 0.5 . | |
| static constexpr std::uint32_t | kDefaultSeed = 0x1e511e00u |
| Fixed seed installed by reset(). | |
| static constexpr double | kMaxRadiusM = 0.25 |
| static constexpr double | kMaxDBiasMs = 1.0 |
| static constexpr double | kMaxReflDelayMs = 6.0 |
| static constexpr double | kMaxReflSpacingMs = 3.0 |
| static constexpr int | kMaxReflections = 4 |
| static constexpr double | kWorstCaseGain = 6.0 |
| The measured constructive-sum bound, in linear gain. | |
A rotary speaker cabinet: two rotors, a crossover, a stereo mic pair and a reflective box.
Mono or stereo in, always stereo out.
| using pulp::signal::LeslieRotaryT< SampleType >::Speed = LeslieSpeed |
|
inline |
|
inlinenoexcept |
Constant-time audio fault recovery; retains controls and makes all old delay samples unreachable without walking the prepared allocations.
|
inline |
|
inline |
|
inline |
|
inline |
The delay a rotor presents at a given phase and mic, in seconds — the modelled acoustic path, so a test can predict the Doppler rather than only observe it.
|
inline |
Rotor phases in cycles, [0, 1).
|
inline |
The rotors' CURRENT rates, mid-ramp included.
Exposed because the inertia asymmetry is the module's headline behaviour and a meter that shows the two rotors converging at different times is worth having.
|
inlinestaticconstexpr |
Zero, and honestly so: every delay in this model is a modelled acoustic path, not processing latency to be compensated.
Reporting the horn's bias delay as latency would have a host pull the whole cabinet earlier in time, which is not what a cabinet does.
|
inline |
The facing phase of each mic, in cycles either side of the cabinet axis.
|
inline |
Sizes the delay lines and filter state. May allocate.
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
Returns to a fresh, silent cabinet already turning at the selected speed.
The rotors are snapped to their target rate rather than started from rest, because reset() means "no history", and a spin-up ramp is history — one that would contaminate the first several seconds of every render made from a fresh state. Inertia is exercised by CHANGING speed, which is where it belongs and where the suite measures it.
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
Motor wow, in cents of rate (1σ). 0 disables the walk.
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
Seconds for the horn to close kSettleFraction of a speed change.
|
inline |
|
inline |
|
inline |
Doppler depth lives here, not in a depth knob: the peak fractional pitch shift is r·ω/c, so the SAME radius gives an order of magnitude different depth at chorale and at tremolo purely from the rate.
That is why the two speeds are voiced by speed alone and never by re-tuning a depth curve (series law 7 — one scale-invariant shape, not two fitted ones).
|
inline |
|
inline |
Stereo width IS the mic angle: 0° puts both mics on the same facing phase and collapses the image, 90° puts them in quadrature for the widest ping-pong.
The tasteful default reads as two mics on the cabinet, which is exactly the classic recording setup.
|
inline |
Mic distance trades Doppler against tremolo.
A close mic sweeps through more of the beam, so it reads as more tremolo and relatively less pitch movement; a far mic is the reverse. "Subtle vintage" is a smaller radius with a farther mic; "in your face" is a larger radius up close.
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
The "box".
Up around −8 dB the short cabinet taps comb the horn into the boxy resonance a close mic on a real cabinet picks up; at the −12 dB default it is a hint of room. A 3.5 ms first reflection cancels the direct signal at 1/(2·0.0035) ≈ 143 Hz and its odd multiples, which is where that boxiness actually comes from.
|
inline |
Construction or preset choice only, never automated (series law 2).
|
inline |
The switch, and the most important control on the cabinet.
Automate THIS, not the individual rate parameters. The whole character lives in the transition: because the horn's inertia is shorter than the drum's, a change to tremolo arrives as a bright shimmer first with the low end smearing in behind it over the next couple of seconds, and a change back coasts down with the drum trailing furthest. Jumping the rates directly skips the effect entirely.
|
inline |
|
inline |
|
inline |
The rates the switch is currently asking for.
|
inlinestatic |
The longest read any legal parameter set can produce, in samples.
Public so the buffer-sizing test can check the shipped sizing against the declared ranges rather than against the current settings.
|
staticconstexpr |
[design parameter] tremolo depth 0.5, range 0 .
. 0.9. Gain runs from 1 − depth facing away to 1 facing the mic, so it is bounded at or below unity and cannot build.
|
staticconstexpr |
[design parameter] crossover 800 Hz, range 700 .
. 900 Hz. The documented Leslie crossover network is around 800 Hz; the exact value is a constant, not a citation.
|
staticconstexpr |
Constant offset added to every modelled delay so the read pointer stays strictly inside the buffer even at the rotor phase where the path length is at its minimum.
[design parameter] default 0.5 ms, range 0.2 .. 1.0 ms.
|
staticconstexpr |
Fixed seed installed by reset().
A construction choice, never a parameter (series law 2).
|
staticconstexpr |
[design parameter] directivity shelf corner 2 kHz, range 1 .. 4 kHz.
|
staticconstexpr |
[design parameter] horn brightness sweep 6 dB, range 0 .. 12 dB.
|
staticconstexpr |
[design parameter] motor wow 3 cents (1σ) of rate, range 0 .
. 10 cents. 0 disables the walk entirely. This is the "not perfectly regulated motor" life; it is off unless asked for, because a default-on stochastic process makes every other test's expectation a distribution.
|
staticconstexpr |
[design parameter] drift correlation time 4 s, range 0.5 .
. 30 s. Slow enough to read as wow rather than as noise on the rate.
|
staticconstexpr |
[design parameter] drum spin-up 3.0 s, range 1.0 .. 8.0 s.
|
staticconstexpr |
[design parameter] drum spin-down 5.0 s, range 1.0 .
. 12.0 s. Longer than its spin-up because a heavy drum coasts.
|
staticconstexpr |
[design parameter] drum brightness sweep 2 dB, range 0 .
. 6 dB. Weaker than the horn's — a bass baffle is far less directional.
|
staticconstexpr |
[design parameter] drum tremolo rate 5.70 Hz (≈342 RPM), range 4.5 .. 6.5.
|
staticconstexpr |
[design parameter] drum rotation radius 0.12 m, range 0.08 .
. 0.18 m. Smaller than the horn's, which is why the low end wobbles more gently than the top shimmers.
|
staticconstexpr |
[design parameter] drum chorale rate 0.60 Hz (≈36 RPM), range 0.5 .. 0.9.
|
staticconstexpr |
[design parameter] horn spin-up 1.0 s, range 0.3 .. 3.0 s.
|
staticconstexpr |
[design parameter] horn spin-down 1.0 s, range 0.3 .. 3.0 s.
|
staticconstexpr |
[design parameter] horn tremolo rate 6.70 Hz (≈400 RPM), range 5.5 .. 7.5.
|
staticconstexpr |
[design parameter] horn rotation radius 0.18 m, range 0.10 .. 0.25 m.
|
staticconstexpr |
[design parameter] horn chorale rate 0.80 Hz (≈48 RPM), range 0.6 .. 1.0.
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
|
staticconstexpr |
[design parameter] mic pair included angle 45°, range 0 .
. 90°. 0 is mono-ish (both mics see the same facing phase), 90 is wide ping-pong.
|
staticconstexpr |
[design parameter] mic distance 1.0 m, range 0.3 .
. 3.0 m. Trades Doppler against tremolo: a closer mic sweeps through more of the beam.
|
staticconstexpr |
[design parameter] 2 reflection taps, range 1 .. 4.
|
staticconstexpr |
[design parameter] reflection low-pass 3 kHz, range 1 .. 8 kHz.
|
staticconstexpr |
[design parameter] first reflection 3.5 ms, range 2.5 .. 6.0 ms.
|
staticconstexpr |
At or below this the reflections are switched off EXACTLY rather than left at a vanishing gain, so "reflections off" is a state a test can assert against rather than a very small number.
[design parameter] default −60 dB, range −80 .. −40 dB.
|
staticconstexpr |
[design parameter] reflection level −12 dB, range −60 .. −6 dB.
|
staticconstexpr |
[design parameter] inter-tap spacing 1.5 ms, range 1.0 .. 3.0 ms.
|
staticconstexpr |
How much of a speed change the quoted accel/decel time covers.
The inertia ramp is a one-pole, which approaches its target asymptotically and therefore has no finite "arrival" — but "the horn spins up in about a second" is a statement about arrival, not about a time constant. This constant is the bridge: the shipped seconds are the time to close this fraction of the distance, and the one-pole τ handed to SlewLimiterT is seconds / ln(1/(1−fraction)). At 0.95 that divisor is almost exactly 3, so a 3 s drum accel is a 1 s τ.
Reading the seconds as τ directly instead would make the drum take three times as long to arrive as its own datasheet-style figure claims, which is the kind of quiet factor-of-three that only shows up when someone compares against a real cabinet. [design parameter] default 0.95, range 0.90 .. 0.99.
|
staticconstexpr |
The measured constructive-sum bound, in linear gain.
There is no feedback path, so this is not a loop gain — it is the worst case of horn + drum + reflections + dry all peaking together, which for a feedforward chain is a finite static number. The suite's parameter sweep measures the actual maximum across the whole space and asserts it stays under this; the Forge registry cites the sweep, not this ceiling.
The measured maximum is 5.12 (+14.2 dB), and most of it is one term: the directivity shelf is a BOOST, running from 0 dB off-axis up to dir_depth_db on-axis, so at that parameter's 12 dB maximum the shelf alone contributes a factor of four before horn, drum, reflections and dry are summed. Anyone budgeting this number from the topology alone will underestimate it badly unless they count that shelf. [design parameter] default 6.0 (+15.6 dB), range 2 .. 12.