MayaFlux 0.2.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) { m_exciter_type = type; }
244
245 /**
246 * @brief Get current exciter type
247 */
248 [[nodiscard]] ExciterType get_exciter_type() const { return m_exciter_type; }
249
250 /**
251 * @brief Set noise burst duration for exciter
252 * @param seconds Duration in seconds
253 */
254 void set_exciter_duration(double seconds);
255
256 /**
257 * @brief Set filter for shaped noise excitation
258 * @param filter Filter node for spectral shaping (FILTERED_NOISE only)
259 */
260 void set_exciter_filter(const std::shared_ptr<Filters::Filter>& filter) { m_exciter_filter = filter; }
261
262 /**
263 * @brief Set custom excitation waveform
264 * @param sample Excitation waveform (SAMPLE only)
265 */
266 void set_exciter_sample(const std::vector<double>& sample);
267
268 /**
269 * @brief Set continuous exciter node (for bowing/blowing)
270 * @param node Source node providing continuous excitation
271 */
272 void set_exciter_node(const std::shared_ptr<Node>& node) { m_exciter_node = node; }
273
274 //-------------------------------------------------------------------------
275 // Waveguide Control
276 //-------------------------------------------------------------------------
277
278 /**
279 * @brief Set fundamental frequency
280 * @param freq Frequency in Hz
281 *
282 * Recomputes delay line length. Fractional part handled via interpolation.
283 */
284 void set_fundamental(double freq);
285
286 /**
287 * @brief Get current fundamental frequency
288 */
289 [[nodiscard]] double get_fundamental() const { return m_fundamental; }
290
291 /**
292 * @brief Set per-sample energy loss factor
293 * @param loss Factor applied per sample (0.99-1.0 typical)
294 *
295 * Controls overall decay time. Values closer to 1.0 sustain longer.
296 */
297 void set_loss_factor(double loss);
298
299 /**
300 * @brief Get current loss factor
301 */
302 [[nodiscard]] double get_loss_factor() const;
303
304 /**
305 * @brief Replace the loop filter
306 * @param filter IIR or FIR filter applied in the feedback loop
307 *
308 * Default is a one-pole averaging filter: y[n] = 0.5*(x[n] + x[n-1])
309 * which simulates frequency-dependent string damping.
310 */
311 void set_loop_filter(const std::shared_ptr<Filters::Filter>& filter);
312
313 /**
314 * @brief Set pickup position along the string
315 * @param position Normalized position (0.0 to 1.0)
316 *
317 * Determines where the output is read from the delay line.
318 * Different positions emphasize different harmonics.
319 */
320 void set_pickup_position(double position);
321
322 /**
323 * @brief Get current pickup position
324 */
325 [[nodiscard]] double get_pickup_position() const;
326
327 /**
328 * @brief Get waveguide type
329 */
330 [[nodiscard]] WaveguideType get_type() const { return m_type; }
331
332 /**
333 * @brief Get read-only access to segments
334 */
335 [[nodiscard]] const std::vector<WaveguideSegment>& get_segments() const { return m_segments; }
336
337 /**
338 * @brief Set filter for the closed-end termination (mouthpiece/nut)
339 * @param filter Filter applied to p_plus as it reflects at the closed end
340 *
341 * Only meaningful for WaveguideType::TUBE. Falls back to loop_filter if unset.
342 * Models reed/embouchure losses and input impedance characteristic.
343 */
344 void set_loop_filter_closed(const std::shared_ptr<Filters::Filter>& filter);
345
346 /**
347 * @brief Set filter for the open-end termination (bell/bridge)
348 * @param filter Filter applied to p_minus as it reflects at the open end
349 *
350 * Only meaningful for WaveguideType::TUBE. Falls back to loop_filter if unset.
351 * Models radiation resistance and bell flare HF rolloff.
352 */
353 void set_loop_filter_open(const std::shared_ptr<Filters::Filter>& filter);
354
355 /**
356 * @brief Set measurement mode for output
357 * @param mode Whether outputs represent pressure or velocity
358 */
359 void set_measurement_mode(MeasurementMode mode) { m_measurement_mode = mode; }
360
361 /**
362 * @brief Get current measurement mode
363 */
364 [[nodiscard]] MeasurementMode get_measurement_mode() const { return m_measurement_mode; }
365
366private:
367 //-------------------------------------------------------------------------
368 // Internal State
369 //-------------------------------------------------------------------------
370
373
374 std::vector<WaveguideSegment> m_segments;
375
376 size_t m_delay_length_integer { 0 };
377 double m_delay_length_fraction { 0.0 };
378 size_t m_pickup_sample { 0 };
379
380 //-------------------------------------------------------------------------
381 // Exciter State
382 //-------------------------------------------------------------------------
383
384 ExciterType m_exciter_type { ExciterType::NOISE_BURST };
385 MeasurementMode m_measurement_mode { MeasurementMode::PRESSURE };
386 double m_exciter_duration { 0.005 };
387 std::vector<double> m_exciter_sample;
388 std::shared_ptr<Filters::Filter> m_exciter_filter;
389 std::shared_ptr<Node> m_exciter_node;
390
391 size_t m_exciter_sample_position {};
392 bool m_exciter_active {};
393 size_t m_exciter_samples_remaining {};
394
395 //-------------------------------------------------------------------------
396 // Output
397 //-------------------------------------------------------------------------
398
399 mutable double m_last_output {};
400
402
403 //-------------------------------------------------------------------------
404 // Internal Methods
405 //-------------------------------------------------------------------------
406
407 void compute_delay_length();
408 void create_default_loop_filter();
409
410 /**
411 * @brief Read from delay line with linear fractional interpolation
412 * @param delay History buffer to read from
413 * @param integer_part Integer delay in samples
414 * @param fraction Fractional delay (0.0 to 1.0)
415 * @return Interpolated sample value
416 */
417 [[nodiscard]] double read_with_interpolation(
419 size_t integer_part,
420 double fraction) const;
421
422 double generate_exciter_sample();
423 void initialize_exciter();
424
425 void update_mapped_parameters();
426 void apply_broadcast_parameter(const std::string& param, double value);
427 void apply_one_to_one_parameter(const std::string& param, const std::shared_ptr<NodeNetwork>& source);
428
429 void process_unidirectional(WaveguideSegment& seg, unsigned int num_samples);
430 void process_bidirectional(WaveguideSegment& seg, unsigned int num_samples);
431 double observe_sample(const WaveguideSegment& seg) const;
432};
433
434} // 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_type(ExciterType type)
Set exciter type.
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