From a5d95aa6e86708ae12d9e25b5daca45cde851efa Mon Sep 17 00:00:00 2001 From: Arne Elster Date: Sun, 9 Jan 2022 23:21:55 +0100 Subject: [PATCH] SoundPlayer: Rework FFT visualization The input to the FFT was distorted by the usage of fabs on the samples. It led to a big DC offset and a distorted spectrum. Simply removing fabs improves the quality of the spectrum a lot. The FFT input should be windowed to reduce spectral leakage. This also improves the visual quality of the spectrum. Also, no need to do a FFT of the whole buffer if we only mean to render 64 bars. A 8192 point FFT may smooth out fast local changes but at 44100 hz samplerate that's 200 ms worth of sound which significantly reduces FPS. A better approach for a fluent visualization is to do small FFTs at the current playing position inside the current buffer. There may be a better way to get the current playing position, but for now it's implemented as an estimation depending on how many frames where already rendered with the current buffer. Also I picked y-axis log scale as a default because there's usually a big difference in energy between low and high frequency bands. log scale looks nicer. --- .../SoundPlayer/BarsVisualizationWidget.cpp | 56 +++++++++---------- .../SoundPlayer/BarsVisualizationWidget.h | 10 +++- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp index 09356b785b..2bfe9fb4e7 100644 --- a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp +++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp @@ -8,6 +8,7 @@ #include "BarsVisualizationWidget.h" #include #include +#include #include #include #include @@ -21,52 +22,47 @@ void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray painter.add_clip_rect(event.rect()); painter.fill_rect(frame_inner_rect(), Color::Black); - for (size_t i = 0; i < samples.size(); i++) - m_fft_samples[i] = samples[i]; + for (size_t i = 0; i < fft_size; i++) + m_fft_samples[i] = samples[i] * m_fft_window[i]; LibDSP::fft(m_fft_samples.span(), false); - double max = AK::sqrt(samples.size() * 2.); - double freq_bin = m_samplerate / (double)samples.size(); + Array groups {}; - constexpr int group_count = 60; - Vector groups; - groups.resize(group_count); - if (m_gfx_falling_bars.size() != group_count) { - m_gfx_falling_bars.resize(group_count); - for (int& i : m_gfx_falling_bars) - i = 0; + for (size_t i = 0; i < fft_size / 2; i += values_per_bar) { + double const magnitude = m_fft_samples[i].magnitude(); + groups[i / values_per_bar] = magnitude; + for (size_t j = 0; j < values_per_bar; j++) { + double const magnitude = m_fft_samples[i + j].magnitude(); + groups[i / values_per_bar] += magnitude; + } + groups[i / values_per_bar] /= values_per_bar; } - for (double& d : groups) - d = 0.; - int bins_per_group = ceil_div((samples.size() - 1) / 2, static_cast(group_count)); - for (size_t i = 1; i < samples.size() / 2; i++) { - groups[i / bins_per_group] += AK::abs(m_fft_samples[i].real()); + double const max_peak_value = AK::sqrt(static_cast(fft_size)); + for (size_t i = 0; i < bar_count; i++) { + groups[i] = AK::log(groups[i] + 1) / AK::log(max_peak_value); + if (m_adjust_frequencies) + groups[i] *= 1 + 3.0 * i / bar_count; } - for (int i = 0; i < group_count; i++) - groups[i] /= max * freq_bin / (m_adjust_frequencies ? (clamp(AK::exp((double)i / group_count * 3.) - 1.75, 1., 15.)) : 1.); - const int horizontal_margin = 30; - const int top_vertical_margin = 15; - const int pixels_inbetween_groups = frame_inner_rect().width() > 350 ? 5 : 2; - int pixel_per_group_width = (frame_inner_rect().width() - horizontal_margin * 2 - pixels_inbetween_groups * (group_count - 1)) / group_count; - int max_height = frame_inner_rect().height() - top_vertical_margin; + int const horizontal_margin = 30; + int const top_vertical_margin = 15; + int const pixels_inbetween_groups = frame_inner_rect().width() > 350 ? 5 : 2; + int const pixel_per_group_width = (frame_inner_rect().width() - horizontal_margin * 2 - pixels_inbetween_groups * (bar_count - 1)) / bar_count; + int const max_height = frame_inner_rect().height() - top_vertical_margin; int current_xpos = horizontal_margin; - for (int g = 0; g < group_count; g++) { + for (size_t g = 0; g < bar_count; g++) { m_gfx_falling_bars[g] = AK::min(clamp(max_height - (int)(groups[g] * max_height * 0.8), 0, max_height), m_gfx_falling_bars[g]); painter.fill_rect(Gfx::Rect(current_xpos, max_height - (int)(groups[g] * max_height * 0.8), pixel_per_group_width, (int)(groups[g] * max_height * 0.8)), Gfx::Color::from_rgb(0x95d437)); painter.fill_rect(Gfx::Rect(current_xpos, m_gfx_falling_bars[g], pixel_per_group_width, 2), Gfx::Color::White); current_xpos += pixel_per_group_width + pixels_inbetween_groups; m_gfx_falling_bars[g] += 3; } - - m_is_using_last = false; } BarsVisualizationWidget::BarsVisualizationWidget() - : m_fft_samples(MUST(FixedArray>::try_create(128))) - , m_is_using_last(false) + : m_is_using_last(false) , m_adjust_frequencies(true) { m_context_menu = GUI::Menu::construct(); @@ -76,7 +72,9 @@ BarsVisualizationWidget::BarsVisualizationWidget() frequency_energy_action->set_checked(true); m_context_menu->add_action(frequency_energy_action); - MUST(set_render_sample_count(128)); + m_fft_window = LibDSP::Window::hann(); + + MUST(set_render_sample_count(fft_size)); } void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event) diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h index f6e903b072..ae44cc3253 100644 --- a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h +++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h @@ -8,6 +8,7 @@ #pragma once #include "VisualizationWidget.h" +#include #include #include #include @@ -25,8 +26,13 @@ private: void render(GUI::PaintEvent&, FixedArray const&) override; void context_menu_event(GUI::ContextMenuEvent& event) override; - FixedArray> m_fft_samples; - Vector m_gfx_falling_bars; + static constexpr size_t fft_size = 256; + static constexpr size_t bar_count = 64; + static constexpr size_t values_per_bar = (fft_size / 2) / bar_count; + + Array, fft_size> m_fft_samples {}; + Array m_fft_window {}; + Array m_gfx_falling_bars {}; bool m_is_using_last; bool m_adjust_frequencies; RefPtr m_context_menu;