Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
forge_eurorack_utility_catalog.hpp
Go to the documentation of this file.
1#pragma once
2
3// Eurorack CV-utility bake catalog.
4//
5// These are the modular primitives with no DAW-plugin analogue -- nobody ships
6// a "Multiple" as a VST -- so they live in their own pack rather than being
7// retrofitted into the musical-DSP packs, which stay shared across targets.
8//
9// Voltage conventions follow the published modular standards: audio +/-5 V,
10// unipolar CV 0..10 V, bipolar CV +/-5 V, gates and triggers 10 V, and Schmitt
11// detection with a 0.1 V low / 1.0 V high hysteresis band.
12
15
16#include <algorithm>
17#include <cmath>
18#include <cstdint>
19
21
23inline constexpr float kAudioPeak = 5.0f;
24inline constexpr float kCvUnipolarMax = 10.0f;
25inline constexpr float kCvBipolarPeak = 5.0f;
26inline constexpr float kGateHigh = 10.0f;
27inline constexpr float kSchmittLow = 0.1f;
28inline constexpr float kSchmittHigh = 1.0f;
29
30inline constexpr state::ParamID kAmount = 1;
31inline constexpr state::ParamID kOffset = 2;
32inline constexpr state::ParamID kSlewRise = 3;
33inline constexpr state::ParamID kSlewFall = 4;
34inline constexpr state::ParamID kDivision = 5;
35
36// ── Attenuverter ─────────────────────────────────────────────────────────────
37// The control that makes a knob "first-class CV": scales and optionally inverts
38// an incoming signal, so a patched CV can add to or subtract from a knob value.
39// Amount is bipolar -1..+1; offset is added afterwards in volts.
40
42
45 t.type_id = "eurorack.attenuverter";
46 t.version = 1;
47 t.num_input_ports = t.num_output_ports = 1;
48 t.default_name = "Attenuverter";
49 t.lowerable = true;
50 t.create = []() -> void* { return new AttenuverterInstance{}; };
51 t.destroy = [](void* p) { delete static_cast<AttenuverterInstance*>(p); };
52 t.prepare = [](void*, double, int) {};
53 t.reset = [](void*) {};
54 t.baked_params = {{kAmount, -1.0f, 1.0f, 1.0f},
56 t.process_instance_baked_param = [](void*, audio::BufferView<float>& out,
57 const audio::BufferView<const float>& in, int n,
58 const BakedParamView& params) {
59 const float* input = in.channel_ptr(0);
60 float* output = out.channel_ptr(0);
61 for (int i = 0; i < n; ++i) {
62 const auto o = static_cast<std::int32_t>(i);
63 output[static_cast<std::size_t>(i)] =
64 input[static_cast<std::size_t>(i)] * params.value_at(kAmount, o)
65 + params.value_at(kOffset, o);
66 }
67 };
68 return t;
69}
70
71// ── Slew limiter ─────────────────────────────────────────────────────────────
72// Independent rise and fall times in seconds. Doubles as a portamento/glide and
73// as a lag processor on a CV.
74
76 float y = 0.0f;
77 double sample_time = 1.0 / 48000.0;
78};
79
82 t.type_id = "eurorack.slew";
83 t.version = 1;
84 t.num_input_ports = t.num_output_ports = 1;
85 t.default_name = "Slew Limiter";
86 t.lowerable = true;
87 t.create = []() -> void* { return new SlewInstance{}; };
88 t.destroy = [](void* p) { delete static_cast<SlewInstance*>(p); };
89 t.prepare = [](void* p, double sr, int) {
90 auto& s = *static_cast<SlewInstance*>(p);
91 s.sample_time = sr > 0.0 ? 1.0 / sr : 1.0 / 48000.0;
92 s.y = 0.0f;
93 };
94 t.reset = [](void* p) { static_cast<SlewInstance*>(p)->y = 0.0f; };
95 // Times in seconds; 0 means "instant", which must not divide by zero.
96 t.baked_params = {{kSlewRise, 0.0f, 10.0f, 0.0f}, {kSlewFall, 0.0f, 10.0f, 0.0f}};
97 t.process_instance_baked_param = [](void* p, audio::BufferView<float>& out,
98 const audio::BufferView<const float>& in, int n,
99 const BakedParamView& params) {
100 auto& s = *static_cast<SlewInstance*>(p);
101 const float* input = in.channel_ptr(0);
102 float* output = out.channel_ptr(0);
103 const auto dt = static_cast<float>(s.sample_time);
104 for (int i = 0; i < n; ++i) {
105 const auto o = static_cast<std::int32_t>(i);
106 const float x = input[static_cast<std::size_t>(i)];
107 const float t_s = (x > s.y) ? params.value_at(kSlewRise, o)
108 : params.value_at(kSlewFall, o);
109 if (t_s <= 0.0f) {
110 s.y = x;
111 } else {
112 // Both published CV ranges span 10 V (0..10 or -5..+5).
113 const float slope = kCvUnipolarMax / t_s;
114 const float step = slope * dt;
115 s.y += std::clamp(x - s.y, -step, step);
116 }
117 output[static_cast<std::size_t>(i)] = s.y;
118 }
119 };
120 return t;
121}
122
123// ── Clock divider ────────────────────────────────────────────────────────────
124// Schmitt-triggered on the published thresholds, emitting a 10 V gate for every
125// Nth rising edge. Division is an integer 1..64 carried as a baked param.
126
128 bool high = false;
129 std::int64_t count = 0;
130 bool out_high = false;
131};
132
135 t.type_id = "eurorack.clock_divider";
136 t.version = 1;
137 t.num_input_ports = t.num_output_ports = 1;
138 t.default_name = "Clock Divider";
139 t.lowerable = true;
140 t.create = []() -> void* { return new ClockDividerInstance{}; };
141 t.destroy = [](void* p) { delete static_cast<ClockDividerInstance*>(p); };
142 t.prepare = [](void* p, double, int) { *static_cast<ClockDividerInstance*>(p) = {}; };
143 t.reset = [](void* p) { *static_cast<ClockDividerInstance*>(p) = {}; };
144 t.baked_params = {{kDivision, 1.0f, 64.0f, 2.0f}};
145 t.process_instance_baked_param = [](void* p, audio::BufferView<float>& out,
146 const audio::BufferView<const float>& in, int n,
147 const BakedParamView& params) {
148 auto& s = *static_cast<ClockDividerInstance*>(p);
149 const float* input = in.channel_ptr(0);
150 float* output = out.channel_ptr(0);
151 for (int i = 0; i < n; ++i) {
152 const auto o = static_cast<std::int32_t>(i);
153 const float v = input[static_cast<std::size_t>(i)];
154 // Schmitt hysteresis: only these two thresholds change state.
155 if (!s.high && v >= kSchmittHigh) {
156 s.high = true;
157 const auto div = static_cast<std::int64_t>(
158 std::max(1.0f, std::round(params.value_at(kDivision, o))));
159 if (++s.count >= div) {
160 s.count = 0;
161 s.out_high = true;
162 } else {
163 s.out_high = false;
164 }
165 } else if (s.high && v <= kSchmittLow) {
166 s.high = false;
167 s.out_high = false;
168 }
169 output[static_cast<std::size_t>(i)] = s.out_high ? kGateHigh : 0.0f;
170 }
171 };
172 return t;
173}
174
175// ── Sample and hold ──────────────────────────────────────────────────────────
176// Input 0 is the signal to capture; input 1 is an independent Schmitt trigger.
177
179 bool high = false;
180 float held = 0.0f;
181};
182
185 t.type_id = "eurorack.sample_hold";
186 t.version = 1;
187 t.num_input_ports = 2;
188 t.num_output_ports = 1;
189 t.default_name = "Sample & Hold";
190 t.lowerable = true;
191 t.create = []() -> void* { return new SampleHoldInstance{}; };
192 t.destroy = [](void* p) { delete static_cast<SampleHoldInstance*>(p); };
193 t.prepare = [](void* p, double, int) { *static_cast<SampleHoldInstance*>(p) = {}; };
194 t.reset = [](void* p) { *static_cast<SampleHoldInstance*>(p) = {}; };
195 t.process_instance = [](void* p, audio::BufferView<float>& out,
196 const audio::BufferView<const float>& in, int n) {
197 auto& s = *static_cast<SampleHoldInstance*>(p);
198 const float* signal = in.channel_ptr(0);
199 const float* trigger = in.channel_ptr(1);
200 float* output = out.channel_ptr(0);
201 for (int i = 0; i < n; ++i) {
202 const auto index = static_cast<std::size_t>(i);
203 const float v = trigger[index];
204 if (!s.high && v >= kSchmittHigh) {
205 s.high = true;
206 s.held = signal[index];
207 } else if (s.high && v <= kSchmittLow) {
208 s.high = false;
209 }
210 output[index] = s.held;
211 }
212 };
213 return t;
214}
215
216// ── Forge semantic descriptors ──────────────────────────────────────────────
217// The factories above remain the numeric authority. These descriptors add the
218// vocabulary needed to discover and author the nodes through the installed
219// Forge catalog without duplicating ranges or defaults.
220
222 return {"eurorack_attenuverter", "Attenuverter",
223 "Scales, inverts, and offsets a Eurorack control-voltage signal.",
224 {}, {{"default", "eurorack.attenuverter"}},
225 {{"amount", kAmount, "Amount", "",
226 "Scales the input from full inversion through zero to unity.",
228 {"offset_v", kOffset, "Offset", "V",
229 "Adds a bipolar voltage offset after scaling.",
231}
232
234 return {"eurorack_slew", "Slew Limiter",
235 "Limits independent rising and falling CV slopes for glide and lag processing.",
236 {}, {{"default", "eurorack.slew"}},
237 {{"rise_s", kSlewRise, "Rise", "s",
238 "Sets the time for a ten-volt upward transition.",
240 {"fall_s", kSlewFall, "Fall", "s",
241 "Sets the time for a ten-volt downward transition.",
243}
244
246 return {"eurorack_clock_divider", "Clock Divider",
247 "Emits a ten-volt gate on every Nth Schmitt-detected input clock edge.",
248 {}, {{"default", "eurorack.clock_divider"}},
249 {{"division", kDivision, "Division", "",
250 "Sets the integer input-clock division from one through sixty-four.",
252}
253
255 return {"eurorack_sample_hold", "Sample & Hold",
256 "Captures the signal input on each Schmitt-detected trigger edge and holds it.",
257 {}, {{"default", "eurorack.sample_hold"}}, {}};
258}
259
260} // namespace pulp::host::eurorack
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Definition custom_node_type.hpp:51
Definition forge_eurorack_utility_catalog.hpp:20
constexpr float kGateHigh
Definition forge_eurorack_utility_catalog.hpp:26
ForgeNodeDescriptor attenuverter_descriptor()
Definition forge_eurorack_utility_catalog.hpp:221
CustomNodeType make_clock_divider_node()
Definition forge_eurorack_utility_catalog.hpp:133
constexpr state::ParamID kAmount
Definition forge_eurorack_utility_catalog.hpp:30
CustomNodeType make_slew_node()
Definition forge_eurorack_utility_catalog.hpp:80
constexpr state::ParamID kSlewFall
Definition forge_eurorack_utility_catalog.hpp:33
constexpr float kAudioPeak
Published voltage standards, in one place so nodes cannot drift apart.
Definition forge_eurorack_utility_catalog.hpp:23
constexpr state::ParamID kDivision
Definition forge_eurorack_utility_catalog.hpp:34
CustomNodeType make_attenuverter_node()
Definition forge_eurorack_utility_catalog.hpp:43
constexpr state::ParamID kOffset
Definition forge_eurorack_utility_catalog.hpp:31
ForgeNodeDescriptor clock_divider_descriptor()
Definition forge_eurorack_utility_catalog.hpp:245
ForgeNodeDescriptor sample_hold_descriptor()
Definition forge_eurorack_utility_catalog.hpp:254
CustomNodeType make_sample_hold_node()
Definition forge_eurorack_utility_catalog.hpp:183
constexpr float kSchmittHigh
Definition forge_eurorack_utility_catalog.hpp:28
constexpr float kCvUnipolarMax
Definition forge_eurorack_utility_catalog.hpp:24
constexpr float kCvBipolarPeak
Definition forge_eurorack_utility_catalog.hpp:25
constexpr float kSchmittLow
Definition forge_eurorack_utility_catalog.hpp:27
constexpr state::ParamID kSlewRise
Definition forge_eurorack_utility_catalog.hpp:32
ForgeNodeDescriptor slew_descriptor()
Definition forge_eurorack_utility_catalog.hpp:233
@ continuous
Definition forge_param_descriptor.hpp:25
@ linear
Definition forge_param_descriptor.hpp:29
Definition additive_bank.hpp:218
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_eurorack_utility_catalog.hpp:41
Definition forge_eurorack_utility_catalog.hpp:127
bool high
Definition forge_eurorack_utility_catalog.hpp:128
bool out_high
Definition forge_eurorack_utility_catalog.hpp:130
std::int64_t count
Definition forge_eurorack_utility_catalog.hpp:129
Definition forge_eurorack_utility_catalog.hpp:178
bool high
Definition forge_eurorack_utility_catalog.hpp:179
float held
Definition forge_eurorack_utility_catalog.hpp:180
Definition forge_eurorack_utility_catalog.hpp:75
double sample_time
Definition forge_eurorack_utility_catalog.hpp:77
float y
Definition forge_eurorack_utility_catalog.hpp:76