From 80da565609fc84dea90e9e30b1b870263e49fc18 Mon Sep 17 00:00:00 2001 From: Eoin O'Neill Date: Mon, 1 Aug 2022 00:41:12 -0700 Subject: [PATCH] Initial support for GME's VGM/SPC playback and tag management. Co-Authored-By: Jonas Kvinge --- .../core/messagehandler.h | 3 - ext/libstrawberry-tagreader/CMakeLists.txt | 2 +- ext/libstrawberry-tagreader/tagreaderbase.cpp | 6 + ext/libstrawberry-tagreader/tagreaderbase.h | 7 +- ext/libstrawberry-tagreader/tagreadergme.cpp | 299 ++++++++++++++++++ ext/libstrawberry-tagreader/tagreadergme.h | 111 +++++++ .../tagreadermessages.proto | 2 + .../tagreadertaglib.cpp | 14 +- ext/libstrawberry-tagreader/tagreadertaglib.h | 7 +- .../tagreadertagparser.cpp | 18 +- .../tagreadertagparser.h | 2 +- ext/strawberry-tagreader/tagreaderworker.cpp | 70 ++-- ext/strawberry-tagreader/tagreaderworker.h | 6 + src/core/song.cpp | 16 +- src/core/song.h | 2 + src/core/tagreaderclient.cpp | 2 + src/widgets/fileview.cpp | 3 +- 17 files changed, 518 insertions(+), 52 deletions(-) create mode 100644 ext/libstrawberry-tagreader/tagreadergme.cpp create mode 100644 ext/libstrawberry-tagreader/tagreadergme.h diff --git a/ext/libstrawberry-common/core/messagehandler.h b/ext/libstrawberry-common/core/messagehandler.h index 5bb6c0501..8e18c1374 100644 --- a/ext/libstrawberry-common/core/messagehandler.h +++ b/ext/libstrawberry-common/core/messagehandler.h @@ -35,9 +35,6 @@ class QIODevice; -#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size()) -#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() - // Reads and writes uint32 length encoded protobufs to a socket. // This base QObject is separate from AbstractMessageHandler because moc can't handle templated classes. // Use AbstractMessageHandler instead. diff --git a/ext/libstrawberry-tagreader/CMakeLists.txt b/ext/libstrawberry-tagreader/CMakeLists.txt index 4d2ba0adc..4571ead0d 100644 --- a/ext/libstrawberry-tagreader/CMakeLists.txt +++ b/ext/libstrawberry-tagreader/CMakeLists.txt @@ -4,7 +4,7 @@ set(MESSAGES tagreadermessages.proto) set(SOURCES tagreaderbase.cpp) if(USE_TAGLIB AND TAGLIB_FOUND) - list(APPEND SOURCES tagreadertaglib.cpp) + list(APPEND SOURCES tagreadertaglib.cpp tagreadergme.cpp) endif() if(USE_TAGPARSER AND TAGPARSER_FOUND) diff --git a/ext/libstrawberry-tagreader/tagreaderbase.cpp b/ext/libstrawberry-tagreader/tagreaderbase.cpp index 3089def54..d382b9039 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.cpp +++ b/ext/libstrawberry-tagreader/tagreaderbase.cpp @@ -26,6 +26,12 @@ const std::string TagReaderBase::kEmbeddedCover = "(embedded)"; TagReaderBase::TagReaderBase() = default; TagReaderBase::~TagReaderBase() = default; +void TagReaderBase::Decode(const QString &tag, std::string *output) { + + output->assign(DataCommaSizeFromQString(tag)); + +} + float TagReaderBase::ConvertPOPMRating(const int POPM_rating) { if (POPM_rating < 0x01) return 0.0F; diff --git a/ext/libstrawberry-tagreader/tagreaderbase.h b/ext/libstrawberry-tagreader/tagreaderbase.h index de5baf904..6f43e8c24 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.h +++ b/ext/libstrawberry-tagreader/tagreaderbase.h @@ -27,6 +27,9 @@ #include "tagreadermessages.pb.h" +#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size()) +#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() + /* * This class holds all useful methods to read and write tags from/to files. * You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient) @@ -38,7 +41,7 @@ class TagReaderBase { virtual bool IsMediaFile(const QString &filename) const = 0; - virtual void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0; + virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0; virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0; virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0; @@ -47,6 +50,8 @@ class TagReaderBase { virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0; virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0; + static void Decode(const QString &tag, std::string *output); + static float ConvertPOPMRating(const int POPM_rating); static int ConvertToPOPMRating(const float rating); diff --git a/ext/libstrawberry-tagreader/tagreadergme.cpp b/ext/libstrawberry-tagreader/tagreadergme.cpp new file mode 100644 index 000000000..30cdee9b3 --- /dev/null +++ b/ext/libstrawberry-tagreader/tagreadergme.cpp @@ -0,0 +1,299 @@ +/* + * Strawberry Music Player + * Copyright 2022, Eoin O'Neill + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "tagreadergme.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/timeconstants.h" +#include "core/messagehandler.h" +#include "tagreaderbase.h" +#include "tagreadertaglib.h" + +bool GME::IsSupportedFormat(const QFileInfo &file_info) { + return file_info.exists() && (file_info.completeSuffix().endsWith("spc") || file_info.completeSuffix().endsWith("vgm")); +} + +bool GME::ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) { + + if (file_info.completeSuffix().endsWith("spc")) { + SPC::Read(file_info, song_info); + return true; + } + if (file_info.completeSuffix().endsWith("vgm")) { + VGM::Read(file_info, song_info); + return true; + } + + return false; + +} + +quint32 GME::UnpackBytes32(const char *const bytes, size_t length) { + + Q_ASSERT(length <= 4 && length > 0); + + quint32 value = 0; + for (size_t i = 0; i < length; i++) { + value |= static_cast(bytes[i]) << (8 * i); + } + + return value; + +} + +void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) { + + QFile file(file_info.filePath()); + if (!file.open(QIODevice::ReadOnly)) return; + + qLog(Debug) << "Reading tags from SPC file" << file_info.fileName(); + + // Check for header -- more reliable than file name alone. + if (!file.read(33).startsWith(QString("SNES-SPC700").toLatin1())) return; + + // First order of business -- get any tag values that exist within the core file information. + // These only allow for a certain number of bytes per field, + // so they will likely be overwritten either by the id666 standard or the APETAG format (as used by other players, such as foobar and winamp) + // + // Make sure to check id6 documentation before changing the read values! + + file.seek(HAS_ID6_OFFSET); + bool has_id6 = (file.read(1)[0] == static_cast(xID6_STATUS::ON)); + + file.seek(SONG_TITLE_OFFSET); + song_info->set_title(QString::fromLatin1(file.read(32)).toStdString()); + + file.seek(GAME_TITLE_OFFSET); + song_info->set_album(QString::fromLatin1(file.read(32)).toStdString()); + + file.seek(ARTIST_OFFSET); + song_info->set_artist(QString::fromLatin1(file.read(32)).toStdString()); + + file.seek(INTRO_LENGTH_OFFSET); + QByteArray length_bytes = file.read(INTRO_LENGTH_SIZE); + quint64 length_in_sec = 0; + if (length_bytes.size() >= INTRO_LENGTH_SIZE) { + length_in_sec = ConvertSPCStringToNum(length_bytes); + + if (!length_in_sec || length_in_sec >= 0x1FFF) { + // This means that parsing the length as a string failed, so get value LE. + length_in_sec = length_bytes[0] | (length_bytes[1] << 8) | (length_bytes[2] << 16); + } + + if (length_in_sec < 0x1FFF) { + song_info->set_length_nanosec(length_in_sec * kNsecPerSec); + } + } + + file.seek(FADE_LENGTH_OFFSET); + QByteArray fade_bytes = file.read(FADE_LENGTH_SIZE); + if (fade_bytes.size() >= FADE_LENGTH_SIZE) { + quint64 fade_length_in_ms = ConvertSPCStringToNum(fade_bytes); + + if (fade_length_in_ms > 0x7FFF) { + fade_length_in_ms = fade_bytes[0] | (fade_bytes[1] << 8) | (fade_bytes[2] << 16) | (fade_bytes[3] << 24); + } + } + + // Check for XID6 data -- this is infrequently used, but being able to fill in data from this is ideal before trying to rely on APETAG values. + // XID6 format follows EA's binary file format standard named "IFF" + file.seek(XID6_OFFSET); + if (has_id6 && file.read(4) == QString("xid6").toLatin1()) { + QByteArray xid6_head_data = file.read(4); + if (xid6_head_data.size() >= 4) { + qint64 xid6_size = xid6_head_data[0] | (xid6_head_data[1] << 8) | (xid6_head_data[2] << 16) | xid6_head_data[3]; + // This should be the size remaining for entire ID6 block, but it seems that most files treat this as the size of the remaining header space... + + qLog(Debug) << file_info.fileName() << "has ID6 tag."; + + while ((file.pos()) + 4 < XID6_OFFSET + xid6_size) { + QByteArray arr = file.read(4); + if (arr.size() < 4) break; + + qint8 id = arr[0]; + qint8 type = arr[1]; + Q_UNUSED(id); + Q_UNUSED(type); + qint16 length = arr[2] | (arr[3] << 8); + + file.read(GetNextMemAddressAlign32bit(length)); + } + } + } + + // Music Players that support SPC tend to support additional tagging data as + // an APETAG entry at the bottom of the file instead of writing into the xid6 tagging space. + // This is where a lot of the extra data for a file is stored, such as genre or replaygain data. + // This data is currently supported by TagLib, so we will simply use that for the remaining values. + TagLib::APE::File ape(file_info.filePath().toStdString().data()); + if (ape.hasAPETag()) { + TagLib::Tag *tag = ape.tag(); + if (!tag) return; + + TagReaderTagLib::Decode(tag->artist(), song_info->mutable_artist()); + TagReaderTagLib::Decode(tag->album(), song_info->mutable_album()); + TagReaderTagLib::Decode(tag->title(), song_info->mutable_title()); + TagReaderTagLib::Decode(tag->genre(), song_info->mutable_genre()); + song_info->set_track(tag->track()); + song_info->set_year(tag->year()); + } + + song_info->set_valid(true); + song_info->set_filetype(spb::tagreader::SongMetadata_FileType_SPC); + +} + +qint16 GME::SPC::GetNextMemAddressAlign32bit(qint16 input) { + return ((input + 0x3) & ~0x3); + // Plus 0x3 for rounding up (not down), AND NOT to flatten out on a 32 bit level. +} + +quint64 GME::SPC::ConvertSPCStringToNum(const QByteArray &arr) { + + quint64 result = 0; + for (auto it = arr.begin(); it != arr.end(); it++) { + unsigned int num = *it - '0'; + if (num > 9) break; + result = (result * 10) + num; // Shift Left and add. + } + + return result; + +} + +void GME::VGM::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) { + + QFile file(file_info.filePath()); + if (!file.open(QIODevice::ReadOnly)) return; + + qLog(Debug) << "Reading tags from VGM file" << file_info.fileName(); + + if (!file.read(4).startsWith(QString("Vgm ").toLatin1())) return; + + file.seek(GD3_TAG_PTR); + QByteArray gd3_head = file.read(4); + if (gd3_head.size() < 4) return; + + quint64 pt = GME::UnpackBytes32(gd3_head, gd3_head.size()); + + file.seek(SAMPLE_COUNT); + QByteArray sample_count_bytes = file.read(4); + file.seek(LOOP_SAMPLE_COUNT); + QByteArray loop_count_bytes = file.read(4); + quint64 length = 0; + + if (!GetPlaybackLength(sample_count_bytes, loop_count_bytes, length)) return; + + file.seek(GD3_TAG_PTR + pt); + QByteArray gd3_version = file.read(4); + + file.seek(file.pos() + 4); + QByteArray gd3_length_bytes = file.read(4); + quint32 gd3_length = GME::UnpackBytes32(gd3_length_bytes, gd3_length_bytes.size()); + + QByteArray gd3Data = file.read(gd3_length); + QTextStream fileTagStream(gd3Data, QIODevice::ReadOnly); + // Stored as 16 bit UTF string, two bytes per letter. +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + fileTagStream.setEncoding(QStringConverter::Utf16); +#else + fileTagStream.setCodec("UTF-8"); +#endif + QStringList strings = fileTagStream.readLine(0).split(QChar('\0')); + if (strings.count() < 10) return; + + // VGM standard dictates string tag data exist in specific order. + // Order alternates between English and Japanese version of data. + // Read GD3 tag standard for more details. + song_info->set_title(strings[0].toStdString()); + song_info->set_album(strings[2].toStdString()); + song_info->set_artist(strings[6].toStdString()); + song_info->set_year(strings[8].left(4).toInt()); + song_info->set_length_nanosec(length * kNsecPerMsec); + song_info->set_valid(true); + song_info->set_filetype(spb::tagreader::SongMetadata_FileType_VGM); + +} + +bool GME::VGM::GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length) { + + if (sample_count_bytes.size() != 4) return false; + if (loop_count_bytes.size() != 4) return false; + + quint64 sample_count = GME::UnpackBytes32(sample_count_bytes, sample_count_bytes.size()); + + if (sample_count <= 0) return false; + + quint64 loop_sample_count = GME::UnpackBytes32(loop_count_bytes, loop_count_bytes.size()); + + if (loop_sample_count <= 0) { + out_length = sample_count * 1000 / SAMPLE_TIMEBASE; + return true; + } + + quint64 intro_length_ms = (sample_count - loop_sample_count) * 1000 / SAMPLE_TIMEBASE; + quint64 loop_length_ms = (loop_sample_count) * 1000 / SAMPLE_TIMEBASE; + out_length = intro_length_ms + (loop_length_ms * 2) + GST_GME_LOOP_TIME_MS; + + return true; + +} + +TagReaderGME::TagReaderGME() = default; +TagReaderGME::~TagReaderGME() = default; + +bool TagReaderGME::IsMediaFile(const QString &filename) const { + QFileInfo fileinfo(filename); + return GME::IsSupportedFormat(fileinfo); +} + +bool TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const { + QFileInfo fileinfo(filename); + return GME::ReadFile(fileinfo, song); +} + +bool TagReaderGME::SaveFile(const QString&, const spb::tagreader::SongMetadata&) const { + return false; +} + +QByteArray TagReaderGME::LoadEmbeddedArt(const QString&) const { + return QByteArray(); +} + +bool TagReaderGME::SaveEmbeddedArt(const QString&, const QByteArray&) { + return false; +} + +bool TagReaderGME::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const { + return false; +} + +bool TagReaderGME::SaveSongRatingToFile(const QString&, const spb::tagreader::SongMetadata&) const { + return false; +} diff --git a/ext/libstrawberry-tagreader/tagreadergme.h b/ext/libstrawberry-tagreader/tagreadergme.h new file mode 100644 index 000000000..c55b45b7a --- /dev/null +++ b/ext/libstrawberry-tagreader/tagreadergme.h @@ -0,0 +1,111 @@ +/* + * Strawberry Music Player + * Copyright 2022, Eoin O'Neill + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TAGREADERGME_H +#define TAGREADERGME_H + +#include + +#include +#include +#include + +#include "tagreaderbase.h" +#include "tagreadermessages.pb.h" + + +namespace GME { +bool IsSupportedFormat(const QFileInfo &file_info); +bool ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info); + +uint32_t UnpackBytes32(const char *const arr, size_t length); + +namespace SPC { +// SPC SPEC: http://vspcplay.raphnet.net/spc_file_format.txt + +constexpr int HAS_ID6_OFFSET = 0x23; +constexpr int SONG_TITLE_OFFSET = 0x2E; +constexpr int GAME_TITLE_OFFSET = 0x4E; +constexpr int DUMPER_OFFSET = 0x6E; +constexpr int COMMENTS_OFFSET = 0x7E; +// It seems that intro length and fade length are inconsistent from file to file. +// It should be looked into within the GME source code to see how GStreamer gets its values for playback length. +constexpr int INTRO_LENGTH_OFFSET = 0xA9; +constexpr int INTRO_LENGTH_SIZE = 3; +constexpr int FADE_LENGTH_OFFSET = 0xAC; +constexpr int FADE_LENGTH_SIZE = 4; +constexpr int ARTIST_OFFSET = 0xB1; +constexpr int XID6_OFFSET = (0x101C0 + 64); + +constexpr int NANO_PER_MS = 1000000; + +enum xID6_STATUS { + ON = 0x26, + OFF = 0x27, +}; + +enum xID6_ID { SongName = 0x01, GameName = 0x02, ArtistName = 0x03 }; + +enum xID6_TYPE { Length = 0x0, String = 0x1, Integer = 0x4 }; + +void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info); +qint16 GetNextMemAddressAlign32bit(qint16 input); +quint64 ConvertSPCStringToNum(const QByteArray &arr); +} // namespace SPC + +namespace VGM { +// VGM SPEC: +// http://www.smspower.org/uploads/Music/vgmspec170.txt?sid=17c810c54633b6dd4982f92f718361c1 +// GD3 TAG SPEC: +// http://www.smspower.org/uploads/Music/gd3spec100.txt +constexpr int GD3_TAG_PTR = 0x14; +constexpr int SAMPLE_COUNT = 0x18; +constexpr int LOOP_SAMPLE_COUNT = 0x20; +constexpr int SAMPLE_TIMEBASE = 44100; +constexpr int GST_GME_LOOP_TIME_MS = 8000; + +void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info); +// Takes in two QByteArrays, expected to be 4 bytes long. Desired length is returned via output parameter out_length. Returns false on error. +bool GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length); + +} // namespace VGM + +} // namespace GME + +// TagReaderGME +// Fulfills Strawberry's Intended interface for tag reading. +class TagReaderGME : public TagReaderBase { + + public: + explicit TagReaderGME(); + ~TagReaderGME(); + + bool IsMediaFile(const QString &filename) const override; + + bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; + bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; + + QByteArray LoadEmbeddedArt(const QString &filename) const override; + bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override; + + bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; + bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; +}; + +#endif diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto index 5a620821b..f058bd3dd 100644 --- a/ext/libstrawberry-tagreader/tagreadermessages.proto +++ b/ext/libstrawberry-tagreader/tagreadermessages.proto @@ -27,6 +27,8 @@ message SongMetadata { S3M = 19; XM = 20; IT = 21; + SPC = 22; + VGM = 23; CDDA = 90; STREAM = 91; } diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.cpp b/ext/libstrawberry-tagreader/tagreadertaglib.cpp index 46d068bbe..f89ad47c1 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.cpp +++ b/ext/libstrawberry-tagreader/tagreadertaglib.cpp @@ -186,7 +186,7 @@ spb::tagreader::SongMetadata_FileType TagReaderTagLib::GuessFileType(TagLib::Fil } -void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const { +bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const { const QByteArray url(QUrl::fromLocalFile(filename).toEncoded()); const QFileInfo fileinfo(filename); @@ -212,7 +212,7 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta std::unique_ptr fileref(factory_->GetFileRef(filename)); if (fileref->isNull()) { qLog(Info) << "TagLib hasn't been able to read" << filename << "file"; - return; + return false; } song->set_filetype(GuessFileType(fileref.get())); @@ -254,9 +254,7 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta } if (TagLib::FLAC::File *file_flac = dynamic_cast(fileref->file())) { - song->set_bitdepth(file_flac->audioProperties()->bitsPerSample()); - if (file_flac->xiphComment()) { ParseOggTag(file_flac->xiphComment()->fieldListMap(), &disc, &compilation, song); TagLib::List pictures = file_flac->pictureList(); @@ -517,6 +515,8 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta if (song->bitrate() <= 0) { song->set_bitrate(-1); } if (song->lastplayed() <= 0) { song->set_lastplayed(-1); } + return song->filetype() != spb::tagreader::SongMetadata_FileType_UNKNOWN; + } void TagReaderTagLib::Decode(const TagLib::String &tag, std::string *output) { @@ -526,12 +526,6 @@ void TagReaderTagLib::Decode(const TagLib::String &tag, std::string *output) { } -void TagReaderTagLib::Decode(const QString &tag, std::string *output) { - - output->assign(DataCommaSizeFromQString(tag)); - -} - void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const { if (!map["COMPOSER"].isEmpty()) Decode(map["COMPOSER"].front(), song->mutable_composer()); diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.h b/ext/libstrawberry-tagreader/tagreadertaglib.h index 2861a2895..4174a4b45 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.h +++ b/ext/libstrawberry-tagreader/tagreadertaglib.h @@ -50,7 +50,7 @@ class TagReaderTagLib : public TagReaderBase { bool IsMediaFile(const QString &filename) const override; - void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; + bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override; @@ -59,12 +59,11 @@ class TagReaderTagLib : public TagReaderBase { bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; + static void Decode(const TagLib::String &tag, std::string *output); + private: spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const; - static void Decode(const TagLib::String &tag, std::string *output); - static void Decode(const QString &tag, std::string *output); - void ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const; void ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const; diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.cpp b/ext/libstrawberry-tagreader/tagreadertagparser.cpp index f80bee7ec..dca23770b 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.cpp +++ b/ext/libstrawberry-tagreader/tagreadertagparser.cpp @@ -93,13 +93,13 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const { } -void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const { +bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const { qLog(Debug) << "Reading tags from" << filename; const QFileInfo fileinfo(filename); - if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return; + if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return false; const QByteArray url(QUrl::fromLocalFile(filename).toEncoded()); @@ -135,19 +135,19 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM taginfo.parseContainerFormat(diag, progress); if (progress.isAborted()) { taginfo.close(); - return; + return false; } taginfo.parseTracks(diag, progress); if (progress.isAborted()) { taginfo.close(); - return; + return false; } taginfo.parseTags(diag, progress); if (progress.isAborted()) { taginfo.close(); - return; + return false; } for (const TagParser::DiagMessage &msg : diag) { @@ -206,7 +206,7 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM if (song->filetype() == spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_UNKNOWN) { taginfo.close(); - return; + return false; } for (const auto tag : taginfo.tags()) { @@ -247,8 +247,12 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM taginfo.close(); + return true; + + } + catch(...) { + return false; } - catch(...) {} } diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.h b/ext/libstrawberry-tagreader/tagreadertagparser.h index 7880613dc..95ff65ba9 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.h +++ b/ext/libstrawberry-tagreader/tagreadertagparser.h @@ -39,7 +39,7 @@ class TagReaderTagParser : public TagReaderBase { bool IsMediaFile(const QString &filename) const override; - void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; + bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override; bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override; QByteArray LoadEmbeddedArt(const QString &filename) const override; diff --git a/ext/strawberry-tagreader/tagreaderworker.cpp b/ext/strawberry-tagreader/tagreaderworker.cpp index 1c59aa1db..1ecc2db19 100644 --- a/ext/strawberry-tagreader/tagreaderworker.cpp +++ b/ext/strawberry-tagreader/tagreaderworker.cpp @@ -34,28 +34,11 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) { spb::tagreader::Message reply; - if (message.has_is_media_file_request()) { - reply.mutable_is_media_file_response()->set_success(tag_reader_.IsMediaFile(QStringFromStdString(message.is_media_file_request().filename()))); - } - else if (message.has_read_file_request()) { - tag_reader_.ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata()); - } - else if (message.has_save_file_request()) { - reply.mutable_save_file_response()->set_success(tag_reader_.SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata())); - } - else if (message.has_load_embedded_art_request()) { - QByteArray data = tag_reader_.LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename())); - reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size()); - } - else if (message.has_save_embedded_art_request()) { - reply.mutable_save_embedded_art_response()->set_success(tag_reader_.SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast(message.save_embedded_art_request().data().size())))); - } - - else if (message.has_save_song_playcount_to_file_request()) { - reply.mutable_save_song_playcount_to_file_response()->set_success(tag_reader_.SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata())); - } - else if (message.has_save_song_rating_to_file_request()) { - reply.mutable_save_song_rating_to_file_response()->set_success(tag_reader_.SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata())); + bool success = HandleMessage(message, reply, &tag_reader_); + if (!success) { +#if defined(USE_TAGLIB) + HandleMessage(message, reply, &tag_reader_gme_); +#endif } SendReply(message, &reply); @@ -63,7 +46,50 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) { } void TagReaderWorker::DeviceClosed() { + AbstractMessageHandler::DeviceClosed(); QCoreApplication::exit(); + +} + +bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase *reader) { + + if (message.has_is_media_file_request()) { + bool success = reader->IsMediaFile(QStringFromStdString(message.is_media_file_request().filename())); + reply.mutable_is_media_file_response()->set_success(success); + return success; + } + else if (message.has_read_file_request()) { + bool success = reader->ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata()); + return success; + } + else if (message.has_save_file_request()) { + bool success = reader->SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata()); + reply.mutable_save_file_response()->set_success(success); + return success; + } + else if (message.has_load_embedded_art_request()) { + QByteArray data = reader->LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename())); + reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size()); + return true; + } + else if (message.has_save_embedded_art_request()) { + bool success = reader->SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast(message.save_embedded_art_request().data().size()))); + reply.mutable_save_embedded_art_response()->set_success(success); + return success; + } + else if (message.has_save_song_playcount_to_file_request()) { + bool success = reader->SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata()); + reply.mutable_save_song_playcount_to_file_response()->set_success(success); + return success; + } + else if (message.has_save_song_rating_to_file_request()) { + bool success = reader->SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata()); + reply.mutable_save_song_rating_to_file_response()->set_success(success); + return success; + } + + return false; + } diff --git a/ext/strawberry-tagreader/tagreaderworker.h b/ext/strawberry-tagreader/tagreaderworker.h index b8753f75c..cb0c12fec 100644 --- a/ext/strawberry-tagreader/tagreaderworker.h +++ b/ext/strawberry-tagreader/tagreaderworker.h @@ -26,9 +26,11 @@ #include "core/messagehandler.h" #if defined(USE_TAGLIB) # include "tagreadertaglib.h" +# include "tagreadergme.h" #elif defined(USE_TAGPARSER) # include "tagreadertagparser.h" #endif + #include "tagreadermessages.pb.h" class QIODevice; @@ -44,8 +46,12 @@ class TagReaderWorker : public AbstractMessageHandler { void DeviceClosed() override; private: + // Handle message using specific TagReaderBase implementation. Returns true on successful message handle. + bool HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase* reader); + #if defined(USE_TAGLIB) TagReaderTagLib tag_reader_; + TagReaderGME tag_reader_gme_; #elif defined(USE_TAGPARSER) TagReaderTagParser tag_reader_; #endif diff --git a/src/core/song.cpp b/src/core/song.cpp index 4ebd49fef..1fda707c2 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -52,7 +52,6 @@ #include #include "core/logging.h" -#include "core/messagehandler.h" #include "core/iconloader.h" #include "engine/enginebase.h" @@ -65,6 +64,9 @@ #include "sqlrow.h" #include "tagreadermessages.pb.h" +#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size()) +#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() + const QStringList Song::kColumns = QStringList() << "title" << "album" << "artist" @@ -156,7 +158,7 @@ const QStringList Song::kArticles = QStringList() << "the " << "a " << "an "; const QStringList Song::kAcceptedExtensions = QStringList() << "wav" << "flac" << "wv" << "ogg" << "oga" << "opus" << "spx" << "ape" << "mpc" << "mp2" << "mp3" << "m4a" << "mp4" << "aac" << "asf" << "asx" << "wma" << "aif << aiff" << "mka" << "tta" << "dsf" << "dsd" - << "ac3" << "dts"; + << "ac3" << "dts" << "spc" << "vgm"; struct Song::Private : public QSharedData { @@ -604,6 +606,8 @@ QString Song::TextForFiletype(FileType filetype) { case Song::FileType_XM: return "Module Music Format"; case Song::FileType_IT: return "Module Music Format"; case Song::FileType_CDDA: return "CDDA"; + case Song::FileType_SPC: return "SNES SPC700"; + case Song::FileType_VGM: return "VGM"; case Song::FileType_Stream: return "Stream"; case Song::FileType_Unknown: default: return QObject::tr("Unknown"); @@ -634,6 +638,8 @@ QString Song::ExtensionForFiletype(FileType filetype) { case Song::FileType_S3M: return "s3m"; case Song::FileType_XM: return "xm"; case Song::FileType_IT: return "it"; + case Song::FileType_SPC: return "spc"; + case Song::FileType_VGM: return "vgm"; case Song::FileType_Unknown: default: return "dat"; } @@ -710,6 +716,8 @@ Song::FileType Song::FiletypeByMimetype(const QString &mimetype) { else if (mimetype.compare("audio/x-ape", Qt::CaseInsensitive) == 0 || mimetype.compare("application/x-ape", Qt::CaseInsensitive) == 0 || mimetype.compare("audio/x-ffmpeg-parsed-ape", Qt::CaseInsensitive) == 0) return Song::FileType_APE; else if (mimetype.compare("audio/x-mod", Qt::CaseInsensitive) == 0) return Song::FileType_MOD; else if (mimetype.compare("audio/x-s3m", Qt::CaseInsensitive) == 0) return Song::FileType_S3M; + else if (mimetype.compare("audio/x-spc", Qt::CaseInsensitive) == 0) return Song::FileType_SPC; + else if (mimetype.compare("audio/x-vgm", Qt::CaseInsensitive) == 0) return Song::FileType_VGM; else return Song::FileType_Unknown; @@ -733,6 +741,8 @@ Song::FileType Song::FiletypeByDescription(const QString &text) { else if (text.compare("audio/x-ffmpeg-parsed-ape", Qt::CaseInsensitive) == 0) return Song::FileType_APE; else if (text.compare("Module Music Format (MOD)", Qt::CaseInsensitive) == 0) return Song::FileType_MOD; else if (text.compare("Module Music Format (MOD)", Qt::CaseInsensitive) == 0) return Song::FileType_S3M; + else if (text.compare("SNES SPC700", Qt::CaseInsensitive) == 0) return Song::FileType_SPC; + else if (text.compare("VGM", Qt::CaseInsensitive) == 0) return Song::FileType_VGM; else return Song::FileType_Unknown; } @@ -760,6 +770,8 @@ Song::FileType Song::FiletypeByExtension(const QString &ext) { else if (ext.compare("s3m", Qt::CaseInsensitive) == 0) return Song::FileType_S3M; else if (ext.compare("xm", Qt::CaseInsensitive) == 0) return Song::FileType_XM; else if (ext.compare("it", Qt::CaseInsensitive) == 0) return Song::FileType_IT; + else if (ext.compare("spc", Qt::CaseInsensitive) == 0) return Song::FileType_SPC; + else if (ext.compare("vgm", Qt::CaseInsensitive) == 0) return Song::FileType_VGM; else return Song::FileType_Unknown; diff --git a/src/core/song.h b/src/core/song.h index 5c5f63e95..6af4e7625 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -106,6 +106,8 @@ class Song { FileType_S3M = 19, FileType_XM = 20, FileType_IT = 21, + FileType_SPC = 22, + FileType_VGM = 23, FileType_CDDA = 90, FileType_Stream = 91, }; diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp index 3383f20c7..72e097906 100644 --- a/src/core/tagreaderclient.cpp +++ b/src/core/tagreaderclient.cpp @@ -39,6 +39,8 @@ #include "tagreaderclient.h" #include "settings/collectionsettingspage.h" +#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length() + const char *TagReaderClient::kWorkerExecutableName = "strawberry-tagreader"; TagReaderClient *TagReaderClient::sInstance = nullptr; diff --git a/src/widgets/fileview.cpp b/src/widgets/fileview.cpp index 438ae89f0..826a29036 100644 --- a/src/widgets/fileview.cpp +++ b/src/widgets/fileview.cpp @@ -52,7 +52,8 @@ const char *FileView::kFileFilter = "*.aif *.aiff *.mka *.tta *.dsf *.dsd " "*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini " "*.ac3 *.dts " - "*.mod *.s3m *.xm *.it"; + "*.mod *.s3m *.xm *.it" + "*.spc *.vgm"; FileView::FileView(QWidget *parent) : QWidget(parent),