Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_synthesis_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Synthesis — bake-layer catalog nodes.
4//
5// The home for the synthesis family: nodes that MAKE a signal, or that make one
6// signal wear another's spectrum. It opens with the sinusoidal ADDITIVE BANK and
7// the channel VOCODER, and it is named for the family rather than for either of
8// them because a third member is being built alongside it:
9//
10// * The GRANULAR ENGINE lands here in its own `granular` namespace, beside
11// `additive` and `vocoder` below. It shares this file's conventions — a
12// source-shaped node, real units in the baked table, a worst-case gain that
13// cites its own DSP suite's asserted invariant — and differs in where its
14// material comes from. Do not add it to a fourth header named after its
15// algorithm; that scatters one set of decisions across several places.
16//
17// ── PORT SHAPE, which is per member and is the thing to get right ─────────
18//
19// Nothing about a node is easier to get wrong by pattern-matching than its
20// ports, and nothing is less visible in a unit test: a node with its two inputs
21// transposed passes every measurement that feeds both ports the same signal.
22//
23// The ADDITIVE BANK is a SOURCE with a GATE. One input port and one output. The
24// input port carries a **gate CV, not audio** — a control signal in the sense
25// the lofi catalog already established when its LFO emits a unipolar control on
26// an audio port and its VCA reads one on port 1. A synthesiser with no way to
27// be struck is not a quiet synthesiser, it is a silent one: the DSP's shared
28// onset stays closed until `retrigger()`, so a gate is not a convenience here,
29// it is the difference between a node that makes sound and one that cannot.
30// Making it a PORT rather than a param is what lets a sequencer, an envelope, or
31// an LFO play the instrument; a host with no CV source drives it from a constant
32// DC input node, which is what this file's suite does.
33//
34// The VOCODER takes TWO INPUTS, and this is the family's real design decision:
35//
36// * **Two ordinary input ports — port 0 = MODULATOR, port 1 = CARRIER — with
37// one mono output.** Precedent is direct: the lofi catalog's dry/wet mixer
38// is two audio inputs (0 = dry, 1 = wet) into one output, and its VCA is
39// signal + CV into one output.
40//
41// * A SIDECHAIN edge was considered and is **not available**. `connect_sidechain`
42// is documented as routing "into the destination PLUGIN's sidechain bus
43// port" and carries a guard that the destination is a Plugin node —
44// "sidechain only makes sense on plugins". A custom catalog node cannot be
45// a sidechain destination, so the choice is not between two conventions;
46// ordinary ports are the only one that exists at this layer. That is a
47// fact about the graph, not a preference, and it is written down here so
48// the next person does not re-litigate it.
49//
50// * ORDER: modulator first. Two things agree on it and nothing argues the
51// other way — `VocoderT::process(modulator, carrier_ext, out)` takes them
52// in that order, so a transposed node would disagree with its own DSP at
53// the call site; and every vocoder ever built labels its inputs
54// "Modulator / Carrier" in that order, so it is also what a graph author
55// reaches for. The VCA's "signal on 0, control on 1" reading does NOT
56// apply: the VCA's port 1 is a unipolar CV, while both of the vocoder's
57// ports carry full-range audio, which makes the dry/wet mixer the closer
58// precedent.
59//
60// * Because a transposition is invisible to a symmetric test, this file's
61// suite asserts ASYMMETRY directly: swapping the two inputs must change the
62// output. A suite that fed both ports the same signal would pass either
63// way, which is exactly how a node ships unusable with green tests.
64//
65// ── REALIZATION vs INJECTABLE PARAM, applied per member ───────────────────
66//
67// The standing rule from the dynamics family: anything that moves
68// `latency_samples()` is frozen at REGISTRATION, because a node whose reported
69// latency changes under the audio thread breaks the host's delay compensation.
70// A genuine TOPOLOGY change is a realization too. Coefficients are params.
71//
72// Both members here report `latency_samples() == 0` in every configuration —
73// the bank is direct time-domain oscillators and the vocoder is all
74// minimum-phase IIR — so the latency clause never fires in this file. What
75// remains is topology and capacity, and they land differently:
76//
77// * ADDITIVE, `voice` → REALIZATION, two registered type ids. Not because it
78// is "a big change", but because `VoiceTable::harmonic` is part of it: a
79// modal voice IGNORES `inharmonicity`, so registering a bell makes an
80// injectable param inert. A param whose existence depends on a choice means
81// that choice is part of what the node IS. (The organ and bell also differ
82// in every partial's ratio, amplitude and decay, so automating between them
83// would step the entire spectrum.) This follows the diode-bridge member's
84// two-type-id split for the same reason.
85//
86// * ADDITIVE, `max_partials` → REALIZATION, a construction argument. It is
87// the ONLY allocating dimension in the DSP (`prepare()` is documented as
88// the only method that may allocate, and only when this grows), and it also
89// fixes the declared maximum of the `partial_count` param — a param whose
90// range is decided at registration cannot itself be a param. Same shape as
91// the feedforward compressor's lookahead ceiling.
92//
93// * ADDITIVE, `envelope_mode` → INJECTABLE PARAM, and this one was checked
94// against the code rather than assumed. It looks like it should click: a
95// per-partial decay that has fallen to 0.3 ought to jump when the shared
96// envelope takes over. It does not. `shared_ar` sets the per-partial decay
97// COEFFICIENT to 1, which freezes each partial's decay where it stands
98// instead of resetting it, so the switch is continuous in both directions.
99// With no discontinuity, no latency change and no topology change, it is
100// the detector-mode case from the dynamics family: one switch selecting
101// which coefficient feeds the same envelope.
102//
103// * ADDITIVE, `retrig_phase` → INJECTABLE PARAM. It is read only inside
104// `retrigger()`, so it cannot take effect mid-note and cannot step anything.
105//
106// * VOCODER → NO REALIZATION AXIS AT ALL, like the FET compressor. Worth
107// stating explicitly because `band_count` looks like a structural argument
108// and is not: the DSP holds every per-band array as a fixed
109// `std::array<…, kMaxBands>` and `set_band_count` moves only the active loop
110// bound, allocating nothing and leaving latency at 0. It is a param, and one
111// of the load-bearing ones.
112//
113// ── PARAM APPLICATION RATE ────────────────────────────────────────────────
114//
115// The dynamics family applies every param per sample, and says why: its setters
116// are "a clamp, a store, and at most one `exp`". That is not true of every
117// setter here, and pretending otherwise would put a filter-bank redesign inside
118// the sample loop. So this file splits them, and names which is which:
119//
120// * PER SAMPLE — setters that clamp and store. Sample-accurate automation.
121// * BLOCK RATE, AND ONLY ON CHANGE — setters that rebuild a filter bank or
122// re-derive per-band ballistics: the vocoder's `band_count`,
123// `freq_lo/hi_hz`, `attack_ms` and `release_ms`. Each of these loops every
124// band computing transcendentals; per-sample they would cost more than the
125// audio. The dry/wet mixer node already established block-rate application
126// where the wrapped API is block-oriented — this is the same call with a
127// different reason.
128//
129// The change guard is not an optimisation detail that can be dropped: without
130// it, holding a knob still would still redesign twenty bands per sample.
131
135
140
141#include <cstddef>
142#include <cstdint>
143
145
146// ── The sinusoidal additive bank ──────────────────────────────────────────
147namespace additive {
148
152enum class Voice : std::uint8_t {
155};
156
157inline constexpr const char* kOrganTypeId = "synthesis.additive_organ";
158inline constexpr const char* kBellTypeId = "synthesis.additive_bell";
159
160// Node-local param ids; the framework namespaces per node, so these may restart
161// at 1 without colliding with any other node's.
162inline constexpr state::ParamID kFundamentalHz = 1; // Hz
163inline constexpr state::ParamID kPartialCount = 2; // stepped, 1 .. max_partials
164inline constexpr state::ParamID kInharmonicity = 3; // B, dimensionless
165inline constexpr state::ParamID kSpectralTilt = 4; // dB/oct
166inline constexpr state::ParamID kMasterGainDb = 5; // dB
167inline constexpr state::ParamID kEnvelopeMode = 6; // stepped 0 = shared AR, 1 = per-partial
168inline constexpr state::ParamID kRetrigPhase = 7; // stepped 0/1/2
169inline constexpr state::ParamID kAttackMs = 8; // ms
170inline constexpr state::ParamID kReleaseMs = 9; // ms
171inline constexpr state::ParamID kDetuneCents = 10; // cents
172
173// NOT DECLARED, deliberately: `morph` and `spectral_domain`.
174//
175// Both act only through the DSP's `SpectralEnvelope` PAIR — `morph` crossfades
176// envelope A against B, and `spectral_domain` chooses whether that envelope is
177// read against absolute Hz or against a ratio to f0. This node supplies no
178// envelopes, and it cannot: an envelope is a breakpoint LIST, and a baked-param
179// table carries scalars. With both envelopes left at their flat defaults, a
180// crossfade between two identical flat curves and a re-mapping of a flat
181// curve's abscissa are both provably no-ops.
182//
183// They were declared in the first draft of this file and the suite caught them:
184// injecting either moved the audio by exactly 0.0. A param that cannot move the
185// audio must not be in the table, because a host would draw a control that does
186// nothing. They become meaningful the day the family can bake a table — at
187// which point the envelope pair is registration-time material (it is voice
188// data, like the partial table it sits beside) and these two become params
189// against it.
190
196
202inline constexpr float kGateOn = 0.5f;
203inline constexpr float kGateOff = 0.25f;
204
212
222
226 int max_partials = kNodeMaxPartials);
227
229
230} // namespace additive
231
232// ── The channel vocoder ───────────────────────────────────────────────────
233namespace vocoder {
234
235inline constexpr const char* kTypeId = "synthesis.vocoder";
236
240inline constexpr PortIndex kModulatorPort = 0;
241inline constexpr PortIndex kCarrierPort = 1;
242
243inline constexpr state::ParamID kBandCount = 1; // stepped, 10 .. 20
244inline constexpr state::ParamID kFreqLoHz = 2; // Hz
245inline constexpr state::ParamID kFreqHiHz = 3; // Hz
246inline constexpr state::ParamID kCarrierSource = 4; // stepped 0 = external, 1 = internal
247inline constexpr state::ParamID kInternalWave = 5; // stepped 0 = saw, 1 = pulse
248inline constexpr state::ParamID kInternalPw = 6; // 0.05 .. 0.95
249inline constexpr state::ParamID kCarrierPitchHz = 7; // Hz — internal carrier
250inline constexpr state::ParamID kNoiseMix = 8; // 0..1
251inline constexpr state::ParamID kAttackMs = 9; // ms
252inline constexpr state::ParamID kReleaseMs = 10; // ms
253inline constexpr state::ParamID kUnvoicedSens = 11; // 0..1
254inline constexpr state::ParamID kSibilanceMix = 12; // 0..1
255inline constexpr state::ParamID kFormantShiftSt = 13; // semitones
256inline constexpr state::ParamID kFormantFreeze = 14; // stepped 0/1
257inline constexpr state::ParamID kOutputTrimDb = 15; // dB
258inline constexpr state::ParamID kDryWet = 16; // 0..1
259
261
262struct Instance {
267 float band_count = -1.0f;
268 float freq_lo = -1.0f;
269 float freq_hi = -1.0f;
270 float attack_ms = -1.0f;
271 float release_ms = -1.0f;
272};
273
312float vocoder_worst_case_gain(double sample_rate = 48000.0);
313
319
323
325
326} // namespace vocoder
327
328namespace cyclic {
329enum class Regime : std::uint8_t { short_frame, long_frame };
330inline constexpr const char* kShortTypeId = "synthesis.cyclic_stretch_short";
331inline constexpr const char* kLongTypeId = "synthesis.cyclic_stretch_long";
332inline constexpr state::ParamID kStretch = 1;
333inline constexpr state::ParamID kCaptureMs = 2;
334inline constexpr state::ParamID kCrossfadeShape = 3;
335inline constexpr state::ParamID kMixPct = 4;
336inline constexpr state::ParamID kOutputDb = 5;
338struct Instance {
340};
343
345} // namespace cyclic
346
347namespace granular {
348inline constexpr const char* kTypeId = "synthesis.granular_live";
349inline constexpr state::ParamID kDensityHz = 1;
350inline constexpr state::ParamID kGrainMs = 2;
351inline constexpr state::ParamID kPosition = 3;
352inline constexpr state::ParamID kPositionSprayMs = 4;
353inline constexpr state::ParamID kPitchSt = 5;
354inline constexpr state::ParamID kPitchSpraySt = 6;
355inline constexpr state::ParamID kPanSpray = 7;
356inline constexpr state::ParamID kJitter = 8;
357inline constexpr state::ParamID kLevelDb = 9;
358inline constexpr state::ParamID kMix = 10;
360struct Instance {
362};
365
367} // namespace granular
368
369} // namespace pulp::host::synthesis
The sinusoidal bank: a tone built by summing sinusoids, one phase accumulator per partial,...
static constexpr int kMaxPartialsDefault
Definition additive_bank.hpp:378
Fixed-cycle splice time-stretch — the S-series sampler algorithm whose ARTEFACTS ARE THE PRODUCT.
A granular synthesis engine: a seeded grain scheduler driving a fixed pool of windowed,...
std::uint32_t PortIndex
Definition graph_types.hpp:8
Definition forge_synthesis_catalog.hpp:147
CustomNodeType make_additive_bank_node(Voice voice=Voice::organ, int max_partials=kNodeMaxPartials)
voice picks the registered type id and the partial table; max_partials is the allocation ceiling.
constexpr int kNodeMaxPartials
Partial ceiling this node instantiates.
Definition forge_synthesis_catalog.hpp:195
constexpr state::ParamID kDetuneCents
Definition forge_synthesis_catalog.hpp:171
constexpr state::ParamID kEnvelopeMode
Definition forge_synthesis_catalog.hpp:167
constexpr const char * kBellTypeId
Definition forge_synthesis_catalog.hpp:158
Voice
The two registered voices.
Definition forge_synthesis_catalog.hpp:152
@ organ
Harmonic, sustained. inharmonicity applies.
Definition forge_synthesis_catalog.hpp:153
@ bell
Modal, per-partial decays. No inharmonicity parameter.
Definition forge_synthesis_catalog.hpp:154
constexpr state::ParamID kSpectralTilt
Definition forge_synthesis_catalog.hpp:165
constexpr float kGateOff
Definition forge_synthesis_catalog.hpp:203
constexpr state::ParamID kInharmonicity
Definition forge_synthesis_catalog.hpp:164
ForgeNodeDescriptor descriptor()
constexpr state::ParamID kAttackMs
Definition forge_synthesis_catalog.hpp:169
constexpr state::ParamID kMasterGainDb
Definition forge_synthesis_catalog.hpp:166
constexpr float kGateOn
Gate thresholds on the CV input, with hysteresis so a noisy or slewed gate cannot retrigger on every ...
Definition forge_synthesis_catalog.hpp:202
constexpr state::ParamID kRetrigPhase
Definition forge_synthesis_catalog.hpp:168
constexpr state::ParamID kFundamentalHz
Definition forge_synthesis_catalog.hpp:162
constexpr const char * kOrganTypeId
Definition forge_synthesis_catalog.hpp:157
constexpr state::ParamID kPartialCount
Definition forge_synthesis_catalog.hpp:163
float additive_bank_worst_case_gain()
Worst-case linear gain for the Forge registry (series law 8).
constexpr state::ParamID kReleaseMs
Definition forge_synthesis_catalog.hpp:170
Definition forge_synthesis_catalog.hpp:328
signal::CyclicStretch Engine
Definition forge_synthesis_catalog.hpp:337
Regime
Definition forge_synthesis_catalog.hpp:329
@ long_frame
Definition forge_synthesis_catalog.hpp:329
@ short_frame
Definition forge_synthesis_catalog.hpp:329
constexpr state::ParamID kMixPct
Definition forge_synthesis_catalog.hpp:335
CustomNodeType make_cyclic_stretch_node(Regime regime=Regime::short_frame)
constexpr state::ParamID kStretch
Definition forge_synthesis_catalog.hpp:332
constexpr state::ParamID kCrossfadeShape
Definition forge_synthesis_catalog.hpp:334
constexpr const char * kShortTypeId
Definition forge_synthesis_catalog.hpp:330
constexpr state::ParamID kCaptureMs
Definition forge_synthesis_catalog.hpp:333
constexpr const char * kLongTypeId
Definition forge_synthesis_catalog.hpp:331
constexpr state::ParamID kOutputDb
Definition forge_synthesis_catalog.hpp:336
ForgeNodeDescriptor descriptor()
Definition forge_synthesis_catalog.hpp:347
constexpr state::ParamID kPitchSpraySt
Definition forge_synthesis_catalog.hpp:354
constexpr state::ParamID kPanSpray
Definition forge_synthesis_catalog.hpp:355
constexpr state::ParamID kGrainMs
Definition forge_synthesis_catalog.hpp:350
constexpr state::ParamID kJitter
Definition forge_synthesis_catalog.hpp:356
constexpr state::ParamID kPitchSt
Definition forge_synthesis_catalog.hpp:353
ForgeNodeDescriptor descriptor()
signal::GranularEngine Engine
Definition forge_synthesis_catalog.hpp:359
CustomNodeType make_granular_node()
constexpr state::ParamID kLevelDb
Definition forge_synthesis_catalog.hpp:357
constexpr state::ParamID kDensityHz
Definition forge_synthesis_catalog.hpp:349
constexpr const char * kTypeId
Definition forge_synthesis_catalog.hpp:348
constexpr state::ParamID kMix
Definition forge_synthesis_catalog.hpp:358
constexpr state::ParamID kPositionSprayMs
Definition forge_synthesis_catalog.hpp:352
constexpr state::ParamID kPosition
Definition forge_synthesis_catalog.hpp:351
Definition forge_synthesis_catalog.hpp:233
constexpr state::ParamID kFreqHiHz
Definition forge_synthesis_catalog.hpp:245
constexpr PortIndex kCarrierPort
Definition forge_synthesis_catalog.hpp:241
constexpr state::ParamID kBandCount
Definition forge_synthesis_catalog.hpp:243
constexpr state::ParamID kInternalPw
Definition forge_synthesis_catalog.hpp:248
constexpr state::ParamID kFreqLoHz
Definition forge_synthesis_catalog.hpp:244
constexpr state::ParamID kDryWet
Definition forge_synthesis_catalog.hpp:258
constexpr PortIndex kModulatorPort
Input port roles.
Definition forge_synthesis_catalog.hpp:240
constexpr state::ParamID kNoiseMix
Definition forge_synthesis_catalog.hpp:250
signal::Vocoder Voc
Definition forge_synthesis_catalog.hpp:260
constexpr state::ParamID kCarrierPitchHz
Definition forge_synthesis_catalog.hpp:249
constexpr state::ParamID kOutputTrimDb
Definition forge_synthesis_catalog.hpp:257
constexpr state::ParamID kCarrierSource
Definition forge_synthesis_catalog.hpp:246
constexpr state::ParamID kReleaseMs
Definition forge_synthesis_catalog.hpp:252
CustomNodeType make_vocoder_node()
Two inputs, one output.
constexpr state::ParamID kInternalWave
Definition forge_synthesis_catalog.hpp:247
constexpr state::ParamID kFormantFreeze
Definition forge_synthesis_catalog.hpp:256
constexpr const char * kTypeId
Definition forge_synthesis_catalog.hpp:235
constexpr state::ParamID kUnvoicedSens
Definition forge_synthesis_catalog.hpp:253
ForgeNodeDescriptor descriptor()
constexpr state::ParamID kAttackMs
Definition forge_synthesis_catalog.hpp:251
constexpr state::ParamID kFormantShiftSt
Definition forge_synthesis_catalog.hpp:255
constexpr state::ParamID kSibilanceMix
Definition forge_synthesis_catalog.hpp:254
float vocoder_worst_case_gain(double sample_rate=48000.0)
Worst-case linear gain for the Forge registry (series law 8).
float vocoder_all_sample_rates_worst_case_gain()
Sample-rate-independent ceiling for registries whose metadata cannot vary with the host rate.
Definition forge_synthesis_catalog.hpp:144
CyclicStretchT< float > CyclicStretch
Definition cyclic_stretch.hpp:853
VocoderT< float > Vocoder
Definition vocoder.hpp:839
GranularEngineT< float > GranularEngine
Definition granular.hpp:933
AdditiveBankT< float > AdditiveBank
Definition additive_bank.hpp:1050
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_synthesis_catalog.hpp:205
signal::AdditiveBank bank
Definition forge_synthesis_catalog.hpp:206
bool gate_high
Latched gate state, so retrigger() fires on the EDGE rather than for every sample the gate is high — ...
Definition forge_synthesis_catalog.hpp:210
Definition forge_synthesis_catalog.hpp:338
Engine engine
Definition forge_synthesis_catalog.hpp:339
Definition forge_synthesis_catalog.hpp:360
Engine engine
Definition forge_synthesis_catalog.hpp:361
Definition forge_synthesis_catalog.hpp:262
float release_ms
Definition forge_synthesis_catalog.hpp:271
signal::Vocoder vocoder
Definition forge_synthesis_catalog.hpp:263
float freq_lo
Definition forge_synthesis_catalog.hpp:268
float freq_hi
Definition forge_synthesis_catalog.hpp:269
float band_count
Last-applied values of the four expensive setters.
Definition forge_synthesis_catalog.hpp:267
float attack_ms
Definition forge_synthesis_catalog.hpp:270
The classic channel vocoder — Dudley's analysis-synthesis speech remaker.