Oversample (#1336)

* WIP Oversample cleanup

* WIP

* WIP

* WIP dynamic interpolation

* WIP cleanup

* Fix math errors

* Add some optional assertions

* Add support for x32 interpolation

* Update proc_replay.cpp

Typo
This commit is contained in:
Kyle Reed 2023-08-02 12:59:26 -07:00 committed by GitHub
parent e2ad0a1b1a
commit 37386c29cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 272 additions and 169 deletions

View File

@ -60,25 +60,28 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
}; };
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth); freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) { option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */ /* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
auto sample_rate = bandwidth;
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */ /* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */ /* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
// For lower bandwidths, (12k5, 16k, 20k), increase the oversample rate to get a higher sample rate.
OversampleRate oversample_rate = base_rate >= 25'000 ? OversampleRate::Rate8x : OversampleRate::Rate16x;
// HackRF suggests a minimum sample rate of 2M.
// Oversampling helps get to higher sample rates when recording lower bandwidths.
uint32_t sampling_rate = toUType(oversample_rate) * base_rate;
// Set up proper anti aliasing BPF bandwidth in MAX2837 before ADC sampling according to the new added BW Options.
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
waterfall.stop(); waterfall.stop();
record_view.set_sampling_rate(sampling_rate, oversample_rate); // NB: Actually updates the baseband.
receiver_model.set_sampling_rate(sampling_rate); // record_view determines the correct oversampling to apply and returns the actual sample rate.
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter); // NB: record_view is what actually updates proc_capture baseband settings.
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
// Update the radio model with the actual sampling rate.
receiver_model.set_sampling_rate(actual_sample_rate);
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
waterfall.start(); waterfall.start();
}; };

View File

@ -124,7 +124,7 @@ void ReplayAppView::start() {
if (reader) { if (reader) {
button_play.set_bitmap(&bitmap_stop); button_play.set_bitmap(&bitmap_stop);
baseband::set_sample_rate(sample_rate * 8); baseband::set_sample_rate(sample_rate, OversampleRate::x8);
replay_thread = std::make_unique<ReplayThread>( replay_thread = std::make_unique<ReplayThread>(
std::move(reader), std::move(reader),
@ -136,7 +136,7 @@ void ReplayAppView::start() {
}); });
} }
transmitter_model.set_sampling_rate(sample_rate * 8); transmitter_model.set_sampling_rate(sample_rate * toUType(OversampleRate::x8));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth); transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable(); transmitter_model.enable();

View File

@ -24,18 +24,18 @@
#include "ui_playlist.hpp" #include "ui_playlist.hpp"
#include "baseband_api.hpp"
#include "convert.hpp" #include "convert.hpp"
#include "file_reader.hpp" #include "file_reader.hpp"
#include "io_file.hpp" #include "io_file.hpp"
#include "io_convert.hpp" #include "io_convert.hpp"
#include "oversample.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp" #include "string_format.hpp"
#include "ui_fileman.hpp" #include "ui_fileman.hpp"
#include "utility.hpp" #include "utility.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include <unistd.h> #include <unistd.h>
#include <fstream> #include <fstream>
@ -266,17 +266,16 @@ void PlaylistView::send_current_track() {
return; return;
} }
// ReplayThread starts immediately on construction so // Update the sample rate in proc_replay baseband.
// these need to be set before creating the ReplayThread. baseband::set_sample_rate(current()->metadata.sample_rate,
get_oversample_rate(current()->metadata.sample_rate));
// ReplayThread starts immediately on construction; must be set before creating.
transmitter_model.set_target_frequency(current()->metadata.center_frequency); transmitter_model.set_target_frequency(current()->metadata.center_frequency);
transmitter_model.set_sampling_rate(current()->metadata.sample_rate * 8); transmitter_model.set_sampling_rate(get_actual_sample_rate(current()->metadata.sample_rate));
transmitter_model.set_baseband_bandwidth(baseband_bandwidth); transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
transmitter_model.enable(); transmitter_model.enable();
// Set baseband sample rate too for waterfall to be correct.
// TODO: Why doesn't the transmitter_model just handle this?
baseband::set_sample_rate(transmitter_model.sampling_rate());
// Reset the transmit progress bar. // Reset the transmit progress bar.
progressbar_transmit.set_value(0); progressbar_transmit.set_value(0);

View File

@ -347,13 +347,8 @@ void spectrum_streaming_stop() {
send_message(&message); send_message(&message);
} }
void set_sample_rate(const uint32_t sample_rate) { void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate) {
SamplerateConfigMessage message{sample_rate}; SampleRateConfigMessage message{sample_rate, oversample_rate};
send_message(&message);
}
void set_oversample_rate(OversampleRate oversample_rate) {
OversampleRateConfigMessage message{oversample_rate};
send_message(&message); send_message(&message);
} }

View File

@ -94,8 +94,8 @@ void shutdown();
void spectrum_streaming_start(); void spectrum_streaming_start();
void spectrum_streaming_stop(); void spectrum_streaming_stop();
void set_sample_rate(const uint32_t sample_rate); /* NB: sample_rate should be desired rate. Don't pre-scale. */
void set_oversample_rate(OversampleRate oversample_rate); void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate = OversampleRate::None);
void capture_start(CaptureConfig* const config); void capture_start(CaptureConfig* const config);
void capture_stop(); void capture_stop();
void replay_start(ReplayConfig* const config); void replay_start(ReplayConfig* const config);

