C8 capture support (#1286)

* C8 conversion

* C8 conversion

* C8 support

* C8 support

* C8 support

* C8 support

* Don't auto-convert GPS C8 files

* C8 support

* C8 support

* C8 support

* Remove hang workaround (different PR)

* Comment change

* Clang

* Clang

* Clang

* Merged change from PR #1287

* C8 support

* C8 support

* Improve bandwidth display

* Merged minor optimization from PR 1289

* Merge change from PR 1289

* Use complex types for C8/C16 conversion

* C8 support

* C8 support

* C8 support

* C8 support

* Roll back changes

* Roll back C8 changes

* C8 support

* C8 support

* C8 support

* C8 support

* C8 support

* Don't transmit samples past EOF

* Don't transmit samples past EOF

* Clang

* Clang attempt

* Clang attempt

* C8 support

* Clang
This commit is contained in:
Mark Thompson 2023-07-22 02:20:56 -05:00 committed by GitHub
parent 8eafe27955
commit d6b0173e7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 300 additions and 44 deletions

View File

@ -179,6 +179,7 @@ set(CPPSRC
file.cpp
freqman_db.cpp
freqman.cpp
io_convert.cpp
io_file.cpp
io_wave.cpp
irq_controls.cpp

View File

@ -44,6 +44,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
&field_lna,
&field_vga,
&option_bandwidth,
&option_format,
&record_view,
&waterfall,
});
@ -62,6 +63,11 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
this->field_frequency.set_step(v);
};
option_format.set_selected_index(0); // Default to C16
option_format.on_change = [this](size_t, uint32_t file_type) {
record_view.set_file_type((RecordView::FileType)file_type);
};
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */

View File

