Line data Source code
1 : #include "gui/views/gui_analyzer.h"
2 : #include "gui/theme/theme.h"
3 : #include "common.h"
4 : #include <imgui.h>
5 : #include <cmath>
6 : #include <algorithm>
7 :
8 : namespace Amplitron {
9 :
10 : // Display axis constants (for drawing only — DSP uses its own internal values)
11 : namespace {
12 : constexpr float kDisplayMinHz = 20.0f;
13 : constexpr float kDisplayMaxHz = 20000.0f;
14 : constexpr float kDisplayMinDb = -90.0f;
15 : constexpr float kDisplayMaxDb = 0.0f;
16 :
17 18 : inline float hz_to_log_norm(float hz) {
18 18 : const float lo = std::log10(kDisplayMinHz);
19 18 : const float hi = std::log10(kDisplayMaxHz);
20 18 : return clamp((std::log10(hz) - lo) / (hi - lo), 0.0f, 1.0f);
21 : }
22 : } // namespace
23 :
24 : // ─────────────────────────────────────────────────────────────────────────────
25 : // VU bar — pure drawing, receives pre-computed scalars
26 : // ─────────────────────────────────────────────────────────────────────────────
27 6 : void GuiAnalyzer::render_vu_bar(const char* id,
28 : const char* label,
29 : float rms_level,
30 : float peak_hold,
31 : bool clip_active,
32 : float clip_flash,
33 : ImU32 base_color,
34 : ImU32 peak_color) {
35 6 : ImGui::PushID(id);
36 6 : ImGui::TextUnformatted(label);
37 6 : ImGui::SameLine();
38 6 : float db_value = (rms_level > 0.0001f) ? (20.0f * std::log10(rms_level)) : -96.0f;
39 6 : ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%.1f dB", db_value);
40 :
41 6 : ImVec2 pos = ImGui::GetCursorScreenPos();
42 6 : float width = ImGui::GetContentRegionAvail().x;
43 6 : float height = 18.0f;
44 6 : ImDrawList* dl = ImGui::GetWindowDrawList();
45 :
46 6 : ImU32 bg_col = Theme::METER_BG;
47 6 : if (clip_active || clip_flash > 0.01f) {
48 0 : const float flash = clamp(clip_flash, 0.0f, 1.0f);
49 0 : int alpha = static_cast<int>(90.0f + flash * 130.0f);
50 0 : bg_col = IM_COL32(180, 30, 30, alpha);
51 0 : }
52 :
53 6 : dl->AddRectFilled(pos, ImVec2(pos.x + width, pos.y + height), bg_col, Theme::ROUNDING_SM);
54 :
55 6 : float rms_fill = clamp(rms_level, 0.0f, 1.0f) * width;
56 6 : dl->AddRectFilled(pos, ImVec2(pos.x + rms_fill, pos.y + height), base_color, Theme::ROUNDING_SM);
57 :
58 6 : float peak_x = pos.x + clamp(peak_hold, 0.0f, 1.0f) * width;
59 6 : dl->AddLine(ImVec2(peak_x, pos.y - 1.0f), ImVec2(peak_x, pos.y + height + 1.0f), peak_color, 2.0f);
60 :
61 6 : if (clip_active || clip_flash > 0.01f)
62 0 : dl->AddText(ImVec2(pos.x + width - 32.0f, pos.y - 1.0f), IM_COL32(255, 90, 90, 255), "CLIP");
63 :
64 6 : ImGui::Dummy(ImVec2(width, height + 6.0f));
65 6 : ImGui::PopID();
66 6 : }
67 :
68 : // ─────────────────────────────────────────────────────────────────────────────
69 : // Spectrum — pure drawing from pre-computed SpectrumAnalyzer arrays
70 : // ─────────────────────────────────────────────────────────────────────────────
71 3 : void GuiAnalyzer::draw_spectrum(ImDrawList* dl,
72 : const ImVec2& pos,
73 : const ImVec2& size) const {
74 3 : if (size.x <= 2.0f || size.y <= 2.0f) return;
75 :
76 3 : const auto& sa = props_.spectrum;
77 3 : const ImVec2 pmax(pos.x + size.x, pos.y + size.y);
78 :
79 3 : dl->AddRect(pos, pmax, IM_COL32(72, 78, 92, 220), Theme::ROUNDING_SM);
80 :
81 : // Reference dB lines
82 3 : const float ref_lines[] = {-60.0f, -48.0f, -36.0f, -24.0f, -12.0f};
83 18 : for (float db : ref_lines) {
84 15 : float t = (db - kDisplayMinDb) / (kDisplayMaxDb - kDisplayMinDb);
85 15 : float y = pmax.y - t * size.y;
86 15 : dl->AddLine(ImVec2(pos.x, y), ImVec2(pmax.x, y), IM_COL32(58, 64, 76, 180), 1.0f);
87 : }
88 :
89 3 : constexpr int BARS = SpectrumAnalyzer::DISPLAY_BARS;
90 3 : const ImU32 input_col = IM_COL32(82, 220, 135, 220);
91 3 : const ImU32 output_col = IM_COL32(92, 170, 255, 220);
92 3 : const ImU32 peak_col = IM_COL32(255, 240, 165, 255);
93 :
94 5 : const auto draw_set = [&](const std::array<float, BARS>& bars,
95 : const std::array<float, BARS>& peaks,
96 : ImU32 bar_col,
97 : float width_scale) {
98 291 : for (int i = 0; i < BARS; ++i) {
99 288 : const float x0 = pos.x + (static_cast<float>(i) / BARS) * size.x;
100 288 : const float x1 = pos.x + (static_cast<float>(i + 1) / BARS) * size.x;
101 288 : const float db = clamp(bars[i], kDisplayMinDb, kDisplayMaxDb);
102 288 : const float t = (db - kDisplayMinDb) / (kDisplayMaxDb - kDisplayMinDb);
103 288 : const float y = pmax.y - t * size.y;
104 :
105 288 : const float center = (x0 + x1) * 0.5f;
106 288 : const float half = (x1 - x0) * 0.5f * width_scale;
107 288 : dl->AddRectFilled(ImVec2(center - half, y), ImVec2(center + half, pmax.y), bar_col, 1.5f);
108 :
109 288 : const float peak_t = (clamp(peaks[i], kDisplayMinDb, kDisplayMaxDb) - kDisplayMinDb) / (kDisplayMaxDb - kDisplayMinDb);
110 288 : const float py = pmax.y - peak_t * size.y;
111 288 : dl->AddLine(ImVec2(center - half, py), ImVec2(center + half, py), peak_col, 1.0f);
112 96 : }
113 4 : };
114 :
115 3 : switch (mode_) {
116 0 : case SpectrumDisplayMode::Input:
117 0 : draw_set(sa.smoothed_input_db, sa.input_peak_db, input_col, 0.82f);
118 0 : break;
119 2 : case SpectrumDisplayMode::Output:
120 3 : draw_set(sa.smoothed_output_db, sa.output_peak_db, output_col, 0.82f);
121 2 : break;
122 0 : case SpectrumDisplayMode::Overlay:
123 0 : draw_set(sa.smoothed_input_db, sa.input_peak_db, input_col, 0.42f);
124 0 : draw_set(sa.smoothed_output_db, sa.output_peak_db, output_col, 0.42f);
125 0 : break;
126 : }
127 :
128 : // Frequency tick lines
129 3 : const float ticks[] = {20.0f, 100.0f, 1000.0f, 5000.0f, 10000.0f, 20000.0f};
130 21 : for (float hz : ticks) {
131 18 : float x = pos.x + hz_to_log_norm(hz) * size.x;
132 18 : dl->AddLine(ImVec2(x, pos.y), ImVec2(x, pmax.y), IM_COL32(52, 58, 72, 180), 1.0f);
133 : }
134 1 : }
135 :
136 : // ─────────────────────────────────────────────────────────────────────────────
137 : // Main panel render
138 : // ─────────────────────────────────────────────────────────────────────────────
139 3 : void GuiAnalyzer::render() {
140 3 : const AnalyzerProps& p = props_;
141 :
142 3 : float panel_h = expanded_ ? 230.0f : 34.0f;
143 3 : ImGui::BeginChild("AnalyzerPanel", ImVec2(0, panel_h), true, ImGuiWindowFlags_NoScrollbar);
144 :
145 3 : const bool expanded = ImGui::CollapsingHeader("Real-Time Analyzer", ImGuiTreeNodeFlags_DefaultOpen);
146 3 : if (expanded != expanded_) {
147 0 : expanded_ = expanded;
148 0 : if (p.on_expanded_changed) p.on_expanded_changed(expanded_);
149 0 : if (p.on_set_analyzer_enabled) p.on_set_analyzer_enabled(expanded_);
150 0 : }
151 :
152 3 : if (!expanded_) {
153 0 : ImGui::EndChild();
154 0 : return;
155 : }
156 :
157 : // ── Mode selector ──
158 3 : int mode_index = static_cast<int>(mode_);
159 3 : ImGui::TextUnformatted("Spectrum:");
160 3 : ImGui::SameLine();
161 3 : ImGui::SetNextItemWidth(150.0f);
162 3 : if (ImGui::Combo("##AnalyzerMode", &mode_index, "Input\0Output\0Overlay\0")) {
163 0 : mode_ = static_cast<SpectrumDisplayMode>(mode_index);
164 0 : if (p.on_mode_changed) p.on_mode_changed(mode_);
165 0 : }
166 :
167 : // ── VU bars (pre-calculated values from props) ──
168 3 : ImGui::Columns(2, "analyzer_vu_cols", false);
169 3 : render_vu_bar("input_vu", "INPUT RMS",
170 3 : p.smoothed_input_rms, p.input_peak_hold,
171 3 : p.input_clip_active, p.input_clip_flash,
172 : IM_COL32(60, 200, 110, 230), IM_COL32(255, 230, 120, 255));
173 3 : ImGui::NextColumn();
174 3 : render_vu_bar("output_vu", "OUTPUT RMS",
175 3 : p.smoothed_output_rms, p.output_peak_hold,
176 3 : p.output_clip_active, p.output_clip_flash,
177 : IM_COL32(80, 170, 245, 230), IM_COL32(255, 230, 120, 255));
178 3 : ImGui::Columns(1);
179 :
180 : // ── Spectrum plot ──
181 3 : ImVec2 plot_pos(ImGui::GetCursorScreenPos());
182 3 : ImVec2 plot_size(ImGui::GetContentRegionAvail().x, 112.0f);
183 3 : ImDrawList* dl = ImGui::GetWindowDrawList();
184 :
185 4 : dl->AddRectFilled(plot_pos,
186 3 : ImVec2(plot_pos.x + plot_size.x, plot_pos.y + plot_size.y),
187 : IM_COL32(20, 22, 28, 255), Theme::ROUNDING_SM);
188 :
189 3 : draw_spectrum(dl, plot_pos, plot_size);
190 3 : ImGui::Dummy(plot_size);
191 :
192 : // ── Frequency axis labels ──
193 3 : const float axis_left = ImGui::GetCursorPosX();
194 3 : const float axis_w = ImGui::GetContentRegionAvail().x;
195 3 : ImGui::TextColored(Theme::TextSecondary(), "20 Hz");
196 3 : ImGui::SameLine(axis_left + axis_w * 0.48f);
197 3 : ImGui::TextColored(Theme::TextSecondary(), "1 kHz");
198 3 : ImGui::SameLine(axis_left + axis_w - 52.0f);
199 3 : ImGui::TextColored(Theme::TextSecondary(), "20 kHz");
200 :
201 3 : ImGui::EndChild();
202 1 : }
203 :
204 : } // namespace Amplitron
|