/* * Copyright (C) 2025 RocketGod * Copyright (C) 2025 HTotoo * * This file is part of PortaPack. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, * Boston, MA 02110-1301, USA. */ #include "ui_grapheq.hpp" /* GraphEq *************************************************************/ GraphEq::GraphEq( Rect parent_rect, bool clickable) : Widget{parent_rect}, clickable_{clickable}, bar_heights(NUM_BARS, 0), prev_bar_heights(NUM_BARS, 0) { if (clickable) { set_focusable(true); // previous_data.resize(length_, 0); } } void GraphEq::set_parent_rect(const Rect new_parent_rect) { Widget::set_parent_rect(new_parent_rect); calculate_params(); } void GraphEq::calculate_params() { y_top = screen_rect().top(); RENDER_HEIGHT = parent_rect().height(); BAR_WIDTH = (parent_rect().width() - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS; HORIZONTAL_OFFSET = screen_rect().left(); } bool GraphEq::is_paused() const { return paused_; } void GraphEq::set_paused(bool paused) { paused_ = paused; needs_background_redraw = true; set_dirty(); } bool GraphEq::is_clickable() const { return clickable_; } void GraphEq::getAccessibilityText(std::string& result) { result = paused_ ? "paused GraphEq" : "GraphEq"; } void GraphEq::getWidgetName(std::string& result) { result = "GraphEq"; } bool GraphEq::on_key(const KeyEvent key) { if (!clickable_) return false; if (key == KeyEvent::Select) { set_paused(!paused_); if (on_select) { on_select(*this); } return true; } return false; } bool GraphEq::on_keyboard(const KeyboardEvent key) { if (!clickable_) return false; if (key == 32 || key == 10) { set_paused(!paused_); if (on_select) { on_select(*this); } return true; } return false; } bool GraphEq::on_touch(const TouchEvent event) { if (!clickable_) return false; switch (event.type) { case TouchEvent::Type::Start: focus(); return true; case TouchEvent::Type::End: set_paused(!paused_); if (on_select) { on_select(*this); } return true; default: return false; } } void GraphEq::set_theme(Color base_color_, Color peak_color_) { base_color = base_color_; peak_color = peak_color_; set_dirty(); } void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) { const float bin_frequency_size = 48000.0f / 128; for (int bar = 0; bar < NUM_BARS; bar++) { float start_freq = FREQUENCY_BANDS[bar]; float end_freq = FREQUENCY_BANDS[bar + 1]; int start_bin = std::max(1, (int)(start_freq / bin_frequency_size)); int end_bin = std::min(127, (int)(end_freq / bin_frequency_size)); if (start_bin >= end_bin) { end_bin = start_bin + 1; } float total_energy = 0; int bin_count = 0; for (int bin = start_bin; bin <= end_bin; bin++) { total_energy += spectrum.db[bin]; bin_count++; } float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0; // Manually boost highs for better visual balance float treble_boost = 1.0f; if (bar == 10) treble_boost = 1.7f; else if (bar >= 9) treble_boost = 1.3f; else if (bar >= 7) treble_boost = 1.3f; // Mid emphasis for a V-shape effect float mid_boost = 1.0f; if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f; float amplified_db = avg_db * treble_boost * mid_boost; if (amplified_db > 255) amplified_db = 255; float band_scale = 1.0f; int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255; if (target_height > RENDER_HEIGHT) { target_height = RENDER_HEIGHT; } // Adjusted to look nice to my eyes float rise_speed = 0.8f; float fall_speed = 1.0f; if (target_height > bar_heights[bar]) { bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed; } else { bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed; } } set_dirty(); } void GraphEq::paint(Painter& painter) { if (!visible()) return; if (!is_calculated) { // calc positions first calculate_params(); is_calculated = true; } if (needs_background_redraw) { painter.fill_rectangle(screen_rect(), Theme::getInstance()->bg_darkest->background); needs_background_redraw = false; } if (paused_) { return; } const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT; uint16_t bottom = screen_rect().bottom(); for (int bar = 0; bar < NUM_BARS; bar++) { int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING); int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT; if (prev_bar_heights[bar] > active_segments) { int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT; int clear_y = bottom - prev_bar_heights[bar] * SEGMENT_HEIGHT; painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Theme::getInstance()->bg_darkest->background); } for (int seg = 0; seg < active_segments; seg++) { int y = bottom - (seg + 1) * SEGMENT_HEIGHT; if (y < y_top) break; Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? peak_color : base_color; painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color); } prev_bar_heights[bar] = active_segments; } }