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