MayaFlux 0.4.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 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
209 std::this_thread::yield();
210
211 m_last_audio_buffer.assign(num_samples, 0.0);
212 m_audio_buffer_lock.clear(std::memory_order_release);
213 return;
214 }
215
217
218 thread_local std::vector<double> scratch;
219 scratch.assign(num_samples, 0.0);
220
221 const double norm = 1.0 / static_cast<double>(m_resonators.size());
222
223 m_node_buffers.assign(m_resonators.size(), {});
224 for (auto& nb : m_node_buffers)
225 nb.reserve(num_samples);
226
227 std::vector<std::optional<std::span<const double>>> net_exc_bufs;
228 if (m_network_exciter) {
229 net_exc_bufs.reserve(m_resonators.size());
230 for (size_t ri = 0; ri < m_resonators.size(); ++ri)
231 net_exc_bufs.push_back(m_network_exciter->get_node_audio_buffer(ri));
232 }
233
234 for (size_t s = 0; s < num_samples; ++s) {
235 for (size_t ri = 0; ri < m_resonators.size(); ++ri) {
236 auto& r = m_resonators[ri];
237 double excitation = 0.0;
238
239 if (r.exciter) {
240 excitation = r.exciter->process_sample(0.0);
241 } else if (!net_exc_bufs.empty() && net_exc_bufs[ri] && s < net_exc_bufs[ri]->size()) {
242 excitation = (*net_exc_bufs[ri])[s];
243 } else if (m_exciter) {
244 excitation = m_exciter->process_sample(0.0);
245 }
246
247 const double out = r.filter->process_sample(excitation) * r.gain;
248 r.last_output = out;
249 m_node_buffers[ri].push_back(out);
250 scratch[s] += out * norm;
251 }
252 }
253
254 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
255 std::this_thread::yield();
256
257 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
259 m_audio_buffer_lock.clear(std::memory_order_release);
260}
261
262std::optional<std::vector<double>> ResonatorNetwork::get_audio_buffer() const
263{
264 if (m_last_audio_buffer.empty()) {
265 return std::nullopt;
266 }
267 return m_last_audio_buffer;
268}
269
270std::optional<double> ResonatorNetwork::get_node_output(size_t index) const
271{
272 if (index >= m_resonators.size()) {
273 return std::nullopt;
274 }
275 return m_resonators[index].last_output;
276}
277
278std::optional<std::span<const double>> ResonatorNetwork::get_node_audio_buffer(size_t index) const
279{
280 if (index >= m_node_buffers.size() || m_node_buffers[index].empty())
281 return std::nullopt;
282 return std::span<const double>(m_node_buffers[index]);
283}
284
285//-----------------------------------------------------------------------------
286// Parameter mappings
287//-----------------------------------------------------------------------------
288
289void ResonatorNetwork::map_parameter(const std::string& param_name,
290 const std::shared_ptr<Node>& source,
291 MappingMode mode)
292{
293 unmap_parameter(param_name);
294
296 m.param_name = param_name;
297 m.mode = mode;
298 m.broadcast_source = source;
299 m_parameter_mappings.push_back(std::move(m));
300}
301
302void ResonatorNetwork::map_parameter(const std::string& param_name,
303 const std::shared_ptr<NodeNetwork>& source_network)
304{
305 unmap_parameter(param_name);
306
308 m.param_name = param_name;
310 m.network_source = source_network;
311 m_parameter_mappings.push_back(std::move(m));
312}
313
314void ResonatorNetwork::unmap_parameter(const std::string& param_name)
315{
316 std::erase_if(m_parameter_mappings,
317 [&](const auto& m) { return m.param_name == param_name; });
318}
319
321{
322 for (const auto& mapping : m_parameter_mappings) {
323 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
325 mapping.param_name,
326 mapping.broadcast_source->get_last_output());
327 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
328 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
329 }
330 }
331}
332
333void ResonatorNetwork::apply_broadcast_parameter(const std::string& param, double value)
334{
335 if (param == "frequency") {
336 set_all_frequencies(value);
337 } else if (param == "q") {
338 set_all_q(value);
339 } else if (param == "gain") {
340 for (auto& r : m_resonators) {
341 r.gain = value;
342 }
343 } else if (param == "scale") {
344 m_output_scale = std::max(0.0, value);
345 }
346}
347
349 const std::shared_ptr<NodeNetwork>& source)
350{
351 const size_t count = std::min(m_resonators.size(), source->get_node_count());
352
353 for (size_t i = 0; i < count; ++i) {
354 const auto val = source->get_node_output(i);
355 if (!val.has_value()) {
356 continue;
357 }
358 if (param == "frequency") {
359 set_frequency(i, *val);
360 } else if (param == "q") {
361 set_q(i, *val);
362 } else if (param == "gain") {
363 m_resonators[i].gain = *val;
364 }
365 }
366}
367
368//-----------------------------------------------------------------------------
369// Excitation control
370//-----------------------------------------------------------------------------
371
372void ResonatorNetwork::set_exciter(const std::shared_ptr<Node>& exciter)
373{
374 m_exciter = exciter;
375}
376
378{
379 m_exciter = nullptr;
380}
381
382void ResonatorNetwork::set_resonator_exciter(size_t index, const std::shared_ptr<Node>& exciter)
383{
384 if (index >= m_resonators.size()) {
385 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
386 "ResonatorNetwork::set_resonator_exciter: index out of range (index={}, resonator_count={})", index, m_resonators.size());
387 }
388 m_resonators[index].exciter = exciter;
389}
390
392{
393 if (index >= m_resonators.size()) {
394 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
395 "ResonatorNetwork::clear_resonator_exciter: index out of range (index={}, resonator_count={})", index, m_resonators.size());
396 }
397 m_resonators[index].exciter = nullptr;
398}
399
400void ResonatorNetwork::set_network_exciter(const std::shared_ptr<NodeNetwork>& network)
401{
402 m_network_exciter = network;
403}
404
409
410//-----------------------------------------------------------------------------
411// Per-resonator parameter control
412//-----------------------------------------------------------------------------
413
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_frequency: index out of range (index={}, resonator_count={})", index, m_resonators.size());
419 }
420 auto& r = m_resonators[index];
421 r.frequency = std::clamp(frequency, 1.0, m_sample_rate * 0.5 - 1.0);
423}
424
425void ResonatorNetwork::set_q(size_t index, double q)
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_q: index out of range (index={}, resonator_count={})", index, m_resonators.size());
430 }
431 auto& r = m_resonators[index];
432 r.q = std::clamp(q, 0.1, 1000.0);
434}
435
436void ResonatorNetwork::set_resonator_gain(size_t index, double gain)
437{
438 if (index >= m_resonators.size()) {
439 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
440 "ResonatorNetwork::set_resonator_gain: index out of range (index={}, resonator_count={})", index, m_resonators.size());
441 }
442 m_resonators[index].gain = gain;
443}
444
445//-----------------------------------------------------------------------------
446// Network-wide control
447//-----------------------------------------------------------------------------
448
450{
451 for (size_t i = 0; i < m_resonators.size(); ++i) {
453 }
454}
455
457{
458 for (size_t i = 0; i < m_resonators.size(); ++i) {
459 set_q(i, q);
460 }
461}
462
464{
465 std::vector<double> freqs, qs;
466 preset_to_vectors(preset, m_resonators.size(), freqs, qs);
467
468 for (size_t i = 0; i < m_resonators.size(); ++i) {
469 m_resonators[i].frequency = freqs[i];
470 m_resonators[i].q = qs[i];
472 }
473}
474
475//-----------------------------------------------------------------------------
476// Metadata
477//-----------------------------------------------------------------------------
478
479std::unordered_map<std::string, std::string> ResonatorNetwork::get_metadata() const
480{
481 auto meta = NodeNetwork::get_metadata();
482
483 meta["num_resonators"] = std::to_string(m_resonators.size());
484 meta["sample_rate"] = std::to_string(m_sample_rate) + " Hz";
485
486 for (const auto& r : m_resonators) {
487 const std::string prefix = "resonator_" + std::to_string(r.index) + "_";
488 meta[prefix + "freq"] = std::to_string(r.frequency) + " Hz";
489 meta[prefix + "q"] = std::to_string(r.q);
490 meta[prefix + "gain"] = std::to_string(r.gain);
491 }
492
493 return meta;
494}
495
496} // 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.
std::atomic_flag m_audio_buffer_lock
Spinlock guarding m_last_audio_buffer.
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.