You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
662 lines
20 KiB
662 lines
20 KiB
/*
|
|
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-only
|
|
*/
|
|
|
|
#include "audio/audio_fsm.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <future>
|
|
#include <memory>
|
|
#include <sstream>
|
|
#include <variant>
|
|
|
|
#include "audio/audio_source.hpp"
|
|
#include "audio/sine_source.hpp"
|
|
#include "cppbor.h"
|
|
#include "cppbor_parse.h"
|
|
#include "drivers/pcm_buffer.hpp"
|
|
#include "esp_heap_caps.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/portmacro.h"
|
|
#include "freertos/projdefs.h"
|
|
#include "tinyfsm.hpp"
|
|
|
|
#include "audio/audio_decoder.hpp"
|
|
#include "audio/audio_events.hpp"
|
|
#include "audio/audio_sink.hpp"
|
|
#include "audio/bt_audio_output.hpp"
|
|
#include "audio/fatfs_stream_factory.hpp"
|
|
#include "audio/i2s_audio_output.hpp"
|
|
#include "audio/stream_cues.hpp"
|
|
#include "audio/track_queue.hpp"
|
|
#include "database/future_fetcher.hpp"
|
|
#include "database/track.hpp"
|
|
#include "drivers/bluetooth.hpp"
|
|
#include "drivers/bluetooth_types.hpp"
|
|
#include "drivers/i2s_dac.hpp"
|
|
#include "drivers/nvs.hpp"
|
|
#include "drivers/storage.hpp"
|
|
#include "drivers/wm8523.hpp"
|
|
#include "events/event_queue.hpp"
|
|
#include "sample.hpp"
|
|
#include "system_fsm/service_locator.hpp"
|
|
#include "system_fsm/system_events.hpp"
|
|
#include "tts/player.hpp"
|
|
|
|
namespace audio {
|
|
|
|
[[maybe_unused]] static const char kTag[] = "audio_fsm";
|
|
|
|
std::shared_ptr<system_fsm::ServiceLocator> AudioState::sServices;
|
|
|
|
std::shared_ptr<FatfsStreamFactory> AudioState::sStreamFactory;
|
|
|
|
std::unique_ptr<Decoder> AudioState::sDecoder;
|
|
std::shared_ptr<SampleProcessor> AudioState::sSampleProcessor;
|
|
|
|
std::shared_ptr<IAudioOutput> AudioState::sOutput;
|
|
std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
|
|
std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
|
|
|
|
// For tracks, keep about two seconds' worth of samples at 2ch 48kHz. This
|
|
// is more headroom than we need for small playback, but it doesn't hurt to
|
|
// keep some PSRAM in our pockets for a rainy day.
|
|
constexpr size_t kTrackDrainLatencySamples = 48000 * 2 * 2;
|
|
|
|
// For system sounds, we intentionally choose codecs that are very fast to
|
|
// decode. This lets us get away with a much smaller drain buffer.
|
|
constexpr size_t kSystemDrainLatencySamples = 48000;
|
|
|
|
std::unique_ptr<drivers::OutputBuffers> AudioState::sDrainBuffers;
|
|
std::optional<IAudioOutput::Format> AudioState::sDrainFormat;
|
|
|
|
StreamCues AudioState::sStreamCues;
|
|
|
|
bool AudioState::sIsPaused = true;
|
|
bool AudioState::sIsTtsPlaying = false;
|
|
|
|
uint8_t AudioState::sUpdateCounter = 0;
|
|
|
|
auto AudioState::emitPlaybackUpdate(bool paused) -> void {
|
|
std::optional<uint32_t> position;
|
|
auto current = sStreamCues.current();
|
|
if (current.first && sDrainFormat) {
|
|
position = ((current.second +
|
|
(sDrainFormat->num_channels * sDrainFormat->sample_rate / 2)) /
|
|
(sDrainFormat->num_channels * sDrainFormat->sample_rate)) +
|
|
current.first->start_offset.value_or(0);
|
|
}
|
|
|
|
// If we've got an elapsed duration and it's more than 5 minutes
|
|
// increment a counter. Every 60 counts (ie, every minute) save the current
|
|
// elapsed position
|
|
if (position && *position > (5 * 60)) {
|
|
sUpdateCounter++;
|
|
if (sUpdateCounter >= 60) {
|
|
sUpdateCounter = 0;
|
|
updateSavedPosition(current.first->uri, *position);
|
|
}
|
|
}
|
|
|
|
PlaybackUpdate event{
|
|
.current_track = current.first,
|
|
.track_position = position,
|
|
.paused = paused,
|
|
};
|
|
|
|
events::System().Dispatch(event);
|
|
events::Ui().Dispatch(event);
|
|
}
|
|
|
|
void AudioState::react(const QueueUpdate& ev) {
|
|
SetTrack cmd{
|
|
.new_track = std::monostate{},
|
|
.seek_to_second = ev.seek_to_second,
|
|
};
|
|
|
|
auto current = sServices->track_queue().current();
|
|
cmd.new_track = current;
|
|
|
|
switch (ev.reason) {
|
|
case QueueUpdate::kExplicitUpdate:
|
|
if (!ev.current_changed) {
|
|
return;
|
|
}
|
|
break;
|
|
case QueueUpdate::kRepeatingLastTrack:
|
|
break;
|
|
case QueueUpdate::kTrackFinished:
|
|
if (!ev.current_changed) {
|
|
cmd.new_track = std::monostate{};
|
|
}
|
|
break;
|
|
case QueueUpdate::kBulkLoadingUpdate:
|
|
// Bulk loading updates are informational only; a separate QueueUpdate
|
|
// event will be sent when loading is done.
|
|
case QueueUpdate::kDeserialised:
|
|
// The current track is deserialised separately in order to retain seek
|
|
// position.
|
|
default:
|
|
return;
|
|
}
|
|
|
|
tinyfsm::FsmList<AudioState>::dispatch(cmd);
|
|
}
|
|
|
|
void AudioState::react(const SetTrack& ev) {
|
|
if (std::holds_alternative<std::monostate>(ev.new_track)) {
|
|
ESP_LOGI(kTag, "playback finished, awaiting drain");
|
|
sDecoder->open({});
|
|
sStreamCues.clear();
|
|
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::shared_ptr<TaggedStream> stream;
|
|
if (std::holds_alternative<database::TrackId>(new_track)) {
|
|
stream = sStreamFactory->create(std::get<database::TrackId>(new_track),
|
|
seek_to);
|
|
} else if (std::holds_alternative<std::string>(new_track)) {
|
|
stream =
|
|
sStreamFactory->create(std::get<std::string>(new_track), seek_to);
|
|
}
|
|
|
|
// Always give the stream to the decoder, even if it turns out to be empty.
|
|
// This has the effect of stopping the current playback, which is generally
|
|
// what the user expects to happen when they say "Play this track!", even
|
|
// if the new track has an issue.
|
|
sDecoder->open(stream);
|
|
|
|
// ...but if the stream that failed is the front of the queue, then we
|
|
// should advance to the next track in order to keep the tunes flowing.
|
|
if (!stream) {
|
|
auto& queue = sServices->track_queue();
|
|
if (new_track == queue.current()) {
|
|
queue.finish();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void AudioState::react(const PlaySineWave& ev) {
|
|
auto tags = std::make_shared<database::TrackTags>();
|
|
|
|
std::stringstream title;
|
|
title << ev.frequency << "Hz Sine Wave";
|
|
tags->title(title.str());
|
|
|
|
sDecoder->open(std::make_shared<TaggedStream>(
|
|
tags, std::make_unique<SineSource>(ev.frequency), title.str()));
|
|
}
|
|
|
|
void AudioState::react(const TogglePlayPause& ev) {
|
|
sIsPaused = !ev.set_to.value_or(sIsPaused);
|
|
if (!sIsPaused && is_in_state<states::Standby>() &&
|
|
sStreamCues.current().first) {
|
|
transit<states::Playback>();
|
|
} else if (sIsPaused && is_in_state<states::Playback>()) {
|
|
transit<states::Standby>();
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const TtsPlaybackChanged& ev) {
|
|
sIsTtsPlaying = ev.is_playing;
|
|
updateOutputMode();
|
|
}
|
|
|
|
void AudioState::react(const internal::DecodingFinished& ev) {
|
|
// If we just finished playing whatever's at the front of the queue, then we
|
|
// need to advanve and start playing the next one ASAP in order to continue
|
|
// gaplessly.
|
|
sServices->bg_worker().Dispatch<void>([=]() {
|
|
auto& queue = sServices->track_queue();
|
|
auto current = queue.current();
|
|
if (std::holds_alternative<std::monostate>(current)) {
|
|
return;
|
|
}
|
|
auto db = sServices->database().lock();
|
|
if (!db) {
|
|
return;
|
|
}
|
|
std::string path;
|
|
if (std::holds_alternative<std::string>(current)) {
|
|
path = std::get<std::string>(current);
|
|
} else if (std::holds_alternative<database::TrackId>(current)) {
|
|
auto tid = std::get<database::TrackId>(current);
|
|
path = db->getTrackPath(tid).value_or("");
|
|
}
|
|
if (path == ev.track->uri) {
|
|
queue.finish();
|
|
incrementPlayCount(ev.track->uri);
|
|
}
|
|
});
|
|
}
|
|
|
|
void AudioState::react(const internal::StreamStarted& ev) {
|
|
if (sDrainFormat != ev.sink_format) {
|
|
sDrainFormat = ev.sink_format;
|
|
ESP_LOGI(kTag, "sink_format=%u ch @ %lu hz", sDrainFormat->num_channels,
|
|
sDrainFormat->sample_rate);
|
|
}
|
|
|
|
sStreamCues.addCue(ev.track, ev.cue_at_sample);
|
|
sStreamCues.update(sDrainBuffers->first.totalReceived());
|
|
|
|
if (!sIsPaused && !is_in_state<states::Playback>()) {
|
|
transit<states::Playback>();
|
|
} else {
|
|
// Make sure everyone knows we've got a track ready to go, even if we're
|
|
// not playing it yet. This mostly matters when restoring the queue from
|
|
// disk after booting.
|
|
emitPlaybackUpdate(true);
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const internal::StreamEnded& ev) {
|
|
sStreamCues.addCue({}, ev.cue_at_sample);
|
|
}
|
|
|
|
void AudioState::react(const system_fsm::HasPhonesChanged& ev) {
|
|
if (ev.has_headphones) {
|
|
events::Audio().Dispatch(audio::OutputModeChanged{
|
|
.set_to = drivers::NvsStorage::Output::kHeadphones});
|
|
} else {
|
|
if (sServices->bluetooth().enabled()) {
|
|
events::Audio().Dispatch(audio::OutputModeChanged{
|
|
.set_to = drivers::NvsStorage::Output::kBluetooth});
|
|
} else {
|
|
// Nothing connected
|
|
transit<states::Standby>();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const system_fsm::BluetoothEvent& ev) {
|
|
using drivers::bluetooth::SimpleEvent;
|
|
if (std::holds_alternative<SimpleEvent>(ev.event)) {
|
|
auto simpleEvent = std::get<SimpleEvent>(ev.event);
|
|
switch (simpleEvent) {
|
|
case SimpleEvent::kConnectionStateChanged: {
|
|
auto bt = sServices->bluetooth();
|
|
if (bt.connectionState() !=
|
|
drivers::Bluetooth::ConnectionState::kConnected) {
|
|
// If BT Disconnected, move to standby state
|
|
events::Audio().Dispatch(audio::OutputModeChanged{
|
|
.set_to = drivers::NvsStorage::Output::kHeadphones});
|
|
transit<states::Standby>();
|
|
return;
|
|
}
|
|
auto dev = sServices->bluetooth().pairedDevice();
|
|
if (!dev) {
|
|
return;
|
|
}
|
|
events::Audio().Dispatch(audio::OutputModeChanged{
|
|
.set_to = drivers::NvsStorage::Output::kBluetooth});
|
|
sBtOutput->SetVolume(sServices->nvs().BluetoothVolume(dev->mac));
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (std::holds_alternative<drivers::bluetooth::RemoteVolumeChanged>(
|
|
ev.event)) {
|
|
auto volume_chg =
|
|
std::get<drivers::bluetooth::RemoteVolumeChanged>(ev.event).new_vol;
|
|
events::Ui().Dispatch(RemoteVolumeChanged{.value = volume_chg});
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const StepUpVolume& ev) {
|
|
if (sOutput->AdjustVolumeUp()) {
|
|
commitVolume();
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const StepDownVolume& ev) {
|
|
if (sOutput->AdjustVolumeDown()) {
|
|
commitVolume();
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const SetVolume& ev) {
|
|
if (ev.db.has_value()) {
|
|
if (sOutput->SetVolumeDb(ev.db.value())) {
|
|
commitVolume();
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
}
|
|
|
|
} else if (ev.percent.has_value()) {
|
|
if (sOutput->SetVolumePct(ev.percent.value())) {
|
|
commitVolume();
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void AudioState::react(const SetVolumeLimit& ev) {
|
|
uint16_t limit_in_dac_units =
|
|
drivers::wm8523::kLineLevelReferenceVolume + (ev.limit_db * 4);
|
|
|
|
sI2SOutput->SetMaxVolume(limit_in_dac_units);
|
|
sServices->nvs().AmpMaxVolume(limit_in_dac_units);
|
|
|
|
events::Ui().Dispatch(VolumeLimitChanged{
|
|
.new_limit_db = ev.limit_db,
|
|
});
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
}
|
|
|
|
void AudioState::react(const SetVolumeBalance& ev) {
|
|
sOutput->SetVolumeImbalance(ev.left_bias);
|
|
sServices->nvs().AmpLeftBias(ev.left_bias);
|
|
|
|
events::Ui().Dispatch(VolumeBalanceChanged{
|
|
.left_bias = ev.left_bias,
|
|
});
|
|
}
|
|
|
|
void AudioState::react(const OutputModeChanged& ev) {
|
|
ESP_LOGI(kTag, "output mode changed");
|
|
auto new_mode = sServices->nvs().OutputMode();
|
|
if (ev.set_to) {
|
|
new_mode = *ev.set_to;
|
|
}
|
|
std::shared_ptr<IAudioOutput> new_output;
|
|
switch (new_mode) {
|
|
case drivers::NvsStorage::Output::kBluetooth:
|
|
new_output = sBtOutput;
|
|
break;
|
|
case drivers::NvsStorage::Output::kHeadphones:
|
|
new_output = sI2SOutput;
|
|
break;
|
|
}
|
|
if (new_output == sOutput) {
|
|
return;
|
|
}
|
|
sOutput->mode(IAudioOutput::Modes::kOff);
|
|
sOutput = new_output;
|
|
sSampleProcessor->SetOutput(sOutput);
|
|
updateOutputMode();
|
|
|
|
// Bluetooth volume isn't 'changed' until we've connected to a device.
|
|
if (new_mode == drivers::NvsStorage::Output::kHeadphones) {
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
}
|
|
}
|
|
|
|
auto AudioState::updateTrackData(std::string uri,
|
|
std::function<void(database::TrackData&)> fn)
|
|
-> void {
|
|
sServices->bg_worker().Dispatch<void>([=]() {
|
|
auto db = sServices->database().lock();
|
|
if (!db) {
|
|
return;
|
|
}
|
|
auto id = db->getTrackID(uri);
|
|
if (!id) {
|
|
return;
|
|
}
|
|
auto track = db->getTrack(*id);
|
|
if (!track) {
|
|
return;
|
|
}
|
|
auto data = track->data().clone();
|
|
std::invoke(fn, *data);
|
|
db->setTrackData(*id, *data);
|
|
});
|
|
}
|
|
|
|
auto AudioState::updateSavedPosition(std::string uri,
|
|
uint32_t position) -> void {
|
|
updateTrackData(
|
|
uri, [=](database::TrackData& data) { data.last_position = position; });
|
|
}
|
|
|
|
auto AudioState::incrementPlayCount(std::string uri) -> void {
|
|
updateTrackData(uri, [&](database::TrackData& data) { data.play_count++; });
|
|
}
|
|
|
|
auto AudioState::updateOutputMode() -> void {
|
|
if (is_in_state<states::Playback>() || sIsTtsPlaying) {
|
|
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
|
|
} else {
|
|
sOutput->mode(IAudioOutput::Modes::kOnPaused);
|
|
}
|
|
}
|
|
|
|
auto AudioState::commitVolume() -> void {
|
|
auto mode = sServices->nvs().OutputMode();
|
|
auto vol = sOutput->GetVolume();
|
|
if (mode == drivers::NvsStorage::Output::kHeadphones) {
|
|
sServices->nvs().AmpCurrentVolume(vol);
|
|
} else if (mode == drivers::NvsStorage::Output::kBluetooth) {
|
|
auto dev = sServices->bluetooth().pairedDevice();
|
|
if (!dev) {
|
|
return;
|
|
}
|
|
sServices->nvs().BluetoothVolume(dev->mac, vol);
|
|
}
|
|
}
|
|
|
|
namespace states {
|
|
|
|
void Uninitialised::react(const system_fsm::BootComplete& ev) {
|
|
sServices = ev.services;
|
|
|
|
sDrainBuffers = std::make_unique<drivers::OutputBuffers>(
|
|
kTrackDrainLatencySamples, kSystemDrainLatencySamples);
|
|
sDrainBuffers->first.suspend(true);
|
|
|
|
sStreamFactory.reset(
|
|
new FatfsStreamFactory(sServices->database(), sServices->tag_parser()));
|
|
sI2SOutput.reset(new I2SAudioOutput(sServices->gpios(), *sDrainBuffers));
|
|
sBtOutput.reset(new BluetoothAudioOutput(
|
|
sServices->bluetooth(), *sDrainBuffers, sServices->bg_worker()));
|
|
|
|
auto& tts_provider = sServices->tts();
|
|
auto tts_player = std::make_unique<tts::Player>(
|
|
sServices->bg_worker(), sDrainBuffers->second, *sStreamFactory);
|
|
tts_provider.player(std::move(tts_player));
|
|
|
|
auto& nvs = sServices->nvs();
|
|
sI2SOutput->SetMaxVolume(nvs.AmpMaxVolume());
|
|
sI2SOutput->SetVolume(nvs.AmpCurrentVolume());
|
|
sI2SOutput->SetVolumeImbalance(nvs.AmpLeftBias());
|
|
|
|
// Always set to headphones output initially
|
|
// Connecting bluetooth will change this
|
|
sOutput = sI2SOutput;
|
|
if (sServices->nvs().OutputMode() ==
|
|
drivers::NvsStorage::Output::kBluetooth) {
|
|
// Ensure Bluetooth gets enabled if it's the default sink.
|
|
sServices->bluetooth().enable(true);
|
|
}
|
|
sOutput->mode(IAudioOutput::Modes::kOnPaused);
|
|
|
|
events::Ui().Dispatch(VolumeLimitChanged{
|
|
.new_limit_db =
|
|
(static_cast<int>(nvs.AmpMaxVolume()) -
|
|
static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume)) /
|
|
4,
|
|
});
|
|
events::Ui().Dispatch(VolumeChanged{
|
|
.percent = sOutput->GetVolumePct(),
|
|
.db = sOutput->GetVolumeDb(),
|
|
});
|
|
events::Ui().Dispatch(VolumeBalanceChanged{
|
|
.left_bias = nvs.AmpLeftBias(),
|
|
});
|
|
|
|
sSampleProcessor.reset(new SampleProcessor(sDrainBuffers->first));
|
|
sSampleProcessor->SetOutput(sOutput);
|
|
|
|
sDecoder.reset(Decoder::Start(sSampleProcessor));
|
|
|
|
transit<Standby>();
|
|
}
|
|
|
|
static const char kQueueKey[] = "audio:queue";
|
|
static const char kCurrentFileKey[] = "audio:current";
|
|
|
|
auto Standby::entry() -> void {
|
|
updateOutputMode();
|
|
}
|
|
|
|
void Standby::react(const system_fsm::KeyLockChanged& ev) {
|
|
if (!ev.locking) {
|
|
return;
|
|
}
|
|
auto current = sStreamCues.current();
|
|
sServices->bg_worker().Dispatch<void>([=]() {
|
|
auto db = sServices->database().lock();
|
|
if (!db) {
|
|
return;
|
|
}
|
|
auto& queue = sServices->track_queue();
|
|
if (queue.totalSize() <= queue.currentPosition()) {
|
|
// Nothing is playing, so don't bother saving the queue.
|
|
db->put(kQueueKey, "");
|
|
return;
|
|
}
|
|
db->put(kQueueKey, queue.serialise());
|
|
|
|
if (current.first && sDrainFormat) {
|
|
uint32_t seconds = (current.second / (sDrainFormat->num_channels *
|
|
sDrainFormat->sample_rate)) +
|
|
current.first->start_offset.value_or(0);
|
|
cppbor::Array current_track{
|
|
cppbor::Tstr{current.first->uri},
|
|
cppbor::Uint{seconds},
|
|
};
|
|
db->put(kCurrentFileKey, current_track.toString());
|
|
}
|
|
});
|
|
}
|
|
|
|
void Standby::react(const system_fsm::SdStateChanged& ev) {
|
|
auto state = sServices->sd();
|
|
if (state != drivers::SdState::kMounted) {
|
|
return;
|
|
}
|
|
sServices->bg_worker().Dispatch<void>([]() {
|
|
auto db = sServices->database().lock();
|
|
if (!db) {
|
|
return;
|
|
}
|
|
|
|
// Open the queue file
|
|
sServices->track_queue().open();
|
|
|
|
// 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,
|
|
});
|
|
}
|
|
}
|
|
|
|
auto queue = db->get(kQueueKey);
|
|
if (queue) {
|
|
// Don't restore the same queue again. This ideally should do nothing,
|
|
// but guards against bad edge cases where restoring the queue ends up
|
|
// causing a crash.
|
|
db->put(kQueueKey, "");
|
|
sServices->track_queue().deserialise(*queue);
|
|
}
|
|
});
|
|
}
|
|
|
|
static TimerHandle_t sHeartbeatTimer;
|
|
|
|
static void heartbeat(TimerHandle_t) {
|
|
events::Audio().Dispatch(internal::StreamHeartbeat{});
|
|
}
|
|
|
|
void Playback::entry() {
|
|
ESP_LOGI(kTag, "audio output resumed");
|
|
sDrainBuffers->first.suspend(false);
|
|
updateOutputMode();
|
|
emitPlaybackUpdate(false);
|
|
|
|
if (!sHeartbeatTimer) {
|
|
sHeartbeatTimer =
|
|
xTimerCreate("stream", pdMS_TO_TICKS(1000), true, NULL, heartbeat);
|
|
}
|
|
xTimerStart(sHeartbeatTimer, portMAX_DELAY);
|
|
}
|
|
|
|
void Playback::exit() {
|
|
ESP_LOGI(kTag, "audio output paused");
|
|
xTimerStop(sHeartbeatTimer, portMAX_DELAY);
|
|
sDrainBuffers->first.suspend(true);
|
|
emitPlaybackUpdate(true);
|
|
}
|
|
|
|
void Playback::react(const system_fsm::SdStateChanged& ev) {
|
|
if (sServices->sd() != drivers::SdState::kMounted) {
|
|
transit<Standby>();
|
|
}
|
|
}
|
|
|
|
void Playback::react(const internal::StreamHeartbeat& ev) {
|
|
sStreamCues.update(sDrainBuffers->first.totalReceived());
|
|
|
|
if (sStreamCues.hasStream()) {
|
|
emitPlaybackUpdate(false);
|
|
} else {
|
|
// Finished the current stream, and there's nothing upcoming. We must be
|
|
// finished.
|
|
transit<Standby>();
|
|
}
|
|
}
|
|
|
|
} // namespace states
|
|
|
|
} // namespace audio
|
|
|
|
FSM_INITIAL_STATE(audio::AudioState, audio::states::Uninitialised)
|
|
|