Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::signal::ChorusEnsembleT< SampleType > Class Template Reference

One chorus engine, four voicings. Stereo in place. More...

#include <chorus_family.hpp>

Classes

struct  Calibration
 One voicing's shipped calibration. More...
struct  JunoModeSpec
 The Juno's per-mode calibration. More...

Public Types

enum class  Voicing : std::uint8_t { ce2 , juno_ensemble , dimension_d , tri_chorus }
enum class  JunoMode : std::uint8_t { mode_I , mode_II , mode_I_plus_II }
 The Juno's three front-panel positions. More...

Public Member Functions

 ChorusEnsembleT ()
void prepare (double sample_rate)
 Sizes both delay lines for kMaxDelayMs plus the interpolation stencil, prepares the composed primitives, and resets.
void set_voicing (Voicing v)
 Switches the calibration table and rewinds every LFO phase.
Voicing voicing () const
void set_juno_mode (JunoMode m)
 Ignored unless the voicing is juno_ensemble.
JunoMode juno_mode () const
void set_rate_hz (SampleType hz)
 LFO rate in Hz, 0.05 .
double rate_hz () const
void set_depth (SampleType depth01)
 Depth macro, 0 .
void set_mix (SampleType mix01)
 Dry/wet, 0 .
void set_stereo_width (SampleType width01)
 0 .
void set_bbd_color (bool on)
 Routes every voice's tap through chardelay::BbdChannel — the clock-domain bandwidth law and compander the multi-character-delay module owns.
bool bbd_color () const
void process (SampleType *left, SampleType *right, int n)
void reset ()
void discard_history () noexcept
 Constant-time audio fault recovery.
int latency_samples () const
 0 for every voicing. See this file's latency note.
double current_delay_ms (int voice) const
 Instantaneous delay applied to voice k on the most recent sample, ms.
int voice_count () const
double bbd_bandwidth_hz () const
 Bandwidth the composed BBD stage is running at, Hz.
double bbd_stage_delay_ms () const
 Delay the BBD colour stage carries when enabled, ms. Zero when off.
double worst_case_gain () const
 Peak output gain this configuration can produce for |input| ≤ 1 at mix = 1, as an L1 (worst-case-over-all-inputs) bound.
double shelf_l1 () const
 L1 norm of the dry low-shelf: its DC gain.
double highpass_l1 () const
 L1 norm of the first-order cross-feed high-pass at the prepared rate.

Static Public Member Functions

static constexpr Calibration calibration (Voicing v)
static constexpr JunoModeSpec juno_spec (JunoMode m)

Static Public Attributes

static constexpr int kMaxVoices = 3
 Most voices any voicing uses (TriChorus). Sizes the per-voice arrays.
static constexpr std::size_t kVoiceSlots = static_cast<std::size_t>(kMaxVoices)
static constexpr int kInterpOrder = 3
 Lagrange interpolation order.
static constexpr int kGuardSamples = kInterpOrder + 1
 Samples of clearance the minimum instantaneous delay must keep from the write pointer so the kInterpOrder + 1-point stencil never reads ahead of it.
static constexpr double kMaxDelayMs = 40.0
 Storage ceiling, milliseconds.
static constexpr double kTrapK = 1.8
 Trapezoid shaping gain: trap = clamp(k · triangle, ±1).
static constexpr double kDimCornerHz = 200.0
 Corner of both the cross-feed high-pass and the complementary dry low-shelf.
static constexpr double kDimBoostDb = 1.5
 Dry low-shelf gain below kDimCornerHz.
static constexpr double kBbdColorCharacter = 1.0
 Position on chardelay::kBbdAxis used for the optional colour stage.
static constexpr double kBbdColorGuardMs = 0.5
 Milliseconds of the wet path left on the Lagrange line at the deepest modulation point when the BBD stage carries the rest.
static constexpr double kTapL1 = 1.25
 L1 norm of one modulated tap — the gain a fractional delay read can actually reach, as opposed to the unity a delay line suggests.

