19#include "../objects/Project.hpp"
29constexpr auto window_function_names =
41 const Project*
const initial_project
43 panel_name(
std::string(
"Signal DFT Panel") + get_imgui_stable_name()),
44 parent_worker(parent_worker),
45 active_project(initial_project),
49 sigc::mem_fun(*
this, &SignalDFTPanel::handle_completed_dft)
52 reset_available_transform_sizes();
55SignalDFTPanel::~SignalDFTPanel() noexcept = default;
61 if (ImGui::Begin(panel_name.c_str())) {
62 if (active_project == nullptr)
63 ImGui::Text(
"No project is loaded.");
65 bool drawn_any = false;
66 std::uint64_t max_sample_count = 0;
68 for (const auto& signal : active_project->observe_signals()) {
69 max_sample_count = std::max(max_sample_count, signal.get_sample_count());
74 update_available_sizes(max_sample_count);
75 draw_configuration_section();
76 draw_preview_section();
78 ImGui::Text(
"No signals are available.");
87 return panel_name.c_str();
91 const Project*
const new_project
94 active_project = new_project;
96 reset_available_transform_sizes();
97 update_spectrum_bounds();
98 reset_viewport_bounds();
101const char* SignalDFTPanel::get_imgui_stable_name() noexcept
103 return "###SignalDFTPanel";
106void SignalDFTPanel::handle_completed_dft(
110 auto spectrum = std::move(result).take_spectrum();
112 if (spectrum ==
nullptr) {
113 LOG_WARN(
"Dropping a null DFT result.");
118 .source_id = result.get_source_id(),
119 .window_function = spectrum->observe_preprocessor(),
120 .transform_size = result.get_transform_size(),
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());
129 cache_slot_it->second.status = CacheValue::State::Success;
130 cache_slot_it->second.spectrum = std::move(spectrum);
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);
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;
139 update_spectrum_bounds(*cache_slot_it->second.spectrum);
141 if (had_no_visible_spectrum)
142 reset_viewport_bounds();
144 app->increment_forced_frames();
148ImPlotPoint SignalDFTPanel::get_indexed_frequency_bin(
150 void*
const user_data
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};
158void SignalDFTPanel::draw_configuration_section() noexcept
160 ImGui::SeparatorText(
"DFT Configuration");
162 if (ImGui::BeginTable(
"##DFTOptionsTable", 2, table_flags)) {
163 ImGui::TableSetupColumn(
"##DFTOptionsTableLabel", ImGuiTableColumnFlags_WidthFixed);
164 ImGui::TableSetupColumn(
"##DFTOptionsTableControl", ImGuiTableColumnFlags_WidthStretch);
166 ImGui::TableNextRow();
167 ImGui::TableNextColumn();
168 draw_configuration_window_function();
170 ImGui::TableNextRow();
171 ImGui::TableNextColumn();
172 draw_configuration_transform_size();
174 ImGui::TableNextRow();
175 ImGui::TableNextColumn();
176 draw_configuration_scale_type();
178 ImGui::TableNextRow();
179 ImGui::TableNextColumn();
180 draw_configuration_preview_actions();
186void SignalDFTPanel::draw_configuration_window_function() noexcept
188 ImGui::TextUnformatted(
"Input Window Function");
189 ImGui::TableNextColumn();
193 if (
auto selected_idx = selected_window.index();
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;
206 if (ImGui::Selectable(window_function_names[item_idx].data(), is_selected) && selected_idx != item_idx) {
207 selected_idx = item_idx;
212 LOG_WARN(
"Invalid window function index was selected. Resetting to the default.");
216 update_spectrum_bounds();
217 reset_viewport_bounds();
221 ImGui::SetItemDefaultFocus();
225 app->increment_forced_frames();
229void SignalDFTPanel::draw_configuration_transform_size() noexcept
231 ImGui::TextUnformatted(
"Transform Size");
232 ImGui::TableNextColumn();
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) {
243 selected_size_log = item_idx + default_size_log;
244 update_spectrum_bounds();
245 reset_viewport_bounds();
249 ImGui::SetItemDefaultFocus();
253 app->increment_forced_frames();
257void SignalDFTPanel::draw_configuration_scale_type() noexcept
259 ImGui::TextUnformatted(
"Logarithmic Frequency Scale");
260 ImGui::TableNextColumn();
262 if (ImGui::Checkbox(
"##DFTOptionsLogScale", &use_log_scale)) {
263 update_spectrum_bounds();
264 reset_viewport_bounds();
268void SignalDFTPanel::draw_configuration_preview_actions() noexcept
270 ImGui::TextUnformatted(
"Preview Actions");
271 ImGui::TableNextColumn();
273 if (ImGui::Button(
"Reset Viewports##DFTOptionsResetViewport"))
274 reset_viewport_bounds();
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();
284void SignalDFTPanel::draw_preview_section() noexcept
286 ImGui::SeparatorText(
"DFT Previews");
288 if (ImPlot::BeginAlignedPlots(
"##DFTAlignedGroup")) {
289 ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
291 for (
const auto& signal :
293 return candidate->is_uniformly_sampled();
295 draw_preview_of_signal(signal);
297 ImPlot::EndAlignedPlots();
298 ImPlot::PopStyleColor();
302void SignalDFTPanel::draw_preview_of_signal(
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) {
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);
318 int plottable_bin_count =
static_cast<int>(spectrum->get_bin_count());
319 CallbackData callback_data = {.spectrum = spectrum, .index_offset = 0};
321 if (plottable_bin_count > 0 && use_log_scale) {
323 --plottable_bin_count;
324 callback_data.index_offset = 1;
329 &SignalDFTPanel::get_indexed_frequency_bin,
342 "Loading DFT of %s with the %s window function...",
345 window_function_names[selected_window.index()].data()
349void SignalDFTPanel::reset_available_transform_sizes()
353 available_sizes.clear();
355 selected_size_log = default_size_log;
358void SignalDFTPanel::update_spectrum_bounds(
362 using BoundType =
decltype(spectrum_bounds.X.Min);
364 const auto bin_count =
static_cast<std::ptrdiff_t>(spectrum.get_bin_count());
368 const ptrdiff_t first_bin_idx = use_log_scale && bin_count > 1 ? 1 : 0;
369 if (first_bin_idx >= bin_count)
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);
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);
384void SignalDFTPanel::update_spectrum_bounds() noexcept
391 const auto selected_transform_size =
std::size_t{1} << selected_size_log;
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);
399void SignalDFTPanel::update_available_sizes(
403 if (available_sizes.empty())
404 reset_available_transform_sizes();
406 if (maximum_sample_count >
std::uint64_t{1} << (available_sizes.size() + default_size_log - 1)) {
415 available_sizes.clear();
424 if (selected_size_log >= available_sizes.size() + default_size_log) {
426 static_cast<unsigned int>(available_sizes.size() + default_size_log - 1);
430void SignalDFTPanel::reset_viewport_bounds() noexcept
432 viewport_bounds = spectrum_bounds;
434 if (use_log_scale && viewport_bounds.X.Min <= 0.0)
435 viewport_bounds.X.Min = 1.0;
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;
442 if (viewport_bounds.Y.Min >= viewport_bounds.Y.Max) {
443 viewport_bounds.Y.Min = 0.0;
444 viewport_bounds.Y.Max = 1.0;
450 const WindowFunctions::AllFunctions window_function,
454 assert(signal !=
nullptr);
457 .source_id =
signal->get_id(),
458 .window_function = window_function,
459 .transform_size = transform_size,
462 auto& entry = spectra_cache[key];
463 if (entry.spectrum !=
nullptr)
464 return entry.spectrum.get();
466 if (entry.status != CacheValue::State::Pending) {
467 entry.status = CacheValue::State::Pending;
474bool SignalDFTPanel::CacheKey::operator==(
478 return key.source_id == source_id && key.transform_size == transform_size &&
479 key.window_function.index() == window_function.
index();
482std::size_t SignalDFTPanel::CacheKeyHash::operator()(
496 return seed ^ value + 0x9e3779b97f4a7c15ULL + (seed << 6U) + (seed >> 2U);
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.
#define LOG_WARN(msg)
Logs an unformatted warning-level message using echomap::Logger::log.
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.
Denotes a completed DFT computation from a DFTTask.
The EchoMap maintains state for the application including WebGPU and Dear ImGui context,...
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...
void submit(std::unique_ptr< ITask > &&task)
Submit some work to the scheduler for execution on the computation thread.
The main EchoMap outermost namespace for all non-exported symbols.
The Constant invocable window function.