mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-21 10:19:03 +03: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:
parent
9edaa033e5
commit
a5d95aa6e8
Notes:
sideshowbarker
2024-07-17 18:13:59 +09:00
Author: https://github.com/standardexe Commit: https://github.com/SerenityOS/serenity/commit/a5d95aa6e8 Pull-request: https://github.com/SerenityOS/serenity/pull/12868
@ -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)
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user