Skip to content

Commit a4ecce3

Browse files
Merge pull request #277 from MohitBareja16/feature/n-input-mixer
feat: implement N-input dynamic mixer with per-channel gain routing
2 parents 837ba12 + 5f61709 commit a4ecce3

18 files changed

Lines changed: 2068 additions & 1329 deletions

src/audio/engine/audio_engine.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,14 @@ class AudioEngine {
258258
*/
259259
void push_param_change(int effect_index, int param_index, float value);
260260

261+
/**
262+
* @brief Enqueue a mixer input gain change from the GUI thread.
263+
* @param node_id ID of the Mixer node.
264+
* @param pin_index Index of the input pin on the mixer.
265+
* @param gain New gain multiplier (0.0–2.0).
266+
*/
267+
void push_mixer_gain_change(int node_id, int pin_index, float gain);
268+
261269
/**
262270
* @brief Enqueue an effect enabled/disabled change from the GUI thread.
263271
* @param effect_index Index of the effect in the chain.

src/audio/engine/audio_engine_api.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ void AudioEngine::push_effect_mix(int effect_index, float mix) {
6060
command_queue_.try_push(cmd);
6161
}
6262

63+
void AudioEngine::push_mixer_gain_change(int node_id, int pin_index, float gain) {
64+
AudioCommand cmd{};
65+
cmd.type = AudioCommand::SetMixerGain;
66+
cmd.effect_index = node_id; // Overload effect_index to mean node_id
67+
cmd.param_index = pin_index; // Overload param_index to mean pin_index
68+
cmd.value = gain;
69+
command_queue_.try_push(cmd);
70+
}
71+
6372
int AudioEngine::get_suggested_buffer_size() const {
6473
float load = cpu_load_.load(std::memory_order_relaxed);
6574
int current = buffer_size_;

src/audio/engine/audio_engine_process.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ void AudioEngine::drain_gain_commands() {
252252
} else if (cmd.type == AudioCommand::SetOutputGain) {
253253
command_queue_.try_pop(cmd);
254254
output_gain_.store(cmd.value, std::memory_order_relaxed);
255+
} else if (cmd.type == AudioCommand::SetMixerGain) {
256+
command_queue_.try_pop(cmd);
257+
if (audio_shadow_executor_) {
258+
audio_shadow_executor_->update_mixer_gain(cmd.effect_index, cmd.param_index, cmd.value);
259+
}
255260
} else {
256261
break;
257262
}
@@ -293,6 +298,9 @@ void AudioEngine::drain_commands() {
293298
}
294299
break;
295300
}
301+
case AudioCommand::SetMixerGain:
302+
// Skip SetMixerGain in the mutex-gated path, it is handled lock-free
303+
break;
296304
case AudioCommand::SetInputGain:
297305
input_gain_.store(cmd.value, std::memory_order_relaxed);
298306
break;

src/audio/engine/audio_graph.cpp

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ int AudioGraph::add_node(const std::string &name, NodeRoutingType type,
1515

1616
// Dynamically configure pin structures using the same unified ID pool
1717
if (type == NodeRoutingType::Mixer || type == NodeRoutingType::MergeSum) {
18-
int inputs_to_create = (num_inputs > 0) ? num_inputs : 2;
18+
int inputs_to_create = std::clamp((num_inputs > 0) ? num_inputs : 2, 2, 8);
1919
for (int i = 0; i < inputs_to_create; ++i) {
2020
node.input_pin_ids.push_back(next_id_++);
2121
}
22+
node.input_gains.assign(inputs_to_create, 1.0f);
2223
node.output_pin_ids.push_back(next_id_++); // 1 Output Pin
2324
} else if (type == NodeRoutingType::Splitter) {
2425
node.input_pin_ids.push_back(next_id_++); // 1 Input Pin
@@ -342,6 +343,151 @@ void AudioGraph::restore_link(const GraphLink& link) {
342343
rebuild_topology();
343344
}
344345
}
346+
bool AudioGraph::add_input_pin(int node_id) {
347+
for (auto &node : nodes_) {
348+
if (node.id == node_id && node.routing_type == NodeRoutingType::Mixer) {
349+
if (node.input_pin_ids.size() < 8) {
350+
node.input_pin_ids.push_back(next_id_++);
351+
node.input_gains.push_back(1.0f);
352+
// Do not necessarily need to rebuild topology if we just added an unconnected pin
353+
return true;
354+
}
355+
return false;
356+
}
357+
}
358+
return false;
359+
}
345360

361+
bool AudioGraph::remove_input_pin(int node_id, int pin_id) {
362+
for (auto &node : nodes_) {
363+
if (node.id == node_id && (node.routing_type == NodeRoutingType::Mixer || node.routing_type == NodeRoutingType::MergeSum)) {
364+
if (node.input_pin_ids.size() > 2) {
365+
if (node.input_gains.size() < node.input_pin_ids.size()) {
366+
node.input_gains.resize(node.input_pin_ids.size(), 1.0f);
367+
}
368+
int index_to_remove = -1;
369+
if (pin_id == -1) {
370+
index_to_remove = node.input_pin_ids.size() - 1;
371+
} else {
372+
for (size_t i = 0; i < node.input_pin_ids.size(); ++i) {
373+
if (node.input_pin_ids[i] == pin_id) {
374+
index_to_remove = i;
375+
break;
376+
}
377+
}
378+
}
379+
if (index_to_remove != -1) {
380+
int pin_to_remove = node.input_pin_ids[index_to_remove];
381+
// Prevent removal if the pin is linked
382+
for (const auto &link : links_) {
383+
if (link.dest_pin_id == pin_to_remove) {
384+
return false;
385+
}
386+
}
387+
node.input_pin_ids.erase(node.input_pin_ids.begin() + index_to_remove);
388+
node.input_gains.erase(node.input_gains.begin() + index_to_remove);
389+
return true;
390+
}
391+
}
392+
return false;
393+
}
394+
}
395+
return false;
396+
}
397+
398+
void AudioGraph::restore_input_pin(int node_id, int pin_id, int index, float gain) {
399+
for (auto &node : nodes_) {
400+
if (node.id == node_id) {
401+
if (index >= 0) {
402+
size_t idx = static_cast<size_t>(index);
403+
if (idx <= node.input_pin_ids.size()) {
404+
node.input_pin_ids.insert(node.input_pin_ids.begin() + idx, pin_id);
405+
while (node.input_gains.size() < idx) {
406+
node.input_gains.push_back(1.0f);
407+
}
408+
node.input_gains.insert(node.input_gains.begin() + idx, gain);
409+
} else {
410+
node.input_pin_ids.push_back(pin_id);
411+
node.input_gains.push_back(gain);
412+
}
413+
} else {
414+
node.input_pin_ids.push_back(pin_id);
415+
node.input_gains.push_back(gain);
416+
}
417+
if (pin_id >= next_id_) next_id_ = pin_id + 1;
418+
break;
419+
}
420+
}
421+
}
422+
423+
void AudioGraph::set_mixer_input_gain(int node_id, size_t pin_index, float gain) {
424+
for (auto &node : nodes_) {
425+
if (node.id == node_id && node.routing_type == NodeRoutingType::Mixer) {
426+
if (pin_index < node.input_gains.size()) {
427+
node.input_gains[pin_index] = std::clamp(gain, 0.0f, 2.0f);
428+
}
429+
break;
430+
}
431+
}
432+
}
433+
434+
bool AudioGraph::add_output_pin(int node_id) {
435+
for (auto &node : nodes_) {
436+
if (node.id == node_id && node.routing_type == NodeRoutingType::Splitter) {
437+
if (node.output_pin_ids.size() < 8) {
438+
node.output_pin_ids.push_back(next_id_++);
439+
return true;
440+
}
441+
return false;
442+
}
443+
}
444+
return false;
445+
}
446+
447+
bool AudioGraph::remove_output_pin(int node_id, int pin_id) {
448+
for (auto &node : nodes_) {
449+
if (node.id == node_id && node.routing_type == NodeRoutingType::Splitter) {
450+
if (node.output_pin_ids.size() > 2) {
451+
int index_to_remove = -1;
452+
if (pin_id == -1) {
453+
index_to_remove = node.output_pin_ids.size() - 1;
454+
} else {
455+
for (size_t i = 0; i < node.output_pin_ids.size(); ++i) {
456+
if (node.output_pin_ids[i] == pin_id) {
457+
index_to_remove = i;
458+
break;
459+
}
460+
}
461+
}
462+
if (index_to_remove != -1) {
463+
int pin_to_remove = node.output_pin_ids[index_to_remove];
464+
for (const auto &link : links_) {
465+
if (link.source_pin_id == pin_to_remove) {
466+
return false;
467+
}
468+
}
469+
node.output_pin_ids.erase(node.output_pin_ids.begin() + index_to_remove);
470+
return true;
471+
}
472+
}
473+
return false;
474+
}
475+
}
476+
return false;
477+
}
478+
479+
void AudioGraph::restore_output_pin(int node_id, int pin_id, int index) {
480+
for (auto &node : nodes_) {
481+
if (node.id == node_id && node.routing_type == NodeRoutingType::Splitter) {
482+
if (index >= 0 && index <= node.output_pin_ids.size()) {
483+
node.output_pin_ids.insert(node.output_pin_ids.begin() + index, pin_id);
484+
} else {
485+
node.output_pin_ids.push_back(pin_id);
486+
}
487+
if (pin_id >= next_id_) next_id_ = pin_id + 1;
488+
break;
489+
}
490+
}
491+
}
346492

347493
} // namespace Amplitron

src/audio/engine/audio_graph.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ struct DSPNode {
3131

3232
float x = 0.0f;
3333
float y = 0.0f;
34+
35+
std::vector<float> input_gains; // Gain for each input pin (Mixer only)
3436
};
3537

3638
struct GraphLink {
@@ -62,6 +64,17 @@ class AudioGraph {
6264
void restore_node(const DSPNode& node);
6365
void restore_link(const GraphLink& link);
6466

67+
// Dynamic Mixer API
68+
bool add_input_pin(int node_id);
69+
bool remove_input_pin(int node_id, int pin_id = -1);
70+
void set_mixer_input_gain(int node_id, size_t pin_index, float gain);
71+
void restore_input_pin(int node_id, int pin_id, int index, float gain);
72+
73+
// Dynamic Splitter API
74+
bool add_output_pin(int node_id);
75+
bool remove_output_pin(int node_id, int pin_id = -1);
76+
void restore_output_pin(int node_id, int pin_id, int index);
77+
6578
// Accessors
6679
const std::vector<int> &get_sorted_nodes() const { return sorted_node_ids_; }
6780
const std::vector<DSPNode> &get_nodes() const { return nodes_; }

src/audio/engine/audio_graph_executor.cpp

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,17 @@ void AudioGraphExecutor::compile(const AudioGraph& graph) {
7878
}
7979

8080
// Trace upstream connections to find which buffers to read from
81-
for (int in_pin : it->input_pin_ids) {
81+
for (size_t pin_idx = 0; pin_idx < it->input_pin_ids.size(); ++pin_idx) {
82+
int in_pin = it->input_pin_ids[pin_idx];
83+
float pin_gain = 1.0f;
84+
if (pin_idx < it->input_gains.size()) {
85+
pin_gain = it->input_gains[pin_idx];
86+
}
8287
for (const auto& link : links) {
8388
if (link.dest_pin_id == in_pin) {
8489
int src_node_id = graph.get_node_from_pin(link.source_pin_id);
8590
if (src_node_id != -1 && node_to_buffer.count(src_node_id)) {
86-
step.input_sources.push_back({ node_to_buffer[src_node_id] });
91+
step.input_sources.push_back({ node_to_buffer[src_node_id], pin_gain, static_cast<int>(pin_idx) });
8792
}
8893
}
8994
}
@@ -128,8 +133,14 @@ void AudioGraphExecutor::process(const float* input, float* output, int num_samp
128133
std::memset(node_input, 0, num_samples * sizeof(float));
129134
for (const auto& src : step.input_sources) {
130135
const float* src_buf = buffer_pool_[src.buffer_index].data();
131-
for (int i = 0; i < num_samples; ++i) {
132-
node_input[i] += src_buf[i];
136+
if (src.gain == 1.0f) {
137+
for (int i = 0; i < num_samples; ++i) {
138+
node_input[i] += src_buf[i];
139+
}
140+
} else {
141+
for (int i = 0; i < num_samples; ++i) {
142+
node_input[i] += src_buf[i] * src.gain;
143+
}
133144
}
134145
}
135146
}
@@ -161,4 +172,18 @@ void AudioGraphExecutor::process(const float* input, float* output, int num_samp
161172
}
162173
}
163174

175+
void AudioGraphExecutor::update_mixer_gain(int node_id, int pin_index, float gain) {
176+
for (auto& step : execution_plan_) {
177+
if (step.node_id == node_id && (step.type == NodeRoutingType::Mixer || step.type == NodeRoutingType::MergeSum)) {
178+
for (auto& src : step.input_sources) {
179+
if (src.pin_index == pin_index) {
180+
src.gain = std::clamp(gain, 0.0f, 2.0f);
181+
break;
182+
}
183+
}
184+
break;
185+
}
186+
}
187+
}
188+
164189
} // namespace Amplitron

src/audio/engine/audio_graph_executor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class AudioGraphExecutor {
2626
// Hot-path processing (Strictly allocation-free and lock-free)
2727
// Adjust the pedal->process signature if your pedals process strictly in-place
2828
void process(const float* input, float* output, int num_samples);
29+
void update_mixer_gain(int node_id, int pin_index, float gain);
2930

3031
private:
3132
int sample_rate_ = 48000;
@@ -34,6 +35,8 @@ class AudioGraphExecutor {
3435

3536
struct InputSource {
3637
int buffer_index; // The pool index to read from
38+
float gain = 1.0f;
39+
int pin_index = 0;
3740
};
3841

3942
struct NodeExecutionStep {

src/audio/utils/spsc_queue.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ struct AudioCommand {
8989
SetEffectParam, // Change an effect parameter value
9090
SetEffectEnabled, // Enable/disable an effect
9191
SetEffectMix, // Change effect wet/dry mix
92+
SetMixerGain, // Change mixer input gain dynamically
9293
SetInputGain, // Change master input gain
9394
SetOutputGain, // Change master output gain
9495
AddEffect, // Signal that effect list changed (swap pointer)

0 commit comments

Comments
 (0)