MayaFlux 0.4.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
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