mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-12 01:29:29 +00:00
Add sliding-frequency audio receiver tuning
This commit is contained in:
@@ -280,6 +280,12 @@ AnalogAudioView::AnalogAudioView(
|
|||||||
field_frequency.on_show_options = [this]() {
|
field_frequency.on_show_options = [this]() {
|
||||||
this->on_show_options_frequency();
|
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]() {
|
field_lna.on_show_options = [this]() {
|
||||||
this->on_show_options_rf_gain();
|
this->on_show_options_rf_gain();
|
||||||
@@ -308,8 +314,10 @@ AnalogAudioView::AnalogAudioView(
|
|||||||
};
|
};
|
||||||
|
|
||||||
waterfall.on_select = [this](int32_t offset) {
|
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
|
#ifdef PRALINE
|
||||||
button_pro.on_select = [this](Button&) { this->on_show_options_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
|
// This call starts the correct baseband image to run
|
||||||
// and sets the radio up as necessary for the given modulation.
|
// and sets the radio up as necessary for the given modulation.
|
||||||
|
sliding_center_frequency = receiver_model.target_frequency();
|
||||||
on_modulation_changed(modulation);
|
on_modulation_changed(modulation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +337,7 @@ AnalogAudioView::AnalogAudioView(
|
|||||||
: AnalogAudioView(nav) {
|
: AnalogAudioView(nav) {
|
||||||
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
||||||
// TODO: Which other settings make sense to override?
|
// 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);
|
on_frequency_step_changed(override.frequency_step);
|
||||||
options_modulation.set_by_value(toUType(override.mode));
|
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_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_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();
|
receiver_model.enable();
|
||||||
|
|
||||||
|
if (sliding_enabled) {
|
||||||
|
baseband::set_audio_ddc_frequency(0);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This doesn't belong here! There's a better way.
|
// TODO: This doesn't belong here! There's a better way.
|
||||||
size_t sampling_rate = 0;
|
size_t sampling_rate = 0;
|
||||||
switch (modulation) {
|
switch (modulation) {
|
||||||
@@ -610,7 +623,64 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AnalogAudioView::on_freqchg(int64_t freq) {
|
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<int32_t>(offset));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef PRALINE
|
#ifdef PRALINE
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ class AnalogAudioView : public View {
|
|||||||
uint8_t zoom_factor_amfm{0}; // initial zoom factor in AMFM 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_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 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_{
|
app_settings::SettingsManager settings_{
|
||||||
"rx_audio",
|
"rx_audio",
|
||||||
@@ -354,6 +358,10 @@ class AnalogAudioView : public View {
|
|||||||
void handle_coded_squelch(uint32_t value);
|
void handle_coded_squelch(uint32_t value);
|
||||||
|
|
||||||
void on_freqchg(int64_t freq);
|
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{
|
MessageHandlerRegistration message_handler_coded_squelch{
|
||||||
Message::ID::CodedSquelch,
|
Message::ID::CodedSquelch,
|
||||||
|
|||||||
@@ -343,6 +343,11 @@ void set_spectrum(
|
|||||||
send_message(&message);
|
send_message(&message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_audio_ddc_frequency(int32_t frequency) {
|
||||||
|
const AudioDDCConfigMessage message{frequency};
|
||||||
|
send_message(&message);
|
||||||
|
}
|
||||||
|
|
||||||
void set_time_sink(
|
void set_time_sink(
|
||||||
const size_t sampling_rate,
|
const size_t sampling_rate,
|
||||||
const size_t trigger) {
|
const size_t trigger) {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ void set_rds_data(const uint16_t message_length);
|
|||||||
void set_spectrum(
|
void set_spectrum(
|
||||||
const size_t sampling_rate,
|
const size_t sampling_rate,
|
||||||
const size_t trigger);
|
const size_t trigger);
|
||||||
|
void set_audio_ddc_frequency(int32_t frequency);
|
||||||
void set_time_sink(
|
void set_time_sink(
|
||||||
const size_t sampling_rate,
|
const size_t sampling_rate,
|
||||||
const size_t trigger);
|
const size_t trigger);
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ void ReceiverModel::set_target_frequency(rf::Frequency f) {
|
|||||||
update_tuning_frequency();
|
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 {
|
uint32_t ReceiverModel::baseband_bandwidth() const {
|
||||||
return settings_.baseband_bandwidth;
|
return settings_.baseband_bandwidth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class ReceiverModel {
|
|||||||
/* The frequency to receive (no offset). */
|
/* The frequency to receive (no offset). */
|
||||||
rf::Frequency target_frequency() const;
|
rf::Frequency target_frequency() const;
|
||||||
void set_target_frequency(rf::Frequency f);
|
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;
|
uint32_t baseband_bandwidth() const;
|
||||||
void set_baseband_bandwidth(uint32_t v);
|
void set_baseband_bandwidth(uint32_t v);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class BoundFrequencyField : public FrequencyField {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
decltype(FrequencyField::on_change) updated{};
|
decltype(FrequencyField::on_change) updated{};
|
||||||
|
std::function<bool(rf::Frequency)> changing{};
|
||||||
|
std::function<void(rf::Frequency)> entered{};
|
||||||
|
|
||||||
BoundFrequencyField(Point parent_pos, NavigationView& nav)
|
BoundFrequencyField(Point parent_pos, NavigationView& nav)
|
||||||
: FrequencyField(parent_pos) {
|
: FrequencyField(parent_pos) {
|
||||||
@@ -47,7 +49,8 @@ class BoundFrequencyField : public FrequencyField {
|
|||||||
set_value(model->target_frequency());
|
set_value(model->target_frequency());
|
||||||
|
|
||||||
on_change = [this](rf::Frequency f) {
|
on_change = [this](rf::Frequency f) {
|
||||||
model->set_target_frequency(f);
|
if (!changing || !changing(f))
|
||||||
|
model->set_target_frequency(f);
|
||||||
if (updated)
|
if (updated)
|
||||||
updated(f);
|
updated(f);
|
||||||
};
|
};
|
||||||
@@ -57,7 +60,10 @@ class BoundFrequencyField : public FrequencyField {
|
|||||||
on_edit_shown();
|
on_edit_shown();
|
||||||
auto freq_view = nav.push<FrequencyKeypadView>(model->target_frequency());
|
auto freq_view = nav.push<FrequencyKeypadView>(model->target_frequency());
|
||||||
freq_view->on_changed = [this](rf::Frequency f) {
|
freq_view->on_changed = [this](rf::Frequency f) {
|
||||||
set_value(f);
|
if (entered)
|
||||||
|
entered(f);
|
||||||
|
else
|
||||||
|
set_value(f);
|
||||||
};
|
};
|
||||||
nav.set_on_pop([this]() {
|
nav.set_on_pop([this]() {
|
||||||
if (on_edit_hidden)
|
if (on_edit_hidden)
|
||||||
@@ -76,4 +82,4 @@ using TxFrequencyField = BoundFrequencyField<TransmitterModel, &portapack::trans
|
|||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|
||||||
#endif // __UI_FREQ_FIELD_H__
|
#endif // __UI_FREQ_FIELD_H__
|
||||||
|
|||||||
@@ -88,16 +88,29 @@ void FrequencyScale::set_spectrum_sampling_rate(const int new_sampling_rate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FrequencyScale::set_channel_filter(
|
void FrequencyScale::set_channel_filter(
|
||||||
|
const int offset,
|
||||||
const int low_frequency,
|
const int low_frequency,
|
||||||
const int high_frequency,
|
const int high_frequency,
|
||||||
const int transition) {
|
const int transition) {
|
||||||
if ((channel_filter_low_frequency != low_frequency) ||
|
const bool shape_changed =
|
||||||
|
(channel_filter_low_frequency != low_frequency) ||
|
||||||
(channel_filter_high_frequency != high_frequency) ||
|
(channel_filter_high_frequency != high_frequency) ||
|
||||||
(channel_filter_transition != transition)) {
|
(channel_filter_transition != transition);
|
||||||
|
const bool offset_changed = channel_filter_offset != offset;
|
||||||
|
|
||||||
|
if (shape_changed) {
|
||||||
|
channel_filter_offset = offset;
|
||||||
channel_filter_low_frequency = low_frequency;
|
channel_filter_low_frequency = low_frequency;
|
||||||
channel_filter_high_frequency = high_frequency;
|
channel_filter_high_frequency = high_frequency;
|
||||||
channel_filter_transition = transition;
|
channel_filter_transition = transition;
|
||||||
set_dirty();
|
set_dirty();
|
||||||
|
} else if (offset_changed) {
|
||||||
|
const auto old_offset = channel_filter_offset;
|
||||||
|
channel_filter_offset = offset;
|
||||||
|
if (live_tuning && spectrum_sampling_rate && drawn())
|
||||||
|
redraw_filter_cursor(old_offset);
|
||||||
|
else
|
||||||
|
set_dirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +136,14 @@ void FrequencyScale::paint(Painter& painter) {
|
|||||||
draw_filter_ranges(painter, r);
|
draw_filter_ranges(painter, r);
|
||||||
draw_frequency_ticks(painter, r);
|
draw_frequency_ticks(painter, r);
|
||||||
|
|
||||||
const Rect r_cursor{
|
if (!live_tuning) {
|
||||||
(screen_width / 2 - 2) + cursor_position, r.bottom() - filter_band_height,
|
const Rect r_cursor{
|
||||||
5, filter_band_height};
|
(screen_width / 2 - 2) + cursor_position, r.bottom() - filter_band_height,
|
||||||
painter.fill_rectangle(
|
5, filter_band_height};
|
||||||
r_cursor,
|
painter.fill_rectangle(
|
||||||
Color::red());
|
r_cursor,
|
||||||
|
Color::red());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FrequencyScale::clear() {
|
void FrequencyScale::clear() {
|
||||||
@@ -184,9 +199,79 @@ void FrequencyScale::draw_frequency_ticks(Painter& painter, const Rect r) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FrequencyScale::redraw_filter_cursor(const int old_offset) {
|
||||||
|
const auto r = screen_rect();
|
||||||
|
const auto x_center = r.width() / 2;
|
||||||
|
const auto trans =
|
||||||
|
channel_filter_transition * spectrum_bins / spectrum_sampling_rate;
|
||||||
|
|
||||||
|
const auto cursor_left = [&](const int offset) {
|
||||||
|
return r.left() + x_center +
|
||||||
|
(offset + channel_filter_low_frequency) *
|
||||||
|
spectrum_bins / spectrum_sampling_rate -
|
||||||
|
trans;
|
||||||
|
};
|
||||||
|
const auto cursor_right = [&](const int offset) {
|
||||||
|
return r.left() + x_center +
|
||||||
|
(offset + channel_filter_high_frequency) *
|
||||||
|
spectrum_bins / spectrum_sampling_rate +
|
||||||
|
trans;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto dirty_left =
|
||||||
|
std::min(cursor_left(old_offset), cursor_left(channel_filter_offset)) - 1;
|
||||||
|
const auto dirty_right =
|
||||||
|
std::max(cursor_right(old_offset), cursor_right(channel_filter_offset)) + 1;
|
||||||
|
const Rect dirty{
|
||||||
|
dirty_left,
|
||||||
|
r.bottom() - filter_band_height,
|
||||||
|
dirty_right - dirty_left,
|
||||||
|
filter_band_height};
|
||||||
|
|
||||||
|
Painter painter;
|
||||||
|
painter.fill_rectangle(dirty, Theme::getInstance()->bg_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) {
|
void FrequencyScale::draw_filter_ranges(Painter& painter, const Rect r) {
|
||||||
if (channel_filter_low_frequency != channel_filter_high_frequency) {
|
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_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;
|
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) {
|
bool FrequencyScale::on_encoder(const EncoderEvent delta) {
|
||||||
|
if (live_tuning) {
|
||||||
|
if (on_select) on_select(delta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
cursor_position += delta;
|
cursor_position += delta;
|
||||||
|
|
||||||
cursor_position = std::min<int32_t>(cursor_position, screen_width / 2 - 1);
|
cursor_position = std::min<int32_t>(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
|
frequency_scale.focus(); // focus on frequency scale to show cursor
|
||||||
|
|
||||||
if (sampling_rate) {
|
if (sampling_rate) {
|
||||||
// screen x to frequency scale x, NB we need two widgets align
|
const int32_t cursor_position = x - (screen_width / 2);
|
||||||
int32_t cursor_position = x - (screen_width / 2);
|
if (!frequency_scale.is_live_tuning()) {
|
||||||
frequency_scale.set_cursor_position(cursor_position);
|
// 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;
|
sampling_rate = spectrum.sampling_rate;
|
||||||
frequency_scale.set_spectrum_sampling_rate(sampling_rate);
|
frequency_scale.set_spectrum_sampling_rate(sampling_rate);
|
||||||
frequency_scale.set_channel_filter(
|
frequency_scale.set_channel_filter(
|
||||||
|
spectrum.channel_filter_offset,
|
||||||
spectrum.channel_filter_low_frequency,
|
spectrum.channel_filter_low_frequency,
|
||||||
spectrum.channel_filter_high_frequency,
|
spectrum.channel_filter_high_frequency,
|
||||||
spectrum.channel_filter_transition);
|
spectrum.channel_filter_transition);
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ class FrequencyScale : public Widget {
|
|||||||
bool on_touch(const TouchEvent touch) override;
|
bool on_touch(const TouchEvent touch) override;
|
||||||
|
|
||||||
void set_spectrum_sampling_rate(const int new_sampling_rate);
|
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_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;
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
@@ -94,15 +96,19 @@ class FrequencyScale : public Widget {
|
|||||||
int32_t cursor_position{0};
|
int32_t cursor_position{0};
|
||||||
int spectrum_sampling_rate{0};
|
int spectrum_sampling_rate{0};
|
||||||
const int spectrum_bins = std::tuple_size<decltype(ChannelSpectrum::db)>::value;
|
const int spectrum_bins = std::tuple_size<decltype(ChannelSpectrum::db)>::value;
|
||||||
|
int channel_filter_offset{0};
|
||||||
int channel_filter_low_frequency{0};
|
int channel_filter_low_frequency{0};
|
||||||
int channel_filter_high_frequency{0};
|
int channel_filter_high_frequency{0};
|
||||||
int channel_filter_transition{0};
|
int channel_filter_transition{0};
|
||||||
|
bool live_tuning{false};
|
||||||
|
|
||||||
void clear();
|
void clear();
|
||||||
void clear_background(Painter& painter, const Rect r);
|
void clear_background(Painter& painter, const Rect r);
|
||||||
|
|
||||||
void draw_frequency_ticks(Painter& painter, const Rect r);
|
void draw_frequency_ticks(Painter& painter, const Rect r);
|
||||||
void draw_filter_ranges(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.
|
/* 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 set_parent_rect(const Rect new_parent_rect) override;
|
||||||
void show_audio_spectrum_view(const bool show);
|
void show_audio_spectrum_view(const bool show);
|
||||||
void load_gradient();
|
void load_gradient();
|
||||||
|
void set_live_tuning(const bool enabled) { frequency_scale.set_live_tuning(enabled); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void update_widgets_rect();
|
void update_widgets_rect();
|
||||||
|
|||||||
@@ -337,6 +337,66 @@ buffer_c16_t FIRC16xR16x16Decim2::execute(
|
|||||||
src.sampling_rate / decimation_factor};
|
src.sampling_rate / decimation_factor};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIRC16xR16x63HalfbandDecim2 ////////////////////////////////////////////
|
||||||
|
|
||||||
|
void FIRC16xR16x63HalfbandDecim2::configure(
|
||||||
|
const std::array<int16_t, taps_count>& 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<uint32_t*>(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<const uint32_t*>(&samples_[samples_head_ + tap]);
|
||||||
|
const auto sample_1 =
|
||||||
|
*reinterpret_cast<const uint32_t*>(&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<const uint32_t*>(&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 ////////////////////////////////////////////////////
|
// FIRC16xR16x32Decim8 ////////////////////////////////////////////////////
|
||||||
|
|
||||||
void FIRC16xR16x32Decim8::configure(
|
void FIRC16xR16x32Decim8::configure(
|
||||||
@@ -625,10 +685,13 @@ buffer_s16_t FIR64AndDecimateBy2Real::execute(
|
|||||||
void FIRAndDecimateComplex::configure_common(
|
void FIRAndDecimateComplex::configure_common(
|
||||||
const size_t taps_count,
|
const size_t taps_count,
|
||||||
const size_t decimation_factor) {
|
const size_t decimation_factor) {
|
||||||
samples_ = std::make_unique<samples_t>(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<samples_t>(taps_count * 2);
|
||||||
taps_reversed_ = std::make_unique<taps_t>(taps_count);
|
taps_reversed_ = std::make_unique<taps_t>(taps_count);
|
||||||
taps_count_ = taps_count;
|
taps_count_ = taps_count;
|
||||||
decimation_factor_ = decimation_factor;
|
decimation_factor_ = decimation_factor;
|
||||||
|
samples_head_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer_c16_t FIRAndDecimateComplex::execute(
|
buffer_c16_t FIRAndDecimateComplex::execute(
|
||||||
@@ -647,15 +710,18 @@ buffer_c16_t FIRAndDecimateComplex::execute(
|
|||||||
const void* src_p = src.p;
|
const void* src_p = src.p;
|
||||||
size_t outer_count = output_samples;
|
size_t outer_count = output_samples;
|
||||||
while (outer_count > 0) {
|
while (outer_count > 0) {
|
||||||
/* Put new samples into delay buffer */
|
/* Put new samples into both halves of the mirrored ring. */
|
||||||
void* z_new_p = &samples_[taps_count_ - decimation_factor_];
|
|
||||||
for (size_t i = 0; i < decimation_factor_; i++) {
|
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<uint32_t*>(&samples_[samples_head_]) = sample;
|
||||||
|
*reinterpret_cast<uint32_t*>(&samples_[samples_head_ + taps_count_]) = sample;
|
||||||
|
if (++samples_head_ == taps_count_)
|
||||||
|
samples_head_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t loop_count = taps_count_ / 8;
|
size_t loop_count = taps_count_ / 8;
|
||||||
void* t_p = &taps_reversed_[0];
|
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_real = 0;
|
||||||
int64_t t_imag = 0;
|
int64_t t_imag = 0;
|
||||||
@@ -712,27 +778,6 @@ buffer_c16_t FIRAndDecimateComplex::execute(
|
|||||||
i_sat,
|
i_sat,
|
||||||
16);
|
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--;
|
outer_count--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,24 @@ class FIRC16xR16x16Decim2 {
|
|||||||
int32_t output_scale = 0;
|
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<int16_t, taps_count>& taps);
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
buffer_c16_t execute(
|
||||||
|
const buffer_c16_t& src,
|
||||||
|
const buffer_c16_t& dst);
|
||||||
|
|
||||||
|
private:
|
||||||
|
alignas(4) std::array<complex16_t, taps_count * 2> samples_{};
|
||||||
|
alignas(4) std::array<int16_t, taps_count> taps_{};
|
||||||
|
size_t samples_head_{0};
|
||||||
|
};
|
||||||
|
|
||||||
class FIRC16xR16x32Decim8 {
|
class FIRC16xR16x32Decim8 {
|
||||||
public:
|
public:
|
||||||
static constexpr size_t taps_count = 32;
|
static constexpr size_t taps_count = 32;
|
||||||
@@ -210,6 +228,23 @@ class FIRAndDecimateComplex {
|
|||||||
configure(taps.data(), taps.size(), decimation_factor);
|
configure(taps.data(), taps.size(), decimation_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
void configure(
|
||||||
|
const std::array<int16_t, N>& 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 <size_t N>
|
||||||
|
void set_taps(const std::array<complex16_t, N>& taps) {
|
||||||
|
if (N == taps_count_) {
|
||||||
|
std::reverse_copy(taps.begin(), taps.end(), &taps_reversed_[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buffer_c16_t execute(
|
buffer_c16_t execute(
|
||||||
const buffer_c16_t& src,
|
const buffer_c16_t& src,
|
||||||
const buffer_c16_t& dst);
|
const buffer_c16_t& dst);
|
||||||
@@ -221,6 +256,7 @@ class FIRAndDecimateComplex {
|
|||||||
std::unique_ptr<taps_t> taps_reversed_{};
|
std::unique_ptr<taps_t> taps_reversed_{};
|
||||||
size_t taps_count_{0};
|
size_t taps_count_{0};
|
||||||
size_t decimation_factor_{1};
|
size_t decimation_factor_{1};
|
||||||
|
size_t samples_head_{0};
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void configure(
|
void configure(
|
||||||
|
|||||||
@@ -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 <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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<uint16_t>(sine_q15_[static_cast<uint8_t>(i + 64)]) |
|
||||||
|
(static_cast<uint32_t>(
|
||||||
|
static_cast<uint16_t>(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<int16_t>(oscillator_first >> 16);
|
||||||
|
const int32_t cosine_first =
|
||||||
|
static_cast<int16_t>(oscillator_first);
|
||||||
|
const int32_t sine =
|
||||||
|
sine_first +
|
||||||
|
(((static_cast<int16_t>(oscillator_next >> 16) -
|
||||||
|
sine_first) *
|
||||||
|
fraction) >>
|
||||||
|
8);
|
||||||
|
const int32_t cosine =
|
||||||
|
cosine_first +
|
||||||
|
(((static_cast<int16_t>(oscillator_next) -
|
||||||
|
cosine_first) *
|
||||||
|
fraction) >>
|
||||||
|
8);
|
||||||
|
const uint32_t oscillator =
|
||||||
|
static_cast<uint16_t>(cosine) |
|
||||||
|
(static_cast<uint32_t>(
|
||||||
|
static_cast<uint16_t>(sine))
|
||||||
|
<< 16);
|
||||||
|
const uint32_t sample =
|
||||||
|
*reinterpret_cast<const uint32_t*>(&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<uint32_t*>(&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<uint32_t>(
|
||||||
|
(static_cast<int64_t>(frequency_) * (int64_t{1} << 32)) /
|
||||||
|
sampling_rate_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::array<int16_t, 256> 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<uint32_t, 256> 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<int16_t, taps_count>& 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<uint32_t>(
|
||||||
|
(static_cast<int64_t>(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<uint32_t>(
|
||||||
|
-((static_cast<int64_t>(
|
||||||
|
static_cast<int32_t>(tap_phase_increment)) *
|
||||||
|
static_cast<int64_t>(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<int16_t>(
|
||||||
|
FrequencyTranslator::rounded_shift(
|
||||||
|
tap * cosine, 14)),
|
||||||
|
static_cast<int16_t>(
|
||||||
|
FrequencyTranslator::rounded_shift(
|
||||||
|
tap * sine, 14))};
|
||||||
|
phase += tap_phase_increment;
|
||||||
|
}
|
||||||
|
decimator_.set_taps(complex_taps_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<int16_t, taps_count> taps_{};
|
||||||
|
std::array<complex16_t, taps_count> 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__*/
|
||||||
@@ -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_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 decim_2_out = decim_2.execute(decim_1_out, dst_buffer);
|
||||||
const auto channel_out = channel_filter.execute(decim_2_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<const CaptureConfigMessage*>(message));
|
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Message::ID::AudioDDCConfig:
|
||||||
|
ddc_config(*reinterpret_cast<const AudioDDCConfigMessage*>(message));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
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_input_fs = baseband_fs;
|
||||||
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
|
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 audio_decim_0_output_fs = decim_0_output_fs / 2;
|
||||||
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
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_input_fs = decim_1_output_fs;
|
||||||
constexpr size_t decim_2_output_fs = decim_2_input_fs / decim_2_decimation_factor;
|
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;
|
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;
|
// const size_t channel_filter_output_fs = channel_filter_input_fs / channel_filter_decimation_factor;
|
||||||
|
|
||||||
decim_0.configure(message.decim_0_filter.taps);
|
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||||
decim_1.configure(message.decim_1_filter.taps);
|
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);
|
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.configure(message.channel_filter.taps, channel_filter_decimation_factor);
|
||||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
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;
|
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}
|
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).
|
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;
|
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) {
|
void NarrowbandAMAudio::capture_config(const CaptureConfigMessage& message) {
|
||||||
if (message.config) {
|
if (message.config) {
|
||||||
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
|
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
#include "dsp_decimate.hpp"
|
#include "dsp_decimate.hpp"
|
||||||
#include "dsp_demodulate.hpp"
|
#include "dsp_demodulate.hpp"
|
||||||
|
#include "dsp_frequency_xlator.hpp"
|
||||||
#include "audio_compressor.hpp"
|
#include "audio_compressor.hpp"
|
||||||
|
|
||||||
#include "audio_output.hpp"
|
#include "audio_output.hpp"
|
||||||
@@ -44,6 +45,7 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t baseband_fs = 3072000;
|
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 decim_2_decimation_factor = 4;
|
||||||
static constexpr size_t channel_filter_decimation_factor = 1;
|
static constexpr size_t channel_filter_decimation_factor = 1;
|
||||||
|
|
||||||
@@ -56,14 +58,19 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
|||||||
audio.data(),
|
audio.data(),
|
||||||
audio.size()};
|
audio.size()};
|
||||||
|
|
||||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
dsp::decimate::FIRC16xR16x16Decim2 audio_decim_0{};
|
||||||
|
dsp::FrequencyTranslatingDecimator32By8 translating_decim_1{};
|
||||||
dsp::decimate::FIRAndDecimateComplex decim_2{};
|
dsp::decimate::FIRAndDecimateComplex decim_2{};
|
||||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||||
int32_t channel_filter_low_f = 0;
|
int32_t channel_filter_low_f = 0;
|
||||||
int32_t channel_filter_high_f = 0;
|
int32_t channel_filter_high_f = 0;
|
||||||
int32_t channel_filter_transition = 0;
|
int32_t channel_filter_transition = 0;
|
||||||
bool configured{false};
|
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.
|
// 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}
|
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
|
#endif
|
||||||
|
|
||||||
void configure(const AMConfigureMessage& message);
|
void configure(const AMConfigureMessage& message);
|
||||||
|
void ddc_config(const AudioDDCConfigMessage& message);
|
||||||
void capture_config(const CaptureConfigMessage& message);
|
void capture_config(const CaptureConfigMessage& message);
|
||||||
|
|
||||||
buffer_f32_t demodulate(const buffer_c16_t& channel);
|
buffer_f32_t demodulate(const buffer_c16_t& channel);
|
||||||
|
|||||||
@@ -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_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);
|
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||||
|
|
||||||
feed_channel_stats(channel_out);
|
feed_channel_stats(channel_out);
|
||||||
@@ -141,6 +157,10 @@ void NarrowbandFMAudio::on_message(const Message* const message) {
|
|||||||
pitch_rssi_config(*reinterpret_cast<const PitchRSSIConfigureMessage*>(message));
|
pitch_rssi_config(*reinterpret_cast<const PitchRSSIConfigureMessage*>(message));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Message::ID::AudioDDCConfig:
|
||||||
|
ddc_config(*reinterpret_cast<const AudioDDCConfigMessage*>(message));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
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_input_fs = baseband_fs;
|
||||||
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
|
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 audio_decim_0_output_fs = decim_0_output_fs / 2;
|
||||||
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
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;
|
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 channel_filter_output_fs = channel_filter_input_fs / message.channel_decimation;
|
||||||
|
|
||||||
const size_t demod_input_fs = channel_filter_output_fs;
|
const size_t demod_input_fs = channel_filter_output_fs;
|
||||||
|
|
||||||
decim_0.configure(message.decim_0_filter.taps);
|
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||||
decim_1.configure(message.decim_1_filter.taps);
|
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);
|
channel_filter.configure(message.channel_filter.taps, message.channel_decimation);
|
||||||
demod.configure(demod_input_fs, message.deviation);
|
demod.configure(demod_input_fs, message.deviation);
|
||||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
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_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_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);
|
audio_output.configure(message.audio_hpf_config, message.audio_deemph_config, (float)message.squelch_level / 100.0);
|
||||||
|
|
||||||
hpf.configure(audio_24k_hpf_30hz_config);
|
hpf.configure(audio_24k_hpf_30hz_config);
|
||||||
@@ -174,6 +198,11 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) {
|
|||||||
configured = true;
|
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) {
|
void NarrowbandFMAudio::pitch_rssi_config(const PitchRSSIConfigureMessage& message) {
|
||||||
pitch_rssi_enabled = message.enabled;
|
pitch_rssi_enabled = message.enabled;
|
||||||
tone_delta = (message.rssi + 1000) * ((1ULL << 32) / 24000);
|
tone_delta = (message.rssi + 1000) * ((1ULL << 32) / 24000);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include "dsp_decimate.hpp"
|
#include "dsp_decimate.hpp"
|
||||||
#include "dsp_demodulate.hpp"
|
#include "dsp_demodulate.hpp"
|
||||||
|
#include "dsp_frequency_xlator.hpp"
|
||||||
#include "dsp_iir.hpp"
|
#include "dsp_iir.hpp"
|
||||||
|
|
||||||
#include "audio_output.hpp"
|
#include "audio_output.hpp"
|
||||||
@@ -50,6 +51,7 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t baseband_fs = 3072000;
|
static constexpr size_t baseband_fs = 3072000;
|
||||||
|
static constexpr auto spectrum_rate_hz = 30.0f;
|
||||||
|
|
||||||
std::array<complex16_t, 512> dst{};
|
std::array<complex16_t, 512> dst{};
|
||||||
const buffer_c16_t dst_buffer{
|
const buffer_c16_t dst_buffer{
|
||||||
@@ -69,8 +71,9 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
|||||||
(int16_t*)tone.data(),
|
(int16_t*)tone.data(),
|
||||||
sizeof(tone) / sizeof(int16_t)};
|
sizeof(tone) / sizeof(int16_t)};
|
||||||
|
|
||||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
dsp::decimate::FIRC16xR16x16Decim2 audio_decim_0{};
|
||||||
|
dsp::FrequencyTranslatingDecimator32By8 translating_decim_1{};
|
||||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||||
int32_t channel_filter_low_f = 0;
|
int32_t channel_filter_low_f = 0;
|
||||||
int32_t channel_filter_high_f = 0;
|
int32_t channel_filter_high_f = 0;
|
||||||
@@ -85,6 +88,9 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
|||||||
AudioOutput audio_output{};
|
AudioOutput audio_output{};
|
||||||
|
|
||||||
SpectrumCollector channel_spectrum{};
|
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_phase{0};
|
||||||
uint32_t tone_delta{0};
|
uint32_t tone_delta{0};
|
||||||
@@ -113,6 +119,7 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
|||||||
void pitch_rssi_config(const PitchRSSIConfigureMessage& message);
|
void pitch_rssi_config(const PitchRSSIConfigureMessage& message);
|
||||||
void configure(const NBFMConfigureMessage& message);
|
void configure(const NBFMConfigureMessage& message);
|
||||||
void capture_config(const CaptureConfigMessage& message);
|
void capture_config(const CaptureConfigMessage& message);
|
||||||
|
void ddc_config(const AudioDDCConfigMessage& message);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /*__PROC_NFM_AUDIO_H__*/
|
#endif /*__PROC_NFM_AUDIO_H__*/
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "spectrum_collector.hpp"
|
#include "spectrum_collector.hpp"
|
||||||
|
|
||||||
#include "dsp_fft.hpp"
|
#include "dsp_fft.hpp"
|
||||||
|
#include "dsp_fir_taps.hpp"
|
||||||
|
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
#include "event_m4.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.
|
* perform the deferred task on the buffer of data we prepared.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
void SpectrumCollector::feed(
|
bool SpectrumCollector::feed(
|
||||||
const buffer_c16_t& channel,
|
const buffer_c16_t& channel,
|
||||||
const int32_t filter_low_frequency,
|
const int32_t filter_low_frequency,
|
||||||
const int32_t filter_high_frequency,
|
const int32_t filter_high_frequency,
|
||||||
@@ -83,11 +84,50 @@ void SpectrumCollector::feed(
|
|||||||
channel_filter_high_frequency = filter_high_frequency;
|
channel_filter_high_frequency = filter_high_frequency;
|
||||||
channel_filter_transition = filter_transition;
|
channel_filter_transition = filter_transition;
|
||||||
|
|
||||||
|
bool block_completed = false;
|
||||||
channel_spectrum_decimator.feed(
|
channel_spectrum_decimator.feed(
|
||||||
channel,
|
channel,
|
||||||
[this](const buffer_c16_t& data) {
|
[this, &block_completed](const buffer_c16_t& data) {
|
||||||
this->post_message(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) {
|
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() {
|
void SpectrumCollector::update() {
|
||||||
// Called from idle thread (after EVT_MASK_SPECTRUM is flagged)
|
// Called from idle thread (after EVT_MASK_SPECTRUM is flagged)
|
||||||
if (streaming && channel_spectrum_request_update) {
|
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. */
|
/* Decimated buffer is full. Compute spectrum. */
|
||||||
fft_c_preswapped(channel_spectrum, 0, 8);
|
fft_c_preswapped(channel_spectrum, 0, 8);
|
||||||
|
|
||||||
ChannelSpectrum spectrum;
|
ChannelSpectrum spectrum;
|
||||||
spectrum.sampling_rate = channel_spectrum_sampling_rate;
|
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_low_frequency = channel_filter_low_frequency;
|
||||||
spectrum.channel_filter_high_frequency = channel_filter_high_frequency;
|
spectrum.channel_filter_high_frequency = channel_filter_high_frequency;
|
||||||
spectrum.channel_filter_transition = channel_filter_transition;
|
spectrum.channel_filter_transition = channel_filter_transition;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "complex.hpp"
|
#include "complex.hpp"
|
||||||
|
|
||||||
#include "block_decimator.hpp"
|
#include "block_decimator.hpp"
|
||||||
|
#include "dsp_decimate.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -40,8 +41,18 @@ class SpectrumCollector {
|
|||||||
void on_message(const Message* const message);
|
void on_message(const Message* const message);
|
||||||
|
|
||||||
void set_decimation_factor(const size_t decimation_factor);
|
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 buffer_c16_t& channel,
|
||||||
const int32_t filter_low_frequency,
|
const int32_t filter_low_frequency,
|
||||||
const int32_t filter_high_frequency,
|
const int32_t filter_high_frequency,
|
||||||
@@ -55,10 +66,19 @@ class SpectrumCollector {
|
|||||||
volatile bool channel_spectrum_request_update{false};
|
volatile bool channel_spectrum_request_update{false};
|
||||||
bool streaming{false};
|
bool streaming{false};
|
||||||
std::array<std::complex<float>, 256> channel_spectrum{};
|
std::array<std::complex<float>, 256> channel_spectrum{};
|
||||||
|
std::array<complex16_t, 1024> filtered_capture_{};
|
||||||
|
std::array<complex16_t, 512> filtered_stage_0_{};
|
||||||
|
std::array<complex16_t, 256> 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};
|
uint32_t channel_spectrum_sampling_rate{0};
|
||||||
int32_t channel_filter_low_frequency{0};
|
int32_t channel_filter_low_frequency{0};
|
||||||
int32_t channel_filter_high_frequency{0};
|
int32_t channel_filter_high_frequency{0};
|
||||||
int32_t channel_filter_transition{0};
|
int32_t channel_filter_transition{0};
|
||||||
|
int32_t channel_filter_offset{0};
|
||||||
|
|
||||||
void post_message(const buffer_c16_t& data);
|
void post_message(const buffer_c16_t& data);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,42 @@ struct fir_taps_complex {
|
|||||||
std::array<complex16_t, N> taps;
|
std::array<complex16_t, N> 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 /////////////////////////////////////////////
|
// NBFM 16K0F3E emission type /////////////////////////////////////////////
|
||||||
|
|
||||||
// IFIR image-reject filter: fs=3072000, pass=8000, stop=344000, decim=8, fout=384000
|
// IFIR image-reject filter: fs=3072000, pass=8000, stop=344000, decim=8, fout=384000
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class Message {
|
|||||||
HunterStop = 112,
|
HunterStop = 112,
|
||||||
TetraBsch = 113,
|
TetraBsch = 113,
|
||||||
TetraDnb = 114,
|
TetraDnb = 114,
|
||||||
|
AudioDDCConfig = 115,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,6 +311,16 @@ class SpectrumStreamingConfigMessage : public Message {
|
|||||||
Mode mode{Mode::Stopped};
|
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 {
|
class WidebandSpectrumConfigMessage : public Message {
|
||||||
public:
|
public:
|
||||||
constexpr WidebandSpectrumConfigMessage(
|
constexpr WidebandSpectrumConfigMessage(
|
||||||
@@ -357,6 +368,7 @@ class AudioSpectrumMessage : public Message {
|
|||||||
struct ChannelSpectrum {
|
struct ChannelSpectrum {
|
||||||
std::array<uint8_t, 256> db{{0}};
|
std::array<uint8_t, 256> db{{0}};
|
||||||
uint32_t sampling_rate{0};
|
uint32_t sampling_rate{0};
|
||||||
|
int32_t channel_filter_offset{0};
|
||||||
int32_t channel_filter_low_frequency{0};
|
int32_t channel_filter_low_frequency{0};
|
||||||
int32_t channel_filter_high_frequency{0};
|
int32_t channel_filter_high_frequency{0};
|
||||||
int32_t channel_filter_transition{0};
|
int32_t channel_filter_transition{0};
|
||||||
|
|||||||
Reference in New Issue
Block a user