@ -58,6 +58,7 @@ class CaptureAppView : public View {
Labels labels{
{{0 * 8, 1 * 16}, "Rate:", Color::light_grey()},
{{11 * 8, 1 * 16}, "Format:", Color::light_grey()},
};
RSSI rssi{
@ -87,6 +88,12 @@ class CaptureAppView : public View {
5,
{}};
OptionsField option_format{
{18 * 8, 1 * 16},
3,
{{"C16", RecordView::FileType::RawS16},
{"C8", RecordView::FileType::RawS8}}};
RecordView record_view{
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
u"BBD_????.*",

View File

@ -26,6 +26,8 @@
#include "ui_fileman.hpp"
#include "io_file.hpp"
#include "io_convert.hpp"
#include "baseband_api.hpp"
#include "metadata_file.hpp"
#include "portapack.hpp"
@ -75,7 +77,8 @@ void ReplayAppView::on_file_changed(const fs::path& new_file_path) {
progressbar.set_max(file_size);
text_filename.set(truncate(file_path.filename().string(), 12));
auto duration = ms_duration(file_size, sample_rate, 4);
uint8_t sample_size = capture_file_sample_size(current()->path);
auto duration = ms_duration(file_size, sample_rate, sample_size);
text_duration.set(to_string_time_ms(duration));
button_play.focus();
@ -110,7 +113,7 @@ void ReplayAppView::start() {
std::unique_ptr<stream::Reader> reader;
auto p = std::make_unique<FileReader>();
auto p = std::make_unique<FileConvertReader>();
auto open_error = p->open(file_path);
if (open_error.is_valid()) {
file_error();
@ -191,7 +194,7 @@ ReplayAppView::ReplayAppView(
};
button_open.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".C16");
auto open_view = nav.push<FileLoadView>(".C*");
open_view->on_changed = [this](fs::path new_file_path) {
on_file_changed(new_file_path);
};

View File

@ -39,7 +39,10 @@ namespace fs = std::filesystem;
namespace ui {
static const fs::path txt_ext{u".TXT"};
static const fs::path ppl_ext{u".PPL"};
static const fs::path c8_ext{u".C8"};
static const fs::path c16_ext{u".C16"};
static const fs::path c32_ext{u".C32"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
static const fs::path bmp_ext{u".BMP"};
} // namespace ui
@ -78,14 +81,17 @@ fs::path get_partner_file(fs::path path) {
return {};
auto ext = path.extension();
if (path_iequal(ext, txt_ext))
ext = c16_ext;
else if (path_iequal(ext, c16_ext))
ext = txt_ext;
else
if (is_cxx_capture_file(path))
path.replace_extension(txt_ext);
else if (path_iequal(ext, txt_ext)) {
path.replace_extension(c8_ext);
if (!fs::file_exists(path))
path.replace_extension(c16_ext);
if (!fs::file_exists(path))
path.replace_extension(c32_ext);
} else
return {};
path.replace_extension(ext);
return fs::file_exists(path) && !fs::is_directory(path) ? path : fs::path{};
}
@ -141,6 +147,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
current_path = dir_path;
entry_list.clear();
auto filtering = !extension_filter.empty();
bool cxx_file = path_iequal(cxx_ext, extension_filter);
text_current.set(dir_path.empty() ? "(sd root)" : truncate(dir_path, 24));
@ -150,7 +157,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
continue;
if (fs::is_regular_file(entry.status())) {
if (!filtering || path_iequal(entry.path().extension(), extension_filter))
if (!filtering || path_iequal(entry.path().extension(), extension_filter) || (cxx_file && is_cxx_capture_file(entry.path())))
insert_sorted(entry_list, {entry.path(), (uint32_t)entry.size(), false});
} else if (fs::is_directory(entry.status())) {
insert_sorted(entry_list, {entry.path(), 0, true});
@ -497,7 +504,7 @@ bool FileManagerView::handle_file_open() {
if (path_iequal(txt_ext, ext)) {
nav_.push<TextEditorView>(path);
return true;
} else if (path_iequal(c16_ext, ext) || path_iequal(ppl_ext, ext)) {
} else if (is_cxx_capture_file(path) || path_iequal(ppl_ext, ext)) {
// TODO: Enough memory to push?
nav_.push<PlaylistView>(path);
return true;

View File

@ -27,6 +27,8 @@
#include "convert.hpp"
#include "file_reader.hpp"
#include "io_file.hpp"
#include "io_convert.hpp"
#include "string_format.hpp"
#include "ui_fileman.hpp"
#include "utility.hpp"
@ -48,7 +50,6 @@ namespace fs = std::filesystem;
namespace ui {
// TODO: consolidate extesions into a shared header?
static const fs::path c16_ext = u".C16";
static const fs::path ppl_ext = u".PPL";
void PlaylistView::load_file(const fs::path& playlist_path) {
@ -258,7 +259,7 @@ void PlaylistView::send_current_track() {
chThdSleepMilliseconds(current()->ms_delay);
// Open the sample file to send.
auto reader = std::make_unique<FileReader>();
auto reader = std::make_unique<FileConvertReader>();
auto error = reader->open(current()->path);
if (error) {
show_file_error(current()->path, "Can't open file to send.");
@ -323,9 +324,10 @@ void PlaylistView::update_ui() {
chDbgAssert(!at_end(), "update_ui #1", "current_index_ invalid");
text_filename.set(current()->path.filename().string());
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, 0) + "Hz");
text_sample_rate.set(unit_auto_scale(current()->metadata.sample_rate, 3, (current()->metadata.sample_rate > 1000000) ? 2 : 0) + "Hz");
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, 4);
uint8_t sample_size = capture_file_sample_size(current()->path);
auto duration = ms_duration(current()->file_size, current()->metadata.sample_rate, sample_size);
text_duration.set(to_string_time_ms(duration));
field_frequency.set_value(current()->metadata.center_frequency);
@ -336,7 +338,7 @@ void PlaylistView::update_ui() {
progressbar_track.set_max(playlist_db_.size() - 1);
progressbar_track.set_value(current_index_);
progressbar_transmit.set_max(current()->file_size);
progressbar_transmit.set_max(current()->file_size * sizeof(complex16_t) / sample_size);
}
button_play.set_bitmap(is_active() ? &bitmap_stop : &bitmap_play);
@ -406,7 +408,7 @@ PlaylistView::PlaylistView(
button_add.on_select = [this, &nav]() {
if (is_active())
return;
auto open_view = nav_.push<FileLoadView>(".C16");
auto open_view = nav_.push<FileLoadView>(".C*");
open_view->push_dir(u"CAPTURES");
open_view->on_changed = [this](fs::path path) {
add_entry(std::move(path));
@ -459,7 +461,7 @@ PlaylistView::PlaylistView(
auto ext = path.extension();
if (path_iequal(ext, ppl_ext))
on_file_changed(path);
else if (path_iequal(ext, c16_ext))
else if (is_cxx_capture_file(path))
add_entry(fs::path{path});
}

View File

@ -21,12 +21,18 @@
*/
#include "file.hpp"
#include "complex.hpp"
#include <algorithm>
#include <codecvt>
#include <cstring>
#include <locale>
namespace fs = std::filesystem;
static const fs::path c8_ext{u".C8"};
static const fs::path c16_ext{u".C16"};
static const fs::path c32_ext{u".C32"};
Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BYTE mode) {
auto result = f_open(&f, reinterpret_cast<const TCHAR*>(filename.c_str()), mode);
if (result == FR_OK) {
@ -507,6 +513,21 @@ bool path_iequal(
return false;
}
bool is_cxx_capture_file(const path& filename) {
auto ext = filename.extension();
return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext) || path_iequal(c32_ext, ext);
}
uint8_t capture_file_sample_size(const path& filename) {
if (path_iequal(filename.extension(), c8_ext))
return sizeof(complex8_t);
if (path_iequal(filename.extension(), c16_ext))
return sizeof(complex16_t);
if (path_iequal(filename.extension(), c32_ext))
return sizeof(complex32_t);
return 0;
}
directory_iterator::directory_iterator(
const std::filesystem::path& path,
const std::filesystem::path& wild)

View File

@ -168,6 +168,8 @@ path operator/(const path& lhs, const path& rhs);
/* Case insensitive path equality on underlying "native" string. */
bool path_iequal(const path& lhs, const path& rhs);
bool is_cxx_capture_file(const path& filename);
uint8_t capture_file_sample_size(const path& filename);
using file_status = BYTE;

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2023 Mark Thompson
*
* 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.
*/
#include "io_convert.hpp"
#include "complex.hpp"
namespace fs = std::filesystem;
static const fs::path c8_ext = u".C8";
static const fs::path c32_ext = u".C32";
namespace file_convert {
// Convert buffer contents from c16 to c8.
// Same buffer used for input & output; input size is bytes; output size is bytes/2.
void c16_to_c8(const void* buffer, File::Size bytes) {
complex16_t* src = (complex16_t*)buffer;
complex8_t* dest = (complex8_t*)buffer;
for (File::Size i = 0; i < bytes / sizeof(complex16_t); i++) {
auto re_out = src[i].real() >> 8;
auto im_out = src[i].imag() >> 8;
dest[i] = {(int8_t)re_out, (int8_t)im_out};
}
}
// Convert c8 buffer to c16 buffer.
// Same buffer used for input & output; input size is bytes; output size is 2*bytes.
void c8_to_c16(const void* buffer, File::Size bytes) {
complex8_t* src = (complex8_t*)buffer;
complex16_t* dest = (complex16_t*)buffer;
uint32_t i = bytes / sizeof(complex8_t);
if (i != 0) {
do {
i--;
auto re_out = (int16_t)src[i].real() * 256; // C8 to C16 conversion;
auto im_out = (int16_t)src[i].imag() * 256; // Can't shift signed numbers left, so using multiply
dest[i] = {(int16_t)re_out, (int16_t)im_out};
} while (i != 0);
}
}
// Convert c32 buffer to c16 buffer.
// Same buffer used for input & output; input size is bytes; output size is bytes/2.
void c32_to_c16(const void* buffer, File::Size bytes) {
complex32_t* src = (complex32_t*)buffer;
complex16_t* dest = (complex16_t*)buffer;
for (File::Size i = 0; i < bytes / sizeof(complex32_t); i++) {
auto re_out = src[i].real() >> 16;
auto im_out = src[i].imag() >> 16;
dest[i] = {(int8_t)re_out, (int8_t)im_out};
}
}
} /* namespace file_convert */
// Automatically enables C8/C16 or C32/C16 conversion based on file extension
Optional<File::Error> FileConvertReader::open(const std::filesystem::path& filename) {
convert_c8_to_c16 = path_iequal(filename.extension(), c8_ext);
convert_c32_to_c16 = path_iequal(filename.extension(), c32_ext);
return file_.open(filename);
}
// If C8 conversion enabled, half the number of bytes are read from the file & expanded to fill the whole buffer.
// If C32 conversion enabled, the full byte count is read from the file, and compressed to half the buffer size.
File::Result<File::Size> FileConvertReader::read(void* const buffer, const File::Size bytes) {
auto read_result = file_.read(buffer, convert_c8_to_c16 ? bytes / 2 : bytes);
if (read_result.is_ok()) {
if (convert_c8_to_c16) {
file_convert::c8_to_c16(buffer, read_result.value());
read_result = read_result.value() * 2;
} else if (convert_c32_to_c16) {
file_convert::c32_to_c16(buffer, read_result.value());
read_result = read_result.value() / 2;
}
bytes_read_ += read_result.value();
}
return read_result;
}
// Automatically enables C8/C16 conversion based on file extension
Optional<File::Error> FileConvertWriter::create(const std::filesystem::path& filename) {
convert_c16_to_c8 = path_iequal(filename.extension(), c8_ext);
return file_.create(filename);
}
// If C8 conversion is enabled, half the number of bytes are written to the file.
File::Result<File::Size> FileConvertWriter::write(const void* const buffer, const File::Size bytes) {
if (convert_c16_to_c8) {
file_convert::c16_to_c8(buffer, bytes);
}
auto write_result = file_.write(buffer, convert_c16_to_c8 ? bytes / 2 : bytes);
if (write_result.is_ok()) {
if (convert_c16_to_c8) {
write_result = write_result.value() * 2;
}
bytes_written_ += write_result.value();
}
return write_result;
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 Mark Thompson
*
* 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.
*/
#pragma once
#include "io_file.hpp"
#include "io.hpp"
#include "file.hpp"
#include "optional.hpp"
#include <cstdint>
namespace file_convert {
void c8_to_c16(const void* buffer, File::Size bytes);
void c32_to_c16(const void* buffer, File::Size bytes);
void c16_to_c8(const void* buffer, File::Size bytes);
} /* namespace file_convert */
class FileConvertReader : public stream::Reader {
public:
FileConvertReader() = default;
FileConvertReader(const FileConvertReader&) = delete;
FileConvertReader& operator=(const FileConvertReader&) = delete;
FileConvertReader(FileConvertReader&& file) = delete;
FileConvertReader& operator=(FileConvertReader&&) = delete;
Optional<File::Error> open(const std::filesystem::path& filename);
File::Result<File::Size> read(void* const buffer, const File::Size bytes) override;
const File& file() const& { return file_; }
bool convert_c8_to_c16{};
bool convert_c32_to_c16{};
protected:
File file_{};
uint64_t bytes_read_{0};
};
class FileConvertWriter : public stream::Writer {
public:
FileConvertWriter() = default;
FileConvertWriter(const FileConvertWriter&) = delete;
FileConvertWriter& operator=(const FileConvertWriter&) = delete;
FileConvertWriter(FileConvertWriter&& file) = delete;
FileConvertWriter& operator=(FileConvertWriter&&) = delete;
Optional<File::Error> create(const std::filesystem::path& filename);
File::Result<File::Size> write(const void* const buffer, const File::Size bytes) override;
const File& file() const& { return file_; }
bool convert_c16_to_c8{};
protected:
File file_{};
uint64_t bytes_written_{0};
};

View File

@ -26,6 +26,7 @@ using namespace portapack;
#include "io_file.hpp"
#include "io_wave.hpp"
#include "io_convert.hpp"
#include "baseband_api.hpp"
#include "metadata_file.hpp"
@ -194,6 +195,7 @@ void RecordView::start() {
}
} break;
case FileType::RawS8:
case FileType::RawS16: {
const auto metadata_file_error =
write_metadata_file(get_metadata_path(base_path),
@ -204,8 +206,8 @@ void RecordView::start() {
return;
}
auto p = std::make_unique<RawFileWriter>();
auto create_error = p->create(base_path.replace_extension(u".C16"));
auto p = std::make_unique<FileConvertWriter>();
auto create_error = p->create(base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16"));
if (create_error.is_valid()) {
handle_error(create_error.value());
} else {

View File

@ -40,8 +40,10 @@ class RecordView : public View {
std::function<void(std::string)> on_error{};
enum FileType {
RawS8 = 1,
RawS16 = 2,
WAV = 3,
RawS32 = 3,
WAV = 4,
};
RecordView(
@ -57,6 +59,8 @@ class RecordView : public View {
void set_sampling_rate(const size_t new_sampling_rate);
void set_file_type(const FileType v) { file_type = v; }
void start();
void stop();
void on_hide() override;
@ -83,7 +87,7 @@ class RecordView : public View {
const std::filesystem::path filename_stem_pattern;
const std::filesystem::path folder;
const FileType file_type;
FileType file_type;
const size_t write_size;
const size_t buffer_count;
size_t sampling_rate{0};

View File

@ -44,27 +44,27 @@ ReplayProcessor::ReplayProcessor() {
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
/* 2.6MHz, 2048 samples */
if (!configured) return;
if (!configured || !stream) return;
// File data is in C8 format, which is what we need
// File samplerate is 2.6MHz, which is what we need
// To fill up the 2048-sample C8 buffer @ 2 bytes per sample = 4096 bytes
if (stream) {
const size_t bytes_to_read = sizeof(*buffer.p) * 1 * (buffer.count);
size_t bytes_read_this_iteration = stream->read(iq_buffer.p, bytes_to_read);
bytes_read += bytes_read_this_iteration;
const size_t bytes_to_read = sizeof(*buffer.p) * 1 * (buffer.count);
size_t bytes_read_this_iteration = stream->read(iq_buffer.p, bytes_to_read);
size_t samples_read_this_iteration = bytes_read_this_iteration / sizeof(*buffer.p);
// NB: Couldn't we have just read the data into buffer.p to start with, or some DMA/cache coherency concern?
//
// for (size_t i = 0; i < buffer.count; i++) {
// auto re_out = iq_buffer.p[i].real();
// auto im_out = iq_buffer.p[i].imag();
// buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
// }
memcpy(buffer.p, iq_buffer.p, bytes_read_this_iteration); // memcpy should be more efficient than 1 byte at a time
}
bytes_read += bytes_read_this_iteration;
spectrum_samples += buffer.count;
// NB: Couldn't we have just read the data into buffer.p to start with, or some DMA/cache coherency concern?
//
// for (size_t i = 0; i < buffer.count; i++) {
// auto re_out = iq_buffer.p[i].real();
// auto im_out = iq_buffer.p[i].imag();
// buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
// }
memcpy(buffer.p, iq_buffer.p, bytes_read_this_iteration); // memcpy should be more efficient than 1 byte at a time
spectrum_samples += samples_read_this_iteration;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;

View File

@ -43,7 +43,7 @@ ReplayProcessor::ReplayProcessor() {
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
/* 4MHz, 2048 samples */
if (!configured) return;
if (!configured || !stream) return;
// File data is in C16 format, we need C8
// File samplerate is 500kHz, we're at 4MHz
@ -52,13 +52,14 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
// 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
if (stream) {
const size_t bytes_to_read = sizeof(*buffer.p) * 2 * (buffer.count / 8); // *2 (C16), /8 (oversampling) should be == 1024
bytes_read += stream->read(iq_buffer.p, bytes_to_read);
}
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;
// Fill and "stretch"
for (size_t i = 0; i < buffer.count; i++) {
for (size_t i = 0; i < oversamples_this_iteration; i++) {
if (i & 7) {
buffer.p[i] = buffer.p[i - 1];
} else {
@ -68,7 +69,7 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
}
}
spectrum_samples += buffer.count;
spectrum_samples += oversamples_this_iteration;
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);