summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
authorcooljqln <cooljqln@noreply.codeberg.org>2024-05-03 04:48:17 +0000
committercooljqln <cooljqln@noreply.codeberg.org>2024-05-03 04:48:17 +0000
commit3ceb8025ee4330c177101ed30ec17dfb0002f41e (patch)
tree58350210f15df7d00d967cac6f30eeceeb031a3c /src/audio
parent964da15a0b84f8e5f00e8abac2f7dfda0bf60488 (diff)
parent9fafd797a5504f458b5fcae4a1d28a68da936315 (diff)
downloadtangara-fw-3ceb8025ee4330c177101ed30ec17dfb0002f41e.tar.gz
Merge pull request 'Break dependency cycles with our components by merging co-dependent components together' (#68) from jqln/component-merge into main
Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/68
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/CMakeLists.txt14
-rw-r--r--src/audio/README.md64
-rw-r--r--src/audio/audio_converter.cpp265
-rw-r--r--src/audio/audio_decoder.cpp147
-rw-r--r--src/audio/audio_fsm.cpp575
-rw-r--r--src/audio/audio_source.cpp55
-rw-r--r--src/audio/bt_audio_output.cpp124
-rw-r--r--src/audio/fatfs_audio_input.cpp163
-rw-r--r--src/audio/fatfs_source.cpp77
-rw-r--r--src/audio/i2s_audio_output.cpp232
-rw-r--r--src/audio/include/audio_converter.hpp73
-rw-r--r--src/audio/include/audio_decoder.hpp56
-rw-r--r--src/audio/include/audio_events.hpp155
-rw-r--r--src/audio/include/audio_fsm.hpp129
-rw-r--r--src/audio/include/audio_sink.hpp87
-rw-r--r--src/audio/include/audio_source.hpp57
-rw-r--r--src/audio/include/bt_audio_output.hpp61
-rw-r--r--src/audio/include/fatfs_audio_input.hpp66
-rw-r--r--src/audio/include/fatfs_source.hpp46
-rw-r--r--src/audio/include/i2s_audio_output.hpp63
-rw-r--r--src/audio/include/readahead_source.hpp60
-rw-r--r--src/audio/include/resample.hpp37
-rw-r--r--src/audio/include/track_queue.hpp170
-rw-r--r--src/audio/readahead_source.cpp140
-rw-r--r--src/audio/resample.cpp55
-rw-r--r--src/audio/test/CMakeLists.txt7
-rw-r--r--src/audio/track_queue.cpp492
27 files changed, 0 insertions, 3470 deletions
diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
deleted file mode 100644
index 635320f4..00000000
--- a/src/audio/CMakeLists.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright 2023 jacqueline <me@jacqueline.id.au>
-#
-# SPDX-License-Identifier: GPL-3.0-only
-
-idf_component_register(
- SRCS "audio_decoder.cpp" "fatfs_audio_input.cpp" "i2s_audio_output.cpp"
- "track_queue.cpp" "audio_fsm.cpp" "audio_converter.cpp" "resample.cpp"
- "fatfs_source.cpp" "bt_audio_output.cpp" "readahead_source.cpp"
- "audio_source.cpp"
- INCLUDE_DIRS "include"
- REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "memory" "tinyfsm"
- "database" "system_fsm" "speexdsp" "millershuffle" "libcppbor")
-
-target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/audio/README.md b/src/audio/README.md
deleted file mode 100644
index 218be2c4..00000000
--- a/src/audio/README.md
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-FatfsAudioReader
- - input if a queue of filenames.
- - output is a cbor stream
- - 1 header, like "this is a new file! this is the file type!
- - followed by length-prefixed chunks of bytes
- - runs in a task, which prompts it to read/write one chunk, then returns.
- - task watches for kill signal, owns storage, etc.
-
-AudioDecoder
- - input is the chunked bytes above.
- - output is also a cbor stream
- - 1 header, which is like a reconfiguration packet thing.
- - "data that follows is this depth, this sample rate"
- - also indicates whether the configuration is 'sudden' for soft muting?
- - then length-prefixed chunks of bytes
-
-AudioOutput
- - input is the output of the decoder
- - outputs via writing to i2s_write, which copies data to a dma buffer
- - therefore, safe for us to consume any kind of reconfiguration here.
- - only issue is that we will need to wait for the dma buffers to drain before
- we can reconfigure the driver. (i2s_zero_dma_buffer)
- - this is important for i2s speed; we should avoid extra copy steps for the raw
- - pcm stream
- - input therefore needs to be two channels: one configuration channel, one bytes
- channel
-
-
-How do things like seeking, and progress work?
- - Reader knows where we are in terms of file size and position
- - Decoder knows sample rate, frames, etc. for knowing how that maps into
- - the time progress
- - Output knows where we are as well in a sense, but only in terms of the PCM
- output. this doesn't correspond to anything very well.
-
- So, to seek:
- - come up with your position. this is likely "where we are plus 10", or a
- specific timecode. the decoder has what we need for the byte position of this
- - tell the reader "hey we need to be in this file at this byte position
- - reader clears its own output buffer (since it's been doing readahead) and
- starts again at the given location
- For current position, the decoder will need to track where in the file it's up
- to.
-
-HEADERS + DATA:
- - cbor seems sensible for headers. allocate a little working buffer, encode the
- data, then send it out on the ringbuffer.
- - the data itself is harder, since tinycbor doesn't support writing chunked indefinite
- length stuff. this is a problem bc we need to give cbor the buffer up front, but
- we don't know exactly how long things will be, so it ends up being slightly awkward
- and inefficient.
- - we could also just like... write the struct i guess? that might be okay.
- - gives us a format like <TYPE ENUM> <LENGTH> <DATA>
- - could be smart with the type, use like a 32 bit int, and encode the length
- - in there?
- - then from the reader's perspective, it's:
- - read 4 bytes, work out what's next
- - read the next X bytes
diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp
deleted file mode 100644
index d2edb0b3..00000000
--- a/src/audio/audio_converter.cpp
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "audio_converter.hpp"
-#include <stdint.h>
-
-#include <algorithm>
-#include <cmath>
-#include <cstdint>
-
-#include "audio_events.hpp"
-#include "audio_sink.hpp"
-#include "esp_heap_caps.h"
-#include "esp_log.h"
-#include "event_queue.hpp"
-#include "freertos/portmacro.h"
-#include "freertos/projdefs.h"
-#include "i2s_dac.hpp"
-
-#include "resample.hpp"
-#include "sample.hpp"
-#include "tasks.hpp"
-
-[[maybe_unused]] static constexpr char kTag[] = "mixer";
-
-static constexpr std::size_t kSampleBufferLength =
- drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2;
-static constexpr std::size_t kSourceBufferLength = kSampleBufferLength * 2;
-
-namespace audio {
-
-SampleConverter::SampleConverter()
- : commands_(xQueueCreate(1, sizeof(Args))),
- resampler_(nullptr),
- source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
- sizeof(sample::Sample) * 2,
- MALLOC_CAP_DMA)),
- leftover_bytes_(0),
- samples_sunk_(0) {
- input_buffer_ = {
- reinterpret_cast<sample::Sample*>(heap_caps_calloc(
- kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
- kSampleBufferLength};
- input_buffer_as_bytes_ = {reinterpret_cast<std::byte*>(input_buffer_.data()),
- input_buffer_.size_bytes()};
-
- resampled_buffer_ = {
- reinterpret_cast<sample::Sample*>(heap_caps_calloc(
- kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
- kSampleBufferLength};
-
- tasks::StartPersistent<tasks::Type::kAudioConverter>([&]() { Main(); });
-}
-
-SampleConverter::~SampleConverter() {
- vQueueDelete(commands_);
- vStreamBufferDelete(source_);
-}
-
-auto SampleConverter::SetOutput(std::shared_ptr<IAudioOutput> output) -> void {
- // FIXME: We should add synchronisation here, but we should be careful about
- // not impacting performance given that the output will change only very
- // rarely (if ever).
- sink_ = output;
-}
-
-auto SampleConverter::beginStream(std::shared_ptr<TrackInfo> track) -> void {
- Args args{
- .track = new std::shared_ptr<TrackInfo>(track),
- .samples_available = 0,
- .is_end_of_stream = false,
- };
- xQueueSend(commands_, &args, portMAX_DELAY);
-}
-
-auto SampleConverter::continueStream(std::span<sample::Sample> input) -> void {
- Args args{
- .track = nullptr,
- .samples_available = input.size(),
- .is_end_of_stream = false,
- };
- xQueueSend(commands_, &args, portMAX_DELAY);
- xStreamBufferSend(source_, input.data(), input.size_bytes(), portMAX_DELAY);
-}
-
-auto SampleConverter::endStream() -> void {
- Args args{
- .track = nullptr,
- .samples_available = 0,
- .is_end_of_stream = true,
- };
- xQueueSend(commands_, &args, portMAX_DELAY);
-}
-
-auto SampleConverter::Main() -> void {
- for (;;) {
- Args args;
- while (!xQueueReceive(commands_, &args, portMAX_DELAY)) {
- }
-
- if (args.track) {
- handleBeginStream(*args.track);
- delete args.track;
- }
- if (args.samples_available) {
- handleContinueStream(args.samples_available);
- }
- if (args.is_end_of_stream) {
- handleEndStream();
- }
- }
-}
-
-auto SampleConverter::handleBeginStream(std::shared_ptr<TrackInfo> track)
- -> void {
- if (track->format != source_format_) {
- resampler_.reset();
- source_format_ = track->format;
- leftover_bytes_ = 0;
-
- auto new_target = sink_->PrepareFormat(track->format);
- if (new_target != target_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));
- }
-
- sink_->Configure(new_target);
- }
- target_format_ = new_target;
- }
-
- samples_sunk_ = 0;
- events::Audio().Dispatch(internal::StreamStarted{
- .track = track,
- .src_format = source_format_,
- .dst_format = target_format_,
- });
-}
-
-auto SampleConverter::handleContinueStream(size_t samples_available) -> void {
- // Loop until we finish reading all the bytes indicated. There might be
- // leftovers from each iteration, and from this process as a whole,
- // depending on the resampling stage.
- size_t bytes_read = 0;
- size_t bytes_to_read = samples_available * sizeof(sample::Sample);
- while (bytes_read < bytes_to_read) {
- // First top up the input buffer, taking care not to overwrite anything
- // remaining from a previous iteration.
- size_t bytes_read_this_it = xStreamBufferReceive(
- source_, input_buffer_as_bytes_.subspan(leftover_bytes_).data(),
- std::min(input_buffer_as_bytes_.size() - leftover_bytes_,
- bytes_to_read - bytes_read),
- portMAX_DELAY);
- bytes_read += bytes_read_this_it;
-
- // Calculate the number of whole samples that are now in the input buffer.
- size_t bytes_in_buffer = bytes_read_this_it + leftover_bytes_;
- size_t samples_in_buffer = bytes_in_buffer / sizeof(sample::Sample);
-
- size_t samples_used = handleSamples(input_buffer_.first(samples_in_buffer));
-
- // Maybe the resampler didn't consume everything. Maybe the last few
- // bytes we read were half a frame. Either way, we need to calculate the
- // size of the remainder in bytes, then move it to the front of our
- // buffer.
- size_t bytes_used = samples_used * sizeof(sample::Sample);
- assert(bytes_used <= bytes_in_buffer);
-
- leftover_bytes_ = bytes_in_buffer - bytes_used;
- if (leftover_bytes_ > 0) {
- std::memmove(input_buffer_as_bytes_.data(),
- input_buffer_as_bytes_.data() + bytes_used, leftover_bytes_);
- }
- }
-}
-
-auto SampleConverter::handleSamples(std::span<sample::Sample> input) -> size_t {
- if (source_format_ == target_format_) {
- // The happiest possible case: the input format matches the output
- // format already.
- sendToSink(input);
- return input.size();
- }
-
- size_t samples_used = 0;
- while (samples_used < input.size()) {
- std::span<sample::Sample> output_source;
- if (source_format_.sample_rate != target_format_.sample_rate) {
- if (resampler_ == nullptr) {
- ESP_LOGI(kTag, "creating new resampler for %lu -> %lu",
- source_format_.sample_rate, target_format_.sample_rate);
- resampler_.reset(new Resampler(source_format_.sample_rate,
- target_format_.sample_rate,
- source_format_.num_channels));
- }
-
- size_t read, written;
- std::tie(read, written) = resampler_->Process(input.subspan(samples_used),
- resampled_buffer_, false);
- samples_used += read;
-
- if (read == 0 && written == 0) {
- // Zero samples used or written. We need more input.
- break;
- }
- output_source = resampled_buffer_.first(written);
- } else {
- output_source = input;
- samples_used = input.size();
- }
-
- sendToSink(output_source);
- }
-
- return samples_used;
-}
-
-auto SampleConverter::handleEndStream() -> void {
- if (resampler_) {
- size_t read, written;
- std::tie(read, written) = resampler_->Process({}, resampled_buffer_, true);
-
- if (written > 0) {
- sendToSink(resampled_buffer_.first(written));
- }
- }
-
- // Send a final update to finish off this stream's samples.
- if (samples_sunk_ > 0) {
- events::Audio().Dispatch(internal::StreamUpdate{
- .samples_sunk = samples_sunk_,
- });
- samples_sunk_ = 0;
- }
- leftover_bytes_ = 0;
-
- events::Audio().Dispatch(internal::StreamEnded{});
-}
-
-auto SampleConverter::sendToSink(std::span<sample::Sample> samples) -> void {
- // Update the number of samples sunk so far *before* actually sinking them,
- // since writing to the stream buffer will block when the buffer gets full.
- samples_sunk_ += samples.size();
- if (samples_sunk_ >=
- target_format_.sample_rate * target_format_.num_channels) {
- events::Audio().Dispatch(internal::StreamUpdate{
- .samples_sunk = samples_sunk_,
- });
- samples_sunk_ = 0;
- }
-
- xStreamBufferSend(sink_->stream(),
- reinterpret_cast<std::byte*>(samples.data()),
- samples.size_bytes(), portMAX_DELAY);
-}
-
-} // namespace audio
diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp
deleted file mode 100644
index baf17e7a..00000000
--- a/src/audio/audio_decoder.cpp
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "audio_decoder.hpp"
-
-#include <cstdint>
-#include <cstdlib>
-
-#include <algorithm>
-#include <cmath>
-#include <cstddef>
-#include <cstdint>
-#include <cstring>
-#include <deque>
-#include <memory>
-#include <span>
-#include <variant>
-
-#include "cbor.h"
-#include "esp_err.h"
-#include "esp_heap_caps.h"
-#include "esp_log.h"
-#include "freertos/portmacro.h"
-#include "freertos/projdefs.h"
-#include "freertos/queue.h"
-#include "freertos/ringbuf.h"
-#include "i2s_dac.hpp"
-
-#include "audio_converter.hpp"
-#include "audio_events.hpp"
-#include "audio_fsm.hpp"
-#include "audio_sink.hpp"
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "event_queue.hpp"
-#include "fatfs_audio_input.hpp"
-#include "sample.hpp"
-#include "tasks.hpp"
-#include "track.hpp"
-#include "types.hpp"
-#include "ui_fsm.hpp"
-
-namespace audio {
-
-[[maybe_unused]] static const char* kTag = "audio_dec";
-
-static constexpr std::size_t kCodecBufferLength =
- drivers::kI2SBufferLengthFrames * sizeof(sample::Sample);
-
-auto Decoder::Start(std::shared_ptr<IAudioSource> source,
- std::shared_ptr<SampleConverter> sink) -> Decoder* {
- Decoder* task = new Decoder(source, sink);
- tasks::StartPersistent<tasks::Type::kAudioDecoder>([=]() { task->Main(); });
- return task;
-}
-
-Decoder::Decoder(std::shared_ptr<IAudioSource> source,
- std::shared_ptr<SampleConverter> mixer)
- : source_(source), converter_(mixer), codec_(), current_format_() {
- ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024);
- codec_buffer_ = {
- reinterpret_cast<sample::Sample*>(heap_caps_calloc(
- kCodecBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
- kCodecBufferLength};
-}
-
-void Decoder::Main() {
- for (;;) {
- if (source_->HasNewStream() || !stream_) {
- std::shared_ptr<TaggedStream> new_stream = source_->NextStream();
- if (new_stream && BeginDecoding(new_stream)) {
- stream_ = new_stream;
- } else {
- continue;
- }
- }
-
- if (ContinueDecoding()) {
- stream_.reset();
- }
- }
-}
-
-auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
- // Ensure any previous codec is freed before creating a new one.
- codec_.reset();
- codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr));
- if (!codec_) {
- ESP_LOGE(kTag, "no codec found for stream");
- return false;
- }
-
- auto open_res = codec_->OpenStream(stream, stream->Offset());
- if (open_res.has_error()) {
- ESP_LOGE(kTag, "codec failed to start: %s",
- codecs::ICodec::ErrorString(open_res.error()).c_str());
- return false;
- }
- stream->SetPreambleFinished();
- current_sink_format_ = IAudioOutput::Format{
- .sample_rate = open_res->sample_rate_hz,
- .num_channels = open_res->num_channels,
- .bits_per_sample = 16,
- };
-
- std::optional<uint32_t> duration;
- if (open_res->total_samples) {
- duration = open_res->total_samples.value() / open_res->num_channels /
- open_res->sample_rate_hz;
- }
-
- converter_->beginStream(std::make_shared<TrackInfo>(TrackInfo{
- .tags = stream->tags(),
- .uri = stream->Filepath(),
- .duration = duration,
- .start_offset = stream->Offset(),
- .bitrate_kbps = open_res->sample_rate_hz,
- .encoding = stream->type(),
- .format = *current_sink_format_,
- }));
-
- return true;
-}
-
-auto Decoder::ContinueDecoding() -> bool {
- auto res = codec_->DecodeTo(codec_buffer_);
- if (res.has_error()) {
- converter_->endStream();
- return true;
- }
-
- if (res->samples_written > 0) {
- converter_->continueStream(codec_buffer_.first(res->samples_written));
- }
-
- if (res->is_stream_finished) {
- converter_->endStream();
- codec_.reset();
- }
-
- return res->is_stream_finished;
-}
-
-} // namespace audio
diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp
deleted file mode 100644
index ffb462f8..00000000
--- a/src/audio/audio_fsm.cpp
+++ /dev/null
@@ -1,575 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "audio_fsm.hpp"
-#include <stdint.h>
-
-#include <future>
-#include <memory>
-#include <variant>
-
-#include "audio_sink.hpp"
-#include "bluetooth_types.hpp"
-#include "cppbor.h"
-#include "cppbor_parse.h"
-#include "esp_heap_caps.h"
-#include "esp_log.h"
-#include "freertos/FreeRTOS.h"
-#include "freertos/portmacro.h"
-#include "freertos/projdefs.h"
-
-#include "audio_converter.hpp"
-#include "audio_decoder.hpp"
-#include "audio_events.hpp"
-#include "bluetooth.hpp"
-#include "bt_audio_output.hpp"
-#include "event_queue.hpp"
-#include "fatfs_audio_input.hpp"
-#include "future_fetcher.hpp"
-#include "i2s_audio_output.hpp"
-#include "i2s_dac.hpp"
-#include "nvs.hpp"
-#include "sample.hpp"
-#include "service_locator.hpp"
-#include "system_events.hpp"
-#include "tinyfsm.hpp"
-#include "track.hpp"
-#include "track_queue.hpp"
-#include "wm8523.hpp"
-
-namespace audio {
-
-[[maybe_unused]] static const char kTag[] = "audio_fsm";
-
-std::shared_ptr<system_fsm::ServiceLocator> AudioState::sServices;
-
-std::shared_ptr<FatfsAudioInput> AudioState::sFileSource;
-std::unique_ptr<Decoder> AudioState::sDecoder;
-std::shared_ptr<SampleConverter> AudioState::sSampleConverter;
-std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
-std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
-std::shared_ptr<IAudioOutput> AudioState::sOutput;
-
-// Two seconds of samples for two channels, at a representative sample rate.
-constexpr size_t kDrainLatencySamples = 48000 * 2 * 2;
-constexpr size_t kDrainBufferSize =
- sizeof(sample::Sample) * kDrainLatencySamples;
-
-StreamBufferHandle_t AudioState::sDrainBuffer;
-std::optional<IAudioOutput::Format> AudioState::sDrainFormat;
-
-std::shared_ptr<TrackInfo> AudioState::sCurrentTrack;
-uint64_t AudioState::sCurrentSamples;
-bool AudioState::sCurrentTrackIsFromQueue;
-
-std::shared_ptr<TrackInfo> AudioState::sNextTrack;
-uint64_t AudioState::sNextTrackCueSamples;
-bool AudioState::sNextTrackIsFromQueue;
-
-bool AudioState::sIsResampling;
-bool AudioState::sIsPaused = true;
-
-auto AudioState::currentPositionSeconds() -> std::optional<uint32_t> {
- if (!sCurrentTrack || !sDrainFormat) {
- return {};
- }
- return sCurrentSamples /
- (sDrainFormat->num_channels * sDrainFormat->sample_rate);
-}
-
-void AudioState::react(const QueueUpdate& ev) {
- SetTrack cmd{
- .new_track = std::monostate{},
- .seek_to_second = {},
- .transition = SetTrack::Transition::kHardCut,
- };
-
- auto current = sServices->track_queue().current();
- if (current) {
- cmd.new_track = *current;
- }
-
- switch (ev.reason) {
- case QueueUpdate::kExplicitUpdate:
- if (!ev.current_changed) {
- return;
- }
- sNextTrackIsFromQueue = true;
- cmd.transition = SetTrack::Transition::kHardCut;
- break;
- case QueueUpdate::kRepeatingLastTrack:
- sNextTrackIsFromQueue = true;
- cmd.transition = SetTrack::Transition::kGapless;
- break;
- case QueueUpdate::kTrackFinished:
- if (!ev.current_changed) {
- cmd.new_track = std::monostate{};
- } else {
- sNextTrackIsFromQueue = true;
- }
- cmd.transition = SetTrack::Transition::kGapless;
- break;
- case QueueUpdate::kDeserialised:
- default:
- // The current track is deserialised separately in order to retain seek
- // position.
- return;
- }
-
- tinyfsm::FsmList<AudioState>::dispatch(cmd);
-}
-
-void AudioState::react(const SetTrack& ev) {
- // Remember the current track if there is one, since we need to preserve some
- // of the state if it turns out this SetTrack event corresponds to seeking
- // within the current track.
- std::string prev_uri;
- bool prev_from_queue = false;
- if (sCurrentTrack) {
- prev_uri = sCurrentTrack->uri;
- prev_from_queue = sCurrentTrackIsFromQueue;
- }
-
- if (ev.transition == SetTrack::Transition::kHardCut) {
- sCurrentTrack.reset();
- sCurrentSamples = 0;
- sCurrentTrackIsFromQueue = false;
- clearDrainBuffer();
- }
-
- if (std::holds_alternative<std::monostate>(ev.new_track)) {
- ESP_LOGI(kTag, "playback finished, awaiting drain");
- sFileSource->SetPath();
- awaitEmptyDrainBuffer();
- sCurrentTrack.reset();
- sDrainFormat.reset();
- sCurrentSamples = 0;
- sCurrentTrackIsFromQueue = false;
- transit<states::Standby>();
- return;
- }
-
- // Move the rest of the work to a background worker, since it may require db
- // lookups to resolve a track id into a path.
- auto new_track = ev.new_track;
- uint32_t seek_to = ev.seek_to_second.value_or(0);
- sServices->bg_worker().Dispatch<void>([=]() {
- std::optional<std::string> path;
- if (std::holds_alternative<database::TrackId>(new_track)) {
- auto db = sServices->database().lock();
- if (db) {
- path = db->getTrackPath(std::get<database::TrackId>(new_track));
- }
- } else if (std::holds_alternative<std::string>(new_track)) {
- path = std::get<std::string>(new_track);
- }
-
- if (path) {
- if (*path == prev_uri) {
- // This was a seek or replay within the same track; don't forget where
- // the track originally came from.
- sNextTrackIsFromQueue = prev_from_queue;
- }
- sFileSource->SetPath(*path, seek_to);
- } else {
- sFileSource->SetPath();
- }
- });
-}
-
-void AudioState::react(const TogglePlayPause& ev) {
- sIsPaused = !ev.set_to.value_or(sIsPaused);
- if (!sIsPaused && is_in_state<states::Standby>() && sCurrentTrack) {
- transit<states::Playback>();
- } else if (sIsPaused && is_in_state<states::Playback>()) {
- transit<states::Standby>();
- }
-}
-
-void AudioState::react(const internal::StreamStarted& ev) {
- sDrainFormat = ev.dst_format;
- sIsResampling = ev.src_format != ev.dst_format;
-
- sNextTrack = ev.track;
- sNextTrackCueSamples = sCurrentSamples + (kDrainLatencySamples / 2);
-
- ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)",
- ev.track->uri.c_str(), sDrainFormat->num_channels,
- sDrainFormat->sample_rate, sIsResampling);
-}
-
-void AudioState::react(const internal::StreamEnded&) {
- ESP_LOGI(kTag, "stream ended");
-
- if (sCurrentTrackIsFromQueue) {
- sServices->track_queue().finish();
- } else {
- tinyfsm::FsmList<AudioState>::dispatch(SetTrack{
- .new_track = std::monostate{},
- .seek_to_second = {},
- .transition = SetTrack::Transition::kGapless,
- });
- }
-}
-
-void AudioState::react(const internal::StreamUpdate& ev) {
- sCurrentSamples += ev.samples_sunk;
-
- if (sNextTrack && sCurrentSamples >= sNextTrackCueSamples) {
- ESP_LOGI(kTag, "next track is now sinking");
- sCurrentTrack = sNextTrack;
- sCurrentSamples -= sNextTrackCueSamples;
- sCurrentSamples += sNextTrack->start_offset.value_or(0) *
- (sDrainFormat->num_channels * sDrainFormat->sample_rate);
- sCurrentTrackIsFromQueue = sNextTrackIsFromQueue;
-
- sNextTrack.reset();
- sNextTrackCueSamples = 0;
- sNextTrackIsFromQueue = false;
- }
-
- if (sCurrentTrack) {
- PlaybackUpdate event{
- .current_track = sCurrentTrack,
- .track_position = currentPositionSeconds(),
- .paused = !is_in_state<states::Playback>(),
- };
- events::System().Dispatch(event);
- events::Ui().Dispatch(event);
- }
-
- if (sCurrentTrack && !sIsPaused && !is_in_state<states::Playback>()) {
- ESP_LOGI(kTag, "ready to play!");
- transit<states::Playback>();
- }
-}
-
-void AudioState::react(const system_fsm::BluetoothEvent& ev) {
- if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) {
- return;
- }
- auto dev = sServices->bluetooth().ConnectedDevice();
- if (!dev) {
- return;
- }
- sBtOutput->SetVolume(sServices->nvs().BluetoothVolume(dev->mac));
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
-}
-
-void AudioState::react(const StepUpVolume& ev) {
- if (sOutput->AdjustVolumeUp()) {
- commitVolume();
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
- }
-}
-
-void AudioState::react(const StepDownVolume& ev) {
- if (sOutput->AdjustVolumeDown()) {
- commitVolume();
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
- }
-}
-
-void AudioState::react(const system_fsm::HasPhonesChanged& ev) {
- if (ev.has_headphones) {
- ESP_LOGI(kTag, "headphones in!");
- } else {
- ESP_LOGI(kTag, "headphones out!");
- }
-}
-
-void AudioState::react(const SetVolume& ev) {
- if (ev.db.has_value()) {
- if (sOutput->SetVolumeDb(ev.db.value())) {
- commitVolume();
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
- }
-
- } else if (ev.percent.has_value()) {
- if (sOutput->SetVolumePct(ev.percent.value())) {
- commitVolume();
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
- }
- }
-}
-
-void AudioState::react(const SetVolumeLimit& ev) {
- uint16_t limit_in_dac_units =
- drivers::wm8523::kLineLevelReferenceVolume + (ev.limit_db * 4);
-
- sI2SOutput->SetMaxVolume(limit_in_dac_units);
- sServices->nvs().AmpMaxVolume(limit_in_dac_units);
-
- events::Ui().Dispatch(VolumeLimitChanged{
- .new_limit_db = ev.limit_db,
- });
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
-}
-
-void AudioState::react(const SetVolumeBalance& ev) {
- sOutput->SetVolumeImbalance(ev.left_bias);
- sServices->nvs().AmpLeftBias(ev.left_bias);
-
- events::Ui().Dispatch(VolumeBalanceChanged{
- .left_bias = ev.left_bias,
- });
-}
-
-void AudioState::react(const OutputModeChanged& ev) {
- ESP_LOGI(kTag, "output mode changed");
- auto new_mode = sServices->nvs().OutputMode();
- sOutput->mode(IAudioOutput::Modes::kOff);
- switch (new_mode) {
- case drivers::NvsStorage::Output::kBluetooth:
- sOutput = sBtOutput;
- break;
- case drivers::NvsStorage::Output::kHeadphones:
- sOutput = sI2SOutput;
- break;
- }
- sOutput->mode(IAudioOutput::Modes::kOnPaused);
- sSampleConverter->SetOutput(sOutput);
-
- // Bluetooth volume isn't 'changed' until we've connected to a device.
- if (new_mode == drivers::NvsStorage::Output::kHeadphones) {
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
- }
-}
-
-auto AudioState::clearDrainBuffer() -> void {
- // Tell the decoder to stop adding new samples. This might not take effect
- // immediately, since the decoder might currently be stuck waiting for space
- // to become available in the drain buffer.
- sFileSource->SetPath();
-
- auto mode = sOutput->mode();
- if (mode == IAudioOutput::Modes::kOnPlaying) {
- // If we're currently playing, then the drain buffer will be actively
- // draining on its own. Just keep trying to reset until it works.
- while (xStreamBufferReset(sDrainBuffer) != pdPASS) {
- }
- } else {
- // If we're not currently playing, then we need to actively pull samples
- // out of the drain buffer to unblock the decoder.
- while (!xStreamBufferIsEmpty(sDrainBuffer)) {
- // Read a little to unblock the decoder.
- uint8_t drain[2048];
- xStreamBufferReceive(sDrainBuffer, drain, sizeof(drain), 0);
-
- // Try to quickly discard the rest.
- xStreamBufferReset(sDrainBuffer);
- }
- }
-}
-
-auto AudioState::awaitEmptyDrainBuffer() -> void {
- if (is_in_state<states::Playback>()) {
- for (int i = 0; i < 10 && !xStreamBufferIsEmpty(sDrainBuffer); i++) {
- vTaskDelay(pdMS_TO_TICKS(250));
- }
- }
- if (!xStreamBufferIsEmpty(sDrainBuffer)) {
- clearDrainBuffer();
- }
-}
-
-auto AudioState::commitVolume() -> void {
- auto mode = sServices->nvs().OutputMode();
- auto vol = sOutput->GetVolume();
- if (mode == drivers::NvsStorage::Output::kHeadphones) {
- sServices->nvs().AmpCurrentVolume(vol);
- } else if (mode == drivers::NvsStorage::Output::kBluetooth) {
- auto dev = sServices->bluetooth().ConnectedDevice();
- if (!dev) {
- return;
- }
- sServices->nvs().BluetoothVolume(dev->mac, vol);
- }
-}
-
-namespace states {
-
-void Uninitialised::react(const system_fsm::BootComplete& ev) {
- sServices = ev.services;
-
- ESP_LOGI(kTag, "allocating drain buffer, size %u KiB",
- kDrainBufferSize / 1024);
-
- auto meta = reinterpret_cast<StaticStreamBuffer_t*>(
- heap_caps_malloc(sizeof(StaticStreamBuffer_t), MALLOC_CAP_DMA));
- auto storage = reinterpret_cast<uint8_t*>(
- heap_caps_malloc(kDrainBufferSize, MALLOC_CAP_SPIRAM));
-
- sDrainBuffer = xStreamBufferCreateStatic(
- kDrainBufferSize, sizeof(sample::Sample), storage, meta);
-
- sFileSource.reset(
- new FatfsAudioInput(sServices->tag_parser(), sServices->bg_worker()));
- sI2SOutput.reset(new I2SAudioOutput(sDrainBuffer, sServices->gpios()));
- sBtOutput.reset(new BluetoothAudioOutput(sDrainBuffer, sServices->bluetooth(),
- sServices->bg_worker()));
-
- auto& nvs = sServices->nvs();
- sI2SOutput->SetMaxVolume(nvs.AmpMaxVolume());
- sI2SOutput->SetVolume(nvs.AmpCurrentVolume());
- sI2SOutput->SetVolumeImbalance(nvs.AmpLeftBias());
-
- if (sServices->nvs().OutputMode() ==
- drivers::NvsStorage::Output::kHeadphones) {
- sOutput = sI2SOutput;
- } else {
- // Ensure Bluetooth gets enabled if it's the default sink.
- sServices->bluetooth().Enable();
- sOutput = sBtOutput;
- }
- sOutput->mode(IAudioOutput::Modes::kOnPaused);
-
- events::Ui().Dispatch(VolumeLimitChanged{
- .new_limit_db =
- (static_cast<int>(nvs.AmpMaxVolume()) -
- static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume)) /
- 4,
- });
- events::Ui().Dispatch(VolumeChanged{
- .percent = sOutput->GetVolumePct(),
- .db = sOutput->GetVolumeDb(),
- });
- events::Ui().Dispatch(VolumeBalanceChanged{
- .left_bias = nvs.AmpLeftBias(),
- });
-
- sSampleConverter.reset(new SampleConverter());
- sSampleConverter->SetOutput(sOutput);
-
- Decoder::Start(sFileSource, sSampleConverter);
-
- transit<Standby>();
-}
-
-static const char kQueueKey[] = "audio:queue";
-static const char kCurrentFileKey[] = "audio:current";
-
-void Standby::react(const system_fsm::KeyLockChanged& ev) {
- if (!ev.locking) {
- return;
- }
- sServices->bg_worker().Dispatch<void>([this]() {
- auto db = sServices->database().lock();
- if (!db) {
- return;
- }
- auto& queue = sServices->track_queue();
- if (queue.totalSize() <= queue.currentPosition()) {
- // Nothing is playing, so don't bother saving the queue.
- db->put(kQueueKey, "");
- return;
- }
- db->put(kQueueKey, queue.serialise());
-
- if (sCurrentTrack) {
- cppbor::Array current_track{
- cppbor::Tstr{sCurrentTrack->uri},
- cppbor::Uint{currentPositionSeconds().value_or(0)},
- };
- db->put(kCurrentFileKey, current_track.toString());
- }
- });
-}
-
-void Standby::react(const system_fsm::StorageMounted& ev) {
- sServices->bg_worker().Dispatch<void>([]() {
- auto db = sServices->database().lock();
- if (!db) {
- return;
- }
-
- // Restore the currently playing file before restoring the queue. This way,
- // we can fall back to restarting the queue's current track if there's any
- // issue restoring the current file.
- auto current = db->get(kCurrentFileKey);
- if (current) {
- // Again, ensure we don't boot-loop by trying to play a track that causes
- // a crash over and over again.
- db->put(kCurrentFileKey, "");
- auto [parsed, unused, err] = cppbor::parse(
- reinterpret_cast<uint8_t*>(current->data()), current->size());
- if (parsed->type() == cppbor::ARRAY) {
- std::string filename = parsed->asArray()->get(0)->asTstr()->value();
- uint32_t pos = parsed->asArray()->get(1)->asUint()->value();
-
- events::Audio().Dispatch(SetTrack{
- .new_track = filename,
- .seek_to_second = pos,
- .transition = SetTrack::Transition::kHardCut,
- });
- }
- }
-
- auto queue = db->get(kQueueKey);
- if (queue) {
- // Don't restore the same queue again. This ideally should do nothing,
- // but guards against bad edge cases where restoring the queue ends up
- // causing a crash.
- db->put(kQueueKey, "");
- sServices->track_queue().deserialise(*queue);
- }
- });
-}
-
-void Playback::entry() {
- ESP_LOGI(kTag, "audio output resumed");
- sOutput->mode(IAudioOutput::Modes::kOnPlaying);
-
- PlaybackUpdate event{
- .current_track = sCurrentTrack,
- .track_position = currentPositionSeconds(),
- .paused = false,
- };
-
- events::System().Dispatch(event);
- events::Ui().Dispatch(event);
-}
-
-void Playback::exit() {
- ESP_LOGI(kTag, "audio output paused");
- sOutput->mode(IAudioOutput::Modes::kOnPaused);
-
- PlaybackUpdate event{
- .current_track = sCurrentTrack,
- .track_position = currentPositionSeconds(),
- .paused = true,
- };
-
- events::System().Dispatch(event);
- events::Ui().Dispatch(event);
-}
-
-} // namespace states
-
-} // namespace audio
-
-FSM_INITIAL_STATE(audio::AudioState, audio::states::Uninitialised)
diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp
deleted file mode 100644
index ee2f617f..00000000
--- a/src/audio/audio_source.cpp
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-TaggedStream::TaggedStream(std::shared_ptr<database::TrackTags> t,
- std::unique_ptr<codecs::IStream> w,
- std::string filepath,
- uint32_t offset)
- : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)), filepath_(filepath), offset_(offset) {}
-
-auto TaggedStream::tags() -> std::shared_ptr<database::TrackTags> {
- return tags_;
-}
-
-auto TaggedStream::Read(std::span<std::byte> dest) -> ssize_t {
- return wrapped_->Read(dest);
-}
-
-auto TaggedStream::CanSeek() -> bool {
- return wrapped_->CanSeek();
-}
-
-auto TaggedStream::SeekTo(int64_t destination, SeekFrom from) -> void {
- wrapped_->SeekTo(destination, from);
-}
-
-auto TaggedStream::CurrentPosition() -> int64_t {
- return wrapped_->CurrentPosition();
-}
-
-auto TaggedStream::Size() -> std::optional<int64_t> {
- return wrapped_->Size();
-}
-
-auto TaggedStream::Offset() -> uint32_t {
- return offset_;
-}
-
-auto TaggedStream::Filepath() -> std::string {
- return filepath_;
-}
-
-auto TaggedStream::SetPreambleFinished() -> void {
- wrapped_->SetPreambleFinished();
-}
-
-} // namespace audio
diff --git a/src/audio/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp
deleted file mode 100644
index 229a38bb..00000000
--- a/src/audio/bt_audio_output.cpp
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "bt_audio_output.hpp"
-
-#include <algorithm>
-#include <cmath>
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-#include <variant>
-
-#include "esp_err.h"
-#include "esp_heap_caps.h"
-#include "freertos/portmacro.h"
-#include "freertos/projdefs.h"
-
-#include "gpios.hpp"
-#include "i2c.hpp"
-#include "i2s_dac.hpp"
-#include "result.hpp"
-#include "tasks.hpp"
-#include "wm8523.hpp"
-
-[[maybe_unused]] static const char* kTag = "BTOUT";
-
-namespace audio {
-
-static constexpr uint16_t kVolumeRange = 60;
-
-BluetoothAudioOutput::BluetoothAudioOutput(StreamBufferHandle_t s,
- drivers::Bluetooth& bt,
- tasks::WorkerPool& p)
- : IAudioOutput(s), bluetooth_(bt), bg_worker_(p), volume_() {}
-
-BluetoothAudioOutput::~BluetoothAudioOutput() {}
-
-auto BluetoothAudioOutput::changeMode(Modes mode) -> void {
- if (mode == Modes::kOnPlaying) {
- bluetooth_.SetSource(stream());
- } else {
- bluetooth_.SetSource(nullptr);
- }
-}
-
-auto BluetoothAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void {
- // FIXME: Support two separate scaling factors in the bluetooth driver.
-}
-
-auto BluetoothAudioOutput::SetVolume(uint16_t v) -> void {
- volume_ = std::clamp<uint16_t>(v, 0, 100);
- bg_worker_.Dispatch<void>([&]() {
- float factor =
- pow(10, static_cast<double>(kVolumeRange) * (volume_ - 100) / 100 / 20);
- bluetooth_.SetVolumeFactor(factor);
- });
-}
-
-auto BluetoothAudioOutput::GetVolume() -> uint16_t {
- return volume_;
-}
-
-auto BluetoothAudioOutput::GetVolumePct() -> uint_fast8_t {
- return static_cast<uint_fast8_t>(round(static_cast<int>(volume_)));
-}
-
-auto BluetoothAudioOutput::SetVolumePct(uint_fast8_t val) -> bool {
- if (val > 100) {
- return false;
- }
- SetVolume(val);
- return true;
-}
-
-auto BluetoothAudioOutput::GetVolumeDb() -> int_fast16_t {
- double pct = GetVolumePct() / 100.0;
- if (pct <= 0) {
- pct = 0.01;
- }
- int_fast16_t db = log(pct) * 20;
- return db;
-}
-
-auto BluetoothAudioOutput::SetVolumeDb(int_fast16_t val) -> bool {
- double pct = exp(val / 20.0) * 100;
- return SetVolumePct(pct);
-}
-
-auto BluetoothAudioOutput::AdjustVolumeUp() -> bool {
- if (volume_ == 100 || !bluetooth_.IsConnected()) {
- return false;
- }
- volume_++;
- SetVolume(volume_);
- return true;
-}
-
-auto BluetoothAudioOutput::AdjustVolumeDown() -> bool {
- if (volume_ == 0 || !bluetooth_.IsConnected()) {
- return false;
- }
- volume_--;
- SetVolume(volume_);
- return true;
-}
-
-auto BluetoothAudioOutput::PrepareFormat(const Format& orig) -> Format {
- // ESP-IDF's current Bluetooth implementation currently handles SBC encoding,
- // but requires a fixed input format.
- return Format{
- .sample_rate = 48000,
- .num_channels = 2,
- .bits_per_sample = 16,
- };
-}
-
-auto BluetoothAudioOutput::Configure(const Format& fmt) -> void {
- // No configuration necessary; the output format is fixed.
-}
-
-} // namespace audio
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
deleted file mode 100644
index e5fb3b21..00000000
--- a/src/audio/fatfs_audio_input.cpp
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "fatfs_audio_input.hpp"
-
-#include <algorithm>
-#include <climits>
-#include <cstddef>
-#include <cstdint>
-#include <functional>
-#include <future>
-#include <memory>
-#include <mutex>
-#include <span>
-#include <string>
-#include <variant>
-
-#include "esp_heap_caps.h"
-#include "esp_log.h"
-#include "ff.h"
-#include "freertos/portmacro.h"
-#include "freertos/projdefs.h"
-#include "readahead_source.hpp"
-
-#include "audio_events.hpp"
-#include "audio_fsm.hpp"
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "event_queue.hpp"
-#include "fatfs_source.hpp"
-#include "future_fetcher.hpp"
-#include "spi.hpp"
-#include "tag_parser.hpp"
-#include "tasks.hpp"
-#include "track.hpp"
-#include "types.hpp"
-
-[[maybe_unused]] static const char* kTag = "SRC";
-
-namespace audio {
-
-FatfsAudioInput::FatfsAudioInput(database::ITagParser& tag_parser,
- tasks::WorkerPool& bg_worker)
- : IAudioSource(),
- tag_parser_(tag_parser),
- bg_worker_(bg_worker),
- new_stream_mutex_(),
- new_stream_(),
- has_new_stream_(false) {}
-
-FatfsAudioInput::~FatfsAudioInput() {}
-
-auto FatfsAudioInput::SetPath(std::optional<std::string> path) -> void {
- if (path) {
- SetPath(*path);
- } else {
- SetPath();
- }
-}
-
-auto FatfsAudioInput::SetPath(const std::string& path, uint32_t offset)
- -> void {
- std::lock_guard<std::mutex> guard{new_stream_mutex_};
- if (OpenFile(path, offset)) {
- has_new_stream_ = true;
- has_new_stream_.notify_one();
- }
-}
-
-auto FatfsAudioInput::SetPath() -> void {
- std::lock_guard<std::mutex> guard{new_stream_mutex_};
- new_stream_.reset();
- has_new_stream_ = true;
- has_new_stream_.notify_one();
-}
-
-auto FatfsAudioInput::HasNewStream() -> bool {
- return has_new_stream_;
-}
-
-auto FatfsAudioInput::NextStream() -> std::shared_ptr<TaggedStream> {
- while (true) {
- has_new_stream_.wait(false);
-
- {
- std::lock_guard<std::mutex> guard{new_stream_mutex_};
- if (!has_new_stream_.exchange(false)) {
- // If the new stream went away, then we need to go back to waiting.
- continue;
- }
-
- if (new_stream_ == nullptr) {
- continue;
- }
-
- auto stream = new_stream_;
- new_stream_ = nullptr;
- return stream;
- }
- }
-}
-
-auto FatfsAudioInput::OpenFile(const std::string& path, uint32_t offset)
- -> bool {
- ESP_LOGI(kTag, "opening file %s", path.c_str());
-
- auto tags = tag_parser_.ReadAndParseTags(path);
- if (!tags) {
- ESP_LOGE(kTag, "failed to read tags");
- return false;
- }
- if (!tags->title()) {
- tags->title(path);
- }
-
- auto stream_type = ContainerToStreamType(tags->encoding());
- if (!stream_type.has_value()) {
- ESP_LOGE(kTag, "couldn't match container to stream");
- return false;
- }
-
- std::unique_ptr<FIL> file = std::make_unique<FIL>();
- FRESULT res;
-
- {
- auto lock = drivers::acquire_spi();
- res = f_open(file.get(), path.c_str(), FA_READ);
- }
-
- if (res != FR_OK) {
- ESP_LOGE(kTag, "failed to open file! res: %i", res);
- return false;
- }
-
- auto source =
- std::make_unique<FatfsSource>(stream_type.value(), std::move(file));
- new_stream_.reset(new TaggedStream(tags, std::move(source), path, offset));
- return true;
-}
-
-auto FatfsAudioInput::ContainerToStreamType(database::Container enc)
- -> std::optional<codecs::StreamType> {
- switch (enc) {
- case database::Container::kMp3:
- return codecs::StreamType::kMp3;
- case database::Container::kWav:
- return codecs::StreamType::kWav;
- case database::Container::kOgg:
- return codecs::StreamType::kVorbis;
- case database::Container::kFlac:
- return codecs::StreamType::kFlac;
- case database::Container::kOpus:
- return codecs::StreamType::kOpus;
- case database::Container::kUnsupported:
- default:
- return {};
- }
-}
-
-} // namespace audio
diff --git a/src/audio/fatfs_source.cpp b/src/audio/fatfs_source.cpp
deleted file mode 100644
index dccdd581..00000000
--- a/src/audio/fatfs_source.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "fatfs_source.hpp"
-#include <sys/_stdint.h>
-
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-
-#include "esp_log.h"
-#include "event_queue.hpp"
-#include "ff.h"
-
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "spi.hpp"
-#include "system_events.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-[[maybe_unused]] static constexpr char kTag[] = "fatfs_src";
-
-FatfsSource::FatfsSource(codecs::StreamType t, std::unique_ptr<FIL> file)
- : IStream(t), file_(std::move(file)) {}
-
-FatfsSource::~FatfsSource() {
- auto lock = drivers::acquire_spi();
- f_close(file_.get());
-}
-
-auto FatfsSource::Read(std::span<std::byte> dest) -> ssize_t {
- auto lock = drivers::acquire_spi();
- if (f_eof(file_.get())) {
- return 0;
- }
- UINT bytes_read = 0;
- FRESULT res = f_read(file_.get(), dest.data(), dest.size(), &bytes_read);
- if (res != FR_OK) {
- events::System().Dispatch(system_fsm::StorageError{.error = res});
- return -1;
- }
- return bytes_read;
-}
-
-auto FatfsSource::CanSeek() -> bool {
- return true;
-}
-
-auto FatfsSource::SeekTo(int64_t destination, SeekFrom from) -> void {
- auto lock = drivers::acquire_spi();
- switch (from) {
- case SeekFrom::kStartOfStream:
- f_lseek(file_.get(), destination);
- break;
- case SeekFrom::kEndOfStream:
- f_lseek(file_.get(), f_size(file_.get()) + destination);
- break;
- case SeekFrom::kCurrentPosition:
- f_lseek(file_.get(), f_tell(file_.get()) + destination);
- break;
- }
-}
-
-auto FatfsSource::CurrentPosition() -> int64_t {
- return f_tell(file_.get());
-}
-
-auto FatfsSource::Size() -> std::optional<int64_t> {
- return f_size(file_.get());
-}
-
-} // namespace audio
diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp
deleted file mode 100644
index bf1c3e5e..00000000
--- a/src/audio/i2s_audio_output.cpp
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "i2s_audio_output.hpp"
-#include <stdint.h>
-
-#include <algorithm>
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-#include <variant>
-
-#include "esp_err.h"
-#include "esp_heap_caps.h"
-#include "freertos/portmacro.h"
-#include "freertos/projdefs.h"
-
-#include "audio_sink.hpp"
-#include "gpios.hpp"
-#include "i2c.hpp"
-#include "i2s_dac.hpp"
-#include "result.hpp"
-#include "wm8523.hpp"
-
-[[maybe_unused]] static const char* kTag = "I2SOUT";
-
-namespace audio {
-
-// Consumer line level = 0.316 VRMS = -10db = 61
-// Professional line level = 1.228 VRMS = +4dB = 111
-// Cliipping level = 2.44 VRMS = 133?
-// all into 650 ohms
-
-static constexpr uint16_t kMaxVolume = 0x1ff;
-static constexpr uint16_t kMinVolume = 0b0;
-static constexpr uint16_t kMaxVolumeBeforeClipping = 0x185;
-static constexpr uint16_t kLineLevelVolume = 0x13d;
-static constexpr uint16_t kDefaultVolume = 0x100;
-
-static constexpr size_t kDrainBufferSize = 8 * 1024;
-
-I2SAudioOutput::I2SAudioOutput(StreamBufferHandle_t s,
- drivers::IGpios& expander)
- : IAudioOutput(s),
- expander_(expander),
- dac_(),
- current_mode_(Modes::kOff),
- current_config_(),
- left_difference_(0),
- current_volume_(kDefaultVolume),
- max_volume_(0) {}
-
-I2SAudioOutput::~I2SAudioOutput() {
- dac_->Stop();
- dac_->SetSource(nullptr);
-}
-
-auto I2SAudioOutput::changeMode(Modes mode) -> void {
- if (mode == current_mode_) {
- return;
- }
- if (mode == Modes::kOff) {
- if (dac_) {
- dac_->Stop();
- dac_.reset();
- }
- return;
- }
- if (current_mode_ == Modes::kOff) {
- if (!dac_) {
- auto instance = drivers::I2SDac::create(expander_);
- if (!instance) {
- return;
- }
- dac_.reset(*instance);
- }
- SetVolume(GetVolume());
- dac_->SetSource(stream());
- dac_->Start();
- }
- current_mode_ = mode;
- dac_->SetPaused(mode == Modes::kOnPaused);
-}
-
-auto I2SAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void {
- left_difference_ = balance;
- SetVolume(GetVolume());
-}
-
-auto I2SAudioOutput::SetMaxVolume(uint16_t max) -> void {
- max_volume_ = std::clamp(max, drivers::wm8523::kAbsoluteMinVolume,
- drivers::wm8523::kAbsoluteMaxVolume);
- SetVolume(GetVolume());
-}
-
-auto I2SAudioOutput::SetVolume(uint16_t vol) -> void {
- current_volume_ = std::clamp(vol, kMinVolume, max_volume_);
-
- int32_t left_unclamped = current_volume_ + left_difference_;
- uint16_t left = std::clamp<int32_t>(left_unclamped, kMinVolume, max_volume_);
-
- using drivers::wm8523::Register;
- drivers::wm8523::WriteRegister(Register::kDacGainLeft, left);
- drivers::wm8523::WriteRegister(Register::kDacGainRight,
- current_volume_ | 0x200);
-}
-
-auto I2SAudioOutput::GetVolume() -> uint16_t {
- return current_volume_;
-}
-
-auto I2SAudioOutput::GetVolumePct() -> uint_fast8_t {
- return (current_volume_ - kMinVolume) * 100 / (max_volume_ - kMinVolume);
-}
-
-auto I2SAudioOutput::SetVolumePct(uint_fast8_t val) -> bool {
- if (val > 100) {
- return false;
- }
- uint16_t vol = (val * (max_volume_ - kMinVolume))/100 + kMinVolume;
- SetVolume(vol);
- return true;
-}
-
-auto I2SAudioOutput::GetVolumeDb() -> int_fast16_t {
- // Add two before dividing in order to round correctly.
- return (static_cast<int>(current_volume_) -
- static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume) + 2) /
- 4;
-}
-
-auto I2SAudioOutput::SetVolumeDb(int_fast16_t val) -> bool {
- SetVolume(val * 4 + static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume) - 2);
- return true;
-}
-
-auto I2SAudioOutput::AdjustVolumeUp() -> bool {
- if (GetVolume() >= max_volume_) {
- return false;
- }
- SetVolume(GetVolume() + 1);
- return true;
-}
-
-auto I2SAudioOutput::AdjustVolumeDown() -> bool {
- if (GetVolume() == kMinVolume) {
- return false;
- }
- if (GetVolume() <= kMinVolume + 1) {
- SetVolume(0);
- } else {
- SetVolume(GetVolume() - 1);
- }
- return true;
-}
-
-auto I2SAudioOutput::PrepareFormat(const Format& orig) -> Format {
- return Format{
- .sample_rate = std::clamp<uint32_t>(orig.sample_rate, 8000, 96000),
- .num_channels = std::min<uint8_t>(orig.num_channels, 2),
- .bits_per_sample = std::clamp<uint8_t>(orig.bits_per_sample, 16, 32),
- };
-}
-
-auto I2SAudioOutput::Configure(const Format& fmt) -> void {
- if (current_config_ && fmt == *current_config_) {
- ESP_LOGI(kTag, "ignoring unchanged format");
- return;
- }
-
- drivers::I2SDac::Channels ch;
- switch (fmt.num_channels) {
- case 1:
- ch = drivers::I2SDac::CHANNELS_MONO;
- break;
- case 2:
- ch = drivers::I2SDac::CHANNELS_STEREO;
- break;
- default:
- ESP_LOGE(kTag, "dropping stream with out of bounds channels");
- return;
- }
-
- drivers::I2SDac::BitsPerSample bps;
- switch (fmt.bits_per_sample) {
- case 16:
- bps = drivers::I2SDac::BPS_16;
- break;
- case 24:
- bps = drivers::I2SDac::BPS_24;
- break;
- case 32:
- bps = drivers::I2SDac::BPS_32;
- break;
- default:
- ESP_LOGE(kTag, "dropping stream with unknown bps");
- return;
- }
-
- drivers::I2SDac::SampleRate sample_rate;
- switch (fmt.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;
- }
-
- dac_->Reconfigure(ch, bps, sample_rate);
- current_config_ = fmt;
-}
-
-} // namespace audio
diff --git a/src/audio/include/audio_converter.hpp b/src/audio/include/audio_converter.hpp
deleted file mode 100644
index 163c6836..00000000
--- a/src/audio/include/audio_converter.hpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <cstdint>
-#include <memory>
-
-#include "audio_events.hpp"
-#include "audio_sink.hpp"
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "resample.hpp"
-#include "sample.hpp"
-
-namespace audio {
-
-/*
- * Handle to a persistent task that converts samples between formats (sample
- * rate, channels, bits per sample), in order to put samples in the preferred
- * format of the current output device. The resulting samples are forwarded
- * to the output device's sink stream.
- */
-class SampleConverter {
- public:
- SampleConverter();
- ~SampleConverter();
-
- auto SetOutput(std::shared_ptr<IAudioOutput>) -> void;
-
- auto beginStream(std::shared_ptr<TrackInfo>) -> void;
- auto continueStream(std::span<sample::Sample>) -> void;
- auto endStream() -> void;
-
- private:
- auto Main() -> void;
-
- auto handleBeginStream(std::shared_ptr<TrackInfo>) -> void;
- auto handleContinueStream(size_t samples_available) -> void;
- auto handleEndStream() -> void;
-
- auto handleSamples(std::span<sample::Sample>) -> size_t;
-
- auto sendToSink(std::span<sample::Sample>) -> void;
-
- struct Args {
- std::shared_ptr<TrackInfo>* track;
- size_t samples_available;
- bool is_end_of_stream;
- };
- QueueHandle_t commands_;
-
- std::unique_ptr<Resampler> resampler_;
-
- StreamBufferHandle_t source_;
- std::span<sample::Sample> input_buffer_;
- std::span<std::byte> input_buffer_as_bytes_;
-
- std::span<sample::Sample> resampled_buffer_;
-
- std::shared_ptr<IAudioOutput> sink_;
- IAudioOutput::Format source_format_;
- IAudioOutput::Format target_format_;
- size_t leftover_bytes_;
-
- uint32_t samples_sunk_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp
deleted file mode 100644
index 8e955f74..00000000
--- a/src/audio/include/audio_decoder.hpp
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <cstdint>
-#include <memory>
-
-#include "audio_converter.hpp"
-#include "audio_events.hpp"
-#include "audio_sink.hpp"
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "track.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-/*
- * Handle to a persistent task that takes bytes from the given source, decodes
- * them into sample::Sample (normalised to 16 bit signed PCM), and then
- * forwards the resulting stream to the given converter.
- */
-class Decoder {
- public:
- static auto Start(std::shared_ptr<IAudioSource> source,
- std::shared_ptr<SampleConverter> converter) -> Decoder*;
-
- auto Main() -> void;
-
- Decoder(const Decoder&) = delete;
- Decoder& operator=(const Decoder&) = delete;
-
- private:
- Decoder(std::shared_ptr<IAudioSource> source,
- std::shared_ptr<SampleConverter> converter);
-
- auto BeginDecoding(std::shared_ptr<TaggedStream>) -> bool;
- auto ContinueDecoding() -> bool;
-
- std::shared_ptr<IAudioSource> source_;
- std::shared_ptr<SampleConverter> converter_;
-
- std::shared_ptr<codecs::IStream> stream_;
- std::unique_ptr<codecs::ICodec> codec_;
-
- std::optional<codecs::ICodec::OutputFormat> current_format_;
- std::optional<IAudioOutput::Format> current_sink_format_;
-
- std::span<sample::Sample> codec_buffer_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp
deleted file mode 100644
index b8a0dba6..00000000
--- a/src/audio/include/audio_events.hpp
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <cstdint>
-#include <memory>
-#include <optional>
-#include <string>
-
-#include "audio_sink.hpp"
-#include "tinyfsm.hpp"
-
-#include "track.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-/*
- * Struct encapsulating information about the decoder's current track.
- */
-struct TrackInfo {
- /*
- * Audio tags extracted from the file. May be absent for files without any
- * parseable tags.
- */
- std::shared_ptr<database::TrackTags> tags;
-
- /*
- * URI that the current track was retrieved from. This is currently always a
- * file path on the SD card.
- */
- std::string uri;
-
- /*
- * The length of this track in seconds. This is either retrieved from the
- * track's tags, or sometimes computed. It may therefore sometimes be
- * inaccurate or missing.
- */
- std::optional<uint32_t> duration;
-
- /* The offset in seconds that this file's decoding started from. */
- std::optional<uint32_t> start_offset;
-
- /* The approximate bitrate of this track in its original encoded form. */
- std::optional<uint32_t> bitrate_kbps;
-
- /* The encoded format of the this track. */
- codecs::StreamType encoding;
-
- IAudioOutput::Format format;
-};
-
-/*
- * Event emitted by the audio FSM when the state of the audio pipeline has
- * changed. This is usually once per second while a track is playing, plus one
- * event each when a track starts or finishes.
- */
-struct PlaybackUpdate : tinyfsm::Event {
- /*
- * The track that is currently being decoded by the audio pipeline. May be
- * absent if there is no current track.
- */
- std::shared_ptr<TrackInfo> current_track;
-
- /*
- * How long the current track has been playing for, in seconds. Will always
- * be present if current_track is present.
- */
- std::optional<uint32_t> track_position;
-
- /* Whether or not the current track is currently being output to a sink. */
- bool paused;
-};
-
-/*
- * Sets a new track to be decoded by the audio pipeline, replacing any
- * currently playing track.
- */
-struct SetTrack : tinyfsm::Event {
- std::variant<std::string, database::TrackId, std::monostate> new_track;
- std::optional<uint32_t> seek_to_second;
-
- enum Transition {
- kHardCut,
- kGapless,
- // TODO: kCrossFade
- };
- Transition transition;
-};
-
-struct TogglePlayPause : tinyfsm::Event {
- std::optional<bool> set_to;
-};
-
-struct QueueUpdate : tinyfsm::Event {
- bool current_changed;
-
- enum Reason {
- kExplicitUpdate,
- kRepeatingLastTrack,
- kTrackFinished,
- kDeserialised,
- };
- Reason reason;
-};
-
-struct StepUpVolume : tinyfsm::Event {};
-struct StepDownVolume : tinyfsm::Event {};
-struct SetVolume : tinyfsm::Event {
- std::optional<uint_fast8_t> percent;
- std::optional<int32_t> db;
-};
-struct SetVolumeBalance : tinyfsm::Event {
- int left_bias;
-};
-
-struct VolumeChanged : tinyfsm::Event {
- uint_fast8_t percent;
- int db;
-};
-struct VolumeBalanceChanged : tinyfsm::Event {
- int left_bias;
-};
-struct VolumeLimitChanged : tinyfsm::Event {
- int new_limit_db;
-};
-
-struct SetVolumeLimit : tinyfsm::Event {
- int limit_db;
-};
-
-struct OutputModeChanged : tinyfsm::Event {};
-
-namespace internal {
-
-struct StreamStarted : tinyfsm::Event {
- std::shared_ptr<TrackInfo> track;
- IAudioOutput::Format src_format;
- IAudioOutput::Format dst_format;
-};
-
-struct StreamUpdate : tinyfsm::Event {
- uint32_t samples_sunk;
-};
-
-struct StreamEnded : tinyfsm::Event {};
-
-} // namespace internal
-
-} // namespace audio
diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp
deleted file mode 100644
index 60afb321..00000000
--- a/src/audio/include/audio_fsm.hpp
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <deque>
-#include <memory>
-#include <vector>
-
-#include "audio_sink.hpp"
-#include "service_locator.hpp"
-#include "tinyfsm.hpp"
-
-#include "audio_decoder.hpp"
-#include "audio_events.hpp"
-#include "bt_audio_output.hpp"
-#include "database.hpp"
-#include "display.hpp"
-#include "fatfs_audio_input.hpp"
-#include "gpios.hpp"
-#include "i2s_audio_output.hpp"
-#include "i2s_dac.hpp"
-#include "storage.hpp"
-#include "system_events.hpp"
-#include "tag_parser.hpp"
-#include "track.hpp"
-#include "track_queue.hpp"
-
-namespace audio {
-
-class AudioState : public tinyfsm::Fsm<AudioState> {
- public:
- virtual ~AudioState() {}
-
- virtual void entry() {}
- virtual void exit() {}
-
- /* Fallback event handler. Does nothing. */
- void react(const tinyfsm::Event& ev) {}
-
- void react(const QueueUpdate&);
- void react(const SetTrack&);
- void react(const TogglePlayPause&);
-
- void react(const internal::StreamStarted&);
- void react(const internal::StreamUpdate&);
- void react(const internal::StreamEnded&);
-
- void react(const StepUpVolume&);
- void react(const StepDownVolume&);
- virtual void react(const system_fsm::HasPhonesChanged&);
-
- void react(const SetVolume&);
- void react(const SetVolumeLimit&);
- void react(const SetVolumeBalance&);
-
- void react(const OutputModeChanged&);
-
- virtual void react(const system_fsm::BootComplete&) {}
- virtual void react(const system_fsm::KeyLockChanged&){};
- virtual void react(const system_fsm::StorageMounted&) {}
- virtual void react(const system_fsm::BluetoothEvent&);
-
- protected:
- auto clearDrainBuffer() -> void;
- auto awaitEmptyDrainBuffer() -> void;
-
- auto playTrack(database::TrackId id) -> void;
- auto commitVolume() -> void;
-
- static std::shared_ptr<system_fsm::ServiceLocator> sServices;
-
- static std::shared_ptr<FatfsAudioInput> sFileSource;
- static std::unique_ptr<Decoder> sDecoder;
- static std::shared_ptr<SampleConverter> sSampleConverter;
- static std::shared_ptr<I2SAudioOutput> sI2SOutput;
- static std::shared_ptr<BluetoothAudioOutput> sBtOutput;
- static std::shared_ptr<IAudioOutput> sOutput;
-
- static StreamBufferHandle_t sDrainBuffer;
-
- static std::shared_ptr<TrackInfo> sCurrentTrack;
- static uint64_t sCurrentSamples;
- static std::optional<IAudioOutput::Format> sDrainFormat;
- static bool sCurrentTrackIsFromQueue;
-
- static std::shared_ptr<TrackInfo> sNextTrack;
- static uint64_t sNextTrackCueSamples;
- static bool sNextTrackIsFromQueue;
-
- static bool sIsResampling;
- static bool sIsPaused;
-
- auto currentPositionSeconds() -> std::optional<uint32_t>;
-};
-
-namespace states {
-
-class Uninitialised : public AudioState {
- public:
- void react(const system_fsm::BootComplete&) override;
- void react(const system_fsm::BluetoothEvent&) override{};
-
- using AudioState::react;
-};
-
-class Standby : public AudioState {
- public:
- void react(const system_fsm::KeyLockChanged&) override;
- void react(const system_fsm::StorageMounted&) override;
-
- using AudioState::react;
-};
-
-class Playback : public AudioState {
- public:
- void entry() override;
- void exit() override;
-
- using AudioState::react;
-};
-
-} // namespace states
-
-} // namespace audio
diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp
deleted file mode 100644
index f31d0d75..00000000
--- a/src/audio/include/audio_sink.hpp
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <cstdint>
-
-#include "esp_heap_caps.h"
-#include "freertos/FreeRTOS.h"
-
-namespace audio {
-
-/*
- * Interface for classes that use PCM samples to create noises for the user.
- *
- * These classes do not generally have any specific task for their work, and
- * simply help to mediate working out the correct PCM format, and then sending
- * those samples to the appropriate hardware driver.
- */
-class IAudioOutput {
- private:
- StreamBufferHandle_t stream_;
-
- public:
- IAudioOutput(StreamBufferHandle_t stream)
- : stream_(stream), mode_(Modes::kOff) {}
-
- virtual ~IAudioOutput() {}
-
- enum class Modes {
- kOff,
- kOnPaused,
- kOnPlaying,
- };
-
- /*
- * Indicates whether this output is currently being sent samples. If this is
- * false, the output should place itself into a low power state.
- */
- auto mode(Modes m) -> void {
- if (mode_ == m) {
- return;
- }
- changeMode(m);
- mode_ = m;
- }
- auto mode() -> Modes { return mode_; }
-
- virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0;
-
- virtual auto SetVolume(uint16_t) -> void = 0;
-
- virtual auto GetVolume() -> uint16_t = 0;
-
- virtual auto GetVolumePct() -> uint_fast8_t = 0;
- virtual auto GetVolumeDb() -> int_fast16_t = 0;
-
- virtual auto SetVolumePct(uint_fast8_t) -> bool = 0;
- virtual auto SetVolumeDb(int_fast16_t) -> bool = 0;
-
- virtual auto AdjustVolumeUp() -> bool = 0;
- virtual auto AdjustVolumeDown() -> bool = 0;
-
- struct Format {
- uint32_t sample_rate;
- uint_fast8_t num_channels;
- uint_fast8_t bits_per_sample;
-
- bool operator==(const Format&) const = default;
- };
-
- virtual auto PrepareFormat(const Format&) -> Format = 0;
- virtual auto Configure(const Format& format) -> void = 0;
-
- auto stream() -> StreamBufferHandle_t { return stream_; }
-
- protected:
- Modes mode_;
-
- virtual auto changeMode(Modes new_mode) -> void = 0;
-};
-
-} // namespace audio
diff --git a/src/audio/include/audio_source.hpp b/src/audio/include/audio_source.hpp
deleted file mode 100644
index f6a34300..00000000
--- a/src/audio/include/audio_source.hpp
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <memory>
-#include "codec.hpp"
-#include "track.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-class TaggedStream : public codecs::IStream {
- public:
- TaggedStream(std::shared_ptr<database::TrackTags>,
- std::unique_ptr<codecs::IStream> wrapped,
- std::string path,
- uint32_t offset = 0
- );
-
- auto tags() -> std::shared_ptr<database::TrackTags>;
-
- auto Read(std::span<std::byte> dest) -> ssize_t override;
-
- auto CanSeek() -> bool override;
-
- auto SeekTo(int64_t destination, SeekFrom from) -> void override;
-
- auto CurrentPosition() -> int64_t override;
-
- auto Size() -> std::optional<int64_t> override;
-
- auto Offset() -> uint32_t;
-
- auto Filepath() -> std::string;
-
- auto SetPreambleFinished() -> void override;
-
- private:
- std::shared_ptr<database::TrackTags> tags_;
- std::unique_ptr<codecs::IStream> wrapped_;
- std::string filepath_;
- int32_t offset_;
-};
-
-class IAudioSource {
- public:
- virtual ~IAudioSource() {}
-
- virtual auto HasNewStream() -> bool = 0;
- virtual auto NextStream() -> std::shared_ptr<TaggedStream> = 0;
-};
-
-} // namespace audio
diff --git a/src/audio/include/bt_audio_output.hpp b/src/audio/include/bt_audio_output.hpp
deleted file mode 100644
index cc3b2462..00000000
--- a/src/audio/include/bt_audio_output.hpp
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <cstdint>
-#include <memory>
-#include <vector>
-
-#include "result.hpp"
-
-#include "audio_sink.hpp"
-#include "bluetooth.hpp"
-#include "gpios.hpp"
-#include "i2s_dac.hpp"
-#include "tasks.hpp"
-
-namespace audio {
-
-class BluetoothAudioOutput : public IAudioOutput {
- public:
- BluetoothAudioOutput(StreamBufferHandle_t,
- drivers::Bluetooth& bt,
- tasks::WorkerPool&);
- ~BluetoothAudioOutput();
-
- auto SetVolumeImbalance(int_fast8_t balance) -> void override;
-
- auto SetVolume(uint16_t) -> void override;
-
- auto GetVolume() -> uint16_t override;
-
- auto GetVolumePct() -> uint_fast8_t override;
- auto SetVolumePct(uint_fast8_t val) -> bool override;
- auto GetVolumeDb() -> int_fast16_t override;
- auto SetVolumeDb(int_fast16_t) -> bool override;
-
- auto AdjustVolumeUp() -> bool override;
- auto AdjustVolumeDown() -> bool override;
-
- auto PrepareFormat(const Format&) -> Format override;
- auto Configure(const Format& format) -> void override;
-
- BluetoothAudioOutput(const BluetoothAudioOutput&) = delete;
- BluetoothAudioOutput& operator=(const BluetoothAudioOutput&) = delete;
-
- protected:
- auto changeMode(Modes) -> void override;
-
- private:
- drivers::Bluetooth& bluetooth_;
- tasks::WorkerPool& bg_worker_;
-
- uint16_t volume_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp
deleted file mode 100644
index 10b7433e..00000000
--- a/src/audio/include/fatfs_audio_input.hpp
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <future>
-#include <memory>
-#include <string>
-
-#include "ff.h"
-#include "freertos/portmacro.h"
-
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "future_fetcher.hpp"
-#include "tag_parser.hpp"
-#include "tasks.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-/*
- * Audio source that fetches data from a FatFs (or exfat i guess) filesystem.
- *
- * All public methods are safe to call from any task.
- */
-class FatfsAudioInput : public IAudioSource {
- public:
- explicit FatfsAudioInput(database::ITagParser&, tasks::WorkerPool&);
- ~FatfsAudioInput();
-
- /*
- * Immediately cease reading any current source, and begin reading from the
- * given file path.
- */
- auto SetPath(std::optional<std::string>) -> void;
- auto SetPath(const std::string&,uint32_t offset = 0) -> void;
- auto SetPath() -> void;
-
- auto HasNewStream() -> bool override;
- auto NextStream() -> std::shared_ptr<TaggedStream> override;
-
- FatfsAudioInput(const FatfsAudioInput&) = delete;
- FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
-
- private:
- auto OpenFile(const std::string& path,uint32_t offset) -> bool;
-
- auto ContainerToStreamType(database::Container)
- -> std::optional<codecs::StreamType>;
-
- database::ITagParser& tag_parser_;
- tasks::WorkerPool& bg_worker_;
-
- std::mutex new_stream_mutex_;
- std::shared_ptr<TaggedStream> new_stream_;
-
- std::atomic<bool> has_new_stream_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/fatfs_source.hpp b/src/audio/include/fatfs_source.hpp
deleted file mode 100644
index ce9b4db8..00000000
--- a/src/audio/include/fatfs_source.hpp
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-
-#include "codec.hpp"
-#include "ff.h"
-
-#include "audio_source.hpp"
-
-namespace audio {
-
-/*
- * Handles coordination with a persistent background task to asynchronously
- * read files from disk into a StreamBuffer.
- */
-class FatfsSource : public codecs::IStream {
- public:
- FatfsSource(codecs::StreamType, std::unique_ptr<FIL> file);
- ~FatfsSource();
-
- auto Read(std::span<std::byte> dest) -> ssize_t override;
-
- auto CanSeek() -> bool override;
-
- auto SeekTo(int64_t destination, SeekFrom from) -> void override;
-
- auto CurrentPosition() -> int64_t override;
-
- auto Size() -> std::optional<int64_t> override;
-
- FatfsSource(const FatfsSource&) = delete;
- FatfsSource& operator=(const FatfsSource&) = delete;
-
- private:
- std::unique_ptr<FIL> file_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp
deleted file mode 100644
index 7954257a..00000000
--- a/src/audio/include/i2s_audio_output.hpp
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <stdint.h>
-#include <cstdint>
-#include <memory>
-#include <vector>
-
-#include "audio_sink.hpp"
-#include "gpios.hpp"
-#include "i2s_dac.hpp"
-#include "result.hpp"
-
-namespace audio {
-
-class I2SAudioOutput : public IAudioOutput {
- public:
- I2SAudioOutput(StreamBufferHandle_t, drivers::IGpios& expander);
- ~I2SAudioOutput();
-
- auto SetMaxVolume(uint16_t) -> void;
- auto SetVolumeDb(uint16_t) -> void;
-
- auto SetVolumeImbalance(int_fast8_t balance) -> void override;
-
- auto SetVolume(uint16_t) -> void override;
-
- auto GetVolume() -> uint16_t override;
-
- auto GetVolumePct() -> uint_fast8_t override;
- auto SetVolumePct(uint_fast8_t val) -> bool override;
- auto GetVolumeDb() -> int_fast16_t override;
- auto SetVolumeDb(int_fast16_t) -> bool override;
-
- auto AdjustVolumeUp() -> bool override;
- auto AdjustVolumeDown() -> bool override;
-
- auto PrepareFormat(const Format&) -> Format override;
- auto Configure(const Format& format) -> void override;
-
- I2SAudioOutput(const I2SAudioOutput&) = delete;
- I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
-
- protected:
- auto changeMode(Modes) -> void override;
-
- private:
- drivers::IGpios& expander_;
- std::unique_ptr<drivers::I2SDac> dac_;
-
- Modes current_mode_;
- std::optional<Format> current_config_;
- int_fast8_t left_difference_;
- uint16_t current_volume_;
- uint16_t max_volume_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/readahead_source.hpp b/src/audio/include/readahead_source.hpp
deleted file mode 100644
index 74a30e1b..00000000
--- a/src/audio/include/readahead_source.hpp
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-
-#include "freertos/FreeRTOS.h"
-
-#include "ff.h"
-#include "freertos/stream_buffer.h"
-
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "tasks.hpp"
-
-namespace audio {
-
-/*
- * Wraps another stream, proactively buffering large chunks of it into memory
- * at a time.
- */
-class ReadaheadSource : public codecs::IStream {
- public:
- ReadaheadSource(tasks::WorkerPool&, std::unique_ptr<codecs::IStream>);
- ~ReadaheadSource();
-
- auto Read(std::span<std::byte> dest) -> ssize_t override;
-
- auto CanSeek() -> bool override;
-
- auto SeekTo(int64_t destination, SeekFrom from) -> void override;
-
- auto CurrentPosition() -> int64_t override;
-
- auto Size() -> std::optional<int64_t> override;
-
- auto SetPreambleFinished() -> void override;
-
- ReadaheadSource(const ReadaheadSource&) = delete;
- ReadaheadSource& operator=(const ReadaheadSource&) = delete;
-
- private:
- auto BeginReadahead() -> void;
-
- tasks::WorkerPool& worker_;
- std::unique_ptr<codecs::IStream> wrapped_;
-
- bool readahead_enabled_;
- std::atomic<bool> is_refilling_;
- StreamBufferHandle_t buffer_;
- int64_t tell_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/resample.hpp b/src/audio/include/resample.hpp
deleted file mode 100644
index 4d48d47f..00000000
--- a/src/audio/include/resample.hpp
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <cstdint>
-#include <span>
-#include <vector>
-
-#include "speex/speex_resampler.h"
-
-#include "sample.hpp"
-
-namespace audio {
-
-class Resampler {
- public:
- Resampler(uint32_t source_sample_rate,
- uint32_t target_sample_rate,
- uint8_t num_channels);
-
- ~Resampler();
-
- auto Process(std::span<sample::Sample> input,
- std::span<sample::Sample> output,
- bool end_of_data) -> std::pair<size_t, size_t>;
-
- private:
- int err_;
- SpeexResamplerState* resampler_;
- uint8_t num_channels_;
-};
-
-} // namespace audio
diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp
deleted file mode 100644
index 5b7c9448..00000000
--- a/src/audio/include/track_queue.hpp
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#pragma once
-
-#include <list>
-#include <memory>
-#include <mutex>
-#include <shared_mutex>
-#include <vector>
-
-#include "audio_events.hpp"
-#include "cppbor_parse.h"
-#include "database.hpp"
-#include "tasks.hpp"
-#include "track.hpp"
-
-namespace audio {
-
-/*
- * Utility that uses a Miller shuffle to yield well-distributed random indexes
- * from within a range.
- */
-class RandomIterator {
- public:
- RandomIterator();
- RandomIterator(size_t size);
-
- auto current() const -> size_t;
-
- auto next() -> void;
- auto prev() -> void;
-
- // Note resizing has the side-effect of restarting iteration.
- auto resize(size_t) -> void;
- auto replay(bool) -> void;
-
- auto seed() -> size_t& { return seed_; }
- auto pos() -> size_t& { return pos_; }
- auto size() -> size_t& { return size_; }
-
- private:
- size_t seed_;
- size_t pos_;
- size_t size_;
- bool replay_;
-};
-
-/*
- * Owns and manages a complete view of the playback queue. Includes the
- * currently playing track, a truncated list of previously played tracks, and
- * all future tracks that have been queued.
- *
- * In order to not use all of our memory, this class deals strictly with track
- * ids. Consumers that need more data than this should fetch it from the
- * database.
- *
- * Instances of this class are broadly safe to use from multiple tasks; each
- * method represents an atomic operation. No guarantees are made about
- * consistency between calls however.
- */
-class TrackQueue {
- public:
- TrackQueue(tasks::WorkerPool& bg_worker);
-
- /* Returns the currently playing track. */
- auto current() const -> std::optional<database::TrackId>;
-
- /* Returns, in order, tracks that have been queued to be played next. */
- auto peekNext(std::size_t limit) const -> std::vector<database::TrackId>;
-
- /*
- * Returns the tracks in the queue that have already been played, ordered
- * most recently played first.
- */
- auto peekPrevious(std::size_t limit) const -> std::vector<database::TrackId>;
-
- auto currentPosition() const -> size_t;
- auto totalSize() const -> size_t;
-
- using Item = std::variant<database::TrackId, database::TrackIterator>;
- auto insert(Item, size_t index = 0) -> void;
- auto append(Item i) -> void;
-
- /*
- * Advances to the next track in the queue, placing the current track at the
- * front of the 'played' queue.
- */
- auto next() -> void;
- auto previous() -> void;
-
- /*
- * Called when the current track finishes
- */
- auto finish() -> void;
-
- auto skipTo(database::TrackId) -> void;
-
- /*
- * Removes all tracks from all queues, and stops any currently playing track.
- */
- auto clear() -> void;
-
- auto random(bool) -> void;
- auto random() const -> bool;
-
- auto repeat(bool) -> void;
- auto repeat() const -> bool;
-
- auto replay(bool) -> void;
- auto replay() const -> bool;
-
- auto serialise() -> std::string;
- auto deserialise(const std::string&) -> void;
-
- // Cannot be copied or moved.
- TrackQueue(const TrackQueue&) = delete;
- TrackQueue& operator=(const TrackQueue&) = delete;
-
- private:
- auto next(QueueUpdate::Reason r) -> void;
-
- mutable std::shared_mutex mutex_;
-
- tasks::WorkerPool& bg_worker_;
-
- size_t pos_;
- std::pmr::vector<database::TrackId> tracks_;
-
- std::optional<RandomIterator> shuffle_;
- bool repeat_;
- bool replay_;
-
- class QueueParseClient : public cppbor::ParseClient {
- public:
- QueueParseClient(TrackQueue& queue);
-
- ParseClient* item(std::unique_ptr<cppbor::Item>& item,
- const uint8_t* hdrBegin,
- const uint8_t* valueBegin,
- const uint8_t* end) override;
-
- ParseClient* itemEnd(std::unique_ptr<cppbor::Item>& item,
- const uint8_t* hdrBegin,
- const uint8_t* valueBegin,
- const uint8_t* end) override;
-
- void error(const uint8_t* position,
- const std::string& errorMessage) override {}
-
- private:
- TrackQueue& queue_;
-
- enum class State {
- kInit,
- kRoot,
- kMetadata,
- kShuffle,
- kTracks,
- kFinished,
- };
- State state_;
- size_t i_;
- };
-};
-
-} // namespace audio
diff --git a/src/audio/readahead_source.cpp b/src/audio/readahead_source.cpp
deleted file mode 100644
index 6276907a..00000000
--- a/src/audio/readahead_source.cpp
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "readahead_source.hpp"
-
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-
-#include "esp_heap_caps.h"
-#include "esp_log.h"
-#include "ff.h"
-
-#include "audio_source.hpp"
-#include "codec.hpp"
-#include "freertos/portmacro.h"
-#include "spi.hpp"
-#include "tasks.hpp"
-#include "types.hpp"
-
-namespace audio {
-
-static constexpr char kTag[] = "readahead";
-static constexpr size_t kBufferSize = 1024 * 512;
-
-ReadaheadSource::ReadaheadSource(tasks::WorkerPool& worker,
- std::unique_ptr<codecs::IStream> wrapped)
- : IStream(wrapped->type()),
- worker_(worker),
- wrapped_(std::move(wrapped)),
- readahead_enabled_(false),
- is_refilling_(false),
- buffer_(xStreamBufferCreateWithCaps(kBufferSize, 1, MALLOC_CAP_SPIRAM)),
- tell_(wrapped_->CurrentPosition()) {}
-
-ReadaheadSource::~ReadaheadSource() {
- is_refilling_.wait(true);
- vStreamBufferDeleteWithCaps(buffer_);
-}
-
-auto ReadaheadSource::Read(std::span<std::byte> dest) -> ssize_t {
- size_t bytes_written = 0;
- // Fill the destination from our buffer, until either the buffer is drained
- // or the destination is full.
- while (!dest.empty() && (is_refilling_ || !xStreamBufferIsEmpty(buffer_))) {
- size_t bytes_read =
- xStreamBufferReceive(buffer_, dest.data(), dest.size_bytes(), 1);
- tell_ += bytes_read;
- bytes_written += bytes_read;
- dest = dest.subspan(bytes_read);
- }
-
- // After the loop, we've either written everything that was asked for, or
- // we're out of data.
- if (!dest.empty()) {
- // Out of data in the buffer. Finish using the wrapped stream.
- size_t extra_bytes = wrapped_->Read(dest);
- tell_ += extra_bytes;
- bytes_written += extra_bytes;
-
- // Check for EOF in the wrapped stream.
- if (extra_bytes < dest.size_bytes()) {
- return bytes_written;
- }
- }
- // After this point, we're done writing to `dest`. It's either empty, or the
- // underlying source is EOF.
-
- // If we're here, then there is more data to be read from the wrapped stream.
- // Ensure the readahead is running.
- if (!is_refilling_ && readahead_enabled_ &&
- xStreamBufferBytesAvailable(buffer_) < kBufferSize / 4) {
- BeginReadahead();
- }
-
- return bytes_written;
-}
-
-auto ReadaheadSource::CanSeek() -> bool {
- return wrapped_->CanSeek();
-}
-
-auto ReadaheadSource::SeekTo(int64_t destination, SeekFrom from) -> void {
- // Seeking blows away all of our prefetched data. To do this safely, we
- // first need to wait for the refill task to finish.
- ESP_LOGI(kTag, "dropping readahead due to seek");
- is_refilling_.wait(true);
- // It's now safe to clear out the buffer.
- xStreamBufferReset(buffer_);
-
- wrapped_->SeekTo(destination, from);
-
- // Make sure our tell is up to date with the new location.
- tell_ = wrapped_->CurrentPosition();
-}
-
-auto ReadaheadSource::CurrentPosition() -> int64_t {
- return tell_;
-}
-
-auto ReadaheadSource::Size() -> std::optional<int64_t> {
- return wrapped_->Size();
-}
-
-auto ReadaheadSource::SetPreambleFinished() -> void {
- readahead_enabled_ = true;
- BeginReadahead();
-}
-
-auto ReadaheadSource::BeginReadahead() -> void {
- is_refilling_ = true;
- std::function<void(void)> refill = [this]() {
- // Try to keep larger than most reasonable FAT sector sizes for more
- // efficient disk reads.
- constexpr size_t kMaxSingleRead = 1024 * 16;
- std::byte working_buf[kMaxSingleRead];
- for (;;) {
- size_t bytes_to_read = std::min<size_t>(
- kMaxSingleRead, xStreamBufferSpacesAvailable(buffer_));
- if (bytes_to_read == 0) {
- break;
- }
- size_t read = wrapped_->Read({working_buf, bytes_to_read});
- if (read > 0) {
- xStreamBufferSend(buffer_, working_buf, read, 0);
- }
- if (read < bytes_to_read) {
- break;
- }
- }
- is_refilling_ = false;
- is_refilling_.notify_all();
- };
- worker_.Dispatch(refill);
-}
-
-} // namespace audio
diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp
deleted file mode 100644
index 1e20392b..00000000
--- a/src/audio/resample.cpp
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-#include "resample.hpp"
-
-#include <algorithm>
-#include <cmath>
-#include <cstdint>
-#include <cstdlib>
-#include <cstring>
-#include <numeric>
-
-#include "esp_log.h"
-#include "speex/speex_resampler.h"
-
-#include "sample.hpp"
-
-namespace audio {
-
-static constexpr int kQuality = SPEEX_RESAMPLER_QUALITY_MIN;
-
-Resampler::Resampler(uint32_t source_sample_rate,
- uint32_t target_sample_rate,
- uint8_t num_channels)
- : err_(0),
- resampler_(speex_resampler_init(num_channels,
- source_sample_rate,
- target_sample_rate,
- kQuality,
- &err_)),
- num_channels_(num_channels) {
- assert(err_ == 0);
-}
-
-Resampler::~Resampler() {
- speex_resampler_destroy(resampler_);
-}
-
-auto Resampler::Process(std::span<sample::Sample> input,
- std::span<sample::Sample> output,
- bool end_of_data) -> std::pair<size_t, size_t> {
- uint32_t samples_used = input.size() / num_channels_;
- uint32_t samples_produced = output.size() / num_channels_;
-
- int err = speex_resampler_process_interleaved_int(
- resampler_, input.data(), &samples_used, output.data(),
- &samples_produced);
- assert(err == 0);
-
- return {samples_used * num_channels_, samples_produced * num_channels_};
-}
-
-} // namespace audio
diff --git a/src/audio/test/CMakeLists.txt b/src/audio/test/CMakeLists.txt
deleted file mode 100644
index 4d580b1c..00000000
--- a/src/audio/test/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright 2023 jacqueline <me@jacqueline.id.au>
-#
-# SPDX-License-Identifier: GPL-3.0-only
-
-idf_component_register(
- SRCS ""
- INCLUDE_DIRS "." REQUIRES catch2 cmock audio)
diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp
deleted file mode 100644
index dbe283c4..00000000
--- a/src/audio/track_queue.cpp
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "track_queue.hpp"
-#include <stdint.h>
-
-#include <algorithm>
-#include <cstdint>
-#include <memory>
-#include <mutex>
-#include <optional>
-#include <shared_mutex>
-#include <variant>
-
-#include "MillerShuffle.h"
-#include "esp_random.h"
-
-#include "audio_events.hpp"
-#include "audio_fsm.hpp"
-#include "cppbor.h"
-#include "cppbor_parse.h"
-#include "database.hpp"
-#include "event_queue.hpp"
-#include "memory_resource.hpp"
-#include "tasks.hpp"
-#include "track.hpp"
-#include "ui_fsm.hpp"
-
-namespace audio {
-
-[[maybe_unused]] static constexpr char kTag[] = "tracks";
-
-using Reason = QueueUpdate::Reason;
-
-RandomIterator::RandomIterator()
- : seed_(0), pos_(0), size_(0), replay_(false) {}
-
-RandomIterator::RandomIterator(size_t size)
- : seed_(), pos_(0), size_(size), replay_(false) {
- esp_fill_random(&seed_, sizeof(seed_));
-}
-
-auto RandomIterator::current() const -> size_t {
- if (pos_ < size_ || replay_) {
- return MillerShuffle(pos_, seed_, size_);
- }
- return size_;
-}
-
-auto RandomIterator::next() -> void {
- // MillerShuffle behaves well with pos > size, returning different
- // permutations each 'cycle'. We therefore don't need to worry about wrapping
- // this value.
- pos_++;
-}
-
-auto RandomIterator::prev() -> void {
- if (pos_ > 0) {
- pos_--;
- }
-}
-
-auto RandomIterator::resize(size_t s) -> void {
- size_ = s;
- // Changing size will yield a different current position anyway, so reset pos
- // to ensure we yield a full sweep of both new and old indexes.
- pos_ = 0;
-}
-
-auto RandomIterator::replay(bool r) -> void {
- replay_ = r;
-}
-
-auto notifyChanged(bool current_changed, Reason reason) -> void {
- QueueUpdate ev{
- .current_changed = current_changed,
- .reason = reason,
- };
- events::Ui().Dispatch(ev);
- events::Audio().Dispatch(ev);
-}
-
-TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker)
- : mutex_(),
- bg_worker_(bg_worker),
- pos_(0),
- tracks_(&memory::kSpiRamResource),
- shuffle_(),
- repeat_(false),
- replay_(false) {}
-
-auto TrackQueue::current() const -> std::optional<database::TrackId> {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- if (pos_ >= tracks_.size()) {
- return {};
- }
- return tracks_[pos_];
-}
-
-auto TrackQueue::peekNext(std::size_t limit) const
- -> std::vector<database::TrackId> {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- std::vector<database::TrackId> out;
- for (size_t i = pos_ + 1; i < pos_ + limit + 1 && i < tracks_.size(); i++) {
- out.push_back(i);
- }
- return out;
-}
-
-auto TrackQueue::peekPrevious(std::size_t limit) const
- -> std::vector<database::TrackId> {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- std::vector<database::TrackId> out;
- for (size_t i = pos_ - 1; i < pos_ - limit - 1 && i >= tracks_.size(); i--) {
- out.push_back(i);
- }
- return out;
-}
-
-auto TrackQueue::currentPosition() const -> size_t {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- return pos_;
-}
-
-auto TrackQueue::totalSize() const -> size_t {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- return tracks_.size();
-}
-
-auto TrackQueue::insert(Item i, size_t index) -> void {
- bool was_queue_empty;
- bool current_changed;
- {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- was_queue_empty = pos_ == tracks_.size();
- current_changed = was_queue_empty || index == pos_;
- }
-
- auto update_shuffler = [=, this]() {
- if (shuffle_) {
- shuffle_->resize(tracks_.size());
- // If there wasn't anything already playing, then we should make sure we
- // begin playback at a random point, instead of always starting with
- // whatever was inserted first and *then* shuffling.
- // We don't base this purely off of current_changed because we would like
- // 'play this track now' (by inserting at the current pos) to work even
- // when shuffling is enabled.
- if (was_queue_empty) {
- pos_ = shuffle_->current();
- }
- }
- };
-
- if (std::holds_alternative<database::TrackId>(i)) {
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- if (index <= tracks_.size()) {
- tracks_.insert(tracks_.begin() + index, std::get<database::TrackId>(i));
- update_shuffler();
- }
- }
- notifyChanged(current_changed, Reason::kExplicitUpdate);
- } else if (std::holds_alternative<database::TrackIterator>(i)) {
- // Iterators can be very large, and retrieving items from them often
- // requires disk i/o. Handle them asynchronously so that inserting them
- // doesn't block.
- bg_worker_.Dispatch<void>([=, this]() {
- database::TrackIterator it = std::get<database::TrackIterator>(i);
- size_t working_pos = index;
- while (true) {
- auto next = *it;
- if (!next) {
- break;
- }
- // Keep this critical section small so that we're not blocking methods
- // like current().
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- if (working_pos <= tracks_.size()) {
- tracks_.insert(tracks_.begin() + working_pos, *next);
- }
- }
- working_pos++;
- it++;
- }
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- update_shuffler();
- }
- notifyChanged(current_changed, Reason::kExplicitUpdate);
- });
- }
-}
-
-auto TrackQueue::append(Item i) -> void {
- size_t end;
- {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- end = tracks_.size();
- }
- insert(i, end);
-}
-
-auto TrackQueue::next() -> void {
- next(Reason::kExplicitUpdate);
-}
-
-auto TrackQueue::next(Reason r) -> void {
- bool changed = true;
-
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- if (shuffle_) {
- shuffle_->next();
- pos_ = shuffle_->current();
- } else {
- if (pos_ + 1 >= tracks_.size()) {
- if (replay_) {
- pos_ = 0;
- } else {
- pos_ = tracks_.size();
- changed = false;
- }
- } else {
- pos_++;
- }
- }
- }
-
- notifyChanged(changed, r);
-}
-
-auto TrackQueue::previous() -> void {
- bool changed = true;
-
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- if (shuffle_) {
- shuffle_->prev();
- pos_ = shuffle_->current();
- } else {
- if (pos_ == 0) {
- if (repeat_) {
- pos_ = tracks_.size() - 1;
- } else {
- changed = false;
- }
- } else {
- pos_--;
- }
- }
- }
-
- notifyChanged(changed, Reason::kExplicitUpdate);
-}
-
-auto TrackQueue::finish() -> void {
- if (repeat_) {
- notifyChanged(true, Reason::kRepeatingLastTrack);
- } else {
- next(Reason::kTrackFinished);
- }
-}
-
-auto TrackQueue::skipTo(database::TrackId id) -> void {
- // Defer this work to the background not because it's particularly
- // long-running (although it could be), but because we want to ensure we
- // only search for the given id after any previously pending iterator
- // insertions have finished.
- bg_worker_.Dispatch<void>([=, this]() {
- bool found = false;
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- for (size_t i = 0; i < tracks_.size(); i++) {
- if (tracks_[i] == id) {
- pos_ = i;
- found = true;
- break;
- }
- }
- }
- if (found) {
- notifyChanged(true, Reason::kExplicitUpdate);
- }
- });
-}
-
-auto TrackQueue::clear() -> void {
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- if (tracks_.empty()) {
- return;
- }
-
- pos_ = 0;
- tracks_.clear();
-
- if (shuffle_) {
- shuffle_->resize(0);
- }
- }
-
- notifyChanged(true, Reason::kExplicitUpdate);
-}
-
-auto TrackQueue::random(bool en) -> void {
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- // Don't check for en == true already; this has the side effect that
- // repeated calls with en == true will re-shuffle.
- if (en) {
- shuffle_.emplace(tracks_.size());
- shuffle_->replay(replay_);
- } else {
- shuffle_.reset();
- }
- }
-
- // Current track doesn't get randomised until next().
- notifyChanged(false, Reason::kExplicitUpdate);
-}
-
-auto TrackQueue::random() const -> bool {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- return shuffle_.has_value();
-}
-
-auto TrackQueue::repeat(bool en) -> void {
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- repeat_ = en;
- }
-
- notifyChanged(false, Reason::kExplicitUpdate);
-}
-
-auto TrackQueue::repeat() const -> bool {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- return repeat_;
-}
-
-auto TrackQueue::replay(bool en) -> void {
- {
- const std::unique_lock<std::shared_mutex> lock(mutex_);
- replay_ = en;
- if (shuffle_) {
- shuffle_->replay(en);
- }
- }
- notifyChanged(false, Reason::kExplicitUpdate);
-}
-
-auto TrackQueue::replay() const -> bool {
- const std::shared_lock<std::shared_mutex> lock(mutex_);
- return replay_;
-}
-
-auto TrackQueue::serialise() -> std::string {
- cppbor::Array tracks{};
- for (database::TrackId track : tracks_) {
- tracks.add(cppbor::Uint(track));
- }
- cppbor::Map encoded;
- encoded.add(cppbor::Uint{0}, cppbor::Array{
- cppbor::Uint{pos_},
- cppbor::Bool{repeat_},
- cppbor::Bool{replay_},
- });
- if (shuffle_) {
- encoded.add(cppbor::Uint{1}, cppbor::Array{
- cppbor::Uint{shuffle_->size()},
- cppbor::Uint{shuffle_->seed()},
- cppbor::Uint{shuffle_->pos()},
- });
- }
- encoded.add(cppbor::Uint{2}, std::move(tracks));
- return encoded.toString();
-}
-
-TrackQueue::QueueParseClient::QueueParseClient(TrackQueue& queue)
- : queue_(queue), state_(State::kInit), i_(0) {}
-
-cppbor::ParseClient* TrackQueue::QueueParseClient::item(
- std::unique_ptr<cppbor::Item>& item,
- const uint8_t* hdrBegin,
- const uint8_t* valueBegin,
- const uint8_t* end) {
- if (state_ == State::kInit) {
- if (item->type() == cppbor::MAP) {
- state_ = State::kRoot;
- }
- } else if (state_ == State::kRoot) {
- if (item->type() == cppbor::UINT) {
- switch (item->asUint()->unsignedValue()) {
- case 0:
- state_ = State::kMetadata;
- break;
- case 1:
- state_ = State::kShuffle;
- break;
- case 2:
- state_ = State::kTracks;
- break;
- default:
- state_ = State::kFinished;
- }
- }
- } else if (state_ == State::kMetadata) {
- if (item->type() == cppbor::ARRAY) {
- i_ = 0;
- } else if (item->type() == cppbor::UINT) {
- queue_.pos_ = item->asUint()->unsignedValue();
- } else if (item->type() == cppbor::SIMPLE) {
- bool val = item->asBool()->value();
- if (i_ == 0) {
- queue_.repeat_ = val;
- } else if (i_ == 1) {
- queue_.replay_ = val;
- }
- i_++;
- }
- } else if (state_ == State::kShuffle) {
- if (item->type() == cppbor::ARRAY) {
- i_ = 0;
- queue_.shuffle_.emplace();
- queue_.shuffle_->replay(queue_.replay_);
- } else if (item->type() == cppbor::UINT) {
- auto val = item->asUint()->unsignedValue();
- switch (i_) {
- case 0:
- queue_.shuffle_->size() = val;
- break;
- case 1:
- queue_.shuffle_->seed() = val;
- break;
- case 2:
- queue_.shuffle_->pos() = val;
- break;
- default:
- break;
- }
- i_++;
- }
- } else if (state_ == State::kTracks) {
- if (item->type() == cppbor::UINT) {
- queue_.tracks_.push_back(item->asUint()->unsignedValue());
- }
- } else if (state_ == State::kFinished) {
- }
- return this;
-}
-
-cppbor::ParseClient* TrackQueue::QueueParseClient::itemEnd(
- std::unique_ptr<cppbor::Item>& item,
- const uint8_t* hdrBegin,
- const uint8_t* valueBegin,
- const uint8_t* end) {
- if (state_ == State::kInit) {
- state_ = State::kFinished;
- } else if (state_ == State::kRoot) {
- state_ = State::kFinished;
- } else if (state_ == State::kMetadata) {
- if (item->type() == cppbor::ARRAY) {
- state_ = State::kRoot;
- }
- } else if (state_ == State::kShuffle) {
- if (item->type() == cppbor::ARRAY) {
- state_ = State::kRoot;
- }
- } else if (state_ == State::kTracks) {
- if (item->type() == cppbor::ARRAY) {
- state_ = State::kRoot;
- }
- } else if (state_ == State::kFinished) {
- }
- return this;
-}
-
-auto TrackQueue::deserialise(const std::string& s) -> void {
- if (s.empty()) {
- return;
- }
- QueueParseClient client{*this};
- const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data());
- cppbor::parse(data, data + s.size(), &client);
- notifyChanged(true, Reason::kDeserialised);
-}
-
-} // namespace audio