Clear the drain buffer when skipping between tracks

custom
jacqueline 1 year ago
parent 9fca08f843
commit 173b09b015
  1. 53
      src/audio/audio_fsm.cpp
  2. 2
      src/audio/bt_audio_output.cpp
  3. 2
      src/audio/i2s_audio_output.cpp
  4. 8
      src/audio/include/audio_events.hpp
  5. 7
      src/audio/include/audio_fsm.hpp
  6. 17
      src/audio/include/audio_sink.hpp
  7. 5
      src/audio/include/bt_audio_output.hpp
  8. 5
      src/audio/include/i2s_audio_output.hpp
  9. 3
      src/audio/include/track_queue.hpp
  10. 43
      src/audio/track_queue.cpp

@ -51,6 +51,10 @@ std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput; 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.
constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4;
StreamBufferHandle_t AudioState::sDrainBuffer;
std::optional<database::TrackId> AudioState::sCurrentTrack; std::optional<database::TrackId> AudioState::sCurrentTrack;
bool AudioState::sIsPlaybackAllowed; bool AudioState::sIsPlaybackAllowed;
@ -129,7 +133,7 @@ void AudioState::react(const SetVolumeBalance& ev) {
void AudioState::react(const OutputModeChanged& ev) { void AudioState::react(const OutputModeChanged& ev) {
ESP_LOGI(kTag, "output mode changed"); ESP_LOGI(kTag, "output mode changed");
auto new_mode = sServices->nvs().OutputMode(); auto new_mode = sServices->nvs().OutputMode();
sOutput->SetMode(IAudioOutput::Modes::kOff); sOutput->mode(IAudioOutput::Modes::kOff);
switch (new_mode) { switch (new_mode) {
case drivers::NvsStorage::Output::kBluetooth: case drivers::NvsStorage::Output::kBluetooth:
sOutput = sBtOutput; sOutput = sBtOutput;
@ -138,7 +142,7 @@ void AudioState::react(const OutputModeChanged& ev) {
sOutput = sI2SOutput; sOutput = sI2SOutput;
break; break;
} }
sOutput->SetMode(IAudioOutput::Modes::kOnPaused); sOutput->mode(IAudioOutput::Modes::kOnPaused);
sSampleConverter->SetOutput(sOutput); sSampleConverter->SetOutput(sOutput);
// Bluetooth volume isn't 'changed' until we've connected to a device. // Bluetooth volume isn't 'changed' until we've connected to a device.
@ -150,6 +154,32 @@ 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.
sFileSource->SetPath();
auto mode = sOutput->mode();
if (mode == IAudioOutput::Modes::kOnPlaying) {
// If we're currently playing, then the drain buffer will be actively
// draining on its own. Just keep trying to reset until it works.
while (xStreamBufferReset(sDrainBuffer) != pdPASS) {
}
} 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::playTrack(database::TrackId id) -> void { auto AudioState::playTrack(database::TrackId id) -> void {
sCurrentTrack = id; sCurrentTrack = id;
sServices->bg_worker().Dispatch<void>([=]() { sServices->bg_worker().Dispatch<void>([=]() {
@ -194,10 +224,6 @@ void AudioState::react(const TogglePlayPause& ev) {
namespace states { namespace states {
// Two seconds of samples for two channels, at a representative sample rate.
constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4;
static StreamBufferHandle_t sDrainBuffer;
void Uninitialised::react(const system_fsm::BootComplete& ev) { void Uninitialised::react(const system_fsm::BootComplete& ev) {
sServices = ev.services; sServices = ev.services;
@ -229,7 +255,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
} else { } else {
sOutput = sBtOutput; sOutput = sBtOutput;
} }
sOutput->SetMode(IAudioOutput::Modes::kOnPaused); sOutput->mode(IAudioOutput::Modes::kOnPaused);
events::Ui().Dispatch(VolumeLimitChanged{ events::Ui().Dispatch(VolumeLimitChanged{
.new_limit_db = .new_limit_db =
@ -272,6 +298,7 @@ void Standby::react(const QueueUpdate& ev) {
if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) {
return; return;
} }
clearDrainBuffer();
playTrack(*current_track); playTrack(*current_track);
} }
@ -315,7 +342,7 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
void Playback::entry() { void Playback::entry() {
ESP_LOGI(kTag, "beginning playback"); ESP_LOGI(kTag, "beginning playback");
sOutput->SetMode(IAudioOutput::Modes::kOnPlaying); sOutput->mode(IAudioOutput::Modes::kOnPlaying);
events::System().Dispatch(PlaybackStarted{}); events::System().Dispatch(PlaybackStarted{});
events::Ui().Dispatch(PlaybackStarted{}); events::Ui().Dispatch(PlaybackStarted{});
@ -323,10 +350,10 @@ void Playback::entry() {
void Playback::exit() { void Playback::exit() {
ESP_LOGI(kTag, "finishing playback"); ESP_LOGI(kTag, "finishing playback");
sOutput->SetMode(IAudioOutput::Modes::kOnPaused); sOutput->mode(IAudioOutput::Modes::kOnPaused);
// Stash the current volume now, in case it changed during playback, since we // Stash the current volume now, in case it changed during playback, since
// might be powering off soon. // we might be powering off soon.
commitVolume(); commitVolume();
events::System().Dispatch(PlaybackStopped{}); events::System().Dispatch(PlaybackStopped{});
@ -343,6 +370,10 @@ void Playback::react(const QueueUpdate& ev) {
if (!ev.current_changed) { if (!ev.current_changed) {
return; return;
} }
// Cut the current track immediately.
if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) {
clearDrainBuffer();
}
auto current_track = sServices->track_queue().current(); auto current_track = sServices->track_queue().current();
if (!current_track) { if (!current_track) {
sFileSource->SetPath(); sFileSource->SetPath();

@ -35,7 +35,7 @@ BluetoothAudioOutput::BluetoothAudioOutput(StreamBufferHandle_t s,
BluetoothAudioOutput::~BluetoothAudioOutput() {} BluetoothAudioOutput::~BluetoothAudioOutput() {}
auto BluetoothAudioOutput::SetMode(Modes mode) -> void { auto BluetoothAudioOutput::changeMode(Modes mode) -> void {
if (mode == Modes::kOnPlaying) { if (mode == Modes::kOnPlaying) {
bluetooth_.SetSource(stream()); bluetooth_.SetSource(stream());
} else { } else {

@ -58,7 +58,7 @@ I2SAudioOutput::~I2SAudioOutput() {
dac_->SetSource(nullptr); dac_->SetSource(nullptr);
} }
auto I2SAudioOutput::SetMode(Modes mode) -> void { auto I2SAudioOutput::changeMode(Modes mode) -> void {
if (mode == current_mode_) { if (mode == current_mode_) {
return; return;
} }

@ -14,7 +14,6 @@
#include "tinyfsm.hpp" #include "tinyfsm.hpp"
#include "track.hpp" #include "track.hpp"
#include "track_queue.hpp"
#include "types.hpp" #include "types.hpp"
namespace audio { namespace audio {
@ -39,6 +38,13 @@ struct PlaybackStopped : tinyfsm::Event {};
struct QueueUpdate : tinyfsm::Event { struct QueueUpdate : tinyfsm::Event {
bool current_changed; bool current_changed;
enum Reason {
kExplicitUpdate,
kRepeatingLastTrack,
kTrackFinished,
};
Reason reason;
}; };
struct PlayFile : tinyfsm::Event { struct PlayFile : tinyfsm::Event {

@ -52,7 +52,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
void react(const OutputModeChanged&); void react(const OutputModeChanged&);
virtual void react(const system_fsm::BootComplete&) {} virtual void react(const system_fsm::BootComplete&) {}
virtual void react(const system_fsm::KeyLockChanged&) {}; virtual void react(const system_fsm::KeyLockChanged&){};
virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::StorageMounted&) {}
virtual void react(const system_fsm::BluetoothEvent&); virtual void react(const system_fsm::BluetoothEvent&);
@ -67,6 +67,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const internal::AudioPipelineIdle&) {} virtual void react(const internal::AudioPipelineIdle&) {}
protected: protected:
auto clearDrainBuffer() -> void;
auto playTrack(database::TrackId id) -> void; auto playTrack(database::TrackId id) -> void;
auto commitVolume() -> void; auto commitVolume() -> void;
@ -79,6 +80,8 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::shared_ptr<BluetoothAudioOutput> sBtOutput; static std::shared_ptr<BluetoothAudioOutput> sBtOutput;
static std::shared_ptr<IAudioOutput> sOutput; static std::shared_ptr<IAudioOutput> sOutput;
static StreamBufferHandle_t sDrainBuffer;
static std::optional<database::TrackId> sCurrentTrack; static std::optional<database::TrackId> sCurrentTrack;
auto readyToPlay() -> bool; auto readyToPlay() -> bool;
@ -91,7 +94,7 @@ class Uninitialised : public AudioState {
public: public:
void react(const system_fsm::BootComplete&) override; void react(const system_fsm::BootComplete&) override;
void react(const system_fsm::BluetoothEvent&) override {}; void react(const system_fsm::BluetoothEvent&) override{};
using AudioState::react; using AudioState::react;
}; };

@ -27,7 +27,8 @@ class IAudioOutput {
StreamBufferHandle_t stream_; StreamBufferHandle_t stream_;
public: public:
IAudioOutput(StreamBufferHandle_t stream) : stream_(stream) {} IAudioOutput(StreamBufferHandle_t stream)
: stream_(stream), mode_(Modes::kOff) {}
virtual ~IAudioOutput() {} virtual ~IAudioOutput() {}
@ -41,7 +42,14 @@ class IAudioOutput {
* Indicates whether this output is currently being sent samples. If this is * Indicates whether this output is currently being sent samples. If this is
* false, the output should place itself into a low power state. * false, the output should place itself into a low power state.
*/ */
virtual auto SetMode(Modes) -> void = 0; auto mode(Modes m) -> void {
if (mode_ == m) {
return;
}
changeMode(m);
mode_ = m;
}
auto mode() -> Modes { return mode_; }
virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0; virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0;
@ -67,6 +75,11 @@ class IAudioOutput {
virtual auto Configure(const Format& format) -> void = 0; virtual auto Configure(const Format& format) -> void = 0;
auto stream() -> StreamBufferHandle_t { return stream_; } auto stream() -> StreamBufferHandle_t { return stream_; }
protected:
Modes mode_;
virtual auto changeMode(Modes new_mode) -> void = 0;
}; };
} // namespace audio } // namespace audio

@ -28,8 +28,6 @@ class BluetoothAudioOutput : public IAudioOutput {
tasks::WorkerPool&); tasks::WorkerPool&);
~BluetoothAudioOutput(); ~BluetoothAudioOutput();
auto SetMode(Modes) -> void override;
auto SetVolumeImbalance(int_fast8_t balance) -> void override; auto SetVolumeImbalance(int_fast8_t balance) -> void override;
auto SetVolume(uint16_t) -> void override; auto SetVolume(uint16_t) -> void override;
@ -48,6 +46,9 @@ class BluetoothAudioOutput : public IAudioOutput {
BluetoothAudioOutput(const BluetoothAudioOutput&) = delete; BluetoothAudioOutput(const BluetoothAudioOutput&) = delete;
BluetoothAudioOutput& operator=(const BluetoothAudioOutput&) = delete; BluetoothAudioOutput& operator=(const BluetoothAudioOutput&) = delete;
protected:
auto changeMode(Modes) -> void override;
private: private:
drivers::Bluetooth& bluetooth_; drivers::Bluetooth& bluetooth_;
tasks::WorkerPool& bg_worker_; tasks::WorkerPool& bg_worker_;

@ -23,8 +23,6 @@ class I2SAudioOutput : public IAudioOutput {
I2SAudioOutput(StreamBufferHandle_t, drivers::IGpios& expander); I2SAudioOutput(StreamBufferHandle_t, drivers::IGpios& expander);
~I2SAudioOutput(); ~I2SAudioOutput();
auto SetMode(Modes) -> void override;
auto SetMaxVolume(uint16_t) -> void; auto SetMaxVolume(uint16_t) -> void;
auto SetVolumeDb(uint16_t) -> void; auto SetVolumeDb(uint16_t) -> void;
@ -46,6 +44,9 @@ class I2SAudioOutput : public IAudioOutput {
I2SAudioOutput(const I2SAudioOutput&) = delete; I2SAudioOutput(const I2SAudioOutput&) = delete;
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete; I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
protected:
auto changeMode(Modes) -> void override;
private: private:
drivers::IGpios& expander_; drivers::IGpios& expander_;
std::unique_ptr<drivers::I2SDac> dac_; std::unique_ptr<drivers::I2SDac> dac_;

@ -12,6 +12,7 @@
#include <shared_mutex> #include <shared_mutex>
#include <vector> #include <vector>
#include "audio_events.hpp"
#include "cppbor_parse.h" #include "cppbor_parse.h"
#include "database.hpp" #include "database.hpp"
#include "tasks.hpp" #include "tasks.hpp"
@ -120,6 +121,8 @@ class TrackQueue {
TrackQueue& operator=(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete;
private: private:
auto next(QueueUpdate::Reason r) -> void;
mutable std::shared_mutex mutex_; mutable std::shared_mutex mutex_;
tasks::WorkerPool& bg_worker_; tasks::WorkerPool& bg_worker_;

@ -33,6 +33,8 @@ namespace audio {
[[maybe_unused]] static constexpr char kTag[] = "tracks"; [[maybe_unused]] static constexpr char kTag[] = "tracks";
using Reason = QueueUpdate::Reason;
RandomIterator::RandomIterator() RandomIterator::RandomIterator()
: seed_(0), pos_(0), size_(0), replay_(false) {} : seed_(0), pos_(0), size_(0), replay_(false) {}
@ -72,8 +74,11 @@ auto RandomIterator::replay(bool r) -> void {
replay_ = r; replay_ = r;
} }
auto notifyChanged(bool current_changed) -> void { auto notifyChanged(bool current_changed, Reason reason) -> void {
QueueUpdate ev{.current_changed = current_changed}; QueueUpdate ev{
.current_changed = current_changed,
.reason = reason,
};
events::Ui().Dispatch(ev); events::Ui().Dispatch(ev);
events::Audio().Dispatch(ev); events::Audio().Dispatch(ev);
} }
@ -157,7 +162,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
update_shuffler(); update_shuffler();
} }
} }
notifyChanged(current_changed); notifyChanged(current_changed, Reason::kExplicitUpdate);
} else if (std::holds_alternative<database::TrackIterator>(i)) { } else if (std::holds_alternative<database::TrackIterator>(i)) {
// Iterators can be very large, and retrieving items from them often // Iterators can be very large, and retrieving items from them often
// requires disk i/o. Handle them asynchronously so that inserting them // requires disk i/o. Handle them asynchronously so that inserting them
@ -185,7 +190,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
update_shuffler(); update_shuffler();
} }
notifyChanged(current_changed); notifyChanged(current_changed, Reason::kExplicitUpdate);
}); });
} }
} }
@ -200,6 +205,10 @@ auto TrackQueue::append(Item i) -> void {
} }
auto TrackQueue::next() -> void { auto TrackQueue::next() -> void {
next(Reason::kExplicitUpdate);
}
auto TrackQueue::next(Reason r) -> void {
bool changed = true; bool changed = true;
{ {
@ -221,7 +230,7 @@ auto TrackQueue::next() -> void {
} }
} }
notifyChanged(changed); notifyChanged(changed, r);
} }
auto TrackQueue::previous() -> void { auto TrackQueue::previous() -> void {
@ -245,22 +254,22 @@ auto TrackQueue::previous() -> void {
} }
} }
notifyChanged(changed); notifyChanged(changed, Reason::kExplicitUpdate);
} }
auto TrackQueue::finish() -> void { auto TrackQueue::finish() -> void {
if (repeat_) { if (repeat_) {
notifyChanged(true); notifyChanged(true, Reason::kRepeatingLastTrack);
} else { } else {
next(); next(Reason::kTrackFinished);
} }
} }
auto TrackQueue::skipTo(database::TrackId id) -> void { auto TrackQueue::skipTo(database::TrackId id) -> void {
// Defer this work to the background not because it's particularly // Defer this work to the background not because it's particularly
// long-running (although it could be), but because we want to ensure we only // long-running (although it could be), but because we want to ensure we
// search for the given id after any previously pending iterator insertions // only search for the given id after any previously pending iterator
// have finished. // insertions have finished.
bg_worker_.Dispatch<void>([=, this]() { bg_worker_.Dispatch<void>([=, this]() {
bool found = false; bool found = false;
{ {
@ -274,7 +283,7 @@ auto TrackQueue::skipTo(database::TrackId id) -> void {
} }
} }
if (found) { if (found) {
notifyChanged(true); notifyChanged(true, Reason::kExplicitUpdate);
} }
}); });
} }
@ -294,7 +303,7 @@ auto TrackQueue::clear() -> void {
} }
} }
notifyChanged(true); notifyChanged(true, Reason::kExplicitUpdate);
} }
auto TrackQueue::random(bool en) -> void { auto TrackQueue::random(bool en) -> void {
@ -311,7 +320,7 @@ auto TrackQueue::random(bool en) -> void {
} }
// Current track doesn't get randomised until next(). // Current track doesn't get randomised until next().
notifyChanged(false); notifyChanged(false, Reason::kExplicitUpdate);
} }
auto TrackQueue::random() const -> bool { auto TrackQueue::random() const -> bool {
@ -325,7 +334,7 @@ auto TrackQueue::repeat(bool en) -> void {
repeat_ = en; repeat_ = en;
} }
notifyChanged(false); notifyChanged(false, Reason::kExplicitUpdate);
} }
auto TrackQueue::repeat() const -> bool { auto TrackQueue::repeat() const -> bool {
@ -341,7 +350,7 @@ auto TrackQueue::replay(bool en) -> void {
shuffle_->replay(en); shuffle_->replay(en);
} }
} }
notifyChanged(false); notifyChanged(false, Reason::kExplicitUpdate);
} }
auto TrackQueue::replay() const -> bool { auto TrackQueue::replay() const -> bool {
@ -477,7 +486,7 @@ auto TrackQueue::deserialise(const std::string& s) -> void {
QueueParseClient client{*this}; QueueParseClient client{*this};
const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data()); const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data());
cppbor::parse(data, data + s.size(), &client); cppbor::parse(data, data + s.size(), &client);
notifyChanged(true); notifyChanged(true, Reason::kExplicitUpdate);
} }
} // namespace audio } // namespace audio

Loading…
Cancel
Save