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
|