MayaFlux 1.0.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 antinode, sign preserved
46 * - Open end (bell): pressure node, sign inverted
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 open end: p_minus[0] = -p_plus[end]
132 * At the closed end: p_plus[0] = p_minus[end]
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 }; ///< Closed-end reflection; a lumped string loop keeps +1 because its two inversions cancel
156 double reflection_open { 1.0 }; ///< Open-end reflection; a tube configures this to -1
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 Sounding fundamental in Hz. A TUBE is a closed-open
184 * bore, so each rail is a quarter of the period long
185 * @param sample_rate Sample rate in Hz
186 */
188 WaveguideType type,
189 double fundamental_freq,
190 double sample_rate = 48000.0);
191
192 //-------------------------------------------------------------------------
193 // NodeNetwork Interface
194 //-------------------------------------------------------------------------
195
196 void process_batch(unsigned int num_samples) override;
197
198 [[nodiscard]] size_t get_node_count() const override { return m_segments.size(); }
199
200 void initialize() override;
201 void reset() override;
202
203 [[nodiscard]] std::optional<double> get_node_output(size_t index) const override;
204 [[nodiscard]] std::unordered_map<std::string, std::string> get_metadata() const override;
205 [[nodiscard]] std::optional<std::span<const double>> get_node_audio_buffer(size_t index) const override;
206
207 //-------------------------------------------------------------------------
208 // Parameter Mapping
209 //-------------------------------------------------------------------------
210
211 void map_parameter(const std::string& param_name,
212 const std::shared_ptr<Node>& source,
213 MappingMode mode = MappingMode::BROADCAST) override;
214
215 void map_parameter(const std::string& param_name,
216 const std::shared_ptr<NodeNetwork>& source) override;
217
218 void unmap_parameter(const std::string& param_name) override;
219
220 //-------------------------------------------------------------------------
221 // Excitation
222 //-------------------------------------------------------------------------
223
224 /**
225 * @brief Pluck the string at a normalized position
226 * @param position Normalized position along string (0.0 to 1.0)
227 * @param strength Excitation amplitude (0.0 to 1.0+)
228 *
229 * Seeds the delay line with a triangle. A string seed has its average
230 * removed, because the lumped loop would otherwise hold it as a DC offset.
231 * Position affects spectral content: 0.5 = center (warm), near 0/1 = bridge (bright).
232 */
233 void pluck(double position = 0.5, double strength = 1.0);
234
235 /**
236 * @brief Strike the string/tube with an impulse
237 * @param position Normalized strike position
238 * @param strength Excitation amplitude
239 */
240 void strike(double position = 0.5, double strength = 1.0);
241
242 /**
243 * @brief Set exciter type
244 */
245 void set_exciter_type(ExciterType type);
246
247 void release();
248
249 /**
250 * @brief Get current exciter type
251 */
252 [[nodiscard]] ExciterType get_exciter_type() const { return m_exciter_type; }
253
254 /**
255 * @brief Set noise burst duration for exciter
256 * @param seconds Duration in seconds
257 */
258 void set_exciter_duration(double seconds);
259
260 /**
261 * @brief Set filter for shaped noise excitation
262 * @param filter Filter node for spectral shaping (FILTERED_NOISE only)
263 */
264 void set_exciter_filter(const std::shared_ptr<Filters::Filter>& filter) { m_exciter_filter = filter; }
265
266 /**
267 * @brief Set custom excitation waveform
268 * @param sample Excitation waveform (SAMPLE only)
269 */
270 void set_exciter_sample(const std::vector<double>& sample);
271
272 /**
273 * @brief Set continuous exciter node (for bowing/blowing)
274 * @param node Source node providing continuous excitation
275 *
276 * If the exciter type is already CONTINUOUS the drive starts immediately,
277 * so the order of set_exciter_type and set_exciter_node does not matter.
278 */
279 void set_exciter_node(const std::shared_ptr<Node>& node)
280 {
281 m_exciter_node = node;
282 if (node && m_exciter_type == ExciterType::CONTINUOUS) {
283 initialize_exciter();
284 }
285 }
286
287 //-------------------------------------------------------------------------
288 // Waveguide Control
289 //-------------------------------------------------------------------------
290
291 /**
292 * @brief Set fundamental frequency
293 * @param freq Frequency in Hz
294 *
295 * Recomputes delay line length. Fractional part handled via interpolation.
296 */
297 void set_fundamental(double freq);
298
299 /**
300 * @brief Get current fundamental frequency
301 */
302 [[nodiscard]] double get_fundamental() const { return m_fundamental; }
303
304 /**
305 * @brief Set per-sample energy loss factor
306 * @param loss Factor applied per sample (0.99-1.0 typical)
307 *
308 * Controls overall decay time. Values closer to 1.0 sustain longer.
309 */
310 void set_loss_factor(double loss);
311
312 /**
313 * @brief Get current loss factor
314 */
315 [[nodiscard]] double get_loss_factor() const;
316
317 /**
318 * @brief Replace the loop filter
319 * @param filter IIR or FIR filter applied in the feedback loop
320 *
321 * Default is a one-pole averaging filter: y[n] = 0.5*(x[n] + x[n-1])
322 * which simulates frequency-dependent string damping.
323 */
324 void set_loop_filter(const std::shared_ptr<Filters::Filter>& filter);
325
326 /**
327 * @brief Set pickup position along the string
328 * @param position Normalized position (0.0 to 1.0)
329 *
330 * Determines where the output is read from the delay line.
331 * Different positions emphasize different harmonics.
332 */
333 void set_pickup_position(double position);
334
335 /**
336 * @brief Get current pickup position
337 */
338 [[nodiscard]] double get_pickup_position() const;
339
340 /**
341 * @brief Get waveguide type
342 */
343 [[nodiscard]] WaveguideType get_type() const { return m_type; }
344
345 /**
346 * @brief Get read-only access to segments
347 */
348 [[nodiscard]] const std::vector<WaveguideSegment>& get_segments() const { return m_segments; }
349
350 /**
351 * @brief Set filter for the closed-end termination (mouthpiece/nut)
352 * @param filter Filter applied to p_plus as it reflects at the closed end
353 *
354 * Only meaningful for WaveguideType::TUBE. Falls back to loop_filter if unset.
355 * Models reed/embouchure losses and input impedance characteristic.
356 */
357 void set_loop_filter_closed(const std::shared_ptr<Filters::Filter>& filter);
358
359 /**
360 * @brief Set filter for the open-end termination (bell/bridge)
361 * @param filter Filter applied to p_minus as it reflects at the open end
362 *
363 * Only meaningful for WaveguideType::TUBE. Falls back to loop_filter if unset.
364 * Models radiation resistance and bell flare HF rolloff.
365 */
366 void set_loop_filter_open(const std::shared_ptr<Filters::Filter>& filter);
367
368 /**
369 * @brief Set measurement mode for output
370 * @param mode Whether outputs represent pressure or velocity
371 */
372 void set_measurement_mode(MeasurementMode mode) { m_measurement_mode = mode; }
373
374 /**
375 * @brief Get current measurement mode
376 */
377 [[nodiscard]] MeasurementMode get_measurement_mode() const { return m_measurement_mode; }
378
379private:
380 //-------------------------------------------------------------------------
381 // Internal State
382 //-------------------------------------------------------------------------
383
386
387 std::vector<WaveguideSegment> m_segments;
388
389 size_t m_delay_length_integer { 0 };
390 double m_delay_length_fraction { 0.0 };
391 size_t m_pickup_sample { 0 };
392 double m_pickup_position { 0.5 };
393
394 //-------------------------------------------------------------------------
395 // Exciter State
396 //-------------------------------------------------------------------------
397
398 ExciterType m_exciter_type { ExciterType::NOISE_BURST };
399 MeasurementMode m_measurement_mode { MeasurementMode::PRESSURE };
400 double m_exciter_duration { 0.005 };
401 std::vector<double> m_exciter_sample;
402 std::shared_ptr<Filters::Filter> m_exciter_filter;
403 std::shared_ptr<Node> m_exciter_node;
404
405 size_t m_exciter_sample_position {};
406 bool m_exciter_active {};
407 size_t m_exciter_samples_remaining {};
408
409 double m_exciter_strength { 1.0 };
410 std::vector<double> m_exciter_node_buffer;
411 size_t m_exciter_node_buffer_pos {};
412
413 //-------------------------------------------------------------------------
414 // Output
415 //-------------------------------------------------------------------------
416
417 mutable double m_last_output {};
418
420
421 //-------------------------------------------------------------------------
422 // Internal Methods
423 //-------------------------------------------------------------------------
424
425 void compute_delay_length();
426 void create_default_loop_filter();
427
428 /**
429 * @brief Read from delay line with linear fractional interpolation
430 * @param delay History buffer to read from
431 * @param integer_part Integer delay in samples
432 * @param fraction Fractional delay (0.0 to 1.0)
433 * @return Interpolated sample value
434 */
435 [[nodiscard]] double read_with_interpolation(
437 size_t integer_part,
438 double fraction) const;
439
440 double generate_exciter_sample();
441 void initialize_exciter();
442
443 void update_mapped_parameters();
444 void apply_broadcast_parameter(const std::string& param, double value);
445 void apply_one_to_one_parameter(const std::string& param, const std::shared_ptr<NodeNetwork>& source);
446
447 void process_unidirectional(WaveguideSegment& seg, unsigned int num_samples, std::vector<double>& out);
448 void process_bidirectional(WaveguideSegment& seg, unsigned int num_samples, std::vector<double>& out);
449 double observe_sample(const WaveguideSegment& seg) const;
450};
451
452} // namespace MayaFlux::Nodes::Network
uint64_t length
Definition Composite.cpp:11
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
uint32_t mode
std::vector< float > * out
glm::vec3 value
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
uint32_t index
Definition VKDevice.cpp:122
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:10
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