EchoMap 2026-07-25 6d3977c
An experimental cross-platform digital signal processing application for sound-source localisation.
Loading...
Searching...
No Matches
SignalDFTPanel.cpp
Go to the documentation of this file.
1
9
10#include "SignalDFTPanel.hpp"
11
12#include <bit>
13
14#include "../EchoMap.hpp"
15#include "../async/Worker.hpp"
19#include "../objects/Project.hpp"
20#include "../objects/Signal.hpp"
21#include "../utility/Logger.hpp"
23
24namespace
25{
26
27using namespace echomap;
28
29constexpr auto window_function_names =
31
32} // namespace
33
34namespace echomap
35{
36
38 Worker* parent_worker,
39 WorkerResultDespatcher& despatcher,
40 EchoMap* app,
41 const Project* const initial_project
42) :
43 panel_name(std::string("Signal DFT Panel") + get_imgui_stable_name()),
44 parent_worker(parent_worker),
45 active_project(initial_project),
46 app(app)
47{
48 connections.emplace_back(despatcher.dft_finished_channel.nominate_consumer(
49 sigc::mem_fun(*this, &SignalDFTPanel::handle_completed_dft)
50 ));
51
52 reset_available_transform_sizes();
53}
54
55SignalDFTPanel::~SignalDFTPanel() noexcept = default;
56
57SignalDFTPanel::SignalDFTPanel(SignalDFTPanel&&) noexcept = default;
58
59void SignalDFTPanel::draw() noexcept
60{
61 if (ImGui::Begin(panel_name.c_str())) {
62 if (active_project == nullptr)
63 ImGui::Text("No project is loaded.");
64 else {
65 bool drawn_any = false;
66 std::uint64_t max_sample_count = 0;
67
68 for (const auto& signal : active_project->observe_signals()) {
69 max_sample_count = std::max(max_sample_count, signal.get_sample_count());
70 drawn_any = true;
71 }
72
73 if (drawn_any) {
74 update_available_sizes(max_sample_count);
75 draw_configuration_section();
76 draw_preview_section();
77 } else
78 ImGui::Text("No signals are available.");
79 }
80 }
81
82 ImGui::End();
83}
84
85const char* SignalDFTPanel::get_imgui_name() const noexcept
86{
87 return panel_name.c_str();
88}
89
91 const Project* const new_project
92)
93{
94 active_project = new_project;
95 spectra_cache.clear();
96 reset_available_transform_sizes();
97 update_spectrum_bounds();
98 reset_viewport_bounds();
99}
100
101const char* SignalDFTPanel::get_imgui_stable_name() noexcept
102{
103 return "###SignalDFTPanel";
104}
105
106void SignalDFTPanel::handle_completed_dft(
107 DFTResult&& result
108)
109{
110 auto spectrum = std::move(result).take_spectrum();
111
112 if (spectrum == nullptr) {
113 LOG_WARN("Dropping a null DFT result.");
114 return;
115 }
116
117 const CacheKey key{
118 .source_id = result.get_source_id(),
119 .window_function = spectrum->observe_preprocessor(),
120 .transform_size = result.get_transform_size(),
121 };
122
123 const auto cache_slot_it = spectra_cache.find(key);
124 if (cache_slot_it == spectra_cache.end()) {
125 LOG_F_WARN("Dropping an unexpected result for the DFT of Signal {}.", spectrum->get_name());
126 return;
127 }
128
129 cache_slot_it->second.status = CacheValue::State::Success;
130 cache_slot_it->second.spectrum = std::move(spectrum);
131
132 const bool result_is_currently_visible = key.window_function.index() == selected_window.index() &&
133 key.transform_size == (std::size_t{1} << selected_size_log);
134
135 if (result_is_currently_visible) {
136 const auto had_no_visible_spectrum =
137 spectrum_bounds.X.Min > spectrum_bounds.X.Max || spectrum_bounds.Y.Min > spectrum_bounds.Y.Max;
138
139 update_spectrum_bounds(*cache_slot_it->second.spectrum);
140
141 if (had_no_visible_spectrum)
142 reset_viewport_bounds();
143
144 app->increment_forced_frames();
145 }
146}
147
148ImPlotPoint SignalDFTPanel::get_indexed_frequency_bin(
149 int index,
150 void* const user_data
151) noexcept
152{
153 const auto* const info = static_cast<CallbackData*>(user_data);
154 index += info->index_offset;
155 return {info->spectrum->cbegin()[index].frequency, info->spectrum->cbegin()[index].magnitude};
156}
157
158void SignalDFTPanel::draw_configuration_section() noexcept
159{
160 ImGui::SeparatorText("DFT Configuration");
161
162 if (ImGui::BeginTable("##DFTOptionsTable", 2, table_flags)) {
163 ImGui::TableSetupColumn("##DFTOptionsTableLabel", ImGuiTableColumnFlags_WidthFixed);
164 ImGui::TableSetupColumn("##DFTOptionsTableControl", ImGuiTableColumnFlags_WidthStretch);
165
166 ImGui::TableNextRow();
167 ImGui::TableNextColumn();
168 draw_configuration_window_function();
169
170 ImGui::TableNextRow();
171 ImGui::TableNextColumn();
172 draw_configuration_transform_size();
173
174 ImGui::TableNextRow();
175 ImGui::TableNextColumn();
176 draw_configuration_scale_type();
177
178 ImGui::TableNextRow();
179 ImGui::TableNextColumn();
180 draw_configuration_preview_actions();
181
182 ImGui::EndTable();
183 }
184}
185
186void SignalDFTPanel::draw_configuration_window_function() noexcept
187{
188 ImGui::TextUnformatted("Input Window Function");
189 ImGui::TableNextColumn();
190
191 ImGui::SetNextItemWidth(-std::numeric_limits<float>::min());
192
193 if (auto selected_idx = selected_window.index();
194 /*
195 * Disabled string_view::data warnings: we know that the views were constructed from string literals, hence
196 * NULL-terminated. Dear ImGui provides no API for specifying the lengths of the expected data, so we can rely
197 * on the termination here.
198 */
199
200 // NOLINTNEXTLINE(*-suspicious-stringview-data-usage)
201 ImGui::BeginCombo("##DFTOptionsWindowFunction", window_function_names[selected_idx].data())) {
202 for (std::size_t item_idx = 0; item_idx < window_function_names.size(); ++item_idx) {
203 const auto is_selected = item_idx == selected_idx;
204
205 // NOLINTNEXTLINE(*-suspicious-stringview-data-usage)
206 if (ImGui::Selectable(window_function_names[item_idx].data(), is_selected) && selected_idx != item_idx) {
207 selected_idx = item_idx;
208
209 try {
211 } catch (const std::out_of_range&) {
212 LOG_WARN("Invalid window function index was selected. Resetting to the default.");
213 selected_window = WindowFunctions::Constant{};
214 }
215
216 update_spectrum_bounds();
217 reset_viewport_bounds();
218 }
219
220 if (is_selected)
221 ImGui::SetItemDefaultFocus();
222 }
223
224 ImGui::EndCombo();
225 app->increment_forced_frames();
226 }
227}
228
229void SignalDFTPanel::draw_configuration_transform_size() noexcept
230{
231 ImGui::TextUnformatted("Transform Size");
232 ImGui::TableNextColumn();
233
234 ImGui::SetNextItemWidth(-std::numeric_limits<float>::min());
235
236 if (ImGui::BeginCombo("##DFTOptionsTransformSize", available_sizes[selected_size_log - default_size_log].c_str())) {
237 for (unsigned int item_idx = 0; item_idx < available_sizes.size(); ++item_idx) {
238 const auto is_selected = item_idx == selected_size_log - default_size_log;
239 if (ImGui::Selectable(available_sizes[item_idx].c_str(), is_selected) &&
240 selected_size_log != item_idx + default_size_log) {
241
242 // The selected size has changed.
243 selected_size_log = item_idx + default_size_log;
244 update_spectrum_bounds();
245 reset_viewport_bounds();
246 }
247
248 if (is_selected)
249 ImGui::SetItemDefaultFocus();
250 }
251
252 ImGui::EndCombo();
253 app->increment_forced_frames();
254 }
255}
256
257void SignalDFTPanel::draw_configuration_scale_type() noexcept
258{
259 ImGui::TextUnformatted("Logarithmic Frequency Scale");
260 ImGui::TableNextColumn();
261
262 if (ImGui::Checkbox("##DFTOptionsLogScale", &use_log_scale)) {
263 update_spectrum_bounds();
264 reset_viewport_bounds();
265 }
266}
267
268void SignalDFTPanel::draw_configuration_preview_actions() noexcept
269{
270 ImGui::TextUnformatted("Preview Actions");
271 ImGui::TableNextColumn();
272
273 if (ImGui::Button("Reset Viewports##DFTOptionsResetViewport"))
274 reset_viewport_bounds();
275 ImGui::SameLine();
276 if (ImGui::Button("Reset Cached DFTs##DFTOptionsResetCache")) {
277 spectra_cache.clear();
278 update_spectrum_bounds();
279 reset_viewport_bounds();
280 app->increment_forced_frames();
281 }
282}
283
284void SignalDFTPanel::draw_preview_section() noexcept
285{
286 ImGui::SeparatorText("DFT Previews");
287
288 if (ImPlot::BeginAlignedPlots("##DFTAlignedGroup")) {
289 ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
290
291 for (const auto& signal :
292 active_project->share_signals() | std::views::filter([](const std::shared_ptr<Signal>& candidate) {
293 return candidate->is_uniformly_sampled();
294 }))
295 draw_preview_of_signal(signal);
296
297 ImPlot::EndAlignedPlots();
298 ImPlot::PopStyleColor();
299 }
300}
301
302void SignalDFTPanel::draw_preview_of_signal(
304) noexcept
305{
306 const auto* const name = signal->get_imgui_name();
307 if (const auto* const spectrum =
308 get_spectra(std::move(signal), selected_window, std::size_t{1} << selected_size_log);
309 spectrum != nullptr) {
310
311 // Case 1: we got a spectrum immediately.
312 if (ImPlot::BeginPlot(spectrum->get_imgui_name())) {
313 ImPlot::SetupAxes("Frequency (Hz)", "Magnitude (dBfs)");
314 ImPlot::SetupAxisScale(ImAxis_X1, use_log_scale ? ImPlotScale_Log10 : ImPlotScale_Linear);
315 ImPlot::SetupAxisLinks(ImAxis_X1, &viewport_bounds.X.Min, &viewport_bounds.X.Max);
316 ImPlot::SetupAxisLinks(ImAxis_Y1, &viewport_bounds.Y.Min, &viewport_bounds.Y.Max);
317
318 int plottable_bin_count = static_cast<int>(spectrum->get_bin_count());
319 CallbackData callback_data = {.spectrum = spectrum, .index_offset = 0};
320
321 if (plottable_bin_count > 0 && use_log_scale) {
322 // If we're plotting on the log scale, discount the DC component if it exists.
323 --plottable_bin_count;
324 callback_data.index_offset = 1;
325 }
326
327 ImPlot::PlotLineG(
328 "",
329 &SignalDFTPanel::get_indexed_frequency_bin,
330 &callback_data,
331 plottable_bin_count,
332 plotting_spec_2d
333 );
334
335 ImPlot::EndPlot();
336 }
337
338 } else
339 // Case 2: we didn't get one immediately. Either it's pending, or it failed.
340 // TODO: indicate failure here as well as loading.
341 ImGui::Text(
342 "Loading DFT of %s with the %s window function...",
343 name,
344 // NOLINTNEXTLINE(*-suspicious-stringview-data-usage)
345 window_function_names[selected_window.index()].data()
346 );
347}
348
349void SignalDFTPanel::reset_available_transform_sizes()
350{
351 // TODO: what if we have signals, but they all have less than 128 samples?
352
353 available_sizes.clear();
354 available_sizes.push_back(std::to_string(std::size_t{1} << default_size_log));
355 selected_size_log = default_size_log;
356}
357
358void SignalDFTPanel::update_spectrum_bounds(
359 const FrequencySpectrum& spectrum
360) noexcept
361{
362 using BoundType = decltype(spectrum_bounds.X.Min);
363
364 const auto bin_count = static_cast<std::ptrdiff_t>(spectrum.get_bin_count());
365 if (bin_count == 0)
366 return;
367
368 const ptrdiff_t first_bin_idx = use_log_scale && bin_count > 1 ? 1 : 0;
369 if (first_bin_idx >= bin_count)
370 return;
371
372 spectrum_bounds.X.Min =
373 std::min(static_cast<BoundType>(spectrum.cbegin()[first_bin_idx].frequency), spectrum_bounds.X.Min);
374 spectrum_bounds.X.Max =
375 std::max(static_cast<BoundType>(spectrum.cbegin()[bin_count - 1].frequency), spectrum_bounds.X.Max);
376
377 for (auto bin_idx = first_bin_idx; bin_idx < bin_count; ++bin_idx) {
378 const auto magnitude = static_cast<BoundType>(spectrum.cbegin()[bin_idx].magnitude);
379 spectrum_bounds.Y.Min = std::min(magnitude, spectrum_bounds.Y.Min);
380 spectrum_bounds.Y.Max = std::max(magnitude, spectrum_bounds.Y.Max);
381 }
382}
383
384void SignalDFTPanel::update_spectrum_bounds() noexcept
385{
386 spectrum_bounds.X.Min = std::numeric_limits<double>::max();
387 spectrum_bounds.X.Max = std::numeric_limits<double>::lowest();
388 spectrum_bounds.Y.Min = std::numeric_limits<double>::max();
389 spectrum_bounds.Y.Max = std::numeric_limits<double>::lowest();
390
391 const auto selected_transform_size = std::size_t{1} << selected_size_log;
392
393 for (const auto& [key, value] : spectra_cache)
394 if (value.spectrum != nullptr && key.window_function.index() == selected_window.index() &&
395 key.transform_size == selected_transform_size)
396 update_spectrum_bounds(*value.spectrum);
397}
398
399void SignalDFTPanel::update_available_sizes(
400 const std::uint64_t maximum_sample_count
401)
402{
403 if (available_sizes.empty())
404 reset_available_transform_sizes();
405
406 if (maximum_sample_count > std::uint64_t{1} << (available_sizes.size() + default_size_log - 1)) {
407 /*
408 * If the maximum sample count can support a large transform size than currently advertised, re-create the list.
409 * (Yes, we could be smarter here and just append the tail.)
410 */
411 constexpr std::uint64_t minimum_transform_size = std::uint64_t{1} << default_size_log;
412 const std::size_t maximum_transform_size =
413 std::max(minimum_transform_size, std::bit_floor(maximum_sample_count));
414
415 available_sizes.clear();
416 for (std::uint64_t size = minimum_transform_size; size <= maximum_transform_size; size <<= std::uint64_t{1}) {
417 available_sizes.push_back(std::to_string(size));
419 break;
420 }
421 }
422
423 // Once we know the correct maximum transform size, bound the selection to the maximum available.
424 if (selected_size_log >= available_sizes.size() + default_size_log) {
425 selected_size_log =
426 static_cast<unsigned int>(available_sizes.size() + default_size_log - 1);
427 }
428}
429
430void SignalDFTPanel::reset_viewport_bounds() noexcept
431{
432 viewport_bounds = spectrum_bounds;
433
434 if (use_log_scale && viewport_bounds.X.Min <= 0.0)
435 viewport_bounds.X.Min = 1.0;
436
437 if (viewport_bounds.X.Min >= viewport_bounds.X.Max) {
438 viewport_bounds.X.Min = use_log_scale ? 1.0 : 0.0;
439 viewport_bounds.X.Max = 1.0;
440 }
441
442 if (viewport_bounds.Y.Min >= viewport_bounds.Y.Max) {
443 viewport_bounds.Y.Min = 0.0;
444 viewport_bounds.Y.Max = 1.0;
445 }
446}
447
448const FrequencySpectrum* SignalDFTPanel::get_spectra(
450 const WindowFunctions::AllFunctions window_function,
451 const std::size_t transform_size
452)
453{
454 assert(signal != nullptr);
455
456 const CacheKey key{
457 .source_id = signal->get_id(),
458 .window_function = window_function,
459 .transform_size = transform_size,
460 };
461
462 auto& entry = spectra_cache[key];
463 if (entry.spectrum != nullptr)
464 return entry.spectrum.get();
465
466 if (entry.status != CacheValue::State::Pending) {
467 entry.status = CacheValue::State::Pending;
468 parent_worker->submit(std::make_unique<DFTTask>(std::move(signal), window_function, transform_size));
469 }
470
471 return nullptr;
472}
473
474bool SignalDFTPanel::CacheKey::operator==(
475 const CacheKey& key
476) const
477{
478 return key.source_id == source_id && key.transform_size == transform_size &&
479 key.window_function.index() == window_function.index();
480}
481
482std::size_t SignalDFTPanel::CacheKeyHash::operator()(
483 const CacheKey& key
484) const noexcept
485{
486 std::size_t seed = std::hash<id_type>{}(key.source_id);
487 seed = combine(seed, std::hash<std::size_t>{}(key.window_function.index()));
488 return combine(seed, std::hash<std::size_t>{}(key.transform_size));
489}
490
491std::size_t SignalDFTPanel::CacheKeyHash::combine(
492 const std::size_t seed,
493 const std::size_t value
494) noexcept
495{
496 return seed ^ value + 0x9e3779b97f4a7c15ULL + (seed << 6U) + (seed >> 2U);
497}
498
499} // namespace echomap
DFTResult specification.
DFTTask specification.
EchoMap class specification.
FrequencySpectrum specification.
EchoMap portable logger specification.
#define LOG_F_WARN(msg,...)
Logs a formatted warning-level message using echomap::Logger::log_f.
Definition Logger.hpp:115
#define LOG_WARN(msg)
Logs an unformatted warning-level message using echomap::Logger::log.
Definition Logger.hpp:162
SignalDFTPanel specification.
Audio signal class specification.
VariantHelpers specification.
constexpr auto variant_name_array
Array of human-readable names for every alternative in a variant.
Variant variant_from_index(const std::size_t index)
Constructs a std::variant holding the alternative at the given runtime index.
Worker specification.
T bit_floor(T... args)
Denotes a completed DFT computation from a DFTTask.
Definition DFTResult.hpp:27
The EchoMap maintains state for the application including WebGPU and Dear ImGui context,...
Definition EchoMap.hpp:35
A Signal sampled in the frequency domain.
Provides an IPanel to display and interact with previews of Signal frequency spectra (i....
void change_active_project(const Project *new_project) override
Updates the active Project being described by the IPanel.
SignalDFTPanel(Worker *parent_worker, WorkerResultDespatcher &despatcher, EchoMap *app, const Project *initial_project=nullptr)
Create a new SignalDFTPanel to display DFTs of Signal waveforms in the frequency domain.
std::unordered_map< CacheKey, CacheValue, CacheKeyHash > spectra_cache
Cached DFT spectra.
void draw() noexcept override
Draw the panel to the active rendering context.
const char * get_imgui_name() const noexcept override
Retrieves the name of the IPanel for Dear ImGui API functions.
Manages channels for WorkerResult message routing.
ResultChannel< DFTResult > dft_finished_channel
Channel to indicate completion of DFTTask.
A Worker provides an encapsulated thread-safe despatch model for submitting work and reviewing result...
Definition Worker.hpp:45
void submit(std::unique_ptr< ITask > &&task)
Submit some work to the scheduler for execution on the computation thread.
Definition Worker.cpp:27
T index(T... args)
T lowest(T... args)
T make_unique(T... args)
T max(T... args)
T min(T... args)
The main EchoMap outermost namespace for all non-exported symbols.
STL namespace.
T signal(T... args)
T size(T... args)
The Constant invocable window function.
T to_string(T... args)