MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
WaveguideNetwork.cpp
Go to the documentation of this file.
2
6
8
9//-----------------------------------------------------------------------------
10// Construction
11//-----------------------------------------------------------------------------
12
14 WaveguideType type,
15 double fundamental_freq,
16 double sample_rate)
17 : m_type(type)
18 , m_fundamental(fundamental_freq)
19{
20 m_sample_rate = static_cast<uint32_t>(sample_rate);
23
25
26 const auto prop_mode = (m_type == WaveguideType::TUBE)
29
30 m_segments.emplace_back(m_delay_length_integer + 2, prop_mode);
31
33}
34
35//-----------------------------------------------------------------------------
36// Initialization
37//-----------------------------------------------------------------------------
38
43
45{
46 for (auto& seg : m_segments) {
47 const auto len = seg.p_plus.capacity();
48 seg.p_plus = Memory::HistoryBuffer<double>(len);
49 seg.p_minus = Memory::HistoryBuffer<double>(len);
50 }
51
52 m_exciter_active = false;
55 m_last_output = 0.0;
56 m_last_audio_buffer.clear();
57}
58
59//-----------------------------------------------------------------------------
60// Delay Length Computation
61//-----------------------------------------------------------------------------
62
64{
65 double total_delay = static_cast<double>(m_sample_rate) / m_fundamental;
66
67 total_delay -= 0.5;
68
69 m_delay_length_integer = static_cast<size_t>(total_delay);
70 m_delay_length_fraction = total_delay - static_cast<double>(m_delay_length_integer);
71
73}
74
75//-----------------------------------------------------------------------------
76// Default Loop Filter
77//-----------------------------------------------------------------------------
78
80{
81 auto filter = std::make_shared<Filters::FIR>(
82 std::vector<double> { 0.5, 0.5 });
83
84 if (!m_segments.empty()) {
85 m_segments[0].loop_filter = filter;
86 }
87}
88
89//-----------------------------------------------------------------------------
90// Fractional Delay Interpolation
91//-----------------------------------------------------------------------------
92
95 size_t integer_part,
96 double fraction) const
97{
98 double s0 = delay[integer_part];
99 double s1 = delay[integer_part + 1];
100 return s0 + fraction * (s1 - s0);
101}
102
103//-----------------------------------------------------------------------------
104// Processing
105//-----------------------------------------------------------------------------
106
108{
110 return seg.p_plus[m_pickup_sample];
111 }
112
113 const double plus = seg.p_plus[m_pickup_sample];
114 const double minus = seg.p_minus[m_pickup_sample];
115
117 return plus + minus;
118
119 return plus - minus;
120}
121
122void WaveguideNetwork::process_batch(unsigned int num_samples)
123{
125
126 if (!is_enabled() || m_segments.empty()) {
127 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
128 std::this_thread::yield();
129
130 m_last_audio_buffer.assign(num_samples, 0.0);
131 m_last_output = 0.0;
132 m_audio_buffer_lock.clear(std::memory_order_release);
133 return;
134 }
135
137
138 thread_local std::vector<double> scratch;
139 scratch.assign(num_samples, 0.0);
140
141 auto& seg = m_segments[0];
142
145 m_exciter_node_buffer_pos, num_samples);
146 }
147
149 process_unidirectional(seg, num_samples, scratch);
150 } else {
151 process_bidirectional(seg, num_samples, scratch);
152 }
153
154 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
155 std::this_thread::yield();
156
157 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
160 m_audio_buffer_lock.clear(std::memory_order_release);
161}
162
164 unsigned int num_samples, std::vector<double>& out)
165{
166 const double continuous_gain = (m_exciter_type == ExciterType::CONTINUOUS && m_exciter_node)
167 ? 1.0 / seg.loss_factor
168 : 1.0;
169
170 for (unsigned int i = 0; i < num_samples; ++i) {
171 const double exciter = generate_exciter_sample();
172
173 const double delayed = read_with_interpolation(
175
176 const double filtered = (m_exciter_type != ExciterType::CONTINUOUS && seg.loop_filter)
177 ? seg.loop_filter->process_sample(delayed)
178 : delayed;
179
180 seg.p_plus.push(
181 exciter + filtered * seg.loss_factor * seg.reflection_closed * continuous_gain);
182
183 out[i] = observe_sample(seg);
184 }
185}
186
188 unsigned int num_samples, std::vector<double>& out)
189{
190 const double continuous_gain = (m_exciter_type == ExciterType::CONTINUOUS && m_exciter_node)
191 ? 1.0 / seg.loss_factor
192 : 1.0;
193
194 for (unsigned int i = 0; i < num_samples; ++i) {
195 const double exciter = generate_exciter_sample();
196
197 const double plus_end = read_with_interpolation(
199
200 const double minus_end = read_with_interpolation(
202
203 auto* filt_open = seg.loop_filter_open ? seg.loop_filter_open.get()
204 : seg.loop_filter.get();
205 auto* filt_closed = seg.loop_filter_closed ? seg.loop_filter_closed.get()
206 : seg.loop_filter.get();
207
208 const double filtered_plus = (m_exciter_type != ExciterType::CONTINUOUS && filt_open)
209 ? filt_open->process_sample(plus_end)
210 : plus_end;
211 const double filtered_minus = (m_exciter_type != ExciterType::CONTINUOUS && filt_closed)
212 ? filt_closed->process_sample(minus_end)
213 : minus_end;
214
215 seg.p_minus.push(filtered_plus * seg.loss_factor * seg.reflection_open * continuous_gain);
216 seg.p_plus.push(exciter + filtered_minus * seg.loss_factor * seg.reflection_closed * continuous_gain);
217
218 out[i] = observe_sample(seg);
219 }
220}
221
222//-----------------------------------------------------------------------------
223// Excitation
224//-----------------------------------------------------------------------------
225
226void WaveguideNetwork::pluck(double position, double strength)
227{
228 position = std::clamp(position, 0.01, 0.99);
229
230 if (m_segments.empty())
231 return;
232
233 auto& seg = m_segments[0];
234 const size_t len = m_delay_length_integer;
235 const auto pluck_sample = static_cast<size_t>(position * static_cast<double>(len));
236 const double effective_strength = (m_exciter_type == ExciterType::CONTINUOUS)
237 ? std::max(strength, 1.0)
238 : strength;
239
240 seg.p_plus = Memory::HistoryBuffer<double>(seg.p_plus.capacity());
241 seg.p_minus = Memory::HistoryBuffer<double>(seg.p_minus.capacity());
242
243 for (size_t s = 0; s < len; ++s) {
244 double value = 0.0;
245 if (s <= pluck_sample) {
246 value = effective_strength * static_cast<double>(s)
247 / static_cast<double>(pluck_sample);
248 } else {
249 value = effective_strength * static_cast<double>(len - s)
250 / static_cast<double>(len - pluck_sample);
251 }
252 seg.p_plus.push(value);
253 }
254
256 m_exciter_active = false;
258 }
259}
260
261void WaveguideNetwork::strike(double position, double strength)
262{
263 position = std::clamp(position, 0.01, 0.99);
264
267 m_exciter_duration = 0.002;
269 }
270
271 if (m_segments.empty())
272 return;
273
274 auto& seg = m_segments[0];
275 const size_t len = m_delay_length_integer;
276 const auto strike_center = static_cast<size_t>(position * static_cast<double>(len));
277 const size_t burst_width = std::max<size_t>(len / 10, 4);
278 const double effective_strength = (m_exciter_type == ExciterType::CONTINUOUS)
279 ? std::max(strength, 1.0)
280 : strength;
281
282 seg.p_plus = Memory::HistoryBuffer<double>(seg.p_plus.capacity());
283 seg.p_minus = Memory::HistoryBuffer<double>(seg.p_minus.capacity());
284
285 for (size_t s = 0; s < len; ++s) {
286 const double dist = std::abs(static_cast<double>(s)
287 - static_cast<double>(strike_center));
288 const double window = std::exp(-(dist * dist)
289 / (2.0 * static_cast<double>(burst_width * burst_width)));
290 seg.p_plus.push(effective_strength * m_random_generator(-1.0, 1.0) * window);
291 }
292
294 m_exciter_active = false;
296 }
297}
298
300{
301 m_exciter_type = type;
304 ? std::numeric_limits<size_t>::max()
305 : 0;
306}
307
313
315{
316 m_exciter_duration = std::max(0.001, seconds);
317}
318
319void WaveguideNetwork::set_exciter_sample(const std::vector<double>& sample)
320{
321 m_exciter_sample = sample;
322}
323
325{
326 m_exciter_active = true;
328
329 switch (m_exciter_type) {
332 break;
333
336 m_exciter_samples_remaining = static_cast<size_t>(
337 m_exciter_duration * static_cast<double>(m_sample_rate));
338 break;
339
342 break;
343
345 m_exciter_samples_remaining = std::numeric_limits<size_t>::max();
346 break;
347 }
348}
349
351{
353 m_exciter_active = false;
354 return 0.0;
355 }
356
358 double sample = 0.0;
359
360 switch (m_exciter_type) {
362 sample = 1.0;
363 break;
364
366 sample = m_random_generator(-1.0, 1.0);
367 break;
368
370 double noise = m_random_generator(-1.0, 1.0);
371 sample = m_exciter_filter ? m_exciter_filter->process_sample(noise) : noise;
372 break;
373 }
374
378 }
379 break;
380
382 break;
383 }
384
385 return sample;
386}
387
388//-----------------------------------------------------------------------------
389// Waveguide Control
390//-----------------------------------------------------------------------------
391
393{
394 m_fundamental = std::max(20.0, freq);
396
397 if (!m_segments.empty()) {
398 const size_t required = m_delay_length_integer + 2;
399 auto& seg = m_segments[0];
400 if (seg.p_plus.capacity() < required) {
401 seg.p_plus = Memory::HistoryBuffer<double>(required);
402 seg.p_minus = Memory::HistoryBuffer<double>(required);
403 }
404 }
405}
406
408{
409 loss = std::clamp(loss, 0.0, 1.0);
410 for (auto& seg : m_segments) {
411 seg.loss_factor = loss;
412 }
413}
414
416{
417 return m_segments.empty() ? 0.996 : m_segments[0].loss_factor;
418}
419
420void WaveguideNetwork::set_loop_filter(const std::shared_ptr<Filters::Filter>& filter)
421{
422 if (!m_segments.empty()) {
423 m_segments[0].loop_filter = filter;
424 }
425}
426
428{
429 position = std::clamp(position, 0.0, 1.0);
430 m_pickup_sample = static_cast<size_t>(position * static_cast<double>(m_delay_length_integer));
431 m_pickup_sample = std::clamp(m_pickup_sample, size_t { 0 }, m_delay_length_integer);
432}
433
435{
436 if (m_delay_length_integer == 0) {
437 return 0.5;
438 }
439 return static_cast<double>(m_pickup_sample) / static_cast<double>(m_delay_length_integer);
440}
441
442void WaveguideNetwork::set_loop_filter_open(const std::shared_ptr<Filters::Filter>& filter)
443{
444 if (!m_segments.empty()) {
445 m_segments[0].loop_filter_open = filter;
446 }
447}
448
449void WaveguideNetwork::set_loop_filter_closed(const std::shared_ptr<Filters::Filter>& filter)
450{
451 if (!m_segments.empty()) {
452 m_segments[0].loop_filter_closed = filter;
453 }
454}
455
456//-----------------------------------------------------------------------------
457// Parameter Mapping
458//-----------------------------------------------------------------------------
459
461{
462 for (const auto& mapping : m_parameter_mappings) {
463 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
464 double value = mapping.broadcast_source->get_last_output();
465 apply_broadcast_parameter(mapping.param_name, value);
466 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
467 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
468 }
469 }
470}
471
472void WaveguideNetwork::apply_broadcast_parameter(const std::string& param, double value)
473{
474 if (param == "frequency") {
475 set_fundamental(value);
476 } else if (param == "damping" || param == "loss") {
477 set_loss_factor(value);
478 } else if (param == "position") {
479 set_pickup_position(value);
480 } else if (param == "scale") {
481 m_output_scale = std::max(0.0, value);
482 }
483}
484
486 const std::string& /*param*/,
487 const std::shared_ptr<NodeNetwork>& /*source*/)
488{
489}
490
492 const std::string& param_name,
493 const std::shared_ptr<Node>& source,
494 MappingMode mode)
495{
496 unmap_parameter(param_name);
497
498 ParameterMapping mapping;
499 mapping.param_name = param_name;
500 mapping.mode = mode;
501 mapping.broadcast_source = source;
502 mapping.network_source = nullptr;
503
504 m_parameter_mappings.push_back(std::move(mapping));
505}
506
508 const std::string& param_name,
509 const std::shared_ptr<NodeNetwork>& source_network)
510{
511 unmap_parameter(param_name);
512
513 ParameterMapping mapping;
514 mapping.param_name = param_name;
516 mapping.broadcast_source = nullptr;
517 mapping.network_source = source_network;
518
519 m_parameter_mappings.push_back(std::move(mapping));
520}
521
522void WaveguideNetwork::unmap_parameter(const std::string& param_name)
523{
524 std::erase_if(m_parameter_mappings,
525 [&](const auto& m) { return m.param_name == param_name; });
526}
527
528//-----------------------------------------------------------------------------
529// Metadata
530//-----------------------------------------------------------------------------
531
532std::unordered_map<std::string, std::string>
534{
535 auto metadata = NodeNetwork::get_metadata();
536
537 metadata["type"] = std::string(Reflect::enum_to_string(m_type));
538 metadata["fundamental"] = std::to_string(m_fundamental) + " Hz";
539 metadata["delay_length"] = std::to_string(m_delay_length_integer)
540 + " + " + std::to_string(m_delay_length_fraction) + " samples";
541 metadata["loss_factor"] = std::to_string(get_loss_factor());
542 metadata["pickup_position"] = std::to_string(get_pickup_position());
543
544 metadata["exciter_type"] = std::string(Reflect::enum_to_string(m_exciter_type));
545
546 return metadata;
547}
548
549std::optional<double> WaveguideNetwork::get_node_output(size_t index) const
550{
551 if (index < m_segments.size() && !m_last_audio_buffer.empty()) {
552 return m_last_output;
553 }
554 return std::nullopt;
555}
556
557std::optional<std::span<const double>> WaveguideNetwork::get_node_audio_buffer(size_t index) const
558{
559 if (index != 0 || m_last_audio_buffer.empty())
560 return std::nullopt;
561
562 return std::span<const double>(m_last_audio_buffer);
563}
564
565} // namespace MayaFlux::Nodes::Network
void push(const T &value)
Push new value to front of history.
History buffer for difference equations and recursive relations.
std::vector< ParameterMapping > m_parameter_mappings
void apply_output_scale()
Apply m_output_scale to m_last_audio_buffer.
virtual void set_topology(Topology topology)
Set the network's topology.
static void extract_node_samples(const std::shared_ptr< Nodes::Node > &node, std::vector< double > &buffer, size_t &buffer_pos, size_t num_samples)
Extract num_samples from node into buffer using snapshot guard.
bool is_enabled() const
Check if network is enabled.
double m_output_scale
Post-processing scalar applied to m_last_audio_buffer each batch.
std::atomic_flag m_audio_buffer_lock
Spinlock guarding m_last_audio_buffer.
void ensure_initialized()
Ensure initialize() is called exactly once.
virtual std::unordered_map< std::string, std::string > get_metadata() const
Get network metadata for debugging/visualization.
std::vector< double > m_last_audio_buffer
void set_output_mode(OutputMode mode)
Set the network's output routing mode.
Kinesis::Stochastic::Stochastic m_random_generator
void set_loop_filter_closed(const std::shared_ptr< Filters::Filter > &filter)
Set filter for the closed-end termination (mouthpiece/nut)
void set_exciter_type(ExciterType type)
Set exciter type.
void set_loop_filter_open(const std::shared_ptr< Filters::Filter > &filter)
Set filter for the open-end termination (bell/bridge)
void strike(double position=0.5, double strength=1.0)
Strike the string/tube with an impulse.
std::unordered_map< std::string, std::string > get_metadata() const override
Get network metadata for debugging/visualization.
void process_unidirectional(WaveguideSegment &seg, unsigned int num_samples, std::vector< double > &out)
WaveguideType
Physical structure being modeled.
@ TUBE
Cylindrical bore (future: clarinet, flute)
void process_bidirectional(WaveguideSegment &seg, unsigned int num_samples, std::vector< double > &out)
void process_batch(unsigned int num_samples) override
Process the network for the given number of samples.
void pluck(double position=0.5, double strength=1.0)
Pluck the string at a normalized position.
void set_loop_filter(const std::shared_ptr< Filters::Filter > &filter)
Replace the loop filter.
double observe_sample(const WaveguideSegment &seg) const
void map_parameter(const std::string &param_name, const std::shared_ptr< Node > &source, MappingMode mode=MappingMode::BROADCAST) override
Map external node output to network parameter.
void set_loss_factor(double loss)
Set per-sample energy loss factor.
void set_fundamental(double freq)
Set fundamental frequency.
void unmap_parameter(const std::string &param_name) override
Remove parameter mapping.
void reset() override
Reset network to initial state.
double get_pickup_position() const
Get current pickup position.
void set_exciter_sample(const std::vector< double > &sample)
Set custom excitation waveform.
std::shared_ptr< Filters::Filter > m_exciter_filter
WaveguideNetwork(WaveguideType type, double fundamental_freq, double sample_rate=48000.0)
Create waveguide network with specified type and frequency.
double read_with_interpolation(const Memory::HistoryBuffer< double > &delay, size_t integer_part, double fraction) const
Read from delay line with linear fractional interpolation.
ExciterType
Excitation signal types for waveguide synthesis.
@ CONTINUOUS
External node as continuous exciter (bowing)
@ NOISE_BURST
Short white noise burst (default for pluck)
@ SAMPLE
User-provided excitation waveform.
std::optional< double > get_node_output(size_t index) const override
Get output of specific internal node (for ONE_TO_ONE mapping)
@ PRESSURE
Output is physical pressure at pickup (p_plus + p_minus)
double get_loss_factor() const
Get current loss factor.
void set_exciter_duration(double seconds)
Set noise burst duration for exciter.
void apply_broadcast_parameter(const std::string &param, double value)
void set_pickup_position(double position)
Set pickup position along the string.
void initialize() override
Called once before first process_batch()
void apply_one_to_one_parameter(const std::string &param, const std::shared_ptr< NodeNetwork > &source)
std::vector< WaveguideSegment > m_segments
std::optional< std::span< const double > > get_node_audio_buffer(size_t index) const override
Get output of specific internal node as audio buffer (for ONE_TO_ONE mapping)
@ RING
Circular: last node connects to first.
MappingMode
Defines how nodes map to external entities (e.g., audio channels, graphics objects)
@ ONE_TO_ONE
Node array/network → network nodes (must match count)
@ BROADCAST
One node → all network nodes.
@ AUDIO_SINK
Aggregated audio samples sent to output.
constexpr std::string_view enum_to_string(EnumType value) noexcept
Universal enum to string converter using magic_enum (original case)
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)
double reflection_open
Reflection coefficient at open end (pressure antinode)
std::shared_ptr< Filters::Filter > loop_filter_closed
BIDIRECTIONAL: closed-end filter (mouthpiece/nut)
std::shared_ptr< Filters::Filter > loop_filter
UNIDIRECTIONAL: single termination filter.
Memory::HistoryBuffer< double > p_plus
Forward-traveling wave rail.
double reflection_closed
Reflection coefficient at closed end (pressure node)
1D delay-line segment supporting both uni- and bidirectional propagation