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