summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
authorailurux <ailuruxx@gmail.com>2024-03-28 16:17:39 +1100
committerailurux <ailuruxx@gmail.com>2024-03-28 16:17:39 +1100
commitc8e67cbd80b53a4e889ce0485546042d5490918c (patch)
treef06314fef2bb9afaf04b924355b34f5277d69241 /src/audio
parentf1c8866b815a92aeda3133fd27051ce7c873cc57 (diff)
parent35a822fe602cdc9e3a3482df3913ea33af6fc8c2 (diff)
downloadtangara-fw-c8e67cbd80b53a4e889ce0485546042d5490918c.tar.gz
Merge branch 'main' into themes
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.cpp346
-rw-r--r--src/audio/bt_audio_output.cpp2
-rw-r--r--src/audio/fatfs_audio_input.cpp1
-rw-r--r--src/audio/i2s_audio_output.cpp3
-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.hpp105
-rw-r--r--src/audio/include/audio_fsm.hpp54
-rw-r--r--src/audio/include/audio_sink.hpp1
-rw-r--r--src/audio/readahead_source.cpp1
-rw-r--r--src/audio/track_queue.cpp2
13 files changed, 495 insertions, 369 deletions
diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp
index 946a0b63..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, pdMS_TO_TICKS(100));
- }
+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 68a8a86b..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,39 +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,
- uint32_t current_seconds)
- : track_(t),
- current_seconds_(current_seconds),
- 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);
@@ -92,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(
@@ -108,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 {
@@ -117,7 +80,6 @@ void Decoder::Main() {
}
if (ContinueDecoding()) {
- events::Audio().Dispatch(internal::InputFileFinished{});
stream_.reset();
}
}
@@ -128,7 +90,7 @@ 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;
}
@@ -145,21 +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(),
- .filepath = stream->Filepath(),
- });
- timer_.reset(new Timer(tags, open_res.value(), stream->Offset()));
-
- PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags};
- events::Audio().Dispatch(ev);
- events::Ui().Dispatch(ev);
+ .format = *current_sink_format_,
+ }));
return true;
}
@@ -167,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 05c7c216..424b0eff 100644
--- a/src/audio/audio_fsm.cpp
+++ b/src/audio/audio_fsm.cpp
@@ -31,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"
@@ -54,13 +54,183 @@ std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
std::shared_ptr<IAudioOutput> AudioState::sOutput;
// Two seconds of samples for two channels, at a representative sample rate.
-constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4;
+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);
+}
-std::optional<database::TrackId> AudioState::sCurrentTrack;
-bool AudioState::sIsPlaybackAllowed;
+void AudioState::react(const internal::StreamEnded&) {
+ ESP_LOGI(kTag, "stream ended");
-static std::optional<std::pair<std::string, uint32_t>> sLastTrackUpdate;
+ 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) {
@@ -184,15 +354,15 @@ auto AudioState::clearDrainBuffer() -> void {
}
}
-auto AudioState::playTrack(database::TrackId id) -> void {
- sCurrentTrack = id;
- sServices->bg_worker().Dispatch<void>([=]() {
- auto db = sServices->database().lock();
- if (!db) {
- return;
+auto AudioState::awaitEmptyDrainBuffer() -> void {
+ if (is_in_state<states::Playback>()) {
+ for (int i = 0; i < 10 && !xStreamBufferIsEmpty(sDrainBuffer); i++) {
+ vTaskDelay(pdMS_TO_TICKS(250));
}
- sFileSource->SetPath(db->getTrackPath(id));
- });
+ }
+ if (!xStreamBufferIsEmpty(sDrainBuffer)) {
+ clearDrainBuffer();
+ }
}
auto AudioState::commitVolume() -> void {
@@ -209,23 +379,6 @@ 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 {
void Uninitialised::react(const system_fsm::BootComplete& ev) {
@@ -283,44 +436,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
transit<Standby>();
}
-void Standby::react(const PlayFile& ev) {
- sCurrentTrack = 0;
- sIsPlaybackAllowed = true;
- sFileSource->SetPath(ev.filename);
-}
-
-void Playback::react(const PlayFile& ev) {
- sFileSource->SetPath(ev.filename);
-}
-
-void Standby::react(const SeekFile& ev) {
- clearDrainBuffer();
- sFileSource->SetPath(ev.filename, ev.offset);
-}
-
-void Playback::react(const SeekFile& ev) {
- clearDrainBuffer();
- sFileSource->SetPath(ev.filename, ev.offset);
-}
-
-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;
- }
- if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) {
- return;
- }
- clearDrainBuffer();
- playTrack(*current_track);
-}
-
static const char kQueueKey[] = "audio:queue";
static const char kCurrentFileKey[] = "audio:current";
@@ -328,7 +443,7 @@ 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;
@@ -341,10 +456,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
}
db->put(kQueueKey, queue.serialise());
- if (sLastTrackUpdate) {
+ if (sCurrentTrack) {
cppbor::Array current_track{
- cppbor::Tstr{sLastTrackUpdate->first},
- cppbor::Uint{sLastTrackUpdate->second},
+ cppbor::Tstr{sCurrentTrack->uri},
+ cppbor::Uint{currentPositionSeconds().value_or(0)},
};
db->put(kCurrentFileKey, current_track.toString());
}
@@ -371,8 +486,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
if (parsed->type() == cppbor::ARRAY) {
std::string filename = parsed->asArray()->get(0)->asTstr()->value();
uint32_t pos = parsed->asArray()->get(1)->asUint()->value();
- sLastTrackUpdate = std::make_pair(filename, pos);
- sFileSource->SetPath(filename, pos);
+
+ events::Audio().Dispatch(SetTrack{
+ .new_track = filename,
+ .seek_to_second = pos,
+ .transition = SetTrack::Transition::kHardCut,
+ });
}
}
@@ -388,76 +507,31 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
}
void Playback::entry() {
- ESP_LOGI(kTag, "beginning playback");
+ ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
- events::System().Dispatch(PlaybackStarted{});
- events::Ui().Dispatch(PlaybackStarted{});
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = false,
+ };
+
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
}
void Playback::exit() {
- ESP_LOGI(kTag, "finishing playback");
+ ESP_LOGI(kTag, "audio output paused");
sOutput->mode(IAudioOutput::Modes::kOnPaused);
- // Stash the current volume now, in case it changed during playback, since
- // we might be powering off soon.
- commitVolume();
-
- events::System().Dispatch(PlaybackStopped{});
- events::Ui().Dispatch(PlaybackStopped{});
-}
-
-void Playback::react(const system_fsm::HasPhonesChanged& ev) {
- if (!ev.has_headphones) {
- transit<Standby>();
- }
-}
-
-void Playback::react(const QueueUpdate& ev) {
- if (!ev.current_changed) {
- return;
- }
- // Cut the current track immediately.
- if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) {
- clearDrainBuffer();
- }
- 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);
- sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed);
-}
-
-void Playback::react(const internal::InputFileOpened& ev) {}
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = true,
+ };
-void Playback::react(const internal::InputFileClosed& ev) {}
-
-void Playback::react(const internal::InputFileFinished& ev) {
- ESP_LOGI(kTag, "finished playing file");
- sLastTrackUpdate.reset();
- 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/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp
index dff98e36..04daf71f 100644
--- a/src/audio/bt_audio_output.cpp
+++ b/src/audio/bt_audio_output.cpp
@@ -83,7 +83,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 74c1154b..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"
diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp
index cd61d97f..3fb99159 100644
--- a/src/audio/i2s_audio_output.cpp
+++ b/src/audio/i2s_audio_output.cpp
@@ -152,9 +152,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 b8aac710..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, uint32_t current_seconds = 0);
-
- 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 a8533646..b8a0dba6 100644
--- a/src/audio/include/audio_events.hpp
+++ b/src/audio/include/audio_events.hpp
@@ -9,8 +9,10 @@
#include <stdint.h>
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
+#include "audio_sink.hpp"
#include "tinyfsm.hpp"
#include "track.hpp"
@@ -18,24 +20,82 @@
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;
- std::string filepath;
-};
-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;
+};
+
+/*
+ * 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 PlaybackStopped : tinyfsm::Event {};
+struct TogglePlayPause : tinyfsm::Event {
+ std::optional<bool> set_to;
+};
struct QueueUpdate : tinyfsm::Event {
bool current_changed;
@@ -49,15 +109,6 @@ struct QueueUpdate : tinyfsm::Event {
Reason reason;
};
-struct PlayFile : tinyfsm::Event {
- std::string filename;
-};
-
-struct SeekFile : tinyfsm::Event {
- uint32_t offset;
- std::string filename;
-};
-
struct StepUpVolume : tinyfsm::Event {};
struct StepDownVolume : tinyfsm::Event {};
struct SetVolume : tinyfsm::Event {
@@ -83,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 13e241be..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&);
@@ -56,19 +65,10 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const system_fsm::StorageMounted&) {}
virtual void react(const system_fsm::BluetoothEvent&);
- virtual void react(const PlayFile&) {}
- virtual void react(const SeekFile&) {}
- 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;
@@ -83,10 +83,19 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static StreamBufferHandle_t sDrainBuffer;
- static std::optional<database::TrackId> sCurrentTrack;
+ static std::shared_ptr<TrackInfo> sCurrentTrack;
+ static uint64_t sCurrentSamples;
+ static std::optional<IAudioOutput::Format> sDrainFormat;
+ static bool sCurrentTrackIsFromQueue;
- auto readyToPlay() -> bool;
- static bool sIsPlaybackAllowed;
+ 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 {
@@ -94,7 +103,6 @@ namespace states {
class Uninitialised : public AudioState {
public:
void react(const system_fsm::BootComplete&) override;
-
void react(const system_fsm::BluetoothEvent&) override{};
using AudioState::react;
@@ -102,10 +110,6 @@ class Uninitialised : public AudioState {
class Standby : public AudioState {
public:
- void react(const PlayFile&) override;
- void react(const SeekFile&) 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;
@@ -117,18 +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 SeekFile&) 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 85c23f5c..e11f3ce0 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 {
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 a3f4c815..dbe283c4 100644
--- a/src/audio/track_queue.cpp
+++ b/src/audio/track_queue.cpp
@@ -136,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]() {