MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ModalNetwork.cpp
Go to the documentation of this file.
1#include "ModalNetwork.hpp"
2
7
9
10//-----------------------------------------------------------------------------
11// Construction
12//-----------------------------------------------------------------------------
13
14ModalNetwork::ModalNetwork(size_t num_modes, double fundamental,
15 Spectrum spectrum, double base_decay)
16 : m_spectrum(spectrum)
17 , m_fundamental(fundamental)
18{
21
22 auto ratios = generate_spectrum_ratios(spectrum, num_modes);
23 initialize_modes(ratios, base_decay);
24}
25
26ModalNetwork::ModalNetwork(const std::vector<double>& frequency_ratios,
27 double fundamental, double base_decay)
28 : m_spectrum(Spectrum::CUSTOM)
29 , m_fundamental(fundamental)
30{
33
34 initialize_modes(frequency_ratios, base_decay);
35}
36
37//-----------------------------------------------------------------------------
38// Spectrum Generation
39//-----------------------------------------------------------------------------
40
42 size_t count)
43{
44 std::vector<double> ratios;
45 ratios.reserve(count);
46
47 switch (spectrum) {
49 // Perfect integer harmonics: 1, 2, 3, 4...
50 for (size_t i = 0; i < count; ++i) {
51 ratios.push_back(static_cast<double>(i + 1));
52 }
53 break;
54
56 // Free-free bar mode ratios, tabulated for the first eight, then constant spacing
57 ratios = { 1.0, 2.756, 5.404, 8.933, 13.344, 18.64, 24.81, 31.86 };
58 while (ratios.size() < count) {
59 double last = ratios.back();
60 ratios.push_back(last + 6.8);
61 }
62 ratios.resize(count);
63 break;
64
66 // f_n = n * f_0 * sqrt(1 + B * n^2)
67 // Using small B = 0.0001 for moderate stretching
68 {
69 constexpr double B = 0.0001;
70 for (size_t n = 1; n <= count; ++n) {
71 auto dn = static_cast<double>(n);
72 double ratio = dn * std::sqrt(1.0 + B * dn * dn);
73 ratios.push_back(ratio);
74 }
75 }
76 break;
77
79 for (size_t i = 0; i < count; ++i) {
80 ratios.push_back(static_cast<double>(i + 1));
81 }
82 break;
83 }
84
85 return ratios;
86}
87
88//-----------------------------------------------------------------------------
89// Mode Initialization
90//-----------------------------------------------------------------------------
91
92void ModalNetwork::initialize_modes(const std::vector<double>& ratios,
93 double base_decay)
94{
95 m_base_decay = base_decay;
96 m_modes.clear();
97 m_modes.reserve(ratios.size());
98
99 for (size_t i = 0; i < ratios.size(); ++i) {
101 mode.index = i;
102 mode.frequency_ratio = ratios[i];
103 mode.base_frequency = m_fundamental * ratios[i];
104 mode.current_frequency = mode.base_frequency;
105
106 mode.decay_time = base_decay / ratios[i];
107
108 mode.initial_amplitude = 1.0 / static_cast<double>(i + 1);
109 mode.amplitude = 0.0;
110
111 mode.oscillator = std::make_shared<Generator::Sine>(static_cast<float>(mode.current_frequency));
112 mode.oscillator->set_in_network(true);
113
114 mode.decay_coefficient = Kinesis::Discrete::decay_per_sample(base_decay, m_sample_rate);
115
116 m_modes.push_back(std::move(mode));
117 }
118}
119
121{
122 const double coefficient = Kinesis::Discrete::decay_per_sample(
124
125 for (auto& mode : m_modes) {
126 mode.decay_coefficient = coefficient;
127 }
128}
129
131{
132 multiplier = std::max(0.01, multiplier);
133 if (multiplier == m_decay_multiplier) {
134 return;
135 }
136
137 m_decay_multiplier = multiplier;
139}
140
142{
143 for (auto& mode : m_modes) {
144 mode.amplitude = 0.0;
145 mode.phase = 0.0;
146 mode.current_frequency = mode.base_frequency;
147 }
148}
149
150//-----------------------------------------------------------------------------
151// Exciter System
152//-----------------------------------------------------------------------------
153
155{
156 m_exciter_duration = std::max(0.001, seconds);
157}
158
160{
162 m_exciter_active = false;
164 for (auto& mode : m_modes)
165 mode.amplitude = 0.0;
166}
167
168void ModalNetwork::set_exciter_sample(const std::vector<double>& sample)
169{
170 m_exciter_sample = sample;
171}
172
174{
175 m_exciter_active = true;
176 m_exciter_strength = strength;
178
179 switch (m_exciter_type) {
182 break;
183
186 m_exciter_samples_remaining = static_cast<size_t>(
188 break;
189
192 break;
193
195 m_exciter_samples_remaining = std::numeric_limits<size_t>::max();
196 break;
197 }
198}
199
201{
203 m_exciter_active = false;
204 return 0.0;
205 }
206
207 const size_t idx = m_exciter_node_buffer_pos < m_exciter_node_buffer.size()
209 : 0;
211 double sample = 0.0;
212
213 switch (m_exciter_type) {
215 sample = 1.0;
216 break;
217
219 sample = m_random_generator(-1.0, 1.0);
220 break;
221
223 double noise = m_random_generator(-1.0, 1.0);
224 sample = m_exciter_filter
225 ? m_exciter_filter->process_sample(noise)
226 : noise;
227 break;
228 }
229
233 break;
234
236 if (m_exciter_node)
237 sample = m_exciter_node_buffer[idx];
238 break;
239 }
240
241 return sample;
242}
243
249
250//-----------------------------------------------------------------------------
251// Spatial Excitation
252//-----------------------------------------------------------------------------
253
254void ModalNetwork::excite_at_position(double position, double strength)
255{
256 position = std::clamp(position, 0.0, 1.0);
257
258 if (m_spatial_distribution.empty()) {
260 }
261
262 initialize_exciter(strength);
263
264 for (size_t i = 0; i < m_modes.size(); ++i) {
265 double spatial_amp = std::sin((i + 1) * M_PI * position);
266 spatial_amp = std::abs(spatial_amp);
267
268 m_modes[i].amplitude = m_modes[i].initial_amplitude * strength * spatial_amp;
269 }
270}
271
272void ModalNetwork::set_spatial_distribution(const std::vector<double>& distribution)
273{
274 if (distribution.size() != m_modes.size()) {
275 return;
276 }
277 m_spatial_distribution = distribution;
278}
279
281{
283 m_spatial_distribution.reserve(m_modes.size());
284
285 for (size_t i = 0; i < m_modes.size(); ++i) {
286 m_spatial_distribution.push_back(1.0);
287 }
288}
289
290//-----------------------------------------------------------------------------
291// Modal Coupling
292//-----------------------------------------------------------------------------
293
294void ModalNetwork::set_mode_coupling(size_t mode_a, size_t mode_b, double strength)
295{
296 if (mode_a >= m_modes.size() || mode_b >= m_modes.size() || mode_a == mode_b) {
297 return;
298 }
299
300 strength = std::clamp(strength, 0.0, 1.0);
301
302 remove_mode_coupling(mode_a, mode_b);
303
304 m_couplings.push_back({ .mode_a = mode_a,
305 .mode_b = mode_b,
306 .strength = strength });
307}
308
309void ModalNetwork::remove_mode_coupling(size_t mode_a, size_t mode_b)
310{
311 std::erase_if(m_couplings,
312 [mode_a, mode_b](const ModeCoupling& c) {
313 return (c.mode_a == mode_a && c.mode_b == mode_b)
314 || (c.mode_a == mode_b && c.mode_b == mode_a);
315 });
316}
317
319{
320 for (const auto& coupling : m_couplings) {
321 auto& mode_a = m_modes[coupling.mode_a];
322 auto& mode_b = m_modes[coupling.mode_b];
323
324 double energy_diff = (mode_a.amplitude - mode_b.amplitude) * coupling.strength;
325
326 mode_a.amplitude -= energy_diff * 0.5;
327 mode_b.amplitude += energy_diff * 0.5;
328 }
329}
330
331//-----------------------------------------------------------------------------
332// Processing
333//-----------------------------------------------------------------------------
334
335void ModalNetwork::process_batch(unsigned int num_samples)
336{
338
339 if (!is_enabled()) {
340 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
341 std::this_thread::yield();
342
343 m_last_audio_buffer.assign(num_samples, 0.0);
344 m_audio_buffer_lock.clear(std::memory_order_release);
345 return;
346 }
347
348 thread_local std::vector<double> scratch;
349 scratch.assign(num_samples, 0.0);
350
352
353 m_node_buffers.assign(m_modes.size(), {});
354 for (auto& nb : m_node_buffers)
355 nb.reserve(num_samples);
356
357 if (m_exciter_node) {
359 m_exciter_node_buffer_pos, num_samples);
360
363 if (!peaks.empty()) {
364 const double peak_scale = 1.0 / static_cast<double>(peaks.size());
365 for (size_t peak_idx : peaks) {
366 const double strength = std::abs(m_exciter_node_buffer[peak_idx])
367 * m_exciter_strength * peak_scale;
368 for (auto& mode : m_modes) {
369 const double target = mode.initial_amplitude * strength;
370 mode.amplitude = std::max(mode.amplitude, target);
371 }
372 }
373 }
376 if (m_exciter_active) {
378 if (!peaks.empty()) {
379 const auto max_peak = *std::ranges::max_element(peaks,
380 [this](size_t a, size_t b) {
381 return std::abs(m_exciter_node_buffer[a]) < std::abs(m_exciter_node_buffer[b]);
382 });
383 const double strength = std::abs(m_exciter_node_buffer[max_peak]) * m_exciter_strength;
384 for (auto& mode : m_modes) {
385 const double target = mode.initial_amplitude * strength;
386 mode.amplitude = std::max<double>(mode.amplitude, target);
387 }
388 }
389 }
390 }
391 }
392
393 for (size_t i = 0; i < num_samples; ++i) {
394 double exciter_signal = generate_exciter_sample();
395
396 if (exciter_signal != 0.0) {
398 for (auto& mode : m_modes) {
399 mode.amplitude += exciter_signal * mode.initial_amplitude
401 }
402 }
403 }
404
405 if (m_coupling_enabled && !m_couplings.empty())
407
408 for (size_t m = 0; m < m_modes.size(); ++m) {
409 auto& mode = m_modes[m];
410
411 if (mode.amplitude > 0.0001) {
412 mode.amplitude *= mode.decay_coefficient;
413 } else {
414 mode.amplitude = 0.0;
415 }
416
417 double sample = mode.oscillator->process_sample(0.0) * mode.amplitude;
418 m_node_buffers[m].push_back(sample);
419 scratch[i] += sample;
420 }
421 }
422
423 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
424 std::this_thread::yield();
425
426 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
428 m_audio_buffer_lock.clear(std::memory_order_release);
429}
430
431//-----------------------------------------------------------------------------
432// Parameter Mapping
433//-----------------------------------------------------------------------------
434
436{
437 for (const auto& mapping : m_parameter_mappings) {
438 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
439 double value = mapping.broadcast_source->get_last_output();
440 apply_broadcast_parameter(mapping.param_name, value);
441 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
442 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
443 }
444 }
445}
446
447void ModalNetwork::apply_broadcast_parameter(const std::string& param,
448 double value)
449{
450 if (param == "frequency") {
452 } else if (param == "decay") {
454 } else if (param == "amplitude") {
455 for (auto& mode : m_modes) {
456 mode.amplitude *= value;
457 }
458 } else if (param == "scale") {
459 m_output_scale = std::max(0.0, value);
460 }
461}
462
464 const std::string& param, const std::shared_ptr<NodeNetwork>& source)
465{
466 if (source->get_node_count() != m_modes.size()) {
467 return;
468 }
469
470 if (param == "amplitude") {
471 for (size_t i = 0; i < m_modes.size(); ++i) {
472 auto val = source->get_node_output(i);
473 if (val) {
474 m_modes[i].amplitude *= *val;
475 }
476 }
477 } else if (param == "detune") {
478 for (size_t i = 0; i < m_modes.size(); ++i) {
479 auto val = source->get_node_output(i);
480 if (val) {
481 double detune_cents = *val * 100.0; // ±100 cents
482 double ratio = std::pow(2.0, detune_cents / 1200.0);
483 m_modes[i].current_frequency = m_modes[i].base_frequency * ratio;
484 m_modes[i].oscillator->set_frequency(m_modes[i].current_frequency);
485 }
486 }
487 }
488}
489
490void ModalNetwork::map_parameter(const std::string& param_name,
491 const std::shared_ptr<Node>& source,
493{
494 unmap_parameter(param_name);
495
496 ParameterMapping mapping;
497 mapping.param_name = param_name;
498 mapping.mode = mode;
499 mapping.broadcast_source = source;
500 mapping.network_source = nullptr;
501
502 m_parameter_mappings.push_back(std::move(mapping));
503}
504
506 const std::string& param_name,
507 const std::shared_ptr<NodeNetwork>& source_network)
508{
509 unmap_parameter(param_name);
510
511 ParameterMapping mapping;
512 mapping.param_name = param_name;
514 mapping.broadcast_source = nullptr;
515 mapping.network_source = source_network;
516
517 m_parameter_mappings.push_back(std::move(mapping));
518}
519
520void ModalNetwork::unmap_parameter(const std::string& param_name)
521{
522 std::erase_if(m_parameter_mappings,
523 [&](const auto& m) { return m.param_name == param_name; });
524}
525
526//-----------------------------------------------------------------------------
527// Modal Control
528//-----------------------------------------------------------------------------
529
530void ModalNetwork::excite(double strength)
531{
532 initialize_exciter(strength);
533
534 for (auto& mode : m_modes) {
535 mode.amplitude = mode.initial_amplitude * strength;
536 }
537}
538
539void ModalNetwork::excite_mode(size_t mode_index, double strength)
540{
541 if (mode_index < m_modes.size()) {
542 m_modes[mode_index].amplitude = m_modes[mode_index].initial_amplitude * strength;
543 }
544}
545
546void ModalNetwork::damp(double damping_factor)
547{
548 for (auto& mode : m_modes) {
549 mode.amplitude *= damping_factor;
550 }
551}
552
554{
556
557 for (auto& mode : m_modes) {
558 mode.base_frequency = m_fundamental * mode.frequency_ratio;
559 mode.current_frequency = mode.base_frequency;
560
561 mode.oscillator->set_frequency(mode.current_frequency);
562 }
563}
564
565//-----------------------------------------------------------------------------
566// Metadata
567//-----------------------------------------------------------------------------
568
569std::unordered_map<std::string, std::string>
571{
573
574 metadata["fundamental"] = std::to_string(m_fundamental) + " Hz";
575 metadata["spectrum"] = [this]() {
576 switch (m_spectrum) {
578 return "HARMONIC";
580 return "INHARMONIC";
582 return "STRETCHED";
583 case Spectrum::CUSTOM:
584 return "CUSTOM";
585 default:
586 return "UNKNOWN";
587 }
588 }();
589 metadata["decay_multiplier"] = std::to_string(m_decay_multiplier);
590
591 double avg_amplitude = 0.0;
592 for (const auto& mode : m_modes) {
593 avg_amplitude += mode.amplitude;
594 }
595 avg_amplitude /= static_cast<double>(m_modes.size());
596 metadata["avg_amplitude"] = std::to_string(avg_amplitude);
597
598 metadata["exciter_type"] = [this]() {
599 switch (m_exciter_type) {
601 return "IMPULSE";
603 return "NOISE_BURST";
605 return "FILTERED_NOISE";
607 return "SAMPLE";
609 return "CONTINUOUS";
610 default:
611 return "UNKNOWN";
612 }
613 }();
614
615 metadata["coupling_enabled"] = m_coupling_enabled ? "true" : "false";
616 metadata["coupling_count"] = std::to_string(m_couplings.size());
617
618 return metadata;
619}
620
621[[nodiscard]] std::optional<double> ModalNetwork::get_node_output(size_t index) const
622{
623 if (m_modes.size() > index) {
624 return m_modes[index].oscillator->get_last_output();
625 }
626 return std::nullopt;
627}
628
629std::optional<std::span<const double>> ModalNetwork::get_node_audio_buffer(size_t index) const
630{
631 if (index >= m_node_buffers.size() || m_node_buffers[index].empty())
632 return std::nullopt;
633 return std::span<const double>(m_node_buffers[index]);
634}
635
636} // namespace MayaFlux::Nodes::Network
Discrete sequence analysis primitives for MayaFlux::Kinesis.
Digital filter coefficient design for MayaFlux::Kinesis.
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
uint32_t mode
size_t a
size_t b
double frequency
std::unordered_map< std::string, std::string > metadata
glm::vec3 value
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
uint32_t index
Definition VKDevice.cpp:122
size_t count
std::shared_ptr< Filters::Filter > m_exciter_filter
void compute_spatial_distribution()
Compute spatial amplitude distribution.
std::optional< double > get_node_output(size_t index) const override
Get output of specific internal node (for ONE_TO_ONE mapping)
std::vector< std::vector< double > > m_node_buffers
Per-mode sample buffers populated each process_batch()
ExciterType
Excitation signal types for modal synthesis.
@ IMPULSE
Single-sample Dirac impulse (default)
@ FILTERED_NOISE
Spectrally-shaped noise burst.
@ CONTINUOUS
External node as continuous exciter.
@ SAMPLE
User-provided excitation waveform.
void process_batch(unsigned int num_samples) override
Process the network for the given number of samples.
void refresh_decay()
Recompute every mode's per-sample decay from the base decay and multiplier.
void apply_one_to_one_parameter(const std::string &param, const std::shared_ptr< NodeNetwork > &source)
Apply one-to-one parameter from another network.
void release()
Release continuous excitation and allow modes to decay naturally.
void set_decay_multiplier(double multiplier)
Set global decay multiplier.
Kinesis::Stochastic::Stochastic m_random_generator
void excite_mode(size_t mode_index, double strength=1.0)
Excite specific mode.
void excite(double strength=1.0)
Excite all modes (strike/pluck)
void initialize_modes(const std::vector< double > &ratios, double base_decay)
Initialize modes with given frequency ratios.
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)
std::vector< ModeCoupling > m_couplings
void set_exciter_type(ExciterType type)
Set exciter type.
void reset() override
Reset network to initial state.
void apply_broadcast_parameter(const std::string &param, double value)
Apply broadcast parameter to all modes.
void compute_mode_coupling()
Apply modal coupling energy transfer.
double generate_exciter_sample()
Generate exciter signal for current sample.
void damp(double damping_factor=0.1)
Damp all modes (rapidly reduce amplitude)
std::unordered_map< std::string, std::string > get_metadata() const override
Get network metadata for debugging/visualization.
std::vector< double > m_exciter_node_buffer
void unmap_parameter(const std::string &param_name) override
Remove parameter mapping.
void set_mode_coupling(size_t mode_a, size_t mode_b, double strength)
Define bidirectional coupling between two modes.
void remove_mode_coupling(size_t mode_a, size_t mode_b)
Remove specific coupling.
void set_fundamental(double frequency)
Set base frequency (fundamental)
void initialize_exciter(double strength)
Initialize exciter for new excitation event.
void excite_at_position(double position, double strength=1.0)
Excite modes based on normalized strike position.
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.
Spectrum
Predefined frequency relationship patterns.
@ STRETCHED
Piano-like stiffness: f, 2.01f, 3.02f, 4.04f...
@ INHARMONIC
Bell-like: f, 2.76f, 5.40f, 8.93f, 13.34f...
@ CUSTOM
User-provided frequency ratios.
@ HARMONIC
Integer harmonics: f, 2f, 3f, 4f...
void update_mapped_parameters()
Update mapped parameters before processing.
static std::vector< double > generate_spectrum_ratios(Spectrum spectrum, size_t count)
Generate frequency ratios for predefined spectra.
void set_exciter_sample(const std::vector< double > &sample)
Set custom excitation sample.
std::vector< double > m_spatial_distribution
ModalNetwork(size_t num_modes, double fundamental=220.0, Spectrum spectrum=Spectrum::HARMONIC, double base_decay=1.0)
Create modal network with predefined spectrum.
void set_exciter_duration(double seconds)
Set noise burst duration.
void set_spatial_distribution(const std::vector< double > &distribution)
Set custom spatial amplitude distribution.
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.
std::vector< size_t > peak_positions(std::span< const double > data, double threshold, size_t min_distance)
Sample indices of local peak maxima in the full span.
Definition Analysis.cpp:669
double decay_per_sample(double seconds, double sample_rate) noexcept
Per-sample factor that falls by 1/e in a given time.
@ INDEPENDENT
No connections, nodes process independently.
@ CUSTOM
User-defined arbitrary topology.
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.