summaryrefslogtreecommitdiff
path: root/Userland/Applications/SoundPlayer/BarsVisualizationWidget.cpp
blob: 4553060de074c96e6c0560d20bebb4f244eac531 (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
/*
 * 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 <AK/TypedTransfer.h>
#include <LibDSP/FFT.h>
#include <LibDSP/Window.h>
#include <LibGUI/Event.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>

void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray<double> const& samples)
{
    GUI::Frame::paint_event(event);
    GUI::Painter painter(*this);

    painter.add_clip_rect(event.rect());
    painter.fill_rect(frame_inner_rect(), Color::Black);

    // First half of data is from previous iteration, second half is from now.
    // This gives us fully overlapping windows, which result in more accurate and visually appealing STFT.
    for (size_t i = 0; i < fft_size / 2; i++)
        m_fft_samples[i] = m_previous_samples[i] * m_fft_window[i];
    for (size_t i = 0; i < fft_size / 2; i++)
        m_fft_samples[i + fft_size / 2] = samples[i] * m_fft_window[i + fft_size / 2];

    AK::TypedTransfer<double>::copy(m_previous_samples.data(), samples.data(), samples.size());

    LibDSP::fft(m_fft_samples.span(), false);

    Array<double, bar_count> groups {};

    for (size_t i = 0; i < fft_size / 2; i += values_per_bar) {
        double const magnitude = m_fft_samples[i].magnitude();
        groups[i / values_per_bar] = magnitude;
        for (size_t j = 0; j < values_per_bar; j++) {
            double const magnitude = m_fft_samples[i + j].magnitude();
            groups[i / values_per_bar] += magnitude;
        }
        groups[i / values_per_bar] /= values_per_bar;
    }

    double const max_peak_value = AK::sqrt(static_cast<double>(fft_size * 2));
    for (size_t i = 0; i < bar_count; i++) {
        groups[i] = AK::log(groups[i] + 1) / AK::log(max_peak_value);
        if (m_adjust_frequencies)
            groups[i] *= 1 + 2.0 * (static_cast<double>(i) - static_cast<double>(bar_count / 3)) / static_cast<double>(bar_count);
    }

    int const horizontal_margin = 30;
    int const top_vertical_margin = 15;
    int const pixels_inbetween_groups = frame_inner_rect().width() > 350 ? 5 : 2;
    int const pixel_per_group_width = (frame_inner_rect().width() - horizontal_margin * 2 - pixels_inbetween_groups * (bar_count - 1)) / bar_count;
    int const max_height = frame_inner_rect().height() - top_vertical_margin;
    int current_xpos = horizontal_margin;
    for (size_t g = 0; g < bar_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;
    }
}

BarsVisualizationWidget::BarsVisualizationWidget()
    : m_is_using_last(false)
    , m_adjust_frequencies(true)
{
    m_context_menu = GUI::Menu::construct();
    auto frequency_energy_action = GUI::Action::create_checkable("Adjust frequency energy (for aesthetics)", [&](GUI::Action& action) {
        m_adjust_frequencies = action.is_checked();
    });
    frequency_energy_action->set_checked(true);
    m_context_menu->add_action(frequency_energy_action);

    m_fft_window = LibDSP::Window<double>::hann<fft_size>();

    // As we use full-overlapping windows, the passed-in data is only half the size of one FFT operation.
    MUST(set_render_sample_count(fft_size / 2));
}

void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event)
{
    m_context_menu->popup(event.screen_position());
}