Detailed Description

template<typename SampleType = float>
class pulp::signal::ChorusEnsembleT< SampleType >

One chorus engine, four voicings. Stereo in place.

Member Enumeration Documentation

◆ JunoMode

template<typename SampleType = float>
enum class pulp::signal::ChorusEnsembleT::JunoMode : std::uint8_t
strong

The Juno's three front-panel positions.

The real unit's two mode switches are not mutually exclusive, so mode_I_plus_II engages both LFOs rather than selecting a third one.

Enumerator
mode_I 
mode_II 
mode_I_plus_II 

◆ Voicing

template<typename SampleType = float>
enum class pulp::signal::ChorusEnsembleT::Voicing : std::uint8_t
strong
Enumerator
ce2 
juno_ensemble 
dimension_d 
tri_chorus 

Constructor & Destructor Documentation

◆ ChorusEnsembleT()

template<typename SampleType = float>
pulp::signal::ChorusEnsembleT< SampleType >::ChorusEnsembleT ( )
inline

Member Function Documentation

◆ bbd_bandwidth_hz()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::bbd_bandwidth_hz ( ) const
inline

Bandwidth the composed BBD stage is running at, Hz.

Zero when the colour stage is off.

◆ bbd_color()

template<typename SampleType = float>
bool pulp::signal::ChorusEnsembleT< SampleType >::bbd_color ( ) const
inline

◆ bbd_stage_delay_ms()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::bbd_stage_delay_ms ( ) const
inline

Delay the BBD colour stage carries when enabled, ms. Zero when off.

◆ calibration()

template<typename SampleType = float>
constexpr Calibration pulp::signal::ChorusEnsembleT< SampleType >::calibration ( Voicing v)
inlinestaticconstexpr

◆ current_delay_ms()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::current_delay_ms ( int voice) const
inline

Instantaneous delay applied to voice k on the most recent sample, ms.

◆ discard_history()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::discard_history ( )
inlinenoexcept

Constant-time audio fault recovery.

Public reset still physically clears storage; this path makes the old samples unreachable and resumes from logical silence without walking the prepared delay allocations.

◆ highpass_l1()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::highpass_l1 ( ) const
inline

L1 norm of the first-order cross-feed high-pass at the prepared rate.

◆ juno_mode()

template<typename SampleType = float>
JunoMode pulp::signal::ChorusEnsembleT< SampleType >::juno_mode ( ) const
inline

◆ juno_spec()

template<typename SampleType = float>
constexpr JunoModeSpec pulp::signal::ChorusEnsembleT< SampleType >::juno_spec ( JunoMode m)
inlinestaticconstexpr

◆ latency_samples()

template<typename SampleType = float>
int pulp::signal::ChorusEnsembleT< SampleType >::latency_samples ( ) const
inline

0 for every voicing. See this file's latency note.

◆ prepare()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::prepare ( double sample_rate)
inline

Sizes both delay lines for kMaxDelayMs plus the interpolation stencil, prepares the composed primitives, and resets.

The only allocating call.

◆ process()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::process ( SampleType * left,
SampleType * right,
int n )
inline

◆ rate_hz()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::rate_hz ( ) const
inline

◆ reset()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::reset ( )
inline

◆ set_bbd_color()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_bbd_color ( bool on)
inline

Routes every voice's tap through chardelay::BbdChannel — the clock-domain bandwidth law and compander the multi-character-delay module owns.

Substitution, stated deliberately. The spec asks for a CharacterDelayT configured to Character::bbd; this holds BbdChannel instead. BbdChannel is that character's device model — the composition is verbatim — while CharacterDelayT is the whole 2 s feedback delay engine wrapped around it, with reverse, tape and diffusion buffers costing megabytes per instance, three instances here, and no API to act as a pure colouring stage (its wet path is its own delay line, which would double every tap's delay).

