Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_modulation_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Forge modulation catalog — the `pulp::signal` modulation toolkit exposed as
4// bake-layer custom nodes.
5//
6// Companion to forge_lofi_catalog.hpp, which carries the lo-fi effect nodes and
7// the first CV pack (lfo / vca / env_follower / filter_cv / delay_cv). This
8// header adds the capabilities that pack cannot express, each one a thin wrapper
9// over a `pulp::signal` primitive rather than catalog-local DSP:
10//
11// * mod_lfo — LfoT: seven waveforms including sample-and-hold and smooth
12// random, shape morphing, triangle/pulse shaping, random
13// segmentation, phase, and the repeat/fade lifecycle. The
14// existing `lfo` node is four fixed shapes with no lifecycle
15// and no random layer.
16// * mod_lpg — LpgT: the vactrol low-pass gate. Loudness and brightness move
17// together, which no combination of the existing nodes produces.
18// * mod_slew — SlewLimiterT: an independent-rise/fall rate limiter on a
19// control signal. What makes a stepped CV musical.
20// * mod_transient — TransientDetectorT: level-INDEPENDENT attack detection.
21// Distinct from `env_follower`, which tracks level and therefore
22// needs re-tuning whenever the source's level changes.
23// * mod_env — ModEnvT: a delay/attack/hold/decay envelope fired by a rising
24// edge on its control input, with depth, velocity response,
25// looping, and configurable trigger re-arming.
26//
27// The CV convention is the one the first pack established: a control signal is a
28// UNIPOLAR [0, 1] signal on an ordinary audio port. Modulation is therefore
29// ordinary graph topology that the existing bake and routing already handle —
30// there are no modulation edges, and nothing here needs a new graph concept.
31//
32// The public declarations stay small; implementations live in the host
33// library's dedicated translation unit so consumers do not compile every
34// factory and descriptor into each including target.
35//
36// KNOWN DUPLICATION, for the migration that follows: `forge_lofi::make_lfo_node`
37// carries its own four-shape oscillator (`forge_lofi_osc`), and the private LFOs
38// inside `signal::chorus` and `signal::phaser` are two more. All three predate
39// `signal::LfoT` and should migrate onto it. They are deliberately left alone
40// here: `forge_lofi::kLfoTypeId`'s triangle runs in the opposite phase to
41// `LfoT`'s, so swapping the implementation would change every artifact already
42// baked against it. That is a migration with its own compatibility story, not a
43// side effect of adding nodes.
44
47
49#include <pulp/signal/lfo.hpp>
50#include <pulp/signal/lpg.hpp>
53
55
56// ── Stable type ids ──────────────────────────────────────────────────────
57inline constexpr const char* kModLfoTypeId = "forge_mod_lfo";
58inline constexpr const char* kModLpgTypeId = "forge_mod_lpg";
59inline constexpr const char* kModSlewTypeId = "forge_mod_slew";
60inline constexpr const char* kModTransientTypeId = "forge_mod_transient";
61inline constexpr const char* kModEnvTypeId = "forge_mod_env";
62
63// ── Injectable macro params (node-local ids, as in the lo-fi catalog) ────
64inline constexpr state::ParamID kModLfoRateHz = 1;
65inline constexpr state::ParamID kModLfoDepth = 2;
66inline constexpr state::ParamID kModLfoWave = 3;
67inline constexpr state::ParamID kModLfoPulseWidth = 4;
69inline constexpr state::ParamID kModLfoDelayMs = 6;
70inline constexpr state::ParamID kModLfoFadeInMs = 7;
71inline constexpr state::ParamID kModLfoShapeMorph = 8;
73inline constexpr state::ParamID kModLfoTriangleBias = 10;
75inline constexpr state::ParamID kModLfoPhaseDegrees = 12;
76inline constexpr state::ParamID kModLfoFadeOutMs = 13;
78inline constexpr state::ParamID kModLfoRepeatCount = 15;
79
80inline constexpr state::ParamID kModLpgDecayMs = 1;
81inline constexpr state::ParamID kModLpgColour = 2;
82inline constexpr state::ParamID kModLpgDroop = 3;
84inline constexpr state::ParamID kModLpgStruck = 5;
85inline constexpr state::ParamID kModLpgRiseMs = 6;
86inline constexpr state::ParamID kModLpgDarknessHz = 7;
89
90inline constexpr state::ParamID kModSlewRiseMs = 1;
91inline constexpr state::ParamID kModSlewFallMs = 2;
92inline constexpr state::ParamID kModSlewCurved = 3;
93
98
99inline constexpr state::ParamID kModEnvAttackMs = 1;
100inline constexpr state::ParamID kModEnvHoldMs = 2;
101inline constexpr state::ParamID kModEnvDecayMs = 3;
102inline constexpr state::ParamID kModEnvCurve = 4;
103inline constexpr state::ParamID kModEnvThreshold = 5;
104inline constexpr state::ParamID kModEnvDelayMs = 6;
105inline constexpr state::ParamID kModEnvDepth = 7;
106inline constexpr state::ParamID kModEnvLoop = 8;
107inline constexpr state::ParamID kModEnvLoopCount = 9;
111inline constexpr state::ParamID kModEnvAttackCurve = 13;
112inline constexpr state::ParamID kModEnvDecayCurve = 14;
113
118
119// ── mod_lfo — the full LFO as a control source ───────────────────────────
120// Zero audio inputs, one output carrying a UNIPOLAR control signal in [0, 1].
121// `cv = clamp(0.5 + 0.5 * depth * lfo, 0, 1)`, the same convention the first CV
122// pack uses, so it drops into any CV input port that already accepts an `lfo`.
123//
124// Every runtime control is read at the current sample offset. Controls that
125// recompute lifecycle state are cached and applied only when their value
126// changes, preserving sample-accurate automation without paying their setup
127// cost for static parameters.
128//
129// The random waveforms are seeded deterministically at reset, so a baked render
130// reproduces exactly.
133 double delay_ms = -1.0;
134 double fade_in_ms = -1.0;
135 double fade_out_ms = -1.0;
136 int fade_curve = -1;
137 int repeat_count = -1;
138};
139
141
142// ── mod_lpg — vactrol low-pass gate ──────────────────────────────────────
143// Port 0 = signal, port 1 = control CV in [0, 1]; one output. The cell drives
144// amplitude and cutoff together, so a falling control darkens as it quietens —
145// which is what makes a pinged LPG read as a struck object rather than a synth
146// patch.
147//
148// `struck` chooses how the CV is read. Gated (0) hands the CV straight to the
149// cell, so an envelope or a fader opens it as a swell. Struck (1) pings the cell
150// on each rising edge of the CV, which is the percussion patch: a re-strike
151// during the decay starts from the cell's current state, so a fast roll
152// crescendos the way a real one does.
153//
154// A struck hit keeps driving the cell for as long as the control is still
155// rising, rather than sampling the control once at the instant it crosses the
156// threshold. Sampling once looks right and is wrong for every source with a
157// finite rise: a slewed or transient-derived control would strike at roughly
158// the threshold level no matter how loud the hit actually was, so velocity
159// would vanish the moment anything smoothed the CV.
160// Re-driving with the running maximum keeps the attack's timing (the first
161// strike still lands on the edge) while the cell charges toward the control's
162// real peak, which is also what a vactrol does while its LED is still
163// brightening.
164//
165// Worst-case gain is unity: the amplitude law is control^1.5 with control in
166// [0, 1], and the cell caps its commanded cutoff at sample_rate / 4, below
167// which the one-pole's peak output provably cannot exceed its peak input. (An
168// uncapped trapezoidal one-pole is NOT attenuate-only — commanded above
169// sample_rate / 4 its step response overshoots, so `brightness` settings past
170// the cap saturate there at low sample rates.)
174 float last_decay_ms = -1.0f;
175 float last_rise_ms = -1.0f;
176 float last_darkness_hz = -1.0f;
177 float last_brightness_hz = -1.0f;
178 float last_refractory_ms = -1.0f;
181 float strike_peak = 0.0f;
182 bool striking = false;
183};
184
187inline constexpr float kDefaultModLpgStrikeThreshold = 0.25f;
188
190
191// ── mod_slew — control-signal rate limiter ───────────────────────────────
192// One control input, one control output. Independent rise and fall times, and a
193// choice of law: linear travels at a fixed slope so the time is exactly the time
194// a full 0 -> 1 move takes; curved is a one-pole, where the time is a constant
195// and the approach is asymptotic.
196//
197// This is what turns a stepped control musical — a sample-and-hold LFO through a
198// short slew burbles instead of clicking, and a square LFO through a 2 ms slew
199// is a trance gate instead of a hazard.
202 float last_rise_ms = -1.0f;
203 float last_fall_ms = -1.0f;
204 int last_curved = -1;
207 bool primed = false;
208};
209
211
212// ── mod_transient — level-independent attack detector (audio -> CV) ──────
213// One audio input, one control output in [0, 1]. It emits the normalized excess
214// of a fast envelope over a slow one, which is a ratio and therefore does not
215// change when the input's level does: the same gesture at -6 dBFS and at
216// -30 dBFS produces the same CV.
217//
218// That is the difference from `env_follower`, which tracks level and whose
219// threshold has to be re-tuned for every source. Use this one when the question
220// is "did something get hit", and `env_follower` when it is "how loud is it".
226
228
229// ── mod_env — per-hit modulation envelope (CV -> CV) ─────────────────────
230// One control input, one control output in [0, 1]. A rising edge on the input
231// past `threshold` fires a delay/attack/hold/decay shape with a settable curve
232// and depth. It ignores gate duration: the input is an event source, not a note.
233//
234// The chain it exists for is `mod_transient -> mod_env -> filter_cv(1)` — a
235// filter sweep whose depth and contour belong to the hit rather than to the
236// input's envelope. Doing that with `env_follower` gives the shape of the sound;
237// this gives the shape you asked for, fired by the sound.
249
251
253
255
257
259
261
262} // namespace pulp::host::forge_modulation
Wave
Definition lfo.hpp:79
The envelope family — AR, AD, AHD, DAHDSR, and a trigger-driven modulation envelope — plus a level-in...
The house low-frequency oscillator: seven waveforms plus a continuous shape morph,...
Buchla-style low-pass gate: one control drives amplitude and brightness together, through a vactrol m...
The small modular utilities every patch reaches for between a modulation source and its destination: ...
Definition forge_modulation_catalog.hpp:54
constexpr state::ParamID kModEnvLoopCount
Definition forge_modulation_catalog.hpp:107
CustomNodeType make_mod_slew_node()
constexpr state::ParamID kModTransientFastMs
Definition forge_modulation_catalog.hpp:94
constexpr state::ParamID kModLpgDarknessHz
Definition forge_modulation_catalog.hpp:86
constexpr state::ParamID kModEnvDecayCurve
Definition forge_modulation_catalog.hpp:112
CustomNodeType make_mod_transient_node()
constexpr state::ParamID kModLpgRefractoryMs
Definition forge_modulation_catalog.hpp:88
constexpr state::ParamID kModEnvCurve
Definition forge_modulation_catalog.hpp:102
constexpr state::ParamID kModEnvThreshold
Definition forge_modulation_catalog.hpp:103
constexpr state::ParamID kModLfoFadeInMs
Definition forge_modulation_catalog.hpp:70
constexpr state::ParamID kModLpgStrikeThreshold
Definition forge_modulation_catalog.hpp:87
constexpr state::ParamID kModEnvHoldMs
Definition forge_modulation_catalog.hpp:100
constexpr state::ParamID kModLpgStruck
Definition forge_modulation_catalog.hpp:84
constexpr state::ParamID kModLfoPulseWidth
Definition forge_modulation_catalog.hpp:67
signal::Lfo::Wave mod_lfo_wave_from_id(float id) noexcept
Waveform selector for mod_lfo, in the order signal::LfoT::Wave declares them.
constexpr state::ParamID kModLfoRandomSegments
Definition forge_modulation_catalog.hpp:74
constexpr state::ParamID kModLfoDelayMs
Definition forge_modulation_catalog.hpp:69
constexpr state::ParamID kModLfoTriangleBias
Definition forge_modulation_catalog.hpp:73
constexpr state::ParamID kModLpgDecayMs
Definition forge_modulation_catalog.hpp:80
constexpr state::ParamID kModSlewFallMs
Definition forge_modulation_catalog.hpp:91
constexpr state::ParamID kModLpgRiseMs
Definition forge_modulation_catalog.hpp:85
constexpr const char * kModSlewTypeId
Definition forge_modulation_catalog.hpp:59
constexpr state::ParamID kModSlewRiseMs
Definition forge_modulation_catalog.hpp:90
constexpr state::ParamID kModEnvVelocitySensitive
Definition forge_modulation_catalog.hpp:109
constexpr state::ParamID kModSlewCurved
Definition forge_modulation_catalog.hpp:92
ForgeNodeDescriptor mod_slew_descriptor()
constexpr state::ParamID kModEnvAttackMs
Definition forge_modulation_catalog.hpp:99
constexpr state::ParamID kModTransientSlowMs
Definition forge_modulation_catalog.hpp:95
constexpr state::ParamID kModEnvDepth
Definition forge_modulation_catalog.hpp:105
constexpr state::ParamID kModLfoMorphEnabled
Definition forge_modulation_catalog.hpp:72
constexpr state::ParamID kModLfoWave
Definition forge_modulation_catalog.hpp:66
constexpr const char * kModEnvTypeId
Definition forge_modulation_catalog.hpp:61
constexpr const char * kModLfoTypeId
Definition forge_modulation_catalog.hpp:57
constexpr state::ParamID kModLfoPhaseDegrees
Definition forge_modulation_catalog.hpp:75
constexpr state::ParamID kModLpgDroop
Definition forge_modulation_catalog.hpp:82
constexpr state::ParamID kModTransientSensitivity
Definition forge_modulation_catalog.hpp:96
constexpr const char * kModTransientTypeId
Definition forge_modulation_catalog.hpp:60
constexpr state::ParamID kModEnvDecayMs
Definition forge_modulation_catalog.hpp:101
ForgeNodeDescriptor mod_lfo_descriptor()
CustomNodeType make_mod_lfo_node()
constexpr state::ParamID kModLpgBrightnessHz
Definition forge_modulation_catalog.hpp:83
constexpr state::ParamID kModLfoDepth
Definition forge_modulation_catalog.hpp:65
constexpr state::ParamID kModEnvIndependentCurves
Definition forge_modulation_catalog.hpp:110
constexpr state::ParamID kModEnvAttackCurve
Definition forge_modulation_catalog.hpp:111
constexpr state::ParamID kModLfoRandomBlend
Definition forge_modulation_catalog.hpp:68
CustomNodeType make_mod_env_node()
constexpr state::ParamID kModLfoShapeMorph
Definition forge_modulation_catalog.hpp:71
constexpr const char * kModLpgTypeId
Definition forge_modulation_catalog.hpp:58
constexpr state::ParamID kModLfoFadeQuadratic
Definition forge_modulation_catalog.hpp:77
ForgeNodeDescriptor mod_env_descriptor()
CustomNodeType make_mod_lpg_node()
constexpr float kDefaultModLpgStrikeThreshold
Control level a rising edge must cross to ping the cell, and the level it must fall back below before...
Definition forge_modulation_catalog.hpp:187
constexpr state::ParamID kModEnvRefractoryMs
Definition forge_modulation_catalog.hpp:108
constexpr state::ParamID kModEnvDelayMs
Definition forge_modulation_catalog.hpp:104
constexpr state::ParamID kModEnvLoop
Definition forge_modulation_catalog.hpp:106
constexpr state::ParamID kModLfoRepeatCount
Definition forge_modulation_catalog.hpp:78
ForgeNodeDescriptor mod_transient_descriptor()
constexpr state::ParamID kModTransientInvert
Definition forge_modulation_catalog.hpp:97
constexpr state::ParamID kModLfoRateHz
Definition forge_modulation_catalog.hpp:64
constexpr state::ParamID kModLpgColour
Definition forge_modulation_catalog.hpp:81
constexpr state::ParamID kModLfoFadeOutMs
Definition forge_modulation_catalog.hpp:76
ForgeNodeDescriptor mod_lpg_descriptor()
SlewLimiterT< float > SlewLimiter
Definition mod_tools.hpp:218
ModEnvT< float > ModEnv
Definition envelope.hpp:651
LfoT< float > Lfo
Definition lfo.hpp:778
TriggerDetectT< float > TriggerDetect
Definition trigger.hpp:161
LpgT< float > Lpg
Definition lpg.hpp:288
TransientDetectorT< float > TransientDetector
Definition envelope.hpp:733
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Definition custom_node_type.hpp:90
Definition forge_param_descriptor.hpp:127
Definition forge_modulation_catalog.hpp:238
float last_attack_ms
Definition forge_modulation_catalog.hpp:242
float last_delay_ms
Definition forge_modulation_catalog.hpp:241
float last_refractory_ms
Definition forge_modulation_catalog.hpp:245
float last_hold_ms
Definition forge_modulation_catalog.hpp:243
signal::TriggerDetect edge
Definition forge_modulation_catalog.hpp:240
int last_loop_count
Definition forge_modulation_catalog.hpp:247
signal::ModEnv envelope
Definition forge_modulation_catalog.hpp:239
float last_decay_ms
Definition forge_modulation_catalog.hpp:244
bool last_loop
Definition forge_modulation_catalog.hpp:246
Definition forge_modulation_catalog.hpp:131
double fade_out_ms
Definition forge_modulation_catalog.hpp:135
int repeat_count
Definition forge_modulation_catalog.hpp:137
double fade_in_ms
Definition forge_modulation_catalog.hpp:134
double delay_ms
Definition forge_modulation_catalog.hpp:133
signal::Lfo lfo
Definition forge_modulation_catalog.hpp:132
int fade_curve
Definition forge_modulation_catalog.hpp:136
Definition forge_modulation_catalog.hpp:171
float last_refractory_ms
Definition forge_modulation_catalog.hpp:178
bool striking
Definition forge_modulation_catalog.hpp:182
float last_brightness_hz
Definition forge_modulation_catalog.hpp:177
signal::Lpg lpg
Definition forge_modulation_catalog.hpp:172
signal::TriggerDetect edge
Definition forge_modulation_catalog.hpp:173
float last_decay_ms
Definition forge_modulation_catalog.hpp:174
float last_rise_ms
Definition forge_modulation_catalog.hpp:175
float last_darkness_hz
Definition forge_modulation_catalog.hpp:176
float strike_peak
Running maximum of the control since the current rising edge, and whether that edge is still open.
Definition forge_modulation_catalog.hpp:181
Definition forge_modulation_catalog.hpp:200
signal::SlewLimiter slew
Definition forge_modulation_catalog.hpp:201
int last_curved
Definition forge_modulation_catalog.hpp:204
bool primed
The first sample after a reset is adopted outright rather than slewed to, so a render starts wherever...
Definition forge_modulation_catalog.hpp:207
float last_rise_ms
Definition forge_modulation_catalog.hpp:202
float last_fall_ms
Definition forge_modulation_catalog.hpp:203
Definition forge_modulation_catalog.hpp:221
float last_fast_ms
Definition forge_modulation_catalog.hpp:223
signal::TransientDetector detector
Definition forge_modulation_catalog.hpp:222
float last_slow_ms
Definition forge_modulation_catalog.hpp:224
The event-domain kit: edge detection, gate generation, clock division and multiplication,...