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>custom
parent
24fde7af0c
commit
0a271d786b
@ -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
|
@ -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
|
@ -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
|
Loading…
Reference in new issue