The split. The BBD is a clocked delay, so it cannot colour without delaying. It is given the fixed sub-delay d_center − d_depth − kBbdColorGuardMs and the Lagrange line carries the remainder including all of the modulation, so the total wet delay stays exactly on the calibration table and every delay, phase and latency property measured with the colour off holds with it on. Driving the BBD's clock from the instantaneous delay instead would be the more literal reading, but it buys nothing measurable: the bandwidth law clamp(stages / t / 3, 300 Hz, 10 kHz) sits on its 10 kHz ceiling at every point of every voicing's sweep (the slowest case is the CE-2 at its 18 ms extreme: 1024 / 0.018 / 3 = 19.0 kHz, still nearly twice the ceiling), so instantaneous and centre evaluation are provably equal here, and a control-rate clock would only add a staircase to the pitch.

◆ set_depth()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_depth ( SampleType depth01)
inline

Depth macro, 0 .

. 1, over the voicing's d_depth. set_depth(1) is exactly the calibration table's excursion.

◆ set_juno_mode()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_juno_mode ( JunoMode m)
inline

Ignored unless the voicing is juno_ensemble.

Rewinds LFO phases, since the mode changes both rates and the delay window.

◆ set_mix()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_mix ( SampleType mix01)
inline

Dry/wet, 0 .

. 1. Note that the wet side is the voicing's whole mix matrix, which carries its own dry term (all four units blend inside the circuit). So mix crossfades bypass against the full circuit, not dry against wet-only, and the dry never disappears at mix = 1.

◆ set_rate_hz()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_rate_hz ( SampleType hz)
inline

LFO rate in Hz, 0.05 .

. 10. Ignored by juno_ensemble, whose three modes run at their own measured fixed rates — that fixed-rate behaviour is the Juno sound, not an omission.

◆ set_stereo_width()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_stereo_width ( SampleType width01)
inline

0 .

. 1. Scales the Dimension D's cross-mix term and the TriChorus's centre-voice split. No effect on ce2 (mono) or juno_ensemble, whose width is fixed by the inverted-phase topology itself.

◆ set_voicing()

template<typename SampleType = float>
void pulp::signal::ChorusEnsembleT< SampleType >::set_voicing ( Voicing v)
inline

Switches the calibration table and rewinds every LFO phase.

Adopts the new voicing's shipped rate: call set_rate_hz after set_voicing, not before, or the table default wins.

◆ shelf_l1()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::shelf_l1 ( ) const
inline

L1 norm of the dry low-shelf: its DC gain.

◆ voice_count()

template<typename SampleType = float>
int pulp::signal::ChorusEnsembleT< SampleType >::voice_count ( ) const
inline

◆ voicing()

template<typename SampleType = float>
Voicing pulp::signal::ChorusEnsembleT< SampleType >::voicing ( ) const
inline

◆ worst_case_gain()

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::worst_case_gain ( ) const
inline

Peak output gain this configuration can produce for |input| ≤ 1 at mix = 1, as an L1 (worst-case-over-all-inputs) bound.

This module has no feedback path — every voicing is dry plus N feedforward taps — so the bound is a closed-form sum, and Forge's worst_case_gain cites it rather than estimating (series law 8).

Two terms are larger than they look, which is why this is computed and not written down as 1 + N:

  • each tap carries kTapL1 = 1.25, not 1;
  • the Dimension D's cross-feed high-pass carries 2 / (1 + tan(π f_c / f_s)) — just under 2, not the unity its passband gain suggests. Passband gain is a steady-state sinusoidal figure; a peak bound needs the impulse response's L1 norm, and for a first-order high-pass those differ by a factor of almost two. Its companion low-shelf is better behaved: a non-negative impulse response makes that one's L1 norm exactly its DC gain.

The sample-rate dependence is real and comes from that high-pass term.

Member Data Documentation

◆ kBbdColorCharacter

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kBbdColorCharacter = 1.0
staticconstexpr

Position on chardelay::kBbdAxis used for the optional colour stage.

[design parameter] default 1.0, range 0 .. 1. 1.0 selects the 1024-stage knot — the MN3007 family, which is the device the CE-2 actually carries — along with that knot's drive and clock-jitter amounts.

