mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-27 21:42:53 +00:00
For DSP reasons I can't explain myself (yet, sorry), short-time Fourier transform (STFT) is much more accurate and aesthetically pleasing when the windows that select the samples for STFT overlap. This implements that behavior by storing the previous samples and performing windowed FFT over both it as well as the current samples. This gives us 50% overlap between windows, a common standard that is nice to look at.
91 lines
3.8 KiB
C++
91 lines
3.8 KiB
C++
/*
|
|
* Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
|
|
* Copyright (c) 2022, the SerenityOS developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "BarsVisualizationWidget.h"
|
|
#include <AK/Math.h>
|
|
#include <AK/TypedTransfer.h>
|
|
#include <LibDSP/FFT.h>
|
|
#include <LibDSP/Window.h>
|
|
#include <LibGUI/Event.h>
|
|
#include <LibGUI/Menu.h>
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGUI/Window.h>
|
|
|
|
void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray<double> const& samples)
|
|
{
|
|
GUI::Frame::paint_event(event);
|
|
GUI::Painter painter(*this);
|
|
|
|
painter.add_clip_rect(event.rect());
|
|
painter.fill_rect(frame_inner_rect(), Color::Black);
|
|
|
|
// First half of data is from previous iteration, second half is from now.
|
|
// This gives us fully overlapping windows, which result in more accurate and visually appealing STFT.
|
|
for (size_t i = 0; i < fft_size / 2; i++)
|
|
m_fft_samples[i] = m_previous_samples[i] * m_fft_window[i];
|
|
for (size_t i = 0; i < fft_size / 2; i++)
|
|
m_fft_samples[i + fft_size / 2] = samples[i] * m_fft_window[i + fft_size / 2];
|
|
|
|
AK::TypedTransfer<double>::copy(m_previous_samples.data(), samples.data(), samples.size());
|
|
|
|
LibDSP::fft(m_fft_samples.span(), false);
|
|
|
|
Array<double, bar_count> groups {};
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 (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;
|
|
}
|
|
}
|
|
|
|
BarsVisualizationWidget::BarsVisualizationWidget()
|
|
: m_is_using_last(false)
|
|
, m_adjust_frequencies(true)
|
|
{
|
|
m_context_menu = GUI::Menu::construct();
|
|
auto frequency_energy_action = GUI::Action::create_checkable("Adjust frequency energy (for aesthetics)", [&](GUI::Action& action) {
|
|
m_adjust_frequencies = action.is_checked();
|
|
});
|
|
frequency_energy_action->set_checked(true);
|
|
m_context_menu->add_action(frequency_energy_action);
|
|
|
|
m_fft_window = LibDSP::Window<double>::hann<fft_size>();
|
|
|
|
// As we use full-overlapping windows, the passed-in data is only half the size of one FFT operation.
|
|
MUST(set_render_sample_count(fft_size / 2));
|
|
}
|
|
|
|
void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event)
|
|
{
|
|
m_context_menu->popup(event.screen_position());
|
|
}
|