daniel/playlist-queue (#83)
Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/83 Reviewed-by: cooljqln <cooljqln@noreply.codeberg.org> Co-authored-by: ailurux <ailuruxx@gmail.com> Co-committed-by: ailurux <ailuruxx@gmail.com>
This commit is contained in:
parent
24fde7af0c
commit
0a271d786b
11 changed files with 379 additions and 136 deletions
lib/fatfs/src
src/tangara
audio
system_fsm
test
ui
test
|
@ -58,7 +58,7 @@
|
|||
/* This option switches f_forward() function. (0:Disable or 1:Enable) */
|
||||
|
||||
|
||||
#define FF_USE_STRFUNC 0
|
||||
#define FF_USE_STRFUNC 1
|
||||
#define FF_PRINT_LLI 0
|
||||
#define FF_PRINT_FLOAT 0
|
||||
#define FF_STRF_ENCODE 3
|
||||
|
|
|
@ -96,9 +96,7 @@ void AudioState::react(const QueueUpdate& ev) {
|
|||
};
|
||||
|
||||
auto current = sServices->track_queue().current();
|
||||
if (current) {
|
||||
cmd.new_track = *current;
|
||||
}
|
||||
cmd.new_track = current;
|
||||
|
||||
switch (ev.reason) {
|
||||
case QueueUpdate::kExplicitUpdate:
|
||||
|
@ -176,18 +174,21 @@ void AudioState::react(const internal::DecodingFinished& ev) {
|
|||
sServices->bg_worker().Dispatch<void>([=]() {
|
||||
auto& queue = sServices->track_queue();
|
||||
auto current = queue.current();
|
||||
if (!current) {
|
||||
if (std::holds_alternative<std::monostate>(current)) {
|
||||
return;
|
||||
}
|
||||
auto db = sServices->database().lock();
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
auto path = db->getTrackPath(*current);
|
||||
if (!path) {
|
||||
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) {
|
||||
if (path == ev.track->uri) {
|
||||
queue.finish();
|
||||
}
|
||||
});
|
||||
|
@ -449,6 +450,9 @@ void Standby::react(const system_fsm::SdStateChanged& ev) {
|
|||
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.
|
||||
|
|
154
src/tangara/audio/playlist.cpp
Normal file
154
src/tangara/audio/playlist.cpp
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright 2024 ailurux <ailuruxx@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
#include "playlist.hpp"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "audio/playlist.hpp"
|
||||
#include "database/database.hpp"
|
||||
#include "esp_log.h"
|
||||
#include "ff.h"
|
||||
|
||||
namespace audio {
|
||||
[[maybe_unused]] static constexpr char kTag[] = "playlist";
|
||||
|
||||
Playlist::Playlist(std::string playlistFilepath)
|
||||
: filepath_(playlistFilepath), mutex_(), total_size_(0), pos_(0) {}
|
||||
|
||||
auto Playlist::open() -> bool {
|
||||
FRESULT res =
|
||||
f_open(&file_, filepath_.c_str(), FA_READ | FA_WRITE | FA_OPEN_ALWAYS);
|
||||
if (res != FR_OK) {
|
||||
ESP_LOGE(kTag, "failed to open file! res: %i", res);
|
||||
return false;
|
||||
}
|
||||
// Count all entries
|
||||
consumeAndCount(-1);
|
||||
// Grab the first one
|
||||
skipTo(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
Playlist::~Playlist() {
|
||||
f_close(&file_);
|
||||
}
|
||||
|
||||
auto Playlist::currentPosition() const -> size_t {
|
||||
return pos_;
|
||||
}
|
||||
|
||||
auto Playlist::size() const -> size_t {
|
||||
return total_size_;
|
||||
}
|
||||
|
||||
auto Playlist::append(Item i) -> void {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
auto offset = f_tell(&file_);
|
||||
// Seek to end and append
|
||||
auto res = f_lseek(&file_, f_size(&file_));
|
||||
if (res != FR_OK) {
|
||||
ESP_LOGE(kTag, "Seek to end of file failed? Error %d", res);
|
||||
return;
|
||||
}
|
||||
// TODO: Resolve paths for track id, etc
|
||||
std::string path;
|
||||
if (std::holds_alternative<std::string>(i)) {
|
||||
path = std::get<std::string>(i);
|
||||
f_printf(&file_, "%s\n", path.c_str());
|
||||
total_size_++;
|
||||
if (current_value_.empty()) {
|
||||
current_value_ = path;
|
||||
}
|
||||
}
|
||||
// Restore position
|
||||
res = f_lseek(&file_, offset);
|
||||
if (res != FR_OK) {
|
||||
ESP_LOGE(kTag, "Failed to restore file position after append?");
|
||||
return;
|
||||
}
|
||||
res = f_sync(&file_);
|
||||
if (res != FR_OK) {
|
||||
ESP_LOGE(kTag, "Failed to sync playlist file after append");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto Playlist::skipTo(size_t position) -> void {
|
||||
pos_ = position;
|
||||
consumeAndCount(position);
|
||||
}
|
||||
|
||||
auto Playlist::next() -> void {
|
||||
if (!atEnd()) {
|
||||
pos_++;
|
||||
skipTo(pos_);
|
||||
}
|
||||
}
|
||||
|
||||
auto Playlist::prev() -> void {
|
||||
// Naive approach to see how that goes for now
|
||||
pos_--;
|
||||
skipTo(pos_);
|
||||
}
|
||||
|
||||
auto Playlist::value() const -> std::string {
|
||||
return current_value_;
|
||||
}
|
||||
|
||||
auto Playlist::clear() -> bool {
|
||||
auto res = f_close(&file_);
|
||||
if (res != FR_OK) {
|
||||
return false;
|
||||
}
|
||||
res =
|
||||
f_open(&file_, filepath_.c_str(), FA_READ | FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (res != FR_OK) {
|
||||
return false;
|
||||
}
|
||||
total_size_ = 0;
|
||||
current_value_.clear();
|
||||
pos_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Playlist::atEnd() -> bool {
|
||||
return pos_ + 1 >= total_size_;
|
||||
}
|
||||
|
||||
auto Playlist::filepath() -> std::string {
|
||||
return filepath_;
|
||||
}
|
||||
|
||||
auto Playlist::consumeAndCount(ssize_t upto) -> bool {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
TCHAR buff[512];
|
||||
size_t count = 0;
|
||||
f_rewind(&file_);
|
||||
while (!f_eof(&file_)) {
|
||||
// TODO: Correctly handle lines longer than this
|
||||
// TODO: Also correctly handle the case where the last entry doesn't end in
|
||||
// \n
|
||||
auto res = f_gets(buff, 512, &file_);
|
||||
if (res == NULL) {
|
||||
ESP_LOGW(kTag, "Error consuming playlist file at line %d", count);
|
||||
return false;
|
||||
}
|
||||
count++;
|
||||
|
||||
if (upto >= 0 && count > upto) {
|
||||
size_t len = strlen(buff);
|
||||
current_value_.assign(buff, len - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (upto < 0) {
|
||||
total_size_ = count;
|
||||
f_rewind(&file_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace audio
|
56
src/tangara/audio/playlist.hpp
Normal file
56
src/tangara/audio/playlist.hpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
/*
|
||||
* Copyright 2024 ailurux <ailuruxx@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include "database/database.hpp"
|
||||
#include "database/track.hpp"
|
||||
#include "ff.h"
|
||||
|
||||
namespace audio {
|
||||
|
||||
/*
|
||||
* Owns and manages a playlist file.
|
||||
* Each line in the playlist file is the absolute filepath of the track to play.
|
||||
* In order to avoid mapping to byte offsets, each line must contain only a
|
||||
* filepath (ie, no comments are supported). This limitation may be removed
|
||||
* later if benchmarks show that the file can be quickly scanned from 'bookmark'
|
||||
* offsets. This is a subset of the m3u format and ideally will be
|
||||
* import/exportable to and from this format, to better support playlists from
|
||||
* beets import and other music management software.
|
||||
*/
|
||||
class Playlist {
|
||||
public:
|
||||
Playlist(std::string playlistFilepath);
|
||||
~Playlist();
|
||||
using Item =
|
||||
std::variant<database::TrackId, database::TrackIterator, std::string>;
|
||||
auto open() -> bool;
|
||||
auto currentPosition() const -> size_t;
|
||||
auto size() const -> size_t;
|
||||
auto append(Item i) -> void;
|
||||
auto skipTo(size_t position) -> void;
|
||||
auto next() -> void;
|
||||
auto prev() -> void;
|
||||
auto value() const -> std::string;
|
||||
auto clear() -> bool;
|
||||
auto atEnd() -> bool;
|
||||
auto filepath() -> std::string;
|
||||
|
||||
private:
|
||||
std::string filepath_;
|
||||
std::mutex mutex_;
|
||||
size_t total_size_;
|
||||
size_t pos_;
|
||||
FIL file_;
|
||||
std::string current_value_;
|
||||
|
||||
auto consumeAndCount(ssize_t upto) -> bool;
|
||||
};
|
||||
|
||||
} // namespace audio
|
|
@ -28,6 +28,7 @@
|
|||
#include "memory_resource.hpp"
|
||||
#include "tasks.hpp"
|
||||
#include "ui/ui_fsm.hpp"
|
||||
#include "track_queue.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
|
@ -83,65 +84,67 @@ auto notifyChanged(bool current_changed, Reason reason) -> void {
|
|||
events::Audio().Dispatch(ev);
|
||||
}
|
||||
|
||||
TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker)
|
||||
TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db)
|
||||
: mutex_(),
|
||||
bg_worker_(bg_worker),
|
||||
pos_(0),
|
||||
tracks_(&memory::kSpiRamResource),
|
||||
db_(db),
|
||||
playlist_("queue.playlist"), // TODO
|
||||
shuffle_(),
|
||||
repeat_(false),
|
||||
replay_(false) {}
|
||||
|
||||
auto TrackQueue::current() const -> std::optional<database::TrackId> {
|
||||
auto TrackQueue::current() const -> TrackItem {
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
if (pos_ >= tracks_.size()) {
|
||||
std::string val = playlist_.value();
|
||||
if (val.empty()) {
|
||||
return {};
|
||||
}
|
||||
return tracks_[pos_];
|
||||
}
|
||||
|
||||
auto TrackQueue::peekNext(std::size_t limit) const
|
||||
-> std::vector<database::TrackId> {
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
std::vector<database::TrackId> out;
|
||||
for (size_t i = pos_ + 1; i < pos_ + limit + 1 && i < tracks_.size(); i++) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto TrackQueue::peekPrevious(std::size_t limit) const
|
||||
-> std::vector<database::TrackId> {
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
std::vector<database::TrackId> out;
|
||||
for (size_t i = pos_ - 1; i < pos_ - limit - 1 && i >= tracks_.size(); i--) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
return val;
|
||||
}
|
||||
|
||||
auto TrackQueue::currentPosition() const -> size_t {
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
return pos_;
|
||||
return playlist_.currentPosition();
|
||||
}
|
||||
|
||||
auto TrackQueue::totalSize() const -> size_t {
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
return tracks_.size();
|
||||
return playlist_.size();
|
||||
}
|
||||
|
||||
auto TrackQueue::open() -> bool {
|
||||
// FIX ME: If playlist opening fails, should probably fall back to a vector of tracks or something
|
||||
// so that we're not necessarily always needing mounted storage
|
||||
return playlist_.open();
|
||||
}
|
||||
|
||||
auto TrackQueue::getFilepath(database::TrackId id) -> std::optional<std::string> {
|
||||
auto db = db_.lock();
|
||||
if (!db) {
|
||||
return {};
|
||||
}
|
||||
return db->getTrackPath(id);
|
||||
}
|
||||
|
||||
|
||||
// TODO WIP: Atm only appends are allowed, this will only ever append regardless of what index
|
||||
// is given. But it is kept like this for compatability for now.
|
||||
auto TrackQueue::insert(Item i, size_t index) -> void {
|
||||
append(i);
|
||||
}
|
||||
|
||||
auto TrackQueue::append(Item i) -> void {
|
||||
bool was_queue_empty;
|
||||
bool current_changed;
|
||||
{
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
was_queue_empty = pos_ == tracks_.size();
|
||||
current_changed = was_queue_empty || index == pos_;
|
||||
was_queue_empty = playlist_.currentPosition() >= playlist_.size();
|
||||
current_changed = was_queue_empty; // Dont support inserts yet
|
||||
}
|
||||
|
||||
auto update_shuffler = [=, this]() {
|
||||
if (shuffle_) {
|
||||
shuffle_->resize(tracks_.size());
|
||||
shuffle_->resize(playlist_.size());
|
||||
// If there wasn't anything already playing, then we should make sure we
|
||||
// begin playback at a random point, instead of always starting with
|
||||
// whatever was inserted first and *then* shuffling.
|
||||
|
@ -149,7 +152,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
|
|||
// 'play this track now' (by inserting at the current pos) to work even
|
||||
// when shuffling is enabled.
|
||||
if (was_queue_empty) {
|
||||
pos_ = shuffle_->current();
|
||||
playlist_.skipTo(shuffle_->current());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -157,10 +160,11 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
|
|||
if (std::holds_alternative<database::TrackId>(i)) {
|
||||
{
|
||||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
if (index <= tracks_.size()) {
|
||||
tracks_.insert(tracks_.begin() + index, std::get<database::TrackId>(i));
|
||||
update_shuffler();
|
||||
auto filename = getFilepath(std::get<database::TrackId>(i));
|
||||
if (filename) {
|
||||
playlist_.append(*filename);
|
||||
}
|
||||
update_shuffler();
|
||||
}
|
||||
notifyChanged(current_changed, Reason::kExplicitUpdate);
|
||||
} else if (std::holds_alternative<database::TrackIterator>(i)) {
|
||||
|
@ -169,7 +173,6 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
|
|||
// doesn't block.
|
||||
bg_worker_.Dispatch<void>([=, this]() {
|
||||
database::TrackIterator it = std::get<database::TrackIterator>(i);
|
||||
size_t working_pos = index;
|
||||
while (true) {
|
||||
auto next = *it;
|
||||
if (!next) {
|
||||
|
@ -179,11 +182,11 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
|
|||
// like current().
|
||||
{
|
||||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
if (working_pos <= tracks_.size()) {
|
||||
tracks_.insert(tracks_.begin() + working_pos, *next);
|
||||
auto filename = *getFilepath(*next);
|
||||
if (!filename.empty()) {
|
||||
playlist_.append(filename);
|
||||
}
|
||||
}
|
||||
working_pos++;
|
||||
it++;
|
||||
}
|
||||
{
|
||||
|
@ -195,15 +198,6 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
auto TrackQueue::append(Item i) -> void {
|
||||
size_t end;
|
||||
{
|
||||
const std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
end = tracks_.size();
|
||||
}
|
||||
insert(i, end);
|
||||
}
|
||||
|
||||
auto TrackQueue::next() -> void {
|
||||
next(Reason::kExplicitUpdate);
|
||||
}
|
||||
|
@ -215,17 +209,16 @@ auto TrackQueue::next(Reason r) -> void {
|
|||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
if (shuffle_) {
|
||||
shuffle_->next();
|
||||
pos_ = shuffle_->current();
|
||||
playlist_.skipTo(shuffle_->current());
|
||||
} else {
|
||||
if (pos_ + 1 >= tracks_.size()) {
|
||||
if (playlist_.atEnd()) {
|
||||
if (replay_) {
|
||||
pos_ = 0;
|
||||
playlist_.skipTo(0);
|
||||
} else {
|
||||
pos_ = tracks_.size();
|
||||
changed = false;
|
||||
}
|
||||
} else {
|
||||
pos_++;
|
||||
playlist_.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -240,16 +233,16 @@ auto TrackQueue::previous() -> void {
|
|||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
if (shuffle_) {
|
||||
shuffle_->prev();
|
||||
pos_ = shuffle_->current();
|
||||
playlist_.skipTo(shuffle_->current());
|
||||
} else {
|
||||
if (pos_ == 0) {
|
||||
if (playlist_.currentPosition() == 0) {
|
||||
if (repeat_) {
|
||||
pos_ = tracks_.size() - 1;
|
||||
playlist_.skipTo(playlist_.size()-1);
|
||||
} else {
|
||||
changed = false;
|
||||
}
|
||||
} else {
|
||||
pos_--;
|
||||
playlist_.prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -265,39 +258,10 @@ auto TrackQueue::finish() -> void {
|
|||
}
|
||||
}
|
||||
|
||||
auto TrackQueue::skipTo(database::TrackId id) -> void {
|
||||
// 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 search for the given id after any previously pending iterator
|
||||
// insertions have finished.
|
||||
bg_worker_.Dispatch<void>([=, this]() {
|
||||
bool found = false;
|
||||
{
|
||||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
for (size_t i = 0; i < tracks_.size(); i++) {
|
||||
if (tracks_[i] == id) {
|
||||
pos_ = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
notifyChanged(true, Reason::kExplicitUpdate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto TrackQueue::clear() -> void {
|
||||
{
|
||||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
if (tracks_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
pos_ = 0;
|
||||
tracks_.clear();
|
||||
|
||||
playlist_.clear();
|
||||
if (shuffle_) {
|
||||
shuffle_->resize(0);
|
||||
}
|
||||
|
@ -309,10 +273,8 @@ auto TrackQueue::clear() -> void {
|
|||
auto TrackQueue::random(bool en) -> void {
|
||||
{
|
||||
const std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
// Don't check for en == true already; this has the side effect that
|
||||
// repeated calls with en == true will re-shuffle.
|
||||
if (en) {
|
||||
shuffle_.emplace(tracks_.size());
|
||||
shuffle_.emplace(playlist_.size());
|
||||
shuffle_->replay(replay_);
|
||||
} else {
|
||||
shuffle_.reset();
|
||||
|
@ -360,14 +322,11 @@ auto TrackQueue::replay() const -> bool {
|
|||
|
||||
auto TrackQueue::serialise() -> std::string {
|
||||
cppbor::Array tracks{};
|
||||
for (database::TrackId track : tracks_) {
|
||||
tracks.add(cppbor::Uint(track));
|
||||
}
|
||||
cppbor::Map encoded;
|
||||
encoded.add(cppbor::Uint{0}, cppbor::Array{
|
||||
cppbor::Uint{pos_},
|
||||
cppbor::Bool{repeat_},
|
||||
cppbor::Bool{replay_},
|
||||
cppbor::Uint{playlist_.currentPosition()},
|
||||
});
|
||||
if (shuffle_) {
|
||||
encoded.add(cppbor::Uint{1}, cppbor::Array{
|
||||
|
@ -376,7 +335,6 @@ auto TrackQueue::serialise() -> std::string {
|
|||
cppbor::Uint{shuffle_->pos()},
|
||||
});
|
||||
}
|
||||
encoded.add(cppbor::Uint{2}, std::move(tracks));
|
||||
return encoded.toString();
|
||||
}
|
||||
|
||||
|
@ -401,9 +359,6 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item(
|
|||
case 1:
|
||||
state_ = State::kShuffle;
|
||||
break;
|
||||
case 2:
|
||||
state_ = State::kTracks;
|
||||
break;
|
||||
default:
|
||||
state_ = State::kFinished;
|
||||
}
|
||||
|
@ -412,7 +367,8 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item(
|
|||
if (item->type() == cppbor::ARRAY) {
|
||||
i_ = 0;
|
||||
} else if (item->type() == cppbor::UINT) {
|
||||
queue_.pos_ = item->asUint()->unsignedValue();
|
||||
auto val = item->asUint()->unsignedValue();
|
||||
queue_.playlist_.skipTo(val);
|
||||
} else if (item->type() == cppbor::SIMPLE) {
|
||||
bool val = item->asBool()->value();
|
||||
if (i_ == 0) {
|
||||
|
@ -444,10 +400,6 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item(
|
|||
}
|
||||
i_++;
|
||||
}
|
||||
} else if (state_ == State::kTracks) {
|
||||
if (item->type() == cppbor::UINT) {
|
||||
queue_.tracks_.push_back(item->asUint()->unsignedValue());
|
||||
}
|
||||
} else if (state_ == State::kFinished) {
|
||||
}
|
||||
return this;
|
||||
|
@ -470,10 +422,6 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::itemEnd(
|
|||
if (item->type() == cppbor::ARRAY) {
|
||||
state_ = State::kRoot;
|
||||
}
|
||||
} else if (state_ == State::kTracks) {
|
||||
if (item->type() == cppbor::ARRAY) {
|
||||
state_ = State::kRoot;
|
||||
}
|
||||
} else if (state_ == State::kFinished) {
|
||||
}
|
||||
return this;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "database/database.hpp"
|
||||
#include "database/track.hpp"
|
||||
#include "tasks.hpp"
|
||||
#include "playlist.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
|
@ -64,22 +65,15 @@ class RandomIterator {
|
|||
*/
|
||||
class TrackQueue {
|
||||
public:
|
||||
TrackQueue(tasks::WorkerPool& bg_worker);
|
||||
TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db);
|
||||
|
||||
/* Returns the currently playing track. */
|
||||
auto current() const -> std::optional<database::TrackId>;
|
||||
|
||||
/* Returns, in order, tracks that have been queued to be played next. */
|
||||
auto peekNext(std::size_t limit) const -> std::vector<database::TrackId>;
|
||||
|
||||
/*
|
||||
* Returns the tracks in the queue that have already been played, ordered
|
||||
* most recently played first.
|
||||
*/
|
||||
auto peekPrevious(std::size_t limit) const -> std::vector<database::TrackId>;
|
||||
using TrackItem = std::variant<std::string, database::TrackId, std::monostate>;
|
||||
auto current() const -> TrackItem;
|
||||
|
||||
auto currentPosition() const -> size_t;
|
||||
auto totalSize() const -> size_t;
|
||||
auto open() -> bool;
|
||||
|
||||
using Item = std::variant<database::TrackId, database::TrackIterator>;
|
||||
auto insert(Item, size_t index = 0) -> void;
|
||||
|
@ -97,8 +91,6 @@ class TrackQueue {
|
|||
*/
|
||||
auto finish() -> void;
|
||||
|
||||
auto skipTo(database::TrackId) -> void;
|
||||
|
||||
/*
|
||||
* Removes all tracks from all queues, and stops any currently playing track.
|
||||
*/
|
||||
|
@ -122,13 +114,14 @@ class TrackQueue {
|
|||
|
||||
private:
|
||||
auto next(QueueUpdate::Reason r) -> void;
|
||||
auto getFilepath(database::TrackId id) -> std::optional<std::string>;
|
||||
|
||||
mutable std::shared_mutex mutex_;
|
||||
|
||||
tasks::WorkerPool& bg_worker_;
|
||||
database::Handle db_;
|
||||
|
||||
size_t pos_;
|
||||
std::pmr::vector<database::TrackId> tracks_;
|
||||
Playlist playlist_;
|
||||
|
||||
std::optional<RandomIterator> shuffle_;
|
||||
bool repeat_;
|
||||
|
@ -159,7 +152,6 @@ class TrackQueue {
|
|||
kRoot,
|
||||
kMetadata,
|
||||
kShuffle,
|
||||
kTracks,
|
||||
kFinished,
|
||||
};
|
||||
State state_;
|
||||
|
|
|
@ -97,7 +97,7 @@ auto Booting::entry() -> void {
|
|||
sServices->samd(), std::unique_ptr<drivers::AdcBattery>(adc)));
|
||||
|
||||
sServices->track_queue(
|
||||
std::make_unique<audio::TrackQueue>(sServices->bg_worker()));
|
||||
std::make_unique<audio::TrackQueue>(sServices->bg_worker(), sServices->database()));
|
||||
sServices->tag_parser(std::make_unique<database::TagParserImpl>());
|
||||
sServices->collator(locale::CreateCollator());
|
||||
sServices->tts(std::make_unique<tts::Provider>());
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
idf_component_register(
|
||||
SRC_DIRS "battery"
|
||||
SRC_DIRS "battery" "audio"
|
||||
INCLUDE_DIRS "." REQUIRES catch2 cmock tangara fixtures)
|
||||
|
|
86
src/tangara/test/audio/test_playlist.cpp
Normal file
86
src/tangara/test/audio/test_playlist.cpp
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2023 ailurux <ailuruxx@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#include "audio/playlist.hpp"
|
||||
|
||||
#include <dirent.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
#include "drivers/gpios.hpp"
|
||||
#include "drivers/i2c.hpp"
|
||||
#include "drivers/storage.hpp"
|
||||
#include "drivers/spi.hpp"
|
||||
#include "i2c_fixture.hpp"
|
||||
#include "spi_fixture.hpp"
|
||||
#include "ff.h"
|
||||
|
||||
namespace audio {
|
||||
|
||||
static const std::string kTestFilename = "test_playlist2.m3u";
|
||||
static const std::string kTestFilePath = kTestFilename;
|
||||
|
||||
TEST_CASE("playlist file", "[integration]") {
|
||||
I2CFixture i2c;
|
||||
SpiFixture spi;
|
||||
std::unique_ptr<drivers::IGpios> gpios{drivers::Gpios::Create(false)};
|
||||
|
||||
if (gpios->Get(drivers::IGpios::Pin::kSdCardDetect)) {
|
||||
// Skip if nothing is inserted.
|
||||
SKIP("no sd card detected; skipping storage tests");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_ptr<drivers::SdStorage> result(drivers::SdStorage::Create(*gpios).value());
|
||||
Playlist plist(kTestFilePath);
|
||||
REQUIRE(plist.clear());
|
||||
|
||||
SECTION("write to the playlist file") {
|
||||
plist.append("test1.mp3");
|
||||
plist.append("test2.mp3");
|
||||
plist.append("test3.mp3");
|
||||
plist.append("test4.wav");
|
||||
plist.append("directory/test1.mp3");
|
||||
plist.append("directory/test2.mp3");
|
||||
plist.append("a/really/long/directory/test1.mp3");
|
||||
plist.append("directory/and/another/test2.mp3");
|
||||
REQUIRE(plist.size() == 8);
|
||||
|
||||
SECTION("read from the playlist file") {
|
||||
Playlist plist2(kTestFilePath);
|
||||
REQUIRE(plist2.size() == 8);
|
||||
REQUIRE(plist2.value() == "test1.mp3");
|
||||
plist2.next();
|
||||
REQUIRE(plist2.value() == "test2.mp3");
|
||||
plist2.prev();
|
||||
REQUIRE(plist2.value() == "test1.mp3");
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK("appending item") {
|
||||
plist.append("A/New/Item.wav");
|
||||
};
|
||||
|
||||
BENCHMARK("opening playlist file") {
|
||||
Playlist plist2(kTestFilePath);
|
||||
REQUIRE(plist2.size() > 100);
|
||||
return plist2.size();
|
||||
};
|
||||
|
||||
BENCHMARK("opening playlist file and appending entry") {
|
||||
Playlist plist2(kTestFilePath);
|
||||
REQUIRE(plist2.size() > 100);
|
||||
plist2.append("A/Nother/New/Item.opus");
|
||||
return plist2.size();
|
||||
};
|
||||
}
|
||||
}
|
||||
} // namespace audio
|
|
@ -398,10 +398,12 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) {
|
|||
|
||||
void UiState::react(const audio::QueueUpdate&) {
|
||||
auto& queue = sServices->track_queue();
|
||||
sQueueSize.setDirect(static_cast<int>(queue.totalSize()));
|
||||
auto queue_size = queue.totalSize();
|
||||
sQueueSize.setDirect(static_cast<int>(queue_size));
|
||||
|
||||
int current_pos = queue.currentPosition();
|
||||
if (queue.current()) {
|
||||
// If there is nothing in the queue, the position should be 0, otherwise, add one because lua
|
||||
if (queue_size > 0) {
|
||||
current_pos++;
|
||||
}
|
||||
sQueuePosition.setDirect(current_pos);
|
||||
|
|
|
@ -11,3 +11,4 @@ CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
|
|||
CONFIG_COMPILER_STACK_CHECK=y
|
||||
CONFIG_ESP_TASK_WDT=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="../partitions.csv"
|
||||
CONFIG_ESP_TASK_WDT_EN=n
|
||||
|
|
Loading…
Add table
Reference in a new issue