MayaFlux 0.5.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{
122 std::vector<double> freqs, qs;
123 preset_to_vectors(preset, num_resonators, freqs, qs);
124 build_resonators(freqs, qs);
125}
126
127ResonatorNetwork::ResonatorNetwork(const std::vector<double>& frequencies,
128 const std::vector<double>& q_values)
129{
131 if (frequencies.size() != q_values.size()) {
132 error<std::invalid_argument>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
133 "ResonatorNetwork: frequencies and q_values vectors must have equal length");
134 }
135 build_resonators(frequencies, q_values);
136}
137
138//-----------------------------------------------------------------------------
139// Internal construction helpers
140//-----------------------------------------------------------------------------
141
142void ResonatorNetwork::build_resonators(const std::vector<double>& frequencies,
143 const std::vector<double>& qs)
144{
145 m_resonators.clear();
146 m_resonators.reserve(frequencies.size());
147
148 for (size_t i = 0; i < frequencies.size(); ++i) {
150 r.frequency = std::clamp(frequencies[i], 1.0, m_sample_rate * 0.5 - 1.0);
151 r.q = std::clamp(qs[i], 0.1, 1000.0);
152 r.gain = 1.0;
153 r.last_output = 0.0;
154 r.index = i;
155 r.filter = std::make_shared<Filters::IIR>(
156 std::vector<double> { 1.0, 0.0, 0.0 },
157 std::vector<double> { 0.0, 0.0, 0.0 });
159 m_resonators.push_back(std::move(r));
160 }
161
162 m_norm_factor.store(1.0 / std::sqrt(static_cast<double>(m_resonators.size())), std::memory_order_release);
163}
164
166{
167 /*
168 * RBJ Audio EQ Cookbook — BPF (constant 0 dB peak gain):
169 *
170 * w0 = 2π f0 / Fs
171 * alpha = sin(w0) / (2 Q)
172 * b0 = alpha
173 * b1 = 0
174 * b2 = -alpha
175 * a0 = 1 + alpha
176 * a1 = -2 cos(w0)
177 * a2 = 1 - alpha
178 *
179 * Normalised (divide through by a0):
180 * b_coefs = { b0/a0, 0, b2/a0 }
181 * a_coefs = { 1, a1/a0, a2/a0 }
182 */
183 const double w0 = 2.0 * std::numbers::pi * r.frequency / m_sample_rate;
184 const double sinw0 = std::sin(w0);
185 const double cosw0 = std::cos(w0);
186 const double alpha = sinw0 / (2.0 * r.q);
187 const double a0 = 1.0 + alpha;
188
189 const std::vector<double> a = {
190 1.0,
191 (-2.0 * cosw0) / a0,
192 (1.0 - alpha) / a0,
193 };
194 const std::vector<double> b = {
195 alpha / a0,
196 0.0,
197 -alpha / a0,
198 };
199
200 r.filter->setACoefficients(a);
201 r.filter->setBCoefficients(b);
202 r.filter->reset();
203}
204
205//-----------------------------------------------------------------------------
206// NodeNetwork interface
207//-----------------------------------------------------------------------------
208
209void ResonatorNetwork::process_batch(unsigned int num_samples)
210{
211 if (m_resonators.empty()) {
212 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
213 std::this_thread::yield();
214
215 m_last_audio_buffer.assign(num_samples, 0.0);
216 m_audio_buffer_lock.clear(std::memory_order_release);
217 return;
218 }
219
221
222 thread_local std::vector<double> scratch;
223 scratch.assign(num_samples, 0.0);
224
225 const double norm = m_norm_factor.load(std::memory_order_acquire);
226
227 m_node_buffers.assign(m_resonators.size(), {});
228 for (auto& nb : m_node_buffers)
229 nb.reserve(num_samples);
230
231 std::vector<std::optional<std::span<const double>>> net_exc_bufs;
232 if (m_network_exciter) {
233 net_exc_bufs.reserve(m_resonators.size());
234 for (size_t ri = 0; ri < m_resonators.size(); ++ri)
235 net_exc_bufs.push_back(m_network_exciter->get_node_audio_buffer(ri));
236 }
237
238 for (size_t s = 0; s < num_samples; ++s) {
239 for (size_t ri = 0; ri < m_resonators.size(); ++ri) {
240 auto& r = m_resonators[ri];
241 double excitation = 0.0;
242
243 if (r.exciter) {
244 excitation = r.exciter->process_sample(0.0);
245 } else if (!net_exc_bufs.empty() && net_exc_bufs[ri] && s < net_exc_bufs[ri]->size()) {
246 excitation = (*net_exc_bufs[ri])[s];
247 } else if (m_exciter) {
248 excitation = m_exciter->process_sample(0.0);
249 }
250
251 const double out = r.filter->process_sample(excitation) * r.gain;
252 r.last_output = out;
253 m_node_buffers[ri].push_back(out);
254 scratch[s] += out * norm;
255 }
256 }
257
258 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
259 std::this_thread::yield();
260
261 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
263 m_audio_buffer_lock.clear(std::memory_order_release);
264}
265
266std::optional<std::vector<double>> ResonatorNetwork::get_audio_buffer() const
267{
268 if (m_last_audio_buffer.empty()) {
269 return std::nullopt;
270 }
271 return m_last_audio_buffer;
272}
273
274std::optional<double> ResonatorNetwork::get_node_output(size_t index) const
275{
276 if (index >= m_resonators.size()) {
277 return std::nullopt;
278 }
279 return m_resonators[index].last_output;
280}
281
282std::optional<std::span<const double>> ResonatorNetwork::get_node_audio_buffer(size_t index) const
283{
284 if (index >= m_node_buffers.size() || m_node_buffers[index].empty())
285 return std::nullopt;
286 return std::span<const double>(m_node_buffers[index]);
287}
288
289//-----------------------------------------------------------------------------
290// Parameter mappings
291//-----------------------------------------------------------------------------
292
293void ResonatorNetwork::map_parameter(const std::string& param_name,
294 const std::shared_ptr<Node>& source,
295 MappingMode mode)
296{
297 unmap_parameter(param_name);
298
300 m.param_name = param_name;
301 m.mode = mode;
302 m.broadcast_source = source;
303 m_parameter_mappings.push_back(std::move(m));
304}
305
306void ResonatorNetwork::map_parameter(const std::string& param_name,
307 const std::shared_ptr<NodeNetwork>& source_network)
308{
309 unmap_parameter(param_name);
310
312 m.param_name = param_name;
314 m.network_source = source_network;
315 m_parameter_mappings.push_back(std::move(m));
316}
317
318void ResonatorNetwork::unmap_parameter(const std::string& param_name)
319{
320 std::erase_if(m_parameter_mappings,
321 [&](const auto& m) { return m.param_name == param_name; });
322}
323
325{
326 for (const auto& mapping : m_parameter_mappings) {
327 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
329 mapping.param_name,
330 mapping.broadcast_source->get_last_output());
331 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
332 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
333 }
334 }
335}
336
337void ResonatorNetwork::apply_broadcast_parameter(const std::string& param, double value)
338{
339 if (param == "frequency") {
341 } else if (param == "q") {
343 } else if (param == "gain") {
344 for (auto& r : m_resonators) {
345 r.gain = value;
346 }
347 } else if (param == "scale") {
348 m_output_scale = std::max(0.0, value);
349 }
350}
351
353 const std::shared_ptr<NodeNetwork>& source)
354{
355 const size_t count = std::min(m_resonators.size(), source->get_node_count());
356
357 for (size_t i = 0; i < count; ++i) {
358 const auto val = source->get_node_output(i);
359 if (!val.has_value()) {
360 continue;
361 }
362 if (param == "frequency") {
363 set_frequency(i, *val);
364 } else if (param == "q") {
365 set_q(i, *val);
366 } else if (param == "gain") {
367 m_resonators[i].gain = *val;
368 }
369 }
370}
371
372//-----------------------------------------------------------------------------
373// Excitation control
374//-----------------------------------------------------------------------------
375
376void ResonatorNetwork::set_exciter(const std::shared_ptr<Node>& exciter)
377{
378 m_exciter = exciter;
379}
380
382{
383 m_exciter = nullptr;
384}
385
386void ResonatorNetwork::set_resonator_exciter(size_t index, const std::shared_ptr<Node>& exciter)
387{
388 if (index >= m_resonators.size()) {
389 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
390 "ResonatorNetwork::set_resonator_exciter: index out of range (index={}, resonator_count={})", index, m_resonators.size());
391 }
392 m_resonators[index].exciter = exciter;
393}
394
396{
397 if (index >= m_resonators.size()) {
398 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
399 "ResonatorNetwork::clear_resonator_exciter: index out of range (index={}, resonator_count={})", index, m_resonators.size());
400 }
401 m_resonators[index].exciter = nullptr;
402}
403
404void ResonatorNetwork::set_network_exciter(const std::shared_ptr<NodeNetwork>& network)
405{
407}
408
413
414//-----------------------------------------------------------------------------
415// Per-resonator parameter control
416//-----------------------------------------------------------------------------
417
419{
420 if (index >= m_resonators.size()) {
421 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
422 "ResonatorNetwork::set_frequency: index out of range (index={}, resonator_count={})", index, m_resonators.size());
423 }
424 auto& r = m_resonators[index];
425 r.frequency = std::clamp(frequency, 1.0, m_sample_rate * 0.5 - 1.0);
427}
428
429void ResonatorNetwork::set_q(size_t index, double q)
430{
431 if (index >= m_resonators.size()) {
432 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
433 "ResonatorNetwork::set_q: index out of range (index={}, resonator_count={})", index, m_resonators.size());
434 }
435 auto& r = m_resonators[index];
436 r.q = std::clamp(q, 0.1, 1000.0);
438}
439
440void ResonatorNetwork::set_resonator_gain(size_t index, double gain)
441{
442 if (index >= m_resonators.size()) {
443 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
444 "ResonatorNetwork::set_resonator_gain: index out of range (index={}, resonator_count={})", index, m_resonators.size());
445 }
446 m_resonators[index].gain = gain;
447}
448
449//-----------------------------------------------------------------------------
450// Network-wide control
451//-----------------------------------------------------------------------------
452
454{
455 for (size_t i = 0; i < m_resonators.size(); ++i) {
457 }
458}
459
461{
462 for (size_t i = 0; i < m_resonators.size(); ++i) {
463 set_q(i, q);
464 }
465}
466
468{
469 std::vector<double> freqs, qs;
470 preset_to_vectors(preset, m_resonators.size(), freqs, qs);
471
472 for (size_t i = 0; i < m_resonators.size(); ++i) {
473 m_resonators[i].frequency = freqs[i];
474 m_resonators[i].q = qs[i];
476 }
477}
478
479//-----------------------------------------------------------------------------
480// Metadata
481//-----------------------------------------------------------------------------
482
483std::unordered_map<std::string, std::string> ResonatorNetwork::get_metadata() const
484{
485 auto meta = NodeNetwork::get_metadata();
486
487 meta["num_resonators"] = std::to_string(m_resonators.size());
488 meta["sample_rate"] = std::to_string(m_sample_rate) + " Hz";
489
490 for (const auto& r : m_resonators) {
491 const std::string prefix = "resonator_" + std::to_string(r.index) + "_";
492 meta[prefix + "freq"] = std::to_string(r.frequency) + " Hz";
493 meta[prefix + "q"] = std::to_string(r.q);
494 meta[prefix + "gain"] = std::to_string(r.gain);
495 }
496
497 return meta;
498}
499
500} // namespace MayaFlux::Nodes::Network
Core::GlobalNetworkConfig network
Definition Config.cpp:39
size_t a
size_t b
double frequency
double q
size_t count
float value
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::vector< double > m_last_audio_buffer
void set_output_mode(OutputMode mode)
Set the network's output routing mode.
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.
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.
std::atomic< double > m_norm_factor
Normalisation factor for summed output, rms scaled by number of active resonators.
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.
@ AUDIO_SINK
Aggregated audio samples sent to output.
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.