Serenity Operating System
at master 115 lines 5.3 kB view raw
1/* 2 * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "BarsVisualizationWidget.h" 9#include <AK/IntegralMath.h> 10#include <AK/Math.h> 11#include <AK/TypedTransfer.h> 12#include <LibDSP/FFT.h> 13#include <LibDSP/Window.h> 14#include <LibGUI/Event.h> 15#include <LibGUI/Menu.h> 16#include <LibGUI/Painter.h> 17#include <LibGUI/Window.h> 18 19void BarsVisualizationWidget::render(GUI::PaintEvent& event, FixedArray<float> const& samples) 20{ 21 GUI::Frame::paint_event(event); 22 GUI::Painter painter(*this); 23 24 painter.add_clip_rect(event.rect()); 25 painter.fill_rect(frame_inner_rect(), Color::Black); 26 27 // First half of data is from previous iteration, second half is from now. 28 // This gives us fully overlapping windows, which result in more accurate and visually appealing STFT. 29 for (size_t i = 0; i < fft_size / 2; i++) 30 m_fft_samples[i] = m_previous_samples[i] * m_fft_window[i]; 31 for (size_t i = 0; i < fft_size / 2; i++) 32 m_fft_samples[i + fft_size / 2] = samples[i] * m_fft_window[i + fft_size / 2]; 33 34 AK::TypedTransfer<float>::copy(m_previous_samples.data(), samples.data(), samples.size()); 35 36 DSP::fft(m_fft_samples.span(), false); 37 38 Array<float, bar_count> groups {}; 39 40 if (m_logarithmic_spectrum) { 41 auto const log_bar_size = static_cast<float>(bar_count) / AK::log2(fft_size); 42 43 for (size_t i = 0; i < bar_count; ++i) { 44 auto const bar_start = i == 0 ? 0 : static_cast<size_t>(floor(AK::pow(2.f, static_cast<float>(i) / log_bar_size))); 45 auto const bar_end = clamp(static_cast<size_t>(floor(AK::pow(2.f, static_cast<float>(i + 1) / log_bar_size))), bar_start + 1, cutoff); 46 auto const values_in_bar = bar_end - bar_start; 47 48 for (size_t sample_index = bar_start; sample_index < bar_start + values_in_bar; sample_index++) { 49 float const magnitude = m_fft_samples[sample_index].magnitude(); 50 groups[i] += magnitude; 51 } 52 groups[i] /= static_cast<float>(values_in_bar); 53 } 54 } else { 55 static constexpr size_t values_per_bar = (fft_size / 2) / bar_count; 56 for (size_t i = 0; i < fft_size / 2; i += values_per_bar) { 57 float const magnitude = m_fft_samples[i].magnitude(); 58 groups[i / values_per_bar] = magnitude; 59 for (size_t j = 0; j < values_per_bar; j++) { 60 float const magnitude = m_fft_samples[i + j].magnitude(); 61 groups[i / values_per_bar] += magnitude; 62 } 63 groups[i / values_per_bar] /= values_per_bar; 64 } 65 } 66 67 float const max_peak_value = AK::sqrt(static_cast<float>(fft_size * 2)); 68 for (size_t i = 0; i < bar_count; i++) { 69 groups[i] = AK::log(groups[i] + 1) / AK::log(max_peak_value); 70 if (m_adjust_frequencies) 71 groups[i] *= 1 + 2.0f * (static_cast<float>(i) - bar_count / 3.0f) / static_cast<float>(bar_count); 72 } 73 74 int const horizontal_margin = 30; 75 int const top_vertical_margin = 15; 76 int const pixels_inbetween_groups = frame_inner_rect().width() > 350 ? 5 : 2; 77 int const pixel_per_group_width = (frame_inner_rect().width() - horizontal_margin * 2 - pixels_inbetween_groups * (bar_count - 1)) / bar_count; 78 int const max_height = AK::max(0, frame_inner_rect().height() - top_vertical_margin); 79 int current_xpos = horizontal_margin; 80 for (size_t g = 0; g < bar_count; g++) { 81 m_gfx_falling_bars[g] = AK::min(clamp(max_height - static_cast<int>(groups[g] * static_cast<float>(max_height) * 0.8f), 0, max_height), m_gfx_falling_bars[g]); 82 painter.fill_rect(Gfx::Rect(current_xpos, max_height - static_cast<int>(groups[g] * static_cast<float>(max_height) * 0.8f), pixel_per_group_width, static_cast<int>(groups[g] * max_height * 0.8f)), Gfx::Color::from_rgb(0x95d437)); 83 painter.fill_rect(Gfx::Rect(current_xpos, m_gfx_falling_bars[g], pixel_per_group_width, 2), Gfx::Color::White); 84 current_xpos += pixel_per_group_width + pixels_inbetween_groups; 85 m_gfx_falling_bars[g] += 3; 86 } 87} 88 89BarsVisualizationWidget::BarsVisualizationWidget() 90 : m_is_using_last(false) 91 , m_adjust_frequencies(true) 92 , m_logarithmic_spectrum(true) 93{ 94 m_context_menu = GUI::Menu::construct(); 95 auto frequency_energy_action = GUI::Action::create_checkable("Adjust frequency energy (for aesthetics)", [&](GUI::Action& action) { 96 m_adjust_frequencies = action.is_checked(); 97 }); 98 frequency_energy_action->set_checked(true); 99 m_context_menu->add_action(frequency_energy_action); 100 auto logarithmic_spectrum_action = GUI::Action::create_checkable("Scale spectrum logarithmically", [&](GUI::Action& action) { 101 m_logarithmic_spectrum = action.is_checked(); 102 }); 103 logarithmic_spectrum_action->set_checked(true); 104 m_context_menu->add_action(logarithmic_spectrum_action); 105 106 m_fft_window = DSP::Window<float>::hann<fft_size>(); 107 108 // As we use full-overlapping windows, the passed-in data is only half the size of one FFT operation. 109 MUST(set_render_sample_count(fft_size / 2)); 110} 111 112void BarsVisualizationWidget::context_menu_event(GUI::ContextMenuEvent& event) 113{ 114 m_context_menu->popup(event.screen_position()); 115}