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