Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
scanner_vibrato.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>
9
10#include <algorithm>
11#include <cmath>
12#include <cstdint>
13
14namespace pulp::signal {
15
16// ═══════════════════════════════════════════════════════════════════════════
17// Hammond scanner vibrato / chorus
18// ═══════════════════════════════════════════════════════════════════════════
19
21enum class ScannerMode : std::uint8_t {
29};
30
32template <typename SampleType = float>
34public:
36
44 static constexpr double kScanHz = 6.9;
45
51 static constexpr double kScanHz50 = kScanHz * 50.0 / 60.0;
52
55 static constexpr double kLineDelayMs = 1.0;
56
64 static constexpr int kScannerTaps = 18;
65
69 static constexpr double kV1 = 0.33;
70 static constexpr double kV2 = 0.66;
71 static constexpr double kV3 = 1.00;
72
75 static constexpr double kChorusMix = 0.5;
76
78 static constexpr double kMaxLineDelayMs = 1.4;
79
86 static constexpr double kWorstCaseGain = 1.5;
87
88 ScannerVibratoT() { update(); }
89
90 void prepare(double sample_rate) {
91 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
92 line_.prepare(worst_case_delay_samples(sample_rate_));
93 scanner_.prepare(sample_rate_);
94 scanner_.set_wave(LfoWave::triangle);
95 blocker_.set_pole(static_cast<SampleType>(
96 1.0 - kDcCornerHz * leslie_detail::kTwoPi / sample_rate_));
97 update();
98 reset();
99 }
100
101 void reset() {
102 line_.reset();
103 scanner_.reset();
104 blocker_.reset();
105 }
106
107 void discard_history() noexcept {
108 line_.discard_history();
109 scanner_.reset();
110 blocker_.reset();
111 }
112
114 mode_ = mode;
115 update();
116 }
117
118 Mode mode() const { return mode_; }
119
120 void set_scan_hz(double hz) {
121 if (!std::isfinite(hz)) return;
122 scan_hz_ = std::clamp(hz, 0.0, 100.0);
123 update();
124 }
125
126 void set_line_ms(double ms) {
127 if (!std::isfinite(ms)) return;
128 line_ms_ = std::clamp(ms, 0.0, kMaxLineDelayMs);
129 update();
130 }
131
132 void set_v1_frac(double frac) {
133 if (!std::isfinite(frac)) return;
134 v1_ = std::clamp(frac, 0.0, 1.0);
135 update();
136 }
137 void set_v2_frac(double frac) {
138 if (!std::isfinite(frac)) return;
139 v2_ = std::clamp(frac, 0.0, 1.0);
140 update();
141 }
142 void set_v3_frac(double frac) {
143 if (!std::isfinite(frac)) return;
144 v3_ = std::clamp(frac, 0.0, 1.0);
145 update();
146 }
147
148 void set_chorus_mix(double mix01) {
149 if (!std::isfinite(mix01)) return;
150 chorus_mix_ = std::clamp(mix01, 0.0, 1.0);
151 update();
152 }
153
154 // ── Observables ───────────────────────────────────────────────────────
155
157 double depth_fraction() const { return depth_frac_; }
158
161 double dry_mix() const { return dry_mix_; }
162
170 double peak_pitch_shift_ratio() const {
171 return depth_frac_ * line_ms_ * 0.001 * 2.0 * scan_hz_;
172 }
173
176 static constexpr int latency_samples() { return 0; }
177
178 SampleType process(SampleType input) {
179 if (!std::isfinite(static_cast<double>(input))) {
181 return SampleType{0};
182 }
183 if (mode_ == Mode::off) return input;
184
185 const double dry = static_cast<double>(input);
186 const double x = static_cast<double>(blocker_.process(input));
187
188 // The scanner position, `[0, 1]`, from the shared LFO in triangle mode.
189 // The triangle is the point, not a stand-in for a sine: a constant-slope
190 // ramp is a constant pitch shift with a sign flip at each turnaround,
191 // which is the boxier motion that keeps this from sounding like every
192 // sine-LFO chorus. Do not smooth it.
193 const double s = static_cast<double>(scanner_.next_unipolar());
194
195 // Physically the pickup sits between two taps and the scanner
196 // crossfades them:
197 //
198 // tap_k delay = (k / (N−1)) · line_ms
199 // pos = depth · s · (N−1); k = floor(pos); f = pos − k
200 // wet = (1−f)·read(tap_k) + f·read(tap_{k+1})
201 //
202 // For an equally spaced line that linear crossfade between two taps IS
203 // a linear interpolation of one continuous delay, so reading the single
204 // fractional delay `depth·s·line_ms` directly is the same signal for
205 // any tap count — and reading it with the cubic kernel is strictly
206 // better than the two-tap linear blend. The tap count survives as
207 // `kScannerTaps`, documenting what is being modelled, rather than as
208 // arithmetic that would only coarsen the result.
209 const double delay_samples = depth_frac_ * s * line_ms_ * 0.001 * sample_rate_;
210
211 line_.push(static_cast<SampleType>(x));
212 const int index = static_cast<int>(std::floor(delay_samples));
213 const double frac = delay_samples - static_cast<double>(index);
214 const double wet = Interpolator::lagrange(frac,
215 static_cast<double>(line_.read(index - 1)),
216 static_cast<double>(line_.read(index)),
217 static_cast<double>(line_.read(index + 1)),
218 static_cast<double>(line_.read(index + 2)));
219
220 // V modes are pure pitch vibrato: the pitch moves, the level does not.
221 // C modes add the dry back, so a stationary-pitch copy beats against a
222 // moving-pitch one and the result is a sweeping comb — shimmer and
223 // thickness rather than wobble. They are DIFFERENT EFFECTS, not two
224 // points on a depth control.
225 return static_cast<SampleType>(snap_to_zero(dry_mix_ * dry + (1.0 - dry_mix_) * wet));
226 }
227
228 void process_block(const SampleType* in, SampleType* out, int n) {
229 for (int i = 0; i < n; ++i) out[i] = process(in[i]);
230 }
231
232 static int worst_case_delay_samples(double sample_rate) {
233 return static_cast<int>(std::ceil(kMaxLineDelayMs * 0.001 * sample_rate)) +
234 leslie_detail::kInterpolatorMargin;
235 }
236
237private:
240 static constexpr double kDcCornerHz = 5.0;
241
242 void update() {
243 switch (mode_) {
244 case Mode::off:
245 depth_frac_ = 0.0;
246 dry_mix_ = 1.0;
247 break;
248 case Mode::v1:
249 depth_frac_ = v1_;
250 dry_mix_ = 0.0;
251 break;
252 case Mode::v2:
253 depth_frac_ = v2_;
254 dry_mix_ = 0.0;
255 break;
256 case Mode::v3:
257 depth_frac_ = v3_;
258 dry_mix_ = 0.0;
259 break;
260 case Mode::c1:
261 depth_frac_ = v1_;
262 dry_mix_ = chorus_mix_;
263 break;
264 case Mode::c2:
265 depth_frac_ = v2_;
266 dry_mix_ = chorus_mix_;
267 break;
268 case Mode::c3:
269 depth_frac_ = v3_;
270 dry_mix_ = chorus_mix_;
271 break;
272 }
273 scanner_.set_rate_hz(scan_hz_);
274 }
275
276 double sample_rate_ = 48000.0;
277 Mode mode_ = Mode::off;
278 double scan_hz_ = kScanHz;
279 double line_ms_ = kLineDelayMs;
280 double v1_ = kV1;
281 double v2_ = kV2;
282 double v3_ = kV3;
283 double chorus_mix_ = kChorusMix;
284 double depth_frac_ = 0.0;
285 double dry_mix_ = 1.0;
286
287 DelayLineT<SampleType> line_{};
288 EffectLfoT<double> scanner_{};
289 DcBlocker<SampleType> blocker_{};
290};
291
294
295} // namespace pulp::signal
The motor-scanned tapped delay line.
Definition scanner_vibrato.hpp:33
static int worst_case_delay_samples(double sample_rate)
Definition scanner_vibrato.hpp:232
static constexpr double kV1
Definition scanner_vibrato.hpp:69
void set_v1_frac(double frac)
Definition scanner_vibrato.hpp:132
void set_v2_frac(double frac)
Definition scanner_vibrato.hpp:137
void set_mode(Mode mode)
Definition scanner_vibrato.hpp:113
static constexpr double kChorusMix
Definition scanner_vibrato.hpp:75
static constexpr double kLineDelayMs
Definition scanner_vibrato.hpp:55
void set_chorus_mix(double mix01)
Definition scanner_vibrato.hpp:148
void set_v3_frac(double frac)
Definition scanner_vibrato.hpp:142
void process_block(const SampleType *in, SampleType *out, int n)
Definition scanner_vibrato.hpp:228
void reset()
Definition scanner_vibrato.hpp:101
static constexpr double kMaxLineDelayMs
Definition scanner_vibrato.hpp:78
double depth_fraction() const
The fraction of the line the scanner currently traverses.
Definition scanner_vibrato.hpp:157
static constexpr double kV3
Definition scanner_vibrato.hpp:71
SampleType process(SampleType input)
Definition scanner_vibrato.hpp:178
void discard_history() noexcept
Definition scanner_vibrato.hpp:107
double peak_pitch_shift_ratio() const
The peak fractional pitch shift this setting produces, |Δf/f|.
Definition scanner_vibrato.hpp:170
static constexpr int latency_samples()
Zero, for the same reason as the Leslie: the line box's delay is the modelled electrical path,...
Definition scanner_vibrato.hpp:176
double dry_mix() const
How much dry is blended back.
Definition scanner_vibrato.hpp:161
Mode mode() const
Definition scanner_vibrato.hpp:118
static constexpr double kWorstCaseGain
Definition scanner_vibrato.hpp:86
static constexpr double kScanHz
Definition scanner_vibrato.hpp:44
static constexpr double kScanHz50
Definition scanner_vibrato.hpp:51
void prepare(double sample_rate)
Definition scanner_vibrato.hpp:90
static constexpr int kScannerTaps
Definition scanner_vibrato.hpp:64
static constexpr double kV2
Definition scanner_vibrato.hpp:70
ScannerVibratoT()
Definition scanner_vibrato.hpp:88
ScannerMode Mode
Definition scanner_vibrato.hpp:35
void set_scan_hz(double hz)
Definition scanner_vibrato.hpp:120
void set_line_ms(double ms)
Definition scanner_vibrato.hpp:126
High-quality sample interpolation for delay lines and resampling.
The house low-frequency oscillator: seven waveforms plus a continuous shape morph,...
Definition additive_bank.hpp:218
@ off
Definition nonlinear_shaping.hpp:24
ScannerMode
The console's six-position vibrato/chorus switch, plus off.
Definition scanner_vibrato.hpp:21
@ c3
Full chorus. The lush one.
Definition scanner_vibrato.hpp:28
@ v2
Medium vibrato.
Definition scanner_vibrato.hpp:24
@ v3
Full vibrato.
Definition scanner_vibrato.hpp:25
@ v1
Shallow vibrato — scanner output only.
Definition scanner_vibrato.hpp:23
@ c2
Medium chorus.
Definition scanner_vibrato.hpp:27
@ c1
Shallow chorus — scanner plus dry.
Definition scanner_vibrato.hpp:26
ScannerVibratoT< double > ScannerVibrato64
Definition scanner_vibrato.hpp:293
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ i
Definition formant_filter_bank.hpp:20
@ triangle
Definition lfo_sampling.hpp:55
ScannerVibratoT< float > ScannerVibrato
Definition scanner_vibrato.hpp:292
static SampleType lagrange(SampleType frac, SampleType ym1, SampleType y0, SampleType y1, SampleType y2)
4-point Lagrange interpolation.
Definition interpolator.hpp:60