MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ResonatorNetwork.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "NodeNetwork.hpp"
4
6
8
9/**
10 * @class ResonatorNetwork
11 * @brief Network of IIR biquad bandpass filters driven by external excitation
12 *
13 * CONCEPT:
14 * ========
15 * ResonatorNetwork implements N second-order IIR bandpass sections (biquads), each
16 * tuned to an independent centre frequency and Q factor. Unlike ModalNetwork,
17 * which synthesises resonance through decaying sinusoidal oscillators in the
18 * frequency domain, ResonatorNetwork operates purely in the time domain: it shapes
19 * the spectrum of whatever signal is injected into it. Feed white noise and the
20 * Network becomes a formant synthesiser; feed a pitched glottal pulse and it voices
21 * a vowel; feed an arbitrary signal and it performs spectral morphing toward the
22 * target formant profile.
23 *
24 * Each resonator computes the RBJ Audio EQ cookbook bilinear-transform biquad
25 * bandpass (constant 0 dB peak gain form):
26 *
27 * H(z) = (1 - z^{-2}) * (b0/a0) / (1 + (a1/a0)*z^{-1} + (a2/a0)*z^{-2})
28 *
29 * Coefficients derived at construction and on every parameter change:
30 *
31 * w0 = 2π f0 / Fs
32 * alpha = sin(w0) / (2 Q)
33 * b0 = sin(w0) / 2 = alpha
34 * b1 = 0
35 * b2 = -sin(w0) / 2 = -alpha
36 * a0 = 1 + alpha
37 * a1 = -2 cos(w0)
38 * a2 = 1 - alpha
39 *
40 * EXCITATION:
41 * ===========
42 * A single shared exciter node can drive all resonators simultaneously (the
43 * classic formant synthesis topology). Alternatively, per-resonator exciter
44 * nodes allow independent spectral injection. If no exciter is set, the network
45 * accepts external samples passed directly to process_sample() / process_batch().
46 *
47 * PARAMETER MAPPING:
48 * ==================
49 * Supports BROADCAST and ONE_TO_ONE modes via the NodeNetwork interface:
50 * - "frequency" — centre frequency of all resonators (BROADCAST) or per-resonator (ONE_TO_ONE)
51 * - "q" — bandwidth/resonance of all resonators (BROADCAST) or per-resonator (ONE_TO_ONE)
52 * - "gain" — amplitude scale per resonator (BROADCAST) or per-resonator (ONE_TO_ONE)
53 * - "retune" — centre frequency like "frequency", written in place so the ring is kept
54 * - "decay" — ring time in seconds, written in place so the ring is kept
55 *
56 * OUTPUT:
57 * =======
58 * Each process_batch() call sums all resonator outputs into a single mixed audio
59 * buffer, RMS-normalised (divided by sqrt(resonator_count)) to keep perceived
60 * loudness roughly stable as resonator count changes. This is a statistical
61 * approximation assuming resonators are not strongly phase-correlated; a shared
62 * exciter driving closely-tuned resonators can peak more coherently than RMS
63 * assumes. FinalLimiterProcessor at the root audio buffer remains the actual
64 * safety backstop. See set_output_scale() for compensating toward raw resonator
65 * count via get_node_count(). Alternatively, get_node_output(index) exposes the
66 * most recent individual resonator output for cross-domain routing.
67 *
68 * USAGE:
69 * ======
70 * @code
71 * // Five-formant vowel synthesiser fed by a noise source
72 * auto network= std::make_shared<ResonatorNetwork>(5,
73 * ResonatorNetwork::FormantPreset::VOWEL_A, 48000.0);
74 *
75 * auto noise = vega.Random(GAUSSIAN);
76 * network->set_exciter(noise);
77 *
78 * auto rb = vega.ResonatorNetwork(5, ResonatorNetwork::FormantPreset::VOWEL_A)[0] | Audio;
79 * rb->set_exciter(noise);
80 *
81 * // Direct frequency/Q control
82 * network->set_frequency(2, 2400.0);
83 * network->set_q(2, 80.0);
84 * @endcode
85 */
86class MAYAFLUX_API ResonatorNetwork : public NodeNetwork {
87public:
88 //-------------------------------------------------------------------------
89 // Presets
90 //-------------------------------------------------------------------------
91
92 /**
93 * @enum FormantPreset
94 * @brief Common vowel and spectral formant configurations
95 *
96 * Provides a starting point for formant synthesis. Frequencies are
97 * approximate averages for a neutral adult voice; Q values model typical
98 * bandwidths (narrower for higher formants).
99 */
100 enum class FormantPreset : uint8_t {
101 NONE, ///< No preset — all resonators initialised at 440 Hz, Q = 10
102 VOWEL_A, ///< Open vowel /a/ (F1≈800, F2≈1200, F3≈2500, F4≈3500, F5≈4500 Hz)
103 VOWEL_E, ///< Front vowel /e/ (F1≈400, F2≈2000, F3≈2600, F4≈3500, F5≈4500 Hz)
104 VOWEL_I, ///< Close front vowel /i/ (F1≈270, F2≈2300, F3≈3000, F4≈3500, F5≈4500 Hz)
105 VOWEL_O, ///< Back vowel /o/ (F1≈500, F2≈900, F3≈2500, F4≈3500, F5≈4500 Hz)
106 VOWEL_U, ///< Close back vowel /u/ (F1≈300, F2≈800, F3≈2300, F4≈3500, F5≈4500 Hz)
107 };
108
109 //-------------------------------------------------------------------------
110 // Per-resonator descriptor
111 //-------------------------------------------------------------------------
112
113 /**
114 * @struct ResonatorNode
115 * @brief State of a single biquad bandpass resonator
116 */
118 std::shared_ptr<Filters::IIR> filter; ///< Underlying biquad IIR
119
120 double frequency; ///< Centre frequency (Hz)
121 double q; ///< Quality factor (dimensionless; higher = narrower bandwidth)
122 double gain; ///< Per-resonator output amplitude scale
123 double last_output; ///< Most recent process_sample output
124 size_t index; ///< Position in network
125
126 std::shared_ptr<Node> exciter; ///< Per-resonator exciter (nullptr = use network-level exciter)
127 };
128
129 //-------------------------------------------------------------------------
130 // Construction
131 //-------------------------------------------------------------------------
132
133 /**
134 * @brief Construct a ResonatorNetwork with a formant preset
135 * @param num_resonators Number of biquad sections to allocate
136 * @param preset Formant frequency/Q configuration to apply at startup
137 *
138 * Resonators beyond the preset's defined count are initialised at 440 Hz,
139 * Q = 10 with unit gain.
140 */
141 ResonatorNetwork(size_t num_resonators,
142 FormantPreset preset = FormantPreset::NONE);
143
144 /**
145 * @brief Construct a ResonatorNetwork with explicit frequency and Q vectors
146 * @param frequencies Centre frequencies in Hz, one per resonator
147 * @param q_values Q factors, one per resonator (must match frequencies.size())
148 * @throws std::invalid_argument if frequencies and q_values differ in size
149 */
150 ResonatorNetwork(const std::vector<double>& frequencies,
151 const std::vector<double>& q_values);
152
153 //-------------------------------------------------------------------------
154 // NodeNetwork Interface
155 //-------------------------------------------------------------------------
156
157 /**
158 * @brief Processes num_samples through all resonators and accumulates output
159 * @param num_samples Number of audio samples to compute
160 *
161 * For each sample, each resonator draws from its individual exciter (or
162 * the network-level exciter, or zero if none) and processes one sample. All
163 * resonator outputs are summed and RMS-normalised (divided by sqrt(resonator_count))
164 * into m_last_audio_buffer before set_output_scale() is applied.
165 */
166 void process_batch(unsigned int num_samples) override;
167
168 /**
169 * @brief Returns the number of resonators in the network
170 */
171 [[nodiscard]] size_t get_node_count() const override { return m_resonators.size(); }
172
173 /**
174 * @brief Returns the mixed audio buffer from the last process_batch() call
175 */
176 [[nodiscard]] std::optional<std::vector<double>> get_audio_buffer() const override;
177
178 /**
179 * @brief Returns the last output sample of the resonator at index
180 * @param index Resonator index (0-based)
181 * @return Last computed output, or nullopt if index is out of range
182 */
183 [[nodiscard]] std::optional<double> get_node_output(size_t index) const override;
184
185 //-------------------------------------------------------------------------
186 // Parameter Mapping (NodeNetwork overrides)
187 //-------------------------------------------------------------------------
188
189 /**
190 * @brief Map a scalar node output to a named networkparameter (BROADCAST)
191 * @param param_name "frequency", "q", "gain", "retune" or "decay"
192 * @param source Node whose get_last_output() is read each process_batch()
193 * @param mode Must be MappingMode::BROADCAST
194 */
195 void map_parameter(const std::string& param_name,
196 const std::shared_ptr<Node>& source,
197 MappingMode mode = MappingMode::BROADCAST) override;
198
199 /**
200 * @brief Map a NodeNetwork's per-node outputs to a named network parameter (ONE_TO_ONE)
201 * @param param_name "frequency", "q", "gain", "retune" or "decay"
202 * @param source_network NodeNetwork with get_node_count() == get_node_count()
203 */
204 void map_parameter(const std::string& param_name,
205 const std::shared_ptr<NodeNetwork>& source_network) override;
206
207 /**
208 * @brief Remove a parameter mapping by name
209 */
210 void unmap_parameter(const std::string& param_name) override;
211
212 //-------------------------------------------------------------------------
213 // Excitation
214 //-------------------------------------------------------------------------
215
216 /**
217 * @brief Set a shared exciter node for all resonators
218 * @param exciter Node providing per-sample excitation (e.g., noise, pulse)
219 *
220 * Per-resonator exciters take priority over this network-level exciter when set.
221 */
222 void set_exciter(const std::shared_ptr<Node>& exciter);
223
224 /**
225 * @brief Clear the network-level exciter
226 */
227 void clear_exciter();
228
229 /**
230 * @brief Set a per-resonator exciter
231 * @param index Resonator index (0-based)
232 * @param exciter Node providing excitation specifically for this resonator
233 * @throws std::out_of_range if index >= get_node_count()
234 */
235 void set_resonator_exciter(size_t index, const std::shared_ptr<Node>& exciter);
236
237 /**
238 * @brief Clear per-resonator exciter, reverting to network-level exciter
239 * @param index Resonator index (0-based)
240 * @throws std::out_of_range if index >= get_node_count()
241 */
242 void clear_resonator_exciter(size_t index);
243
244 /**
245 * @brief Set a NodeNetwork as a source of per-resonator excitation (ONE_TO_ONE)
246 * @param network NodeNetwork with get_node_count() == get_node_count()
247 */
248 void set_network_exciter(const std::shared_ptr<NodeNetwork>& network);
249
250 /**
251 * @brief Clear the network exciter
252 */
253 void clear_network_exciter();
254
255 //-------------------------------------------------------------------------
256 // Per-resonator parameter control
257 //-------------------------------------------------------------------------
258
259 /**
260 * @brief Set centre frequency of a single resonator and recompute its coefficients
261 * @param index Resonator index (0-based)
262 * @param frequency New centre frequency in Hz (clamped to [1.0, sample_rate/2 - 1])
263 * @throws std::out_of_range if index >= get_node_count()
264 */
265 void set_frequency(size_t index, double frequency);
266
267 /**
268 * @brief Set Q factor of a single resonator and recompute its coefficients
269 * @param index Resonator index (0-based)
270 * @param q New quality factor (clamped to [0.1, 1000.0])
271 * @throws std::out_of_range if index >= get_node_count()
272 */
273 void set_q(size_t index, double q);
274
275 /**
276 * @brief Set amplitude gain of a single resonator
277 * @param index Resonator index (0-based)
278 * @param gain New linear amplitude scale
279 * @throws std::out_of_range if index >= get_node_count()
280 */
281 void set_resonator_gain(size_t index, double gain);
282
283 /**
284 * @brief Set the gain of the first resonators from an array
285 * @param gains One linear amplitude scale per resonator
286 */
287 void set_resonator_gain(std::span<const double> gains);
288
289 /**
290 * @brief Move a resonator's centre frequency without clearing its state
291 * @param index Resonator index (0-based)
292 * @param frequency New centre frequency in Hz, clamped below Nyquist
293 * @throws std::out_of_range if index >= get_node_count()
294 *
295 * The stored damping number is kept, so the ring time scales with the
296 * new frequency. Follow with set_decay to hold it. Written in place and
297 * not atomic with respect to a sample in flight.
298 */
299 void retune(size_t index, double frequency);
300
301 /**
302 * @brief Retune the first resonators from an array, keeping every ring
303 * @param frequencies One centre frequency in Hz per resonator; a shorter
304 * array leaves the rest as they are, extra entries are ignored
305 */
306 void retune(std::span<const double> frequencies);
307
308 /**
309 * @brief Set how long a resonator rings, without clearing its state
310 * @param index Resonator index (0-based)
311 * @param seconds Time for the ring envelope to fall by 1/e, no upper limit
312 * @throws std::out_of_range if index >= get_node_count()
313 *
314 * Sets the pole radius directly and derives the stored q from it, so q
315 * follows rather than leads and is not limited to the range set_q accepts.
316 * Written in place and not atomic with respect to a sample in flight.
317 */
318 void set_decay(size_t index, double seconds);
319
320 /**
321 * @brief Set the ring time of the first resonators from an array
322 * @param seconds One ring time per resonator, as for the single form
323 */
324 void set_decay(std::span<const double> seconds);
325
326 //-------------------------------------------------------------------------
327 // network-wide control
328 //-------------------------------------------------------------------------
329
330 /**
331 * @brief Set centre frequency of all resonators uniformly
332 * @param frequency New centre frequency in Hz
333 */
334 void set_all_frequencies(double frequency);
335
336 /**
337 * @brief Set Q factor of all resonators uniformly
338 * @param q New quality factor
339 */
340 void set_all_q(double q);
341
342 /**
343 * @brief Apply a FormantPreset to the current network
344 * @param preset Target vowel/formant configuration
345 *
346 * Resonators that exceed the preset's defined count retain their current parameters.
347 */
348 void apply_preset(FormantPreset preset);
349
350 //-------------------------------------------------------------------------
351 // Read-only access
352 //-------------------------------------------------------------------------
353
354 /**
355 * @brief Read-only access to all resonator descriptors
356 */
357 [[nodiscard]] const std::vector<ResonatorNode>& get_resonators() const { return m_resonators; }
358
359 /**
360 * @brief Current audio sample rate
361 */
362 [[nodiscard]] double get_sample_rate() const { return m_sample_rate; }
363
364 [[nodiscard]] std::optional<std::span<const double>>
365 get_node_audio_buffer(size_t index) const override;
366
367 //-------------------------------------------------------------------------
368 // Metadata
369 //-------------------------------------------------------------------------
370
371 /**
372 * @brief Returns network metadata for debugging and visualisation
373 *
374 * Exposes "num_resonators", "sample_rate", and per-resonator
375 * frequency/Q/gain entries keyed as "resonator_N_freq" etc.
376 */
377 [[nodiscard]] std::unordered_map<std::string, std::string> get_metadata() const override;
378
379private:
380 //-------------------------------------------------------------------------
381 // Internal helpers
382 //-------------------------------------------------------------------------
383
384 /**
385 * @brief Compute RBJ biquad bandpass coefficients and push them into a resonator's IIR
386 * @param r Resonator to update (reads r.frequency, r.q, m_sample_rate)
387 */
388 void compute_biquad(ResonatorNode& r);
389
390 /**
391 * @brief Write the same biquad as compute_biquad through the filter's edit spans
392 * @param r Resonator to update (reads r.frequency, r.q, m_sample_rate)
393 *
394 * Leaves the filter history untouched.
395 */
396 void write_biquad_in_place(ResonatorNode& r);
397
398 /**
399 * @brief Initialise all resonators from a frequency/Q pair list
400 * @param frequencies Centre frequencies in Hz
401 * @param qs Q factors
402 */
403 void build_resonators(const std::vector<double>& frequencies,
404 const std::vector<double>& qs);
405
406 /**
407 * @brief Translate a FormantPreset into parallel frequency/Q vectors
408 * @param preset Requested preset
409 * @param n Number of resonators to populate (may be less than preset's defined count)
410 * @param out_freqs Output frequency vector
411 * @param out_qs Output Q vector
412 */
413 static void preset_to_vectors(FormantPreset preset,
414 size_t n,
415 std::vector<double>& out_freqs,
416 std::vector<double>& out_qs);
417
418 /**
419 * @brief Apply all registered parameter mappings for the current cycle
420 */
421 void update_mapped_parameters();
422
423 /**
424 * @brief Apply a BROADCAST value to the named parameter across all resonators
425 * @param param "frequency", "q", "gain", "retune" or "decay"
426 * @param value Scalar value from source node
427 */
428 void apply_broadcast_parameter(const std::string& param, double value);
429
430 /**
431 * @brief Apply ONE_TO_ONE values from a source network to the named parameter
432 * @param param "frequency", "q", "gain", "retune" or "decay"
433 * @param source NodeNetwork providing one value per resonator
434 */
435 void apply_one_to_one_parameter(const std::string& param,
436 const std::shared_ptr<NodeNetwork>& source);
437
438 //-------------------------------------------------------------------------
439 // Data
440 //-------------------------------------------------------------------------
441
442 std::vector<ResonatorNode> m_resonators;
443
444 std::shared_ptr<Node> m_exciter; ///< networ-level shared exciter (may be nullptr)
445
446 std::shared_ptr<NodeNetwork> m_network_exciter; ///< Optional NodeNetwork exciter for ONE_TO_ONE mapping (may be nullptr)
447
448 std::vector<std::vector<double>> m_node_buffers; ///< Per-resonator sample buffers populated each process_batch()
449
450 std::atomic<double> m_norm_factor { 1.0 }; ///< Normalisation factor for summed output, rms scaled by number of active resonators
451
453 std::string param_name;
455 std::shared_ptr<Node> broadcast_source;
456 std::shared_ptr<NodeNetwork> network_source;
457 };
458
459 std::vector<ParameterMapping> m_parameter_mappings;
460};
461
462} // namespace MayaFlux::Nodes::Network
Core::GlobalNetworkConfig network
Definition Config.cpp:43
uint32_t mode
double frequency
double q
glm::vec3 value
uint32_t index
Definition VKDevice.cpp:122
float gain
Abstract base class for structured collections of nodes with defined relationships.
size_t get_node_count() const override
Returns the number of resonators in the network.
std::vector< std::vector< double > > m_node_buffers
Per-resonator sample buffers populated each process_batch()
std::vector< ParameterMapping > m_parameter_mappings
std::shared_ptr< Node > m_exciter
networ-level shared exciter (may be nullptr)
FormantPreset
Common vowel and spectral formant configurations.
double get_sample_rate() const
Current audio sample rate.
const std::vector< ResonatorNode > & get_resonators() const
Read-only access to all resonator descriptors.
std::shared_ptr< NodeNetwork > m_network_exciter
Optional NodeNetwork exciter for ONE_TO_ONE mapping (may be nullptr)
Network of IIR biquad bandpass filters driven by external excitation.
MappingMode
Defines how nodes map to external entities (e.g., audio channels, graphics objects)
std::shared_ptr< Filters::IIR > filter
Underlying biquad IIR.
double q
Quality factor (dimensionless; higher = narrower bandwidth)
std::shared_ptr< Node > exciter
Per-resonator exciter (nullptr = use network-level exciter)
double last_output
Most recent process_sample output.
double gain
Per-resonator output amplitude scale.
State of a single biquad bandpass resonator.