mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2024-12-04 23:45:26 +00:00
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:
parent
e2ad0a1b1a
commit
37386c29cb
@ -60,25 +60,28 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
};
|
||||
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
|
||||
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
|
||||
/* 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. 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);
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
waterfall.stop();
|
||||
record_view.set_sampling_rate(sampling_rate, oversample_rate); // NB: Actually updates the baseband.
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// 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();
|
||||
};
|
||||
|
||||
|
@ -124,7 +124,7 @@ void ReplayAppView::start() {
|
||||
|
||||
if (reader) {
|
||||
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>(
|
||||
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.enable();
|
||||
|
||||
|
@ -24,18 +24,18 @@
|
||||
|
||||
#include "ui_playlist.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "convert.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "io_convert.hpp"
|
||||
|
||||
#include "oversample.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
|
||||
@ -266,17 +266,16 @@ void PlaylistView::send_current_track() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ReplayThread starts immediately on construction so
|
||||
// these need to be set before creating the ReplayThread.
|
||||
// Update the sample rate in proc_replay baseband.
|
||||
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_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.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.
|
||||
progressbar_transmit.set_value(0);
|
||||
|
||||
|
@ -347,13 +347,8 @@ void spectrum_streaming_stop() {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_sample_rate(const uint32_t sample_rate) {
|
||||
SamplerateConfigMessage message{sample_rate};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_oversample_rate(OversampleRate oversample_rate) {
|
||||
OversampleRateConfigMessage message{oversample_rate};
|
||||
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate) {
|
||||
SampleRateConfigMessage message{sample_rate, oversample_rate};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
|
@ -94,8 +94,8 @@ void shutdown();
|
||||
void spectrum_streaming_start();
|
||||
void spectrum_streaming_stop();
|
||||
|
||||
void set_sample_rate(const uint32_t sample_rate);
|
||||
void set_oversample_rate(OversampleRate oversample_rate);
|
||||
/* NB: sample_rate should be desired rate. Don't pre-scale. */
|
||||
void set_sample_rate(uint32_t sample_rate, OversampleRate oversample_rate = OversampleRate::None);
|
||||
void capture_start(CaptureConfig* const config);
|
||||
void capture_stop();
|
||||
void replay_start(ReplayConfig* const config);
|
||||
|
@ -80,6 +80,8 @@ uint32_t ReplayThread::run() {
|
||||
chThdSleep(100);
|
||||
};
|
||||
|
||||
constexpr size_t block_size = 512;
|
||||
|
||||
// While empty buffers fifo is not empty...
|
||||
while (!buffers.empty()) {
|
||||
prefill_buffer = buffers.get_prefill();
|
||||
@ -87,10 +89,10 @@ uint32_t ReplayThread::run() {
|
||||
if (prefill_buffer == nullptr) {
|
||||
buffers.put_app(prefill_buffer);
|
||||
} else {
|
||||
size_t blocks = config.read_size / 512;
|
||||
size_t blocks = config.read_size / block_size;
|
||||
|
||||
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()) {
|
||||
return READ_ERROR;
|
||||
}
|
||||
|
@ -380,33 +380,34 @@ void WaterfallView::on_audio_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) {
|
||||
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.
|
||||
return 1750000; // Minimum BPF MAX2837 for all those lower BW options.
|
||||
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
|
||||
case 0 ... 2'000'000: // BW Captured range (0 <= 250kHz max) fs = 8 x 250 kHz.
|
||||
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
|
||||
// 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.
|
||||
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).
|
||||
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).
|
||||
return 3500000;
|
||||
case 8'800'000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz. (1Mhz showed slightly higher noise background).
|
||||
return 3'500'000;
|
||||
|
||||
case 14000000: // 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.
|
||||
return 5000000;
|
||||
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.
|
||||
return 5'000'000;
|
||||
|
||||
case 16000000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
|
||||
// Good BPF, good matching, but LCD flickers, refresh rate should be < 20 Hz, reasonable picture.
|
||||
return 6000000;
|
||||
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.
|
||||
return 6'000'000;
|
||||
|
||||
case 20000000: // 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.
|
||||
return 7000000;
|
||||
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.
|
||||
return 7'000'000;
|
||||
|
||||
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.
|
||||
return 8000000;
|
||||
return 8'000'000;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ using namespace portapack;
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
@ -101,27 +102,28 @@ void RecordView::focus() {
|
||||
button_record.focus();
|
||||
}
|
||||
|
||||
void RecordView::set_sampling_rate(size_t new_sampling_rate, OversampleRate new_oversample_rate) {
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
|
||||
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.
|
||||
uint32_t RecordView::set_sampling_rate(uint32_t new_sampling_rate) {
|
||||
// Determine the oversampling needed (if any) and the actual sampling rate.
|
||||
auto oversample_rate = get_oversample_rate(new_sampling_rate);
|
||||
auto actual_sampling_rate = new_sampling_rate * toUType(oversample_rate);
|
||||
|
||||
We keep original black background in all the correct IQ .C16 files BW's Options */
|
||||
if (new_sampling_rate > 4'800'000) { // > BW >600kHz (fs=8*BW), (750kHz...2750kHz)
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >600kHz
|
||||
* 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());
|
||||
} else {
|
||||
button_record.set_background(ui::Color::black());
|
||||
}
|
||||
|
||||
if (new_sampling_rate != sampling_rate ||
|
||||
new_oversample_rate != oversample_rate) {
|
||||
if (sampling_rate != new_sampling_rate) {
|
||||
stop();
|
||||
|
||||
sampling_rate = new_sampling_rate;
|
||||
oversample_rate = new_oversample_rate;
|
||||
baseband::set_sample_rate(sampling_rate);
|
||||
baseband::set_oversample_rate(oversample_rate);
|
||||
baseband::set_sample_rate(sampling_rate, oversample_rate);
|
||||
|
||||
button_record.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();
|
||||
}
|
||||
|
||||
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
|
||||
@ -202,8 +222,7 @@ void RecordView::start() {
|
||||
case FileType::RawS8:
|
||||
case FileType::RawS16: {
|
||||
const auto metadata_file_error = write_metadata_file(
|
||||
get_metadata_path(base_path),
|
||||
{receiver_model.target_frequency(), sampling_rate / toUType(oversample_rate)});
|
||||
get_metadata_path(base_path), {receiver_model.target_frequency(), sampling_rate});
|
||||
if (metadata_file_error.is_valid()) {
|
||||
handle_error(metadata_file_error.value());
|
||||
return;
|
||||
@ -278,12 +297,7 @@ void RecordView::update_status_display() {
|
||||
// - 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.
|
||||
const auto bytes_per_sample = file_type == FileType::RawS16 ? 4 : 2;
|
||||
// WAV files are not oversampled, but C8 and C16 are. Divide by the
|
||||
// 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 bytes_per_second = sampling_rate * bytes_per_sample;
|
||||
const uint32_t available_seconds = space_info.free / bytes_per_second;
|
||||
const uint32_t seconds = available_seconds % 60;
|
||||
const uint32_t available_minutes = available_seconds / 60;
|
||||
|
@ -56,16 +56,11 @@ class RecordView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
/* Sets the sampling rate and the oversampling "decimation" rate.
|
||||
* These values are passed down to the baseband proc_capture. For
|
||||
* Audio (WAV) recording, the OversampleRate should not be
|
||||
* specified and the default will be used. */
|
||||
/* TODO: Currently callers are expected to have already multiplied the
|
||||
* 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);
|
||||
/* Sets the sampling rate for the baseband.
|
||||
* NB: Do not pre-apply any oversampling. This function will determine
|
||||
* the correct amount of oversampling and return the actual sample rate
|
||||
* that can be used to configure the radio or other UI element. */
|
||||
uint32_t set_sampling_rate(uint32_t new_sampling_rate);
|
||||
|
||||
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_error(const File::Error error);
|
||||
|
||||
OversampleRate get_oversample_rate(uint32_t sample_rate);
|
||||
|
||||
// bool pitch_rssi_enabled = false;
|
||||
|
||||
// Time Stamp
|
||||
@ -98,8 +95,7 @@ class RecordView : public View {
|
||||
FileType file_type;
|
||||
const size_t write_size;
|
||||
const size_t buffer_count;
|
||||
size_t sampling_rate{0};
|
||||
OversampleRate oversample_rate{OversampleRate::Rate8x};
|
||||
uint32_t sampling_rate{0};
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Rectangle rect_background{
|
||||
|
@ -77,8 +77,8 @@ void AudioTXProcessor::on_message(const Message* const message) {
|
||||
replay_config(*reinterpret_cast<const ReplayConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig:
|
||||
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ class AudioTXProcessor : public BasebandProcessor {
|
||||
bool configured{false};
|
||||
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 replay_config(const ReplayConfigMessage& message);
|
||||
|
||||
|
@ -66,19 +66,9 @@ void CaptureProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig: {
|
||||
auto config = reinterpret_cast<const SamplerateConfigMessage*>(message);
|
||||
baseband_fs = config->sample_rate;
|
||||
update_for_rate_change();
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
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:
|
||||
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);
|
||||
|
||||
auto decim_0_factor = oversample_rate == OversampleRate::Rate8x
|
||||
auto decim_0_factor = oversample_rate == OversampleRate::x8
|
||||
? decim_0_4.decimation_factor
|
||||
: decim_0_8.decimation_factor;
|
||||
|
||||
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
|
||||
|
||||
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) {
|
||||
switch (oversample_rate) {
|
||||
case OversampleRate::Rate8x:
|
||||
case OversampleRate::x8:
|
||||
return decim_0_4.execute(src, dst);
|
||||
|
||||
case OversampleRate::Rate16x:
|
||||
case OversampleRate::x16:
|
||||
return decim_0_8.execute(src, dst);
|
||||
|
||||
default:
|
||||
|
@ -42,7 +42,7 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
size_t baseband_fs = 3072000;
|
||||
size_t baseband_fs = 3072000; // aka: sample_rate
|
||||
static constexpr auto spectrum_rate_hz = 50.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
@ -67,15 +67,14 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
SpectrumCollector channel_spectrum{};
|
||||
size_t spectrum_interval_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. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
/* Called to update members when the sample rate or oversample rate is changed. */
|
||||
void update_for_rate_change();
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
|
||||
/* Dispatch to the correct decim_0 based on oversample rate. */
|
||||
|
@ -83,8 +83,8 @@ void GPSReplayProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig:
|
||||
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
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_thread.set_sampling_rate(baseband_fs);
|
||||
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
|
||||
|
@ -64,7 +64,7 @@ class GPSReplayProcessor : public BasebandProcessor {
|
||||
bool configured{false};
|
||||
uint32_t bytes_read{0};
|
||||
|
||||
void samplerate_config(const SamplerateConfigMessage& message);
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void replay_config(const ReplayConfigMessage& message);
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
@ -41,43 +41,75 @@ ReplayProcessor::ReplayProcessor() {
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 4MHz, 2048 samples */
|
||||
// Change to 1 to enable buffer assertions in replay.
|
||||
#define BUFFER_SIZE_ASSERT 0
|
||||
|
||||
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||
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
|
||||
// File samplerate is 500kHz, we're at 4MHz
|
||||
// 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);
|
||||
// Wrap the IQ data array in a buffer with the correct sample_rate.
|
||||
buffer_c16_t iq_buffer{iq.data(), iq.size(), baseband_fs / interpolation_factor};
|
||||
|
||||
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"
|
||||
for (size_t i = 0; i < oversamples_this_iteration; i++) {
|
||||
if (i & 7) {
|
||||
buffer.p[i] = buffer.p[i - 1];
|
||||
} else {
|
||||
auto re_out = iq_buffer.p[i >> 3].real() >> 8;
|
||||
auto im_out = iq_buffer.p[i >> 3].imag() >> 8;
|
||||
buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
|
||||
#if BUFFER_SIZE_ASSERT
|
||||
// Verify the output buffer size is divisible by the interpolation factor.
|
||||
if (samples_to_read * interpolation_factor != buffer.count)
|
||||
chDbgPanic("Output not div.");
|
||||
|
||||
// Is the input smaple buffer big enough?
|
||||
if (samples_to_read > iq_buffer.size())
|
||||
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) {
|
||||
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;
|
||||
shared_memory.application_queue.push(txprogress_message);
|
||||
}
|
||||
@ -90,8 +122,8 @@ void ReplayProcessor::on_message(const Message* const message) {
|
||||
channel_spectrum.on_message(message);
|
||||
break;
|
||||
|
||||
case Message::ID::SamplerateConfig:
|
||||
samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
|
||||
case Message::ID::SampleRateConfig:
|
||||
sample_rate_config(*reinterpret_cast<const SampleRateConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::ReplayConfig:
|
||||
@ -110,9 +142,11 @@ void ReplayProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate;
|
||||
void ReplayProcessor::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);
|
||||
|
||||
spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ class ReplayProcessor : public BasebandProcessor {
|
||||
size_t baseband_fs = 3072000;
|
||||
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{};
|
||||
|
||||
int32_t channel_filter_low_f = 0;
|
||||
@ -58,8 +59,9 @@ class ReplayProcessor : public BasebandProcessor {
|
||||
|
||||
bool configured{false};
|
||||
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);
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
@ -49,6 +49,7 @@ using Timestamp = lpc43xx::rtc::RTC;
|
||||
|
||||
template <typename T>
|
||||
struct buffer_t {
|
||||
using Type = T;
|
||||
T* const p;
|
||||
const size_t count;
|
||||
const uint32_t sampling_rate;
|
||||
|
@ -78,7 +78,7 @@ class Message {
|
||||
ReplayThreadDone = 21,
|
||||
AFSKRxConfigure = 22,
|
||||
StatusRefresh = 23,
|
||||
SamplerateConfig = 24,
|
||||
SampleRateConfig = 24,
|
||||
BTLERxConfigure = 25,
|
||||
NRFRxConfigure = 26,
|
||||
TXProgress = 27,
|
||||
@ -111,7 +111,6 @@ class Message {
|
||||
APRSRxConfigure = 54,
|
||||
SpectrumPainterBufferRequestConfigure = 55,
|
||||
SpectrumPainterBufferResponseConfigure = 56,
|
||||
OversampleRateConfig = 57,
|
||||
MAX
|
||||
};
|
||||
|
||||
@ -799,32 +798,37 @@ class RetuneMessage : public Message {
|
||||
uint32_t range = 0;
|
||||
};
|
||||
|
||||
class SamplerateConfigMessage : public Message {
|
||||
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. */
|
||||
/* Oversample/Interpolation sample rate multipliers. */
|
||||
enum class OversampleRate : uint8_t {
|
||||
Rate8x = 8,
|
||||
Rate16x = 16,
|
||||
/* Use either to indicate there's no oversampling needed. */
|
||||
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:
|
||||
constexpr OversampleRateConfigMessage(
|
||||
constexpr SampleRateConfigMessage(
|
||||
uint32_t sample_rate,
|
||||
OversampleRate oversample_rate)
|
||||
: Message{ID::OversampleRateConfig},
|
||||
: Message{ID::SampleRateConfig},
|
||||
sample_rate(sample_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 {
|
||||
|
61
firmware/common/oversample.hpp
Normal file
61
firmware/common/oversample.hpp
Normal 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__*/
|
Loading…
Reference in New Issue
Block a user