summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/audio_converter.cpp232
-rw-r--r--src/audio/audio_decoder.cpp76
-rw-r--r--src/audio/audio_fsm.cpp392
-rw-r--r--src/audio/audio_source.cpp14
-rw-r--r--src/audio/bt_audio_output.cpp4
-rw-r--r--src/audio/fatfs_audio_input.cpp9
-rw-r--r--src/audio/i2s_audio_output.cpp5
-rw-r--r--src/audio/include/audio_converter.hpp21
-rw-r--r--src/audio/include/audio_decoder.hpp20
-rw-r--r--src/audio/include/audio_events.hpp106
-rw-r--r--src/audio/include/audio_fsm.hpp58
-rw-r--r--src/audio/include/audio_sink.hpp18
-rw-r--r--src/audio/include/audio_source.hpp11
-rw-r--r--src/audio/include/bt_audio_output.hpp5
-rw-r--r--src/audio/include/fatfs_audio_input.hpp4
-rw-r--r--src/audio/include/i2s_audio_output.hpp5
-rw-r--r--src/audio/include/track_queue.hpp3
-rw-r--r--src/audio/readahead_source.cpp1
-rw-r--r--src/audio/track_queue.cpp45
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