Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_saturator_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Saturation toolkit — bake-layer catalog node.
4//
5// Wraps pulp::signal::SaturatorT as a lowerable custom node, following the same
6// shape as the lo-fi kit's make_waveshaper_node() and the character delay's
7// per-realization factory. Where those two differ, this follows the delay: the
8// SHAPE is a registration-time realization rather than an injectable param.
9//
10// That choice is worth stating, because a four-member enum looks like an
11// obvious candidate for a stepped knob. Two reasons it is not:
12//
13// 1. The four curves have genuinely different harmonic signatures and
14// different gain-at-bias behaviour (`cubic_soft` alone clamps its bias, at
15// a different value from the rest). A baked build authored as `sinh_arc`
16// glue should stay glue for the artifact's life, not become a fuzz because
17// a control-thread write moved an enum.
18// 2. Crossfading between shapes to make the switch click-free would mean
19// running two shapers, which doubles the cost of the cheapest stage in the
20// catalog to buy a control nobody asked for.
21//
22// The ALIAS POLICY is likewise a realization, not a param, and for a harder
23// reason: it changes `latency_samples()`, and a node whose reported latency
24// moves under the audio thread breaks the host's delay compensation.
25//
26// MONO, one port in and one out. Saturation is a per-sample memoryless function
27// with no cross-channel state at all, so a stereo instrument would be exactly
28// two independent copies — which is what instancing two nodes already gives,
29// without pretending there is a stereo image to preserve.
30//
31// WET+DRY, unlike the character delay: the module owns its own `mix` because
32// the dry path has to be latency-aligned with the wet one, and only the module
33// knows what its latency is. Composing an external dry/wet mixer around an
34// `oversample_2x` saturator would comb.
35
38
40
41#include <cstddef>
42#include <cstdint>
43
45
48
49// ── Stable type ids ───────────────────────────────────────────────────────
50inline constexpr const char* kTanhTypeId = "saturator.tanh";
51inline constexpr const char* kAtanTypeId = "saturator.atan";
52inline constexpr const char* kCubicTypeId = "saturator.cubic";
53inline constexpr const char* kSinhArcTypeId = "saturator.sinh_arc";
54
55// ── Injectable param ids ──────────────────────────────────────────────────
56// Node-local; the framework namespaces per node so two nodes never collide.
57inline constexpr state::ParamID kDriveDb = 1; // dB
58inline constexpr state::ParamID kBias = 2; // −1..+1 normalized
59inline constexpr state::ParamID kTonePreHz = 3; // Hz, 0 = off
60inline constexpr state::ParamID kMix = 4; // 0..1
61inline constexpr state::ParamID kOutputTrimDb = 5; // dB
62inline constexpr state::ParamID kToneTracking = 6; // stepped 0/1
63inline constexpr state::ParamID kToneDeHz = 7; // Hz, used when tracking is off
64inline constexpr state::ParamID kPreBoostDb = 8; // dB
65
69
70inline const char* saturator_type_id(Shape shape) {
71 switch (shape) {
72 case Shape::atan_soft:
73 return kAtanTypeId;
74 case Shape::cubic_soft:
75 return kCubicTypeId;
76 case Shape::sinh_arc:
77 return kSinhArcTypeId;
78 case Shape::tanh_soft:
79 default:
80 return kTanhTypeId;
81 }
82}
83
84inline const char* saturator_default_name(Shape shape) {
85 switch (shape) {
86 case Shape::atan_soft:
87 return "Saturator (Arctan)";
88 case Shape::cubic_soft:
89 return "Saturator (Cubic)";
90 case Shape::sinh_arc:
91 return "Saturator (Sinh-Arc)";
92 case Shape::tanh_soft:
93 default:
94 return "Saturator (Tanh)";
95 }
96}
97
98inline const char* alias_policy_id(AliasPolicy policy) noexcept {
99 switch (policy) {
100 case AliasPolicy::oversample_2x:
101 return "x2";
102 case AliasPolicy::off:
103 return "off";
104 case AliasPolicy::adaa:
105 default:
106 return "adaa";
107 }
108}
109
110inline Shape normalized_shape(Shape shape) noexcept {
111 switch (shape) {
112 case Shape::tanh_soft:
113 case Shape::atan_soft:
114 case Shape::cubic_soft:
115 case Shape::sinh_arc:
116 return shape;
117 default:
118 return Shape::tanh_soft;
119 }
120}
121
123 switch (policy) {
124 case AliasPolicy::adaa:
125 case AliasPolicy::oversample_2x:
126 case AliasPolicy::off:
127 return policy;
128 default:
129 return AliasPolicy::adaa;
130 }
131}
132
141inline float saturator_worst_case_gain(Shape shape) {
142 signal::Saturator probe;
143 probe.set_shape(shape);
144 probe.set_bias(1.0); // clamped per shape by the DSP
145 return static_cast<float>(probe.worst_case_gain());
146}
147
153 AliasPolicy alias_policy = AliasPolicy::adaa) {
154 const Shape fixed_shape = normalized_shape(shape);
155 const AliasPolicy fixed_alias_policy = normalized_alias_policy(alias_policy);
157 t.type_id = saturator_type_id(fixed_shape);
158 if (fixed_alias_policy != AliasPolicy::adaa)
159 t.type_id += std::string{"."} + alias_policy_id(fixed_alias_policy);
160 t.version = 1;
161 t.num_input_ports = 1;
162 t.num_output_ports = 1;
163 t.default_name = saturator_default_name(fixed_shape);
164 t.lowerable = true;
165 t.latency_samples = [fixed_shape, fixed_alias_policy](double sample_rate) {
166 signal::Saturator probe;
167 probe.set_shape(fixed_shape);
168 probe.set_alias_policy(fixed_alias_policy);
169 probe.prepare(sample_rate);
170 return probe.latency_samples();
171 };
172
173 t.create = []() -> void* { return new SaturatorInstance{}; };
174 t.destroy = [](void* p) { delete static_cast<SaturatorInstance*>(p); };
175 t.prepare = [fixed_shape, fixed_alias_policy](void* p, double sr, int /*max_block*/) {
176 auto* s = static_cast<SaturatorInstance*>(p);
177 s->saturator.set_shape(fixed_shape);
178 s->saturator.set_alias_policy(fixed_alias_policy);
179 s->saturator.prepare(sr);
180 };
181 t.reset = [](void* p) { static_cast<SaturatorInstance*>(p)->saturator.reset(); };
182
183 // Ranges and defaults are the module's canonical contract, in REAL units;
184 // the Forge layer mirrors them and owns the display curve. Every row here
185 // is a design-parameter declaration per the series contract.
186 using Sat = signal::Saturator;
187 t.baked_params.push_back({kDriveDb, static_cast<float>(Sat::kDriveDbMin),
188 static_cast<float>(Sat::kDriveDbMax),
189 static_cast<float>(Sat::kDriveDbDefault)});
190 t.baked_params.push_back({kBias, -1.0f, 1.0f, 0.0f});
191 t.baked_params.push_back({kTonePreHz, 0.0f, static_cast<float>(Sat::kTonePreHzMax),
192 static_cast<float>(Sat::kTonePreHzDefault)});
193 t.baked_params.push_back({kMix, 0.0f, 1.0f, 1.0f});
194 t.baked_params.push_back({kOutputTrimDb, -static_cast<float>(Sat::kOutputTrimDbMax),
195 static_cast<float>(Sat::kOutputTrimDbMax), 0.0f});
196 t.baked_params.push_back({kToneTracking, 0.0f, 1.0f, 1.0f});
197 t.baked_params.push_back({kToneDeHz, 0.0f, static_cast<float>(Sat::kTonePreHzMax),
198 static_cast<float>(Sat::kTonePreHzDefault)});
199 t.baked_params.push_back({kPreBoostDb, 0.0f, static_cast<float>(Sat::kPreBoostDbMax),
200 static_cast<float>(Sat::kPreBoostDbDefault)});
201
202 t.process_instance_baked_param = [](void* p, audio::BufferView<float>& out,
203 const audio::BufferView<const float>& in, int n,
204 const BakedParamView& params) {
205 auto* s = static_cast<SaturatorInstance*>(p);
206 const float* input = in.channel_ptr(0);
207 float* output = out.channel_ptr(0);
208
209 // Sample-at-a-time so every knob is sample-accurate. The drive and bias
210 // setters recompute three doubles; the tone setter redesigns a biquad,
211 // which is why the tone corner is expected to be an automation-rare
212 // control rather than a per-sample sweep. Both are still cheaper than
213 // the transcendental the shaper itself evaluates.
214 for (int k = 0; k < n; ++k) {
215 const auto offset = static_cast<std::int32_t>(k);
216 s->saturator.set_drive_db(params.value_at(kDriveDb, offset));
217 s->saturator.set_bias(params.value_at(kBias, offset));
218 s->saturator.set_tone_pre_hz(params.value_at(kTonePreHz, offset));
219 s->saturator.set_tone_tracking(params.value_at(kToneTracking, offset) >= 0.5f);
220 s->saturator.set_tone_de_hz(params.value_at(kToneDeHz, offset));
221 s->saturator.set_pre_boost_db(params.value_at(kPreBoostDb, offset));
222 s->saturator.set_mix(params.value_at(kMix, offset));
223 s->saturator.set_output_trim_db(params.value_at(kOutputTrimDb, offset));
224 output[static_cast<std::size_t>(k)] =
225 s->saturator.process(input[static_cast<std::size_t>(k)]);
226 }
227 };
228 return t;
229}
230
232 return {
233 "saturator",
234 "Saturator",
235 "Memoryless waveshaping with four harmonic curves and a construction-time "
236 "anti-aliasing policy.",
237 {{"shape",
238 "Shape",
239 "Selects the fixed transfer curve and harmonic character.",
240 {{"tanh", "Tanh", 0.0f},
241 {"atan", "Arctan", 1.0f},
242 {"cubic", "Cubic", 2.0f},
243 {"sinh_arc", "Sinh-Arc", 3.0f}}},
244 {"alias_policy",
245 "Alias Policy",
246 "Selects antiderivative anti-aliasing, two-times oversampling, or the raw curve.",
247 {{"adaa", "ADAA", 0.0f}, {"x2", "2x Oversampling", 1.0f}, {"off", "Off", 2.0f}}}},
248 {{"tanh_adaa", "saturator.tanh", {{"shape", "tanh"}, {"alias_policy", "adaa"}}},
249 {"tanh_x2", "saturator.tanh.x2", {{"shape", "tanh"}, {"alias_policy", "x2"}}},
250 {"tanh_off", "saturator.tanh.off", {{"shape", "tanh"}, {"alias_policy", "off"}}},
251 {"atan_adaa", "saturator.atan", {{"shape", "atan"}, {"alias_policy", "adaa"}}},
252 {"atan_x2", "saturator.atan.x2", {{"shape", "atan"}, {"alias_policy", "x2"}}},
253 {"atan_off", "saturator.atan.off", {{"shape", "atan"}, {"alias_policy", "off"}}},
254 {"cubic_adaa", "saturator.cubic", {{"shape", "cubic"}, {"alias_policy", "adaa"}}},
255 {"cubic_x2", "saturator.cubic.x2", {{"shape", "cubic"}, {"alias_policy", "x2"}}},
256 {"cubic_off", "saturator.cubic.off", {{"shape", "cubic"}, {"alias_policy", "off"}}},
257 {"sinh_arc_adaa",
258 "saturator.sinh_arc",
259 {{"shape", "sinh_arc"}, {"alias_policy", "adaa"}}},
260 {"sinh_arc_x2",
261 "saturator.sinh_arc.x2",
262 {{"shape", "sinh_arc"}, {"alias_policy", "x2"}}},
263 {"sinh_arc_off",
264 "saturator.sinh_arc.off",
265 {{"shape", "sinh_arc"}, {"alias_policy", "off"}}}},
266 {{"drive_db",
267 kDriveDb,
268 "Drive",
269 "dB",
270 "Input gain applied before the transfer curve.",
273 {},
274 {}},
275 {"bias",
276 kBias,
277 "Bias",
278 "",
279 "Offsets the transfer curve to introduce asymmetric harmonics.",
282 {},
283 {}},
284 {"tone_pre_hz",
286 "Pre Tone",
287 "Hz",
288 "Sets the pre-emphasis corner; zero disables the stage.",
291 {},
292 {}},
293 {"mix",
294 kMix,
295 "Mix",
296 "%",
297 "Blends the latency-aligned dry and saturated signals.",
300 {},
301 {}},
302 {"output_trim_db",
304 "Output Trim",
305 "dB",
306 "Applies gain after the wet/dry blend.",
309 {},
310 {}},
311 {"tone_tracking",
313 "Tone Tracking",
314 "",
315 "Makes the de-emphasis corner track the pre-emphasis corner.",
318 {{"off", "Off", 0.0f}, {"on", "On", 1.0f}},
319 {}},
320 {"tone_de_hz",
321 kToneDeHz,
322 "De-emphasis",
323 "Hz",
324 "Sets the independent post-shaper de-emphasis corner when tracking is off.",
327 {},
328 {}},
329 {"pre_boost_db",
331 "Pre Boost",
332 "dB",
333 "Boosts the pre-emphasis stage before nonlinear shaping.",
336 {},
337 {}}},
338 };
339}
340
341} // namespace pulp::host::saturator
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Definition custom_node_type.hpp:51
void set_alias_policy(AliasPolicy policy)
Select the alias policy.
Definition saturator.hpp:548
void prepare(double sample_rate)
Sizes the oversampler and the dry-path alignment delay. May allocate.
Definition saturator.hpp:465
void set_bias(double normalized)
Bias as a normalised [-1, +1].
Definition saturator.hpp:507
void set_shape(Shape shape)
Definition saturator.hpp:490
double worst_case_gain() const
The supremum of |shaped(x) / x| over every input, at the current shape and bias — the bound Forge's r...
Definition saturator.hpp:670
int latency_samples() const noexcept
The integer delay the host should compensate, matched on the dry path.
Definition saturator.hpp:574
Definition forge_saturator_catalog.hpp:44
constexpr const char * kTanhTypeId
Definition forge_saturator_catalog.hpp:50
constexpr state::ParamID kTonePreHz
Definition forge_saturator_catalog.hpp:59
AliasPolicy normalized_alias_policy(AliasPolicy policy) noexcept
Definition forge_saturator_catalog.hpp:122
constexpr state::ParamID kPreBoostDb
Definition forge_saturator_catalog.hpp:64
signal::SaturatorShape Shape
Definition forge_saturator_catalog.hpp:46
signal::SaturatorAliasPolicy AliasPolicy
Definition forge_saturator_catalog.hpp:47
constexpr state::ParamID kMix
Definition forge_saturator_catalog.hpp:60
CustomNodeType make_saturator_node(Shape shape, AliasPolicy alias_policy=AliasPolicy::adaa)
One factory, four registered realizations.
Definition forge_saturator_catalog.hpp:152
const char * alias_policy_id(AliasPolicy policy) noexcept
Definition forge_saturator_catalog.hpp:98
constexpr state::ParamID kDriveDb
Definition forge_saturator_catalog.hpp:57
constexpr const char * kSinhArcTypeId
Definition forge_saturator_catalog.hpp:53
constexpr state::ParamID kOutputTrimDb
Definition forge_saturator_catalog.hpp:61
float saturator_worst_case_gain(Shape shape)
The largest gain this realization can present, for the Forge registry's worst_case_gain row (series l...
Definition forge_saturator_catalog.hpp:141
constexpr const char * kAtanTypeId
Definition forge_saturator_catalog.hpp:51
constexpr const char * kCubicTypeId
Definition forge_saturator_catalog.hpp:52
Shape normalized_shape(Shape shape) noexcept
Definition forge_saturator_catalog.hpp:110
constexpr state::ParamID kToneDeHz
Definition forge_saturator_catalog.hpp:63
ForgeNodeDescriptor saturator_descriptor()
Definition forge_saturator_catalog.hpp:231
constexpr state::ParamID kToneTracking
Definition forge_saturator_catalog.hpp:62
constexpr state::ParamID kBias
Definition forge_saturator_catalog.hpp:58
const char * saturator_type_id(Shape shape)
Definition forge_saturator_catalog.hpp:70
const char * saturator_default_name(Shape shape)
Definition forge_saturator_catalog.hpp:84
@ stepped
Definition forge_param_descriptor.hpp:25
@ continuous
Definition forge_param_descriptor.hpp:25
@ logarithmic
Definition forge_param_descriptor.hpp:29
@ linear
Definition forge_param_descriptor.hpp:29
SaturatorShape
Which member of the waveshaper family a SaturatorT evaluates.
Definition saturator.hpp:136
SaturatorAliasPolicy
How a SaturatorT keeps the harmonics it generates from folding back.
Definition saturator.hpp:144
SaturatorT< float > Saturator
Definition saturator.hpp:844
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
The memoryless saturation stage every drive, fuzz, tube and tape-warmth block in the catalog composes...
Definition custom_node_type.hpp:90
Definition forge_param_descriptor.hpp:127
Definition forge_saturator_catalog.hpp:66
signal::Saturator saturator
Definition forge_saturator_catalog.hpp:67