LCOV - code coverage report
Current view: top level - src/gui - gui_manager_frame.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 62.9 % 272 171
Test Date: 2026-06-07 15:51:50 Functions: 86.2 % 29 25

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

Generated by: LCOV version 2.0-1