MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ModalNetwork.cpp
Go to the documentation of this file.
1#include "ModalNetwork.hpp"
2
5
7
8//-----------------------------------------------------------------------------
9// Construction
10//-----------------------------------------------------------------------------
11
12ModalNetwork::ModalNetwork(size_t num_modes, double fundamental,
13 Spectrum spectrum, double base_decay)
14 : m_spectrum(spectrum)
15 , m_fundamental(fundamental)
16{
19
20 auto ratios = generate_spectrum_ratios(spectrum, num_modes);
21 initialize_modes(ratios, base_decay);
22}
23
24ModalNetwork::ModalNetwork(const std::vector<double>& frequency_ratios,
25 double fundamental, double base_decay)
26 : m_spectrum(Spectrum::CUSTOM)
27 , m_fundamental(fundamental)
28{
31
32 initialize_modes(frequency_ratios, base_decay);
33}
34
35//-----------------------------------------------------------------------------
36// Spectrum Generation
37//-----------------------------------------------------------------------------
38
40 size_t count)
41{
42 std::vector<double> ratios;
43 ratios.reserve(count);
44
45 switch (spectrum) {
47 // Perfect integer harmonics: 1, 2, 3, 4...
48 for (size_t i = 0; i < count; ++i) {
49 ratios.push_back(static_cast<double>(i + 1));
50 }
51 break;
52
54 // Bell-like spectrum (approximate mode ratios for circular plates)
55 // Based on Bessel function zeros
56 ratios = { 1.0, 2.756, 5.404, 8.933, 13.344, 18.64, 24.81, 31.86 };
57 while (ratios.size() < count) {
58 double last = ratios.back();
59 ratios.push_back(last + 6.8);
60 }
61 ratios.resize(count);
62 break;
63
65 // f_n = n * f_0 * sqrt(1 + B * n^2)
66 // Using small B = 0.0001 for moderate stretching
67 {
68 constexpr double B = 0.0001;
69 for (size_t n = 1; n <= count; ++n) {
70 double ratio = n * std::sqrt(1.0 + B * n * n);
71 ratios.push_back(ratio);
72 }
73 }
74 break;
75
77 for (size_t i = 0; i < count; ++i) {
78 ratios.push_back(static_cast<double>(i + 1));
79 }
80 break;
81 }
82
83 return ratios;
84}
85
86//-----------------------------------------------------------------------------
87// Mode Initialization
88//-----------------------------------------------------------------------------
89
90void ModalNetwork::initialize_modes(const std::vector<double>& ratios,
91 double base_decay)
92{
93 m_modes.clear();
94 m_modes.reserve(ratios.size());
95
96 for (size_t i = 0; i < ratios.size(); ++i) {
97 ModalNode mode;
98 mode.index = i;
99 mode.frequency_ratio = ratios[i];
100 mode.base_frequency = m_fundamental * ratios[i];
101 mode.current_frequency = mode.base_frequency;
102
103 mode.decay_time = base_decay / ratios[i];
104
105 mode.initial_amplitude = 1.0 / double(i + 1);
106 mode.amplitude = 0.0;
107
108 mode.oscillator = std::make_shared<Generator::Sine>(static_cast<float>(mode.current_frequency));
109 mode.oscillator->set_in_network(true);
110
111 mode.decay_coefficient = std::exp(-1.0 / (base_decay * m_sample_rate));
112
113 m_modes.push_back(std::move(mode));
114 }
115}
116
118{
119 for (auto& mode : m_modes) {
120 mode.amplitude = 0.0;
121 mode.phase = 0.0;
122 mode.current_frequency = mode.base_frequency;
123 }
124}
125
126//-----------------------------------------------------------------------------
127// Exciter System
128//-----------------------------------------------------------------------------
129
131{
132 m_exciter_duration = std::max(0.001, seconds);
133}
134
135void ModalNetwork::set_exciter_sample(const std::vector<double>& sample)
136{
137 m_exciter_sample = sample;
138}
139
140void ModalNetwork::initialize_exciter(double /*strength*/)
141{
142 m_exciter_active = true;
144
145 switch (m_exciter_type) {
148 break;
149
152 m_exciter_samples_remaining = static_cast<size_t>(
154 break;
155
158 break;
159
161 m_exciter_samples_remaining = std::numeric_limits<size_t>::max();
162 break;
163 }
164}
165
167{
169 m_exciter_active = false;
170 return 0.0;
171 }
172
174 double sample = 0.0;
175
176 switch (m_exciter_type) {
178 sample = 1.0;
179 break;
180
182 sample = m_random_generator(-1.0, 1.0);
183 break;
184
186 double noise = m_random_generator(-1.0, 1.0);
187 if (m_exciter_filter) {
188 sample = m_exciter_filter->process_sample(noise);
189 } else {
190 sample = noise;
191 }
192 break;
193 }
194
198 }
199 break;
200
202 if (m_exciter_node) {
203 sample = m_exciter_node->process_sample(0.0);
204 }
205 break;
206 }
207
208 return sample;
209}
210
211//-----------------------------------------------------------------------------
212// Spatial Excitation
213//-----------------------------------------------------------------------------
214
215void ModalNetwork::excite_at_position(double position, double strength)
216{
217 position = std::clamp(position, 0.0, 1.0);
218
219 if (m_spatial_distribution.empty()) {
221 }
222
223 initialize_exciter(strength);
224
225 for (size_t i = 0; i < m_modes.size(); ++i) {
226 double spatial_amp = std::sin((i + 1) * M_PI * position);
227 spatial_amp = std::abs(spatial_amp);
228
229 m_modes[i].amplitude = m_modes[i].initial_amplitude * strength * spatial_amp;
230 }
231}
232
233void ModalNetwork::set_spatial_distribution(const std::vector<double>& distribution)
234{
235 if (distribution.size() != m_modes.size()) {
236 return;
237 }
238 m_spatial_distribution = distribution;
239}
240
242{
244 m_spatial_distribution.reserve(m_modes.size());
245
246 for (size_t i = 0; i < m_modes.size(); ++i) {
247 m_spatial_distribution.push_back(1.0);
248 }
249}
250
251//-----------------------------------------------------------------------------
252// Modal Coupling
253//-----------------------------------------------------------------------------
254
255void ModalNetwork::set_mode_coupling(size_t mode_a, size_t mode_b, double strength)
256{
257 if (mode_a >= m_modes.size() || mode_b >= m_modes.size() || mode_a == mode_b) {
258 return;
259 }
260
261 strength = std::clamp(strength, 0.0, 1.0);
262
263 remove_mode_coupling(mode_a, mode_b);
264
265 m_couplings.push_back({ .mode_a = mode_a,
266 .mode_b = mode_b,
267 .strength = strength });
268}
269
270void ModalNetwork::remove_mode_coupling(size_t mode_a, size_t mode_b)
271{
272 std::erase_if(m_couplings,
273 [mode_a, mode_b](const ModeCoupling& c) {
274 return (c.mode_a == mode_a && c.mode_b == mode_b)
275 || (c.mode_a == mode_b && c.mode_b == mode_a);
276 });
277}
278
280{
281 for (const auto& coupling : m_couplings) {
282 auto& mode_a = m_modes[coupling.mode_a];
283 auto& mode_b = m_modes[coupling.mode_b];
284
285 double energy_diff = (mode_a.amplitude - mode_b.amplitude) * coupling.strength;
286
287 mode_a.amplitude -= energy_diff * 0.5;
288 mode_b.amplitude += energy_diff * 0.5;
289 }
290}
291
292//-----------------------------------------------------------------------------
293// Processing
294//-----------------------------------------------------------------------------
295
296void ModalNetwork::process_batch(unsigned int num_samples)
297{
299
300 if (!is_enabled()) {
301 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
302 std::this_thread::yield();
303
304 m_last_audio_buffer.assign(num_samples, 0.0);
305 m_audio_buffer_lock.clear(std::memory_order_release);
306 return;
307 }
308
309 thread_local std::vector<double> scratch;
310 scratch.assign(num_samples, 0.0);
311
313
314 m_node_buffers.assign(m_modes.size(), {});
315 for (auto& nb : m_node_buffers)
316 nb.reserve(num_samples);
317
318 for (size_t i = 0; i < num_samples; ++i) {
319 double exciter_signal = generate_exciter_sample();
320
321 if (m_coupling_enabled && !m_couplings.empty())
323
324 for (size_t m = 0; m < m_modes.size(); ++m) {
325 auto& mode = m_modes[m];
326
327 if (mode.amplitude > 0.0001) {
328 mode.amplitude *= mode.decay_coefficient;
329 } else {
330 mode.amplitude = 0.0;
331 }
332
333 double sample = mode.oscillator->process_sample(0.0) * mode.amplitude;
334 m_node_buffers[m].push_back(sample);
335 scratch[i] += sample;
336 }
337 }
338
339 while (m_audio_buffer_lock.test_and_set(std::memory_order_acquire))
340 std::this_thread::yield();
341
342 m_last_audio_buffer.assign(scratch.begin(), scratch.end());
344 m_audio_buffer_lock.clear(std::memory_order_release);
345}
346
347//-----------------------------------------------------------------------------
348// Parameter Mapping
349//-----------------------------------------------------------------------------
350
352{
353 for (const auto& mapping : m_parameter_mappings) {
354 if (mapping.mode == MappingMode::BROADCAST && mapping.broadcast_source) {
355 double value = mapping.broadcast_source->get_last_output();
356 apply_broadcast_parameter(mapping.param_name, value);
357 } else if (mapping.mode == MappingMode::ONE_TO_ONE && mapping.network_source) {
358 apply_one_to_one_parameter(mapping.param_name, mapping.network_source);
359 }
360 }
361}
362
363void ModalNetwork::apply_broadcast_parameter(const std::string& param,
364 double value)
365{
366 if (param == "frequency") {
367 set_fundamental(value);
368 } else if (param == "decay") {
369 m_decay_multiplier = std::max(0.01, value);
370 } else if (param == "amplitude") {
371 for (auto& mode : m_modes) {
372 mode.amplitude *= value;
373 }
374 } else if (param == "scale") {
375 m_output_scale = std::max(0.0, value);
376 }
377}
378
380 const std::string& param, const std::shared_ptr<NodeNetwork>& source)
381{
382 if (source->get_node_count() != m_modes.size()) {
383 return;
384 }
385
386 if (param == "amplitude") {
387 for (size_t i = 0; i < m_modes.size(); ++i) {
388 auto val = source->get_node_output(i);
389 if (val) {
390 m_modes[i].amplitude *= *val;
391 }
392 }
393 } else if (param == "detune") {
394 for (size_t i = 0; i < m_modes.size(); ++i) {
395 auto val = source->get_node_output(i);
396 if (val) {
397 double detune_cents = *val * 100.0; // ±100 cents
398 double ratio = std::pow(2.0, detune_cents / 1200.0);
399 m_modes[i].current_frequency = m_modes[i].base_frequency * ratio;
400 m_modes[i].oscillator->set_frequency(m_modes[i].current_frequency);
401 }
402 }
403 }
404}
405
406void ModalNetwork::map_parameter(const std::string& param_name,
407 const std::shared_ptr<Node>& source,
408 MappingMode mode)
409{
410 unmap_parameter(param_name);
411
412 ParameterMapping mapping;
413 mapping.param_name = param_name;
414 mapping.mode = mode;
415 mapping.broadcast_source = source;
416 mapping.network_source = nullptr;
417
418 m_parameter_mappings.push_back(std::move(mapping));
419}
420
422 const std::string& param_name,
423 const std::shared_ptr<NodeNetwork>& source_network)
424{
425 unmap_parameter(param_name);
426
427 ParameterMapping mapping;
428 mapping.param_name = param_name;
430 mapping.broadcast_source = nullptr;
431 mapping.network_source = source_network;
432
433 m_parameter_mappings.push_back(std::move(mapping));
434}
435
436void ModalNetwork::unmap_parameter(const std::string& param_name)
437{
438 std::erase_if(m_parameter_mappings,
439 [&](const auto& m) { return m.param_name == param_name; });
440}
441
442//-----------------------------------------------------------------------------
443// Modal Control
444//-----------------------------------------------------------------------------
445
446void ModalNetwork::excite(double strength)
447{
448 initialize_exciter(strength);
449
450 for (auto& mode : m_modes) {
451 mode.amplitude = mode.initial_amplitude * strength;
452 }
453}
454
455void ModalNetwork::excite_mode(size_t mode_index, double strength)
456{
457 if (mode_index < m_modes.size()) {
458 m_modes[mode_index].amplitude = m_modes[mode_index].initial_amplitude * strength;
459 }
460}
461
462void ModalNetwork::damp(double damping_factor)
463{
464 for (auto& mode : m_modes) {
465 mode.amplitude *= damping_factor;
466 }
467}
468
470{
472
473 for (auto& mode : m_modes) {
474 mode.base_frequency = m_fundamental * mode.frequency_ratio;
475 mode.current_frequency = mode.base_frequency;
476
477 mode.oscillator->set_frequency(mode.current_frequency);
478 }
479}
480
481//-----------------------------------------------------------------------------
482// Metadata
483//-----------------------------------------------------------------------------
484
485std::unordered_map<std::string, std::string>
487{
488 auto metadata = NodeNetwork::get_metadata();
489
490 metadata["fundamental"] = std::to_string(m_fundamental) + " Hz";
491 metadata["spectrum"] = [this]() {
492 switch (m_spectrum) {
494 return "HARMONIC";
496 return "INHARMONIC";
498 return "STRETCHED";
499 case Spectrum::CUSTOM:
500 return "CUSTOM";
501 default:
502 return "UNKNOWN";
503 }
504 }();
505 metadata["decay_multiplier"] = std::to_string(m_decay_multiplier);
506
507 double avg_amplitude = 0.0;
508 for (const auto& mode : m_modes) {
509 avg_amplitude += mode.amplitude;
510 }
511 avg_amplitude /= (double)m_modes.size();
512 metadata["avg_amplitude"] = std::to_string(avg_amplitude);
513
514 metadata["exciter_type"] = [this]() {
515 switch (m_exciter_type) {
517 return "IMPULSE";
519 return "NOISE_BURST";
521 return "FILTERED_NOISE";
523 return "SAMPLE";
525 return "CONTINUOUS";
526 default:
527 return "UNKNOWN";
528 }
529 }();
530
531 metadata["coupling_enabled"] = m_coupling_enabled ? "true" : "false";
532 metadata["coupling_count"] = std::to_string(m_couplings.size());
533
534 return metadata;
535}
536
537[[nodiscard]] std::optional<double> ModalNetwork::get_node_output(size_t index) const
538{
539 if (m_modes.size() > index) {
540 return m_modes[index].oscillator->get_last_output();
541 }
542 return std::nullopt;
543}
544
545std::optional<std::span<const double>> ModalNetwork::get_node_audio_buffer(size_t index) const
546{
547 if (index >= m_node_buffers.size() || m_node_buffers[index].empty())
548 return std::nullopt;
549 return std::span<const double>(m_node_buffers[index]);
550}
551
552} // namespace MayaFlux::Nodes::Network
#define B(method_name, full_type_name)
Definition Creator.hpp:131
Eigen::Index count
double frequency
glm::vec3 position
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()
@ 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 apply_one_to_one_parameter(const std::string &param, const std::shared_ptr< NodeNetwork > &source)
Apply one-to-one parameter from another network.
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 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.
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.
@ 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.