|
|
@ -36,6 +36,7 @@ |
|
|
|
#include "sample.hpp" |
|
|
|
#include "sample.hpp" |
|
|
|
#include "service_locator.hpp" |
|
|
|
#include "service_locator.hpp" |
|
|
|
#include "system_events.hpp" |
|
|
|
#include "system_events.hpp" |
|
|
|
|
|
|
|
#include "tinyfsm.hpp" |
|
|
|
#include "track.hpp" |
|
|
|
#include "track.hpp" |
|
|
|
#include "track_queue.hpp" |
|
|
|
#include "track_queue.hpp" |
|
|
|
#include "wm8523.hpp" |
|
|
|
#include "wm8523.hpp" |
|
|
@ -54,13 +55,158 @@ std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput; |
|
|
|
std::shared_ptr<IAudioOutput> AudioState::sOutput; |
|
|
|
std::shared_ptr<IAudioOutput> AudioState::sOutput; |
|
|
|
|
|
|
|
|
|
|
|
// Two seconds of samples for two channels, at a representative sample rate.
|
|
|
|
// 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; |
|
|
|
|
|
|
|
constexpr size_t kDrainBufferSize = |
|
|
|
|
|
|
|
sizeof(sample::Sample) * kDrainLatencySamples * 4; |
|
|
|
|
|
|
|
|
|
|
|
StreamBufferHandle_t AudioState::sDrainBuffer; |
|
|
|
StreamBufferHandle_t AudioState::sDrainBuffer; |
|
|
|
|
|
|
|
|
|
|
|
std::optional<database::TrackId> AudioState::sCurrentTrack; |
|
|
|
std::shared_ptr<TrackInfo> AudioState::sCurrentTrack; |
|
|
|
bool AudioState::sIsPlaybackAllowed; |
|
|
|
uint64_t AudioState::sCurrentSamples; |
|
|
|
|
|
|
|
std::optional<IAudioOutput::Format> AudioState::sCurrentFormat; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<TrackInfo> AudioState::sNextTrack; |
|
|
|
|
|
|
|
uint64_t AudioState::sNextTrackCueSamples; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool AudioState::sIsResampling; |
|
|
|
|
|
|
|
bool AudioState::sIsPaused = true; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto AudioState::currentPositionSeconds() -> std::optional<uint32_t> { |
|
|
|
|
|
|
|
if (!sCurrentTrack || !sCurrentFormat) { |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return sCurrentSamples / |
|
|
|
|
|
|
|
(sCurrentFormat->num_channels * sCurrentFormat->sample_rate); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void AudioState::react(const QueueUpdate& ev) { |
|
|
|
|
|
|
|
if (!ev.current_changed && ev.reason != QueueUpdate::kRepeatingLastTrack) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SetTrack::Transition transition; |
|
|
|
|
|
|
|
switch (ev.reason) { |
|
|
|
|
|
|
|
case QueueUpdate::kExplicitUpdate: |
|
|
|
|
|
|
|
transition = SetTrack::Transition::kHardCut; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case QueueUpdate::kRepeatingLastTrack: |
|
|
|
|
|
|
|
case QueueUpdate::kTrackFinished: |
|
|
|
|
|
|
|
transition = SetTrack::Transition::kGapless; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case QueueUpdate::kDeserialised: |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
// The current track is deserialised separately in order to retain seek
|
|
|
|
|
|
|
|
// position.
|
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SetTrack cmd{ |
|
|
|
|
|
|
|
.new_track = {}, |
|
|
|
|
|
|
|
.seek_to_second = 0, |
|
|
|
|
|
|
|
.transition = transition, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
static std::optional<std::pair<std::string, uint32_t>> sLastTrackUpdate; |
|
|
|
auto current = sServices->track_queue().current(); |
|
|
|
|
|
|
|
if (current) { |
|
|
|
|
|
|
|
cmd.new_track = *current; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tinyfsm::FsmList<AudioState>::dispatch(cmd); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void AudioState::react(const SetTrack& ev) { |
|
|
|
|
|
|
|
if (ev.transition == SetTrack::Transition::kHardCut) { |
|
|
|
|
|
|
|
clearDrainBuffer(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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::DecoderOpened& ev) { |
|
|
|
|
|
|
|
ESP_LOGI(kTag, "decoder opened %s", ev.track->uri.c_str()); |
|
|
|
|
|
|
|
sNextTrack = ev.track; |
|
|
|
|
|
|
|
sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void AudioState::react(const internal::DecoderClosed&) { |
|
|
|
|
|
|
|
ESP_LOGI(kTag, "decoder closed"); |
|
|
|
|
|
|
|
// FIXME: only when we were playing the current track
|
|
|
|
|
|
|
|
sServices->track_queue().finish(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void AudioState::react(const internal::DecoderError&) { |
|
|
|
|
|
|
|
ESP_LOGW(kTag, "decoder errored"); |
|
|
|
|
|
|
|
// FIXME: only when we were playing the current track
|
|
|
|
|
|
|
|
sServices->track_queue().finish(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void AudioState::react(const internal::ConverterConfigurationChanged& ev) { |
|
|
|
|
|
|
|
sCurrentFormat = ev.dst_format; |
|
|
|
|
|
|
|
sIsResampling = ev.src_format != ev.dst_format; |
|
|
|
|
|
|
|
ESP_LOGI(kTag, "output format now %u ch @ %lu hz (resample=%i)", |
|
|
|
|
|
|
|
sCurrentFormat->num_channels, sCurrentFormat->sample_rate, |
|
|
|
|
|
|
|
sIsResampling); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void AudioState::react(const internal::ConverterProgress& ev) { |
|
|
|
|
|
|
|
ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); |
|
|
|
|
|
|
|
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) * |
|
|
|
|
|
|
|
(sCurrentFormat->num_channels * sCurrentFormat->sample_rate); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sNextTrack.reset(); |
|
|
|
|
|
|
|
sNextTrackCueSamples = 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
void AudioState::react(const system_fsm::BluetoothEvent& ev) { |
|
|
|
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { |
|
|
|
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { |
|
|
@ -184,17 +330,6 @@ 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; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
sFileSource->SetPath(db->getTrackPath(id)); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
auto AudioState::commitVolume() -> void { |
|
|
|
auto AudioState::commitVolume() -> void { |
|
|
|
auto mode = sServices->nvs().OutputMode(); |
|
|
|
auto mode = sServices->nvs().OutputMode(); |
|
|
|
auto vol = sOutput->GetVolume(); |
|
|
|
auto vol = sOutput->GetVolume(); |
|
|
@ -209,23 +344,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 { |
|
|
|
namespace states { |
|
|
|
|
|
|
|
|
|
|
|
void Uninitialised::react(const system_fsm::BootComplete& ev) { |
|
|
|
void Uninitialised::react(const system_fsm::BootComplete& ev) { |
|
|
@ -283,44 +401,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { |
|
|
|
transit<Standby>(); |
|
|
|
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 kQueueKey[] = "audio:queue"; |
|
|
|
static const char kCurrentFileKey[] = "audio:current"; |
|
|
|
static const char kCurrentFileKey[] = "audio:current"; |
|
|
|
|
|
|
|
|
|
|
@ -328,7 +408,7 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { |
|
|
|
if (!ev.locking) { |
|
|
|
if (!ev.locking) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
sServices->bg_worker().Dispatch<void>([]() { |
|
|
|
sServices->bg_worker().Dispatch<void>([this]() { |
|
|
|
auto db = sServices->database().lock(); |
|
|
|
auto db = sServices->database().lock(); |
|
|
|
if (!db) { |
|
|
|
if (!db) { |
|
|
|
return; |
|
|
|
return; |
|
|
@ -341,10 +421,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { |
|
|
|
} |
|
|
|
} |
|
|
|
db->put(kQueueKey, queue.serialise()); |
|
|
|
db->put(kQueueKey, queue.serialise()); |
|
|
|
|
|
|
|
|
|
|
|
if (sLastTrackUpdate) { |
|
|
|
if (sCurrentTrack) { |
|
|
|
cppbor::Array current_track{ |
|
|
|
cppbor::Array current_track{ |
|
|
|
cppbor::Tstr{sLastTrackUpdate->first}, |
|
|
|
cppbor::Tstr{sCurrentTrack->uri}, |
|
|
|
cppbor::Uint{sLastTrackUpdate->second}, |
|
|
|
cppbor::Uint{currentPositionSeconds().value_or(0)}, |
|
|
|
}; |
|
|
|
}; |
|
|
|
db->put(kCurrentFileKey, current_track.toString()); |
|
|
|
db->put(kCurrentFileKey, current_track.toString()); |
|
|
|
} |
|
|
|
} |
|
|
@ -371,8 +451,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) { |
|
|
|
if (parsed->type() == cppbor::ARRAY) { |
|
|
|
if (parsed->type() == cppbor::ARRAY) { |
|
|
|
std::string filename = parsed->asArray()->get(0)->asTstr()->value(); |
|
|
|
std::string filename = parsed->asArray()->get(0)->asTstr()->value(); |
|
|
|
uint32_t pos = parsed->asArray()->get(1)->asUint()->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 +472,31 @@ void Standby::react(const system_fsm::StorageMounted& ev) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Playback::entry() { |
|
|
|
void Playback::entry() { |
|
|
|
ESP_LOGI(kTag, "beginning playback"); |
|
|
|
ESP_LOGI(kTag, "audio output resumed"); |
|
|
|
sOutput->mode(IAudioOutput::Modes::kOnPlaying); |
|
|
|
sOutput->mode(IAudioOutput::Modes::kOnPlaying); |
|
|
|
|
|
|
|
|
|
|
|
events::System().Dispatch(PlaybackStarted{}); |
|
|
|
PlaybackUpdate event{ |
|
|
|
events::Ui().Dispatch(PlaybackStarted{}); |
|
|
|
.current_track = sCurrentTrack, |
|
|
|
|
|
|
|
.track_position = currentPositionSeconds(), |
|
|
|
|
|
|
|
.paused = false, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
events::System().Dispatch(event); |
|
|
|
|
|
|
|
events::Ui().Dispatch(event); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Playback::exit() { |
|
|
|
void Playback::exit() { |
|
|
|
ESP_LOGI(kTag, "finishing playback"); |
|
|
|
ESP_LOGI(kTag, "audio output paused"); |
|
|
|
sOutput->mode(IAudioOutput::Modes::kOnPaused); |
|
|
|
sOutput->mode(IAudioOutput::Modes::kOnPaused); |
|
|
|
|
|
|
|
|
|
|
|
// Stash the current volume now, in case it changed during playback, since
|
|
|
|
PlaybackUpdate event{ |
|
|
|
// we might be powering off soon.
|
|
|
|
.current_track = sCurrentTrack, |
|
|
|
commitVolume(); |
|
|
|
.track_position = currentPositionSeconds(), |
|
|
|
|
|
|
|
.paused = true, |
|
|
|
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) {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void Playback::react(const internal::InputFileClosed& ev) {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void Playback::react(const internal::InputFileFinished& ev) { |
|
|
|
events::System().Dispatch(event); |
|
|
|
ESP_LOGI(kTag, "finished playing file"); |
|
|
|
events::Ui().Dispatch(event); |
|
|
|
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>(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} // namespace states
|
|
|
|
} // namespace states
|
|
|
|