summaryrefslogtreecommitdiff
path: root/Userland/Applications
diff options
context:
space:
mode:
authorCesar Torres <shortanemoia@protonmail.com>2021-03-21 12:50:58 +0100
committerAndreas Kling <kling@serenityos.org>2021-03-27 10:20:55 +0100
commitf6f2f67c56bbdf9aa8e15e7345d199e293becd8d (patch)
tree2288f107bf8576a9965851d55f6f632aadf980b0 /Userland/Applications
parentb02f01843ae8c14647d2037c8c4bc4c975ca0162 (diff)
downloadserenity-f6f2f67c56bbdf9aa8e15e7345d199e293becd8d.zip
SoundPlayer: Add a 'Bars' audio visualization
Diffstat (limited to 'Userland/Applications')
-rw-r--r--Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp148
-rw-r--r--Userland/Applications/SoundPlayer/BarsVisualizationWidget.h55
2 files changed, 203 insertions, 0 deletions
diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp
new file mode 100644
index 0000000000..bb19713241
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BarsVisualizationWidget.h"
+#include "AudioAlgorithms.h"
+#include <AK/Complex.h>
+#include <LibGUI/Event.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Window.h>
+#include <math.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_null() || m_sample_buffer.is_empty())
+ return;
+
+ fft(m_sample_buffer, false);
+ double max = sqrt(m_sample_count * 2);
+
+ //TODO: don't hardcode this!
+ double freq_bin = 44100 / 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) * freq_bin;
+
+ for (int i = 1; i < m_sample_count / 2; i++) {
+ groups[(i * freq_bin) / bins_per_group] += fabs(m_sample_buffer.data()[i].real());
+ }
+ for (int i = 0; i < group_count; i++)
+ groups[i] /= max * freq_bin / (m_adjust_frequencies ? (clamp(pow(M_E, (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 = 5;
+ 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()
+{
+}
+
+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;
+ VERIFY(buffer->sample_count() >= 256);
+ 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] = (fabs(buffer->samples()[i].left) + 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::mousedown_event(GUI::MouseEvent& event)
+{
+ Widget::mousedown_event(event);
+ if (event.button() == GUI::Right) {
+ m_context_menu->popup(event.position());
+ }
+}
+
diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h
new file mode 100644
index 0000000000..914c2a115c
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "VisualizationBase.h"
+#include <AK/Complex.h>
+#include <LibAudio/Buffer.h>
+#include <LibGUI/Frame.h>
+
+class BarsVisualizationWidget final : public GUI::Frame
+ , public Visualization {
+ C_OBJECT(BarsVisualizationWidget)
+
+public:
+ ~BarsVisualizationWidget() override;
+ void set_buffer(RefPtr<Audio::Buffer> buffer) override;
+
+private:
+ void set_buffer(RefPtr<Audio::Buffer> buffer, int samples_to_use);
+ BarsVisualizationWidget();
+ void paint_event(GUI::PaintEvent&) override;
+ void mousedown_event(GUI::MouseEvent& event) override;
+
+ Vector<Complex<double>> m_sample_buffer;
+ Vector<int> m_gfx_falling_bars;
+ int m_last_id;
+ int m_sample_count;
+ bool m_is_using_last;
+ bool m_adjust_frequencies;
+ RefPtr<GUI::Menu> m_context_menu;
+};