diff --git a/src/audio/engine/audio_command_dispatcher.cpp b/src/audio/engine/audio_command_dispatcher.cpp index 153c4f85a..e726823a6 100644 --- a/src/audio/engine/audio_command_dispatcher.cpp +++ b/src/audio/engine/audio_command_dispatcher.cpp @@ -101,13 +101,9 @@ void AudioCommandDispatcher::drain_commands(std::atomic& input_gain, if (node_id >= 0 && node_id < static_cast(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; }; diff --git a/src/audio/engine/audio_command_dispatcher.h b/src/audio/engine/audio_command_dispatcher.h index 10a5b5234..b815d1e1e 100644 --- a/src/audio/engine/audio_command_dispatcher.h +++ b/src/audio/engine/audio_command_dispatcher.h @@ -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& input_gain, std::atomic& output_gain, std::shared_ptr& 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& input_gain, std::atomic& output_gain, std::shared_ptr& executor, AudioGraph& main_graph, std::vector>& 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 command_queue_; + std::atomic error_flag_{false}; }; } // namespace Amplitron diff --git a/src/audio/engine/audio_engine_process.cpp b/src/audio/engine/audio_engine_process.cpp index ccabaaed4..facb3975e 100644 --- a/src/audio/engine/audio_engine_process.cpp +++ b/src/audio/engine/audio_engine_process.cpp @@ -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(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(frame_count) * 2 * sizeof(float)); + } + return; } const bool analyzer_on = analyzer_capture_->is_analyzer_enabled(); @@ -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_; diff --git a/tests/integration/test_audio_engine.cpp b/tests/integration/test_audio_engine.cpp index bea641cf2..c7fb780cd 100644 --- a/tests/integration/test_audio_engine.cpp +++ b/tests/integration/test_audio_engine.cpp @@ -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 in(large_frame_count, 0.5f); - std::vector out(large_frame_count * 2, 0.0f); + std::vector 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(large_frame_count)); - ASSERT_GE(engine.test_process_buffer_right().size(), static_cast(large_frame_count)); + // Check that the internal buffers did NOT resize (they stay at 16384) + ASSERT_LT(engine.test_process_buffer().size(), static_cast(large_frame_count)); + + // Check that the output was safely zeroed out + for (float sample : out) { + ASSERT_NEAR(sample, 0.0f, 1e-6f); + } } diff --git a/tests/unit/test_audio_command_dispatcher.cpp b/tests/unit/test_audio_command_dispatcher.cpp index a3ab7d641..8b004bd70 100644 --- a/tests/unit/test_audio_command_dispatcher.cpp +++ b/tests/unit/test_audio_command_dispatcher.cpp @@ -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); @@ -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()); }