From 9edaa033e59e40bc8ac8084e52b2801a1d938243 Mon Sep 17 00:00:00 2001 From: Arne Elster Date: Sun, 9 Jan 2022 22:46:06 +0100 Subject: [PATCH] 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. --- .../AlbumCoverVisualizationWidget.cpp | 4 - .../AlbumCoverVisualizationWidget.h | 2 +- .../SoundPlayer/BarsVisualizationWidget.cpp | 68 +++------------ .../SoundPlayer/BarsVisualizationWidget.h | 11 +-- .../Applications/SoundPlayer/SampleWidget.cpp | 43 ++++------ .../Applications/SoundPlayer/SampleWidget.h | 12 +-- .../SoundPlayer/VisualizationWidget.h | 82 ++++++++++++++++++- 7 files changed, 115 insertions(+), 107 deletions(-) diff --git a/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.cpp b/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.cpp index 2e69a68b00e..888c555257d 100644 --- a/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.cpp +++ b/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.cpp @@ -57,7 +57,3 @@ void AlbumCoverVisualizationWidget::start_new_file(StringView filename) else m_album_cover = album_cover_or_error.value(); } - -void AlbumCoverVisualizationWidget::set_buffer(RefPtr) -{ -} diff --git a/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.h b/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.h index 9cc54c5d0b8..29fa5b5c6e3 100644 --- a/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.h +++ b/Userland/Applications/SoundPlayer/AlbumCoverVisualizationWidget.h @@ -16,10 +16,10 @@ class AlbumCoverVisualizationWidget final : public VisualizationWidget { public: ~AlbumCoverVisualizationWidget() override = default; - void set_buffer(RefPtr) override; void start_new_file(StringView) override; private: + void render(GUI::PaintEvent&, FixedArray const&) override { } void paint_event(GUI::PaintEvent&) override; AlbumCoverVisualizationWidget() = default; ErrorOr> get_album_cover(StringView const filename); diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp index 648e141fa1b..09356b785bd 100644 --- a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp +++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp @@ -13,9 +13,7 @@ #include #include -u32 round_previous_power_of_2(u32 x); - -void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event) +void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray const& samples) { GUI::Frame::paint_event(event); GUI::Painter painter(*this); @@ -23,13 +21,13 @@ void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event) painter.add_clip_rect(event.rect()); painter.fill_rect(frame_inner_rect(), Color::Black); - if (m_sample_buffer.is_empty()) - return; + for (size_t i = 0; i < samples.size(); i++) + m_fft_samples[i] = samples[i]; - LibDSP::fft(m_sample_buffer.span(), false); - double max = AK::sqrt(m_sample_count * 2.); + LibDSP::fft(m_fft_samples.span(), false); + 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; Vector groups; @@ -42,9 +40,9 @@ void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event) 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()); + int bins_per_group = ceil_div((samples.size() - 1) / 2, static_cast(group_count)); + for (size_t i = 1; i < samples.size() / 2; i++) { + groups[i / bins_per_group] += AK::abs(m_fft_samples[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.); @@ -67,7 +65,8 @@ void BarsVisualizationWidget::paint_event(GUI::PaintEvent& event) } BarsVisualizationWidget::BarsVisualizationWidget() - : m_is_using_last(false) + : m_fft_samples(MUST(FixedArray>::try_create(128))) + , m_is_using_last(false) , m_adjust_frequencies(true) { m_context_menu = GUI::Menu::construct(); @@ -76,54 +75,11 @@ BarsVisualizationWidget::BarsVisualizationWidget() }); frequency_energy_action->set_checked(true); m_context_menu->add_action(frequency_energy_action); -} -// 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 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 buffer) -{ - if (buffer.is_null()) - return; - if (m_last_id == buffer->id()) - return; - set_buffer(buffer, buffer->sample_count()); + MUST(set_render_sample_count(128)); } void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event) { m_context_menu->popup(event.screen_position()); } - -void BarsVisualizationWidget::set_samplerate(int samplerate) -{ - m_samplerate = samplerate; -} diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h index c0b7aa6062a..f6e903b0729 100644 --- a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h +++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h @@ -9,6 +9,7 @@ #include "VisualizationWidget.h" #include +#include #include #include @@ -17,21 +18,15 @@ class BarsVisualizationWidget final : public VisualizationWidget { public: ~BarsVisualizationWidget() override = default; - void set_buffer(RefPtr buffer) override; - void set_samplerate(int samplerate) override; private: BarsVisualizationWidget(); - void set_buffer(RefPtr buffer, int samples_to_use); - void paint_event(GUI::PaintEvent&) override; + void render(GUI::PaintEvent&, FixedArray const&) override; void context_menu_event(GUI::ContextMenuEvent& event) override; - Vector> m_sample_buffer; + FixedArray> m_fft_samples; Vector m_gfx_falling_bars; - int m_last_id; - int m_sample_count; - int m_samplerate; bool m_is_using_last; bool m_adjust_frequencies; RefPtr m_context_menu; diff --git a/Userland/Applications/SoundPlayer/SampleWidget.cpp b/Userland/Applications/SoundPlayer/SampleWidget.cpp index 47d2dfe46cc..3abf2eb8860 100644 --- a/Userland/Applications/SoundPlayer/SampleWidget.cpp +++ b/Userland/Applications/SoundPlayer/SampleWidget.cpp @@ -10,7 +10,12 @@ #include #include -void SampleWidget::paint_event(GUI::PaintEvent& event) +SampleWidget::SampleWidget() +{ + MUST(set_render_sample_count(512)); +} + +void SampleWidget::render(GUI::PaintEvent& event, FixedArray const& samples) { GUI::Frame::paint_event(event); GUI::Painter painter(*this); @@ -18,38 +23,26 @@ void SampleWidget::paint_event(GUI::PaintEvent& event) painter.add_clip_rect(event.rect()); painter.fill_rect(frame_inner_rect(), Color::Black); - float sample_max = 0; - int count = 0; int x_offset = frame_inner_rect().x(); int x = x_offset; int y_offset = frame_inner_rect().center().y(); - if (m_buffer) { - int samples_per_pixel = m_buffer->sample_count() / 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 = AK::fabs((float)m_buffer->samples()[sample_index].left); + if (samples.size() > 0) { + float samples_per_pixel = samples.size() / static_cast(frame_inner_rect().width()); + float sample_index = 0; - sample_max = max(sample, sample_max); - ++count; + while (sample_index < samples.size()) { + float sample = AK::abs(samples[sample_index]); + for (size_t i = 1; i < static_cast(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(-sample_max * frame_inner_rect().height() / 2) }; - Gfx::IntPoint max_point = { x++, y_offset + static_cast(sample_max * frame_inner_rect().height() / 2) }; - painter.draw_line(min_point, max_point, Color::Green); - - count = 0; - sample_max = 0; - } + Gfx::IntPoint min_point = { x, y_offset + static_cast(-sample * frame_inner_rect().height() / 2) }; + Gfx::IntPoint max_point = { x, y_offset + static_cast(sample * frame_inner_rect().height() / 2) }; + painter.draw_line(min_point, max_point, Color::Green); + sample_index += samples_per_pixel; + x++; } } else { painter.draw_line({ x, y_offset }, { frame_inner_rect().width(), y_offset }, Color::Green); } } - -void SampleWidget::set_buffer(RefPtr buffer) -{ - if (m_buffer == buffer) - return; - m_buffer = buffer; - update(); -} diff --git a/Userland/Applications/SoundPlayer/SampleWidget.h b/Userland/Applications/SoundPlayer/SampleWidget.h index e3da71d291b..019880b9661 100644 --- a/Userland/Applications/SoundPlayer/SampleWidget.h +++ b/Userland/Applications/SoundPlayer/SampleWidget.h @@ -10,20 +10,12 @@ #include "VisualizationWidget.h" #include -namespace Audio { -class Buffer; -} - class SampleWidget final : public VisualizationWidget { C_OBJECT(SampleWidget) public: virtual ~SampleWidget() override = default; - void set_buffer(RefPtr) override; - private: - SampleWidget() = default; - virtual void paint_event(GUI::PaintEvent&) override; - - RefPtr m_buffer; + SampleWidget(); + virtual void render(GUI::PaintEvent&, FixedArray const& samples) override; }; diff --git a/Userland/Applications/SoundPlayer/VisualizationWidget.h b/Userland/Applications/SoundPlayer/VisualizationWidget.h index da28b02a77e..e11f233ab9b 100644 --- a/Userland/Applications/SoundPlayer/VisualizationWidget.h +++ b/Userland/Applications/SoundPlayer/VisualizationWidget.h @@ -6,18 +6,94 @@ #pragma once +#include +#include #include #include +#include class VisualizationWidget : public GUI::Frame { C_OBJECT(VisualizationWidget) public: - virtual void set_buffer(RefPtr buffer) = 0; - virtual void set_samplerate(int) { } + virtual void render(GUI::PaintEvent&, FixedArray const& samples) = 0; + + void set_buffer(RefPtr 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(buffer->sample_count())) + m_sample_buffer.resize(buffer->sample_count()); + + for (size_t i = 0; i < static_cast(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::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 set_render_sample_count(size_t count) + { + auto new_buffer = TRY(FixedArray::try_create(count)); + m_render_buffer.swap(new_buffer); + return {}; + } virtual void start_new_file(StringView) { } protected: - VisualizationWidget() = default; + int m_samplerate; + int m_last_buffer_id; + size_t m_frame_count; + Vector m_sample_buffer; + FixedArray 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; };