diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp index 91621f4b4..98668edfd 100644 --- a/firmware/application/apps/analog_audio_app.cpp +++ b/firmware/application/apps/analog_audio_app.cpp @@ -37,6 +37,28 @@ using namespace tonekey; namespace ui { +namespace { + +constexpr uint8_t zoom_filter_offset(const OptionsField::value_t zoom_option) { + return zoom_option == (int)AMSpectrumZoomOption::X1 ? 0 : 6; +} + +constexpr AMConfigureMessage::Zoom_waterfall zoom_spectrum_factor( + const OptionsField::value_t zoom_option) { + switch ((AMSpectrumZoomOption)zoom_option) { + case AMSpectrumZoomOption::X2: + return AMConfigureMessage::Zoom_waterfall::ZOOM_x_2; + case AMSpectrumZoomOption::X3: + return AMConfigureMessage::Zoom_waterfall::ZOOM_x_3; + case AMSpectrumZoomOption::X4: + return AMConfigureMessage::Zoom_waterfall::ZOOM_x_4; + default: + return AMConfigureMessage::Zoom_waterfall::ZOOM_x_1; + } +} + +} // namespace + /* AMOptionsView *********************************************************/ AMOptionsView::AMOptionsView( @@ -52,8 +74,10 @@ AMOptionsView::AMOptionsView( &zoom_config, }); - zoom_config.on_change = [this, view](size_t, OptionsField::value_t n) { // n , has two option values. when GUI =zoom+1 => (0), when GUI=zoom+2 (6) - receiver_model.set_am_configuration(view->get_previous_AM_mode_option() + n); // n (0 or 6) + 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), + zoom_spectrum_factor(n)); view->set_zoom_factor(AM_MODULATION, n); view->set_previous_zoom_option(n); }; @@ -61,11 +85,13 @@ AMOptionsView::AMOptionsView( // restore zoom selection zoom_config.set_by_value(view->get_zoom_factor(AM_MODULATION)); - freqman_set_bandwidth_option(AM_MODULATION, options_config); // freqman.cpp to the options_config, only allowing 5 modes freqman_bandwidths[AM] {"DSB 9k", 0}, {"DSB 6k", 1}, {"USB+3k", 2}, {"LSB-3k", 3}, {"CW", 4}, - options_config.set_by_value(receiver_model.am_configuration() - view->get_previous_zoom_option()); // restore AM GUI option mode , AM FIR index filters (0..11) values , am_configs has 12 fir index elements. + freqman_set_bandwidth_option(AM_MODULATION, options_config); // freqman.cpp to the options_config, only allowing 5 modes freqman_bandwidths[AM] {"DSB 9k", 0}, {"DSB 6k", 1}, {"USB+3k", 2}, {"LSB-3k", 3}, {"CW", 4}, + options_config.set_by_value(receiver_model.am_configuration() - zoom_filter_offset(view->get_previous_zoom_option())); options_config.on_change = [this, view](size_t, OptionsField::value_t n) { - receiver_model.set_am_configuration(n + view->get_previous_zoom_option()); // we select proper FIR AM filter (0..11), = 0..4 GUI AM modes + offset +6 (if zoom+2) - view->set_previous_AM_mode_option(n); // (0..4) allowing 5 AM modes (DSB9K, DSB6K, USB,LSB, CW) + receiver_model.set_am_configuration( + n + zoom_filter_offset(view->get_previous_zoom_option()), + zoom_spectrum_factor(view->get_previous_zoom_option())); + view->set_previous_AM_mode_option(n); }; } @@ -154,7 +180,9 @@ AMFMAptOptionsView::AMFMAptOptionsView( options_config.set_by_value(receiver_model.amfm_configuration()); zoom_config.on_change = [this, view](size_t, OptionsField::value_t n) { - receiver_model.set_amfm_configuration(5 + n); + receiver_model.set_amfm_configuration( + 5 + zoom_filter_offset(n), + zoom_spectrum_factor(n)); view->set_zoom_factor(AMFM_MODULATION, n); }; @@ -645,10 +673,19 @@ void AnalogAudioView::set_frequency_absolute(rf::Frequency frequency) { } int32_t AnalogAudioView::sliding_limit() const { - const bool zoom_x2 = - receiver_model.modulation() == ReceiverModel::Mode::AMAudio && - previous_zoom != 0; - return zoom_x2 ? sliding_limit_zoom_x2 : sliding_limit_zoom_x1; + if (receiver_model.modulation() != ReceiverModel::Mode::AMAudio) + return sliding_limit_zoom_x1; + + switch (previous_zoom) { + case (int)AMSpectrumZoomOption::X2: + return sliding_limit_zoom_x2; + case (int)AMSpectrumZoomOption::X3: + return sliding_limit_zoom_x3; + case (int)AMSpectrumZoomOption::X4: + return sliding_limit_zoom_x4; + default: + return sliding_limit_zoom_x1; + } } void AnalogAudioView::reset_sliding_frequency(ReceiverModel::Mode modulation) { diff --git a/firmware/application/apps/analog_audio_app.hpp b/firmware/application/apps/analog_audio_app.hpp index f3a89bc10..4da4aad0a 100644 --- a/firmware/application/apps/analog_audio_app.hpp +++ b/firmware/application/apps/analog_audio_app.hpp @@ -69,6 +69,13 @@ class PralineOptionsView : public View { }; #endif +enum class AMSpectrumZoomOption : int32_t { + X1 = 0, + X2 = 6, + X3 = 12, + X4 = 18, +}; + class AMOptionsView : public View { public: AMOptionsView(AnalogAudioView* view, Rect parent_rect, const Style* style); @@ -89,9 +96,10 @@ class AMOptionsView : public View { OptionsField zoom_config{ {UI_POS_X_RIGHT(7), UI_POS_Y(0)}, 7, - {{"ZOOM x1", 0}, - {"ZOOM x2", 6}} // offset index AM modes array FIR filters. - }; + {{"ZOOM x1", (int)AMSpectrumZoomOption::X1}, + {"ZOOM x2", (int)AMSpectrumZoomOption::X2}, + {"ZOOM x3", (int)AMSpectrumZoomOption::X3}, + {"ZOOM x4", (int)AMSpectrumZoomOption::X4}}}; }; class AMFMAptOptionsView : public View { @@ -114,9 +122,10 @@ class AMFMAptOptionsView : public View { OptionsField zoom_config{ {UI_POS_X_RIGHT(7), UI_POS_Y(0)}, 7, - {{"ZOOM x1", 0}, - {"ZOOM x2", 6}} // offset index array filters. - }; + {{"ZOOM x1", (int)AMSpectrumZoomOption::X1}, + {"ZOOM x2", (int)AMSpectrumZoomOption::X2}, + {"ZOOM x3", (int)AMSpectrumZoomOption::X3}, + {"ZOOM x4", (int)AMSpectrumZoomOption::X4}}}; }; class NBFMOptionsView : public View { @@ -265,12 +274,14 @@ class AnalogAudioView : public View { NavigationView& nav_; RxRadioState radio_state_{}; uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839 - uint8_t zoom_factor_am{0}; // initial zoom factor in AM mode - uint8_t zoom_factor_amfm{0}; // initial zoom factor in AMFM mode - uint8_t previous_AM_mode_option{0}; // GUI 5 AM modes : (0..4 ) (DSB9K, DSB6K, USB,LSB, CW). Used to select proper FIR filter (0..11) AM mode + offset 0 (zoom+1) or +6 (if zoom+2) - uint8_t previous_zoom{0}; // GUI ZOOM+1, ZOOM+2 , equivalent to two values offset 0 (zoom+1) or +6 (if zoom+2) + uint8_t zoom_factor_am{0}; + uint8_t zoom_factor_amfm{0}; + uint8_t previous_AM_mode_option{0}; + uint8_t previous_zoom{0}; static constexpr int32_t sliding_limit_zoom_x1 = 50000; static constexpr int32_t sliding_limit_zoom_x2 = 30000; + static constexpr int32_t sliding_limit_zoom_x3 = 15000; + static constexpr int32_t sliding_limit_zoom_x4 = 6250; rf::Frequency sliding_center_frequency{0}; bool sliding_enabled{false}; diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 4daf524dc..d2e65542b 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -71,6 +71,10 @@ static void send_message(const Message* const message) { } void AMConfig::apply() const { + apply((AMConfigureMessage::Zoom_waterfall)spectrum_decimation_factor); +} + +void AMConfig::apply(const AMConfigureMessage::Zoom_waterfall spectrum_zoom) 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 . @@ -78,7 +82,7 @@ void AMConfig::apply() const { 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) - spectrum_decimation_factor}; // var parameter , waterfall no zoom : 1 ,for zoom x 2 : 2 + (size_t)spectrum_zoom}; 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 e192168f3..9ef524d95 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -41,9 +41,10 @@ struct AMConfig { const fir_taps_complex<64> channel; const AMConfigureMessage::Modulation modulation; 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; // used to handle LCD AM waterfall zoom x1 / zoom x2. + const size_t spectrum_decimation_factor; void apply() const; + void apply(AMConfigureMessage::Zoom_waterfall spectrum_zoom) const; }; struct NBFMConfig { diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index cfd1906af..4025fac4c 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -57,6 +57,12 @@ static constexpr std::array am_configs{{ {taps_6k0_narrow_decim_1, taps_6k0_decim_2, taps_2k6_usb_wefax_channel, AMConfigureMessage::Modulation::SSB_FM, apt_audio_12k_lpf_1500hz_config, (int)AMConfigureMessage::Zoom_waterfall::ZOOM_x_2}, // SSB USB+FM to demod. Subcarrier FM Audio Tones to get APT Weather Fax with waterfall zoom x 2 (we need taps_6k0_narrow_decim_1 to minimize aliasing) }}; +AMConfigureMessage::Zoom_waterfall spectrum_zoom_for_am_config(const size_t index) { + return index < am_configs.size() + ? (AMConfigureMessage::Zoom_waterfall)am_configs[index].spectrum_decimation_factor + : AMConfigureMessage::Zoom_waterfall::ZOOM_x_1; +} + static constexpr std::array nbfm_configs{{ {taps_4k25_decim_0, taps_4k25_decim_1, taps_4k25_channel, 2500}, {taps_11k0_decim_0, taps_11k0_decim_1, taps_11k0_channel, 2500}, @@ -166,6 +172,17 @@ uint8_t ReceiverModel::am_configuration() const { void ReceiverModel::set_am_configuration(uint8_t n) { if (n < am_configs.size()) { settings_.am_config_index = n; + am_spectrum_zoom_ = spectrum_zoom_for_am_config(n); + update_modulation(); + } +} + +void ReceiverModel::set_am_configuration( + uint8_t n, + AMConfigureMessage::Zoom_waterfall spectrum_zoom) { + if (n < am_configs.size()) { + settings_.am_config_index = n; + am_spectrum_zoom_ = spectrum_zoom; update_modulation(); } } @@ -177,6 +194,17 @@ uint8_t ReceiverModel::amfm_configuration() const { void ReceiverModel::set_amfm_configuration(uint8_t n) { if (n < am_configs.size()) { settings_.amfm_config_index = n; + am_spectrum_zoom_ = spectrum_zoom_for_am_config(n); + update_modulation(); + } +} + +void ReceiverModel::set_amfm_configuration( + uint8_t n, + AMConfigureMessage::Zoom_waterfall spectrum_zoom) { + if (n < am_configs.size()) { + settings_.amfm_config_index = n; + am_spectrum_zoom_ = spectrum_zoom; update_modulation(); } } @@ -288,6 +316,7 @@ void ReceiverModel::enable() { void ReceiverModel::disable() { enabled_ = false; + am_spectrum_zoom_ = spectrum_zoom_for_am_config(settings_.am_config_index); // TODO: Responsibility for enabling/disabling the radio is muddy. // Some happens in ReceiverModel, some inside radio namespace. @@ -296,6 +325,7 @@ void ReceiverModel::disable() { void ReceiverModel::initialize() { settings_ = settings_t{}; + am_spectrum_zoom_ = spectrum_zoom_for_am_config(settings_.am_config_index); } void ReceiverModel::set_configuration_without_update( @@ -309,6 +339,7 @@ void ReceiverModel::set_configuration_without_update( settings_.mode = new_mode; settings_.frequency_step = new_frequency_step; settings_.am_config_index = new_am_config_index; + am_spectrum_zoom_ = spectrum_zoom_for_am_config(new_am_config_index); settings_.nbfm_config_index = new_nbfm_config_index; settings_.wfm_config_index = new_wfm_config_index; settings_.wfmam_config_index = new_wfmam_config_index; @@ -455,11 +486,11 @@ void ReceiverModel::update_modulation() { } void ReceiverModel::update_am_configuration() { - am_configs[am_configuration()].apply(); + am_configs[am_configuration()].apply(am_spectrum_zoom_); } void ReceiverModel::update_amfm_configuration() { - am_configs[amfm_configuration()].apply(); // update with different index for Wefax. + am_configs[amfm_configuration()].apply(am_spectrum_zoom_); // update with different index for Wefax. } void ReceiverModel::update_nbfm_configuration() { diff --git a/firmware/application/receiver_model.hpp b/firmware/application/receiver_model.hpp index 414208541..22f1ab090 100644 --- a/firmware/application/receiver_model.hpp +++ b/firmware/application/receiver_model.hpp @@ -91,9 +91,11 @@ class ReceiverModel { uint8_t am_configuration() const; void set_am_configuration(uint8_t n); + void set_am_configuration(uint8_t n, AMConfigureMessage::Zoom_waterfall spectrum_zoom); uint8_t amfm_configuration() const; void set_amfm_configuration(uint8_t n); + void set_amfm_configuration(uint8_t n, AMConfigureMessage::Zoom_waterfall spectrum_zoom); uint8_t nbfm_configuration() const; void set_nbfm_configuration(uint8_t n); @@ -140,6 +142,7 @@ class ReceiverModel { settings_t& settings() { return settings_; } private: + AMConfigureMessage::Zoom_waterfall am_spectrum_zoom_{AMConfigureMessage::Zoom_waterfall::ZOOM_x_1}; settings_t settings_{}; bool enabled_ = false; rf::Frequency hidden_offset = 0; // when we need to hide the offset from user, we set this. like when WeFax needs -300Hz. diff --git a/firmware/baseband/dsp_decimate.cpp b/firmware/baseband/dsp_decimate.cpp index e7aef4f4a..0396df945 100644 --- a/firmware/baseband/dsp_decimate.cpp +++ b/firmware/baseband/dsp_decimate.cpp @@ -337,6 +337,59 @@ buffer_c16_t FIRC16xR16x16Decim2::execute( src.sampling_rate / decimation_factor}; } +// FIRC16xR16x32Decim4 //////////////////////////////////////////////////// + +void FIRC16xR16x32Decim4::configure( + const std::array& taps, + const int32_t scale) { + std::copy(taps.cbegin(), taps.cend(), taps_.begin()); + output_scale = scale; + z_.fill({}); +} + +buffer_c16_t FIRC16xR16x32Decim4::execute( + const buffer_c16_t& src, + const buffer_c16_t& dst) { + vec2_s16* const z = static_cast(__builtin_assume_aligned(z_.data(), 4)); + const vec2_s16* const t = static_cast(__builtin_assume_aligned(taps_.data(), 4)); + uint32_t* const d = static_cast(__builtin_assume_aligned(dst.p, 4)); + + const auto k = output_scale; + const size_t count = src.count / decimation_factor; + + for (size_t i = 0; i < count; i++) { + const vec2_s16* const in = static_cast(__builtin_assume_aligned(&src.p[i * decimation_factor], 4)); + + complex32_t accum; + + accum = mac_shift(z, t, 0, accum); + accum = mac_shift(z, t, 1, accum); + + accum = mac_shift_and_store(z, t, decimation_factor, 0, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 1, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 2, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 3, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 4, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 5, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 6, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 7, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 8, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 9, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 10, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 11, accum); + + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 0, taps_count, accum); + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 1, taps_count, accum); + + d[i] = scale_round_and_pack(accum, k); + } + + return { + dst.p, + count, + src.sampling_rate / decimation_factor}; +} + // FIRC16xR16x63HalfbandDecim2 //////////////////////////////////////////// void FIRC16xR16x63HalfbandDecim2::configure( diff --git a/firmware/baseband/dsp_decimate.hpp b/firmware/baseband/dsp_decimate.hpp index 8e9bc4283..f9523708f 100644 --- a/firmware/baseband/dsp_decimate.hpp +++ b/firmware/baseband/dsp_decimate.hpp @@ -170,6 +170,28 @@ class FIRC16xR16x16Decim2 { int32_t output_scale = 0; }; +class FIRC16xR16x32Decim4 { + public: + static constexpr size_t taps_count = 32; + static constexpr size_t decimation_factor = 4; + + using sample_t = complex16_t; + using tap_t = int16_t; + + void configure( + const std::array& taps, + const int32_t scale = c16_to_c32_sat_scalar); + + buffer_c16_t execute( + const buffer_c16_t& src, + const buffer_c16_t& dst); + + private: + std::array z_{}; + std::array taps_{}; + int32_t output_scale = 0; +}; + class FIRC16xR16x63HalfbandDecim2 { public: static constexpr size_t taps_count = 63; diff --git a/firmware/baseband/filtered_spectrum_collector.cpp b/firmware/baseband/filtered_spectrum_collector.cpp index c0a7ec5f1..fc485cc4c 100644 --- a/firmware/baseband/filtered_spectrum_collector.cpp +++ b/firmware/baseband/filtered_spectrum_collector.cpp @@ -12,8 +12,22 @@ #include "dsp_fir_taps.hpp" #include "event_m4.hpp" +#include "ch.h" + #include +namespace { + +constexpr bool valid_decimation( + const size_t decimation_factor, + const size_t maximum_decimation) { + return decimation_factor >= 2 && + decimation_factor <= maximum_decimation && + (decimation_factor & (decimation_factor - 1)) == 0; +} + +} // namespace + void FilteredSpectrumCollector::on_message(const Message* const message) { if (message->id == Message::ID::UpdateSpectrum) { update(); @@ -21,11 +35,17 @@ void FilteredSpectrumCollector::on_message(const Message* const message) { SpectrumCollector::on_message(message); } -void FilteredSpectrumCollector::start_capture( +bool FilteredSpectrumCollector::start_capture( const size_t decimation_factor) { + if (!valid_decimation(decimation_factor, maximum_decimation)) { + capture_ready_ = false; + return false; + } + capture_decimation_ = decimation_factor; capture_count_ = 0; capture_ready_ = false; + return true; } bool FilteredSpectrumCollector::feed( @@ -38,7 +58,7 @@ bool FilteredSpectrumCollector::feed( filter_high_frequency, filter_transition); - const size_t required_samples = 256 * capture_decimation_; + const size_t required_samples = fft_samples * capture_decimation_; const size_t copy_count = std::min( channel.count, required_samples - capture_count_); std::copy_n( @@ -70,7 +90,7 @@ void FilteredSpectrumCollector::update() { decim_0_.configure(taps_audio_spectrum_halfband.taps); const buffer_c16_t capture{ capture_.data(), - 256 * capture_decimation_, + fft_samples * capture_decimation_, sampling_rate_ * capture_decimation_}; const buffer_c16_t stage_0{ stage_0_.data(), @@ -88,3 +108,125 @@ void FilteredSpectrumCollector::update() { } capture_ready_ = false; } + +void AMFilteredSpectrumCollector::on_message(const Message* const message) { + if (message->id == Message::ID::UpdateSpectrum) { + update(); + } + SpectrumCollector::on_message(message); +} + +bool AMFilteredSpectrumCollector::start_capture( + const size_t decimation_factor) { + if (!valid_decimation(decimation_factor, maximum_decimation)) { + return false; + } + + chSysLock(); + if (capture_state_ != CaptureState::Idle) { + chSysUnlock(); + return false; + } + + capture_decimation_ = decimation_factor; + capture_count_ = 0; + capture_state_ = CaptureState::Capturing; + chSysUnlock(); + return true; +} + +bool AMFilteredSpectrumCollector::feed( + const buffer_c16_t& channel, + const int32_t filter_low_frequency, + const int32_t filter_high_frequency, + const int32_t filter_transition) { + chSysLock(); + if (capture_state_ != CaptureState::Capturing) { + chSysUnlock(); + return false; + } + chSysUnlock(); + + set_filter( + filter_low_frequency, + filter_high_frequency, + filter_transition); + + const size_t required_samples = fft_samples * capture_decimation_; + const size_t copy_count = std::min( + channel.count, required_samples - capture_count_); + std::copy_n( + channel.p, + copy_count, + capture_.begin() + capture_count_); + capture_count_ += copy_count; + + if (capture_count_ == required_samples) { + sampling_rate_ = channel.sampling_rate / capture_decimation_; + const bool streaming = is_streaming(); + + chSysLock(); + if (streaming) { + capture_state_ = CaptureState::Pending; + } else { + capture_state_ = CaptureState::Idle; + } + chSysUnlock(); + + if (streaming) { + EventDispatcher::events_flag(EVT_MASK_SPECTRUM); + } + return true; + } + return false; +} + +void AMFilteredSpectrumCollector::update() { + chSysLock(); + if (capture_state_ != CaptureState::Pending) { + chSysUnlock(); + return; + } + capture_state_ = CaptureState::Processing; + const size_t capture_decimation = capture_decimation_; + const uint32_t sampling_rate = sampling_rate_; + chSysUnlock(); + + size_t filtered_count = fft_samples * capture_decimation; + uint32_t filtered_sampling_rate = sampling_rate * capture_decimation; + size_t remaining_decimation = capture_decimation; + + if (remaining_decimation == maximum_decimation) { + decimator_4_.configure(taps_audio_spectrum_decim_4.taps); + const buffer_c16_t input{ + capture_.data(), + filtered_count, + filtered_sampling_rate}; + const buffer_c16_t output{ + capture_.data(), + filtered_count / decimator_4_.decimation_factor}; + const auto filtered = decimator_4_.execute(input, output); + filtered_count = filtered.count; + filtered_sampling_rate = filtered.sampling_rate; + remaining_decimation /= decimator_4_.decimation_factor; + } + + for (; remaining_decimation > 1; remaining_decimation /= 2) { + decimator_.configure(taps_audio_spectrum_halfband.taps); + const buffer_c16_t input{ + capture_.data(), + filtered_count, + filtered_sampling_rate}; + const buffer_c16_t output{ + capture_.data(), + filtered_count / 2}; + const auto filtered = decimator_.execute(input, output); + filtered_count = filtered.count; + filtered_sampling_rate = filtered.sampling_rate; + } + + post_message({capture_.data(), filtered_count, filtered_sampling_rate}); + chSysLock(); + capture_state_ = CaptureState::Idle; + chSysUnlock(); +} diff --git a/firmware/baseband/filtered_spectrum_collector.hpp b/firmware/baseband/filtered_spectrum_collector.hpp index 4d8e2edfd..b2e8f4fe0 100644 --- a/firmware/baseband/filtered_spectrum_collector.hpp +++ b/firmware/baseband/filtered_spectrum_collector.hpp @@ -19,7 +19,7 @@ class FilteredSpectrumCollector : public SpectrumCollector { public: void on_message(const Message* const message); - void start_capture(const size_t decimation_factor); + bool start_capture(const size_t decimation_factor); bool feed( const buffer_c16_t& channel, const int32_t filter_low_frequency, @@ -27,6 +27,9 @@ class FilteredSpectrumCollector : public SpectrumCollector { const int32_t filter_transition); private: + static constexpr size_t fft_samples = 256; + static constexpr size_t maximum_decimation = 4; + std::array capture_{}; std::array stage_0_{}; std::array stage_1_{}; @@ -40,4 +43,37 @@ class FilteredSpectrumCollector : public SpectrumCollector { void update(); }; +class AMFilteredSpectrumCollector : public SpectrumCollector { + public: + void on_message(const Message* const message); + + bool start_capture(const size_t decimation_factor); + bool feed( + const buffer_c16_t& channel, + const int32_t filter_low_frequency, + const int32_t filter_high_frequency, + const int32_t filter_transition); + + private: + enum class CaptureState : uint8_t { + Idle, + Capturing, + Pending, + Processing, + }; + + static constexpr size_t fft_samples = 256; + static constexpr size_t maximum_decimation = 16; + + std::array capture_{}; + dsp::decimate::FIRC16xR16x32Decim4 decimator_4_{}; + dsp::decimate::FIRC16xR16x63HalfbandDecim2 decimator_{}; + size_t capture_count_{0}; + size_t capture_decimation_{1}; + uint32_t sampling_rate_{0}; + CaptureState capture_state_{CaptureState::Idle}; + + void update(); +}; + #endif /*__FILTERED_SPECTRUM_COLLECTOR_H__*/ diff --git a/firmware/baseband/proc_am_audio.cpp b/firmware/baseband/proc_am_audio.cpp index 32b497cc0..e2f9e9687 100644 --- a/firmware/baseband/proc_am_audio.cpp +++ b/firmware/baseband/proc_am_audio.cpp @@ -51,8 +51,8 @@ void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) { if (!spectrum_capture_active && spectrum_samples >= spectrum_interval_samples) { spectrum_samples -= spectrum_interval_samples; - channel_spectrum.start_capture(spectrum_zoom_x2 ? 4 : 2); - spectrum_capture_active = true; + spectrum_capture_active = + channel_spectrum.start_capture(spectrum_decimation_factor); } if (spectrum_capture_active && @@ -146,7 +146,7 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) { channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs; modulation_ssb = (int)message.modulation; // now sending by message , 3 types of AM demod : enum class Modulation : int32_t {DSB = 0, SSB = 1, SSB_FM = 2} - spectrum_zoom_x2 = message.channel_spectrum_decimation_factor == 2; + spectrum_decimation_factor = 2 * message.channel_spectrum_decimation_factor; channel_spectrum.set_decimation_factor(1); spectrum_interval_samples = decim_0_output_fs / spectrum_rate_hz; diff --git a/firmware/baseband/proc_am_audio.hpp b/firmware/baseband/proc_am_audio.hpp index 7ccf37054..705a55079 100644 --- a/firmware/baseband/proc_am_audio.hpp +++ b/firmware/baseband/proc_am_audio.hpp @@ -70,7 +70,7 @@ class NarrowbandAMAudio : public BasebandProcessor { size_t spectrum_interval_samples{0}; size_t spectrum_samples{0}; bool spectrum_capture_active{false}; - bool spectrum_zoom_x2{false}; + size_t spectrum_decimation_factor{2}; // bool modulation_ssb = false; // Origianlly we only had 2 AM demod types {DSB = 0, SSB = 1} , and we could handle it with bool var , 1 bit. int8_t modulation_ssb = 0; // Now we have 3 AM demod types we will send now index integer {DSB = 0, SSB = 1, SSB_FM = 2} @@ -80,7 +80,7 @@ class NarrowbandAMAudio : public BasebandProcessor { FeedForwardCompressor audio_compressor{}; AudioOutput audio_output{}; - FilteredSpectrumCollector channel_spectrum{}; + AMFilteredSpectrumCollector channel_spectrum{}; /* NB: Threads should be the last members in the class definition. */ #ifdef PRALINE diff --git a/firmware/baseband/proc_nfm_audio.cpp b/firmware/baseband/proc_nfm_audio.cpp index 3e79afb32..a1dfc40d5 100644 --- a/firmware/baseband/proc_nfm_audio.cpp +++ b/firmware/baseband/proc_nfm_audio.cpp @@ -57,8 +57,7 @@ void NarrowbandFMAudio::execute(const buffer_c8_t& buffer) { if (!spectrum_capture_active && spectrum_samples >= spectrum_interval_samples) { spectrum_samples -= spectrum_interval_samples; - channel_spectrum.start_capture(2); - spectrum_capture_active = true; + spectrum_capture_active = channel_spectrum.start_capture(2); } if (spectrum_capture_active && diff --git a/firmware/common/dsp_fir_taps.hpp b/firmware/common/dsp_fir_taps.hpp index 94bca8f17..a1c8de3f3 100644 --- a/firmware/common/dsp_fir_taps.hpp +++ b/firmware/common/dsp_fir_taps.hpp @@ -77,6 +77,8 @@ constexpr fir_taps_real<16> taps_audio_wide_halfband_0{ * contiguous 256-sample FFT frame is being collected: * 384kHz -> 192kHz (Zoom x1) * 192kHz -> 96kHz (additional stage for Zoom x2) + * 96kHz -> 48kHz (additional stage for Zoom x3) + * 96kHz -> 48kHz -> 24kHz (after the direct /4 stage for Zoom x4) */ constexpr fir_taps_real<63> taps_audio_spectrum_halfband{ .low_frequency_normalized = -0.23f, @@ -149,6 +151,49 @@ constexpr fir_taps_real<63> taps_audio_spectrum_halfband{ }}, }; +/* Direct 384kHz -> 96kHz spectrum decimator for AM Zoom x4. + * Equiripple low-pass: passband edge 25.92kHz, stopband edge 70.08kHz. + */ +constexpr fir_taps_real<32> taps_audio_spectrum_decim_4{ + .low_frequency_normalized = -25920.0f / 384000.0f, + .high_frequency_normalized = 25920.0f / 384000.0f, + .transition_normalized = 44160.0f / 384000.0f, + .taps = {{ + -26, + -62, + -79, + -24, + 126, + 312, + 377, + 154, + -389, + -1020, + -1263, + -616, + 1137, + 3685, + 6234, + 7829, + 7829, + 6234, + 3685, + 1137, + -616, + -1263, + -1020, + -389, + 154, + 377, + 312, + 126, + -24, + -79, + -62, + -26, + }}, +}; + // NBFM 16K0F3E emission type ///////////////////////////////////////////// // IFIR image-reject filter: fs=3072000, pass=8000, stop=344000, decim=8, fout=384000 diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index a9d71b62a..030d99c91 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -728,6 +728,8 @@ class AMConfigureMessage : public Message { enum class Zoom_waterfall : size_t { ZOOM_x_1 = 1, ZOOM_x_2 = 2, + ZOOM_x_3 = 4, + ZOOM_x_4 = 8, }; constexpr AMConfigureMessage(