diff --git a/src/audio/engine/analyzer_capture.cpp b/src/audio/engine/analyzer_capture.cpp index b23b23c2e..d86772226 100644 --- a/src/audio/engine/analyzer_capture.cpp +++ b/src/audio/engine/analyzer_capture.cpp @@ -82,4 +82,111 @@ void AnalyzerCapture::capture_output(const float* output, int count) { } } +bool AnalyzerCapture::register_pedal_analyzer(int node_id) { + if (node_id < 0) return false; + + // Check if already registered + for (int i = 0; i < MAX_PEDAL_ANALYZERS; ++i) { + if (pedal_captures_[i].node_id_.load(std::memory_order_acquire) == node_id) { + return true; + } + } + + // Find an empty slot + for (int i = 0; i < MAX_PEDAL_ANALYZERS; ++i) { + if (pedal_captures_[i].node_id_.load(std::memory_order_relaxed) == -1) { + int expected = -1; + if (pedal_captures_[i].node_id_.compare_exchange_strong(expected, node_id, + std::memory_order_acq_rel)) { + pedal_captures_[i].reset(); + return true; + } + } + } + + return false; +} + +void AnalyzerCapture::unregister_pedal_analyzer(int node_id) { + if (node_id < 0) return; + for (int i = 0; i < MAX_PEDAL_ANALYZERS; ++i) { + int expected = node_id; + if (pedal_captures_[i].node_id_.compare_exchange_strong(expected, -1, + std::memory_order_acq_rel)) { + pedal_captures_[i].reset(); + return; + } + } +} + +uint64_t AnalyzerCapture::get_pedal_analyzer_sequence(int node_id) const { + for (int i = 0; i < MAX_PEDAL_ANALYZERS; ++i) { + if (pedal_captures_[i].node_id_.load(std::memory_order_acquire) == node_id) { + return pedal_captures_[i].sequence_.load(std::memory_order_acquire); + } + } + return 0; +} + +bool AnalyzerCapture::copy_pedal_analyzer_snapshot(int node_id, float* input_dest, + float* output_dest, int sample_count) const { + if (!input_dest || !output_dest || sample_count <= 0) { + return false; + } + for (int i = 0; i < MAX_PEDAL_ANALYZERS; ++i) { + if (pedal_captures_[i].node_id_.load(std::memory_order_acquire) == node_id) { + const auto& pc = pedal_captures_[i]; + const int count = std::min(sample_count, ANALYZER_FFT_SIZE); + std::lock_guard lock(pc.mutex_); + const uint64_t seq = pc.sequence_.load(std::memory_order_relaxed); + if (seq == 0) { + return false; + } + std::memcpy(input_dest, pc.snapshot_input_.data(), + static_cast(count) * sizeof(float)); + std::memcpy(output_dest, pc.snapshot_output_.data(), + static_cast(count) * sizeof(float)); + return true; + } + } + return false; +} + +void AnalyzerCapture::capture_pedal(int node_id, const float* input, const float* output, + int count) { + for (int i = 0; i < MAX_PEDAL_ANALYZERS; ++i) { + if (pedal_captures_[i].node_id_.load(std::memory_order_relaxed) == node_id) { + auto& pc = pedal_captures_[i]; + int cap = pc.capture_index_.load(std::memory_order_relaxed); + for (int s = 0; s < count; ++s) { + pc.capture_input_[cap] = input[s]; + pc.capture_output_[cap] = output[s]; + cap = (cap + 1) & ANALYZER_FFT_MASK; + } + pc.capture_index_.store(cap, std::memory_order_relaxed); + + int current_samples = pc.samples_since_publish_.load(std::memory_order_relaxed) + count; + pc.samples_since_publish_.store(current_samples, std::memory_order_relaxed); + if (current_samples >= ANALYZER_HOP_SIZE) { + if (pc.mutex_.try_lock()) { + const int start = pc.capture_index_.load(std::memory_order_relaxed); + const int first_chunk = ANALYZER_FFT_SIZE - start; + std::memcpy(pc.snapshot_input_.data(), pc.capture_input_.data() + start, + static_cast(first_chunk) * sizeof(float)); + std::memcpy(pc.snapshot_input_.data() + first_chunk, pc.capture_input_.data(), + static_cast(start) * sizeof(float)); + std::memcpy(pc.snapshot_output_.data(), pc.capture_output_.data() + start, + static_cast(first_chunk) * sizeof(float)); + std::memcpy(pc.snapshot_output_.data() + first_chunk, pc.capture_output_.data(), + static_cast(start) * sizeof(float)); + pc.sequence_.fetch_add(1, std::memory_order_release); + pc.samples_since_publish_.store(0, std::memory_order_relaxed); + pc.mutex_.unlock(); + } + } + break; + } + } +} + } // namespace Amplitron diff --git a/src/audio/engine/analyzer_capture.h b/src/audio/engine/analyzer_capture.h index 2915d74b4..60aad59b6 100644 --- a/src/audio/engine/analyzer_capture.h +++ b/src/audio/engine/analyzer_capture.h @@ -17,6 +17,7 @@ class AnalyzerCapture : public IAnalyzerProvider { static constexpr int ANALYZER_FFT_SIZE = 2048; static constexpr int ANALYZER_FFT_MASK = ANALYZER_FFT_SIZE - 1; static constexpr int ANALYZER_HOP_SIZE = 1024; + static constexpr int MAX_PEDAL_ANALYZERS = 4; AnalyzerCapture(); ~AnalyzerCapture() override = default; @@ -27,10 +28,16 @@ class AnalyzerCapture : public IAnalyzerProvider { uint64_t get_analyzer_sequence() const override; bool copy_analyzer_snapshot(float* input_dest, float* output_dest, int sample_count) const override; + bool register_pedal_analyzer(int node_id) override; + void unregister_pedal_analyzer(int node_id) override; + uint64_t get_pedal_analyzer_sequence(int node_id) const override; + bool copy_pedal_analyzer_snapshot(int node_id, float* input_dest, float* output_dest, + int sample_count) const override; // Audio thread capture methods void capture_input(const float* input, int count); void capture_output(const float* output, int count); + void capture_pedal(int node_id, const float* input, const float* output, int count); private: std::atomic enabled_{false}; @@ -46,6 +53,32 @@ class AnalyzerCapture : public IAnalyzerProvider { std::array snapshot_input_{}; std::array snapshot_output_{}; std::atomic sequence_{0}; + + // Per-pedal analyzer captures + struct PedalCapture { + std::atomic node_id_{-1}; + std::array capture_input_{}; + std::array capture_output_{}; + std::atomic capture_index_{0}; + std::atomic samples_since_publish_{0}; + + mutable std::mutex mutex_; + std::array snapshot_input_{}; + std::array snapshot_output_{}; + std::atomic sequence_{0}; + + void reset() { + std::lock_guard lock(mutex_); + capture_input_.fill(0.0f); + capture_output_.fill(0.0f); + snapshot_input_.fill(0.0f); + snapshot_output_.fill(0.0f); + capture_index_.store(0, std::memory_order_relaxed); + samples_since_publish_.store(0, std::memory_order_relaxed); + sequence_.store(0, std::memory_order_release); + } + }; + std::array pedal_captures_{}; }; } // namespace Amplitron diff --git a/src/audio/engine/audio_engine.h b/src/audio/engine/audio_engine.h index 4d6edd19e..aa9d7bfde 100644 --- a/src/audio/engine/audio_engine.h +++ b/src/audio/engine/audio_engine.h @@ -217,6 +217,11 @@ class AudioEngine : public IAudioEngine { */ bool copy_analyzer_snapshot(float* input_dest, float* output_dest, int sample_count) const override; + bool register_pedal_analyzer(int node_id) override; + void unregister_pedal_analyzer(int node_id) override; + uint64_t get_pedal_analyzer_sequence(int node_id) const override; + bool copy_pedal_analyzer_snapshot(int node_id, float* input_dest, float* output_dest, + int sample_count) const override; /** * @brief Set the master input gain (enqueued to audio thread via SPSC queue). diff --git a/src/audio/engine/audio_engine_api.cpp b/src/audio/engine/audio_engine_api.cpp index 5dcaa7acf..27ab07b49 100644 --- a/src/audio/engine/audio_engine_api.cpp +++ b/src/audio/engine/audio_engine_api.cpp @@ -70,4 +70,22 @@ bool AudioEngine::copy_analyzer_snapshot(float* input_dest, float* output_dest, return analyzer_capture_->copy_analyzer_snapshot(input_dest, output_dest, sample_count); } +bool AudioEngine::register_pedal_analyzer(int node_id) { + return analyzer_capture_->register_pedal_analyzer(node_id); +} + +void AudioEngine::unregister_pedal_analyzer(int node_id) { + analyzer_capture_->unregister_pedal_analyzer(node_id); +} + +uint64_t AudioEngine::get_pedal_analyzer_sequence(int node_id) const { + return analyzer_capture_->get_pedal_analyzer_sequence(node_id); +} + +bool AudioEngine::copy_pedal_analyzer_snapshot(int node_id, float* input_dest, float* output_dest, + int sample_count) const { + return analyzer_capture_->copy_pedal_analyzer_snapshot(node_id, input_dest, output_dest, + sample_count); +} + } // namespace Amplitron diff --git a/src/audio/engine/audio_engine_process.cpp b/src/audio/engine/audio_engine_process.cpp index ccabaaed4..3fc201a8a 100644 --- a/src/audio/engine/audio_engine_process.cpp +++ b/src/audio/engine/audio_engine_process.cpp @@ -71,7 +71,7 @@ void AudioEngine::process_audio(const float* input, float* output, int frame_cou // Pass your mono/stereo buffers to the executor we built audio_shadow_executor_->process(process_buffer_.data(), process_buffer_right_.data(), - frame_count); + frame_count, analyzer_capture_.get()); std::memcpy(process_buffer_.data(), process_buffer_right_.data(), static_cast(frame_count) * sizeof(float)); } diff --git a/src/audio/engine/audio_graph_executor.cpp b/src/audio/engine/audio_graph_executor.cpp index 94b7d69e5..f02afe9d9 100644 --- a/src/audio/engine/audio_graph_executor.cpp +++ b/src/audio/engine/audio_graph_executor.cpp @@ -3,6 +3,8 @@ #include #include +#include "audio/engine/analyzer_capture.h" + namespace Amplitron { AudioGraphExecutor::AudioGraphExecutor() {} @@ -116,7 +118,8 @@ void AudioGraphExecutor::update_transport_state(float bpm) { } } -void AudioGraphExecutor::process(const float* input, float* output, int num_samples) { +void AudioGraphExecutor::process(const float* input, float* output, int num_samples, + AnalyzerCapture* capture) { if (num_samples > max_block_size_) { std::memset(output, 0, static_cast(num_samples) * sizeof(float)); return; @@ -163,6 +166,10 @@ void AudioGraphExecutor::process(const float* input, float* output, int num_samp } else { std::memcpy(node_output, node_input, num_samples * sizeof(float)); } + + if (capture) { + capture->capture_pedal(step.node_id, node_input, node_output, num_samples); + } } // Accumulate/mix outputs from all explicit sink nodes into the final output buffer diff --git a/src/audio/engine/audio_graph_executor.h b/src/audio/engine/audio_graph_executor.h index 88082cc5e..dc5794c80 100644 --- a/src/audio/engine/audio_graph_executor.h +++ b/src/audio/engine/audio_graph_executor.h @@ -37,6 +37,8 @@ class PassthroughProcessor : public INodeProcessor { } }; +class AnalyzerCapture; + class AudioGraphExecutor { public: friend class AudioEngine; @@ -78,7 +80,8 @@ class AudioGraphExecutor { // Hot-path processing (Strictly allocation-free and lock-free) // Adjust the pedal->process signature if your pedals process strictly in-place - void process(const float* input, float* output, int num_samples); + void process(const float* input, float* output, int num_samples, + AnalyzerCapture* capture = nullptr); void update_mixer_gain(int node_id, int pin_index, float gain); std::shared_ptr get_effect_by_node_id(int node_id) const { diff --git a/src/audio/engine/i_audio_engine.h b/src/audio/engine/i_audio_engine.h index 3240d377c..610bbccbe 100644 --- a/src/audio/engine/i_audio_engine.h +++ b/src/audio/engine/i_audio_engine.h @@ -144,6 +144,11 @@ class IAnalyzerProvider { virtual uint64_t get_analyzer_sequence() const = 0; virtual bool copy_analyzer_snapshot(float* input_dest, float* output_dest, int sample_count) const = 0; + virtual bool register_pedal_analyzer(int node_id) = 0; + virtual void unregister_pedal_analyzer(int node_id) = 0; + virtual uint64_t get_pedal_analyzer_sequence(int node_id) const = 0; + virtual bool copy_pedal_analyzer_snapshot(int node_id, float* input_dest, float* output_dest, + int sample_count) const = 0; }; /** diff --git a/src/gui/pedalboard/pedal_widget.cpp b/src/gui/pedalboard/pedal_widget.cpp index 37196566e..2430a8d7e 100644 --- a/src/gui/pedalboard/pedal_widget.cpp +++ b/src/gui/pedalboard/pedal_widget.cpp @@ -23,6 +23,12 @@ PedalWidget::PedalWidget(IAudioEngine& engine, std::shared_ptr effect, i assign_colors(); } +PedalWidget::~PedalWidget() { + if (analyzer_open_) { + engine_.unregister_pedal_analyzer(index_); + } +} + /** @brief Look up pedal_color_ and led_color_ from the theme's effect color table. */ void PedalWidget::assign_colors() { const auto* entry = get_effect_color(effect_->name()); @@ -69,6 +75,42 @@ bool PedalWidget::render(float zoom) { dl->AddRectFilled(p0, p1, Theme::PEDAL_BYPASS_OVERLAY, Theme::ROUNDING_MD * zoom); } + // --- Spectrum analyzer button --- + float btn_x = p0.x + pedal_width - 50.0f * zoom; + float btn_y = is_amp ? (p0.y + 16.0f * zoom) : (p0.y + 12.0f * zoom); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f * zoom); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f * zoom, 2.0f * zoom)); + + ImVec4 btn_col = + analyzer_open_ ? ImVec4(0.16f, 0.66f, 0.4f, 0.6f) : ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, btn_col); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.4f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.4f, 0.4f, 0.6f)); + + ImGui::SetCursorScreenPos(ImVec2(btn_x, btn_y)); + char btn_id[64]; + std::snprintf(btn_id, sizeof(btn_id), "📊##spec_%d", index_); + + ImGui::SetNextItemAllowOverlap(); + if (ImGui::Button(btn_id, ImVec2(20.0f * zoom, 20.0f * zoom))) { + if (analyzer_open_) { + engine_.unregister_pedal_analyzer(index_); + analyzer_open_ = false; + } else { + if (engine_.register_pedal_analyzer(index_)) { + analyzer_open_ = true; + analyzer_last_sequence_ = 0; + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Toggle pedal spectrum analyzer"); + } + + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(2); + // --- Tuner custom display --- bool is_tuner = !is_amp && (std::strcmp(effect_->name(), "Tuner") == 0); if (is_tuner) { @@ -128,6 +170,10 @@ bool PedalWidget::render(float zoom) { render_footswitch_and_extras(dl, p0, p1, pedal_width, pedal_height, is_amp, enabled, should_remove, zoom); + if (analyzer_open_) { + render_spectrum_overlay(dl, p0, pedal_width, zoom); + } + ImGui::PopID(); return should_remove; } @@ -200,4 +246,93 @@ void PedalWidget::commit_param_change(int param_index, float old_val, float new_ history_->push_executed(std::move(cmd)); } +void PedalWidget::render_spectrum_overlay(ImDrawList* dl, ImVec2 pedal_pos, float pedal_width, + float zoom) { + // 1. Update/poll spectrum data + uint64_t seq = engine_.get_pedal_analyzer_sequence(index_); + float dt = std::max(ImGui::GetIO().DeltaTime, 1.0f / 240.0f); + if (seq != analyzer_last_sequence_) { + if (engine_.copy_pedal_analyzer_snapshot(index_, analyzer_input_buf_.data(), + analyzer_output_buf_.data(), + SpectrumAnalyzer::FFT_SIZE)) { + spectrum_analyzer_.update(analyzer_input_buf_.data(), analyzer_output_buf_.data(), + engine_.get_sample_rate(), dt); + analyzer_last_sequence_ = seq; + } + } else { + spectrum_analyzer_.update(analyzer_input_buf_.data(), analyzer_output_buf_.data(), + engine_.get_sample_rate(), dt); + } + + // 2. Draw overlay floating right above the pedal header! + ImVec2 overlay_size(pedal_width, 100.0f * zoom); + ImVec2 overlay_pos(pedal_pos.x, pedal_pos.y - overlay_size.y - 8.0f * zoom); + + // Draw background + dl->AddRectFilled(overlay_pos, + ImVec2(overlay_pos.x + overlay_size.x, overlay_pos.y + overlay_size.y), + IM_COL32(15, 16, 20, 240), Theme::ROUNDING_SM * zoom); + dl->AddRect(overlay_pos, ImVec2(overlay_pos.x + overlay_size.x, overlay_pos.y + overlay_size.y), + IM_COL32(72, 78, 92, 220), Theme::ROUNDING_SM * zoom, 0, 1.5f * zoom); + + // Reference dB lines + const float ref_lines[] = {-60.0f, -40.0f, -20.0f}; + for (float db : ref_lines) { + float t = (db - (-80.0f)) / 80.0f; + float y = overlay_pos.y + overlay_size.y * (1.0f - t); + dl->AddLine(ImVec2(overlay_pos.x, y), ImVec2(overlay_pos.x + overlay_size.x, y), + IM_COL32(58, 64, 76, 100), 1.0f * zoom); + } + + // Frequency tick lines + const float ticks[] = {100.0f, 1000.0f, 10000.0f}; + for (float hz : ticks) { + const float lo = std::log10(20.0f); + const float hi = std::log10(20000.0f); + float norm = std::clamp((std::log10(hz) - lo) / (hi - lo), 0.0f, 1.0f); + float x = overlay_pos.x + norm * overlay_size.x; + dl->AddLine(ImVec2(x, overlay_pos.y), ImVec2(x, overlay_pos.y + overlay_size.y), + IM_COL32(52, 58, 72, 100), 1.0f * zoom); + } + + // Render the curves + const auto& smoothed_in = spectrum_analyzer_.smoothed_input_db(); + const auto& smoothed_out = spectrum_analyzer_.smoothed_output_db(); + + // Helper lambda to draw curve + auto draw_curve = [&](const std::array& bars, + ImU32 color) { + constexpr int BARS = SpectrumAnalyzer::DISPLAY_BARS; + ImVec2 prev_pt; + for (int i = 0; i < BARS; ++i) { + float x = overlay_pos.x + (static_cast(i) / (BARS - 1)) * overlay_size.x; + float db = std::clamp(bars[i], -80.0f, 0.0f); + float t = (db - (-80.0f)) / 80.0f; + float y = overlay_pos.y + overlay_size.y * (1.0f - t); + ImVec2 pt(x, y); + if (i > 0) { + dl->AddLine(prev_pt, pt, color, 1.8f * zoom); + } + prev_pt = pt; + } + }; + + // Blue curve = signal entering pedal (pre-processing) + draw_curve(smoothed_in, IM_COL32(92, 170, 255, 230)); + // Green curve = signal leaving pedal (post-processing) + draw_curve(smoothed_out, IM_COL32(82, 220, 135, 230)); + + // Legend/Label + ImGui::SetCursorScreenPos(ImVec2(overlay_pos.x + 8.0f * zoom, overlay_pos.y + 6.0f * zoom)); + ImGui::SetWindowFontScale(zoom * 0.7f); + ImGui::TextColored(Theme::TextSecondary(), "Pre "); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.36f, 0.67f, 1.0f, 1.0f), "[In]"); + ImGui::SameLine(); + ImGui::TextColored(Theme::TextSecondary(), " | Post "); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.32f, 0.86f, 0.53f, 1.0f), "[Out]"); + ImGui::SetWindowFontScale(1.0f); +} + } // namespace Amplitron diff --git a/src/gui/pedalboard/pedal_widget.h b/src/gui/pedalboard/pedal_widget.h index 50460733b..7c7f2ac92 100644 --- a/src/gui/pedalboard/pedal_widget.h +++ b/src/gui/pedalboard/pedal_widget.h @@ -2,6 +2,7 @@ #include +#include "audio/dsp/spectrum_analyzer.h" #include "audio/effects/core/effect.h" #include "common.h" @@ -29,6 +30,7 @@ class PedalWidget { * @param index Position in the signal chain (used for ImGui IDs). */ PedalWidget(IAudioEngine& engine, std::shared_ptr effect, int index); + ~PedalWidget(); /** * @brief Render the pedal widget for one frame. @@ -74,6 +76,7 @@ class PedalWidget { void render_footswitch_and_extras(ImDrawList* dl, ImVec2 p0, ImVec2 p1, float pedal_width, float pedal_height, bool is_amp, bool enabled, bool& should_remove, float zoom); + void render_spectrum_overlay(ImDrawList* dl, ImVec2 pedal_pos, float pedal_width, float zoom); IAudioEngine& engine_; std::shared_ptr effect_; @@ -93,6 +96,12 @@ class PedalWidget { ImVec4 pedal_color_; ///< Pedal body color derived from effect type. ImVec4 led_color_; ///< LED / accent color derived from effect type. + bool analyzer_open_ = false; + SpectrumAnalyzer spectrum_analyzer_; + uint64_t analyzer_last_sequence_ = 0; + std::array analyzer_input_buf_{}; + std::array analyzer_output_buf_{}; + /** @brief Look up pedal_color_ and led_color_ from the theme table. */ void assign_colors(); diff --git a/tests/integration/test_audio_engine.cpp b/tests/integration/test_audio_engine.cpp index bea641cf2..9ca17b597 100644 --- a/tests/integration/test_audio_engine.cpp +++ b/tests/integration/test_audio_engine.cpp @@ -7,6 +7,7 @@ #include "audio/effects/distortion/distortion.h" #include "audio/effects/distortion/overdrive.h" +#include "audio/engine/analyzer_capture.h" #include "audio/engine/audio_engine.h" #include "test_fixtures.h" #include "test_framework.h" @@ -543,3 +544,111 @@ TEST_F(AudioEngineTest, ProcessAudioResizesInternalBuffersWhenFrameCountExceedsC 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)); } + +TEST_F(AudioEngineTest, GetterTestCoverage) { + ASSERT_EQ(engine.test_process_buffer().size(), 16384u); + ASSERT_EQ(engine.test_process_buffer_right().size(), 16384u); + ASSERT_NEAR(engine.test_process_buffer()[0], 0.0f, 1e-6f); + ASSERT_NEAR(engine.test_process_buffer_right()[0], 0.0f, 1e-6f); + ASSERT_NE(engine.test_audio_shadow_executor(), nullptr); + ASSERT_TRUE(engine.test_audio_shadow_executor()->test_execution_plan().empty()); +} + +TEST(AnalyzerCapturePedalRegistration) { + AnalyzerCapture capture; + // Enabled by default is false + ASSERT_FALSE(capture.is_analyzer_enabled()); + + // Register first 4 slots successfully + ASSERT_TRUE(capture.register_pedal_analyzer(10)); + ASSERT_TRUE(capture.register_pedal_analyzer(20)); + ASSERT_TRUE(capture.register_pedal_analyzer(30)); + ASSERT_TRUE(capture.register_pedal_analyzer(40)); + + // 5th should fail + ASSERT_FALSE(capture.register_pedal_analyzer(50)); + + // Duplicate registrations should return true/succeed immediately + ASSERT_TRUE(capture.register_pedal_analyzer(10)); + ASSERT_TRUE(capture.register_pedal_analyzer(30)); + + // Register with negative node ID should fail + ASSERT_FALSE(capture.register_pedal_analyzer(-1)); + ASSERT_FALSE(capture.register_pedal_analyzer(-99)); + + // Unregister invalid or unregistered ID has no effect/safe + capture.unregister_pedal_analyzer(-5); + capture.unregister_pedal_analyzer(999); + + // Unregister active pedal analyzer + capture.unregister_pedal_analyzer(20); + // Slot 20 is now free, registration should succeed + ASSERT_TRUE(capture.register_pedal_analyzer(50)); +} + +TEST(AnalyzerCapturePedalPublishAndSnapshot) { + AnalyzerCapture capture; + // Enable capture + capture.set_analyzer_enabled(true); + ASSERT_TRUE(capture.is_analyzer_enabled()); + + int node_id = 15; + ASSERT_TRUE(capture.register_pedal_analyzer(node_id)); + + // Initial sequence is 0 + ASSERT_EQ(capture.get_pedal_analyzer_sequence(node_id), 0u); + + // Attempting snapshot copy when sequence is 0 should fail + std::vector input_snap(2048, 0.0f); + std::vector output_snap(2048, 0.0f); + ASSERT_FALSE( + capture.copy_pedal_analyzer_snapshot(node_id, input_snap.data(), output_snap.data(), 1024)); + ASSERT_FALSE(capture.copy_pedal_analyzer_snapshot(node_id, nullptr, output_snap.data(), 1024)); + ASSERT_FALSE(capture.copy_pedal_analyzer_snapshot(node_id, input_snap.data(), nullptr, 1024)); + ASSERT_FALSE( + capture.copy_pedal_analyzer_snapshot(node_id, input_snap.data(), output_snap.data(), -5)); + ASSERT_FALSE(capture.copy_pedal_analyzer_snapshot(999, input_snap.data(), output_snap.data(), + 1024)); // unregistered + + // Sequence for unregistered node + ASSERT_EQ(capture.get_pedal_analyzer_sequence(999), 0u); + + // Feed pedal capture with samples < HOP_SIZE (1024). Should not publish snapshot yet. + std::vector input_samples(512, 0.25f); + std::vector output_samples(512, 0.75f); + capture.capture_pedal(node_id, input_samples.data(), output_samples.data(), 512); + ASSERT_EQ(capture.get_pedal_analyzer_sequence(node_id), 0u); + + // Feed another 512 samples. Total is 1024 (>= HOP_SIZE), so it should publish. + capture.capture_pedal(node_id, input_samples.data(), output_samples.data(), 512); + ASSERT_GT(capture.get_pedal_analyzer_sequence(node_id), 0u); + + // Now copy snapshot should succeed + bool success = + capture.copy_pedal_analyzer_snapshot(node_id, input_snap.data(), output_snap.data(), 2048); + ASSERT_TRUE(success); + ASSERT_NEAR(input_snap[1024], 0.25f, 1e-6f); + ASSERT_NEAR(output_snap[1024], 0.75f, 1e-6f); + + // Test capture_pedal for unregistered node (no-op) + capture.capture_pedal(999, input_samples.data(), output_samples.data(), 512); +} + +TEST_F(AudioEngineTest, AudioEnginePedalAnalyzerWrappers) { + engine.set_analyzer_enabled(true); + ASSERT_TRUE(engine.is_analyzer_enabled()); + + int node_id = 88; + ASSERT_TRUE(engine.register_pedal_analyzer(node_id)); + ASSERT_EQ(engine.get_pedal_analyzer_sequence(node_id), 0u); + + std::vector input_snap(2048, 0.0f); + std::vector output_snap(2048, 0.0f); + ASSERT_FALSE( + engine.copy_pedal_analyzer_snapshot(node_id, input_snap.data(), output_snap.data(), 1024)); + + engine.unregister_pedal_analyzer(node_id); + // Registration should fail / be cleaned up + ASSERT_FALSE( + engine.copy_pedal_analyzer_snapshot(node_id, input_snap.data(), output_snap.data(), 1024)); +} diff --git a/tests/test_fixtures.h b/tests/test_fixtures.h index 124a8dd46..b22036e43 100644 --- a/tests/test_fixtures.h +++ b/tests/test_fixtures.h @@ -10,11 +10,12 @@ #include #include -#include "audio/engine/audio_engine.h" #include "test_framework.h" #define private public #define protected public +#include "audio/engine/analyzer_capture.h" +#include "audio/engine/audio_engine.h" #include "audio/recorder/recorder.h" #include "gui/gui_manager.h" #include "gui/pedalboard/pedal_board.h" diff --git a/tests/ui/test_gui_keyboard_shortcuts.cpp b/tests/ui/test_gui_keyboard_shortcuts.cpp index 3595b5afb..75ae30db7 100644 --- a/tests/ui/test_gui_keyboard_shortcuts.cpp +++ b/tests/ui/test_gui_keyboard_shortcuts.cpp @@ -97,3 +97,9 @@ TEST_F(PresetTest, gui_keyboard_shortcuts_click_close_button) { // The show flag should now be set to false after clicking Close ASSERT_FALSE(show); } + +TEST_F(PresetTest, gui_keyboard_shortcuts_render_default_forwarding) { + ScopedImGuiContext imgui; + GuiKeyboardShortcuts gks; + gks.render(); +} diff --git a/tests/ui/test_pedal_board_menu.cpp b/tests/ui/test_pedal_board_menu.cpp index a174a3aa5..248c375a1 100644 --- a/tests/ui/test_pedal_board_menu.cpp +++ b/tests/ui/test_pedal_board_menu.cpp @@ -5,20 +5,20 @@ #include "audio/effects/amp_cab/amp_simulator.h" #include "audio/effects/distortion/overdrive.h" #include "gui/commands/command_history.h" +#include "test_fixtures.h" +#include "test_framework.h" #define private public #include "gui/pedalboard/pedal_board.h" #include "gui/pedalboard/pedal_widget.h" #undef private #include "gui/views/gui_midi.h" #include "midi/midi_manager.h" -#include "test_fixtures.h" -#include "test_framework.h" using namespace Amplitron; using namespace TestFramework; -static ImGuiID get_item_id(const char *window_substr, const char *item_id_str) { - ImGuiContext &g = *GImGui; +static ImGuiID get_item_id(const char* window_substr, const char* item_id_str) { + ImGuiContext& g = *GImGui; ImGuiID popup_id = ImGui::GetID(window_substr); char popup_window_name[64]; snprintf(popup_window_name, sizeof(popup_window_name), "##Popup_%08x", popup_id); @@ -32,10 +32,10 @@ static ImGuiID get_item_id(const char *window_substr, const char *item_id_str) { return 0; } -static void click_item(const char *window_substr, const char *item_id_str) { +static void click_item(const char* window_substr, const char* item_id_str) { ImGuiID id = get_item_id(window_substr, item_id_str); if (id != 0) { - ImGuiContext &g = *GImGui; + ImGuiContext& g = *GImGui; g.NavActivateId = id; g.NavActivateDownId = id; g.NavActivatePressedId = id; @@ -244,7 +244,7 @@ TEST_F(PresetTest, test_pedal_board_menu_extended) { "+ Signal Splitter Node (1 In -> N-Out)", "+ Signal Mixer Node (N-In -> 1 Out)"}; - for (const auto &eff_name : effects_to_add) { + for (const auto& eff_name : effects_to_add) { ImGui::OpenPopup("AddPedalPopup"); TestAccessor::render_add_pedal_menu(board); advance_test_frame(); @@ -265,7 +265,7 @@ TEST_F(PresetTest, test_pedal_board_menu_extended) { const std::vector amp_models = {"Clean American", "British Crunch", "High Gain Modern", "Jazz Warm"}; - for (const auto &model_name : amp_models) { + for (const auto& model_name : amp_models) { ImGui::OpenPopup("AmpSelectorPopup"); TestAccessor::render_amp_selector(board); advance_test_frame(); diff --git a/tests/ui/test_pedal_widget.cpp b/tests/ui/test_pedal_widget.cpp index af1cfe3c6..afc4df0b5 100644 --- a/tests/ui/test_pedal_widget.cpp +++ b/tests/ui/test_pedal_widget.cpp @@ -163,3 +163,51 @@ TEST_F(PresetTest, test_pedal_widget_footswitch_click_interaction) { ImGui::End(); engine.shutdown(); } + +TEST_F(PresetTest, test_pedal_widget_analyzer_toggle_and_render) { + ScopedImGuiContext imgui; + AudioEngine engine; + ASSERT_TRUE(engine.initialize()); + + auto od = std::make_shared(); + PedalWidget widget(engine, od, 0); + + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(1024, 768)); + ImGui::Begin("TestWindow"); + + // 1. Initial State: Analyzer should be closed + ASSERT_FALSE(widget.analyzer_open_); + + // 2. Set to true and register analyzer manually + widget.analyzer_open_ = true; + ASSERT_TRUE(engine.register_pedal_analyzer(0)); + + // Feed mock data + std::vector input_samples(1024, 0.4f); + std::vector output_samples(1024, 0.8f); + engine.analyzer_capture_->capture_pedal(0, input_samples.data(), output_samples.data(), 1024); + + // Render 1: Should copy snapshot and draw the curves + widget.render(1.0f); + + // Render 1.5: Sequence number is now equal, should hit the else decay branch + widget.render(1.0f); + + // 3. Set to false and unregister analyzer + widget.analyzer_open_ = false; + engine.unregister_pedal_analyzer(0); + + // Render 2: Should render standard pedal + widget.render(1.0f); + + ImGui::End(); + + // 4. Test destructor behavior when analyzer is open + { + PedalWidget temp_widget(engine, od, 1); + temp_widget.analyzer_open_ = true; + } + + engine.shutdown(); +}