Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_effect_modulation_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Modulation — bake-layer catalog nodes.
4//
5// The home for the modulation family: effects whose character comes from
6// something moving underneath the signal rather than from a nonlinearity. It
7// opens with the single-sideband frequency shifter and is named for the family
8// rather than for that one member, because the chorus, phaser and vibrato
9// lineages that follow share this file's conventions — a moving delay or phase
10// relationship, a rate or offset in real units, a wet path that a mix control
11// blends against dry — and differ only in what is moving and how far.
12//
13// TRUE STEREO, two ports in and two out as ONE logical wire. The shifter is not
14// a per-rail memoryless function: `stereo_split` mode drives the left channel
15// up and the right down from a SHARED carrier, so the two rails must be
16// processed in the same call to stay phase-locked. Instancing it dual-mono
17// would silently reduce that mode to two identical up-shifts — which sounds
18// fine right up until someone reaches for the stereo control and nothing
19// happens.
20//
21// EVERY CHOICE ON THE SHIFTER IS AN INJECTABLE PARAM, none is a
22// registration-time realization, and that is worth stating because it is the
23// opposite of the saturator's arrangement. The rule is that anything which
24// changes reported latency must be frozen at registration, because a node whose
25// latency moves under the audio thread breaks the host's delay compensation.
26// The shifter reports zero latency in every mode: the Hilbert network is IIR
27// allpass with no bulk delay, the mode switch only changes the sign and scale
28// applied to the quadrature term, and the feedback delay lives inside a loop
29// rather than in the signal's path to the output. Nothing there can move
30// latency, so nothing there has to be baked in.
31//
32// The three lineages that follow are NOT all like that, and the differences are
33// the interesting part of this file:
34//
35// - **Chorus** freezes its VOICING, its Juno mode and its BBD colour stage at
36// registration. Latency is zero for all of them, so the latency rule is not
37// what forces it — topology is. The four voicings differ in VOICE COUNT and
38// in the mix matrix that combines them, and `set_voicing` rewinds every LFO
39// phase and overwrites the rate. See `chorus::make_chorus_node`.
40// - **Phaser** freezes its STAGE COUNT, for the same reason the diode-bridge
41// compressor freezes its detector position: the module's own published notch
42// law takes the stage count as an argument, so the measured response is a
43// different function per count, not a different value of one function.
44// - **Vibrato** ships THREE nodes rather than one with a mode knob, because
45// the three engines are different signal-theoretic objects — one modulates a
46// delay (real, frequency-independent pitch modulation) and two modulate
47// allpass corners (frequency-DEPENDENT apparent pitch, plus moving notches).
48// A mode knob would imply they are points on a continuum.
49//
50// And the delay vibrato is the one member in this file where the LATENCY rule
51// bites: its reported latency is `ceil(base_delay + amplitude)` and the
52// amplitude scales as `depth / rate`, so it moves by a factor of ~280 across the
53// DSP's declared ranges. Its rate FLOOR is therefore a registration-time
54// argument, exactly like the VCA compressor's lookahead — see
55// `vibrato::make_delay_vibrato_node`.
56//
57// TWO SETTERS IN THIS FAMILY MUST NOT BE CALLED PER SAMPLE, and both would fail
58// silently rather than loudly. `ChorusEnsembleT::set_voicing` /
59// `set_juno_mode` rewind every LFO phase, so injecting either every sample
60// freezes the modulation at phase zero — a chorus that does not chorus.
61// `DelayVibratoT::set_delay_ms` / `set_fade_in_ms` re-arm the onset envelope and
62// zero its depth, so injecting them every sample pins the depth envelope at zero
63// — a vibrato that does not vibrate. The first pair is handled by making them
64// realizations; the second by applying them only when the value actually
65// changes. Both are the kind of bug that renders as "the effect is subtle".
66//
67// The one thing that IS fixed at registration is the feedback line's capacity,
68// and it is fixed by the DSP header's `kMaxLoopMs` rather than by a node-local
69// ceiling: `prepare()` sizes for it, so the `fb_delay_ms` knob's top is the
70// largest delay the node can ever hold without allocating.
71//
72// WET AND DRY BOTH: unlike a compressor, whose output IS the processed signal,
73// a frequency shifter is routinely run as a blend — the classic barberpole
74// setting is a wet spiral sitting under a dry source. The DSP block owns the
75// mix so that `mix < 100 %` blends dry against the whole wet chain including
76// the recirculated tail, which is what makes turning the control down attenuate
77// the barberpole rather than disable it.
78
80
81#include <vector>
82
87
88#include <cmath>
89#include <string>
90
92
93// ── Stable type ids ───────────────────────────────────────────────────────
94inline constexpr const char* kSsbFrequencyShifterTypeId = "modulation.frequency_shifter_ssb";
95
96// ── Injectable param ids ──────────────────────────────────────────────────
97// Node-local; the framework namespaces per node so two nodes never collide.
98inline constexpr state::ParamID kShiftHz = 1; // Hz, signed: + up, − down
99inline constexpr state::ParamID kFeedback = 2; // 0..kMaxFeedback
100inline constexpr state::ParamID kFeedbackDelayMs = 3; // ms
101inline constexpr state::ParamID kMix = 4; // %
102inline constexpr state::ParamID kShiftMode = 5; // stepped 0..3, see below
103inline constexpr state::ParamID kStereoSpread = 6; // %
104
107inline constexpr float kModeUp = 0.0f;
108inline constexpr float kModeDown = 1.0f;
109inline constexpr float kModeDualMono = 2.0f;
110inline constexpr float kModeStereoSplit = 3.0f;
111
124
129 const int step = static_cast<int>(std::lround(value));
130 switch (step) {
134 default: return signal::FrequencyShiftMode::up;
135 }
136}
137
140
141// ═══════════════════════════════════════════════════════════════════════════
142// Chorus — four voicings of one ensemble engine
143// ═══════════════════════════════════════════════════════════════════════════
144
145namespace chorus {
146
150
151// ── Injectable param ids ──────────────────────────────────────────────────
152inline constexpr state::ParamID kRateHz = 1; // Hz
153inline constexpr state::ParamID kDepth = 2; // %
154inline constexpr state::ParamID kMix = 3; // %
155inline constexpr state::ParamID kStereoWidth = 4; // %
156
159inline constexpr double kRateMinHz = 0.05;
160inline constexpr double kRateMaxHz = 10.0;
161
169std::string chorus_type_id(Voicing voicing, JunoMode juno_mode, bool bbd_color);
170
171std::string chorus_display_name(Voicing voicing, JunoMode juno_mode, bool bbd_color);
172
173struct Instance {
175};
176
189float chorus_worst_case_gain(Voicing voicing, double sample_rate);
190
213 JunoMode juno_mode = JunoMode::mode_I, bool bbd_color = false);
214
215} // namespace chorus
216
217// ═══════════════════════════════════════════════════════════════════════════
218// Phaser — a swept allpass cascade
219// ═══════════════════════════════════════════════════════════════════════════
220
221namespace phaser {
222
224
225// ── Injectable param ids ──────────────────────────────────────────────────
226inline constexpr state::ParamID kRateHz = 1; // Hz
227inline constexpr state::ParamID kDepth = 2; // %
228inline constexpr state::ParamID kCenterHz = 3; // Hz
229inline constexpr state::ParamID kFeedback = 4; // −kFeedbackMax .. +kFeedbackMax
230inline constexpr state::ParamID kMix = 5; // %
231inline constexpr state::ParamID kStereoSpread = 6; // cycles, 0 .. 0.5
232inline constexpr state::ParamID kStaggerRatio = 7; // ratio
233inline constexpr state::ParamID kWave = 8; // stepped 0..6
234
236inline constexpr double kRateMinHz = 0.02;
237inline constexpr double kRateMaxHz = 10.0;
239inline constexpr double kCenterMinHz = 20.0;
240inline constexpr double kCenterMaxHz = 5000.0;
241
244inline constexpr float kWaveSine = 0.0f;
245inline constexpr float kWaveTriangle = 1.0f;
246inline constexpr float kWaveSawUp = 2.0f;
247inline constexpr float kWaveSawDown = 3.0f;
248inline constexpr float kWaveSquare = 4.0f;
249inline constexpr float kWaveSampleHold = 5.0f;
250inline constexpr float kWaveSmoothRandom = 6.0f;
251
255inline signal::LfoWave wave_from_param(float value) {
256 const int step = static_cast<int>(std::lround(value));
257 switch (step) {
258 case 1: return signal::LfoWave::triangle;
259 case 2: return signal::LfoWave::saw_up;
260 case 3: return signal::LfoWave::saw_down;
261 case 4: return signal::LfoWave::square;
262 case 5: return signal::LfoWave::sample_hold;
263 case 6: return signal::LfoWave::smooth_random;
264 default: return signal::LfoWave::sine;
265 }
266}
267
268struct Instance {
270};
271
272std::string phaser_type_id(int stages);
273
282
301
302} // namespace phaser
303
304// ═══════════════════════════════════════════════════════════════════════════
305// Vibrato — three lineages, three nodes
306// ═══════════════════════════════════════════════════════════════════════════
307
308namespace vibrato {
309
310// Public for compatibility with the modulation catalog's response tests and
311// downstream characterization tools. The implementation remains in the
312// catalog translation unit so the public header carries no DSP helper body.
313double allpass_cascade_l1(const std::vector<double>& corners_hz, double sample_rate,
314 int length = 200000);
315
316// ── Delay vibrato — the only one that really shifts pitch ─────────────────
317
318namespace delay_line {
319
321
322// Injectable parameter ids.
323inline constexpr state::ParamID kRateHz = 1;
324inline constexpr state::ParamID kDepthCents = 2;
325inline constexpr state::ParamID kDelayMs = 3;
326inline constexpr state::ParamID kFadeInMs = 4;
327
328struct Instance {
330 // Last values applied to the onset setters. Those setters re-arm the
331 // lifecycle envelope, so the process callback updates them only when the
332 // injected value changes.
333 float applied_delay_ms = -1.0f;
334 float applied_fade_in_ms = -1.0f;
335};
336
337// The floor is part of the realization identity because it changes the
338// injectable rate range.
339std::string delay_vibrato_type_id(float min_rate_hz);
340
343
345CustomNodeType make_delay_vibrato_node(float min_rate_hz = 4.0f);
346
347} // namespace delay_line
348
349// ── Phase vibrato — the Magnatone varistor lineage ────────────────────────
350
351namespace phase {
352
354
355// Injectable parameter ids.
356inline constexpr state::ParamID kRateHz = 1;
357inline constexpr state::ParamID kDepth = 2;
358inline constexpr state::ParamID kCenterHz = 3;
359inline constexpr state::ParamID kMix = 4;
360
361struct Instance {
363};
364
365std::string phase_vibrato_type_id(int stages);
366
368float phase_vibrato_worst_case_gain(int stages, double sample_rate);
369
372
373} // namespace phase
374
375// ── Univibe — the lamp-and-photocell lineage ──────────────────────────────
376
377namespace univibe {
378
381
382inline constexpr state::ParamID kRateHz = 1;
383inline constexpr state::ParamID kDepth = 2;
384inline constexpr state::ParamID kMode = 3;
385inline constexpr float kModeVibrato = 0.0f;
386inline constexpr float kModeChorus = 1.0f;
387inline constexpr const char* kTypeId = "modulation.vibrato.univibe";
388
389struct Instance {
391};
392
395
397float univibe_worst_case_gain(double sample_rate);
398
401
402} // namespace univibe
403
404} // namespace vibrato
405
406} // namespace pulp::host::modulation
407
408#include <pulp/host/detail/forge_effect_modulation_catalog_descriptor.hpp>
409#include <pulp/host/detail/forge_effect_modulation_extended_catalog.hpp>
The chorus family — one engine, four documented voicings selected as data.
JunoMode
Definition chorus_family.hpp:131
Voicing
Definition chorus_family.hpp:126
static constexpr int kDefaultStageCount
Definition vibrato.hpp:357
static constexpr int kStageCountDefault
Definition phaser_stages.hpp:315
Mode
Definition vibrato.hpp:493
The single-sideband (Bode/Moog) frequency shifter: a Hilbert phase-difference network,...
Definition forge_effect_modulation_catalog.hpp:145
std::string chorus_type_id(Voicing voicing, JunoMode juno_mode, bool bbd_color)
The type id for one realization.
constexpr state::ParamID kDepth
Definition forge_effect_modulation_catalog.hpp:153
constexpr double kRateMinHz
The registered rate span, in Hz.
Definition forge_effect_modulation_catalog.hpp:159
Engine::Voicing Voicing
Definition forge_effect_modulation_catalog.hpp:148
Engine::JunoMode JunoMode
Definition forge_effect_modulation_catalog.hpp:149
constexpr state::ParamID kRateHz
Definition forge_effect_modulation_catalog.hpp:152
constexpr state::ParamID kStereoWidth
Definition forge_effect_modulation_catalog.hpp:155
float chorus_worst_case_gain(Voicing voicing, double sample_rate)
Worst-case linear gain for the Forge registry (series law 8).
signal::ChorusEnsemble Engine
Definition forge_effect_modulation_catalog.hpp:147
constexpr state::ParamID kMix
Definition forge_effect_modulation_catalog.hpp:154
constexpr double kRateMaxHz
Definition forge_effect_modulation_catalog.hpp:160
std::string chorus_display_name(Voicing voicing, JunoMode juno_mode, bool bbd_color)
CustomNodeType make_chorus_node(Voicing voicing=Voicing::ce2, JunoMode juno_mode=JunoMode::mode_I, bool bbd_color=false)
One chorus voicing as a lowerable custom node.
Definition forge_effect_modulation_catalog.hpp:221
signal::PhaserStages Engine
Definition forge_effect_modulation_catalog.hpp:223
constexpr float kWaveSine
The wave param's stepped encoding, in LfoWave's own declaration order so the two cannot drift apart.
Definition forge_effect_modulation_catalog.hpp:244
constexpr state::ParamID kRateHz
Definition forge_effect_modulation_catalog.hpp:226
constexpr float kWaveSampleHold
Definition forge_effect_modulation_catalog.hpp:249
constexpr state::ParamID kMix
Definition forge_effect_modulation_catalog.hpp:230
constexpr double kCenterMinHz
The registered centre-frequency span, in Hz.
Definition forge_effect_modulation_catalog.hpp:239
constexpr double kCenterMaxHz
Definition forge_effect_modulation_catalog.hpp:240
constexpr state::ParamID kDepth
Definition forge_effect_modulation_catalog.hpp:227
constexpr double kRateMaxHz
Definition forge_effect_modulation_catalog.hpp:237
CustomNodeType make_phaser_node(int stages=Engine::kStageCountDefault)
A swept allpass cascade as a lowerable custom node.
constexpr float kWaveSmoothRandom
Definition forge_effect_modulation_catalog.hpp:250
constexpr state::ParamID kStereoSpread
Definition forge_effect_modulation_catalog.hpp:231
std::string phaser_type_id(int stages)
constexpr state::ParamID kCenterHz
Definition forge_effect_modulation_catalog.hpp:228
constexpr state::ParamID kWave
Definition forge_effect_modulation_catalog.hpp:233
constexpr float kWaveSquare
Definition forge_effect_modulation_catalog.hpp:248
constexpr double kRateMinHz
The registered rate span, in Hz — the DSP's documented single-pot range.
Definition forge_effect_modulation_catalog.hpp:236
signal::LfoWave wave_from_param(float value)
Decodes the stepped wave param.
Definition forge_effect_modulation_catalog.hpp:255
constexpr float kWaveSawUp
Definition forge_effect_modulation_catalog.hpp:246
constexpr state::ParamID kFeedback
Definition forge_effect_modulation_catalog.hpp:229
constexpr float kWaveTriangle
Definition forge_effect_modulation_catalog.hpp:245
constexpr state::ParamID kStaggerRatio
Definition forge_effect_modulation_catalog.hpp:232
float phaser_worst_case_gain()
Worst-case linear gain for the Forge registry (series law 8).
constexpr float kWaveSawDown
Definition forge_effect_modulation_catalog.hpp:247
Definition forge_effect_modulation_catalog.hpp:318
constexpr state::ParamID kFadeInMs
Definition forge_effect_modulation_catalog.hpp:326
constexpr state::ParamID kRateHz
Definition forge_effect_modulation_catalog.hpp:323
CustomNodeType make_delay_vibrato_node(float min_rate_hz=4.0f)
True pitch vibrato as a lowerable mono custom node.
std::string delay_vibrato_type_id(float min_rate_hz)
constexpr state::ParamID kDelayMs
Definition forge_effect_modulation_catalog.hpp:325
float delay_vibrato_worst_case_gain()
Worst-case linear gain at the registered interpolator ceiling.
constexpr state::ParamID kDepthCents
Definition forge_effect_modulation_catalog.hpp:324
signal::DelayVibrato Engine
Definition forge_effect_modulation_catalog.hpp:320
Definition forge_effect_modulation_catalog.hpp:351
std::string phase_vibrato_type_id(int stages)
constexpr state::ParamID kDepth
Definition forge_effect_modulation_catalog.hpp:357
constexpr state::ParamID kCenterHz
Definition forge_effect_modulation_catalog.hpp:358
signal::PhaseVibrato Engine
Definition forge_effect_modulation_catalog.hpp:353
float phase_vibrato_worst_case_gain(int stages, double sample_rate)
Worst-case linear gain at the lowest registered allpass corner.
constexpr state::ParamID kMix
Definition forge_effect_modulation_catalog.hpp:359
constexpr state::ParamID kRateHz
Definition forge_effect_modulation_catalog.hpp:356
CustomNodeType make_phase_vibrato_node(int stages=Engine::kDefaultStageCount)
Phase vibrato as a lowerable mono custom node.
Definition forge_effect_modulation_catalog.hpp:377
constexpr float kModeVibrato
Definition forge_effect_modulation_catalog.hpp:385
Mode mode_from_param(float value)
Decodes the stepped mode parameter.
constexpr float kModeChorus
Definition forge_effect_modulation_catalog.hpp:386
constexpr state::ParamID kDepth
Definition forge_effect_modulation_catalog.hpp:383
Engine::Mode Mode
Definition forge_effect_modulation_catalog.hpp:380
signal::UniVibe Engine
Definition forge_effect_modulation_catalog.hpp:379
constexpr state::ParamID kMode
Definition forge_effect_modulation_catalog.hpp:384
float univibe_worst_case_gain(double sample_rate)
Worst-case linear gain at the lowest registered allpass corner.
CustomNodeType make_univibe_node()
Univibe as a lowerable stereo-in/stereo-out custom node.
constexpr state::ParamID kRateHz
Definition forge_effect_modulation_catalog.hpp:382
constexpr const char * kTypeId
Definition forge_effect_modulation_catalog.hpp:387
Definition forge_effect_modulation_catalog.hpp:308
double allpass_cascade_l1(const std::vector< double > &corners_hz, double sample_rate, int length=200000)
Definition forge_effect_modulation_catalog.hpp:91
constexpr state::ParamID kFeedback
Definition forge_effect_modulation_catalog.hpp:99
constexpr float kModeUp
The mode param's stepped encoding.
Definition forge_effect_modulation_catalog.hpp:107
constexpr const char * kSsbFrequencyShifterTypeId
Definition forge_effect_modulation_catalog.hpp:94
CustomNodeType make_frequency_shifter_node()
The Bode/Moog single-sideband frequency shifter as a lowerable custom node.
float ssb_frequency_shifter_worst_case_gain()
Worst-case linear gain for the Forge registry (series law 8).
constexpr state::ParamID kMix
Definition forge_effect_modulation_catalog.hpp:101
constexpr float kModeStereoSplit
Definition forge_effect_modulation_catalog.hpp:110
constexpr float kModeDown
Definition forge_effect_modulation_catalog.hpp:108
constexpr state::ParamID kStereoSpread
Definition forge_effect_modulation_catalog.hpp:103
signal::FrequencyShiftMode mode_from_param(float value)
Decodes the stepped mode param.
Definition forge_effect_modulation_catalog.hpp:128
constexpr state::ParamID kShiftMode
Definition forge_effect_modulation_catalog.hpp:102
constexpr state::ParamID kFeedbackDelayMs
Definition forge_effect_modulation_catalog.hpp:100
constexpr float kModeDualMono
Definition forge_effect_modulation_catalog.hpp:109
constexpr state::ParamID kShiftHz
Definition forge_effect_modulation_catalog.hpp:98
ChorusEnsembleT< float > ChorusEnsemble
Definition chorus_family.hpp:726
FrequencyShiftMode
Which sideband a SsbFrequencyShifterT keeps, and how it spreads it.
Definition frequency_shifter_ssb.hpp:314
@ stereo_split
Up on the left, down on the right, scaled by the spread.
Definition frequency_shifter_ssb.hpp:318
@ dual_mono
Two independent identical mono shifters. See the note below.
Definition frequency_shifter_ssb.hpp:317
@ up
Upper sideband on both channels: f → f + Δf.
Definition frequency_shifter_ssb.hpp:315
@ down
Lower sideband on both channels: f → f − Δf.
Definition frequency_shifter_ssb.hpp:316
DelayVibratoT< float > DelayVibrato
Definition vibrato.hpp:316
PhaseVibratoT< float > PhaseVibrato
Definition vibrato.hpp:467
PhaserStagesT< float > PhaserStages
Definition phaser_stages.hpp:647
LfoWave
The effect-wave vocabulary, shared by these functions and by EffectLfoT (lfo.hpp),...
Definition lfo_sampling.hpp:53
@ sample_hold
Definition lfo_sampling.hpp:59
@ square
Definition lfo_sampling.hpp:58
@ saw_down
Definition lfo_sampling.hpp:57
@ sine
Definition lfo_sampling.hpp:54
@ saw_up
Definition lfo_sampling.hpp:56
@ smooth_random
Definition lfo_sampling.hpp:60
@ triangle
Definition lfo_sampling.hpp:55
UniVibeT< float > UniVibe
Definition vibrato.hpp:637
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
The cascaded-allpass phaser: a 4-stage "Small Stone style" sweep and a general 4–12-stage digital pha...
Definition custom_node_type.hpp:90
Definition forge_effect_modulation_catalog.hpp:173
Engine engine
Definition forge_effect_modulation_catalog.hpp:174
Definition forge_effect_modulation_catalog.hpp:268
Engine engine
Definition forge_effect_modulation_catalog.hpp:269
Definition forge_effect_modulation_catalog.hpp:328
float applied_delay_ms
Definition forge_effect_modulation_catalog.hpp:333
float applied_fade_in_ms
Definition forge_effect_modulation_catalog.hpp:334
Engine engine
Definition forge_effect_modulation_catalog.hpp:329
Definition forge_effect_modulation_catalog.hpp:361
Engine engine
Definition forge_effect_modulation_catalog.hpp:362
Definition forge_effect_modulation_catalog.hpp:389
Engine engine
Definition forge_effect_modulation_catalog.hpp:390
Three vibrato lineages that are not variations on one idea.