From b8ad44c43109d9bdbe0cbc53a9776c6715cd0fbd Mon Sep 17 00:00:00 2001 From: Mo <83806223+Xmoo26@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:54:54 +0200 Subject: [PATCH] Opt-in channel-power squelch for AM audio RX (#3315) * AM: add opt-in channel-power squelch for AM audio RX Add an SDR++-style power squelch on the complex channel signal for the Analog Audio RX app in AM modes (DSB/SSB/CW). The squelch measures the mean channel power, converts it to dBFS, and mutes the demodulated audio when the signal is below a user-set threshold. Being carrier-based it keys on the AM carrier (e.g. airband/ATC) rather than on audio-band noise, which the existing FM squelch could not do for AM. A new 'SQ' field (0-99, 0 = off) is added to the AM options view and is persisted per app via app_settings. Default is 0, so existing AM behaviour is unchanged unless the user enables it. Threshold maps 1..99 to -80..-20 dBFS. Touches: message (AMConfigureMessage.squelch_level), baseband_api (AMConfig::apply), receiver_model (am_squelch_level get/set), app_settings (persistence), proc_am_audio (DSP), analog_audio_app (UI). * Refactor audio power calculation to use magnitude squared Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- firmware/application/app_settings.cpp | 3 ++ firmware/application/app_settings.hpp | 3 +- .../application/apps/analog_audio_app.cpp | 7 +++++ .../application/apps/analog_audio_app.hpp | 11 ++++++++ firmware/application/baseband_api.cpp | 9 +++--- firmware/application/baseband_api.hpp | 4 +-- firmware/application/receiver_model.cpp | 11 +++++++- firmware/application/receiver_model.hpp | 4 +++ firmware/baseband/proc_am_audio.cpp | 28 +++++++++++++++++++ firmware/baseband/proc_am_audio.hpp | 1 + firmware/common/message.hpp | 7 +++-- 11 files changed, 78 insertions(+), 10 deletions(-) diff --git a/firmware/application/app_settings.cpp b/firmware/application/app_settings.cpp index 51c7a13f2..2801cf4ec 100644 --- a/firmware/application/app_settings.cpp +++ b/firmware/application/app_settings.cpp @@ -203,6 +203,7 @@ void copy_to_radio_model(const AppSettings& settings) { settings.wfm_config_index, settings.wfmam_config_index, settings.squelch); + receiver_model.set_am_squelch_level(settings.am_squelch); } receiver_model.set_frequency_step(settings.step); @@ -227,6 +228,7 @@ void copy_from_radio_model(AppSettings& settings) { settings.vga = receiver_model.vga(); settings.rx_amp = receiver_model.rf_amp(); settings.squelch = receiver_model.squelch_level(); + settings.am_squelch = receiver_model.am_squelch_level(); settings.modulation = static_cast(receiver_model.modulation()); settings.am_config_index = receiver_model.am_configuration(); @@ -288,6 +290,7 @@ SettingsManager::SettingsManager( bindings_.emplace_back("wfm_config_index"sv, &settings_.wfm_config_index); bindings_.emplace_back("wfmam_config_index"sv, &settings_.wfmam_config_index); bindings_.emplace_back("squelch"sv, &settings_.squelch); + bindings_.emplace_back("am_squelch"sv, &settings_.am_squelch); } // Common model settings. diff --git a/firmware/application/app_settings.hpp b/firmware/application/app_settings.hpp index 27b82db63..da52ffb8a 100644 --- a/firmware/application/app_settings.hpp +++ b/firmware/application/app_settings.hpp @@ -40,7 +40,7 @@ // Bring in the string_view literal. using std::literals::operator""sv; -#define COMMON_APP_SETTINGS_COUNT 19 +#define COMMON_APP_SETTINGS_COUNT 20 /* Represents a named setting bound to a variable instance. */ /* Using void* instead of std::variant, because variant is a pain to dispatch over. */ @@ -150,6 +150,7 @@ struct AppSettings { uint8_t wfm_config_index = 0; uint8_t wfmam_config_index = 0; uint8_t squelch = 80; + uint8_t am_squelch = 0; // AM channel-power squelch threshold (0 = off) uint8_t volume; // NOTE: update COMMON_APP_SETTINGS_COUNT when adding to this }; diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp index 98668edfd..9913a402a 100644 --- a/firmware/application/apps/analog_audio_app.cpp +++ b/firmware/application/apps/analog_audio_app.cpp @@ -72,8 +72,15 @@ AMOptionsView::AMOptionsView( &label_config, &options_config, &zoom_config, + &label_squelch, + &field_squelch, }); + field_squelch.set_value(receiver_model.am_squelch_level()); + field_squelch.on_change = [](int32_t v) { + receiver_model.set_am_squelch_level(v); + }; + zoom_config.on_change = [this, view](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration( view->get_previous_AM_mode_option() + zoom_filter_offset(n), diff --git a/firmware/application/apps/analog_audio_app.hpp b/firmware/application/apps/analog_audio_app.hpp index 4da4aad0a..637fc21f0 100644 --- a/firmware/application/apps/analog_audio_app.hpp +++ b/firmware/application/apps/analog_audio_app.hpp @@ -100,6 +100,17 @@ class AMOptionsView : public View { {"ZOOM x2", (int)AMSpectrumZoomOption::X2}, {"ZOOM x3", (int)AMSpectrumZoomOption::X3}, {"ZOOM x4", (int)AMSpectrumZoomOption::X4}}}; + + Text label_squelch{ + {UI_POS_X(10), UI_POS_Y(0), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}, + "SQ"}; + NumberField field_squelch{ + {UI_POS_X(13), UI_POS_Y(0)}, + 2, + {0, 99}, + 1, + ' ', + }; }; class AMFMAptOptionsView : public View { diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index d2e65542b..d3be5c00f 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -70,11 +70,11 @@ static void send_message(const Message* const message) { } } -void AMConfig::apply() const { - apply((AMConfigureMessage::Zoom_waterfall)spectrum_decimation_factor); +void AMConfig::apply(const uint8_t squelch_level) const { + apply((AMConfigureMessage::Zoom_waterfall)spectrum_decimation_factor, squelch_level); } -void AMConfig::apply(const AMConfigureMessage::Zoom_waterfall spectrum_zoom) const { +void AMConfig::apply(const AMConfigureMessage::Zoom_waterfall spectrum_zoom, const uint8_t squelch_level) const { const AMConfigureMessage message{ taps_6k0_decim_0, // common FIR filter taps pre-decim_0 to all 6 x AM mod types.(AM-9K, AM-6K, USB, LSB, CW, AMFM-WFAX) decim_1, // var decim_1 FIR taps filter , variable values , to handle two spectrum decim factor 1 and 2 (zoom) and more APT LPF filtered . @@ -82,7 +82,8 @@ void AMConfig::apply(const AMConfigureMessage::Zoom_waterfall spectrum_zoom) con channel, // var channel FIR taps filter , variable values, depending selected AM mode, each one different (DSB-9K, DSB-6K, USB-3K, LSB-3K,CW,AMFM-WFAX) modulation, // var parameter . enum class Modulation : int32_t {DSB = 0, SSB = 1, SSB_FM = 2} audio_12k_iir_filter_config, // var parameter , 300 Hz hpf all except Wefax (1.500Hz lpf) - (size_t)spectrum_zoom}; + (size_t)spectrum_zoom, + squelch_level}; send_message(&message); audio::set_rate(audio::Rate::Hz_12000); } diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 9ef524d95..4083859ae 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -43,8 +43,8 @@ struct AMConfig { const iir_biquad_config_t audio_12k_iir_filter_config; // added to handle two var IIR filter types : 300 hpf(as before) , 1500Hz lpf for Wefax. const size_t spectrum_decimation_factor; - void apply() const; - void apply(AMConfigureMessage::Zoom_waterfall spectrum_zoom) const; + void apply(uint8_t squelch_level = 0) const; + void apply(AMConfigureMessage::Zoom_waterfall spectrum_zoom, uint8_t squelch_level = 0) const; }; struct NBFMConfig { diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index 4025fac4c..638a2a3a1 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -251,6 +251,15 @@ void ReceiverModel::set_squelch_level(uint8_t v) { update_modulation(); } +uint8_t ReceiverModel::am_squelch_level() const { + return settings_.am_squelch_level; +} + +void ReceiverModel::set_am_squelch_level(uint8_t v) { + settings_.am_squelch_level = v; + update_modulation(); +} + void ReceiverModel::set_antenna_bias() { update_antenna_bias(); } @@ -486,7 +495,7 @@ void ReceiverModel::update_modulation() { } void ReceiverModel::update_am_configuration() { - am_configs[am_configuration()].apply(am_spectrum_zoom_); + am_configs[am_configuration()].apply(am_spectrum_zoom_, am_squelch_level()); } void ReceiverModel::update_amfm_configuration() { diff --git a/firmware/application/receiver_model.hpp b/firmware/application/receiver_model.hpp index 22f1ab090..2d24859b1 100644 --- a/firmware/application/receiver_model.hpp +++ b/firmware/application/receiver_model.hpp @@ -61,6 +61,7 @@ class ReceiverModel { uint8_t nbfm_config_index = 0; uint8_t wfm_config_index = 0; uint8_t squelch_level = 80; + uint8_t am_squelch_level = 0; // AM channel-power squelch threshold (0 = off) }; /* The frequency to receive (no offset). */ @@ -109,6 +110,9 @@ class ReceiverModel { uint8_t squelch_level() const; void set_squelch_level(uint8_t v); + uint8_t am_squelch_level() const; + void set_am_squelch_level(uint8_t v); + void set_antenna_bias(); volume_t headphone_volume() const; diff --git a/firmware/baseband/proc_am_audio.cpp b/firmware/baseband/proc_am_audio.cpp index e2f9e9687..2e612a0f8 100644 --- a/firmware/baseband/proc_am_audio.cpp +++ b/firmware/baseband/proc_am_audio.cpp @@ -27,6 +27,7 @@ #include "event_m4.hpp" #include +#include #include "dsp_hilbert.hpp" // Phase 2: Constructor to start threads AFTER object is fully initialized @@ -72,6 +73,32 @@ void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) { feed_channel_stats(channel_out); auto audio = demodulate(channel_out); // now 3 AM demodulation types : demod_am, demod_ssb, demod_ssb_fm (for Wefax) + + // SDR++-style power squelch on the complex channel signal (opt-in; 0 = off). + // Measures mean channel power, converts to dBFS, and mutes the audio when + // the signal is below the user threshold. Carrier-based, so it keys on the + // AM carrier (e.g. airband/ATC) rather than on audio-band noise. + if (squelch_level > 0 && channel_out.count > 0) { + uint64_t sum_mag_sq = 0; + for (size_t i = 0; i < channel_out.count; i++) { + const int32_t re = channel_out.p[i].real(); + const int32_t im = channel_out.p[i].imag(); + sum_mag_sq += static_cast(re) * re + static_cast(im) * im; + } + + constexpr float full_scale_mag2 = 32768.0f * 32768.0f; + const float mean_mag2_norm = + static_cast(sum_mag_sq) / (static_cast(channel_out.count) * full_scale_mag2); + const float level_dbfs = (mean_mag2_norm > 0.0f) ? mag2_to_dbv_norm(mean_mag2_norm) : -200.0f; + + // Map squelch_level 1..99 to a threshold of -80..-20 dBFS. + const float threshold_dbfs = -80.0f + (squelch_level - 1) * (60.0f / 98.0f); + if (level_dbfs < threshold_dbfs) { + for (size_t i = 0; i < audio.count; i++) + audio.p[i] = 0.0f; + } + } + audio_compressor.execute_in_place(audio); audio_output.write(audio); } @@ -151,6 +178,7 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) { spectrum_interval_samples = decim_0_output_fs / spectrum_rate_hz; audio_output.configure(message.audio_hpf_lpf_config); // hpf in all AM demod modes (AM-6K/9K, USB/LSB,DSB), except Wefax (lpf there). + squelch_level = message.squelch_level; configured = true; } diff --git a/firmware/baseband/proc_am_audio.hpp b/firmware/baseband/proc_am_audio.hpp index 705a55079..16e510465 100644 --- a/firmware/baseband/proc_am_audio.hpp +++ b/firmware/baseband/proc_am_audio.hpp @@ -67,6 +67,7 @@ class NarrowbandAMAudio : public BasebandProcessor { int32_t channel_filter_high_f = 0; int32_t channel_filter_transition = 0; bool configured{false}; + uint8_t squelch_level{0}; // AM channel-power squelch threshold (0 = off), SDR++-style size_t spectrum_interval_samples{0}; size_t spectrum_samples{0}; bool spectrum_capture_active{false}; diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 030d99c91..ea3bb9060 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -739,7 +739,8 @@ class AMConfigureMessage : public Message { const fir_taps_complex<64> channel_filter, const Modulation modulation, const iir_biquad_config_t audio_hpf_lpf_config, - const size_t channel_spectrum_decimation_factor) + const size_t channel_spectrum_decimation_factor, + const uint8_t squelch_level = 0) : Message{ID::AMConfigure}, decim_0_filter(decim_0_filter), @@ -748,7 +749,8 @@ class AMConfigureMessage : public Message { channel_filter(channel_filter), modulation{modulation}, audio_hpf_lpf_config(audio_hpf_lpf_config), - channel_spectrum_decimation_factor(channel_spectrum_decimation_factor) { + channel_spectrum_decimation_factor(channel_spectrum_decimation_factor), + squelch_level(squelch_level) { } const fir_taps_real<24> decim_0_filter; @@ -758,6 +760,7 @@ class AMConfigureMessage : public Message { const Modulation modulation; const iir_biquad_config_t audio_hpf_lpf_config; const size_t channel_spectrum_decimation_factor; + const uint8_t squelch_level; // AM channel-power squelch threshold (0 = off) }; // TODO: Put this somewhere else, or at least the implementation part.