MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
WaveguideNetwork.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "NodeNetwork.hpp"
4
7
9class Generator;
10}
11
13class Filter;
14}
15
17
18/**
19 * @class WaveguideNetwork
20 * @brief Digital waveguide synthesis via uni- and bidirectional delay-line architectures
21 *
22 * CONCEPT:
23 * ========
24 * Digital waveguide synthesis models vibrating structures as traveling waves
25 * propagating through delay lines. A loop filter at each termination simulates
26 * frequency-dependent energy loss. This complements ModalNetwork (frequency-domain)
27 * with time-domain physical modeling: where ModalNetwork decomposes resonance into
28 * independent modes, WaveguideNetwork simulates wave propagation directly.
29 *
30 * PROPAGATION MODES:
31 * ==================
32 * WaveguideSegment is direction-agnostic and supports both modes via PropagationMode:
33 *
34 * UNIDIRECTIONAL (STRING):
35 * Single loop. Wave circulates on p_plus only. Karplus-Strong extended model.
36 * Loop filter at the single termination controls frequency-dependent damping.
37 *
38 * exciter ──► p_plus ──► [delay N] ──► loop_filter ──► loss ──┐
39 * output ◄── tap(pickup_sample) │
40 * └─────────────────────────────────────────────────┘
41 *
42 * BIDIRECTIONAL (TUBE):
43 * Two rails. p_plus travels toward the open end, p_minus returns toward the
44 * closed end. Reflection sign at each termination determines harmonic series:
45 * - Closed end (mouthpiece): pressure node, sign preserved → odd harmonics
46 * - Open end (bell): pressure antinode, sign inverted → adds even harmonics
47 * Output is the physical pressure sum p_plus[pickup] + p_minus[pickup].
48 *
49 * exciter ──► p_plus ──► [delay N] ──► loop_filter ──► loss ──► open-end (−)
50 * output ◄── tap │
51 * p_minus ◄── [delay N] ◄── loop_filter ◄── loss ◄── closed-end (+)
52 *
53 * EXCITATION:
54 * ===========
55 * pluck() seeds p_plus with a triangle waveform (shaped initial displacement).
56 * strike() seeds p_plus with a Gaussian-windowed noise burst at the strike point.
57 * Both clear p_minus, ensuring a clean bidirectional state on re-excitation.
58 * Continuous and sample-based exciters inject per-sample into p_plus at the
59 * closed end on every call to process_batch().
60 *
61 * USAGE:
62 * ======
63 * ```cpp
64 * // Plucked string
65 * auto string = std::make_shared<WaveguideNetwork>(
66 * WaveguideNetwork::WaveguideType::STRING, 220.0);
67 * string->pluck(0.3, 0.8);
68 *
69 * // Cylindrical bore (clarinet-like, odd harmonics)
70 * auto tube = std::make_shared<WaveguideNetwork>(
71 * WaveguideNetwork::WaveguideType::TUBE, 220.0);
72 * tube->strike(0.1, 0.9);
73 *
74 * // Via vega API:
75 * auto string = vega.WaveguideNetwork(WaveguideType::STRING, 220.0)[0] | Audio;
76 * string->pluck(0.3, 0.8);
77 * ```
78 *
79 * PARAMETER MAPPING:
80 * ==================
81 * - "frequency": Fundamental frequency in Hz (BROADCAST)
82 * - "damping" / "loss": Loop filter cutoff / loss factor (BROADCAST)
83 * - "position": Pickup position along the delay line (BROADCAST)
84 */
85class MAYAFLUX_API WaveguideNetwork : public NodeNetwork {
86public:
87 /**
88 * @enum WaveguideType
89 * @brief Physical structure being modeled
90 */
91 enum class WaveguideType : uint8_t {
92 STRING, ///< 1D string (Karplus-Strong extended)
93 TUBE, ///< Cylindrical bore (future: clarinet, flute)
94 };
95
96 /**
97 * @enum ExciterType
98 * @brief Excitation signal types for waveguide synthesis
99 */
100 enum class ExciterType : uint8_t {
101 IMPULSE, ///< Single-sample Dirac impulse
102 NOISE_BURST, ///< Short white noise burst (default for pluck)
103 FILTERED_NOISE, ///< Spectrally-shaped noise burst
104 SAMPLE, ///< User-provided excitation waveform
105 CONTINUOUS ///< External node as continuous exciter (bowing)
106 };
107
108 /**
109 * @enum MeasurementMode
110 * @brief Whether node outputs represent pressure or velocity (for future use)
111 * Pressure: output is physical pressure at pickup (p_plus + p_minus)
112 * Velocity: output is particle velocity at pickup (p_plus - p_minus)
113 */
114 enum class MeasurementMode : uint8_t {
115 PRESSURE, ///< Output is physical pressure at pickup (p_plus + p_minus)
116 VELOCITY ///< Output is particle velocity at pickup (p_plus - p_minus)
117 };
118
119 /**
120 * @struct WaveguideSegment
121 * @brief 1D delay-line segment supporting both uni- and bidirectional propagation
122 *
123 * UNIDIRECTIONAL (STRING):
124 * Only p_plus is active. Wave circulates in a single loop.
125 * p_minus is allocated but never written or read.
126 * Output tapped at pickup_sample along p_plus.
127 *
128 * BIDIRECTIONAL (TUBE):
129 * Both rails active. p_plus travels toward the open end (bell),
130 * p_minus travels back toward the mouthpiece.
131 * At the closed end: p_minus[0] = -p_plus[end] (pressure inversion)
132 * At the open end: p_plus[0] = -p_minus[end] (approximate open reflection)
133 * Output is p_plus[pickup] + p_minus[pickup] (physical pressure sum).
134 *
135 * Both rails share the same integer/fractional delay length and loop filter.
136 * The propagation mode is set once at construction and never changes.
137 */
139 /**
140 * @enum PropagationMode
141 * @brief Whether this segment uses one or two traveling-wave rails
142 */
143 enum class PropagationMode : uint8_t {
144 UNIDIRECTIONAL, ///< Single loop (STRING)
145 BIDIRECTIONAL, ///< Forward + backward rails (TUBE)
146 };
147
148 Memory::HistoryBuffer<double> p_plus; ///< Forward-traveling wave rail
149 Memory::HistoryBuffer<double> p_minus; ///< Backward-traveling wave rail (BIDIRECTIONAL only)
150 std::shared_ptr<Filters::Filter> loop_filter; ///< UNIDIRECTIONAL: single termination filter
151 std::shared_ptr<Filters::Filter> loop_filter_closed; ///< BIDIRECTIONAL: closed-end filter (mouthpiece/nut)
152 std::shared_ptr<Filters::Filter> loop_filter_open; ///< BIDIRECTIONAL: open-end filter (bell/bridge)
153 PropagationMode mode { PropagationMode::UNIDIRECTIONAL };
154 double loss_factor { 0.996 };
155 double reflection_closed { -1.0 }; ///< Reflection coefficient at closed end (pressure node)
156 double reflection_open { 1.0 }; ///< Reflection coefficient at open end (pressure antinode)
157
158 /**
159 * @brief Construct segment with both rails at the specified length
160 * @param length Delay-line length in samples
161 * @param prop_mode Propagation mode; determines which rails are active
162 *
163 * Both rails are always allocated regardless of mode to avoid
164 * conditional sizing logic at call sites. The UNIDIRECTIONAL path
165 * simply never touches p_minus.
166 */
167 explicit WaveguideSegment(size_t length,
168 PropagationMode prop_mode = PropagationMode::UNIDIRECTIONAL)
169 : p_plus(length)
170 , p_minus(length)
171 , mode(prop_mode)
172 {
173 }
174 };
175
176 //-------------------------------------------------------------------------
177 // Construction
178 //-------------------------------------------------------------------------
179
180 /**
181 * @brief Create waveguide network with specified type and frequency
182 * @param type Physical structure to model
183 * @param fundamental_freq Fundamental frequency in Hz
184 * @param sample_rate Sample rate in Hz
185 */
187 WaveguideType type,
188 double fundamental_freq,
189 double sample_rate = 48000.0);
190
191 //-------------------------------------------------------------------------
192 // NodeNetwork Interface
193 //-------------------------------------------------------------------------
194
195 void process_batch(unsigned int num_samples) override;
196
197 [[nodiscard]] size_t get_node_count() const override { return m_segments.size(); }
198
199 void initialize() override;
200 void reset() override;
201
202 [[nodiscard]] std::optional<double> get_node_output(size_t index) const override;
203 [[nodiscard]] std::unordered_map<std::string, std::string> get_metadata() const override;
204 [[nodiscard]] std::optional<std::span<const double>> get_node_audio_buffer(size_t index) const override;
205
206 //-------------------------------------------------------------------------
207 // Parameter Mapping
208 //-------------------------------------------------------------------------
209
210 void map_parameter(const std::string& param_name,
211 const std::shared_ptr<Node>& source,
212 MappingMode mode = MappingMode::BROADCAST) override;
213
214 void map_parameter(const std::string& param_name,
215 const std::shared_ptr<NodeNetwork>& source) override;
216
217 void unmap_parameter(const std::string& param_name) override;
218
219 //-------------------------------------------------------------------------
220 // Excitation
221 //-------------------------------------------------------------------------
222
223 /**
224 * @brief Pluck the string at a normalized position
225 * @param position Normalized position along string (0.0 to 1.0)
226 * @param strength Excitation amplitude (0.0 to 1.0+)
227 *
228 * Fills the delay line with a shaped noise burst. Position affects
229 * spectral content: 0.5 = center (warm), near 0/1 = bridge (bright).
230 */
231 void pluck(double position = 0.5, double strength = 1.0);
232
233 /**
234 * @brief Strike the string/tube with an impulse
235 * @param position Normalized strike position
236 * @param strength Excitation amplitude
237 */
238 void strike(double position = 0.5, double strength = 1.0);
239
240 /**
241 * @brief Set exciter type
242 */
243 void set_exciter_type(ExciterType type);
244
245 void release();
246
247 /**
248 * @brief Get current exciter type
249 */
250 [[nodiscard]] ExciterType get_exciter_type() const { return m_exciter_type; }
251
252 /**
253 * @brief Set noise burst duration for exciter
254 * @param seconds Duration in seconds
255 */
256 void set_exciter_duration(double seconds);
257
258 /**
259 * @brief Set filter for shaped noise excitation
260 * @param filter Filter node for spectral shaping (FILTERED_NOISE only)
261 */
262 void set_exciter_filter(const std::shared_ptr<Filters::Filter>& filter) { m_exciter_filter = filter; }
263
264 /**
265 * @brief Set custom excitation waveform
266 * @param sample Excitation waveform (SAMPLE only)
267 */
268 void set_exciter_sample(const std::vector<double>& sample);
269
270 /**
271 * @brief Set continuous exciter node (for bowing/blowing)
272 * @param node Source node providing continuous excitation
273 */
274 void set_exciter_node(const std::shared_ptr<Node>& node) { m_exciter_node = node; }
275
276 //-------------------------------------------------------------------------
277 // Waveguide Control
278 //-------------------------------------------------------------------------
279
280 /**
281 * @brief Set fundamental frequency
282 * @param freq Frequency in Hz
283 *
284 * Recomputes delay line length. Fractional part handled via interpolation.
285 */
286 void set_fundamental(double freq);
287
288 /**
289 * @brief Get current fundamental frequency
290 */
291 [[nodiscard]] double get_fundamental() const { return m_fundamental; }
292
293 /**
294 * @brief Set per-sample energy loss factor
295 * @param loss Factor applied per sample (0.99-1.0 typical)
296 *
297 * Controls overall decay time. Values closer to 1.0 sustain longer.
298 */
299 void set_loss_factor(double loss);
300
301 /**
302 * @brief Get current loss factor
303 */
304 [[nodiscard]] double get_loss_factor() const;
305
306 /**
307 * @brief Replace the loop filter
308 * @param filter IIR or FIR filter applied in the feedback loop
309 *
310 * Default is a one-pole averaging filter: y[n] = 0.5*(x[n] + x[n-1])
311 * which simulates frequency-dependent string damping.
312 */
313 void set_loop_filter(const std::shared_ptr<Filters::Filter>& filter);
314
315 /**
316 * @brief Set pickup position along the string
317 * @param position Normalized position (0.0 to 1.0)
318 *
319 * Determines where the output is read from the delay line.
320 * Different positions emphasize different harmonics.
321 */
322 void set_pickup_position(double position);
323
324 /**
325 * @brief Get current pickup position
326 */
327 [[nodiscard]] double get_pickup_position() const;
328
329 /**
330 * @brief Get waveguide type
331 */
332 [[nodiscard]] WaveguideType get_type() const { return m_type; }
333
334 /**
335 * @brief Get read-only access to segments
336 */
337 [[nodiscard]] const std::vector<WaveguideSegment>& get_segments() const { return m_segments; }
338
339 /**
340 * @brief Set filter for the closed-end termination (mouthpiece/nut)
341 * @param filter Filter applied to p_plus as it reflects at the closed end
342 *
343 * Only meaningful for WaveguideType::TUBE. Falls back to loop_filter if unset.
344 * Models reed/embouchure losses and input impedance characteristic.
345 */
346 void set_loop_filter_closed(const std::shared_ptr<Filters::Filter>& filter);
347
348 /**
349 * @brief Set filter for the open-end termination (bell/bridge)
350 * @param filter Filter applied to p_minus as it reflects at the open end
351 *
352 * Only meaningful for WaveguideType::TUBE. Falls back to loop_filter if unset.
353 * Models radiation resistance and bell flare HF rolloff.
354 */
355 void set_loop_filter_open(const std::shared_ptr<Filters::Filter>& filter);
356
357 /**
358 * @brief Set measurement mode for output
359 * @param mode Whether outputs represent pressure or velocity
360 */
361 void set_measurement_mode(MeasurementMode mode) { m_measurement_mode = mode; }
362
363 /**
364 * @brief Get current measurement mode
365 */
366 [[nodiscard]] MeasurementMode get_measurement_mode() const { return m_measurement_mode; }
367
368private:
369 //-------------------------------------------------------------------------
370 // Internal State
371 //-------------------------------------------------------------------------
372
375
376 std::vector<WaveguideSegment> m_segments;
377
378 size_t m_delay_length_integer { 0 };
379 double m_delay_length_fraction { 0.0 };
380 size_t m_pickup_sample { 0 };
381
382 //-------------------------------------------------------------------------
383 // Exciter State
384 //-------------------------------------------------------------------------
385
386 ExciterType m_exciter_type { ExciterType::NOISE_BURST };
387 MeasurementMode m_measurement_mode { MeasurementMode::PRESSURE };
388 double m_exciter_duration { 0.005 };
389 std::vector<double> m_exciter_sample;
390 std::shared_ptr<Filters::Filter> m_exciter_filter;
391 std::shared_ptr<Node> m_exciter_node;
392
393 size_t m_exciter_sample_position {};
394 bool m_exciter_active {};
395 size_t m_exciter_samples_remaining {};
396
397 double m_exciter_strength { 1.0 };
398 std::vector<double> m_exciter_node_buffer;
399 size_t m_exciter_node_buffer_pos {};
400
401 //-------------------------------------------------------------------------
402 // Output
403 //-------------------------------------------------------------------------
404
405 mutable double m_last_output {};
406
408
409 //-------------------------------------------------------------------------
410 // Internal Methods
411 //-------------------------------------------------------------------------
412
413 void compute_delay_length();
414 void create_default_loop_filter();
415
416 /**
417 * @brief Read from delay line with linear fractional interpolation
418 * @param delay History buffer to read from
419 * @param integer_part Integer delay in samples
420 * @param fraction Fractional delay (0.0 to 1.0)
421 * @return Interpolated sample value
422 */
423 [[nodiscard]] double read_with_interpolation(
425 size_t integer_part,
426 double fraction) const;
427
428 double generate_exciter_sample();
429 void initialize_exciter();
430
431 void update_mapped_parameters();
432 void apply_broadcast_parameter(const std::string& param, double value);
433 void apply_one_to_one_parameter(const std::string& param, const std::shared_ptr<NodeNetwork>& source);
434
435 void process_unidirectional(WaveguideSegment& seg, unsigned int num_samples, std::vector<double>& out);
436 void process_bidirectional(WaveguideSegment& seg, unsigned int num_samples, std::vector<double>& out);
437 double observe_sample(const WaveguideSegment& seg) const;
438};
439
440} // namespace MayaFlux::Nodes::Network
float value
Unified generative infrastructure for stochastic and procedural algorithms.
History buffer for difference equations and recursive relations.
Abstract base class for structured collections of nodes with defined relationships.
Kinesis::Stochastic::Stochastic m_random_generator
MeasurementMode get_measurement_mode() const
Get current measurement mode.
WaveguideType get_type() const
Get waveguide type.
void set_exciter_filter(const std::shared_ptr< Filters::Filter > &filter)
Set filter for shaped noise excitation.
void set_exciter_node(const std::shared_ptr< Node > &node)
Set continuous exciter node (for bowing/blowing)
size_t get_node_count() const override
Get the number of nodes in the network.
WaveguideType
Physical structure being modeled.
ExciterType get_exciter_type() const
Get current exciter type.
const std::vector< WaveguideSegment > & get_segments() const
Get read-only access to segments.
std::shared_ptr< Filters::Filter > m_exciter_filter
ExciterType
Excitation signal types for waveguide synthesis.
MeasurementMode
Whether node outputs represent pressure or velocity (for future use) Pressure: output is physical pre...
void set_measurement_mode(MeasurementMode mode)
Set measurement mode for output.
std::vector< WaveguideSegment > m_segments
double get_fundamental() const
Get current fundamental frequency.
Digital waveguide synthesis via uni- and bidirectional delay-line architectures.
void initialize()
Definition main.cpp:11
MappingMode
Defines how nodes map to external entities (e.g., audio channels, graphics objects)
Memory::HistoryBuffer< double > p_minus
Backward-traveling wave rail (BIDIRECTIONAL only)
std::shared_ptr< Filters::Filter > loop_filter_open
BIDIRECTIONAL: open-end filter (bell/bridge)
PropagationMode
Whether this segment uses one or two traveling-wave rails.
std::shared_ptr< Filters::Filter > loop_filter_closed
BIDIRECTIONAL: closed-end filter (mouthpiece/nut)
WaveguideSegment(size_t length, PropagationMode prop_mode=PropagationMode::UNIDIRECTIONAL)
Construct segment with both rails at the specified length.
std::shared_ptr< Filters::Filter > loop_filter
UNIDIRECTIONAL: single termination filter.
Memory::HistoryBuffer< double > p_plus
Forward-traveling wave rail.
1D delay-line segment supporting both uni- and bidirectional propagation