mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-17 08:32:54 +00:00
When the bars visualization receives a new buffer, it checks if it needs a new buffer, which is only the case after it has repainted. However, after then setting m_is_using_last, which is the flag for this, it checks the buffer size of the passed buffer and returns if that is too small. This means that if the visualizer receives a buffer that is too small, and because of external circumstances the update doesn't run after the buffer modification routine, the m_is_using_last variable is stuck at true, which means that the visualization incorrectly believes that the passed buffer is old and we need not update. This simply fixes that by resetting m_is_using_last if the buffer we're passed is too small, because in that case, we're clearly not using the last buffer anymore. Note: This bug is not exposed by the current SoundPlayer behavior. It will become an issue with future changes, so we should fix it regardless.
128 lines
4.3 KiB
C++
128 lines
4.3 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 <LibDSP/FFT.h>
|
|
#include <LibGUI/Event.h>
|
|
#include <LibGUI/Menu.h>
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGUI/Window.h>
|
|
|
|
u32 round_previous_power_of_2(u32 x);
|
|
|
|
void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event)
|
|
{
|
|
GUI::Frame::paint_event(event);
|
|
GUI::Painter painter(*this);
|
|
|
|
painter.add_clip_rect(event.rect());
|
|
painter.fill_rect(frame_inner_rect(), Color::Black);
|
|
|
|
if (m_sample_buffer.is_empty())
|
|
return;
|
|
|
|
LibDSP::fft(m_sample_buffer, false);
|
|
double max = AK::sqrt(m_sample_count * 2.);
|
|
|
|
double freq_bin = m_samplerate / (double)m_sample_count;
|
|
|
|
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 (double& d : groups)
|
|
d = 0.;
|
|
|
|
int bins_per_group = ceil_div((m_sample_count - 1) / 2, group_count);
|
|
for (int i = 1; i < m_sample_count / 2; i++) {
|
|
groups[i / bins_per_group] += AK::fabs(m_sample_buffer.data()[i].real());
|
|
}
|
|
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 current_xpos = horizontal_margin;
|
|
for (int g = 0; g < group_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_last_id(-1)
|
|
, m_is_using_last(false)
|
|
, m_adjust_frequencies(false)
|
|
{
|
|
m_context_menu = GUI::Menu::construct();
|
|
m_context_menu->add_action(GUI::Action::create_checkable("Adjust frequency energy (for aesthetics)", [&](GUI::Action& action) {
|
|
m_adjust_frequencies = action.is_checked();
|
|
}));
|
|
}
|
|
|
|
// black magic from Hacker's delight
|
|
u32 round_previous_power_of_2(u32 x)
|
|
{
|
|
x = x | (x >> 1);
|
|
x = x | (x >> 2);
|
|
x = x | (x >> 4);
|
|
x = x | (x >> 8);
|
|
x = x | (x >> 16);
|
|
return x - (x >> 1);
|
|
}
|
|
|
|
void BarsVisualizationWidget::set_buffer(RefPtr<Audio::Buffer> buffer, int samples_to_use)
|
|
{
|
|
if (m_is_using_last)
|
|
return;
|
|
m_is_using_last = true;
|
|
// FIXME: We should dynamically adapt to the sample count and e.g. perform the fft over multiple buffers.
|
|
// For now, the visualizer doesn't work with extremely low global sample rates.
|
|
if (buffer->sample_count() < 256) {
|
|
m_is_using_last = false;
|
|
return;
|
|
}
|
|
m_sample_count = round_previous_power_of_2(samples_to_use);
|
|
m_sample_buffer.resize(m_sample_count);
|
|
for (int i = 0; i < m_sample_count; i++) {
|
|
m_sample_buffer.data()[i] = (AK::fabs(buffer->samples()[i].left) + AK::fabs(buffer->samples()[i].right)) / 2.;
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void BarsVisualizationWidget::set_buffer(RefPtr<Audio::Buffer> buffer)
|
|
{
|
|
if (buffer.is_null())
|
|
return;
|
|
if (m_last_id == buffer->id())
|
|
return;
|
|
set_buffer(buffer, buffer->sample_count());
|
|
}
|
|
|
|
void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event)
|
|
{
|
|
m_context_menu->popup(event.screen_position());
|
|
}
|
|
|
|
void BarsVisualizationWidget::set_samplerate(int samplerate)
|
|
{
|
|
m_samplerate = samplerate;
|
|
}
|