diff options
| author | cooljqln <cooljqln@noreply.codeberg.org> | 2024-05-03 04:48:17 +0000 |
|---|---|---|
| committer | cooljqln <cooljqln@noreply.codeberg.org> | 2024-05-03 04:48:17 +0000 |
| commit | 3ceb8025ee4330c177101ed30ec17dfb0002f41e (patch) | |
| tree | 58350210f15df7d00d967cac6f30eeceeb031a3c /src/audio | |
| parent | 964da15a0b84f8e5f00e8abac2f7dfda0bf60488 (diff) | |
| parent | 9fafd797a5504f458b5fcae4a1d28a68da936315 (diff) | |
| download | tangara-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')
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 |
