MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
WaveguideNetwork.cpp
Go to the documentation of this file.
2
6
8
9namespace {
10
11 constexpr double k_continuous_ceiling = 4.0;
12
13 /**
14 * @brief Largest magnitude currently held on either rail
15 */
16 double rail_peak(const WaveguideNetwork::WaveguideSegment& seg)
17 {
18 double level = 0.0;
19 for (size_t i = 0; i < seg.p_plus.capacity(); ++i) {
20 level = std::max({ level, std::abs(seg.p_plus[i]), std::abs(seg.p_minus[i]) });
21 }
22 return level;
23 }
24
25 /**
26 * @brief Uniform scale for a continuous source that backs off as the loop fills
27 *
28 * The gain is re-evaluated once per interval, from the level of the whole
29 * loop rather than the sample at hand, and applied to the drive only. It is
30 * 1.0 while the loop sits below half the ceiling and falls linearly to 0.0 at
31 * the ceiling. Because every sample of an interval is scaled alike and the
32 * circulating wave is never clipped, the waveform keeps its shape and a
33 * source feeding a resonance settles instead of growing without bound.
34 */
35 class DriveLimiter {
36 public:
37 explicit DriveLimiter(size_t interval)
38 : m_interval(std::max<size_t>(interval, 1))
39 {
40 }
41
42 double gain(const WaveguideNetwork::WaveguideSegment& seg)
43 {
44 if (m_until == 0) {
45 const double knee = 0.5 * k_continuous_ceiling;
46 const double over = rail_peak(seg) - knee;
47 m_gain = over <= 0.0 ? 1.0 : std::max(0.0, 1.0 - over / knee);
49 }
50 --m_until;
51 return m_gain;
52 }
53
54 private:
55 size_t m_interval;
56 size_t m_until { 0 };
57 double m_gain { 1.0 };
58 };
59
60}
61
62//-----------------------------------------------------------------------------
63// Construction
64//-----------------------------------------------------------------------------
65
68 double fundamental_freq,
69 double sample_rate)
70 : m_type(type)
71 , m_fundamental(fundamental_freq)
72{
73 m_sample_rate = static_cast<uint32_t>(sample_rate);
76
78
79 const auto prop_mode = (m_type == WaveguideType::TUBE)
82
83 m_segments.emplace_back(m_delay_length_integer + 2, prop_mode);
85 m_segments[0].reflection_open = -1.0;
86 }
87
89}
90
91//-----------------------------------------------------------------------------
92// Initialization
93//-----------------------------------------------------------------------------
94
99
101{
102 for (auto& seg : m_segments) {
103 const auto len = seg.p_plus.capacity();
104 seg.p_plus = Memory::HistoryBuffer<double>(len);
105 seg.p_minus = Memory::HistoryBuffer<double>(len);
106 if (seg.loop_filter)
107 seg.loop_filter->reset();
108 if (seg.loop_filter_open)
109 seg.loop_filter_open->reset();
110 if (seg.loop_filter_closed)
111 seg.loop_filter_closed->reset();
112 }
113
115 m_exciter_filter->reset();
116 m_exciter_active = false;
120 m_last_output = 0.0;
121 m_last_audio_buffer.clear();
122}
123
124//-----------------------------------------------------------------------------
125// Delay Length Computation
126//-----------------------------------------------------------------------------
127
129{
130 const double passes = (m_type == WaveguideType::TUBE) ? 4.0 : 1.0;
131 double total_delay = static_cast<double>(m_sample_rate) / (passes * m_fundamental);
132
133 total_delay -= 0.5;
134
135 m_delay_length_integer = static_cast<size_t>(total_delay);
136 m_delay_length_fraction = total_delay - static_cast<double>(m_delay_length_integer);
137
138 m_pickup_sample = static_cast<size_t>(m_pickup_position * static_cast<double>(m_delay_length_integer));
139}
140
141//-----------------------------------------------------------------------------
142// Default Loop Filter
143//-----------------------------------------------------------------------------
144
146{
147 auto filter = std::make_shared<Filters::FIR>(
148 std::vector<double> { 0.5, 0.5 });
149
150 if (!m_segments.empty()) {
151 m_segments[0].loop_filter = filter;
152 }
153}
154
155//-----------------------------------------------------------------------------
156// Fractional Delay Interpolation
157//-----------------------------------------------------------------------------
158
161 size_t integer_part,
162 double fraction) const
163{
164 double s0 = delay[integer_part];
165 double s1 = delay[integer_part + 1];
166 return s0 + fraction * (s1 - s0);
167}
168
169//-----------------------------------------------------------------------------
170// Processing
171//-----------------------------------------------------------------------------
172
174{
176 return seg.p_plus[m_pickup_sample];
177 }
178
179 const double plus = seg.p_plus[m_pickup_sample];
180 const double minus = seg.p_minus[m_pickup_sample];
181
183 return plus + minus;
184
185 return plus - minus;
186}
187
188void WaveguideNetwork::process_batch(unsigned int num_samples)
189{
191
192 if (!is_enabled() || m_segments.empty()) {
193 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
194 std::this_thread::yield();
195
196 m_last_audio_buffer.assign(num_samples, 0.0);
197 m_last_output = 0.0;
198 m_audio_buffer_lock.clear(std::memory_order_release);
199 return;
200 }
201
203
204 thread_local std::vector<double> scratch;
205 scratch.assign(num_samples, 0.0);
206
207 auto& seg = m_segments[0];
208
211 m_exciter_node_buffer_pos, num_samples);
212 }
213
215 process_unidirectional(seg, num_samples, scratch);
216 } else {
217 process_bidirectional(seg, num_samples, scratch);
218 }
219
220 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
221 std::this_thread::yield();
222
223 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
226 m_audio_buffer_lock.clear(std::memory_order_release);
227}
228
230 unsigned int num_samples, std::vector<double>& out)
231{
232 const bool continuous = m_exciter_active
234 const double continuous_gain = continuous
235 ? 1.0 / seg.loss_factor
236 : 1.0;
237 DriveLimiter limiter(m_delay_length_integer);
238
239 for (unsigned int i = 0; i < num_samples; ++i) {
240 const double exciter = generate_exciter_sample();
241
242 const double delayed = read_with_interpolation(
244
245 const double filtered = (!continuous && seg.loop_filter)
246 ? seg.loop_filter->process_sample(delayed)
247 : delayed;
248
249 const double returning = filtered * seg.loss_factor * seg.reflection_closed * continuous_gain;
250 const double drive = continuous ? exciter * limiter.gain(seg) : exciter;
251
252 seg.p_plus.push(drive + returning);
253
254 out[i] = observe_sample(seg);
255 }
256}
257
259 unsigned int num_samples, std::vector<double>& out)
260{
261 const bool continuous = m_exciter_active
263 const double continuous_gain = continuous
264 ? 1.0 / seg.loss_factor
265 : 1.0;
266 DriveLimiter limiter(m_delay_length_integer);
267
268 for (unsigned int i = 0; i < num_samples; ++i) {
269 const double exciter = generate_exciter_sample();
270
271 const double plus_end = read_with_interpolation(
273
274 const double minus_end = read_with_interpolation(
276
277 auto* filt_open = seg.loop_filter_open ? seg.loop_filter_open.get()
278 : seg.loop_filter.get();
279 auto* filt_closed = seg.loop_filter_closed ? seg.loop_filter_closed.get()
280 : seg.loop_filter.get();
281
282 const double filtered_plus = (!continuous && filt_open)
283 ? filt_open->process_sample(plus_end)
284 : plus_end;
285 const double filtered_minus = (!continuous && filt_closed)
286 ? filt_closed->process_sample(minus_end)
287 : minus_end;
288
289 const double returning = filtered_minus * seg.loss_factor * seg.reflection_closed * continuous_gain;
290 const double drive = continuous ? exciter * limiter.gain(seg) : exciter;
291
292 seg.p_minus.push(filtered_plus * seg.loss_factor * seg.reflection_open * continuous_gain);
293 seg.p_plus.push(drive + returning);
294
295 out[i] = observe_sample(seg);
296 }
297}
298
299//-----------------------------------------------------------------------------
300// Excitation
301//-----------------------------------------------------------------------------
302
303void WaveguideNetwork::pluck(double position, double strength)
304{
305 position = std::clamp(position, 0.01, 0.99);
306
307 if (m_segments.empty())
308 return;
309
310 auto& seg = m_segments[0];
311 const size_t len = m_delay_length_integer;
312 if (len < 2)
313 return;
314
315 const auto pluck_sample = std::clamp(
316 static_cast<size_t>(position * static_cast<double>(len)), size_t { 1 }, len - 1);
317 const double effective_strength = (m_exciter_type == ExciterType::CONTINUOUS)
318 ? std::max(strength, 1.0)
319 : strength;
320
321 seg.p_plus = Memory::HistoryBuffer<double>(seg.p_plus.capacity());
322 seg.p_minus = Memory::HistoryBuffer<double>(seg.p_minus.capacity());
323
324 std::vector<double> shape(len);
325 for (size_t s = 0; s < len; ++s) {
326 if (s <= pluck_sample) {
327 shape[s] = effective_strength * static_cast<double>(s)
328 / static_cast<double>(pluck_sample);
329 } else {
330 shape[s] = effective_strength * static_cast<double>(len - s)
331 / static_cast<double>(len - pluck_sample);
332 }
333 }
334
335 double offset = 0.0;
337 offset = std::accumulate(shape.begin(), shape.end(), 0.0) / static_cast<double>(len);
338 }
339
340 for (const double value : shape) {
341 seg.p_plus.push(value - offset);
342 }
343
345 m_exciter_active = false;
347 }
348}
349
350void WaveguideNetwork::strike(double position, double strength)
351{
352 position = std::clamp(position, 0.01, 0.99);
353
356 m_exciter_duration = 0.002;
358 }
359
360 if (m_segments.empty())
361 return;
362
363 auto& seg = m_segments[0];
364 const size_t len = m_delay_length_integer;
365 const auto strike_center = static_cast<size_t>(position * static_cast<double>(len));
366 const size_t burst_width = std::max<size_t>(len / 10, 4);
367 const double effective_strength = (m_exciter_type == ExciterType::CONTINUOUS)
368 ? std::max(strength, 1.0)
369 : strength;
370
371 seg.p_plus = Memory::HistoryBuffer<double>(seg.p_plus.capacity());
372 seg.p_minus = Memory::HistoryBuffer<double>(seg.p_minus.capacity());
373
374 for (size_t s = 0; s < len; ++s) {
375 const double dist = std::abs(static_cast<double>(s)
376 - static_cast<double>(strike_center));
377 const double window = std::exp(-(dist * dist)
378 / (2.0 * static_cast<double>(burst_width * burst_width)));
379 seg.p_plus.push(effective_strength * m_random_generator(-1.0, 1.0) * window);
380 }
381
383 m_exciter_active = false;
385 }
386}
387
389{
393 ? std::numeric_limits<size_t>::max()
394 : 0;
395}
396
402
404{
405 m_exciter_duration = std::max(0.001, seconds);
406}
407
408void WaveguideNetwork::set_exciter_sample(const std::vector<double>& sample)
409{
410 m_exciter_sample = sample;
411}
412
414{
415 m_exciter_active = true;
417
418 switch (m_exciter_type) {
421 break;
422
425 m_exciter_samples_remaining = static_cast<size_t>(
426 m_exciter_duration * static_cast<double>(m_sample_rate));
427 break;
428
431 break;
432
434 m_exciter_samples_remaining = std::numeric_limits<size_t>::max();
435 break;
436 }
437}
438
440{
442 m_exciter_active = false;
443 return 0.0;
444 }
445
447 double sample = 0.0;
448
449 switch (m_exciter_type) {
451 sample = 1.0;
452 break;
453
455 sample = m_random_generator(-1.0, 1.0);
456 break;
457
459 double noise = m_random_generator(-1.0, 1.0);
460 sample = m_exciter_filter ? m_exciter_filter->process_sample(noise) : noise;
461 break;
462 }
463
467 }
468 break;
469
473 }
474 break;
475 }
476
477 return sample;
478}
479
480//-----------------------------------------------------------------------------
481// Waveguide Control
482//-----------------------------------------------------------------------------
483
485{
486 m_fundamental = std::max(20.0, freq);
488
489 if (!m_segments.empty()) {
490 const size_t required = m_delay_length_integer + 2;
491 auto& seg = m_segments[0];
492 if (seg.p_plus.capacity() < required) {
493 seg.p_plus = Memory::HistoryBuffer<double>(required);
494 seg.p_minus = Memory::HistoryBuffer<double>(required);
495 }
496 }
497}
498
500{
501 loss = std::clamp(loss, 0.0, 1.0);
502 for (auto& seg : m_segments) {
503 seg.loss_factor = loss;
504 }
505}
506
508{
509 return m_segments.empty() ? 0.996 : m_segments[0].loss_factor;
510}
511
512void WaveguideNetwork::set_loop_filter(const std::shared_ptr<Filters::Filter>& filter)
513{
514 if (!m_segments.empty()) {
515 m_segments[0].loop_filter = filter;
516 }
517}
518
520{
521 m_pickup_position = std::clamp(position, 0.0, 1.0);
522 m_pickup_sample = static_cast<size_t>(m_pickup_position * static_cast<double>(m_delay_length_integer));
523 m_pickup_sample = std::clamp(m_pickup_sample, size_t { 0 }, m_delay_length_integer);
524}
525
527{
528 if (m_delay_length_integer == 0) {
529 return 0.5;
530 }
531 return static_cast<double>(m_pickup_sample) / static_cast<double>(m_delay_length_integer);
532}
533
534void WaveguideNetwork::set_loop_filter_open(const std::shared_ptr<Filters::Filter>& filter)
535{
536 if (!m_segments.empty()) {
537 m_segments[0].loop_filter_open = filter;
538 }
539}
540
541void WaveguideNetwork::set_loop_filter_closed(const std::shared_ptr<Filters::Filter>& filter)
542{
543 if (!m_segments.empty()) {
544 m_segments[0].loop_filter_closed = filter;
545 }
546}
547
548//-----------------------------------------------------------------------------
549// Parameter Mapping
550//-----------------------------------------------------------------------------
551
553{
554 for (const auto& mapping : m_parameter_mappings) {
555 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
556 double value = mapping.broadcast_source->get_last_output();
557 apply_broadcast_parameter(mapping.param_name, value);
558 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
559 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
560 }
561 }
562}
563
564void WaveguideNetwork::apply_broadcast_parameter(const std::string& param, double value)
565{
566 if (param == "frequency") {
568 } else if (param == "damping" || param == "loss") {
570 } else if (param == "position") {
572 } else if (param == "scale") {
573 m_output_scale = std::max(0.0, value);
574 }
575}
576
578 const std::string& /*param*/,
579 const std::shared_ptr<NodeNetwork>& /*source*/)
580{
581}
582
584 const std::string& param_name,
585 const std::shared_ptr<Node>& source,
587{
588 unmap_parameter(param_name);
589
590 ParameterMapping mapping;
591 mapping.param_name = param_name;
592 mapping.mode = mode;
593 mapping.broadcast_source = source;
594 mapping.network_source = nullptr;
595
596 m_parameter_mappings.push_back(std::move(mapping));
597}
598
600 const std::string& param_name,
601 const std::shared_ptr<NodeNetwork>& source_network)
602{
603 unmap_parameter(param_name);
604
605 ParameterMapping mapping;
606 mapping.param_name = param_name;
608 mapping.broadcast_source = nullptr;
609 mapping.network_source = source_network;
610
611 m_parameter_mappings.push_back(std::move(mapping));
612}
613
614void WaveguideNetwork::unmap_parameter(const std::string& param_name)
615{
616 std::erase_if(m_parameter_mappings,
617 [&](const auto& m) { return m.param_name == param_name; });
618}
619
620//-----------------------------------------------------------------------------
621// Metadata
622//-----------------------------------------------------------------------------
623
624std::unordered_map<std::string, std::string>
626{
628
629 metadata["type"] = std::string(Reflect::enum_to_string(m_type));
630 metadata["fundamental"] = std::to_string(m_fundamental) + " Hz";
631 metadata["delay_length"] = std::to_string(m_delay_length_integer)
632 + " + " + std::to_string(m_delay_length_fraction) + " samples";
633 metadata["loss_factor"] = std::to_string(get_loss_factor());
634 metadata["pickup_position"] = std::to_string(get_pickup_position());
635
636 metadata["exciter_type"] = std::string(Reflect::enum_to_string(m_exciter_type));
637
638 return metadata;
639}
640
641std::optional<double> WaveguideNetwork::get_node_output(size_t index) const
642{
643 if (index < m_segments.size() && !m_last_audio_buffer.empty()) {
644 return m_last_output;
645 }
646 return std::nullopt;
647}
648
649std::optional<std::span<const double>> WaveguideNetwork::get_node_audio_buffer(size_t index) const
650{
651 if (index != 0 || m_last_audio_buffer.empty())
652 return std::nullopt;
653
654 return std::span<const double>(m_last_audio_buffer);
655}
656
657} // namespace MayaFlux::Nodes::Network
uint64_t offset
Definition Composite.cpp:10
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
std::weak_ptr< Core::Window > window
Definition Picker.cpp:23
uint32_t mode
std::unordered_map< std::string, std::string > metadata
glm::ivec3 max
std::vector< float > * out
glm::vec3 value
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
uint32_t index
Definition VKDevice.cpp:122
float gain
size_t m_until
size_t m_interval
double m_gain
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.
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.
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.
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
Open-end reflection; a tube configures this to -1.
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
Closed-end reflection; a lumped string loop keeps +1 because its two inversions cancel.
1D delay-line segment supporting both uni- and bidirectional propagation