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
|