View File

@ -80,6 +80,8 @@ uint32_t ReplayThread::run() {
chThdSleep(100); chThdSleep(100);
}; };
constexpr size_t block_size = 512;
// While empty buffers fifo is not empty... // While empty buffers fifo is not empty...
while (!buffers.empty()) { while (!buffers.empty()) {
prefill_buffer = buffers.get_prefill(); prefill_buffer = buffers.get_prefill();
@ -87,10 +89,10 @@ uint32_t ReplayThread::run() {
if (prefill_buffer == nullptr) { if (prefill_buffer == nullptr) {
buffers.put_app(prefill_buffer); buffers.put_app(prefill_buffer);
} else { } else {
size_t blocks = config.read_size / 512; size_t blocks = config.read_size / block_size;
for (size_t c = 0; c < blocks; c++) { for (size_t c = 0; c < blocks; c++) {
auto read_result = reader->read(&((uint8_t*)prefill_buffer->data())[c * 512], 512); auto read_result = reader->read(&((uint8_t*)prefill_buffer->data())[c * block_size], block_size);
if (read_result.is_error()) { if (read_result.is_error()) {
return READ_ERROR; return READ_ERROR;
} }

View File

@ -380,33 +380,34 @@ void WaterfallView::on_audio_spectrum() {
} /* namespace spectrum */ } /* namespace spectrum */
// TODO: Comments below refer to a fixed oversample rate (8x), cleanup.
uint32_t filter_bandwidth_for_sampling_rate(int32_t sampling_rate) { uint32_t filter_bandwidth_for_sampling_rate(int32_t sampling_rate) {
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem. switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
case 0 ... 2000000: // BW Captured range (0 <= 250kHz max) fs = 8 x 250 kHz. case 0 ... 2'000'000: // BW Captured range (0 <= 250kHz max) fs = 8 x 250 kHz.
return 1750000; // Minimum BPF MAX2837 for all those lower BW options. return 1'750'000; // Minimum BPF MAX2837 for all those lower BW options.
case 4000000 ... 6000000: // BW capture range (500k...750kHz max) fs_max = 8 x 750kHz = 6Mhz case 4'000'000 ... 6'000'000: // BW capture range (500k...750kHz max) fs_max = 8 x 750kHz = 6Mhz
// BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz). // BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz).
return 2500000; // In some IC, MAX2837 appears as 2250000, but both work similarly. return 2'500'000; // In some IC, MAX2837 appears as 2250000, but both work similarly.
case 8800000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz. (1Mhz showed slightly higher noise background). case 8'800'000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz. (1Mhz showed slightly higher noise background).
return 3500000; return 3'500'000;
case 14000000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz case 14'000'000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture. // Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
return 5000000; return 5'000'000;
case 16000000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz case 16'000'000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture. // Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
return 6000000; return 6'000'000;
case 20000000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz case 20'000'000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture. // Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
return 7000000; return 7'000'000;
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz = 22Mhz max ADC sampling and others. default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz = 22Mhz max ADC sampling and others.
// We tested also 9Mhz FPB slightly too much noise floor, better at 8Mhz. // We tested also 9Mhz FPB slightly too much noise floor, better at 8Mhz.
return 8000000; return 8'000'000;
} }
} }

View File

@ -30,6 +30,7 @@ using namespace portapack;
#include "baseband_api.hpp" #include "baseband_api.hpp"
#include "metadata_file.hpp" #include "metadata_file.hpp"
#include "oversample.hpp"
#include "rtc_time.hpp" #include "rtc_time.hpp"
#include "string_format.hpp" #include "string_format.hpp"
#include "utility.hpp" #include "utility.hpp"
@ -101,27 +102,28 @@ void RecordView::focus() {
button_record.focus(); button_record.focus();
} }
void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_oversample_rate) { uint32_t RecordView::set_sampling_rate(uint32_t new_sampling_rate) {
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz // Determine the oversampling needed (if any) and the actual sampling rate.
where we are NOT recording full IQ .C16 files (recorded files are decimated ones). auto oversample_rate = get_oversample_rate(new_sampling_rate);
Those decimated recorded files, has not the full IQ samples. auto actual_sampling_rate = new_sampling_rate * toUType(oversample_rate);
are ok as recorded spectrum indication, but they should not be used by Replay app.
We keep original black background in all the correct IQ .C16 files BW's Options */ /* We are changing "REC" icon background to yellow in BW rec Options >600kHz
if (new_sampling_rate > 4'800'000) { // > BW >600kHz (fs=8*BW), (750kHz...2750kHz) * where we are NOT recording full IQ .C16 files (recorded files are decimated ones).
* Those decimated recorded files, has not the full IQ samples.
* are ok as recorded spectrum indication, but they should not be used by Replay app.
* We keep original black background in all the correct IQ .C16 files BW's Options. */
if (actual_sampling_rate > 4'800'000) { // > BW >600kHz (fs=8*BW), (750kHz...2750kHz)
button_record.set_background(ui::Color::yellow()); button_record.set_background(ui::Color::yellow());
} else { } else {
button_record.set_background(ui::Color::black()); button_record.set_background(ui::Color::black());
} }
if (new_sampling_rate != sampling_rate || if (sampling_rate != new_sampling_rate) {
new_oversample_rate != oversample_rate) {
stop(); stop();
sampling_rate = new_sampling_rate; sampling_rate = new_sampling_rate;
oversample_rate = new_oversample_rate; baseband::set_sample_rate(sampling_rate, oversample_rate);
baseband::set_sample_rate(sampling_rate);
baseband::set_oversample_rate(oversample_rate);
button_record.hidden(sampling_rate == 0); button_record.hidden(sampling_rate == 0);
text_record_filename.hidden(sampling_rate == 0); text_record_filename.hidden(sampling_rate == 0);
@ -131,6 +133,24 @@ void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_
update_status_display(); update_status_display();
} }
return actual_sampling_rate;
}
OversampleRate RecordView::get_oversample_rate(uint32_t sample_rate) {
// No oversampling necessary for baseband audio processors.
if (file_type == FileType::WAV)
return OversampleRate::None;
auto rate = ::get_oversample_rate(sample_rate);
// Currently proc_capture only supports x8 and x16 for decimation.
if (rate < OversampleRate::x8)
rate = OversampleRate::x8;
else if (rate > OversampleRate::x16)
rate = OversampleRate::x16;
return rate;
} }
// Setter for datetime and frequency filename // Setter for datetime and frequency filename
@ -202,8 +222,7 @@ void RecordView::start() {
case FileType::RawS8: case FileType::RawS8:
case FileType::RawS16: { case FileType::RawS16: {
const auto metadata_file_error = write_metadata_file( const auto metadata_file_error = write_metadata_file(
get_metadata_path(base_path), get_metadata_path(base_path), {receiver_model.target_frequency(), sampling_rate});
{receiver_model.target_frequency(), sampling_rate / toUType(oversample_rate)});
if (metadata_file_error.is_valid()) { if (metadata_file_error.is_valid()) {
handle_error(metadata_file_error.value()); handle_error(metadata_file_error.value());
return; return;
@ -278,12 +297,7 @@ void RecordView::update_status_display() {
// - C8 captures 2 (I,Q) int8_t per sample or '2' bytes per sample. // - C8 captures 2 (I,Q) int8_t per sample or '2' bytes per sample.
// - C16 captures 2 (I,Q) int16_t per sample or '4' bytes per sample. // - C16 captures 2 (I,Q) int16_t per sample or '4' bytes per sample.
const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2; const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2;
// WAV files are not oversampled, but C8 and C16 are. Divide by the const uint32_t bytes_per_second = sampling_rate * bytes_per_sample;
// oversample rate to get the effective sample rate.
const auto effective_sampling_rate = file_type == FileType::WAV
? sampling_rate
: sampling_rate / toUType(oversample_rate);
const uint32_t bytes_per_second = effective_sampling_rate * bytes_per_sample;
const uint32_t available_seconds = space_info.free / bytes_per_second; const uint32_t available_seconds = space_info.free / bytes_per_second;
const uint32_t seconds = available_seconds % 60; const uint32_t seconds = available_seconds % 60;
const uint32_t available_minutes = available_seconds / 60; const uint32_t available_minutes = available_seconds / 60;

View File

@ -56,16 +56,11 @@ class RecordView : public View {
void focus() override; void focus() override;
/* Sets the sampling rate and the oversampling "decimation" rate. /* Sets the sampling rate for the baseband.
* These values are passed down to the baseband proc_capture. For * NB: Do not pre-apply any oversampling. This function will determine
* Audio (WAV) recording, the OversampleRate should not be * the correct amount of oversampling and return the actual sample rate
* specified and the default will be used. */ * that can be used to configure the radio or other UI element. */
/* TODO: Currently callers are expected to have already multiplied the uint32_t set_sampling_rate(uint32_t new_sampling_rate);
* sample_rate with the oversample rate. It would be better move that
* logic to a single place. */
void set_sampling_rate(
size_t new_sampling_rate,
OversampleRate new_oversample_rate = OversampleRate::Rate8x);
void set_file_type(const FileType v) { file_type = v; } void set_file_type(const FileType v) { file_type = v; }
@ -87,6 +82,8 @@ class RecordView : public View {
void handle_capture_thread_done(const File::Error error); void handle_capture_thread_done(const File::Error error);
void handle_error(const File::Error error); void handle_error(const File::Error error);
OversampleRate get_oversample_rate(uint32_t sample_rate);
// bool pitch_rssi_enabled = false; // bool pitch_rssi_enabled = false;
// Time Stamp // Time Stamp
@ -98,8 +95,7 @@ class RecordView : public View {
FileType file_type; FileType file_type;
const size_t write_size; const size_t write_size;
const size_t buffer_count; const size_t buffer_count;
size_t sampling_rate{0}; uint32_t sampling_rate{0};
OversampleRate oversample_rate{OversampleRate::Rate8x};
SignalToken signal_token_tick_second{}; SignalToken signal_token_tick_second{};
Rectangle rect_background{ Rectangle rect_background{

View File

@ -77,8 +77,8 @@ void AudioTXProcessor::on_message(const Message* const message) {
replay_config(*reinterpret_cast<const ReplayConfigMessage*>(message)); replay_config(*reinterpret_cast<const ReplayConfigMessage*>(message));
break; break;
case Message::ID::SamplerateConfig: case Message::ID::SampleRateConfig:
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message)); sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break; break;
case Message::ID::FIFOData: case Message::ID::FIFOData:
@ -108,7 +108,7 @@ void AudioTXProcessor::replay_config(const ReplayConfigMessage& message) {
} }
} }
void AudioTXProcessor::samplerate_config(const SamplerateConfigMessage& message) { void AudioTXProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
resample_inc = (((uint64_t)message.sample_rate) << 16) / baseband_fs; // 16.16 fixed point message.sample_rate resample_inc = (((uint64_t)message.sample_rate) << 16) / baseband_fs; // 16.16 fixed point message.sample_rate
} }

View File

@ -53,7 +53,7 @@ class AudioTXProcessor : public BasebandProcessor {
bool configured{false}; bool configured{false};
uint32_t bytes_read{0}; uint32_t bytes_read{0};
void samplerate_config(const SamplerateConfigMessage& message); void sample_rate_config(const SampleRateConfigMessage& message);
void audio_config(const AudioTXConfigMessage& message); void audio_config(const AudioTXConfigMessage& message);
void replay_config(const ReplayConfigMessage& message); void replay_config(const ReplayConfigMessage& message);

View File

@ -66,19 +66,9 @@ void CaptureProcessor::on_message(const Message* const message) {
channel_spectrum.on_message(message); channel_spectrum.on_message(message);
break; break;
case Message::ID::SamplerateConfig: { case Message::ID::SampleRateConfig:
auto config = reinterpret_cast<const SamplerateConfigMessage*>(message); sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
baseband_fs = config->sample_rate;
update_for_rate_change();
break; break;
}
case Message::ID::OversampleRateConfig: {
auto config = reinterpret_cast<const OversampleRateConfigMessage*>(message);
oversample_rate = config->oversample_rate;
update_for_rate_change();
break;
}
case Message::ID::CaptureConfig: case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message)); capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
@ -89,13 +79,15 @@ void CaptureProcessor::on_message(const Message* const message) {
} }
} }
void CaptureProcessor::update_for_rate_change() { void CaptureProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
baseband_fs = message.sample_rate * toUType(message.oversample_rate);
oversample_rate = message.oversample_rate;
baseband_thread.set_sampling_rate(baseband_fs); baseband_thread.set_sampling_rate(baseband_fs);
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x auto decim_0_factor = oversample_rate == OversampleRate::x8
? decim_0_4.decimation_factor ? decim_0_4.decimation_factor
: decim_0_8.decimation_factor; : decim_0_8.decimation_factor;
size_t decim_0_output_fs = baseband_fs / decim_0_factor; size_t decim_0_output_fs = baseband_fs / decim_0_factor;
size_t decim_1_input_fs = decim_0_output_fs; size_t decim_1_input_fs = decim_0_output_fs;
@ -119,10 +111,10 @@ void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) { buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) {
switch (oversample_rate) { switch (oversample_rate) {
case OversampleRate::Rate8x: case OversampleRate::x8:
return decim_0_4.execute(src, dst); return decim_0_4.execute(src, dst);
case OversampleRate::Rate16x: case OversampleRate::x16:
return decim_0_8.execute(src, dst); return decim_0_8.execute(src, dst);
default: default:

View File

@ -42,7 +42,7 @@ class CaptureProcessor : public BasebandProcessor {
void on_message(const Message* const message) override; void on_message(const Message* const message) override;
private: private:
size_t baseband_fs = 3072000; size_t baseband_fs = 3072000; // aka: sample_rate
static constexpr auto spectrum_rate_hz = 50.0f; static constexpr auto spectrum_rate_hz = 50.0f;
std::array<complex16_t, 512> dst{}; std::array<complex16_t, 512> dst{};
@ -67,15 +67,14 @@ class CaptureProcessor : public BasebandProcessor {
SpectrumCollector channel_spectrum{}; SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0; size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0; size_t spectrum_samples = 0;
OversampleRate oversample_rate{OversampleRate::Rate8x}; OversampleRate oversample_rate{OversampleRate::x8};
/* NB: Threads should be the last members in the class definition. */ /* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{ BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false}; baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
RSSIThread rssi_thread{}; RSSIThread rssi_thread{};
/* Called to update members when the sample rate or oversample rate is changed. */ void sample_rate_config(const SampleRateConfigMessage& message);
void update_for_rate_change();
void capture_config(const CaptureConfigMessage& message); void capture_config(const CaptureConfigMessage& message);
/* Dispatch to the correct decim_0 based on oversample rate. */ /* Dispatch to the correct decim_0 based on oversample rate. */

View File

@ -83,8 +83,8 @@ void GPSReplayProcessor::on_message(const Message* const message) {
channel_spectrum.on_message(message); channel_spectrum.on_message(message);
break; break;
case Message::ID::SamplerateConfig: case Message::ID::SampleRateConfig:
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message)); sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break; break;
case Message::ID::ReplayConfig: case Message::ID::ReplayConfig:
@ -103,7 +103,7 @@ void GPSReplayProcessor::on_message(const Message* const message) {
} }
} }
void GPSReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) { void GPSReplayProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
baseband_fs = message.sample_rate; baseband_fs = message.sample_rate;
baseband_thread.set_sampling_rate(baseband_fs); baseband_thread.set_sampling_rate(baseband_fs);
spectrum_interval_samples = baseband_fs / spectrum_rate_hz; spectrum_interval_samples = baseband_fs / spectrum_rate_hz;

View File

@ -64,7 +64,7 @@ class GPSReplayProcessor : public BasebandProcessor {
bool configured{false}; bool configured{false};
uint32_t bytes_read{0}; uint32_t bytes_read{0};
void samplerate_config(const SamplerateConfigMessage& message); void sample_rate_config(const SampleRateConfigMessage& message);
void replay_config(const ReplayConfigMessage& message); void replay_config(const ReplayConfigMessage& message);
TXProgressMessage txprogress_message{}; TXProgressMessage txprogress_message{};

View File

@ -41,43 +41,75 @@ ReplayProcessor::ReplayProcessor() {
baseband_thread.start(); baseband_thread.start();
} }
void ReplayProcessor::execute(const buffer_c8_t& buffer) { // Change to 1 to enable buffer assertions in replay.
/* 4MHz, 2048 samples */ #define BUFFER_SIZE_ASSERT 0
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
if (!configured || !stream) return; if (!configured || !stream) return;
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / 8}; // Because this is actually adding samples, alias
// oversample_rate so the math below is more clear.
const size_t interpolation_factor = toUType(oversample_rate);
// File data is in C16 format, we need C8 // Wrap the IQ data array in a buffer with the correct sample_rate.
// File samplerate is 500kHz, we're at 4MHz buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / interpolation_factor};
// iq_buffer can only be 512 C16 samples (RAM limitation)
// To fill up the 2048-sample C8 buffer, we need:
// 2048 samples * 2 bytes per sample = 4096 bytes
// Since we're oversampling by 4M/500k = 8, we only need 2048/8 = 256 samples from the file and duplicate them 8 times each
// So 256 * 4 bytes per sample (C16) = 1024 bytes from the file
const size_t bytes_to_read = sizeof(*buffer.p) * 2 * (buffer.count / 8); // *2 (C16), /8 (oversampling) should be == 1024
size_t bytes_read_this_iteration = stream->read(iq_buffer.p, bytes_to_read);
size_t oversamples_this_iteration = bytes_read_this_iteration * 8 / (sizeof(*buffer.p) * 2);
bytes_read += bytes_read_this_iteration; // The IQ data in stream is C16 format and needs to be converted to C8 (N * 2).
// The data also needs to be interpolated so the effective sample rate is closer
// to 4Mhz. Because interpolation repeats a sample multiple times, fewer bytes
// are needed from the source stream in order to fill the buffer (count / oversample).
// Together the C16->C8 conversion and the interpolation give the number of
// bytes that need to be read from the source stream.
const size_t samples_to_read = buffer.count / interpolation_factor;
const size_t bytes_to_read = samples_to_read * sizeof(buffer_c16_t::Type);
// Fill and "stretch" #if BUFFER_SIZE_ASSERT
for (size_t i = 0; i < oversamples_this_iteration; i++) { // Verify the output buffer size is divisible by the interpolation factor.
if (i & 7) { if (samples_to_read * interpolation_factor != buffer.count)
buffer.p[i] = buffer.p[i - 1]; chDbgPanic("Output not div.");
} else {
auto re_out = iq_buffer.p[i >> 3].real() >> 8; // Is the input smaple buffer big enough?
auto im_out = iq_buffer.p[i >> 3].imag() >> 8; if (samples_to_read > iq_buffer.size())
buffer.p[i] = {(int8_t)re_out, (int8_t)im_out}; chDbgPanic("IQ buf ovf.");
#endif
// Read the C16 IQ data from the source stream.
size_t current_bytes_read = stream->read(iq_buffer.p, bytes_to_read);
// Compute the number of samples were actually read from the source.
size_t samples_read = current_bytes_read / sizeof(buffer_c16_t::Type);
// Write converted source samples to the output buffer with interpolation.
for (auto i = 0u; i < samples_read; ++i) {
int8_t re_out = iq_buffer.p[i].real() >> 8;
int8_t im_out = iq_buffer.p[i].imag() >> 8;
auto out_value = buffer_c8_t::Type{re_out, im_out};
// Interpolate sample.
for (auto j = 0u; j < interpolation_factor; ++j) {
size_t index = i * interpolation_factor + j;
buffer.p[index] = out_value;
#if BUFFER_SIZE_ASSERT
// Verify the index is within bounds.
if (index >= buffer.count)
chDbgPanic("Output bounds");
#endif
} }
} }
spectrum_samples += oversamples_this_iteration; // Update tracking stats.
bytes_read += current_bytes_read;
spectrum_samples += samples_read * interpolation_factor;
if (spectrum_samples >= spectrum_interval_samples) { if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples; spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(iq_buffer, channel_filter_low_f, channel_filter_high_f, channel_filter_transition); channel_spectrum.feed(
iq_buffer, channel_filter_low_f,
channel_filter_high_f, channel_filter_transition);
txprogress_message.progress = bytes_read; // Inform UI about progress // Inform UI about progress.
txprogress_message.progress = bytes_read;
txprogress_message.done = false; txprogress_message.done = false;
shared_memory.application_queue.push(txprogress_message); shared_memory.application_queue.push(txprogress_message);
} }
@ -90,8 +122,8 @@ void ReplayProcessor::on_message(const Message* const message) {
channel_spectrum.on_message(message); channel_spectrum.on_message(message);
break; break;
case Message::ID::SamplerateConfig: case Message::ID::SampleRateConfig:
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message)); sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
break; break;
case Message::ID::ReplayConfig: case Message::ID::ReplayConfig:
@ -110,9 +142,11 @@ void ReplayProcessor::on_message(const Message* const message) {
} }
} }
void ReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) { void ReplayProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
baseband_fs = message.sample_rate; baseband_fs = message.sample_rate * toUType(message.oversample_rate);
oversample_rate = message.oversample_rate;
baseband_thread.set_sampling_rate(baseband_fs); baseband_thread.set_sampling_rate(baseband_fs);
spectrum_interval_samples = baseband_fs / spectrum_rate_hz; spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
} }

