mirror of
https://github.com/eried/portapack-mayhem.git
synced 2024-10-01 01:26:06 -04:00
Improved trimming (#1458)
* Add threshold UI * WIP Better trimming * Rewrite mostly done WIP * WIP - trim idea * WIP threshold trimming * WIP with new design * Cleanup
This commit is contained in:
parent
ef03f020ce
commit
a6a1483083
@ -182,6 +182,7 @@ set(CPPSRC
|
|||||||
io_convert.cpp
|
io_convert.cpp
|
||||||
io_file.cpp
|
io_file.cpp
|
||||||
io_wave.cpp
|
io_wave.cpp
|
||||||
|
iq_trim.cpp
|
||||||
irq_controls.cpp
|
irq_controls.cpp
|
||||||
irq_lcd_frame.cpp
|
irq_lcd_frame.cpp
|
||||||
irq_rtc.cpp
|
irq_rtc.cpp
|
||||||
|
@ -30,41 +30,72 @@ namespace fs = std::filesystem;
|
|||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
IQTrimView::IQTrimView(NavigationView& nav) {
|
IQTrimView::IQTrimView(NavigationView& nav)
|
||||||
|
: nav_{nav} {
|
||||||
add_children({
|
add_children({
|
||||||
&labels,
|
&labels,
|
||||||
&field_path,
|
&field_path,
|
||||||
&text_range,
|
&field_start,
|
||||||
|
&field_end,
|
||||||
|
&text_samples,
|
||||||
|
&text_max,
|
||||||
|
&field_cutoff,
|
||||||
&button_trim,
|
&button_trim,
|
||||||
});
|
});
|
||||||
|
|
||||||
field_path.on_select = [this, &nav](TextField&) {
|
field_path.on_select = [this](TextField&) {
|
||||||
auto open_view = nav.push<FileLoadView>(".C*");
|
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) {
|
||||||
read_capture(path);
|
|
||||||
path_ = std::move(path);
|
path_ = std::move(path);
|
||||||
|
profile_capture();
|
||||||
|
compute_range();
|
||||||
refresh_ui();
|
refresh_ui();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
button_trim.on_select = [this, &nav](Button&) {
|
text_samples.set_style(&Styles::light_grey);
|
||||||
if (!path_.empty() && trim_range_.file_size > 0) {
|
text_max.set_style(&Styles::light_grey);
|
||||||
progress_ui.show_trimming();
|
|
||||||
TrimFile(path_, trim_range_);
|
field_start.on_change = [this](int32_t v) {
|
||||||
read_capture(path_);
|
if (field_end.value() < v)
|
||||||
|
field_end.set_value(v, false);
|
||||||
|
set_dirty();
|
||||||
|
};
|
||||||
|
|
||||||
|
field_end.on_change = [this](int32_t v) {
|
||||||
|
if (field_start.value() > v)
|
||||||
|
field_start.set_value(v, false);
|
||||||
|
set_dirty();
|
||||||
|
};
|
||||||
|
|
||||||
|
field_cutoff.set_value(7); // 7% of max is a good default.
|
||||||
|
field_cutoff.on_change = [this](int32_t) {
|
||||||
|
compute_range();
|
||||||
|
refresh_ui();
|
||||||
|
};
|
||||||
|
|
||||||
|
button_trim.on_select = [this](Button&) {
|
||||||
|
if (trim_capture()) {
|
||||||
|
profile_capture();
|
||||||
|
compute_range();
|
||||||
refresh_ui();
|
refresh_ui();
|
||||||
} else {
|
|
||||||
nav.display_modal("Error", "Select a file first.");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void IQTrimView::paint(Painter& painter) {
|
void IQTrimView::paint(Painter& painter) {
|
||||||
if (!path_.empty()) {
|
if (info_) {
|
||||||
|
uint32_t power_cutoff = field_cutoff.value() * static_cast<uint64_t>(info_->max_power) / 100;
|
||||||
|
|
||||||
// Draw power buckets.
|
// Draw power buckets.
|
||||||
for (size_t i = 0; i < power_buckets_.size(); ++i) {
|
for (size_t i = 0; i < power_buckets_.size(); ++i) {
|
||||||
auto amp = power_buckets_[i].power;
|
auto power = power_buckets_[i].power;
|
||||||
|
uint8_t amp = 0;
|
||||||
|
|
||||||
|
if (power > power_cutoff && info_->max_power > 0)
|
||||||
|
amp = (255ULL * power) / info_->max_power;
|
||||||
|
|
||||||
painter.draw_vline(
|
painter.draw_vline(
|
||||||
pos_lines + Point{(int)i, 0},
|
pos_lines + Point{(int)i, 0},
|
||||||
height_lines,
|
height_lines,
|
||||||
@ -72,8 +103,8 @@ void IQTrimView::paint(Painter& painter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw trim range edges.
|
// Draw trim range edges.
|
||||||
int start_x = screen_width * trim_range_.start / trim_range_.file_size;
|
int start_x = screen_width * field_start.value() / info_->sample_count;
|
||||||
int end_x = screen_width * trim_range_.end / trim_range_.file_size;
|
int end_x = screen_width * field_end.value() / info_->sample_count;
|
||||||
|
|
||||||
painter.draw_vline(
|
painter.draw_vline(
|
||||||
pos_lines + Point{start_x, 0},
|
pos_lines + Point{start_x, 0},
|
||||||
@ -92,27 +123,72 @@ void IQTrimView::focus() {
|
|||||||
|
|
||||||
void IQTrimView::refresh_ui() {
|
void IQTrimView::refresh_ui() {
|
||||||
field_path.set_text(path_.filename().string());
|
field_path.set_text(path_.filename().string());
|
||||||
text_range.set(to_string_dec_uint(trim_range_.start) + "-" + to_string_dec_uint(trim_range_.end));
|
text_samples.set(to_string_dec_uint(info_->sample_count));
|
||||||
|
text_max.set(to_string_dec_uint(info_->max_power));
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IQTrimView::read_capture(const fs::path& path) {
|
void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
|
||||||
|
auto max_range = info_ ? info_->sample_count : 0;
|
||||||
|
auto step = info_ ? info_->sample_count / screen_width : 0;
|
||||||
|
|
||||||
|
field_start.set_range(0, max_range);
|
||||||
|
field_start.set_step(step);
|
||||||
|
field_end.set_range(0, max_range);
|
||||||
|
field_end.set_step(step);
|
||||||
|
|
||||||
|
field_start.set_value(trim_range.start_sample, false);
|
||||||
|
field_end.set_value(trim_range.end_sample, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IQTrimView::profile_capture() {
|
||||||
power_buckets_ = {};
|
power_buckets_ = {};
|
||||||
PowerBuckets buckets{
|
iq::PowerBuckets buckets{
|
||||||
.p = power_buckets_.data(),
|
.p = power_buckets_.data(),
|
||||||
.size = power_buckets_.size()};
|
.size = power_buckets_.size()};
|
||||||
|
|
||||||
progress_ui.show_reading();
|
progress_ui.show_reading();
|
||||||
auto range = ComputeTrimRange(path, amp_threshold, &buckets, progress_ui.get_callback());
|
info_ = iq::profile_capture(path_, buckets);
|
||||||
progress_ui.clear();
|
progress_ui.clear();
|
||||||
|
|
||||||
if (range) {
|
|
||||||
trim_range_ = *range;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
trim_range_ = {};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
void IQTrimView::compute_range() {
|
||||||
|
if (!info_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
iq::PowerBuckets buckets{
|
||||||
|
.p = power_buckets_.data(),
|
||||||
|
.size = power_buckets_.size()};
|
||||||
|
auto trim_range = iq::compute_trim_range(*info_, buckets, field_cutoff.value());
|
||||||
|
|
||||||
|
update_range_controls(trim_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IQTrimView::trim_capture() {
|
||||||
|
if (!info_) {
|
||||||
|
nav_.display_modal("Error", "Open a file first.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool trimmed = false;
|
||||||
|
iq::TrimRange trim_range{
|
||||||
|
static_cast<uint32_t>(field_start.value()),
|
||||||
|
static_cast<uint32_t>(field_end.value()),
|
||||||
|
info_->sample_size};
|
||||||
|
|
||||||
|
if (trim_range.start_sample >= trim_range.end_sample) {
|
||||||
|
nav_.display_modal("Error", "Invalid trimming range.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress_ui.show_trimming();
|
||||||
|
trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback());
|
||||||
|
progress_ui.clear();
|
||||||
|
|
||||||
|
if (!trimmed)
|
||||||
|
nav_.display_modal("Error", "Trimming failed.");
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include "file.hpp"
|
#include "file.hpp"
|
||||||
#include "iq_trim.hpp"
|
#include "iq_trim.hpp"
|
||||||
|
#include "optional.hpp"
|
||||||
#include "ui.hpp"
|
#include "ui.hpp"
|
||||||
#include "ui_navigation.hpp"
|
#include "ui_navigation.hpp"
|
||||||
#include "ui_styles.hpp"
|
#include "ui_styles.hpp"
|
||||||
@ -49,7 +50,7 @@ class TrimProgressUI {
|
|||||||
|
|
||||||
void show_progress(uint8_t percent) {
|
void show_progress(uint8_t percent) {
|
||||||
auto width = percent * screen_width / 100;
|
auto width = percent * screen_width / 100;
|
||||||
p.draw_hline({0, 6 * 16}, width, Color::yellow());
|
p.draw_hline({0, 6 * 16 + 2}, width, Color::yellow());
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
@ -73,18 +74,36 @@ class IQTrimView : public View {
|
|||||||
void focus() override;
|
void focus() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/* Update controls with latest values. */
|
||||||
void refresh_ui();
|
void refresh_ui();
|
||||||
bool read_capture(const std::filesystem::path& path);
|
|
||||||
|
/* Update the start/end controls with trim range info. */
|
||||||
|
void update_range_controls(iq::TrimRange trim_range);
|
||||||
|
|
||||||
|
/* Collect capture info and samples to draw the UI. */
|
||||||
|
void profile_capture();
|
||||||
|
|
||||||
|
/* Determine the start and end buckets based on the cutoff. */
|
||||||
|
void compute_range();
|
||||||
|
|
||||||
|
/* Trims the capture file based on the settings. */
|
||||||
|
bool trim_capture();
|
||||||
|
|
||||||
|
NavigationView& nav_;
|
||||||
|
|
||||||
std::filesystem::path path_{};
|
std::filesystem::path path_{};
|
||||||
TrimRange trim_range_{};
|
Optional<iq::CaptureInfo> info_{};
|
||||||
std::array<PowerBuckets::Bucket, screen_width> power_buckets_{};
|
std::array<iq::PowerBuckets::Bucket, screen_width> power_buckets_{};
|
||||||
uint8_t amp_threshold = 5;
|
|
||||||
TrimProgressUI progress_ui{};
|
TrimProgressUI progress_ui{};
|
||||||
|
|
||||||
Labels labels{
|
Labels labels{
|
||||||
{{0 * 8, 0 * 16}, "Capture File:", Color::light_grey()},
|
{{0 * 8, 0 * 16}, "Capture File:", Color::light_grey()},
|
||||||
{{0 * 8, 6 * 16}, "Range:", Color::light_grey()},
|
{{0 * 8, 6 * 16}, "Start :", Color::light_grey()},
|
||||||
|
{{0 * 8, 7 * 16}, "End :", Color::light_grey()},
|
||||||
|
{{0 * 8, 8 * 16}, "Samples:", Color::light_grey()},
|
||||||
|
{{0 * 8, 9 * 16}, "Max Pwr:", Color::light_grey()},
|
||||||
|
{{0 * 8, 10 * 16}, "Cutoff :", Color::light_grey()},
|
||||||
|
{{12 * 8, 10 * 16}, "%", Color::light_grey()},
|
||||||
};
|
};
|
||||||
|
|
||||||
TextField field_path{
|
TextField field_path{
|
||||||
@ -94,12 +113,37 @@ class IQTrimView : public View {
|
|||||||
Point pos_lines{0 * 8, 4 * 16};
|
Point pos_lines{0 * 8, 4 * 16};
|
||||||
Dim height_lines{2 * 16};
|
Dim height_lines{2 * 16};
|
||||||
|
|
||||||
Text text_range{
|
NumberField field_start{
|
||||||
{7 * 8, 6 * 16, 20 * 8, 1 * 16},
|
{9 * 8, 6 * 16},
|
||||||
{}};
|
10,
|
||||||
|
{0, 0},
|
||||||
|
1,
|
||||||
|
' '};
|
||||||
|
|
||||||
|
NumberField field_end{
|
||||||
|
{9 * 8, 7 * 16},
|
||||||
|
10,
|
||||||
|
{0, 0},
|
||||||
|
1,
|
||||||
|
' '};
|
||||||
|
|
||||||
|
Text text_samples{
|
||||||
|
{9 * 8, 8 * 16, 10 * 8, 1 * 16},
|
||||||
|
"0"};
|
||||||
|
|
||||||
|
Text text_max{
|
||||||
|
{9 * 8, 9 * 16, 10 * 8, 1 * 16},
|
||||||
|
"0"};
|
||||||
|
|
||||||
|
NumberField field_cutoff{
|
||||||
|
{9 * 8, 10 * 16},
|
||||||
|
3,
|
||||||
|
{1, 100},
|
||||||
|
1,
|
||||||
|
' '};
|
||||||
|
|
||||||
Button button_trim{
|
Button button_trim{
|
||||||
{11 * 8, 9 * 16, 8 * 8, 3 * 16},
|
{20 * 8, 16 * 16, 8 * 8, 2 * 16},
|
||||||
"Trim"};
|
"Trim"};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -279,46 +279,6 @@ std::vector<std::filesystem::path> scan_root_directories(const std::filesystem::
|
|||||||
return directory_list;
|
return directory_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::filesystem_error trim_file(const std::filesystem::path& file_path, uint64_t start, uint64_t length) {
|
|
||||||
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
|
|
||||||
uint8_t buffer[buffer_size];
|
|
||||||
auto temp_path = file_path + u"-tmp";
|
|
||||||
|
|
||||||
/* Scope for File instances. */
|
|
||||||
{
|
|
||||||
File src;
|
|
||||||
File dst;
|
|
||||||
|
|
||||||
auto error = src.open(file_path);
|
|
||||||
if (error) return error.value();
|
|
||||||
|
|
||||||
error = dst.create(temp_path);
|
|
||||||
if (error) return error.value();
|
|
||||||
|
|
||||||
src.seek(start);
|
|
||||||
auto remaining = length;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
auto result = src.read(buffer, buffer_size);
|
|
||||||
if (result.is_error()) return result.error();
|
|
||||||
|
|
||||||
auto to_write = std::min(remaining, *result);
|
|
||||||
|
|
||||||
result = dst.write(buffer, to_write);
|
|
||||||
if (result.is_error()) return result.error();
|
|
||||||
|
|
||||||
remaining -= *result;
|
|
||||||
|
|
||||||
if (*result < buffer_size || remaining == 0)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete original and overwrite with temp file.
|
|
||||||
delete_file(file_path);
|
|
||||||
return rename_file(temp_path, file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path) {
|
std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path) {
|
||||||
return {f_unlink(reinterpret_cast<const TCHAR*>(file_path.c_str()))};
|
return {f_unlink(reinterpret_cast<const TCHAR*>(file_path.c_str()))};
|
||||||
}
|
}
|
||||||
|
@ -265,7 +265,6 @@ struct FATTimestamp {
|
|||||||
uint16_t FAT_time;
|
uint16_t FAT_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::filesystem::filesystem_error trim_file(const std::filesystem::path& file_path, uint64_t start, uint64_t length);
|
|
||||||
std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path);
|
std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path);
|
||||||
std::filesystem::filesystem_error rename_file(const std::filesystem::path& file_path, const std::filesystem::path& new_name);
|
std::filesystem::filesystem_error rename_file(const std::filesystem::path& file_path, const std::filesystem::path& new_name);
|
||||||
std::filesystem::filesystem_error copy_file(const std::filesystem::path& file_path, const std::filesystem::path& dest_path);
|
std::filesystem::filesystem_error copy_file(const std::filesystem::path& file_path, const std::filesystem::path& dest_path);
|
||||||
@ -302,6 +301,7 @@ static_assert(sizeof(FIL::err) == 1, "FatFs FIL::err size not expected.");
|
|||||||
#define FR_BAD_SEEK (0x102)
|
#define FR_BAD_SEEK (0x102)
|
||||||
#define FR_UNEXPECTED (0x103)
|
#define FR_UNEXPECTED (0x103)
|
||||||
|
|
||||||
|
/* NOTE: sizeof(File) == 556 bytes because of the FIL's buf member. */
|
||||||
class File {
|
class File {
|
||||||
public:
|
public:
|
||||||
using Size = uint64_t;
|
using Size = uint64_t;
|
||||||
|
195
firmware/application/iq_trim.cpp
Normal file
195
firmware/application/iq_trim.cpp
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Kyle Reed
|
||||||
|
*
|
||||||
|
* 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 "iq_trim.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "string_format.hpp"
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace iq {
|
||||||
|
|
||||||
|
/* Trimming helpers based on the sample type (complex8 or complex16). */
|
||||||
|
template <typename T>
|
||||||
|
uint32_t power(T value) {
|
||||||
|
auto real = value.real();
|
||||||
|
auto imag = value.imag();
|
||||||
|
return (real * real) + (imag * imag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collects capture file metadata and sample power buckets. */
|
||||||
|
template <typename T>
|
||||||
|
Optional<CaptureInfo> profile_capture(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
PowerBuckets& buckets,
|
||||||
|
uint8_t samples_per_bucket) {
|
||||||
|
File f;
|
||||||
|
auto error = f.open(path);
|
||||||
|
if (error)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
CaptureInfo info{
|
||||||
|
.file_size = f.size(),
|
||||||
|
.sample_count = f.size() / sizeof(T),
|
||||||
|
.sample_size = sizeof(T),
|
||||||
|
.max_power = 0};
|
||||||
|
|
||||||
|
auto profile_samples = buckets.size * samples_per_bucket;
|
||||||
|
auto sample_interval = info.sample_count / profile_samples;
|
||||||
|
uint32_t bucket_width = std::max(1ULL, info.sample_count / buckets.size);
|
||||||
|
uint64_t sample_index = 0;
|
||||||
|
T value{};
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
f.seek(sample_index * info.sample_size);
|
||||||
|
auto result = f.read(&value, info.sample_size);
|
||||||
|
if (!result) return {}; // Read failed.
|
||||||
|
|
||||||
|
if (*result != info.sample_size)
|
||||||
|
break; // EOF
|
||||||
|
|
||||||
|
auto mag_squared = power(value);
|
||||||
|
|
||||||
|
if (mag_squared > info.max_power)
|
||||||
|
info.max_power = mag_squared;
|
||||||
|
|
||||||
|
auto bucket_index = sample_index / bucket_width;
|
||||||
|
buckets.add(bucket_index, mag_squared);
|
||||||
|
sample_index += sample_interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<CaptureInfo> profile_capture(
|
||||||
|
const fs::path& path,
|
||||||
|
PowerBuckets& buckets,
|
||||||
|
uint8_t samples_per_bucket) {
|
||||||
|
auto sample_size = fs::capture_file_sample_size(path);
|
||||||
|
|
||||||
|
switch (sample_size) {
|
||||||
|
case sizeof(complex16_t):
|
||||||
|
return profile_capture<complex16_t>(path, buckets, samples_per_bucket);
|
||||||
|
|
||||||
|
case sizeof(complex8_t):
|
||||||
|
return profile_capture<complex8_t>(path, buckets, samples_per_bucket);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TrimRange compute_trim_range(
|
||||||
|
CaptureInfo info,
|
||||||
|
const PowerBuckets& buckets,
|
||||||
|
uint8_t cutoff_percent) {
|
||||||
|
bool has_start = false;
|
||||||
|
uint8_t start_bucket = 0;
|
||||||
|
uint8_t end_bucket = 0;
|
||||||
|
|
||||||
|
uint32_t power_cutoff = cutoff_percent * static_cast<uint64_t>(info.max_power) / 100;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < buckets.size; ++i) {
|
||||||
|
auto power = buckets.p[i].power;
|
||||||
|
|
||||||
|
if (power > power_cutoff) {
|
||||||
|
if (has_start)
|
||||||
|
end_bucket = i;
|
||||||
|
else {
|
||||||
|
start_bucket = i;
|
||||||
|
end_bucket = i;
|
||||||
|
has_start = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End should be the first bucket after the last with signal.
|
||||||
|
// This makes the math downstream simpler. NB: may be > buckets.size.
|
||||||
|
end_bucket++;
|
||||||
|
|
||||||
|
auto samples_per_bucket = info.sample_count / buckets.size;
|
||||||
|
return {
|
||||||
|
start_bucket * samples_per_bucket,
|
||||||
|
end_bucket * samples_per_bucket,
|
||||||
|
info.sample_size};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool trim_capture_with_range(
|
||||||
|
const fs::path& path,
|
||||||
|
TrimRange range,
|
||||||
|
const std::function<void(uint8_t)>& on_progress) {
|
||||||
|
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
|
||||||
|
uint8_t buffer[buffer_size];
|
||||||
|
auto temp_path = path + u"-tmp";
|
||||||
|
|
||||||
|
// end_sample is the first sample to _not_ include.
|
||||||
|
auto start_byte = range.start_sample * range.sample_size;
|
||||||
|
auto end_byte = (range.end_sample * range.sample_size);
|
||||||
|
auto length = end_byte - start_byte;
|
||||||
|
|
||||||
|
// 'File' is 556 bytes! Heap alloc to avoid overflowing the stack.
|
||||||
|
auto src = std::make_unique<File>();
|
||||||
|
auto dst = std::make_unique<File>();
|
||||||
|
|
||||||
|
auto error = src->open(path);
|
||||||
|
if (error) return false;
|
||||||
|
|
||||||
|
error = dst->create(temp_path);
|
||||||
|
if (error) return false;
|
||||||
|
|
||||||
|
src->seek(start_byte);
|
||||||
|
auto processed = 0UL;
|
||||||
|
auto next_report = 0UL;
|
||||||
|
auto report_interval = length / 20UL;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
auto result = src->read(buffer, buffer_size);
|
||||||
|
if (result.is_error()) return false;
|
||||||
|
|
||||||
|
auto remaining = length - processed;
|
||||||
|
auto to_write = std::min(remaining, *result);
|
||||||
|
|
||||||
|
result = dst->write(buffer, to_write);
|
||||||
|
if (result.is_error()) return false;
|
||||||
|
|
||||||
|
processed += *result;
|
||||||
|
|
||||||
|
if (*result < buffer_size || processed >= length)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (processed >= next_report) {
|
||||||
|
on_progress(100 * processed / length);
|
||||||
|
next_report += report_interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close files before renaming/deleting.
|
||||||
|
src.reset();
|
||||||
|
dst.reset();
|
||||||
|
|
||||||
|
// Delete original and overwrite with temp file.
|
||||||
|
delete_file(path);
|
||||||
|
rename_file(temp_path, path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace iq
|
@ -29,146 +29,65 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
struct TrimRange {
|
namespace iq {
|
||||||
uint64_t start;
|
|
||||||
uint64_t end;
|
/* Information about a capture. */
|
||||||
uint64_t sample_count;
|
struct CaptureInfo {
|
||||||
uint64_t file_size;
|
uint64_t file_size;
|
||||||
|
uint64_t sample_count;
|
||||||
uint8_t sample_size;
|
uint8_t sample_size;
|
||||||
|
uint32_t max_power;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Holds sample average power by bucket. */
|
||||||
struct PowerBuckets {
|
struct PowerBuckets {
|
||||||
struct Bucket {
|
struct Bucket {
|
||||||
uint8_t power;
|
uint32_t power = 0;
|
||||||
|
uint8_t count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
Bucket* p = nullptr;
|
Bucket* p = nullptr;
|
||||||
size_t size = 0;
|
const size_t size = 0;
|
||||||
|
|
||||||
void add(size_t index) {
|
/* Add the power to the bucket average at index. */
|
||||||
// This originally was meant to be an average power for the bucket,
|
void add(size_t index, uint32_t power) {
|
||||||
// but it was a lot of extra math just for a little bit of UI and
|
|
||||||
// the math really slowed down processing. Instead, just count the
|
|
||||||
// number of samples above the threshold.
|
|
||||||
if (index < size) {
|
if (index < size) {
|
||||||
if (p[index].power < 255)
|
auto& b = p[index];
|
||||||
p[index].power++;
|
auto avg = static_cast<uint64_t>(b.power) * b.count;
|
||||||
|
|
||||||
|
b.count++;
|
||||||
|
b.power = (power + avg) / b.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool TrimFile(const std::filesystem::path& path, TrimRange range) {
|
/* Data needed to trim a capture by sample range.
|
||||||
// NB: range.end should be included in the trimmed result, so '+ sample_size'.
|
* end_sample is the sample *after* the last to keep. */
|
||||||
auto result = trim_file(path, range.start, (range.end - range.start) + range.sample_size);
|
struct TrimRange {
|
||||||
return result.ok();
|
uint64_t start_sample;
|
||||||
}
|
uint64_t end_sample;
|
||||||
|
uint8_t sample_size;
|
||||||
template <typename T>
|
|
||||||
class IQTrimmer {
|
|
||||||
static constexpr uint8_t max_amp = 0xFF;
|
|
||||||
static constexpr typename T::value_type max_value = std::numeric_limits<typename T::value_type>::max();
|
|
||||||
static constexpr uint32_t max_mag_squared{2 * (max_value * max_value)}; // NB: Braces to detect narrowing.
|
|
||||||
|
|
||||||
public:
|
|
||||||
static Optional<TrimRange> ComputeTrimRange(
|
|
||||||
const std::filesystem::path& path,
|
|
||||||
uint8_t amp_threshold, // 0 - 255
|
|
||||||
PowerBuckets* buckets,
|
|
||||||
std::function<void(uint8_t)> on_progress) {
|
|
||||||
TrimRange range{};
|
|
||||||
|
|
||||||
File f;
|
|
||||||
auto error = f.open(path);
|
|
||||||
if (error)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
|
|
||||||
uint8_t buffer[buffer_size];
|
|
||||||
|
|
||||||
bool has_start = false;
|
|
||||||
size_t sample_index = 0;
|
|
||||||
File::Offset offset = 0;
|
|
||||||
uint8_t last_progress = 0;
|
|
||||||
size_t samples_per_bucket = 1;
|
|
||||||
// Scale from 0-255 to 0-max_mag_squared.
|
|
||||||
uint32_t threshold = (max_mag_squared * amp_threshold) / max_amp;
|
|
||||||
T value{};
|
|
||||||
|
|
||||||
range.file_size = f.size();
|
|
||||||
range.sample_size = sizeof(T);
|
|
||||||
range.sample_count = range.file_size / range.sample_size;
|
|
||||||
|
|
||||||
if (buckets)
|
|
||||||
samples_per_bucket = std::max(1ULL, range.sample_count / buckets->size);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
auto result = f.read(buffer, buffer_size);
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
for (size_t i = 0; i < *result; i += sizeof(T)) {
|
|
||||||
++sample_index;
|
|
||||||
|
|
||||||
value = *reinterpret_cast<T*>(&buffer[i]);
|
|
||||||
auto real = value.real();
|
|
||||||
auto imag = value.imag();
|
|
||||||
uint32_t mag_squared = (real * real) + (imag * imag);
|
|
||||||
|
|
||||||
// Update range if above threshold.
|
|
||||||
if (mag_squared >= threshold) {
|
|
||||||
if (has_start) {
|
|
||||||
range.end = offset + i;
|
|
||||||
} else {
|
|
||||||
range.start = offset + i;
|
|
||||||
range.end = range.start;
|
|
||||||
has_start = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the optional power bucket.
|
|
||||||
if (buckets) {
|
|
||||||
auto bucket_index = sample_index / samples_per_bucket;
|
|
||||||
buckets->add(bucket_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (*result < buffer_size)
|
|
||||||
break;
|
|
||||||
|
|
||||||
offset += *result;
|
|
||||||
|
|
||||||
if (on_progress) {
|
|
||||||
uint8_t current_progress = 100 * offset / range.file_size;
|
|
||||||
if (last_progress != current_progress) {
|
|
||||||
on_progress(current_progress);
|
|
||||||
last_progress = current_progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline Optional<TrimRange> ComputeTrimRange(
|
/* Collects capture file metadata and samples power buckets. */
|
||||||
|
Optional<CaptureInfo> profile_capture(
|
||||||
const std::filesystem::path& path,
|
const std::filesystem::path& path,
|
||||||
uint8_t amp_threshold = 5,
|
PowerBuckets& buckets,
|
||||||
PowerBuckets* buckets = nullptr,
|
uint8_t samples_per_bucket = 10);
|
||||||
std::function<void(uint8_t)> on_progress = nullptr) {
|
|
||||||
Optional<TrimRange> range;
|
|
||||||
auto sample_size = std::filesystem::capture_file_sample_size(path);
|
|
||||||
|
|
||||||
switch (sample_size) {
|
/* Computes the trimming range given profiling info.
|
||||||
case sizeof(complex16_t):
|
* Cutoff percent is a number 1-100. */
|
||||||
return IQTrimmer<complex16_t>::ComputeTrimRange(path, amp_threshold, buckets, on_progress);
|
TrimRange compute_trim_range(
|
||||||
|
CaptureInfo info,
|
||||||
|
const PowerBuckets& buckets,
|
||||||
|
uint8_t cutoff_percent);
|
||||||
|
|
||||||
case sizeof(complex8_t):
|
/* Trims the capture file with the specified range. */
|
||||||
return IQTrimmer<complex8_t>::ComputeTrimRange(path, amp_threshold, buckets, on_progress);
|
bool trim_capture_with_range(
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
TrimRange range,
|
||||||
|
const std::function<void(uint8_t)>& on_progress);
|
||||||
|
|
||||||
default:
|
} // namespace iq
|
||||||
return {};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif /*__IQ_TRIM_H__*/
|
#endif /*__IQ_TRIM_H__*/
|
||||||
|
@ -36,6 +36,7 @@ using namespace portapack;
|
|||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
@ -303,17 +304,25 @@ void RecordView::update_status_display() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RecordView::trim_capture() {
|
void RecordView::trim_capture() {
|
||||||
if (file_type != FileType::WAV && auto_trim && !trim_path.empty()) {
|
using bucket_t = iq::PowerBuckets::Bucket;
|
||||||
trim_ui.show_reading();
|
|
||||||
auto range = ComputeTrimRange(
|
if (file_type != FileType::WAV && auto_trim && !trim_path.empty()) {
|
||||||
trim_path,
|
// Need to heap alloc the buckets in this case. The large static buffer overflows the stack.
|
||||||
/*threshold*/ 5,
|
std::vector<bucket_t> buckets(size_t(255), bucket_t{});
|
||||||
/*power array*/ nullptr,
|
;
|
||||||
trim_ui.get_callback());
|
iq::PowerBuckets power_buckets{
|
||||||
|
.p = &buckets[0],
|
||||||
|
.size = buckets.size()};
|
||||||
|
|
||||||
|
trim_ui.show_reading();
|
||||||
|
auto info = iq::profile_capture(trim_path, power_buckets);
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
// 7% - decent trimming without being too aggressive.
|
||||||
|
auto trim_range = iq::compute_trim_range(*info, power_buckets, 7);
|
||||||
|
|
||||||
if (range) {
|
|
||||||
trim_ui.show_trimming();
|
trim_ui.show_trimming();
|
||||||
TrimFile(trim_path, *range);
|
iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback());
|
||||||
}
|
}
|
||||||
|
|
||||||
trim_ui.clear();
|
trim_ui.clear();
|
||||||
|
@ -1987,6 +1987,9 @@ void SymField::set_value(uint64_t value) {
|
|||||||
value_[i] = uint_to_char(temp, radix);
|
value_[i] = uint_to_char(temp, radix);
|
||||||
v /= radix;
|
v /= radix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (on_change)
|
||||||
|
on_change(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SymField::set_value(std::string_view value) {
|
void SymField::set_value(std::string_view value) {
|
||||||
|
Loading…
Reference in New Issue
Block a user