summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/CMakeLists.txt2
-rw-r--r--src/audio/audio_decoder.cpp14
-rw-r--r--src/audio/audio_fsm.cpp82
-rw-r--r--src/audio/audio_task.cpp14
-rw-r--r--src/audio/fatfs_audio_input.cpp20
-rw-r--r--src/audio/include/audio_decoder.hpp3
-rw-r--r--src/audio/include/audio_events.hpp21
-rw-r--r--src/audio/include/audio_fsm.hpp33
-rw-r--r--src/audio/include/fatfs_audio_input.hpp2
-rw-r--r--src/audio/include/stream_info.hpp9
-rw-r--r--src/audio/include/track_queue.hpp85
-rw-r--r--src/audio/stream_info.cpp4
-rw-r--r--src/audio/track_queue.cpp128
13 files changed, 343 insertions, 74 deletions
diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index 2e085306..38e367aa 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -4,7 +4,7 @@
idf_component_register(
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp"
- "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp"
+ "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp"
"stream_event.cpp" "pipeline.cpp" "stream_info.cpp" "audio_fsm.cpp"
INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm")
diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp
index 966a8c37..b8054574 100644
--- a/src/audio/audio_decoder.cpp
+++ b/src/audio/audio_decoder.cpp
@@ -53,7 +53,7 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
}
const auto& new_format = std::get<StreamInfo::Encoded>(info.format);
- current_input_format_ = info.format;
+ current_input_format_ = new_format;
ESP_LOGI(kTag, "creating new decoder");
auto result = codecs::CreateCodecForType(new_format.type);
@@ -112,6 +112,15 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
.sample_rate = format.sample_rate_hz,
};
+ if (format.duration_seconds) {
+ duration_seconds_from_decoder_ = format.duration_seconds;
+ } else if (format.bits_per_second &&
+ current_input_format_->duration_bytes) {
+ duration_seconds_from_decoder_ =
+ (current_input_format_->duration_bytes.value() - res.first) * 8 /
+ format.bits_per_second.value() / format.num_channels;
+ }
+
if (info.seek_to_seconds) {
seek_to_sample_ = *info.seek_to_seconds * format.sample_rate_hz;
} else {
@@ -144,6 +153,9 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
if (!has_prepared_output_ && !output->prepare(*current_output_format_)) {
return;
}
+ if (duration_seconds_from_decoder_) {
+ output->set_duration(*duration_seconds_from_decoder_);
+ }
has_prepared_output_ = true;
// Parse frames and produce samples.
diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp
index 805dffc4..5f4f8783 100644
--- a/src/audio/audio_fsm.cpp
+++ b/src/audio/audio_fsm.cpp
@@ -19,6 +19,7 @@
#include "pipeline.hpp"
#include "system_events.hpp"
#include "track.hpp"
+#include "track_queue.hpp"
namespace audio {
@@ -32,11 +33,13 @@ std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
-std::deque<AudioState::EnqueuedItem> AudioState::sTrackQueue;
+TrackQueue* AudioState::sTrackQueue;
auto AudioState::Init(drivers::IGpios* gpio_expander,
- std::weak_ptr<database::Database> database) -> bool {
+ std::weak_ptr<database::Database> database,
+ TrackQueue* queue) -> bool {
sIGpios = gpio_expander;
+ sTrackQueue = queue;
auto dac = drivers::I2SDac::create(gpio_expander);
if (!dac) {
@@ -94,26 +97,23 @@ void Uninitialised::react(const system_fsm::BootComplete&) {
transit<Standby>();
}
-void Standby::react(const InputFileOpened& ev) {
+void Standby::react(const internal::InputFileOpened& ev) {
transit<Playback>();
}
-void Standby::react(const PlayTrack& ev) {
+void Standby::react(const QueueUpdate& ev) {
+ auto current_track = sTrackQueue->GetCurrent();
+ if (!current_track) {
+ return;
+ }
+
auto db = sDatabase.lock();
if (!db) {
ESP_LOGW(kTag, "database not open; ignoring play request");
return;
}
- if (ev.data) {
- sFileSource->OpenFile(ev.data->filepath());
- } else {
- sFileSource->OpenFile(db->GetTrackPath(ev.id));
- }
-}
-
-void Standby::react(const PlayFile& ev) {
- sFileSource->OpenFile(ev.filename);
+ sFileSource->OpenFile(db->GetTrackPath(*current_track));
}
void Playback::entry() {
@@ -126,42 +126,50 @@ void Playback::exit() {
sI2SOutput->SetInUse(false);
}
-void Playback::react(const PlayTrack& ev) {
- sTrackQueue.push_back(EnqueuedItem(ev.id));
-}
+void Playback::react(const QueueUpdate& ev) {
+ auto current_track = sTrackQueue->GetCurrent();
+ if (!current_track) {
+ // TODO: return to standby?
+ return;
+ }
-void Playback::react(const PlayFile& ev) {
- sTrackQueue.push_back(EnqueuedItem(ev.filename));
+ auto db = sDatabase.lock();
+ if (!db) {
+ return;
+ }
+
+ // TODO: what if we just finished this, and are preemptively loading the next
+ // one?
+ sFileSource->OpenFile(db->GetTrackPath(*current_track));
}
void Playback::react(const PlaybackUpdate& ev) {
- ESP_LOGI(kTag, "elapsed: %lu", ev.seconds_elapsed);
+ ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
+ ev.seconds_total);
}
-void Playback::react(const InputFileOpened& ev) {}
+void Playback::react(const internal::InputFileOpened& ev) {}
-void Playback::react(const InputFileFinished& ev) {
- ESP_LOGI(kTag, "finished file");
- if (sTrackQueue.empty()) {
+void Playback::react(const internal::InputFileClosed& ev) {
+ ESP_LOGI(kTag, "finished reading file");
+ auto upcoming = sTrackQueue->GetUpcoming(1);
+ if (upcoming.empty()) {
return;
}
- EnqueuedItem next_item = sTrackQueue.front();
- sTrackQueue.pop_front();
-
- if (std::holds_alternative<std::string>(next_item)) {
- sFileSource->OpenFile(std::get<std::string>(next_item));
- } else if (std::holds_alternative<database::TrackId>(next_item)) {
- auto db = sDatabase.lock();
- if (!db) {
- ESP_LOGW(kTag, "database not open; ignoring play request");
- return;
- }
- sFileSource->OpenFile(
- db->GetTrackPath(std::get<database::TrackId>(next_item)));
+ auto db = sDatabase.lock();
+ if (!db) {
+ return;
}
+ ESP_LOGI(kTag, "preemptively opening next file");
+ sFileSource->OpenFile(db->GetTrackPath(upcoming.front()));
+}
+
+void Playback::react(const internal::InputFileFinished& ev) {
+ ESP_LOGI(kTag, "finished playing file");
+ sTrackQueue->Next();
}
-void Playback::react(const AudioPipelineIdle& ev) {
+void Playback::react(const internal::AudioPipelineIdle& ev) {
transit<Standby>();
}
diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp
index 24bc7be7..babe6849 100644
--- a/src/audio/audio_task.cpp
+++ b/src/audio/audio_task.cpp
@@ -37,6 +37,7 @@
#include "stream_message.hpp"
#include "sys/_stdint.h"
#include "tasks.hpp"
+#include "ui_fsm.hpp"
namespace audio {
@@ -87,7 +88,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
}
if (previously_had_work && !has_work) {
- events::Dispatch<AudioPipelineIdle, AudioState>({});
+ events::Dispatch<internal::AudioPipelineIdle, AudioState>({});
}
previously_had_work = has_work;
@@ -136,6 +137,10 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
if (sink_stream.is_producer_finished()) {
sink_stream.mark_consumer_finished();
+ if (current_second > 0 || current_sample_in_second > 0) {
+ events::Dispatch<internal::InputFileFinished, AudioState>({});
+ }
+
current_second = 0;
previous_second = 0;
current_sample_in_second = 0;
@@ -185,8 +190,11 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
current_sample_in_second -= pcm.sample_rate;
}
if (previous_second != current_second) {
- events::Dispatch<PlaybackUpdate, AudioState>(
- {.seconds_elapsed = current_second});
+ events::Dispatch<PlaybackUpdate, AudioState, ui::UiState>({
+ .seconds_elapsed = current_second,
+ .seconds_total =
+ sink_stream.info().duration_seconds.value_or(current_second),
+ });
}
previous_second = current_second;
}
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
index 894ac842..da605a40 100644
--- a/src/audio/fatfs_audio_input.cpp
+++ b/src/audio/fatfs_audio_input.cpp
@@ -56,6 +56,7 @@ auto FatfsAudioInput::OpenFile(std::future<std::optional<std::string>>&& path)
}
auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
+ current_path_.reset();
if (is_file_open_) {
f_close(&current_file_);
is_file_open_ = false;
@@ -68,6 +69,11 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
ESP_LOGI(kTag, "opening file %s", path.c_str());
+ FILINFO info;
+ if (f_stat(path.c_str(), &info) != FR_OK) {
+ ESP_LOGE(kTag, "failed to stat file");
+ }
+
database::TagParserImpl tag_parser;
database::TrackTags tags;
if (!tag_parser.ReadAndParseTags(path, &tags)) {
@@ -95,7 +101,10 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
.sample_rate = static_cast<uint32_t>(*tags.sample_rate),
};
} else {
- current_format_ = StreamInfo::Encoded{*stream_type};
+ current_format_ = StreamInfo::Encoded{
+ .type = *stream_type,
+ .duration_bytes = info.fsize,
+ };
}
FRESULT res = f_open(&current_file_, path.c_str(), FA_READ);
@@ -104,7 +113,8 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
return false;
}
- events::Dispatch<InputFileOpened, AudioState>({});
+ events::Dispatch<internal::InputFileOpened, AudioState>({});
+ current_path_ = path;
is_file_open_ = true;
return true;
}
@@ -124,9 +134,10 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
if (pending_path_->wait_for(std::chrono::seconds(0)) ==
std::future_status::ready) {
auto result = pending_path_->get();
- if (result) {
+ if (result && result != current_path_) {
OpenFile(*result);
}
+ pending_path_ = {};
}
}
}
@@ -173,10 +184,11 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
f_close(&current_file_);
is_file_open_ = false;
+ current_path_.reset();
has_prepared_output_ = false;
output->mark_producer_finished();
- events::Dispatch<InputFileFinished, AudioState>({});
+ events::Dispatch<internal::InputFileClosed, AudioState>({});
}
}
diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp
index aa051685..a6b4754a 100644
--- a/src/audio/include/audio_decoder.hpp
+++ b/src/audio/include/audio_decoder.hpp
@@ -40,8 +40,9 @@ class AudioDecoder : public IAudioElement {
private:
std::unique_ptr<codecs::ICodec> current_codec_;
- std::optional<StreamInfo::Format> current_input_format_;
+ std::optional<StreamInfo::Encoded> current_input_format_;
std::optional<StreamInfo::Format> current_output_format_;
+ std::optional<std::size_t> duration_seconds_from_decoder_;
std::optional<std::size_t> seek_to_sample_;
bool has_prepared_output_;
bool has_samples_to_send_;
diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp
index 019b65a2..8af3703a 100644
--- a/src/audio/include/audio_events.hpp
+++ b/src/audio/include/audio_events.hpp
@@ -13,26 +13,31 @@
#include "tinyfsm.hpp"
#include "track.hpp"
+#include "track_queue.hpp"
namespace audio {
-struct PlayFile : tinyfsm::Event {
- std::string filename;
-};
-
-struct PlayTrack : tinyfsm::Event {
- database::TrackId id;
- std::optional<database::TrackData> data;
+struct PlaybackStarted : tinyfsm::Event {
+ database::Track track;
};
struct PlaybackUpdate : tinyfsm::Event {
uint32_t seconds_elapsed;
+ uint32_t seconds_total;
};
+struct QueueUpdate : tinyfsm::Event {};
+
+struct VolumeChanged : tinyfsm::Event {};
+
+namespace internal {
+
struct InputFileOpened : tinyfsm::Event {};
+struct InputFileClosed : tinyfsm::Event {};
struct InputFileFinished : tinyfsm::Event {};
+
struct AudioPipelineIdle : tinyfsm::Event {};
-struct VolumeChanged : tinyfsm::Event {};
+} // namespace internal
} // namespace audio
diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp
index dadbd072..7910f4e2 100644
--- a/src/audio/include/audio_fsm.hpp
+++ b/src/audio/include/audio_fsm.hpp
@@ -22,13 +22,15 @@
#include "track.hpp"
#include "system_events.hpp"
+#include "track_queue.hpp"
namespace audio {
class AudioState : public tinyfsm::Fsm<AudioState> {
public:
static auto Init(drivers::IGpios* gpio_expander,
- std::weak_ptr<database::Database>) -> bool;
+ std::weak_ptr<database::Database>,
+ TrackQueue* queue) -> bool;
virtual ~AudioState() {}
@@ -45,14 +47,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
void react(const system_fsm::HasPhonesChanged&);
virtual void react(const system_fsm::BootComplete&) {}
- virtual void react(const PlayTrack&) {}
- virtual void react(const PlayFile&) {}
+ virtual void react(const QueueUpdate&) {}
virtual void react(const PlaybackUpdate&) {}
- virtual void react(const InputFileOpened&) {}
- virtual void react(const InputFileFinished&) {}
- virtual void react(const AudioPipelineIdle&) {}
+ 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:
static drivers::IGpios* sIGpios;
@@ -63,8 +65,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
- typedef std::variant<database::TrackId, std::string> EnqueuedItem;
- static std::deque<EnqueuedItem> sTrackQueue;
+ static TrackQueue* sTrackQueue;
};
namespace states {
@@ -77,9 +78,8 @@ class Uninitialised : public AudioState {
class Standby : public AudioState {
public:
- void react(const InputFileOpened&) override;
- void react(const PlayTrack&) override;
- void react(const PlayFile&) override;
+ void react(const internal::InputFileOpened&) override;
+ void react(const QueueUpdate&) override;
using AudioState::react;
};
@@ -89,14 +89,13 @@ class Playback : public AudioState {
void entry() override;
void exit() override;
- void react(const PlayTrack&) override;
- void react(const PlayFile&) override;
-
+ void react(const QueueUpdate&) override;
void react(const PlaybackUpdate&) override;
- void react(const InputFileOpened&) override;
- void react(const InputFileFinished&) override;
- void react(const AudioPipelineIdle&) 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/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp
index 77d3b96d..56f92fcf 100644
--- a/src/audio/include/fatfs_audio_input.hpp
+++ b/src/audio/include/fatfs_audio_input.hpp
@@ -34,6 +34,7 @@ class FatfsAudioInput : public IAudioElement {
FatfsAudioInput();
~FatfsAudioInput();
+ auto CurrentFile() -> std::optional<std::string> { return current_path_; }
auto OpenFile(std::future<std::optional<std::string>>&& path) -> void;
auto OpenFile(const std::string& path) -> bool;
@@ -50,6 +51,7 @@ class FatfsAudioInput : public IAudioElement {
-> std::optional<codecs::StreamType>;
std::optional<std::future<std::optional<std::string>>> pending_path_;
+ std::optional<std::string> current_path_;
FIL current_file_;
bool is_file_open_;
bool has_prepared_output_;
diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp
index 4db3e5fd..69bf3c4b 100644
--- a/src/audio/include/stream_info.hpp
+++ b/src/audio/include/stream_info.hpp
@@ -30,13 +30,16 @@ struct StreamInfo {
bool is_consumer_finished = true;
- //
- std::optional<uint32_t> seek_to_seconds{};
+ std::optional<std::uint32_t> duration_seconds;
+
+ std::optional<std::uint32_t> seek_to_seconds{};
struct Encoded {
// The codec that this stream is associated with.
codecs::StreamType type;
+ std::optional<std::size_t> duration_bytes;
+
bool operator==(const Encoded&) const = default;
};
@@ -95,6 +98,8 @@ class OutputStream {
bool prepare(const StreamInfo::Format& new_format);
+ void set_duration(std::size_t);
+
const StreamInfo& info() const;
cpp::span<std::byte> data() const;
diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp
new file mode 100644
index 00000000..840d71ee
--- /dev/null
+++ b/src/audio/include/track_queue.hpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <deque>
+#include <mutex>
+#include <vector>
+
+#include "track.hpp"
+
+namespace audio {
+
+/*
+ * Owns and manages a complete view of the playback queue. Includes the
+ * currently playing track, a truncated list of previously played tracks, and
+ * all future tracks that have been queued.
+ *
+ * In order to not use all of our memory, this class deals strictly with track
+ * ids. Consumers that need more data than this should fetch it from the
+ * database.
+ *
+ * Instances of this class are broadly safe to use from multiple tasks; each
+ * method represents an atomic operation. No guarantees are made about
+ * consistency between calls however. For example, there may be data changes
+ * between consecutive calls to AddNext() and GetUpcoming();
+ */
+class TrackQueue {
+ public:
+ TrackQueue();
+
+ /* Returns the currently playing track. */
+ auto GetCurrent() const -> std::optional<database::TrackId>;
+ /* Returns, in order, tracks that have been queued to be played next. */
+ auto GetUpcoming(std::size_t limit) const -> std::vector<database::TrackId>;
+
+ /*
+ * Enqueues a track, placing it immediately after the current track and
+ * before anything already queued.
+ *
+ * If there is no current track, the given track will begin playback.
+ */
+ auto AddNext(database::TrackId) -> void;
+ auto AddNext(const std::vector<database::TrackId>&) -> void;
+
+ /*
+ * Enqueues a track, placing it the end of all enqueued tracks.
+ *
+ * If there is no current track, the given track will begin playback.
+ */
+ auto AddLast(database::TrackId) -> void;
+ auto AddLast(const std::vector<database::TrackId>&) -> void;
+
+ /*
+ * Advances to the next track in the queue, placing the current track at the
+ * front of the 'played' queue.
+ */
+ auto Next() -> void;
+ auto Previous() -> void;
+
+ /*
+ * Removes all tracks from all queues, and stops any currently playing track.
+ */
+ auto Clear() -> void;
+ /*
+ * Removes a specific track from the queue of upcoming tracks. Has no effect
+ * on the currently playing track.
+ */
+ auto RemoveUpcoming(database::TrackId) -> void;
+
+ TrackQueue(const TrackQueue&) = delete;
+ TrackQueue& operator=(const TrackQueue&) = delete;
+
+ private:
+ mutable std::mutex mutex_;
+
+ std::deque<database::TrackId> played_;
+ std::deque<database::TrackId> upcoming_;
+ std::optional<database::TrackId> current_;
+};
+
+} // namespace audio
diff --git a/src/audio/stream_info.cpp b/src/audio/stream_info.cpp
index 748cb9ef..3927e5f8 100644
--- a/src/audio/stream_info.cpp
+++ b/src/audio/stream_info.cpp
@@ -64,6 +64,10 @@ bool OutputStream::prepare(const StreamInfo::Format& new_format) {
return false;
}
+void OutputStream::set_duration(std::size_t seconds) {
+ raw_->info->duration_seconds = seconds;
+}
+
const StreamInfo& OutputStream::info() const {
return *raw_->info;
}
diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp
new file mode 100644
index 00000000..1c233f8f
--- /dev/null
+++ b/src/audio/track_queue.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "track_queue.hpp"
+
+#include <algorithm>
+#include <mutex>
+
+#include "audio_events.hpp"
+#include "audio_fsm.hpp"
+#include "event_queue.hpp"
+#include "track.hpp"
+#include "ui_fsm.hpp"
+
+namespace audio {
+
+TrackQueue::TrackQueue() {}
+
+auto TrackQueue::GetCurrent() const -> std::optional<database::TrackId> {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ return current_;
+}
+
+auto TrackQueue::GetUpcoming(std::size_t limit) const
+ -> std::vector<database::TrackId> {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ std::vector<database::TrackId> ret;
+ limit = std::min(limit, upcoming_.size());
+ std::for_each_n(upcoming_.begin(), limit,
+ [&](const auto i) { ret.push_back(i); });
+ return ret;
+}
+
+auto TrackQueue::AddNext(database::TrackId t) -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ if (!current_) {
+ current_ = t;
+ } else {
+ upcoming_.push_front(t);
+ }
+
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::AddNext(const std::vector<database::TrackId>& t) -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ std::for_each(t.rbegin(), t.rend(),
+ [&](const auto i) { upcoming_.push_front(i); });
+ if (!current_) {
+ current_ = upcoming_.front();
+ upcoming_.pop_front();
+ }
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::AddLast(database::TrackId t) -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ if (!current_) {
+ current_ = t;
+ } else {
+ upcoming_.push_back(t);
+ }
+
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::AddLast(const std::vector<database::TrackId>& t) -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ std::for_each(t.begin(), t.end(),
+ [&](const auto i) { upcoming_.push_back(i); });
+ if (!current_) {
+ current_ = upcoming_.front();
+ upcoming_.pop_front();
+ }
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::Next() -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ if (current_) {
+ played_.push_front(*current_);
+ }
+ if (!upcoming_.empty()) {
+ current_ = upcoming_.front();
+ upcoming_.pop_front();
+ } else {
+ current_.reset();
+ }
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::Previous() -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ if (current_) {
+ upcoming_.push_front(*current_);
+ }
+ if (!played_.empty()) {
+ current_ = played_.front();
+ played_.pop_front();
+ } else {
+ current_.reset();
+ }
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::Clear() -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ played_.clear();
+ upcoming_.clear();
+ current_.reset();
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+auto TrackQueue::RemoveUpcoming(database::TrackId t) -> void {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ for (auto it = upcoming_.begin(); it != upcoming_.end(); it++) {
+ if (*it == t) {
+ upcoming_.erase(it);
+ return;
+ }
+ }
+ events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
+}
+
+} // namespace audio