Line data Source code
1 : #include "gui/views/gui_recording.h"
2 :
3 : #include <imgui.h>
4 :
5 : #include <algorithm>
6 : #include <cmath>
7 : #include <cstdint>
8 :
9 : #include "common.h"
10 : #include "gui/dialogs/file_dialog.h"
11 : #include "gui/theme/theme.h"
12 :
13 : namespace Amplitron {
14 :
15 9 : void GuiRecording::render() {
16 9 : const RecordingProps& p = props_;
17 :
18 9 : float font_scale = ImGui::GetFontSize() / 14.0f;
19 12 : float base_h = ImGui::GetFrameHeight() + ImGui::GetStyle().WindowPadding.y * 2.0f +
20 9 : ImGui::GetStyle().WindowBorderSize * 2.0f;
21 9 : float panel_height = p.is_recording ? (base_h + 80.0f) : base_h;
22 :
23 9 : ImGui::BeginChild("RecordingPanel", ImVec2(0, panel_height), true,
24 : ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
25 :
26 9 : if (p.is_recording) {
27 : // ── Pause / Resume ──
28 6 : if (p.is_paused) {
29 3 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
30 3 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
31 3 : if (ImGui::Button("RESUME", ImVec2(80 * font_scale, 0)) && p.on_resume) p.on_resume();
32 3 : ImGui::PopStyleColor(2);
33 1 : } else {
34 3 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.5f, 0.1f, 1.0f));
35 3 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.7f, 0.2f, 1.0f));
36 3 : if (ImGui::Button("PAUSE", ImVec2(80 * font_scale, 0)) && p.on_pause) p.on_pause();
37 3 : ImGui::PopStyleColor(2);
38 : }
39 :
40 6 : ImGui::SameLine();
41 :
42 : // ── Stop ──
43 6 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.1f, 0.1f, 1.0f));
44 6 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.2f, 0.2f, 1.0f));
45 6 : if (ImGui::Button("STOP", ImVec2(80 * font_scale, 0))) {
46 0 : if (p.on_stop) p.on_stop();
47 0 : set_state([](RecordingState& st) { st.needs_save = true; });
48 0 : }
49 6 : ImGui::PopStyleColor(2);
50 :
51 6 : ImGui::SameLine();
52 :
53 : // ── Blink indicator ──
54 6 : float t = static_cast<float>(ImGui::GetTime());
55 6 : if (p.is_paused) {
56 3 : ImGui::TextColored(ImVec4(0.8f, 0.7f, 0.2f, 1.0f), " PAUSED");
57 1 : } else {
58 3 : float blink = (std::sin(t * 4.0f) > 0.0f) ? 1.0f : 0.3f;
59 3 : ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.15f, 0.15f, blink));
60 3 : ImGui::Text(" REC");
61 3 : ImGui::PopStyleColor();
62 : }
63 :
64 6 : ImGui::SameLine();
65 :
66 : // ── Timer ──
67 6 : int mins = static_cast<int>(p.duration) / 60;
68 6 : int secs = static_cast<int>(p.duration) % 60;
69 6 : int ms = static_cast<int>((p.duration - static_cast<int>(p.duration)) * 10);
70 6 : ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), " %02d:%02d.%d", mins, secs, ms);
71 :
72 6 : ImGui::SameLine();
73 :
74 : // ── Peak meter ──
75 6 : float peak = p.current_peak;
76 16 : ImGui::TextColored(peak > 0.9f ? ImVec4(1, 0.2f, 0.2f, 1)
77 6 : : peak > 0.6f ? ImVec4(1, 0.8f, 0.2f, 1)
78 2 : : ImVec4(0.2f, 0.8f, 0.2f, 1),
79 2 : " Peak: %.1f dB", peak > 0.0001f ? 20.0f * std::log10(peak) : -96.0f);
80 :
81 6 : ImGui::SameLine(ImGui::GetContentRegionAvail().x - 120);
82 6 : int64_t file_bytes = p.samples_written * 2 * p.channels;
83 6 : if (file_bytes > 1024 * 1024)
84 0 : ImGui::Text("%.1f MB", file_bytes / (1024.0f * 1024.0f));
85 : else
86 6 : ImGui::Text("%.0f KB", file_bytes / 1024.0f);
87 :
88 : // ── Waveform ──
89 6 : ImGui::Spacing();
90 6 : ImVec2 wave_pos = ImGui::GetCursorScreenPos();
91 6 : float wave_w = ImGui::GetContentRegionAvail().x;
92 6 : float wave_h = 50.0f * font_scale;
93 6 : ImDrawList* draw = ImGui::GetWindowDrawList();
94 :
95 6 : draw->AddRectFilled(wave_pos, ImVec2(wave_pos.x + wave_w, wave_pos.y + wave_h),
96 : IM_COL32(20, 18, 16, 255), 4.0f);
97 :
98 6 : float center_y = wave_pos.y + wave_h * 0.5f;
99 6 : draw->AddLine(ImVec2(wave_pos.x, center_y), ImVec2(wave_pos.x + wave_w, center_y),
100 : IM_COL32(60, 55, 48, 255));
101 :
102 6 : ImU32 wave_color = p.is_paused ? IM_COL32(180, 160, 50, 200) : IM_COL32(200, 80, 60, 220);
103 5 : ImU32 wave_color_bright =
104 6 : p.is_paused ? IM_COL32(220, 200, 80, 255) : IM_COL32(255, 100, 70, 255);
105 :
106 6 : if (p.waveform_buf && p.waveform_size > 0) {
107 6 : int num_bars = std::max(1, static_cast<int>(wave_w));
108 6 : float samples_per_pixel = static_cast<float>(p.waveform_size) / num_bars;
109 2214 : for (int i = 0; i < num_bars; ++i) {
110 2208 : int idx = static_cast<int>(i * samples_per_pixel);
111 2208 : if (idx >= p.waveform_size) idx = p.waveform_size - 1;
112 2208 : float val = p.waveform_buf[idx];
113 2208 : float bar_h = val * wave_h * 0.48f;
114 2208 : if (bar_h < 0.5f) continue;
115 0 : float x = wave_pos.x + i;
116 0 : ImU32 col = val > 0.8f ? wave_color_bright : wave_color;
117 0 : draw->AddLine(ImVec2(x, center_y - bar_h), ImVec2(x, center_y + bar_h), col);
118 0 : }
119 2 : }
120 :
121 6 : draw->AddRect(wave_pos, ImVec2(wave_pos.x + wave_w, wave_pos.y + wave_h),
122 : IM_COL32(70, 65, 55, 255), 4.0f);
123 6 : ImGui::Dummy(ImVec2(wave_w, wave_h));
124 :
125 5 : } else if (p.has_unsaved) {
126 : // ── Unsaved recording ──
127 0 : {
128 0 : float avail = ImGui::GetContentRegionAvail().y;
129 0 : float row_h = ImGui::GetFrameHeight();
130 0 : float offset = std::max(0.0f, (avail - row_h) * 0.5f);
131 0 : ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offset);
132 : }
133 :
134 0 : float content_w = ImGui::CalcTextSize("Recording complete").x +
135 0 : ImGui::CalcTextSize(" 999.9 s | ").x + 100.0f + 80.0f +
136 0 : ImGui::GetStyle().ItemSpacing.x * 3.0f;
137 0 : float start_x = (ImGui::GetContentRegionAvail().x - content_w) * 0.5f;
138 0 : if (start_x > 0.0f) ImGui::SetCursorPosX(start_x);
139 :
140 0 : ImGui::TextColored(Theme::Gold(), "Recording complete");
141 0 : ImGui::SameLine();
142 0 : ImGui::Text(" %.1f s | ", p.duration);
143 0 : ImGui::SameLine();
144 :
145 0 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
146 0 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
147 0 : if (ImGui::Button("Save As...", ImVec2(100 * font_scale, 0))) {
148 0 : set_state([](RecordingState& st) { st.needs_save = true; });
149 0 : }
150 0 : ImGui::PopStyleColor(2);
151 :
152 0 : ImGui::SameLine();
153 0 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.1f, 0.1f, 1.0f));
154 0 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.6f, 0.15f, 0.15f, 1.0f));
155 0 : if (ImGui::Button("Discard", ImVec2(80 * font_scale, 0))) {
156 0 : if (p.on_discard) p.on_discard();
157 0 : set_state([](RecordingState& st) { st.status_msg = "Recording discarded."; });
158 0 : }
159 0 : ImGui::PopStyleColor(2);
160 :
161 0 : } else {
162 : // ── Ready ──
163 1 : {
164 3 : float avail = ImGui::GetContentRegionAvail().y;
165 3 : constexpr float row_h = 28.0f;
166 3 : float offset = std::max(0.0f, (avail - row_h) * 0.5f);
167 3 : ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offset);
168 : }
169 3 : ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.05f, 0.05f, 1.0f));
170 3 : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.1f, 0.1f, 1.0f));
171 3 : ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.9f, 0.15f, 0.15f, 1.0f));
172 3 : if (ImGui::Button("REC", ImVec2(90 * font_scale, 0))) {
173 0 : if (p.on_start) p.on_start();
174 0 : }
175 3 : ImGui::PopStyleColor(3);
176 :
177 3 : ImGui::SameLine();
178 4 : ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), " Ready to record | WAV 16-bit %d Hz",
179 3 : p.sample_rate);
180 : }
181 :
182 9 : ImGui::EndChild();
183 9 : }
184 :
185 3 : void GuiRecording::render_save_dialog(std::function<void(const std::string& dest)> on_save_done) {
186 3 : if (!state_.needs_save) return;
187 3 : state_.needs_save = false;
188 :
189 8 : std::string dest = show_save_dialog("recording.wav", "WAV Audio", "wav");
190 3 : if (!dest.empty() && on_save_done) on_save_done(dest);
191 3 : }
192 :
193 : } // namespace Amplitron
|