◆ kBbdColorGuardMs

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kBbdColorGuardMs = 0.5
staticconstexpr

Milliseconds of the wet path left on the Lagrange line at the deepest modulation point when the BBD stage carries the rest.

Keeps the line's read distance clear of kGuardSamples at every sample rate this module supports. [design parameter] default 0.5 ms (24 samples at 48 kHz), range 0.2 .. 2.0 ms.

◆ kDimBoostDb

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kDimBoostDb = 1.5
staticconstexpr

Dry low-shelf gain below kDimCornerHz.

[design parameter] default +1.5 dB, range 0.5 .. 3.0 dB. Honest gap, as above.

◆ kDimCornerHz

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kDimCornerHz = 200.0
staticconstexpr

Corner of both the cross-feed high-pass and the complementary dry low-shelf.

One constant, because the shelf exists precisely to restore what the high-passed cross-feed cancels — a mismatched pair would leave a hole or a bump at the crossover. [design parameter] default 200 Hz, range 100 .. 400 Hz. Honest gap: the SDD-320's actual corner is not published.

◆ kGuardSamples

template<typename SampleType = float>
int pulp::signal::ChorusEnsembleT< SampleType >::kGuardSamples = kInterpOrder + 1
staticconstexpr

Samples of clearance the minimum instantaneous delay must keep from the write pointer so the kInterpOrder + 1-point stencil never reads ahead of it.

[design parameter] default 4, range 2 .. 4 — tracks kInterpOrder + 1.

◆ kInterpOrder

template<typename SampleType = float>
int pulp::signal::ChorusEnsembleT< SampleType >::kInterpOrder = 3
staticconstexpr

Lagrange interpolation order.

[design parameter] default 3, range 1 (linear) .. 3. 3 is what ships; the guard below tracks it.

◆ kMaxDelayMs

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kMaxDelayMs = 40.0
staticconstexpr

Storage ceiling, milliseconds.

Not a musical parameter: it is sized so that any retune inside the calibration tables' declared ranges fits without reallocating (the widest is the CE-2's d_center 25 ms + d_depth 10 ms = 35 ms). [design parameter] default 40 ms, range 35 .. 200 ms.

◆ kMaxVoices

template<typename SampleType = float>
int pulp::signal::ChorusEnsembleT< SampleType >::kMaxVoices = 3
staticconstexpr

Most voices any voicing uses (TriChorus). Sizes the per-voice arrays.

◆ kTapL1

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kTapL1 = 1.25
staticconstexpr

L1 norm of one modulated tap — the gain a fractional delay read can actually reach, as opposed to the unity a delay line suggests.

A delay line's own L1 norm is 1, but the tap is a delay line read through the 4-point Lagrange kernel, and that kernel's coefficients are not all non-negative. At a half-sample offset they are (−1/16, 9/16, 9/16, −1/16), whose absolute sum is 20/16 = 1.25 — the maximum over the fractional interval, and exactly attainable by an input whose signs match. Any bound that treats a modulated tap as unity gain is 25 % low; the acceptance suite recomputes this number directly from the shipped kernel rather than trusting the algebra. [derived constant, not a design parameter: it is a property of kInterpOrder = 3, and moving to linear interpolation would make it 1.]

◆ kTrapK

template<typename SampleType = float>
double pulp::signal::ChorusEnsembleT< SampleType >::kTrapK = 1.8
staticconstexpr

Trapezoid shaping gain: trap = clamp(k · triangle, ±1).

The dwell fraction at each modulation extreme is 1 − 1/k of each half cycle. [design parameter] default 1.8 (≈44.4 % dwell), range 1.2 .. 3.0.

◆ kVoiceSlots

template<typename SampleType = float>
std::size_t pulp::signal::ChorusEnsembleT< SampleType >::kVoiceSlots = static_cast<std::size_t>(kMaxVoices)
staticconstexpr

The documentation for this class was generated from the following file: