mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-14 02:29:29 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea38a0fe48 | |||
| fb2e576b34 | |||
| 1d79e30d4b | |||
| be372e12bc | |||
| 00853f526a | |||
| 37ca7a601c | |||
| b50d18eafc | |||
| 1070d951e6 | |||
| a980fe21ac | |||
| 9e96715176 | |||
| fecfe8b8fc | |||
| dfdd52c667 | |||
| 695e6d19f4 | |||
| 6e9ecf8e7b | |||
| fd158e873a |
@@ -143,7 +143,7 @@ class WFMAMAptOptionsView : public View {
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
16, // Max option char length "80khz (NOAA Apt)" example.
|
||||
16, // Max option char length "80k-NOAA Apt LPF" , example.
|
||||
{
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
|
||||
@@ -105,10 +105,10 @@ void WFMConfig::apply() const {
|
||||
|
||||
void WFMAMConfig::apply() const {
|
||||
const WFMAMConfigureMessage message{
|
||||
decim_0, // Fixed 24 taps array : taps_16k0_decim_0
|
||||
decim_1, // Fixed 32 taps array : taps_84k_wfm_decim_1
|
||||
taps_64_lp_1875_2166, // Fixed channel audio filter , 64 taps array , to filter DSB AM 2k4 carrier before demod. AM .
|
||||
17000, // NOAA satellite tx , FM deviation = +-17Khz.
|
||||
decim_0, // Fixed 24 taps array : taps_16k0_decim_0
|
||||
decim_1, // Dynamic 32 taps array : taps_80k_wfmam_decim_1, 38k_wfmam
|
||||
taps_64_lp_bpf, // Dynamic 64 taps array , to filter modulated DSB AM 2k4 carrier before demod. AM .(LPF / BPF)
|
||||
17000, // NOAA satellite tx , FM deviation = +-17Khz.
|
||||
apt_audio_12k_notch_2k4_config,
|
||||
apt_audio_12k_lpf_2000hz_config};
|
||||
send_message(&message);
|
||||
|
||||
@@ -65,6 +65,7 @@ struct WFMConfig {
|
||||
struct WFMAMConfig {
|
||||
const fir_taps_real<24> decim_0; // To handle WFM filter BW=40K for NOAA APT
|
||||
const fir_taps_real<32> decim_1;
|
||||
const fir_taps_real<64> taps_64_lp_bpf; // to handle dynamically LPF / BPF .
|
||||
|
||||
void apply() const;
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+168
-54
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2024 HTotoo
|
||||
* Copyright (C) 2025 RocketGod
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -26,6 +27,7 @@
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "oversample.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
@@ -33,20 +35,118 @@ using namespace ui;
|
||||
|
||||
namespace ui::external_app::fmradio {
|
||||
|
||||
#include "external/ui_grapheq.cpi"
|
||||
|
||||
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; };
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
audio_spectrum_update = false; // Reset spectrum update flag
|
||||
std::fill(audio_spectrum, audio_spectrum + 128, 0); // Clear spectrum buffer
|
||||
waveform.set_dirty();
|
||||
receiver_mode = static_cast<ReceiverModel::Mode>(mod);
|
||||
bool is_ssb = (mod == static_cast<int32_t>(ReceiverModel::Mode::AMAudio) &&
|
||||
(field_modulation.selected_index() == 3 || field_modulation.selected_index() == 4));
|
||||
|
||||
switch (mod) {
|
||||
case static_cast<int32_t>(ReceiverModel::Mode::AMAudio):
|
||||
audio_sampling_rate = audio::Rate::Hz_24000; // Increased to 24 kHz for better AM/SSB audio
|
||||
freqman_set_bandwidth_option(0, field_bw); // AM_MODULATION
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_mode = ReceiverModel::Mode::AMAudio;
|
||||
field_bw.set_by_value(0); // DSB default
|
||||
receiver_model.set_modulation(receiver_mode);
|
||||
if (is_ssb) {
|
||||
receiver_model.set_am_configuration(field_modulation.selected_index() == 3 ? 1 : 2); // 1=USB, 2=LSB
|
||||
} else {
|
||||
receiver_model.set_am_configuration(0); // DSB
|
||||
}
|
||||
field_bw.on_change = [this](size_t index, OptionsField::value_t n) {
|
||||
radio_bw = index;
|
||||
receiver_model.set_am_configuration(n);
|
||||
};
|
||||
show_hide_gfx(false);
|
||||
break;
|
||||
case static_cast<int32_t>(ReceiverModel::Mode::NarrowbandFMAudio):
|
||||
audio_sampling_rate = audio::Rate::Hz_24000;
|
||||
freqman_set_bandwidth_option(1, field_bw); // NFM_MODULATION
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_mode = ReceiverModel::Mode::NarrowbandFMAudio;
|
||||
field_bw.set_by_value(2); // 16k default
|
||||
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t index, OptionsField::value_t n) {
|
||||
radio_bw = index;
|
||||
receiver_model.set_nbfm_configuration(n);
|
||||
};
|
||||
show_hide_gfx(false);
|
||||
break;
|
||||
case static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio):
|
||||
audio_sampling_rate = audio::Rate::Hz_48000;
|
||||
freqman_set_bandwidth_option(2, field_bw); // WFM_MODULATION
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_mode = ReceiverModel::Mode::WidebandFMAudio;
|
||||
field_bw.set_by_value(0); // 200k default
|
||||
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t index, OptionsField::value_t n) {
|
||||
radio_bw = index;
|
||||
receiver_model.set_wfm_configuration(n);
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
receiver_model.set_modulation(receiver_mode);
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
audio::set_rate(audio_sampling_rate);
|
||||
audio::output::start();
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
FmRadioView::FmRadioView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
|
||||
add_children({&rssi,
|
||||
&field_rf_amp,
|
||||
add_children({&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&field_frequency,
|
||||
&field_bw,
|
||||
&text_mode_label,
|
||||
&field_modulation,
|
||||
&btn_fav_save,
|
||||
&txt_save_help,
|
||||
&btn_fav_0,
|
||||
@@ -60,12 +160,16 @@ FmRadioView::FmRadioView(NavigationView& nav)
|
||||
&btn_fav_8,
|
||||
&btn_fav_9,
|
||||
&audio,
|
||||
&waveform});
|
||||
&waveform,
|
||||
&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] == 0) {
|
||||
freq_fav_list[i] = 87000000;
|
||||
if (freq_fav_list[i].frequency == 0) {
|
||||
freq_fav_list[i].frequency = 87000000;
|
||||
freq_fav_list[i].modulation = static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,42 +177,20 @@ FmRadioView::FmRadioView(NavigationView& nav)
|
||||
field_frequency.set_value(87000000);
|
||||
}
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
|
||||
field_frequency.set_step(25000);
|
||||
receiver_model.enable();
|
||||
audio::output::start();
|
||||
change_mode(static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio));
|
||||
field_modulation.set_by_value(static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio));
|
||||
|
||||
btn_fav_0.on_select = [this](Button&) {
|
||||
on_btn_clicked(0);
|
||||
};
|
||||
btn_fav_1.on_select = [this](Button&) {
|
||||
on_btn_clicked(1);
|
||||
};
|
||||
btn_fav_2.on_select = [this](Button&) {
|
||||
on_btn_clicked(2);
|
||||
};
|
||||
btn_fav_3.on_select = [this](Button&) {
|
||||
on_btn_clicked(3);
|
||||
};
|
||||
btn_fav_4.on_select = [this](Button&) {
|
||||
on_btn_clicked(4);
|
||||
};
|
||||
btn_fav_5.on_select = [this](Button&) {
|
||||
on_btn_clicked(5);
|
||||
};
|
||||
btn_fav_6.on_select = [this](Button&) {
|
||||
on_btn_clicked(6);
|
||||
};
|
||||
btn_fav_7.on_select = [this](Button&) {
|
||||
on_btn_clicked(7);
|
||||
};
|
||||
btn_fav_8.on_select = [this](Button&) {
|
||||
on_btn_clicked(8);
|
||||
};
|
||||
btn_fav_9.on_select = [this](Button&) {
|
||||
on_btn_clicked(9);
|
||||
};
|
||||
btn_fav_0.on_select = [this](Button&) { on_btn_clicked(0); };
|
||||
btn_fav_1.on_select = [this](Button&) { on_btn_clicked(1); };
|
||||
btn_fav_2.on_select = [this](Button&) { on_btn_clicked(2); };
|
||||
btn_fav_3.on_select = [this](Button&) { on_btn_clicked(3); };
|
||||
btn_fav_4.on_select = [this](Button&) { on_btn_clicked(4); };
|
||||
btn_fav_5.on_select = [this](Button&) { on_btn_clicked(5); };
|
||||
btn_fav_6.on_select = [this](Button&) { on_btn_clicked(6); };
|
||||
btn_fav_7.on_select = [this](Button&) { on_btn_clicked(7); };
|
||||
btn_fav_8.on_select = [this](Button&) { on_btn_clicked(8); };
|
||||
btn_fav_9.on_select = [this](Button&) { on_btn_clicked(9); };
|
||||
|
||||
btn_fav_save.on_select = [this](Button&) {
|
||||
save_fav = !save_fav;
|
||||
@@ -117,20 +199,44 @@ FmRadioView::FmRadioView(NavigationView& nav)
|
||||
txt_save_help.set_dirty();
|
||||
};
|
||||
|
||||
field_modulation.on_change = [this](size_t index, int32_t mod) {
|
||||
change_mode(mod);
|
||||
if (index == 3 || index == 4) { // USB or LSB
|
||||
receiver_model.set_am_configuration(index == 3 ? 1 : 2); // 1=USB, 2=LSB
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
if (save_fav) {
|
||||
save_fav = false;
|
||||
freq_fav_list[i] = field_frequency.value();
|
||||
freq_fav_list[i].frequency = field_frequency.value();
|
||||
freq_fav_list[i].modulation = field_modulation.selected_index_value();
|
||||
freq_fav_list[i].bandwidth = radio_bw;
|
||||
update_fav_btn_texts();
|
||||
txt_save_help.visible(save_fav);
|
||||
txt_save_help.set_text("");
|
||||
txt_save_help.set_dirty();
|
||||
return;
|
||||
}
|
||||
field_frequency.set_value(freq_fav_list[i]);
|
||||
field_frequency.set_value(freq_fav_list[i].frequency);
|
||||
field_modulation.set_by_value(freq_fav_list[i].modulation);
|
||||
change_mode(freq_fav_list[i].modulation);
|
||||
}
|
||||
|
||||
std::string FmRadioView::to_nice_freq(rf::Frequency freq) {
|
||||
@@ -141,16 +247,16 @@ std::string FmRadioView::to_nice_freq(rf::Frequency freq) {
|
||||
}
|
||||
|
||||
void FmRadioView::update_fav_btn_texts() {
|
||||
btn_fav_0.set_text(to_nice_freq(freq_fav_list[0]));
|
||||
btn_fav_1.set_text(to_nice_freq(freq_fav_list[1]));
|
||||
btn_fav_2.set_text(to_nice_freq(freq_fav_list[2]));
|
||||
btn_fav_3.set_text(to_nice_freq(freq_fav_list[3]));
|
||||
btn_fav_4.set_text(to_nice_freq(freq_fav_list[4]));
|
||||
btn_fav_5.set_text(to_nice_freq(freq_fav_list[5]));
|
||||
btn_fav_6.set_text(to_nice_freq(freq_fav_list[6]));
|
||||
btn_fav_7.set_text(to_nice_freq(freq_fav_list[7]));
|
||||
btn_fav_8.set_text(to_nice_freq(freq_fav_list[8]));
|
||||
btn_fav_9.set_text(to_nice_freq(freq_fav_list[9]));
|
||||
btn_fav_0.set_text(to_nice_freq(freq_fav_list[0].frequency));
|
||||
btn_fav_1.set_text(to_nice_freq(freq_fav_list[1].frequency));
|
||||
btn_fav_2.set_text(to_nice_freq(freq_fav_list[2].frequency));
|
||||
btn_fav_3.set_text(to_nice_freq(freq_fav_list[3].frequency));
|
||||
btn_fav_4.set_text(to_nice_freq(freq_fav_list[4].frequency));
|
||||
btn_fav_5.set_text(to_nice_freq(freq_fav_list[5].frequency));
|
||||
btn_fav_6.set_text(to_nice_freq(freq_fav_list[6].frequency));
|
||||
btn_fav_7.set_text(to_nice_freq(freq_fav_list[7].frequency));
|
||||
btn_fav_8.set_text(to_nice_freq(freq_fav_list[8].frequency));
|
||||
btn_fav_9.set_text(to_nice_freq(freq_fav_list[9].frequency));
|
||||
}
|
||||
|
||||
FmRadioView::~FmRadioView() {
|
||||
@@ -160,9 +266,17 @@ FmRadioView::~FmRadioView() {
|
||||
}
|
||||
|
||||
void FmRadioView::on_audio_spectrum() {
|
||||
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();
|
||||
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) {
|
||||
audio_spectrum[i] = ((int16_t)audio_spectrum_data->db[i] - 127) * 256;
|
||||
}
|
||||
waveform.set_dirty();
|
||||
} else {
|
||||
// Fallback: Clear waveform if no valid data
|
||||
std::fill(audio_spectrum, audio_spectrum + 128, 0);
|
||||
waveform.set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::fmradio
|
||||
|
||||
+107
-16
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2024 HTotoo
|
||||
* Copyright (C) 2025 RocketGod
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -38,11 +39,16 @@
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "freqman_db.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::fmradio {
|
||||
|
||||
#include "external/ui_grapheq.hpp"
|
||||
|
||||
#define FMR_BTNGRID_TOP 60
|
||||
|
||||
class FmRadioView : public View {
|
||||
@@ -54,31 +60,65 @@ 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};
|
||||
rf::Frequency freq_fav_list[12] = {0};
|
||||
|
||||
struct Favorite {
|
||||
rf::Frequency frequency = 0;
|
||||
int32_t modulation = static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio);
|
||||
uint8_t bandwidth = 0;
|
||||
};
|
||||
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,
|
||||
{{"favlist0"sv, &freq_fav_list[0]},
|
||||
{"favlist1"sv, &freq_fav_list[1]},
|
||||
{"favlist2"sv, &freq_fav_list[2]},
|
||||
{"favlist3"sv, &freq_fav_list[3]},
|
||||
{"favlist4"sv, &freq_fav_list[4]},
|
||||
{"favlist5"sv, &freq_fav_list[5]},
|
||||
{"favlist6"sv, &freq_fav_list[6]},
|
||||
{"favlist7"sv, &freq_fav_list[7]},
|
||||
{"favlist8"sv, &freq_fav_list[8]},
|
||||
{"favlist9"sv, &freq_fav_list[9]},
|
||||
{"favlist10"sv, &freq_fav_list[10]},
|
||||
{"favlist11"sv, &freq_fav_list[11]}}};
|
||||
{{"favlist0_freq"sv, &freq_fav_list[0].frequency},
|
||||
{"favlist1_freq"sv, &freq_fav_list[1].frequency},
|
||||
{"favlist2_freq"sv, &freq_fav_list[2].frequency},
|
||||
{"favlist3_freq"sv, &freq_fav_list[3].frequency},
|
||||
{"favlist4_freq"sv, &freq_fav_list[4].frequency},
|
||||
{"favlist5_freq"sv, &freq_fav_list[5].frequency},
|
||||
{"favlist6_freq"sv, &freq_fav_list[6].frequency},
|
||||
{"favlist7_freq"sv, &freq_fav_list[7].frequency},
|
||||
{"favlist8_freq"sv, &freq_fav_list[8].frequency},
|
||||
{"favlist9_freq"sv, &freq_fav_list[9].frequency},
|
||||
{"favlist10_freq"sv, &freq_fav_list[10].frequency},
|
||||
{"favlist11_freq"sv, &freq_fav_list[11].frequency},
|
||||
{"favlist0_mod"sv, &freq_fav_list[0].modulation},
|
||||
{"favlist1_mod"sv, &freq_fav_list[1].modulation},
|
||||
{"favlist2_mod"sv, &freq_fav_list[2].modulation},
|
||||
{"favlist3_mod"sv, &freq_fav_list[3].modulation},
|
||||
{"favlist4_mod"sv, &freq_fav_list[4].modulation},
|
||||
{"favlist5_mod"sv, &freq_fav_list[5].modulation},
|
||||
{"favlist6_mod"sv, &freq_fav_list[6].modulation},
|
||||
{"favlist7_mod"sv, &freq_fav_list[7].modulation},
|
||||
{"favlist8_mod"sv, &freq_fav_list[8].modulation},
|
||||
{"favlist9_mod"sv, &freq_fav_list[9].modulation},
|
||||
{"favlist10_mod"sv, &freq_fav_list[10].modulation},
|
||||
{"favlist11_mod"sv, &freq_fav_list[11].modulation},
|
||||
{"favlist0_bw"sv, &freq_fav_list[0].bandwidth},
|
||||
{"favlist1_bw"sv, &freq_fav_list[1].bandwidth},
|
||||
{"favlist2_bw"sv, &freq_fav_list[2].bandwidth},
|
||||
{"favlist3_bw"sv, &freq_fav_list[3].bandwidth},
|
||||
{"favlist4_bw"sv, &freq_fav_list[4].bandwidth},
|
||||
{"favlist5_bw"sv, &freq_fav_list[5].bandwidth},
|
||||
{"favlist6_bw"sv, &freq_fav_list[6].bandwidth},
|
||||
{"favlist7_bw"sv, &freq_fav_list[7].bandwidth},
|
||||
{"favlist8_bw"sv, &freq_fav_list[8].bandwidth},
|
||||
{"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},
|
||||
{"theme"sv, ¤t_theme}}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
@@ -95,6 +135,24 @@ class FmRadioView : public View {
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
OptionsField field_bw{
|
||||
{10 * 8, FMR_BTNGRID_TOP + 6 * 34},
|
||||
6,
|
||||
{}};
|
||||
|
||||
Text text_mode_label{
|
||||
{20 * 8, FMR_BTNGRID_TOP + 6 * 34, 5 * 8, 1 * 28},
|
||||
"MODE:"};
|
||||
|
||||
OptionsField field_modulation{
|
||||
{26 * 8, FMR_BTNGRID_TOP + 6 * 34},
|
||||
4,
|
||||
{{"AM", static_cast<int32_t>(ReceiverModel::Mode::AMAudio)},
|
||||
{"NFM", static_cast<int32_t>(ReceiverModel::Mode::NarrowbandFMAudio)},
|
||||
{"WFM", static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio)},
|
||||
{"USB", static_cast<int32_t>(ReceiverModel::Mode::AMAudio)},
|
||||
{"LSB", static_cast<int32_t>(ReceiverModel::Mode::AMAudio)}}};
|
||||
|
||||
TextField txt_save_help{
|
||||
{2, FMR_BTNGRID_TOP + 6 * 34 - 20, 12 * 8, 16},
|
||||
" "};
|
||||
@@ -103,7 +161,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,
|
||||
@@ -111,6 +169,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}, "---"};
|
||||
@@ -124,10 +184,41 @@ class FmRadioView : public View {
|
||||
|
||||
Button btn_fav_save{{2, FMR_BTNGRID_TOP + 6 * 34, 7 * 8, 1 * 28}, "Save"};
|
||||
bool save_fav = false;
|
||||
|
||||
void on_btn_clicked(uint8_t i);
|
||||
void update_fav_btn_texts();
|
||||
std::string to_nice_freq(rf::Frequency freq);
|
||||
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<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)}};
|
||||
|
||||
MessageHandlerRegistration message_handler_audio_spectrum{
|
||||
Message::ID::AudioSpectrum,
|
||||
|
||||
+6
-104
@@ -20,10 +20,12 @@ using namespace portapack;
|
||||
|
||||
namespace ui::external_app::gfxeq {
|
||||
|
||||
#include "external/ui_grapheq.cpi"
|
||||
|
||||
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 +71,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 +90,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
|
||||
+5
-33
@@ -22,6 +22,8 @@
|
||||
|
||||
namespace ui::external_app::gfxeq {
|
||||
|
||||
#include "external/ui_grapheq.hpp"
|
||||
|
||||
class gfxEQView : public View {
|
||||
public:
|
||||
gfxEQView(NavigationView& nav);
|
||||
@@ -32,45 +34,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<int, NUM_BARS + 1> 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<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)},
|
||||
@@ -100,6 +74,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 +86,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<const AudioSpectrumMessage*>(p);
|
||||
this->update_audio_spectrum(*message.data);
|
||||
this->set_dirty();
|
||||
this->gr.update_audio_spectrum(*message.data);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
|
||||
+20
-5
@@ -78,7 +78,7 @@ void HopperView::set_hopper_channel(uint32_t i, uint32_t width, uint64_t center,
|
||||
hopper_channels[i].enabled = true;
|
||||
hopper_channels[i].width = (width * 0xFFFFFFULL) / 1536000;
|
||||
hopper_channels[i].center = center;
|
||||
hopper_channels[i].duration = 30720 * duration;
|
||||
hopper_channels[i].duration = duration ? 3072 * duration : 10;
|
||||
}
|
||||
|
||||
void HopperView::start_tx() {
|
||||
@@ -274,7 +274,7 @@ HopperView::HopperView(
|
||||
|
||||
options_type.set_selected_index(3); // Rand CW
|
||||
options_speed.set_selected_index(3); // 10kHz
|
||||
options_hop.set_selected_index(1); // 50ms
|
||||
options_hop.set_selected_index(3); // 50ms
|
||||
button_transmit.set_style(&style_val);
|
||||
|
||||
field_timetx.set_value(30);
|
||||
@@ -327,10 +327,25 @@ HopperView::HopperView(
|
||||
};
|
||||
|
||||
button_transmit.on_select = [this](Button&) {
|
||||
if (jamming || cooling)
|
||||
if (jamming || cooling) {
|
||||
stop_tx();
|
||||
else
|
||||
start_tx();
|
||||
} else {
|
||||
// if hop speed is 0, alert the user that this will cause a freeze on UI
|
||||
if (options_hop.selected_index_value() == 0) {
|
||||
nav_.display_modal(
|
||||
"Warning", "Hopping set to 0ms (fastest).\n\nTHIS WILL FREEZE THE HACKRF,\npress RESET button to stop\n\nAre you sure?", YESNO, [this](bool choice) {
|
||||
if (choice) {
|
||||
// Wait for UI update before the freeze
|
||||
chThdSleepMilliseconds(50);
|
||||
start_tx();
|
||||
}
|
||||
},
|
||||
TRUE);
|
||||
} else {
|
||||
// if hop speed is not 0, just start the transmission
|
||||
start_tx();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
menu_freq_list.on_left = [this]() {
|
||||
|
||||
+17
-8
@@ -143,6 +143,13 @@ class HopperView : public View {
|
||||
{"FM tone", 1},
|
||||
{"CW sweep", 2},
|
||||
{"Rand CW", 3},
|
||||
{"Sine", 4},
|
||||
{"Square", 5},
|
||||
{"Sawtooth", 6},
|
||||
{"Triangle", 7},
|
||||
{"Chirp", 8},
|
||||
{"Gauss", 9},
|
||||
{"Brute", 10},
|
||||
}};
|
||||
|
||||
Text text_range_number{
|
||||
@@ -163,14 +170,16 @@ class HopperView : public View {
|
||||
|
||||
OptionsField options_hop{
|
||||
{7 * 8, 27 * 8},
|
||||
5,
|
||||
{{"10ms ", 1},
|
||||
{"50ms ", 5},
|
||||
{"100ms", 10},
|
||||
{"1s ", 100},
|
||||
{"2s ", 200},
|
||||
{"5s ", 500},
|
||||
{"10s ", 1000}}};
|
||||
6,
|
||||
{{"0ms !!", 0},
|
||||
{"1ms ", 1},
|
||||
{"10ms ", 10},
|
||||
{"50ms ", 50},
|
||||
{"100ms", 100},
|
||||
{"1s ", 1000},
|
||||
{"2s ", 2000},
|
||||
{"5s ", 5000},
|
||||
{"10s ", 10000}}};
|
||||
|
||||
NumberField field_timetx{
|
||||
{7 * 8, 29 * 8},
|
||||
|
||||
+129
-48
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -36,7 +37,6 @@ void RangeView::focus() {
|
||||
}
|
||||
|
||||
void RangeView::update_start(rf::Frequency f) {
|
||||
// Change everything except max
|
||||
frequency_range.min = f;
|
||||
button_start.set_text(to_string_short_freq(f));
|
||||
|
||||
@@ -48,7 +48,6 @@ void RangeView::update_start(rf::Frequency f) {
|
||||
}
|
||||
|
||||
void RangeView::update_stop(rf::Frequency f) {
|
||||
// Change everything except min
|
||||
frequency_range.max = f;
|
||||
button_stop.set_text(to_string_short_freq(f));
|
||||
|
||||
@@ -60,7 +59,6 @@ void RangeView::update_stop(rf::Frequency f) {
|
||||
}
|
||||
|
||||
void RangeView::update_center(rf::Frequency f) {
|
||||
// Change min/max/center, keep width
|
||||
center = f;
|
||||
button_center.set_text(to_string_short_freq(center));
|
||||
|
||||
@@ -75,7 +73,6 @@ void RangeView::update_center(rf::Frequency f) {
|
||||
}
|
||||
|
||||
void RangeView::update_width(uint32_t w) {
|
||||
// Change min/max/width, keep center
|
||||
width = w;
|
||||
|
||||
button_width.set_text(to_string_short_freq(width));
|
||||
@@ -91,7 +88,6 @@ void RangeView::update_width(uint32_t w) {
|
||||
}
|
||||
|
||||
void RangeView::paint(Painter&) {
|
||||
// Draw lines and arrows
|
||||
Rect r;
|
||||
Point p;
|
||||
Coord c;
|
||||
@@ -162,7 +158,7 @@ RangeView::RangeView(NavigationView& nav) {
|
||||
auto load_view = nav.push<FrequencyLoadView>();
|
||||
load_view->on_frequency_loaded = [this](rf::Frequency value) {
|
||||
update_center(value);
|
||||
update_width(100000); // 100kHz default jamming bandwidth when loading unique frequency
|
||||
update_width(100000);
|
||||
};
|
||||
load_view->on_range_loaded = [this](rf::Frequency start, rf::Frequency stop) {
|
||||
update_start(start);
|
||||
@@ -193,10 +189,37 @@ void JammerView::set_jammer_channel(uint32_t i, uint32_t width, uint64_t center,
|
||||
jammer_channels[i].enabled = true;
|
||||
jammer_channels[i].width = (width * 0xFFFFFFULL) / 1536000;
|
||||
jammer_channels[i].center = center;
|
||||
jammer_channels[i].duration = 30720 * duration;
|
||||
jammer_channels[i].duration = duration ? 30720 * duration : 3000;
|
||||
}
|
||||
|
||||
void JammerView::start_tx() {
|
||||
if (update_config()) {
|
||||
jamming = true;
|
||||
button_transmit.set_style(&style_cancel);
|
||||
button_transmit.set_text("STOP");
|
||||
|
||||
transmitter_model.set_rf_amp(field_amp.value());
|
||||
transmitter_model.set_tx_gain(field_gain.value());
|
||||
transmitter_model.set_baseband_bandwidth(28'000'000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_jammer(true, (JammerType)options_type.selected_index(), options_speed.selected_index_value());
|
||||
mscounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void JammerView::stop_tx() {
|
||||
button_transmit.set_style(&style_val);
|
||||
button_transmit.set_text("START");
|
||||
transmitter_model.disable();
|
||||
baseband::set_jammer(false, JammerType::TYPE_FSK, 0);
|
||||
jamming = false;
|
||||
cooling = false;
|
||||
seconds = 0;
|
||||
mscounter = 0;
|
||||
}
|
||||
|
||||
bool JammerView::update_config() {
|
||||
uint32_t c, i = 0;
|
||||
size_t num_channels;
|
||||
rf::Frequency start_freq, range_bw, range_bw_sub, ch_width;
|
||||
@@ -204,24 +227,19 @@ void JammerView::start_tx() {
|
||||
|
||||
size_t hop_value = options_hop.selected_index_value();
|
||||
|
||||
// Disable all channels by default
|
||||
for (c = 0; c < JAMMER_MAX_CH; c++)
|
||||
jammer_channels[c].enabled = false;
|
||||
|
||||
// Generate jamming channels with JAMMER_MAX_CH maximum width
|
||||
// Convert ranges min/max to center/bw
|
||||
for (size_t r = 0; r < 3; r++) {
|
||||
if (range_views[r]->frequency_range.enabled) {
|
||||
range_bw = abs(range_views[r]->frequency_range.max - range_views[r]->frequency_range.min);
|
||||
|
||||
// Get lower bound
|
||||
if (range_views[r]->frequency_range.min < range_views[r]->frequency_range.max)
|
||||
start_freq = range_views[r]->frequency_range.min;
|
||||
else
|
||||
start_freq = range_views[r]->frequency_range.max;
|
||||
|
||||
if (range_bw >= JAMMER_CH_WIDTH) {
|
||||
// Split range in multiple channels
|
||||
num_channels = 0;
|
||||
range_bw_sub = range_bw;
|
||||
|
||||
@@ -241,7 +259,6 @@ void JammerView::start_tx() {
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// Range fits in a single channel
|
||||
if (i >= JAMMER_MAX_CH) {
|
||||
out_of_ranges = true;
|
||||
} else {
|
||||
@@ -254,43 +271,24 @@ void JammerView::start_tx() {
|
||||
|
||||
if (!out_of_ranges && i) {
|
||||
text_range_total.set("/" + to_string_dec_uint(i, 2));
|
||||
|
||||
jamming = true;
|
||||
button_transmit.set_style(&style_cancel);
|
||||
button_transmit.set_text("STOP");
|
||||
|
||||
transmitter_model.set_rf_amp(field_amp.value());
|
||||
transmitter_model.set_tx_gain(field_gain.value());
|
||||
transmitter_model.set_baseband_bandwidth(28'000'000); // Although tx is narrowband , let's use Max TX LPF .
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_jammer(true, (JammerType)options_type.selected_index(), options_speed.selected_index_value());
|
||||
mscounter = 0; // euquiq: Reset internal ms counter for do_timer()
|
||||
return true;
|
||||
} else {
|
||||
if (out_of_ranges)
|
||||
nav_.display_modal("Error", "Jamming bandwidth too large.\nMust be less than 24MHz.");
|
||||
else
|
||||
nav_.display_modal("Error", "No range enabled.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void JammerView::stop_tx() {
|
||||
button_transmit.set_style(&style_val);
|
||||
button_transmit.set_text("START");
|
||||
transmitter_model.disable();
|
||||
baseband::set_jammer(false, JammerType::TYPE_FSK, 0);
|
||||
jamming = false;
|
||||
cooling = false;
|
||||
}
|
||||
|
||||
// called each 1/60th of second
|
||||
void JammerView::on_timer() {
|
||||
if (++mscounter == 60) {
|
||||
if (++mscounter >= 60) {
|
||||
mscounter = 0;
|
||||
if (jamming) {
|
||||
int32_t timepause = field_timepause.value();
|
||||
if (cooling) {
|
||||
if (++seconds >= field_timepause.value()) { // Re-start TX
|
||||
transmitter_model.set_baseband_bandwidth(28'000'000); // Although tx is narrowband , let's use Max TX LPF .
|
||||
if (timepause == 0 || ++seconds >= timepause) {
|
||||
transmitter_model.set_baseband_bandwidth(28'000'000);
|
||||
transmitter_model.enable();
|
||||
button_transmit.set_text("STOP");
|
||||
baseband::set_jammer(true, (JammerType)options_type.selected_index(), options_speed.selected_index_value());
|
||||
@@ -306,8 +304,7 @@ void JammerView::on_timer() {
|
||||
seconds = 0;
|
||||
}
|
||||
} else {
|
||||
if (++seconds >= field_timetx.value()) // Start cooling period:
|
||||
{
|
||||
if (timepause && ++seconds >= field_timetx.value()) {
|
||||
transmitter_model.disable();
|
||||
button_transmit.set_text("PAUSED");
|
||||
baseband::set_jammer(false, JammerType::TYPE_FSK, 0);
|
||||
@@ -327,11 +324,9 @@ void JammerView::on_timer() {
|
||||
}
|
||||
}
|
||||
|
||||
JammerView::JammerView(
|
||||
NavigationView& nav)
|
||||
JammerView::JammerView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
Rect view_rect = {0, 3 * 8, screen_width, 80};
|
||||
// baseband::run_image(portapack::spi_flash::image_tag_jammer);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&tab_view,
|
||||
@@ -355,15 +350,101 @@ JammerView::JammerView(
|
||||
view_range_b.set_parent_rect(view_rect);
|
||||
view_range_c.set_parent_rect(view_rect);
|
||||
|
||||
options_type.set_selected_index(3); // Rand CW
|
||||
options_speed.set_selected_index(3); // 10kHz
|
||||
options_hop.set_selected_index(1); // 50ms
|
||||
button_transmit.set_style(&style_val);
|
||||
view_range_a.check_enabled.set_value(true);
|
||||
view_range_a.frequency_range.enabled = true;
|
||||
view_range_a.update_center(315'000'000);
|
||||
view_range_a.update_width(1'000'000);
|
||||
|
||||
options_type.set_selected_index(3);
|
||||
options_speed.set_selected_index(3);
|
||||
options_hop.set_selected_index(0);
|
||||
field_timetx.set_value(30);
|
||||
field_timepause.set_value(1);
|
||||
field_timepause.set_value(0);
|
||||
field_jitter.set_value(0);
|
||||
field_gain.set_value(transmitter_model.tx_gain());
|
||||
field_amp.set_value(transmitter_model.rf_amp());
|
||||
button_transmit.set_style(&style_val);
|
||||
|
||||
options_type.on_change = [this](size_t, OptionsField::value_t) {
|
||||
if (jamming) update_config();
|
||||
if (jamming && !cooling) baseband::set_jammer(true, (JammerType)options_type.selected_index(), options_speed.selected_index_value());
|
||||
};
|
||||
|
||||
options_speed.on_change = [this](size_t, OptionsField::value_t) {
|
||||
if (jamming) update_config();
|
||||
if (jamming && !cooling) baseband::set_jammer(true, (JammerType)options_type.selected_index(), options_speed.selected_index_value());
|
||||
};
|
||||
|
||||
options_hop.on_change = [this](size_t, OptionsField::value_t) {
|
||||
if (jamming) update_config();
|
||||
};
|
||||
|
||||
field_timetx.on_change = [this](int32_t) {
|
||||
if (jamming) update_config();
|
||||
};
|
||||
|
||||
field_timepause.on_change = [this](int32_t) {
|
||||
if (jamming) update_config();
|
||||
};
|
||||
|
||||
field_jitter.on_change = [this](int32_t) {
|
||||
if (jamming) update_config();
|
||||
};
|
||||
|
||||
field_gain.on_change = [this](int32_t v) {
|
||||
if (jamming) transmitter_model.set_tx_gain(v);
|
||||
};
|
||||
|
||||
field_amp.on_change = [this](int32_t v) {
|
||||
if (jamming) transmitter_model.set_rf_amp(v);
|
||||
};
|
||||
|
||||
for (auto range_view : range_views) {
|
||||
range_view->check_enabled.on_select = [this](Checkbox&, bool) {
|
||||
if (jamming) update_config();
|
||||
};
|
||||
range_view->button_start.on_select = [this, range_view](Button&) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(range_view->frequency_range.min);
|
||||
new_view->on_changed = [this, range_view](rf::Frequency f) {
|
||||
range_view->update_start(f);
|
||||
if (jamming) update_config();
|
||||
};
|
||||
};
|
||||
range_view->button_stop.on_select = [this, range_view](Button&) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(range_view->frequency_range.max);
|
||||
new_view->on_changed = [this, range_view](rf::Frequency f) {
|
||||
range_view->update_stop(f);
|
||||
if (jamming) update_config();
|
||||
};
|
||||
};
|
||||
range_view->button_center.on_select = [this, range_view](Button&) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(range_view->center);
|
||||
new_view->on_changed = [this, range_view](rf::Frequency f) {
|
||||
range_view->update_center(f);
|
||||
if (jamming) update_config();
|
||||
};
|
||||
};
|
||||
range_view->button_width.on_select = [this, range_view](Button&) {
|
||||
auto new_view = nav_.push<FrequencyKeypadView>(range_view->width);
|
||||
new_view->on_changed = [this, range_view](rf::Frequency f) {
|
||||
range_view->update_width(f);
|
||||
if (jamming) update_config();
|
||||
};
|
||||
};
|
||||
range_view->button_load_range.on_select = [this, range_view](Button&) {
|
||||
auto load_view = nav_.push<FrequencyLoadView>();
|
||||
load_view->on_frequency_loaded = [this, range_view](rf::Frequency value) {
|
||||
range_view->update_center(value);
|
||||
range_view->update_width(100000);
|
||||
if (jamming) update_config();
|
||||
};
|
||||
load_view->on_range_loaded = [this, range_view](rf::Frequency start, rf::Frequency stop) {
|
||||
range_view->update_start(start);
|
||||
range_view->update_stop(stop);
|
||||
if (jamming) update_config();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
button_transmit.on_select = [this](Button&) {
|
||||
if (jamming || cooling)
|
||||
|
||||
+29
-25
@@ -41,27 +41,12 @@ class RangeView : public View {
|
||||
RangeView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
jammer_range_t frequency_range{false, 0, 0};
|
||||
|
||||
private:
|
||||
void update_start(rf::Frequency f);
|
||||
void update_stop(rf::Frequency f);
|
||||
void update_center(rf::Frequency f);
|
||||
void update_width(uint32_t w);
|
||||
|
||||
uint32_t width{};
|
||||
rf::Frequency center{};
|
||||
|
||||
const Style& style_info = *Theme::getInstance()->fg_medium;
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 8 * 8 + 4}, LanguageHelper::currentMessages[LANG_START], Theme::getInstance()->fg_light->foreground},
|
||||
{{23 * 8, 8 * 8 + 4}, LanguageHelper::currentMessages[LANG_STOP], Theme::getInstance()->fg_light->foreground},
|
||||
{{12 * 8, 5 * 8 - 4}, "Center", Theme::getInstance()->fg_light->foreground},
|
||||
{{12 * 8 + 4, 13 * 8}, "Width", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Checkbox check_enabled{
|
||||
{1 * 8, 4},
|
||||
12,
|
||||
@@ -74,15 +59,32 @@ class RangeView : public View {
|
||||
Button button_start{
|
||||
{0 * 8, 11 * 8, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
Button button_stop{
|
||||
{19 * 8, 11 * 8, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
Button button_center{
|
||||
{76, 4 * 15 - 4, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
Button button_width{
|
||||
{76, 8 * 15, 11 * 8, 28},
|
||||
""};
|
||||
|
||||
void update_start(rf::Frequency f);
|
||||
void update_stop(rf::Frequency f);
|
||||
void update_center(rf::Frequency f);
|
||||
void update_width(uint32_t w);
|
||||
|
||||
private:
|
||||
const Style& style_info = *Theme::getInstance()->fg_medium;
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 8 * 8 + 4}, LanguageHelper::currentMessages[LANG_START], Theme::getInstance()->fg_light->foreground},
|
||||
{{23 * 8, 8 * 8 + 4}, LanguageHelper::currentMessages[LANG_STOP], Theme::getInstance()->fg_light->foreground},
|
||||
{{12 * 8, 5 * 8 - 4}, "Center", Theme::getInstance()->fg_light->foreground},
|
||||
{{12 * 8 + 4, 13 * 8}, "Width", Theme::getInstance()->fg_light->foreground}};
|
||||
};
|
||||
|
||||
class JammerView : public View {
|
||||
@@ -110,15 +112,16 @@ class JammerView : public View {
|
||||
void start_tx();
|
||||
void on_timer();
|
||||
void stop_tx();
|
||||
bool update_config();
|
||||
void set_jammer_channel(uint32_t i, uint32_t width, uint64_t center, uint32_t duration);
|
||||
void on_retune(const rf::Frequency freq, const uint32_t range);
|
||||
|
||||
JammerChannel* jammer_channels = (JammerChannel*)shared_memory.bb_data.data;
|
||||
bool jamming{false};
|
||||
bool cooling{false}; // euquiq: Indicates jammer in cooldown
|
||||
uint16_t seconds = 0; // euquiq: seconds counter for toggling tx / cooldown
|
||||
int16_t mscounter = 0; // euquiq: Internal ms counter for do_timer()
|
||||
lfsr_word_t lfsr_v = 1; // euquiq: Used to generate "random" Jitter
|
||||
bool cooling{false};
|
||||
uint16_t seconds{0};
|
||||
int16_t mscounter{0};
|
||||
lfsr_word_t lfsr_v{1};
|
||||
|
||||
const Style& style_val = *Theme::getInstance()->fg_green;
|
||||
const Style& style_cancel = *Theme::getInstance()->fg_red;
|
||||
@@ -140,8 +143,8 @@ class JammerView : public View {
|
||||
{{1 * 8, 25 * 8}, "Speed:", Theme::getInstance()->fg_light->foreground},
|
||||
{{3 * 8, 27 * 8}, "Hop:", Theme::getInstance()->fg_light->foreground},
|
||||
{{4 * 8, 29 * 8}, "TX:", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 31 * 8}, "Sle3p:", Theme::getInstance()->fg_light->foreground}, // euquiq: Token of appreciation to TheSle3p, which made this ehnancement a reality with his bounty.
|
||||
{{0 * 8, 33 * 8}, "Jitter:", Theme::getInstance()->fg_light->foreground}, // Maybe the repository curator can keep the "mystype" for some versions.
|
||||
{{1 * 8, 31 * 8}, "Sleep:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 33 * 8}, "Jitter:", Theme::getInstance()->fg_light->foreground},
|
||||
{{11 * 8, 29 * 8}, "Secs.", Theme::getInstance()->fg_light->foreground},
|
||||
{{11 * 8, 31 * 8}, "Secs.", Theme::getInstance()->fg_light->foreground},
|
||||
{{11 * 8, 33 * 8}, "/60", Theme::getInstance()->fg_light->foreground},
|
||||
@@ -182,7 +185,8 @@ class JammerView : public View {
|
||||
OptionsField options_hop{
|
||||
{7 * 8, 27 * 8},
|
||||
5,
|
||||
{{"10ms ", 1},
|
||||
{{"Off ", 0},
|
||||
{"10ms ", 1},
|
||||
{"50ms ", 5},
|
||||
{"100ms", 10},
|
||||
{"1s ", 100},
|
||||
@@ -201,7 +205,7 @@ class JammerView : public View {
|
||||
NumberField field_timepause{
|
||||
{8 * 8, 31 * 8},
|
||||
2,
|
||||
{1, 60},
|
||||
{0, 60},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
@@ -209,7 +213,7 @@ class JammerView : public View {
|
||||
NumberField field_jitter{
|
||||
{8 * 8, 33 * 8},
|
||||
2,
|
||||
{1, 60},
|
||||
{0, 60},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
@@ -98,6 +98,31 @@ static msg_t loopthread_fn(void* arg) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MorseView::on_set_tone(NavigationView& nav) {
|
||||
tone_input_buffer = to_string_dec_uint(tone);
|
||||
|
||||
text_prompt(nav, tone_input_buffer, 4, ENTER_KEYBOARD_MODE_DIGITS, [this](std::string& buffer) {
|
||||
bool is_digit_only = true;
|
||||
for (size_t i = 0; i < tone_input_buffer.size(); ++i) {
|
||||
if (tone_input_buffer[i] < '0' || tone_input_buffer[i] > '9') {
|
||||
is_digit_only = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!buffer.empty() && is_digit_only) {
|
||||
int new_tone = atoi(buffer.c_str());
|
||||
if (new_tone >= 100 && new_tone <= 9999) {
|
||||
tone = new_tone;
|
||||
field_tone.set_value(tone);
|
||||
update_tx_duration();
|
||||
} else {
|
||||
nav_.display_modal("Out of range", "Tone must be between 100 and 9999 Hz");
|
||||
}
|
||||
} else {
|
||||
nav_.display_modal("Invalid input", "Please enter digits only");
|
||||
}
|
||||
});
|
||||
}
|
||||
void MorseView::on_set_text(NavigationView& nav) {
|
||||
text_prompt(nav, buffer, 28, ENTER_KEYBOARD_MODE_ALPHA);
|
||||
}
|
||||
@@ -250,6 +275,10 @@ MorseView::MorseView(
|
||||
tone = value;
|
||||
};
|
||||
|
||||
field_tone.on_select = [this, &nav](NumberField&) {
|
||||
this->on_set_tone(nav);
|
||||
};
|
||||
|
||||
button_message.on_select = [this, &nav](Button&) {
|
||||
this->on_set_text(nav);
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ class MorseView : public View {
|
||||
NavigationView& nav_;
|
||||
std::string message{};
|
||||
uint32_t time_units{0};
|
||||
std::string tone_input_buffer{}; // Holds the tone value while the text prompt is open
|
||||
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
@@ -100,6 +101,7 @@ class MorseView : public View {
|
||||
|
||||
bool start_tx();
|
||||
void update_tx_duration();
|
||||
void on_set_tone(NavigationView& nav);
|
||||
void on_set_text(NavigationView& nav);
|
||||
void set_foxhunt(size_t i);
|
||||
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ __attribute__((section(".external_app.app_random_password.application_informatio
|
||||
0x01,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.menu_location = */ app_location_t::UTILITIES,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_afsk_rx */ {'P', 'A', 'F', 'R'},
|
||||
|
||||
@@ -108,8 +108,9 @@ options_t freqman_bandwidths[6] = {
|
||||
},
|
||||
{
|
||||
// WFMAM for NOAA satellites, 137 Mhz band
|
||||
{"80kHz (NOAA Apt)", 0}, // Fixed RX demod- WFM config Index 1 : FM+AM for Audio NOAA APT ones.
|
||||
{"38kHz (NOAA Apt)", 1},
|
||||
{"80k-NOAA Apt LPF", 0}, // Captured RF IQ filtered BW 80K, APT baseband filtered with Low Pass Filter 4k5 fc -3dB
|
||||
{"38k-NOAA Apt LPF", 1}, // Captured RF IQ filtered BW 38K, APT baseband filtered with Low Pass Filter 4k5 fc -3dB
|
||||
{"38k-NOAA Apt BPF", 2}, // Captured RF IQ filtered BW 38K, APT baseband filtered BPF centred to the 2k4 AM subcarrier, BW = 2KHz
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -69,9 +69,10 @@ static constexpr std::array<baseband::WFMConfig, 3> wfm_configs{{
|
||||
{taps_80k_wfm_decim_0, taps_80k_wfm_decim_1},
|
||||
}};
|
||||
|
||||
static constexpr std::array<baseband::WFMAMConfig, 2> wfmam_configs{{
|
||||
{taps_16k0_decim_0, taps_80k_wfmam_decim_1},
|
||||
{taps_16k0_decim_0, taps_38k_wfmam_decim_1},
|
||||
static constexpr std::array<baseband::WFMAMConfig, 3> wfmam_configs{{
|
||||
{taps_16k0_decim_0, taps_80k_wfmam_decim_1, taps_64_lp_1875_2166},
|
||||
{taps_16k0_decim_0, taps_38k_wfmam_decim_1, taps_64_lp_1875_2166},
|
||||
{taps_16k0_decim_0, taps_38k_wfmam_decim_1, taps_64_bpf_2k4_bw_2k},
|
||||
}};
|
||||
|
||||
} /* namespace */
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
|
||||
Copyright (C) 2025 HTotoo
|
||||
|
||||
|
||||
# External UI elements
|
||||
|
||||
|
||||
## Read carefully
|
||||
|
||||
|
||||
These widgets are only used in external apps!
|
||||
The concept is, we move these widgets out of FW space, and compile them with external apps. To all separately. This is multiple include, but since we compile the widget under different namespaces (and those namespaces will be under the external_app namespace) these all will be removed from the base firmware.
|
||||
|
||||
This way we can free up some spaces, and still reuse the same widgets in different apps easily.
|
||||
|
||||
## How to create external widget
|
||||
|
||||
You create a **hpp** file indet the ui/external folder, and include everything you need for your widget as you normally wanted to do.
|
||||
**Don't forget the ifdef guards!**
|
||||
**Never use any namespace for your class there!** The namespace of the actual ext app will be used where you include this.
|
||||
|
||||
Then create a **cpi** file, where you include your hpp file, and create the implementation of your widget class. Still, don't use namespace.
|
||||
|
||||
You won't need to include these files in any cmake file! (see usage, why)
|
||||
|
||||
## How to use these widgets
|
||||
|
||||
This is important, so follow the rules carefully. If it works, doesn't mean, you did it good!
|
||||
In your external app's hpp file, you need to include the widget's hpp file.
|
||||
**But be careful**, you must include it under the ext app's namespace, before your first class. For examlpe:
|
||||
|
||||
namespace ui::external_app::gfxeq {
|
||||
#include "external/ui_grapheq.hpp"
|
||||
class gfxEQView : public View {
|
||||
Then you must include the cpi file in the external app's cpp file. Again, it must be under the ext app's namespace!
|
||||
|
||||
using namespace portapack;
|
||||
namespace ui::external_app::gfxeq {
|
||||
#include "external/ui_grapheq.cpi"
|
||||
gfxEQView::gfxEQView(NavigationView& nav)
|
||||
|
||||
This must be done under each ext app that uses the same widget.
|
||||
This way, the widget will be compiled multiple times under the external_app namespace, but those will be removed from the base. Each app will be able to use it, easy to handle, easy to create.
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef __UI_GRAPHEQ_H__
|
||||
#define __UI_GRAPHEQ_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
class GraphEq : public Widget {
|
||||
public:
|
||||
std::function<void(GraphEq&)> 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<ui::Dim> bar_heights;
|
||||
std::vector<ui::Dim> 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<int16_t, NUM_BARS + 1> 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()
|
||||
};
|
||||
|
||||
#endif /*__UI_GRAPHEQ_H__*/
|
||||
@@ -157,7 +157,7 @@ void NoaaAptRx::configure(const NoaaAptRxConfigureMessage& message) {
|
||||
channel_filter_high_f = taps_38k_wfmam_decim_1.high_frequency_normalized * decim_1_input_fs;
|
||||
channel_filter_transition = taps_38k_wfmam_decim_1.transition_normalized * decim_1_input_fs;
|
||||
demod.configure(demod_input_fs, 17000);
|
||||
audio_filter.configure(taps_64_lp_1875_2166.taps);
|
||||
audio_filter.configure(taps_64_bpf_2k4_bw_2k.taps);
|
||||
audio_output.configure(apt_audio_12k_notch_2k4_config, apt_audio_12k_lpf_2000hz_config);
|
||||
// channel_spectrum.set_decimation_factor(1);
|
||||
update_params();
|
||||
|
||||
@@ -1491,6 +1491,26 @@ constexpr fir_taps_real<64> taps_64_lp_1875_2166{
|
||||
}},
|
||||
};
|
||||
|
||||
/* 1st Wideband FM demod baseband filter of audio AM tones ,
|
||||
to pass all DSB band of AM fsubcarrier 2.4Khz mod. with APT */
|
||||
/* 24kHz int16_t input
|
||||
* -> FIR filter, BPF center 2k4 carrier ,APT BW 2kHz
|
||||
* -> 12kHz int16_t output, gain of 1.0 (I think).
|
||||
*/
|
||||
constexpr fir_taps_real<64> taps_64_bpf_2k4_bw_2k{
|
||||
.low_frequency_normalized = -0.1875f, // not updated, this is just for LPF , waterfall GUI, we are not using in BPF NOAA app.
|
||||
.high_frequency_normalized = 0.1875f, // not used GUI in NOAA App.
|
||||
.transition_normalized = 0.03f, // not used GUI in NOAA app.
|
||||
.taps = {{-45, -29, 32, 63, 0, -125, -181, -81, 61,
|
||||
0, -329, -635, -551, -147, 0, -547, -1404, -1625,
|
||||
-849, 0, -414, -2118, -3358, -2422, 0, 911, -1792,
|
||||
-6126, -6773, 0, 11839, 21131, 21131, 11839, 0, -6773,
|
||||
-6126, -1792, 911, 0, -2422, -3358, -2118, -414, 0,
|
||||
-849, -1625, -1404, -547, 0, -147, -551, -635, -329,
|
||||
0, 61, -81, -181, -125, 0, 63, 32, -29,
|
||||
-45}},
|
||||
};
|
||||
|
||||
// TPMS decimation filters ////////////////////////////////////////////////
|
||||
|
||||
// IFIR image-reject filter: fs=2457600, pass=100000, stop=407200, decim=4, fout=614400
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#define __JAMMER_H__
|
||||
|
||||
#define JAMMER_CH_WIDTH 1000000
|
||||
#define JAMMER_MAX_CH 24
|
||||
#define JAMMER_MAX_CH 80
|
||||
|
||||
namespace jammer {
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ struct SharedMemory {
|
||||
union {
|
||||
ToneData tones_data;
|
||||
struct {
|
||||
JammerChannel jammer_channels[24];
|
||||
JammerChannel jammer_channels[80];
|
||||
HopperChannel hopper_channels[24];
|
||||
} dummy_seperate;
|
||||
uint8_t data[512];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2025 RocketGod
|
||||
* Copyright (C) 2025 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -2667,7 +2669,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 +2690,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 +2822,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(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2025 RocketGod
|
||||
* Copyright (C) 2025 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -1015,6 +1017,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<void(GraphEq&)> 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<ui::Dim> bar_heights;
|
||||
std::vector<ui::Dim> 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<int16_t, NUM_BARS + 1> 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);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
87,108,FM BROADCAST USA
|
||||
76,108,FM BROADCAST BRAZIL
|
||||
76,95,FM BROADCAST JAPAN
|
||||
65,74,FM BROADCAST RUSSIA
|
||||
65,74,FM OIRT EAST EU
|
||||
# CITIZENS BAND RADIO
|
||||
26,28,CB RADIO
|
||||
# COMMON PUBLIC SERVICE BANDS
|
||||
|
||||
Reference in New Issue
Block a user