From a1a7b9b39fbef755ab3c487503502f97b71e2648 Mon Sep 17 00:00:00 2001 From: vansh-09 Date: Sun, 17 May 2026 20:29:16 +0530 Subject: [PATCH 1/7] feat: Add JACK audio backend for ultra-low latency on Linux (#58) --- CMakeLists.txt | 92 +++++++++++-- scripts/setup_dependencies.sh | 6 +- src/audio/audio_backend_jack.cpp | 19 +++ src/audio/audio_backend_jack_internal.h | 16 +++ src/audio/audio_backend_jack_lifecycle.cpp | 143 +++++++++++++++++++++ src/gui/pedal_widget_body.cpp | 2 +- tests/test_jack_backend.cpp | 42 ++++++ tests/test_json_serialization.cpp | 3 +- 8 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 src/audio/audio_backend_jack.cpp create mode 100644 src/audio/audio_backend_jack_internal.h create mode 100644 src/audio/audio_backend_jack_lifecycle.cpp create mode 100644 tests/test_jack_backend.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index de5297c2bf..e5bed92ccd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,16 @@ if(NOT PORTAUDIO_FOUND) "${CMAKE_SOURCE_DIR}/external/portaudio/lib") endif() +# Ensure PORTAUDIO_LIBRARIES is a full path when possible (helps macOS Homebrew installs) +if(PORTAUDIO_LIBRARIES) + if(NOT IS_ABSOLUTE "${PORTAUDIO_LIBRARIES}") + find_library(PORTAUDIO_LIB_FULL NAMES portaudio PATHS /opt/homebrew/lib /usr/local/lib /usr/lib) + if(PORTAUDIO_LIB_FULL) + set(PORTAUDIO_LIBRARIES ${PORTAUDIO_LIB_FULL} CACHE STRING "PortAudio library full path" FORCE) + endif() + endif() +endif() + # SDL2 if(PkgConfig_FOUND) pkg_check_modules(SDL2 sdl2) @@ -91,6 +101,21 @@ if(NOT SDL2_FOUND) endif() endif() +# pkg_check_modules can yield a bare library name (e.g. "SDL2") and a separate +# library directory. Add that directory explicitly on Homebrew/macOS builds and +# prefer a full path when available so the linker does not need to guess. +if(SDL2_LIBRARY_DIRS) + link_directories(${SDL2_LIBRARY_DIRS}) +endif() +if(SDL2_LIBRARIES) + if(NOT IS_ABSOLUTE "${SDL2_LIBRARIES}") + find_library(SDL2_LIB_FULL NAMES SDL2 PATHS /opt/homebrew/lib /usr/local/lib /usr/lib) + if(SDL2_LIB_FULL) + set(SDL2_LIBRARIES ${SDL2_LIB_FULL} CACHE STRING "SDL2 library full path" FORCE) + endif() + endif() +endif() + # RtMidi (MIDI input) # Skip auto-detection when paths are provided explicitly (e.g. CI -D flags). if(NOT RTMIDI_LIBRARIES) @@ -108,6 +133,17 @@ if(NOT RTMIDI_LIBRARIES) "${CMAKE_SOURCE_DIR}/external/rtmidi") endif() endif() +if(RTMIDI_LIBRARY_DIRS) + link_directories(${RTMIDI_LIBRARY_DIRS}) +endif() +if(RTMIDI_LIBRARIES) + if(NOT IS_ABSOLUTE "${RTMIDI_LIBRARIES}") + find_library(RTMIDI_LIB_FULL NAMES rtmidi PATHS /opt/homebrew/lib /usr/local/lib /usr/lib) + if(RTMIDI_LIB_FULL) + set(RTMIDI_LIBRARIES ${RTMIDI_LIB_FULL} CACHE STRING "RtMidi library full path" FORCE) + endif() + endif() +endif() # pkg_check_modules sets RTMIDI_LIBRARIES to just the name ("rtmidi") and # RTMIDI_LIBRARY_DIRS to the directory (/opt/homebrew/lib). Without # link_directories the linker never searches that path, so we add it @@ -212,6 +248,13 @@ endif() # --- Platform-specific sources (selected at configure time) --- # Mobile (Android/iOS) and Web use SDL audio backend + stub file dialog # Android 8+ uses Oboe (AAudio exclusive mode) for low-latency audio. +if(UNIX AND NOT APPLE) + set(DEFAULT_WITH_JACK ON) +else() + set(DEFAULT_WITH_JACK OFF) +endif() +option(WITH_JACK "Build with JACK audio backend support" ${DEFAULT_WITH_JACK}) + if(EMSCRIPTEN OR IOS) set(BACKEND_SOURCES src/audio/audio_backend_sdl.cpp) set(DIALOG_SOURCES src/gui/file_dialog_web.cpp) @@ -219,11 +262,20 @@ elseif(ANDROID) set(BACKEND_SOURCES src/audio/audio_backend_oboe.cpp) set(DIALOG_SOURCES src/gui/file_dialog_web.cpp) else() - set(BACKEND_SOURCES - src/audio/audio_backend_portaudio.cpp - src/audio/audio_backend_portaudio_lifecycle.cpp - src/audio/audio_backend_portaudio_devices.cpp - ) + if(WITH_JACK) + find_path(JACK_INCLUDE_DIRS jack/jack.h) + find_library(JACK_LIBRARIES NAMES jack) + set(BACKEND_SOURCES + src/audio/audio_backend_jack.cpp + src/audio/audio_backend_jack_lifecycle.cpp + ) + else() + set(BACKEND_SOURCES + src/audio/audio_backend_portaudio.cpp + src/audio/audio_backend_portaudio_lifecycle.cpp + src/audio/audio_backend_portaudio_devices.cpp + ) + endif() set(DIALOG_SOURCES src/gui/file_dialog_native.cpp src/gui/file_dialog_native_open.cpp @@ -445,6 +497,7 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS ${IMGUI_DIR} ${IMGUI_DIR}/backends ${PORTAUDIO_INCLUDE_DIRS} + $<$:${JACK_INCLUDE_DIRS}> ${SDL2_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIR} ${RTMIDI_INCLUDE_DIRS} @@ -456,11 +509,13 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS target_compile_definitions(Amplitron PRIVATE AMPLITRON_VERSION="${AMPLITRON_VERSION}" AMPLITRON_HAS_MIDI + $<$:WITH_JACK> ) target_link_libraries(Amplitron PRIVATE nlohmann_json::nlohmann_json ${PORTAUDIO_LIBRARIES} + $<$:${JACK_LIBRARIES}> ${SDL2_LIBRARIES} ${OPENGL_LIBRARIES} ${RTMIDI_LIBRARIES} @@ -496,6 +551,10 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS tests/test_unsaved_preset.cpp ) + if(WITH_JACK) + list(APPEND TEST_SOURCES tests/test_jack_backend.cpp) + endif() + # Effect + core sources needed by tests (no GUI, no main) set(CORE_SOURCES src/preset_manager.cpp @@ -506,10 +565,21 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS src/audio/audio_engine_chain.cpp src/audio/audio_engine_process.cpp src/audio/audio_engine_api.cpp - src/audio/audio_backend_portaudio.cpp - src/audio/audio_backend_portaudio_lifecycle.cpp - src/audio/audio_backend_portaudio_devices.cpp - src/audio/recorder.cpp + ) + + if(WITH_JACK) + list(APPEND CORE_SOURCES + src/audio/audio_backend_jack.cpp + src/audio/audio_backend_jack_lifecycle.cpp + ) + else() + list(APPEND CORE_SOURCES + src/audio/audio_backend_portaudio.cpp + src/audio/audio_backend_portaudio_lifecycle.cpp + src/audio/audio_backend_portaudio_devices.cpp + ) + endif() + list(APPEND CORE_SOURCES src/audio/recorder.cpp src/audio/recorder_session.cpp src/audio/recorder_io.cpp src/audio/recorder_wav.cpp @@ -561,10 +631,14 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS # DEFINE AMPLITRON_HAS_MIDI FOR TEST BUILDS # ============================================================ target_compile_definitions(amplitron-tests PRIVATE AMPLITRON_HAS_MIDI) + if(WITH_JACK) + target_compile_definitions(amplitron-tests PRIVATE WITH_JACK=1) + endif() target_link_libraries(amplitron-tests PRIVATE nlohmann_json::nlohmann_json ${PORTAUDIO_LIBRARIES} + $<$:${JACK_LIBRARIES}> ${SDL2_LIBRARIES} ${OPENGL_LIBRARIES} ${RTMIDI_LIBRARIES} diff --git a/scripts/setup_dependencies.sh b/scripts/setup_dependencies.sh index 91d75158cc..47b0f7b875 100755 --- a/scripts/setup_dependencies.sh +++ b/scripts/setup_dependencies.sh @@ -75,7 +75,8 @@ install_deps() { build-essential cmake pkg-config \ libportaudio2 portaudio19-dev \ libsdl2-dev \ - libgl1-mesa-dev + libgl1-mesa-dev \ + libjack-jackd2-dev elif command -v dnf &> /dev/null; then echo "Detected Fedora/RHEL. Installing dependencies..." sudo dnf install -y \ @@ -92,7 +93,8 @@ install_deps() { mesa elif command -v brew &> /dev/null; then echo "Detected macOS with Homebrew. Installing dependencies..." - brew install cmake portaudio sdl2 + brew update + brew install cmake portaudio rtmidi sdl2 pkg-config else echo "WARNING: Could not detect package manager." echo "Please install manually: cmake, portaudio, sdl2, opengl dev headers" diff --git a/src/audio/audio_backend_jack.cpp b/src/audio/audio_backend_jack.cpp new file mode 100644 index 0000000000..3527a87e84 --- /dev/null +++ b/src/audio/audio_backend_jack.cpp @@ -0,0 +1,19 @@ +// Minimal JACK backend factory and helpers +#include "audio/audio_backend.h" +#include "audio/audio_backend_jack_internal.h" +#include + +namespace Amplitron +{ + + AudioBackendState *create_audio_backend() + { + return new AudioBackendState(); + } + + void destroy_audio_backend(AudioBackendState *state) + { + delete state; + } + +} // namespace Amplitron diff --git a/src/audio/audio_backend_jack_internal.h b/src/audio/audio_backend_jack_internal.h new file mode 100644 index 0000000000..c4c174f115 --- /dev/null +++ b/src/audio/audio_backend_jack_internal.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace Amplitron +{ + + struct AudioBackendState + { + jack_client_t *client = nullptr; + jack_port_t *input_port = nullptr; + jack_port_t *output_port = nullptr; + int measured_latency_ms = 0; + }; + +} // namespace Amplitron diff --git a/src/audio/audio_backend_jack_lifecycle.cpp b/src/audio/audio_backend_jack_lifecycle.cpp new file mode 100644 index 0000000000..7d312c3736 --- /dev/null +++ b/src/audio/audio_backend_jack_lifecycle.cpp @@ -0,0 +1,143 @@ +// JACK backend — lifecycle and process callback +#include "audio/audio_engine.h" +#include "audio/audio_backend_jack_internal.h" +#include +#include +#include + +namespace Amplitron +{ + + // Static process callback + static int jack_process(jack_nframes_t nframes, void *arg) + { + AudioEngine *engine = static_cast(arg); + AudioBackendState *be = engine->backend_; + if (!be || !be->input_port || !be->output_port) + return 0; + + float *in = static_cast(jack_port_get_buffer(be->input_port, nframes)); + float *out = static_cast(jack_port_get_buffer(be->output_port, nframes)); + + if (!in || !out) + { + if (out) + std::memset(out, 0, nframes * 2 * sizeof(float)); + return 0; + } + + engine->process_audio(in, out, static_cast(nframes)); + return 0; + } + + bool AudioEngine::initialize() + { + // Try to open JACK client without auto-starting server + const char *client_name = "Amplitron"; + jack_options_t options = JackNoStartServer; + jack_status_t status; + + // Create backend state if not present + if (!backend_) + backend_ = create_audio_backend(); + + backend_->client = jack_client_open(client_name, options, &status, nullptr); + if (!backend_->client) + { + std::cerr << "JACK: Could not connect to JACK server (is it running?)" << std::endl; + destroy_audio_backend(backend_); + backend_ = nullptr; + return false; + } + + // Register process callback + if (jack_set_process_callback(backend_->client, jack_process, this) != 0) + { + std::cerr << "JACK: Failed to set process callback" << std::endl; + jack_client_close(backend_->client); + backend_->client = nullptr; + destroy_audio_backend(backend_); + backend_ = nullptr; + return false; + } + + // Register ports + backend_->input_port = jack_port_register(backend_->client, "in_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + backend_->output_port = jack_port_register(backend_->client, "out_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + if (!backend_->input_port || !backend_->output_port) + { + std::cerr << "JACK: Failed to register ports" << std::endl; + jack_client_close(backend_->client); + backend_->client = nullptr; + destroy_audio_backend(backend_); + backend_ = nullptr; + return false; + } + + // Retrieve sample rate and buffer size from JACK server + sample_rate_ = jack_get_sample_rate(backend_->client); + buffer_size_ = static_cast(jack_get_buffer_size(backend_->client)); + + initialized_ = true; + std::cout << "[JACK] Audio subsystem initialized. Rate=" << sample_rate_ << " Buffer=" << buffer_size_ << std::endl; + return true; + } + + void AudioEngine::shutdown() + { + stop(); + if (backend_) + { + if (backend_->client) + { + jack_client_close(backend_->client); + backend_->client = nullptr; + } + destroy_audio_backend(backend_); + backend_ = nullptr; + } + initialized_ = false; + } + + bool AudioEngine::start() + { + if (!initialized_ || running_) + return false; + if (!backend_ || !backend_->client) + return false; + + if (jack_activate(backend_->client) != 0) + { + std::cerr << "JACK: Failed to activate client" << std::endl; + return false; + } + + running_ = true; + std::cout << "[JACK] Audio stream started" << std::endl; + return true; + } + + void AudioEngine::stop() + { + if (initialized_ && running_) + { + if (backend_ && backend_->client) + { + jack_deactivate(backend_->client); + } + running_ = false; + } + } + + // Device name stubs + std::string AudioEngine::get_input_device_name() const { return "JACK Input"; } + std::string AudioEngine::get_output_device_name() const { return "JACK Output"; } + + std::vector AudioEngine::get_input_devices() const { return {{0, "JACK Input", 1, 0, (double)sample_rate_, false}}; } + + std::vector AudioEngine::get_output_devices() const { return {{0, "JACK Output", 0, 2, (double)sample_rate_, false}}; } + + bool AudioEngine::set_input_device(int) { return true; } + bool AudioEngine::set_output_device(int) { return true; } + +} // namespace Amplitron diff --git a/src/gui/pedal_widget_body.cpp b/src/gui/pedal_widget_body.cpp index 33299fd7ed..ddcd117f26 100644 --- a/src/gui/pedal_widget_body.cpp +++ b/src/gui/pedal_widget_body.cpp @@ -11,7 +11,7 @@ namespace Amplitron { -void PedalWidget::render_amp_cabinet(ImDrawList* dl, ImVec2 p0, ImVec2 p1, float pedal_width, float pedal_height) { +void PedalWidget::render_amp_cabinet(ImDrawList* dl, ImVec2 p0, ImVec2 p1, float pedal_width, float /*pedal_height*/) { ImU32 cab_body = IM_COL32(30, 22, 16, 255); ImU32 cab_border = IM_COL32(90, 70, 40, 255); ImU32 cab_grille = IM_COL32(18, 14, 10, 255); diff --git a/tests/test_jack_backend.cpp b/tests/test_jack_backend.cpp new file mode 100644 index 0000000000..208f082c4b --- /dev/null +++ b/tests/test_jack_backend.cpp @@ -0,0 +1,42 @@ +#ifdef WITH_JACK +#include "audio/audio_backend_jack_internal.h" +#include "audio/audio_engine.h" +#include +#include +#include + +using namespace Amplitron; + +TEST(JackBackend, InitializeAndStart) +{ + AudioEngine engine; + // initialization requires jackd running; this test is guarded by WITH_JACK + bool ok = engine.initialize(); + if (!ok) + { + GTEST_SKIP() << "jackd not running; skipping integration test"; + } + EXPECT_TRUE(ok); + EXPECT_TRUE(engine.start()); + engine.stop(); + engine.shutdown(); +} + +TEST(JackBackend, ProcessCallbackInvoked) +{ + AudioEngine engine; + if (!engine.initialize()) + { + GTEST_SKIP() << "jackd not running; skipping integration test"; + } + + std::atomic invoked{false}; + // Wrap engine->process_audio by installing a temporary effect? Simpler: spawn thread and rely on start() + EXPECT_TRUE(engine.start()); + // wait briefly + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + engine.stop(); + engine.shutdown(); +} + +#endif // WITH_JACK diff --git a/tests/test_json_serialization.cpp b/tests/test_json_serialization.cpp index 0129c9275f..c4bc6a5302 100644 --- a/tests/test_json_serialization.cpp +++ b/tests/test_json_serialization.cpp @@ -153,7 +153,8 @@ TEST(json_preset_roundtrip_via_string) { ASSERT_TRUE(!json_str.empty()); bool is_valid = true; try { - nlohmann::json::parse(json_str); + auto parsed = nlohmann::json::parse(json_str); + (void)parsed; } catch (...) { is_valid = false; } From 53a420122777cec2afc2dbb543b368201a593060 Mon Sep 17 00:00:00 2001 From: vansh-09 Date: Sun, 17 May 2026 20:41:18 +0530 Subject: [PATCH 2/7] style: include remaining formatting updates --- src/gui/pedal_widget_body.cpp | 507 ++++++++++++++++-------------- tests/test_json_serialization.cpp | 148 +++++---- 2 files changed, 349 insertions(+), 306 deletions(-) diff --git a/src/gui/pedal_widget_body.cpp b/src/gui/pedal_widget_body.cpp index ddcd117f26..511026f0ba 100644 --- a/src/gui/pedal_widget_body.cpp +++ b/src/gui/pedal_widget_body.cpp @@ -9,265 +9,288 @@ #include #include -namespace Amplitron { - -void PedalWidget::render_amp_cabinet(ImDrawList* dl, ImVec2 p0, ImVec2 p1, float pedal_width, float /*pedal_height*/) { - ImU32 cab_body = IM_COL32(30, 22, 16, 255); - ImU32 cab_border = IM_COL32(90, 70, 40, 255); - ImU32 cab_grille = IM_COL32(18, 14, 10, 255); - ImU32 cab_grille_line = IM_COL32(38, 30, 22, 180); - - dl->AddRectFilled(p0, p1, cab_body, Theme::ROUNDING_MD); - dl->AddRect(p0, p1, cab_border, Theme::ROUNDING_MD, 0, 2.5f); - - dl->AddRectFilled( - ImVec2(p0.x + 6, p0.y + 6), - ImVec2(p1.x - 6, p0.y + 10), - Theme::ACCENT_GOLD_DIM, 2.0f); - - ImVec2 plate_p0 = ImVec2(p0.x + 8, p0.y + 14); - ImVec2 plate_p1 = ImVec2(p1.x - 8, p0.y + 50); - dl->AddRectFilled(plate_p0, plate_p1, - IM_COL32(46, 38, 28, 220), Theme::ROUNDING_SM); - dl->AddRect(plate_p0, plate_p1, - IM_COL32(70, 58, 38, 180), Theme::ROUNDING_SM, 0, 1.0f); - - ImGui::SetCursorScreenPos(ImVec2(p0.x + 12, p0.y + 18)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::Gold()); - ImGui::Text("AMP"); - ImGui::PopStyleColor(); - - int model_idx = static_cast(effect_->params()[0].value); - const auto& models = get_amp_models(); - const char* model_name = "Unknown"; - if (model_idx >= 0 && model_idx < static_cast(models.size())) { - model_name = models[model_idx].name; - } - ImVec2 mn_size = ImGui::CalcTextSize(model_name); - float mn_x = p0.x + (pedal_width - mn_size.x) * 0.5f; - ImGui::SetCursorScreenPos(ImVec2(mn_x, p0.y + 33)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextPrimary()); - ImGui::Text("%s", model_name); - ImGui::PopStyleColor(); - - float led_x = p1.x - 22; - float led_y = p0.y + 26; - dl->AddCircleFilled(ImVec2(led_x, led_y), 5, Theme::LED_GREEN); - dl->AddCircleFilled(ImVec2(led_x, led_y), 8, Theme::LED_GREEN_GLOW & 0x30FFFFFF); - - float grille_top = p1.y - 100; - float grille_bottom = p1.y - 12; - float grille_left = p0.x + 12; - float grille_right = p1.x - 12; - - dl->AddRectFilled( - ImVec2(grille_left, grille_top), - ImVec2(grille_right, grille_bottom), - cab_grille, Theme::ROUNDING_SM); - dl->AddRect( - ImVec2(grille_left, grille_top), - ImVec2(grille_right, grille_bottom), - IM_COL32(50, 40, 28, 180), Theme::ROUNDING_SM, 0, 1.0f); - - for (float gy = grille_top + 6; gy < grille_bottom - 4; gy += 5.0f) { - dl->AddLine( - ImVec2(grille_left + 4, gy), - ImVec2(grille_right - 4, gy), - cab_grille_line, 1.0f); - } +namespace Amplitron +{ + + void PedalWidget::render_amp_cabinet(ImDrawList *dl, ImVec2 p0, ImVec2 p1, float pedal_width, float /*pedal_height*/) + { + ImU32 cab_body = IM_COL32(30, 22, 16, 255); + ImU32 cab_border = IM_COL32(90, 70, 40, 255); + ImU32 cab_grille = IM_COL32(18, 14, 10, 255); + ImU32 cab_grille_line = IM_COL32(38, 30, 22, 180); + + dl->AddRectFilled(p0, p1, cab_body, Theme::ROUNDING_MD); + dl->AddRect(p0, p1, cab_border, Theme::ROUNDING_MD, 0, 2.5f); + + dl->AddRectFilled( + ImVec2(p0.x + 6, p0.y + 6), + ImVec2(p1.x - 6, p0.y + 10), + Theme::ACCENT_GOLD_DIM, 2.0f); + + ImVec2 plate_p0 = ImVec2(p0.x + 8, p0.y + 14); + ImVec2 plate_p1 = ImVec2(p1.x - 8, p0.y + 50); + dl->AddRectFilled(plate_p0, plate_p1, + IM_COL32(46, 38, 28, 220), Theme::ROUNDING_SM); + dl->AddRect(plate_p0, plate_p1, + IM_COL32(70, 58, 38, 180), Theme::ROUNDING_SM, 0, 1.0f); + + ImGui::SetCursorScreenPos(ImVec2(p0.x + 12, p0.y + 18)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::Gold()); + ImGui::Text("AMP"); + ImGui::PopStyleColor(); - dl->AddRectFilled( - ImVec2(p0.x + 6, p1.y - 10), - ImVec2(p1.x - 6, p1.y - 6), - Theme::ACCENT_GOLD_DIM, 2.0f); -} - -void PedalWidget::render_tuner_display(ImDrawList* dl, ImVec2 p0, float pedal_width) { - auto* tuner = dynamic_cast(effect_.get()); - if (tuner) { - float cx = p0.x + pedal_width * 0.5f; - - bool has_signal = tuner->signal_detected.load(std::memory_order_relaxed); - int note_idx = tuner->detected_note.load(std::memory_order_relaxed); - int octave = tuner->detected_octave.load(std::memory_order_relaxed); - float cents = tuner->detected_cents.load(std::memory_order_relaxed); - float freq = tuner->detected_freq.load(std::memory_order_relaxed); - - float display_y = p0.y + 55; - - if (has_signal && note_idx >= 0) { - char note_buf[16]; - snprintf(note_buf, sizeof(note_buf), "%s%d", - TunerPedal::note_name(note_idx), octave); - ImVec2 note_size = ImGui::CalcTextSize(note_buf); - float note_x = cx - note_size.x * 1.5f; - dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f, - ImVec2(note_x, display_y), - Theme::TEXT_PRIMARY, note_buf); - - display_y += 45; - - char cents_buf[32]; - snprintf(cents_buf, sizeof(cents_buf), "%+.1f cents", cents); - ImVec2 cents_text_size = ImGui::CalcTextSize(cents_buf); - ImGui::SetCursorScreenPos(ImVec2(cx - cents_text_size.x * 0.5f, display_y)); - float abs_cents = std::fabs(cents); - ImVec4 cents_col = (abs_cents < 2.0f) - ? ImVec4(0.2f, 0.9f, 0.3f, 1.0f) - : (abs_cents < 15.0f) - ? ImVec4(0.9f, 0.8f, 0.2f, 1.0f) - : ImVec4(0.9f, 0.2f, 0.2f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_Text, cents_col); - ImGui::TextUnformatted(cents_buf); - ImGui::PopStyleColor(); + int model_idx = static_cast(effect_->params()[0].value); + const auto &models = get_amp_models(); + const char *model_name = "Unknown"; + if (model_idx >= 0 && model_idx < static_cast(models.size())) + { + model_name = models[model_idx].name; + } + ImVec2 mn_size = ImGui::CalcTextSize(model_name); + float mn_x = p0.x + (pedal_width - mn_size.x) * 0.5f; + ImGui::SetCursorScreenPos(ImVec2(mn_x, p0.y + 33)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextPrimary()); + ImGui::Text("%s", model_name); + ImGui::PopStyleColor(); - display_y += 22; - - float bar_w = pedal_width - 30; - float bar_h = 10; - float bar_x = p0.x + 15; - float bar_y = display_y; - dl->AddRectFilled( - ImVec2(bar_x, bar_y), - ImVec2(bar_x + bar_w, bar_y + bar_h), - Theme::KNOB_BG, 3.0f); - float center_x = bar_x + bar_w * 0.5f; + float led_x = p1.x - 22; + float led_y = p0.y + 26; + dl->AddCircleFilled(ImVec2(led_x, led_y), 5, Theme::LED_GREEN); + dl->AddCircleFilled(ImVec2(led_x, led_y), 8, Theme::LED_GREEN_GLOW & 0x30FFFFFF); + + float grille_top = p1.y - 100; + float grille_bottom = p1.y - 12; + float grille_left = p0.x + 12; + float grille_right = p1.x - 12; + + dl->AddRectFilled( + ImVec2(grille_left, grille_top), + ImVec2(grille_right, grille_bottom), + cab_grille, Theme::ROUNDING_SM); + dl->AddRect( + ImVec2(grille_left, grille_top), + ImVec2(grille_right, grille_bottom), + IM_COL32(50, 40, 28, 180), Theme::ROUNDING_SM, 0, 1.0f); + + for (float gy = grille_top + 6; gy < grille_bottom - 4; gy += 5.0f) + { dl->AddLine( - ImVec2(center_x, bar_y - 1), - ImVec2(center_x, bar_y + bar_h + 1), - Theme::TEXT_DIM, 1.5f); - float needle_norm = clamp(cents / 50.0f, -1.0f, 1.0f); - float needle_x = center_x + needle_norm * (bar_w * 0.5f); - ImU32 needle_col = ImGui::ColorConvertFloat4ToU32(cents_col); - dl->AddRectFilled( - ImVec2(needle_x - 3, bar_y - 2), - ImVec2(needle_x + 3, bar_y + bar_h + 2), - needle_col, 2.0f); - - display_y += bar_h + 14; - - char freq_buf[32]; - snprintf(freq_buf, sizeof(freq_buf), "%.1f Hz", freq); - ImVec2 freq_size = ImGui::CalcTextSize(freq_buf); - ImGui::SetCursorScreenPos(ImVec2(cx - freq_size.x * 0.5f, display_y)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary()); - ImGui::TextUnformatted(freq_buf); - ImGui::PopStyleColor(); - - display_y += 22; - } else { - const char* no_sig = "---"; - ImVec2 ns_size = ImGui::CalcTextSize(no_sig); - dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f, - ImVec2(cx - ns_size.x * 1.5f, display_y), - Theme::TEXT_DIM, no_sig); - display_y += 45; - - const char* waiting = "Play a note..."; - ImVec2 wt_size = ImGui::CalcTextSize(waiting); - ImGui::SetCursorScreenPos(ImVec2(cx - wt_size.x * 0.5f, display_y)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim()); - ImGui::TextUnformatted(waiting); - ImGui::PopStyleColor(); - - display_y += 22; + ImVec2(grille_left + 4, gy), + ImVec2(grille_right - 4, gy), + cab_grille_line, 1.0f); } - display_y += 8; - bool mute_on = effect_->params()[0].value >= 0.5f; - const char* mute_label = mute_on ? "[MUTE ON]" : "[MUTE OFF]"; - ImVec2 ml_size = ImGui::CalcTextSize(mute_label); - ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y)); - ImGui::PushStyleColor(ImGuiCol_Text, - mute_on ? ImVec4(0.9f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 0.7f, 0.3f, 1.0f)); - ImGui::TextUnformatted(mute_label); - ImGui::PopStyleColor(); + dl->AddRectFilled( + ImVec2(p0.x + 6, p1.y - 10), + ImVec2(p1.x - 6, p1.y - 6), + Theme::ACCENT_GOLD_DIM, 2.0f); + } - ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y)); - ImGui::SetNextItemAllowOverlap(); - ImGui::InvisibleButton("##tuner_mute_toggle", ml_size); - if (ImGui::IsItemClicked()) { - float new_val = mute_on ? 0.0f : 1.0f; - effect_->params()[0].value = new_val; - engine_.push_param_change(index_, 0, new_val); - } - if (ImGui::IsItemHovered()) { - if (!effect_->params()[0].tooltip.empty()) { - ImGui::SetTooltip("Click to toggle mute\n\n%s", effect_->params()[0].tooltip.c_str()); - } else { - ImGui::SetTooltip("Click to toggle mute"); + void PedalWidget::render_tuner_display(ImDrawList *dl, ImVec2 p0, float pedal_width) + { + auto *tuner = dynamic_cast(effect_.get()); + if (tuner) + { + float cx = p0.x + pedal_width * 0.5f; + + bool has_signal = tuner->signal_detected.load(std::memory_order_relaxed); + int note_idx = tuner->detected_note.load(std::memory_order_relaxed); + int octave = tuner->detected_octave.load(std::memory_order_relaxed); + float cents = tuner->detected_cents.load(std::memory_order_relaxed); + float freq = tuner->detected_freq.load(std::memory_order_relaxed); + + float display_y = p0.y + 55; + + if (has_signal && note_idx >= 0) + { + char note_buf[16]; + snprintf(note_buf, sizeof(note_buf), "%s%d", + TunerPedal::note_name(note_idx), octave); + ImVec2 note_size = ImGui::CalcTextSize(note_buf); + float note_x = cx - note_size.x * 1.5f; + dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f, + ImVec2(note_x, display_y), + Theme::TEXT_PRIMARY, note_buf); + + display_y += 45; + + char cents_buf[32]; + snprintf(cents_buf, sizeof(cents_buf), "%+.1f cents", cents); + ImVec2 cents_text_size = ImGui::CalcTextSize(cents_buf); + ImGui::SetCursorScreenPos(ImVec2(cx - cents_text_size.x * 0.5f, display_y)); + float abs_cents = std::fabs(cents); + ImVec4 cents_col = (abs_cents < 2.0f) + ? ImVec4(0.2f, 0.9f, 0.3f, 1.0f) + : (abs_cents < 15.0f) + ? ImVec4(0.9f, 0.8f, 0.2f, 1.0f) + : ImVec4(0.9f, 0.2f, 0.2f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Text, cents_col); + ImGui::TextUnformatted(cents_buf); + ImGui::PopStyleColor(); + + display_y += 22; + + float bar_w = pedal_width - 30; + float bar_h = 10; + float bar_x = p0.x + 15; + float bar_y = display_y; + dl->AddRectFilled( + ImVec2(bar_x, bar_y), + ImVec2(bar_x + bar_w, bar_y + bar_h), + Theme::KNOB_BG, 3.0f); + float center_x = bar_x + bar_w * 0.5f; + dl->AddLine( + ImVec2(center_x, bar_y - 1), + ImVec2(center_x, bar_y + bar_h + 1), + Theme::TEXT_DIM, 1.5f); + float needle_norm = clamp(cents / 50.0f, -1.0f, 1.0f); + float needle_x = center_x + needle_norm * (bar_w * 0.5f); + ImU32 needle_col = ImGui::ColorConvertFloat4ToU32(cents_col); + dl->AddRectFilled( + ImVec2(needle_x - 3, bar_y - 2), + ImVec2(needle_x + 3, bar_y + bar_h + 2), + needle_col, 2.0f); + + display_y += bar_h + 14; + + char freq_buf[32]; + snprintf(freq_buf, sizeof(freq_buf), "%.1f Hz", freq); + ImVec2 freq_size = ImGui::CalcTextSize(freq_buf); + ImGui::SetCursorScreenPos(ImVec2(cx - freq_size.x * 0.5f, display_y)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary()); + ImGui::TextUnformatted(freq_buf); + ImGui::PopStyleColor(); + + display_y += 22; } - } - } -} - -void PedalWidget::render_ir_cabinet_display(ImVec2 p0, float pedal_width) { - auto* ir_cab = dynamic_cast(effect_.get()); - if (ir_cab) { - float cx = p0.x + pedal_width * 0.5f; - float display_y = p0.y + 50; - - float btn_w = pedal_width - 30; - ImGui::SetCursorScreenPos(ImVec2(p0.x + 15, display_y)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.20f, 0.16f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.30f, 0.18f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.42f, 0.20f, 1.0f)); - char load_id[64]; - snprintf(load_id, sizeof(load_id), "Load IR##ir_load_%d", index_); - if (ImGui::Button(load_id, ImVec2(btn_w, 22))) { - std::string path = show_open_dialog("Load Impulse Response", - "WAV Audio", "wav"); - if (!path.empty()) { - ir_cab->load_ir(path); + else + { + const char *no_sig = "---"; + ImVec2 ns_size = ImGui::CalcTextSize(no_sig); + dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f, + ImVec2(cx - ns_size.x * 1.5f, display_y), + Theme::TEXT_DIM, no_sig); + display_y += 45; + + const char *waiting = "Play a note..."; + ImVec2 wt_size = ImGui::CalcTextSize(waiting); + ImGui::SetCursorScreenPos(ImVec2(cx - wt_size.x * 0.5f, display_y)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim()); + ImGui::TextUnformatted(waiting); + ImGui::PopStyleColor(); + + display_y += 22; } - } - ImGui::PopStyleColor(3); - display_y += 28; - - if (ir_cab->has_ir()) { - const std::string& ir_name = ir_cab->ir_name(); - std::string display_name = ir_name; - if (display_name.size() > 20) { - display_name = display_name.substr(0, 17) + "..."; - } - ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str()); - ImGui::SetCursorScreenPos(ImVec2(cx - name_size.x * 0.5f, display_y)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextPrimary()); - ImGui::TextUnformatted(display_name.c_str()); + display_y += 8; + bool mute_on = effect_->params()[0].value >= 0.5f; + const char *mute_label = mute_on ? "[MUTE ON]" : "[MUTE OFF]"; + ImVec2 ml_size = ImGui::CalcTextSize(mute_label); + ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y)); + ImGui::PushStyleColor(ImGuiCol_Text, + mute_on ? ImVec4(0.9f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 0.7f, 0.3f, 1.0f)); + ImGui::TextUnformatted(mute_label); ImGui::PopStyleColor(); - display_y += 18; - - char dur_buf[32]; - snprintf(dur_buf, sizeof(dur_buf), "%.1f ms", ir_cab->ir_duration_ms()); - ImVec2 dur_size = ImGui::CalcTextSize(dur_buf); - ImGui::SetCursorScreenPos(ImVec2(cx - dur_size.x * 0.5f, display_y)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary()); - ImGui::TextUnformatted(dur_buf); - ImGui::PopStyleColor(); + ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y)); + ImGui::SetNextItemAllowOverlap(); + ImGui::InvisibleButton("##tuner_mute_toggle", ml_size); + if (ImGui::IsItemClicked()) + { + float new_val = mute_on ? 0.0f : 1.0f; + effect_->params()[0].value = new_val; + engine_.push_param_change(index_, 0, new_val); + } + if (ImGui::IsItemHovered()) + { + if (!effect_->params()[0].tooltip.empty()) + { + ImGui::SetTooltip("Click to toggle mute\n\n%s", effect_->params()[0].tooltip.c_str()); + } + else + { + ImGui::SetTooltip("Click to toggle mute"); + } + } + } + } - display_y += 22; + void PedalWidget::render_ir_cabinet_display(ImVec2 p0, float pedal_width) + { + auto *ir_cab = dynamic_cast(effect_.get()); + if (ir_cab) + { + float cx = p0.x + pedal_width * 0.5f; + float display_y = p0.y + 50; + float btn_w = pedal_width - 30; ImGui::SetCursorScreenPos(ImVec2(p0.x + 15, display_y)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.12f, 0.10f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.15f, 0.12f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.20f, 0.15f, 1.0f)); - char clear_id[64]; - snprintf(clear_id, sizeof(clear_id), "Clear##ir_clear_%d", index_); - if (ImGui::Button(clear_id, ImVec2(btn_w, 20))) { - ir_cab->clear_ir(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.20f, 0.16f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.30f, 0.18f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.42f, 0.20f, 1.0f)); + char load_id[64]; + snprintf(load_id, sizeof(load_id), "Load IR##ir_load_%d", index_); + if (ImGui::Button(load_id, ImVec2(btn_w, 22))) + { + std::string path = show_open_dialog("Load Impulse Response", + "WAV Audio", "wav"); + if (!path.empty()) + { + ir_cab->load_ir(path); + } } ImGui::PopStyleColor(3); - } else { - const char* no_ir = "No IR loaded"; - ImVec2 ni_size = ImGui::CalcTextSize(no_ir); - ImGui::SetCursorScreenPos(ImVec2(cx - ni_size.x * 0.5f, display_y)); - ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim()); - ImGui::TextUnformatted(no_ir); - ImGui::PopStyleColor(); + + display_y += 28; + + if (ir_cab->has_ir()) + { + const std::string &ir_name = ir_cab->ir_name(); + std::string display_name = ir_name; + if (display_name.size() > 20) + { + display_name = display_name.substr(0, 17) + "..."; + } + ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str()); + ImGui::SetCursorScreenPos(ImVec2(cx - name_size.x * 0.5f, display_y)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextPrimary()); + ImGui::TextUnformatted(display_name.c_str()); + ImGui::PopStyleColor(); + + display_y += 18; + + char dur_buf[32]; + snprintf(dur_buf, sizeof(dur_buf), "%.1f ms", ir_cab->ir_duration_ms()); + ImVec2 dur_size = ImGui::CalcTextSize(dur_buf); + ImGui::SetCursorScreenPos(ImVec2(cx - dur_size.x * 0.5f, display_y)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary()); + ImGui::TextUnformatted(dur_buf); + ImGui::PopStyleColor(); + + display_y += 22; + + ImGui::SetCursorScreenPos(ImVec2(p0.x + 15, display_y)); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.12f, 0.10f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.15f, 0.12f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.20f, 0.15f, 1.0f)); + char clear_id[64]; + snprintf(clear_id, sizeof(clear_id), "Clear##ir_clear_%d", index_); + if (ImGui::Button(clear_id, ImVec2(btn_w, 20))) + { + ir_cab->clear_ir(); + } + ImGui::PopStyleColor(3); + } + else + { + const char *no_ir = "No IR loaded"; + ImVec2 ni_size = ImGui::CalcTextSize(no_ir); + ImGui::SetCursorScreenPos(ImVec2(cx - ni_size.x * 0.5f, display_y)); + ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim()); + ImGui::TextUnformatted(no_ir); + ImGui::PopStyleColor(); + } } } -} } // namespace Amplitron diff --git a/tests/test_json_serialization.cpp b/tests/test_json_serialization.cpp index c4bc6a5302..1e86ec8af4 100644 --- a/tests/test_json_serialization.cpp +++ b/tests/test_json_serialization.cpp @@ -39,45 +39,46 @@ using namespace Amplitron; // Helpers // ----------------------------------------------------------------------- -static PresetData make_test_preset() { +static PresetData make_test_preset() +{ PresetData p; - p.name = "Test Preset"; - p.description = "Created by test_json_serialization"; - p.input_gain = 0.75f; - p.output_gain = 0.85f; + p.name = "Test Preset"; + p.description = "Created by test_json_serialization"; + p.input_gain = 0.75f; + p.output_gain = 0.85f; { PresetData::EffectData fx; - fx.type = "Noise Gate"; + fx.type = "Noise Gate"; fx.enabled = true; - fx.mix = 1.0f; - fx.params = {{"Threshold", -40.0f}, {"Attack", 0.5f}, {"Release", 50.0f}}; + fx.mix = 1.0f; + fx.params = {{"Threshold", -40.0f}, {"Attack", 0.5f}, {"Release", 50.0f}}; p.effects.push_back(fx); } { PresetData::EffectData fx; - fx.type = "Overdrive"; + fx.type = "Overdrive"; fx.enabled = false; - fx.mix = 0.8f; - fx.params = {{"Drive", 2.5f}, {"Tone", 0.6f}, {"Level", 0.7f}}; + fx.mix = 0.8f; + fx.params = {{"Drive", 2.5f}, {"Tone", 0.6f}, {"Level", 0.7f}}; p.effects.push_back(fx); } { PresetData::EffectData fx; - fx.type = "IR Cabinet"; - fx.enabled = true; - fx.mix = 1.0f; + fx.type = "IR Cabinet"; + fx.enabled = true; + fx.mix = 1.0f; fx.metadata = {{"ir_path", "/some/path/cabinet.wav"}}; p.effects.push_back(fx); } MidiMapping m; - m.cc_number = 74; + m.cc_number = 74; m.midi_channel = 0; - m.target_type = MidiTargetType::EffectParam; - m.mode = MidiMappingMode::Continuous; - m.effect_name = "Overdrive"; - m.param_name = "Drive"; + m.target_type = MidiTargetType::EffectParam; + m.mode = MidiMappingMode::Continuous; + m.effect_name = "Overdrive"; + m.param_name = "Drive"; p.midi_mappings.push_back(m); return p; @@ -87,12 +88,13 @@ static PresetData make_test_preset() { // [AC2] Effect parameter serialization // ----------------------------------------------------------------------- -TEST(json_effect_data_to_json_contains_expected_keys) { +TEST(json_effect_data_to_json_contains_expected_keys) +{ PresetData::EffectData fx; - fx.type = "Reverb"; + fx.type = "Reverb"; fx.enabled = true; - fx.mix = 0.5f; - fx.params = {{"Decay", 0.8f}, {"Damp", 0.3f}, {"Level", 0.4f}}; + fx.mix = 0.5f; + fx.params = {{"Decay", 0.8f}, {"Damp", 0.3f}, {"Level", 0.4f}}; nlohmann::json j; Amplitron::to_json(j, fx); @@ -107,12 +109,13 @@ TEST(json_effect_data_to_json_contains_expected_keys) { ASSERT_NEAR(j["params"]["Decay"].get(), 0.8f, 0.001f); } -TEST(json_effect_data_roundtrip) { +TEST(json_effect_data_roundtrip) +{ PresetData::EffectData original; - original.type = "Equalizer"; + original.type = "Equalizer"; original.enabled = true; - original.mix = 0.9f; - original.params = {{"Bass", 3.0f}, {"Mid", -2.0f}, {"Treble", 1.5f}}; + original.mix = 0.9f; + original.params = {{"Bass", 3.0f}, {"Mid", -2.0f}, {"Treble", 1.5f}}; original.metadata = {{"custom_key", "custom_value"}}; // Serialise @@ -123,13 +126,14 @@ TEST(json_effect_data_roundtrip) { PresetData::EffectData restored; Amplitron::from_json(j, restored); - ASSERT_EQ(restored.type, original.type); + ASSERT_EQ(restored.type, original.type); ASSERT_EQ(restored.enabled, original.enabled); - ASSERT_NEAR(restored.mix, original.mix, 0.001f); + ASSERT_NEAR(restored.mix, original.mix, 0.001f); ASSERT_EQ(restored.params.size(), original.params.size()); // Verify each parameter - for (size_t i = 0; i < original.params.size(); ++i) { + for (size_t i = 0; i < original.params.size(); ++i) + { ASSERT_EQ(restored.params[i].first, original.params[i].first); ASSERT_NEAR(restored.params[i].second, original.params[i].second, 0.001f); } @@ -143,7 +147,8 @@ TEST(json_effect_data_roundtrip) { // [AC3] Full preset round-trip (string → parse → re-serialise) // ----------------------------------------------------------------------- -TEST(json_preset_roundtrip_via_string) { +TEST(json_preset_roundtrip_via_string) +{ PresetData original = make_test_preset(); // Serialise to JSON string @@ -152,10 +157,13 @@ TEST(json_preset_roundtrip_via_string) { // Validate it is parseable as valid JSON ASSERT_TRUE(!json_str.empty()); bool is_valid = true; - try { + try + { auto parsed = nlohmann::json::parse(json_str); (void)parsed; - } catch (...) { + } + catch (...) + { is_valid = false; } ASSERT_TRUE(is_valid); @@ -166,40 +174,41 @@ TEST(json_preset_roundtrip_via_string) { ASSERT_TRUE(ok); // Top-level fields - ASSERT_EQ(restored.name, original.name); + ASSERT_EQ(restored.name, original.name); ASSERT_EQ(restored.description, original.description); - ASSERT_NEAR(restored.input_gain, original.input_gain, 0.001f); + ASSERT_NEAR(restored.input_gain, original.input_gain, 0.001f); ASSERT_NEAR(restored.output_gain, original.output_gain, 0.001f); // Effect chain length ASSERT_EQ(restored.effects.size(), original.effects.size()); // First effect: Noise Gate - ASSERT_EQ(restored.effects[0].type, std::string("Noise Gate")); + ASSERT_EQ(restored.effects[0].type, std::string("Noise Gate")); ASSERT_EQ(restored.effects[0].enabled, true); - ASSERT_NEAR(restored.effects[0].mix, 1.0f, 0.001f); + ASSERT_NEAR(restored.effects[0].mix, 1.0f, 0.001f); ASSERT_EQ(restored.effects[0].params.size(), 3u); ASSERT_NEAR(restored.effects[0].params[0].second, -40.0f, 0.001f); // Second effect: Overdrive (disabled) - ASSERT_EQ(restored.effects[1].type, std::string("Overdrive")); + ASSERT_EQ(restored.effects[1].type, std::string("Overdrive")); ASSERT_EQ(restored.effects[1].enabled, false); - ASSERT_NEAR(restored.effects[1].mix, 0.8f, 0.001f); + ASSERT_NEAR(restored.effects[1].mix, 0.8f, 0.001f); // Third effect: IR Cabinet with metadata - ASSERT_EQ(restored.effects[2].type, std::string("IR Cabinet")); + ASSERT_EQ(restored.effects[2].type, std::string("IR Cabinet")); ASSERT_EQ(restored.effects[2].metadata.count("ir_path"), 1u); ASSERT_EQ(restored.effects[2].metadata.at("ir_path"), std::string("/some/path/cabinet.wav")); // MIDI mappings ASSERT_EQ(restored.midi_mappings.size(), 1u); - ASSERT_EQ(restored.midi_mappings[0].cc_number, 74); + ASSERT_EQ(restored.midi_mappings[0].cc_number, 74); ASSERT_EQ(restored.midi_mappings[0].effect_name, std::string("Overdrive")); - ASSERT_EQ(restored.midi_mappings[0].param_name, std::string("Drive")); + ASSERT_EQ(restored.midi_mappings[0].param_name, std::string("Drive")); } -TEST(json_roundtrip_via_file) { +TEST(json_roundtrip_via_file) +{ PresetData original = make_test_preset(); original.name = "FileRoundtripTest"; @@ -221,7 +230,7 @@ TEST(json_roundtrip_via_file) { ASSERT_TRUE(loaded); ASSERT_EQ(static_cast(engine.effects().size()), 2); // IR Cabinet skipped (no real IR) - ASSERT_NEAR(engine.get_input_gain(), 0.75f, 0.01f); + ASSERT_NEAR(engine.get_input_gain(), 0.75f, 0.01f); ASSERT_NEAR(engine.get_output_gain(), 0.85f, 0.01f); std::remove(path.c_str()); @@ -232,7 +241,8 @@ TEST(json_roundtrip_via_file) { // [AC3] Proof-of-concept: dump default signal chain as JSON to stdout // ----------------------------------------------------------------------- -TEST(json_preset_signal_chain_dump) { +TEST(json_preset_signal_chain_dump) +{ // Build the default signal chain that Amplitron starts with AudioEngine engine; engine.initialize(); @@ -249,16 +259,18 @@ TEST(json_preset_signal_chain_dump) { // Collect state into a PresetData (same logic as PresetManager::save_preset) PresetData state; - state.name = "Default Signal Chain"; + state.name = "Default Signal Chain"; state.description = "Proof-of-concept dump for issue #96"; - state.input_gain = engine.get_input_gain(); + state.input_gain = engine.get_input_gain(); state.output_gain = engine.get_output_gain(); - for (auto& fx : engine.effects()) { + for (auto &fx : engine.effects()) + { PresetData::EffectData fd; - fd.type = fx->name(); + fd.type = fx->name(); fd.enabled = fx->is_enabled(); - fd.mix = fx->get_mix(); - for (auto& p : fx->params()) { + fd.mix = fx->get_mix(); + for (auto &p : fx->params()) + { fd.params.push_back({p.name, p.value}); } state.effects.push_back(fd); @@ -279,7 +291,8 @@ TEST(json_preset_signal_chain_dump) { ASSERT_EQ(j["effects"].size(), 6u); // Every effect block must have type, enabled, mix, params - for (auto& jfx : j["effects"]) { + for (auto &jfx : j["effects"]) + { ASSERT_TRUE(jfx.contains("type")); ASSERT_TRUE(jfx.contains("enabled")); ASSERT_TRUE(jfx.contains("mix")); @@ -291,13 +304,15 @@ TEST(json_preset_signal_chain_dump) { bool ok = from_json_ext(json_str, restored); ASSERT_TRUE(ok); ASSERT_EQ(restored.effects.size(), state.effects.size()); - for (size_t i = 0; i < state.effects.size(); ++i) { - ASSERT_EQ(restored.effects[i].type, state.effects[i].type); + for (size_t i = 0; i < state.effects.size(); ++i) + { + ASSERT_EQ(restored.effects[i].type, state.effects[i].type); ASSERT_EQ(restored.effects[i].enabled, state.effects[i].enabled); - ASSERT_NEAR(restored.effects[i].mix, state.effects[i].mix, 0.001f); + ASSERT_NEAR(restored.effects[i].mix, state.effects[i].mix, 0.001f); ASSERT_EQ(restored.effects[i].params.size(), state.effects[i].params.size()); - for (size_t p = 0; p < state.effects[i].params.size(); ++p) { + for (size_t p = 0; p < state.effects[i].params.size(); ++p) + { ASSERT_EQ(restored.effects[i].params[p].first, state.effects[i].params[p].first); ASSERT_NEAR(restored.effects[i].params[p].second, @@ -308,16 +323,18 @@ TEST(json_preset_signal_chain_dump) { engine.shutdown(); } -TEST(json_invalid_json_returns_false) { +TEST(json_invalid_json_returns_false) +{ PresetData preset; bool ok = from_json_ext("{ this is not valid json !!!", preset); ASSERT_FALSE(ok); } -TEST(json_empty_effects_list_roundtrip) { +TEST(json_empty_effects_list_roundtrip) +{ PresetData original; - original.name = "Empty Chain"; - original.input_gain = 0.5f; + original.name = "Empty Chain"; + original.input_gain = 0.5f; original.output_gain = 0.5f; // No effects, no midi mappings @@ -330,7 +347,8 @@ TEST(json_empty_effects_list_roundtrip) { ASSERT_EQ(restored.midi_mappings.size(), 0u); } -TEST(json_can_load_existing_factory_presets) { +TEST(json_can_load_existing_factory_presets) +{ // Verify the new parser is backward-compatible with the existing preset files const std::vector factory_presets = { "presets/01_Sparkling_Clean.json", @@ -340,12 +358,14 @@ TEST(json_can_load_existing_factory_presets) { int loaded_count = 0; - for (const auto& path : factory_presets) { + for (const auto &path : factory_presets) + { std::ifstream f(path); - if (!f.is_open()) continue; // Skip if not found in test environment + if (!f.is_open()) + continue; // Skip if not found in test environment std::string content((std::istreambuf_iterator(f)), - std::istreambuf_iterator()); + std::istreambuf_iterator()); f.close(); PresetData preset; From be7a85b107e2658c5301d7eaaa7804287343de55 Mon Sep 17 00:00:00 2001 From: vansh-09 Date: Sun, 17 May 2026 21:32:15 +0530 Subject: [PATCH 3/7] fix: resolve JACK backend build failures --- CMakeLists.txt | 22 +- src/audio/audio_backend_jack_internal.h | 2 + src/audio/audio_backend_jack_lifecycle.cpp | 5 +- src/audio/audio_engine.h | 644 +++++++++++---------- tests/test_jack_backend.cpp | 5 +- 5 files changed, 352 insertions(+), 326 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5bed92ccd..379446ab7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -263,8 +263,25 @@ elseif(ANDROID) set(DIALOG_SOURCES src/gui/file_dialog_web.cpp) else() if(WITH_JACK) - find_path(JACK_INCLUDE_DIRS jack/jack.h) - find_library(JACK_LIBRARIES NAMES jack) + if(NOT JACK_INCLUDE_DIRS OR NOT JACK_LIBRARIES) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(JACK QUIET jack) + endif() + if(NOT JACK_FOUND) + find_path(JACK_INCLUDE_DIRS jack/jack.h + PATHS /usr/include /usr/local/include /opt/homebrew/include + "C:/Program Files/JACK/include" + "${CMAKE_SOURCE_DIR}/external/jack") + find_library(JACK_LIBRARIES NAMES jack + PATHS /usr/lib /usr/local/lib /opt/homebrew/lib + "C:/Program Files/JACK/lib" + "${CMAKE_SOURCE_DIR}/external/jack") + endif() + endif() + if(JACK_LIBRARY_DIRS) + link_directories(${JACK_LIBRARY_DIRS}) + endif() set(BACKEND_SOURCES src/audio/audio_backend_jack.cpp src/audio/audio_backend_jack_lifecycle.cpp @@ -622,6 +639,7 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS ${IMGUI_DIR} ${IMGUI_DIR}/backends ${PORTAUDIO_INCLUDE_DIRS} + $<$:${JACK_INCLUDE_DIRS}> ${SDL2_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIR} ${RTMIDI_INCLUDE_DIRS} diff --git a/src/audio/audio_backend_jack_internal.h b/src/audio/audio_backend_jack_internal.h index c4c174f115..6643005730 100644 --- a/src/audio/audio_backend_jack_internal.h +++ b/src/audio/audio_backend_jack_internal.h @@ -1,5 +1,6 @@ #pragma once +#ifdef WITH_JACK #include namespace Amplitron @@ -14,3 +15,4 @@ namespace Amplitron }; } // namespace Amplitron +#endif // WITH_JACK diff --git a/src/audio/audio_backend_jack_lifecycle.cpp b/src/audio/audio_backend_jack_lifecycle.cpp index 7d312c3736..557e708c66 100644 --- a/src/audio/audio_backend_jack_lifecycle.cpp +++ b/src/audio/audio_backend_jack_lifecycle.cpp @@ -1,4 +1,5 @@ // JACK backend — lifecycle and process callback +#include "audio/audio_backend.h" #include "audio/audio_engine.h" #include "audio/audio_backend_jack_internal.h" #include @@ -22,7 +23,7 @@ namespace Amplitron if (!in || !out) { if (out) - std::memset(out, 0, nframes * 2 * sizeof(float)); + std::memset(out, 0, nframes * sizeof(float)); return 0; } @@ -135,7 +136,7 @@ namespace Amplitron std::vector AudioEngine::get_input_devices() const { return {{0, "JACK Input", 1, 0, (double)sample_rate_, false}}; } - std::vector AudioEngine::get_output_devices() const { return {{0, "JACK Output", 0, 2, (double)sample_rate_, false}}; } + std::vector AudioEngine::get_output_devices() const { return {{0, "JACK Output", 0, 1, (double)sample_rate_, false}}; } bool AudioEngine::set_input_device(int) { return true; } bool AudioEngine::set_output_device(int) { return true; } diff --git a/src/audio/audio_engine.h b/src/audio/audio_engine.h index 4d2b3bfde5..29d8c7428e 100644 --- a/src/audio/audio_engine.h +++ b/src/audio/audio_engine.h @@ -7,342 +7,350 @@ #include // FORWARD DECLARATIONS -namespace Amplitron { - -struct AudioDeviceInfo { - int index; - std::string name; - int max_input_channels; - int max_output_channels; - double default_sample_rate; - bool is_usb_device; -}; - -struct AudioBackendState; - -/** - * @brief Core audio processing engine. - * - * Manages the audio stream (via a platform backend), the effect chain, - * master gain controls, CPU load monitoring, and a lock-free SPSC command - * queue for thread-safe GUI-to-audio parameter updates. - * - * All platform-specific code (PortAudio / SDL) lives in separate - * compilation units; the engine itself is platform-agnostic. - */ -class AudioEngine { -public: - /** @brief Construct the engine with default settings. */ - AudioEngine(); - - /** @brief Destructor — shuts down the audio stream if still running. */ - ~AudioEngine(); - - /** @brief Initialize the audio back-end. @return true on success. */ - bool initialize(); - - /** @brief Release audio back-end resources. */ - void shutdown(); - - /** @brief Open and start the audio stream. @return true on success. */ - bool start(); - - /** @brief Stop the audio stream. */ - void stop(); - - /** @brief Stop and restart the stream (manual recovery). @return true on success. */ - bool restart(); - - /** @brief Return the last error message, or empty string. */ - std::string get_last_error() const { return last_error_; } - - /** @brief Clear the stored error message. */ - void clear_error() { last_error_.clear(); } +namespace Amplitron +{ -#ifdef AMPLITRON_ANDROID_OBOE - /** - * @brief Return a human-readable label for the Oboe sharing mode negotiated at runtime. - * "AAudio exclusive mode" when AAudio exclusive path is active; "OpenSL ES (shared)" otherwise. - * Used by the Android settings UI to display the actual backend, not a hardcoded string. - */ - const char* get_oboe_sharing_mode_label() const; -#endif - - /** @brief Enumerate available audio input devices. */ - std::vector get_input_devices() const; + struct AudioDeviceInfo + { + int index; + std::string name; + int max_input_channels; + int max_output_channels; + double default_sample_rate; + bool is_usb_device; + }; - /** @brief Enumerate available audio output devices. */ - std::vector get_output_devices() const; + struct AudioBackendState; /** - * @brief Select the input device by index. - * @return true if the device was set successfully. - */ - bool set_input_device(int device_index); - - /** - * @brief Select the output device by index. - * @return true if the device was set successfully. - */ - bool set_output_device(int device_index); - - /** @brief Return the current input device index. */ - int get_input_device() const { return input_device_; } - - /** @brief Return the current output device index. */ - int get_output_device() const { return output_device_; } - - /** @brief Return the human-readable input device name. */ - std::string get_input_device_name() const; - - /** @brief Return the human-readable output device name. */ - std::string get_output_device_name() const; - - /** - * @brief Append an effect to the end of the chain (mutex-protected). - * @param effect Shared pointer to the effect to add. - */ - void add_effect(std::shared_ptr effect); - - /** - * @brief Insert an effect at a specific index in the chain (mutex-protected). - * @param index Position to insert at. If index >= size, appends to the end. - * @param effect Shared pointer to the effect to insert. - */ - void insert_effect(int index, std::shared_ptr effect); - - /** - * @brief Remove the effect at @p index from the chain (mutex-protected). - * @param index Zero-based position in the effect chain. - */ - void remove_effect(int index); - - /** - * @brief Move an effect from position @p from to position @p to (mutex-protected). - * @param from Source index. - * @param to Destination index. - */ - void move_effect(int from, int to); - - /** @brief Direct access to the effect chain vector (GUI thread only). */ - std::vector>& effects() { return effects_; } - - /** - * @brief Atomically replace the entire effect chain (mutex-protected). + * @brief Core audio processing engine. * - * Used by LoadPresetCommand undo/redo so the audio thread never observes - * a half-applied state. + * Manages the audio stream (via a platform backend), the effect chain, + * master gain controls, CPU load monitoring, and a lock-free SPSC command + * queue for thread-safe GUI-to-audio parameter updates. * - * @param new_effects The complete new effect chain to install. + * All platform-specific code (PortAudio / SDL) lives in separate + * compilation units; the engine itself is platform-agnostic. */ - void restore_effects_state(std::vector> new_effects); - - /** - * @brief Set the audio buffer size (takes effect on next stream restart). - * @param size Buffer size in samples. - */ - void set_buffer_size(int size); - - /** - * @brief Set the audio sample rate (takes effect on next stream restart). - * @param rate Sample rate in Hz. - */ - void set_sample_rate(int rate); - - /** @brief Return the current buffer size in samples. */ - int get_buffer_size() const { return buffer_size_; } - - /** @brief Return the current sample rate in Hz. */ - int get_sample_rate() const { return sample_rate_; } - - /** @brief Return true if the audio stream is actively running. */ - bool is_running() const { return running_; } + class AudioEngine + { + public: + /** @brief Construct the engine with default settings. */ + AudioEngine(); - /** @brief Return the most recent input peak level (0.0–1.0, atomic). */ - float get_input_level() const { return input_level_.load(); } + /** @brief Destructor — shuts down the audio stream if still running. */ + ~AudioEngine(); - /** @brief Return the most recent output peak level (0.0–1.0, atomic). */ - float get_output_level() const { return output_level_.load(); } + /** @brief Initialize the audio back-end. @return true on success. */ + bool initialize(); - /** @brief Return the most recent input RMS level (0.0–1.0, atomic). */ - float get_input_rms() const { return input_rms_.load(std::memory_order_relaxed); } + /** @brief Release audio back-end resources. */ + void shutdown(); - /** @brief Return the most recent output RMS level (0.0–1.0, atomic). */ - float get_output_rms() const { return output_rms_.load(std::memory_order_relaxed); } + /** @brief Open and start the audio stream. @return true on success. */ + bool start(); - /** @brief Consume one-shot input clipping flag set by audio thread. */ - bool consume_input_clipped() { return input_clipped_.exchange(false, std::memory_order_acq_rel); } + /** @brief Stop the audio stream. */ + void stop(); - /** @brief Consume one-shot output clipping flag set by audio thread. */ - bool consume_output_clipped() { return output_clipped_.exchange(false, std::memory_order_acq_rel); } + /** @brief Stop and restart the stream (manual recovery). @return true on success. */ + bool restart(); - /** @brief FFT size used for GUI analyzer snapshots. */ - static constexpr int ANALYZER_FFT_SIZE = 2048; - static constexpr int ANALYZER_FFT_MASK = ANALYZER_FFT_SIZE - 1; + /** @brief Return the last error message, or empty string. */ + std::string get_last_error() const { return last_error_; } - /** @brief Enable/disable analyzer capture in the audio callback (GUI thread). */ - void set_analyzer_enabled(bool enabled) { analyzer_enabled_.store(enabled, std::memory_order_release); } + /** @brief Clear the stored error message. */ + void clear_error() { last_error_.clear(); } - /** @brief Return true if analyzer capture is active. */ - bool is_analyzer_enabled() const { return analyzer_enabled_.load(std::memory_order_acquire); } - - /** @brief Snapshot sequence counter; increments when new analyzer data is published. */ - uint64_t get_analyzer_sequence() const { - return analyzer_sequence_.load(std::memory_order_acquire); - } - - /** - * @brief Copy latest pre/post-chain analyzer snapshots (GUI thread). - * @param input_dest Destination buffer for pre-chain samples. - * @param output_dest Destination buffer for post-chain samples. - * @param sample_count Number of samples to copy (clamped to ANALYZER_FFT_SIZE). - * @return true if at least one snapshot has been published. - */ - bool copy_analyzer_snapshot(float* input_dest, float* output_dest, int sample_count) const; - - /** - * @brief Set the master input gain (enqueued to audio thread via SPSC queue). - * @param gain Linear gain multiplier. - */ - void set_input_gain(float gain); - - /** - * @brief Set the master output gain (enqueued to audio thread via SPSC queue). - * @param gain Linear gain multiplier. - */ - void set_output_gain(float gain); - - /** @brief Return the current input gain (atomic relaxed read). */ - float get_input_gain() const { return input_gain_.load(std::memory_order_relaxed); } - - /** @brief Return the current output gain (atomic relaxed read). */ - float get_output_gain() const { return output_gain_.load(std::memory_order_relaxed); } - - /** - * @brief Enqueue a parameter value change from the GUI thread (lock-free). - * @param effect_index Index of the effect in the chain. - * @param param_index Index of the parameter within the effect. - * @param value New parameter value. - */ - void push_param_change(int effect_index, int param_index, float value); - - /** - * @brief Enqueue an effect enabled/disabled change from the GUI thread. - * @param effect_index Index of the effect in the chain. - * @param enabled >0.5 means enabled. - */ - void push_effect_enabled(int effect_index, float enabled); - - /** - * @brief Enqueue a dry/wet mix change from the GUI thread. - * @param effect_index Index of the effect in the chain. - * @param mix New mix value (0.0–1.0). - */ - void push_effect_mix(int effect_index, float mix); - - /** @brief Return the current CPU load fraction (0.0–1.0, atomic). */ - float get_cpu_load() const { return cpu_load_.load(std::memory_order_relaxed); } - - /** @brief Suggest a new buffer size based on current CPU load. */ - int get_suggested_buffer_size() const; - - /** @brief Return true if automatic buffer-size tuning is enabled. */ - bool is_auto_buffer_enabled() const { return auto_buffer_enabled_; } - - /** @brief Enable or disable automatic buffer-size tuning. */ - void set_auto_buffer_enabled(bool enabled) { auto_buffer_enabled_ = enabled; } - - /** @brief Access the built-in audio recorder. */ - Recorder& recorder() { return recorder_; } - - /** - * @brief Set a tuner tap that receives pre-chain audio each callback. - * - * The tap is processed before the effect chain. If its mute param is - * active it will zero the buffer, silencing the downstream chain. - * Protected by effect_mutex_. - */ - void set_tuner_tap(std::shared_ptr tap); +#ifdef AMPLITRON_ANDROID_OBOE + /** + * @brief Return a human-readable label for the Oboe sharing mode negotiated at runtime. + * "AAudio exclusive mode" when AAudio exclusive path is active; "OpenSL ES (shared)" otherwise. + * Used by the Android settings UI to display the actual backend, not a hardcoded string. + */ + const char *get_oboe_sharing_mode_label() const; +#endif - /** @brief Remove the tuner tap. */ - void clear_tuner_tap(); + /** @brief Enumerate available audio input devices. */ + std::vector get_input_devices() const; + + /** @brief Enumerate available audio output devices. */ + std::vector get_output_devices() const; + + /** + * @brief Select the input device by index. + * @return true if the device was set successfully. + */ + bool set_input_device(int device_index); + + /** + * @brief Select the output device by index. + * @return true if the device was set successfully. + */ + bool set_output_device(int device_index); - /** @brief Return true if a tuner tap is currently installed. */ - bool has_tuner_tap() const; + /** @brief Return the current input device index. */ + int get_input_device() const { return input_device_; } + + /** @brief Return the current output device index. */ + int get_output_device() const { return output_device_; } + + /** @brief Return the human-readable input device name. */ + std::string get_input_device_name() const; + + /** @brief Return the human-readable output device name. */ + std::string get_output_device_name() const; + + /** + * @brief Append an effect to the end of the chain (mutex-protected). + * @param effect Shared pointer to the effect to add. + */ + void add_effect(std::shared_ptr effect); + + /** + * @brief Insert an effect at a specific index in the chain (mutex-protected). + * @param index Position to insert at. If index >= size, appends to the end. + * @param effect Shared pointer to the effect to insert. + */ + void insert_effect(int index, std::shared_ptr effect); + + /** + * @brief Remove the effect at @p index from the chain (mutex-protected). + * @param index Zero-based position in the effect chain. + */ + void remove_effect(int index); + + /** + * @brief Move an effect from position @p from to position @p to (mutex-protected). + * @param from Source index. + * @param to Destination index. + */ + void move_effect(int from, int to); + + /** @brief Direct access to the effect chain vector (GUI thread only). */ + std::vector> &effects() { return effects_; } + + /** + * @brief Atomically replace the entire effect chain (mutex-protected). + * + * Used by LoadPresetCommand undo/redo so the audio thread never observes + * a half-applied state. + * + * @param new_effects The complete new effect chain to install. + */ + void restore_effects_state(std::vector> new_effects); + + /** + * @brief Set the audio buffer size (takes effect on next stream restart). + * @param size Buffer size in samples. + */ + void set_buffer_size(int size); + + /** + * @brief Set the audio sample rate (takes effect on next stream restart). + * @param rate Sample rate in Hz. + */ + void set_sample_rate(int rate); + + /** @brief Return the current buffer size in samples. */ + int get_buffer_size() const { return buffer_size_; } + + /** @brief Return the current sample rate in Hz. */ + int get_sample_rate() const { return sample_rate_; } + + /** @brief Return true if the audio stream is actively running. */ + bool is_running() const { return running_; } + + /** @brief Return the most recent input peak level (0.0–1.0, atomic). */ + float get_input_level() const { return input_level_.load(); } + + /** @brief Return the most recent output peak level (0.0–1.0, atomic). */ + float get_output_level() const { return output_level_.load(); } + + /** @brief Return the most recent input RMS level (0.0–1.0, atomic). */ + float get_input_rms() const { return input_rms_.load(std::memory_order_relaxed); } + + /** @brief Return the most recent output RMS level (0.0–1.0, atomic). */ + float get_output_rms() const { return output_rms_.load(std::memory_order_relaxed); } + + /** @brief Consume one-shot input clipping flag set by audio thread. */ + bool consume_input_clipped() { return input_clipped_.exchange(false, std::memory_order_acq_rel); } + + /** @brief Consume one-shot output clipping flag set by audio thread. */ + bool consume_output_clipped() { return output_clipped_.exchange(false, std::memory_order_acq_rel); } + + /** @brief FFT size used for GUI analyzer snapshots. */ + static constexpr int ANALYZER_FFT_SIZE = 2048; + static constexpr int ANALYZER_FFT_MASK = ANALYZER_FFT_SIZE - 1; + + /** @brief Enable/disable analyzer capture in the audio callback (GUI thread). */ + void set_analyzer_enabled(bool enabled) { analyzer_enabled_.store(enabled, std::memory_order_release); } + + /** @brief Return true if analyzer capture is active. */ + bool is_analyzer_enabled() const { return analyzer_enabled_.load(std::memory_order_acquire); } + + /** @brief Snapshot sequence counter; increments when new analyzer data is published. */ + uint64_t get_analyzer_sequence() const + { + return analyzer_sequence_.load(std::memory_order_acquire); + } + + /** + * @brief Copy latest pre/post-chain analyzer snapshots (GUI thread). + * @param input_dest Destination buffer for pre-chain samples. + * @param output_dest Destination buffer for post-chain samples. + * @param sample_count Number of samples to copy (clamped to ANALYZER_FFT_SIZE). + * @return true if at least one snapshot has been published. + */ + bool copy_analyzer_snapshot(float *input_dest, float *output_dest, int sample_count) const; + + /** + * @brief Set the master input gain (enqueued to audio thread via SPSC queue). + * @param gain Linear gain multiplier. + */ + void set_input_gain(float gain); + + /** + * @brief Set the master output gain (enqueued to audio thread via SPSC queue). + * @param gain Linear gain multiplier. + */ + void set_output_gain(float gain); + + /** @brief Return the current input gain (atomic relaxed read). */ + float get_input_gain() const { return input_gain_.load(std::memory_order_relaxed); } + + /** @brief Return the current output gain (atomic relaxed read). */ + float get_output_gain() const { return output_gain_.load(std::memory_order_relaxed); } + + /** + * @brief Enqueue a parameter value change from the GUI thread (lock-free). + * @param effect_index Index of the effect in the chain. + * @param param_index Index of the parameter within the effect. + * @param value New parameter value. + */ + void push_param_change(int effect_index, int param_index, float value); + + /** + * @brief Enqueue an effect enabled/disabled change from the GUI thread. + * @param effect_index Index of the effect in the chain. + * @param enabled >0.5 means enabled. + */ + void push_effect_enabled(int effect_index, float enabled); + + /** + * @brief Enqueue a dry/wet mix change from the GUI thread. + * @param effect_index Index of the effect in the chain. + * @param mix New mix value (0.0–1.0). + */ + void push_effect_mix(int effect_index, float mix); + + /** @brief Return the current CPU load fraction (0.0–1.0, atomic). */ + float get_cpu_load() const { return cpu_load_.load(std::memory_order_relaxed); } + + /** @brief Suggest a new buffer size based on current CPU load. */ + int get_suggested_buffer_size() const; + + /** @brief Return true if automatic buffer-size tuning is enabled. */ + bool is_auto_buffer_enabled() const { return auto_buffer_enabled_; } + + /** @brief Enable or disable automatic buffer-size tuning. */ + void set_auto_buffer_enabled(bool enabled) { auto_buffer_enabled_ = enabled; } + + /** @brief Access the built-in audio recorder. */ + Recorder &recorder() { return recorder_; } + + /** + * @brief Set a tuner tap that receives pre-chain audio each callback. + * + * The tap is processed before the effect chain. If its mute param is + * active it will zero the buffer, silencing the downstream chain. + * Protected by effect_mutex_. + */ + void set_tuner_tap(std::shared_ptr tap); + + /** @brief Remove the tuner tap. */ + void clear_tuner_tap(); + + /** @brief Return true if a tuner tap is currently installed. */ + bool has_tuner_tap() const; + + /** + * @brief Run the DSP pipeline on a block of audio samples. + * + * Called by the platform backend's audio callback. Public so that + * backend compilation units (which are not class members) can invoke it. + */ + void process_audio(const float *input, float *output, int frame_count); + +#ifdef WITH_JACK + friend int jack_process(jack_nframes_t nframes, void *arg); +#endif - /** - * @brief Run the DSP pipeline on a block of audio samples. - * - * Called by the platform backend's audio callback. Public so that - * backend compilation units (which are not class members) can invoke it. - */ - void process_audio(const float* input, float* output, int frame_count); - - // MIDI instance is managed by the GUI thread's MidiManager. - -private: - // Platform backend state (defined in the backend .cpp that is compiled) - AudioBackendState* backend_ = nullptr; - - bool initialized_ = false; - bool running_ = false; - - int input_device_ = -1; - int output_device_ = -1; - int sample_rate_ = DEFAULT_SAMPLE_RATE; - int buffer_size_ = DEFAULT_BUFFER_SIZE; - - std::atomic input_gain_{1.0f}; - std::atomic output_gain_{0.8f}; - - std::atomic input_level_{0.0f}; - std::atomic output_level_{0.0f}; - std::atomic input_rms_{0.0f}; - std::atomic output_rms_{0.0f}; - std::atomic input_clipped_{false}; - std::atomic output_clipped_{false}; - std::atomic analyzer_enabled_{false}; - - std::vector> effects_; - std::vector process_buffer_; - std::vector process_buffer_right_; - std::mutex effect_mutex_; - Recorder recorder_; - std::shared_ptr tuner_tap_; - std::string last_error_; - - // Audio-thread-private shadow of the effect chain. - // Copied from effects_ / tuner_tap_ whenever effect_mutex_ is acquired - // and topology_dirty_ is set, avoiding unnecessary shared_ptr churn on - // every callback when the chain is stable. - std::vector> audio_shadow_effects_; - std::shared_ptr audio_shadow_tuner_; - std::atomic topology_dirty_{true}; - - // Lock-free GUI -> Audio command queue (256 slots) - SPSCQueue command_queue_; - void drain_commands(); // Must be called while holding effect_mutex_ - void drain_gain_commands(); // Safe to call without effect_mutex_ - - // CPU load watchdog for buffer auto-tuning - std::atomic cpu_load_{0.0f}; - std::atomic callback_duration_us_{0.0f}; - bool auto_buffer_enabled_ = false; - - // Audio-thread capture for GUI analyzer snapshots. - static constexpr int ANALYZER_HOP_SIZE = 1024; - std::array analyzer_capture_input_{}; - std::array analyzer_capture_output_{}; - int analyzer_capture_index_ = 0; - int analyzer_samples_since_publish_ = 0; - - // Shared snapshot buffers (audio thread writes with try_lock, GUI reads with lock). - mutable std::mutex analyzer_mutex_; - std::array analyzer_snapshot_input_{}; - std::array analyzer_snapshot_output_{}; - std::atomic analyzer_sequence_{0}; - - // (MIDI instance removed - use MidiManager) -}; + // MIDI instance is managed by the GUI thread's MidiManager. + + private: + // Platform backend state (defined in the backend .cpp that is compiled) + AudioBackendState *backend_ = nullptr; + + bool initialized_ = false; + bool running_ = false; + + int input_device_ = -1; + int output_device_ = -1; + int sample_rate_ = DEFAULT_SAMPLE_RATE; + int buffer_size_ = DEFAULT_BUFFER_SIZE; + + std::atomic input_gain_{1.0f}; + std::atomic output_gain_{0.8f}; + + std::atomic input_level_{0.0f}; + std::atomic output_level_{0.0f}; + std::atomic input_rms_{0.0f}; + std::atomic output_rms_{0.0f}; + std::atomic input_clipped_{false}; + std::atomic output_clipped_{false}; + std::atomic analyzer_enabled_{false}; + + std::vector> effects_; + std::vector process_buffer_; + std::vector process_buffer_right_; + std::mutex effect_mutex_; + Recorder recorder_; + std::shared_ptr tuner_tap_; + std::string last_error_; + + // Audio-thread-private shadow of the effect chain. + // Copied from effects_ / tuner_tap_ whenever effect_mutex_ is acquired + // and topology_dirty_ is set, avoiding unnecessary shared_ptr churn on + // every callback when the chain is stable. + std::vector> audio_shadow_effects_; + std::shared_ptr audio_shadow_tuner_; + std::atomic topology_dirty_{true}; + + // Lock-free GUI -> Audio command queue (256 slots) + SPSCQueue command_queue_; + void drain_commands(); // Must be called while holding effect_mutex_ + void drain_gain_commands(); // Safe to call without effect_mutex_ + + // CPU load watchdog for buffer auto-tuning + std::atomic cpu_load_{0.0f}; + std::atomic callback_duration_us_{0.0f}; + bool auto_buffer_enabled_ = false; + + // Audio-thread capture for GUI analyzer snapshots. + static constexpr int ANALYZER_HOP_SIZE = 1024; + std::array analyzer_capture_input_{}; + std::array analyzer_capture_output_{}; + int analyzer_capture_index_ = 0; + int analyzer_samples_since_publish_ = 0; + + // Shared snapshot buffers (audio thread writes with try_lock, GUI reads with lock). + mutable std::mutex analyzer_mutex_; + std::array analyzer_snapshot_input_{}; + std::array analyzer_snapshot_output_{}; + std::atomic analyzer_sequence_{0}; + + // (MIDI instance removed - use MidiManager) + }; } // namespace Amplitron diff --git a/tests/test_jack_backend.cpp b/tests/test_jack_backend.cpp index 208f082c4b..b4d5c8fa03 100644 --- a/tests/test_jack_backend.cpp +++ b/tests/test_jack_backend.cpp @@ -22,16 +22,13 @@ TEST(JackBackend, InitializeAndStart) engine.shutdown(); } -TEST(JackBackend, ProcessCallbackInvoked) +TEST(JackBackend, StartStopSmoke) { AudioEngine engine; if (!engine.initialize()) { GTEST_SKIP() << "jackd not running; skipping integration test"; } - - std::atomic invoked{false}; - // Wrap engine->process_audio by installing a temporary effect? Simpler: spawn thread and rely on start() EXPECT_TRUE(engine.start()); // wait briefly std::this_thread::sleep_for(std::chrono::milliseconds(200)); From 3ce1c84b6cb0bcc2b414b25ee30e46c2e0bcfafd Mon Sep 17 00:00:00 2001 From: vansh-09 Date: Sun, 17 May 2026 22:29:25 +0530 Subject: [PATCH 4/7] fix: include JACK header for friend declaration --- src/audio/audio_engine.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/audio/audio_engine.h b/src/audio/audio_engine.h index 29d8c7428e..38301ea488 100644 --- a/src/audio/audio_engine.h +++ b/src/audio/audio_engine.h @@ -6,6 +6,10 @@ #include "audio/spsc_queue.h" #include +#ifdef WITH_JACK +#include +#endif + // FORWARD DECLARATIONS namespace Amplitron { From 26211aa0a129b2b716170a7a70e0c36ac99d2d78 Mon Sep 17 00:00:00 2001 From: vansh-09 Date: Mon, 18 May 2026 14:09:30 +0530 Subject: [PATCH 5/7] fix: align JACK callback declaration --- src/audio/audio_backend_jack_lifecycle.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/audio/audio_backend_jack_lifecycle.cpp b/src/audio/audio_backend_jack_lifecycle.cpp index 557e708c66..850b61b86b 100644 --- a/src/audio/audio_backend_jack_lifecycle.cpp +++ b/src/audio/audio_backend_jack_lifecycle.cpp @@ -9,8 +9,8 @@ namespace Amplitron { - // Static process callback - static int jack_process(jack_nframes_t nframes, void *arg) + // JACK process callback + int jack_process(jack_nframes_t nframes, void *arg) { AudioEngine *engine = static_cast(arg); AudioBackendState *be = engine->backend_; From 43712c7f42d3ef7c2f6984240391254f87269805 Mon Sep 17 00:00:00 2001 From: vansh-09 Date: Mon, 18 May 2026 19:55:03 +0530 Subject: [PATCH 6/7] fix: complete JACK backend CI path --- CMakeLists.txt | 3 +++ src/audio/audio_backend_jack_lifecycle.cpp | 16 ++++++++++++++++ tests/test_jack_backend.cpp | 21 ++++++++------------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 379446ab7c..7917b43ec2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,6 +279,9 @@ else() "${CMAKE_SOURCE_DIR}/external/jack") endif() endif() + if(NOT JACK_INCLUDE_DIRS OR NOT JACK_LIBRARIES) + message(FATAL_ERROR "WITH_JACK=ON but JACK development files were not found") + endif() if(JACK_LIBRARY_DIRS) link_directories(${JACK_LIBRARY_DIRS}) endif() diff --git a/src/audio/audio_backend_jack_lifecycle.cpp b/src/audio/audio_backend_jack_lifecycle.cpp index 850b61b86b..c66e7f2257 100644 --- a/src/audio/audio_backend_jack_lifecycle.cpp +++ b/src/audio/audio_backend_jack_lifecycle.cpp @@ -130,6 +130,22 @@ namespace Amplitron } } + bool AudioEngine::restart() + { + stop(); + bool ok = start(); + if (!ok) + { + last_error_ = "Failed to restart audio stream. Check JACK server and device settings."; + std::cerr << "[Amplitron] " << last_error_ << std::endl; + } + else + { + last_error_.clear(); + } + return ok; + } + // Device name stubs std::string AudioEngine::get_input_device_name() const { return "JACK Input"; } std::string AudioEngine::get_output_device_name() const { return "JACK Output"; } diff --git a/tests/test_jack_backend.cpp b/tests/test_jack_backend.cpp index b4d5c8fa03..3acdd50d1b 100644 --- a/tests/test_jack_backend.cpp +++ b/tests/test_jack_backend.cpp @@ -1,36 +1,31 @@ #ifdef WITH_JACK +#include "test_framework.h" #include "audio/audio_backend_jack_internal.h" #include "audio/audio_engine.h" -#include -#include #include using namespace Amplitron; -TEST(JackBackend, InitializeAndStart) +TEST(JackBackend_InitializeAndStart) { AudioEngine engine; - // initialization requires jackd running; this test is guarded by WITH_JACK - bool ok = engine.initialize(); - if (!ok) + if (!engine.initialize()) { - GTEST_SKIP() << "jackd not running; skipping integration test"; + return; } - EXPECT_TRUE(ok); - EXPECT_TRUE(engine.start()); + ASSERT_TRUE(engine.start()); engine.stop(); engine.shutdown(); } -TEST(JackBackend, StartStopSmoke) +TEST(JackBackend_StartStopSmoke) { AudioEngine engine; if (!engine.initialize()) { - GTEST_SKIP() << "jackd not running; skipping integration test"; + return; } - EXPECT_TRUE(engine.start()); - // wait briefly + ASSERT_TRUE(engine.start()); std::this_thread::sleep_for(std::chrono::milliseconds(200)); engine.stop(); engine.shutdown(); From 1ee8ded7c709c0b168f12eefaa765faba21afa8c Mon Sep 17 00:00:00 2001 From: Vansh Date: Wed, 20 May 2026 15:15:46 +0530 Subject: [PATCH 7/7] Removed the mrege Removed analyzer-related methods and member variables from the audio engine header. --- src/audio/audio_engine.h | 195 --------------------------------------- 1 file changed, 195 deletions(-) diff --git a/src/audio/audio_engine.h b/src/audio/audio_engine.h index e7c30abd5f..38301ea488 100644 --- a/src/audio/audio_engine.h +++ b/src/audio/audio_engine.h @@ -74,104 +74,6 @@ namespace Amplitron */ const char *get_oboe_sharing_mode_label() const; #endif - /** @brief Return true if analyzer capture is active. */ - bool is_analyzer_enabled() const { return analyzer_enabled_.load(std::memory_order_acquire); } - - /** @brief Snapshot sequence counter; increments when new analyzer data is published. */ - uint64_t get_analyzer_sequence() const { - return analyzer_sequence_.load(std::memory_order_acquire); - } - - /** - * @brief Copy latest pre/post-chain analyzer snapshots (GUI thread). - * @param input_dest Destination buffer for pre-chain samples. - * @param output_dest Destination buffer for post-chain samples. - * @param sample_count Number of samples to copy (clamped to ANALYZER_FFT_SIZE). - * @return true if at least one snapshot has been published. - */ - bool copy_analyzer_snapshot(float* input_dest, float* output_dest, int sample_count) const; - - /** - * @brief Set the master input gain (enqueued to audio thread via SPSC queue). - * @param gain Linear gain multiplier. - */ - void set_input_gain(float gain); - - /** - * @brief Set the master output gain (enqueued to audio thread via SPSC queue). - * @param gain Linear gain multiplier. - */ - void set_output_gain(float gain); - - /** @brief Return the current input gain (atomic relaxed read). */ - float get_input_gain() const { return input_gain_.load(std::memory_order_relaxed); } - - /** @brief Return the current output gain (atomic relaxed read). */ - float get_output_gain() const { return output_gain_.load(std::memory_order_relaxed); } - - /** @brief Toggle the metronome on/off (atomic update). */ - void toggle_metronome(); - - /** @brief Set the metronome BPM (atomic update). */ - void set_metronome_bpm(int bpm); - - /** @brief Set the metronome click volume (atomic update). */ - void set_metronome_volume(float volume); - - /** @brief Return the current metronome enabled state (atomic relaxed read). */ - bool get_metronome_enabled() const { return metronome_enabled_state_.load(std::memory_order_relaxed); } - - /** @brief Return the current metronome BPM (atomic relaxed read). */ - int get_metronome_bpm() const { return metronome_bpm_state_.load(std::memory_order_relaxed); } - - /** @brief Return the current metronome volume (atomic relaxed read). */ - float get_metronome_volume() const { return metronome_volume_state_.load(std::memory_order_relaxed); } - - /** - * @brief Enqueue a parameter value change from the GUI thread (lock-free). - * @param effect_index Index of the effect in the chain. - * @param param_index Index of the parameter within the effect. - * @param value New parameter value. - */ - void push_param_change(int effect_index, int param_index, float value); - - /** - * @brief Enqueue an effect enabled/disabled change from the GUI thread. - * @param effect_index Index of the effect in the chain. - * @param enabled >0.5 means enabled. - */ - void push_effect_enabled(int effect_index, float enabled); - - /** - * @brief Enqueue a dry/wet mix change from the GUI thread. - * @param effect_index Index of the effect in the chain. - * @param mix New mix value (0.0–1.0). - */ - void push_effect_mix(int effect_index, float mix); - - /** @brief Return the current CPU load fraction (0.0–1.0, atomic). */ - float get_cpu_load() const { return cpu_load_.load(std::memory_order_relaxed); } - - /** @brief Suggest a new buffer size based on current CPU load. */ - int get_suggested_buffer_size() const; - - /** @brief Return true if automatic buffer-size tuning is enabled. */ - bool is_auto_buffer_enabled() const { return auto_buffer_enabled_; } - - /** @brief Enable or disable automatic buffer-size tuning. */ - void set_auto_buffer_enabled(bool enabled) { auto_buffer_enabled_ = enabled; } - - /** @brief Access the built-in audio recorder. */ - Recorder& recorder() { return recorder_; } - - /** - * @brief Set a tuner tap that receives pre-chain audio each callback. - * - * The tap is processed before the effect chain. If its mute param is - * active it will zero the buffer, silencing the downstream chain. - * Protected by effect_mutex_. - */ - void set_tuner_tap(std::shared_ptr tap); /** @brief Enumerate available audio input devices. */ std::vector get_input_devices() const; @@ -454,102 +356,5 @@ namespace Amplitron // (MIDI instance removed - use MidiManager) }; - /** - * @brief Run the DSP pipeline on a block of audio samples. - * - * Called by the platform backend's audio callback. Public so that - * backend compilation units (which are not class members) can invoke it. - */ - void process_audio(const float* input, float* output, int frame_count); - - // MIDI instance is managed by the GUI thread's MidiManager. - -private: - // Platform backend state (defined in the backend .cpp that is compiled) - AudioBackendState* backend_ = nullptr; - - bool initialized_ = false; - bool running_ = false; - - int input_device_ = -1; - int output_device_ = -1; - int sample_rate_ = DEFAULT_SAMPLE_RATE; - int buffer_size_ = DEFAULT_BUFFER_SIZE; - - std::atomic input_gain_{1.0f}; - std::atomic output_gain_{0.8f}; - std::atomic metronome_enabled_state_{false}; - std::atomic metronome_bpm_state_{120}; - std::atomic metronome_volume_state_{0.5f}; - - std::atomic input_level_{0.0f}; - std::atomic output_level_{0.0f}; - std::atomic input_rms_{0.0f}; - std::atomic output_rms_{0.0f}; - std::atomic input_clipped_{false}; - std::atomic output_clipped_{false}; - std::atomic analyzer_enabled_{false}; - - std::vector> effects_; - std::vector process_buffer_; - std::vector process_buffer_right_; - std::mutex effect_mutex_; - Recorder recorder_; - std::shared_ptr tuner_tap_; - std::string last_error_; - - // Audio-thread-private shadow of the effect chain. - // Copied from effects_ / tuner_tap_ whenever effect_mutex_ is acquired - // and topology_dirty_ is set, avoiding unnecessary shared_ptr churn on - // every callback when the chain is stable. - std::vector> audio_shadow_effects_; - std::shared_ptr audio_shadow_tuner_; - std::atomic topology_dirty_{true}; - - // Lock-free GUI -> Audio command queue (256 slots) - SPSCQueue command_queue_; - void drain_commands(); // Must be called while holding effect_mutex_ - void drain_gain_commands(); // Safe to call without effect_mutex_ - void update_metronome_timing(); - - // CPU load watchdog for buffer auto-tuning - std::atomic cpu_load_{0.0f}; - std::atomic callback_duration_us_{0.0f}; - bool auto_buffer_enabled_ = false; - - // Audio-thread capture for GUI analyzer snapshots. - static constexpr int ANALYZER_HOP_SIZE = 1024; - std::array analyzer_capture_input_{}; - std::array analyzer_capture_output_{}; - int analyzer_capture_index_ = 0; - int analyzer_samples_since_publish_ = 0; - - // Shared snapshot buffers (audio thread writes with try_lock, GUI reads with lock). - mutable std::mutex analyzer_mutex_; - std::array analyzer_snapshot_input_{}; - std::array analyzer_snapshot_output_{}; - std::atomic analyzer_sequence_{0}; - - // Metronome state (audio thread only) - bool metronome_enabled_ = false; - int metronome_bpm_ = 120; - float metronome_volume_ = 0.5f; - - float metronome_volume_smoothed_ = 0.0f; - float metronome_volume_smooth_alpha_ = 0.05f; - float metronome_bpm_smoothed_ = 120.0f; - float metronome_bpm_smooth_alpha_ = 0.05f; - - int metronome_sample_rate_ = 0; - double metronome_samples_per_beat_ = 0.0; - double metronome_sample_counter_ = 0.0; - int metronome_click_samples_total_ = 0; - int metronome_click_samples_remaining_ = 0; - float metronome_click_phase_ = 0.0f; - float metronome_click_phase_inc_ = 0.0f; - float metronome_click_env_ = 0.0f; - float metronome_click_decay_ = 0.0f; - // (MIDI instance removed - use MidiManager) -}; } // namespace Amplitron