Use libogg + our own parser for ogg files

This is somewhat faster than relying on libtags to parse these, and also better handles cornercases such as tags that cross physical page boundaries.
custom
jacqueline 8 months ago
parent b4a2b4fb6f
commit e6921dc055
  1. 2
      src/tangara/database/database.hpp
  2. 171
      src/tangara/database/tag_parser.cpp
  3. 24
      src/tangara/database/tag_parser.hpp
  4. 2
      src/tangara/database/track.cpp

@ -37,7 +37,7 @@
namespace database { namespace database {
const uint8_t kCurrentDbVersion = 6; const uint8_t kCurrentDbVersion = 7;
struct SearchKey; struct SearchKey;
class Record; class Record;

@ -6,14 +6,20 @@
#include "database/tag_parser.hpp" #include "database/tag_parser.hpp"
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <iomanip> #include <iomanip>
#include <memory>
#include <mutex> #include <mutex>
#include "database/track.hpp"
#include "debug.hpp"
#include "drivers/spi.hpp" #include "drivers/spi.hpp"
#include "esp_log.h" #include "esp_log.h"
#include "ff.h" #include "ff.h"
#include "ogg/ogg.h"
#include "tags.h" #include "tags.h"
#include "memory_resource.hpp" #include "memory_resource.hpp"
@ -106,10 +112,18 @@ static void toc(Tagctx* ctx, int ms, int offset) {}
static const std::size_t kBufSize = 1024; static const std::size_t kBufSize = 1024;
[[maybe_unused]] static const char* kTag = "TAGS"; [[maybe_unused]] static const char* kTag = "TAGS";
TagParserImpl::TagParserImpl() {} TagParserImpl::TagParserImpl() {
parsers_.emplace_back(new OggTagParser());
parsers_.emplace_back(new GenericTagParser());
}
auto TagParserImpl::ReadAndParseTags(std::string_view path) auto TagParserImpl::ReadAndParseTags(std::string_view path)
-> std::shared_ptr<TrackTags> { -> std::shared_ptr<TrackTags> {
if (path.empty()) {
return {};
}
// Check the cache first to see if we can skip parsing this file completely.
{ {
std::lock_guard<std::mutex> lock{cache_mutex_}; std::lock_guard<std::mutex> lock{cache_mutex_};
std::optional<std::shared_ptr<TrackTags>> cached = std::optional<std::shared_ptr<TrackTags>> cached =
@ -119,7 +133,15 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path)
} }
} }
std::shared_ptr<TrackTags> tags = parseNew(path); // Nothing in the cache; try each of our parsers.
std::shared_ptr<TrackTags> tags;
for (auto& parser : parsers_) {
tags = parser->ReadAndParseTags(path);
if (tags) {
break;
}
}
if (!tags) { if (!tags) {
return {}; return {};
} }
@ -135,6 +157,7 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path)
} }
} }
// Store the result in the cache for later.
{ {
std::lock_guard<std::mutex> lock{cache_mutex_}; std::lock_guard<std::mutex> lock{cache_mutex_};
cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags); cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags);
@ -143,7 +166,148 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path)
return tags; return tags;
} }
auto TagParserImpl::parseNew(std::string_view p) -> std::shared_ptr<TrackTags> { OggTagParser::OggTagParser() {
nameToTag_["TITLE"] = Tag::kTitle;
nameToTag_["ALBUM"] = Tag::kAlbum;
nameToTag_["ARTIST"] = Tag::kArtist;
nameToTag_["ALBUMARTIST"] = Tag::kAlbumArtist;
nameToTag_["TRACKNUMBER"] = Tag::kTrack;
nameToTag_["GENRE"] = Tag::kGenres;
nameToTag_["DISC"] = Tag::kDisc;
}
auto OggTagParser::ReadAndParseTags(std::string_view p)
-> std::shared_ptr<TrackTags> {
if (!p.ends_with(".ogg") && !p.ends_with(".opus") && !p.ends_with(".ogx")) {
return {};
}
ogg_sync_state sync;
ogg_sync_init(&sync);
ogg_page page;
ogg_stream_state stream;
bool stream_init = false;
std::string path{p};
FIL file;
if (f_open(&file, path.c_str(), FA_READ) != FR_OK) {
ESP_LOGW(kTag, "failed to open file '%s'", path.c_str());
return {};
}
std::shared_ptr<TrackTags> tags;
// The comments packet is the second in the stream. This is *usually* the
// second page, sometimes overflowing onto the third page. There is no
// guarantee of this however, so we read the first five pages before giving
// up just in case. We don't try to read more pages than this as it could take
// quite some time, with no likely benefit.
for (int i = 0; i < 5; i++) {
// Load up the sync with data until we have a complete page.
while (ogg_sync_pageout(&sync, &page) != 1) {
char* buffer = ogg_sync_buffer(&sync, 512);
UINT br;
FRESULT fres = f_read(&file, buffer, 512, &br);
if (fres != FR_OK || br == 0) {
goto finish;
}
int res = ogg_sync_wrote(&sync, br);
if (res != 0) {
goto finish;
}
}
// Ensure the stream has the correct serialno. pagein and packetout both
// give no results if the serialno is incorrect.
if (ogg_page_bos(&page)) {
ogg_stream_init(&stream, ogg_page_serialno(&page));
stream_init = true;
}
if (ogg_stream_pagein(&stream, &page) < 0) {
goto finish;
}
// Try to pull out a packet.
ogg_packet packet;
if (ogg_stream_packetout(&stream, &packet) == 1) {
// We're interested in the second packet (packetno == 1) only.
if (packet.packetno < 1) {
continue;
}
if (packet.packetno > 1) {
goto finish;
}
tags = TrackTags::create();
if (memcmp(packet.packet, "OpusTags", 8) == 0) {
std::span<unsigned char> data{packet.packet,
static_cast<size_t>(packet.bytes)};
tags->encoding(Container::kOpus);
parseComments(*tags, data.subspan(8));
} else if (packet.packet[0] == 3 &&
memcmp(packet.packet + 1, "vorbis", 6) == 0) {
std::span<unsigned char> data{packet.packet,
static_cast<size_t>(packet.bytes)};
tags->encoding(Container::kOgg);
parseComments(*tags, data.subspan(7));
}
break;
}
}
finish:
if (stream_init) {
ogg_stream_clear(&stream);
}
ogg_sync_clear(&sync);
f_close(&file);
return tags;
}
auto OggTagParser::parseComments(TrackTags& res, std::span<unsigned char> data)
-> void {
uint64_t vendor_len = parseLength(data);
uint64_t num_tags = parseLength(data.subspan(4 + vendor_len));
data = data.subspan(4 + vendor_len + 4);
for (size_t i = 0; i < num_tags; i++) {
uint64_t size = parseLength(data);
std::string_view tag = {
reinterpret_cast<const char*>(data.subspan(4).data()),
static_cast<size_t>(size)};
auto split = tag.find("=");
if (split != std::string::npos) {
std::string_view key = tag.substr(0, split);
std::string_view val = tag.substr(split + 1);
std::string key_upper{key};
std::transform(key.begin(), key.end(), key_upper.begin(), ::toupper);
if (nameToTag_.contains(key_upper) && !val.empty()) {
res.set(nameToTag_[key_upper], val);
}
}
data = data.subspan(4 + size);
}
}
auto OggTagParser::parseLength(std::span<unsigned char> data) -> uint64_t {
return static_cast<uint64_t>(data[3]) << 24 |
static_cast<uint64_t>(data[2]) << 16 |
static_cast<uint64_t>(data[1]) << 8 |
static_cast<uint64_t>(data[0]) << 0;
}
auto GenericTagParser::ReadAndParseTags(std::string_view p)
-> std::shared_ptr<TrackTags> {
std::string path{p}; std::string path{p};
libtags::Aux aux; libtags::Aux aux;
auto out = TrackTags::create(); auto out = TrackTags::create();
@ -151,7 +315,6 @@ auto TagParserImpl::parseNew(std::string_view p) -> std::shared_ptr<TrackTags> {
if (f_stat(path.c_str(), &aux.info) != FR_OK || if (f_stat(path.c_str(), &aux.info) != FR_OK ||
f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) { f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) {
ESP_LOGW(kTag, "failed to open file %s", path.c_str());
return {}; return {};
} }

@ -6,6 +6,7 @@
#pragma once #pragma once
#include <stdint.h>
#include <string> #include <string>
#include "database/track.hpp" #include "database/track.hpp"
@ -27,7 +28,7 @@ class TagParserImpl : public ITagParser {
-> std::shared_ptr<TrackTags> override; -> std::shared_ptr<TrackTags> override;
private: private:
auto parseNew(std::string_view path) -> std::shared_ptr<TrackTags>; std::vector<std::unique_ptr<ITagParser>> parsers_;
/* /*
* Cache of tags that have already been extracted from files. Ideally this * Cache of tags that have already been extracted from files. Ideally this
@ -35,10 +36,25 @@ class TagParserImpl : public ITagParser {
*/ */
std::mutex cache_mutex_; std::mutex cache_mutex_;
util::LruCache<8, std::pmr::string, std::shared_ptr<TrackTags>> cache_; util::LruCache<8, std::pmr::string, std::shared_ptr<TrackTags>> cache_;
};
class OggTagParser : public ITagParser {
public:
OggTagParser();
auto ReadAndParseTags(std::string_view path)
-> std::shared_ptr<TrackTags> override;
private:
auto parseComments(TrackTags&, std::span<unsigned char> data) -> void;
auto parseLength(std::span<unsigned char> data) -> uint64_t;
std::unordered_map<std::string, Tag> nameToTag_;
};
// We could also consider keeping caches of artist name -> std::string and class GenericTagParser : public ITagParser {
// similar. This hasn't been done yet, as this isn't a common workload in public:
// any of our UI. auto ReadAndParseTags(std::string_view path)
-> std::shared_ptr<TrackTags> override;
}; };
} // namespace database } // namespace database

@ -148,7 +148,7 @@ auto TrackTags::set(Tag t, std::string_view v) -> void {
track(v); track(v);
break; break;
case Tag::kAlbumOrder: case Tag::kAlbumOrder:
// This tag is derices from disc and track, and so it can't be set. // This tag is derived from disc and track, and so it can't be set.
break; break;
case Tag::kGenres: case Tag::kGenres:
genres(v); genres(v);

Loading…
Cancel
Save