Line data Source code
1 : #include "gui/components/screen.h"
2 :
3 : #include <algorithm>
4 : #include <cmath>
5 : #include <cstdio>
6 :
7 : #include "audio/effects/amp_cab/cabinet_sim.h"
8 : #include "audio/effects/dynamics/multiband_compressor.h"
9 : #include "audio/effects/utility/looper.h"
10 : #include "audio/effects/utility/tuner.h"
11 : #include "audio/engine/audio_engine.h"
12 : #include "common.h"
13 : #include "gui/dialogs/file_dialog.h"
14 : #include "gui/theme/theme.h"
15 : #include "gui/views/gui_midi.h"
16 : #include "midi/midi_manager.h"
17 :
18 : #ifdef __EMSCRIPTEN__
19 : #include <emscripten.h>
20 : extern "C" {
21 : EMSCRIPTEN_KEEPALIVE void load_ir_callback_screen(uintptr_t cab_ptr, const char* path) {
22 : if (cab_ptr && path) {
23 : auto* cab = reinterpret_cast<Amplitron::CabinetSim*>(cab_ptr);
24 : cab->load_ir(path);
25 : }
26 : }
27 : }
28 : #endif
29 :
30 : namespace Amplitron {
31 :
32 : // Static variables to track active knob drag states across frames for accurate undo commitment
33 : static bool s_knob_was_active = false;
34 : static int s_active_param_index = -1;
35 : static float s_param_value_before_drag = 0.0f;
36 2 : static std::string s_active_knob_id = "";
37 :
38 : static int s_popup_active_param_index = -1;
39 : static float s_popup_param_value_before_edit = 0.0f;
40 2 : static std::string s_active_popup_id = "";
41 :
42 342 : void ScreenComponent::render(ImDrawList* dl, ImVec2 p0, float pedal_width, float zoom,
43 : const ScreenProps& props) {
44 342 : if (!props.effect) return;
45 :
46 342 : switch (props.type) {
47 24 : case ScreenType::Tuner:
48 36 : render_tuner_display(dl, p0, pedal_width, zoom, props);
49 36 : break;
50 36 : case ScreenType::Cabinet:
51 54 : render_ir_cabinet_display(dl, p0, pedal_width, zoom, props);
52 54 : break;
53 48 : case ScreenType::Looper:
54 72 : render_looper_display(dl, p0, pedal_width, zoom, props);
55 72 : break;
56 120 : case ScreenType::MultiBandCompressor:
57 180 : render_multiband_compressor_display(dl, p0, pedal_width, zoom, props);
58 180 : break;
59 : }
60 114 : }
61 :
62 36 : void ScreenComponent::render_tuner_display(ImDrawList* dl, ImVec2 p0, float pedal_width, float zoom,
63 : const ScreenProps& props) {
64 36 : auto* tuner = dynamic_cast<TunerPedal*>(props.effect.get());
65 36 : if (tuner) {
66 36 : float cx = p0.x + pedal_width * 0.5f;
67 :
68 36 : bool has_signal = tuner->signal_detected.load(std::memory_order_relaxed);
69 36 : int note_idx = tuner->detected_note.load(std::memory_order_relaxed);
70 36 : int octave = tuner->detected_octave.load(std::memory_order_relaxed);
71 36 : float cents = tuner->detected_cents.load(std::memory_order_relaxed);
72 36 : float freq = tuner->detected_freq.load(std::memory_order_relaxed);
73 :
74 36 : float display_y = p0.y + 55 * zoom;
75 :
76 36 : if (has_signal && note_idx >= 0) {
77 3 : char note_buf[16];
78 9 : snprintf(note_buf, sizeof(note_buf), "%s%d", TunerPedal::note_name(note_idx), octave);
79 9 : ImVec2 note_size = ImGui::CalcTextSize(note_buf);
80 9 : float note_x = cx - note_size.x * 1.5f;
81 12 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f, ImVec2(note_x, display_y),
82 3 : Theme::TEXT_PRIMARY, note_buf);
83 :
84 9 : display_y += 45 * zoom;
85 :
86 3 : char cents_buf[32];
87 9 : snprintf(cents_buf, sizeof(cents_buf), "%+.1f cents", cents);
88 9 : ImVec2 cents_text_size = ImGui::CalcTextSize(cents_buf);
89 9 : ImGui::SetCursorScreenPos(ImVec2(cx - cents_text_size.x * 0.5f, display_y));
90 9 : float abs_cents = std::fabs(cents);
91 12 : ImVec4 cents_col = (abs_cents < 2.0f) ? ImVec4(0.2f, 0.9f, 0.3f, 1.0f)
92 9 : : (abs_cents < 15.0f) ? ImVec4(0.9f, 0.8f, 0.2f, 1.0f)
93 6 : : ImVec4(0.9f, 0.2f, 0.2f, 1.0f);
94 9 : ImGui::PushStyleColor(ImGuiCol_Text, cents_col);
95 9 : ImGui::TextUnformatted(cents_buf);
96 9 : ImGui::PopStyleColor();
97 :
98 9 : display_y += 22 * zoom;
99 :
100 9 : float bar_w = pedal_width - 30 * zoom;
101 9 : float bar_h = 10 * zoom;
102 9 : float bar_x = p0.x + 15 * zoom;
103 9 : float bar_y = display_y;
104 12 : dl->AddRectFilled(ImVec2(bar_x, bar_y), ImVec2(bar_x + bar_w, bar_y + bar_h),
105 3 : Theme::KNOB_BG, 3.0f * zoom);
106 9 : float center_x = bar_x + bar_w * 0.5f;
107 15 : dl->AddLine(ImVec2(center_x, bar_y - 1 * zoom),
108 9 : ImVec2(center_x, bar_y + bar_h + 1 * zoom), Theme::TEXT_DIM, 1.5f * zoom);
109 9 : float needle_norm = clamp(cents / 50.0f, -1.0f, 1.0f);
110 9 : float needle_x = center_x + needle_norm * (bar_w * 0.5f);
111 9 : ImU32 needle_col = ImGui::ColorConvertFloat4ToU32(cents_col);
112 15 : dl->AddRectFilled(ImVec2(needle_x - 3 * zoom, bar_y - 2 * zoom),
113 9 : ImVec2(needle_x + 3 * zoom, bar_y + bar_h + 2 * zoom), needle_col,
114 3 : 2.0f * zoom);
115 :
116 9 : display_y += bar_h + 14 * zoom;
117 :
118 3 : char freq_buf[32];
119 9 : snprintf(freq_buf, sizeof(freq_buf), "%.1f Hz", freq);
120 9 : ImVec2 freq_size = ImGui::CalcTextSize(freq_buf);
121 9 : ImGui::SetCursorScreenPos(ImVec2(cx - freq_size.x * 0.5f, display_y));
122 9 : ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary());
123 9 : ImGui::TextUnformatted(freq_buf);
124 9 : ImGui::PopStyleColor();
125 :
126 9 : display_y += 22 * zoom;
127 6 : } else {
128 27 : const char* no_sig = "---";
129 27 : ImVec2 ns_size = ImGui::CalcTextSize(no_sig);
130 36 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 2.0f,
131 27 : ImVec2(cx - ns_size.x * 1.5f, display_y), Theme::TEXT_DIM, no_sig);
132 27 : display_y += 45 * zoom;
133 :
134 27 : const char* waiting = "Play a note...";
135 27 : ImVec2 wt_size = ImGui::CalcTextSize(waiting);
136 27 : ImGui::SetCursorScreenPos(ImVec2(cx - wt_size.x * 0.5f, display_y));
137 27 : ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim());
138 27 : ImGui::TextUnformatted(waiting);
139 27 : ImGui::PopStyleColor();
140 :
141 27 : display_y += 22 * zoom;
142 : }
143 :
144 36 : display_y += 8 * zoom;
145 36 : bool mute_on = props.effect->params()[0].value >= 0.5f;
146 36 : const char* mute_label = mute_on ? "[MUTE ON]" : "[MUTE OFF]";
147 36 : ImVec2 ml_size = ImGui::CalcTextSize(mute_label);
148 36 : ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y));
149 36 : ImGui::PushStyleColor(ImGuiCol_Text, mute_on ? ImVec4(0.9f, 0.3f, 0.3f, 1.0f)
150 3 : : ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
151 36 : ImGui::TextUnformatted(mute_label);
152 36 : ImGui::PopStyleColor();
153 :
154 36 : ImGui::SetCursorScreenPos(ImVec2(cx - ml_size.x * 0.5f, display_y));
155 36 : ImGui::SetNextItemAllowOverlap();
156 36 : ImGui::InvisibleButton("##tuner_mute_toggle", ml_size);
157 36 : if (ImGui::IsItemClicked()) {
158 0 : float new_val = mute_on ? 0.0f : 1.0f;
159 0 : props.effect->params()[0].value = new_val;
160 0 : if (props.engine) {
161 0 : props.engine->push_param_change(props.index, 0, new_val);
162 0 : }
163 0 : }
164 36 : if (ImGui::IsItemHovered()) {
165 0 : if (!props.effect->params()[0].tooltip.empty()) {
166 0 : ImGui::SetTooltip("Click to toggle mute\n\n%s",
167 0 : props.effect->params()[0].tooltip.c_str());
168 0 : } else {
169 0 : ImGui::SetTooltip("Click to toggle mute");
170 : }
171 0 : }
172 12 : }
173 36 : }
174 :
175 54 : void ScreenComponent::render_ir_cabinet_display(ImDrawList* dl, ImVec2 p0, float pedal_width,
176 : float zoom, const ScreenProps& props) {
177 54 : auto* ir_cab = dynamic_cast<CabinetSim*>(props.effect.get());
178 54 : if (ir_cab) {
179 54 : float cx = p0.x + pedal_width * 0.5f;
180 54 : float display_y = p0.y + 50 * zoom;
181 :
182 54 : float btn_w = pedal_width - 30 * zoom;
183 54 : ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
184 54 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.20f, 0.16f, 1.0f));
185 54 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.30f, 0.18f, 1.0f));
186 54 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.42f, 0.20f, 1.0f));
187 18 : char load_id[64];
188 54 : snprintf(load_id, sizeof(load_id), "Load IR##ir_load_%d", props.index);
189 54 : if (ImGui::Button(load_id, ImVec2(btn_w, 22 * zoom))) {
190 : #ifdef __EMSCRIPTEN__
191 : EM_ASM(
192 : {
193 : var cab_ptr = $0;
194 : var input = document.createElement('input');
195 : input.type = 'file';
196 : input.accept = '.wav';
197 : input.onchange = function(e) {
198 : var file = e.target.files[0];
199 : var reader = new FileReader();
200 : reader.onload = function(re) {
201 : var data = new Uint8Array(re.target.result);
202 : var path = "/ir_" + file.name;
203 : FS.writeFile(path, data);
204 : Module.ccall('load_ir_callback_screen', 'v', [ 'number', 'string' ],
205 : [ cab_ptr, path ]);
206 : };
207 : reader.readAsArrayBuffer(file);
208 : };
209 : input.click();
210 : },
211 : (uintptr_t)ir_cab);
212 : #else
213 0 : std::string path = show_open_dialog("Load Impulse Response", "WAV Audio", "wav");
214 0 : if (!path.empty()) {
215 0 : ir_cab->load_ir(path);
216 0 : }
217 : #endif
218 0 : }
219 54 : ImGui::PopStyleColor(3);
220 :
221 54 : display_y += 28 * zoom;
222 :
223 54 : if (ir_cab->has_ir()) {
224 9 : const std::string& ir_name = ir_cab->ir_name();
225 9 : std::string display_name = ir_name;
226 9 : if (display_name.size() > 20) {
227 0 : display_name = display_name.substr(0, 17) + "...";
228 0 : }
229 9 : ImVec2 name_size = ImGui::CalcTextSize(display_name.c_str());
230 9 : ImGui::SetCursorScreenPos(ImVec2(cx - name_size.x * 0.5f, display_y));
231 9 : ImGui::PushStyleColor(ImGuiCol_Text, Theme::TEXT_PRIMARY);
232 9 : ImGui::TextUnformatted(display_name.c_str());
233 9 : ImGui::PopStyleColor();
234 :
235 9 : display_y += 18 * zoom;
236 :
237 3 : char dur_buf[32];
238 9 : snprintf(dur_buf, sizeof(dur_buf), "%.1f ms", ir_cab->ir_duration_ms());
239 9 : ImVec2 dur_size = ImGui::CalcTextSize(dur_buf);
240 9 : ImGui::SetCursorScreenPos(ImVec2(cx - dur_size.x * 0.5f, display_y));
241 9 : ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextSecondary());
242 9 : ImGui::TextUnformatted(dur_buf);
243 9 : ImGui::PopStyleColor();
244 :
245 9 : display_y += 22 * zoom;
246 :
247 9 : ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
248 9 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.12f, 0.10f, 1.0f));
249 9 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.15f, 0.12f, 1.0f));
250 9 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.20f, 0.15f, 1.0f));
251 3 : char clear_id[64];
252 9 : snprintf(clear_id, sizeof(clear_id), "Clear##ir_clear_%d", props.index);
253 9 : if (ImGui::Button(clear_id, ImVec2(btn_w, 20 * zoom))) {
254 0 : ir_cab->clear_ir();
255 0 : }
256 9 : ImGui::PopStyleColor(3);
257 9 : } else {
258 45 : const char* no_ir = "No IR loaded";
259 45 : ImVec2 ni_size = ImGui::CalcTextSize(no_ir);
260 45 : ImGui::SetCursorScreenPos(ImVec2(cx - ni_size.x * 0.5f, display_y));
261 45 : ImGui::PushStyleColor(ImGuiCol_Text, Theme::TextDim());
262 45 : ImGui::TextUnformatted(no_ir);
263 45 : ImGui::PopStyleColor();
264 : }
265 18 : }
266 54 : }
267 :
268 72 : void ScreenComponent::render_looper_display(ImDrawList* dl, ImVec2 p0, float pedal_width,
269 : float zoom, const ScreenProps& props) {
270 72 : auto* looper = dynamic_cast<Looper*>(props.effect.get());
271 72 : if (!looper) return;
272 :
273 72 : float cx = p0.x + pedal_width * 0.5f;
274 72 : float display_y = p0.y + 55 * zoom;
275 :
276 72 : Looper::State st = looper->state();
277 72 : bool has_loop = looper->has_loop();
278 72 : int loop_len = looper->loop_length_samples();
279 72 : int play_pos = looper->playhead_samples();
280 :
281 72 : const char* state_label = "EMPTY";
282 72 : ImVec4 state_col = Theme::TextDim();
283 72 : switch (st) {
284 21 : case Looper::State::Empty:
285 42 : state_label = "EMPTY";
286 42 : state_col = Theme::TextDim();
287 42 : break;
288 0 : case Looper::State::Idle:
289 0 : state_label = "STOP";
290 0 : state_col = Theme::TextSecondary();
291 0 : break;
292 2 : case Looper::State::Recording:
293 3 : state_label = "REC";
294 3 : state_col = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
295 3 : break;
296 2 : case Looper::State::Playing:
297 3 : state_label = "PLAY";
298 3 : state_col = ImVec4(0.2f, 0.9f, 0.3f, 1.0f);
299 3 : break;
300 2 : case Looper::State::Overdubbing:
301 3 : state_label = "DUB";
302 3 : state_col = ImVec4(0.95f, 0.80f, 0.25f, 1.0f);
303 3 : break;
304 : }
305 :
306 72 : ImVec2 st_size = ImGui::CalcTextSize(state_label);
307 72 : ImGui::SetCursorScreenPos(ImVec2(cx - st_size.x * 0.5f, display_y));
308 72 : ImGui::PushStyleColor(ImGuiCol_Text, state_col);
309 72 : ImGui::TextUnformatted(state_label);
310 72 : ImGui::PopStyleColor();
311 :
312 72 : display_y += 18 * zoom;
313 :
314 72 : float bar_w = pedal_width - 30 * zoom;
315 72 : float progress = 0.0f;
316 72 : if (has_loop && loop_len > 0) {
317 6 : progress = clamp(static_cast<float>(play_pos) / static_cast<float>(loop_len), 0.0f, 1.0f);
318 2 : }
319 72 : ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
320 72 : ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.12f, 0.11f, 0.10f, 1.0f));
321 72 : ImGui::PushStyleColor(ImGuiCol_PlotHistogram, state_col);
322 72 : ImGui::ProgressBar(progress, ImVec2(bar_w, 8 * zoom), "");
323 72 : ImGui::PopStyleColor(2);
324 :
325 72 : display_y += 16 * zoom;
326 :
327 72 : float btn_w_total = bar_w;
328 72 : float btn_gap = 8.0f * zoom;
329 72 : float btn_w = (btn_w_total - btn_gap) * 0.5f;
330 72 : float btn_h = 22.0f * zoom;
331 :
332 : // Row 1: Record / Play
333 72 : ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
334 72 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.12f, 0.12f, 1.0f));
335 72 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.50f, 0.18f, 0.18f, 1.0f));
336 72 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.65f, 0.22f, 0.22f, 1.0f));
337 24 : char rec_id[64];
338 72 : std::snprintf(rec_id, sizeof(rec_id), "Record##looper_rec_%d", props.index);
339 72 : if (ImGui::Button(rec_id, ImVec2(btn_w, btn_h))) {
340 0 : looper->request_record_toggle();
341 0 : }
342 72 : ImGui::PopStyleColor(3);
343 72 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("Start/stop recording (new loop)");
344 :
345 72 : ImGui::SameLine(0.0f, btn_gap);
346 72 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.14f, 0.30f, 0.18f, 1.0f));
347 72 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.42f, 0.22f, 1.0f));
348 72 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.22f, 0.55f, 0.28f, 1.0f));
349 24 : char play_id[64];
350 72 : std::snprintf(play_id, sizeof(play_id), "Play/Stop##looper_play_%d", props.index);
351 72 : if (ImGui::Button(play_id, ImVec2(btn_w, btn_h))) {
352 0 : looper->request_play_toggle();
353 0 : }
354 72 : ImGui::PopStyleColor(3);
355 72 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle playback (keeps loop in memory)");
356 :
357 72 : display_y += btn_h + 6 * zoom;
358 :
359 : // Row 2: Overdub / Clear
360 72 : ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
361 72 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.30f, 0.26f, 0.10f, 1.0f));
362 72 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.40f, 0.34f, 0.12f, 1.0f));
363 72 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.52f, 0.45f, 0.15f, 1.0f));
364 24 : char dub_id[64];
365 72 : std::snprintf(dub_id, sizeof(dub_id), "Overdub##looper_dub_%d", props.index);
366 72 : if (ImGui::Button(dub_id, ImVec2(btn_w, btn_h))) {
367 0 : looper->request_overdub_toggle();
368 0 : }
369 72 : ImGui::PopStyleColor(3);
370 72 : if (ImGui::IsItemHovered())
371 3 : ImGui::SetTooltip("Toggle overdub mode (record over existing loop)");
372 :
373 72 : ImGui::SameLine(0.0f, btn_gap);
374 72 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.12f, 0.10f, 1.0f));
375 72 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.35f, 0.15f, 0.12f, 1.0f));
376 72 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.50f, 0.20f, 0.15f, 1.0f));
377 24 : char clr_id[64];
378 72 : std::snprintf(clr_id, sizeof(clr_id), "Clear##looper_clear_%d", props.index);
379 72 : if (ImGui::Button(clr_id, ImVec2(btn_w, btn_h))) {
380 0 : looper->request_clear();
381 0 : }
382 72 : ImGui::PopStyleColor(3);
383 72 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("Clear loop from memory");
384 :
385 72 : display_y += btn_h + 8 * zoom;
386 :
387 : // Loop Level slider (param 0)
388 72 : if (!props.effect->params().empty()) {
389 72 : float& level = props.effect->params()[0].value;
390 72 : ImGui::SetCursorScreenPos(ImVec2(p0.x + 15 * zoom, display_y));
391 72 : ImGui::SetNextItemWidth(bar_w);
392 24 : char slider_id[64];
393 72 : std::snprintf(slider_id, sizeof(slider_id), "##looper_level_%d", props.index);
394 72 : if (ImGui::SliderFloat(slider_id, &level, 0.0f, 1.0f, "Loop Level: %.2f")) {
395 0 : level = clamp(level, 0.0f, 1.0f);
396 0 : if (props.engine) {
397 0 : props.engine->push_param_change(props.index, 0, level);
398 0 : }
399 0 : }
400 :
401 24 : char popup_id[128];
402 72 : std::snprintf(popup_id, sizeof(popup_id), "Popup_%s", slider_id);
403 :
404 72 : if (ImGui::IsItemActivated()) {
405 0 : s_active_popup_id = popup_id;
406 0 : s_popup_active_param_index = 0;
407 0 : s_popup_param_value_before_edit = level;
408 0 : }
409 72 : if (ImGui::IsItemDeactivatedAfterEdit() && s_popup_active_param_index == 0 &&
410 0 : s_active_popup_id == popup_id) {
411 0 : if (level != s_popup_param_value_before_edit && props.on_commit_param_change) {
412 0 : props.on_commit_param_change(0, s_popup_param_value_before_edit, level);
413 0 : }
414 0 : s_popup_active_param_index = -1;
415 0 : s_active_popup_id = "";
416 0 : }
417 72 : if (ImGui::IsItemHovered() && !props.effect->params()[0].tooltip.empty()) {
418 0 : ImGui::SetTooltip("%s", props.effect->params()[0].tooltip.c_str());
419 0 : }
420 24 : }
421 24 : }
422 :
423 180 : void ScreenComponent::render_multiband_compressor_display(ImDrawList* dl, ImVec2 p0,
424 : float pedal_width, float zoom,
425 : const ScreenProps& props) {
426 180 : auto* mb_comp = dynamic_cast<MultiBandCompressor*>(props.effect.get());
427 180 : if (!mb_comp) return;
428 :
429 180 : auto& params = props.effect->params();
430 180 : if (params.size() < 18) return;
431 :
432 180 : const auto* entry = get_effect_color(props.effect->name());
433 180 : ImVec4 led_color = entry ? entry->led_color : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
434 :
435 : // Outer boundaries
436 180 : ImVec2 p1 = ImVec2(p0.x + pedal_width, p0.y + Theme::PEDAL_HEIGHT * zoom);
437 :
438 : // Dynamic horizontal divider separating header/plate and controls
439 300 : dl->AddLine(ImVec2(p0.x + 8.0f * zoom, p0.y + 48.0f * zoom),
440 180 : ImVec2(p1.x - 8.0f * zoom, p0.y + 48.0f * zoom), Theme::BORDER_DARK, 1.0f * zoom);
441 :
442 180 : float col_width = (pedal_width - 24.0f * zoom) / 3.0f;
443 :
444 : // --- REUSABLE KNOB HELPER (LAMBDA) ---
445 3000 : auto render_mb_knob = [&](ImDrawList* dl, ImVec2 center, int pi, float radius,
446 : const char* label_prefix) {
447 2880 : auto& param = params[pi];
448 960 : char label[64];
449 3840 : std::snprintf(label, sizeof(label), "##knob_%s_%d_%d_%s", props.effect->name(), props.index,
450 960 : pi, label_prefix);
451 :
452 2880 : float r = radius * zoom;
453 2880 : float knob_hit_size = r * Theme::KNOB_HIT_MULT;
454 :
455 2880 : ImGui::SetCursorScreenPos(
456 2880 : ImVec2(center.x - knob_hit_size * 0.5f, center.y - knob_hit_size * 0.5f));
457 2880 : ImGui::SetNextItemAllowOverlap();
458 2880 : ImGui::InvisibleButton(label, ImVec2(knob_hit_size, knob_hit_size));
459 :
460 2880 : bool is_hovered = ImGui::IsItemHovered();
461 2880 : bool is_active = ImGui::IsItemActive();
462 :
463 2880 : float range = param.max_val - param.min_val;
464 2880 : if (range <= 0.0001f) range = 1.0f;
465 :
466 2880 : if (is_active && !s_knob_was_active) {
467 3 : s_knob_was_active = true;
468 3 : s_active_param_index = pi;
469 3 : s_param_value_before_drag = param.value;
470 3 : s_active_knob_id = label;
471 1 : }
472 :
473 1921 : if (is_active && s_active_knob_id == label) {
474 3 : float mdy = ImGui::GetIO().MousePos.y - ImGui::GetIO().MousePosPrev.y;
475 3 : if (mdy != 0.0f) {
476 0 : float sensitivity = 0.005f;
477 0 : float value_delta = -mdy * sensitivity * range;
478 0 : if (ImGui::GetIO().KeyShift) value_delta *= 0.2f;
479 0 : if (ImGui::GetIO().KeyCtrl) value_delta *= 3.0f;
480 :
481 0 : float new_val = clamp(param.value + value_delta, param.min_val, param.max_val);
482 0 : if (new_val != param.value) {
483 0 : param.value = new_val;
484 0 : if (props.engine) {
485 0 : props.engine->push_param_change(props.index, pi, new_val);
486 0 : }
487 0 : }
488 0 : }
489 1 : }
490 :
491 2881 : if (s_knob_was_active && !is_active && s_active_param_index == pi &&
492 3 : s_active_knob_id == label) {
493 3 : float new_val = param.value;
494 3 : if (new_val != s_param_value_before_drag && props.on_commit_param_change) {
495 0 : props.on_commit_param_change(pi, s_param_value_before_drag, new_val);
496 0 : }
497 3 : s_active_param_index = -1;
498 3 : s_knob_was_active = false;
499 3 : s_active_knob_id = "";
500 1 : }
501 :
502 2880 : if (is_hovered && std::fabs(ImGui::GetIO().MouseWheel) > 0.0f) {
503 3 : float old_val = param.value;
504 3 : float step = range * 0.03f;
505 3 : if (ImGui::GetIO().KeyShift) step *= 0.2f;
506 2 : float new_val =
507 3 : clamp(param.value + ImGui::GetIO().MouseWheel * step, param.min_val, param.max_val);
508 3 : if (new_val != old_val) {
509 3 : param.value = new_val;
510 3 : if (props.engine) {
511 3 : props.engine->push_param_change(props.index, pi, new_val);
512 1 : }
513 3 : if (props.on_commit_param_change) {
514 3 : props.on_commit_param_change(pi, old_val, new_val);
515 1 : }
516 1 : }
517 1 : }
518 :
519 2880 : if (is_hovered && ImGui::IsMouseDoubleClicked(0)) {
520 0 : float old_val = param.value;
521 0 : float new_val = param.default_val;
522 0 : if (new_val != old_val) {
523 0 : param.value = new_val;
524 0 : if (props.engine) {
525 0 : props.engine->push_param_change(props.index, pi, new_val);
526 0 : }
527 0 : if (props.on_commit_param_change) {
528 0 : props.on_commit_param_change(pi, old_val, new_val);
529 0 : }
530 0 : }
531 0 : }
532 :
533 2880 : if (is_hovered && ImGui::IsMouseClicked(1)) {
534 0 : ImGui::OpenPopup(label);
535 0 : }
536 2880 : if (ImGui::BeginPopup(label)) {
537 0 : ImGui::Text("%s", param.name.c_str());
538 0 : ImGui::SetNextItemWidth(120);
539 0 : float slider_val = param.value;
540 0 : if (ImGui::SliderFloat("##edit", &slider_val, param.min_val, param.max_val, "%.2f")) {
541 0 : param.value = slider_val;
542 0 : if (props.engine) {
543 0 : props.engine->push_param_change(props.index, pi, slider_val);
544 0 : }
545 0 : }
546 0 : if (ImGui::IsItemActivated()) {
547 0 : s_active_popup_id = label;
548 0 : s_popup_active_param_index = pi;
549 0 : s_popup_param_value_before_edit = param.value;
550 0 : }
551 0 : if (ImGui::IsItemDeactivatedAfterEdit() && s_popup_active_param_index == pi &&
552 0 : s_active_popup_id == label) {
553 0 : if (param.value != s_popup_param_value_before_edit &&
554 0 : props.on_commit_param_change) {
555 0 : props.on_commit_param_change(pi, s_popup_param_value_before_edit, param.value);
556 0 : }
557 0 : s_popup_active_param_index = -1;
558 0 : s_active_popup_id = "";
559 0 : }
560 0 : if (ImGui::Button("Reset")) {
561 0 : float old_val = param.value;
562 0 : float new_val = param.default_val;
563 0 : if (new_val != old_val) {
564 0 : param.value = new_val;
565 0 : if (props.engine) {
566 0 : props.engine->push_param_change(props.index, pi, new_val);
567 0 : }
568 0 : if (props.on_commit_param_change) {
569 0 : props.on_commit_param_change(pi, old_val, new_val);
570 0 : }
571 0 : }
572 0 : ImGui::CloseCurrentPopup();
573 0 : }
574 0 : ImGui::Separator();
575 0 : ImGui::TextColored(Theme::Gold(), "MIDI Control");
576 0 : if (props.gui_midi) {
577 0 : if (props.gui_midi->render_remove_mapping_item(props.effect->name(), param.name)) {
578 0 : ImGui::CloseCurrentPopup();
579 0 : }
580 0 : if (props.gui_midi->render_learn_menu_item(props.effect->name(), param.name)) {
581 0 : ImGui::CloseCurrentPopup();
582 0 : }
583 0 : ImGui::Spacing();
584 0 : if (props.gui_midi->render_remove_bypass_item(props.effect->name())) {
585 0 : ImGui::CloseCurrentPopup();
586 0 : }
587 0 : if (props.gui_midi->render_learn_bypass_item(props.effect->name())) {
588 0 : ImGui::CloseCurrentPopup();
589 0 : }
590 0 : } else {
591 0 : ImGui::TextDisabled("MIDI manager not available");
592 : }
593 0 : ImGui::EndPopup();
594 0 : }
595 :
596 : // Draw track
597 2880 : float normalized = (param.value - param.min_val) / range;
598 2880 : constexpr float ARC_START = 2.356f;
599 2880 : constexpr float ARC_RANGE = 4.712f;
600 2880 : float track_radius = r + 2.5f * zoom;
601 2880 : int segments = 30;
602 89280 : for (int s = 0; s < segments; ++s) {
603 86400 : float t0 = static_cast<float>(s) / segments;
604 86400 : float t1 = static_cast<float>(s + 1) / segments;
605 86400 : float a0 = ARC_START + t0 * ARC_RANGE;
606 86400 : float a1 = ARC_START + t1 * ARC_RANGE;
607 :
608 86400 : bool filled = t0 <= normalized;
609 57600 : ImU32 seg_color =
610 86400 : filled ? ImGui::ColorConvertFloat4ToU32(led_color) : Theme::KNOB_TRACK_OFF;
611 :
612 172800 : dl->AddLine(ImVec2(center.x + std::cos(a0) * track_radius,
613 86400 : center.y + std::sin(a0) * track_radius),
614 144000 : ImVec2(center.x + std::cos(a1) * track_radius,
615 86400 : center.y + std::sin(a1) * track_radius),
616 86400 : seg_color, 2.0f * zoom);
617 28800 : }
618 :
619 2872 : ImU32 knob_bg =
620 2880 : is_active ? Theme::KNOB_ACTIVE : (is_hovered ? Theme::KNOB_HOVER : Theme::KNOB_FACE);
621 2880 : dl->AddCircleFilled(center, r, Theme::KNOB_BG);
622 2880 : dl->AddCircleFilled(center, r - 1.0f * zoom, knob_bg);
623 :
624 : #ifndef AMPLITRON_NO_MIDI
625 1568 : if (props.gui_midi && props.gui_midi->midi().is_learning() &&
626 2224 : props.gui_midi->midi().learn_effect_name() == props.effect->name() &&
627 0 : props.gui_midi->midi().learn_param_name() == param.name) {
628 0 : float time = static_cast<float>(ImGui::GetTime());
629 0 : float alpha = (std::sin(time * 2.0f * 3.14159f * 10.0f) + 1.0f) * 0.5f;
630 0 : ImU32 outline_col =
631 0 : ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.6f, 1.0f, 0.4f + alpha * 0.6f));
632 0 : dl->AddCircle(center, r + 3.0f * zoom, outline_col, 0, 2.0f * zoom);
633 0 : }
634 : #endif
635 :
636 2880 : float pointer_angle = ARC_START + normalized * ARC_RANGE;
637 2880 : float ptr_inner = r * 0.25f;
638 2880 : float ptr_outer = r - 2.0f * zoom;
639 4800 : ImVec2 ptr_from = ImVec2(center.x + std::cos(pointer_angle) * ptr_inner,
640 2880 : center.y + std::sin(pointer_angle) * ptr_inner);
641 4800 : ImVec2 ptr_to = ImVec2(center.x + std::cos(pointer_angle) * ptr_outer,
642 2880 : center.y + std::sin(pointer_angle) * ptr_outer);
643 2880 : ImU32 ptr_color = is_active ? Theme::ACCENT_GOLD_HOT : Theme::ACCENT_GOLD;
644 2880 : dl->AddLine(ptr_from, ptr_to, ptr_color, 2.0f * zoom);
645 :
646 : // Tooltip
647 2880 : if (is_hovered || is_active) {
648 24 : std::string val_str = Theme::formatParameterValue(param.value, param.unit);
649 24 : std::string min_str = Theme::formatParameterValue(param.min_val, param.unit);
650 24 : std::string max_str = Theme::formatParameterValue(param.max_val, param.unit);
651 8 : std::string midi_info =
652 32 : props.gui_midi ? props.gui_midi->get_mapping_info(props.effect->name(), param.name)
653 24 : : "";
654 :
655 24 : if (param.tooltip.empty()) {
656 0 : ImGui::SetTooltip("%s: %s\nRange: [%s, %s]%s", param.name.c_str(), val_str.c_str(),
657 0 : min_str.c_str(), max_str.c_str(), midi_info.c_str());
658 0 : } else {
659 32 : ImGui::SetTooltip("%s: %s\nRange: [%s, %s]\n\n%s%s", param.name.c_str(),
660 8 : val_str.c_str(), min_str.c_str(), max_str.c_str(),
661 8 : param.tooltip.c_str(), midi_info.c_str());
662 : }
663 24 : }
664 :
665 : // Labels
666 2880 : const char* short_name = param.name.c_str();
667 2880 : if (std::strncmp(short_name, "Low ", 4) == 0)
668 900 : short_name += 4;
669 1980 : else if (std::strncmp(short_name, "Mid ", 4) == 0)
670 900 : short_name += 4;
671 1080 : else if (std::strncmp(short_name, "High ", 5) == 0)
672 900 : short_name += 5;
673 :
674 2880 : ImVec2 text_size = ImGui::CalcTextSize(short_name);
675 3840 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.85f,
676 2880 : ImVec2(center.x - text_size.x * 0.5f, center.y + r + 5.0f * zoom),
677 960 : Theme::TEXT_SECONDARY, short_name);
678 :
679 2880 : std::string val_display = Theme::formatParameterValue(param.value, param.unit);
680 2880 : ImVec2 val_size = ImGui::CalcTextSize(val_display.c_str());
681 4799 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.75f,
682 3840 : ImVec2(center.x - val_size.x * 0.5f, center.y - r - 13.0f * zoom),
683 960 : is_active ? Theme::ACCENT_GOLD_HOT : Theme::TEXT_DIM, val_display.c_str());
684 2880 : };
685 :
686 : // --- REUSABLE SLIDER HELPER (LAMBDA) ---
687 480 : auto render_xover_slider = [&](ImDrawList* dl, float track_x, int pi, const char* label_prefix,
688 : bool ticks_on_left) {
689 120 : (void)ticks_on_left;
690 360 : auto& param = params[pi];
691 120 : char label[64];
692 480 : std::snprintf(label, sizeof(label), "##slider_%s_%d_%d_%s", props.effect->name(),
693 360 : props.index, pi, label_prefix);
694 :
695 360 : float track_top = p0.y + 90.0f * zoom;
696 360 : float track_bottom = p0.y + 260.0f * zoom;
697 360 : float range = param.max_val - param.min_val;
698 360 : if (range <= 0.0001f) range = 1.0f;
699 360 : float normalized = (param.value - param.min_val) / range;
700 360 : float handle_y = track_bottom - normalized * (track_bottom - track_top);
701 :
702 : // Click-and-drag detection box
703 360 : ImGui::SetCursorScreenPos(ImVec2(track_x - 12.0f * zoom, track_top));
704 360 : ImGui::SetNextItemAllowOverlap();
705 360 : ImGui::InvisibleButton(label, ImVec2(24.0f * zoom, track_bottom - track_top));
706 :
707 360 : bool is_hovered = ImGui::IsItemHovered();
708 360 : bool is_active = ImGui::IsItemActive();
709 :
710 360 : if (is_active && !s_knob_was_active) {
711 0 : s_knob_was_active = true;
712 0 : s_active_param_index = pi;
713 0 : s_param_value_before_drag = param.value;
714 0 : s_active_knob_id = label;
715 0 : }
716 :
717 240 : if (is_active && s_active_knob_id == label) {
718 0 : float my = ImGui::GetIO().MousePos.y;
719 0 : float norm = (track_bottom - my) / (track_bottom - track_top);
720 0 : norm = clamp(norm, 0.0f, 1.0f);
721 0 : float new_val = param.min_val + norm * range;
722 :
723 : // Prevent crossover overlap
724 0 : if (pi == 0) {
725 0 : float high_val = params[1].value;
726 0 : if (new_val >= high_val) new_val = high_val - 10.0f;
727 0 : } else if (pi == 1) {
728 0 : float low_val = params[0].value;
729 0 : if (new_val <= low_val) new_val = low_val + 10.0f;
730 0 : }
731 :
732 0 : if (new_val != param.value) {
733 0 : param.value = new_val;
734 0 : if (props.engine) {
735 0 : props.engine->push_param_change(props.index, pi, new_val);
736 0 : }
737 0 : }
738 0 : }
739 :
740 360 : if (s_knob_was_active && !is_active && s_active_param_index == pi &&
741 0 : s_active_knob_id == label) {
742 0 : float new_val = param.value;
743 0 : if (new_val != s_param_value_before_drag && props.on_commit_param_change) {
744 0 : props.on_commit_param_change(pi, s_param_value_before_drag, new_val);
745 0 : }
746 0 : s_active_param_index = -1;
747 0 : s_knob_was_active = false;
748 0 : s_active_knob_id = "";
749 0 : }
750 :
751 360 : if (is_hovered && std::fabs(ImGui::GetIO().MouseWheel) > 0.0f) {
752 3 : float old_val = param.value;
753 3 : float step = range * 0.02f;
754 3 : if (ImGui::GetIO().KeyShift) step *= 0.2f;
755 2 : float new_val =
756 3 : clamp(param.value + ImGui::GetIO().MouseWheel * step, param.min_val, param.max_val);
757 :
758 : // Prevent crossover overlap
759 3 : if (pi == 0) {
760 3 : float high_val = params[1].value;
761 3 : if (new_val >= high_val) new_val = high_val - 10.0f;
762 1 : } else if (pi == 1) {
763 0 : float low_val = params[0].value;
764 0 : if (new_val <= low_val) new_val = low_val + 10.0f;
765 0 : }
766 :
767 3 : if (new_val != old_val) {
768 3 : param.value = new_val;
769 3 : if (props.engine) {
770 3 : props.engine->push_param_change(props.index, pi, new_val);
771 1 : }
772 3 : if (props.on_commit_param_change) {
773 3 : props.on_commit_param_change(pi, old_val, new_val);
774 1 : }
775 1 : }
776 1 : }
777 :
778 360 : if (is_hovered && ImGui::IsMouseDoubleClicked(0)) {
779 0 : float old_val = param.value;
780 0 : float new_val = param.default_val;
781 :
782 : // Prevent crossover overlap on reset
783 0 : if (pi == 0) {
784 0 : float high_val = params[1].value;
785 0 : if (new_val >= high_val) new_val = high_val - 10.0f;
786 0 : } else if (pi == 1) {
787 0 : float low_val = params[0].value;
788 0 : if (new_val <= low_val) new_val = low_val + 10.0f;
789 0 : }
790 :
791 0 : if (new_val != old_val) {
792 0 : param.value = new_val;
793 0 : if (props.engine) {
794 0 : props.engine->push_param_change(props.index, pi, new_val);
795 0 : }
796 0 : if (props.on_commit_param_change) {
797 0 : props.on_commit_param_change(pi, old_val, new_val);
798 0 : }
799 0 : }
800 0 : }
801 :
802 : // Draw track vertical line
803 600 : dl->AddRectFilled(ImVec2(track_x - 1.5f * zoom, track_top),
804 360 : ImVec2(track_x + 1.5f * zoom, track_bottom), Theme::KNOB_TRACK_OFF,
805 360 : 1.5f * zoom);
806 :
807 : // Draw Ticks & Labels
808 360 : if (pi == 0) { // Low crossover (50 to 1000 Hz)
809 180 : float tick_hzs[] = {50.0f, 200.0f, 500.0f, 1000.0f};
810 900 : for (float hz : tick_hzs) {
811 720 : float norm = (hz - param.min_val) / range;
812 720 : float ty = track_bottom - norm * (track_bottom - track_top);
813 960 : dl->AddLine(ImVec2(track_x - 4.0f * zoom, ty), ImVec2(track_x, ty),
814 240 : Theme::BORDER_LIGHT, 1.0f * zoom);
815 :
816 240 : char tick_lbl[16];
817 720 : if (hz >= 1000.0f)
818 180 : std::snprintf(tick_lbl, sizeof(tick_lbl), "1k");
819 : else
820 540 : std::snprintf(tick_lbl, sizeof(tick_lbl), "%.0f", hz);
821 :
822 720 : ImVec2 tsz = ImGui::CalcTextSize(tick_lbl);
823 960 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.65f,
824 1200 : ImVec2(track_x - 6.0f * zoom - tsz.x, ty - tsz.y * 0.5f),
825 240 : Theme::TEXT_DIM, tick_lbl);
826 : }
827 60 : } else { // High crossover (1000 to 15000 Hz)
828 180 : float tick_hzs[] = {1000.0f, 4000.0f, 8000.0f, 12000.0f, 15000.0f};
829 1080 : for (float hz : tick_hzs) {
830 900 : float norm = (hz - param.min_val) / range;
831 900 : float ty = track_bottom - norm * (track_bottom - track_top);
832 1200 : dl->AddLine(ImVec2(track_x, ty), ImVec2(track_x + 4.0f * zoom, ty),
833 300 : Theme::BORDER_LIGHT, 1.0f * zoom);
834 :
835 300 : char tick_lbl[16];
836 900 : if (hz >= 1000.0f)
837 900 : std::snprintf(tick_lbl, sizeof(tick_lbl), "%.0fk", hz / 1000.0f);
838 : else
839 0 : std::snprintf(tick_lbl, sizeof(tick_lbl), "%.0f", hz);
840 :
841 900 : ImVec2 tsz = ImGui::CalcTextSize(tick_lbl);
842 1200 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.65f,
843 1500 : ImVec2(track_x + 6.0f * zoom, ty - tsz.y * 0.5f), Theme::TEXT_DIM,
844 300 : tick_lbl);
845 : }
846 : }
847 :
848 : // Draw pill handle
849 360 : ImVec2 handle_center = ImVec2(track_x, handle_y);
850 360 : ImU32 handle_bg =
851 360 : is_active ? Theme::KNOB_ACTIVE : (is_hovered ? Theme::KNOB_HOVER : Theme::KNOB_FACE);
852 357 : ImU32 border_col = (is_active || is_hovered) ? Theme::ACCENT_GOLD_HOT : Theme::ACCENT_GOLD;
853 :
854 600 : dl->AddRectFilled(ImVec2(track_x - 8.0f * zoom, handle_y - 5.0f * zoom),
855 360 : ImVec2(track_x + 8.0f * zoom, handle_y + 5.0f * zoom), Theme::KNOB_BG,
856 360 : 3.0f * zoom);
857 600 : dl->AddRectFilled(ImVec2(track_x - 7.0f * zoom, handle_y - 4.0f * zoom),
858 360 : ImVec2(track_x + 7.0f * zoom, handle_y + 4.0f * zoom), handle_bg,
859 360 : 2.0f * zoom);
860 600 : dl->AddRect(ImVec2(track_x - 8.0f * zoom, handle_y - 5.0f * zoom),
861 360 : ImVec2(track_x + 8.0f * zoom, handle_y + 5.0f * zoom), border_col, 3.0f * zoom,
862 360 : 0, 1.5f * zoom);
863 360 : dl->AddCircleFilled(handle_center, 2.0f * zoom, border_col);
864 :
865 : // Value text at the top of the track
866 360 : std::string val_str = Theme::formatParameterValue(param.value, param.unit);
867 360 : ImVec2 vsz = ImGui::CalcTextSize(val_str.c_str());
868 600 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.75f,
869 360 : ImVec2(track_x - vsz.x * 0.5f, track_top - vsz.y - 4.0f * zoom),
870 120 : is_active ? Theme::ACCENT_GOLD_HOT : Theme::TEXT_SECONDARY, val_str.c_str());
871 :
872 : // Header text above the value
873 360 : const char* s_name = (pi == 0) ? "Low X" : "High X";
874 360 : ImVec2 nsz = ImGui::CalcTextSize(s_name);
875 360 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.75f,
876 360 : ImVec2(track_x - nsz.x * 0.5f, track_top - vsz.y - nsz.y - 6.0f * zoom),
877 120 : Theme::TEXT_DIM, s_name);
878 :
879 360 : if (is_hovered || is_active) {
880 3 : std::string midi_info =
881 12 : props.gui_midi ? props.gui_midi->get_mapping_info(props.effect->name(), param.name)
882 9 : : "";
883 18 : ImGui::SetTooltip(
884 : "%s: %s\nRange: [%s, %s]%s\n\nDrag vertically to adjust\nShift=fine, "
885 : "Ctrl=coarse\nDbl-click to reset",
886 3 : param.name.c_str(), val_str.c_str(),
887 15 : Theme::formatParameterValue(param.min_val, param.unit).c_str(),
888 15 : Theme::formatParameterValue(param.max_val, param.unit).c_str(), midi_info.c_str());
889 9 : }
890 360 : };
891 :
892 : // --- RENDER 3 COLUMNS & THEIR METERS/KNOBS ---
893 180 : const char* titles[3] = {"LOW BAND", "MID BAND", "HIGH BAND"};
894 180 : int band_param_offsets[3] = {2, 7, 12};
895 :
896 720 : for (int b = 0; b < 3; ++b) {
897 540 : float col_left = p0.x + 12.0f * zoom + b * col_width;
898 540 : float col_center = col_left + col_width * 0.5f;
899 :
900 : // Title
901 540 : ImVec2 tsz = ImGui::CalcTextSize(titles[b]);
902 720 : dl->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 0.9f,
903 540 : ImVec2(col_center - tsz.x * 0.5f, p0.y + 55.0f * zoom), Theme::TEXT_PRIMARY,
904 180 : titles[b]);
905 :
906 : // Horizontal Gain Reduction Meter
907 540 : float meter_y = p0.y + 76.0f * zoom;
908 540 : float meter_h = 7.0f * zoom;
909 540 : float meter_w = col_width - 24.0f * zoom;
910 540 : float meter_x = col_left + 12.0f * zoom;
911 :
912 720 : dl->AddRectFilled(ImVec2(meter_x, meter_y), ImVec2(meter_x + meter_w, meter_y + meter_h),
913 180 : Theme::METER_BG, 3.0f * zoom);
914 720 : dl->AddRect(ImVec2(meter_x, meter_y), ImVec2(meter_x + meter_w, meter_y + meter_h),
915 180 : Theme::BORDER_DARK, 3.0f * zoom, 0, 1.0f * zoom);
916 :
917 540 : float gr_db = mb_comp->get_gain_reduction_db(b);
918 540 : float norm_gr = clamp(gr_db / 20.0f, 0.0f, 1.0f);
919 :
920 540 : if (norm_gr > 0.0f) {
921 0 : float fill_x1 = meter_x + meter_w;
922 0 : float fill_x0 = meter_x + meter_w - norm_gr * meter_w;
923 0 : ImU32 fill_color = Theme::METER_GREEN;
924 0 : if (gr_db > 12.0f)
925 0 : fill_color = Theme::METER_RED;
926 0 : else if (gr_db > 6.0f)
927 0 : fill_color = Theme::METER_YELLOW;
928 :
929 0 : dl->AddRectFilled(ImVec2(fill_x0, meter_y + 1.0f * zoom),
930 0 : ImVec2(fill_x1 - 1.0f * zoom, meter_y + meter_h - 1.0f * zoom),
931 0 : fill_color, 2.0f * zoom);
932 0 : }
933 :
934 : // GR Meter Ticks
935 540 : float tick_dbs[] = {0.0f, -3.0f, -6.0f, -12.0f, -20.0f};
936 3240 : for (float db : tick_dbs) {
937 2700 : float t_norm = -db / 20.0f;
938 2700 : float tx = meter_x + meter_w * (1.0f - t_norm);
939 3600 : dl->AddLine(ImVec2(tx, meter_y), ImVec2(tx, meter_y + meter_h + 2.0f * zoom),
940 900 : Theme::BORDER_MID, 1.0f * zoom);
941 : }
942 :
943 : // Render Knobs
944 540 : int p_offset = band_param_offsets[b];
945 540 : float k_radius = 12.0f;
946 :
947 540 : float kx_left = col_left + col_width * 0.28f;
948 540 : float kx_right = col_left + col_width * 0.72f;
949 :
950 : // Row 1: Threshold & Ratio
951 540 : render_mb_knob(dl, ImVec2(kx_left, p0.y + 120.0f * zoom), p_offset + 0, k_radius, "thresh");
952 540 : render_mb_knob(dl, ImVec2(kx_right, p0.y + 120.0f * zoom), p_offset + 1, k_radius, "ratio");
953 :
954 : // Row 2: Attack & Release
955 540 : render_mb_knob(dl, ImVec2(kx_left, p0.y + 185.0f * zoom), p_offset + 2, k_radius, "attack");
956 540 : render_mb_knob(dl, ImVec2(kx_right, p0.y + 185.0f * zoom), p_offset + 3, k_radius,
957 : "release");
958 :
959 : // Row 3: Makeup (Centered)
960 540 : render_mb_knob(dl, ImVec2(col_center, p0.y + 248.0f * zoom), p_offset + 4, k_radius,
961 : "makeup");
962 180 : }
963 :
964 : // --- RENDER 2 INTERACTIVE CROSSOVER SLIDERS ---
965 180 : float x1 = p0.x + 12.0f * zoom + col_width;
966 180 : float x2 = p0.x + 12.0f * zoom + 2.0f * col_width;
967 :
968 180 : render_xover_slider(dl, x1, 0, "low", true);
969 180 : render_xover_slider(dl, x2, 1, "high", false);
970 :
971 : // --- RENDER GLOBAL OUT GAIN ---
972 240 : render_mb_knob(dl,
973 240 : ImVec2(p0.x + pedal_width - 40.0f * zoom,
974 240 : p0.y + Theme::PEDAL_HEIGHT * zoom - Theme::SWITCH_BOTTOM_OFFSET * zoom +
975 180 : 10.0f * zoom),
976 : 17, 13.0f, "outgain");
977 60 : }
978 :
979 : } // namespace Amplitron
|