diff --git a/firmware/application/apps/capture_app.cpp b/firmware/application/apps/capture_app.cpp index 43a5d1f2f..37a9919d3 100644 --- a/firmware/application/apps/capture_app.cpp +++ b/firmware/application/apps/capture_app.cpp @@ -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(); }; diff --git a/firmware/application/apps/replay_app.cpp b/firmware/application/apps/replay_app.cpp index 31809dbf0..279f35dde 100644 --- a/firmware/application/apps/replay_app.cpp +++ b/firmware/application/apps/replay_app.cpp @@ -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( 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(); diff --git a/firmware/application/apps/ui_playlist.cpp b/firmware/application/apps/ui_playlist.cpp index ecc37d7a4..c4edbaac0 100644 --- a/firmware/application/apps/ui_playlist.cpp +++ b/firmware/application/apps/ui_playlist.cpp @@ -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 #include @@ -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); diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index d702fde7b..3ae12c5bd 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -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); } diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index d4ade99d9..df652d906 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -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); diff --git a/firmware/application/replay_thread.cpp b/firmware/application/replay_thread.cpp index 02f5b0132..caa83e803 100644 --- a/firmware/application/replay_thread.cpp +++ b/firmware/application/replay_thread.cpp @@ -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; } diff --git a/firmware/application/ui/ui_spectrum.cpp b/firmware/application/ui/ui_spectrum.cpp index 816f3ea35..03adfa069 100644 --- a/firmware/application/ui/ui_spectrum.cpp +++ b/firmware/application/ui/ui_spectrum.cpp @@ -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; } } diff --git a/firmware/application/ui_record_view.cpp b/firmware/application/ui_record_view.cpp index b1eac5d3d..e5f1e880f 100644 --- a/firmware/application/ui_record_view.cpp +++ b/firmware/application/ui_record_view.cpp @@ -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; diff --git a/firmware/application/ui_record_view.hpp b/firmware/application/ui_record_view.hpp index fedec2075..710ff8862 100644 --- a/firmware/application/ui_record_view.hpp +++ b/firmware/application/ui_record_view.hpp @@ -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{ diff --git a/firmware/baseband/proc_audiotx.cpp b/firmware/baseband/proc_audiotx.cpp index 39d2ea1f6..22d237a46 100644 --- a/firmware/baseband/proc_audiotx.cpp +++ b/firmware/baseband/proc_audiotx.cpp @@ -77,8 +77,8 @@ void AudioTXProcessor::on_message(const Message* const message) { replay_config(*reinterpret_cast(message)); break; - case Message::ID::SamplerateConfig: - samplerate_config(*reinterpret_cast(message)); + case Message::ID::SampleRateConfig: + sample_rate_config(*reinterpret_cast(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 } diff --git a/firmware/baseband/proc_audiotx.hpp b/firmware/baseband/proc_audiotx.hpp index db1dea011..d6cb06e5b 100644 --- a/firmware/baseband/proc_audiotx.hpp +++ b/firmware/baseband/proc_audiotx.hpp @@ -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); diff --git a/firmware/baseband/proc_capture.cpp b/firmware/baseband/proc_capture.cpp index 20004e328..19fa0b93f 100644 --- a/firmware/baseband/proc_capture.cpp +++ b/firmware/baseband/proc_capture.cpp @@ -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(message); - baseband_fs = config->sample_rate; - update_for_rate_change(); + case Message::ID::SampleRateConfig: + sample_rate_config(*reinterpret_cast(message)); break; - } - - case Message::ID::OversampleRateConfig: { - auto config = reinterpret_cast(message); - oversample_rate = config->oversample_rate; - update_for_rate_change(); - break; - } case Message::ID::CaptureConfig: capture_config(*reinterpret_cast(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: diff --git a/firmware/baseband/proc_capture.hpp b/firmware/baseband/proc_capture.hpp index 3bf17bd17..824aee53b 100644 --- a/firmware/baseband/proc_capture.hpp +++ b/firmware/baseband/proc_capture.hpp @@ -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 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. */ diff --git a/firmware/baseband/proc_gps_sim.cpp b/firmware/baseband/proc_gps_sim.cpp index e1db49887..11fe7c10d 100644 --- a/firmware/baseband/proc_gps_sim.cpp +++ b/firmware/baseband/proc_gps_sim.cpp @@ -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(message)); + case Message::ID::SampleRateConfig: + sample_rate_config(*reinterpret_cast(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; diff --git a/firmware/baseband/proc_gps_sim.hpp b/firmware/baseband/proc_gps_sim.hpp index 768cfc955..b518453ad 100644 --- a/firmware/baseband/proc_gps_sim.hpp +++ b/firmware/baseband/proc_gps_sim.hpp @@ -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{}; diff --git a/firmware/baseband/proc_replay.cpp b/firmware/baseband/proc_replay.cpp index 787211baa..7df2cb475 100644 --- a/firmware/baseband/proc_replay.cpp +++ b/firmware/baseband/proc_replay.cpp @@ -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(message)); + case Message::ID::SampleRateConfig: + sample_rate_config(*reinterpret_cast(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; } diff --git a/firmware/baseband/proc_replay.hpp b/firmware/baseband/proc_replay.hpp index f32c35f3e..885946dd6 100644 --- a/firmware/baseband/proc_replay.hpp +++ b/firmware/baseband/proc_replay.hpp @@ -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 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{}; diff --git a/firmware/common/buffer.hpp b/firmware/common/buffer.hpp index 975f1e698..b447565b3 100644 --- a/firmware/common/buffer.hpp +++ b/firmware/common/buffer.hpp @@ -49,6 +49,7 @@ using Timestamp = lpc43xx::rtc::RTC; template struct buffer_t { + using Type = T; T* const p; const size_t count; const uint32_t sampling_rate; diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index a87dc1be4..4704b92fc 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -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 { diff --git a/firmware/common/oversample.hpp b/firmware/common/oversample.hpp new file mode 100644 index 000000000..9510f4502 --- /dev/null +++ b/firmware/common/oversample.hpp @@ -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__*/