Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions src/audio/engine/audio_command_dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,9 @@ void AudioCommandDispatcher::drain_commands(std::atomic<float>& input_gain,
if (node_id >= 0 && node_id < static_cast<int>(dummy_effects.size())) {
// Comments on node_id semantics: node_id is used as a 0-based linear index fallback
// for the GUI and tests
std::cerr << "[AudioCommandDispatcher] Node ID " << node_id
<< " not found in executor or graph; falling back to dummy_effects index."
<< std::endl;
return dummy_effects[node_id];
}
std::cerr << "[AudioCommandDispatcher] Node ID " << node_id
<< " lookup failed completely." << std::endl;
error_flag_.store(true, std::memory_order_release);
return nullptr;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

Expand Down
59 changes: 58 additions & 1 deletion src/audio/engine/audio_command_dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,80 @@ class AudioCommandDispatcher {
AudioCommandDispatcher() = default;
~AudioCommandDispatcher() = default;

/**
* @brief Pushes a parameter change command for a specific effect.
* @param effect_index The index of the effect in the signal chain.
* @param param_index The index of the parameter to change.
* @param value The new parameter value.
*/
void push_param_change(int effect_index, int param_index, float value);

/**
* @brief Pushes a gain change command for a specific mixer node pin.
* @param node_id The ID of the mixer node.
* @param pin_index The index of the input pin on the mixer.
* @param gain The new gain multiplier.
*/
void push_mixer_gain_change(int node_id, int pin_index, float gain);

/**
* @brief Pushes an enable/disable command for a specific effect.
* @param effect_index The index of the effect in the signal chain.
* @param enabled 1.0f to enable, 0.0f to disable.
*/
void push_effect_enabled(int effect_index, float enabled);

/**
* @brief Pushes a mix (dry/wet) change command for a specific effect.
* @param effect_index The index of the effect in the signal chain.
* @param mix The new mix ratio (0.0f to 1.0f).
*/
void push_effect_mix(int effect_index, float mix);

/**
* @brief Pushes a global input gain change command.
* @param gain The new input gain multiplier.
*/
void push_input_gain(float gain);

/**
* @brief Pushes a global output gain change command.
* @param gain The new output gain multiplier.
*/
void push_output_gain(float gain);

// Audio thread side
/**
* @brief Drains only the global gain commands from the queue, applying them directly.
* @param input_gain Reference to the atomic input gain variable to update.
* @param output_gain Reference to the atomic output gain variable to update.
* @param executor Shared pointer to the audio graph executor to handle mixer node gains.
*/
void drain_gain_commands(std::atomic<float>& input_gain, std::atomic<float>& output_gain,
std::shared_ptr<AudioGraphExecutor>& executor);

/**
* @brief Drains all commands from the queue and applies them to the audio graph and effects.
* @param input_gain Reference to the atomic input gain variable to update.
* @param output_gain Reference to the atomic output gain variable to update.
* @param executor Shared pointer to the audio graph executor.
* @param main_graph Reference to the main audio graph.
* @param dummy_effects Fallback list of effects used for legacy sequential routing.
*/
void drain_commands(std::atomic<float>& input_gain, std::atomic<float>& output_gain,
std::shared_ptr<AudioGraphExecutor>& executor, AudioGraph& main_graph,
std::vector<std::shared_ptr<Effect>>& dummy_effects);

/**
* @brief Polls and atomically resets the error flag.
* @return true if an error was logged since the last check, false otherwise.
*/
bool check_and_clear_error() {
return error_flag_.exchange(false, std::memory_order_acq_rel);
}

private:
SPSCQueue<AudioCommand, 256> command_queue_;
std::atomic<bool> error_flag_{false};
};

} // namespace Amplitron
11 changes: 9 additions & 2 deletions src/audio/engine/audio_engine_process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ void AudioEngine::process_audio(const float* input, float* output, int frame_cou
auto t_start = std::chrono::steady_clock::now();

if (frame_count > static_cast<int>(process_buffer_.size())) {
process_buffer_.resize(frame_count, 0.0f);
process_buffer_right_.resize(frame_count, 0.0f);
if (output) {
std::memset(output, 0, static_cast<size_t>(frame_count) * 2 * sizeof(float));
}
return;
}

const bool analyzer_on = analyzer_capture_->is_analyzer_enabled();
Expand Down Expand Up @@ -52,6 +54,11 @@ void AudioEngine::process_audio(const float* input, float* output, int frame_cou
if (effect_mutex_.try_lock()) {
command_dispatcher_.drain_commands(input_gain_, output_gain_, audio_shadow_executor_,
main_graph_, dummy_effects_);

// Consume the error flag so it doesn't stay latched forever.
// In a real application, this might trigger a GUI notification via a non-blocking queue.
command_dispatcher_.check_and_clear_error();

if (topology_dirty_.exchange(false, std::memory_order_acq_rel)) {
audio_shadow_executor_ = main_executor_;
audio_shadow_tuner_ = tuner_tap_;
Expand Down
18 changes: 11 additions & 7 deletions tests/integration/test_audio_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,15 +531,19 @@ TEST_F(AudioEngineTest, TunerTapSampleRateAndReset) {
engine.clear_tuner_tap();
}

TEST_F(AudioEngineTest, ProcessAudioResizesInternalBuffersWhenFrameCountExceedsCapacity) {
// Default internal buffer capacity is 16384. Request a size larger than this.
const int large_frame_count = 17000;
TEST_F(AudioEngineTest, ProcessAudioZerosOutputWhenFrameCountExceedsCapacity) {
// Derive a frame count that exceeds the current internal buffer capacity.
const int large_frame_count = engine.test_process_buffer().capacity() + 1;
std::vector<float> in(large_frame_count, 0.5f);
std::vector<float> out(large_frame_count * 2, 0.0f);
std::vector<float> out(large_frame_count * 2, 1.0f); // Pre-fill with 1.0f

engine.process_audio(in.data(), out.data(), large_frame_count);

// Check that the internal buffers resized accordingly
ASSERT_GE(engine.test_process_buffer().size(), static_cast<size_t>(large_frame_count));
ASSERT_GE(engine.test_process_buffer_right().size(), static_cast<size_t>(large_frame_count));
// Check that the internal buffers did NOT resize (they stay at 16384)
ASSERT_LT(engine.test_process_buffer().size(), static_cast<size_t>(large_frame_count));

// Check that the output was safely zeroed out
for (float sample : out) {
ASSERT_NEAR(sample, 0.0f, 1e-6f);
}
}
7 changes: 7 additions & 0 deletions tests/unit/test_audio_command_dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ TEST(AudioCommandDispatcher_DrainCommands) {
dispatcher.push_input_gain(0.2f);
dispatcher.push_output_gain(0.3f);

// Verify error flag is clear before out-of-bounds push
dispatcher.drain_commands(input_gain, output_gain, executor, main_graph, dummy_effects);
ASSERT_FALSE(dispatcher.check_and_clear_error());

// 6. Unknown command / failure to lookup completely (effect_index out of bounds for
// dummy_effects)
dispatcher.push_param_change(99, 0, 1.0f);
Expand All @@ -90,4 +94,7 @@ TEST(AudioCommandDispatcher_DrainCommands) {
ASSERT_NEAR(fx1->get_mix(), 0.7f, 0.01f);
ASSERT_NEAR(input_gain.load(), 0.2f, 0.01f);
ASSERT_NEAR(output_gain.load(), 0.3f, 0.01f);

// Verify that the invalid node lookup safely set the atomic error flag instead of throwing/printing
ASSERT_TRUE(dispatcher.check_and_clear_error());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}