summaryrefslogtreecommitdiff
path: root/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp
blob: 3e6ca3d75bbc6c8ab58fd47e0076aa78837c0f14 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/*
 * 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.span(), 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;
}