daniel/playlist-queue ()

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:
ailurux 2024-07-16 01:23:43 +00:00 committed by cooljqln
parent 24fde7af0c
commit 0a271d786b
11 changed files with 379 additions and 136 deletions

View file

@ -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

View file

@ -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.

View 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

View 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

View file

@ -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;

View file

@ -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_;

View file

@ -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>());

View file

@ -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)

View 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

View file

@ -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);

View file

@ -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