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 {
const uint8_t kCurrentDbVersion = 6;
const uint8_t kCurrentDbVersion = 7;
struct SearchKey;
class Record;

@ -6,14 +6,20 @@
#include "database/tag_parser.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <memory>
#include <mutex>
#include "database/track.hpp"
#include "debug.hpp"
#include "drivers/spi.hpp"
#include "esp_log.h"
#include "ff.h"
#include "ogg/ogg.h"
#include "tags.h"
#include "memory_resource.hpp"
@ -106,10 +112,18 @@ static void toc(Tagctx* ctx, int ms, int offset) {}
static const std::size_t kBufSize = 1024;
[[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)
-> 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::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) {
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_};
cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags);
@ -143,7 +166,148 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path)
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};
libtags::Aux aux;
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 ||
f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) {
ESP_LOGW(kTag, "failed to open file %s", path.c_str());
return {};
}

@ -6,6 +6,7 @@
#pragma once
#include <stdint.h>
#include <string>
#include "database/track.hpp"
@ -27,7 +28,7 @@ class TagParserImpl : public ITagParser {
-> std::shared_ptr<TrackTags> override;
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
@ -35,10 +36,25 @@ class TagParserImpl : public ITagParser {
*/
std::mutex cache_mutex_;
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
// similar. This hasn't been done yet, as this isn't a common workload in
// any of our UI.
class GenericTagParser : public ITagParser {
public:
auto ReadAndParseTags(std::string_view path)
-> std::shared_ptr<TrackTags> override;
};
} // namespace database

@ -148,7 +148,7 @@ auto TrackTags::set(Tag t, std::string_view v) -> void {
track(v);
break;
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;
case Tag::kGenres:
genres(v);

Loading…
Cancel
Save