From 955a8ce303a9f8fd6a34009934e3d7aaeff3ec17 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 1 Aug 2023 12:13:48 +1000 Subject: Basic nvs init + bluetooth in the build --- src/drivers/CMakeLists.txt | 3 +- src/drivers/bluetooth.cpp | 11 ++++++ src/drivers/include/bluetooth.hpp | 19 +++++++++ src/drivers/include/nvs.hpp | 27 +++++++++++++ src/drivers/nvs.cpp | 73 +++++++++++++++++++++++++++++++++++ src/system_fsm/booting.cpp | 4 +- src/system_fsm/include/system_fsm.hpp | 2 + src/system_fsm/system_fsm.cpp | 1 + 8 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/drivers/bluetooth.cpp create mode 100644 src/drivers/include/bluetooth.hpp create mode 100644 src/drivers/include/nvs.hpp create mode 100644 src/drivers/nvs.cpp (limited to 'src') diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 40cd0c4f..9774f80b 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" "spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" + "nvs.cpp" "bluetooth.cpp" INCLUDE_DIRS "include" - REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks") + REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "bt") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp new file mode 100644 index 00000000..9748522f --- /dev/null +++ b/src/drivers/bluetooth.cpp @@ -0,0 +1,11 @@ +#include "bluetooth.hpp" + +#include "esp_bt.h" + +namespace drivers { + +auto Bluetooth::Enable() -> Bluetooth* { + return nullptr; +} + +} // namespace drivers diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp new file mode 100644 index 00000000..f3a4b2ac --- /dev/null +++ b/src/drivers/include/bluetooth.hpp @@ -0,0 +1,19 @@ + +#pragma once + +#include + +namespace drivers { + +class Bluetooth { + public: + static auto Enable() -> Bluetooth*; + Bluetooth(); + ~Bluetooth(); + + struct Device {}; + auto Scan() -> std::vector; + private: + }; + +} diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp new file mode 100644 index 00000000..be783583 --- /dev/null +++ b/src/drivers/include/nvs.hpp @@ -0,0 +1,27 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "esp_err.h" +#include "nvs.h" + +namespace drivers { + +class NvsStorage { + public: + static auto Open() -> NvsStorage*; + + auto SchemaVersion() -> uint8_t; + + explicit NvsStorage(nvs_handle_t); + ~NvsStorage(); + + private: + nvs_handle_t handle_; +}; + +} // namespace drivers \ No newline at end of file diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp new file mode 100644 index 00000000..a2de9518 --- /dev/null +++ b/src/drivers/nvs.cpp @@ -0,0 +1,73 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "nvs.hpp" +#include + +#include +#include + +#include "esp_log.h" +#include "nvs.h" +#include "nvs_flash.h" + +namespace drivers { + +static constexpr char kTag[] = "nvm"; +static constexpr uint8_t kSchemaVersion = 1; + +static constexpr char kKeyVersion[] = "ver"; + +auto NvsStorage::Open() -> NvsStorage* { + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES) { + ESP_LOGW(kTag, "partition needs initialisation"); + nvs_flash_erase(); + err = nvs_flash_init(); + } + if (err != ESP_OK) { + ESP_LOGE(kTag, "failed to init nvm"); + return nullptr; + } + + nvs_handle_t handle; + if ((err = nvs_open("tangara", NVS_READWRITE, &handle)) != ESP_OK) { + ESP_LOGE(kTag, "failed to open nvs namespace"); + return nullptr; + } + + std::unique_ptr instance = std::make_unique(handle); + if (instance->SchemaVersion() < kSchemaVersion) { + ESP_LOGW(kTag, "namespace needs downgrading"); + nvs_erase_all(handle); + nvs_set_u8(handle, kKeyVersion, kSchemaVersion); + err = nvs_commit(handle); + if (err != ESP_OK) { + ESP_LOGW(kTag, "failed to init namespace"); + return nullptr; + } + } + + ESP_LOGI(kTag, "nvm storage initialised okay"); + return instance.release(); +} + +NvsStorage::NvsStorage(nvs_handle_t handle) : handle_(handle) {} + +NvsStorage::~NvsStorage() { + nvs_close(handle_); + nvs_flash_deinit(); +} + +auto NvsStorage::SchemaVersion() -> uint8_t { + uint8_t ret; + if (nvs_get_u8(handle_, kKeyVersion, &ret) != ESP_OK) { + return UINT8_MAX; + } + return ret; +} + +} // namespace drivers diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 4686748e..3d6c6a46 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -13,6 +13,7 @@ #include "event_queue.hpp" #include "gpios.hpp" #include "lvgl/lvgl.h" +#include "nvs.hpp" #include "relative_wheel.hpp" #include "spi.hpp" #include "system_events.hpp" @@ -50,9 +51,10 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); sSamd.reset(drivers::Samd::Create()); sBattery.reset(drivers::Battery::Create()); + sNvs.reset(drivers::NvsStorage::Open()); sTagParser.reset(new database::TagParserImpl()); - if (!sSamd || !sBattery) { + if (!sSamd || !sBattery || !sNvs) { events::System().Dispatch(FatalError{}); events::Ui().Dispatch(FatalError{}); return; diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index 6f0eb563..dc188780 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -16,6 +16,7 @@ #include "relative_wheel.hpp" #include "samd.hpp" #include "storage.hpp" +#include "nvs.hpp" #include "tag_parser.hpp" #include "tinyfsm.hpp" #include "touchwheel.hpp" @@ -54,6 +55,7 @@ class SystemState : public tinyfsm::Fsm { protected: static std::shared_ptr sGpios; static std::shared_ptr sSamd; + static std::shared_ptr sNvs; static std::shared_ptr sTouch; static std::shared_ptr sRelativeTouch; diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 5f85d43c..527a8770 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -17,6 +17,7 @@ namespace system_fsm { std::shared_ptr SystemState::sGpios; std::shared_ptr SystemState::sSamd; +std::shared_ptr SystemState::sNvs; std::shared_ptr SystemState::sTouch; std::shared_ptr SystemState::sRelativeTouch; -- cgit v1.2.3 From fbebc525117f18d5751e6951bc4ffcc51f70dcc4 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 1 Aug 2023 14:00:31 +1000 Subject: Add libsamplerate for resampling decoder output --- src/audio/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 428ea691..22a0160c 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -7,6 +7,6 @@ idf_component_register( "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" INCLUDE_DIRS "include" - REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist") + REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist" "libsamplerate") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) -- cgit v1.2.3 From 3511852f39cd5023ec8e6d0b94cc69f34e9201ed Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 3 Aug 2023 15:32:28 +1000 Subject: Add very limited resampling (it's slow as shit) --- src/audio/CMakeLists.txt | 2 +- src/audio/audio_task.cpp | 81 ++++++--- src/audio/i2s_audio_output.cpp | 37 ++-- src/audio/include/audio_sink.hpp | 4 +- src/audio/include/audio_task.hpp | 7 +- src/audio/include/i2s_audio_output.hpp | 4 +- src/audio/include/sink_mixer.hpp | 88 ++++++++++ src/audio/include/stream_info.hpp | 35 +++- src/audio/sink_mixer.cpp | 301 +++++++++++++++++++++++++++++++++ src/drivers/bluetooth.cpp | 2 +- src/drivers/i2s_dac.cpp | 1 - src/drivers/include/bluetooth.hpp | 19 ++- src/drivers/include/i2s_dac.hpp | 6 +- src/system_fsm/include/system_fsm.hpp | 2 +- src/tasks/tasks.cpp | 16 ++ src/tasks/tasks.hpp | 2 + 16 files changed, 544 insertions(+), 63 deletions(-) create mode 100644 src/audio/include/sink_mixer.hpp create mode 100644 src/audio/sink_mixer.cpp (limited to 'src') diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 22a0160c..9e50f8ff 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -5,7 +5,7 @@ idf_component_register( SRCS "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" - "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" + "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "sink_mixer.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist" "libsamplerate") diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index 7e0fb207..c3498965 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -34,6 +34,7 @@ #include "freertos/queue.h" #include "freertos/ringbuf.h" #include "pipeline.hpp" +#include "sink_mixer.hpp" #include "span.hpp" #include "arena.hpp" @@ -115,14 +116,12 @@ AudioTask::AudioTask(IAudioSource* source, IAudioSink* sink) : source_(source), sink_(sink), codec_(), + mixer_(new SinkMixer(sink->stream())), timer_(), has_begun_decoding_(false), current_input_format_(), current_output_format_(), - sample_buffer_(reinterpret_cast( - heap_caps_malloc(kSampleBufferSize, - MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))), - sample_buffer_len_(kSampleBufferSize) {} + codec_buffer_(new RawStream(kSampleBufferSize)) {} void AudioTask::Main() { for (;;) { @@ -246,13 +245,17 @@ auto AudioTask::BeginDecoding(InputStream& stream) -> bool { return false; } + OutputStream writer{codec_buffer_.get()}; + writer.prepare(new_format, {}); + return true; } auto AudioTask::ContinueDecoding(InputStream& stream) -> bool { while (!stream.data().empty()) { - auto res = codec_->ContinueStream(stream.data(), - {sample_buffer_, sample_buffer_len_}); + OutputStream writer{codec_buffer_.get()}; + + auto res = codec_->ContinueStream(stream.data(), writer.data()); stream.consume(res.first); @@ -263,9 +266,10 @@ auto AudioTask::ContinueDecoding(InputStream& stream) -> bool { return false; } } else { - xStreamBufferSend(sink_->stream(), sample_buffer_, - res.second->bytes_written, portMAX_DELAY); - timer_->AddBytes(res.second->bytes_written); + writer.add(res.second->bytes_written); + + InputStream reader{codec_buffer_.get()}; + SendToSink(reader); } } return true; @@ -284,21 +288,22 @@ auto AudioTask::FinishDecoding(InputStream& stream) -> void { std::unique_ptr mad_buffer; mad_buffer.reset(new RawStream(stream.data().size_bytes() + 8)); - OutputStream writer{mad_buffer.get()}; + OutputStream mad_writer{mad_buffer.get()}; std::copy(stream.data().begin(), stream.data().end(), - writer.data().begin()); - std::fill(writer.data().begin(), writer.data().end(), std::byte{0}); + mad_writer.data().begin()); + std::fill(mad_writer.data().begin(), mad_writer.data().end(), std::byte{0}); InputStream padded_stream{mad_buffer.get()}; - auto res = codec_->ContinueStream(stream.data(), - {sample_buffer_, sample_buffer_len_}); + OutputStream writer{codec_buffer_.get()}; + auto res = codec_->ContinueStream(stream.data(), writer.data()); if (res.second.has_error()) { return; } - xStreamBufferSend(sink_->stream(), sample_buffer_, - res.second->bytes_written, portMAX_DELAY); - timer_->AddBytes(res.second->bytes_written); + writer.add(res.second->bytes_written); + + InputStream reader{codec_buffer_.get()}; + SendToSink(reader); } } @@ -319,24 +324,31 @@ auto AudioTask::ForwardPcmStream(StreamInfo::Pcm& format, xStreamBufferSend(sink_->stream(), samples.data(), samples.size_bytes(), portMAX_DELAY); timer_->AddBytes(samples.size_bytes()); + InputStream reader{codec_buffer_.get()}; + SendToSink(reader); + return true; } auto AudioTask::ConfigureSink(const StreamInfo::Pcm& format, const Duration& duration) -> bool { if (format != current_output_format_) { - // The new format is different to the old one. Wait for the sink to drain - // before continuing. - while (!xStreamBufferIsEmpty(sink_->stream())) { - ESP_LOGI(kTag, "waiting for sink stream to drain..."); - // TODO(jacqueline): Get the sink drain ISR to notify us of this - // via semaphore instead of busy-ish waiting. - vTaskDelay(pdMS_TO_TICKS(100)); - } + current_output_format_ = format; + StreamInfo::Pcm new_sink_format = sink_->PrepareFormat(format); + if (new_sink_format != current_sink_format_) { + current_sink_format_ = new_sink_format; + + // The new format is different to the old one. Wait for the sink to drain + // before continuing. + while (!xStreamBufferIsEmpty(sink_->stream())) { + ESP_LOGI(kTag, "waiting for sink stream to drain..."); + // TODO(jacqueline): Get the sink drain ISR to notify us of this + // via semaphore instead of busy-ish waiting. + vTaskDelay(pdMS_TO_TICKS(10)); + } - ESP_LOGI(kTag, "configuring sink"); - if (!sink_->Configure(format)) { - return false; + ESP_LOGI(kTag, "configuring sink"); + sink_->Configure(new_sink_format); } } @@ -345,4 +357,17 @@ auto AudioTask::ConfigureSink(const StreamInfo::Pcm& format, return true; } +auto AudioTask::SendToSink(InputStream& stream) -> void { + std::size_t bytes_to_send = stream.data().size_bytes(); + std::size_t bytes_sent; + if (stream.info().format_as() == current_sink_format_) { + bytes_sent = xStreamBufferSend(sink_->stream(), stream.data().data(), + bytes_to_send, portMAX_DELAY); + stream.consume(bytes_sent); + } else { + bytes_sent = mixer_->MixAndSend(stream, current_sink_format_.value()); + } + timer_->AddBytes(bytes_sent); +} + } // namespace audio diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index bb413b38..e53dbe2a 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -115,10 +115,19 @@ auto I2SAudioOutput::AdjustVolumeDown() -> bool { return true; } -auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> bool { +auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) + -> StreamInfo::Pcm { + return StreamInfo::Pcm{ + .channels = std::min(orig.channels, 2), + .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), + .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), + }; +} + +auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> void { if (current_config_ && pcm == *current_config_) { ESP_LOGI(kTag, "ignoring unchanged format"); - return true; + return; } ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz", pcm.channels, @@ -134,7 +143,7 @@ auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> bool { break; default: ESP_LOGE(kTag, "dropping stream with out of bounds channels"); - return false; + return; } drivers::I2SDac::BitsPerSample bps; @@ -150,30 +159,36 @@ auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> bool { break; default: ESP_LOGE(kTag, "dropping stream with unknown bps"); - return false; + return; } drivers::I2SDac::SampleRate sample_rate; switch (pcm.sample_rate) { + case 8000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_8; + break; + case 32000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_32; + break; case 44100: sample_rate = drivers::I2SDac::SAMPLE_RATE_44_1; break; case 48000: sample_rate = drivers::I2SDac::SAMPLE_RATE_48; break; + case 88200: + sample_rate = drivers::I2SDac::SAMPLE_RATE_88_2; + break; + case 96000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_96; + break; default: ESP_LOGE(kTag, "dropping stream with unknown rate"); - return false; + return; } dac_->Reconfigure(ch, bps, sample_rate); current_config_ = pcm; - - return true; -} - -auto I2SAudioOutput::Send(const cpp::span& data) -> void { - dac_->WriteData(data); } } // namespace audio diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index 261f7c79..20bb6da6 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -38,8 +38,8 @@ class IAudioSink { virtual auto AdjustVolumeUp() -> bool = 0; virtual auto AdjustVolumeDown() -> bool = 0; - virtual auto Configure(const StreamInfo::Pcm& format) -> bool = 0; - virtual auto Send(const cpp::span& data) -> void = 0; + virtual auto PrepareFormat(const StreamInfo::Pcm&) -> StreamInfo::Pcm = 0; + virtual auto Configure(const StreamInfo::Pcm& format) -> void = 0; auto stream() -> StreamBufferHandle_t { return stream_; } }; diff --git a/src/audio/include/audio_task.hpp b/src/audio/include/audio_task.hpp index f6e9789b..b27aa039 100644 --- a/src/audio/include/audio_task.hpp +++ b/src/audio/include/audio_task.hpp @@ -14,6 +14,7 @@ #include "audio_source.hpp" #include "codec.hpp" #include "pipeline.hpp" +#include "sink_mixer.hpp" #include "stream_info.hpp" namespace audio { @@ -63,18 +64,20 @@ class AudioTask { auto ForwardPcmStream(StreamInfo::Pcm&, cpp::span) -> bool; auto ConfigureSink(const StreamInfo::Pcm&, const Duration&) -> bool; + auto SendToSink(InputStream&) -> void; IAudioSource* source_; IAudioSink* sink_; std::unique_ptr codec_; + std::unique_ptr mixer_; std::unique_ptr timer_; bool has_begun_decoding_; std::optional current_input_format_; std::optional current_output_format_; + std::optional current_sink_format_; - std::byte* sample_buffer_; - std::size_t sample_buffer_len_; + std::unique_ptr codec_buffer_; }; } // namespace audio diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp index 43155711..e0f791c5 100644 --- a/src/audio/include/i2s_audio_output.hpp +++ b/src/audio/include/i2s_audio_output.hpp @@ -35,8 +35,8 @@ class I2SAudioOutput : public IAudioSink { auto AdjustVolumeUp() -> bool override; auto AdjustVolumeDown() -> bool override; - auto Configure(const StreamInfo::Pcm& format) -> bool override; - auto Send(const cpp::span& data) -> void override; + auto PrepareFormat(const StreamInfo::Pcm&) -> StreamInfo::Pcm override; + auto Configure(const StreamInfo::Pcm& format) -> void override; I2SAudioOutput(const I2SAudioOutput&) = delete; I2SAudioOutput& operator=(const I2SAudioOutput&) = delete; diff --git a/src/audio/include/sink_mixer.hpp b/src/audio/include/sink_mixer.hpp new file mode 100644 index 00000000..632ffa2e --- /dev/null +++ b/src/audio/include/sink_mixer.hpp @@ -0,0 +1,88 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +#include "samplerate.h" + +#include "audio_decoder.hpp" +#include "audio_sink.hpp" +#include "audio_source.hpp" +#include "codec.hpp" +#include "pipeline.hpp" +#include "stream_info.hpp" + +namespace audio { + +/* + * Handles the final downmix + resample + quantisation stage of audio, + * generation sending the result directly to an IAudioSink. + */ +class SinkMixer { + public: + SinkMixer(StreamBufferHandle_t dest); + ~SinkMixer(); + + auto MixAndSend(InputStream&, const StreamInfo::Pcm&) -> std::size_t; + + private: + auto Main() -> void; + + auto SetTargetFormat(const StreamInfo::Pcm& format) -> void; + auto HandleBytes() -> void; + + template + auto ConvertFixedToFloating(InputStream&, OutputStream&) -> void; + auto Resample(float, int, InputStream&, OutputStream&) -> void; + template + auto Quantise(InputStream&) -> std::size_t; + + enum class Command { + kReadBytes, + kSetSourceFormat, + kSetTargetFormat, + }; + + struct Args { + Command cmd; + StreamInfo::Pcm format; + }; + + QueueHandle_t commands_; + SemaphoreHandle_t is_idle_; + + SRC_STATE* resampler_; + + std::unique_ptr input_stream_; + std::unique_ptr floating_point_stream_; + std::unique_ptr resampled_stream_; + + cpp::span quantisation_buffer_; + cpp::span quantisation_buffer_as_shorts_; + cpp::span quantisation_buffer_as_ints_; + + StreamInfo::Pcm target_format_; + StreamBufferHandle_t source_; + StreamBufferHandle_t sink_; +}; + +template <> +auto SinkMixer::ConvertFixedToFloating(InputStream&, OutputStream&) + -> void; +template <> +auto SinkMixer::ConvertFixedToFloating(InputStream&, OutputStream&) + -> void; + +template <> +auto SinkMixer::Quantise(InputStream&) -> std::size_t; +template <> +auto SinkMixer::Quantise(InputStream&) -> std::size_t; + +} // namespace audio diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp index d48c39a8..d31e035c 100644 --- a/src/audio/include/stream_info.hpp +++ b/src/audio/include/stream_info.hpp @@ -56,6 +56,18 @@ class StreamInfo { bool operator==(const Encoded&) const = default; }; + /* + * Two-channel, interleaved, 32-bit floating point pcm samples. + */ + struct FloatingPointPcm { + // Number of channels in this stream. + uint8_t channels; + // The sample rate. + uint32_t sample_rate; + + bool operator==(const FloatingPointPcm&) const = default; + }; + struct Pcm { // Number of channels in this stream. uint8_t channels; @@ -64,10 +76,14 @@ class StreamInfo { // The sample rate. uint32_t sample_rate; + auto real_bytes_per_sample() const -> uint8_t { + return bits_per_sample == 16 ? 2 : 4; + } + bool operator==(const Pcm&) const = default; }; - typedef std::variant Format; + typedef std::variant Format; auto format() const -> const Format& { return format_; } auto set_format(Format f) -> void { format_ = f; } @@ -98,6 +114,12 @@ class RawStream { auto info() -> StreamInfo& { return info_; } auto data() -> cpp::span; + template + auto data_as() -> cpp::span { + auto orig = data(); + return {reinterpret_cast(orig.data()), orig.size_bytes() / sizeof(T)}; + } + auto empty() const -> bool { return info_.bytes_in_stream() == 0; } private: StreamInfo info_; @@ -114,6 +136,12 @@ class InputStream { const StreamInfo& info() const; cpp::span data() const; + template + auto data_as() const -> cpp::span { + auto orig = data(); + return {reinterpret_cast(orig.data()), + orig.size_bytes() / sizeof(T)}; + } private: RawStream* raw_; @@ -131,6 +159,11 @@ class OutputStream { const StreamInfo& info() const; cpp::span data() const; + template + auto data_as() const -> cpp::span { + auto orig = data(); + return {reinterpret_cast(orig.data()), orig.size_bytes() / sizeof(T)}; + } private: RawStream* raw_; diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp new file mode 100644 index 00000000..072dc9b7 --- /dev/null +++ b/src/audio/sink_mixer.cpp @@ -0,0 +1,301 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "sink_mixer.hpp" + +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/portmacro.h" +#include "freertos/projdefs.h" +#include "samplerate.h" + +#include "stream_info.hpp" +#include "tasks.hpp" + +static constexpr char kTag[] = "mixer"; + +static constexpr std::size_t kSourceBufferLength = 4 * 1024; +static constexpr std::size_t kInputBufferLength = 4 * 1024; +static constexpr std::size_t kReformatBufferLength = 4 * 1024; +static constexpr std::size_t kResampleBufferLength = kReformatBufferLength; +static constexpr std::size_t kQuantisedBufferLength = 2 * 1024; + +namespace audio { + +SinkMixer::SinkMixer(StreamBufferHandle_t dest) + : commands_(xQueueCreate(1, sizeof(Args))), + is_idle_(xSemaphoreCreateBinary()), + resampler_(nullptr), + source_(xStreamBufferCreate(kSourceBufferLength, 1)), + sink_(dest) { + input_stream_.reset(new RawStream(kInputBufferLength)); + floating_point_stream_.reset(new RawStream(kReformatBufferLength)); + resampled_stream_.reset(new RawStream(kResampleBufferLength)); + + quantisation_buffer_ = { + reinterpret_cast(heap_caps_malloc( + kQuantisedBufferLength, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + kQuantisedBufferLength}; + quantisation_buffer_as_ints_ = { + reinterpret_cast(quantisation_buffer_.data()), + quantisation_buffer_.size_bytes() / 4}; + quantisation_buffer_as_shorts_ = { + reinterpret_cast(quantisation_buffer_.data()), + quantisation_buffer_.size_bytes() / 2}; + + tasks::StartPersistent([&]() { Main(); }); +} + +SinkMixer::~SinkMixer() { + vQueueDelete(commands_); + vSemaphoreDelete(is_idle_); + vStreamBufferDelete(source_); + heap_caps_free(quantisation_buffer_.data()); + if (resampler_ != nullptr) { + src_delete(resampler_); + } +} + +auto SinkMixer::MixAndSend(InputStream& input, const StreamInfo::Pcm& target) + -> std::size_t { + if (input.info().format_as() != + input_stream_->info().format_as()) { + xSemaphoreTake(is_idle_, portMAX_DELAY); + Args args{ + .cmd = Command::kSetSourceFormat, + .format = input.info().format_as().value(), + }; + xQueueSend(commands_, &args, portMAX_DELAY); + xSemaphoreGive(is_idle_); + } + if (target_format_ != target) { + xSemaphoreTake(is_idle_, portMAX_DELAY); + Args args{ + .cmd = Command::kSetTargetFormat, + .format = target, + }; + xQueueSend(commands_, &args, portMAX_DELAY); + xSemaphoreGive(is_idle_); + } + + Args args{ + .cmd = Command::kReadBytes, + .format = {}, + }; + xQueueSend(commands_, &args, portMAX_DELAY); + + auto buf = input.data(); + std::size_t bytes_sent = + xStreamBufferSend(source_, buf.data(), buf.size_bytes(), portMAX_DELAY); + input.consume(bytes_sent); + return bytes_sent; +} + +auto SinkMixer::Main() -> void { + OutputStream input_receiver{input_stream_.get()}; + xSemaphoreGive(is_idle_); + + for (;;) { + Args args; + while (!xQueueReceive(commands_, &args, portMAX_DELAY)) { + } + switch (args.cmd) { + case Command::kSetSourceFormat: + ESP_LOGI(kTag, "setting source format"); + input_receiver.prepare(args.format, {}); + break; + case Command::kSetTargetFormat: + ESP_LOGI(kTag, "setting target format"); + target_format_ = args.format; + break; + case Command::kReadBytes: + xSemaphoreTake(is_idle_, 0); + while (!xStreamBufferIsEmpty(source_)) { + auto buf = input_receiver.data(); + std::size_t bytes_received = xStreamBufferReceive( + source_, buf.data(), buf.size_bytes(), portMAX_DELAY); + input_receiver.add(bytes_received); + HandleBytes(); + } + xSemaphoreGive(is_idle_); + break; + } + } +} + +auto SinkMixer::HandleBytes() -> void { + InputStream input{input_stream_.get()}; + auto pcm = input.info().format_as(); + if (!pcm) { + ESP_LOGE(kTag, "mixer got unsupported data"); + return; + } + + if (*pcm == target_format_) { + // The happiest possible case: the input format matches the output + // format already. Streams like this should probably have bypassed the + // mixer. + // TODO(jacqueline): Make this an error; it's slow to use the mixer in this + // case, compared to just writing directly to the sink. + auto buf = input.data(); + std::size_t bytes_sent = + xStreamBufferSend(sink_, buf.data(), buf.size_bytes(), portMAX_DELAY); + input.consume(bytes_sent); + return; + } + + // Work out the resampling ratio using floating point arithmetic, since + // relying on the FPU for this will be much faster, and the difference in + // accuracy is unlikely to be noticeable. + float src_ratio = static_cast(target_format_.sample_rate) / + static_cast(pcm->sample_rate); + + // Loop until we don't have any complete frames left in the input stream, + // where a 'frame' is one complete sample per channel. + while (!input_stream_->empty()) { + // The first step of both resampling and requantising is to convert the + // fixed point pcm input data into 32 bit floating point samples. + OutputStream floating_writer{floating_point_stream_.get()}; + if (pcm->bits_per_sample == 16) { + ConvertFixedToFloating(input, floating_writer); + } else { + // FIXME: We should consider treating 24 bit and 32 bit samples + // differently. + ConvertFixedToFloating(input, floating_writer); + } + + InputStream floating_reader{floating_point_stream_.get()}; + + while (!floating_point_stream_->empty()) { + RawStream* quantisation_source; + if (pcm->sample_rate != target_format_.sample_rate) { + // The input data needs to be resampled before being sent to the sink. + OutputStream resample_writer{resampled_stream_.get()}; + Resample(src_ratio, pcm->channels, floating_reader, resample_writer); + quantisation_source = resampled_stream_.get(); + } else { + // The input data already has an acceptable sample rate. All we need to + // do is quantise it. + quantisation_source = floating_point_stream_.get(); + } + + InputStream quantise_reader{quantisation_source}; + while (!quantisation_source->empty()) { + std::size_t samples_available; + if (target_format_.bits_per_sample == 16) { + samples_available = Quantise(quantise_reader); + } else { + samples_available = Quantise(quantise_reader); + } + + assert(samples_available * target_format_.real_bytes_per_sample() <= + quantisation_buffer_.size_bytes()); + + std::size_t bytes_sent = xStreamBufferSend( + sink_, quantisation_buffer_.data(), + samples_available * target_format_.real_bytes_per_sample(), + portMAX_DELAY); + assert(bytes_sent == + samples_available * target_format_.real_bytes_per_sample()); + } + } + } +} + +template <> +auto SinkMixer::ConvertFixedToFloating(InputStream& in_str, + OutputStream& out_str) -> void { + auto in = in_str.data_as(); + auto out = out_str.data_as(); + std::size_t samples_converted = std::min(in.size(), out.size()); + + src_short_to_float_array(in.data(), out.data(), samples_converted); + + in_str.consume(samples_converted * sizeof(short)); + out_str.add(samples_converted * sizeof(float)); +} + +template <> +auto SinkMixer::ConvertFixedToFloating(InputStream& in_str, + OutputStream& out_str) -> void { + auto in = in_str.data_as(); + auto out = out_str.data_as(); + std::size_t samples_converted = std::min(in.size(), out.size()); + + src_int_to_float_array(in.data(), out.data(), samples_converted); + + in_str.consume(samples_converted * sizeof(int)); + out_str.add(samples_converted * sizeof(float)); +} + +auto SinkMixer::Resample(float src_ratio, + int channels, + InputStream& in, + OutputStream& out) -> void { + if (resampler_ == nullptr || src_get_channels(resampler_) != channels) { + if (resampler_ != nullptr) { + src_delete(resampler_); + } + + ESP_LOGI(kTag, "creating new resampler with %u channels", channels); + + int err = 0; + resampler_ = src_new(SRC_LINEAR, channels, &err); + assert(resampler_ != NULL); + assert(err == 0); + } + + auto in_buf = in.data_as(); + auto out_buf = out.data_as(); + + src_set_ratio(resampler_, src_ratio); + SRC_DATA args{ + .data_in = in_buf.data(), + .data_out = out_buf.data(), + .input_frames = static_cast(in_buf.size()), + .output_frames = static_cast(out_buf.size()), + .input_frames_used = 0, + .output_frames_gen = 0, + .end_of_input = 0, + .src_ratio = src_ratio, + }; + int err = src_process(resampler_, &args); + if (err != 0) { + ESP_LOGE(kTag, "resampler error: %s", src_strerror(err)); + } + + in.consume(args.input_frames_used * sizeof(float)); + out.add(args.output_frames_gen * sizeof(float)); +} + +template <> +auto SinkMixer::Quantise(InputStream& in) -> std::size_t { + auto src = in.data_as(); + cpp::span dest = quantisation_buffer_as_shorts_; + dest = dest.first(std::min(src.size(), dest.size())); + + src_float_to_short_array(src.data(), dest.data(), dest.size()); + + in.consume(dest.size() * sizeof(float)); + return dest.size(); +} + +template <> +auto SinkMixer::Quantise(InputStream& in) -> std::size_t { + auto src = in.data_as(); + cpp::span dest = quantisation_buffer_as_ints_; + dest = dest.first(std::min(src.size(), dest.size())); + + src_float_to_int_array(src.data(), dest.data(), dest.size()); + + in.consume(dest.size() * sizeof(float)); + return dest.size(); +} + +} // namespace audio diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 9748522f..a02fa620 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -5,7 +5,7 @@ namespace drivers { auto Bluetooth::Enable() -> Bluetooth* { - return nullptr; + return nullptr; } } // namespace drivers diff --git a/src/drivers/i2s_dac.cpp b/src/drivers/i2s_dac.cpp index c835fb1f..885321d1 100644 --- a/src/drivers/i2s_dac.cpp +++ b/src/drivers/i2s_dac.cpp @@ -161,7 +161,6 @@ auto I2SDac::Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) word_length = 0b10; break; case BPS_32: - // TODO(jacqueline): Error on this? It's not supported anymore. slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT; slot_config_.ws_width = 32; word_length = 0b11; diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp index f3a4b2ac..22b58c8b 100644 --- a/src/drivers/include/bluetooth.hpp +++ b/src/drivers/include/bluetooth.hpp @@ -6,14 +6,15 @@ namespace drivers { class Bluetooth { - public: - static auto Enable() -> Bluetooth*; - Bluetooth(); - ~Bluetooth(); + public: + static auto Enable() -> Bluetooth*; + Bluetooth(); + ~Bluetooth(); - struct Device {}; - auto Scan() -> std::vector; - private: - }; + struct Device {}; + auto Scan() -> std::vector; -} + private: +}; + +} // namespace drivers diff --git a/src/drivers/include/i2s_dac.hpp b/src/drivers/include/i2s_dac.hpp index 06c0dc16..889ba68c 100644 --- a/src/drivers/include/i2s_dac.hpp +++ b/src/drivers/include/i2s_dac.hpp @@ -51,14 +51,12 @@ class I2SDac { BPS_32 = I2S_DATA_BIT_WIDTH_32BIT, }; enum SampleRate { - SAMPLE_RATE_11_025 = 11025, - SAMPLE_RATE_16 = 16000, - SAMPLE_RATE_22_05 = 22050, + SAMPLE_RATE_8 = 8000, SAMPLE_RATE_32 = 32000, SAMPLE_RATE_44_1 = 44100, SAMPLE_RATE_48 = 48000, + SAMPLE_RATE_88_2 = 88200, SAMPLE_RATE_96 = 96000, - SAMPLE_RATE_192 = 192000, }; auto Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) -> void; diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index dc188780..d30a712c 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -13,10 +13,10 @@ #include "database.hpp" #include "display.hpp" #include "gpios.hpp" +#include "nvs.hpp" #include "relative_wheel.hpp" #include "samd.hpp" #include "storage.hpp" -#include "nvs.hpp" #include "tag_parser.hpp" #include "tinyfsm.hpp" #include "touchwheel.hpp" diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp index 861c7bf0..34c690f3 100644 --- a/src/tasks/tasks.cpp +++ b/src/tasks/tasks.cpp @@ -34,6 +34,10 @@ auto Name() -> std::string { return "AUDIO"; } template <> +auto Name() -> std::string { + return "MIXER"; +} +template <> auto Name() -> std::string { return "DB"; } @@ -77,6 +81,14 @@ auto AllocateStack() -> cpp::span { size}; } +template <> +auto AllocateStack() -> cpp::span { + std::size_t size = 4 * 1024; + return {static_cast( + heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + size}; +} + // Leveldb is designed for non-embedded use cases, where stack space isn't so // much of a concern. It therefore uses an eye-wateringly large amount of stack. template <> @@ -105,6 +117,10 @@ auto Priority() -> UBaseType_t; // Realtime audio is the entire point of this device, so give this task the // highest priority. template <> +auto Priority() -> UBaseType_t { + return 12; +} +template <> auto Priority() -> UBaseType_t { return 11; } diff --git a/src/tasks/tasks.hpp b/src/tasks/tasks.hpp index 742bb3cc..1321aab8 100644 --- a/src/tasks/tasks.hpp +++ b/src/tasks/tasks.hpp @@ -36,6 +36,8 @@ enum class Type { kFileStreamer, // The main audio pipeline task. kAudio, + // TODO + kMixer, // Task for running database queries. kDatabase, // Task for internal database operations -- cgit v1.2.3 From 31f6123b7b7b21c005267ca98a64ef6d492d553e Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 3 Aug 2023 16:04:27 +1000 Subject: Tweak buffer size and placement --- src/audio/include/stream_info.hpp | 2 ++ src/audio/sink_mixer.cpp | 12 ++++++------ src/audio/stream_info.cpp | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp index d31e035c..7cf9e847 100644 --- a/src/audio/include/stream_info.hpp +++ b/src/audio/include/stream_info.hpp @@ -16,6 +16,7 @@ #include #include +#include "esp_heap_caps.h" #include "freertos/FreeRTOS.h" #include "freertos/ringbuf.h" #include "freertos/stream_buffer.h" @@ -110,6 +111,7 @@ class OutputStream; class RawStream { public: explicit RawStream(std::size_t size); + RawStream(std::size_t size, uint32_t); ~RawStream(); auto info() -> StreamInfo& { return info_; } diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index 072dc9b7..79e6f3d3 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -20,11 +20,11 @@ static constexpr char kTag[] = "mixer"; -static constexpr std::size_t kSourceBufferLength = 4 * 1024; -static constexpr std::size_t kInputBufferLength = 4 * 1024; -static constexpr std::size_t kReformatBufferLength = 4 * 1024; +static constexpr std::size_t kSourceBufferLength = 2 * 1024; +static constexpr std::size_t kInputBufferLength = 2 * 1024; +static constexpr std::size_t kReformatBufferLength = 8 * 1024; static constexpr std::size_t kResampleBufferLength = kReformatBufferLength; -static constexpr std::size_t kQuantisedBufferLength = 2 * 1024; +static constexpr std::size_t kQuantisedBufferLength = 1 * 1024; namespace audio { @@ -35,8 +35,8 @@ SinkMixer::SinkMixer(StreamBufferHandle_t dest) source_(xStreamBufferCreate(kSourceBufferLength, 1)), sink_(dest) { input_stream_.reset(new RawStream(kInputBufferLength)); - floating_point_stream_.reset(new RawStream(kReformatBufferLength)); - resampled_stream_.reset(new RawStream(kResampleBufferLength)); + floating_point_stream_.reset(new RawStream(kReformatBufferLength, MALLOC_CAP_SPIRAM)); + resampled_stream_.reset(new RawStream(kResampleBufferLength, MALLOC_CAP_SPIRAM)); quantisation_buffer_ = { reinterpret_cast(heap_caps_malloc( diff --git a/src/audio/stream_info.cpp b/src/audio/stream_info.cpp index 6efe297e..749e880e 100644 --- a/src/audio/stream_info.cpp +++ b/src/audio/stream_info.cpp @@ -30,6 +30,14 @@ RawStream::RawStream(std::size_t size) assert(buffer_ != NULL); } +RawStream::RawStream(std::size_t size, uint32_t caps) + : info_(), + buffer_size_(size), + buffer_(reinterpret_cast( + heap_caps_malloc(size, caps))) { + assert(buffer_ != NULL); +} + RawStream::~RawStream() { heap_caps_free(buffer_); } -- cgit v1.2.3 From 3b240d1cd5c52caf189ca036a1a841f7e6d84ccd Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 3 Aug 2023 16:16:46 +1000 Subject: remove stb_vorbis it doesnt work very well --- src/codecs/CMakeLists.txt | 4 ++-- src/codecs/codec.cpp | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/codecs/CMakeLists.txt b/src/codecs/CMakeLists.txt index 478d4d3f..1d76b85c 100644 --- a/src/codecs/CMakeLists.txt +++ b/src/codecs/CMakeLists.txt @@ -3,8 +3,8 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "codec.cpp" "mad.cpp" "foxenflac.cpp" "stbvorbis.cpp" + SRCS "codec.cpp" "mad.cpp" "foxenflac.cpp" INCLUDE_DIRS "include" - REQUIRES "result" "span" "libmad" "libfoxenflac" "stb_vorbis") + REQUIRES "result" "span" "libmad" "libfoxenflac") target_compile_options("${COMPONENT_LIB}" PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp index e23b8702..404ea214 100644 --- a/src/codecs/codec.cpp +++ b/src/codecs/codec.cpp @@ -11,7 +11,6 @@ #include "foxenflac.hpp" #include "mad.hpp" -#include "stbvorbis.hpp" #include "types.hpp" namespace codecs { @@ -22,8 +21,6 @@ auto CreateCodecForType(StreamType type) -> std::optional { return new MadMp3Decoder(); case StreamType::kFlac: return new FoxenFlacDecoder(); - case StreamType::kVorbis: - return new StbVorbisDecoder(); default: return {}; } -- cgit v1.2.3 From 60f767713227b5405b855e6e6e2a0475ecd96bcc Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 4 Aug 2023 20:07:44 +1000 Subject: Do our own resampling --- src/audio/CMakeLists.txt | 2 +- src/audio/audio_task.cpp | 12 +- src/audio/i2s_audio_output.cpp | 7 + src/audio/include/fir.h | 131 ++++++++++++++++++ src/audio/include/resample.hpp | 41 ++++++ src/audio/include/sink_mixer.hpp | 31 +---- src/audio/include/stream_info.hpp | 2 +- src/audio/resample.cpp | 260 +++++++++++++++++++++++++++++++++++ src/audio/sink_mixer.cpp | 211 +++++++++-------------------- src/codecs/foxenflac.cpp | 6 +- src/codecs/include/codec.hpp | 6 +- src/codecs/include/foxenflac.hpp | 3 +- src/codecs/include/mad.hpp | 3 +- src/codecs/include/sample.hpp | 59 ++++++++ src/codecs/mad.cpp | 40 ++---- src/codecs/sample.cpp | 275 ++++++++++++++++++++++++++++++++++++++ 16 files changed, 872 insertions(+), 217 deletions(-) create mode 100644 src/audio/include/fir.h create mode 100644 src/audio/include/resample.hpp create mode 100644 src/audio/resample.cpp create mode 100644 src/codecs/include/sample.hpp create mode 100644 src/codecs/sample.cpp (limited to 'src') diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 9e50f8ff..ddfc7eb4 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -5,7 +5,7 @@ idf_component_register( SRCS "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" - "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "sink_mixer.cpp" + "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "sink_mixer.cpp" "resample.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist" "libsamplerate") diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index c3498965..d7c0c209 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -34,6 +34,7 @@ #include "freertos/queue.h" #include "freertos/ringbuf.h" #include "pipeline.hpp" +#include "sample.hpp" #include "sink_mixer.hpp" #include "span.hpp" @@ -225,7 +226,7 @@ auto AudioTask::BeginDecoding(InputStream& stream) -> bool { codecs::ICodec::OutputFormat format = res.second.value(); StreamInfo::Pcm new_format{ .channels = format.num_channels, - .bits_per_sample = format.bits_per_sample, + .bits_per_sample = 32, .sample_rate = format.sample_rate_hz, }; @@ -255,7 +256,8 @@ auto AudioTask::ContinueDecoding(InputStream& stream) -> bool { while (!stream.data().empty()) { OutputStream writer{codec_buffer_.get()}; - auto res = codec_->ContinueStream(stream.data(), writer.data()); + auto res = + codec_->ContinueStream(stream.data(), writer.data_as()); stream.consume(res.first); @@ -266,7 +268,7 @@ auto AudioTask::ContinueDecoding(InputStream& stream) -> bool { return false; } } else { - writer.add(res.second->bytes_written); + writer.add(res.second->samples_written * sizeof(sample::Sample)); InputStream reader{codec_buffer_.get()}; SendToSink(reader); @@ -295,12 +297,12 @@ auto AudioTask::FinishDecoding(InputStream& stream) -> void { InputStream padded_stream{mad_buffer.get()}; OutputStream writer{codec_buffer_.get()}; - auto res = codec_->ContinueStream(stream.data(), writer.data()); + auto res = codec_->ContinueStream(stream.data(), writer.data_as()); if (res.second.has_error()) { return; } - writer.add(res.second->bytes_written); + writer.add(res.second->samples_written * sizeof(sample::Sample)); InputStream reader{codec_buffer_.get()}; SendToSink(reader); diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index e53dbe2a..4eab3e02 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -117,11 +117,18 @@ auto I2SAudioOutput::AdjustVolumeDown() -> bool { auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) -> StreamInfo::Pcm { + /* return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), }; + */ + return StreamInfo::Pcm{ + .channels = std::min(orig.channels, 2), + .bits_per_sample = 16, + .sample_rate = 48000, + }; } auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> void { diff --git a/src/audio/include/fir.h b/src/audio/include/fir.h new file mode 100644 index 00000000..e50c3eff --- /dev/null +++ b/src/audio/include/fir.h @@ -0,0 +1,131 @@ +/* + * FIR filter coefficients from resample-1.x smallfilter.h + * see Digital Audio Resampling Home Page located at + * http://ccrma.stanford.edu/~jos/resample/ + */ +32767, 32766, 32764, 32760, 32755, 32749, 32741, 32731, 32721, 32708, +32695, 32679, 32663, 32645, 32625, 32604, 32582, 32558, 32533, 32506, +32478, 32448, 32417, 32385, 32351, 32316, 32279, 32241, 32202, 32161, +32119, 32075, 32030, 31984, 31936, 31887, 31836, 31784, 31731, 31676, +31620, 31563, 31504, 31444, 31383, 31320, 31256, 31191, 31124, 31056, +30987, 30916, 30845, 30771, 30697, 30621, 30544, 30466, 30387, 30306, +30224, 30141, 30057, 29971, 29884, 29796, 29707, 29617, 29525, 29433, +29339, 29244, 29148, 29050, 28952, 28852, 28752, 28650, 28547, 28443, +28338, 28232, 28125, 28017, 27908, 27797, 27686, 27574, 27461, 27346, +27231, 27115, 26998, 26879, 26760, 26640, 26519, 26398, 26275, 26151, +26027, 25901, 25775, 25648, 25520, 25391, 25262, 25131, 25000, 24868, +24735, 24602, 24467, 24332, 24197, 24060, 23923, 23785, 23647, 23507, +23368, 23227, 23086, 22944, 22802, 22659, 22515, 22371, 22226, 22081, +21935, 21789, 21642, 21494, 21346, 21198, 21049, 20900, 20750, 20600, +20449, 20298, 20146, 19995, 19842, 19690, 19537, 19383, 19230, 19076, +18922, 18767, 18612, 18457, 18302, 18146, 17990, 17834, 17678, 17521, +17365, 17208, 17051, 16894, 16737, 16579, 16422, 16264, 16106, 15949, +15791, 15633, 15475, 15317, 15159, 15001, 14843, 14685, 14527, 14369, +14212, 14054, 13896, 13739, 13581, 13424, 13266, 13109, 12952, 12795, +12639, 12482, 12326, 12170, 12014, 11858, 11703, 11548, 11393, 11238, +11084, 10929, 10776, 10622, 10469, 10316, 10164, 10011, 9860, 9708, +9557, 9407, 9256, 9106, 8957, 8808, 8659, 8511, 8364, 8216, 8070, +7924, 7778, 7633, 7488, 7344, 7200, 7057, 6914, 6773, 6631, 6490, +6350, 6210, 6071, 5933, 5795, 5658, 5521, 5385, 5250, 5115, 4981, +4848, 4716, 4584, 4452, 4322, 4192, 4063, 3935, 3807, 3680, 3554, +3429, 3304, 3180, 3057, 2935, 2813, 2692, 2572, 2453, 2335, 2217, +2101, 1985, 1870, 1755, 1642, 1529, 1418, 1307, 1197, 1088, 979, 872, +765, 660, 555, 451, 348, 246, 145, 44, -54, -153, -250, -347, -443, +-537, -631, -724, -816, -908, -998, -1087, -1175, -1263, -1349, -1435, +-1519, -1603, -1685, -1767, -1848, -1928, -2006, -2084, -2161, -2237, +-2312, -2386, -2459, -2531, -2603, -2673, -2742, -2810, -2878, -2944, +-3009, -3074, -3137, -3200, -3261, -3322, -3381, -3440, -3498, -3554, +-3610, -3665, -3719, -3772, -3824, -3875, -3925, -3974, -4022, -4069, +-4116, -4161, -4205, -4249, -4291, -4333, -4374, -4413, -4452, -4490, +-4527, -4563, -4599, -4633, -4666, -4699, -4730, -4761, -4791, -4820, +-4848, -4875, -4901, -4926, -4951, -4974, -4997, -5019, -5040, -5060, +-5080, -5098, -5116, -5133, -5149, -5164, -5178, -5192, -5205, -5217, +-5228, -5238, -5248, -5257, -5265, -5272, -5278, -5284, -5289, -5293, +-5297, -5299, -5301, -5303, -5303, -5303, -5302, -5300, -5298, -5295, +-5291, -5287, -5282, -5276, -5270, -5263, -5255, -5246, -5237, -5228, +-5217, -5206, -5195, -5183, -5170, -5157, -5143, -5128, -5113, -5097, +-5081, -5064, -5047, -5029, -5010, -4991, -4972, -4952, -4931, -4910, +-4889, -4867, -4844, -4821, -4797, -4774, -4749, -4724, -4699, -4673, +-4647, -4620, -4593, -4566, -4538, -4510, -4481, -4452, -4422, -4393, +-4363, -4332, -4301, -4270, -4238, -4206, -4174, -4142, -4109, -4076, +-4042, -4009, -3975, -3940, -3906, -3871, -3836, -3801, -3765, -3729, +-3693, -3657, -3620, -3584, -3547, -3510, -3472, -3435, -3397, -3360, +-3322, -3283, -3245, -3207, -3168, -3129, -3091, -3052, -3013, -2973, +-2934, -2895, -2855, -2816, -2776, -2736, -2697, -2657, -2617, -2577, +-2537, -2497, -2457, -2417, -2377, -2337, -2297, -2256, -2216, -2176, +-2136, -2096, -2056, -2016, -1976, -1936, -1896, -1856, -1817, -1777, +-1737, -1698, -1658, -1619, -1579, -1540, -1501, -1462, -1423, -1384, +-1345, -1306, -1268, -1230, -1191, -1153, -1115, -1077, -1040, -1002, +-965, -927, -890, -854, -817, -780, -744, -708, -672, -636, -600, +-565, -530, -494, -460, -425, -391, -356, -322, -289, -255, -222, +-189, -156, -123, -91, -59, -27, 4, 35, 66, 97, 127, 158, 188, 218, +247, 277, 306, 334, 363, 391, 419, 447, 474, 501, 528, 554, 581, 606, +632, 657, 683, 707, 732, 756, 780, 803, 827, 850, 872, 895, 917, 939, +960, 981, 1002, 1023, 1043, 1063, 1082, 1102, 1121, 1139, 1158, 1176, +1194, 1211, 1228, 1245, 1262, 1278, 1294, 1309, 1325, 1340, 1354, +1369, 1383, 1397, 1410, 1423, 1436, 1448, 1461, 1473, 1484, 1496, +1507, 1517, 1528, 1538, 1548, 1557, 1566, 1575, 1584, 1592, 1600, +1608, 1616, 1623, 1630, 1636, 1643, 1649, 1654, 1660, 1665, 1670, +1675, 1679, 1683, 1687, 1690, 1694, 1697, 1700, 1702, 1704, 1706, +1708, 1709, 1711, 1712, 1712, 1713, 1713, 1713, 1713, 1712, 1711, +1710, 1709, 1708, 1706, 1704, 1702, 1700, 1697, 1694, 1691, 1688, +1685, 1681, 1677, 1673, 1669, 1664, 1660, 1655, 1650, 1644, 1639, +1633, 1627, 1621, 1615, 1609, 1602, 1596, 1589, 1582, 1575, 1567, +1560, 1552, 1544, 1536, 1528, 1520, 1511, 1503, 1494, 1485, 1476, +1467, 1458, 1448, 1439, 1429, 1419, 1409, 1399, 1389, 1379, 1368, +1358, 1347, 1337, 1326, 1315, 1304, 1293, 1282, 1271, 1260, 1248, +1237, 1225, 1213, 1202, 1190, 1178, 1166, 1154, 1142, 1130, 1118, +1106, 1094, 1081, 1069, 1057, 1044, 1032, 1019, 1007, 994, 981, 969, +956, 943, 931, 918, 905, 892, 879, 867, 854, 841, 828, 815, 802, 790, +777, 764, 751, 738, 725, 713, 700, 687, 674, 662, 649, 636, 623, 611, +598, 585, 573, 560, 548, 535, 523, 510, 498, 486, 473, 461, 449, 437, +425, 413, 401, 389, 377, 365, 353, 341, 330, 318, 307, 295, 284, 272, +261, 250, 239, 228, 217, 206, 195, 184, 173, 163, 152, 141, 131, 121, +110, 100, 90, 80, 70, 60, 51, 41, 31, 22, 12, 3, -5, -14, -23, -32, +-41, -50, -59, -67, -76, -84, -93, -101, -109, -117, -125, -133, -140, +-148, -156, -163, -170, -178, -185, -192, -199, -206, -212, -219, +-226, -232, -239, -245, -251, -257, -263, -269, -275, -280, -286, +-291, -297, -302, -307, -312, -317, -322, -327, -332, -336, -341, +-345, -349, -354, -358, -362, -366, -369, -373, -377, -380, -384, +-387, -390, -394, -397, -400, -402, -405, -408, -411, -413, -416, +-418, -420, -422, -424, -426, -428, -430, -432, -433, -435, -436, +-438, -439, -440, -442, -443, -444, -445, -445, -446, -447, -447, +-448, -448, -449, -449, -449, -449, -449, -449, -449, -449, -449, +-449, -449, -448, -448, -447, -447, -446, -445, -444, -443, -443, +-442, -441, -440, -438, -437, -436, -435, -433, -432, -430, -429, +-427, -426, -424, -422, -420, -419, -417, -415, -413, -411, -409, +-407, -405, -403, -400, -398, -396, -393, -391, -389, -386, -384, +-381, -379, -376, -374, -371, -368, -366, -363, -360, -357, -355, +-352, -349, -346, -343, -340, -337, -334, -331, -328, -325, -322, +-319, -316, -313, -310, -307, -304, -301, -298, -294, -291, -288, +-285, -282, -278, -275, -272, -269, -265, -262, -259, -256, -252, +-249, -246, -243, -239, -236, -233, -230, -226, -223, -220, -217, +-213, -210, -207, -204, -200, -197, -194, -191, -187, -184, -181, +-178, -175, -172, -168, -165, -162, -159, -156, -153, -150, -147, +-143, -140, -137, -134, -131, -128, -125, -122, -120, -117, -114, +-111, -108, -105, -102, -99, -97, -94, -91, -88, -86, -83, -80, -78, +-75, -72, -70, -67, -65, -62, -59, -57, -55, -52, -50, -47, -45, -43, +-40, -38, -36, -33, -31, -29, -27, -25, -22, -20, -18, -16, -14, -12, +-10, -8, -6, -4, -2, 0, 0, 2, 4, 6, 8, 9, 11, 13, 14, 16, 17, 19, 21, +22, 24, 25, 27, 28, 29, 31, 32, 33, 35, 36, 37, 38, 40, 41, 42, 43, +44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, 59, +59, 60, 61, 62, 62, 63, 63, 64, 64, 65, 66, 66, 66, 67, 67, 68, 68, +69, 69, 69, 70, 70, 70, 70, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, +72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, +72, 71, 71, 71, 71, 71, 70, 70, 70, 70, 69, 69, 69, 69, 68, 68, 68, +67, 67, 67, 66, 66, 66, 65, 65, 64, 64, 64, 63, 63, 62, 62, 62, 61, +61, 60, 60, 59, 59, 58, 58, 58, 57, 57, 56, 56, 55, 55, 54, 54, 53, +53, 52, 52, 51, 51, 50, 50, 49, 48, 48, 47, 47, 46, 46, 45, 45, 44, +44, 43, 43, 42, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, +35, 34, 34, 33, 33, 32, 32, 31, 31, 30, 30, 29, 29, 28, 28, 27, 27, +26, 26, 25, 25, 24, 24, 23, 23, 23, 22, 22, 21, 21, 20, 20, 20, 19, +19, 18, 18, 17, 17, 17, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 12, +12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6, 6, +6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, +1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, +-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, +-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, +-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, +-2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, +-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, diff --git a/src/audio/include/resample.hpp b/src/audio/include/resample.hpp new file mode 100644 index 00000000..d7933470 --- /dev/null +++ b/src/audio/include/resample.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include "span.hpp" + +#include "sample.hpp" + +namespace audio { + +class Channel; + +class Resampler { + public: + Resampler(uint32_t source_sample_rate, + uint32_t target_sample_rate, + uint8_t num_channels); + + ~Resampler(); + + auto source_sample_rate() -> uint32_t { return source_sample_rate_; } + auto target_sample_rate() -> uint32_t { return target_sample_rate_; } + auto channels() -> uint_fast8_t { return num_channels_; } + + auto Process(cpp::span input, + cpp::span output, + bool end_of_data) -> std::pair; + + private: + auto ApplyDither(cpp::span) -> void; + + uint32_t source_sample_rate_; + uint32_t target_sample_rate_; + uint32_t factor_; + + uint8_t num_channels_; + std::vector channels_; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/audio/include/sink_mixer.hpp b/src/audio/include/sink_mixer.hpp index 632ffa2e..1bf12016 100644 --- a/src/audio/include/sink_mixer.hpp +++ b/src/audio/include/sink_mixer.hpp @@ -10,6 +10,8 @@ #include #include +#include "resample.hpp" +#include "sample.hpp" #include "samplerate.h" #include "audio_decoder.hpp" @@ -38,12 +40,10 @@ class SinkMixer { auto SetTargetFormat(const StreamInfo::Pcm& format) -> void; auto HandleBytes() -> void; - template - auto ConvertFixedToFloating(InputStream&, OutputStream&) -> void; - auto Resample(float, int, InputStream&, OutputStream&) -> void; - template - auto Quantise(InputStream&) -> std::size_t; - + auto Resample(InputStream&, OutputStream&) -> bool; + auto ApplyDither(cpp::span samples, uint_fast8_t bits) -> void; + auto Downscale(cpp::span, cpp::span) -> void; + enum class Command { kReadBytes, kSetSourceFormat, @@ -58,31 +58,14 @@ class SinkMixer { QueueHandle_t commands_; SemaphoreHandle_t is_idle_; - SRC_STATE* resampler_; + std::unique_ptr resampler_; std::unique_ptr input_stream_; - std::unique_ptr floating_point_stream_; std::unique_ptr resampled_stream_; - cpp::span quantisation_buffer_; - cpp::span quantisation_buffer_as_shorts_; - cpp::span quantisation_buffer_as_ints_; - StreamInfo::Pcm target_format_; StreamBufferHandle_t source_; StreamBufferHandle_t sink_; }; -template <> -auto SinkMixer::ConvertFixedToFloating(InputStream&, OutputStream&) - -> void; -template <> -auto SinkMixer::ConvertFixedToFloating(InputStream&, OutputStream&) - -> void; - -template <> -auto SinkMixer::Quantise(InputStream&) -> std::size_t; -template <> -auto SinkMixer::Quantise(InputStream&) -> std::size_t; - } // namespace audio diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp index 7cf9e847..01dd282a 100644 --- a/src/audio/include/stream_info.hpp +++ b/src/audio/include/stream_info.hpp @@ -77,7 +77,7 @@ class StreamInfo { // The sample rate. uint32_t sample_rate; - auto real_bytes_per_sample() const -> uint8_t { + auto bytes_per_sample() const -> uint8_t { return bits_per_sample == 16 ? 2 : 4; } diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp new file mode 100644 index 00000000..93ea1034 --- /dev/null +++ b/src/audio/resample.cpp @@ -0,0 +1,260 @@ +#include "resample.hpp" + +#include +#include +#include +#include +#include + +#include "esp_log.h" + +#include "sample.hpp" +#include "stream_info.hpp" + +namespace audio { + +static constexpr size_t kFilterSize = 1536; + +constexpr auto calc_deltas(const std::array& filter) + -> std::array { + std::array deltas; + for (size_t n = 0; n < kFilterSize - 1; n++) + deltas[n] = filter[n + 1] - filter[n]; + return deltas; +} + +static const std::array kFilter{ +#include "fir.h" +}; + +static const std::array kFilterDeltas = + calc_deltas(kFilter); + +class Channel { + public: + Channel(uint32_t src_rate, + uint32_t dest_rate, + size_t chunk_size, + size_t skip); + ~Channel(); + + auto output_chunk_size() -> size_t { return output_chunk_size_; } + + auto FlushSamples(cpp::span out) -> size_t; + auto AddSample(sample::Sample, cpp::span out) -> std::size_t; + auto ApplyFilter() -> sample::Sample; + + private: + size_t output_chunk_size_; + size_t skip_; + + uint32_t factor_; /* factor */ + + uint32_t time_; /* time */ + + uint32_t time_per_filter_iteration_; /* output step */ + uint32_t filter_step_; /* filter step */ + uint32_t filter_end_; /* filter end */ + + int32_t unity_scale_; /* unity scale */ + + int32_t samples_per_filter_wing_; /* extra samples */ + int32_t latest_sample_; /* buffer index */ + cpp::span sample_buffer_; /* the buffer */ +}; + +enum { + Nl = 8, /* 2^Nl samples per zero crossing in fir */ + Nη = 8, /* phase bits for filter interpolation */ + kPhaseBits = Nl + Nη, /* phase bits (fract of fixed point) */ + One = 1 << kPhaseBits, +}; + +Channel::Channel(uint32_t irate, uint32_t orate, size_t count, size_t skip) + : skip_(skip) { + factor_ = ((uint64_t)orate << kPhaseBits) / irate; + if (factor_ != One) { + time_per_filter_iteration_ = ((uint64_t)irate << kPhaseBits) / orate; + filter_step_ = 1 << (Nl + Nη); + filter_end_ = kFilterSize << Nη; + samples_per_filter_wing_ = 1 + (filter_end_ / filter_step_); + unity_scale_ = 13128; /* unity scale factor for fir */ + if (factor_ < One) { + unity_scale_ *= factor_; + unity_scale_ >>= kPhaseBits; + filter_step_ *= factor_; + filter_step_ >>= kPhaseBits; + samples_per_filter_wing_ *= time_per_filter_iteration_; + samples_per_filter_wing_ >>= kPhaseBits; + } + latest_sample_ = samples_per_filter_wing_; + time_ = latest_sample_ << kPhaseBits; + + size_t buf_size = samples_per_filter_wing_ * 2 + count; + int32_t* buf = new int32_t[buf_size]; + sample_buffer_ = {buf, buf_size}; + count += buf_size; /* account for buffer accumulation */ + } + output_chunk_size_ = ((uint64_t)count * factor_) >> kPhaseBits; +} + +Channel::~Channel() { + delete sample_buffer_.data(); +} + +auto Channel::ApplyFilter() -> sample::Sample { + uint32_t iteration, p, i; + int32_t *sample, a; + + int64_t value = 0; + + // I did my best, but I'll be honest with you I've no idea about any of this + // maths stuff. + + // Left wing of the filter. + sample = &sample_buffer_[time_ >> kPhaseBits]; + p = time_ & ((1 << kPhaseBits) - 1); + iteration = factor_ < One ? (factor_ * p) >> kPhaseBits : p; + while (iteration < filter_end_) { + i = iteration >> Nη; + a = iteration & ((1 << Nη) - 1); + iteration += filter_step_; + a *= kFilterDeltas[i]; + a >>= Nη; + a += kFilter[i]; + value += static_cast(*--sample) * a; + } + + // Right wing of the filter. + sample = &sample_buffer_[time_ >> kPhaseBits]; + p = (One - p) & ((1 << kPhaseBits) - 1); + iteration = factor_ < One ? (factor_ * p) >> kPhaseBits : p; + if (p == 0) /* skip h[0] as it was already been summed above if p == 0 */ + iteration += filter_step_; + while (iteration < filter_end_) { + i = iteration >> Nη; + a = iteration & ((1 << Nη) - 1); + iteration += filter_step_; + a *= kFilterDeltas[i]; + a >>= Nη; + a += kFilter[i]; + value += static_cast(*sample++) * a; + } + + /* scale */ + value >>= 2; + value *= unity_scale_; + value >>= 27; + + return sample::Clip(value); +} + +auto Channel::FlushSamples(cpp::span out) -> size_t { + size_t zeroes_needed = (2 * samples_per_filter_wing_) - latest_sample_; + size_t produced = 0; + while (zeroes_needed > 0) { + produced += AddSample(0, out.subspan(produced)); + zeroes_needed--; + } + return produced; +} + +auto Channel::AddSample(sample::Sample in, cpp::span out) + -> size_t { + // Add the latest sample to our working buffer. + sample_buffer_[latest_sample_++] = in; + + // If we don't have enough samples to run the filter, then bail out and wait + // for more. + if (latest_sample_ < 2 * samples_per_filter_wing_) { + return 0; + } + + // Apply the filter to the buffered samples. First, we work out how long (in + // samples) we can run the filter for before running out. This isn't as + // trivial as it might look; e.g. depending on the resampling factor we might + // be doubling the number of samples, or halving them. + uint32_t max_time = (latest_sample_ - samples_per_filter_wing_) << kPhaseBits; + size_t samples_output = 0; + while (time_ < max_time) { + out[skip_ * samples_output++] = ApplyFilter(); + time_ += time_per_filter_iteration_; + } + + // If we are approaching the end of our buffer, we need to shift all the data + // in it down to the front to make room for more samples. + int32_t current_sample = time_ >> kPhaseBits; + if (current_sample >= (sample_buffer_.size() - samples_per_filter_wing_)) { + // NB: bit shifting back and forth means we're only modifying `time` by + // whole samples. + time_ -= current_sample << kPhaseBits; + time_ += samples_per_filter_wing_ << kPhaseBits; + + int32_t new_current_sample = time_ >> kPhaseBits; + new_current_sample -= samples_per_filter_wing_; + current_sample -= samples_per_filter_wing_; + + int32_t samples_to_move = latest_sample_ - current_sample; + if (samples_to_move > 0) { + auto samples = sample_buffer_.subspan(current_sample, samples_to_move); + std::copy_backward(samples.begin(), samples.end(), + sample_buffer_.first(new_current_sample).end()); + latest_sample_ = new_current_sample + samples_to_move; + } else { + latest_sample_ = new_current_sample; + } + } + + return samples_output; +} + +static const size_t kChunkSizeSamples = 256; + +Resampler::Resampler(uint32_t source_sample_rate, + uint32_t target_sample_rate, + uint8_t num_channels) + : source_sample_rate_(source_sample_rate), + target_sample_rate_(target_sample_rate), + factor_(((uint64_t)target_sample_rate << kPhaseBits) / + source_sample_rate), + num_channels_(num_channels), + channels_() { + for (int i = 0; i < num_channels; i++) { + channels_.emplace_back(source_sample_rate, target_sample_rate, + kChunkSizeSamples, num_channels); + } +} + +Resampler::~Resampler() {} + +auto Resampler::Process(cpp::span input, + cpp::span output, + bool end_of_data) -> std::pair { + size_t samples_used = 0; + std::vector samples_produced = {num_channels_, 0}; + size_t total_samples_produced = 0; + + size_t slop = (factor_ >> kPhaseBits) + 1; + + uint_fast8_t cur_channel = 0; + + while (input.size() > samples_used && + output.size() > total_samples_produced + slop) { + // Work out where the next set of samples should be placed. + size_t next_output_index = + (samples_produced[cur_channel] * num_channels_) + cur_channel; + + // Generate the next samples + size_t new_samples = channels_[cur_channel].AddSample( + input[samples_used++], output.subspan(next_output_index)); + + samples_produced[cur_channel] += new_samples; + total_samples_produced += new_samples; + + cur_channel = (cur_channel + 1) % num_channels_; + } + + return {samples_used, total_samples_produced}; +} + +} // namespace audio diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index 79e6f3d3..ba306626 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -13,6 +13,8 @@ #include "esp_log.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" +#include "resample.hpp" +#include "sample.hpp" #include "samplerate.h" #include "stream_info.hpp" @@ -21,10 +23,7 @@ static constexpr char kTag[] = "mixer"; static constexpr std::size_t kSourceBufferLength = 2 * 1024; -static constexpr std::size_t kInputBufferLength = 2 * 1024; -static constexpr std::size_t kReformatBufferLength = 8 * 1024; -static constexpr std::size_t kResampleBufferLength = kReformatBufferLength; -static constexpr std::size_t kQuantisedBufferLength = 1 * 1024; +static constexpr std::size_t kSampleBufferLength = 4 * 1024; namespace audio { @@ -34,20 +33,8 @@ SinkMixer::SinkMixer(StreamBufferHandle_t dest) resampler_(nullptr), source_(xStreamBufferCreate(kSourceBufferLength, 1)), sink_(dest) { - input_stream_.reset(new RawStream(kInputBufferLength)); - floating_point_stream_.reset(new RawStream(kReformatBufferLength, MALLOC_CAP_SPIRAM)); - resampled_stream_.reset(new RawStream(kResampleBufferLength, MALLOC_CAP_SPIRAM)); - - quantisation_buffer_ = { - reinterpret_cast(heap_caps_malloc( - kQuantisedBufferLength, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), - kQuantisedBufferLength}; - quantisation_buffer_as_ints_ = { - reinterpret_cast(quantisation_buffer_.data()), - quantisation_buffer_.size_bytes() / 4}; - quantisation_buffer_as_shorts_ = { - reinterpret_cast(quantisation_buffer_.data()), - quantisation_buffer_.size_bytes() / 2}; + input_stream_.reset(new RawStream(kSampleBufferLength)); + resampled_stream_.reset(new RawStream(kSampleBufferLength)); tasks::StartPersistent([&]() { Main(); }); } @@ -56,10 +43,6 @@ SinkMixer::~SinkMixer() { vQueueDelete(commands_); vSemaphoreDelete(is_idle_); vStreamBufferDelete(source_); - heap_caps_free(quantisation_buffer_.data()); - if (resampler_ != nullptr) { - src_delete(resampler_); - } } auto SinkMixer::MixAndSend(InputStream& input, const StreamInfo::Pcm& target) @@ -109,10 +92,12 @@ auto SinkMixer::Main() -> void { case Command::kSetSourceFormat: ESP_LOGI(kTag, "setting source format"); input_receiver.prepare(args.format, {}); + resampler_.reset(); break; case Command::kSetTargetFormat: ESP_LOGI(kTag, "setting target format"); target_format_ = args.format; + resampler_.reset(); break; case Command::kReadBytes: xSemaphoreTake(is_idle_, 0); @@ -150,152 +135,84 @@ auto SinkMixer::HandleBytes() -> void { return; } - // Work out the resampling ratio using floating point arithmetic, since - // relying on the FPU for this will be much faster, and the difference in - // accuracy is unlikely to be noticeable. - float src_ratio = static_cast(target_format_.sample_rate) / - static_cast(pcm->sample_rate); - - // Loop until we don't have any complete frames left in the input stream, - // where a 'frame' is one complete sample per channel. while (!input_stream_->empty()) { - // The first step of both resampling and requantising is to convert the - // fixed point pcm input data into 32 bit floating point samples. - OutputStream floating_writer{floating_point_stream_.get()}; - if (pcm->bits_per_sample == 16) { - ConvertFixedToFloating(input, floating_writer); + RawStream* output_source; + if (pcm->sample_rate != target_format_.sample_rate) { + OutputStream resampled_writer{resampled_stream_.get()}; + if (Resample(input, resampled_writer)) { + // Zero samples used or written. We need more input. + break; + } + output_source = resampled_stream_.get(); } else { - // FIXME: We should consider treating 24 bit and 32 bit samples - // differently. - ConvertFixedToFloating(input, floating_writer); + output_source = input_stream_.get(); } - InputStream floating_reader{floating_point_stream_.get()}; + if (target_format_.bits_per_sample == 16) { + // This is slightly scary; we're basically reaching into the internals of + // the stream buffer to do in-place conversion of samples. Saving an + // extra buffer + copy into that buffer is certainly worth it however. + cpp::span src = + output_source->data_as().first( + output_source->info().bytes_in_stream() / sizeof(sample::Sample)); + cpp::span dest = output_source->data_as().first( + output_source->info().bytes_in_stream() / sizeof(int16_t)); - while (!floating_point_stream_->empty()) { - RawStream* quantisation_source; - if (pcm->sample_rate != target_format_.sample_rate) { - // The input data needs to be resampled before being sent to the sink. - OutputStream resample_writer{resampled_stream_.get()}; - Resample(src_ratio, pcm->channels, floating_reader, resample_writer); - quantisation_source = resampled_stream_.get(); - } else { - // The input data already has an acceptable sample rate. All we need to - // do is quantise it. - quantisation_source = floating_point_stream_.get(); - } + ApplyDither(src, 16); + Downscale(src, dest); - InputStream quantise_reader{quantisation_source}; - while (!quantisation_source->empty()) { - std::size_t samples_available; - if (target_format_.bits_per_sample == 16) { - samples_available = Quantise(quantise_reader); - } else { - samples_available = Quantise(quantise_reader); - } + output_source->info().bytes_in_stream() /= 2; + } - assert(samples_available * target_format_.real_bytes_per_sample() <= - quantisation_buffer_.size_bytes()); + InputStream output{output_source}; + cpp::span buf = output.data(); - std::size_t bytes_sent = xStreamBufferSend( - sink_, quantisation_buffer_.data(), - samples_available * target_format_.real_bytes_per_sample(), - portMAX_DELAY); - assert(bytes_sent == - samples_available * target_format_.real_bytes_per_sample()); - } + size_t bytes_sent = 0; + while (bytes_sent < buf.size_bytes()) { + auto cropped = buf.subspan(bytes_sent); + bytes_sent += xStreamBufferSend(sink_, cropped.data(), + cropped.size_bytes(), portMAX_DELAY); } + output.consume(bytes_sent); } } -template <> -auto SinkMixer::ConvertFixedToFloating(InputStream& in_str, - OutputStream& out_str) -> void { - auto in = in_str.data_as(); - auto out = out_str.data_as(); - std::size_t samples_converted = std::min(in.size(), out.size()); +auto SinkMixer::Resample(InputStream& in, OutputStream& out) -> bool { + if (resampler_ == nullptr) { + ESP_LOGI(kTag, "creating new resampler"); + auto format = in.info().format_as(); + resampler_.reset(new Resampler( + format->sample_rate, target_format_.sample_rate, format->channels)); + } - src_short_to_float_array(in.data(), out.data(), samples_converted); + auto res = resampler_->Process(in.data_as(), + out.data_as(), false); - in_str.consume(samples_converted * sizeof(short)); - out_str.add(samples_converted * sizeof(float)); -} + ESP_LOGI(kTag, "resampler sent %u samples, consumed %u, produced %u", + in.data().size(), res.first, res.second); -template <> -auto SinkMixer::ConvertFixedToFloating(InputStream& in_str, - OutputStream& out_str) -> void { - auto in = in_str.data_as(); - auto out = out_str.data_as(); - std::size_t samples_converted = std::min(in.size(), out.size()); + in.consume(res.first * sizeof(sample::Sample)); + out.add(res.first * sizeof(sample::Sample)); - src_int_to_float_array(in.data(), out.data(), samples_converted); - - in_str.consume(samples_converted * sizeof(int)); - out_str.add(samples_converted * sizeof(float)); + return res.first == 0 && res.second == 0; } -auto SinkMixer::Resample(float src_ratio, - int channels, - InputStream& in, - OutputStream& out) -> void { - if (resampler_ == nullptr || src_get_channels(resampler_) != channels) { - if (resampler_ != nullptr) { - src_delete(resampler_); - } - - ESP_LOGI(kTag, "creating new resampler with %u channels", channels); - - int err = 0; - resampler_ = src_new(SRC_LINEAR, channels, &err); - assert(resampler_ != NULL); - assert(err == 0); +auto SinkMixer::Downscale(cpp::span samples, + cpp::span output) -> void { + for (size_t i = 0; i < samples.size(); i++) { + output[i] = sample::ToSigned16Bit(samples[i]); } - - auto in_buf = in.data_as(); - auto out_buf = out.data_as(); - - src_set_ratio(resampler_, src_ratio); - SRC_DATA args{ - .data_in = in_buf.data(), - .data_out = out_buf.data(), - .input_frames = static_cast(in_buf.size()), - .output_frames = static_cast(out_buf.size()), - .input_frames_used = 0, - .output_frames_gen = 0, - .end_of_input = 0, - .src_ratio = src_ratio, - }; - int err = src_process(resampler_, &args); - if (err != 0) { - ESP_LOGE(kTag, "resampler error: %s", src_strerror(err)); - } - - in.consume(args.input_frames_used * sizeof(float)); - out.add(args.output_frames_gen * sizeof(float)); } -template <> -auto SinkMixer::Quantise(InputStream& in) -> std::size_t { - auto src = in.data_as(); - cpp::span dest = quantisation_buffer_as_shorts_; - dest = dest.first(std::min(src.size(), dest.size())); - - src_float_to_short_array(src.data(), dest.data(), dest.size()); - - in.consume(dest.size() * sizeof(float)); - return dest.size(); -} - -template <> -auto SinkMixer::Quantise(InputStream& in) -> std::size_t { - auto src = in.data_as(); - cpp::span dest = quantisation_buffer_as_ints_; - dest = dest.first(std::min(src.size(), dest.size())); - - src_float_to_int_array(src.data(), dest.data(), dest.size()); - - in.consume(dest.size() * sizeof(float)); - return dest.size(); +auto SinkMixer::ApplyDither(cpp::span samples, + uint_fast8_t bits) -> void { + static uint32_t prnd; + for (auto& s : samples) { + prnd = (prnd * 0x19660dL + 0x3c6ef35fL) & 0xffffffffL; + s = sample::Clip( + static_cast(s) + + (static_cast(prnd) >> (sizeof(sample::Sample) - bits))); + } } } // namespace audio diff --git a/src/codecs/foxenflac.cpp b/src/codecs/foxenflac.cpp index 3a727ce2..b676f82a 100644 --- a/src/codecs/foxenflac.cpp +++ b/src/codecs/foxenflac.cpp @@ -12,6 +12,7 @@ #include "esp_log.h" #include "foxen/flac.h" +#include "sample.hpp" namespace codecs { @@ -47,7 +48,6 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span input) OutputFormat format{ .num_channels = static_cast(channels), - .bits_per_sample = 32, // libfoxenflac output is fixed-size. .sample_rate_hz = static_cast(fs), .duration_seconds = {}, .bits_per_second = {}, @@ -62,7 +62,7 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span input) } auto FoxenFlacDecoder::ContinueStream(cpp::span input, - cpp::span output) + cpp::span output) -> Result { cpp::span output_as_samples{ reinterpret_cast(output.data()), output.size_bytes() / 4}; @@ -78,7 +78,7 @@ auto FoxenFlacDecoder::ContinueStream(cpp::span input, if (samples_written > 0) { return {bytes_read, - OutputInfo{.bytes_written = samples_written * 4, + OutputInfo{.samples_written = samples_written, .is_finished_writing = state == FLAC_END_OF_FRAME}}; } diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index e8be8f0a..f260aca4 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -16,6 +16,7 @@ #include #include +#include "sample.hpp" #include "result.hpp" #include "span.hpp" #include "types.hpp" @@ -61,7 +62,6 @@ class ICodec { struct OutputFormat { uint8_t num_channels; - uint8_t bits_per_sample; uint32_t sample_rate_hz; std::optional duration_seconds; @@ -76,7 +76,7 @@ class ICodec { -> Result = 0; struct OutputInfo { - std::size_t bytes_written; + std::size_t samples_written; bool is_finished_writing; }; @@ -84,7 +84,7 @@ class ICodec { * Writes PCM samples to the given output buffer. */ virtual auto ContinueStream(cpp::span input, - cpp::span output) + cpp::span output) -> Result = 0; virtual auto SeekStream(cpp::span input, diff --git a/src/codecs/include/foxenflac.hpp b/src/codecs/include/foxenflac.hpp index cce1b762..abfa6d80 100644 --- a/src/codecs/include/foxenflac.hpp +++ b/src/codecs/include/foxenflac.hpp @@ -14,6 +14,7 @@ #include #include "foxen/flac.h" +#include "sample.hpp" #include "span.hpp" #include "codec.hpp" @@ -26,7 +27,7 @@ class FoxenFlacDecoder : public ICodec { ~FoxenFlacDecoder(); auto BeginStream(cpp::span) -> Result override; - auto ContinueStream(cpp::span, cpp::span) + auto ContinueStream(cpp::span, cpp::span) -> Result override; auto SeekStream(cpp::span input, std::size_t target_sample) -> Result override; diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp index fbae560c..b81e4acb 100644 --- a/src/codecs/include/mad.hpp +++ b/src/codecs/include/mad.hpp @@ -13,6 +13,7 @@ #include #include "mad.h" +#include "sample.hpp" #include "span.hpp" #include "codec.hpp" @@ -35,7 +36,7 @@ class MadMp3Decoder : public ICodec { * Writes samples for the current frame. */ auto ContinueStream(cpp::span input, - cpp::span output) + cpp::span output) -> Result override; auto SeekStream(cpp::span input, std::size_t target_sample) diff --git a/src/codecs/include/sample.hpp b/src/codecs/include/sample.hpp new file mode 100644 index 00000000..7209673b --- /dev/null +++ b/src/codecs/include/sample.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include + +#include + +namespace sample { + +// A signed, 32-bit PCM sample. +typedef int32_t Sample; + +constexpr auto Clip(int64_t v) -> Sample { + if (v > INT32_MAX) + return INT32_MAX; + if (v < INT32_MIN) + return INT32_MIN; + return v; +} + +constexpr auto FromSigned(int32_t src, uint_fast8_t bits) -> Sample { + // Left-align samples, effectively scaling them up to 32 bits. + return src << (sizeof(Sample) * 8 - bits); +} + +constexpr auto FromUnsigned(uint32_t src, uint_fast8_t bits) -> Sample { + // Left-align, then substract the max value / 2 to make the sample centred + // around zero. + return (src << (sizeof(uint32_t) * 8 - bits)) - (~0UL >> 1); +} + +constexpr auto FromFloat(float src) -> Sample { + return std::clamp(src, -1.0f, 1.0f) * static_cast(INT32_MAX); +} + +constexpr auto FromDouble(double src) -> Sample { + return std::clamp(src, -1.0, 1.0) * static_cast(INT32_MAX); +} + +constexpr auto FromMad(mad_fixed_t src) -> Sample { + // Round the bottom bits. + src += (1L << (MAD_F_FRACBITS - 24)); + + // Clip the leftover bits to within range. + if (src >= MAD_F_ONE) + src = MAD_F_ONE - 1; + else if (src < -MAD_F_ONE) + src = -MAD_F_ONE; + + // Quantize. + return FromSigned(src >> (MAD_F_FRACBITS + 1 - 24), 24); +} + +constexpr auto ToSigned16Bit(Sample src) -> uint16_t { + return src >> 16; +} + +} // namespace sample diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp index 29e34a0f..a2739bcd 100644 --- a/src/codecs/mad.cpp +++ b/src/codecs/mad.cpp @@ -17,24 +17,11 @@ #include "codec.hpp" #include "esp_log.h" #include "result.hpp" +#include "sample.hpp" #include "types.hpp" namespace codecs { -static uint32_t mad_fixed_to_pcm(mad_fixed_t sample, uint8_t bits) { - // Round the bottom bits. - sample += (1L << (MAD_F_FRACBITS - bits)); - - // Clip the leftover bits to within range. - if (sample >= MAD_F_ONE) - sample = MAD_F_ONE - 1; - else if (sample < -MAD_F_ONE) - sample = -MAD_F_ONE; - - // Quantize. - return sample >> (MAD_F_FRACBITS + 1 - bits); -} - MadMp3Decoder::MadMp3Decoder() { mad_stream_init(&stream_); mad_frame_init(&frame_); @@ -83,7 +70,6 @@ auto MadMp3Decoder::BeginStream(const cpp::span input) uint8_t channels = MAD_NCHANNELS(&header); OutputFormat output{ .num_channels = channels, - .bits_per_sample = 24, // We always scale to 24 bits .sample_rate_hz = header.samplerate, .duration_seconds = {}, .bits_per_second = {}, @@ -100,7 +86,7 @@ auto MadMp3Decoder::BeginStream(const cpp::span input) } auto MadMp3Decoder::ContinueStream(cpp::span input, - cpp::span output) + cpp::span output) -> Result { std::size_t bytes_read = 0; if (current_sample_ < 0) { @@ -133,32 +119,24 @@ auto MadMp3Decoder::ContinueStream(cpp::span input, bytes_read = GetBytesUsed(input.size_bytes()); } - size_t output_byte = 0; + size_t output_sample = 0; while (current_sample_ < synth_.pcm.length) { - if (output_byte + (4 * synth_.pcm.channels) >= output.size()) { - // We can't fit the next sample into the buffer. Stop now, and also avoid - // writing the sample for only half the channels. - return {bytes_read, OutputInfo{.bytes_written = output_byte, + if (output_sample + synth_.pcm.channels >= output.size()) { + // We can't fit the next full frame into the buffer. + return {bytes_read, OutputInfo{.samples_written = output_sample, .is_finished_writing = false}}; } for (int channel = 0; channel < synth_.pcm.channels; channel++) { - uint32_t sample_24 = - mad_fixed_to_pcm(synth_.pcm.samples[channel][current_sample_], 24); - - // 24 bit samples must still be aligned to 32 bits. The LSB is ignored. - output[output_byte++] = static_cast(0); - - output[output_byte++] = static_cast((sample_24)&0xFF); - output[output_byte++] = static_cast((sample_24 >> 8) & 0xFF); - output[output_byte++] = static_cast((sample_24 >> 16) & 0xFF); + output[output_sample++] = + sample::FromMad(synth_.pcm.samples[channel][current_sample_]); } current_sample_++; } // We wrote everything! Reset, ready for the next frame. current_sample_ = -1; - return {bytes_read, OutputInfo{.bytes_written = output_byte, + return {bytes_read, OutputInfo{.samples_written = output_sample, .is_finished_writing = true}}; } diff --git a/src/codecs/sample.cpp b/src/codecs/sample.cpp new file mode 100644 index 00000000..7bf14197 --- /dev/null +++ b/src/codecs/sample.cpp @@ -0,0 +1,275 @@ +#include "sample.hpp" + +namespace audio { + +namespace sample { + +void siconv(int* dst, uint8_t* src, int bits, int skip, int count) { + int i, v, s, b; + + b = (bits + 7) / 8; + s = sizeof(int) * 8 - bits; + while (count--) { + v = 0; + i = b; + switch (b) { + case 4: + v = src[--i]; + case 3: + v = (v << 8) | src[--i]; + case 2: + v = (v << 8) | src[--i]; + case 1: + v = (v << 8) | src[--i]; + } + *dst++ = v << s; + src += skip; + } +} + +void Siconv(int* dst, uint8_t* src, int bits, int skip, int count) { + int i, v, s, b; + + b = (bits + 7) / 8; + s = sizeof(int) * 8 - bits; + while (count--) { + v = 0; + i = 0; + switch (b) { + case 4: + v = src[i++]; + case 3: + v = (v << 8) | src[i++]; + case 2: + v = (v << 8) | src[i++]; + case 1: + v = (v << 8) | src[i]; + } + *dst++ = v << s; + src += skip; + } +} + +void uiconv(int* dst, uint8_t* src, int bits, int skip, int count) { + int i, s, b; + uint32_t v; + + b = (bits + 7) / 8; + s = sizeof(uint32_t) * 8 - bits; + while (count--) { + v = 0; + i = b; + switch (b) { + case 4: + v = src[--i]; + case 3: + v = (v << 8) | src[--i]; + case 2: + v = (v << 8) | src[--i]; + case 1: + v = (v << 8) | src[--i]; + } + *dst++ = (v << s) - (~0UL >> 1); + src += skip; + } +} + +void Uiconv(int* dst, uint8_t* src, int bits, int skip, int count) { + int i, s, b; + uint32_t v; + + b = (bits + 7) / 8; + s = sizeof(uint32_t) * 8 - bits; + while (count--) { + v = 0; + i = 0; + switch (b) { + case 4: + v = src[i++]; + case 3: + v = (v << 8) | src[i++]; + case 2: + v = (v << 8) | src[i++]; + case 1: + v = (v << 8) | src[i]; + } + *dst++ = (v << s) - (~0UL >> 1); + src += skip; + } +} + +void ficonv(int* dst, uint8_t* src, int bits, int skip, int count) { + if (bits == 32) { + while (count--) { + float f; + + f = *((float*)src), src += skip; + if (f > 1.0) + *dst++ = INT32_MAX; + else if (f < -1.0) + *dst++ = INT32_MIN; + else + *dst++ = f * ((float)INT32_MAX); + } + } else { + while (count--) { + double d; + + d = *((double*)src), src += skip; + if (d > 1.0) + *dst++ = INT32_MAX; + else if (d < -1.0) + *dst++ = INT32_MIN; + else + *dst++ = d * ((double)INT32_MAX); + } + } +} + +void aiconv(int* dst, uint8_t* src, int, int skip, int count) { + int t, seg; + uint8_t a; + + while (count--) { + a = *src, src += skip; + a ^= 0x55; + t = (a & 0xf) << 4; + seg = (a & 0x70) >> 4; + switch (seg) { + case 0: + t += 8; + break; + case 1: + t += 0x108; + break; + default: + t += 0x108; + t <<= seg - 1; + } + t = (a & 0x80) ? t : -t; + *dst++ = t << (sizeof(int) * 8 - 16); + } +} + +void µiconv(int* dst, uint8_t* src, int, int skip, int count) { + int t; + uint8_t u; + + while (count--) { + u = *src, src += skip; + u = ~u; + t = ((u & 0xf) << 3) + 0x84; + t <<= (u & 0x70) >> 4; + t = u & 0x80 ? 0x84 - t : t - 0x84; + *dst++ = t << (sizeof(int) * 8 - 16); + } +} + +void soconv(int* src, uint8_t* dst, int bits, int skip, int count) { + int i, v, s, b; + + b = (bits + 7) / 8; + s = sizeof(int) * 8 - bits; + while (count--) { + v = *src++ >> s; + i = 0; + switch (b) { + case 4: + dst[i++] = v, v >>= 8; + case 3: + dst[i++] = v, v >>= 8; + case 2: + dst[i++] = v, v >>= 8; + case 1: + dst[i] = v; + } + dst += skip; + } +} + +void Soconv(int* src, uint8_t* dst, int bits, int skip, int count) { + int i, v, s, b; + + b = (bits + 7) / 8; + s = sizeof(int) * 8 - bits; + while (count--) { + v = *src++ >> s; + i = b; + switch (b) { + case 4: + dst[--i] = v, v >>= 8; + case 3: + dst[--i] = v, v >>= 8; + case 2: + dst[--i] = v, v >>= 8; + case 1: + dst[--i] = v; + } + dst += skip; + } +} + +void uoconv(int* src, uint8_t* dst, int bits, int skip, int count) { + int i, s, b; + uint32_t v; + + b = (bits + 7) / 8; + s = sizeof(uint32_t) * 8 - bits; + while (count--) { + v = ((~0UL >> 1) + *src++) >> s; + i = 0; + switch (b) { + case 4: + dst[i++] = v, v >>= 8; + case 3: + dst[i++] = v, v >>= 8; + case 2: + dst[i++] = v, v >>= 8; + case 1: + dst[i] = v; + } + dst += skip; + } +} + +void Uoconv(int* src, uint8_t* dst, int bits, int skip, int count) { + int i, s, b; + uint32_t v; + + b = (bits + 7) / 8; + s = sizeof(uint32_t) * 8 - bits; + while (count--) { + v = ((~0UL >> 1) + *src++) >> s; + i = b; + switch (b) { + case 4: + dst[--i] = v, v >>= 8; + case 3: + dst[--i] = v, v >>= 8; + case 2: + dst[--i] = v, v >>= 8; + case 1: + dst[--i] = v; + } + dst += skip; + } +} + +void foconv(int* src, uint8_t* dst, int bits, int skip, int count) { + if (bits == 32) { + while (count--) { + *((float*)dst) = *src++ / ((float)INT32_MAX); + dst += skip; + } + } else { + while (count--) { + *((double*)dst) = *src++ / ((double)INT32_MAX); + dst += skip; + } + } +} + + + } + +} -- cgit v1.2.3 From 4118d880c3f20dbd9304a3f50d6d111f194592c8 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Aug 2023 09:47:44 +1000 Subject: Fix dangle build issues, do some tweaks to investigate performance --- src/audio/CMakeLists.txt | 2 +- src/audio/i2s_audio_output.cpp | 2 +- src/audio/include/sink_mixer.hpp | 1 - src/audio/resample.cpp | 3 ++- src/audio/sink_mixer.cpp | 10 +++------- 5 files changed, 7 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index ddfc7eb4..bd4ba32d 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -7,6 +7,6 @@ idf_component_register( "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "sink_mixer.cpp" "resample.cpp" INCLUDE_DIRS "include" - REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist" "libsamplerate") + REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 4eab3e02..41640e7a 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -127,7 +127,7 @@ auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), .bits_per_sample = 16, - .sample_rate = 48000, + .sample_rate = 44100, }; } diff --git a/src/audio/include/sink_mixer.hpp b/src/audio/include/sink_mixer.hpp index 1bf12016..e8a2d8cc 100644 --- a/src/audio/include/sink_mixer.hpp +++ b/src/audio/include/sink_mixer.hpp @@ -12,7 +12,6 @@ #include "resample.hpp" #include "sample.hpp" -#include "samplerate.h" #include "audio_decoder.hpp" #include "audio_sink.hpp" diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp index 93ea1034..6f7e670e 100644 --- a/src/audio/resample.cpp +++ b/src/audio/resample.cpp @@ -231,7 +231,8 @@ auto Resampler::Process(cpp::span input, cpp::span output, bool end_of_data) -> std::pair { size_t samples_used = 0; - std::vector samples_produced = {num_channels_, 0}; + std::vector samples_produced = {}; + samples_produced.resize(num_channels_, 0); size_t total_samples_produced = 0; size_t slop = (factor_ >> kPhaseBits) + 1; diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index ba306626..8a40fd63 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -15,7 +15,6 @@ #include "freertos/projdefs.h" #include "resample.hpp" #include "sample.hpp" -#include "samplerate.h" #include "stream_info.hpp" #include "tasks.hpp" @@ -23,7 +22,7 @@ static constexpr char kTag[] = "mixer"; static constexpr std::size_t kSourceBufferLength = 2 * 1024; -static constexpr std::size_t kSampleBufferLength = 4 * 1024; +static constexpr std::size_t kSampleBufferLength = 2 * 1024; namespace audio { @@ -33,8 +32,8 @@ SinkMixer::SinkMixer(StreamBufferHandle_t dest) resampler_(nullptr), source_(xStreamBufferCreate(kSourceBufferLength, 1)), sink_(dest) { - input_stream_.reset(new RawStream(kSampleBufferLength)); - resampled_stream_.reset(new RawStream(kSampleBufferLength)); + input_stream_.reset(new RawStream(kSampleBufferLength, MALLOC_CAP_SPIRAM)); + resampled_stream_.reset(new RawStream(kSampleBufferLength, MALLOC_CAP_SPIRAM)); tasks::StartPersistent([&]() { Main(); }); } @@ -188,9 +187,6 @@ auto SinkMixer::Resample(InputStream& in, OutputStream& out) -> bool { auto res = resampler_->Process(in.data_as(), out.data_as(), false); - ESP_LOGI(kTag, "resampler sent %u samples, consumed %u, produced %u", - in.data().size(), res.first, res.second); - in.consume(res.first * sizeof(sample::Sample)); out.add(res.first * sizeof(sample::Sample)); -- cgit v1.2.3 From a66c3428063017f2233b6b15d5ce6c920d5c9095 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Aug 2023 14:26:04 +1000 Subject: Resampling *basically* working? Just cleanup and buffering issues --- src/audio/i2s_audio_output.cpp | 7 +- src/audio/include/audio_sink.hpp | 1 - src/audio/include/resample.hpp | 15 +- src/audio/resample.cpp | 343 +++++++++++++++------------------------ src/audio/sink_mixer.cpp | 21 ++- src/codecs/include/sample.hpp | 8 +- src/tasks/tasks.hpp | 11 +- 7 files changed, 176 insertions(+), 230 deletions(-) (limited to 'src') diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 41640e7a..09dc1ce8 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -117,18 +117,19 @@ auto I2SAudioOutput::AdjustVolumeDown() -> bool { auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) -> StreamInfo::Pcm { - /* return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), }; - */ + /* return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), .bits_per_sample = 16, - .sample_rate = 44100, + //.sample_rate = std::clamp(orig.sample_rate, 8000, 96000), + .sample_rate = 32000, }; + */ } auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> void { diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index 20bb6da6..28acdc31 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -17,7 +17,6 @@ namespace audio { class IAudioSink { private: - // TODO: tune. at least about 12KiB seems right for mp3 static const std::size_t kDrainBufferSize = 24 * 1024; StreamBufferHandle_t stream_; diff --git a/src/audio/include/resample.hpp b/src/audio/include/resample.hpp index d7933470..32b6fde8 100644 --- a/src/audio/include/resample.hpp +++ b/src/audio/include/resample.hpp @@ -9,8 +9,6 @@ namespace audio { -class Channel; - class Resampler { public: Resampler(uint32_t source_sample_rate, @@ -28,14 +26,19 @@ class Resampler { bool end_of_data) -> std::pair; private: - auto ApplyDither(cpp::span) -> void; + auto Subsample(int channel) -> float; + auto ApplyFilter(cpp::span filter, cpp::span input) -> float; uint32_t source_sample_rate_; uint32_t target_sample_rate_; - uint32_t factor_; - + float factor_; uint8_t num_channels_; - std::vector channels_; + + std::vector channel_buffers_; + size_t channel_buffer_size_; + + float output_offset_; + int32_t input_index_; }; } // namespace audio \ No newline at end of file diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp index 6f7e670e..aa4c8f2a 100644 --- a/src/audio/resample.cpp +++ b/src/audio/resample.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "esp_log.h" @@ -13,249 +14,173 @@ namespace audio { -static constexpr size_t kFilterSize = 1536; +static constexpr char kTag[] = "resample"; -constexpr auto calc_deltas(const std::array& filter) - -> std::array { - std::array deltas; - for (size_t n = 0; n < kFilterSize - 1; n++) - deltas[n] = filter[n + 1] - filter[n]; - return deltas; -} +static constexpr double kLowPassRatio = 0.5; +static constexpr size_t kNumFilters = 8; +static constexpr size_t kTapsPerFilter = 8; -static const std::array kFilter{ -#include "fir.h" -}; - -static const std::array kFilterDeltas = - calc_deltas(kFilter); - -class Channel { - public: - Channel(uint32_t src_rate, - uint32_t dest_rate, - size_t chunk_size, - size_t skip); - ~Channel(); - - auto output_chunk_size() -> size_t { return output_chunk_size_; } - - auto FlushSamples(cpp::span out) -> size_t; - auto AddSample(sample::Sample, cpp::span out) -> std::size_t; - auto ApplyFilter() -> sample::Sample; - - private: - size_t output_chunk_size_; - size_t skip_; - - uint32_t factor_; /* factor */ - - uint32_t time_; /* time */ - - uint32_t time_per_filter_iteration_; /* output step */ - uint32_t filter_step_; /* filter step */ - uint32_t filter_end_; /* filter end */ - - int32_t unity_scale_; /* unity scale */ - - int32_t samples_per_filter_wing_; /* extra samples */ - int32_t latest_sample_; /* buffer index */ - cpp::span sample_buffer_; /* the buffer */ -}; - -enum { - Nl = 8, /* 2^Nl samples per zero crossing in fir */ - Nη = 8, /* phase bits for filter interpolation */ - kPhaseBits = Nl + Nη, /* phase bits (fract of fixed point) */ - One = 1 << kPhaseBits, -}; - -Channel::Channel(uint32_t irate, uint32_t orate, size_t count, size_t skip) - : skip_(skip) { - factor_ = ((uint64_t)orate << kPhaseBits) / irate; - if (factor_ != One) { - time_per_filter_iteration_ = ((uint64_t)irate << kPhaseBits) / orate; - filter_step_ = 1 << (Nl + Nη); - filter_end_ = kFilterSize << Nη; - samples_per_filter_wing_ = 1 + (filter_end_ / filter_step_); - unity_scale_ = 13128; /* unity scale factor for fir */ - if (factor_ < One) { - unity_scale_ *= factor_; - unity_scale_ >>= kPhaseBits; - filter_step_ *= factor_; - filter_step_ >>= kPhaseBits; - samples_per_filter_wing_ *= time_per_filter_iteration_; - samples_per_filter_wing_ >>= kPhaseBits; - } - latest_sample_ = samples_per_filter_wing_; - time_ = latest_sample_ << kPhaseBits; +typedef std::array Filter; +static std::array sFilters{}; +static bool sFiltersInitialised = false; - size_t buf_size = samples_per_filter_wing_ * 2 + count; - int32_t* buf = new int32_t[buf_size]; - sample_buffer_ = {buf, buf_size}; - count += buf_size; /* account for buffer accumulation */ - } - output_chunk_size_ = ((uint64_t)count * factor_) >> kPhaseBits; -} +auto InitFilter(int index) -> void; -Channel::~Channel() { - delete sample_buffer_.data(); -} +Resampler::Resampler(uint32_t source_sample_rate, + uint32_t target_sample_rate, + uint8_t num_channels) + : source_sample_rate_(source_sample_rate), + target_sample_rate_(target_sample_rate), + factor_(static_cast(target_sample_rate) / + static_cast(source_sample_rate)), + num_channels_(num_channels) { + channel_buffers_.resize(num_channels); + channel_buffer_size_ = kTapsPerFilter * 16; -auto Channel::ApplyFilter() -> sample::Sample { - uint32_t iteration, p, i; - int32_t *sample, a; - - int64_t value = 0; - - // I did my best, but I'll be honest with you I've no idea about any of this - // maths stuff. - - // Left wing of the filter. - sample = &sample_buffer_[time_ >> kPhaseBits]; - p = time_ & ((1 << kPhaseBits) - 1); - iteration = factor_ < One ? (factor_ * p) >> kPhaseBits : p; - while (iteration < filter_end_) { - i = iteration >> Nη; - a = iteration & ((1 << Nη) - 1); - iteration += filter_step_; - a *= kFilterDeltas[i]; - a >>= Nη; - a += kFilter[i]; - value += static_cast(*--sample) * a; + for (int i = 0; i < num_channels; i++) { + channel_buffers_[i] = + static_cast(calloc(sizeof(float), channel_buffer_size_)); } - // Right wing of the filter. - sample = &sample_buffer_[time_ >> kPhaseBits]; - p = (One - p) & ((1 << kPhaseBits) - 1); - iteration = factor_ < One ? (factor_ * p) >> kPhaseBits : p; - if (p == 0) /* skip h[0] as it was already been summed above if p == 0 */ - iteration += filter_step_; - while (iteration < filter_end_) { - i = iteration >> Nη; - a = iteration & ((1 << Nη) - 1); - iteration += filter_step_; - a *= kFilterDeltas[i]; - a >>= Nη; - a += kFilter[i]; - value += static_cast(*sample++) * a; + output_offset_ = kTapsPerFilter / 2.0f; + input_index_ = kTapsPerFilter; + + if (!sFiltersInitialised) { + sFiltersInitialised = true; + for (int i = 0; i < kNumFilters + 1; i++) { + InitFilter(i); + } } +} - /* scale */ - value >>= 2; - value *= unity_scale_; - value >>= 27; +Resampler::~Resampler() {} - return sample::Clip(value); -} +auto Resampler::Process(cpp::span input, + cpp::span output, + bool end_of_data) -> std::pair { + size_t samples_used = 0; + size_t samples_produced = 0; + + size_t input_frames = input.size() / num_channels_; + size_t output_frames = output.size() / num_channels_; + + int half_taps = kTapsPerFilter / 2, i; + while (output_frames > 0) { + if (output_offset_ >= input_index_ - half_taps) { + if (input_frames > 0) { + if (input_index_ == channel_buffer_size_) { + for (i = 0; i < num_channels_; ++i) { + memmove(channel_buffers_[i], + channel_buffers_[i] + channel_buffer_size_ - kTapsPerFilter, + kTapsPerFilter * sizeof(float)); + } + + output_offset_ -= channel_buffer_size_ - kTapsPerFilter; + input_index_ -= channel_buffer_size_ - kTapsPerFilter; + } + + for (i = 0; i < num_channels_; ++i) { + channel_buffers_[i][input_index_] = + sample::ToFloat(input[samples_used++]); + } + + input_index_++; + input_frames--; + } else + break; + } else { + for (i = 0; i < num_channels_; i++) { + output[samples_produced++] = sample::FromFloat(Subsample(i)); + } -auto Channel::FlushSamples(cpp::span out) -> size_t { - size_t zeroes_needed = (2 * samples_per_filter_wing_) - latest_sample_; - size_t produced = 0; - while (zeroes_needed > 0) { - produced += AddSample(0, out.subspan(produced)); - zeroes_needed--; + output_offset_ += (1.0f / factor_); + } } - return produced; + + return {samples_used, samples_produced}; } -auto Channel::AddSample(sample::Sample in, cpp::span out) - -> size_t { - // Add the latest sample to our working buffer. - sample_buffer_[latest_sample_++] = in; +auto InitFilter(int index) -> void { + const double a0 = 0.35875; + const double a1 = 0.48829; + const double a2 = 0.14128; + const double a3 = 0.01168; - // If we don't have enough samples to run the filter, then bail out and wait - // for more. - if (latest_sample_ < 2 * samples_per_filter_wing_) { - return 0; - } + double fraction = + static_cast(index) / static_cast(kNumFilters); + double filter_sum = 0.0; - // Apply the filter to the buffered samples. First, we work out how long (in - // samples) we can run the filter for before running out. This isn't as - // trivial as it might look; e.g. depending on the resampling factor we might - // be doubling the number of samples, or halving them. - uint32_t max_time = (latest_sample_ - samples_per_filter_wing_) << kPhaseBits; - size_t samples_output = 0; - while (time_ < max_time) { - out[skip_ * samples_output++] = ApplyFilter(); - time_ += time_per_filter_iteration_; - } + // "dist" is the absolute distance from the sinc maximum to the filter tap to + // be calculated, in radians "ratio" is that distance divided by half the tap + // count such that it reaches π at the window extremes + + // Note that with this scaling, the odd terms of the Blackman-Harris + // calculation appear to be negated with respect to the reference formula + // version. + + Filter& filter = sFilters[index]; + std::array working_buffer{}; + for (int i = 0; i < kTapsPerFilter; ++i) { + double dist = fabs((kTapsPerFilter / 2.0 - 1.0) + fraction - i) * M_PI; + double ratio = dist / (kTapsPerFilter / 2.0); + double value; + + if (dist != 0.0) { + value = sin(dist * kLowPassRatio) / (dist * kLowPassRatio); - // If we are approaching the end of our buffer, we need to shift all the data - // in it down to the front to make room for more samples. - int32_t current_sample = time_ >> kPhaseBits; - if (current_sample >= (sample_buffer_.size() - samples_per_filter_wing_)) { - // NB: bit shifting back and forth means we're only modifying `time` by - // whole samples. - time_ -= current_sample << kPhaseBits; - time_ += samples_per_filter_wing_ << kPhaseBits; - - int32_t new_current_sample = time_ >> kPhaseBits; - new_current_sample -= samples_per_filter_wing_; - current_sample -= samples_per_filter_wing_; - - int32_t samples_to_move = latest_sample_ - current_sample; - if (samples_to_move > 0) { - auto samples = sample_buffer_.subspan(current_sample, samples_to_move); - std::copy_backward(samples.begin(), samples.end(), - sample_buffer_.first(new_current_sample).end()); - latest_sample_ = new_current_sample + samples_to_move; + // Blackman-Harris window + value *= a0 + a1 * cos(ratio) + a2 * cos(2 * ratio) + a3 * cos(3 * ratio); } else { - latest_sample_ = new_current_sample; + value = 1.0; } + + working_buffer[i] = value; + filter_sum += value; } - return samples_output; -} + // filter should have unity DC gain -static const size_t kChunkSizeSamples = 256; + double scaler = 1.0 / filter_sum; + double error = 0.0; -Resampler::Resampler(uint32_t source_sample_rate, - uint32_t target_sample_rate, - uint8_t num_channels) - : source_sample_rate_(source_sample_rate), - target_sample_rate_(target_sample_rate), - factor_(((uint64_t)target_sample_rate << kPhaseBits) / - source_sample_rate), - num_channels_(num_channels), - channels_() { - for (int i = 0; i < num_channels; i++) { - channels_.emplace_back(source_sample_rate, target_sample_rate, - kChunkSizeSamples, num_channels); + for (int i = kTapsPerFilter / 2; i < kTapsPerFilter; + i = kTapsPerFilter - i - (i >= kTapsPerFilter / 2)) { + working_buffer[i] *= scaler; + filter[i] = working_buffer[i] - error; + error += static_cast(filter[i]) - working_buffer[i]; } } -Resampler::~Resampler() {} +auto Resampler::Subsample(int channel) -> float { + float sum1, sum2; -auto Resampler::Process(cpp::span input, - cpp::span output, - bool end_of_data) -> std::pair { - size_t samples_used = 0; - std::vector samples_produced = {}; - samples_produced.resize(num_channels_, 0); - size_t total_samples_produced = 0; + cpp::span source{channel_buffers_[channel], channel_buffer_size_}; - size_t slop = (factor_ >> kPhaseBits) + 1; + int offset_integral = std::floor(output_offset_); + source = source.subspan(offset_integral); + float offset_fractional = output_offset_ - offset_integral; - uint_fast8_t cur_channel = 0; + int filter_index = offset_fractional * kNumFilters; + offset_fractional *= kNumFilters; - while (input.size() > samples_used && - output.size() > total_samples_produced + slop) { - // Work out where the next set of samples should be placed. - size_t next_output_index = - (samples_produced[cur_channel] * num_channels_) + cur_channel; + sum1 = ApplyFilter(sFilters[filter_index], + {source.data() - kTapsPerFilter / 2 + 1, kTapsPerFilter}); - // Generate the next samples - size_t new_samples = channels_[cur_channel].AddSample( - input[samples_used++], output.subspan(next_output_index)); + offset_fractional -= filter_index; - samples_produced[cur_channel] += new_samples; - total_samples_produced += new_samples; + sum2 = ApplyFilter(sFilters[filter_index + 1], + {source.data() - kTapsPerFilter / 2 + 1, kTapsPerFilter}); - cur_channel = (cur_channel + 1) % num_channels_; - } + return (sum2 * offset_fractional) + (sum1 * (1.0f - offset_fractional)); +} - return {samples_used, total_samples_produced}; +auto Resampler::ApplyFilter(cpp::span filter, cpp::span input) + -> float { + float sum = 0.0; + for (int i = 0; i < kTapsPerFilter; i++) { + sum += filter[i] * input[i]; + } + return sum; } } // namespace audio diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index 8a40fd63..176fb4a3 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -13,6 +13,7 @@ #include "esp_log.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" +#include "idf_additions.h" #include "resample.hpp" #include "sample.hpp" @@ -21,8 +22,8 @@ static constexpr char kTag[] = "mixer"; -static constexpr std::size_t kSourceBufferLength = 2 * 1024; -static constexpr std::size_t kSampleBufferLength = 2 * 1024; +static constexpr std::size_t kSourceBufferLength = 8 * 1024; +static constexpr std::size_t kSampleBufferLength = 240 * 2 * sizeof(int32_t); namespace audio { @@ -30,12 +31,14 @@ SinkMixer::SinkMixer(StreamBufferHandle_t dest) : commands_(xQueueCreate(1, sizeof(Args))), is_idle_(xSemaphoreCreateBinary()), resampler_(nullptr), - source_(xStreamBufferCreate(kSourceBufferLength, 1)), + source_(xStreamBufferCreateWithCaps(kSourceBufferLength, + 1, + MALLOC_CAP_SPIRAM)), sink_(dest) { - input_stream_.reset(new RawStream(kSampleBufferLength, MALLOC_CAP_SPIRAM)); - resampled_stream_.reset(new RawStream(kSampleBufferLength, MALLOC_CAP_SPIRAM)); + input_stream_.reset(new RawStream(kSampleBufferLength)); + resampled_stream_.reset(new RawStream(kSampleBufferLength)); - tasks::StartPersistent([&]() { Main(); }); + tasks::StartPersistent(1, [&]() { Main(); }); } SinkMixer::~SinkMixer() { @@ -154,13 +157,13 @@ auto SinkMixer::HandleBytes() -> void { cpp::span src = output_source->data_as().first( output_source->info().bytes_in_stream() / sizeof(sample::Sample)); - cpp::span dest = output_source->data_as().first( - output_source->info().bytes_in_stream() / sizeof(int16_t)); + cpp::span dest{reinterpret_cast(src.data()), + src.size()}; ApplyDither(src, 16); Downscale(src, dest); - output_source->info().bytes_in_stream() /= 2; + output_source->info().bytes_in_stream() = dest.size_bytes(); } InputStream output{output_source}; diff --git a/src/codecs/include/sample.hpp b/src/codecs/include/sample.hpp index 7209673b..f8e08cdc 100644 --- a/src/codecs/include/sample.hpp +++ b/src/codecs/include/sample.hpp @@ -52,8 +52,14 @@ constexpr auto FromMad(mad_fixed_t src) -> Sample { return FromSigned(src >> (MAD_F_FRACBITS + 1 - 24), 24); } -constexpr auto ToSigned16Bit(Sample src) -> uint16_t { +constexpr auto ToSigned16Bit(Sample src) -> int16_t { return src >> 16; } +static constexpr float kFactor = 1.0f / static_cast(INT32_MAX); + +constexpr auto ToFloat(Sample src) -> float { + return src * kFactor; +} + } // namespace sample diff --git a/src/tasks/tasks.hpp b/src/tasks/tasks.hpp index 1321aab8..fe65ffcc 100644 --- a/src/tasks/tasks.hpp +++ b/src/tasks/tasks.hpp @@ -57,11 +57,20 @@ auto PersistentMain(void* fn) -> void; template auto StartPersistent(const std::function& fn) -> void { + StaticTask_t* task_buffer = new StaticTask_t; + cpp::span stack = AllocateStack(); + xTaskCreateStatic(&PersistentMain, Name().c_str(), + stack.size(), new std::function(fn), + Priority(), stack.data(), task_buffer); +} + +template +auto StartPersistent(BaseType_t core, const std::function& fn) -> void { StaticTask_t* task_buffer = new StaticTask_t; cpp::span stack = AllocateStack(); xTaskCreateStaticPinnedToCore(&PersistentMain, Name().c_str(), stack.size(), new std::function(fn), - Priority(), stack.data(), task_buffer, 0); + Priority(), stack.data(), task_buffer, core); } class Worker { -- cgit v1.2.3 From c38754401b95642b5e61fd273c2adf7d76a829fe Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Aug 2023 18:22:15 +1000 Subject: Downscaling working! --- src/audio/audio_task.cpp | 3 ++- src/audio/i2s_audio_output.cpp | 16 ++++++++-------- src/audio/include/resample.hpp | 2 +- src/audio/include/sink_mixer.hpp | 5 +++-- src/audio/sink_mixer.cpp | 14 +++++++++----- src/audio/stream_info.cpp | 3 +-- src/codecs/include/codec.hpp | 2 +- src/codecs/sample.cpp | 9 ++++----- src/tasks/tasks.hpp | 9 +++++---- 9 files changed, 34 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index d7c0c209..7c038730 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -297,7 +297,8 @@ auto AudioTask::FinishDecoding(InputStream& stream) -> void { InputStream padded_stream{mad_buffer.get()}; OutputStream writer{codec_buffer_.get()}; - auto res = codec_->ContinueStream(stream.data(), writer.data_as()); + auto res = + codec_->ContinueStream(stream.data(), writer.data_as()); if (res.second.has_error()) { return; } diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 09dc1ce8..e8aa8975 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -117,19 +117,19 @@ auto I2SAudioOutput::AdjustVolumeDown() -> bool { auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) -> StreamInfo::Pcm { - return StreamInfo::Pcm{ - .channels = std::min(orig.channels, 2), - .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), - .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), - }; - /* + /* +return StreamInfo::Pcm{ + .channels = std::min(orig.channels, 2), + .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), + .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), +}; + */ return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), .bits_per_sample = 16, //.sample_rate = std::clamp(orig.sample_rate, 8000, 96000), - .sample_rate = 32000, + .sample_rate = 44100, }; - */ } auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> void { diff --git a/src/audio/include/resample.hpp b/src/audio/include/resample.hpp index 32b6fde8..3855415a 100644 --- a/src/audio/include/resample.hpp +++ b/src/audio/include/resample.hpp @@ -23,7 +23,7 @@ class Resampler { auto Process(cpp::span input, cpp::span output, - bool end_of_data) -> std::pair; + bool end_of_data) -> std::pair; private: auto Subsample(int channel) -> float; diff --git a/src/audio/include/sink_mixer.hpp b/src/audio/include/sink_mixer.hpp index e8a2d8cc..d1e9aa8a 100644 --- a/src/audio/include/sink_mixer.hpp +++ b/src/audio/include/sink_mixer.hpp @@ -40,9 +40,10 @@ class SinkMixer { auto HandleBytes() -> void; auto Resample(InputStream&, OutputStream&) -> bool; - auto ApplyDither(cpp::span samples, uint_fast8_t bits) -> void; + auto ApplyDither(cpp::span samples, uint_fast8_t bits) + -> void; auto Downscale(cpp::span, cpp::span) -> void; - + enum class Command { kReadBytes, kSetSourceFormat, diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index 176fb4a3..a2bf229b 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -137,7 +137,7 @@ auto SinkMixer::HandleBytes() -> void { return; } - while (!input_stream_->empty()) { + while (input_stream_->info().bytes_in_stream() >= sizeof(sample::Sample)) { RawStream* output_source; if (pcm->sample_rate != target_format_.sample_rate) { OutputStream resampled_writer{resampled_stream_.get()}; @@ -150,6 +150,9 @@ auto SinkMixer::HandleBytes() -> void { output_source = input_stream_.get(); } + size_t bytes_consumed = output_source->info().bytes_in_stream(); + size_t bytes_to_send = output_source->info().bytes_in_stream(); + if (target_format_.bits_per_sample == 16) { // This is slightly scary; we're basically reaching into the internals of // the stream buffer to do in-place conversion of samples. Saving an @@ -163,19 +166,20 @@ auto SinkMixer::HandleBytes() -> void { ApplyDither(src, 16); Downscale(src, dest); - output_source->info().bytes_in_stream() = dest.size_bytes(); + bytes_consumed = src.size_bytes(); + bytes_to_send = src.size_bytes() / 2; } InputStream output{output_source}; cpp::span buf = output.data(); size_t bytes_sent = 0; - while (bytes_sent < buf.size_bytes()) { - auto cropped = buf.subspan(bytes_sent); + while (bytes_sent < bytes_to_send) { + auto cropped = buf.subspan(bytes_sent, bytes_to_send - bytes_sent); bytes_sent += xStreamBufferSend(sink_, cropped.data(), cropped.size_bytes(), portMAX_DELAY); } - output.consume(bytes_sent); + output.consume(bytes_consumed); } } diff --git a/src/audio/stream_info.cpp b/src/audio/stream_info.cpp index 749e880e..73dbf91b 100644 --- a/src/audio/stream_info.cpp +++ b/src/audio/stream_info.cpp @@ -33,8 +33,7 @@ RawStream::RawStream(std::size_t size) RawStream::RawStream(std::size_t size, uint32_t caps) : info_(), buffer_size_(size), - buffer_(reinterpret_cast( - heap_caps_malloc(size, caps))) { + buffer_(reinterpret_cast(heap_caps_malloc(size, caps))) { assert(buffer_ != NULL); } diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index f260aca4..32ebef69 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -16,8 +16,8 @@ #include #include -#include "sample.hpp" #include "result.hpp" +#include "sample.hpp" #include "span.hpp" #include "types.hpp" diff --git a/src/codecs/sample.cpp b/src/codecs/sample.cpp index 7bf14197..a1fe9bfd 100644 --- a/src/codecs/sample.cpp +++ b/src/codecs/sample.cpp @@ -268,8 +268,7 @@ void foconv(int* src, uint8_t* dst, int bits, int skip, int count) { } } } - - - } - -} + +} // namespace sample + +} // namespace audio diff --git a/src/tasks/tasks.hpp b/src/tasks/tasks.hpp index fe65ffcc..a0c201d5 100644 --- a/src/tasks/tasks.hpp +++ b/src/tasks/tasks.hpp @@ -59,13 +59,14 @@ template auto StartPersistent(const std::function& fn) -> void { StaticTask_t* task_buffer = new StaticTask_t; cpp::span stack = AllocateStack(); - xTaskCreateStatic(&PersistentMain, Name().c_str(), - stack.size(), new std::function(fn), - Priority(), stack.data(), task_buffer); + xTaskCreateStatic(&PersistentMain, Name().c_str(), stack.size(), + new std::function(fn), Priority(), + stack.data(), task_buffer); } template -auto StartPersistent(BaseType_t core, const std::function& fn) -> void { +auto StartPersistent(BaseType_t core, const std::function& fn) + -> void { StaticTask_t* task_buffer = new StaticTask_t; cpp::span stack = AllocateStack(); xTaskCreateStaticPinnedToCore(&PersistentMain, Name().c_str(), -- cgit v1.2.3 From 40475b15e833dff496b17929024bddbbe10041b0 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Aug 2023 19:15:53 +1000 Subject: Remove unused pcmconv impls --- src/codecs/sample.cpp | 274 -------------------------------------------------- 1 file changed, 274 deletions(-) delete mode 100644 src/codecs/sample.cpp (limited to 'src') diff --git a/src/codecs/sample.cpp b/src/codecs/sample.cpp deleted file mode 100644 index a1fe9bfd..00000000 --- a/src/codecs/sample.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#include "sample.hpp" - -namespace audio { - -namespace sample { - -void siconv(int* dst, uint8_t* src, int bits, int skip, int count) { - int i, v, s, b; - - b = (bits + 7) / 8; - s = sizeof(int) * 8 - bits; - while (count--) { - v = 0; - i = b; - switch (b) { - case 4: - v = src[--i]; - case 3: - v = (v << 8) | src[--i]; - case 2: - v = (v << 8) | src[--i]; - case 1: - v = (v << 8) | src[--i]; - } - *dst++ = v << s; - src += skip; - } -} - -void Siconv(int* dst, uint8_t* src, int bits, int skip, int count) { - int i, v, s, b; - - b = (bits + 7) / 8; - s = sizeof(int) * 8 - bits; - while (count--) { - v = 0; - i = 0; - switch (b) { - case 4: - v = src[i++]; - case 3: - v = (v << 8) | src[i++]; - case 2: - v = (v << 8) | src[i++]; - case 1: - v = (v << 8) | src[i]; - } - *dst++ = v << s; - src += skip; - } -} - -void uiconv(int* dst, uint8_t* src, int bits, int skip, int count) { - int i, s, b; - uint32_t v; - - b = (bits + 7) / 8; - s = sizeof(uint32_t) * 8 - bits; - while (count--) { - v = 0; - i = b; - switch (b) { - case 4: - v = src[--i]; - case 3: - v = (v << 8) | src[--i]; - case 2: - v = (v << 8) | src[--i]; - case 1: - v = (v << 8) | src[--i]; - } - *dst++ = (v << s) - (~0UL >> 1); - src += skip; - } -} - -void Uiconv(int* dst, uint8_t* src, int bits, int skip, int count) { - int i, s, b; - uint32_t v; - - b = (bits + 7) / 8; - s = sizeof(uint32_t) * 8 - bits; - while (count--) { - v = 0; - i = 0; - switch (b) { - case 4: - v = src[i++]; - case 3: - v = (v << 8) | src[i++]; - case 2: - v = (v << 8) | src[i++]; - case 1: - v = (v << 8) | src[i]; - } - *dst++ = (v << s) - (~0UL >> 1); - src += skip; - } -} - -void ficonv(int* dst, uint8_t* src, int bits, int skip, int count) { - if (bits == 32) { - while (count--) { - float f; - - f = *((float*)src), src += skip; - if (f > 1.0) - *dst++ = INT32_MAX; - else if (f < -1.0) - *dst++ = INT32_MIN; - else - *dst++ = f * ((float)INT32_MAX); - } - } else { - while (count--) { - double d; - - d = *((double*)src), src += skip; - if (d > 1.0) - *dst++ = INT32_MAX; - else if (d < -1.0) - *dst++ = INT32_MIN; - else - *dst++ = d * ((double)INT32_MAX); - } - } -} - -void aiconv(int* dst, uint8_t* src, int, int skip, int count) { - int t, seg; - uint8_t a; - - while (count--) { - a = *src, src += skip; - a ^= 0x55; - t = (a & 0xf) << 4; - seg = (a & 0x70) >> 4; - switch (seg) { - case 0: - t += 8; - break; - case 1: - t += 0x108; - break; - default: - t += 0x108; - t <<= seg - 1; - } - t = (a & 0x80) ? t : -t; - *dst++ = t << (sizeof(int) * 8 - 16); - } -} - -void µiconv(int* dst, uint8_t* src, int, int skip, int count) { - int t; - uint8_t u; - - while (count--) { - u = *src, src += skip; - u = ~u; - t = ((u & 0xf) << 3) + 0x84; - t <<= (u & 0x70) >> 4; - t = u & 0x80 ? 0x84 - t : t - 0x84; - *dst++ = t << (sizeof(int) * 8 - 16); - } -} - -void soconv(int* src, uint8_t* dst, int bits, int skip, int count) { - int i, v, s, b; - - b = (bits + 7) / 8; - s = sizeof(int) * 8 - bits; - while (count--) { - v = *src++ >> s; - i = 0; - switch (b) { - case 4: - dst[i++] = v, v >>= 8; - case 3: - dst[i++] = v, v >>= 8; - case 2: - dst[i++] = v, v >>= 8; - case 1: - dst[i] = v; - } - dst += skip; - } -} - -void Soconv(int* src, uint8_t* dst, int bits, int skip, int count) { - int i, v, s, b; - - b = (bits + 7) / 8; - s = sizeof(int) * 8 - bits; - while (count--) { - v = *src++ >> s; - i = b; - switch (b) { - case 4: - dst[--i] = v, v >>= 8; - case 3: - dst[--i] = v, v >>= 8; - case 2: - dst[--i] = v, v >>= 8; - case 1: - dst[--i] = v; - } - dst += skip; - } -} - -void uoconv(int* src, uint8_t* dst, int bits, int skip, int count) { - int i, s, b; - uint32_t v; - - b = (bits + 7) / 8; - s = sizeof(uint32_t) * 8 - bits; - while (count--) { - v = ((~0UL >> 1) + *src++) >> s; - i = 0; - switch (b) { - case 4: - dst[i++] = v, v >>= 8; - case 3: - dst[i++] = v, v >>= 8; - case 2: - dst[i++] = v, v >>= 8; - case 1: - dst[i] = v; - } - dst += skip; - } -} - -void Uoconv(int* src, uint8_t* dst, int bits, int skip, int count) { - int i, s, b; - uint32_t v; - - b = (bits + 7) / 8; - s = sizeof(uint32_t) * 8 - bits; - while (count--) { - v = ((~0UL >> 1) + *src++) >> s; - i = b; - switch (b) { - case 4: - dst[--i] = v, v >>= 8; - case 3: - dst[--i] = v, v >>= 8; - case 2: - dst[--i] = v, v >>= 8; - case 1: - dst[--i] = v; - } - dst += skip; - } -} - -void foconv(int* src, uint8_t* dst, int bits, int skip, int count) { - if (bits == 32) { - while (count--) { - *((float*)dst) = *src++ / ((float)INT32_MAX); - dst += skip; - } - } else { - while (count--) { - *((double*)dst) = *src++ / ((double)INT32_MAX); - dst += skip; - } - } -} - -} // namespace sample - -} // namespace audio -- cgit v1.2.3 From d71682d26eaee182dede0caf6499aefc2133183c Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Aug 2023 20:47:42 +1000 Subject: Add a console command for task-level performance checks --- src/app_console/app_console.cpp | 96 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) (limited to 'src') diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 6faa27d0..a2fd749b 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -7,11 +7,13 @@ #include "app_console.hpp" #include +#include #include #include #include #include +#include #include #include #include @@ -25,6 +27,7 @@ #include "esp_log.h" #include "event_queue.hpp" #include "ff.h" +#include "freertos/projdefs.h" #include "index.hpp" #include "track.hpp" @@ -324,6 +327,98 @@ void RegisterDbDump() { esp_console_cmd_register(&cmd); } +int CmdTaskStats(int argc, char** argv) { + static const std::string usage = "usage: task_stats"; + if (argc != 1) { + std::cout << usage << std::endl; + return 1; + } + + // Pad the number of tasks so that uxTaskGetSystemState still returns info if + // new tasks are started during measurement. + size_t num_tasks = uxTaskGetNumberOfTasks() + 4; + TaskStatus_t* start_status = new TaskStatus_t[num_tasks]; + TaskStatus_t* end_status = new TaskStatus_t[num_tasks]; + uint32_t start_elapsed_ticks = 0; + uint32_t end_elapsed_ticks = 0; + + size_t start_num_tasks = + uxTaskGetSystemState(start_status, num_tasks, &start_elapsed_ticks); + + vTaskDelay(pdMS_TO_TICKS(2500)); + + size_t end_num_tasks = + uxTaskGetSystemState(end_status, num_tasks, &end_elapsed_ticks); + + std::vector> info_strings; + for (int i = 0; i < start_num_tasks; i++) { + int k = -1; + for (int j = 0; j < end_num_tasks; j++) { + if (start_status[i].xHandle == end_status[j].xHandle) { + k = j; + break; + } + } + + if (k >= 0) { + uint32_t run_time = + end_status[k].ulRunTimeCounter - start_status[i].ulRunTimeCounter; + + float time_percent = + static_cast(run_time) / + static_cast(end_elapsed_ticks - start_elapsed_ticks); + + auto depth = uxTaskGetStackHighWaterMark2(start_status[i].xHandle); + float depth_kib = static_cast(depth) / 1024.0f; + + std::ostringstream str; + str << start_status[i].pcTaskName; + if (str.str().size() < 8) { + str << "\t\t"; + } else { + str << "\t"; + } + + str << std::fixed << std::setprecision(1) << depth_kib; + str << " KiB"; + if (depth_kib >= 10) { + str << "\t"; + } else { + str << "\t\t"; + } + + str << std::fixed << std::setprecision(1) << time_percent * 100; + str << "%"; + + info_strings.push_back({run_time, str.str()}); + } + } + + std::sort(info_strings.begin(), info_strings.end(), + [](const auto& first, const auto& second) { + return first.first >= second.first; + }); + + std::cout << "name\t\tfree stack\trun time" << std::endl; + for (const auto& i : info_strings) { + std::cout << i.second << std::endl; + } + + delete[] start_status; + delete[] end_status; + + return 0; +} + +void RegisterTaskStates() { + esp_console_cmd_t cmd{.command = "task_stats", + .help = "prints performance info for all tasks", + .hint = NULL, + .func = &CmdTaskStats, + .argtable = NULL}; + esp_console_cmd_register(&cmd); +} + auto AppConsole::RegisterExtraComponents() -> void { RegisterListDir(); RegisterPlayFile(); @@ -336,6 +431,7 @@ auto AppConsole::RegisterExtraComponents() -> void { RegisterDbTracks(); RegisterDbIndex(); RegisterDbDump(); + RegisterTaskStates(); } } // namespace console -- cgit v1.2.3 From 49f82d2f3d31f5ecb26f1f45d091e346da515314 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Aug 2023 20:47:59 +1000 Subject: Use a timer to keep framerate consistent --- src/ui/lvgl_task.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/ui/lvgl_task.cpp b/src/ui/lvgl_task.cpp index 06a6b28b..340282ee 100644 --- a/src/ui/lvgl_task.cpp +++ b/src/ui/lvgl_task.cpp @@ -48,12 +48,25 @@ namespace ui { static const char* kTag = "lv_task"; +static const TickType_t kMaxFrameRate = pdMS_TO_TICKS(33); + +static int sTimerId; +static SemaphoreHandle_t sFrameSemaphore; + +auto next_frame(TimerHandle_t) { + xSemaphoreGive(sFrameSemaphore); +} void LvglMain(std::weak_ptr weak_touch_wheel, std::weak_ptr weak_display) { ESP_LOGI(kTag, "init lvgl"); lv_init(); + sFrameSemaphore = xSemaphoreCreateBinary(); + auto timer = + xTimerCreate("lvgl_frame", kMaxFrameRate, pdTRUE, &sTimerId, next_frame); + xTimerStart(timer, portMAX_DELAY); + lv_theme_t* base_theme = lv_theme_basic_init(NULL); lv_disp_set_theme(NULL, base_theme); static themes::Theme sTheme{}; @@ -80,9 +93,9 @@ void LvglMain(std::weak_ptr weak_touch_wheel, } lv_task_handler(); - // 30 FPS - // TODO(jacqueline): make this dynamic - vTaskDelay(pdMS_TO_TICKS(33)); + + // Wait for the signal to loop again. + xSemaphoreTake(sFrameSemaphore, portMAX_DELAY); } } -- cgit v1.2.3 From 6c99f9f2fee0928987fe944c8ed29878064df87a Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 8 Aug 2023 11:36:10 +1000 Subject: Fix resampler issue, do a little performance tuning --- src/audio/fatfs_audio_input.cpp | 9 ++++++--- src/audio/i2s_audio_output.cpp | 12 ++---------- src/audio/resample.cpp | 33 +++++++++++++++++++++++++-------- src/audio/sink_mixer.cpp | 2 +- 4 files changed, 34 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 0c3ef20d..73586f09 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -30,6 +30,7 @@ #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "future_fetcher.hpp" +#include "idf_additions.h" #include "span.hpp" #include "stream_info.hpp" #include "tag_parser.hpp" @@ -40,8 +41,8 @@ static const char* kTag = "SRC"; namespace audio { -static constexpr UINT kFileBufferSize = 4096 * 2; -static constexpr UINT kStreamerBufferSize = 4096; +static constexpr UINT kFileBufferSize = 8 * 1024; +static constexpr UINT kStreamerBufferSize = 64 * 1024; static StreamBufferHandle_t sForwardDest = nullptr; @@ -143,7 +144,9 @@ FatfsAudioInput::FatfsAudioInput( : IAudioSource(), tag_parser_(tag_parser), has_data_(xSemaphoreCreateBinary()), - streamer_buffer_(xStreamBufferCreate(kStreamerBufferSize, 1)), + streamer_buffer_(xStreamBufferCreateWithCaps(kStreamerBufferSize, + 1, + MALLOC_CAP_SPIRAM)), streamer_(new FileStreamer(streamer_buffer_, has_data_)), input_buffer_(new RawStream(kFileBufferSize)), source_mutex_(), diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index e8aa8975..e53dbe2a 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -117,18 +117,10 @@ auto I2SAudioOutput::AdjustVolumeDown() -> bool { auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) -> StreamInfo::Pcm { - /* -return StreamInfo::Pcm{ - .channels = std::min(orig.channels, 2), - .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), - .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), -}; - */ return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), - .bits_per_sample = 16, - //.sample_rate = std::clamp(orig.sample_rate, 8000, 96000), - .sample_rate = 44100, + .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), + .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), }; } diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp index aa4c8f2a..7accd0a1 100644 --- a/src/audio/resample.cpp +++ b/src/audio/resample.cpp @@ -17,8 +17,8 @@ namespace audio { static constexpr char kTag[] = "resample"; static constexpr double kLowPassRatio = 0.5; -static constexpr size_t kNumFilters = 8; -static constexpr size_t kTapsPerFilter = 8; +static constexpr size_t kNumFilters = 64; +static constexpr size_t kTapsPerFilter = 16; typedef std::array Filter; static std::array sFilters{}; @@ -64,12 +64,15 @@ auto Resampler::Process(cpp::span input, size_t input_frames = input.size() / num_channels_; size_t output_frames = output.size() / num_channels_; - int half_taps = kTapsPerFilter / 2, i; + int half_taps = kTapsPerFilter / 2; while (output_frames > 0) { if (output_offset_ >= input_index_ - half_taps) { if (input_frames > 0) { + // Check whether the channel buffers will overflow with the addition of + // this sample. If so, we need to move the remaining contents back to + // the beginning of the buffer. if (input_index_ == channel_buffer_size_) { - for (i = 0; i < num_channels_; ++i) { + for (int i = 0; i < num_channels_; ++i) { memmove(channel_buffers_[i], channel_buffers_[i] + channel_buffer_size_ - kTapsPerFilter, kTapsPerFilter * sizeof(float)); @@ -79,21 +82,23 @@ auto Resampler::Process(cpp::span input, input_index_ -= channel_buffer_size_ - kTapsPerFilter; } - for (i = 0; i < num_channels_; ++i) { + for (int i = 0; i < num_channels_; ++i) { channel_buffers_[i][input_index_] = sample::ToFloat(input[samples_used++]); } input_index_++; input_frames--; - } else + } else { break; + } } else { - for (i = 0; i < num_channels_; i++) { + for (int i = 0; i < num_channels_; i++) { output[samples_produced++] = sample::FromFloat(Subsample(i)); } output_offset_ += (1.0f / factor_); + output_frames--; } } @@ -160,8 +165,20 @@ auto Resampler::Subsample(int channel) -> float { source = source.subspan(offset_integral); float offset_fractional = output_offset_ - offset_integral; - int filter_index = offset_fractional * kNumFilters; + /* +// no interpolate +size_t filter_index = std::floor(offset_fractional * kNumFilters + 0.5f); +//ESP_LOGI(kTag, "selected filter %u of %u", filter_index, kNumFilters); +int start_offset = kTapsPerFilter / 2 + 1; +//ESP_LOGI(kTag, "using offset of %i, length %u", start_offset, kTapsPerFilter); + +return ApplyFilter( + sFilters[filter_index], + {source.data() - start_offset, kTapsPerFilter}); + */ + offset_fractional *= kNumFilters; + int filter_index = std::floor(offset_fractional); sum1 = ApplyFilter(sFilters[filter_index], {source.data() - kTapsPerFilter / 2 + 1, kTapsPerFilter}); diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index a2bf229b..5a5a8616 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -195,7 +195,7 @@ auto SinkMixer::Resample(InputStream& in, OutputStream& out) -> bool { out.data_as(), false); in.consume(res.first * sizeof(sample::Sample)); - out.add(res.first * sizeof(sample::Sample)); + out.add(res.second * sizeof(sample::Sample)); return res.first == 0 && res.second == 0; } -- cgit v1.2.3 From 93ccf11fc506b95221ce0c5eddaed9e0e6c8b3b5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 8 Aug 2023 13:47:08 +1000 Subject: Investigate and improve core pinning for resampler --- src/app_console/app_console.cpp | 22 +++++++- src/audio/audio_task.cpp | 5 +- src/audio/i2s_audio_output.cpp | 3 +- src/audio/resample.cpp | 116 ++++++++++++++++++++-------------------- src/audio/sink_mixer.cpp | 5 +- 5 files changed, 90 insertions(+), 61 deletions(-) (limited to 'src') diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index a2fd749b..8686ac58 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -19,6 +19,7 @@ #include #include +#include "FreeRTOSConfig.h" #include "audio_events.hpp" #include "audio_fsm.hpp" #include "database.hpp" @@ -27,6 +28,7 @@ #include "esp_log.h" #include "event_queue.hpp" #include "ff.h" +#include "freertos/FreeRTOSConfig_arch.h" #include "freertos/projdefs.h" #include "index.hpp" #include "track.hpp" @@ -328,6 +330,12 @@ void RegisterDbDump() { } int CmdTaskStats(int argc, char** argv) { + if (!configUSE_TRACE_FACILITY) { + std::cout << "configUSE_TRACE_FACILITY must be enabled" << std::endl; + std::cout << "also consider configTASKLIST_USE_COREID" << std::endl; + return 1; + } + static const std::string usage = "usage: task_stats"; if (argc != 1) { std::cout << usage << std::endl; @@ -379,6 +387,14 @@ int CmdTaskStats(int argc, char** argv) { str << "\t"; } + if (configTASKLIST_INCLUDE_COREID) { + if (start_status[i].xCoreID == tskNO_AFFINITY) { + str << "any\t"; + } else { + str << start_status[i].xCoreID << "\t"; + } + } + str << std::fixed << std::setprecision(1) << depth_kib; str << " KiB"; if (depth_kib >= 10) { @@ -399,7 +415,11 @@ int CmdTaskStats(int argc, char** argv) { return first.first >= second.first; }); - std::cout << "name\t\tfree stack\trun time" << std::endl; + std::cout << "name\t\t"; + if (configTASKLIST_INCLUDE_COREID) { + std::cout << "core\t"; + } + std::cout << "free stack\trun time" << std::endl; for (const auto& i : info_strings) { std::cout << i.second << std::endl; } diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index 7c038730..75b44594 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -109,7 +109,10 @@ auto Timer::bytes_to_samples(uint32_t bytes) -> uint32_t { auto AudioTask::Start(IAudioSource* source, IAudioSink* sink) -> AudioTask* { AudioTask* task = new AudioTask(source, sink); - tasks::StartPersistent([=]() { task->Main(); }); + // Pin to CORE1 because codecs should be fixed point anyway, and being on + // the opposite core to the mixer maximises throughput in the worst case + // (some heavy codec like opus + resampling for bluetooth). + tasks::StartPersistent(1, [=]() { task->Main(); }); return task; } diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index e53dbe2a..d60ddfa4 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -120,7 +120,8 @@ auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig) return StreamInfo::Pcm{ .channels = std::min(orig.channels, 2), .bits_per_sample = std::clamp(orig.bits_per_sample, 16, 32), - .sample_rate = std::clamp(orig.sample_rate, 8000, 96000), + .sample_rate = 44100, + //.sample_rate = std::clamp(orig.sample_rate, 8000, 96000), }; } diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp index 7accd0a1..430a6a26 100644 --- a/src/audio/resample.cpp +++ b/src/audio/resample.cpp @@ -1,4 +1,17 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ #include "resample.hpp" +/* + * This file contains the implementation for a 32-bit floating point resampler. + * It is largely based on David Bryant's ART resampler, which is BSD-licensed, + * and available at https://github.com/dbry/audio-resampler/. + * + * This resampler uses windowed sinc interpolation filters, with an additional + * lowpass filter to reduce aliasing. + */ #include #include @@ -14,13 +27,11 @@ namespace audio { -static constexpr char kTag[] = "resample"; - static constexpr double kLowPassRatio = 0.5; static constexpr size_t kNumFilters = 64; -static constexpr size_t kTapsPerFilter = 16; +static constexpr size_t kFilterSize = 16; -typedef std::array Filter; +typedef std::array Filter; static std::array sFilters{}; static bool sFiltersInitialised = false; @@ -35,15 +46,15 @@ Resampler::Resampler(uint32_t source_sample_rate, static_cast(source_sample_rate)), num_channels_(num_channels) { channel_buffers_.resize(num_channels); - channel_buffer_size_ = kTapsPerFilter * 16; + channel_buffer_size_ = kFilterSize * 16; for (int i = 0; i < num_channels; i++) { channel_buffers_[i] = static_cast(calloc(sizeof(float), channel_buffer_size_)); } - output_offset_ = kTapsPerFilter / 2.0f; - input_index_ = kTapsPerFilter; + output_offset_ = kFilterSize / 2.0f; + input_index_ = kFilterSize; if (!sFiltersInitialised) { sFiltersInitialised = true; @@ -64,7 +75,7 @@ auto Resampler::Process(cpp::span input, size_t input_frames = input.size() / num_channels_; size_t output_frames = output.size() / num_channels_; - int half_taps = kTapsPerFilter / 2; + int half_taps = kFilterSize / 2; while (output_frames > 0) { if (output_offset_ >= input_index_ - half_taps) { if (input_frames > 0) { @@ -74,12 +85,12 @@ auto Resampler::Process(cpp::span input, if (input_index_ == channel_buffer_size_) { for (int i = 0; i < num_channels_; ++i) { memmove(channel_buffers_[i], - channel_buffers_[i] + channel_buffer_size_ - kTapsPerFilter, - kTapsPerFilter * sizeof(float)); + channel_buffers_[i] + channel_buffer_size_ - kFilterSize, + kFilterSize * sizeof(float)); } - output_offset_ -= channel_buffer_size_ - kTapsPerFilter; - input_index_ -= channel_buffer_size_ - kTapsPerFilter; + output_offset_ -= channel_buffer_size_ - kFilterSize; + input_index_ -= channel_buffer_size_ - kFilterSize; } for (int i = 0; i < num_channels_; ++i) { @@ -97,7 +108,11 @@ auto Resampler::Process(cpp::span input, output[samples_produced++] = sample::FromFloat(Subsample(i)); } - output_offset_ += (1.0f / factor_); + // NOTE: floating point division here is potentially slow due to FPU + // limitations. Consider explicitly bunding the xtensa libgcc divsion via + // reciprocal implementation if we care about portability between + // compilers. + output_offset_ += 1.0f / factor_; output_frames--; } } @@ -105,36 +120,34 @@ auto Resampler::Process(cpp::span input, return {samples_used, samples_produced}; } +/* + * Constructs the filter in-place for the given index of sFilters. This only + * needs to be done once, per-filter. 64-bit math is okay here, because filters + * will not be initialised within a performance critical path. + */ auto InitFilter(int index) -> void { - const double a0 = 0.35875; - const double a1 = 0.48829; - const double a2 = 0.14128; - const double a3 = 0.01168; + Filter& filter = sFilters[index]; + std::array working_buffer{}; - double fraction = - static_cast(index) / static_cast(kNumFilters); + double fraction = index / static_cast(kNumFilters); double filter_sum = 0.0; - // "dist" is the absolute distance from the sinc maximum to the filter tap to - // be calculated, in radians "ratio" is that distance divided by half the tap - // count such that it reaches π at the window extremes - - // Note that with this scaling, the odd terms of the Blackman-Harris - // calculation appear to be negated with respect to the reference formula - // version. + for (int i = 0; i < kFilterSize; ++i) { + // "dist" is the absolute distance from the sinc maximum to the filter tap + // to be calculated, in radians. + double dist = fabs((kFilterSize / 2.0 - 1.0) + fraction - i) * M_PI; + // "ratio" is that distance divided by half the tap count such that it + // reaches π at the window extremes + double ratio = dist / (kFilterSize / 2.0); - Filter& filter = sFilters[index]; - std::array working_buffer{}; - for (int i = 0; i < kTapsPerFilter; ++i) { - double dist = fabs((kTapsPerFilter / 2.0 - 1.0) + fraction - i) * M_PI; - double ratio = dist / (kTapsPerFilter / 2.0); double value; - if (dist != 0.0) { value = sin(dist * kLowPassRatio) / (dist * kLowPassRatio); - // Blackman-Harris window - value *= a0 + a1 * cos(ratio) + a2 * cos(2 * ratio) + a3 * cos(3 * ratio); + // Hann window. We could alternatively use a Blackman Harris window, + // however our unusually small filter size makes the Hann window's + // steeper cutoff more important. + value *= 0.5 * (1.0 + cos(ratio)); } else { value = 1.0; } @@ -143,50 +156,39 @@ auto InitFilter(int index) -> void { filter_sum += value; } - // filter should have unity DC gain - + // Filter should have unity DC gain double scaler = 1.0 / filter_sum; double error = 0.0; - for (int i = kTapsPerFilter / 2; i < kTapsPerFilter; - i = kTapsPerFilter - i - (i >= kTapsPerFilter / 2)) { + for (int i = kFilterSize / 2; i < kFilterSize; + i = kFilterSize - i - (i >= kFilterSize / 2)) { working_buffer[i] *= scaler; filter[i] = working_buffer[i] - error; error += static_cast(filter[i]) - working_buffer[i]; } } +/* + * Performs sub-sampling with interpolation for the given channel. Assumes that + * the channel buffer has already been filled with samples. + */ auto Resampler::Subsample(int channel) -> float { - float sum1, sum2; - cpp::span source{channel_buffers_[channel], channel_buffer_size_}; int offset_integral = std::floor(output_offset_); source = source.subspan(offset_integral); float offset_fractional = output_offset_ - offset_integral; - /* -// no interpolate -size_t filter_index = std::floor(offset_fractional * kNumFilters + 0.5f); -//ESP_LOGI(kTag, "selected filter %u of %u", filter_index, kNumFilters); -int start_offset = kTapsPerFilter / 2 + 1; -//ESP_LOGI(kTag, "using offset of %i, length %u", start_offset, kTapsPerFilter); - -return ApplyFilter( - sFilters[filter_index], - {source.data() - start_offset, kTapsPerFilter}); - */ - offset_fractional *= kNumFilters; int filter_index = std::floor(offset_fractional); - sum1 = ApplyFilter(sFilters[filter_index], - {source.data() - kTapsPerFilter / 2 + 1, kTapsPerFilter}); + float sum1 = ApplyFilter(sFilters[filter_index], + {source.data() - kFilterSize / 2 + 1, kFilterSize}); offset_fractional -= filter_index; - sum2 = ApplyFilter(sFilters[filter_index + 1], - {source.data() - kTapsPerFilter / 2 + 1, kTapsPerFilter}); + float sum2 = ApplyFilter(sFilters[filter_index + 1], + {source.data() - kFilterSize / 2 + 1, kFilterSize}); return (sum2 * offset_fractional) + (sum1 * (1.0f - offset_fractional)); } @@ -194,7 +196,7 @@ return ApplyFilter( auto Resampler::ApplyFilter(cpp::span filter, cpp::span input) -> float { float sum = 0.0; - for (int i = 0; i < kTapsPerFilter; i++) { + for (int i = 0; i < kFilterSize; i++) { sum += filter[i] * input[i]; } return sum; diff --git a/src/audio/sink_mixer.cpp b/src/audio/sink_mixer.cpp index 5a5a8616..6c72c8b0 100644 --- a/src/audio/sink_mixer.cpp +++ b/src/audio/sink_mixer.cpp @@ -38,7 +38,10 @@ SinkMixer::SinkMixer(StreamBufferHandle_t dest) input_stream_.reset(new RawStream(kSampleBufferLength)); resampled_stream_.reset(new RawStream(kSampleBufferLength)); - tasks::StartPersistent(1, [&]() { Main(); }); + // Pin to CORE0 because we need the FPU. + // FIXME: A fixed point implementation could run freely on either core, + // which should lead to a big performance increase. + tasks::StartPersistent(0, [&]() { Main(); }); } SinkMixer::~SinkMixer() { -- cgit v1.2.3 From 520ec6d98a761e1d96b5bea299819c096ce08ac3 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 8 Aug 2023 17:04:34 +1000 Subject: Add skeleton of bluetooth FSM --- src/drivers/CMakeLists.txt | 2 +- src/drivers/bluetooth.cpp | 246 +++++++++++++++++++++++++++++++++++++- src/drivers/include/bluetooth.hpp | 98 ++++++++++++++- 3 files changed, 338 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 9774f80b..a64495f0 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -7,5 +7,5 @@ idf_component_register( "spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "nvs.cpp" "bluetooth.cpp" INCLUDE_DIRS "include" - REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "bt") + REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "bt" "tinyfsm") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index a02fa620..f9ab4e95 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -1,11 +1,253 @@ #include "bluetooth.hpp" +#include + +#include +#include +#include + +#include "esp_a2dp_api.h" +#include "esp_avrc_api.h" #include "esp_bt.h" +#include "esp_bt_device.h" +#include "esp_bt_main.h" +#include "esp_gap_bt_api.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "tinyfsm/include/tinyfsm.hpp" namespace drivers { -auto Bluetooth::Enable() -> Bluetooth* { - return nullptr; +static constexpr char kTag[] = "bluetooth"; + +static std::atomic sStream; + +auto gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t* param) -> void { + tinyfsm::FsmList::dispatch( + bluetooth::events::internal::Gap{.type = event, .param = param}); +} + +auto avrcp_cb(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) + -> void { + tinyfsm::FsmList::dispatch( + bluetooth::events::internal::Avrc{.type = event, .param = param}); +} + +auto a2dp_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t* param) -> void { + tinyfsm::FsmList::dispatch( + bluetooth::events::internal::A2dp{.type = event, .param = param}); +} + +auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t { + if (buf == nullptr || buf_size <= 0) { + return 0; + } + StreamBufferHandle_t stream = sStream.load(); + if (stream == nullptr) { + return 0; + } + return xStreamBufferReceive(stream, buf, buf_size, 0); +} + +Bluetooth::Bluetooth() { + tinyfsm::FsmList::start(); +} + +auto Bluetooth::Enable() -> bool { + tinyfsm::FsmList::dispatch( + bluetooth::events::Enable{}); + + return !bluetooth::BluetoothState::is_in_state(); +} + +auto Bluetooth::Disable() -> void { + tinyfsm::FsmList::dispatch( + bluetooth::events::Disable{}); +} + +auto DeviceName() -> std::string { + uint8_t mac[8]{0}; + esp_efuse_mac_get_default(mac); + std::ostringstream name; + name << "TANGARA " << std::hex << mac[0] << mac[1]; + return name.str(); +} + +namespace bluetooth { + +static bool sIsFirstEntry = true; + +void Disabled::entry() { + if (sIsFirstEntry) { + // We only use BT Classic, to claw back ~60KiB from the BLE firmware. + esp_bt_controller_mem_release(ESP_BT_MODE_BLE); + sIsFirstEntry = false; + return; + } + + esp_bluedroid_disable(); + esp_bluedroid_deinit(); + esp_bt_controller_disable(); +} + +void Disabled::react(const events::Enable&) { + esp_bt_controller_config_t config = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + if (esp_bt_controller_init(&config) != ESP_OK) { + ESP_LOGE(kTag, "initialize controller failed"); + return; + } + + if (esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT) != ESP_OK) { + ESP_LOGE(kTag, "enable controller failed"); + return; + } + + if (esp_bluedroid_init() != ESP_OK) { + ESP_LOGE(kTag, "initialize bluedroid failed"); + return; + } + + if (esp_bluedroid_enable() != ESP_OK) { + ESP_LOGE(kTag, "enable bluedroid failed"); + return; + } + + // Enable Secure Simple Pairing + esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE; + esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO; + esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t)); + + // Set a reasonable name for the device. + std::string name = DeviceName(); + esp_bt_dev_set_device_name(name.c_str()); + + // Initialise GAP. This controls advertising our device, and scanning for + // other devices. + esp_bt_gap_register_callback(gap_cb); + + // Initialise AVRCP. This handles playback controls; play/pause/volume/etc. + // esp_avrc_ct_init(); + // esp_avrc_ct_register_callback(avrcp_cb); + + // Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC + // encoder only supports 2 channels of interleaved 16 bit samples, at + // 44.1kHz, so there is no additional configuration to be done for the + // stream itself. + esp_a2d_source_init(); + esp_a2d_register_callback(a2dp_cb); + esp_a2d_source_register_data_callback(a2dp_data_cb); + + // Don't let anyone interact with us before we're ready. + esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE); + + transit(); } +static constexpr uint8_t kDiscoveryTimeSeconds = 10; +static constexpr uint8_t kDiscoveryMaxResults = 0; + +void Scanning::entry() { + ESP_LOGI(kTag, "scanning for devices"); + esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, + kDiscoveryTimeSeconds, kDiscoveryMaxResults); +} + +void Scanning::exit() { + esp_bt_gap_cancel_discovery(); +} + +auto OnDeviceDiscovered(esp_bt_gap_cb_param_t* param) -> void { + ESP_LOGI(kTag, "device discovered"); +} + +void Scanning::react(const events::internal::Gap& ev) { + switch (ev.type) { + case ESP_BT_GAP_DISC_RES_EVT: + OnDeviceDiscovered(ev.param); + break; + case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: + if (ev.param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) { + ESP_LOGI(kTag, "still scanning"); + esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, + kDiscoveryTimeSeconds, kDiscoveryMaxResults); + } + break; + case ESP_BT_GAP_MODE_CHG_EVT: + // todo: mode change. is this important? + ESP_LOGI(kTag, "GAP mode changed"); + break; + default: + ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type); + } +} + +void Connecting::entry() { + ESP_LOGI(kTag, "connecting to device"); + esp_a2d_source_connect(nullptr); +} + +void Connecting::exit() {} + +void Connecting::react(const events::internal::Gap& ev) { + switch (ev.type) { + case ESP_BT_GAP_AUTH_CMPL_EVT: + // todo: auth completed. check if we succeeded. + break; + case ESP_BT_GAP_PIN_REQ_EVT: + // todo: device needs a pin to connect. + break; + case ESP_BT_GAP_CFM_REQ_EVT: + // todo: device needs user to click okay. + break; + case ESP_BT_GAP_KEY_NOTIF_EVT: + // todo: device is telling us a password? + break; + case ESP_BT_GAP_KEY_REQ_EVT: + // todo: device needs a password + break; + case ESP_BT_GAP_MODE_CHG_EVT: + // todo: mode change. is this important? + break; + default: + ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type); + } +} + +void Connecting::react(const events::internal::A2dp& ev) { + switch (ev.type) { + case ESP_A2D_CONNECTION_STATE_EVT: + // todo: connection state changed. we might be connected! + break; + default: + ESP_LOGW(kTag, "unhandled A2DP event: %u", ev.type); + } +} + +void Connected::react(const events::internal::A2dp& ev) { + switch (ev.type) { + case ESP_A2D_CONNECTION_STATE_EVT: + // todo: connection state changed. we might have dropped + break; + case ESP_A2D_AUDIO_STATE_EVT: + // todo: audio state changed. who knows, dude. + break; + default: + ESP_LOGW(kTag, "unhandled A2DP event: %u", ev.type); + } +} + +void Connected::react(const events::internal::Avrc& ev) { + switch (ev.type) { + case ESP_AVRC_CT_CONNECTION_STATE_EVT: + // todo: avrc connected. send our capabilities. + default: + ESP_LOGW(kTag, "unhandled AVRC event: %u", ev.type); + } +} + +} // namespace bluetooth + } // namespace drivers + +FSM_INITIAL_STATE(drivers::bluetooth::BluetoothState, + drivers::bluetooth::Disabled) diff --git a/src/drivers/include/bluetooth.hpp b/src/drivers/include/bluetooth.hpp index 22b58c8b..2b5e6a8d 100644 --- a/src/drivers/include/bluetooth.hpp +++ b/src/drivers/include/bluetooth.hpp @@ -1,20 +1,108 @@ #pragma once +#include #include +#include +#include +#include "esp_a2dp_api.h" +#include "esp_avrc_api.h" +#include "esp_gap_bt_api.h" +#include "tinyfsm.hpp" +#include "tinyfsm/include/tinyfsm.hpp" + namespace drivers { +/* + * A handle used to interact with the bluetooth state machine. + */ class Bluetooth { public: - static auto Enable() -> Bluetooth*; Bluetooth(); - ~Bluetooth(); - struct Device {}; - auto Scan() -> std::vector; + auto Enable() -> bool; + auto Disable() -> void; + + auto SetSource(StreamBufferHandle_t) -> void; +}; + +namespace bluetooth { + +namespace events { +struct Enable : public tinyfsm::Event {}; +struct Disable : public tinyfsm::Event {}; + +namespace internal { +struct Gap : public tinyfsm::Event { + esp_bt_gap_cb_event_t type; + esp_bt_gap_cb_param_t* param; +}; +struct A2dp : public tinyfsm::Event { + esp_a2d_cb_event_t type; + esp_a2d_cb_param_t* param; +}; +struct Avrc : public tinyfsm::Event { + esp_avrc_ct_cb_event_t type; + esp_avrc_ct_cb_param_t* param; +}; +} // namespace internal +} // namespace events + +class BluetoothState : public tinyfsm::Fsm { + public: + virtual ~BluetoothState(){}; + + virtual void entry() {} + virtual void exit() {} + + virtual void react(const events::Enable& ev){}; + virtual void react(const events::Disable& ev) = 0; + + virtual void react(const events::internal::Gap& ev) = 0; + virtual void react(const events::internal::A2dp& ev) = 0; + virtual void react(const events::internal::Avrc& ev){}; +}; + +class Disabled : public BluetoothState { + void entry() override; + + void react(const events::Enable& ev) override; + void react(const events::Disable& ev) override{}; + + void react(const events::internal::Gap& ev) override {} + void react(const events::internal::A2dp& ev) override {} +}; + +class Scanning : public BluetoothState { + void entry() override; + void exit() override; + + void react(const events::Disable& ev) override; - private: + void react(const events::internal::Gap& ev) override; + void react(const events::internal::A2dp& ev) override; }; +class Connecting : public BluetoothState { + void entry() override; + void exit() override; + + void react(const events::Disable& ev) override; + void react(const events::internal::Gap& ev) override; + void react(const events::internal::A2dp& ev) override; +}; + +class Connected : public BluetoothState { + void entry() override; + void exit() override; + + void react(const events::Disable& ev) override; + void react(const events::internal::Gap& ev) override; + void react(const events::internal::A2dp& ev) override; + void react(const events::internal::Avrc& ev) override; +}; + +} // namespace bluetooth + } // namespace drivers -- cgit v1.2.3 From 592f231627843bc44ebaaa4506aec26da1f56499 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 8 Aug 2023 17:11:13 +1000 Subject: Improve sd card errors --- src/drivers/storage.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/drivers/storage.cpp b/src/drivers/storage.cpp index db257dee..f253a79a 100644 --- a/src/drivers/storage.cpp +++ b/src/drivers/storage.cpp @@ -63,7 +63,7 @@ auto SdStorage::Create(IGpios* gpio) -> cpp::result { // Will return ESP_ERR_INVALID_RESPONSE if there is no card esp_err_t err = sdmmc_card_init(host.get(), card.get()); if (err != ESP_OK) { - ESP_LOGW(kTag, "Failed to read, err: %d", err); + ESP_LOGW(kTag, "Failed to read, err: %s", esp_err_to_name(err)); return cpp::fail(Error::FAILED_TO_READ); } @@ -74,7 +74,21 @@ auto SdStorage::Create(IGpios* gpio) -> cpp::result { // Mount right now, not on first operation. FRESULT ferr = f_mount(fs, "", 1); if (ferr != FR_OK) { - ESP_LOGW(kTag, "Failed to mount, err: %d", ferr); + std::string err_str; + switch (ferr) { + case FR_DISK_ERR: + err_str = "FR_DISK_ERR"; + break; + case FR_NOT_READY: + err_str = "FR_NOT_READY"; + break; + case FR_NO_FILESYSTEM: + err_str = "FR_NO_FILESYSTEM"; + break; + default: + err_str = std::to_string(ferr); + } + ESP_LOGW(kTag, "Failed to mount, err: %s", err_str.c_str()); return cpp::fail(Error::FAILED_TO_MOUNT); } -- cgit v1.2.3