diff --git a/firmware/application/external/fmradio/main.cpp b/firmware/application/external/fmradio/main.cpp index 040fbe8c5..8f2f2c98f 100644 --- a/firmware/application/external/fmradio/main.cpp +++ b/firmware/application/external/fmradio/main.cpp @@ -38,7 +38,7 @@ __attribute__((section(".external_app.app_fmradio.application_information"), use /*.header_version = */ CURRENT_HEADER_VERSION, /*.app_version = */ VERSION_MD5, - /*.app_name = */ "FM Radio", + /*.app_name = */ "Radio", /*.bitmap_data = */ { 0x00, 0x00, diff --git a/firmware/application/external/fmradio/ui_fmradio.cpp b/firmware/application/external/fmradio/ui_fmradio.cpp index 621e00449..290b88cdf 100644 --- a/firmware/application/external/fmradio/ui_fmradio.cpp +++ b/firmware/application/external/fmradio/ui_fmradio.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 HT Otto + * Copyright (C) 2024 HTotoo * Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com * * This file is part of PortaPack. @@ -39,6 +39,28 @@ void FmRadioView::focus() { field_frequency.focus(); } +void FmRadioView::show_hide_gfx(bool show) { + gr.hidden(!show); + gr.set_paused(!show); + waveform.set_paused(show); + btn_fav_0.hidden(show); + btn_fav_1.hidden(show); + btn_fav_2.hidden(show); + btn_fav_3.hidden(show); + btn_fav_4.hidden(show); + btn_fav_5.hidden(show); + btn_fav_6.hidden(show); + btn_fav_7.hidden(show); + btn_fav_8.hidden(show); + btn_fav_9.hidden(show); + txt_save_help.hidden(show); + btn_fav_save.hidden(show); + field_bw.hidden(show); + field_modulation.hidden(show); + text_mode_label.hidden(show); + set_dirty(); +} + void FmRadioView::change_mode(int32_t mod) { field_bw.on_change = [this](size_t n, OptionsField::value_t) { (void)n; }; @@ -48,8 +70,8 @@ void FmRadioView::change_mode(int32_t mod) { audio_spectrum_update = false; // Reset spectrum update flag std::fill(audio_spectrum, audio_spectrum + 128, 0); // Clear spectrum buffer - - ReceiverModel::Mode receiver_mode = static_cast(mod); + waveform.set_dirty(); + receiver_mode = static_cast(mod); bool is_ssb = (mod == static_cast(ReceiverModel::Mode::AMAudio) && (field_modulation.selected_index() == 3 || field_modulation.selected_index() == 4)); @@ -70,6 +92,7 @@ void FmRadioView::change_mode(int32_t mod) { radio_bw = index; receiver_model.set_am_configuration(n); }; + show_hide_gfx(false); break; case static_cast(ReceiverModel::Mode::NarrowbandFMAudio): audio_sampling_rate = audio::Rate::Hz_24000; @@ -82,6 +105,7 @@ void FmRadioView::change_mode(int32_t mod) { radio_bw = index; receiver_model.set_nbfm_configuration(n); }; + show_hide_gfx(false); break; case static_cast(ReceiverModel::Mode::WidebandFMAudio): audio_sampling_rate = audio::Rate::Hz_48000; @@ -106,7 +130,6 @@ void FmRadioView::change_mode(int32_t mod) { audio::set_rate(audio_sampling_rate); audio::output::start(); receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack - receiver_model.enable(); } @@ -136,8 +159,10 @@ FmRadioView::FmRadioView(NavigationView& nav) &btn_fav_9, &audio, &waveform, - &rssi}); + &rssi, + &gr}); + txt_save_help.set_focusable(false); txt_save_help.visible(false); for (uint8_t i = 0; i < 12; ++i) { if (freq_fav_list[i].frequency == 0) { @@ -179,7 +204,20 @@ FmRadioView::FmRadioView(NavigationView& nav) } }; + waveform.on_select = [this](Waveform&) { + if (receiver_mode != ReceiverModel::Mode::WidebandFMAudio) { // only there is spectrum message + return; + } + show_hide_gfx(!btn_fav_0.hidden()); + }; + gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color); + gr.on_select = [this](GraphEq&) { + current_theme = (current_theme + 1) % themes.size(); + gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color); + gr.set_paused(false); + }; update_fav_btn_texts(); + show_hide_gfx(false); } void FmRadioView::on_btn_clicked(uint8_t i) { @@ -226,8 +264,9 @@ FmRadioView::~FmRadioView() { } void FmRadioView::on_audio_spectrum() { + if (gr.visible() && audio_spectrum_data) gr.update_audio_spectrum(*audio_spectrum_data); if (audio_spectrum_data && audio_spectrum_data->db.size() <= 128) { - for (size_t i = 0; i < audio_spectrum_data->db.size(); i++) { + for (size_t i = 0; i < audio_spectrum_data->db.size(); ++i) { audio_spectrum[i] = ((int16_t)audio_spectrum_data->db[i] - 127) * 256; } waveform.set_dirty(); diff --git a/firmware/application/external/fmradio/ui_fmradio.hpp b/firmware/application/external/fmradio/ui_fmradio.hpp index 2d3e96f5c..d7295dd89 100644 --- a/firmware/application/external/fmradio/ui_fmradio.hpp +++ b/firmware/application/external/fmradio/ui_fmradio.hpp @@ -58,13 +58,14 @@ class FmRadioView : public View { void focus() override; - std::string title() const override { return "FM radio"; }; + std::string title() const override { return "Radio"; }; private: NavigationView& nav_; RxRadioState radio_state_{}; int16_t audio_spectrum[128]{0}; bool audio_spectrum_update = false; + ReceiverModel::Mode receiver_mode = ReceiverModel::Mode::WidebandFMAudio; AudioSpectrum* audio_spectrum_data{nullptr}; struct Favorite { rf::Frequency frequency = 0; @@ -74,7 +75,7 @@ class FmRadioView : public View { Favorite freq_fav_list[12]; audio::Rate audio_sampling_rate = audio::Rate::Hz_48000; uint8_t radio_bw = 0; - + uint32_t current_theme{0}; app_settings::SettingsManager settings_{ "rx_fmradio", app_settings::Mode::RX, @@ -114,7 +115,8 @@ class FmRadioView : public View { {"favlist9_bw"sv, &freq_fav_list[9].bandwidth}, {"favlist10_bw"sv, &freq_fav_list[10].bandwidth}, {"favlist11_bw"sv, &freq_fav_list[11].bandwidth}, - {"radio_bw"sv, &radio_bw}}}; + {"radio_bw"sv, &radio_bw}, + {"theme"sv, ¤t_theme}}}; RFAmpField field_rf_amp{ {13 * 8, 0 * 16}}; @@ -157,7 +159,7 @@ class FmRadioView : public View { {21 * 8, 10, 6 * 8, 4}}; Waveform waveform{ - {0, 20, screen_width, 2 * 16}, + {0, 20, UI_POS_MAXWIDTH, 2 * 16}, audio_spectrum, 128, 0, @@ -165,6 +167,8 @@ class FmRadioView : public View { Theme::getInstance()->bg_darkest->foreground, true}; + GraphEq gr{{2, FMR_BTNGRID_TOP, UI_POS_MAXWIDTH - 4, UI_POS_MAXHEIGHT - FMR_BTNGRID_TOP}, true}; + Button btn_fav_0{{2, FMR_BTNGRID_TOP + 0 * 34, 10 * 8, 28}, "---"}; Button btn_fav_1{{2 + 15 * 8, FMR_BTNGRID_TOP + 0 * 34, 10 * 8, 28}, "---"}; Button btn_fav_2{{2, FMR_BTNGRID_TOP + 1 * 34, 10 * 8, 28}, "---"}; @@ -185,6 +189,35 @@ class FmRadioView : public View { void on_audio_spectrum(); void change_mode(int32_t mod); + void show_hide_gfx(bool show); + + struct ColorTheme { + Color base_color; + Color peak_color; + }; + + const std::array themes{ + ColorTheme{Color(255, 0, 255), Color(255, 255, 255)}, + ColorTheme{Color(0, 255, 0), Color(255, 0, 0)}, + ColorTheme{Color(0, 0, 255), Color(255, 255, 0)}, + ColorTheme{Color(255, 128, 0), Color(255, 0, 128)}, + ColorTheme{Color(128, 0, 255), Color(0, 255, 255)}, + ColorTheme{Color(255, 255, 0), Color(0, 255, 128)}, + ColorTheme{Color(255, 0, 0), Color(0, 128, 255)}, + ColorTheme{Color(0, 255, 128), Color(255, 128, 255)}, + ColorTheme{Color(128, 128, 128), Color(255, 255, 255)}, + ColorTheme{Color(255, 64, 0), Color(0, 255, 64)}, + ColorTheme{Color(0, 128, 128), Color(255, 192, 0)}, + ColorTheme{Color(0, 255, 0), Color(0, 128, 0)}, + ColorTheme{Color(32, 64, 32), Color(0, 255, 0)}, + ColorTheme{Color(64, 0, 128), Color(255, 0, 255)}, + ColorTheme{Color(0, 64, 0), Color(0, 255, 128)}, + ColorTheme{Color(255, 255, 255), Color(0, 0, 255)}, + ColorTheme{Color(128, 0, 0), Color(255, 128, 0)}, + ColorTheme{Color(0, 128, 255), Color(255, 255, 128)}, + ColorTheme{Color(64, 64, 64), Color(255, 0, 0)}, + ColorTheme{Color(255, 192, 0), Color(0, 64, 128)}}; + MessageHandlerRegistration message_handler_audio_spectrum{ Message::ID::AudioSpectrum, [this](const Message* const p) { diff --git a/firmware/application/external/gfxeq/ui_gfxeq.cpp b/firmware/application/external/gfxeq/ui_gfxeq.cpp index 07637dc34..2c39828fd 100644 --- a/firmware/application/external/gfxeq/ui_gfxeq.cpp +++ b/firmware/application/external/gfxeq/ui_gfxeq.cpp @@ -21,9 +21,9 @@ using namespace portapack; namespace ui::external_app::gfxeq { gfxEQView::gfxEQView(NavigationView& nav) - : nav_{nav}, bar_heights(NUM_BARS, 0), prev_bar_heights(NUM_BARS, 0) { + : nav_{nav} { add_children({&button_frequency, &field_rf_amp, &field_lna, &field_vga, - &button_mood, &field_volume}); + &button_mood, &field_volume, &gr}); audio::output::stop(); receiver_model.disable(); @@ -69,6 +69,7 @@ gfxEQView::gfxEQView(NavigationView& nav) }; button_mood.on_select = [this](Button&) { this->cycle_theme(); }; + gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color); } // needed to answer usb serial frequency set @@ -87,110 +88,9 @@ void gfxEQView::focus() { button_frequency.focus(); } -void gfxEQView::on_show() { - needs_background_redraw = true; -} - -void gfxEQView::on_hide() { - needs_background_redraw = true; -} - -void gfxEQView::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; - } - } -} - -void gfxEQView::render_equalizer(Painter& painter) { - const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT; - const ColorTheme& theme = themes[current_theme]; - - 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 = screen_height - prev_bar_heights[bar] * SEGMENT_HEIGHT; - painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Color(0, 0, 0)); - } - - for (int seg = 0; seg < active_segments; seg++) { - int y = screen_height - (seg + 1) * SEGMENT_HEIGHT; - if (y < header_height) break; - - Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? theme.peak_color : theme.base_color; - painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color); - } - - prev_bar_heights[bar] = active_segments; - } -} - -void gfxEQView::paint(Painter& painter) { - if (needs_background_redraw) { - painter.fill_rectangle({0, header_height, screen_width, RENDER_HEIGHT}, Color(0, 0, 0)); - needs_background_redraw = false; - } - render_equalizer(painter); -} - void gfxEQView::cycle_theme() { current_theme = (current_theme + 1) % themes.size(); + gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color); } } // namespace ui::external_app::gfxeq \ No newline at end of file diff --git a/firmware/application/external/gfxeq/ui_gfxeq.hpp b/firmware/application/external/gfxeq/ui_gfxeq.hpp index b935672cf..6cd95070a 100644 --- a/firmware/application/external/gfxeq/ui_gfxeq.hpp +++ b/firmware/application/external/gfxeq/ui_gfxeq.hpp @@ -32,45 +32,17 @@ class gfxEQView : public View { void focus() override; std::string title() const override { return "gfxEQ"; } - void on_show() override; - void on_hide() override; - void paint(Painter& painter) override; void on_freqchg(int64_t freq); private: - static constexpr ui::Dim header_height = 2 * 16; - static constexpr int RENDER_HEIGHT = 288; - static constexpr int NUM_BARS = 11; - static constexpr int BAR_SPACING = 2; - int BAR_WIDTH = (screen_width - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS; - static constexpr int HORIZONTAL_OFFSET = 2; - static constexpr int SEGMENT_HEIGHT = 10; - - static constexpr std::array FREQUENCY_BANDS = { - 375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body) - 750, // Upper bass punch (e.g., bass guitar punch, kick drum attack) - 1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body) - 2250, // Midrange clarity (e.g., vocal presence, snare crack) - 3375, // Upper midrange bite (e.g., instrument definition, vocal articulation) - 4875, // Presence and edge (e.g., guitar bite, vocal sibilance start) - 6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity) - 9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals) - 13125, // High treble sparkle (e.g., subtle overtones, synth shimmer) - 16875, // Upper treble airiness (e.g., faint harmonics, room ambiance) - 20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor) - 24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static) - }; - struct ColorTheme { Color base_color; Color peak_color; }; NavigationView& nav_; - bool needs_background_redraw{false}; - std::vector bar_heights; - std::vector prev_bar_heights; + uint32_t current_theme{0}; const std::array themes{ ColorTheme{Color(255, 0, 255), Color(255, 255, 255)}, @@ -100,6 +72,7 @@ class gfxEQView : public View { VGAGainField field_vga{{18 * 8, 0 * 16}}; Button button_mood{{21 * 8, 0, 6 * 8, 16}, "MOOD"}; AudioVolumeField field_volume{{screen_width - 2 * 8, 0 * 16}}; + GraphEq gr{{2, UI_POS_DEFAULT_HEIGHT, UI_POS_MAXWIDTH - 4, UI_POS_HEIGHT_REMAINING(2)}, false}; rf::Frequency frequency_value{93100000}; @@ -111,16 +84,13 @@ class gfxEQView : public View { {{"theme", ¤t_theme}, {"frequency", &frequency_value}}}; - void update_audio_spectrum(const AudioSpectrum& spectrum); - void render_equalizer(Painter& painter); void cycle_theme(); MessageHandlerRegistration message_handler_audio_spectrum{ Message::ID::AudioSpectrum, [this](const Message* const p) { const auto message = *reinterpret_cast(p); - this->update_audio_spectrum(*message.data); - this->set_dirty(); + this->gr.update_audio_spectrum(*message.data); }}; MessageHandlerRegistration message_handler_freqchg{ diff --git a/firmware/common/ui.hpp b/firmware/common/ui.hpp index 49e52c84e..61a3ce31f 100644 --- a/firmware/common/ui.hpp +++ b/firmware/common/ui.hpp @@ -57,6 +57,8 @@ namespace ui { #define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum)*UI_POS_DEFAULT_HEIGHT))) // remaining px from the charnum-th character to the right of the screen #define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum)*UI_POS_DEFAULT_WIDTH))) +// px width of the screen +#define UI_POS_MAXHEIGHT (screen_height) // Escape sequences for colored text; second character is index into term_colors[] #define STR_COLOR_BLACK "\x1B\x00" diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 8fbb0c883..c2b1080bf 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -2667,7 +2667,6 @@ bool Waveform::is_clickable() const { } void Waveform::getAccessibilityText(std::string& result) { - // no idea what this is in use in any places, but others have it result = paused_ ? "paused waveform" : "waveform"; } @@ -2689,7 +2688,6 @@ bool Waveform::on_key(const KeyEvent key) { } bool Waveform::on_keyboard(const KeyboardEvent key) { - // no idea what this is for, but others have it if (!clickable_) return false; if (key == 32 || key == 10) { @@ -2822,6 +2820,204 @@ void Waveform::paint(Painter& painter) { } } +/* 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; + } +} + /* VuMeter **************************************************************/ VuMeter::VuMeter( diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index dfda3edca..d9bfc6e7e 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -1015,6 +1015,66 @@ class Waveform : public Widget { bool if_ever_painted_pause{false}; // for prevent the "hidden" label keeps painting and being expensive }; +class GraphEq : public Widget { + public: + std::function on_select{}; + + GraphEq(Rect parent_rect, bool clickable = false); + GraphEq(const GraphEq&) = delete; + GraphEq(GraphEq&&) = delete; + GraphEq& operator=(const GraphEq&) = delete; + GraphEq& operator=(GraphEq&&) = delete; + + bool is_paused() const; + void set_paused(bool paused); + bool is_clickable() const; + + void paint(Painter& painter) override; + bool on_key(const KeyEvent key) override; + bool on_touch(const TouchEvent event) override; + bool on_keyboard(const KeyboardEvent event) override; + void set_parent_rect(const Rect new_parent_rect) override; + + void getAccessibilityText(std::string& result) override; + void getWidgetName(std::string& result) override; + void update_audio_spectrum(const AudioSpectrum& spectrum); + void set_theme(Color base_color, Color peak_color); + + private: + bool is_calculated{false}; + bool paused_{false}; + bool clickable_{false}; + bool needs_background_redraw{true}; // Redraw background only when needed. + Color base_color = Color(255, 0, 255); + Color peak_color = Color(255, 255, 255); + std::vector bar_heights; + std::vector prev_bar_heights; + + ui::Dim y_top = 2 * 16; + ui::Dim RENDER_HEIGHT = 288; + ui::Dim BAR_WIDTH = 20; + ui::Dim HORIZONTAL_OFFSET = 2; + static const int NUM_BARS = 11; + static const int BAR_SPACING = 2; + static const int SEGMENT_HEIGHT = 10; + static constexpr std::array FREQUENCY_BANDS = { + 375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body) + 750, // Upper bass punch (e.g., bass guitar punch, kick drum attack) + 1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body) + 2250, // Midrange clarity (e.g., vocal presence, snare crack) + 3375, // Upper midrange bite (e.g., instrument definition, vocal articulation) + 4875, // Presence and edge (e.g., guitar bite, vocal sibilance start) + 6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity) + 9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals) + 13125, // High treble sparkle (e.g., subtle overtones, synth shimmer) + 16875, // Upper treble airiness (e.g., faint harmonics, room ambiance) + 20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor) + 24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static) + }; + + void calculate_params(); // re calculate some parameters based on parent_rect() +}; + class VuMeter : public Widget { public: VuMeter(Rect parent_rect, uint32_t LEDs, bool show_max);