View File

@ -44,6 +44,7 @@ class ReplayProcessor : public BasebandProcessor {
size_t baseband_fs = 3072000; size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f; static constexpr auto spectrum_rate_hz = 50.0f;
// Holds the read IQ data chunk from the file to send.
std::array<complex16_t, 256> iq{}; std::array<complex16_t, 256> iq{};
int32_t channel_filter_low_f = 0; int32_t channel_filter_low_f = 0;
@ -58,8 +59,9 @@ class ReplayProcessor : public BasebandProcessor {
bool configured{false}; bool configured{false};
uint32_t bytes_read{0}; uint32_t bytes_read{0};
OversampleRate oversample_rate = OversampleRate::x8;
void samplerate_config(const SamplerateConfigMessage& message); void sample_rate_config(const SampleRateConfigMessage& message);
void replay_config(const ReplayConfigMessage& message); void replay_config(const ReplayConfigMessage& message);
TXProgressMessage txprogress_message{}; TXProgressMessage txprogress_message{};

View File

@ -49,6 +49,7 @@ using Timestamp = lpc43xx::rtc::RTC;
template <typename T> template <typename T>
struct buffer_t { struct buffer_t {
using Type = T;
T* const p; T* const p;
const size_t count; const size_t count;
const uint32_t sampling_rate; const uint32_t sampling_rate;

View File

@ -78,7 +78,7 @@ class Message {
ReplayThreadDone = 21, ReplayThreadDone = 21,
AFSKRxConfigure = 22, AFSKRxConfigure = 22,
StatusRefresh = 23, StatusRefresh = 23,
SamplerateConfig = 24, SampleRateConfig = 24,
BTLERxConfigure = 25, BTLERxConfigure = 25,
NRFRxConfigure = 26, NRFRxConfigure = 26,
TXProgress = 27, TXProgress = 27,
@ -111,7 +111,6 @@ class Message {
APRSRxConfigure = 54, APRSRxConfigure = 54,
SpectrumPainterBufferRequestConfigure = 55, SpectrumPainterBufferRequestConfigure = 55,
SpectrumPainterBufferResponseConfigure = 56, SpectrumPainterBufferResponseConfigure = 56,
OversampleRateConfig = 57,
MAX MAX
}; };
@ -799,32 +798,37 @@ class RetuneMessage : public Message {
uint32_t range = 0; uint32_t range = 0;
}; };
class SamplerateConfigMessage : public Message { /* Oversample/Interpolation sample rate multipliers. */
public:
constexpr SamplerateConfigMessage(
const uint32_t sample_rate)
: Message{ID::SamplerateConfig},
sample_rate(sample_rate) {
}
const uint32_t sample_rate = 0;
};
/* Controls decimation handling in proc_capture. */
enum class OversampleRate : uint8_t { enum class OversampleRate : uint8_t {
Rate8x = 8, /* Use either to indicate there's no oversampling needed. */
Rate16x = 16, None = 1,
x1 = None,
// 4x would make sense to have, but need to ensure it doesn't
// overrun the IQ read buffer in proc_replay.
/* Oversample rate of 8 times the sample rate. */
x8 = 8,
/* Oversample rate of 16 times the sample rate. */
x16 = 16,
/* Oversample rate of 32 times the sample rate. */
x32 = 32,
}; };
class OversampleRateConfigMessage : public Message { class SampleRateConfigMessage : public Message {
public: public:
constexpr OversampleRateConfigMessage( constexpr SampleRateConfigMessage(
uint32_t sample_rate,
OversampleRate oversample_rate) OversampleRate oversample_rate)
: Message{ID::OversampleRateConfig}, : Message{ID::SampleRateConfig},
sample_rate(sample_rate),
oversample_rate(oversample_rate) { oversample_rate(oversample_rate) {
} }
const OversampleRate oversample_rate{OversampleRate::Rate8x}; const uint32_t sample_rate = 0;
const OversampleRate oversample_rate = OversampleRate::None;
}; };
class AudioLevelReportMessage : public Message { class AudioLevelReportMessage : public Message {

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2023 Kyle Reed, zxkmm
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
/* Helpers for handling oversampling and interpolation. */
#ifndef __OVERSAMPLE_H__
#define __OVERSAMPLE_H__
#include "message.hpp"
#include "utility.hpp"
/* TODO:
* The decision to oversample/interpolate should only be a baseband concern.
* However, the baseband can't set up the radio (M0 code), so the apps also
* need to know about the "actual" sample rate so the radio settings can be
* applied correctly. Ideally the baseband would tell the apps what the
* actual sample rate is. Currently the apps are telling the baseband and
* that feels like a separation of concerns problem. */
/* HackRF suggests a minimum sample rate of 2M so a oversample rate is applied
* to the sample rate (pre-scale) to get the sample rate closer to that target.
* The baseband needs to know how to correctly decimate (or interpolate) so
* the set of allowed scalars is fixed (See OversampleRate enum).
* In testing, a minimum rate of 400kHz seems to the functional minimum.
*/
/* Gets the oversample rate for a given sample rate.
* The oversample rate is used to increase the sample rate to improve SNR and quality.
* This is also used as the interpolation rate when replaying captures. */
inline OversampleRate get_oversample_rate(uint32_t sample_rate) {
if (sample_rate < 25'000) return OversampleRate::x32;
if (sample_rate < 50'000) return OversampleRate::x16;
return OversampleRate::x8;
}
/* Gets the actual sample rate for a given sample rate.
* This is the rate with the correct oversampling rate applied. */
inline uint32_t get_actual_sample_rate(uint32_t sample_rate) {
return sample_rate * toUType(get_oversample_rate(sample_rate));
}
#endif /*__OVERSAMPLE_H__*/