LCOV - code coverage report
Current view: top level - src/gui - gui_manager_frame.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 60.1 % 298 179
Test Date: 2026-06-01 11:15:25 Functions: 86.2 % 29 25

            Line data    Source code
       1              : #include "gui/gui_manager.h"
       2              : #include "gui/pedalboard/pedal_board.h"
       3              : #include "gui/theme/theme.h"
       4              : #include "gui/gl_setup.h"
       5              : #include "gui/commands/command.h"
       6              : #include "gui/state/gui_graph_state.h"
       7              : #include "audio/effects/tuner.h"
       8              : #include <imgui.h>
       9              : #include <imgui_impl_opengl3.h>
      10              : #include <imgui_impl_sdl2.h>
      11              : #include <SDL2/SDL.h>
      12              : #include <SDL.h>
      13              : #include <algorithm>
      14              : #include <cmath>
      15              : 
      16              : namespace Amplitron {
      17              : 
      18              : // ─────────────────────────────────────────────────────────────────────────────
      19              : // Prop-assembly helpers
      20              : // ─────────────────────────────────────────────────────────────────────────────
      21              : 
      22            9 : RecordingProps GuiManager::build_recording_props() {
      23            9 :     auto& rec = engine_.recorder();
      24            9 :     const bool is_recording = rec.is_recording();
      25              : 
      26              :     // Fill waveform buffer (raw copy, no math)
      27            9 :     if (is_recording) {
      28            6 :         rec.get_waveform(rec_waveform_buf_, Recorder::WAVEFORM_SIZE);
      29            2 :     }
      30              : 
      31            9 :     RecordingProps p;
      32            9 :     p.is_recording    = is_recording;
      33            9 :     p.is_paused       = rec.is_paused();
      34            9 :     p.has_unsaved     = rec.has_unsaved();
      35            9 :     p.duration        = rec.get_duration();
      36            9 :     p.current_peak    = rec.get_current_peak();
      37            9 :     p.samples_written = rec.get_samples_written();
      38            9 :     p.channels        = rec.get_channels();
      39            9 :     p.sample_rate     = engine_.get_sample_rate();
      40            9 :     p.waveform_buf    = is_recording ? rec_waveform_buf_ : nullptr;
      41            9 :     p.waveform_size   = is_recording ? Recorder::WAVEFORM_SIZE : 0;
      42              : 
      43           12 :     p.on_resume  = [&rec]() { rec.resume(); };
      44           12 :     p.on_pause   = [&rec]() { rec.pause(); };
      45           12 :     p.on_stop    = [&rec]() { rec.stop(); };
      46           18 :     p.on_start   = [this, &rec]() {
      47            3 :         rec.start(Recorder::generate_filename(), engine_.get_sample_rate(), 2);
      48            9 :     };
      49           12 :     p.on_discard = [&rec]() { rec.discard(); };
      50            9 :     return p;
      51            3 : }
      52              : 
      53            6 : TunerProps GuiManager::build_tuner_props() {
      54            6 :     TunerPedal* t = tuner_pedal_.get();
      55            6 :     TunerProps p;
      56            6 :     p.has_signal = t->signal_detected.load(std::memory_order_relaxed);
      57            6 :     p.note_idx   = t->detected_note.load(std::memory_order_relaxed);
      58            6 :     p.octave     = t->detected_octave.load(std::memory_order_relaxed);
      59            6 :     p.cents      = t->detected_cents.load(std::memory_order_relaxed);
      60            6 :     p.freq       = t->detected_freq.load(std::memory_order_relaxed);
      61            6 :     p.mute_on    = t->params()[0].value >= 0.5f;
      62            6 :     p.a4_ref     = t->params()[1].value;
      63            6 :     p.note_name_fn = [](int idx) { return TunerPedal::note_name(idx); };
      64            9 :     p.on_mute_changed    = [t](bool mute) { t->params()[0].value = mute ? 1.0f : 0.0f; };
      65            9 :     p.on_a4_ref_changed  = [t](float ref) { t->params()[1].value = ref; };
      66            6 :     return p;
      67            2 : }
      68              : 
      69            6 : SettingsProps GuiManager::build_settings_props() {
      70            6 :     SettingsProps p;
      71            6 :     p.input_device_name  = engine_.get_input_device_name();
      72            6 :     p.output_device_name = engine_.get_output_device_name();
      73            8 :     p.device_error       = engine_.get_last_error();
      74            6 :     p.buffer_size        = engine_.get_buffer_size();
      75            6 :     p.sample_rate        = engine_.get_sample_rate();
      76            6 :     p.suggested_buf      = engine_.get_suggested_buffer_size();
      77            6 :     p.latency_ms         = (p.sample_rate > 0) ? (1000.0f * p.buffer_size / p.sample_rate) : 0.0f;
      78            6 :     p.cpu_load           = engine_.get_cpu_load();
      79            6 :     p.auto_buf           = engine_.is_auto_buffer_enabled();
      80            6 :     p.current_input      = engine_.get_input_device();
      81            6 :     p.current_output     = engine_.get_output_device();
      82              : 
      83           12 :     for (auto& dev : engine_.get_input_devices())
      84           10 :         p.input_devices.push_back({dev.index, dev.name, dev.is_usb_device});
      85           12 :     for (auto& dev : engine_.get_output_devices())
      86           10 :         p.output_devices.push_back({dev.index, dev.name, dev.is_usb_device});
      87              : 
      88              : #ifdef AMPLITRON_ANDROID_OBOE
      89              :     p.oboe_mode_label = engine_.get_oboe_sharing_mode_label();
      90              : #endif
      91              : 
      92            9 :     p.on_buffer_size_changed   = [this](int s) { engine_.set_buffer_size(s); };
      93            9 :     p.on_sample_rate_changed   = [this](int r) { engine_.set_sample_rate(r); };
      94            8 :     p.on_auto_buf_changed      = [this](bool b) { engine_.set_auto_buffer_enabled(b); };
      95            8 :     p.on_clear_error           = [this]() { engine_.clear_error(); };
      96            9 :     p.on_input_device_changed  = [this](int i) { engine_.set_input_device(i); };
      97            9 :     p.on_output_device_changed = [this](int i) { engine_.set_output_device(i); };
      98            6 :     return p;
      99            6 : }
     100              : 
     101            3 : AnalyzerProps GuiManager::build_analyzer_props() {
     102            3 :     const float dt = std::max(ImGui::GetIO().DeltaTime, 1.0f / 240.0f);
     103              : 
     104              :     // Drive DSP updates in the engine (no math in UI thread)
     105            3 :     engine_.update_level_analyzer(dt);
     106            3 :     engine_.update_spectrum_analyzer(dt);
     107              : 
     108            3 :     const auto& la = engine_.level_analyzer();
     109              : 
     110            3 :     AnalyzerProps p;
     111            3 :     p.smoothed_input_rms  = la.smoothed_input_rms();
     112            3 :     p.smoothed_output_rms = la.smoothed_output_rms();
     113            3 :     p.input_peak_hold     = la.input_peak_hold();
     114            3 :     p.output_peak_hold    = la.output_peak_hold();
     115            3 :     p.input_clip_active   = la.input_clip_flash() > 0.01f;
     116            3 :     p.output_clip_active  = la.output_clip_flash() > 0.01f;
     117            3 :     p.input_clip_flash    = la.input_clip_flash();
     118            3 :     p.output_clip_flash   = la.output_clip_flash();
     119            3 :     const auto& sa = engine_.spectrum_analyzer();
     120            3 :     p.spectrum.smoothed_input_db  = sa.smoothed_input_db();
     121            3 :     p.spectrum.smoothed_output_db = sa.smoothed_output_db();
     122            3 :     p.spectrum.input_peak_db      = sa.input_peak_db();
     123            3 :     p.spectrum.output_peak_db     = sa.output_peak_db();
     124              : 
     125            8 :     p.on_set_analyzer_enabled = [this](bool enabled) {
     126            3 :         engine_.set_analyzer_enabled(enabled);
     127            3 :     };
     128            3 :     return p;
     129            1 : }
     130              : 
     131            9 : SnapshotsProps GuiManager::build_snapshots_props() {
     132            9 :     SnapshotsProps p;
     133           45 :     for (int i = 0; i < SnapshotManager::NUM_SLOTS; ++i) {
     134           36 :         p.slots[i].is_filled = snapshot_manager_.has_slot(i);
     135           36 :         p.slots[i].is_active = (snapshot_manager_.active_slot() == i);
     136           36 :         p.slots[i].label     = SnapshotManager::SLOT_LABELS[i];
     137           12 :     }
     138           18 :     p.on_recall_slot = [this](int slot) {
     139            3 :         recallSnapshotFromSlot(slot);
     140            7 :     };
     141           18 :     p.on_save_slot = [this](int slot) {
     142            3 :         snapshot_manager_.save_slot(slot, engine_);
     143            3 :         snapshot_manager_.set_active_slot(slot);
     144            7 :     };
     145           18 :     p.on_clear_slot = [this](int slot) {
     146            3 :         snapshot_manager_.clear_slot(slot);
     147            7 :     };
     148            9 :     return p;
     149            3 : }
     150              : 
     151              : // ─────────────────────────────────────────────────────────────────────────────
     152              : // Toggle audio mute
     153              : // ─────────────────────────────────────────────────────────────────────────────
     154            6 : void GuiManager::toggle_audio_mute_state() {
     155            6 :     if (engine_.is_running()) {
     156            3 :         engine_.stop();
     157            3 :         audio_muted_ = true;
     158            1 :     } else {
     159            3 :         engine_.restart();
     160            3 :         audio_muted_ = false;
     161              :     }
     162            6 : }
     163              : 
     164            0 : void GuiManager::set_show_tuner(bool show) {
     165            0 :     show_tuner_ = show;
     166            0 :     if (show_tuner_) {
     167            0 :         tuner_pedal_->set_enabled(true);
     168            0 :         engine_.set_tuner_tap(tuner_pedal_);
     169            0 :     } else {
     170            0 :         engine_.clear_tuner_tap();
     171            0 :         tuner_pedal_->set_enabled(false);
     172              :     }
     173            0 : }
     174              : 
     175            3 : void GuiManager::recallSnapshotFromSlot(int slot) {
     176            3 :     if (!snapshot_manager_.has_slot(slot)) return;
     177            3 :     auto before          = SnapshotManager::capture(engine_);
     178            3 :     const auto* after    = snapshot_manager_.get_slot(slot);
     179            4 :     command_history_.execute(std::make_unique<RecallSnapshotCommand>(
     180            1 :         engine_,
     181            1 :         before.effects, before.input_gain, before.output_gain,
     182            3 :         after->effects,  after->input_gain,  after->output_gain
     183              :     ));
     184            3 :     snapshot_manager_.set_active_slot(slot);
     185            3 :     if (pedal_board_) pedal_board_->rebuild_widgets();
     186            3 : }
     187              : 
     188              : // ─────────────────────────────────────────────────────────────────────────────
     189              : // run_frame — reactive root render loop
     190              : // ─────────────────────────────────────────────────────────────────────────────
     191            0 : bool GuiManager::run_frame() {
     192            0 :     SDL_Event event;
     193            0 :     while (SDL_PollEvent(&event)) {
     194            0 :         ImGui_ImplSDL2_ProcessEvent(&event);
     195            0 :         if (event.type == SDL_QUIT) return false;
     196            0 :         if (event.type == SDL_WINDOWEVENT &&
     197            0 :             event.window.event == SDL_WINDOWEVENT_CLOSE &&
     198            0 :             event.window.windowID == SDL_GetWindowID(window_))
     199            0 :             return false;
     200              :     }
     201              : 
     202            0 :     midi_manager_.poll(engine_);
     203              : 
     204            0 :     ImGui_ImplOpenGL3_NewFrame();
     205            0 :     ImGui_ImplSDL2_NewFrame();
     206            0 :     ImGui::NewFrame();
     207              : 
     208              :     // ── Keyboard shortcuts ──
     209            0 :     {
     210            0 :         ImGuiIO& io = ImGui::GetIO();
     211            0 :         bool mod = io.KeySuper || io.KeyCtrl;
     212              : 
     213            0 :         if (mod && !io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z)) {
     214            0 :             if (command_history_.undo() && pedal_board_)
     215            0 :                 pedal_board_->rebuild_widgets();
     216            0 :         }
     217            0 :         if ((mod && io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z)) ||
     218            0 :             (mod && !io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Y))) {
     219            0 :             if (command_history_.redo() && pedal_board_)
     220            0 :                 pedal_board_->rebuild_widgets();
     221            0 :         }
     222            0 :         if (!io.WantTextInput && !ImGui::IsAnyItemActive() && ImGui::IsKeyPressed(ImGuiKey_M))
     223            0 :             toggle_audio_mute_state();
     224              : 
     225              :         // Ctrl/Cmd+1–4: recall snapshot slot A–D
     226              :         static const ImGuiKey digit_keys[4] = { ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4 };
     227            0 :         for (int i = 0; i < 4; ++i) {
     228            0 :             if (mod && !io.KeyShift && ImGui::IsKeyPressed(digit_keys[i])) {
     229            0 :                 recallSnapshotFromSlot(i);
     230            0 :             }
     231            0 :         }
     232              :     }
     233              : 
     234              :     // ── Menu bar ──
     235            0 :     render_menu_bar();
     236              : 
     237              :     // ── Full-window layout ──
     238            0 :     SDL_GetWindowSize(window_, &window_width_, &window_height_);
     239            0 :     ImGui::SetNextWindowPos(ImVec2(0, 20));
     240            0 :     ImGui::SetNextWindowSize(ImVec2(static_cast<float>(window_width_),
     241            0 :                                     static_cast<float>(window_height_) - 20));
     242            0 :     ImGui::Begin("##MainArea", nullptr,
     243              :         ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
     244              :         ImGuiWindowFlags_NoMove     | ImGuiWindowFlags_NoCollapse |
     245              :         ImGuiWindowFlags_NoBringToFrontOnFocus);
     246              : 
     247            0 :     const bool is_fullscreen = GuiGraphState::get_instance().is_fullscreen;
     248              : 
     249            0 :     if (!is_fullscreen) {
     250            0 :         render_master_controls();
     251            0 :         ImGui::Separator();
     252              : 
     253              :         // ── GuiRecording (reactive) ──
     254            0 :         gui_recording_.set_props(build_recording_props());
     255            0 :         gui_recording_.render();
     256              : 
     257            0 :         ImGui::Separator();
     258              : 
     259              :         // ── GuiSnapshots (reactive) ──
     260            0 :         gui_snapshots_.set_props(build_snapshots_props());
     261            0 :         gui_snapshots_.render();
     262              : 
     263            0 :         ImGui::Separator();
     264            0 :     }
     265              : 
     266            0 :     float analyzer_reserved_h = is_fullscreen ? 0.0f : gui_analyzer_.analyzer_reserved_height();
     267            0 :     ImGui::BeginChild("PedalBoardRegion", ImVec2(0, -analyzer_reserved_h), false);
     268            0 :     if (pedal_board_) pedal_board_->render();
     269            0 :     ImGui::EndChild();
     270              : 
     271            0 :     if (!is_fullscreen) {
     272            0 :         ImGui::Separator();
     273              :         // ── GuiAnalyzer (reactive) ──
     274            0 :         gui_analyzer_.set_props(build_analyzer_props());
     275            0 :         gui_analyzer_.render();
     276            0 :     }
     277              : 
     278            0 :     ImGui::End();
     279              : 
     280              :     // ── Popups / floating windows ──
     281            0 :     if (show_settings_) {
     282            0 :         gui_settings_.set_props(build_settings_props());
     283            0 :         gui_settings_.render(show_settings_);
     284            0 :     }
     285            0 :     if (show_save_preset_)  gui_presets_.render_save_popup(show_save_preset_);
     286            0 :     if (show_load_preset_)  gui_presets_.render_load_popup(show_load_preset_);
     287            0 :     if (gui_recording_.needs_save_dialog()) {
     288            0 :         gui_recording_.render_save_dialog([this](const std::string& dest) {
     289            0 :             auto& rec = engine_.recorder();
     290            0 :             if (rec.save_to(dest)) {
     291            0 :                 rec.write_metadata(dest, engine_);
     292            0 :             }
     293            0 :         });
     294            0 :     }
     295            0 :     if (show_tuner_) {
     296            0 :         gui_tuner_.set_props(build_tuner_props());
     297            0 :         bool current_show = show_tuner_;
     298            0 :         gui_tuner_.render(current_show);
     299            0 :         if (!current_show) {
     300            0 :             set_show_tuner(false);
     301            0 :         }
     302            0 :     }
     303            0 :     if (show_midi_) gui_midi_.render(show_midi_);
     304              : 
     305              :     // ── Toast overlay ──
     306            0 :     if (toast_timer_ > 0.0f) {
     307            0 :         toast_timer_ -= ImGui::GetIO().DeltaTime;
     308            0 :         ImGuiIO& io = ImGui::GetIO();
     309            0 :         ImVec2 toast_pos = ImVec2(io.DisplaySize.x - 20.0f, io.DisplaySize.y - 20.0f);
     310            0 :         ImGui::SetNextWindowPos(toast_pos, ImGuiCond_Always, ImVec2(1.0f, 1.0f));
     311            0 :         ImGui::SetNextWindowBgAlpha(0.75f);
     312            0 :         ImGui::Begin("##toast", nullptr,
     313              :             ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
     314              :             ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
     315              :             ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove);
     316            0 :         ImGui::Text("%s", toast_message_.c_str());
     317            0 :         ImGui::End();
     318            0 :     }
     319              : 
     320              :     // ── Render ──
     321            0 :     ImGui::Render();
     322            0 :     int display_w, display_h;
     323            0 :     SDL_GL_GetDrawableSize(window_, &display_w, &display_h);
     324            0 :     glViewport(0, 0, display_w, display_h);
     325            0 :     glClearColor(0.078f, 0.071f, 0.063f, 1.0f);
     326            0 :     glClear(GL_COLOR_BUFFER_BIT);
     327            0 :     ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
     328            0 :     SDL_GL_SwapWindow(window_);
     329              : 
     330            0 :     return true;
     331            0 : }
     332              : 
     333              : // ─────────────────────────────────────────────────────────────────────────────
     334              : // Master controls strip (smooth metering stays in GuiManager)
     335              : // ─────────────────────────────────────────────────────────────────────────────
     336            3 : void GuiManager::render_master_controls() {
     337            3 :     smoothed_input_level_  += (engine_.get_input_level()  - smoothed_input_level_)  * 0.3f;
     338            3 :     smoothed_output_level_ += (engine_.get_output_level() - smoothed_output_level_) * 0.3f;
     339              : 
     340            3 :     ImGui::BeginChild("MasterControls", ImVec2(0, 150), true, ImGuiWindowFlags_NoScrollbar);
     341            3 :     ImGui::Columns(4, "master_cols", false);
     342              : 
     343              :     // Input gain
     344            3 :     ImGui::Text("INPUT");
     345            3 :     float input_gain = engine_.get_input_gain();
     346            3 :     if (ImGui::SliderFloat("##InputGain", &input_gain, 0.0f, 5.0f, "%.2f"))
     347            0 :         engine_.set_input_gain(input_gain);
     348              : 
     349            3 :     ImGui::NextColumn();
     350              : 
     351              :     // Input meter
     352            3 :     ImGui::Text("IN LEVEL");
     353            3 :     ImVec2 meter_pos = ImGui::GetCursorScreenPos();
     354            3 :     float  meter_w   = ImGui::GetColumnWidth() - 20;
     355            3 :     ImDrawList* dl   = ImGui::GetWindowDrawList();
     356            3 :     dl->AddRectFilled(meter_pos, ImVec2(meter_pos.x + meter_w, meter_pos.y + 20),
     357              :                       Theme::METER_BG, Theme::ROUNDING_SM);
     358            3 :     float fill = std::min(smoothed_input_level_, 1.0f) * meter_w;
     359            4 :     ImU32 meter_color = (smoothed_input_level_ > 0.9f) ? Theme::METER_RED :
     360            2 :                         (smoothed_input_level_ > 0.6f) ? Theme::METER_YELLOW :
     361              :                                                           Theme::METER_GREEN;
     362            4 :     dl->AddRectFilled(meter_pos, ImVec2(meter_pos.x + fill, meter_pos.y + 20),
     363            1 :                       meter_color, Theme::ROUNDING_SM);
     364            3 :     ImGui::Dummy(ImVec2(meter_w, 20));
     365              : 
     366            3 :     ImGui::NextColumn();
     367              : 
     368              :     // Output meter
     369            3 :     ImGui::Text("OUT LEVEL");
     370            3 :     meter_pos = ImGui::GetCursorScreenPos();
     371            3 :     meter_w   = ImGui::GetColumnWidth() - 20;
     372            3 :     dl->AddRectFilled(meter_pos, ImVec2(meter_pos.x + meter_w, meter_pos.y + 20),
     373              :                       Theme::METER_BG, Theme::ROUNDING_SM);
     374            3 :     fill = std::min(smoothed_output_level_, 1.0f) * meter_w;
     375            4 :     meter_color = (smoothed_output_level_ > 0.9f) ? Theme::METER_RED :
     376            2 :                   (smoothed_output_level_ > 0.6f) ? Theme::METER_YELLOW :
     377              :                                                      Theme::METER_GREEN;
     378            4 :     dl->AddRectFilled(meter_pos, ImVec2(meter_pos.x + fill, meter_pos.y + 20),
     379            1 :                       meter_color, Theme::ROUNDING_SM);
     380            3 :     ImGui::Dummy(ImVec2(meter_w, 20));
     381              : 
     382            3 :     ImGui::NextColumn();
     383              : 
     384              :     // Output gain
     385            3 :     ImGui::Text("OUTPUT");
     386            3 :     if (audio_muted_) {
     387            0 :         ImGui::SameLine();
     388            0 :         ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "MUTED");
     389            0 :     }
     390            3 :     float output_gain = engine_.get_output_gain();
     391            3 :     if (ImGui::SliderFloat("##OutputGain", &output_gain, 0.0f, 2.0f, "%.2f"))
     392            0 :         engine_.set_output_gain(output_gain);
     393              : 
     394            3 :     ImGui::Columns(1);
     395            3 :     ImGui::Separator();
     396            3 :     ImGui::Columns(3, "metronome_cols", false);
     397              : 
     398            3 :     ImGui::Text("METRONOME");
     399            3 :     bool metronome_on = engine_.get_metronome_enabled();
     400            4 :     if (ImGui::Button(metronome_on ? "Stop" : "Play"))
     401            0 :         engine_.toggle_metronome();
     402              : 
     403            3 :     ImGui::NextColumn();
     404              : 
     405            3 :     int bpm = engine_.get_metronome_bpm();
     406            3 :     if (ImGui::SliderInt("BPM", &bpm, 40, 240))
     407            0 :         engine_.set_metronome_bpm(bpm);
     408              : 
     409            3 :     ImGui::NextColumn();
     410              : 
     411            3 :     float click = engine_.get_metronome_volume();
     412            3 :     if (ImGui::SliderFloat("Click", &click, 0.0f, 1.0f, "%.2f"))
     413            0 :         engine_.set_metronome_volume(click);
     414              : 
     415            3 :     ImGui::Columns(1);
     416            3 :     ImGui::EndChild();
     417            3 : }
     418              : 
     419              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1