1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 03:27:45 +00:00

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.
This commit is contained in:
Arne Elster 2022-01-09 23:21:55 +01:00 committed by Andreas Kling
parent 9edaa033e5
commit a5d95aa6e8
2 changed files with 35 additions and 31 deletions

View file

@ -8,6 +8,7 @@
#include "BarsVisualizationWidget.h" #include "BarsVisualizationWidget.h"
#include <AK/Math.h> #include <AK/Math.h>
#include <LibDSP/FFT.h> #include <LibDSP/FFT.h>
#include <LibDSP/Window.h>
#include <LibGUI/Event.h> #include <LibGUI/Event.h>
#include <LibGUI/Menu.h> #include <LibGUI/Menu.h>
#include <LibGUI/Painter.h> #include <LibGUI/Painter.h>
@ -21,52 +22,47 @@ void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray<double>
painter.add_clip_rect(event.rect()); painter.add_clip_rect(event.rect());
painter.fill_rect(frame_inner_rect(), Color::Black); painter.fill_rect(frame_inner_rect(), Color::Black);
for (size_t i = 0; i < samples.size(); i++) for (size_t i = 0; i < fft_size; i++)
m_fft_samples[i] = samples[i]; m_fft_samples[i] = samples[i] * m_fft_window[i];
LibDSP::fft(m_fft_samples.span(), false); LibDSP::fft(m_fft_samples.span(), false);
double max = AK::sqrt(samples.size() * 2.);
double freq_bin = m_samplerate / (double)samples.size(); Array<double, bar_count> groups {};
constexpr int group_count = 60; for (size_t i = 0; i < fft_size / 2; i += values_per_bar) {
Vector<double, group_count> groups; double const magnitude = m_fft_samples[i].magnitude();
groups.resize(group_count); groups[i / values_per_bar] = magnitude;
if (m_gfx_falling_bars.size() != group_count) { for (size_t j = 0; j < values_per_bar; j++) {
m_gfx_falling_bars.resize(group_count); double const magnitude = m_fft_samples[i + j].magnitude();
for (int& i : m_gfx_falling_bars) groups[i / values_per_bar] += magnitude;
i = 0; }
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<size_t>(group_count)); double const max_peak_value = AK::sqrt(static_cast<double>(fft_size));
for (size_t i = 1; i < samples.size() / 2; i++) { for (size_t i = 0; i < bar_count; i++) {
groups[i / bins_per_group] += AK::abs(m_fft_samples[i].real()); 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; int const horizontal_margin = 30;
const int top_vertical_margin = 15; int const top_vertical_margin = 15;
const int pixels_inbetween_groups = frame_inner_rect().width() > 350 ? 5 : 2; int const 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 const pixel_per_group_width = (frame_inner_rect().width() - horizontal_margin * 2 - pixels_inbetween_groups * (bar_count - 1)) / bar_count;
int max_height = frame_inner_rect().height() - top_vertical_margin; int const max_height = frame_inner_rect().height() - top_vertical_margin;
int current_xpos = horizontal_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]); 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, 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); 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; current_xpos += pixel_per_group_width + pixels_inbetween_groups;
m_gfx_falling_bars[g] += 3; m_gfx_falling_bars[g] += 3;
} }
m_is_using_last = false;
} }
BarsVisualizationWidget::BarsVisualizationWidget() BarsVisualizationWidget::BarsVisualizationWidget()
: m_fft_samples(MUST(FixedArray<Complex<double>>::try_create(128))) : m_is_using_last(false)
, m_is_using_last(false)
, m_adjust_frequencies(true) , m_adjust_frequencies(true)
{ {
m_context_menu = GUI::Menu::construct(); m_context_menu = GUI::Menu::construct();
@ -76,7 +72,9 @@ BarsVisualizationWidget::BarsVisualizationWidget()
frequency_energy_action->set_checked(true); frequency_energy_action->set_checked(true);
m_context_menu->add_action(frequency_energy_action); m_context_menu->add_action(frequency_energy_action);
MUST(set_render_sample_count(128)); m_fft_window = LibDSP::Window<double>::hann<fft_size>();
MUST(set_render_sample_count(fft_size));
} }
void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event) void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event)

View file

@ -8,6 +8,7 @@
#pragma once #pragma once
#include "VisualizationWidget.h" #include "VisualizationWidget.h"
#include <AK/Array.h>
#include <AK/Complex.h> #include <AK/Complex.h>
#include <AK/FixedArray.h> #include <AK/FixedArray.h>
#include <LibAudio/Buffer.h> #include <LibAudio/Buffer.h>
@ -25,8 +26,13 @@ private:
void render(GUI::PaintEvent&, FixedArray<double> const&) override; void render(GUI::PaintEvent&, FixedArray<double> const&) override;
void context_menu_event(GUI::ContextMenuEvent& event) override; void context_menu_event(GUI::ContextMenuEvent& event) override;
FixedArray<Complex<double>> m_fft_samples; static constexpr size_t fft_size = 256;
Vector<int> m_gfx_falling_bars; static constexpr size_t bar_count = 64;
static constexpr size_t values_per_bar = (fft_size / 2) / bar_count;
Array<Complex<double>, fft_size> m_fft_samples {};
Array<double, fft_size> m_fft_window {};
Array<int, bar_count> m_gfx_falling_bars {};
bool m_is_using_last; bool m_is_using_last;
bool m_adjust_frequencies; bool m_adjust_frequencies;
RefPtr<GUI::Menu> m_context_menu; RefPtr<GUI::Menu> m_context_menu;