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