mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 06:22:46 +00:00 
			
		
		
		
	 d2510d0caa
			
		
	
	
		d2510d0caa
		
	
	
	
	
		
			
			Now that we have y-axis (gain) logarithmic display, we should also have x-axis (frequency) logarithmic display; that's how our ears work. This can be turned off with an option, but it generally looks much nicer.
		
			
				
	
	
		
			114 lines
		
	
	
	
		
			5.1 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			114 lines
		
	
	
	
		
			5.1 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * 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 {};
 | |
| 
 | |
|     if (m_logarithmic_spectrum) {
 | |
|         auto const log_bar_size = static_cast<double>(bar_count) / AK::log2(fft_size);
 | |
| 
 | |
|         for (size_t i = 0; i < bar_count; ++i) {
 | |
|             auto const bar_start = i == 0 ? 0 : static_cast<size_t>(floor(AK::pow(2., static_cast<double>(i) / log_bar_size)));
 | |
|             auto const bar_end = clamp(static_cast<size_t>(floor(AK::pow(2., static_cast<double>(i + 1) / log_bar_size))), bar_start + 1, cutoff);
 | |
|             auto const values_in_bar = bar_end - bar_start;
 | |
| 
 | |
|             for (size_t sample_index = bar_start; sample_index < bar_start + values_in_bar; sample_index++) {
 | |
|                 double const magnitude = m_fft_samples[sample_index].magnitude();
 | |
|                 groups[i] += magnitude;
 | |
|             }
 | |
|             groups[i] /= static_cast<double>(values_in_bar);
 | |
|         }
 | |
|     } else {
 | |
|         static constexpr size_t values_per_bar = (fft_size / 2) / bar_count;
 | |
|         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_logarithmic_spectrum(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);
 | |
|     auto logarithmic_spectrum_action = GUI::Action::create_checkable("Scale spectrum logarithmically", [&](GUI::Action& action) {
 | |
|         m_logarithmic_spectrum = action.is_checked();
 | |
|     });
 | |
|     logarithmic_spectrum_action->set_checked(true);
 | |
|     m_context_menu->add_action(logarithmic_spectrum_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());
 | |
| }
 |