diff --git a/firmware/application/apps/soundboard_app.cpp b/firmware/application/apps/soundboard_app.cpp index e35ba91b..fba44132 100644 --- a/firmware/application/apps/soundboard_app.cpp +++ b/firmware/application/apps/soundboard_app.cpp @@ -89,6 +89,7 @@ void SoundBoardView::start_tx(const uint32_t id) { uint32_t tone_key_index = options_tone_key.selected_index(); uint32_t sample_rate; + uint8_t bits_per_sample; stop(); @@ -104,6 +105,7 @@ void SoundBoardView::start_tx(const uint32_t id) { // button_play.set_bitmap(&bitmap_stop); sample_rate = reader->sample_rate(); + bits_per_sample = reader->bits_per_sample(); replay_thread = std::make_unique( std::move(reader), @@ -120,7 +122,7 @@ void SoundBoardView::start_tx(const uint32_t id) { transmitter_model.channel_bandwidth(), 0, // Gain is unused 8, // shift_bits_s16, default 8 bits, but also unused - 8, // bits per sample + bits_per_sample, TONES_F2D(tone_key_frequency(tone_key_index), TONES_SAMPLERATE), false, // AM false, // DSB @@ -172,7 +174,7 @@ void SoundBoardView::refresh_list() { if (entry_extension == ".WAV") { if (reader->open(u"/WAV/" + entry.path().native())) { - if ((reader->channels() == 1) && (reader->bits_per_sample() == 8)) { + if ((reader->channels() == 1) && ((reader->bits_per_sample() == 8) || (reader->bits_per_sample() == 16))) { // sounds[c].ms_duration = reader->ms_duration(); // sounds[c].path = u"WAV/" + entry.path().native(); if (count >= (page - 1) * 100 && count < page * 100) { diff --git a/firmware/application/apps/ui_view_wav.cpp b/firmware/application/apps/ui_view_wav.cpp index f2793659..5d8d10a3 100644 --- a/firmware/application/apps/ui_view_wav.cpp +++ b/firmware/application/apps/ui_view_wav.cpp @@ -38,9 +38,17 @@ void ViewWavView::update_scale(int32_t new_scale) { } void ViewWavView::refresh_waveform() { + uint8_t bits_per_sample = wav_reader->bits_per_sample(); + for (size_t i = 0; i < 240; i++) { wav_reader->data_seek(position + (i * scale)); - wav_reader->read(&waveform_buffer[i], sizeof(int16_t)); + if (bits_per_sample == 8) { + uint8_t sample; + wav_reader->read(&sample, 1); + waveform_buffer[i] = (sample - 0x80) * 256; + } else { + wav_reader->read(&waveform_buffer[i], 2); + } } waveform.set_dirty(); @@ -73,13 +81,29 @@ void ViewWavView::paint(Painter& painter) { painter.draw_vline({(Coord)i, 11 * 16}, 8, spectrum_rgb2_lut[amplitude_buffer[i] << 1]); } -void ViewWavView::on_pos_changed() { - position = (field_pos_seconds.value() * wav_reader->sample_rate()) + field_pos_samples.value(); +void ViewWavView::on_pos_time_changed() { + position = (uint64_t)((field_pos_seconds.value() * 1000) + field_pos_milliseconds.value()) * wav_reader->sample_rate() / 1000; + field_pos_milliseconds.set_range(0, ((uint32_t)field_pos_seconds.value() == wav_reader->ms_duration() / 1000) ? wav_reader->ms_duration() % 1000 : 999); + if (!updating_position) { + updating_position = true; // prevent recursion + field_pos_samples.set_value(position); + updating_position = false; + } + refresh_waveform(); +} + +void ViewWavView::on_pos_sample_changed() { + position = field_pos_samples.value(); + if (!updating_position) { + updating_position = true; // prevent recursion + field_pos_seconds.set_value(field_pos_samples.value() / wav_reader->sample_rate()); + field_pos_milliseconds.set_value((field_pos_samples.value() * 1000ull / wav_reader->sample_rate()) % 1000); + updating_position = false; + } refresh_waveform(); } void ViewWavView::load_wav(std::filesystem::path file_path) { - int16_t sample; uint32_t average; wav_file_path = file_path; @@ -91,18 +115,27 @@ void ViewWavView::load_wav(std::filesystem::path file_path) { wav_reader->rewind(); text_samplerate.set(to_string_dec_uint(wav_reader->sample_rate()) + "Hz"); + text_bits_per_sample.set(to_string_dec_uint(wav_reader->bits_per_sample(), 2)); text_title.set(wav_reader->title()); // Fill amplitude buffer, world's worst downsampling uint64_t skip = wav_reader->sample_count() / (240 * subsampling_factor); + uint8_t bits_per_sample = wav_reader->bits_per_sample(); 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); - average += (abs(sample) >> 8); + if (bits_per_sample == 8) { + uint8_t sample; + wav_reader->read(&sample, 1); + average += sample / 2; + } else { + int16_t sample; + wav_reader->read(&sample, 2); + average += (abs(sample) >> 8); + } } amplitude_buffer[i] = average / subsampling_factor; @@ -154,6 +187,7 @@ void ViewWavView::file_error() { void ViewWavView::start_playback() { uint32_t sample_rate; + uint8_t bits_per_sample; auto reader = std::make_unique(); @@ -167,6 +201,7 @@ void ViewWavView::start_playback() { button_play.set_bitmap(&bitmap_stop); sample_rate = reader->sample_rate(); + bits_per_sample = reader->bits_per_sample(); progressbar.set_max(reader->sample_count()); @@ -180,18 +215,19 @@ void ViewWavView::start_playback() { }); baseband::set_audiotx_config( - 1536000 / 20, // Rate of sending progress updates - 0, // Transmit BW = 0 = not transmitting - 0, // Gain - unused - 8, // shift_bits_s16, default 8 bits - unused - 16, // bits per sample - 0, // tone key disabled - false, // AM - false, // DSB - false, // USB - false // LSB + 1536000 / 20, // Rate of sending progress updates + 0, // Transmit BW = 0 = not transmitting + 0, // Gain - unused + 8, // shift_bits_s16, default 8 bits - unused + bits_per_sample, // bits_per_sample + 0, // tone key disabled + false, // AM + false, // DSB + false, // USB + false // LSB ); baseband::set_sample_rate(sample_rate); + transmitter_model.set_sampling_rate(1536000); audio::output::start(); } @@ -211,11 +247,13 @@ ViewWavView::ViewWavView( &text_samplerate, &text_title, &text_duration, + &text_bits_per_sample, &button_open, &button_play, &waveform, &progressbar, &field_pos_seconds, + &field_pos_milliseconds, &field_pos_samples, &field_scale, &field_cursor_a, @@ -234,12 +272,16 @@ ViewWavView::ViewWavView( file_error(); 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."); + if ((wav_reader->channels() != 1) || ((wav_reader->bits_per_sample() != 8) && (wav_reader->bits_per_sample() != 16))) { + nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n8 or 16-bit mono files."); return; } load_wav(file_path); field_pos_seconds.focus(); + field_pos_seconds.set_range(0, wav_reader->ms_duration() / 1000); + field_pos_milliseconds.set_range(0, (wav_reader->ms_duration() < 1000) ? wav_reader->ms_duration() % 1000 : 999); + field_pos_samples.set_range(0, wav_reader->sample_count() - 1); + field_scale.set_range(1, wav_reader->sample_count() / 240); }); }; }; @@ -257,10 +299,13 @@ ViewWavView::ViewWavView( update_scale(value); }; field_pos_seconds.on_change = [this](int32_t) { - on_pos_changed(); + on_pos_time_changed(); + }; + field_pos_milliseconds.on_change = [this](int32_t) { + on_pos_time_changed(); }; field_pos_samples.on_change = [this](int32_t) { - on_pos_changed(); + on_pos_sample_changed(); }; field_cursor_a.on_change = [this](int32_t v) { diff --git a/firmware/application/apps/ui_view_wav.hpp b/firmware/application/apps/ui_view_wav.hpp index 1958bfcb..0d02951f 100644 --- a/firmware/application/apps/ui_view_wav.hpp +++ b/firmware/application/apps/ui_view_wav.hpp @@ -49,7 +49,8 @@ class ViewWavView : public View { void update_scale(int32_t new_scale); void refresh_waveform(); void refresh_measurements(); - void on_pos_changed(); + void on_pos_time_changed(); + void on_pos_sample_changed(); void load_wav(std::filesystem::path file_path); void reset_controls(); bool is_active(); @@ -74,30 +75,35 @@ class ViewWavView : public View { int32_t scale{1}; uint64_t ns_per_pixel{}; uint64_t position{}; + bool updating_position{false}; Labels labels{ {{0 * 8, 0 * 16}, "File:", Color::light_grey()}, - {{0 * 8, 1 * 16}, "Samplerate:", Color::light_grey()}, + {{2 * 8, 1 * 16}, "-bit mono", Color::light_grey()}, {{0 * 8, 2 * 16}, "Title:", Color::light_grey()}, {{0 * 8, 3 * 16}, "Duration:", Color::light_grey()}, - {{0 * 8, 12 * 16}, "Position: s Scale:", Color::light_grey()}, - {{0 * 8, 13 * 16}, "Cursor A:", Color::dark_cyan()}, - {{0 * 8, 14 * 16}, "Cursor B:", Color::dark_magenta()}, - {{0 * 8, 15 * 16}, "Delta:", Color::light_grey()}, + {{0 * 8, 12 * 16}, "Position: . s Scale:", Color::light_grey()}, + {{0 * 8, 13 * 16}, " Sample:", Color::light_grey()}, + {{0 * 8, 14 * 16}, "Cursor A:", Color::dark_cyan()}, + {{0 * 8, 15 * 16}, "Cursor B:", Color::dark_magenta()}, + {{0 * 8, 16 * 16}, "Delta:", Color::light_grey()}, {{24 * 8, 18 * 16}, "Vol:", Color::light_grey()}}; Text text_filename{ - {5 * 8, 0 * 16, 12 * 8, 16}, + {5 * 8, 0 * 16, 18 * 8, 16}, ""}; Text text_samplerate{ - {11 * 8, 1 * 16, 8 * 8, 16}, + {12 * 8, 1 * 16, 10 * 8, 16}, ""}; Text text_title{ - {6 * 8, 2 * 16, 18 * 8, 16}, + {6 * 8, 2 * 16, 17 * 8, 16}, ""}; Text text_duration{ - {9 * 8, 3 * 16, 18 * 8, 16}, + {9 * 8, 3 * 16, 20 * 8, 16}, ""}; + Text text_bits_per_sample{ + {0 * 8, 1 * 16, 2 * 8, 16}, + "16"}; Button button_open{ {24 * 8, 8, 6 * 8, 2 * 16}, "Open"}; @@ -122,32 +128,34 @@ class ViewWavView : public View { NumberField field_pos_seconds{ {9 * 8, 12 * 16}, + 4, + {0, 0}, + 1, + ' ', + true}; + NumberField field_pos_milliseconds{ + {14 * 8, 12 * 16}, 3, {0, 999}, 1, - ' '}; + '0', + true}; NumberField field_pos_samples{ - {14 * 8, 12 * 16}, - 6, - {0, 999999}, - 1, - '0'}; - NumberField field_scale{ - {28 * 8, 12 * 16}, - 2, - {1, 40}, - 1, - ' '}; - - NumberField field_cursor_a{ {9 * 8, 13 * 16}, - 3, - {0, 239}, + 9, + {0, 0}, + 1, + '0', + true}; + NumberField field_scale{ + {26 * 8, 12 * 16}, + 4, + {1, 9999}, 1, ' ', true}; - NumberField field_cursor_b{ + NumberField field_cursor_a{ {9 * 8, 14 * 16}, 3, {0, 239}, @@ -155,8 +163,16 @@ class ViewWavView : public View { ' ', true}; + NumberField field_cursor_b{ + {9 * 8, 15 * 16}, + 3, + {0, 239}, + 1, + ' ', + true}; + Text text_delta{ - {7 * 8, 15 * 16, 30 * 8, 16}, + {7 * 8, 16 * 16, 30 * 8, 16}, "-"}; MessageHandlerRegistration message_handler_replay_thread_error{ diff --git a/firmware/application/io_wave.cpp b/firmware/application/io_wave.cpp index e72302e3..2b6cd862 100644 --- a/firmware/application/io_wave.cpp +++ b/firmware/application/io_wave.cpp @@ -27,7 +27,7 @@ bool WAVFileReader::open(const std::filesystem::path& path) { size_t i = 0; char ch; const uint8_t tag_INAM[4] = {'I', 'N', 'A', 'M'}; - char title_buffer[32]; + char title_buffer[32]{0}; uint32_t riff_size, data_end, title_size; size_t search_limit = 0; @@ -37,11 +37,17 @@ bool WAVFileReader::open(const std::filesystem::path& path) { return true; } + // Reinitialize to avoid old data when switching files + title_string = ""; + sample_rate_ = 0; + bytes_per_sample = 0; + auto error = file_.open(path); if (!error.is_valid()) { file_.read((void*)&header, sizeof(header)); // Read header (RIFF and WAVE) + // TODO: Work needed here to process RIFF file format correctly, i.e. properly skip over LIST & INFO chunks riff_size = header.cksize + 8; data_start = header.fmt.cksize + 28; data_size_ = header.data.cksize; diff --git a/firmware/baseband/proc_audiotx.cpp b/firmware/baseband/proc_audiotx.cpp index e79ffdc8..d7991cce 100644 --- a/firmware/baseband/proc_audiotx.cpp +++ b/firmware/baseband/proc_audiotx.cpp @@ -30,7 +30,8 @@ void AudioTXProcessor::execute(const buffer_c8_t& buffer) { if (!configured) return; - int32_t audio_sample_m; + buffer_s16_t audio_buffer{audio_data, AUDIO_OUTPUT_BUFFER_SIZE, sampling_rate}; + int16_t audio_sample_s16; // Zero-order hold (poop) for (size_t i = 0; i < buffer.count; i++) { @@ -46,15 +47,16 @@ void AudioTXProcessor::execute(const buffer_c8_t& buffer) { if (bytes_per_sample == 1) { sample = audio_sample - 0x80; - audio_sample_m = sample * 256; + audio_sample_s16 = sample * 256; } else { - audio_sample_m = audio_sample; + audio_sample_s16 = (int16_t)audio_sample; + sample = audio_sample_s16 / 256; } // Output to speaker too if (!tone_key_enabled) { uint32_t imod32 = i & (AUDIO_OUTPUT_BUFFER_SIZE - 1); - audio_data[imod32] = audio_sample_m; + audio_data[imod32] = audio_sample_s16; if (imod32 == (AUDIO_OUTPUT_BUFFER_SIZE - 1)) audio_output.write_unprocessed(audio_buffer); } @@ -133,6 +135,7 @@ void AudioTXProcessor::replay_config(const ReplayConfigMessage& message) { void AudioTXProcessor::sample_rate_config(const SampleRateConfigMessage& message) { resample_inc = (((uint64_t)message.sample_rate) << 16) / baseband_fs; // 16.16 fixed point message.sample_rate + sampling_rate = message.sample_rate; } int main() { diff --git a/firmware/baseband/proc_audiotx.hpp b/firmware/baseband/proc_audiotx.hpp index 2a111e6b..e0b66019 100644 --- a/firmware/baseband/proc_audiotx.hpp +++ b/firmware/baseband/proc_audiotx.hpp @@ -51,11 +51,10 @@ class AudioTXProcessor : public BasebandProcessor { uint32_t audio_sample{}; int32_t sample{0}, delta{}; int8_t re{0}, im{0}; - int8_t bytes_per_sample{1}; - int16_t audio_sample_s16{}; + uint8_t bytes_per_sample{1}; + uint32_t sampling_rate{48000}; int16_t audio_data[AUDIO_OUTPUT_BUFFER_SIZE]; - buffer_s16_t audio_buffer{audio_data, AUDIO_OUTPUT_BUFFER_SIZE, 48000}; AudioOutput audio_output{}; size_t progress_interval_samples = 0, progress_samples = 0;