diff options
| author | ailurux <ailuruxx@gmail.com> | 2024-04-02 11:13:50 +1100 |
|---|---|---|
| committer | ailurux <ailuruxx@gmail.com> | 2024-04-02 11:13:50 +1100 |
| commit | e20ebe7574db5aedc73f07b7bb3a0a01eae93c84 (patch) | |
| tree | 34c93ec8a80e282f3ce3e47dd60c41e46de0f8b3 /src/audio | |
| parent | a750af35aa6afda40aadca8f7cf8db75f41a43b2 (diff) | |
| parent | 0d0c4b2307cac8436fea7276956f293262b265ed (diff) | |
| download | tangara-fw-e20ebe7574db5aedc73f07b7bb3a0a01eae93c84.tar.gz | |
Merge branch 'main' into lua-volume
Diffstat (limited to 'src/audio')
| -rw-r--r-- | src/audio/audio_converter.cpp | 232 | ||||
| -rw-r--r-- | src/audio/audio_decoder.cpp | 76 | ||||
| -rw-r--r-- | src/audio/audio_fsm.cpp | 392 | ||||
| -rw-r--r-- | src/audio/audio_source.cpp | 14 | ||||
| -rw-r--r-- | src/audio/bt_audio_output.cpp | 4 | ||||
| -rw-r--r-- | src/audio/fatfs_audio_input.cpp | 9 | ||||
| -rw-r--r-- | src/audio/i2s_audio_output.cpp | 5 | ||||
| -rw-r--r-- | src/audio/include/audio_converter.hpp | 21 | ||||
| -rw-r--r-- | src/audio/include/audio_decoder.hpp | 20 | ||||
| -rw-r--r-- | src/audio/include/audio_events.hpp | 106 | ||||
| -rw-r--r-- | src/audio/include/audio_fsm.hpp | 58 | ||||
| -rw-r--r-- | src/audio/include/audio_sink.hpp | 18 | ||||
| -rw-r--r-- | src/audio/include/audio_source.hpp | 11 | ||||
| -rw-r--r-- | src/audio/include/bt_audio_output.hpp | 5 | ||||
| -rw-r--r-- | src/audio/include/fatfs_audio_input.hpp | 4 | ||||
| -rw-r--r-- | src/audio/include/i2s_audio_output.hpp | 5 | ||||
| -rw-r--r-- | src/audio/include/track_queue.hpp | 3 | ||||
| -rw-r--r-- | src/audio/readahead_source.cpp | 1 | ||||
| -rw-r--r-- | src/audio/track_queue.cpp | 45 |
19 files changed, 651 insertions, 378 deletions
diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index dc2fef95..eb1cde80 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -5,18 +5,20 @@ */ #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 "idf_additions.h" #include "resample.hpp" #include "sample.hpp" @@ -25,7 +27,7 @@ [[maybe_unused]] static constexpr char kTag[] = "mixer"; static constexpr std::size_t kSampleBufferLength = - drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); + drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2; static constexpr std::size_t kSourceBufferLength = kSampleBufferLength * 2; namespace audio { @@ -35,7 +37,9 @@ SampleConverter::SampleConverter() resampler_(nullptr), source_(xStreamBufferCreateWithCaps(kSourceBufferLength, sizeof(sample::Sample) * 2, - MALLOC_CAP_DMA)) { + 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)), @@ -63,24 +67,32 @@ auto SampleConverter::SetOutput(std::shared_ptr<IAudioOutput> output) -> void { sink_ = output; } -auto SampleConverter::ConvertSamples(cpp::span<sample::Sample> input, - const IAudioOutput::Format& format, - bool is_eos) -> void { +auto SampleConverter::beginStream(std::shared_ptr<TrackInfo> track) -> void { Args args{ - .format = format, + .track = new std::shared_ptr<TrackInfo>(track), + .samples_available = 0, + .is_end_of_stream = false, + }; + xQueueSend(commands_, &args, portMAX_DELAY); +} + +auto SampleConverter::continueStream(cpp::span<sample::Sample> input) -> void { + Args args{ + .track = nullptr, .samples_available = input.size(), - .is_end_of_stream = is_eos, + .is_end_of_stream = false, }; xQueueSend(commands_, &args, portMAX_DELAY); + xStreamBufferSend(source_, input.data(), input.size_bytes(), portMAX_DELAY); +} - cpp::span<std::byte> input_as_bytes = { - reinterpret_cast<std::byte*>(input.data()), input.size_bytes()}; - size_t bytes_sent = 0; - while (bytes_sent < input_as_bytes.size()) { - bytes_sent += - xStreamBufferSend(source_, input_as_bytes.subspan(bytes_sent).data(), - input_as_bytes.size() - bytes_sent, 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 { @@ -88,75 +100,94 @@ auto SampleConverter::Main() -> void { Args args; while (!xQueueReceive(commands_, &args, portMAX_DELAY)) { } - if (args.format != source_format_) { - resampler_.reset(); - source_format_ = args.format; - leftover_bytes_ = 0; - - auto new_target = sink_->PrepareFormat(args.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; + + if (args.track) { + handleBeginStream(*args.track); + delete args.track; + } + if (args.samples_available) { + handleContinueStream(args.samples_available); + } + if (args.is_end_of_stream) { + handleEndStream(); } + } +} - // 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 = args.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), - args.is_end_of_stream && bytes_read == bytes_to_read); - - // 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::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::HandleSamples(cpp::span<sample::Sample> input, - bool is_eos) -> size_t { +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(cpp::span<sample::Sample> input) -> size_t { if (source_format_ == target_format_) { // The happiest possible case: the input format matches the output // format already. - std::size_t bytes_sent = xStreamBufferSend( - sink_->stream(), input.data(), input.size_bytes(), portMAX_DELAY); - return bytes_sent / sizeof(sample::Sample); + sendToSink(input); + return input.size(); } size_t samples_used = 0; @@ -173,7 +204,7 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input, size_t read, written; std::tie(read, written) = resampler_->Process(input.subspan(samples_used), - resampled_buffer_, is_eos); + resampled_buffer_, false); samples_used += read; if (read == 0 && written == 0) { @@ -186,16 +217,49 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input, samples_used = input.size(); } - size_t bytes_sent = 0; - size_t bytes_to_send = output_source.size_bytes(); - while (bytes_sent < bytes_to_send) { - bytes_sent += xStreamBufferSend( - sink_->stream(), - reinterpret_cast<std::byte*>(output_source.data()) + bytes_sent, - bytes_to_send - bytes_sent, portMAX_DELAY); - } + 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(cpp::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 index b0a973d9..90c69c16 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -5,6 +5,7 @@ */ #include "audio_decoder.hpp" +#include <stdint.h> #include <cstdint> #include <cstdlib> @@ -50,38 +51,6 @@ namespace audio { static constexpr std::size_t kCodecBufferLength = drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); -Timer::Timer(std::shared_ptr<Track> t, - const codecs::ICodec::OutputFormat& format) - : track_(t), - current_seconds_(0), - current_sample_in_second_(0), - samples_per_second_(format.sample_rate_hz * format.num_channels), - total_duration_seconds_(format.total_samples.value_or(0) / - format.num_channels / format.sample_rate_hz) { - track_->duration = total_duration_seconds_; -} - -auto Timer::AddSamples(std::size_t samples) -> void { - bool incremented = false; - current_sample_in_second_ += samples; - while (current_sample_in_second_ >= samples_per_second_) { - current_seconds_++; - current_sample_in_second_ -= samples_per_second_; - incremented = true; - } - - if (incremented) { - if (total_duration_seconds_ < current_seconds_) { - total_duration_seconds_ = current_seconds_; - track_->duration = total_duration_seconds_; - } - - PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_}; - events::Audio().Dispatch(ev); - events::Ui().Dispatch(ev); - } -} - auto Decoder::Start(std::shared_ptr<IAudioSource> source, std::shared_ptr<SampleConverter> sink) -> Decoder* { Decoder* task = new Decoder(source, sink); @@ -91,11 +60,7 @@ auto Decoder::Start(std::shared_ptr<IAudioSource> source, Decoder::Decoder(std::shared_ptr<IAudioSource> source, std::shared_ptr<SampleConverter> mixer) - : source_(source), - converter_(mixer), - codec_(), - timer_(), - current_format_() { + : 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( @@ -107,7 +72,6 @@ void Decoder::Main() { for (;;) { if (source_->HasNewStream() || !stream_) { std::shared_ptr<TaggedStream> new_stream = source_->NextStream(); - ESP_LOGI(kTag, "decoder has new stream"); if (new_stream && BeginDecoding(new_stream)) { stream_ = new_stream; } else { @@ -116,7 +80,6 @@ void Decoder::Main() { } if (ContinueDecoding()) { - events::Audio().Dispatch(internal::InputFileFinished{}); stream_.reset(); } } @@ -127,11 +90,11 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool { codec_.reset(); codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); if (!codec_) { - ESP_LOGE(kTag, "no codec found"); + ESP_LOGE(kTag, "no codec found for stream"); return false; } - auto open_res = codec_->OpenStream(stream); + 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()); @@ -144,20 +107,21 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool { .bits_per_sample = 16, }; - ESP_LOGI(kTag, "stream started ok"); - events::Audio().Dispatch(internal::InputFileOpened{}); + 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; + } - auto tags = std::make_shared<Track>(Track{ + converter_->beginStream(std::make_shared<TrackInfo>(TrackInfo{ .tags = stream->tags(), - .db_info = {}, + .uri = stream->Filepath(), + .duration = duration, + .start_offset = stream->Offset(), .bitrate_kbps = open_res->sample_rate_hz, .encoding = stream->type(), - }); - timer_.reset(new Timer(tags, open_res.value())); - - PlaybackUpdate ev{.seconds_elapsed = 0, .track = tags}; - events::Audio().Dispatch(ev); - events::Ui().Dispatch(ev); + .format = *current_sink_format_, + })); return true; } @@ -165,20 +129,16 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool { auto Decoder::ContinueDecoding() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { + converter_->endStream(); return true; } if (res->samples_written > 0) { - converter_->ConvertSamples(codec_buffer_.first(res->samples_written), - current_sink_format_.value(), - res->is_stream_finished); - } - - if (timer_) { - timer_->AddSamples(res->samples_written); + converter_->continueStream(codec_buffer_.first(res->samples_written)); } if (res->is_stream_finished) { + converter_->endStream(); codec_.reset(); } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 7800802e..a8f1260f 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -13,6 +13,8 @@ #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" @@ -29,11 +31,11 @@ #include "future_fetcher.hpp" #include "i2s_audio_output.hpp" #include "i2s_dac.hpp" -#include "idf_additions.h" #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" @@ -51,8 +53,184 @@ std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput; std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput; std::shared_ptr<IAudioOutput> AudioState::sOutput; -std::optional<database::TrackId> AudioState::sCurrentTrack; -bool AudioState::sIsPlaybackAllowed; +// 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) { + 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) { + 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) { @@ -146,7 +324,7 @@ void AudioState::react(const SetVolumeBalance& ev) { void AudioState::react(const OutputModeChanged& ev) { ESP_LOGI(kTag, "output mode changed"); auto new_mode = sServices->nvs().OutputMode(); - sOutput->SetMode(IAudioOutput::Modes::kOff); + sOutput->mode(IAudioOutput::Modes::kOff); switch (new_mode) { case drivers::NvsStorage::Output::kBluetooth: sOutput = sBtOutput; @@ -155,7 +333,7 @@ void AudioState::react(const OutputModeChanged& ev) { sOutput = sI2SOutput; break; } - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); sSampleConverter->SetOutput(sOutput); // Bluetooth volume isn't 'changed' until we've connected to a device. @@ -167,15 +345,41 @@ void AudioState::react(const OutputModeChanged& ev) { } } -auto AudioState::playTrack(database::TrackId id) -> void { - sCurrentTrack = id; - sServices->bg_worker().Dispatch<void>([=]() { - auto db = sServices->database().lock(); - if (!db) { - return; +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) { } - sFileSource->SetPath(db->getTrackPath(id)); - }); + } 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 { @@ -192,29 +396,8 @@ auto AudioState::commitVolume() -> void { } } -auto AudioState::readyToPlay() -> bool { - return sCurrentTrack.has_value() && sIsPlaybackAllowed; -} - -void AudioState::react(const TogglePlayPause& ev) { - sIsPlaybackAllowed = !sIsPlaybackAllowed; - if (readyToPlay()) { - if (!is_in_state<states::Playback>()) { - transit<states::Playback>(); - } - } else { - if (!is_in_state<states::Standby>()) { - transit<states::Standby>(); - } - } -} - namespace states { -// Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; -static StreamBufferHandle_t sDrainBuffer; - void Uninitialised::react(const system_fsm::BootComplete& ev) { sServices = ev.services; @@ -246,7 +429,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } else { sOutput = sBtOutput; } - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); events::Ui().Dispatch(VolumeLimitChanged{ .new_limit_db = @@ -270,35 +453,14 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { transit<Standby>(); } -void Standby::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); -} - -void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); -} - -void Standby::react(const internal::InputFileOpened& ev) { - if (readyToPlay()) { - transit<Playback>(); - } -} - -void Standby::react(const QueueUpdate& ev) { - auto current_track = sServices->track_queue().current(); - if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { - return; - } - playTrack(*current_track); -} - 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>([]() { + sServices->bg_worker().Dispatch<void>([this]() { auto db = sServices->database().lock(); if (!db) { return; @@ -310,6 +472,14 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { 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()); + } }); } @@ -319,82 +489,66 @@ void Standby::react(const system_fsm::StorageMounted& ev) { if (!db) { return; } - auto res = db->get(kQueueKey); - if (res) { + + // 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(*res); + sServices->track_queue().deserialise(*queue); } }); } void Playback::entry() { - ESP_LOGI(kTag, "beginning playback"); - sOutput->SetMode(IAudioOutput::Modes::kOnPlaying); - - events::System().Dispatch(PlaybackStarted{}); - events::Ui().Dispatch(PlaybackStarted{}); -} - -void Playback::exit() { - ESP_LOGI(kTag, "finishing playback"); - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); - - // Stash the current volume now, in case it changed during playback, since we - // might be powering off soon. - commitVolume(); + ESP_LOGI(kTag, "audio output resumed"); + sOutput->mode(IAudioOutput::Modes::kOnPlaying); - events::System().Dispatch(PlaybackStopped{}); - events::Ui().Dispatch(PlaybackStopped{}); -} + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = false, + }; -void Playback::react(const system_fsm::HasPhonesChanged& ev) { - if (!ev.has_headphones) { - transit<Standby>(); - } + events::System().Dispatch(event); + events::Ui().Dispatch(event); } -void Playback::react(const QueueUpdate& ev) { - if (!ev.current_changed) { - return; - } - auto current_track = sServices->track_queue().current(); - if (!current_track) { - sFileSource->SetPath(); - sCurrentTrack.reset(); - transit<Standby>(); - return; - } - playTrack(*current_track); -} - -void Playback::react(const PlaybackUpdate& ev) { - ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, - ev.track->duration); -} - -void Playback::react(const internal::InputFileOpened& ev) {} +void Playback::exit() { + ESP_LOGI(kTag, "audio output paused"); + sOutput->mode(IAudioOutput::Modes::kOnPaused); -void Playback::react(const internal::InputFileClosed& ev) {} + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = true, + }; -void Playback::react(const internal::InputFileFinished& ev) { - ESP_LOGI(kTag, "finished playing file"); - sServices->track_queue().finish(); - if (!sServices->track_queue().current()) { - for (int i = 0; i < 20; i++) { - if (xStreamBufferIsEmpty(sDrainBuffer)) { - break; - } - vTaskDelay(pdMS_TO_TICKS(200)); - } - transit<Standby>(); - } -} - -void Playback::react(const internal::AudioPipelineIdle& ev) { - transit<Standby>(); + events::System().Dispatch(event); + events::Ui().Dispatch(event); } } // namespace states diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp index 44de1d1b..d9e8e04a 100644 --- a/src/audio/audio_source.cpp +++ b/src/audio/audio_source.cpp @@ -11,8 +11,10 @@ namespace audio { TaggedStream::TaggedStream(std::shared_ptr<database::TrackTags> t, - std::unique_ptr<codecs::IStream> w) - : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)) {} + 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_; @@ -38,6 +40,14 @@ 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(); } diff --git a/src/audio/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp index f431a49b..20ed7bb3 100644 --- a/src/audio/bt_audio_output.cpp +++ b/src/audio/bt_audio_output.cpp @@ -36,7 +36,7 @@ BluetoothAudioOutput::BluetoothAudioOutput(StreamBufferHandle_t s, BluetoothAudioOutput::~BluetoothAudioOutput() {} -auto BluetoothAudioOutput::SetMode(Modes mode) -> void { +auto BluetoothAudioOutput::changeMode(Modes mode) -> void { if (mode == Modes::kOnPlaying) { bluetooth_.SetSource(stream()); } else { @@ -99,7 +99,7 @@ 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 = 44100, + .sample_rate = 48000, .num_channels = 2, .bits_per_sample = 16, }; diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 7726a94a..29d32390 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -22,7 +22,6 @@ #include "ff.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" -#include "idf_additions.h" #include "readahead_source.hpp" #include "span.hpp" @@ -62,9 +61,9 @@ auto FatfsAudioInput::SetPath(std::optional<std::string> path) -> void { } } -auto FatfsAudioInput::SetPath(const std::string& path) -> void { +auto FatfsAudioInput::SetPath(const std::string& path,uint32_t offset) -> void { std::lock_guard<std::mutex> guard{new_stream_mutex_}; - if (OpenFile(path)) { + if (OpenFile(path, offset)) { has_new_stream_ = true; has_new_stream_.notify_one(); } @@ -103,7 +102,7 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr<TaggedStream> { } } -auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { +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); @@ -136,7 +135,7 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { auto source = std::make_unique<FatfsSource>(stream_type.value(), std::move(file)); - new_stream_.reset(new TaggedStream(tags, std::move(source))); + new_stream_.reset(new TaggedStream(tags, std::move(source), path, offset)); return true; } diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 7739fa17..2a251685 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -58,7 +58,7 @@ I2SAudioOutput::~I2SAudioOutput() { dac_->SetSource(nullptr); } -auto I2SAudioOutput::SetMode(Modes mode) -> void { +auto I2SAudioOutput::changeMode(Modes mode) -> void { if (mode == current_mode_) { return; } @@ -166,9 +166,6 @@ auto I2SAudioOutput::Configure(const Format& fmt) -> void { return; } - ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz", - fmt.num_channels, fmt.bits_per_sample, fmt.sample_rate); - drivers::I2SDac::Channels ch; switch (fmt.num_channels) { case 1: diff --git a/src/audio/include/audio_converter.hpp b/src/audio/include/audio_converter.hpp index c2ebde60..232b5d8e 100644 --- a/src/audio/include/audio_converter.hpp +++ b/src/audio/include/audio_converter.hpp @@ -6,9 +6,11 @@ #pragma once +#include <stdint.h> #include <cstdint> #include <memory> +#include "audio_events.hpp" #include "audio_sink.hpp" #include "audio_source.hpp" #include "codec.hpp" @@ -30,18 +32,23 @@ class SampleConverter { auto SetOutput(std::shared_ptr<IAudioOutput>) -> void; - auto ConvertSamples(cpp::span<sample::Sample>, - const IAudioOutput::Format& format, - bool is_eos) -> void; + auto beginStream(std::shared_ptr<TrackInfo>) -> void; + auto continueStream(cpp::span<sample::Sample>) -> void; + auto endStream() -> void; private: auto Main() -> void; - auto SetTargetFormat(const IAudioOutput::Format& format) -> void; - auto HandleSamples(cpp::span<sample::Sample>, bool) -> size_t; + auto handleBeginStream(std::shared_ptr<TrackInfo>) -> void; + auto handleContinueStream(size_t samples_available) -> void; + auto handleEndStream() -> void; + + auto handleSamples(cpp::span<sample::Sample>) -> size_t; + + auto sendToSink(cpp::span<sample::Sample>) -> void; struct Args { - IAudioOutput::Format format; + std::shared_ptr<TrackInfo>* track; size_t samples_available; bool is_end_of_stream; }; @@ -59,6 +66,8 @@ class SampleConverter { 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 index 318e6fd4..89f0f43c 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -20,25 +20,6 @@ namespace audio { /* - * Sample-based timer for the current elapsed playback time. - */ -class Timer { - public: - Timer(std::shared_ptr<Track>, const codecs::ICodec::OutputFormat& format); - - auto AddSamples(std::size_t) -> void; - - private: - std::shared_ptr<Track> track_; - - uint32_t current_seconds_; - uint32_t current_sample_in_second_; - uint32_t samples_per_second_; - - uint32_t total_duration_seconds_; -}; - -/* * 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. @@ -65,7 +46,6 @@ class Decoder { std::shared_ptr<codecs::IStream> stream_; std::unique_ptr<codecs::ICodec> codec_; - std::unique_ptr<Timer> timer_; std::optional<codecs::ICodec::OutputFormat> current_format_; std::optional<IAudioOutput::Format> current_sink_format_; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 03584062..b8a0dba6 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -9,40 +9,104 @@ #include <stdint.h> #include <cstdint> #include <memory> +#include <optional> #include <string> +#include "audio_sink.hpp" #include "tinyfsm.hpp" #include "track.hpp" -#include "track_queue.hpp" #include "types.hpp" namespace audio { -struct Track { +/* + * 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; - std::shared_ptr<database::TrackData> db_info; - uint32_t duration; - uint32_t bitrate_kbps; + /* + * 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; -}; -struct PlaybackStarted : tinyfsm::Event {}; + 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 { - uint32_t seconds_elapsed; - std::shared_ptr<Track> track; + /* + * 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; }; -struct PlaybackStopped : tinyfsm::Event {}; +/* + * 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; -}; -struct PlayFile : tinyfsm::Event { - std::string filename; + enum Reason { + kExplicitUpdate, + kRepeatingLastTrack, + kTrackFinished, + kDeserialised, + }; + Reason reason; }; struct StepUpVolume : tinyfsm::Event {}; @@ -70,17 +134,21 @@ struct SetVolumeLimit : tinyfsm::Event { int limit_db; }; -struct TogglePlayPause : tinyfsm::Event {}; - struct OutputModeChanged : tinyfsm::Event {}; namespace internal { -struct InputFileOpened : tinyfsm::Event {}; -struct InputFileClosed : tinyfsm::Event {}; -struct InputFileFinished : tinyfsm::Event {}; +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 AudioPipelineIdle : tinyfsm::Event {}; +struct StreamEnded : tinyfsm::Event {}; } // namespace internal diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 29ec489a..60afb321 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -6,6 +6,7 @@ #pragma once +#include <stdint.h> #include <deque> #include <memory> #include <vector> @@ -41,6 +42,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> { /* 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&); @@ -52,21 +61,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> { void react(const OutputModeChanged&); virtual void react(const system_fsm::BootComplete&) {} - virtual void react(const system_fsm::KeyLockChanged&) {}; + virtual void react(const system_fsm::KeyLockChanged&){}; virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::BluetoothEvent&); - virtual void react(const PlayFile&) {} - virtual void react(const QueueUpdate&) {} - virtual void react(const PlaybackUpdate&) {} - void react(const TogglePlayPause&); - - virtual void react(const internal::InputFileOpened&) {} - virtual void react(const internal::InputFileClosed&) {} - virtual void react(const internal::InputFileFinished&) {} - virtual void react(const internal::AudioPipelineIdle&) {} - protected: + auto clearDrainBuffer() -> void; + auto awaitEmptyDrainBuffer() -> void; + auto playTrack(database::TrackId id) -> void; auto commitVolume() -> void; @@ -79,10 +81,21 @@ class AudioState : public tinyfsm::Fsm<AudioState> { static std::shared_ptr<BluetoothAudioOutput> sBtOutput; static std::shared_ptr<IAudioOutput> sOutput; - static std::optional<database::TrackId> sCurrentTrack; + static StreamBufferHandle_t sDrainBuffer; - auto readyToPlay() -> bool; - static bool sIsPlaybackAllowed; + 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 { @@ -90,17 +103,13 @@ namespace states { class Uninitialised : public AudioState { public: void react(const system_fsm::BootComplete&) override; - - void react(const system_fsm::BluetoothEvent&) override {}; + void react(const system_fsm::BluetoothEvent&) override{}; using AudioState::react; }; class Standby : public AudioState { public: - void react(const PlayFile&) override; - void react(const internal::InputFileOpened&) override; - void react(const QueueUpdate&) override; void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::StorageMounted&) override; @@ -112,17 +121,6 @@ class Playback : public AudioState { void entry() override; void exit() override; - void react(const system_fsm::HasPhonesChanged&) override; - - void react(const PlayFile&) override; - void react(const QueueUpdate&) override; - void react(const PlaybackUpdate&) override; - - void react(const internal::InputFileOpened&) override; - void react(const internal::InputFileClosed&) override; - void react(const internal::InputFileFinished&) override; - void react(const internal::AudioPipelineIdle&) override; - using AudioState::react; }; diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index 116410f6..f31d0d75 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -11,7 +11,6 @@ #include "esp_heap_caps.h" #include "freertos/FreeRTOS.h" -#include "idf_additions.h" namespace audio { @@ -27,7 +26,8 @@ class IAudioOutput { StreamBufferHandle_t stream_; public: - IAudioOutput(StreamBufferHandle_t stream) : stream_(stream) {} + IAudioOutput(StreamBufferHandle_t stream) + : stream_(stream), mode_(Modes::kOff) {} virtual ~IAudioOutput() {} @@ -41,7 +41,14 @@ class IAudioOutput { * Indicates whether this output is currently being sent samples. If this is * false, the output should place itself into a low power state. */ - virtual auto SetMode(Modes) -> void = 0; + 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; @@ -70,6 +77,11 @@ class IAudioOutput { 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 index 68145f5b..b38acd7a 100644 --- a/src/audio/include/audio_source.hpp +++ b/src/audio/include/audio_source.hpp @@ -16,7 +16,10 @@ namespace audio { class TaggedStream : public codecs::IStream { public: TaggedStream(std::shared_ptr<database::TrackTags>, - std::unique_ptr<codecs::IStream> wrapped); + std::unique_ptr<codecs::IStream> wrapped, + std::string path, + uint32_t offset = 0 + ); auto tags() -> std::shared_ptr<database::TrackTags>; @@ -30,11 +33,17 @@ class TaggedStream : public codecs::IStream { 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 { diff --git a/src/audio/include/bt_audio_output.hpp b/src/audio/include/bt_audio_output.hpp index dff25131..74b0301a 100644 --- a/src/audio/include/bt_audio_output.hpp +++ b/src/audio/include/bt_audio_output.hpp @@ -28,8 +28,6 @@ class BluetoothAudioOutput : public IAudioOutput { tasks::WorkerPool&); ~BluetoothAudioOutput(); - auto SetMode(Modes) -> void override; - auto SetVolumeImbalance(int_fast8_t balance) -> void override; auto SetVolume(uint16_t) -> void override; @@ -50,6 +48,9 @@ class BluetoothAudioOutput : public IAudioOutput { BluetoothAudioOutput(const BluetoothAudioOutput&) = delete; BluetoothAudioOutput& operator=(const BluetoothAudioOutput&) = delete; + protected: + auto changeMode(Modes) -> void override; + private: drivers::Bluetooth& bluetooth_; tasks::WorkerPool& bg_worker_; diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp index 4cccbb46..10b7433e 100644 --- a/src/audio/include/fatfs_audio_input.hpp +++ b/src/audio/include/fatfs_audio_input.hpp @@ -39,7 +39,7 @@ class FatfsAudioInput : public IAudioSource { * given file path. */ auto SetPath(std::optional<std::string>) -> void; - auto SetPath(const std::string&) -> void; + auto SetPath(const std::string&,uint32_t offset = 0) -> void; auto SetPath() -> void; auto HasNewStream() -> bool override; @@ -49,7 +49,7 @@ class FatfsAudioInput : public IAudioSource { FatfsAudioInput& operator=(const FatfsAudioInput&) = delete; private: - auto OpenFile(const std::string& path) -> bool; + auto OpenFile(const std::string& path,uint32_t offset) -> bool; auto ContainerToStreamType(database::Container) -> std::optional<codecs::StreamType>; diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp index 538eafb6..7954257a 100644 --- a/src/audio/include/i2s_audio_output.hpp +++ b/src/audio/include/i2s_audio_output.hpp @@ -23,8 +23,6 @@ class I2SAudioOutput : public IAudioOutput { I2SAudioOutput(StreamBufferHandle_t, drivers::IGpios& expander); ~I2SAudioOutput(); - auto SetMode(Modes) -> void override; - auto SetMaxVolume(uint16_t) -> void; auto SetVolumeDb(uint16_t) -> void; @@ -48,6 +46,9 @@ class I2SAudioOutput : public IAudioOutput { 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_; diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index e4fd7881..5b7c9448 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -12,6 +12,7 @@ #include <shared_mutex> #include <vector> +#include "audio_events.hpp" #include "cppbor_parse.h" #include "database.hpp" #include "tasks.hpp" @@ -120,6 +121,8 @@ class TrackQueue { TrackQueue& operator=(const TrackQueue&) = delete; private: + auto next(QueueUpdate::Reason r) -> void; + mutable std::shared_mutex mutex_; tasks::WorkerPool& bg_worker_; diff --git a/src/audio/readahead_source.cpp b/src/audio/readahead_source.cpp index c7b960d2..fe7ac3bd 100644 --- a/src/audio/readahead_source.cpp +++ b/src/audio/readahead_source.cpp @@ -17,7 +17,6 @@ #include "audio_source.hpp" #include "codec.hpp" #include "freertos/portmacro.h" -#include "idf_additions.h" #include "spi.hpp" #include "tasks.hpp" #include "types.hpp" diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index b75230fc..dbe283c4 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -33,6 +33,8 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "tracks"; +using Reason = QueueUpdate::Reason; + RandomIterator::RandomIterator() : seed_(0), pos_(0), size_(0), replay_(false) {} @@ -72,8 +74,11 @@ auto RandomIterator::replay(bool r) -> void { replay_ = r; } -auto notifyChanged(bool current_changed) -> void { - QueueUpdate ev{.current_changed = current_changed}; +auto notifyChanged(bool current_changed, Reason reason) -> void { + QueueUpdate ev{ + .current_changed = current_changed, + .reason = reason, + }; events::Ui().Dispatch(ev); events::Audio().Dispatch(ev); } @@ -131,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { { const std::shared_lock<std::shared_mutex> lock(mutex_); was_queue_empty = pos_ == tracks_.size(); - current_changed = pos_ == was_queue_empty || index == pos_; + current_changed = was_queue_empty || index == pos_; } auto update_shuffler = [=, this]() { @@ -157,7 +162,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { update_shuffler(); } } - notifyChanged(current_changed); + 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 @@ -185,7 +190,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { const std::unique_lock<std::shared_mutex> lock(mutex_); update_shuffler(); } - notifyChanged(current_changed); + notifyChanged(current_changed, Reason::kExplicitUpdate); }); } } @@ -200,6 +205,10 @@ auto TrackQueue::append(Item i) -> void { } auto TrackQueue::next() -> void { + next(Reason::kExplicitUpdate); +} + +auto TrackQueue::next(Reason r) -> void { bool changed = true; { @@ -221,7 +230,7 @@ auto TrackQueue::next() -> void { } } - notifyChanged(changed); + notifyChanged(changed, r); } auto TrackQueue::previous() -> void { @@ -245,22 +254,22 @@ auto TrackQueue::previous() -> void { } } - notifyChanged(changed); + notifyChanged(changed, Reason::kExplicitUpdate); } auto TrackQueue::finish() -> void { if (repeat_) { - notifyChanged(true); + notifyChanged(true, Reason::kRepeatingLastTrack); } else { - next(); + 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. + // 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; { @@ -274,7 +283,7 @@ auto TrackQueue::skipTo(database::TrackId id) -> void { } } if (found) { - notifyChanged(true); + notifyChanged(true, Reason::kExplicitUpdate); } }); } @@ -294,7 +303,7 @@ auto TrackQueue::clear() -> void { } } - notifyChanged(true); + notifyChanged(true, Reason::kExplicitUpdate); } auto TrackQueue::random(bool en) -> void { @@ -311,7 +320,7 @@ auto TrackQueue::random(bool en) -> void { } // Current track doesn't get randomised until next(). - notifyChanged(false); + notifyChanged(false, Reason::kExplicitUpdate); } auto TrackQueue::random() const -> bool { @@ -325,7 +334,7 @@ auto TrackQueue::repeat(bool en) -> void { repeat_ = en; } - notifyChanged(false); + notifyChanged(false, Reason::kExplicitUpdate); } auto TrackQueue::repeat() const -> bool { @@ -341,7 +350,7 @@ auto TrackQueue::replay(bool en) -> void { shuffle_->replay(en); } } - notifyChanged(false); + notifyChanged(false, Reason::kExplicitUpdate); } auto TrackQueue::replay() const -> bool { @@ -477,7 +486,7 @@ auto TrackQueue::deserialise(const std::string& s) -> void { QueueParseClient client{*this}; const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data()); cppbor::parse(data, data + s.size(), &client); - notifyChanged(true); + notifyChanged(true, Reason::kDeserialised); } } // namespace audio |
