mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2024-12-14 12:08:40 +00:00
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:
parent
8eafe27955
commit
d6b0173e7a
@ -179,6 +179,7 @@ set(CPPSRC
|
|||||||
file.cpp
|
file.cpp
|
||||||
freqman_db.cpp
|
freqman_db.cpp
|
||||||
freqman.cpp
|
freqman.cpp
|
||||||
|
io_convert.cpp
|
||||||
io_file.cpp
|
io_file.cpp
|
||||||
io_wave.cpp
|
io_wave.cpp
|
||||||
irq_controls.cpp
|
irq_controls.cpp
|
||||||
|
@ -44,6 +44,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
|||||||
&field_lna,
|
&field_lna,
|
||||||
&field_vga,
|
&field_vga,
|
||||||
&option_bandwidth,
|
&option_bandwidth,
|
||||||
|
&option_format,
|
||||||
&record_view,
|
&record_view,
|
||||||
&waterfall,
|
&waterfall,
|
||||||
});
|
});
|
||||||
@ -62,6 +63,11 @@ CaptureAppView::CaptureAppView(NavigationView& nav)
|
|||||||
this->field_frequency.set_step(v);
|
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) {
|
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
|
||||||
sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side
|
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. */
|
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||||
|
@ -58,6 +58,7 @@ class CaptureAppView : public View {
|
|||||||
|
|
||||||
Labels labels{
|
Labels labels{
|
||||||
{{0 * 8, 1 * 16}, "Rate:", Color::light_grey()},
|
{{0 * 8, 1 * 16}, "Rate:", Color::light_grey()},
|
||||||
|
{{11 * 8, 1 * 16}, "Format:", Color::light_grey()},
|
||||||
};
|
};
|
||||||
|
|
||||||
RSSI rssi{
|
RSSI rssi{
|
||||||
@ -87,6 +88,12 @@ class CaptureAppView : public View {
|
|||||||
5,
|
5,
|
||||||
{}};
|
{}};
|
||||||
|
|
||||||
|
OptionsField option_format{
|
||||||
|
{18 * 8, 1 * 16},
|
||||||
|
3,
|
||||||
|
{{"C16", RecordView::FileType::RawS16},
|
||||||
|
{"C8", RecordView::FileType::RawS8}}};
|
||||||
|
|
||||||
RecordView record_view{
|
RecordView record_view{
|
||||||
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
|
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
|
||||||
u"BBD_????.*",
|
u"BBD_????.*",
|
||||||
|
@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
#include "ui_fileman.hpp"
|
#include "ui_fileman.hpp"
|
||||||
#include "io_file.hpp"
|
#include "io_file.hpp"
|
||||||
|
#include "io_convert.hpp"
|
||||||
|
|
||||||
#include "baseband_api.hpp"
|
#include "baseband_api.hpp"
|
||||||
#include "metadata_file.hpp"
|
#include "metadata_file.hpp"
|
||||||
#include "portapack.hpp"
|
#include "portapack.hpp"
|
||||||
@ -75,7 +77,8 @@ void ReplayAppView::on_file_changed(const fs::path& new_file_path) {
|
|||||||
progressbar.set_max(file_size);
|
progressbar.set_max(file_size);
|
||||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
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));
|
text_duration.set(to_string_time_ms(duration));
|
||||||
|
|
||||||
button_play.focus();
|
button_play.focus();
|
||||||
@ -110,7 +113,7 @@ void ReplayAppView::start() {
|
|||||||
|
|
||||||
std::unique_ptr<stream::Reader> reader;
|
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);
|
auto open_error = p->open(file_path);
|
||||||
if (open_error.is_valid()) {
|
if (open_error.is_valid()) {
|
||||||
file_error();
|
file_error();
|
||||||
@ -191,7 +194,7 @@ ReplayAppView::ReplayAppView(
|
|||||||
};
|
};
|
||||||
|
|
||||||
button_open.on_select = [this, &nav](Button&) {
|
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) {
|
open_view->on_changed = [this](fs::path new_file_path) {
|
||||||
on_file_changed(new_file_path);
|
on_file_changed(new_file_path);
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,10 @@ namespace fs = std::filesystem;
|
|||||||
namespace ui {
|
namespace ui {
|
||||||
static const fs::path txt_ext{u".TXT"};
|
static const fs::path txt_ext{u".TXT"};
|
||||||
static const fs::path ppl_ext{u".PPL"};
|
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 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 png_ext{u".PNG"};
|
||||||
static const fs::path bmp_ext{u".BMP"};
|
static const fs::path bmp_ext{u".BMP"};
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
@ -78,14 +81,17 @@ fs::path get_partner_file(fs::path path) {
|
|||||||
return {};
|
return {};
|
||||||
auto ext = path.extension();
|
auto ext = path.extension();
|
||||||
|
|
||||||
if (path_iequal(ext, txt_ext))
|
if (is_cxx_capture_file(path))
|
||||||
ext = c16_ext;
|
path.replace_extension(txt_ext);
|
||||||
else if (path_iequal(ext, c16_ext))
|
else if (path_iequal(ext, txt_ext)) {
|
||||||
ext = txt_ext;
|
path.replace_extension(c8_ext);
|
||||||
else
|
if (!fs::file_exists(path))
|
||||||
|
path.replace_extension(c16_ext);
|
||||||
|
if (!fs::file_exists(path))
|
||||||
|
path.replace_extension(c32_ext);
|
||||||
|
} else
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
path.replace_extension(ext);
|
|
||||||
return fs::file_exists(path) && !fs::is_directory(path) ? path : fs::path{};
|
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;
|
current_path = dir_path;
|
||||||
entry_list.clear();
|
entry_list.clear();
|
||||||
auto filtering = !extension_filter.empty();
|
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));
|
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;
|
continue;
|
||||||
|
|
||||||
if (fs::is_regular_file(entry.status())) {
|
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});
|
insert_sorted(entry_list, {entry.path(), (uint32_t)entry.size(), false});
|
||||||
} else if (fs::is_directory(entry.status())) {
|
} else if (fs::is_directory(entry.status())) {
|
||||||
insert_sorted(entry_list, {entry.path(), 0, true});
|
insert_sorted(entry_list, {entry.path(), 0, true});
|
||||||
@ -497,7 +504,7 @@ bool FileManagerView::handle_file_open() {
|
|||||||
if (path_iequal(txt_ext, ext)) {
|
if (path_iequal(txt_ext, ext)) {
|
||||||
nav_.push<TextEditorView>(path);
|
nav_.push<TextEditorView>(path);
|
||||||
return true;
|
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?
|
// TODO: Enough memory to push?
|
||||||
nav_.push<PlaylistView>(path);
|
nav_.push<PlaylistView>(path);
|
||||||
return true;
|
return true;
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
#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 "string_format.hpp"
|
#include "string_format.hpp"
|
||||||
#include "ui_fileman.hpp"
|
#include "ui_fileman.hpp"
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
@ -48,7 +50,6 @@ namespace fs = std::filesystem;
|
|||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
// TODO: consolidate extesions into a shared header?
|
// TODO: consolidate extesions into a shared header?
|
||||||
static const fs::path c16_ext = u".C16";
|
|
||||||
static const fs::path ppl_ext = u".PPL";
|
static const fs::path ppl_ext = u".PPL";
|
||||||
|
|
||||||
void PlaylistView::load_file(const fs::path& playlist_path) {
|
void PlaylistView::load_file(const fs::path& playlist_path) {
|
||||||
@ -258,7 +259,7 @@ void PlaylistView::send_current_track() {
|
|||||||
chThdSleepMilliseconds(current()->ms_delay);
|
chThdSleepMilliseconds(current()->ms_delay);
|
||||||
|
|
||||||
// Open the sample file to send.
|
// Open the sample file to send.
|
||||||
auto reader = std::make_unique<FileReader>();
|
auto reader = std::make_unique<FileConvertReader>();
|
||||||
auto error = reader->open(current()->path);
|
auto error = reader->open(current()->path);
|
||||||
if (error) {
|
if (error) {
|
||||||
show_file_error(current()->path, "Can't open file to send.");
|
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");
|
chDbgAssert(!at_end(), "update_ui #1", "current_index_ invalid");
|
||||||
|
|
||||||
text_filename.set(current()->path.filename().string());
|
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));
|
text_duration.set(to_string_time_ms(duration));
|
||||||
field_frequency.set_value(current()->metadata.center_frequency);
|
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_max(playlist_db_.size() - 1);
|
||||||
progressbar_track.set_value(current_index_);
|
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);
|
button_play.set_bitmap(is_active() ? &bitmap_stop : &bitmap_play);
|
||||||
@ -406,7 +408,7 @@ PlaylistView::PlaylistView(
|
|||||||
button_add.on_select = [this, &nav]() {
|
button_add.on_select = [this, &nav]() {
|
||||||
if (is_active())
|
if (is_active())
|
||||||
return;
|
return;
|
||||||
auto open_view = nav_.push<FileLoadView>(".C16");
|
auto open_view = nav_.push<FileLoadView>(".C*");
|
||||||
open_view->push_dir(u"CAPTURES");
|
open_view->push_dir(u"CAPTURES");
|
||||||
open_view->on_changed = [this](fs::path path) {
|
open_view->on_changed = [this](fs::path path) {
|
||||||
add_entry(std::move(path));
|
add_entry(std::move(path));
|
||||||
@ -459,7 +461,7 @@ PlaylistView::PlaylistView(
|
|||||||
auto ext = path.extension();
|
auto ext = path.extension();
|
||||||
if (path_iequal(ext, ppl_ext))
|
if (path_iequal(ext, ppl_ext))
|
||||||
on_file_changed(path);
|
on_file_changed(path);
|
||||||
else if (path_iequal(ext, c16_ext))
|
else if (is_cxx_capture_file(path))
|
||||||
add_entry(fs::path{path});
|
add_entry(fs::path{path});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,12 +21,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "file.hpp"
|
#include "file.hpp"
|
||||||
|
#include "complex.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <codecvt>
|
#include <codecvt>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <locale>
|
#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) {
|
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);
|
auto result = f_open(&f, reinterpret_cast<const TCHAR*>(filename.c_str()), mode);
|
||||||
if (result == FR_OK) {
|
if (result == FR_OK) {
|
||||||
@ -507,6 +513,21 @@ bool path_iequal(
|
|||||||
return false;
|
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(
|
directory_iterator::directory_iterator(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
const std::filesystem::path& wild)
|
const std::filesystem::path& wild)
|
||||||
|
@ -168,6 +168,8 @@ path operator/(const path& lhs, const path& rhs);
|
|||||||
|
|
||||||
/* Case insensitive path equality on underlying "native" string. */
|
/* Case insensitive path equality on underlying "native" string. */
|
||||||
bool path_iequal(const path& lhs, const path& rhs);
|
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;
|
using file_status = BYTE;
|
||||||
|
|
||||||
|
119
firmware/application/io_convert.cpp
Normal file
119
firmware/application/io_convert.cpp
Normal 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;
|
||||||
|
}
|
81
firmware/application/io_convert.hpp
Normal file
81
firmware/application/io_convert.hpp
Normal 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};
|
||||||
|
};
|
@ -26,6 +26,7 @@ using namespace portapack;
|
|||||||
|
|
||||||
#include "io_file.hpp"
|
#include "io_file.hpp"
|
||||||
#include "io_wave.hpp"
|
#include "io_wave.hpp"
|
||||||
|
#include "io_convert.hpp"
|
||||||
|
|
||||||
#include "baseband_api.hpp"
|
#include "baseband_api.hpp"
|
||||||
#include "metadata_file.hpp"
|
#include "metadata_file.hpp"
|
||||||
@ -194,6 +195,7 @@ void RecordView::start() {
|
|||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
case FileType::RawS8:
|
||||||
case FileType::RawS16: {
|
case FileType::RawS16: {
|
||||||
const auto metadata_file_error =
|
const auto metadata_file_error =
|
||||||
write_metadata_file(get_metadata_path(base_path),
|
write_metadata_file(get_metadata_path(base_path),
|
||||||
@ -204,8 +206,8 @@ void RecordView::start() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto p = std::make_unique<RawFileWriter>();
|
auto p = std::make_unique<FileConvertWriter>();
|
||||||
auto create_error = p->create(base_path.replace_extension(u".C16"));
|
auto create_error = p->create(base_path.replace_extension((file_type == FileType::RawS8) ? u".C8" : u".C16"));
|
||||||
if (create_error.is_valid()) {
|
if (create_error.is_valid()) {
|
||||||
handle_error(create_error.value());
|
handle_error(create_error.value());
|
||||||
} else {
|
} else {
|
||||||
|
@ -40,8 +40,10 @@ class RecordView : public View {
|
|||||||
std::function<void(std::string)> on_error{};
|
std::function<void(std::string)> on_error{};
|
||||||
|
|
||||||
enum FileType {
|
enum FileType {
|
||||||
|
RawS8 = 1,
|
||||||
RawS16 = 2,
|
RawS16 = 2,
|
||||||
WAV = 3,
|
RawS32 = 3,
|
||||||
|
WAV = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
RecordView(
|
RecordView(
|
||||||
@ -57,6 +59,8 @@ class RecordView : public View {
|
|||||||
|
|
||||||
void set_sampling_rate(const size_t new_sampling_rate);
|
void set_sampling_rate(const size_t new_sampling_rate);
|
||||||
|
|
||||||
|
void set_file_type(const FileType v) { file_type = v; }
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
void on_hide() override;
|
void on_hide() override;
|
||||||
@ -83,7 +87,7 @@ class RecordView : public View {
|
|||||||
|
|
||||||
const std::filesystem::path filename_stem_pattern;
|
const std::filesystem::path filename_stem_pattern;
|
||||||
const std::filesystem::path folder;
|
const std::filesystem::path folder;
|
||||||
const 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};
|
size_t sampling_rate{0};
|
||||||
|
@ -44,14 +44,15 @@ ReplayProcessor::ReplayProcessor() {
|
|||||||
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
/* 2.6MHz, 2048 samples */
|
/* 2.6MHz, 2048 samples */
|
||||||
|
|
||||||
if (!configured) return;
|
if (!configured || !stream) return;
|
||||||
|
|
||||||
// File data is in C8 format, which is what we need
|
// File data is in C8 format, which is what we need
|
||||||
// File samplerate is 2.6MHz, 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
|
// 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);
|
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 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);
|
||||||
|
|
||||||
bytes_read += bytes_read_this_iteration;
|
bytes_read += bytes_read_this_iteration;
|
||||||
|
|
||||||
// NB: Couldn't we have just read the data into buffer.p to start with, or some DMA/cache coherency concern?
|
// NB: Couldn't we have just read the data into buffer.p to start with, or some DMA/cache coherency concern?
|
||||||
@ -62,9 +63,8 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
|||||||
// buffer.p[i] = {(int8_t)re_out, (int8_t)im_out};
|
// 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
|
memcpy(buffer.p, iq_buffer.p, bytes_read_this_iteration); // memcpy should be more efficient than 1 byte at a time
|
||||||
}
|
|
||||||
|
|
||||||
spectrum_samples += buffer.count;
|
spectrum_samples += samples_read_this_iteration;
|
||||||
if (spectrum_samples >= spectrum_interval_samples) {
|
if (spectrum_samples >= spectrum_interval_samples) {
|
||||||
spectrum_samples -= spectrum_interval_samples;
|
spectrum_samples -= spectrum_interval_samples;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ ReplayProcessor::ReplayProcessor() {
|
|||||||
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
/* 4MHz, 2048 samples */
|
/* 4MHz, 2048 samples */
|
||||||
|
|
||||||
if (!configured) return;
|
if (!configured || !stream) return;
|
||||||
|
|
||||||
// File data is in C16 format, we need C8
|
// File data is in C16 format, we need C8
|
||||||
// File samplerate is 500kHz, we're at 4MHz
|
// 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
|
// 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
|
// 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
|
// 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
|
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);
|
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"
|
// 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) {
|
if (i & 7) {
|
||||||
buffer.p[i] = buffer.p[i - 1];
|
buffer.p[i] = buffer.p[i - 1];
|
||||||
} else {
|
} 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) {
|
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);
|
||||||
|
Loading…
Reference in New Issue
Block a user