Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
chorus_family.hpp File Reference

The chorus family — one engine, four documented voicings selected as data. More...

#include <pulp/signal/character_delay/bbd.hpp>
#include <pulp/signal/delay_line.hpp>
#include <pulp/signal/interpolator.hpp>
#include <pulp/signal/lfo.hpp>
#include <pulp/signal/tpt_filter.hpp>
#include <pulp/signal/units.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>

Go to the source code of this file.

Classes

class  pulp::signal::ChorusEnsembleT< SampleType >
 One chorus engine, four voicings. Stereo in place. More...
struct  pulp::signal::ChorusEnsembleT< SampleType >::Calibration
 One voicing's shipped calibration. More...
struct  pulp::signal::ChorusEnsembleT< SampleType >::JunoModeSpec
 The Juno's per-mode calibration. More...

Namespaces

namespace  pulp
namespace  pulp::signal

Typedefs

using pulp::signal::ChorusEnsemble = ChorusEnsembleT<float>
using pulp::signal::ChorusEnsemble64 = ChorusEnsembleT<double>

Detailed Description

The chorus family — one engine, four documented voicings selected as data.

A chorus is a modulated short delay line mixed with the dry signal. What separates a Boss CE-2 from a Juno ensemble from a Roland Dimension D from a three-voice TriChorus is not the delay and not the interpolator: it is how many taps there are, what phase relationship their modulators hold, and how the taps are summed back into L and R. So the frame here — a fractional delay read, an LfoT per voice, a dry/wet blend — is shared, and each voicing is a row in a calibration table plus a branch in the mix matrix. Nothing about a voicing's identity lives in a different code path through the delay or the interpolation core, which is what makes the four auditable against each other.

  • CE-2 — one mono BBD tap, triangle LFO, duplicated to L and R. The reference guitar-chorus timbre.
  • Juno ensemble — two taps, one per channel, LFOs an exact half cycle apart, no cross-mix. Three fixed-rate modes (I / II / I+II).
  • Dimension D — two inverted-phase taps like the Juno, plus the complementary cross-mix: each channel's wet tap subtracted from the opposite channel through a high-pass, with the dry bass-boosted to replace what the cross-cancellation takes out. Width without warble.
  • TriChorus — three sine-modulated voices at 120° spacing, routed L / centre-split / R. No two voices are ever in phase, which is why it is audibly denser than a two-tap ensemble.

Relationship to chorus.hpp

pulp::signal::ChorusT (chorus.hpp) is an earlier, thinner prototype: a single sine LFO, a quadrature-offset stereo pair, depth expressed as a fraction of the centre delay, and linear interpolation. It is not a subset of this class — its depth law and its 90°-offset stereo image belong to no documented hardware voicing — so this module ships alongside it rather than extending it in place. ChorusT is public API (signal.hpp exports it, test/test_signal.cpp covers it); removing it is a coordinated public-API change, not something to fold into this module's landing. New callers should use ChorusEnsembleT; ChorusT is superseded and should be retired in a follow-up that also updates its exporter and its test.

Composition — what this module does NOT own

  • Modulation. Every voice's modulator is a pulp::signal::LfoT, with N-voice spacing expressed as set_phase_offset(k / N). The 0.5-cycle offset that gives the Juno and the Dimension D their inverted pair is LfoT's documented exact inversion for odd-symmetric shapes, asserted in that toolkit's own suite. There is no LFO in this file.
  • Interpolation. Interpolator::lagrange — 4-point (3rd-order) Lagrange, per Laakso et al. 1996. There is no interpolation kernel in this file, only the stencil fetch.
  • Ring storage. DelayLineT, read at four integer offsets around the fractional target.
  • BBD colour. chardelay::BbdChannel — the clock-domain bandwidth law and compander that the multi-character-delay module owns. See set_bbd_color for the one substitution made and why.

Constants: published vs design parameter

Published, cited at the constant: the Juno's three-mode delay ranges and LFO rates (pendragon-andyh's measured Juno-60 circuit analysis) and the CE-2's practical 5–40 ms delay zone (ElectroSmash's open circuit analysis of the MN3007/MN3101 pair). Everything else is a design parameter carrying a default and a range: the CE-2's exact centre/depth/rate inside its cited zone (Boss never published pot calibration curves), and the whole of the Dimension D and TriChorus calibrations (no source documents either family at that precision — an honest gap, not an omission).

Anti-aliasing policy (series law 4)

The audio path is linear when bbd_color is off, so there is nothing to oversample. Three things could be mistaken for a nonlinearity:

  1. The modulated read is a resampling operation, not a nonlinearity. Its error is the Lagrange kernel's passband droop and imaging, which is the cited design tradeoff (kInterpOrder), not an aliasing policy.
  2. The trapezoid clamp (§ Dimension D) is a hard nonlinearity applied to the modulation signal, at LFO rate. Its harmonics are multiples of a sub-10 Hz fundamental and never enter the audio path except as delay modulation. Oversampling it would be meaningless.
  3. The one genuine audio-path nonlinearity, the BBD's asymmetric soft clip, lives inside chardelay::BbdChannel, which already runs it at kBbdOversample× behind a matched band-limiting pair. That policy is inherited by composition and is deliberately not restated here.

Latency (series law 5)

latency_samples() is 0 for every voicing and every mode. The Lagrange stencil spans kInterpOrder + 1 samples around the fractional read position, so it stays strictly behind the write pointer as long as the minimum instantaneous delay stays kGuardSamples clear of it. Every shipped calibration clears that guard by a factor of 19 or more — the tightest is the Juno's 79.7 samples of minimum delay against a 4-sample guard at 48 kHz. Enabling bbd_color only adds group delay (measured: 3 samples, from the composed stage's band-limiting pair and its clocked write/read alignment), never lookahead.

RT contract: prepare() allocates the two delay lines and is the only function in this class that may. set_*, process, and reset allocate nothing, take no locks, and perform no I/O. set_voicing/set_juno_mode recompute filter and BBD coefficients, so they are control-thread calls; they are allocation-free and safe to call from the audio thread if a host insists. Every buffer is sized in prepare() for kMaxDelayMs, which covers the widest retune the calibration tables' declared ranges allow, so no set_* can outgrow the storage.