Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_character_delay_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Multi-character delay — bake-layer catalog node.
4//
5// Wraps pulp::signal::CharacterDelayT as a lowerable custom node with
6// injectable baked params, following the same shape as the lo-fi kit's
7// make_delay_node() / make_filter_node(). That kit's simple feedback delay
8// stays exactly as it is; this is a different, larger instrument and complements
9// rather than replaces it.
10//
11// Per-character type ids mirror the per-mode "svf" realization pattern: the
12// CHARACTER is a registration-time choice, not an injectable param. A baked
13// build authored as tape is tape for the artifact's life, and no control-thread
14// write can turn it into a BBD mid-render. The characters differ in buffer
15// topology and in which filters exist at all, so making the choice a parameter
16// would mean either running all five and crossfading (five times the cost) or
17// switching topology under the audio thread (a click at best).
18//
19// Stereo, two ports in and two out, as ONE logical stereo wire — the same
20// convention the lo-fi kit's ping-pong node uses. The module is genuinely
21// stereo (crossfeed, per-channel instability decorrelation, per-channel
22// segment lengths in reverse), so instancing it dual-mono would silently drop
23// half of what it does.
24//
25// WET ONLY, like the DSP: compose with make_drywet_node() for a mix control.
26// This differs from the lo-fi delay node, which outputs dry + wet; it is the
27// deliberate choice, because at five characters and 1.1 feedback the useful
28// dry/wet balance is a per-patch decision, not a fixed sum.
29
32
34
35#include <algorithm>
36#include <cmath>
37#include <cstddef>
38#include <cstdint>
39
41
44
45// ── Stable type ids ───────────────────────────────────────────────────────
46inline constexpr const char* kCleanTypeId = "delay.clean";
47inline constexpr const char* kVintageTypeId = "delay.vintage";
48inline constexpr const char* kTapeTypeId = "delay.tape";
49inline constexpr const char* kBbdTypeId = "delay.bbd";
50inline constexpr const char* kDiffusionTypeId = "delay.diffusion";
54inline constexpr const char* kTapePhysicalTypeId = "delay.tape_physical";
55
56// ── Injectable param ids ──────────────────────────────────────────────────
57// Node-local; the framework namespaces per node so two nodes never collide.
58inline constexpr state::ParamID kTimeMs = 1; // ms, log-tapered by the host
59inline constexpr state::ParamID kTimeOffset = 2; // × on the right channel's time
60inline constexpr state::ParamID kFeedback = 3; // 0–1.1, displayed 0–110%
61inline constexpr state::ParamID kCrossfeed = 4; // %
62inline constexpr state::ParamID kCharacter = 5; // %
63inline constexpr state::ParamID kModRate = 6; // %, internally log-mapped
64inline constexpr state::ParamID kModDepth = 7; // %
65inline constexpr state::ParamID kDuck = 8; // %
66inline constexpr state::ParamID kFreeze = 9; // stepped 0/1
67inline constexpr state::ParamID kReverse = 10; // stepped 0/1
68
70inline constexpr float kMaxTimeMs = 2000.0f;
71
75namespace detail {
76inline bool baked_switch_on(float value) noexcept {
77 return std::isfinite(value) && value >= 0.5f;
78}
79} // namespace detail
80
84
86inline const char* character_delay_type_id(Character character,
87 TapeTier tier = TapeTier::standard) {
88 switch (character) {
89 case Character::vintage_digital: return kVintageTypeId;
90 case Character::tape:
91 return tier == TapeTier::physical ? kTapePhysicalTypeId : kTapeTypeId;
92 case Character::bbd: return kBbdTypeId;
93 case Character::diffusion: return kDiffusionTypeId;
94 case Character::clean:
95 default: return kCleanTypeId;
96 }
97}
98
99inline const char* character_delay_default_name(Character character, TapeTier tier) {
100 switch (character) {
101 case Character::vintage_digital: return "Vintage Delay";
102 case Character::tape:
103 return tier == TapeTier::physical ? "Tape Delay (Physical)" : "Tape Delay";
104 case Character::bbd: return "BBD Delay";
105 case Character::diffusion: return "Diffusion Delay";
106 case Character::clean:
107 default: return "Clean Delay";
108 }
109}
110
118 TapeTier tier = TapeTier::standard,
119 float tape_speed_ips = 7.5f) {
121 t.type_id = character_delay_type_id(character, tier);
122 t.version = 1;
123 t.num_input_ports = 2; // 0 = left, 1 = right (ONE logical stereo wire)
124 t.num_output_ports = 2;
125 t.default_name = character_delay_default_name(character, tier);
126 t.lowerable = true;
127
128 t.create = []() -> void* { return new CharacterDelayInstance{}; };
129 t.destroy = [](void* p) { delete static_cast<CharacterDelayInstance*>(p); };
130 t.prepare = [character, tier, tape_speed_ips](void* p, double sr, int /*max_block*/) {
131 auto* s = static_cast<CharacterDelayInstance*>(p);
132 s->delay.set_character(character);
133 s->delay.set_tape_tier(tier);
134 s->delay.set_tape_speed_ips(tape_speed_ips);
135 s->delay.set_sample_rate(sr);
136 };
137 t.reset = [](void* p) { static_cast<CharacterDelayInstance*>(p)->delay.reset(); };
138
139 // Ranges and defaults are the module's canonical contract; the Forge layer
140 // mirrors them and is responsible for the display units and for converting
141 // musical divisions to milliseconds (the DSP has no tempo input by design).
142 t.baked_params.push_back({kTimeMs, 1.0f, kMaxTimeMs, 350.0f});
143 t.baked_params.push_back({kTimeOffset, 0.5f, 1.5f, 1.0f});
144 t.baked_params.push_back({kFeedback, 0.0f, 1.1f, 0.35f});
145 t.baked_params.push_back({kCrossfeed, 0.0f, 1.0f, 0.0f});
146 t.baked_params.push_back({kCharacter, 0.0f, 1.0f, 0.5f});
147 t.baked_params.push_back({kModRate, 0.0f, 1.0f, 0.3f});
148 t.baked_params.push_back({kModDepth, 0.0f, 1.0f, 0.0f});
149 t.baked_params.push_back({kDuck, 0.0f, 1.0f, 0.0f});
150 t.baked_params.push_back({kFreeze, 0.0f, 1.0f, 0.0f});
151 t.baked_params.push_back({kReverse, 0.0f, 1.0f, 0.0f});
152
153 t.process_instance_baked_param = [](void* p, audio::BufferView<float>& out,
154 const audio::BufferView<const float>& in, int n,
155 const BakedParamView& params) {
156 auto* s = static_cast<CharacterDelayInstance*>(p);
157 const float* in_left = in.channel_ptr(0);
158 const float* in_right = in.channel_ptr(1);
159 float* out_left = out.channel_ptr(0);
160 float* out_right = out.channel_ptr(1);
161
162 // One sample at a time so every knob is sample-accurate. The setters
163 // are a clamp and a store — the module smooths internally and only
164 // recomputes coefficients at its own control rate — so this costs an
165 // inlined call per param rather than a filter redesign per sample.
166 for (int k = 0; k < n; ++k) {
167 const auto offset = static_cast<std::int32_t>(k);
168 s->delay.set_time_ms(params.value_at(kTimeMs, offset));
169 s->delay.set_time_offset(params.value_at(kTimeOffset, offset));
170 s->delay.set_feedback(params.value_at(kFeedback, offset));
171 s->delay.set_crossfeed(params.value_at(kCrossfeed, offset));
172 s->delay.set_character_amount(params.value_at(kCharacter, offset));
173 s->delay.set_mod(params.value_at(kModRate, offset),
174 params.value_at(kModDepth, offset));
175 s->delay.set_duck(params.value_at(kDuck, offset));
176 s->delay.set_freeze(detail::baked_switch_on(params.value_at(kFreeze, offset)));
177 s->delay.set_reverse(detail::baked_switch_on(params.value_at(kReverse, offset)));
178
179 float left = in_left[static_cast<std::size_t>(k)];
180 float right = in_right[static_cast<std::size_t>(k)];
181 s->delay.process(&left, &right, 1);
182 out_left[static_cast<std::size_t>(k)] = left;
183 out_right[static_cast<std::size_t>(k)] = right;
184 }
185 };
186 return t;
187}
188
191 d.key = "character_delay";
192 d.label = "Character Delay";
193 d.description = "Stereo wet-only delay spanning clean, vintage digital, tape, bucket-brigade, "
194 "and diffused echoes.";
195 d.axes = {{"character",
196 "Character",
197 "Delay-line and coloration topology fixed when the node is built.",
198 {{"clean", "Clean", 0.0f},
199 {"vintage", "Vintage Digital", 1.0f},
200 {"tape", "Tape", 2.0f},
201 {"tape_physical", "Tape (Physical)", 3.0f},
202 {"bbd", "BBD", 4.0f},
203 {"diffusion", "Diffusion", 5.0f}}}};
204 d.realizations = {
205 {"clean", kCleanTypeId, {{"character", "clean"}}},
206 {"vintage", kVintageTypeId, {{"character", "vintage"}}},
207 {"tape", kTapeTypeId, {{"character", "tape"}}},
208 {"tape_physical", kTapePhysicalTypeId, {{"character", "tape_physical"}}},
209 {"bbd", kBbdTypeId, {{"character", "bbd"}}},
210 {"diffusion", kDiffusionTypeId, {{"character", "diffusion"}}},
211 };
212 d.params = {
213 {"time_ms", kTimeMs, "Time", "ms", "Base delay time.", ForgeParamKind::continuous,
215 {"time_offset", kTimeOffset, "Time Offset", "x",
216 "Right-channel delay-time multiplier for stereo offset.", ForgeParamKind::continuous,
218 {"feedback", kFeedback, "Feedback", "%",
219 "Amount of delayed signal returned to the delay input.", ForgeParamKind::continuous,
221 {"crossfeed", kCrossfeed, "Crossfeed", "%",
222 "Amount of each channel fed into the opposite channel's feedback path.",
224 {"character", kCharacter, "Character", "%",
225 "Intensity of the selected delay topology's coloration.", ForgeParamKind::continuous,
227 {"mod_rate", kModRate, "Modulation Rate", "%",
228 "Rate of delay-time instability within the selected character.",
230 {"mod_depth", kModDepth, "Modulation Depth", "%", "Depth of delay-time instability.",
232 {"duck", kDuck, "Duck", "%", "Reduces delayed output while the input is active.",
234 {"freeze",
235 kFreeze,
236 "Freeze",
237 "",
238 "Holds the current delay contents in a sustaining feedback loop.",
241 {{"off", "Off", 0.0f}, {"on", "On", 1.0f}}},
242 {"reverse",
243 kReverse,
244 "Reverse",
245 "",
246 "Reads completed delay segments in reverse.",
249 {{"off", "Off", 0.0f}, {"on", "On", 1.0f}}},
250 };
251 return d;
252}
253
254} // namespace pulp::host::character_delay
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Definition custom_node_type.hpp:51
Character
Definition character_delay.hpp:87
TapeTier
Definition character_delay.hpp:88
Stepped controls fail closed on malformed host automation.
Definition forge_character_delay_catalog.hpp:75
bool baked_switch_on(float value) noexcept
Definition forge_character_delay_catalog.hpp:76
Definition forge_character_delay_catalog.hpp:40
constexpr state::ParamID kCharacter
Definition forge_character_delay_catalog.hpp:62
CustomNodeType make_character_delay_node(Character character, TapeTier tier=TapeTier::standard, float tape_speed_ips=7.5f)
One factory, six registered realizations.
Definition forge_character_delay_catalog.hpp:117
constexpr state::ParamID kModRate
Definition forge_character_delay_catalog.hpp:63
ForgeNodeDescriptor character_delay_descriptor()
Definition forge_character_delay_catalog.hpp:189
const char * character_delay_default_name(Character character, TapeTier tier)
Definition forge_character_delay_catalog.hpp:99
constexpr const char * kBbdTypeId
Definition forge_character_delay_catalog.hpp:49
constexpr const char * kVintageTypeId
Definition forge_character_delay_catalog.hpp:47
constexpr state::ParamID kTimeOffset
Definition forge_character_delay_catalog.hpp:59
constexpr state::ParamID kTimeMs
Definition forge_character_delay_catalog.hpp:58
signal::CharacterDelay::TapeTier TapeTier
Definition forge_character_delay_catalog.hpp:43
constexpr const char * kTapePhysicalTypeId
The physical tape tier is its own realization of the same factory: the tier is a construction-time co...
Definition forge_character_delay_catalog.hpp:54
constexpr state::ParamID kDuck
Definition forge_character_delay_catalog.hpp:65
constexpr const char * kDiffusionTypeId
Definition forge_character_delay_catalog.hpp:50
constexpr const char * kCleanTypeId
Definition forge_character_delay_catalog.hpp:46
constexpr state::ParamID kFreeze
Definition forge_character_delay_catalog.hpp:66
constexpr const char * kTapeTypeId
Definition forge_character_delay_catalog.hpp:48
constexpr state::ParamID kCrossfeed
Definition forge_character_delay_catalog.hpp:61
const char * character_delay_type_id(Character character, TapeTier tier=TapeTier::standard)
Stable type id for a (character, tier) pair.
Definition forge_character_delay_catalog.hpp:86
constexpr state::ParamID kFeedback
Definition forge_character_delay_catalog.hpp:60
constexpr float kMaxTimeMs
Longest addressable delay, in ms. Matches the DSP's own maximum.
Definition forge_character_delay_catalog.hpp:70
constexpr state::ParamID kReverse
Definition forge_character_delay_catalog.hpp:67
signal::CharacterDelay::Character Character
Definition forge_character_delay_catalog.hpp:42
constexpr state::ParamID kModDepth
Definition forge_character_delay_catalog.hpp:64
@ 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
CharacterDelayT< float > CharacterDelay
Definition character_delay.hpp:854
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_character_delay_catalog.hpp:81
signal::CharacterDelay delay
Definition forge_character_delay_catalog.hpp:82