Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_fdn_reverb_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Multirate FDN reverb — the bake-layer catalog node.
4//
5// Wraps signal::FdnReverbT as five registered node types (room / hall / galaxy
6// / shimmer / lofi) that are ONE realization of ONE engine. A mode stamps the
7// twelve baked params' defaults and the per-mode output lowpass; it never
8// selects a code path, so every mode runs byte-identical DSP and the difference
9// between a room and a galaxy is entirely in the numbers. That is what makes
10// "five reverbs" cost one implementation to maintain and one suite to prove.
11//
12// TRUE STEREO, WET ONLY. The tank's fan-out already puts left into the even
13// lines and right into the odd, so the node's two ports are the L and R halves
14// of one logical wire (Forge instances it once across both rails) rather than a
15// dual-mono pair. It emits wet only: dry/wet is the graph's `make_drywet_node`,
16// so this block never has to guess what "dry" means for its caller.
17//
18// Header-only, like the rest of the catalog: FdnReverbT is a header-only
19// template, so including this pulls in no new link dependency for pulp-host.
20//
21// TIER. A thirteenth baked param, appended after the engine's twelve, selects
22// the engine behind the same knobs: 0 runs the multirate network (the default,
23// so a patch baked before the param existed renders exactly as it did), 1 the
24// classic comb/allpass tier (signal::ClassicReverbT). A tier change is a clean
25// flush of the engine it switches to. The network-only knobs (low damping,
26// modulation, shimmer, drive, bloom, tank rate) stay declared on every
27// realization and have no effect on the classic tier.
28//
29// PARAMETER GRANULARITY. The twelve knobs are read at the engine's own control
30// cadence (32 samples) rather than once per block: the node walks the block in
31// control-rate chunks and re-reads the BakedParamView at each one. That is the
32// honest granularity — the engine re-derives its delay lengths, decay gains and
33// damping coefficients on exactly that cadence and smooths continuous params
34// over 5 ms, so a finer read would be discarded, and a block-rate read would
35// make a knob sweep step audibly at large buffer sizes.
36
39
41
42#include <algorithm>
43#include <cstddef>
44#include <cstdint>
45
47
48namespace fdn = pulp::signal::fdn;
49
50// ── Stable type ids ──────────────────────────────────────────────────────
51// One per mode. The id is what a baked artifact stores, so it is frozen: a
52// re-voicing of a mode changes its defaults, never its id.
53inline constexpr const char* kRoomTypeId = "reverb.room";
54inline constexpr const char* kHallTypeId = "reverb.hall";
55inline constexpr const char* kGalaxyTypeId = "reverb.galaxy";
56inline constexpr const char* kShimmerTypeId = "reverb.shimmer";
57inline constexpr const char* kLofiTypeId = "reverb.lofi";
58
59inline constexpr const char* type_id_for(fdn::Mode mode) {
60 switch (mode) {
61 case fdn::Mode::room: return kRoomTypeId;
62 case fdn::Mode::hall: return kHallTypeId;
63 case fdn::Mode::galaxy: return kGalaxyTypeId;
64 case fdn::Mode::shimmer: return kShimmerTypeId;
65 case fdn::Mode::lofi: return kLofiTypeId;
66 case fdn::Mode::count: break;
67 }
68 return kHallTypeId;
69}
70
71inline constexpr const char* display_name_for(fdn::Mode mode) {
72 switch (mode) {
73 case fdn::Mode::room: return "Room";
74 case fdn::Mode::hall: return "Hall";
75 case fdn::Mode::galaxy: return "Galaxy";
76 case fdn::Mode::shimmer: return "Shimmer";
77 case fdn::Mode::lofi: return "Lo-Fi";
78 case fdn::Mode::count: break;
79 }
80 return "Reverb";
81}
82
83// ── Injectable param ids ─────────────────────────────────────────────────
84// Node-local (the framework namespaces per node), and deliberately laid out as
85// `1 + the engine's own parameter index` so the two orders can never drift:
86// param_id_for / engine_param_for are exact inverses, and the contract test
87// walks the whole table rather than trusting the mapping.
88inline constexpr state::ParamID param_id_for(fdn::Param p) {
89 return static_cast<state::ParamID>(static_cast<int>(p) + 1);
90}
91
92inline constexpr fdn::Param engine_param_for(state::ParamID id) {
93 return static_cast<fdn::Param>(static_cast<int>(id) - 1);
94}
95
96inline constexpr state::ParamID kDecay = param_id_for(fdn::Param::decay);
97inline constexpr state::ParamID kSize = param_id_for(fdn::Param::size);
98inline constexpr state::ParamID kPredelay = param_id_for(fdn::Param::predelay);
99inline constexpr state::ParamID kDampHi = param_id_for(fdn::Param::damp_hi);
100inline constexpr state::ParamID kDampLo = param_id_for(fdn::Param::damp_lo);
101inline constexpr state::ParamID kDiffusion = param_id_for(fdn::Param::diffusion);
102inline constexpr state::ParamID kMod = param_id_for(fdn::Param::mod);
103inline constexpr state::ParamID kShimmer = param_id_for(fdn::Param::shimmer);
104inline constexpr state::ParamID kDrive = param_id_for(fdn::Param::drive);
105inline constexpr state::ParamID kBloom = param_id_for(fdn::Param::bloom);
106inline constexpr state::ParamID kWidth = param_id_for(fdn::Param::width);
107inline constexpr state::ParamID kTankRate = param_id_for(fdn::Param::tank_rate);
108// Not an engine parameter: the node-local id after the engine's table. Append
109// new node params after this one; never renumber.
110inline constexpr state::ParamID kTier = param_id_for(fdn::Param::count);
111
112inline constexpr fdn::Tier tier_for(float value) {
113 return value >= 0.5f ? fdn::Tier::classic : fdn::Tier::network;
114}
115
116// Worst-case linear gain the node can present, for the path-gain lint. The tank
117// is unitary and the realized per-pass gain is provably below kGainCeil, so the
118// wet tail cannot exceed its own steady state; the output soft limiter caps the
119// wet return at kWetLimiterHeadroom regardless, which is the hard bound.
120inline constexpr float kWorstCaseGain = static_cast<float>(fdn::kWetLimiterHeadroom);
121
124 fdn::Mode mode = fdn::Mode::hall;
125};
126
127// Declare the twelve baked params with the mode's stamped defaults. Ranges come
128// from the engine's own parameter table and defaults from the mode table, so
129// there is no second copy of either to drift.
130inline void declare_params(CustomNodeType& t, fdn::Mode mode) {
131 for (int p = 0; p < fdn::kNumParams; ++p) {
132 const fdn::ParamSpec& spec = fdn::kParamSpecs[static_cast<std::size_t>(p)];
133 const auto param = static_cast<fdn::Param>(p);
134 t.baked_params.push_back({param_id_for(param), static_cast<float>(spec.min),
135 static_cast<float>(spec.max),
136 static_cast<float>(fdn::mode_default(mode, param))});
137 }
138 t.baked_params.push_back({kTier, 0.0f, 1.0f, 0.0f});
139}
140
141inline CustomNodeType make_fdn_reverb_node(fdn::Mode mode) {
143 t.type_id = type_id_for(mode);
144 t.version = 1;
145 t.num_input_ports = 2;
146 t.num_output_ports = 2;
147 t.default_name = display_name_for(mode);
148 t.lowerable = true;
149
150 const auto captured_mode = mode;
151 t.create = [captured_mode]() -> void* {
152 auto* s = new ReverbInstance{};
153 s->mode = captured_mode;
154 return s;
155 };
156 t.destroy = [](void* p) { delete static_cast<ReverbInstance*>(p); };
157 t.prepare = [](void* p, double sr, int max_block) {
158 auto* s = static_cast<ReverbInstance*>(p);
159 // prepare() is the only allocating call; it sizes for the worst case
160 // (96 kHz tank at this block size) so a live tank_rate injection never
161 // reaches the allocator.
162 s->engine.prepare(sr, std::max(max_block, 1));
163 s->engine.set_mode(s->mode);
164 };
165 t.reset = [](void* p) { static_cast<ReverbInstance*>(p)->engine.reset(); };
166
167 declare_params(t, mode);
168
169 t.process_instance_baked_param =
170 [](void* p, audio::BufferView<float>& out,
171 const audio::BufferView<const float>& in, int n, const BakedParamView& params) {
172 auto* s = static_cast<ReverbInstance*>(p);
173 const float* il = in.channel_ptr(0);
174 const float* ir = in.channel_ptr(1);
175 float* ol = out.channel_ptr(0);
176 float* or_ = out.channel_ptr(1);
177
178 for (int offset = 0; offset < n; offset += fdn::kControlRateSamples) {
179 const int chunk = std::min(n - offset, fdn::kControlRateSamples);
180 const auto at = static_cast<std::int32_t>(offset);
181 for (int k = 0; k < fdn::kNumParams; ++k) {
182 const auto param = static_cast<fdn::Param>(k);
183 s->engine.set_parameter(
184 param, static_cast<double>(params.value_at(param_id_for(param), at)));
185 }
186 s->engine.set_tier(tier_for(params.value_at(kTier, at)));
187 s->engine.process_block(il + offset, ir + offset, ol + offset,
188 or_ + offset, chunk);
189 }
190 };
191 return t;
192}
193
196 d.key = "fdn_reverb";
197 d.label = "FDN Reverb";
198 d.description = "True-stereo multirate feedback-delay-network reverb with five musical "
199 "voicings and a wet-only output, plus a lighter classic comb/allpass "
200 "tier behind the same controls.";
201 d.axes = {{"mode",
202 "Mode",
203 "A complete voicing of the shared tank, including its starting "
204 "parameter values and output damping.",
205 {{"room", "Room", 0.0f},
206 {"hall", "Hall", 1.0f},
207 {"galaxy", "Galaxy", 2.0f},
208 {"shimmer", "Shimmer", 3.0f},
209 {"lofi", "Lo-Fi", 4.0f}}}};
210 d.realizations = {
211 {"room", kRoomTypeId, {{"mode", "room"}}},
212 {"hall", kHallTypeId, {{"mode", "hall"}}},
213 {"galaxy", kGalaxyTypeId, {{"mode", "galaxy"}}},
214 {"shimmer", kShimmerTypeId, {{"mode", "shimmer"}}},
215 {"lofi", kLofiTypeId, {{"mode", "lofi"}}},
216 };
217 d.params = {
218 {"decay", kDecay, "Decay", "s", "Reverberation decay time.", ForgeParamKind::continuous,
220 {"size", kSize, "Size", "%", "Scales the tank's delay-line lengths.",
222 {"predelay", kPredelay, "Predelay", "ms",
223 "Time between the dry event and the start of the reverberant field.",
225 {"damp_hi", kDampHi, "High Damping", "%", "Shortens the decay of high frequencies.",
227 {"damp_lo", kDampLo, "Low Damping", "%", "Shortens the decay of low frequencies.",
229 {"diffusion", kDiffusion, "Diffusion", "%",
230 "Controls echo density and the smoothness of the tail.", ForgeParamKind::continuous,
232 {"mod", kMod, "Modulation", "%",
233 "Modulates tank delay lengths to soften static resonances.", ForgeParamKind::continuous,
235 {"shimmer", kShimmer, "Shimmer", "%",
236 "Amount of pitch-raised energy recirculated through the tank.", ForgeParamKind::continuous,
238 {"drive", kDrive, "Drive", "%", "Nonlinear drive within the reverberant return.",
240 {"bloom", kBloom, "Bloom", "%", "Builds late-tail energy after the initial reflections.",
242 {"width", kWidth, "Width", "%", "Stereo spread of the wet return.",
244 {"tank_rate", kTankRate, "Tank Rate", "Hz", "Internal multirate tank processing frequency.",
246 {"tier",
247 kTier,
248 "Tier",
249 "",
250 "Feedback-delay network, or the lighter classic comb and allpass reverb. Low "
251 "damping, modulation, shimmer, drive, bloom, and tank rate apply to the network only.",
254 {{"fdn", "FDN", 0.0f}, {"classic", "Classic", 1.0f}}},
255 };
256 return d;
257}
258
259} // namespace pulp::host::forge_fdn
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Definition custom_node_type.hpp:51
Definition fdn_reverb.hpp:56
Definition forge_fdn_reverb_catalog.hpp:46
CustomNodeType make_fdn_reverb_node(fdn::Mode mode)
Definition forge_fdn_reverb_catalog.hpp:141
constexpr const char * kShimmerTypeId
Definition forge_fdn_reverb_catalog.hpp:56
constexpr const char * kHallTypeId
Definition forge_fdn_reverb_catalog.hpp:54
constexpr state::ParamID kDampHi
Definition forge_fdn_reverb_catalog.hpp:99
constexpr state::ParamID kDecay
Definition forge_fdn_reverb_catalog.hpp:96
constexpr state::ParamID kPredelay
Definition forge_fdn_reverb_catalog.hpp:98
constexpr fdn::Param engine_param_for(state::ParamID id)
Definition forge_fdn_reverb_catalog.hpp:92
constexpr state::ParamID kDrive
Definition forge_fdn_reverb_catalog.hpp:104
constexpr float kWorstCaseGain
Definition forge_fdn_reverb_catalog.hpp:120
constexpr state::ParamID kShimmer
Definition forge_fdn_reverb_catalog.hpp:103
constexpr const char * type_id_for(fdn::Mode mode)
Definition forge_fdn_reverb_catalog.hpp:59
ForgeNodeDescriptor fdn_reverb_descriptor()
Definition forge_fdn_reverb_catalog.hpp:194
constexpr state::ParamID kWidth
Definition forge_fdn_reverb_catalog.hpp:106
constexpr const char * display_name_for(fdn::Mode mode)
Definition forge_fdn_reverb_catalog.hpp:71
constexpr state::ParamID kTankRate
Definition forge_fdn_reverb_catalog.hpp:107
constexpr state::ParamID kDiffusion
Definition forge_fdn_reverb_catalog.hpp:101
constexpr state::ParamID kSize
Definition forge_fdn_reverb_catalog.hpp:97
constexpr const char * kRoomTypeId
Definition forge_fdn_reverb_catalog.hpp:53
constexpr state::ParamID kDampLo
Definition forge_fdn_reverb_catalog.hpp:100
constexpr state::ParamID kMod
Definition forge_fdn_reverb_catalog.hpp:102
constexpr state::ParamID kTier
Definition forge_fdn_reverb_catalog.hpp:110
void declare_params(CustomNodeType &t, fdn::Mode mode)
Definition forge_fdn_reverb_catalog.hpp:130
constexpr state::ParamID kBloom
Definition forge_fdn_reverb_catalog.hpp:105
constexpr const char * kLofiTypeId
Definition forge_fdn_reverb_catalog.hpp:57
constexpr fdn::Tier tier_for(float value)
Definition forge_fdn_reverb_catalog.hpp:112
constexpr const char * kGalaxyTypeId
Definition forge_fdn_reverb_catalog.hpp:55
constexpr state::ParamID param_id_for(fdn::Param p)
Definition forge_fdn_reverb_catalog.hpp:88
@ 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
Definition config.hpp:15
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_fdn_reverb_catalog.hpp:122
fdn::Mode mode
Definition forge_fdn_reverb_catalog.hpp:124
signal::FdnReverbT< float > engine
Definition forge_fdn_reverb_catalog.hpp:123