diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index f944d85c2..66a9ceb3e 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -203,6 +203,10 @@ set(EXTCPPSRC #level external/level/main.cpp external/level/ui_level.cpp + + #gfxEQ + external/gfxeq/main.cpp + external/gfxeq/ui_gfxeq.cpp ) set(EXTAPPLIST @@ -255,4 +259,5 @@ set(EXTAPPLIST debug_pmem scanner level + gfxeq ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 5b83ae36a..4f03965f0 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -72,6 +72,7 @@ MEMORY ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k ram_external_app_level (rwx) : org = 0xADE10000, len = 32k + ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k } SECTIONS @@ -368,4 +369,10 @@ SECTIONS KEEP(*(.external_app.app_level.application_information)); *(*ui*external_app*level*); } > ram_external_app_level + + .external_app_gfxeq : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_gfxeq.application_information)); + *(*ui*external_app*gfxeq*); + } > ram_external_app_gfxeq } diff --git a/firmware/application/external/gfxeq/main.cpp b/firmware/application/external/gfxeq/main.cpp new file mode 100644 index 000000000..65c50028d --- /dev/null +++ b/firmware/application/external/gfxeq/main.cpp @@ -0,0 +1,36 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "ui_gfxeq.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::gfxeq { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::gfxeq + +extern "C" { +__attribute__((section(".external_app.app_gfxeq.application_information"), used)) application_information_t _application_information_gfxeq = { + (uint8_t*)0x00000000, + ui::external_app::gfxeq::initialize_app, + CURRENT_HEADER_VERSION, + VERSION_MD5, + "gfxEQ", + {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + ui::Color::green().v, + app_location_t::RX, + -1, + {'P', 'N', 'F', 'M'}, + 0x00000000, +}; + +} // namespace ui::external_app::gfxeq \ No newline at end of file diff --git a/firmware/application/external/gfxeq/ui_gfxeq.cpp b/firmware/application/external/gfxeq/ui_gfxeq.cpp new file mode 100644 index 000000000..deba295a1 --- /dev/null +++ b/firmware/application/external/gfxeq/ui_gfxeq.cpp @@ -0,0 +1,201 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "ui_gfxeq.hpp" +#include "ui.hpp" +#include "ui_freqman.hpp" +#include "tone_key.hpp" +#include "analog_audio_app.hpp" +#include "portapack.hpp" +#include "audio.hpp" +#include "baseband_api.hpp" +#include "dsp_fir_taps.hpp" + +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) { + add_children({&button_frequency, &field_rf_amp, &field_lna, &field_vga, + &button_mood, &field_volume}); + + audio::output::stop(); + receiver_model.disable(); + baseband::shutdown(); + + baseband::run_image(portapack::spi_flash::image_tag_wfm_audio); + + receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio); + receiver_model.set_wfm_configuration(1); // 200k => 0 , 180k => 1 , 40k => 2. Set to 1 or 2 for better reception + receiver_model.set_sampling_rate(3072000); + receiver_model.set_baseband_bandwidth(1750000); + + audio::set_rate(audio::Rate::Hz_48000); + audio::output::start(); + receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack + // + receiver_model.enable(); + + receiver_model.set_target_frequency(frequency_value); // Retune to actual freq + button_frequency.set_text("<" + to_string_short_freq(frequency_value) + ">"); + + button_frequency.on_select = [this, &nav](ButtonWithEncoder& button) { + auto new_view = nav_.push(frequency_value); + new_view->on_changed = [this, &button](rf::Frequency f) { + frequency_value = f; + receiver_model.set_target_frequency(f); // Retune to actual freq + button_frequency.set_text("<" + to_string_short_freq(frequency_value) + ">"); + }; + }; + + button_frequency.on_change = [this]() { + int64_t def_step = 25000; + frequency_value = frequency_value + (button_frequency.get_encoder_delta() * def_step); + if (frequency_value < 1) { + frequency_value = 1; + } + if (frequency_value > (MAX_UFREQ - def_step)) { + frequency_value = MAX_UFREQ; + } + button_frequency.set_encoder_delta(0); + receiver_model.set_target_frequency(frequency_value); // Retune to actual freq + button_frequency.set_text("<" + to_string_short_freq(frequency_value) + ">"); + }; + + button_mood.on_select = [this](Button&) { this->cycle_theme(); }; +} + +// needed to answer usb serial frequency set +void gfxEQView::on_freqchg(int64_t freq) { + receiver_model.set_target_frequency(freq); // Retune to actual freq + button_frequency.set_text("<" + to_string_short_freq(freq) + ">"); +} + +gfxEQView::~gfxEQView() { + audio::output::stop(); + receiver_model.disable(); + baseband::shutdown(); +} + +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; + + // Apply standard EQ frequency response curve (inverted V shape) + for (int bin = start_bin; bin <= end_bin; bin++) { + float weight = 1.0f; + float normalized_bin = bin / 127.0f; // 0.0 to 1.0 + + // Boosting mid frequencies per standard graphic EQ curve + if (normalized_bin >= 0.2f && normalized_bin <= 0.7f) { + // Create an inverted V shape with peak at 0.45 (middle frequencies) + float distance_from_mid = fabs(normalized_bin - 0.45f); + weight = 2.2f - (distance_from_mid * 2.0f); // Max 2.2x boost at center + } + + // Add extra low-frequency sensitivity + if (bar < 5) { + weight *= (1.8f - (bar * 0.15f)); + } + + total_energy += spectrum.db[bin] * weight; + bin_count++; + } + + uint8_t avg_db = bin_count > 0 ? (total_energy / bin_count) : 0; + + // Scale all bands to reasonable levels + float band_scale = 0.85f; + + // Get the height in display units + int target_height = (avg_db * RENDER_HEIGHT * band_scale) / 255; + + // Cap maximum height to prevent overshoot + if (target_height > RENDER_HEIGHT) { + target_height = RENDER_HEIGHT; + } + + // Apply different speeds for rise and fall + float rise_speed = 0.7f; + float fall_speed = 0.12f; + + if (target_height > bar_heights[bar]) { + // Fast rise response + bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed; + } else { + // Slow fall response + 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(); +} + +} // 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 new file mode 100644 index 000000000..df2029e29 --- /dev/null +++ b/firmware/application/external/gfxeq/ui_gfxeq.hpp @@ -0,0 +1,127 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#ifndef __UI_GFXEQ_HPP__ +#define __UI_GFXEQ_HPP__ + +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "message.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "ui_spectrum.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" + +namespace ui::external_app::gfxeq { + +class gfxEQView : public View { + public: + gfxEQView(NavigationView& nav); + ~gfxEQView(); + + gfxEQView(const gfxEQView&) = delete; + gfxEQView& operator=(const gfxEQView&) = delete; + + 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 SCREEN_WIDTH = 240; + static constexpr int SCREEN_HEIGHT = 320; + static constexpr int RENDER_HEIGHT = 288; + static constexpr int NUM_BARS = 14; + static constexpr int BAR_SPACING = 2; + static constexpr 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 = { + 20, 40, 80, 160, 320, 640, 1000, 1600, 2500, 4000, + 6000, 9000, 12000, 16000, 24000}; + + 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)}, + 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)}}; + + ButtonWithEncoder button_frequency{{0 * 8, 0 * 16 + 4, 11 * 8, 1 * 8}, ""}; + RFAmpField field_rf_amp{{13 * 8, 0 * 16}}; + LNAGainField field_lna{{15 * 8, 0 * 16}}; + VGAGainField field_vga{{18 * 8, 0 * 16}}; + Button button_mood{{21 * 8, 0, 6 * 8, 16}, "MOOD"}; + AudioVolumeField field_volume{{28 * 8, 0 * 16}}; + + rf::Frequency frequency_value{93100000}; + + RxRadioState rx_radio_state_{}; + + app_settings::SettingsManager settings_{ + "rx_gfx_eq", + app_settings::Mode::RX, + {{"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(); + }}; + + MessageHandlerRegistration message_handler_freqchg{ + Message::ID::FreqChangeCommand, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freqchg(message->freq); + }}; +}; + +} // namespace ui::external_app::gfxeq + +#endif \ No newline at end of file