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