diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index dd8b0441..63f4f969 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -213,6 +213,7 @@ set(CPPSRC ui_touch_calibration.cpp ui_touchtunes.cpp ui_transmitter.cpp + ui_view_wav.cpp ui_whipcalc.cpp # ui_loadmodule.cpp recent_entries.cpp diff --git a/firmware/application/io_wave.cpp b/firmware/application/io_wave.cpp index 57cc5568..b3997588 100644 --- a/firmware/application/io_wave.cpp +++ b/firmware/application/io_wave.cpp @@ -97,6 +97,10 @@ uint32_t WAVFileReader::ms_duration() { return ((data_size_ * 1000) / sample_rate_) / bytes_per_sample; } +void WAVFileReader::data_seek(const uint64_t Offset) { + file.seek(data_start + (Offset * bytes_per_sample)); +} + /*int WAVFileReader::seek_mss(const uint16_t minutes, const uint8_t seconds, const uint32_t samples) { const auto result = file.seek(data_start + ((((minutes * 60) + seconds) * sample_rate_) + samples) * bytes_per_sample); @@ -118,6 +122,10 @@ uint32_t WAVFileReader::data_size() { return data_size_; } +uint32_t WAVFileReader::sample_count() { + return data_size_ / bytes_per_sample; +} + uint16_t WAVFileReader::bits_per_sample() { return header.fmt.wBitsPerSample; } diff --git a/firmware/application/io_wave.hpp b/firmware/application/io_wave.hpp index baf43cd6..6cdf4ab7 100644 --- a/firmware/application/io_wave.hpp +++ b/firmware/application/io_wave.hpp @@ -112,12 +112,14 @@ public: virtual ~WAVFileReader() = default; bool open(const std::filesystem::path& path); + void data_seek(const uint64_t Offset); void rewind(); uint32_t ms_duration(); //int seek_mss(const uint16_t minutes, const uint8_t seconds, const uint32_t samples); uint16_t channels(); uint32_t sample_rate(); uint32_t data_size(); + uint32_t sample_count(); uint16_t bits_per_sample(); std::string title(); diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp index 154dff24..63913048 100755 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -32,11 +32,19 @@ //BUG: SCANNER Lock on frequency, if frequency jump, still locked on first one //BUG: SCANNER Multiple slices +//TODO: Cap Wav viewer position +//TODO: Adapt wav viewer position step +//TODO: Optimize wav viewer refresh +//TODO: Remove make_bistream from encoders.cpp, too complex, stinks. bitstream_append should be enough. +//TODO: Continue work on proc_afskrx_corr, see python script (it works !) +//TODO: Super simple text file viewer +//TODO: De bruijn sequence scanner for encoders +//TODO: FILEMAN Rename folders +//TODO: FILEMAN Move files //TODO: Frequency and bw settings were removed from modemsetup, put those back in LCR TX +//TODO: Use separate thread for scanning in EPAR TX //TODO: Use separate thread for scanning in LCR TX //TODO: REPLAY Convert C16 to C8 on M0 core -//TODO: Use TabView -//TODO: De bruijn sequence scanner for encoders //TODO: Make freqman refresh simpler (use previous black rectangle method) //TODO: Merge AFSK and TONES procs ? //TODO: NFM RX mode: nav.pop on squelch diff --git a/firmware/application/ui_fileman.cpp b/firmware/application/ui_fileman.cpp index 2a2cfb98..b83d6a13 100644 --- a/firmware/application/ui_fileman.cpp +++ b/firmware/application/ui_fileman.cpp @@ -201,17 +201,17 @@ FileLoadView::FileLoadView( // Resize menu view to fill screen menu_view.set_parent_rect({ 0, 3 * 8, 240, 29 * 8 }); - // Just to allow exit on left - menu_view.on_left = [&nav, this]() { - nav.pop(); - }; - refresh_list(); on_select_entry = [&nav, this]() { - nav_.pop(); - if (on_changed) - on_changed(entry_list[menu_view.highlighted()].entry_path); + if (entry_list[menu_view.highlighted()].is_directory) { + load_directory_contents(get_selected_path()); + refresh_list(); + } else { + nav_.pop(); + if (on_changed) + on_changed(entry_list[menu_view.highlighted()].entry_path); + } }; } @@ -288,6 +288,7 @@ FileManagerView::FileManagerView( }; button_delete.on_select = [this, &nav](Button&) { + // Use display_modal ? nav.push("Delete", "Delete " + entry_list[menu_view.highlighted()].entry_path.filename().string() + "\nAre you sure ?", YESNO, [this](bool choice) { if (choice) diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index d07ef1f8..a31edb48 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -59,6 +59,7 @@ #include "ui_soundboard.hpp" #include "ui_sstvtx.hpp" #include "ui_touchtunes.hpp" +#include "ui_view_wav.hpp" #include "ui_whipcalc.hpp" #include "analog_audio_app.hpp" @@ -363,6 +364,7 @@ SystemMenuView::SystemMenuView(NavigationView& nav) { { "Capture", ui::Color::blue(), &bitmap_icon_capture, [&nav](){ nav.push(); } }, { "Replay", ui::Color::grey(), &bitmap_icon_replay, [&nav](){ nav.push(); } }, { "Scanner/search", ui::Color::orange(), &bitmap_icon_closecall, [&nav](){ nav.push(); } }, + { "Wave file viewer", ui::Color::blue(), nullptr, [&nav](){ nav.push(); } }, { "Utilities", ui::Color::purple(), &bitmap_icon_utilities, [&nav](){ nav.push(); } }, { "Setup", ui::Color::white(), &bitmap_icon_setup, [&nav](){ nav.push(); } }, //{ "Debug", ui::Color::white(), nullptr, [&nav](){ nav.push(); } }, diff --git a/firmware/application/ui_view_wav.cpp b/firmware/application/ui_view_wav.cpp new file mode 100644 index 00000000..4719053e --- /dev/null +++ b/firmware/application/ui_view_wav.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * + * 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 "ui_view_wav.hpp" +#include "ui_fileman.hpp" + +using namespace portapack; + +#include "string_format.hpp" + +namespace ui { + +void ViewWavView::update_scale(int32_t new_scale) { + scale = new_scale; + ns_per_pixel = (1000000000UL / wav_reader->sample_rate()) * scale; + field_pos_samples.set_step(scale); + refresh_waveform(); +} + +void ViewWavView::refresh_waveform() { + int16_t sample; + + for (size_t i = 0; i < 240; i++) { + wav_reader->data_seek(position + (i * scale)); + wav_reader->read(&sample, 2); + + waveform_buffer[i] = sample >> 8; + } + + uint64_t span_ns = ns_per_pixel * abs(field_cursor_b.value() - field_cursor_a.value()); + if (span_ns) + text_delta.set(to_string_dec_uint(span_ns / 1000) + "us (" + to_string_dec_uint(1000000000UL / span_ns) + "Hz)"); + else + text_delta.set("0us ?Hz"); + + //waveform.set_dirty(); + + set_dirty(); +} + +void ViewWavView::paint(Painter& painter) { + // Waveform limits + painter.draw_hline({ 0, 6 * 16 - 1 }, 240, Color::grey()); + painter.draw_hline({ 0, 10 * 16 }, 240, Color::grey()); + + // 0~127 to 0~15 color index + for (size_t i = 0; i < 240; i++) + painter.draw_vline({ (Coord)i, 11 * 16 }, 8, amplitude_colors[amplitude_buffer[i] >> 3]); + + // Window + uint64_t w_start = (position * 240) / wav_reader->sample_count(); + uint64_t w_width = (scale * 240) / (wav_reader->sample_count() / 240); + painter.fill_rectangle({ 0, 10 * 16 + 1, 240, 16 }, Color::black()); + painter.fill_rectangle({ (Coord)w_start, 21 * 8, (Dim)w_width + 1, 8 }, Color::white()); + display.draw_line({ 0, 10 * 16 + 1 }, { (Coord)w_start, 21 * 8 }, Color::white()); + display.draw_line({ 239, 10 * 16 + 1 }, { (Coord)(w_start + w_width), 21 * 8 }, Color::white()); + + // Cursors + painter.fill_rectangle({ 0, 6 * 16 - 8, 240, 7 }, Color::black()); + painter.draw_vline({ (Coord)field_cursor_a.value(), 11 * 8 }, 7, Color::cyan()); + painter.draw_vline({ (Coord)field_cursor_b.value(), 11 * 8 }, 7, Color::magenta()); +} + +void ViewWavView::on_pos_changed() { + position = (field_pos_seconds.value() * wav_reader->sample_rate()) + field_pos_samples.value(); + refresh_waveform(); +} + +void ViewWavView::load_wav(std::filesystem::path file_path) { + int16_t sample; + uint32_t average; + + if (!wav_reader->open(file_path)) { + nav_.display_modal("Error", "Couldn't open file.", INFO, nullptr); + return; + } + + if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) { + nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files.", INFO, nullptr); + return; + } + + text_filename.set(file_path.filename().string()); + auto ms_duration = wav_reader->ms_duration(); + text_duration.set(to_string_dec_uint(ms_duration / 1000) + "s" + to_string_dec_uint(ms_duration % 1000) + "ms"); + + wav_reader->rewind(); + + text_samplerate.set(to_string_dec_uint(wav_reader->sample_rate()) + "Hz"); + text_title.set(wav_reader->title()); + + // Fill amplitude buffer, world's worst downsampling + uint64_t skip = wav_reader->sample_count() / (240 * subsampling_factor); + + for (size_t i = 0; i < 240; i++) { + average = 0; + + for (size_t s = 0; s < subsampling_factor; s++) { + wav_reader->data_seek(((i * subsampling_factor) + s) * skip); + wav_reader->read(&sample, 2); + + if (sample < 0) + sample = -sample; + + sample >>= 8; + + average += sample; + } + + amplitude_buffer[i] = average / subsampling_factor; + } + + reset_controls(); + update_scale(1); +} + +void ViewWavView::reset_controls() { + field_scale.set_value(1); + field_scale.on_change = [this](int32_t value) { + update_scale(value); + }; + + field_pos_seconds.set_value(0); + field_pos_seconds.on_change = [this](int32_t) { + on_pos_changed(); + }; + field_pos_samples.set_value(0); + field_pos_samples.on_change = [this](int32_t) { + on_pos_changed(); + }; +} + +ViewWavView::ViewWavView( + NavigationView& nav +) : nav_(nav) +{ + wav_reader = std::make_unique(); + + add_children({ + &labels, + &text_filename, + &text_samplerate, + &text_title, + &text_duration, + &button_open, + &waveform, + &field_pos_seconds, + &field_pos_samples, + &field_scale, + &field_cursor_a, + &field_cursor_b, + &text_delta + }); + + button_open.on_select = [this, &nav](Button&) { + auto open_view = nav.push(); + open_view->on_changed = [this](std::filesystem::path file_path) { + load_wav(file_path); + }; + }; + + reset_controls(); + + field_cursor_a.set_value(0); + field_cursor_a.on_change = [this](int32_t) { + refresh_waveform(); + }; + field_cursor_b.set_value(0); + field_cursor_b.on_change = [this](int32_t) { + refresh_waveform(); + }; +} + +void ViewWavView::focus() { + button_open.focus(); +} + +} /* namespace ui */ diff --git a/firmware/application/ui_view_wav.hpp b/firmware/application/ui_view_wav.hpp new file mode 100644 index 00000000..bd9c5525 --- /dev/null +++ b/firmware/application/ui_view_wav.hpp @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * + * 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 "ui.hpp" +#include "ui_navigation.hpp" +#include "io_wave.hpp" + +namespace ui { + +class ViewWavView : public View { +public: + ViewWavView(NavigationView& nav); + + void focus() override; + void paint(Painter&) override; + + std::string title() const override { return "WAV viewer"; }; + +private: + NavigationView& nav_; + static constexpr uint32_t subsampling_factor = 8; + + void update_scale(int32_t new_scale); + void refresh_waveform(); + void on_pos_changed(); + void load_wav(std::filesystem::path file_path); + void reset_controls(); + + const Color amplitude_colors[16] = { + { 0x00, 0x3F, 0xB0 }, + { 0x00, 0x6D, 0xB5 }, + { 0x00, 0x9C, 0xBA }, + { 0x00, 0xBF, 0xB0 }, + { 0x00, 0xC5, 0x86 }, + { 0x00, 0xCA, 0x5A }, + { 0x00, 0xCF, 0x2A }, + { 0x06, 0xD4, 0x00 }, + { 0x3A, 0xDA, 0x00 }, + { 0x71, 0xDF, 0x00 }, + { 0xAA, 0xE4, 0x00 }, + { 0xE6, 0xE9, 0x00 }, + { 0xEF, 0xB9, 0x00 }, + { 0xF4, 0x83, 0x00 }, + { 0xF9, 0x4B, 0x00 }, + { 0xFF, 0x0F, 0x00 } + }; + + std::unique_ptr wav_reader { }; + + int8_t waveform_buffer[240] { }; + uint8_t amplitude_buffer[240] { }; + int32_t scale { 1 }; + uint64_t ns_per_pixel { }; + uint64_t position { }; + + Labels labels { + { { 0 * 8, 0 * 16 }, "File:", Color::light_grey() }, + { { 0 * 8, 1 * 16 }, "Samplerate:", Color::light_grey() }, + { { 0 * 8, 2 * 16 }, "Title:", Color::light_grey() }, + { { 0 * 8, 3 * 16 }, "Duration:", Color::light_grey() }, + { { 0 * 8, 11 * 16 }, "Position: s Scale:", Color::light_grey() }, + { { 0 * 8, 12 * 16 }, "Cursor A:", Color::dark_cyan() }, + { { 0 * 8, 13 * 16 }, "Cursor B:", Color::dark_magenta() }, + { { 0 * 8, 14 * 16 }, "Delta:", Color::light_grey() } + }; + + Text text_filename { + { 5 * 8, 0 * 16, 12 * 8, 16 }, + "" + }; + Text text_samplerate { + { 11 * 8, 1 * 16, 8 * 8, 16 }, + "" + }; + Text text_title { + { 6 * 8, 2 * 16, 18 * 8, 16 }, + "" + }; + Text text_duration { + { 9 * 8, 3 * 16, 18 * 8, 16 }, + "" + }; + Button button_open { + { 24 * 8, 8, 6 * 8, 2 * 16 }, + "Open" + }; + + Waveform waveform { + { 0, 5 * 16, 240, 64 }, + waveform_buffer, + 240, + 0, + false, + Color::white() + }; + + NumberField field_pos_seconds { + { 9 * 8, 11 * 16 }, + 3, + { 0, 999 }, + 1, + ' ' + }; + NumberField field_pos_samples { + { 14 * 8, 11 * 16 }, + 6, + { 0, 999999 }, + 1, + '0' + }; + NumberField field_scale { + { 28 * 8, 11 * 16 }, + 2, + { 1, 40 }, + 1, + ' ' + }; + + NumberField field_cursor_a { + { 9 * 8, 12 * 16 }, + 3, + { 0, 239 }, + 1, + ' ', + true + }; + + NumberField field_cursor_b { + { 9 * 8, 13 * 16 }, + 3, + { 0, 239 }, + 1, + ' ', + true + }; + + Text text_delta { + { 6 * 8, 14 * 16, 30 * 8, 16 }, + "-" + }; +}; + +} /* namespace ui */ diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index f8674b4a..d8babd5a 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1236,6 +1236,10 @@ void NumberField::set_range(const int32_t min, const int32_t max) { set_value(value(), false); } +void NumberField::set_step(const int32_t new_step) { + step = new_step; +} + void NumberField::paint(Painter& painter) { const auto text = to_string_dec_int(value_, length_, fill_char); diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index e4382559..995db50e 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -535,6 +535,7 @@ public: int32_t value() const; void set_value(int32_t new_value, bool trigger_change = true); void set_range(const int32_t min, const int32_t max); + void set_step(const int32_t new_step); void paint(Painter& painter) override; @@ -544,7 +545,7 @@ public: private: range_t range; - const int32_t step; + int32_t step; const int length_; const char fill_char; int32_t value_ { 0 }; diff --git a/firmware/portapack-h1-havoc.bin b/firmware/portapack-h1-havoc.bin index 252ccfba..fc5a836c 100644 Binary files a/firmware/portapack-h1-havoc.bin and b/firmware/portapack-h1-havoc.bin differ