From ccf093b5d5313e9c3ff36551a7d513b4503ab7f4 Mon Sep 17 00:00:00 2001 From: Belousov Oleg Date: Mon, 27 Jul 2026 21:07:48 +0300 Subject: [PATCH] Add sliding-frequency audio receiver tuning --- .../application/apps/analog_audio_app.cpp | 78 +++++- .../application/apps/analog_audio_app.hpp | 8 + firmware/application/baseband_api.cpp | 5 + firmware/application/baseband_api.hpp | 1 + firmware/application/receiver_model.cpp | 9 + firmware/application/receiver_model.hpp | 1 + firmware/application/ui/ui_freq_field.hpp | 12 +- firmware/application/ui/ui_spectrum.cpp | 117 ++++++++- firmware/application/ui/ui_spectrum.hpp | 9 +- firmware/baseband/dsp_decimate.cpp | 97 ++++++-- firmware/baseband/dsp_decimate.hpp | 36 +++ firmware/baseband/dsp_frequency_xlator.hpp | 232 ++++++++++++++++++ firmware/baseband/proc_am_audio.cpp | 46 +++- firmware/baseband/proc_am_audio.hpp | 12 +- firmware/baseband/proc_nfm_audio.cpp | 45 +++- firmware/baseband/proc_nfm_audio.hpp | 11 +- firmware/baseband/spectrum_collector.cpp | 70 +++++- firmware/baseband/spectrum_collector.hpp | 22 +- firmware/common/dsp_fir_taps.hpp | 36 +++ firmware/common/message.hpp | 12 + 20 files changed, 790 insertions(+), 69 deletions(-) create mode 100644 firmware/baseband/dsp_frequency_xlator.hpp diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp index 5e0adfd17..91621f4b4 100644 --- a/firmware/application/apps/analog_audio_app.cpp +++ b/firmware/application/apps/analog_audio_app.cpp @@ -280,6 +280,12 @@ AnalogAudioView::AnalogAudioView( field_frequency.on_show_options = [this]() { this->on_show_options_frequency(); }; + field_frequency.changing = [this](rf::Frequency frequency) { + return this->on_frequency_changed(frequency); + }; + field_frequency.entered = [this](rf::Frequency frequency) { + this->set_frequency_absolute(frequency); + }; field_lna.on_show_options = [this]() { this->on_show_options_rf_gain(); @@ -308,8 +314,10 @@ AnalogAudioView::AnalogAudioView( }; waterfall.on_select = [this](int32_t offset) { - field_frequency.set_value(receiver_model.target_frequency() + offset); + field_frequency.set_value( + field_frequency.value() + offset * receiver_model.frequency_step()); }; + waterfall.set_live_tuning(true); #ifdef PRALINE button_pro.on_select = [this](Button&) { this->on_show_options_praline(); }; @@ -319,6 +327,7 @@ AnalogAudioView::AnalogAudioView( // This call starts the correct baseband image to run // and sets the radio up as necessary for the given modulation. + sliding_center_frequency = receiver_model.target_frequency(); on_modulation_changed(modulation); } @@ -328,7 +337,7 @@ AnalogAudioView::AnalogAudioView( : AnalogAudioView(nav) { // Settings to override when launched from another app (versus from AppSettings .ini file) // TODO: Which other settings make sense to override? - field_frequency.set_value(override.frequency_app_override); + set_frequency_absolute(override.frequency_app_override); on_frequency_step_changed(override.frequency_step); options_modulation.set_by_value(toUType(override.mode)); } @@ -573,10 +582,14 @@ void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) { receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000); receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000); - receiver_model.set_hidden_offset(modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); // wefax needs to be shifted, see wefax rx app. + reset_sliding_frequency(modulation); receiver_model.enable(); + if (sliding_enabled) { + baseband::set_audio_ddc_frequency(0); + } + // TODO: This doesn't belong here! There's a better way. size_t sampling_rate = 0; switch (modulation) { @@ -610,7 +623,64 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) { } void AnalogAudioView::on_freqchg(int64_t freq) { - field_frequency.set_value(freq); + set_frequency_absolute(freq); +} + +void AnalogAudioView::set_frequency_absolute(rf::Frequency frequency) { + if (!sliding_enabled) { + field_frequency.set_value(frequency); + return; + } + + sliding_center_frequency = frequency; + + /* set_value() does not call on_change when the displayed frequency is + * already equal, so reset the hardware and DDC explicitly in that case. */ + if (field_frequency.value() == frequency) { + receiver_model.set_target_frequency_with_hidden_offset(frequency, 0); + baseband::set_audio_ddc_frequency(0); + } else { + field_frequency.set_value(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; +} + +void AnalogAudioView::reset_sliding_frequency(ReceiverModel::Mode modulation) { + sliding_enabled = + modulation == ReceiverModel::Mode::AMAudio || + modulation == ReceiverModel::Mode::NarrowbandFMAudio; + sliding_center_frequency = receiver_model.target_frequency(); + + /* AMFM keeps its existing Wefax offset; sliding applies to AM and NFM. */ + receiver_model.set_hidden_offset( + modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); +} + +bool AnalogAudioView::on_frequency_changed(rf::Frequency frequency) { + if (!sliding_enabled) + return false; + + const auto limit = sliding_limit(); + int64_t offset = frequency - sliding_center_frequency; + if (offset > limit) { + sliding_center_frequency = frequency - limit; + offset = limit; + } else if (offset < -limit) { + sliding_center_frequency = frequency + limit; + offset = -limit; + } + + /* Store the displayed frequency and retune the hardware centre atomically. */ + receiver_model.set_target_frequency_with_hidden_offset( + frequency, sliding_center_frequency - frequency); + baseband::set_audio_ddc_frequency(static_cast(offset)); + return true; } #ifdef PRALINE diff --git a/firmware/application/apps/analog_audio_app.hpp b/firmware/application/apps/analog_audio_app.hpp index 2e1b1ccfb..f3a89bc10 100644 --- a/firmware/application/apps/analog_audio_app.hpp +++ b/firmware/application/apps/analog_audio_app.hpp @@ -269,6 +269,10 @@ class AnalogAudioView : public View { 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) + static constexpr int32_t sliding_limit_zoom_x1 = 50000; + static constexpr int32_t sliding_limit_zoom_x2 = 30000; + rf::Frequency sliding_center_frequency{0}; + bool sliding_enabled{false}; app_settings::SettingsManager settings_{ "rx_audio", @@ -354,6 +358,10 @@ class AnalogAudioView : public View { void handle_coded_squelch(uint32_t value); void on_freqchg(int64_t freq); + bool on_frequency_changed(rf::Frequency frequency); + void set_frequency_absolute(rf::Frequency frequency); + int32_t sliding_limit() const; + void reset_sliding_frequency(ReceiverModel::Mode modulation); MessageHandlerRegistration message_handler_coded_squelch{ Message::ID::CodedSquelch, diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index cea9a51e0..173e277de 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -343,6 +343,11 @@ void set_spectrum( send_message(&message); } +void set_audio_ddc_frequency(int32_t frequency) { + const AudioDDCConfigMessage message{frequency}; + send_message(&message); +} + void set_time_sink( const size_t sampling_rate, const size_t trigger) { diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 10eee20b5..4953cae6d 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -100,6 +100,7 @@ void set_rds_data(const uint16_t message_length); void set_spectrum( const size_t sampling_rate, const size_t trigger); +void set_audio_ddc_frequency(int32_t frequency); void set_time_sink( const size_t sampling_rate, const size_t trigger); diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index 716286c16..34ff9ffe9 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -88,6 +88,15 @@ void ReceiverModel::set_target_frequency(rf::Frequency f) { update_tuning_frequency(); } +void ReceiverModel::set_target_frequency_with_hidden_offset( + rf::Frequency f, + rf::Frequency offset) { + persistent_memory::set_target_frequency(f); + settings_.frequency_app_override = f; + hidden_offset = offset; + update_tuning_frequency(); +} + uint32_t ReceiverModel::baseband_bandwidth() const { return settings_.baseband_bandwidth; } diff --git a/firmware/application/receiver_model.hpp b/firmware/application/receiver_model.hpp index ea9317bf5..414208541 100644 --- a/firmware/application/receiver_model.hpp +++ b/firmware/application/receiver_model.hpp @@ -66,6 +66,7 @@ class ReceiverModel { /* The frequency to receive (no offset). */ rf::Frequency target_frequency() const; void set_target_frequency(rf::Frequency f); + void set_target_frequency_with_hidden_offset(rf::Frequency f, rf::Frequency offset); uint32_t baseband_bandwidth() const; void set_baseband_bandwidth(uint32_t v); diff --git a/firmware/application/ui/ui_freq_field.hpp b/firmware/application/ui/ui_freq_field.hpp index d604cf1da..57b8d8c29 100644 --- a/firmware/application/ui/ui_freq_field.hpp +++ b/firmware/application/ui/ui_freq_field.hpp @@ -39,6 +39,8 @@ class BoundFrequencyField : public FrequencyField { public: decltype(FrequencyField::on_change) updated{}; + std::function changing{}; + std::function entered{}; BoundFrequencyField(Point parent_pos, NavigationView& nav) : FrequencyField(parent_pos) { @@ -47,7 +49,8 @@ class BoundFrequencyField : public FrequencyField { set_value(model->target_frequency()); on_change = [this](rf::Frequency f) { - model->set_target_frequency(f); + if (!changing || !changing(f)) + model->set_target_frequency(f); if (updated) updated(f); }; @@ -57,7 +60,10 @@ class BoundFrequencyField : public FrequencyField { on_edit_shown(); auto freq_view = nav.push(model->target_frequency()); freq_view->on_changed = [this](rf::Frequency f) { - set_value(f); + if (entered) + entered(f); + else + set_value(f); }; nav.set_on_pop([this]() { if (on_edit_hidden) @@ -76,4 +82,4 @@ using TxFrequencyField = BoundFrequencyFieldbg_darkest->background); + draw_filter_ranges(painter, r); + restore_tick_lines(painter, r, dirty); +} + +void FrequencyScale::restore_tick_lines( + Painter& painter, + const Rect r, + const Rect dirty) { + const auto draw_if_dirty = [&](const Coord x) { + if (x >= dirty.left() && x < dirty.right()) { + painter.fill_rectangle( + {x, dirty.top(), 1, dirty.height()}, + Theme::getInstance()->bg_darkest->foreground); + } + }; + + const auto x_center = r.left() + r.width() / 2; + draw_if_dirty(x_center); + + constexpr int tick_count_max = 4; + float rough_tick_interval = float(spectrum_sampling_rate) / tick_count_max; + int magnitude = 1; + while (rough_tick_interval >= 10.0f) { + rough_tick_interval /= 10; + magnitude *= 10; + } + + const int tick_interval = std::ceil(rough_tick_interval); + auto tick_offset = tick_interval; + while ((tick_offset * magnitude) < spectrum_sampling_rate / 2) { + const Dim pixel_offset = + tick_offset * magnitude * spectrum_bins / spectrum_sampling_rate; + draw_if_dirty(x_center - pixel_offset); + draw_if_dirty(x_center + pixel_offset); + tick_offset += tick_interval; + } +} + void FrequencyScale::draw_filter_ranges(Painter& painter, const Rect r) { if (channel_filter_low_frequency != channel_filter_high_frequency) { - const auto x_center = r.width() / 2; + const auto x_center = r.width() / 2 + + channel_filter_offset * spectrum_bins / spectrum_sampling_rate; const auto x_low = x_center + channel_filter_low_frequency * spectrum_bins / spectrum_sampling_rate; const auto x_high = x_center + channel_filter_high_frequency * spectrum_bins / spectrum_sampling_rate; @@ -220,6 +305,11 @@ void FrequencyScale::on_blur() { } bool FrequencyScale::on_encoder(const EncoderEvent delta) { + if (live_tuning) { + if (on_select) on_select(delta); + return true; + } + cursor_position += delta; cursor_position = std::min(cursor_position, screen_width / 2 - 1); @@ -326,9 +416,11 @@ WaterfallView::WaterfallView(const bool cursor) { frequency_scale.focus(); // focus on frequency scale to show cursor if (sampling_rate) { - // screen x to frequency scale x, NB we need two widgets align - int32_t cursor_position = x - (screen_width / 2); - frequency_scale.set_cursor_position(cursor_position); + const int32_t cursor_position = x - (screen_width / 2); + if (!frequency_scale.is_live_tuning()) { + // screen x to frequency scale x, NB we need two widgets align + frequency_scale.set_cursor_position(cursor_position); + } } }; @@ -405,6 +497,7 @@ void WaterfallView::on_channel_spectrum(const ChannelSpectrum& spectrum) { sampling_rate = spectrum.sampling_rate; frequency_scale.set_spectrum_sampling_rate(sampling_rate); frequency_scale.set_channel_filter( + spectrum.channel_filter_offset, spectrum.channel_filter_low_frequency, spectrum.channel_filter_high_frequency, spectrum.channel_filter_transition); diff --git a/firmware/application/ui/ui_spectrum.hpp b/firmware/application/ui/ui_spectrum.hpp index d2d8ce868..b201930d0 100644 --- a/firmware/application/ui/ui_spectrum.hpp +++ b/firmware/application/ui/ui_spectrum.hpp @@ -83,8 +83,10 @@ class FrequencyScale : public Widget { bool on_touch(const TouchEvent touch) override; void set_spectrum_sampling_rate(const int new_sampling_rate); - void set_channel_filter(const int low_frequency, const int high_frequency, const int transition); + void set_channel_filter(const int offset, const int low_frequency, const int high_frequency, const int transition); void set_cursor_position(const int32_t position); + void set_live_tuning(const bool enabled) { live_tuning = enabled; } + bool is_live_tuning() const { return live_tuning; } void paint(Painter& painter) override; @@ -94,15 +96,19 @@ class FrequencyScale : public Widget { int32_t cursor_position{0}; int spectrum_sampling_rate{0}; const int spectrum_bins = std::tuple_size::value; + int channel_filter_offset{0}; int channel_filter_low_frequency{0}; int channel_filter_high_frequency{0}; int channel_filter_transition{0}; + bool live_tuning{false}; void clear(); void clear_background(Painter& painter, const Rect r); void draw_frequency_ticks(Painter& painter, const Rect r); void draw_filter_ranges(Painter& painter, const Rect r); + void redraw_filter_cursor(const int old_offset); + void restore_tick_lines(Painter& painter, const Rect r, const Rect dirty); }; /* NB: These visualizations rely on having a baseband image running. @@ -147,6 +153,7 @@ class WaterfallView : public View { void set_parent_rect(const Rect new_parent_rect) override; void show_audio_spectrum_view(const bool show); void load_gradient(); + void set_live_tuning(const bool enabled) { frequency_scale.set_live_tuning(enabled); } private: void update_widgets_rect(); diff --git a/firmware/baseband/dsp_decimate.cpp b/firmware/baseband/dsp_decimate.cpp index 7b0af12ab..e7aef4f4a 100644 --- a/firmware/baseband/dsp_decimate.cpp +++ b/firmware/baseband/dsp_decimate.cpp @@ -337,6 +337,66 @@ buffer_c16_t FIRC16xR16x16Decim2::execute( src.sampling_rate / decimation_factor}; } +// FIRC16xR16x63HalfbandDecim2 //////////////////////////////////////////// + +void FIRC16xR16x63HalfbandDecim2::configure( + const std::array& taps) { + std::copy(taps.cbegin(), taps.cend(), taps_.begin()); + reset(); +} + +void FIRC16xR16x63HalfbandDecim2::reset() { + samples_.fill({}); + samples_head_ = 0; +} + +buffer_c16_t FIRC16xR16x63HalfbandDecim2::execute( + const buffer_c16_t& src, + const buffer_c16_t& dst) { + auto* dst_p = reinterpret_cast(dst.p); + + for (size_t output = 0; output < src.count / decimation_factor; ++output) { + for (size_t i = 0; i < decimation_factor; ++i) { + const auto sample = src.p[output * decimation_factor + i]; + samples_[samples_head_] = sample; + samples_[samples_head_ + taps_count] = sample; + if (++samples_head_ == taps_count) + samples_head_ = 0; + } + + int64_t real = 0; + int64_t imag = 0; + for (size_t tap = 0; tap < taps_count; tap += 4) { + const auto sample_0 = + *reinterpret_cast(&samples_[samples_head_ + tap]); + const auto sample_1 = + *reinterpret_cast(&samples_[samples_head_ + tap + 2]); + const auto real_pair = __PKHBT(sample_0, sample_1, 16); + const auto imag_pair = __PKHTB(sample_1, sample_0, 16); + const auto taps_pair = uint32_t(uint16_t(taps_[tap])) | + (uint32_t(uint16_t(taps_[tap + 2])) << 16); + real = __SMLALD(real_pair, taps_pair, real); + imag = __SMLALD(imag_pair, taps_pair, imag); + } + + const auto center = + *reinterpret_cast(&samples_[samples_head_ + taps_count / 2]); + real += int16_t(center) * taps_[taps_count / 2]; + imag += int16_t(center >> 16) * taps_[taps_count / 2]; + + const auto real_s16 = + __SSAT((real + (real >= 0 ? 32768 : -32768)) / 65536, 16); + const auto imag_s16 = + __SSAT((imag + (imag >= 0 ? 32768 : -32768)) / 65536, 16); + *(dst_p++) = __PKHBT(real_s16, imag_s16, 16); + } + + return { + dst.p, + src.count / decimation_factor, + src.sampling_rate / decimation_factor}; +} + // FIRC16xR16x32Decim8 //////////////////////////////////////////////////// void FIRC16xR16x32Decim8::configure( @@ -625,10 +685,13 @@ buffer_s16_t FIR64AndDecimateBy2Real::execute( void FIRAndDecimateComplex::configure_common( const size_t taps_count, const size_t decimation_factor) { - samples_ = std::make_unique(taps_count); + /* Mirror the delay line so a convolution always sees one contiguous + * taps_count window, even when the logical head wraps. */ + samples_ = std::make_unique(taps_count * 2); taps_reversed_ = std::make_unique(taps_count); taps_count_ = taps_count; decimation_factor_ = decimation_factor; + samples_head_ = 0; } buffer_c16_t FIRAndDecimateComplex::execute( @@ -647,15 +710,18 @@ buffer_c16_t FIRAndDecimateComplex::execute( const void* src_p = src.p; size_t outer_count = output_samples; while (outer_count > 0) { - /* Put new samples into delay buffer */ - void* z_new_p = &samples_[taps_count_ - decimation_factor_]; + /* Put new samples into both halves of the mirrored ring. */ for (size_t i = 0; i < decimation_factor_; i++) { - *__SIMD32(z_new_p)++ = *__SIMD32(src_p)++; + const uint32_t sample = *__SIMD32(src_p)++; + *reinterpret_cast(&samples_[samples_head_]) = sample; + *reinterpret_cast(&samples_[samples_head_ + taps_count_]) = sample; + if (++samples_head_ == taps_count_) + samples_head_ = 0; } size_t loop_count = taps_count_ / 8; void* t_p = &taps_reversed_[0]; - void* z_p = &samples_[0]; + void* z_p = &samples_[samples_head_]; int64_t t_real = 0; int64_t t_imag = 0; @@ -712,27 +778,6 @@ buffer_c16_t FIRAndDecimateComplex::execute( i_sat, 16); - /* Shift sample buffer left/down by decimation factor. */ - const size_t unroll_factor = 4; - size_t shift_count = (taps_count_ - decimation_factor_) / unroll_factor; - - void* t = &samples_[0]; - const void* s = &samples_[decimation_factor_]; - - while (shift_count > 0) { - *__SIMD32(t)++ = *__SIMD32(s)++; - *__SIMD32(t)++ = *__SIMD32(s)++; - *__SIMD32(t)++ = *__SIMD32(s)++; - *__SIMD32(t)++ = *__SIMD32(s)++; - shift_count--; - } - - shift_count = (taps_count_ - decimation_factor_) % unroll_factor; - while (shift_count > 0) { - *__SIMD32(t)++ = *__SIMD32(s)++; - shift_count--; - } - outer_count--; } diff --git a/firmware/baseband/dsp_decimate.hpp b/firmware/baseband/dsp_decimate.hpp index a5f8a9cd2..8e9bc4283 100644 --- a/firmware/baseband/dsp_decimate.hpp +++ b/firmware/baseband/dsp_decimate.hpp @@ -170,6 +170,24 @@ class FIRC16xR16x16Decim2 { int32_t output_scale = 0; }; +class FIRC16xR16x63HalfbandDecim2 { + public: + static constexpr size_t taps_count = 63; + static constexpr size_t decimation_factor = 2; + + void configure(const std::array& taps); + void reset(); + + buffer_c16_t execute( + const buffer_c16_t& src, + const buffer_c16_t& dst); + + private: + alignas(4) std::array samples_{}; + alignas(4) std::array taps_{}; + size_t samples_head_{0}; +}; + class FIRC16xR16x32Decim8 { public: static constexpr size_t taps_count = 32; @@ -210,6 +228,23 @@ class FIRAndDecimateComplex { configure(taps.data(), taps.size(), decimation_factor); } + template + void configure( + const std::array& taps, + const size_t decimation_factor) { + configure_common(N, decimation_factor); + for (size_t i = 0; i < N; ++i) { + taps_reversed_[i] = {taps[N - 1 - i], 0}; + } + } + + template + void set_taps(const std::array& taps) { + if (N == taps_count_) { + std::reverse_copy(taps.begin(), taps.end(), &taps_reversed_[0]); + } + } + buffer_c16_t execute( const buffer_c16_t& src, const buffer_c16_t& dst); @@ -221,6 +256,7 @@ class FIRAndDecimateComplex { std::unique_ptr taps_reversed_{}; size_t taps_count_{0}; size_t decimation_factor_{1}; + size_t samples_head_{0}; template void configure( diff --git a/firmware/baseband/dsp_frequency_xlator.hpp b/firmware/baseband/dsp_frequency_xlator.hpp new file mode 100644 index 000000000..7265f5119 --- /dev/null +++ b/firmware/baseband/dsp_frequency_xlator.hpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2026 + * + * This file is part of PortaPack. + */ + +#ifndef __DSP_FREQUENCY_XLATOR_H__ +#define __DSP_FREQUENCY_XLATOR_H__ + +#include "dsp_decimate.hpp" +#include "dsp_types.hpp" + +#include +#include + +namespace dsp { + +/* Fixed-point complex mixer for the Audio RX channelizer. */ +class FrequencyTranslator { + public: + FrequencyTranslator() { + for (size_t i = 0; i < oscillator_q15_.size(); ++i) { + oscillator_q15_[i] = + static_cast(sine_q15_[static_cast(i + 64)]) | + (static_cast( + static_cast(sine_q15_[i])) + << 16); + } + } + + void set_sample_rate(const uint32_t sampling_rate) { + sampling_rate_ = sampling_rate; + update_phase_increment(); + } + + void set_frequency(const int32_t frequency) { + frequency_ = frequency; + update_phase_increment(); + } + + buffer_c16_t execute(const buffer_c16_t& src, const buffer_c16_t& dst) { + auto phase = phase_; + for (size_t i = 0; i < src.count; ++i) { + const uint8_t index = phase >> 24; + const uint8_t next = index + 1; + const int32_t fraction = (phase >> 16) & 0xff; + const uint32_t oscillator_first = oscillator_q15_[index]; + const uint32_t oscillator_next = oscillator_q15_[next]; + const int32_t sine_first = + static_cast(oscillator_first >> 16); + const int32_t cosine_first = + static_cast(oscillator_first); + const int32_t sine = + sine_first + + (((static_cast(oscillator_next >> 16) - + sine_first) * + fraction) >> + 8); + const int32_t cosine = + cosine_first + + (((static_cast(oscillator_next) - + cosine_first) * + fraction) >> + 8); + const uint32_t oscillator = + static_cast(cosine) | + (static_cast( + static_cast(sine)) + << 16); + const uint32_t sample = + *reinterpret_cast(&src.p[i]); + + /* Two packed dual-16-bit multiplies implement + * (I+jQ) * (cos-j sin). */ + const int32_t out_i = + rounded_shift(__SMUAD(sample, oscillator), 15); + const int32_t out_q = + rounded_shift(__SMUSDX(oscillator, sample), 15); + *reinterpret_cast(&dst.p[i]) = + __PKHBT(__SSAT(out_i, 16), __SSAT(out_q, 16), 16); + phase += phase_increment_; + } + phase_ = phase; + return {dst.p, src.count, src.sampling_rate}; + } + + private: + friend class FrequencyTranslatingDecimator32By8; + + static constexpr int32_t rounded_shift( + const int32_t value, + const uint32_t bits) { + const int32_t rounding = int32_t{1} << (bits - 1); + return value >= 0 + ? (value + rounding) >> bits + : -((-value + rounding) >> bits); + } + + void update_phase_increment() { + if (sampling_rate_) { + phase_increment_ = static_cast( + (static_cast(frequency_) * (int64_t{1} << 32)) / + sampling_rate_); + } + } + + static constexpr std::array sine_q15_{{ + 0, 804, 1608, 2410, 3212, 4011, 4808, 5602, 6393, 7179, 7962, 8739, 9512, 10278, 11039, 11793, + 12539, 13279, 14010, 14732, 15446, 16151, 16846, 17530, 18204, 18868, 19519, 20159, 20787, 21403, 22005, 22594, + 23170, 23731, 24279, 24811, 25329, 25832, 26319, 26790, 27245, 27683, 28105, 28510, 28898, 29268, 29621, 29956, + 30273, 30571, 30852, 31113, 31356, 31580, 31785, 31971, 32137, 32285, 32412, 32521, 32609, 32678, 32728, 32757, + 32767, 32757, 32728, 32678, 32609, 32521, 32412, 32285, 32137, 31971, 31785, 31580, 31356, 31113, 30852, 30571, + 30273, 29956, 29621, 29268, 28898, 28510, 28105, 27683, 27245, 26790, 26319, 25832, 25329, 24811, 24279, 23731, + 23170, 22594, 22005, 21403, 20787, 20159, 19519, 18868, 18204, 17530, 16846, 16151, 15446, 14732, 14010, 13279, + 12539, 11793, 11039, 10278, 9512, 8739, 7962, 7179, 6393, 5602, 4808, 4011, 3212, 2410, 1608, 804, + 0, -804, -1608, -2410, -3212, -4011, -4808, -5602, -6393, -7179, -7962, -8739, -9512, -10278, -11039, -11793, + -12539, -13279, -14010, -14732, -15446, -16151, -16846, -17530, -18204, -18868, -19519, -20159, -20787, -21403, -22005, -22594, + -23170, -23731, -24279, -24811, -25329, -25832, -26319, -26790, -27245, -27683, -28105, -28510, -28898, -29268, -29621, -29956, + -30273, -30571, -30852, -31113, -31356, -31580, -31785, -31971, -32137, -32285, -32412, -32521, -32609, -32678, -32728, -32757, + -32767, -32757, -32728, -32678, -32609, -32521, -32412, -32285, -32137, -31971, -31785, -31580, -31356, -31113, -30852, -30571, + -30273, -29956, -29621, -29268, -28898, -28510, -28105, -27683, -27245, -26790, -26319, -25832, -25329, -24811, -24279, -23731, + -23170, -22594, -22005, -21403, -20787, -20159, -19519, -18868, -18204, -17530, -16846, -16151, -15446, -14732, -14010, -13279, + -12539, -11793, -11039, -10278, -9512, -8739, -7962, -7179, -6393, -5602, -4808, -4011, -3212, -2410, -1608, -804, + }}; + + std::array oscillator_q15_{}; + uint32_t phase_{0}; + uint32_t phase_increment_{0}; + uint32_t sampling_rate_{192000}; + int32_t frequency_{0}; +}; + +/* + * Frequency-translating 32-tap FIR decimator. Frequency translation is + * split between coefficients modulated when tuning changes and a cheap + * output-rate phase rotation. This avoids running an NCO at the 384kHz + * input rate. + */ +class FrequencyTranslatingDecimator32By8 { + public: + static constexpr size_t decimation_factor = 8; + static constexpr size_t taps_count = 32; + + void configure( + const std::array& taps, + const uint32_t input_sampling_rate) { + taps_ = taps; + input_sampling_rate_ = input_sampling_rate; + decimator_.configure(complex_taps_, decimation_factor); + output_xlator_.set_sample_rate(input_sampling_rate / decimation_factor); + update_taps(); + } + + void set_frequency(const int32_t frequency) { + frequency_ = frequency; + output_xlator_.set_frequency(frequency); + update_taps(); + } + + buffer_c16_t execute( + const buffer_c16_t& src, + const buffer_c16_t& dst) { + const auto filtered = decimator_.execute(src, dst); + return output_xlator_.execute(filtered, dst); + } + + private: + static void oscillator( + const uint32_t phase, + int32_t& sine, + int32_t& cosine) { + const uint8_t index = phase >> 24; + const uint8_t next = index + 1; + const uint8_t cosine_index = index + 64; + const uint8_t cosine_next = cosine_index + 1; + const int32_t fraction = (phase >> 16) & 0xff; + const int32_t sine_first = FrequencyTranslator::sine_q15_[index]; + const int32_t cosine_first = FrequencyTranslator::sine_q15_[cosine_index]; + sine = sine_first + + (((FrequencyTranslator::sine_q15_[next] - sine_first) * fraction) >> 8); + cosine = cosine_first + + (((FrequencyTranslator::sine_q15_[cosine_next] - cosine_first) * fraction) >> 8); + } + + void update_taps() { + if (!input_sampling_rate_) + return; + + const uint32_t tap_phase_increment = static_cast( + (static_cast(frequency_) * (int64_t{1} << 32)) / + input_sampling_rate_); + + /* Centre the modulation on the FIR midpoint. Besides changing + * only a constant output phase, this makes the two coefficients + * in each symmetric pair complex conjugates. That property is + * important after quantization: starting at tap zero accumulated + * a one-sided phase and rounding error across the whole filter. */ + uint32_t phase = static_cast( + -((static_cast( + static_cast(tap_phase_increment)) * + static_cast(taps_count - 1)) / + 2)); + for (size_t i = 0; i < taps_count; ++i) { + int32_t sine; + int32_t cosine; + oscillator(phase, sine, cosine); + /* FIRAndDecimateComplex uses Q16 coefficients; the source + * real-tap filters use Q15 coefficients. */ + const int32_t tap = taps_[i]; + complex_taps_[i] = { + static_cast( + FrequencyTranslator::rounded_shift( + tap * cosine, 14)), + static_cast( + FrequencyTranslator::rounded_shift( + tap * sine, 14))}; + phase += tap_phase_increment; + } + decimator_.set_taps(complex_taps_); + } + + std::array taps_{}; + std::array complex_taps_{}; + decimate::FIRAndDecimateComplex decimator_{}; + FrequencyTranslator output_xlator_{}; + uint32_t input_sampling_rate_{0}; + int32_t frequency_{0}; +}; + +} /* namespace dsp */ + +#endif /*__DSP_FREQUENCY_XLATOR_H__*/ diff --git a/firmware/baseband/proc_am_audio.cpp b/firmware/baseband/proc_am_audio.cpp index a4bb87487..a53d6b24e 100644 --- a/firmware/baseband/proc_am_audio.cpp +++ b/firmware/baseband/proc_am_audio.cpp @@ -45,10 +45,26 @@ void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) { } const auto decim_0_out = decim_0.execute(buffer, dst_buffer); - const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); + const auto audio_decim_0_out = audio_decim_0.execute(decim_0_out, dst_buffer); - channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition); + spectrum_samples += decim_0_out.count; + if (!spectrum_capture_active && + spectrum_samples >= spectrum_interval_samples) { + spectrum_samples -= spectrum_interval_samples; + channel_spectrum.start_filtered_capture(spectrum_zoom_x2 ? 4 : 2); + spectrum_capture_active = true; + } + if (spectrum_capture_active && + channel_spectrum.feed_filtered( + audio_decim_0_out, + channel_filter_low_f, + channel_filter_high_f, + channel_filter_transition)) { + spectrum_capture_active = false; + } + + const auto decim_1_out = translating_decim_1.execute(audio_decim_0_out, dst_buffer); const auto decim_2_out = decim_2.execute(decim_1_out, dst_buffer); const auto channel_out = channel_filter.execute(decim_2_out, dst_buffer); @@ -97,6 +113,10 @@ void NarrowbandAMAudio::on_message(const Message* const message) { capture_config(*reinterpret_cast(message)); break; + case Message::ID::AudioDDCConfig: + ddc_config(*reinterpret_cast(message)); + break; + default: break; } @@ -106,17 +126,19 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) { constexpr size_t decim_0_input_fs = baseband_fs; constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor; - constexpr size_t decim_1_input_fs = decim_0_output_fs; - constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor; - + constexpr size_t audio_decim_0_output_fs = decim_0_output_fs / 2; + constexpr size_t decim_1_output_fs = + audio_decim_0_output_fs / translating_decim_1.decimation_factor; constexpr size_t decim_2_input_fs = decim_1_output_fs; constexpr size_t decim_2_output_fs = decim_2_input_fs / decim_2_decimation_factor; constexpr size_t channel_filter_input_fs = decim_2_output_fs; // const size_t channel_filter_output_fs = channel_filter_input_fs / channel_filter_decimation_factor; - decim_0.configure(message.decim_0_filter.taps); - decim_1.configure(message.decim_1_filter.taps); + decim_0.configure(message.decim_0_filter.taps, 33554432); + audio_decim_0.configure(taps_audio_wide_halfband_0.taps); + translating_decim_1.configure( + message.decim_1_filter.taps, audio_decim_0_output_fs); decim_2.configure(message.decim_2_filter.taps, decim_2_decimation_factor); channel_filter.configure(message.channel_filter.taps, channel_filter_decimation_factor); channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs; @@ -124,12 +146,20 @@ 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} - channel_spectrum.set_decimation_factor(message.channel_spectrum_decimation_factor); + spectrum_zoom_x2 = message.channel_spectrum_decimation_factor == 2; + channel_spectrum.set_decimation_factor(1); + 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). configured = true; } +void NarrowbandAMAudio::ddc_config(const AudioDDCConfigMessage& message) { + translating_decim_1.set_frequency(message.frequency); + channel_spectrum.set_channel_filter_offset(message.frequency); +} + void NarrowbandAMAudio::capture_config(const CaptureConfigMessage& message) { if (message.config) { audio_output.set_stream(std::make_unique(message.config)); diff --git a/firmware/baseband/proc_am_audio.hpp b/firmware/baseband/proc_am_audio.hpp index 296094889..4cd60528d 100644 --- a/firmware/baseband/proc_am_audio.hpp +++ b/firmware/baseband/proc_am_audio.hpp @@ -28,6 +28,7 @@ #include "dsp_decimate.hpp" #include "dsp_demodulate.hpp" +#include "dsp_frequency_xlator.hpp" #include "audio_compressor.hpp" #include "audio_output.hpp" @@ -44,6 +45,7 @@ class NarrowbandAMAudio : public BasebandProcessor { private: static constexpr size_t baseband_fs = 3072000; + static constexpr auto spectrum_rate_hz = 30.0f; static constexpr size_t decim_2_decimation_factor = 4; static constexpr size_t channel_filter_decimation_factor = 1; @@ -56,14 +58,19 @@ class NarrowbandAMAudio : public BasebandProcessor { audio.data(), audio.size()}; - dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; - dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{}; + dsp::decimate::FIRC16xR16x16Decim2 audio_decim_0{}; + dsp::FrequencyTranslatingDecimator32By8 translating_decim_1{}; dsp::decimate::FIRAndDecimateComplex decim_2{}; dsp::decimate::FIRAndDecimateComplex channel_filter{}; int32_t channel_filter_low_f = 0; int32_t channel_filter_high_f = 0; int32_t channel_filter_transition = 0; bool configured{false}; + size_t spectrum_interval_samples{0}; + size_t spectrum_samples{0}; + bool spectrum_capture_active{false}; + bool spectrum_zoom_x2{false}; // 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} @@ -86,6 +93,7 @@ class NarrowbandAMAudio : public BasebandProcessor { #endif void configure(const AMConfigureMessage& message); + void ddc_config(const AudioDDCConfigMessage& message); void capture_config(const CaptureConfigMessage& message); buffer_f32_t demodulate(const buffer_c16_t& channel); diff --git a/firmware/baseband/proc_nfm_audio.cpp b/firmware/baseband/proc_nfm_audio.cpp index 72a156599..688a2b5ce 100644 --- a/firmware/baseband/proc_nfm_audio.cpp +++ b/firmware/baseband/proc_nfm_audio.cpp @@ -51,10 +51,26 @@ void NarrowbandFMAudio::execute(const buffer_c8_t& buffer) { } const auto decim_0_out = decim_0.execute(buffer, dst_buffer); - const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); + const auto audio_decim_0_out = audio_decim_0.execute(decim_0_out, dst_buffer); - channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition); + spectrum_samples += decim_0_out.count; + if (!spectrum_capture_active && + spectrum_samples >= spectrum_interval_samples) { + spectrum_samples -= spectrum_interval_samples; + channel_spectrum.start_filtered_capture(2); + spectrum_capture_active = true; + } + if (spectrum_capture_active && + channel_spectrum.feed_filtered( + audio_decim_0_out, + channel_filter_low_f, + channel_filter_high_f, + channel_filter_transition)) { + spectrum_capture_active = false; + } + + const auto decim_1_out = translating_decim_1.execute(audio_decim_0_out, dst_buffer); const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); feed_channel_stats(channel_out); @@ -141,6 +157,10 @@ void NarrowbandFMAudio::on_message(const Message* const message) { pitch_rssi_config(*reinterpret_cast(message)); break; + case Message::ID::AudioDDCConfig: + ddc_config(*reinterpret_cast(message)); + break; + default: break; } @@ -150,22 +170,26 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) { constexpr size_t decim_0_input_fs = baseband_fs; constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor; - constexpr size_t decim_1_input_fs = decim_0_output_fs; - constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor; - + constexpr size_t audio_decim_0_output_fs = decim_0_output_fs / 2; + constexpr size_t decim_1_output_fs = + audio_decim_0_output_fs / translating_decim_1.decimation_factor; constexpr size_t channel_filter_input_fs = decim_1_output_fs; const size_t channel_filter_output_fs = channel_filter_input_fs / message.channel_decimation; const size_t demod_input_fs = channel_filter_output_fs; - decim_0.configure(message.decim_0_filter.taps); - decim_1.configure(message.decim_1_filter.taps); + decim_0.configure(message.decim_0_filter.taps, 33554432); + audio_decim_0.configure(taps_audio_wide_halfband_0.taps); + translating_decim_1.configure( + message.decim_1_filter.taps, audio_decim_0_output_fs); channel_filter.configure(message.channel_filter.taps, message.channel_decimation); demod.configure(demod_input_fs, message.deviation); channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs; channel_filter_high_f = message.channel_filter.high_frequency_normalized * channel_filter_input_fs; channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs; - channel_spectrum.set_decimation_factor(1.0f); + channel_spectrum.set_decimation_factor(1); + spectrum_interval_samples = + decim_0_output_fs / spectrum_rate_hz; audio_output.configure(message.audio_hpf_config, message.audio_deemph_config, (float)message.squelch_level / 100.0); hpf.configure(audio_24k_hpf_30hz_config); @@ -174,6 +198,11 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) { configured = true; } +void NarrowbandFMAudio::ddc_config(const AudioDDCConfigMessage& message) { + translating_decim_1.set_frequency(message.frequency); + channel_spectrum.set_channel_filter_offset(message.frequency); +} + void NarrowbandFMAudio::pitch_rssi_config(const PitchRSSIConfigureMessage& message) { pitch_rssi_enabled = message.enabled; tone_delta = (message.rssi + 1000) * ((1ULL << 32) / 24000); diff --git a/firmware/baseband/proc_nfm_audio.hpp b/firmware/baseband/proc_nfm_audio.hpp index a66975503..52b78ae8e 100644 --- a/firmware/baseband/proc_nfm_audio.hpp +++ b/firmware/baseband/proc_nfm_audio.hpp @@ -29,6 +29,7 @@ #include "dsp_decimate.hpp" #include "dsp_demodulate.hpp" +#include "dsp_frequency_xlator.hpp" #include "dsp_iir.hpp" #include "audio_output.hpp" @@ -50,6 +51,7 @@ class NarrowbandFMAudio : public BasebandProcessor { private: static constexpr size_t baseband_fs = 3072000; + static constexpr auto spectrum_rate_hz = 30.0f; std::array dst{}; const buffer_c16_t dst_buffer{ @@ -69,8 +71,9 @@ class NarrowbandFMAudio : public BasebandProcessor { (int16_t*)tone.data(), sizeof(tone) / sizeof(int16_t)}; - dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; - dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{}; + dsp::decimate::FIRC16xR16x16Decim2 audio_decim_0{}; + dsp::FrequencyTranslatingDecimator32By8 translating_decim_1{}; dsp::decimate::FIRAndDecimateComplex channel_filter{}; int32_t channel_filter_low_f = 0; int32_t channel_filter_high_f = 0; @@ -85,6 +88,9 @@ class NarrowbandFMAudio : public BasebandProcessor { AudioOutput audio_output{}; SpectrumCollector channel_spectrum{}; + size_t spectrum_interval_samples{0}; + size_t spectrum_samples{0}; + bool spectrum_capture_active{false}; uint32_t tone_phase{0}; uint32_t tone_delta{0}; @@ -113,6 +119,7 @@ class NarrowbandFMAudio : public BasebandProcessor { void pitch_rssi_config(const PitchRSSIConfigureMessage& message); void configure(const NBFMConfigureMessage& message); void capture_config(const CaptureConfigMessage& message); + void ddc_config(const AudioDDCConfigMessage& message); }; #endif /*__PROC_NFM_AUDIO_H__*/ diff --git a/firmware/baseband/spectrum_collector.cpp b/firmware/baseband/spectrum_collector.cpp index e8bb5311a..4e116bd04 100644 --- a/firmware/baseband/spectrum_collector.cpp +++ b/firmware/baseband/spectrum_collector.cpp @@ -22,6 +22,7 @@ #include "spectrum_collector.hpp" #include "dsp_fft.hpp" +#include "dsp_fir_taps.hpp" #include "utility.hpp" #include "event_m4.hpp" @@ -73,7 +74,7 @@ void SpectrumCollector::set_decimation_factor( * perform the deferred task on the buffer of data we prepared. */ -void SpectrumCollector::feed( +bool SpectrumCollector::feed( const buffer_c16_t& channel, const int32_t filter_low_frequency, const int32_t filter_high_frequency, @@ -83,11 +84,50 @@ void SpectrumCollector::feed( channel_filter_high_frequency = filter_high_frequency; channel_filter_transition = filter_transition; + bool block_completed = false; channel_spectrum_decimator.feed( channel, - [this](const buffer_c16_t& data) { + [this, &block_completed](const buffer_c16_t& data) { this->post_message(data); + block_completed = true; }); + return block_completed; +} + +void SpectrumCollector::start_filtered_capture( + const size_t decimation_factor) { + filtered_capture_decimation_ = decimation_factor; + filtered_capture_count_ = 0; + filtered_capture_ready_ = false; +} + +bool SpectrumCollector::feed_filtered( + const buffer_c16_t& channel, + const int32_t filter_low_frequency, + const int32_t filter_high_frequency, + const int32_t filter_transition) { + channel_filter_low_frequency = filter_low_frequency; + channel_filter_high_frequency = filter_high_frequency; + channel_filter_transition = filter_transition; + + const size_t required_samples = 256 * filtered_capture_decimation_; + const size_t copy_count = std::min( + channel.count, required_samples - filtered_capture_count_); + std::copy_n( + channel.p, + copy_count, + filtered_capture_.begin() + filtered_capture_count_); + filtered_capture_count_ += copy_count; + + if (filtered_capture_count_ == required_samples) { + channel_spectrum_sampling_rate = + channel.sampling_rate / filtered_capture_decimation_; + filtered_capture_ready_ = true; + channel_spectrum_request_update = true; + EventDispatcher::events_flag(EVT_MASK_SPECTRUM); + return true; + } + return false; } void SpectrumCollector::post_message(const buffer_c16_t& data) { @@ -131,11 +171,37 @@ static typename T::value_type spectrum_window_blackman_3(const T& s, const size_ void SpectrumCollector::update() { // Called from idle thread (after EVT_MASK_SPECTRUM is flagged) if (streaming && channel_spectrum_request_update) { + if (filtered_capture_ready_) { + filtered_decim_0_.configure(taps_audio_spectrum_halfband.taps); + const buffer_c16_t capture{ + filtered_capture_.data(), + 256 * filtered_capture_decimation_, + channel_spectrum_sampling_rate * filtered_capture_decimation_}; + const buffer_c16_t stage_0{ + filtered_stage_0_.data(), + filtered_stage_0_.size()}; + const auto filtered = filtered_decim_0_.execute(capture, stage_0); + + if (filtered_capture_decimation_ == 4) { + filtered_decim_1_.configure(taps_audio_spectrum_halfband.taps); + const buffer_c16_t stage_1{ + filtered_stage_1_.data(), + filtered_stage_1_.size()}; + const auto zoom_filtered = + filtered_decim_1_.execute(filtered, stage_1); + fft_swap(zoom_filtered, channel_spectrum); + } else { + fft_swap(filtered, channel_spectrum); + } + filtered_capture_ready_ = false; + } + /* Decimated buffer is full. Compute spectrum. */ fft_c_preswapped(channel_spectrum, 0, 8); ChannelSpectrum spectrum; spectrum.sampling_rate = channel_spectrum_sampling_rate; + spectrum.channel_filter_offset = channel_filter_offset; spectrum.channel_filter_low_frequency = channel_filter_low_frequency; spectrum.channel_filter_high_frequency = channel_filter_high_frequency; spectrum.channel_filter_transition = channel_filter_transition; diff --git a/firmware/baseband/spectrum_collector.hpp b/firmware/baseband/spectrum_collector.hpp index 15d193ba7..78c173956 100644 --- a/firmware/baseband/spectrum_collector.hpp +++ b/firmware/baseband/spectrum_collector.hpp @@ -29,6 +29,7 @@ #include "complex.hpp" #include "block_decimator.hpp" +#include "dsp_decimate.hpp" #include #include @@ -40,8 +41,18 @@ class SpectrumCollector { void on_message(const Message* const message); void set_decimation_factor(const size_t decimation_factor); + void set_channel_filter_offset(const int32_t offset) { + channel_filter_offset = offset; + } - void feed( + bool feed( + const buffer_c16_t& channel, + const int32_t filter_low_frequency, + const int32_t filter_high_frequency, + const int32_t filter_transition); + + void start_filtered_capture(const size_t decimation_factor); + bool feed_filtered( const buffer_c16_t& channel, const int32_t filter_low_frequency, const int32_t filter_high_frequency, @@ -55,10 +66,19 @@ class SpectrumCollector { volatile bool channel_spectrum_request_update{false}; bool streaming{false}; std::array, 256> channel_spectrum{}; + std::array filtered_capture_{}; + std::array filtered_stage_0_{}; + std::array filtered_stage_1_{}; + dsp::decimate::FIRC16xR16x63HalfbandDecim2 filtered_decim_0_{}; + dsp::decimate::FIRC16xR16x63HalfbandDecim2 filtered_decim_1_{}; + size_t filtered_capture_count_{0}; + size_t filtered_capture_decimation_{1}; + bool filtered_capture_ready_{false}; uint32_t channel_spectrum_sampling_rate{0}; int32_t channel_filter_low_frequency{0}; int32_t channel_filter_high_frequency{0}; int32_t channel_filter_transition{0}; + int32_t channel_filter_offset{0}; void post_message(const buffer_c16_t& data); diff --git a/firmware/common/dsp_fir_taps.hpp b/firmware/common/dsp_fir_taps.hpp index 1f32898f3..7fc346b57 100644 --- a/firmware/common/dsp_fir_taps.hpp +++ b/firmware/common/dsp_fir_taps.hpp @@ -44,6 +44,42 @@ struct fir_taps_complex { std::array taps; }; +/* + * 768kHz -> 384kHz half-band prefilter. The broad 80...304kHz + * transition makes this stage inexpensive while protecting the useful band. + */ +constexpr fir_taps_real<16> taps_audio_wide_halfband_0{ + .low_frequency_normalized = -80000.0f / 768000.0f, + .high_frequency_normalized = 80000.0f / 768000.0f, + .transition_normalized = 224000.0f / 768000.0f, + .taps = {{ + -171, 0, 1144, 0, -4481, 0, 19892, 32767, + 19892, 0, -4481, 0, 1144, 0, -171, 0, + }}, +}; + +/* + * Spectrum capture anti-alias half-band filter. It is run only while a + * contiguous 256-sample FFT frame is being collected: + * 384kHz -> 192kHz (Zoom x1) + * 192kHz -> 96kHz (additional stage for Zoom x2) + */ +constexpr fir_taps_real<63> taps_audio_spectrum_halfband{ + .low_frequency_normalized = -0.23f, + .high_frequency_normalized = 0.23f, + .transition_normalized = 0.04f, + .taps = {{ + -15, 0, 37, 0, -70, 0, 117, 0, + -184, 0, 274, 0, -393, 0, 548, 0, + -751, 0, 1018, 0, -1374, 0, 1872, 0, + -2622, 0, 3910, 0, -6794, 0, 20812, 32767, + 20812, 0, -6794, 0, 3910, 0, -2622, 0, + 1872, 0, -1374, 0, 1018, 0, -751, 0, + 548, 0, -393, 0, 274, 0, -184, 0, + 117, 0, -70, 0, 37, 0, -16, + }}, +}; + // 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 818c8d36f..a9d71b62a 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -170,6 +170,7 @@ class Message { HunterStop = 112, TetraBsch = 113, TetraDnb = 114, + AudioDDCConfig = 115, MAX }; @@ -310,6 +311,16 @@ class SpectrumStreamingConfigMessage : public Message { Mode mode{Mode::Stopped}; }; +class AudioDDCConfigMessage : public Message { + public: + constexpr AudioDDCConfigMessage(int32_t frequency) + : Message{ID::AudioDDCConfig}, + frequency{frequency} { + } + + int32_t frequency{0}; +}; + class WidebandSpectrumConfigMessage : public Message { public: constexpr WidebandSpectrumConfigMessage( @@ -357,6 +368,7 @@ class AudioSpectrumMessage : public Message { struct ChannelSpectrum { std::array db{{0}}; uint32_t sampling_rate{0}; + int32_t channel_filter_offset{0}; int32_t channel_filter_low_frequency{0}; int32_t channel_filter_high_frequency{0}; int32_t channel_filter_transition{0};