mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 14:47:44 +00:00
SoundPlayer: Auto refresh visualization widgets
Visualization widgets should only have to tell how many samples they need per frame and have a render method which receives all data relevant to draw the next frame.
This commit is contained in:
parent
02462b6068
commit
9edaa033e5
7 changed files with 115 additions and 107 deletions
|
@ -57,7 +57,3 @@ void AlbumCoverVisualizationWidget::start_new_file(StringView filename)
|
||||||
else
|
else
|
||||||
m_album_cover = album_cover_or_error.value();
|
m_album_cover = album_cover_or_error.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AlbumCoverVisualizationWidget::set_buffer(RefPtr<Audio::Buffer>)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,10 +16,10 @@ class AlbumCoverVisualizationWidget final : public VisualizationWidget {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
~AlbumCoverVisualizationWidget() override = default;
|
~AlbumCoverVisualizationWidget() override = default;
|
||||||
void set_buffer(RefPtr<Audio::Buffer>) override;
|
|
||||||
void start_new_file(StringView) override;
|
void start_new_file(StringView) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void render(GUI::PaintEvent&, FixedArray<double> const&) override { }
|
||||||
void paint_event(GUI::PaintEvent&) override;
|
void paint_event(GUI::PaintEvent&) override;
|
||||||
AlbumCoverVisualizationWidget() = default;
|
AlbumCoverVisualizationWidget() = default;
|
||||||
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> get_album_cover(StringView const filename);
|
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> get_album_cover(StringView const filename);
|
||||||
|
|
|
@ -13,9 +13,7 @@
|
||||||
#include <LibGUI/Painter.h>
|
#include <LibGUI/Painter.h>
|
||||||
#include <LibGUI/Window.h>
|
#include <LibGUI/Window.h>
|
||||||
|
|
||||||
u32 round_previous_power_of_2(u32 x);
|
void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray<double> const& samples)
|
||||||
|
|
||||||
void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event)
|
|
||||||
{
|
{
|
||||||
GUI::Frame::paint_event(event);
|
GUI::Frame::paint_event(event);
|
||||||
GUI::Painter painter(*this);
|
GUI::Painter painter(*this);
|
||||||
|
@ -23,13 +21,13 @@ void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event)
|
||||||
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);
|
||||||
|
|
||||||
if (m_sample_buffer.is_empty())
|
for (size_t i = 0; i < samples.size(); i++)
|
||||||
return;
|
m_fft_samples[i] = samples[i];
|
||||||
|
|
||||||
LibDSP::fft(m_sample_buffer.span(), false);
|
LibDSP::fft(m_fft_samples.span(), false);
|
||||||
double max = AK::sqrt(m_sample_count * 2.);
|
double max = AK::sqrt(samples.size() * 2.);
|
||||||
|
|
||||||
double freq_bin = m_samplerate / (double)m_sample_count;
|
double freq_bin = m_samplerate / (double)samples.size();
|
||||||
|
|
||||||
constexpr int group_count = 60;
|
constexpr int group_count = 60;
|
||||||
Vector<double, group_count> groups;
|
Vector<double, group_count> groups;
|
||||||
|
@ -42,9 +40,9 @@ void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event)
|
||||||
for (double& d : groups)
|
for (double& d : groups)
|
||||||
d = 0.;
|
d = 0.;
|
||||||
|
|
||||||
int bins_per_group = ceil_div((m_sample_count - 1) / 2, group_count);
|
int bins_per_group = ceil_div((samples.size() - 1) / 2, static_cast<size_t>(group_count));
|
||||||
for (int i = 1; i < m_sample_count / 2; i++) {
|
for (size_t i = 1; i < samples.size() / 2; i++) {
|
||||||
groups[i / bins_per_group] += AK::fabs(m_sample_buffer.data()[i].real());
|
groups[i / bins_per_group] += AK::abs(m_fft_samples[i].real());
|
||||||
}
|
}
|
||||||
for (int i = 0; i < group_count; i++)
|
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.);
|
groups[i] /= max * freq_bin / (m_adjust_frequencies ? (clamp(AK::exp((double)i / group_count * 3.) - 1.75, 1., 15.)) : 1.);
|
||||||
|
@ -67,7 +65,8 @@ void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event)
|
||||||
}
|
}
|
||||||
|
|
||||||
BarsVisualizationWidget::BarsVisualizationWidget()
|
BarsVisualizationWidget::BarsVisualizationWidget()
|
||||||
: m_is_using_last(false)
|
: m_fft_samples(MUST(FixedArray<Complex<double>>::try_create(128)))
|
||||||
|
, 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,54 +75,11 @@ 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);
|
||||||
}
|
|
||||||
|
|
||||||
// black magic from Hacker's delight
|
MUST(set_render_sample_count(128));
|
||||||
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)
|
void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event)
|
||||||
{
|
{
|
||||||
m_context_menu->popup(event.screen_position());
|
m_context_menu->popup(event.screen_position());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BarsVisualizationWidget::set_samplerate(int samplerate)
|
|
||||||
{
|
|
||||||
m_samplerate = samplerate;
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
#include "VisualizationWidget.h"
|
#include "VisualizationWidget.h"
|
||||||
#include <AK/Complex.h>
|
#include <AK/Complex.h>
|
||||||
|
#include <AK/FixedArray.h>
|
||||||
#include <LibAudio/Buffer.h>
|
#include <LibAudio/Buffer.h>
|
||||||
#include <LibGUI/Frame.h>
|
#include <LibGUI/Frame.h>
|
||||||
|
|
||||||
|
@ -17,21 +18,15 @@ class BarsVisualizationWidget final : public VisualizationWidget {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
~BarsVisualizationWidget() override = default;
|
~BarsVisualizationWidget() override = default;
|
||||||
void set_buffer(RefPtr<Audio::Buffer> buffer) override;
|
|
||||||
void set_samplerate(int samplerate) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BarsVisualizationWidget();
|
BarsVisualizationWidget();
|
||||||
void set_buffer(RefPtr<Audio::Buffer> buffer, int samples_to_use);
|
|
||||||
|
|
||||||
void paint_event(GUI::PaintEvent&) 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;
|
||||||
|
|
||||||
Vector<Complex<double>> m_sample_buffer;
|
FixedArray<Complex<double>> m_fft_samples;
|
||||||
Vector<int> m_gfx_falling_bars;
|
Vector<int> m_gfx_falling_bars;
|
||||||
int m_last_id;
|
|
||||||
int m_sample_count;
|
|
||||||
int m_samplerate;
|
|
||||||
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;
|
||||||
|
|
|
@ -10,7 +10,12 @@
|
||||||
#include <LibAudio/Buffer.h>
|
#include <LibAudio/Buffer.h>
|
||||||
#include <LibGUI/Painter.h>
|
#include <LibGUI/Painter.h>
|
||||||
|
|
||||||
void SampleWidget::paint_event(GUI::PaintEvent& event)
|
SampleWidget::SampleWidget()
|
||||||
|
{
|
||||||
|
MUST(set_render_sample_count(512));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SampleWidget::render(GUI::PaintEvent& event, FixedArray<double> const& samples)
|
||||||
{
|
{
|
||||||
GUI::Frame::paint_event(event);
|
GUI::Frame::paint_event(event);
|
||||||
GUI::Painter painter(*this);
|
GUI::Painter painter(*this);
|
||||||
|
@ -18,38 +23,26 @@ void SampleWidget::paint_event(GUI::PaintEvent& event)
|
||||||
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);
|
||||||
|
|
||||||
float sample_max = 0;
|
|
||||||
int count = 0;
|
|
||||||
int x_offset = frame_inner_rect().x();
|
int x_offset = frame_inner_rect().x();
|
||||||
int x = x_offset;
|
int x = x_offset;
|
||||||
int y_offset = frame_inner_rect().center().y();
|
int y_offset = frame_inner_rect().center().y();
|
||||||
|
|
||||||
if (m_buffer) {
|
if (samples.size() > 0) {
|
||||||
int samples_per_pixel = m_buffer->sample_count() / frame_inner_rect().width();
|
float samples_per_pixel = samples.size() / static_cast<float>(frame_inner_rect().width());
|
||||||
for (int sample_index = 0; sample_index < m_buffer->sample_count() && (x - x_offset) < frame_inner_rect().width(); ++sample_index) {
|
float sample_index = 0;
|
||||||
float sample = AK::fabs((float)m_buffer->samples()[sample_index].left);
|
|
||||||
|
|
||||||
sample_max = max(sample, sample_max);
|
while (sample_index < samples.size()) {
|
||||||
++count;
|
float sample = AK::abs(samples[sample_index]);
|
||||||
|
for (size_t i = 1; i < static_cast<size_t>(samples_per_pixel + 0.5f); i++)
|
||||||
|
sample = max(sample, AK::abs(samples[sample_index]));
|
||||||
|
|
||||||
if (count >= samples_per_pixel) {
|
Gfx::IntPoint min_point = { x, y_offset + static_cast<int>(-sample * frame_inner_rect().height() / 2) };
|
||||||
Gfx::IntPoint min_point = { x, y_offset + static_cast<int>(-sample_max * frame_inner_rect().height() / 2) };
|
Gfx::IntPoint max_point = { x, y_offset + static_cast<int>(sample * frame_inner_rect().height() / 2) };
|
||||||
Gfx::IntPoint max_point = { x++, y_offset + static_cast<int>(sample_max * frame_inner_rect().height() / 2) };
|
painter.draw_line(min_point, max_point, Color::Green);
|
||||||
painter.draw_line(min_point, max_point, Color::Green);
|
sample_index += samples_per_pixel;
|
||||||
|
x++;
|
||||||
count = 0;
|
|
||||||
sample_max = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
painter.draw_line({ x, y_offset }, { frame_inner_rect().width(), y_offset }, Color::Green);
|
painter.draw_line({ x, y_offset }, { frame_inner_rect().width(), y_offset }, Color::Green);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SampleWidget::set_buffer(RefPtr<Audio::Buffer> buffer)
|
|
||||||
{
|
|
||||||
if (m_buffer == buffer)
|
|
||||||
return;
|
|
||||||
m_buffer = buffer;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,20 +10,12 @@
|
||||||
#include "VisualizationWidget.h"
|
#include "VisualizationWidget.h"
|
||||||
#include <LibGUI/Frame.h>
|
#include <LibGUI/Frame.h>
|
||||||
|
|
||||||
namespace Audio {
|
|
||||||
class Buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SampleWidget final : public VisualizationWidget {
|
class SampleWidget final : public VisualizationWidget {
|
||||||
C_OBJECT(SampleWidget)
|
C_OBJECT(SampleWidget)
|
||||||
public:
|
public:
|
||||||
virtual ~SampleWidget() override = default;
|
virtual ~SampleWidget() override = default;
|
||||||
|
|
||||||
void set_buffer(RefPtr<Audio::Buffer>) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SampleWidget() = default;
|
SampleWidget();
|
||||||
virtual void paint_event(GUI::PaintEvent&) override;
|
virtual void render(GUI::PaintEvent&, FixedArray<double> const& samples) override;
|
||||||
|
|
||||||
RefPtr<Audio::Buffer> m_buffer;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,18 +6,94 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/Forward.h>
|
||||||
|
#include <AK/TypedTransfer.h>
|
||||||
#include <LibAudio/Buffer.h>
|
#include <LibAudio/Buffer.h>
|
||||||
#include <LibGUI/Frame.h>
|
#include <LibGUI/Frame.h>
|
||||||
|
#include <LibGUI/Painter.h>
|
||||||
|
|
||||||
class VisualizationWidget : public GUI::Frame {
|
class VisualizationWidget : public GUI::Frame {
|
||||||
C_OBJECT(VisualizationWidget)
|
C_OBJECT(VisualizationWidget)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual void set_buffer(RefPtr<Audio::Buffer> buffer) = 0;
|
virtual void render(GUI::PaintEvent&, FixedArray<double> const& samples) = 0;
|
||||||
virtual void set_samplerate(int) { }
|
|
||||||
|
void set_buffer(RefPtr<Audio::Buffer> buffer)
|
||||||
|
{
|
||||||
|
if (buffer.is_null())
|
||||||
|
return;
|
||||||
|
if (buffer->id() == m_last_buffer_id)
|
||||||
|
return;
|
||||||
|
m_last_buffer_id = buffer->id();
|
||||||
|
|
||||||
|
if (m_sample_buffer.size() != static_cast<size_t>(buffer->sample_count()))
|
||||||
|
m_sample_buffer.resize(buffer->sample_count());
|
||||||
|
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(buffer->sample_count()); i++)
|
||||||
|
m_sample_buffer.data()[i] = (buffer->samples()[i].left + buffer->samples()[i].right) / 2.;
|
||||||
|
|
||||||
|
m_frame_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void set_samplerate(int samplerate)
|
||||||
|
{
|
||||||
|
m_samplerate = samplerate;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void paint_event(GUI::PaintEvent& event) override
|
||||||
|
{
|
||||||
|
if (m_sample_buffer.size() == 0) {
|
||||||
|
Frame::paint_event(event);
|
||||||
|
GUI::Painter painter(*this);
|
||||||
|
painter.add_clip_rect(event.rect());
|
||||||
|
painter.fill_rect(frame_inner_rect(), Color::Black);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_render_buffer.size() == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
size_t buffer_position = (m_frame_count * REFRESH_TIME_MILLISECONDS) * m_samplerate / 1000;
|
||||||
|
if (buffer_position + m_render_buffer.size() >= m_sample_buffer.size())
|
||||||
|
buffer_position = m_sample_buffer.size() - m_render_buffer.size();
|
||||||
|
|
||||||
|
AK::TypedTransfer<double>::copy(m_render_buffer.data(), m_sample_buffer.span().slice(buffer_position).data(), m_render_buffer.size());
|
||||||
|
|
||||||
|
render(event, m_render_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void timer_event(Core::TimerEvent&) override
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
m_frame_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t frame_count() const { return m_frame_count; }
|
||||||
|
|
||||||
|
ErrorOr<void> set_render_sample_count(size_t count)
|
||||||
|
{
|
||||||
|
auto new_buffer = TRY(FixedArray<double>::try_create(count));
|
||||||
|
m_render_buffer.swap(new_buffer);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
virtual void start_new_file(StringView) { }
|
virtual void start_new_file(StringView) { }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
VisualizationWidget() = default;
|
int m_samplerate;
|
||||||
|
int m_last_buffer_id;
|
||||||
|
size_t m_frame_count;
|
||||||
|
Vector<double> m_sample_buffer;
|
||||||
|
FixedArray<double> m_render_buffer;
|
||||||
|
|
||||||
|
static constexpr size_t REFRESH_TIME_MILLISECONDS = 30;
|
||||||
|
|
||||||
|
VisualizationWidget()
|
||||||
|
: m_samplerate(-1)
|
||||||
|
, m_last_buffer_id(-1)
|
||||||
|
, m_frame_count(0)
|
||||||
|
{
|
||||||
|
start_timer(REFRESH_TIME_MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
virtual ~VisualizationWidget() = default;
|
virtual ~VisualizationWidget() = default;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue