MayaFlux 0.5.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 *
54 * OUTPUT:
55 * =======
56 * Each process_batch() call sums all resonator outputs into a single mixed audio
57 * buffer, RMS-normalised (divided by sqrt(resonator_count)) to keep perceived
58 * loudness roughly stable as resonator count changes. This is a statistical
59 * approximation assuming resonators are not strongly phase-correlated; a shared
60 * exciter driving closely-tuned resonators can peak more coherently than RMS
61 * assumes. FinalLimiterProcessor at the root audio buffer remains the actual
62 * safety backstop. See set_output_scale() for compensating toward raw resonator
63 * count via get_node_count(). Alternatively, get_node_output(index) exposes the
64 * most recent individual resonator output for cross-domain routing.
65 *
66 * USAGE:
67 * ======
68 * @code
69 * // Five-formant vowel synthesiser fed by a noise source
70 * auto network= std::make_shared<ResonatorNetwork>(5,
71 * ResonatorNetwork::FormantPreset::VOWEL_A, 48000.0);
72 *
73 * auto noise = vega.Random(GAUSSIAN);
74 * network->set_exciter(noise);
75 *
76 * auto rb = vega.ResonatorNetwork(5, ResonatorNetwork::FormantPreset::VOWEL_A)[0] | Audio;
77 * rb->set_exciter(noise);
78 *
79 * // Direct frequency/Q control
80 * network->set_frequency(2, 2400.0);
81 * network->set_q(2, 80.0);
82 * @endcode
83 */
84class MAYAFLUX_API ResonatorNetwork : public NodeNetwork {
85public:
86 //-------------------------------------------------------------------------
87 // Presets
88 //-------------------------------------------------------------------------
89
90 /**
91 * @enum FormantPreset
92 * @brief Common vowel and spectral formant configurations
93 *
94 * Provides a starting point for formant synthesis. Frequencies are
95 * approximate averages for a neutral adult voice; Q values model typical
96 * bandwidths (narrower for higher formants).
97 */
98 enum class FormantPreset : uint8_t {
99 NONE, ///< No preset — all resonators initialised at 440 Hz, Q = 10
100 VOWEL_A, ///< Open vowel /a/ (F1≈800, F2≈1200, F3≈2500, F4≈3500, F5≈4500 Hz)
101 VOWEL_E, ///< Front vowel /e/ (F1≈400, F2≈2000, F3≈2600, F4≈3500, F5≈4500 Hz)
102 VOWEL_I, ///< Close front vowel /i/ (F1≈270, F2≈2300, F3≈3000, F4≈3500, F5≈4500 Hz)
103 VOWEL_O, ///< Back vowel /o/ (F1≈500, F2≈900, F3≈2500, F4≈3500, F5≈4500 Hz)
104 VOWEL_U, ///< Close back vowel /u/ (F1≈300, F2≈800, F3≈2300, F4≈3500, F5≈4500 Hz)
105 };
106
107 //-------------------------------------------------------------------------
108 // Per-resonator descriptor
109 //-------------------------------------------------------------------------
110
111 /**
112 * @struct ResonatorNode
113 * @brief State of a single biquad bandpass resonator
114 */
116 std::shared_ptr<Filters::IIR> filter; ///< Underlying biquad IIR
117
118 double frequency; ///< Centre frequency (Hz)
119 double q; ///< Quality factor (dimensionless; higher = narrower bandwidth)
120 double gain; ///< Per-resonator output amplitude scale
121 double last_output; ///< Most recent process_sample output
122 size_t index; ///< Position in network
123
124 std::shared_ptr<Node> exciter; ///< Per-resonator exciter (nullptr = use network-level exciter)
125 };
126
127 //-------------------------------------------------------------------------
128 // Construction
129 //-------------------------------------------------------------------------
130
131 /**
132 * @brief Construct a ResonatorNetwork with a formant preset
133 * @param num_resonators Number of biquad sections to allocate
134 * @param preset Formant frequency/Q configuration to apply at startup
135 *
136 * Resonators beyond the preset's defined count are initialised at 440 Hz,
137 * Q = 10 with unit gain.
138 */
139 ResonatorNetwork(size_t num_resonators,
140 FormantPreset preset = FormantPreset::NONE);
141
142 /**
143 * @brief Construct a ResonatorNetwork with explicit frequency and Q vectors
144 * @param frequencies Centre frequencies in Hz, one per resonator
145 * @param q_values Q factors, one per resonator (must match frequencies.size())
146 * @throws std::invalid_argument if frequencies and q_values differ in size
147 */
148 ResonatorNetwork(const std::vector<double>& frequencies,
149 const std::vector<double>& q_values);
150
151 //-------------------------------------------------------------------------
152 // NodeNetwork Interface
153 //-------------------------------------------------------------------------
154
155 /**
156 * @brief Processes num_samples through all resonators and accumulates output
157 * @param num_samples Number of audio samples to compute
158 *
159 * For each sample, each resonator draws from its individual exciter (or
160 * the network-level exciter, or zero if none) and processes one sample. All
161 * resonator outputs are summed and RMS-normalised (divided by sqrt(resonator_count))
162 * into m_last_audio_buffer before set_output_scale() is applied.
163 */
164 void process_batch(unsigned int num_samples) override;
165
166 /**
167 * @brief Returns the number of resonators in the network
168 */
169 [[nodiscard]] size_t get_node_count() const override { return m_resonators.size(); }
170
171 /**
172 * @brief Returns the mixed audio buffer from the last process_batch() call
173 */
174 [[nodiscard]] std::optional<std::vector<double>> get_audio_buffer() const override;
175
176 /**
177 * @brief Returns the last output sample of the resonator at index
178 * @param index Resonator index (0-based)
179 * @return Last computed output, or nullopt if index is out of range
180 */
181 [[nodiscard]] std::optional<double> get_node_output(size_t index) const override;
182
183 //-------------------------------------------------------------------------
184 // Parameter Mapping (NodeNetwork overrides)
185 //-------------------------------------------------------------------------
186
187 /**
188 * @brief Map a scalar node output to a named networkparameter (BROADCAST)
189 * @param param_name "frequency", "q", or "gain"
190 * @param source Node whose get_last_output() is read each process_batch()
191 * @param mode Must be MappingMode::BROADCAST
192 */
193 void map_parameter(const std::string& param_name,
194 const std::shared_ptr<Node>& source,
195 MappingMode mode = MappingMode::BROADCAST) override;
196
197 /**
198 * @brief Map a NodeNetwork's per-node outputs to a named network parameter (ONE_TO_ONE)
199 * @param param_name "frequency", "q", or "gain"
200 * @param source_network NodeNetwork with get_node_count() == get_node_count()
201 */
202 void map_parameter(const std::string& param_name,
203 const std::shared_ptr<NodeNetwork>& source_network) override;
204
205 /**
206 * @brief Remove a parameter mapping by name
207 */
208 void unmap_parameter(const std::string& param_name) override;
209
210 //-------------------------------------------------------------------------
211 // Excitation
212 //-------------------------------------------------------------------------
213
214 /**
215 * @brief Set a shared exciter node for all resonators
216 * @param exciter Node providing per-sample excitation (e.g., noise, pulse)
217 *
218 * Per-resonator exciters take priority over this network-level exciter when set.
219 */
220 void set_exciter(const std::shared_ptr<Node>& exciter);
221
222 /**
223 * @brief Clear the network-level exciter
224 */
225 void clear_exciter();
226
227 /**
228 * @brief Set a per-resonator exciter
229 * @param index Resonator index (0-based)
230 * @param exciter Node providing excitation specifically for this resonator
231 * @throws std::out_of_range if index >= get_node_count()
232 */
233 void set_resonator_exciter(size_t index, const std::shared_ptr<Node>& exciter);
234
235 /**
236 * @brief Clear per-resonator exciter, reverting to network-level exciter
237 * @param index Resonator index (0-based)
238 * @throws std::out_of_range if index >= get_node_count()
239 */
240 void clear_resonator_exciter(size_t index);
241
242 /**
243 * @brief Set a NodeNetwork as a source of per-resonator excitation (ONE_TO_ONE)
244 * @param network NodeNetwork with get_node_count() == get_node_count()
245 */
246 void set_network_exciter(const std::shared_ptr<NodeNetwork>& network);
247
248 /**
249 * @brief Clear the network exciter
250 */
251 void clear_network_exciter();
252
253 //-------------------------------------------------------------------------
254 // Per-resonator parameter control
255 //-------------------------------------------------------------------------
256
257 /**
258 * @brief Set centre frequency of a single resonator and recompute its coefficients
259 * @param index Resonator index (0-based)
260 * @param frequency New centre frequency in Hz (clamped to [1.0, sample_rate/2 - 1])
261 * @throws std::out_of_range if index >= get_node_count()
262 */
263 void set_frequency(size_t index, double frequency);
264
265 /**
266 * @brief Set Q factor of a single resonator and recompute its coefficients
267 * @param index Resonator index (0-based)
268 * @param q New quality factor (clamped to [0.1, 1000.0])
269 * @throws std::out_of_range if index >= get_node_count()
270 */
271 void set_q(size_t index, double q);
272
273 /**
274 * @brief Set amplitude gain of a single resonator
275 * @param index Resonator index (0-based)
276 * @param gain New linear amplitude scale
277 * @throws std::out_of_range if index >= get_node_count()
278 */
279 void set_resonator_gain(size_t index, double gain);
280
281 //-------------------------------------------------------------------------
282 // network-wide control
283 //-------------------------------------------------------------------------
284
285 /**
286 * @brief Set centre frequency of all resonators uniformly
287 * @param frequency New centre frequency in Hz
288 */
289 void set_all_frequencies(double frequency);
290
291 /**
292 * @brief Set Q factor of all resonators uniformly
293 * @param q New quality factor
294 */
295 void set_all_q(double q);
296
297 /**
298 * @brief Apply a FormantPreset to the current network
299 * @param preset Target vowel/formant configuration
300 *
301 * Resonators that exceed the preset's defined count retain their current parameters.
302 */
303 void apply_preset(FormantPreset preset);
304
305 //-------------------------------------------------------------------------
306 // Read-only access
307 //-------------------------------------------------------------------------
308
309 /**
310 * @brief Read-only access to all resonator descriptors
311 */
312 [[nodiscard]] const std::vector<ResonatorNode>& get_resonators() const { return m_resonators; }
313
314 /**
315 * @brief Current audio sample rate
316 */
317 [[nodiscard]] double get_sample_rate() const { return m_sample_rate; }
318
319 [[nodiscard]] std::optional<std::span<const double>>
320 get_node_audio_buffer(size_t index) const override;
321
322 //-------------------------------------------------------------------------
323 // Metadata
324 //-------------------------------------------------------------------------
325
326 /**
327 * @brief Returns network metadata for debugging and visualisation
328 *
329 * Exposes "num_resonators", "sample_rate", and per-resonator
330 * frequency/Q/gain entries keyed as "resonator_N_freq" etc.
331 */
332 [[nodiscard]] std::unordered_map<std::string, std::string> get_metadata() const override;
333
334private:
335 //-------------------------------------------------------------------------
336 // Internal helpers
337 //-------------------------------------------------------------------------
338
339 /**
340 * @brief Compute RBJ biquad bandpass coefficients and push them into a resonator's IIR
341 * @param r Resonator to update (reads r.frequency, r.q, m_sample_rate)
342 */
343 void compute_biquad(ResonatorNode& r);
344
345 /**
346 * @brief Initialise all resonators from a frequency/Q pair list
347 * @param frequencies Centre frequencies in Hz
348 * @param qs Q factors
349 */
350 void build_resonators(const std::vector<double>& frequencies,
351 const std::vector<double>& qs);
352
353 /**
354 * @brief Translate a FormantPreset into parallel frequency/Q vectors
355 * @param preset Requested preset
356 * @param n Number of resonators to populate (may be less than preset's defined count)
357 * @param out_freqs Output frequency vector
358 * @param out_qs Output Q vector
359 */
360 static void preset_to_vectors(FormantPreset preset,
361 size_t n,
362 std::vector<double>& out_freqs,
363 std::vector<double>& out_qs);
364
365 /**
366 * @brief Apply all registered parameter mappings for the current cycle
367 */
368 void update_mapped_parameters();
369
370 /**
371 * @brief Apply a BROADCAST value to the named parameter across all resonators
372 * @param param "frequency", "q", or "gain"
373 * @param value Scalar value from source node
374 */
375 void apply_broadcast_parameter(const std::string& param, double value);
376
377 /**
378 * @brief Apply ONE_TO_ONE values from a source network to the named parameter
379 * @param param "frequency", "q", or "gain"
380 * @param source NodeNetwork providing one value per resonator
381 */
382 void apply_one_to_one_parameter(const std::string& param,
383 const std::shared_ptr<NodeNetwork>& source);
384
385 //-------------------------------------------------------------------------
386 // Data
387 //-------------------------------------------------------------------------
388
389 std::vector<ResonatorNode> m_resonators;
390
391 std::shared_ptr<Node> m_exciter; ///< networ-level shared exciter (may be nullptr)
392
393 std::shared_ptr<NodeNetwork> m_network_exciter; ///< Optional NodeNetwork exciter for ONE_TO_ONE mapping (may be nullptr)
394
395 std::vector<std::vector<double>> m_node_buffers; ///< Per-resonator sample buffers populated each process_batch()
396
397 std::atomic<double> m_norm_factor { 1.0 }; ///< Normalisation factor for summed output, rms scaled by number of active resonators
398
400 std::string param_name;
402 std::shared_ptr<Node> broadcast_source;
403 std::shared_ptr<NodeNetwork> network_source;
404 };
405
406 std::vector<ParameterMapping> m_parameter_mappings;
407};
408
409} // namespace MayaFlux::Nodes::Network
Core::GlobalNetworkConfig network
Definition Config.cpp:39
double frequency
double q
float value
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.