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
Notes: sideshowbarker 2024-07-17 18:13:59 +09:00
2 changed files with 35 additions and 31 deletions

View File

@ -8,6 +8,7 @@
#include "BarsVisualizationWidget.h"
#include <AK/Math.h>
#include <LibDSP/FFT.h>
#include <LibDSP/Window.h>
#include <LibGUI/Event.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
@ -21,52 +22,47 @@ void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray<double>
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<double, bar_count> groups {};
constexpr int group_count = 60;
Vector<double, group_count> 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<size_t>(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<double>(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<Complex<double>>::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<double>::hann<fft_size>();
MUST(set_render_sample_count(fft_size));
}
void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event)

View File

@ -8,6 +8,7 @@
#pragma once
#include "VisualizationWidget.h"
#include <AK/Array.h>
#include <AK/Complex.h>
#include <AK/FixedArray.h>
#include <LibAudio/Buffer.h>
@ -25,8 +26,13 @@ private:
void render(GUI::PaintEvent&, FixedArray<double> const&) override;
void context_menu_event(GUI::ContextMenuEvent& event) override;
FixedArray<Complex<double>> m_fft_samples;
Vector<int> 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<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_adjust_frequencies;
RefPtr<GUI::Menu> m_context_menu;