MayaFlux 0.2.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ResonatorNetwork.cpp
Go to the documentation of this file.
2
4
6
8
9//-----------------------------------------------------------------------------
10// Preset tables
11//-----------------------------------------------------------------------------
12
13namespace {
14
15 struct FormantEntry {
16 double frequency;
17 double q;
18 };
19
20 /*
21 * Source: Peterson & Barney (1952) / Hillenbrand et al. (1995) averaged values.
22 * Five formants; lower formants have broader absolute bandwidths modelled by
23 * lower Q. Q approximated as F / BW with BW ≈ 50–80 Hz for F1, scaling upward.
24 */
25 const FormantEntry k_vowel_a[] = {
26 { .frequency = 800.0, .q = 16.0 },
27 { .frequency = 1200.0, .q = 30.0 },
28 { .frequency = 2500.0, .q = 55.0 },
29 { .frequency = 3500.0, .q = 70.0 },
30 { .frequency = 4500.0, .q = 90.0 },
31 };
32
33 const FormantEntry k_vowel_e[] = {
34 { .frequency = 400.0, .q = 10.0 },
35 { .frequency = 2000.0, .q = 45.0 },
36 { .frequency = 2600.0, .q = 55.0 },
37 { .frequency = 3500.0, .q = 70.0 },
38 { .frequency = 4500.0, .q = 90.0 },
39 };
40
41 const FormantEntry k_vowel_i[] = {
42 { .frequency = 270.0, .q = 7.0 },
43 { .frequency = 2300.0, .q = 50.0 },
44 { .frequency = 3000.0, .q = 60.0 },
45 { .frequency = 3500.0, .q = 70.0 },
46 { .frequency = 4500.0, .q = 90.0 },
47 };
48
49 const FormantEntry k_vowel_o[] = {
50 { .frequency = 500.0, .q = 12.0 },
51 { .frequency = 900.0, .q = 22.0 },
52 { .frequency = 2500.0, .q = 55.0 },
53 { .frequency = 3500.0, .q = 70.0 },
54 { .frequency = 4500.0, .q = 90.0 },
55 };
56
57 const FormantEntry k_vowel_u[] = {
58 { .frequency = 300.0, .q = 8.0 },
59 { .frequency = 800.0, .q = 20.0 },
60 { .frequency = 2300.0, .q = 50.0 },
61 { .frequency = 3500.0, .q = 70.0 },
62 { .frequency = 4500.0, .q = 90.0 },
63 };
64
65 constexpr size_t k_preset_formant_count = 5;
66
67} // namespace
68
69//-----------------------------------------------------------------------------
70// Static helper
71//-----------------------------------------------------------------------------
72
74 size_t n,
75 std::vector<double>& out_freqs,
76 std::vector<double>& out_qs)
77{
78 const FormantEntry* table = nullptr;
79
80 switch (preset) {
82 table = k_vowel_a;
83 break;
85 table = k_vowel_e;
86 break;
88 table = k_vowel_i;
89 break;
91 table = k_vowel_o;
92 break;
94 table = k_vowel_u;
95 break;
96 default:
97 break;
98 }
99
100 out_freqs.resize(n, 440.0);
101 out_qs.resize(n, 10.0);
102
103 if (!table) {
104 return;
105 }
106
107 const size_t defined = std::min(n, k_preset_formant_count);
108 for (size_t i = 0; i < defined; ++i) {
109 out_freqs[i] = table[i].frequency;
110 out_qs[i] = table[i].q;
111 }
112}
113
114//-----------------------------------------------------------------------------
115// Construction
116//-----------------------------------------------------------------------------
117
119 FormantPreset preset)
120{
121 std::vector<double> freqs, qs;
122 preset_to_vectors(preset, num_resonators, freqs, qs);
123 build_resonators(freqs, qs);
124}
125
126ResonatorNetwork::ResonatorNetwork(const std::vector<double>& frequencies,
127 const std::vector<double>& q_values)
128{
129 if (frequencies.size() != q_values.size()) {
130 error<std::invalid_argument>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
131 "ResonatorNetwork: frequencies and q_values vectors must have equal length");
132 }
133 build_resonators(frequencies, q_values);
134}
135
136//-----------------------------------------------------------------------------
137// Internal construction helpers
138//-----------------------------------------------------------------------------
139
140void ResonatorNetwork::build_resonators(const std::vector<double>& frequencies,
141 const std::vector<double>& qs)
142{
143 m_resonators.clear();
144 m_resonators.reserve(frequencies.size());
145
146 for (size_t i = 0; i < frequencies.size(); ++i) {
148 r.frequency = std::clamp(frequencies[i], 1.0, m_sample_rate * 0.5 - 1.0);
149 r.q = std::clamp(qs[i], 0.1, 1000.0);
150 r.gain = 1.0;
151 r.last_output = 0.0;
152 r.index = i;
153 r.filter = std::make_shared<Filters::IIR>(
154 std::vector<double> { 1.0, 0.0, 0.0 },
155 std::vector<double> { 0.0, 0.0, 0.0 });
157 m_resonators.push_back(std::move(r));
158 }
159}
160
162{
163 /*
164 * RBJ Audio EQ Cookbook — BPF (constant 0 dB peak gain):
165 *
166 * w0 = 2π f0 / Fs
167 * alpha = sin(w0) / (2 Q)
168 * b0 = alpha
169 * b1 = 0
170 * b2 = -alpha
171 * a0 = 1 + alpha
172 * a1 = -2 cos(w0)
173 * a2 = 1 - alpha
174 *
175 * Normalised (divide through by a0):
176 * b_coefs = { b0/a0, 0, b2/a0 }
177 * a_coefs = { 1, a1/a0, a2/a0 }
178 */
179 const double w0 = 2.0 * std::numbers::pi * r.frequency / m_sample_rate;
180 const double sinw0 = std::sin(w0);
181 const double cosw0 = std::cos(w0);
182 const double alpha = sinw0 / (2.0 * r.q);
183 const double a0 = 1.0 + alpha;
184
185 const std::vector<double> a = {
186 1.0,
187 (-2.0 * cosw0) / a0,
188 (1.0 - alpha) / a0,
189 };
190 const std::vector<double> b = {
191 alpha / a0,
192 0.0,
193 -alpha / a0,
194 };
195
196 r.filter->setACoefficients(a);
197 r.filter->setBCoefficients(b);
198 r.filter->reset();
199}
200
201//-----------------------------------------------------------------------------
202// NodeNetwork interface
203//-----------------------------------------------------------------------------
204
205void ResonatorNetwork::process_batch(unsigned int num_samples)
206{
207 if (m_resonators.empty()) {
208 m_last_audio_buffer.assign(num_samples, 0.0);
209 return;
210 }
211
213
214 m_last_audio_buffer.assign(num_samples, 0.0);
215 const double norm = 1.0 / static_cast<double>(m_resonators.size());
216
217 m_node_buffers.assign(m_resonators.size(), {});
218 for (auto& nb : m_node_buffers)
219 nb.reserve(num_samples);
220
221 std::vector<std::optional<std::span<const double>>> net_exc_bufs;
222 if (m_network_exciter) {
223 net_exc_bufs.reserve(m_resonators.size());
224 for (size_t ri = 0; ri < m_resonators.size(); ++ri)
225 net_exc_bufs.push_back(m_network_exciter->get_node_audio_buffer(ri));
226 }
227
228 for (size_t s = 0; s < num_samples; ++s) {
229 for (size_t ri = 0; ri < m_resonators.size(); ++ri) {
230 auto& r = m_resonators[ri];
231 double excitation = 0.0;
232
233 if (r.exciter) {
234 excitation = r.exciter->process_sample(0.0);
235 } else if (!net_exc_bufs.empty() && net_exc_bufs[ri] && s < net_exc_bufs[ri]->size()) {
236 excitation = (*net_exc_bufs[ri])[s];
237 } else if (m_exciter) {
238 excitation = m_exciter->process_sample(0.0);
239 }
240
241 const double out = r.filter->process_sample(excitation) * r.gain;
242 r.last_output = out;
243 m_node_buffers[ri].push_back(out);
244 m_last_audio_buffer[s] += out * norm;
245 }
246 }
247
249}
250
251std::optional<std::vector<double>> ResonatorNetwork::get_audio_buffer() const
252{
253 if (m_last_audio_buffer.empty()) {
254 return std::nullopt;
255 }
256 return m_last_audio_buffer;
257}
258
259std::optional<double> ResonatorNetwork::get_node_output(size_t index) const
260{
261 if (index >= m_resonators.size()) {
262 return std::nullopt;
263 }
264 return m_resonators[index].last_output;
265}
266
267std::optional<std::span<const double>> ResonatorNetwork::get_node_audio_buffer(size_t index) const
268{
269 if (index >= m_node_buffers.size() || m_node_buffers[index].empty())
270 return std::nullopt;
271 return std::span<const double>(m_node_buffers[index]);
272}
273
274//-----------------------------------------------------------------------------
275// Parameter mappings
276//-----------------------------------------------------------------------------
277
278void ResonatorNetwork::map_parameter(const std::string& param_name,
279 const std::shared_ptr<Node>& source,
280 MappingMode mode)
281{
282 unmap_parameter(param_name);
283
285 m.param_name = param_name;
286 m.mode = mode;
287 m.broadcast_source = source;
288 m_parameter_mappings.push_back(std::move(m));
289}
290
291void ResonatorNetwork::map_parameter(const std::string& param_name,
292 const std::shared_ptr<NodeNetwork>& source_network)
293{
294 unmap_parameter(param_name);
295
297 m.param_name = param_name;
299 m.network_source = source_network;
300 m_parameter_mappings.push_back(std::move(m));
301}
302
303void ResonatorNetwork::unmap_parameter(const std::string& param_name)
304{
305 std::erase_if(m_parameter_mappings,
306 [&](const auto& m) { return m.param_name == param_name; });
307}
308
310{
311 for (const auto& mapping : m_parameter_mappings) {
312 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
314 mapping.param_name,
315 mapping.broadcast_source->get_last_output());
316 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
317 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
318 }
319 }
320}
321
322void ResonatorNetwork::apply_broadcast_parameter(const std::string& param, double value)
323{
324 if (param == "frequency") {
325 set_all_frequencies(value);
326 } else if (param == "q") {
327 set_all_q(value);
328 } else if (param == "gain") {
329 for (auto& r : m_resonators) {
330 r.gain = value;
331 }
332 } else if (param == "scale") {
333 m_output_scale = std::max(0.0, value);
334 }
335}
336
338 const std::shared_ptr<NodeNetwork>& source)
339{
340 const size_t count = std::min(m_resonators.size(), source->get_node_count());
341
342 for (size_t i = 0; i < count; ++i) {
343 const auto val = source->get_node_output(i);
344 if (!val.has_value()) {
345 continue;
346 }
347 if (param == "frequency") {
348 set_frequency(i, *val);
349 } else if (param == "q") {
350 set_q(i, *val);
351 } else if (param == "gain") {
352 m_resonators[i].gain = *val;
353 }
354 }
355}
356
357//-----------------------------------------------------------------------------
358// Excitation control
359//-----------------------------------------------------------------------------
360
361void ResonatorNetwork::set_exciter(const std::shared_ptr<Node>& exciter)
362{
363 m_exciter = exciter;
364}
365
367{
368 m_exciter = nullptr;
369}
370
371void ResonatorNetwork::set_resonator_exciter(size_t index, const std::shared_ptr<Node>& exciter)
372{
373 if (index >= m_resonators.size()) {
374 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
375 "ResonatorNetwork::set_resonator_exciter: index out of range (index={}, resonator_count={})", index, m_resonators.size());
376 }
377 m_resonators[index].exciter = exciter;
378}
379
381{
382 if (index >= m_resonators.size()) {
383 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
384 "ResonatorNetwork::clear_resonator_exciter: index out of range (index={}, resonator_count={})", index, m_resonators.size());
385 }
386 m_resonators[index].exciter = nullptr;
387}
388
389void ResonatorNetwork::set_network_exciter(const std::shared_ptr<NodeNetwork>& network)
390{
391 m_network_exciter = network;
392}
393
398
399//-----------------------------------------------------------------------------
400// Per-resonator parameter control
401//-----------------------------------------------------------------------------
402
404{
405 if (index >= m_resonators.size()) {
406 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
407 "ResonatorNetwork::set_frequency: index out of range (index={}, resonator_count={})", index, m_resonators.size());
408 }
409 auto& r = m_resonators[index];
410 r.frequency = std::clamp(frequency, 1.0, m_sample_rate * 0.5 - 1.0);
412}
413
414void ResonatorNetwork::set_q(size_t index, double q)
415{
416 if (index >= m_resonators.size()) {
417 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
418 "ResonatorNetwork::set_q: index out of range (index={}, resonator_count={})", index, m_resonators.size());
419 }
420 auto& r = m_resonators[index];
421 r.q = std::clamp(q, 0.1, 1000.0);
423}
424
425void ResonatorNetwork::set_resonator_gain(size_t index, double gain)
426{
427 if (index >= m_resonators.size()) {
428 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
429 "ResonatorNetwork::set_resonator_gain: index out of range (index={}, resonator_count={})", index, m_resonators.size());
430 }
431 m_resonators[index].gain = gain;
432}
433
434//-----------------------------------------------------------------------------
435// Network-wide control
436//-----------------------------------------------------------------------------
437
439{
440 for (size_t i = 0; i < m_resonators.size(); ++i) {
442 }
443}
444
446{
447 for (size_t i = 0; i < m_resonators.size(); ++i) {
448 set_q(i, q);
449 }
450}
451
453{
454 std::vector<double> freqs, qs;
455 preset_to_vectors(preset, m_resonators.size(), freqs, qs);
456
457 for (size_t i = 0; i < m_resonators.size(); ++i) {
458 m_resonators[i].frequency = freqs[i];
459 m_resonators[i].q = qs[i];
461 }
462}
463
464//-----------------------------------------------------------------------------
465// Metadata
466//-----------------------------------------------------------------------------
467
468std::unordered_map<std::string, std::string> ResonatorNetwork::get_metadata() const
469{
470 auto meta = NodeNetwork::get_metadata();
471
472 meta["num_resonators"] = std::to_string(m_resonators.size());
473 meta["sample_rate"] = std::to_string(m_sample_rate) + " Hz";
474
475 for (const auto& r : m_resonators) {
476 const std::string prefix = "resonator_" + std::to_string(r.index) + "_";
477 meta[prefix + "freq"] = std::to_string(r.frequency) + " Hz";
478 meta[prefix + "q"] = std::to_string(r.q);
479 meta[prefix + "gain"] = std::to_string(r.gain);
480 }
481
482 return meta;
483}
484
485} // namespace MayaFlux::Nodes::Network
Eigen::Index count
size_t a
size_t b
double frequency
double q
void apply_output_scale()
Apply m_output_scale to m_last_audio_buffer.
double m_output_scale
Post-processing scalar applied to m_last_audio_buffer each batch.
virtual std::unordered_map< std::string, std::string > get_metadata() const
Get network metadata for debugging/visualization.
std::optional< std::span< const double > > get_node_audio_buffer(size_t index) const override
Get output of specific internal node as audio buffer (for ONE_TO_ONE mapping)
ResonatorNetwork(size_t num_resonators, FormantPreset preset=FormantPreset::NONE)
Construct a ResonatorNetwork with a formant preset.
void map_parameter(const std::string &param_name, const std::shared_ptr< Node > &source, MappingMode mode=MappingMode::BROADCAST) override
Map a scalar node output to a named networkparameter (BROADCAST)
std::unordered_map< std::string, std::string > get_metadata() const override
Returns network metadata for debugging and visualisation.
void clear_resonator_exciter(size_t index)
Clear per-resonator exciter, reverting to network-level exciter.
std::vector< double > m_last_audio_buffer
Mixed output from last process_batch()
void set_resonator_gain(size_t index, double gain)
Set amplitude gain of a single resonator.
void compute_biquad(ResonatorNode &r)
Compute RBJ biquad bandpass coefficients and push them into a resonator's IIR.
void set_all_q(double q)
Set Q factor of all resonators uniformly.
void set_resonator_exciter(size_t index, const std::shared_ptr< Node > &exciter)
Set a per-resonator exciter.
void unmap_parameter(const std::string &param_name) override
Remove a parameter mapping by name.
void set_frequency(size_t index, double frequency)
Set centre frequency of a single resonator and recompute its coefficients.
void apply_preset(FormantPreset preset)
Apply a FormantPreset to the current network.
static void preset_to_vectors(FormantPreset preset, size_t n, std::vector< double > &out_freqs, std::vector< double > &out_qs)
Translate a FormantPreset into parallel frequency/Q vectors.
void build_resonators(const std::vector< double > &frequencies, const std::vector< double > &qs)
Initialise all resonators from a frequency/Q pair list.
std::vector< std::vector< double > > m_node_buffers
Per-resonator sample buffers populated each process_batch()
void apply_broadcast_parameter(const std::string &param, double value)
Apply a BROADCAST value to the named parameter across all resonators.
void set_q(size_t index, double q)
Set Q factor of a single resonator and recompute its coefficients.
void set_all_frequencies(double frequency)
Set centre frequency of all resonators uniformly.
void clear_network_exciter()
Clear the network exciter.
std::vector< ParameterMapping > m_parameter_mappings
std::optional< double > get_node_output(size_t index) const override
Returns the last output sample of the resonator at index.
std::shared_ptr< Node > m_exciter
networ-level shared exciter (may be nullptr)
void process_batch(unsigned int num_samples) override
Processes num_samples through all resonators and accumulates output.
void clear_exciter()
Clear the network-level exciter.
void set_network_exciter(const std::shared_ptr< NodeNetwork > &network)
Set a NodeNetwork as a source of per-resonator excitation (ONE_TO_ONE)
FormantPreset
Common vowel and spectral formant configurations.
@ VOWEL_O
Back vowel /o/ (F1≈500, F2≈900, F3≈2500, F4≈3500, F5≈4500 Hz)
@ VOWEL_I
Close front vowel /i/ (F1≈270, F2≈2300, F3≈3000, F4≈3500, F5≈4500 Hz)
@ VOWEL_A
Open vowel /a/ (F1≈800, F2≈1200, F3≈2500, F4≈3500, F5≈4500 Hz)
@ VOWEL_E
Front vowel /e/ (F1≈400, F2≈2000, F3≈2600, F4≈3500, F5≈4500 Hz)
@ VOWEL_U
Close back vowel /u/ (F1≈300, F2≈800, F3≈2300, F4≈3500, F5≈4500 Hz)
std::optional< std::vector< double > > get_audio_buffer() const override
Returns the mixed audio buffer from the last process_batch() call.
void apply_one_to_one_parameter(const std::string &param, const std::shared_ptr< NodeNetwork > &source)
Apply ONE_TO_ONE values from a source network to the named parameter.
void update_mapped_parameters()
Apply all registered parameter mappings for the current cycle.
void set_exciter(const std::shared_ptr< Node > &exciter)
Set a shared exciter node for all resonators.
std::shared_ptr< NodeNetwork > m_network_exciter
Optional NodeNetwork exciter for ONE_TO_ONE mapping (may be nullptr)
@ NodeProcessing
Node graph processing (Nodes::NodeGraphManager)
@ Nodes
DSP Generator and Filter Nodes, graph pipeline, node management.
MappingMode
Defines how nodes map to external entities (e.g., audio channels, graphics objects)
@ ONE_TO_ONE
Node array/network → network nodes (must match count)
@ BROADCAST
One node → all network nodes.
std::shared_ptr< Filters::IIR > filter
Underlying biquad IIR.
double q
Quality factor (dimensionless; higher = narrower bandwidth)
double last_output
Most recent process_sample output.
double gain
Per-resonator output amplitude scale.
State of a single biquad bandpass resonator.