MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ResonatorNetwork.cpp
Go to the documentation of this file.
2
5
7
9
10//-----------------------------------------------------------------------------
11// Preset tables
12//-----------------------------------------------------------------------------
13
14namespace {
15
16 struct FormantEntry {
17 double frequency;
18 double q;
19 };
20
21 /*
22 * Source: Peterson & Barney (1952) / Hillenbrand et al. (1995) averaged values.
23 * Five formants; lower formants have broader absolute bandwidths modelled by
24 * lower Q. Q approximated as F / BW with BW ≈ 50–80 Hz for F1, scaling upward.
25 */
26 const FormantEntry k_vowel_a[] = {
27 { .frequency = 800.0, .q = 16.0 },
28 { .frequency = 1200.0, .q = 30.0 },
29 { .frequency = 2500.0, .q = 55.0 },
30 { .frequency = 3500.0, .q = 70.0 },
31 { .frequency = 4500.0, .q = 90.0 },
32 };
33
34 const FormantEntry k_vowel_e[] = {
35 { .frequency = 400.0, .q = 10.0 },
36 { .frequency = 2000.0, .q = 45.0 },
37 { .frequency = 2600.0, .q = 55.0 },
38 { .frequency = 3500.0, .q = 70.0 },
39 { .frequency = 4500.0, .q = 90.0 },
40 };
41
42 const FormantEntry k_vowel_i[] = {
43 { .frequency = 270.0, .q = 7.0 },
44 { .frequency = 2300.0, .q = 50.0 },
45 { .frequency = 3000.0, .q = 60.0 },
46 { .frequency = 3500.0, .q = 70.0 },
47 { .frequency = 4500.0, .q = 90.0 },
48 };
49
50 const FormantEntry k_vowel_o[] = {
51 { .frequency = 500.0, .q = 12.0 },
52 { .frequency = 900.0, .q = 22.0 },
53 { .frequency = 2500.0, .q = 55.0 },
54 { .frequency = 3500.0, .q = 70.0 },
55 { .frequency = 4500.0, .q = 90.0 },
56 };
57
58 const FormantEntry k_vowel_u[] = {
59 { .frequency = 300.0, .q = 8.0 },
60 { .frequency = 800.0, .q = 20.0 },
61 { .frequency = 2300.0, .q = 50.0 },
62 { .frequency = 3500.0, .q = 70.0 },
63 { .frequency = 4500.0, .q = 90.0 },
64 };
65
66 constexpr size_t k_preset_formant_count = 5;
67
68} // namespace
69
70//-----------------------------------------------------------------------------
71// Static helper
72//-----------------------------------------------------------------------------
73
75 size_t n,
76 std::vector<double>& out_freqs,
77 std::vector<double>& out_qs)
78{
79 const FormantEntry* table = nullptr;
80
81 switch (preset) {
83 table = k_vowel_a;
84 break;
86 table = k_vowel_e;
87 break;
89 table = k_vowel_i;
90 break;
92 table = k_vowel_o;
93 break;
95 table = k_vowel_u;
96 break;
97 default:
98 break;
99 }
100
101 out_freqs.resize(n, 440.0);
102 out_qs.resize(n, 10.0);
103
104 if (!table) {
105 return;
106 }
107
108 const size_t defined = std::min(n, k_preset_formant_count);
109 for (size_t i = 0; i < defined; ++i) {
110 out_freqs[i] = table[i].frequency;
111 out_qs[i] = table[i].q;
112 }
113}
114
115//-----------------------------------------------------------------------------
116// Construction
117//-----------------------------------------------------------------------------
118
120 FormantPreset preset)
121{
123 std::vector<double> freqs, qs;
124 preset_to_vectors(preset, num_resonators, freqs, qs);
125 build_resonators(freqs, qs);
126}
127
128ResonatorNetwork::ResonatorNetwork(const std::vector<double>& frequencies,
129 const std::vector<double>& q_values)
130{
132 if (frequencies.size() != q_values.size()) {
133 error<std::invalid_argument>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
134 "ResonatorNetwork: frequencies and q_values vectors must have equal length");
135 }
136 build_resonators(frequencies, q_values);
137}
138
139//-----------------------------------------------------------------------------
140// Internal construction helpers
141//-----------------------------------------------------------------------------
142
143void ResonatorNetwork::build_resonators(const std::vector<double>& frequencies,
144 const std::vector<double>& qs)
145{
146 m_resonators.clear();
147 m_resonators.reserve(frequencies.size());
148
149 for (size_t i = 0; i < frequencies.size(); ++i) {
151 r.frequency = std::clamp(frequencies[i], 1.0, m_sample_rate * 0.5 - 1.0);
152 r.q = std::clamp(qs[i], 0.1, 1000.0);
153 r.gain = 1.0;
154 r.last_output = 0.0;
155 r.index = i;
156 r.filter = std::make_shared<Filters::IIR>(
157 std::vector<double> { 1.0, 0.0, 0.0 },
158 std::vector<double> { 0.0, 0.0, 0.0 });
160 m_resonators.push_back(std::move(r));
161 }
162
163 m_norm_factor.store(1.0 / std::sqrt(static_cast<double>(m_resonators.size())), std::memory_order_release);
164}
165
167{
169
170 r.filter->setACoefficients({ 1.0, biquad.a1, biquad.a2 });
171 r.filter->setBCoefficients({ biquad.b0, biquad.b1, biquad.b2 });
172 r.filter->reset();
173}
174
175//-----------------------------------------------------------------------------
176// NodeNetwork interface
177//-----------------------------------------------------------------------------
178
179void ResonatorNetwork::process_batch(unsigned int num_samples)
180{
181 if (m_resonators.empty()) {
182 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
183 std::this_thread::yield();
184
185 m_last_audio_buffer.assign(num_samples, 0.0);
186 m_audio_buffer_lock.clear(std::memory_order_release);
187 return;
188 }
189
191
192 thread_local std::vector<double> scratch;
193 scratch.assign(num_samples, 0.0);
194
195 const double norm = m_norm_factor.load(std::memory_order_acquire);
196
197 m_node_buffers.resize(m_resonators.size());
198 for (auto& nb : m_node_buffers) {
199 nb.clear();
200 nb.reserve(num_samples);
201 }
202
203 thread_local std::vector<std::optional<std::span<const double>>> net_exc_bufs;
204 net_exc_bufs.clear();
205 if (m_network_exciter) {
206 net_exc_bufs.reserve(m_resonators.size());
207 for (size_t ri = 0; ri < m_resonators.size(); ++ri)
208 net_exc_bufs.push_back(m_network_exciter->get_node_audio_buffer(ri));
209 }
210
211 for (size_t s = 0; s < num_samples; ++s) {
212 for (size_t ri = 0; ri < m_resonators.size(); ++ri) {
213 auto& r = m_resonators[ri];
214 double excitation = 0.0;
215
216 if (r.exciter) {
217 excitation = r.exciter->process_sample(0.0);
218 } else if (!net_exc_bufs.empty() && net_exc_bufs[ri] && s < net_exc_bufs[ri]->size()) {
219 excitation = (*net_exc_bufs[ri])[s];
220 } else if (m_exciter) {
221 excitation = m_exciter->process_sample(0.0);
222 }
223
224 const double out = r.filter->process_sample(excitation) * r.gain;
225 r.last_output = out;
226 m_node_buffers[ri].push_back(out);
227 scratch[s] += out * norm;
228 }
229 }
230
231 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
232 std::this_thread::yield();
233
234 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
236 m_audio_buffer_lock.clear(std::memory_order_release);
237}
238
239std::optional<std::vector<double>> ResonatorNetwork::get_audio_buffer() const
240{
241 if (m_last_audio_buffer.empty()) {
242 return std::nullopt;
243 }
244 return m_last_audio_buffer;
245}
246
247std::optional<double> ResonatorNetwork::get_node_output(size_t index) const
248{
249 if (index >= m_resonators.size()) {
250 return std::nullopt;
251 }
252 return m_resonators[index].last_output;
253}
254
255std::optional<std::span<const double>> ResonatorNetwork::get_node_audio_buffer(size_t index) const
256{
257 if (index >= m_node_buffers.size() || m_node_buffers[index].empty())
258 return std::nullopt;
259 return std::span<const double>(m_node_buffers[index]);
260}
261
262//-----------------------------------------------------------------------------
263// Parameter mappings
264//-----------------------------------------------------------------------------
265
266void ResonatorNetwork::map_parameter(const std::string& param_name,
267 const std::shared_ptr<Node>& source,
269{
270 unmap_parameter(param_name);
271
273 m.param_name = param_name;
274 m.mode = mode;
275 m.broadcast_source = source;
276 m_parameter_mappings.push_back(std::move(m));
277}
278
279void ResonatorNetwork::map_parameter(const std::string& param_name,
280 const std::shared_ptr<NodeNetwork>& source_network)
281{
282 unmap_parameter(param_name);
283
285 m.param_name = param_name;
287 m.network_source = source_network;
288 m_parameter_mappings.push_back(std::move(m));
289}
290
291void ResonatorNetwork::unmap_parameter(const std::string& param_name)
292{
293 std::erase_if(m_parameter_mappings,
294 [&](const auto& m) { return m.param_name == param_name; });
295}
296
298{
299 for (const auto& mapping : m_parameter_mappings) {
300 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
302 mapping.param_name,
303 mapping.broadcast_source->get_last_output());
304 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
305 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
306 }
307 }
308}
309
310void ResonatorNetwork::apply_broadcast_parameter(const std::string& param, double value)
311{
312 if (param == "frequency") {
314 } else if (param == "q") {
316 } else if (param == "gain") {
317 for (auto& r : m_resonators) {
318 r.gain = value;
319 }
320 } else if (param == "scale") {
321 m_output_scale = std::max(0.0, value);
322 } else if (param == "retune") {
323 for (size_t i = 0; i < m_resonators.size(); ++i) {
324 retune(i, value);
325 }
326 } else if (param == "decay") {
327 for (size_t i = 0; i < m_resonators.size(); ++i) {
328 set_decay(i, value);
329 }
330 }
331}
332
334 const std::shared_ptr<NodeNetwork>& source)
335{
336 const size_t count = std::min(m_resonators.size(), source->get_node_count());
337
338 for (size_t i = 0; i < count; ++i) {
339 const auto val = source->get_node_output(i);
340 if (!val.has_value()) {
341 continue;
342 }
343 if (param == "frequency") {
344 set_frequency(i, *val);
345 } else if (param == "q") {
346 set_q(i, *val);
347 } else if (param == "gain") {
348 m_resonators[i].gain = *val;
349 } else if (param == "retune") {
350 retune(i, *val);
351 } else if (param == "decay") {
352 set_decay(i, *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{
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
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
435{
436 if (index >= m_resonators.size()) {
437 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
438 "ResonatorNetwork::retune: index out of range (index={}, resonator_count={})", index, m_resonators.size());
439 }
440 auto& r = m_resonators[index];
441 r.frequency = std::clamp(frequency, 1.0, m_sample_rate * 0.5 - 1.0);
443}
444
445void ResonatorNetwork::set_decay(size_t index, double seconds)
446{
447 if (index >= m_resonators.size()) {
448 error<std::out_of_range>(Journal::Component::Nodes, Journal::Context::NodeProcessing, std::source_location::current(),
449 "ResonatorNetwork::set_decay: index out of range (index={}, resonator_count={})", index, m_resonators.size());
450 }
451 auto& r = m_resonators[index];
452
453 r.q = Kinesis::Discrete::q_from_decay(r.frequency, seconds, m_sample_rate);
455}
456
457void ResonatorNetwork::retune(std::span<const double> frequencies)
458{
459 const size_t count = std::min(frequencies.size(), m_resonators.size());
460 for (size_t i = 0; i < count; ++i) {
461 retune(i, frequencies[i]);
462 }
463}
464
465void ResonatorNetwork::set_decay(std::span<const double> seconds)
466{
467 const size_t count = std::min(seconds.size(), m_resonators.size());
468 for (size_t i = 0; i < count; ++i) {
469 set_decay(i, seconds[i]);
470 }
471}
472
473void ResonatorNetwork::set_resonator_gain(std::span<const double> gains)
474{
475 const size_t count = std::min(gains.size(), m_resonators.size());
476 for (size_t i = 0; i < count; ++i) {
477 m_resonators[i].gain = gains[i];
478 }
479}
480
482{
484
485 auto a = r.filter->edit_feedback_coefs();
486 auto b = r.filter->edit_feedforward_coefs();
487
488 a[0] = biquad.a1;
489 a[1] = biquad.a2;
490 b[0] = biquad.b0;
491 b[1] = biquad.b1;
492 b[2] = biquad.b2;
493}
494
495//-----------------------------------------------------------------------------
496// Network-wide control
497//-----------------------------------------------------------------------------
498
500{
501 for (size_t i = 0; i < m_resonators.size(); ++i) {
503 }
504}
505
507{
508 for (size_t i = 0; i < m_resonators.size(); ++i) {
509 set_q(i, q);
510 }
511}
512
514{
515 std::vector<double> freqs, qs;
516 preset_to_vectors(preset, m_resonators.size(), freqs, qs);
517
518 for (size_t i = 0; i < m_resonators.size(); ++i) {
519 m_resonators[i].frequency = freqs[i];
520 m_resonators[i].q = qs[i];
522 }
523}
524
525//-----------------------------------------------------------------------------
526// Metadata
527//-----------------------------------------------------------------------------
528
529std::unordered_map<std::string, std::string> ResonatorNetwork::get_metadata() const
530{
531 auto meta = NodeNetwork::get_metadata();
532
533 meta["num_resonators"] = std::to_string(m_resonators.size());
534 meta["sample_rate"] = std::to_string(m_sample_rate) + " Hz";
535
536 for (const auto& r : m_resonators) {
537 const std::string prefix = "resonator_" + std::to_string(r.index) + "_";
538 meta[prefix + "freq"] = std::to_string(r.frequency) + " Hz";
539 meta[prefix + "q"] = std::to_string(r.q);
540 meta[prefix + "gain"] = std::to_string(r.gain);
541 }
542
543 return meta;
544}
545
546} // namespace MayaFlux::Nodes::Network
Digital filter coefficient design for MayaFlux::Kinesis.
Core::GlobalNetworkConfig network
Definition Config.cpp:43
uint32_t mode
size_t a
size_t b
double frequency
double q
std::vector< float > * out
glm::vec3 value
uint32_t index
Definition VKDevice.cpp:122
size_t count
float gain
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 write_biquad_in_place(ResonatorNode &r)
Write the same biquad as compute_biquad through the filter's edit spans.
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
void set_decay(size_t index, double seconds)
Set how long a resonator rings, without clearing its state.
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)
void retune(size_t index, double frequency)
Move a resonator's centre frequency without clearing its state.
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.
void biquad_bandpass(double frequency, double q, double sample_rate, std::vector< double > &a, std::vector< double > &b)
Second-order bandpass coefficients, constant 0 dB peak gain.
double q_from_decay(double frequency, double seconds, double sample_rate) noexcept
Q that gives the bandpass section a stated ring time.
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.