GFX EQ App (#2607)

* Make the beginnings of rf3d
* Name change...
* Add mood button
* Remove forced amp settings and add persistent user settings
* Fix options bar layout and SettingsManager
* Make the background paint to black again after opening fq modal
* fix audio/mod/settings and cleaned unneeded parts
* Mapped bars to audio spectrum
* Improved frequency response... still needs work i think
* add on_freqchg to be able to answer to serial frequency change command
* Made calculations for 14 bars to fit screen and little adjustments
* Visual improvements
Co-authored-by: gullradriel
This commit is contained in:
RocketGod 2025-04-09 05:27:51 -07:00 committed by GitHub
parent 809abb6842
commit 288f6bd517
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 376 additions and 0 deletions

View File

@ -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
)

View File

@ -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
}

View File

@ -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<gfxEQView>();
}
} // 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

View File

@ -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<FrequencyKeypadView>(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

View File

@ -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<int, NUM_BARS + 1> 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<int> bar_heights;
std::vector<int> prev_bar_heights;
uint32_t current_theme{0};
const std::array<ColorTheme, 20> 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", &current_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<const AudioSpectrumMessage*>(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<const FreqChangeCommandMessage*>(p);
this->on_freqchg(message->freq);
}};
};
} // namespace ui::external_app::gfxeq
#endif