From 86d4b1725723d84372ded0038b95947b6ed18d3d Mon Sep 17 00:00:00 2001 From: Mark Thompson <129641948+NotherNgineer@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:14:25 -0500 Subject: [PATCH] Simple amplification option in IQ Trim app (#1506) * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload --- firmware/application/apps/ui_iq_trim.cpp | 21 ++++++++- firmware/application/apps/ui_iq_trim.hpp | 11 ++++- firmware/application/iq_trim.cpp | 57 ++++++++++++++++++++++-- firmware/application/iq_trim.hpp | 11 ++++- firmware/application/ui_record_view.cpp | 2 +- 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/firmware/application/apps/ui_iq_trim.cpp b/firmware/application/apps/ui_iq_trim.cpp index 6f438538..edd944d2 100644 --- a/firmware/application/apps/ui_iq_trim.cpp +++ b/firmware/application/apps/ui_iq_trim.cpp @@ -40,6 +40,7 @@ IQTrimView::IQTrimView(NavigationView& nav) &text_samples, &text_max, &field_cutoff, + &field_amplify, &button_trim, }); @@ -72,6 +73,11 @@ IQTrimView::IQTrimView(NavigationView& nav) refresh_ui(); }; + field_amplify.set_value(1); // 1X is default (no amplification) + field_amplify.on_change = [this](int32_t) { + refresh_ui(); + }; + button_trim.on_select = [this](Button&) { if (trim_capture()) { profile_capture(); @@ -133,7 +139,18 @@ void IQTrimView::focus() { void IQTrimView::refresh_ui() { field_path.set_text(path_.filename().string()); text_samples.set(to_string_dec_uint(info_->sample_count)); - text_max.set(to_string_dec_uint(info_->max_power)); + + // show max power after amplification applied + uint64_t power_amp = field_amplify.value() * field_amplify.value() * field_amplify.value() * field_amplify.value(); + text_max.set(to_string_dec_uint(info_->max_power * power_amp)); + + // show max power in red if amplification is too high, causing clipping + uint32_t clipping_limit = (fs::capture_file_sample_size(path_) == sizeof(complex8_t)) ? 0x80 : 0x8000; + if ((field_amplify.value() * info_->max_iq) > clipping_limit) + text_max.set_style(&Styles::red); + else + text_max.set_style(&Styles::light_grey); + set_dirty(); } @@ -191,7 +208,7 @@ bool IQTrimView::trim_capture() { } progress_ui.show_trimming(); - trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback()); + trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback(), field_amplify.value()); progress_ui.clear(); if (!trimmed) diff --git a/firmware/application/apps/ui_iq_trim.hpp b/firmware/application/apps/ui_iq_trim.hpp index d9303d6b..3b8f044b 100644 --- a/firmware/application/apps/ui_iq_trim.hpp +++ b/firmware/application/apps/ui_iq_trim.hpp @@ -107,6 +107,8 @@ class IQTrimView : public View { {{0 * 8, 9 * 16}, "Max Pwr:", Color::light_grey()}, {{0 * 8, 10 * 16}, "Cutoff :", Color::light_grey()}, {{12 * 8, 10 * 16}, "%", Color::light_grey()}, + {{0 * 8, 12 * 16}, "Amplify:", Color::light_grey()}, + {{10 * 8, 12 * 16}, "x", Color::light_grey()}, }; TextField field_path{ @@ -135,7 +137,7 @@ class IQTrimView : public View { "0"}; Text text_max{ - {9 * 8, 9 * 16, 10 * 8, 1 * 16}, + {9 * 8, 9 * 16, 20 * 8, 1 * 16}, "0"}; NumberField field_cutoff{ @@ -145,6 +147,13 @@ class IQTrimView : public View { 1, ' '}; + NumberField field_amplify{ + {9 * 8, 12 * 16}, + 1, + {1, 9}, + 1, + ' '}; + Button button_trim{ {20 * 8, 16 * 16, 8 * 8, 2 * 16}, "Trim"}; diff --git a/firmware/application/iq_trim.cpp b/firmware/application/iq_trim.cpp index 335f30e0..a3e551e9 100644 --- a/firmware/application/iq_trim.cpp +++ b/firmware/application/iq_trim.cpp @@ -36,6 +36,13 @@ uint32_t power(T value) { return (real * real) + (imag * imag); } +template +uint32_t iq_max(T value) { + auto real = abs(value.real()); + auto imag = abs(value.imag()); + return (real > imag) ? real : imag; +} + /* Collects capture file metadata and sample power buckets. */ template Optional profile_capture( @@ -51,7 +58,8 @@ Optional profile_capture( .file_size = f.size(), .sample_count = f.size() / sizeof(T), .sample_size = sizeof(T), - .max_power = 0}; + .max_power = 0, + .max_iq = 0}; auto profile_samples = buckets.size * samples_per_bucket; auto sample_interval = info.sample_count / profile_samples; @@ -67,8 +75,11 @@ Optional profile_capture( if (*result != info.sample_size) break; // EOF - auto mag_squared = power(value); + auto max_iq = iq_max(value); + if (max_iq > info.max_iq) + info.max_iq = max_iq; + auto mag_squared = power(value); if (mag_squared > info.max_power) info.max_power = mag_squared; @@ -133,13 +144,50 @@ TrimRange compute_trim_range( info.sample_size}; } +void amplify_iq_buffer(uint8_t* buffer, uint32_t length, uint32_t amplification, uint8_t sample_size) { + uint32_t mult_count = length / sample_size / 2; + + switch (sample_size) { + case sizeof(complex16_t): { + int16_t* buf_ptr = (int16_t*)buffer; + for (uint32_t i = 0; i < mult_count; i++) { + int32_t val = *buf_ptr * amplification; + if (val > 0x7FFF) + val = 0x7FFF; + else if (val < -0x7FFF) + val = -0x7FFF; + *buf_ptr++ = val; + } + break; + } + + case sizeof(complex8_t): { + int8_t* buf_ptr = (int8_t*)buffer; + for (uint32_t i = 0; i < mult_count; i++) { + int32_t val = *buf_ptr * amplification; + if (val > 0x7F) + val = 0x7F; + else if (val < -0x7F) + val = -0x7F; + *buf_ptr++ = val; + } + break; + } + + default: + break; + } +} + bool trim_capture_with_range( const fs::path& path, TrimRange range, - const std::function& on_progress) { + const std::function& on_progress, + const uint32_t amplification) { constexpr size_t buffer_size = std::filesystem::max_file_block_size; uint8_t buffer[buffer_size]; auto temp_path = path + u"-tmp"; + auto sample_size = fs::capture_file_sample_size(path); // end_sample is the first sample to _not_ include. auto start_byte = range.start_sample * range.sample_size; @@ -168,6 +216,9 @@ bool trim_capture_with_range( auto remaining = length - processed; auto to_write = std::min(remaining, *result); + if (amplification > 1) + amplify_iq_buffer(buffer, to_write, amplification, sample_size); + result = dst->write(buffer, to_write); if (result.is_error()) return false; diff --git a/firmware/application/iq_trim.hpp b/firmware/application/iq_trim.hpp index a6d1546f..7ad3e081 100644 --- a/firmware/application/iq_trim.hpp +++ b/firmware/application/iq_trim.hpp @@ -37,6 +37,7 @@ struct CaptureInfo { uint64_t sample_count; uint8_t sample_size; uint32_t max_power; + uint32_t max_iq; }; /* Holds sample average power by bucket. */ @@ -82,11 +83,19 @@ TrimRange compute_trim_range( const PowerBuckets& buckets, uint8_t cutoff_percent); +/* Multiplies samples in an IQ buffer by amplification value */ +void amplify_iq_buffer( + uint8_t* buffer, + uint32_t length, + uint32_t amplification, + uint8_t sample_size); + /* Trims the capture file with the specified range. */ bool trim_capture_with_range( const std::filesystem::path& path, TrimRange range, - const std::function& on_progress); + const std::function& on_progress, + const uint32_t amplification); } // namespace iq diff --git a/firmware/application/ui_record_view.cpp b/firmware/application/ui_record_view.cpp index 31e136f7..0478d97f 100644 --- a/firmware/application/ui_record_view.cpp +++ b/firmware/application/ui_record_view.cpp @@ -322,7 +322,7 @@ void RecordView::trim_capture() { auto trim_range = iq::compute_trim_range(*info, power_buckets, 7); trim_ui.show_trimming(); - iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback()); + iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback(), 1); } trim_ui.clear();