|
|
|
@ -5,8 +5,8 @@ |
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
#include "audio/audio_fsm.hpp" |
|
|
|
|
#include <stdint.h> |
|
|
|
|
|
|
|
|
|
#include <cstdint> |
|
|
|
|
#include <future> |
|
|
|
|
#include <memory> |
|
|
|
|
#include <variant> |
|
|
|
@ -18,6 +18,7 @@ |
|
|
|
|
#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" |
|
|
|
@ -25,6 +26,7 @@ |
|
|
|
|
#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" |
|
|
|
@ -38,7 +40,6 @@ |
|
|
|
|
#include "sample.hpp" |
|
|
|
|
#include "system_fsm/service_locator.hpp" |
|
|
|
|
#include "system_fsm/system_events.hpp" |
|
|
|
|
#include "tinyfsm.hpp" |
|
|
|
|
|
|
|
|
|
namespace audio { |
|
|
|
|
|
|
|
|
@ -47,11 +48,13 @@ namespace audio { |
|
|
|
|
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; |
|
|
|
|
std::shared_ptr<IAudioOutput> AudioState::sOutput; |
|
|
|
|
|
|
|
|
|
// Two seconds of samples for two channels, at a representative sample rate.
|
|
|
|
|
constexpr size_t kDrainLatencySamples = 48000 * 2 * 2; |
|
|
|
@ -61,30 +64,33 @@ constexpr size_t kDrainBufferSize = |
|
|
|
|
StreamBufferHandle_t AudioState::sDrainBuffer; |
|
|
|
|
std::optional<IAudioOutput::Format> AudioState::sDrainFormat; |
|
|
|
|
|
|
|
|
|
std::shared_ptr<TrackInfo> AudioState::sCurrentTrack; |
|
|
|
|
uint64_t AudioState::sCurrentSamples; |
|
|
|
|
bool AudioState::sCurrentTrackIsFromQueue; |
|
|
|
|
StreamCues AudioState::sStreamCues; |
|
|
|
|
|
|
|
|
|
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 {}; |
|
|
|
|
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)) + |
|
|
|
|
current.first->start_offset.value_or(0); |
|
|
|
|
} |
|
|
|
|
return sCurrentSamples / |
|
|
|
|
(sDrainFormat->num_channels * sDrainFormat->sample_rate); |
|
|
|
|
|
|
|
|
|
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 = {}, |
|
|
|
|
.transition = SetTrack::Transition::kHardCut, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
auto current = sServices->track_queue().current(); |
|
|
|
@ -97,20 +103,13 @@ void AudioState::react(const QueueUpdate& ev) { |
|
|
|
|
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: |
|
|
|
@ -123,32 +122,9 @@ void AudioState::react(const QueueUpdate& ev) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioState::react(const SetTrack& ev) { |
|
|
|
|
// Remember the current track if there is one, since we need to preserve some
|
|
|
|
|
// of the state if it turns out this SetTrack event corresponds to seeking
|
|
|
|
|
// within the current track.
|
|
|
|
|
std::string prev_uri; |
|
|
|
|
bool prev_from_queue = false; |
|
|
|
|
if (sCurrentTrack) { |
|
|
|
|
prev_uri = sCurrentTrack->uri; |
|
|
|
|
prev_from_queue = sCurrentTrackIsFromQueue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
sDecoder->open({}); |
|
|
|
|
awaitEmptyDrainBuffer(); |
|
|
|
|
sCurrentTrack.reset(); |
|
|
|
|
sDrainFormat.reset(); |
|
|
|
|
sCurrentSamples = 0; |
|
|
|
|
sCurrentTrackIsFromQueue = false; |
|
|
|
|
transit<states::Standby>(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -166,81 +142,62 @@ void AudioState::react(const SetTrack& ev) { |
|
|
|
|
sStreamFactory->create(std::get<std::string>(new_track), seek_to); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// This was a seek or replay within the same track; don't forget where
|
|
|
|
|
// the track originally came from.
|
|
|
|
|
// FIXME:
|
|
|
|
|
// sNextTrackIsFromQueue = prev_from_queue;
|
|
|
|
|
sDecoder->open(stream); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioState::react(const TogglePlayPause& ev) { |
|
|
|
|
sIsPaused = !ev.set_to.value_or(sIsPaused); |
|
|
|
|
if (!sIsPaused && is_in_state<states::Standby>() && sCurrentTrack) { |
|
|
|
|
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 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::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 (!current) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
auto db = sServices->database().lock(); |
|
|
|
|
if (!db) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
auto path = db->getTrackPath(*current); |
|
|
|
|
if (!path) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if (*path == ev.track->uri) { |
|
|
|
|
queue.finish(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (sCurrentTrack) { |
|
|
|
|
PlaybackUpdate event{ |
|
|
|
|
.current_track = sCurrentTrack, |
|
|
|
|
.track_position = currentPositionSeconds(), |
|
|
|
|
.paused = !is_in_state<states::Playback>(), |
|
|
|
|
}; |
|
|
|
|
events::System().Dispatch(event); |
|
|
|
|
events::Ui().Dispatch(event); |
|
|
|
|
} |
|
|
|
|
sStreamCues.addCue(ev.track, ev.cue_at_sample); |
|
|
|
|
|
|
|
|
|
if (sCurrentTrack && !sIsPaused && !is_in_state<states::Playback>()) { |
|
|
|
|
ESP_LOGI(kTag, "ready to play!"); |
|
|
|
|
if (!sIsPaused && !is_in_state<states::Playback>()) { |
|
|
|
|
transit<states::Playback>(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioState::react(const internal::StreamEnded& ev) { |
|
|
|
|
sStreamCues.addCue({}, ev.cue_at_sample); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioState::react(const system_fsm::BluetoothEvent& ev) { |
|
|
|
|
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { |
|
|
|
|
return; |
|
|
|
@ -276,14 +233,6 @@ void AudioState::react(const StepDownVolume& ev) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioState::react(const system_fsm::HasPhonesChanged& ev) { |
|
|
|
|
if (ev.has_headphones) { |
|
|
|
|
ESP_LOGI(kTag, "headphones in!"); |
|
|
|
|
} else { |
|
|
|
|
ESP_LOGI(kTag, "headphones out!"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioState::react(const SetVolume& ev) { |
|
|
|
|
if (ev.db.has_value()) { |
|
|
|
|
if (sOutput->SetVolumeDb(ev.db.value())) { |
|
|
|
@ -354,43 +303,6 @@ void AudioState::react(const OutputModeChanged& ev) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
sDecoder->open({}); |
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
} |
|
|
|
|
} 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 { |
|
|
|
|
auto mode = sServices->nvs().OutputMode(); |
|
|
|
|
auto vol = sOutput->GetVolume(); |
|
|
|
@ -455,7 +367,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { |
|
|
|
|
.left_bias = nvs.AmpLeftBias(), |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
sSampleProcessor.reset(new SampleProcessor()); |
|
|
|
|
sSampleProcessor.reset(new SampleProcessor(sDrainBuffer)); |
|
|
|
|
sSampleProcessor->SetOutput(sOutput); |
|
|
|
|
|
|
|
|
|
sDecoder.reset(Decoder::Start(sSampleProcessor)); |
|
|
|
@ -470,7 +382,8 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { |
|
|
|
|
if (!ev.locking) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
sServices->bg_worker().Dispatch<void>([this]() { |
|
|
|
|
auto current = sStreamCues.current(); |
|
|
|
|
sServices->bg_worker().Dispatch<void>([=]() { |
|
|
|
|
auto db = sServices->database().lock(); |
|
|
|
|
if (!db) { |
|
|
|
|
return; |
|
|
|
@ -483,10 +396,13 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { |
|
|
|
|
} |
|
|
|
|
db->put(kQueueKey, queue.serialise()); |
|
|
|
|
|
|
|
|
|
if (sCurrentTrack) { |
|
|
|
|
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{sCurrentTrack->uri}, |
|
|
|
|
cppbor::Uint{currentPositionSeconds().value_or(0)}, |
|
|
|
|
cppbor::Tstr{current.first->uri}, |
|
|
|
|
cppbor::Uint{seconds}, |
|
|
|
|
}; |
|
|
|
|
db->put(kCurrentFileKey, current_track.toString()); |
|
|
|
|
} |
|
|
|
@ -521,7 +437,6 @@ void Standby::react(const system_fsm::SdStateChanged& ev) { |
|
|
|
|
events::Audio().Dispatch(SetTrack{ |
|
|
|
|
.new_track = filename, |
|
|
|
|
.seek_to_second = pos, |
|
|
|
|
.transition = SetTrack::Transition::kHardCut, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -537,32 +452,29 @@ void Standby::react(const system_fsm::SdStateChanged& ev) { |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static TimerHandle_t sHeartbeatTimer; |
|
|
|
|
|
|
|
|
|
static void heartbeat(TimerHandle_t) { |
|
|
|
|
events::Audio().Dispatch(internal::StreamHeartbeat{}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Playback::entry() { |
|
|
|
|
ESP_LOGI(kTag, "audio output resumed"); |
|
|
|
|
sOutput->mode(IAudioOutput::Modes::kOnPlaying); |
|
|
|
|
emitPlaybackUpdate(false); |
|
|
|
|
|
|
|
|
|
PlaybackUpdate event{ |
|
|
|
|
.current_track = sCurrentTrack, |
|
|
|
|
.track_position = currentPositionSeconds(), |
|
|
|
|
.paused = false, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
events::System().Dispatch(event); |
|
|
|
|
events::Ui().Dispatch(event); |
|
|
|
|
if (!sHeartbeatTimer) { |
|
|
|
|
sHeartbeatTimer = |
|
|
|
|
xTimerCreate("stream", pdMS_TO_TICKS(250), true, NULL, heartbeat); |
|
|
|
|
} |
|
|
|
|
xTimerStart(sHeartbeatTimer, portMAX_DELAY); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Playback::exit() { |
|
|
|
|
ESP_LOGI(kTag, "audio output paused"); |
|
|
|
|
xTimerStop(sHeartbeatTimer, portMAX_DELAY); |
|
|
|
|
sOutput->mode(IAudioOutput::Modes::kOnPaused); |
|
|
|
|
|
|
|
|
|
PlaybackUpdate event{ |
|
|
|
|
.current_track = sCurrentTrack, |
|
|
|
|
.track_position = currentPositionSeconds(), |
|
|
|
|
.paused = true, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
events::System().Dispatch(event); |
|
|
|
|
events::Ui().Dispatch(event); |
|
|
|
|
emitPlaybackUpdate(true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Playback::react(const system_fsm::SdStateChanged& ev) { |
|
|
|
@ -571,6 +483,17 @@ void Playback::react(const system_fsm::SdStateChanged& ev) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Playback::react(const internal::StreamHeartbeat& ev) { |
|
|
|
|
sStreamCues.update(sOutput->samplesUsed()); |
|
|
|
|
auto current = sStreamCues.current(); |
|
|
|
|
|
|
|
|
|
if (!current.first) { |
|
|
|
|
transit<Standby>(); |
|
|
|
|
} else { |
|
|
|
|
emitPlaybackUpdate(false); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} // namespace states
|
|
|
|
|
|
|
|
|
|
} // namespace audio
|
|
|
|
|