Scan for audio properties, metadata, and embedded images in one single pass. Removed now useless lmsmetadata library + reworked code accordingly

This commit is contained in:
emeric
2025-11-02 16:23:10 +01:00
parent 8e554b258c
commit b25e3b9b1e
112 changed files with 4005 additions and 3379 deletions
+40
View File
@@ -0,0 +1,40 @@
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
add_library(lmsaudio STATIC
impl/ffmpeg/AudioFile.cpp
impl/ffmpeg/AudioFileInfo.cpp
impl/ffmpeg/ImageReader.cpp
impl/ffmpeg/TagReader.cpp
impl/ffmpeg/Transcoder.cpp
impl/ffmpeg/Utils.cpp
impl/taglib/AudioFileInfo.cpp
impl/taglib/ImageReader.cpp
impl/taglib/TagReader.cpp
impl/taglib/Utils.cpp
impl/AudioTypes.cpp
impl/ImageReader.cpp
impl/ParseAudioFileInfo.cpp
impl/TagReader.cpp
)
target_include_directories(lmsaudio INTERFACE
include
)
target_include_directories(lmsaudio PRIVATE
include
${AVCODEC_INCLUDE_DIR}
${AVFORMAT_INCLUDE_DIR}
${AVUTIL_INCLUDE_DIR}
)
target_link_libraries(lmsaudio PUBLIC
lmscore
std::filesystem
)
target_link_libraries(lmsaudio PRIVATE
PkgConfig::LIBAV
PkgConfig::Taglib
)
+109
View File
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "audio/AudioTypes.hpp"
namespace lms::audio
{
core::LiteralString containerTypeToString(ContainerType type)
{
switch (type)
{
case ContainerType::AIFF:
return "AIFF";
case ContainerType::APE:
return "APE";
case ContainerType::ASF:
return "ASF";
case ContainerType::DSF:
return "DSF";
case ContainerType::FLAC:
return "FLAC";
case ContainerType::MP4:
return "MP4";
case ContainerType::MPC:
return "MPC";
case ContainerType::MPEG:
return "MPEG";
case ContainerType::Ogg:
return "Ogg";
case ContainerType::Shorten:
return "Shorten";
case ContainerType::TrueAudio:
return "TrueAudio";
case ContainerType::WAV:
return "WAV";
case ContainerType::WavPack:
return "WavPack";
}
return "";
}
core::LiteralString codecTypeToString(CodecType type)
{
switch (type)
{
case CodecType::AAC:
return "AAC";
case CodecType::AC3:
return "AC3";
case CodecType::ALAC:
return "ALAC";
case CodecType::APE:
return "APE";
case CodecType::EAC3:
return "EAC3";
case CodecType::DSD:
return "DSD";
case CodecType::FLAC:
return "FLAC";
case CodecType::MP3:
return "MP3";
case CodecType::MP4ALS:
return "MP4ALS";
case CodecType::MPC7:
return "MPC7";
case CodecType::MPC8:
return "MPC8";
case CodecType::Opus:
return "Opus";
case CodecType::PCM:
return "PCM";
case CodecType::Shorten:
return "Shorten";
case CodecType::TrueAudio:
return "TrueAudio";
case CodecType::Vorbis:
return "Vorbis";
case CodecType::WavPack:
return "WavPack";
case CodecType::WMA1:
return "WMA1";
case CodecType::WMA2:
return "WMA2";
case CodecType::WMA9Pro:
return "WMA9Pro";
case CodecType::WMA9Lossless:
return "WMA9Lossless";
}
return "";
}
} // namespace lms::audio
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "audio/IImageReader.hpp"
namespace lms::audio
{
core::LiteralString imageTypeToString(Image::Type type)
{
switch (type)
{
case Image::Type::Other:
return "Other";
case Image::Type::FileIcon:
return "FileIcon";
case Image::Type::OtherFileIcon:
return "OtherFileIcon";
case Image::Type::FrontCover:
return "FrontCover";
case Image::Type::BackCover:
return "BackCover";
case Image::Type::LeafletPage:
return "LeafletPage";
case Image::Type::Media:
return "Media";
case Image::Type::LeadArtist:
return "LeadArtist";
case Image::Type::Artist:
return "Artist";
case Image::Type::Conductor:
return "Conductor";
case Image::Type::Band:
return "Band";
case Image::Type::Composer:
return "Composer";
case Image::Type::Lyricist:
return "Lyricist";
case Image::Type::RecordingLocation:
return "RecordingLocation";
case Image::Type::DuringRecording:
return "DuringRecording";
case Image::Type::DuringPerformance:
return "DuringPerformance";
case Image::Type::MovieScreenCapture:
return "MovieScreenCapture";
case Image::Type::ColouredFish:
return "ColouredFish";
case Image::Type::Illustration:
return "Illustration";
case Image::Type::BandLogo:
return "BandLogo";
case Image::Type::PublisherLogo:
return "PublisherLogo";
case Image::Type::Unknown:
break;
}
return "Unknown";
}
} // namespace lms::audio
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <memory>
#include "audio/IAudioFileInfo.hpp"
#include "ffmpeg/AudioFileInfo.hpp"
#include "ffmpeg/Utils.hpp"
#include "taglib/AudioFileInfo.hpp"
#include "taglib/Utils.hpp"
namespace lms::audio
{
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
{
switch (parserOptions.parser)
{
case ParserOptions::Parser::TagLib:
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
case ParserOptions::Parser::FFmpeg:
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
}
return {};
}
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::Parser parser)
{
switch (parser)
{
case ParserOptions::Parser::TagLib:
return taglib::utils::getSupportedExtensions();
case ParserOptions::Parser::FFmpeg:
return ffmpeg::utils::getSupportedExtensions();
}
return {};
}
} // namespace lms::audio
+277
View File
@@ -0,0 +1,277 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "audio/ITagReader.hpp"
namespace lms::audio
{
core::LiteralString tagTypeToString(TagType type)
{
switch (type)
{
case TagType::AcoustID:
return "AcoustID";
case TagType::AcoustIDFingerprint:
return "AcoustIDFingerprint";
case TagType::Advisory:
return "Advisory";
case TagType::Album:
return "Album";
case TagType::AlbumArtist:
return "AlbumArtist";
case TagType::AlbumArtists:
return "AlbumArtists";
case TagType::AlbumArtistSortOrder:
return "AlbumArtistSortOrder";
case TagType::AlbumArtistsSortOrder:
return "AlbumArtistsSortOrder";
case TagType::AlbumComment:
return "AlbumComment";
case TagType::AlbumSortOrder:
return "AlbumSortOrder";
case TagType::Arranger:
return "Arranger";
case TagType::Artist:
return "Artist";
case TagType::ArtistSortOrder:
return "ArtistSortOrder";
case TagType::Artists:
return "Artists";
case TagType::ArtistsSortOrder:
return "ArtistsSortOrder";
case TagType::ASIN:
return "ASIN";
case TagType::Barcode:
return "Barcode";
case TagType::BPM:
return "BPM";
case TagType::CatalogNumber:
return "CatalogNumber";
case TagType::Comment:
return "Comment";
case TagType::Compilation:
return "Compilation";
case TagType::Composer:
return "Composer";
case TagType::ComposerSortOrder:
return "ComposerSortOrder";
case TagType::Composers:
return "Composers";
case TagType::ComposersSortOrder:
return "ComposersSortOrder";
case TagType::Conductor:
return "Conductor";
case TagType::ConductorSortOrder:
return "ConductorSortOrder";
case TagType::Conductors:
return "Conductors";
case TagType::ConductorsSortOrder:
return "ConductorsSortOrder";
case TagType::Copyright:
return "Copyright";
case TagType::CopyrightURL:
return "CopyrightURL";
case TagType::Date:
return "Date";
case TagType::Director:
return "Director";
case TagType::DiscNumber:
return "DiscNumber";
case TagType::DiscSubtitle:
return "DiscSubtitle";
case TagType::EncodedBy:
return "EncodedBy";
case TagType::EncoderSettings:
return "EncoderSettings";
case TagType::EncodingTime:
return "EncodingTime";
case TagType::Engineer:
return "Engineer";
case TagType::GaplessPlayback:
return "GaplessPlayback";
case TagType::Genre:
return "Genre";
case TagType::Grouping:
return "Grouping";
case TagType::InitialKey:
return "InitialKey";
case TagType::ISRC:
return "ISRC";
case TagType::Language:
return "Language";
case TagType::Lyricist:
return "Lyricist";
case TagType::LyricistSortOrder:
return "LyricistSortOrder";
case TagType::Lyricists:
return "Lyricists";
case TagType::LyricistsSortOrder:
return "LyricistsSortOrder";
case TagType::Media:
return "Media";
case TagType::MixDJ:
return "MixDJ";
case TagType::Mixer:
return "Mixer";
case TagType::MixerSortOrder:
return "MixerSortOrder";
case TagType::Mixers:
return "Mixers";
case TagType::MixersSortOrder:
return "MixersSortOrder";
case TagType::Movement:
return "Movement";
case TagType::MovementCount:
return "MovementCount";
case TagType::MovementNumber:
return "MovementNumber";
case TagType::Mood:
return "Mood";
case TagType::MusicBrainzArtistID:
return "MusicBrainzArtistID";
case TagType::MusicBrainzArrangerID:
return "MusicBrainzArrangerID";
case TagType::MusicBrainzComposerID:
return "MusicBrainzComposerID";
case TagType::MusicBrainzConductorID:
return "MusicBrainzConductorID";
case TagType::MusicBrainzDirectorID:
return "MusicBrainzDirectorID";
case TagType::MusicBrainzDiscID:
return "MusicBrainzDiscID";
case TagType::MusicBrainzLyricistID:
return "MusicBrainzLyricistID";
case TagType::MusicBrainzProducerID:
return "MusicBrainzProducerID";
case TagType::MusicBrainzOriginalArtistID:
return "MusicBrainzOriginalArtistID";
case TagType::MusicBrainzRecordingID:
return "MusicBrainzRecordingID";
case TagType::MusicBrainzRemixerID:
return "MusicBrainzRemixerID";
case TagType::MusicBrainzWorkID:
return "MusicBrainzWorkID";
case TagType::Remixer:
return "Remixer";
case TagType::Script:
return "Script";
case TagType::ShowName:
return "ShowName";
case TagType::ShowNameSortOrder:
return "ShowNameSortOrder";
case TagType::ShowWorkAndMovement:
return "ShowWorkAndMovement";
case TagType::Subtitle:
return "Subtitle";
case TagType::TrackNumber:
return "TrackNumber";
case TagType::TrackTitle:
return "TrackTitle";
case TagType::TrackTitleSortOrder:
return "TrackTitleSortOrder";
case TagType::WorkTitle:
return "WorkTitle";
case TagType::Writer:
return "Writer";
case TagType::License:
return "License";
case TagType::MusicBrainzOriginalReleaseID:
return "MusicBrainzOriginalReleaseID";
case TagType::MusicBrainzMixerID:
return "MusicBrainzMixerID";
case TagType::MusicBrainzReleaseArtistID:
return "MusicBrainzReleaseArtistID";
case TagType::MusicBrainzReleaseGroupID:
return "MusicBrainzReleaseGroupID";
case TagType::MusicBrainzReleaseID:
return "MusicBrainzReleaseID";
case TagType::MusicBrainzTrackID:
return "MusicBrainzTrackID";
case TagType::MusicIPFingerprint:
return "MusicIPFingerprint";
case TagType::MusicIPPUID:
return "MusicIPPUID";
case TagType::OriginalAlbum:
return "OriginalAlbum";
case TagType::OriginalArtist:
return "OriginalArtist";
case TagType::OriginalFilename:
return "OriginalFilename";
case TagType::OriginalReleaseDate:
return "OriginalReleaseDate";
case TagType::OriginalReleaseYear:
return "OriginalReleaseYear";
case TagType::Podcast:
return "Podcast";
case TagType::PodcastURL:
return "PodcastURL";
case TagType::Producer:
return "Producer";
case TagType::ProducerSortOrder:
return "ProducerSortOrder";
case TagType::Producers:
return "Producers";
case TagType::ProducersSortOrder:
return "ProducersSortOrder";
case TagType::Rating:
return "Rating";
case TagType::RecordLabel:
return "RecordLabel";
case TagType::ReleaseCountry:
return "ReleaseCountry";
case TagType::ReleaseDate:
return "ReleaseDate";
case TagType::ReleaseStatus:
return "ReleaseStatus";
case TagType::ReleaseType:
return "ReleaseType";
case TagType::RemixerSortOrder:
return "RemixerSortOrder";
case TagType::Remixers:
return "Remixers";
case TagType::RemixersSortOrder:
return "RemixersSortOrder";
case TagType::ReplayGainAlbumGain:
return "ReplayGainAlbumGain";
case TagType::ReplayGainAlbumPeak:
return "ReplayGainAlbumPeak";
case TagType::ReplayGainAlbumRange:
return "ReplayGainAlbumRange";
case TagType::ReplayGainReferenceLoudness:
return "ReplayGainReferenceLoudness";
case TagType::ReplayGainTrackGain:
return "ReplayGainTrackGain";
case TagType::ReplayGainTrackPeak:
return "ReplayGainTrackPeak";
case TagType::ReplayGainTrackRange:
return "ReplayGainTrackRange";
case TagType::TotalDiscs:
return "TotalDiscs";
case TagType::TotalTracks:
return "TotalTracks";
case TagType::Website:
return "Website";
case TagType::Count:
break;
}
return "";
}
} // namespace lms::audio
+365
View File
@@ -0,0 +1,365 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFile.hpp"
#include <array>
#include <unordered_map>
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
}
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::ffmpeg
{
namespace
{
std::string averror_to_string(int error)
{
std::array<char, 128> buf = { 0 };
if (::av_strerror(error, buf.data(), buf.size()) == 0)
return buf.data();
return "Unknown error";
}
class AudioFileException : public AudioFileParsingException
{
public:
AudioFileException(int avError)
: AudioFileParsingException{ averror_to_string(avError) }
{
}
};
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
{
if (!dictionnary)
return;
AVDictionaryEntry* tag = NULL;
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
{
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
}
}
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
{
if (name == "aiff")
return ContainerType::AIFF;
if (name == "ape")
return ContainerType::APE;
if (name.starts_with("asf"))
return ContainerType::ASF;
if (name == "dsf")
return ContainerType::DSF;
if (name == "flac")
return ContainerType::FLAC;
if (name.find("mp4") != std::string_view::npos)
return ContainerType::MP4;
if (name.starts_with("mpc"))
return ContainerType::MPC;
if (name == "mp3")
return ContainerType::MPEG;
if (name == "ogg")
return ContainerType::Ogg;
if (name == "shn")
return ContainerType::Shorten;
if (name == "tta")
return ContainerType::TrueAudio;
if (name == "wav")
return ContainerType::WAV;
if (name == "wv")
return ContainerType::WavPack;
return std::nullopt;
}
std::optional<CodecType> avcodecToCodecType(AVCodecID codec)
{
switch (codec)
{
case AV_CODEC_ID_MP3:
return CodecType::MP3;
case AV_CODEC_ID_AAC:
return CodecType::AAC;
case AV_CODEC_ID_AC3:
return CodecType::AC3;
case AV_CODEC_ID_VORBIS:
return CodecType::Vorbis;
case AV_CODEC_ID_WMAV1:
return CodecType::WMA1;
case AV_CODEC_ID_WMAV2:
return CodecType::WMA2;
case AV_CODEC_ID_WMAPRO:
return CodecType::WMA9Pro;
case AV_CODEC_ID_WMALOSSLESS:
return CodecType::WMA9Lossless;
case AV_CODEC_ID_FLAC:
return CodecType::FLAC;
case AV_CODEC_ID_ALAC:
return CodecType::ALAC;
case AV_CODEC_ID_WAVPACK:
return CodecType::WavPack;
case AV_CODEC_ID_MUSEPACK7:
return CodecType::MPC7;
case AV_CODEC_ID_MUSEPACK8:
return CodecType::MPC8;
case AV_CODEC_ID_APE:
return CodecType::APE;
case AV_CODEC_ID_EAC3:
return CodecType::EAC3;
case AV_CODEC_ID_MP4ALS:
return CodecType::MP4ALS;
case AV_CODEC_ID_OPUS:
return CodecType::Opus;
case AV_CODEC_ID_SHORTEN:
return CodecType::Shorten;
case AV_CODEC_ID_DSD_LSBF:
case AV_CODEC_ID_DSD_LSBF_PLANAR:
case AV_CODEC_ID_DSD_MSBF:
case AV_CODEC_ID_DSD_MSBF_PLANAR:
return CodecType::DSD;
default:
return std::nullopt;
}
}
} // namespace
AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p }
{
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
if (error < 0)
{
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
throw AudioFileException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
avformat_close_input(&_context);
throw AudioFileException{ error };
}
}
AudioFile::~AudioFile()
{
avformat_close_input(&_context);
}
const std::filesystem::path& AudioFile::getPath() const
{
return _p;
}
ContainerInfo AudioFile::getContainerInfo() const
{
ContainerInfo info;
info.container = avdemuxerToContainerType(_context->iformat->name);
info.containerName = _context->iformat->name;
info.bitrate = _context->bit_rate;
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
return info;
}
AudioFile::MetadataMap AudioFile::getMetaData() const
{
MetadataMap res;
getMetaDataFromDictionnary(_context->metadata, res);
// HACK for OGG files
// If we did not find tags, search metadata in streams
if (res.empty())
{
for (std::size_t i{}; i < _context->nb_streams; ++i)
{
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
if (!res.empty())
break;
}
}
return res;
}
std::vector<StreamInfo> AudioFile::getStreamInfo() const
{
std::vector<StreamInfo> res;
for (std::size_t i{}; i < _context->nb_streams; ++i)
{
std::optional<StreamInfo> streamInfo{ getStreamInfo(i) };
if (streamInfo)
res.emplace_back(std::move(*streamInfo));
}
return res;
}
std::optional<std::size_t> AudioFile::getBestStreamIndex() const
{
int res = ::av_find_best_stream(_context,
AVMEDIA_TYPE_AUDIO,
-1, // Auto
-1, // Auto
NULL,
0);
if (res < 0)
return std::nullopt;
return res;
}
std::optional<StreamInfo> AudioFile::getBestStreamInfo() const
{
std::optional<StreamInfo> res;
std::optional<std::size_t> bestStreamIndex{ getBestStreamIndex() };
if (bestStreamIndex)
res = getStreamInfo(*bestStreamIndex);
return res;
}
bool AudioFile::hasAttachedPictures() const
{
for (std::size_t i = 0; i < _context->nb_streams; ++i)
{
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
return true;
}
return false;
}
void AudioFile::visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const
{
static const std::unordered_map<int, std::string> codecMimeMap{
{ AV_CODEC_ID_BMP, "image/bmp" },
{ AV_CODEC_ID_GIF, "image/gif" },
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
{ AV_CODEC_ID_PNG, "image/png" },
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
};
for (std::size_t i = 0; i < _context->nb_streams; ++i)
{
AVStream* avstream = _context->streams[i];
// Skip attached pics
if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
continue;
if (avstream->codecpar == nullptr)
{
LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
continue;
}
MetadataMap metadata;
getMetaDataFromDictionnary(avstream->metadata, metadata);
Picture picture;
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
if (itMime != codecMimeMap.end())
{
picture.mimeType = itMime->second;
}
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
}
const AVPacket& pkt{ avstream->attached_pic };
picture.data = std::span{ reinterpret_cast<const std::byte*>(pkt.data), static_cast<std::size_t>(pkt.size) };
func(picture, metadata);
}
}
std::optional<StreamInfo> AudioFile::getStreamInfo(std::size_t streamIndex) const
{
std::optional<StreamInfo> res;
AVStream* avstream{ _context->streams[streamIndex] };
assert(avstream);
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
return res;
if (!avstream->codecpar)
{
LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
return res;
}
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
return res;
res.emplace();
res->index = streamIndex;
res->codec = avcodecToCodecType(avstream->codecpar->codec_id);
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
if (avstream->codecpar->bit_rate)
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
if (avstream->codecpar->bits_per_coded_sample)
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
else if (avstream->codecpar->bits_per_raw_sample)
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_raw_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
if (avstream->codecpar->channels)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
if (avstream->codecpar->ch_layout.nb_channels)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
#endif
assert(!res->codecName.empty()); // doc says it is never NULL
if (avstream->codecpar->sample_rate)
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
return res;
}
} // namespace lms::audio::ffmpeg
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <optional>
#include <span>
#include <string>
#include <vector>
#include "audio/AudioTypes.hpp"
extern "C"
{
struct AVFormatContext;
}
namespace lms::audio::ffmpeg
{
struct Picture
{
std::string mimeType;
std::span<const std::byte> data; // valid as long as IAudioFile exists
};
struct ContainerInfo
{
std::optional<ContainerType> container;
std::string containerName;
std::size_t bitrate{};
std::chrono::milliseconds duration{};
};
struct StreamInfo
{
size_t index{};
std::optional<CodecType> codec;
std::string codecName;
std::optional<size_t> bitrate;
std::optional<std::size_t> bitsPerSample;
std::optional<std::size_t> channelCount;
std::optional<std::size_t> sampleRate;
};
class AudioFile
{
public:
AudioFile(const std::filesystem::path& p);
~AudioFile();
AudioFile(const AudioFile&) = delete;
AudioFile& operator=(const AudioFile&) = delete;
using MetadataMap = std::unordered_map<std::string, std::string>;
const std::filesystem::path& getPath() const;
ContainerInfo getContainerInfo() const;
MetadataMap getMetaData() const;
std::vector<StreamInfo> getStreamInfo() const;
std::optional<StreamInfo> getBestStreamInfo() const;
std::optional<std::size_t> getBestStreamIndex() const;
bool hasAttachedPictures() const;
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p;
AVFormatContext* _context{};
};
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileInfo.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "AudioFile.hpp"
#include "ImageReader.hpp"
#include "TagReader.hpp"
namespace lms::audio::ffmpeg
{
namespace
{
AudioProperties computeAudioProperties(const AudioFile& audioFile)
{
AudioProperties audioProperties;
const auto containerInfo{ audioFile.getContainerInfo() };
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
if (!bestStreamInfo)
throw AudioFileParsingException{ audioFile.getPath(), "Cannot find best audio stream" };
if (!containerInfo.container)
throw AudioFileParsingException{ audioFile.getPath(), "Unhandled container type '" + containerInfo.containerName + "'" };
audioProperties.container = *containerInfo.container;
audioProperties.duration = containerInfo.duration;
audioProperties.codec = bestStreamInfo->codec;
audioProperties.bitrate = bestStreamInfo->bitrate;
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
audioProperties.channelCount = bestStreamInfo->channelCount;
audioProperties.sampleRate = bestStreamInfo->sampleRate;
return audioProperties;
}
} // namespace
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
: _audioFile{ std::make_unique<AudioFile>(filePath) }
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
{
}
AudioFileInfo::~AudioFileInfo() = default;
const AudioProperties& AudioFileInfo::getAudioProperties() const
{
return *_audioProperties;
}
const IImageReader& AudioFileInfo::getImageReader() const
{
return *_imageReader;
}
const ITagReader& AudioFileInfo::getTagReader() const
{
return *_tagReader;
}
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::ffmpeg
{
class AudioFile;
class TagReader;
class ImageReader;
class AudioFileInfo final : public IAudioFileInfo
{
public:
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
~AudioFileInfo();
AudioFileInfo(const AudioFileInfo&) = delete;
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
private:
const AudioProperties& getAudioProperties() const override;
const IImageReader& getImageReader() const override;
const ITagReader& getTagReader() const override;
std::unique_ptr<AudioFile> _audioFile;
std::unique_ptr<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ImageReader.hpp"
#include <algorithm>
#include "core/String.hpp"
#include "AudioFile.hpp"
namespace lms::audio::ffmpeg
{
ImageReader::ImageReader(const AudioFile& audioFile)
: _audioFile{ audioFile }
{
}
ImageReader::~ImageReader() = default;
void ImageReader::visitImages(const ImageVisitor& visitor) const
{
auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) {
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
} };
_audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
Image image;
image.data = picture.data;
image.mimeType = picture.mimeType;
if (metaDataHasKeyword(metaData, "front"))
image.type = Image::Type::FrontCover;
else if (metaDataHasKeyword(metaData, "back"))
image.type = Image::Type::BackCover;
visitor(image);
});
}
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/IImageReader.hpp"
namespace lms::audio::ffmpeg
{
class AudioFile;
class ImageReader : public IImageReader
{
public:
ImageReader(const AudioFile& audioFile);
~ImageReader() override;
ImageReader(const ImageReader&) = delete;
ImageReader& operator=(const ImageReader&) = delete;
private:
void visitImages(const ImageVisitor& visitor) const override;
const AudioFile& _audioFile;
};
} // namespace lms::audio::ffmpeg
+213
View File
@@ -0,0 +1,213 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagReader.hpp"
#include "core/ILogger.hpp"
#include "core/String.hpp"
namespace lms::audio::ffmpeg
{
namespace
{
// Mapping to internal avformat names and/or common alternative custom names
static const std::unordered_map<TagType, std::vector<std::string>> avFormatTagMapping{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
{ TagType::Advisory, { "ITUNESADVISORY" } },
{ TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
{ TagType::AlbumArtist, { "ALBUMARTIST", "ALBUM_ARTIST" } },
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } },
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
{ TagType::AlbumComment, { "ALBUMCOMMENT", "MUSICBRAINZ_ALBUMCOMMENT, MUSICBRAINZ ALBUM COMMENT", "MUSICBRAINZ/ALBUM COMMENT", "ALBUMVERSION", "VERSION" } },
{ TagType::AlbumSortOrder, { "ALBUMSORT", "ALBUM-SORT" } },
{ TagType::Arranger, { "ARRANGER" } },
{ TagType::Artist, { "ARTIST" } },
{ TagType::ArtistSortOrder, { "ARTISTSORT", "ARTIST-SORT", "WM/ARTISTSORTORDER" } },
{ TagType::Artists, { "ARTISTS", "WM/ARTISTS" } },
{ TagType::ArtistsSortOrder, { "ARTISTSSORT", "ARTISTS-SORT", "WM/ARTISTSSORTORDER" } },
{ TagType::ASIN, { "ASIN" } },
{ TagType::Barcode, { "BARCODE", "WM/BARCODE" } },
{ TagType::BPM, { "BPM" } },
{ TagType::CatalogNumber, { "CATALOGNUMBER", "WM/CATALOGNO" } },
{ TagType::Comment, { "COMMENT" } },
{ TagType::Compilation, { "COMPILATION", "TCMP" } },
{ TagType::Composer, { "COMPOSER" } },
{ TagType::Composers, { "COMPOSERS" } },
{ TagType::ComposerSortOrder, { "COMPOSERSORT", "TSOC" } },
{ TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
{ TagType::Conductor, { "CONDUCTOR" } },
{ TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
{ TagType::Conductors, { "CONDUCTORS" } },
{ TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
{ TagType::Copyright, { "COPYRIGHT" } },
{ TagType::CopyrightURL, { "COPYRIGHTURL" } },
{ TagType::Date, { "DATE", "YEAR", "WM/YEAR" } },
{ TagType::Director, { "DIRECTOR" } },
{ TagType::DiscNumber, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" } },
{ TagType::DiscSubtitle, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" } },
{ TagType::EncodedBy, { "ENCODEDBY" } },
{ TagType::EncodingTime, { "ENCODINGTIME", "TDEN" } },
{ TagType::Engineer, { "ENGINEER" } },
{ TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
{ TagType::Genre, { "GENRE" } },
{ TagType::Grouping, { "GROUPING", "WM/CONTENTGROUPDESCRIPTION", "ALBUMGROUPING" } },
{ TagType::InitialKey, { "INITIALKEY" } },
{ TagType::ISRC, { "ISRC", "WM/ISRC", "TSRC" } },
{ TagType::Language, { "LANGUAGE" } },
{ TagType::License, { "LICENSE" } },
{ TagType::Lyricist, { "LYRICIST" } },
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
{ TagType::Lyricists, { "LYRICISTS" } },
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
{ TagType::Media, { "TMED", "MEDIA", "WM/MEDIA" } },
{ TagType::MixDJ, { "DJMIXER" } },
{ TagType::Mixer, { "MIXER" } },
{ TagType::MixerSortOrder, { "MIXERSORT" } },
{ TagType::Mixers, { "MIXERS" } },
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
{ TagType::Mood, { "MOOD" } },
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
{ TagType::MusicBrainzConductorID, { "MUSICBRAINZ_CONDUCTORID", "MUSICBRAINZ CONDUCTOR ID", "MUSICBRAINZ/CONDUCTOR ID" } },
{ TagType::MusicBrainzDirectorID, { "MUSICBRAINZ_DIRECTORID", "MUSICBRAINZ DIRECTOR ID", "MUSICBRAINZ/DIRECTOR ID" } },
{ TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
{ TagType::MusicBrainzLyricistID, { "MUSICBRAINZ_LYRICISTID", "MUSICBRAINZ LYRICIST ID", "MUSICBRAINZ/LYRICIST ID" } },
{ TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
{ TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
{ TagType::MusicBrainzMixerID, { "MUSICBRAINZ_MIXERID", "MUSICBRAINZ MIXER ID", "MUSICBRAINZ/MIXER ID" } },
{ TagType::MusicBrainzProducerID, { "MUSICBRAINZ_PRODUCERID", "MUSICBRAINZ PRODUCER ID", "MUSICBRAINZ/PRODUCER ID" } },
{ TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
{ TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
{ TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
{ TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
{ TagType::MusicBrainzRemixerID, { "MUSICBRAINZ_REMIXERID", "MUSICBRAINZ REMIXER ID", "MUSICBRAINZ/REMIXER ID" } },
{ TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
{ TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
{ TagType::OriginalArtist, { "ORIGINALARTIST" } },
{ TagType::OriginalFilename, { "ORIGINALFILENAME" } },
{ TagType::OriginalReleaseDate, { "ORIGINALDATE", "TDOR", "WM/ORIGINALRELEASETIME" } },
{ TagType::OriginalReleaseYear, { "ORIGINALYEAR", "TORY", "WM/ORIGINALRELEASEYEAR" } },
{ TagType::Podcast, { "PODCAST" } },
{ TagType::PodcastURL, { "PODCASTURL" } },
{ TagType::Producer, { "PRODUCER" } },
{ TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
{ TagType::Producers, { "PRODUCERS" } },
{ TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
{ TagType::RecordLabel, { "LABEL", "PUBLISHER", "ORGANIZATION" } },
{ TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
{ TagType::ReleaseDate, { "RELEASEDATE" } },
{ TagType::ReleaseStatus, { "RELEASESTATUS" } },
{ TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
{ TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
{ TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
{ TagType::Remixers, { "REMIXERS" } },
{ TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
{ TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
{ TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
{ TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
{ TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
{ TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
{ TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
{ TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
{ TagType::Script, { "SCRIPT", "WM/SCRIPT" } },
{ TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
{ TagType::Subtitle, { "SUBTITLE" } },
{ TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS" } },
{ TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
{ TagType::TrackNumber, { "TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER" } },
{ TagType::TrackTitle, { "TITLE" } },
{ TagType::TrackTitleSortOrder, { "TITLESORT" } },
{ TagType::WorkTitle, { "WORK" } },
{ TagType::Writer, { "WRITER" } },
};
} // namespace
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
: _audioFile{ audioFile }
, _metaDataMap{ audioFile.getMetaData() }
{
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
for (const auto& [key, value] : _metaDataMap)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
}
}
TagReader::~TagReader() = default;
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ avFormatTagMapping.find(tag) };
if (itTagNames == std::cend(avFormatTagMapping))
return;
for (const std::string& tagName : itTagNames->second)
{
bool visited{};
visitTagValues(tagName, [&](std::string_view value) {
visited = true;
visitor(value);
});
if (visited)
break;
}
}
void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
{
auto itValues{ _metaDataMap.find(std::string{ key }) };
if (itValues == std::cend(_metaDataMap))
return;
visitor(itValues->second);
}
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value);
});
}
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{
// MPEG files: need to visit LYRICS-language entries
for (const auto& [tag, value] : _metaDataMap)
{
constexpr std::string_view lyricsPrefix{ "LYRICS-" };
if (tag.starts_with(lyricsPrefix))
{
const std::string language{ core::stringUtils::stringToLower(tag.substr(lyricsPrefix.size())) };
visitor(language, value);
}
}
// otherwise, just visit regular LYRICS tag with no language
visitTagValues("LYRICS", [&](std::string_view value) {
visitor("", value);
});
}
} // namespace lms::audio::ffmpeg
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/ITagReader.hpp"
#include "AudioFile.hpp"
namespace lms::audio::ffmpeg
{
class TagReader : public ITagReader
{
public:
TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs);
~TagReader() override;
TagReader(const TagReader&) = delete;
TagReader& operator=(const TagReader&) = delete;
private:
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
const AudioFile& _audioFile;
AudioFile::MetadataMap _metaDataMap;
};
} // namespace lms::audio::ffmpeg
+232
View File
@@ -0,0 +1,232 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Transcoder.hpp"
#include <atomic>
#include <iomanip>
#include "core/IChildProcessManager.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "audio/Exception.hpp"
#include "audio/TranscodeTypes.hpp"
namespace lms::audio
{
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters)
{
return std::make_unique<ffmpeg::Transcoder>(parameters);
}
} // namespace lms::audio
namespace lms::audio::ffmpeg
{
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
static std::atomic<size_t> globalId{};
static std::filesystem::path ffmpegPath;
void Transcoder::init()
{
ffmpegPath = core::Service<core::IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
if (!std::filesystem::exists(ffmpegPath))
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
}
Transcoder::Transcoder(const TranscodeParameters& parameters)
: _debugId{ globalId++ }
, _inputParams{ parameters.inputParameters }
, _outputParams{ parameters.outputParameters }
{
start();
}
Transcoder::~Transcoder() = default;
void Transcoder::start()
{
if (ffmpegPath.empty())
init();
try
{
if (!std::filesystem::exists(_inputParams.filePath))
throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" };
if (!std::filesystem::is_regular_file(_inputParams.filePath))
throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" };
}
catch (const std::filesystem::filesystem_error& e)
{
// TODO store/raise e.code()
throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
}
LOG(INFO, "Transcoding file " << _inputParams.filePath);
std::vector<std::string> args;
args.emplace_back(ffmpegPath.string());
// Make sure:
// - we do not produce anything in the stderr output
// - we do not rely on input
// in order not to block the whole forked process
args.emplace_back("-loglevel");
args.emplace_back("quiet");
args.emplace_back("-nostdin");
// input Offset
{
args.emplace_back("-ss");
std::ostringstream oss;
oss << std::fixed << std::showpoint << std::setprecision(3) << (_inputParams.offset.count() / float{ 1'000 });
args.emplace_back(oss.str());
}
// Input file
args.emplace_back("-i");
args.emplace_back(_inputParams.filePath.string());
if (_outputParams.stripMetadata)
{
// Strip metadata
args.emplace_back("-map_metadata");
args.emplace_back("-1");
}
// Skip video flows (including covers)
args.emplace_back("-vn");
// Output bitrates
if (_outputParams.bitrate)
{
args.emplace_back("-b:a");
args.emplace_back(std::to_string(*_outputParams.bitrate));
}
// Codecs and formats
if (_outputParams.format)
{
switch (*_outputParams.format)
{
case OutputFormat::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case OutputFormat::MATROSKA_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("matroska");
break;
case OutputFormat::OGG_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case OutputFormat::WEBM_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("webm");
break;
default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
}
}
args.emplace_back("pipe:1");
LOG(DEBUG, "Dumping args (" << args.size() << ")");
for (const std::string& arg : args)
LOG(DEBUG, "Arg = '" << arg << "'");
// Caution: stdin must have been closed before
try
{
_childProcess = core::Service<core::IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
}
catch (core::ChildProcessException& exception)
{
throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
}
}
void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
{
assert(_childProcess);
return _childProcess->asyncRead(buffer, bufferSize, [readCallback{ std::move(readCallback) }](core::IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) {
readCallback(nbBytesRead);
});
}
std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
{
assert(_childProcess);
return _childProcess->readSome(buffer, bufferSize);
}
std::string_view Transcoder::getOutputMimeType() const
{
// TODO: use input mime type
if (_outputParams.format)
{
switch (*_outputParams.format)
{
case OutputFormat::MP3:
return "audio/mpeg";
case OutputFormat::OGG_OPUS:
return "audio/opus";
case OutputFormat::MATROSKA_OPUS:
return "audio/x-matroska";
case OutputFormat::OGG_VORBIS:
return "audio/ogg";
case OutputFormat::WEBM_VORBIS:
return "audio/webm";
}
}
return "application/octet-stream"; // default, should not happen
}
bool Transcoder::finished() const
{
assert(_childProcess);
return _childProcess->finished();
}
} // namespace lms::audio::ffmpeg
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/ITranscoder.hpp"
namespace lms::core
{
class IChildProcess;
}
namespace lms::audio::ffmpeg
{
class Transcoder : public ITranscoder
{
public:
Transcoder(const TranscodeParameters& parameters);
~Transcoder() override;
Transcoder(const Transcoder&) = delete;
Transcoder& operator=(const Transcoder&) = delete;
private:
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback) override;
std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
std::string_view getOutputMimeType() const override;
const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; }
bool finished() const override;
static void init();
void start();
const std::size_t _debugId{};
const TranscodeInputParameters _inputParams;
const TranscodeOutputParameters _outputParams;
std::unique_ptr<core::IChildProcess> _childProcess;
};
} // namespace lms::audio::ffmpeg
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Utils.hpp"
namespace lms::audio::ffmpeg::utils
{
std::span<const std::filesystem::path> getSupportedExtensions()
{
// TODO: list demuxers to retrieve supported formats
static const std::array<std::filesystem::path, 18> fileExtensions{
".aac",
".alac",
".aif",
".aiff",
".ape",
".dsf",
".flac",
".m4a",
".m4b",
".mp3",
".mpc",
".oga",
".ogg",
".opus",
".shn",
".wav",
".wma",
".wv",
};
return fileExtensions;
}
} // namespace lms::audio::ffmpeg::utils
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <span>
namespace lms::audio::ffmpeg::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
} // namespace lms::audio::ffmpeg::utils
@@ -0,0 +1,227 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AudioFileInfo.hpp"
#include <cassert>
#include "TagLibDefs.hpp"
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/asffile.h>
#include <taglib/flacfile.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/trueaudiofile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsffile.h>
#endif
#if LMS_TAGLIB_HAS_SHORTEN
#include <taglib/shortenfile.h>
#endif
#include "audio/IAudioFileInfo.hpp"
#include "Utils.hpp"
namespace lms::audio::taglib
{
namespace
{
AudioProperties computeAudioProperties(const ::TagLib::File& file)
{
assert(file.audioProperties());
const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
AudioProperties audioProperties;
// Common properties
audioProperties.bitrate = static_cast<std::size_t>(properties.bitrate() * 1000);
audioProperties.channelCount = static_cast<std::size_t>(properties.channels());
audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
audioProperties.sampleRate = static_cast<std::size_t>(properties.sampleRate());
// Guess container from the file type
if (const auto* apeFile{ dynamic_cast<const ::TagLib::APE::File*>(&file) })
{
audioProperties.container = ContainerType::APE;
audioProperties.codec = CodecType::APE; // TODO version?
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
}
else if (const auto* asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&file) })
{
audioProperties.container = ContainerType::ASF;
switch (asfFile->audioProperties()->codec())
{
case ::TagLib::ASF::Properties::Codec::WMA1:
audioProperties.codec = CodecType::WMA1;
break;
case ::TagLib::ASF::Properties::Codec::WMA2:
audioProperties.codec = CodecType::WMA2;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
audioProperties.codec = CodecType::WMA9Lossless;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
audioProperties.codec = CodecType::WMA9Pro;
break;
case ::TagLib::ASF::Properties::Codec::Unknown:
audioProperties.codec = std::nullopt;
break;
}
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
}
#if LMS_TAGLIB_HAS_DSF
else if (const auto* dsfFile{ dynamic_cast<const ::TagLib::DSF::File*>(&file) })
{
audioProperties.container = ContainerType::DSF;
audioProperties.codec = CodecType::DSD;
audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_DSF
else if (const auto* flacFile{ dynamic_cast<const ::TagLib::FLAC::File*>(&file) })
{
audioProperties.container = ContainerType::FLAC;
audioProperties.codec = CodecType::FLAC;
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
}
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&file) })
{
audioProperties.container = ContainerType::MP4;
switch (mp4File->audioProperties()->codec())
{
case ::TagLib::MP4::Properties::Codec::AAC:
audioProperties.codec = CodecType::AAC;
break;
case ::TagLib::MP4::Properties::Codec::ALAC:
audioProperties.codec = CodecType::ALAC;
break;
case ::TagLib::MP4::Properties::Codec::Unknown:
audioProperties.codec = std::nullopt;
break;
}
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
}
else if (const auto* mpcFile{ dynamic_cast<const ::TagLib::MPC::File*>(&file) })
{
audioProperties.container = ContainerType::MPC;
switch (mpcFile->audioProperties()->mpcVersion())
{
case 7:
audioProperties.codec = CodecType::MPC7;
break;
case 8:
audioProperties.codec = CodecType::MPC8;
break;
}
}
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
{
const auto& properties{ *mpegFile->audioProperties() };
audioProperties.container = ContainerType::MPEG;
if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
&& mpegFile->audioProperties()->layer() == 3)
audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
audioProperties.codec = CodecType::AAC;
}
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Opus;
}
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Vorbis;
}
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
{
audioProperties.container = ContainerType::AIFF;
audioProperties.codec = CodecType::PCM;
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavFile{ dynamic_cast<const ::TagLib::RIFF::WAV::File*>(&file) })
{
audioProperties.container = ContainerType::WAV;
audioProperties.codec = CodecType::PCM;
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
}
#if LMS_TAGLIB_HAS_SHORTEN
else if (const auto* shortenFile{ dynamic_cast<const ::TagLib::Shorten::File*>(&file) })
{
audioProperties.container = ContainerType::Shorten;
audioProperties.codec = CodecType::Shorten;
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
}
#endif // LMS_TAGLIB_HAS_SHORTEN
else if (const auto* trueAudioFile{ dynamic_cast<const ::TagLib::TrueAudio::File*>(&file) })
{
audioProperties.container = ContainerType::TrueAudio;
audioProperties.codec = CodecType::TrueAudio;
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
}
else if (const auto* wavPackFile{ dynamic_cast<const ::TagLib::WavPack::File*>(&file) })
{
audioProperties.container = ContainerType::WavPack;
audioProperties.codec = CodecType::WavPack;
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
}
return audioProperties;
}
} // namespace
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
: _filePath{ filePath }
, _file{ utils::parseFile(filePath, readStyle) }
, _audioProperties{ computeAudioProperties(*_file) }
, _tagReader{ *_file, enableExtraDebugLogs }
, _imageReader{ *_file }
{
}
const AudioProperties& AudioFileInfo::getAudioProperties() const
{
return _audioProperties;
}
const IImageReader& AudioFileInfo::getImageReader() const
{
return _imageReader;
}
const ITagReader& AudioFileInfo::getTagReader() const
{
return _tagReader;
}
} // namespace lms::audio::taglib
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <taglib/tfile.h>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IImageReader.hpp"
#include "audio/ITagReader.hpp"
#include "ImageReader.hpp"
#include "TagReader.hpp"
namespace lms::audio::taglib
{
class AudioFileInfo final : public IAudioFileInfo
{
public:
AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
private:
const AudioProperties& getAudioProperties() const override;
const IImageReader& getImageReader() const override;
const ITagReader& getTagReader() const override;
const std::filesystem::path _filePath;
std::unique_ptr<::TagLib::File> _file;
const AudioProperties _audioProperties;
const TagReader _tagReader;
const ImageReader _imageReader;
};
} // namespace lms::audio::taglib
+418
View File
@@ -0,0 +1,418 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ImageReader.hpp"
#include "TagLibDefs.hpp"
#include <taglib/aifffile.h>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/attachedpictureframe.h>
#include <taglib/flacfile.h>
#include <taglib/flacpicture.h>
#include <taglib/id3v2tag.h>
#include <taglib/mp4coverart.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/tfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#include "core/String.hpp"
namespace lms::audio::taglib
{
namespace
{
Image::Type imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
{
switch (type)
{
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
return Image::Type::Other;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
return Image::Type::FileIcon;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
return Image::Type::FrontCover;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
return Image::Type::BackCover;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
return Image::Type::LeafletPage;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
return Image::Type::Media;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
return Image::Type::LeadArtist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
return Image::Type::Artist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
return Image::Type::Conductor;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
return Image::Type::Band;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
return Image::Type::Composer;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
return Image::Type::Lyricist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
return Image::Type::DuringRecording;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
return Image::Type::ColouredFish;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
return Image::Type::Illustration;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
return Image::Type::BandLogo;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
return Image::Type::Unknown;
}
Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
{
switch (type)
{
case ::TagLib::ASF::Picture::Type::Other:
return Image::Type::Other;
case ::TagLib::ASF::Picture::Type::FileIcon:
return Image::Type::FileIcon;
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case ::TagLib::ASF::Picture::Type::FrontCover:
return Image::Type::FrontCover;
case ::TagLib::ASF::Picture::Type::BackCover:
return Image::Type::BackCover;
case ::TagLib::ASF::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
case ::TagLib::ASF::Picture::Type::Media:
return Image::Type::Media;
case ::TagLib::ASF::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
case ::TagLib::ASF::Picture::Type::Artist:
return Image::Type::Artist;
case ::TagLib::ASF::Picture::Type::Conductor:
return Image::Type::Conductor;
case ::TagLib::ASF::Picture::Type::Band:
return Image::Type::Band;
case ::TagLib::ASF::Picture::Type::Composer:
return Image::Type::Composer;
case ::TagLib::ASF::Picture::Type::Lyricist:
return Image::Type::Lyricist;
case ::TagLib::ASF::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case ::TagLib::ASF::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
case ::TagLib::ASF::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case ::TagLib::ASF::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
case ::TagLib::ASF::Picture::Type::Illustration:
return Image::Type::Illustration;
case ::TagLib::ASF::Picture::Type::BandLogo:
return Image::Type::BandLogo;
case ::TagLib::ASF::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
return Image::Type::Unknown;
}
Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
{
switch (type)
{
case ::TagLib::FLAC::Picture::Type::Other:
return Image::Type::Other;
case ::TagLib::FLAC::Picture::Type::FileIcon:
return Image::Type::FileIcon;
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case ::TagLib::FLAC::Picture::Type::FrontCover:
return Image::Type::FrontCover;
case ::TagLib::FLAC::Picture::Type::BackCover:
return Image::Type::BackCover;
case ::TagLib::FLAC::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
case ::TagLib::FLAC::Picture::Type::Media:
return Image::Type::Media;
case ::TagLib::FLAC::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
case ::TagLib::FLAC::Picture::Type::Artist:
return Image::Type::Artist;
case ::TagLib::FLAC::Picture::Type::Conductor:
return Image::Type::Conductor;
case ::TagLib::FLAC::Picture::Type::Band:
return Image::Type::Band;
case ::TagLib::FLAC::Picture::Type::Composer:
return Image::Type::Composer;
case ::TagLib::FLAC::Picture::Type::Lyricist:
return Image::Type::Lyricist;
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case ::TagLib::FLAC::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case ::TagLib::FLAC::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
case ::TagLib::FLAC::Picture::Type::Illustration:
return Image::Type::Illustration;
case ::TagLib::FLAC::Picture::Type::BandLogo:
return Image::Type::BandLogo;
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
return Image::Type::Unknown;
}
const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format)
{
switch (format)
{
case ::TagLib::MP4::CoverArt::Format::BMP:
return "image/bmp";
case ::TagLib::MP4::CoverArt::Format::GIF:
return "image/gif";
case ::TagLib::MP4::CoverArt::Format::JPEG:
return "image/jpeg";
case ::TagLib::MP4::CoverArt::Format::PNG:
return "image/png";
case ::TagLib::MP4::CoverArt::Format::Unknown:
return "application/octet-stream";
}
return "application/octet-stream";
}
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
{
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
return Image::Type::FrontCover;
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
return Image::Type::BackCover;
return Image::Type::Unknown;
}
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor)
{
const auto& frameListMap{ id3v2Tags.frameListMap() };
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
{
const auto* attachedPictureFrame{ dynamic_cast<const ::TagLib::ID3v2::AttachedPictureFrame*>(frame) };
if (!attachedPictureFrame)
continue;
::TagLib::ByteVector picture{ attachedPictureFrame->picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.type = imageTypeFromfromID3v2(attachedPictureFrame->type());
image.description = attachedPictureFrame->description().to8Bit(true);
image.mimeType = attachedPictureFrame->mimeType().to8Bit(true);
image.data = pictureData;
visitor(image);
}
}
void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor)
{
for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
{
::TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
if (!asfPicture.isValid())
continue;
::TagLib::ByteVector picture{ asfPicture.picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.type = imageTypeFromfromASF(asfPicture.type());
image.description = asfPicture.description().to8Bit(true);
image.mimeType = asfPicture.mimeType().to8Bit(true);
image.data = pictureData;
visitor(image);
}
}
void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor)
{
const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
if (!coverItem.isValid())
return;
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList)
return;
#endif
::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
bool firstCover{ true };
for (TagLib::MP4::CoverArt& coverArt : coverArtList)
{
::TagLib::ByteVector picture{ coverArt.data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.mimeType = mp4ImageFormatToMimeType(coverArt.format());
image.data = pictureData;
// By convention, consider the first cover art as the front cover
image.type = firstCover ? Image::Type::FrontCover : Image::Type::Unknown;
firstCover = false;
visitor(image);
}
}
void visitFLACImages(const ::TagLib::List<TagLib::FLAC::Picture*>& pictureList, const ImageReader::ImageVisitor& visitor)
{
for (TagLib::FLAC::Picture* flacPicture : pictureList)
{
::TagLib::ByteVector picture{ flacPicture->data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.type = imageTypeFromfromFLAC(flacPicture->type());
image.description = flacPicture->description().to8Bit(true);
image.mimeType = flacPicture->mimeType().to8Bit(true);
image.data = pictureData;
visitor(image);
}
}
void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor)
{
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
const ::TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
for (const ::TagLib::VariantMap& pictureProperty : pictureProperties)
{
Image image;
::TagLib::ByteVector picture;
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
if (auto it{ pictureProperty.find("mimeType") }; it != pictureProperty.cend())
image.mimeType = it->second.toString().to8Bit(true);
if (auto it{ pictureProperty.find("description") }; it != pictureProperty.cend())
image.description = it->second.toString().to8Bit(true);
if (auto it{ pictureProperty.find("data") }; it != pictureProperty.cend())
{
picture = it->second.toByteVector();
image.data = { reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
}
if (!image.data.empty())
visitor(image);
}
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
}
} // namespace
ImageReader::ImageReader(::TagLib::File& file)
: _file{ file }
{
}
ImageReader::~ImageReader() = default;
void ImageReader::visitImages(const ImageVisitor& visitor) const
{
// MP3
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
{
if (mp3File->hasID3v2Tag())
visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
}
// MP4
else if (const TagLib::MP4::File * mp4File{ dynamic_cast<const TagLib::MP4::File*>(&_file) })
{
visitMP4Images(*mp4File, visitor);
}
// WMA
else if (const TagLib::ASF::File * asfFile{ dynamic_cast<const TagLib::ASF::File*>(&_file) })
{
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
visitASFImages(*tag, visitor);
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
{
if (flacFile->hasID3v2Tag()) // usage discouraged
visitID3V2Images(*flacFile->ID3v2Tag(), visitor);
else
visitFLACImages(flacFile->pictureList(), visitor);
}
// Ogg vorbis
else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<const TagLib::Ogg::Vorbis::File*>(&_file) })
{
visitFLACImages(vorbisFile->tag()->pictureList(), visitor);
}
// Ogg Opus
else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(&_file) })
{
visitFLACImages(opusFile->tag()->pictureList(), visitor);
}
// Aiff
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(&_file) })
{
if (aiffFile->hasID3v2Tag())
visitID3V2Images(*aiffFile->tag(), visitor);
}
// Wav
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(&_file) })
{
if (wavFile->hasID3v2Tag())
visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
{
if (mpcFile->hasAPETag())
visitAPEImages(*mpcFile->APETag(), visitor);
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
{
if (wavPackFile->hasAPETag())
visitAPEImages(*wavPackFile->APETag(), visitor);
}
}
} // namespace lms::audio::taglib
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "audio/IImageReader.hpp"
namespace TagLib
{
class File;
}
namespace lms::audio::taglib
{
class ImageReader : public IImageReader
{
public:
ImageReader(TagLib::File& _file);
~ImageReader() override;
ImageReader(const ImageReader&) = delete;
ImageReader& operator=(const ImageReader&) = delete;
private:
void visitImages(const ImageVisitor& visitor) const override;
TagLib::File& _file;
};
} // namespace lms::audio::taglib
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <taglib/taglib.h>
#if (TAGLIB_MAJOR_VERSION >= 2)
#define LMS_TAGLIB_HAS_DSF 1
#endif
// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
#define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1
#endif
// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2))
#define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
#endif
// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1))
#define LMS_TAGLIB_HAS_SHORTEN 1
#endif
+482
View File
@@ -0,0 +1,482 @@
/*
* Copyright (C) 2016 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagReader.hpp"
#include <unordered_map>
#include "TagLibDefs.hpp"
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/flacfile.h>
#include <taglib/id3v2tag.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/oggflacfile.h>
#include <taglib/opusfile.h>
#include <taglib/speexfile.h>
#include <taglib/synchronizedlyricsframe.h>
#include <taglib/tag.h>
#include <taglib/tfile.h>
#include <taglib/tpropertymap.h>
#include <taglib/trueaudiofile.h>
#include <taglib/unsynchronizedlyricsframe.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsdifffile.h>
#include <taglib/dsffile.h>
#endif
#include "core/ILogger.hpp"
#include "core/String.hpp"
namespace lms::audio::taglib
{
namespace
{
// Mapping to internal taglib names and/or common alternative custom names
const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
{ TagType::Advisory, { "ITUNESADVISORY" } },
{ TagType::Album, { "ALBUM" } },
{ TagType::AlbumArtist, { "ALBUMARTIST" } },
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT" } },
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
{ TagType::AlbumComment, { "ALBUMCOMMENT", "MUSICBRAINZ_ALBUMCOMMENT, MUSICBRAINZ ALBUM COMMENT", "ALBUMVERSION", "VERSION" } },
{ TagType::AlbumSortOrder, { "ALBUMSORT" } },
{ TagType::Arranger, { "ARRANGER" } },
{ TagType::Artist, { "ARTIST" } },
{ TagType::ArtistSortOrder, { "ARTISTSORT" } },
{ TagType::Artists, { "ARTISTS" } },
{ TagType::ArtistsSortOrder, { "ARTISTSSORT" } },
{ TagType::ASIN, { "ASIN" } },
{ TagType::Barcode, { "BARCODE" } },
{ TagType::BPM, { "BPM" } },
{ TagType::CatalogNumber, { "CATALOGNUMBER" } },
{ TagType::Comment, { "COMMENT" } },
{ TagType::Compilation, { "COMPILATION" } },
{ TagType::Composer, { "COMPOSER" } },
{ TagType::Composers, { "COMPOSERS" } },
{ TagType::ComposerSortOrder, { "COMPOSERSORT" } },
{ TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
{ TagType::Conductor, { "CONDUCTOR" } },
{ TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
{ TagType::Conductors, { "CONDUCTORS" } },
{ TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
{ TagType::Copyright, { "COPYRIGHT" } },
{ TagType::CopyrightURL, { "COPYRIGHTURL" } },
{ TagType::Date, { "DATE", "YEAR" } },
{ TagType::Director, { "DIRECTOR" } },
{ TagType::DiscNumber, { "DISCNUMBER", "DISC" } },
{ TagType::DiscSubtitle, { "DISCSUBTITLE", "SETSUBTITLE" } },
{ TagType::EncodedBy, { "ENCODEDBY" } },
{ TagType::Engineer, { "ENGINEER" } },
{ TagType::EncodingTime, { "ENCODINGTIME" } },
{ TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
{ TagType::Genre, { "GENRE" } },
{ TagType::Grouping, { "GROUPING", "ALBUMGROUPING" } },
{ TagType::InitialKey, { "INITIALKEY" } },
{ TagType::ISRC, { "ISRC" } },
{ TagType::Language, { "LANGUAGE" } },
{ TagType::License, { "LICENSE" } },
{ TagType::Lyricist, { "LYRICIST" } },
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
{ TagType::Lyricists, { "LYRICISTS" } },
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
{ TagType::Media, { "MEDIA" } },
{ TagType::MixDJ, { "DJMIXER" } },
{ TagType::Mixer, { "MIXER" } },
{ TagType::MixerSortOrder, { "MIXERSORT" } },
{ TagType::Mixers, { "MIXERS" } },
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
{ TagType::Mood, { "MOOD" } },
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
{ TagType::MusicBrainzConductorID, { "MUSICBRAINZ_CONDUCTORID", "MUSICBRAINZ CONDUCTOR ID", "MUSICBRAINZ/CONDUCTOR ID" } },
{ TagType::MusicBrainzDirectorID, { "MUSICBRAINZ_DIRECTORID", "MUSICBRAINZ DIRECTOR ID", "MUSICBRAINZ/DIRECTOR ID" } },
{ TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
{ TagType::MusicBrainzLyricistID, { "MUSICBRAINZ_LYRICISTID", "MUSICBRAINZ LYRICIST ID", "MUSICBRAINZ/LYRICIST ID" } },
{ TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
{ TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
{ TagType::MusicBrainzMixerID, { "MUSICBRAINZ_MIXERID", "MUSICBRAINZ MIXER ID", "MUSICBRAINZ/MIXER ID" } },
{ TagType::MusicBrainzProducerID, { "MUSICBRAINZ_PRODUCERID", "MUSICBRAINZ PRODUCER ID", "MUSICBRAINZ/PRODUCER ID" } },
{ TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
{ TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
{ TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
{ TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
{ TagType::MusicBrainzRemixerID, { "MUSICBRAINZ_REMIXERID", "MUSICBRAINZ REMIXER ID", "MUSICBRAINZ/REMIXER ID" } },
{ TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
{ TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
{ TagType::OriginalArtist, { "ORIGINALARTIST" } },
{ TagType::OriginalFilename, { "ORIGINALFILENAME" } },
{ TagType::OriginalReleaseDate, { "ORIGINALDATE" } },
{ TagType::OriginalReleaseYear, { "ORIGINALYEAR" } },
{ TagType::Podcast, { "PODCAST" } },
{ TagType::PodcastURL, { "PODCASTURL" } },
{ TagType::Producer, { "PRODUCER" } },
{ TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
{ TagType::Producers, { "PRODUCERS" } },
{ TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
{ TagType::RecordLabel, { "LABEL", "PUBLISHER", "ORGANIZATION" } },
{ TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
{ TagType::ReleaseDate, { "RELEASEDATE" } },
{ TagType::ReleaseStatus, { "RELEASESTATUS" } },
{ TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
{ TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
{ TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
{ TagType::Remixers, { "REMIXERS" } },
{ TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
{ TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
{ TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
{ TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
{ TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
{ TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
{ TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
{ TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
{ TagType::Script, { "SCRIPT" } },
{ TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
{ TagType::Subtitle, { "SUBTITLE" } },
{ TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS" } },
{ TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
{ TagType::TrackNumber, { "TRACKNUMBER" } },
{ TagType::TrackTitle, { "TITLE" } },
{ TagType::TrackTitleSortOrder, { "TITLESORT" } },
{ TagType::WorkTitle, { "WORK" } },
{ TagType::Writer, { "WRITER" } },
};
void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src)
{
for (auto&& [tag, values] : src)
{
if (dst.find(tag) == std::cend(dst))
dst[tag] = std::move(values);
}
}
void dedupTagValues(TagLib::PropertyMap& propertyMap)
{
for (auto& [key, values] : propertyMap)
{
if (values.size() <= 1)
continue;
::TagLib::StringList newList;
for (const ::TagLib::String& value : values)
{
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; }))
newList.append(value);
}
if (values != newList)
{
LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
values = newList;
}
}
}
} // namespace
TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs)
: _file{ file }
{
_propertyMap = _file.properties();
enableExtraDebugLogs &= core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG);
if (enableExtraDebugLogs)
{
for (const auto& [key, values] : _propertyMap)
{
for (const auto& value : values)
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value.to8Bit(true) << "'");
}
for (const auto& value : _propertyMap.unsupportedData())
LMS_LOG(METADATA, DEBUG, "Unknown value: '" << value.to8Bit(true) << "'");
}
// Some tags may not be known by TagLib
auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) {
if (!apeTag)
return;
mergeTagMaps(_propertyMap, apeTag->properties());
};
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
// Dedup values for some tags that may be written in both a standard tag and in a custom tag
dedupTagValues(_propertyMap);
const auto& frameListMap{ id3v2Tags.frameListMap() };
// Get some extra tags that may not be known by taglib
if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE"))
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
// consider each frame hold a different set of lyrics
// Synchronized lyrics frames
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
{
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
std::string lyrics;
for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
{
std::chrono::milliseconds timestamp{};
switch (lyricsFrame->timestampFormat())
{
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
timestamp = std::chrono::milliseconds{ synchedText.time };
break;
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
{
const ::TagLib::AudioProperties* properties{ file.audioProperties() };
if (properties && properties->sampleRate())
timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() };
}
break;
case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
break;
}
if (!lyrics.empty())
lyrics += '\n';
lyrics += core::stringUtils::formatTimestamp(timestamp);
lyrics += synchedText.text.to8Bit(true);
}
_id3v2Lyrics.emplace(language, std::move(lyrics));
}
// Unsynchronized lyrics frames
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
{
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
if (!lyricsFrame)
continue; // TODO log or assert?
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
_id3v2Lyrics.emplace(language, lyricsFrame->text().to8Bit(true));
}
};
// WMA
if (const ::TagLib::ASF::File * asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&_file) })
{
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
{
for (const auto& [name, attributeList] : tag->attributeListMap())
{
if (attributeList.isEmpty())
continue;
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
if (enableExtraDebugLogs)
{
for (const auto& attribute : attributeList)
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "<Non unicode>" }) << "'");
}
if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
continue;
::TagLib::StringList strAttributes;
for (const ::TagLib::ASF::Attribute& attribute : attributeList)
{
if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
strAttributes.append(attribute.toString());
}
if (!strAttributes.isEmpty())
_propertyMap[strName] = strAttributes;
}
// Merge artists that may have been saved only in Author (see #597)
if (auto itAuthor{ _propertyMap.find("AUTHOR") }; itAuthor != _propertyMap.end() && _propertyMap.unsupportedData().contains("Author"))
{
if (!_propertyMap.contains("ARTISTS"))
{
auto& artistEntries{ _propertyMap["ARTIST"] };
for (const auto& author : itAuthor->second)
{
if (!artistEntries.contains(author))
artistEntries.append(author);
}
}
}
}
}
// MP3
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
{
if (mp3File->hasID3v2Tag())
processID3v2Tags(*mp3File->ID3v2Tag(false));
getAPETags(mp3File->APETag());
}
// MP4
else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&_file) })
{
// Taglib does not expose rtng in properties
if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
{
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte)
#endif
_propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) };
}
if (!_propertyMap.contains("ORIGINALDATE"))
{
// For now:
// * TagLib 2.0 only parses ----:com.apple.iTunes:ORIGINALDATE
// / TagLib <2.0 only parses ----:com.apple.iTunes:originaldate
const auto& tags{ mp4File->tag()->itemMap() };
for (const auto& origDateString : { "----:com.apple.iTunes:originaldate", "----:com.apple.iTunes:ORIGINALDATE" })
{
auto itOrigDateTag{ tags.find(origDateString) };
if (itOrigDateTag != std::cend(tags))
{
const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
if (!dates.isEmpty())
{
_propertyMap["ORIGINALDATE"] = dates.front();
break;
}
}
}
}
}
// MPC
else if (::TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
{
getAPETags(wavPackFile->APETag());
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
{
if (flacFile->hasID3v2Tag()) // discouraged usage
processID3v2Tags(*flacFile->ID3v2Tag());
}
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<const TagLib::RIFF::AIFF::File*>(&_file) })
{
if (aiffFile->hasID3v2Tag())
processID3v2Tags(*aiffFile->tag());
}
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<const TagLib::RIFF::WAV::File*>(&_file) })
{
if (wavFile->hasID3v2Tag())
processID3v2Tags(*wavFile->ID3v2Tag());
}
}
TagReader::~TagReader() = default;
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{
auto itTagNames{ tagLibTagMapping.find(tag) };
if (itTagNames == std::cend(tagLibTagMapping))
return;
for (const std::string& tagName : itTagNames->second)
{
bool visited{};
visitTagValues(tagName, [&](std::string_view value) {
visited = true;
visitor(value);
});
if (visited)
break;
}
}
void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
{
::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 };
auto itValues{ _propertyMap.find(key) };
if (itValues == std::cend(_propertyMap))
return;
for (const ::TagLib::String& value : itValues->second)
visitor(value.to8Bit(true));
}
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
{
visitTagValues("PERFORMER", [&](std::string_view value) {
visitor("", value);
});
for (const auto& [key, values] : _propertyMap)
{
if (key.startsWith("PERFORMER:")) // startsWith is not case sensitive
{
std::string performerStr{ key.to8Bit(true) };
const std::size_t rolePos{ performerStr.find(':') };
assert(rolePos != std::string::npos);
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) };
for (const ::TagLib::String& value : values)
{
const std::string name{ value.to8Bit(true) };
visitor(role, name);
}
}
}
}
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
{
if (!_id3v2Lyrics.empty())
{
for (const auto& [language, lyrics] : _id3v2Lyrics)
visitor(language, lyrics);
}
else
{
// otherwise, just visit regular LYRICS tag with no language
visitTagValues("LYRICS", [&](std::string_view value) {
visitor("", value);
});
}
}
} // namespace lms::audio::taglib
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <map>
#include <string>
#include <taglib/tpropertymap.h>
#include "audio/ITagReader.hpp"
namespace TagLib
{
class File;
}
namespace lms::audio::taglib
{
class TagReader : public ITagReader
{
public:
TagReader(::TagLib::File& file, bool enableExtraDebugLogs);
~TagReader() override;
TagReader(const TagReader&) = delete;
TagReader& operator=(const TagReader&) = delete;
private:
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
::TagLib::File& _file;
::TagLib::PropertyMap _propertyMap; // case-insensitive keys
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
};
} // namespace lms::audio::taglib
+245
View File
@@ -0,0 +1,245 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Utils.hpp"
#include "TagLibDefs.hpp"
#include <taglib/aifffile.h>
#include <taglib/apefile.h>
#include <taglib/asffile.h>
#include <taglib/audioproperties.h>
#include <taglib/flacfile.h>
#include <taglib/id3v2framefactory.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/oggflacfile.h>
#include <taglib/opusfile.h>
#include <taglib/speexfile.h>
#include <taglib/tfile.h>
#include <taglib/tfilestream.h>
#include <taglib/trueaudiofile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#if LMS_TAGLIB_HAS_DSF
#include <taglib/dsffile.h>
#endif
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/String.hpp"
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::taglib::utils
{
std::span<const std::filesystem::path> getSupportedExtensions()
{
static const std::vector<std::filesystem::path> supportedExtensions{
".mp3", ".mp2", ".aac", ".ogg", ".oga", ".flac", ".spx", ".opus",
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
".wav",
#if LMS_TAGLIB_HAS_DSF
".dsf"
#endif
};
return std::span<const std::filesystem::path>{ supportedExtensions };
}
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
{
switch (readStyle)
{
case ParserOptions::AudioPropertiesReadStyle::Fast:
return TagLib::AudioProperties::ReadStyle::Fast;
case ParserOptions::AudioPropertiesReadStyle::Average:
return TagLib::AudioProperties::ReadStyle::Average;
case ParserOptions::AudioPropertiesReadStyle::Accurate:
return TagLib::AudioProperties::ReadStyle::Accurate;
}
throw Exception{ "Cannot convert read style" };
}
TagLib::FileStream createFileStream(const std::filesystem::path& p)
{
FILE* file{ std::fopen(p.c_str(), "r") };
if (!file)
{
const std::error_code ec{ errno, std::generic_category() };
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
throw IOException{ "fopen failed", ec };
}
int fd{ ::fileno(file) };
if (fd == -1)
{
const std::error_code ec{ errno, std::generic_category() };
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
throw IOException{ "fileno failed", ec };
}
return TagLib::FileStream{ fd, true };
}
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
{
constexpr bool readAudioProperties{ true };
std::unique_ptr<TagLib::File> file;
if (extension.empty())
return file;
const std::string ext{ core::stringUtils::stringToUpper(extension.string().substr(1)) };
// MP3
if (ext == "MP3" || ext == "MP2" || ext == "AAC")
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
// VORBIS
else if (ext == "OGG")
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "OGA")
{
/* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
if (!file->isValid())
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
}
else if (ext == "FLAC")
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
else if (ext == "SPX")
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "OPUS")
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
// APE
else if (ext == "MPC")
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "WV")
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "APE")
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
// TRUEAUDIO
else if (ext == "TTA")
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
// MP4
else if (ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V")
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
// ASF
else if (ext == "WMA" || ext == "ASF")
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
// RIFF
else if (ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC")
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (ext == "WAV")
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if LMS_TAGLIB_HAS_DSF
else if (ext == "DSF")
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
#endif
if (file && !file->isValid())
file.reset();
return file;
}
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
{
constexpr bool readAudioProperties{ true };
std::unique_ptr<TagLib::File> file;
if (TagLib::MPEG::File::isSupported(stream))
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
// VORBIS
else if (TagLib::Ogg::Vorbis::File::isSupported(stream))
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::Ogg::FLAC::File::isSupported(stream))
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::FLAC::File::isSupported(stream))
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
else if (TagLib::Ogg::Speex::File::isSupported(stream))
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::Ogg::Opus::File::isSupported(stream))
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
// APE
else if (TagLib::MPC::File::isSupported(stream))
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::WavPack::File::isSupported(stream))
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::APE::File::isSupported(stream))
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
// TRUEAUDIO
else if (TagLib::TrueAudio::File::isSupported(stream))
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
// MP4
else if (TagLib::MP4::File::isSupported(stream))
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
//_ASF
else if (TagLib::ASF::File::isSupported(stream))
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
// RIFF
else if (TagLib::RIFF::AIFF::File::isSupported(stream))
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
else if (TagLib::RIFF::WAV::File::isSupported(stream))
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
#if LMS_TAGLIB_HAS_DSF
else if (TagLib::DSF::File::isSupported(stream))
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
#endif
if (file && !file->isValid())
file.reset();
return file;
}
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) };
TagLib::FileStream fileStream{ createFileStream(p) };
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) };
if (!file)
{
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension");
file = parseFileByContent(&fileStream, tagLibReadStyle);
if (!file)
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content");
}
if (!file)
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
throw AudioFileParsingException{ p, "Parsing failed" };
}
if (!file->audioProperties())
{
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
throw AudioFileNoAudioPropertiesException{ p };
}
return file;
}
} // namespace lms::audio::taglib::utils
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <memory>
#include <span>
#include <taglib/tfile.h>
#include "audio/IAudioFileInfo.hpp"
namespace lms::audio::taglib::utils
{
std::span<const std::filesystem::path> getSupportedExtensions();
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
} // namespace lms::audio::taglib::utils
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <optional>
#include "core/LiteralString.hpp"
namespace lms::audio
{
enum class ContainerType
{
AIFF,
APE, // Monkey's Audio
ASF, // Advanced Systems Format
DSF,
FLAC,
MP4,
MPC, // Musepack
MPEG,
Ogg,
Shorten,
TrueAudio,
WAV,
WavPack,
};
core::LiteralString containerTypeToString(ContainerType type);
enum class CodecType
{
AAC,
AC3,
ALAC, // Apple Lossless Audio Codec (ALAC)
APE, // Monkey's Audio
EAC3, // Enhanced AC-3
DSD, // DSD
FLAC, // Flac
MP3,
MP4ALS, // MPEG-4 Audio Lossless Coding
MPC7, // Musepack
MPC8, // Musepack
Opus, // Opus
PCM,
Shorten, // Shorten (shn)
TrueAudio,
Vorbis,
WavPack, // WavPack
WMA1,
WMA2,
WMA9Pro,
WMA9Lossless,
};
core::LiteralString codecTypeToString(CodecType type);
struct AudioProperties
{
std::optional<ContainerType> container;
std::optional<CodecType> codec;
std::chrono::milliseconds duration{};
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;
std::optional<unsigned> sampleRate;
};
} // namespace lms::audio
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "core/Exception.hpp"
namespace lms::audio
{
class Exception : public core::LmsException
{
public:
using LmsException::LmsException;
};
} // namespace lms::audio
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <span>
#include "audio/Exception.hpp"
namespace lms::audio
{
class AudioProperties;
class IImageReader;
class ITagReader;
class IAudioFileInfo
{
public:
virtual ~IAudioFileInfo() = default;
virtual const AudioProperties& getAudioProperties() const = 0;
virtual const IImageReader& getImageReader() const = 0;
virtual const ITagReader& getTagReader() const = 0;
};
class AudioFileParsingException : public Exception
{
public:
AudioFileParsingException(const std::filesystem::path& path, std::string_view error = "")
: Exception{ error }, _path{ path } {}
const std::filesystem::path& getPath() const { return _path; }
private:
std::filesystem::path _path;
};
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
{
public:
using AudioFileParsingException::AudioFileParsingException;
};
class IOException : public Exception
{
public:
IOException(std::string_view message, std::error_code err)
: Exception{ std::string{ message } + ": " + err.message() }
, _err{ err }
{
}
std::error_code getErrorCode() const { return _err; }
private:
std::error_code _err;
};
struct ParserOptions
{
enum class Parser
{
TagLib,
FFmpeg,
};
enum class AudioPropertiesReadStyle
{
Fast,
Average,
Accurate,
};
Parser parser{ Parser::TagLib };
AudioPropertiesReadStyle readStyle{ AudioPropertiesReadStyle::Average };
bool enableExtraDebugLogs{};
};
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions = ParserOptions{});
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::ParserOptions::Parser parser);
} // namespace lms::audio
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <span>
#include <string>
#include "core/LiteralString.hpp"
namespace lms::audio
{
struct Image
{
// See TagLib types (based on ID3v2 APIC types)
enum class Type
{
// No information
Unknown,
// A type not enumerated below
Other,
// 32x32 PNG image that should be used as the file icon
FileIcon,
// File icon of a different size or format
OtherFileIcon,
// Front cover image of the album
FrontCover,
// Back cover image of the album
BackCover,
// Inside leaflet page of the album
LeafletPage,
// Image from the album itself
Media,
// Picture of the lead artist or soloist
LeadArtist,
// Picture of the artist or performer
Artist,
// Picture of the conductor
Conductor,
// Picture of the band or orchestra
Band,
// Picture of the composer
Composer,
// Picture of the lyricist or text writer
Lyricist,
// Picture of the recording location or studio
RecordingLocation,
// Picture of the artists during recording
DuringRecording,
// Picture of the artists during performance
DuringPerformance,
// Picture from a movie or video related to the track
MovieScreenCapture,
// Picture of a large, coloured fish
ColouredFish,
// Illustration related to the track
Illustration,
// Logo of the band or performer
BandLogo,
// Logo of the publisher (record company)
PublisherLogo
};
Type type{ Type::Unknown };
std::string mimeType{ "application/octet-stream" };
std::string description;
std::span<const std::byte> data;
};
core::LiteralString imageTypeToString(Image::Type type);
class IImageReader
{
public:
virtual ~IImageReader() = default;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(const ImageVisitor& visitor) const = 0;
};
} // namespace lms::audio
+177
View File
@@ -0,0 +1,177 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <string_view>
#include "core/LiteralString.hpp"
namespace lms::audio
{
// prefer using picard internal names
// see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
enum class TagType
{
AcoustID,
AcoustIDFingerprint,
Advisory, // non standard
Album,
AlbumArtist,
AlbumArtists, // non standard
AlbumArtistSortOrder,
AlbumArtistsSortOrder, // non standard
AlbumComment, // non standard
AlbumSortOrder,
Arranger,
Artist,
ArtistSortOrder,
Artists,
ArtistsSortOrder, // non standard
ASIN,
Barcode,
BPM,
CatalogNumber,
Comment,
Compilation,
Composer,
ComposerSortOrder,
Composers, // non standard
ComposersSortOrder, // non standard
Conductor,
ConductorSortOrder, // non standard
Conductors, // non standard
ConductorsSortOrder, // non standard
Copyright,
CopyrightURL, // non standard
Date,
Director,
DiscNumber,
DiscSubtitle,
EncodedBy,
EncoderSettings,
EncodingTime,
Engineer,
GaplessPlayback,
Genre,
Grouping,
InitialKey,
ISRC,
Language,
License,
Lyricist,
LyricistSortOrder, // non standard
Lyricists, // non standard
LyricistsSortOrder, // non standard
// Lyrics, Handled separately
Media,
MixDJ,
Mixer,
MixerSortOrder, // non standard
Mixers, // non standard
MixersSortOrder, // non standard
Mood,
Movement,
MovementCount,
MovementNumber,
MusicBrainzArrangerID, // non standard
MusicBrainzArtistID,
MusicBrainzComposerID, // non standard
MusicBrainzConductorID, // non standard
MusicBrainzDirectorID, // non standard
MusicBrainzDiscID,
MusicBrainzLyricistID, // non standard
MusicBrainzOriginalArtistID,
MusicBrainzOriginalReleaseID,
MusicBrainzMixerID, // non standard
MusicBrainzProducerID, // non standard
MusicBrainzRecordingID,
MusicBrainzReleaseArtistID,
MusicBrainzReleaseGroupID,
MusicBrainzReleaseID,
MusicBrainzRemixerID, // non standard
MusicBrainzTrackID,
MusicBrainzWorkID,
MusicIPFingerprint,
MusicIPPUID,
OriginalAlbum,
OriginalArtist,
OriginalFilename,
OriginalReleaseDate,
OriginalReleaseYear,
// Performers, Handled separately
Podcast,
PodcastURL,
Producer,
ProducerSortOrder, // non standard
Producers, // non standard
ProducersSortOrder, // non standard
Rating,
RecordLabel,
ReleaseCountry,
ReleaseDate,
ReleaseStatus,
ReleaseType,
Remixer,
RemixerSortOrder,
Remixers,
RemixersSortOrder,
ReplayGainAlbumGain,
ReplayGainAlbumPeak,
ReplayGainAlbumRange,
ReplayGainReferenceLoudness,
ReplayGainTrackGain,
ReplayGainTrackPeak,
ReplayGainTrackRange,
Script,
ShowName,
ShowNameSortOrder,
ShowWorkAndMovement,
Subtitle,
TotalDiscs,
TotalTracks,
TrackNumber,
TrackTitle,
TrackTitleSortOrder,
Website,
WorkTitle,
Writer,
Count, // Special value used to count the number of tags
};
core::LiteralString tagTypeToString(TagType type);
class ITagReader
{
public:
virtual ~ITagReader() = default;
using TagValueVisitor = std::function<void(std::string_view value)>;
virtual void visitTagValues(TagType tag, TagValueVisitor visitor) const = 0;
virtual void visitTagValues(std::string_view tag, TagValueVisitor visitor) const = 0;
using PerformerVisitor = std::function<void(std::string_view role, std::string_view artist)>;
virtual void visitPerformerTags(PerformerVisitor visitor) const = 0;
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
};
} // namespace lms::audio
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstddef>
#include <functional>
#include <memory>
#include <string_view>
#include "TranscodeTypes.hpp"
namespace lms::audio
{
class ITranscoder
{
public:
virtual ~ITranscoder() = default;
// non blocking calls
using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
virtual void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback callback) = 0;
virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0;
virtual std::string_view getOutputMimeType() const = 0;
virtual const TranscodeOutputParameters& getOutputParameters() const = 0;
virtual bool finished() const = 0;
};
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters);
} // namespace lms::audio
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS 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.
*
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <optional>
// #include "AudioTypes.hpp"
namespace lms::audio
{
// TODO deprecated
enum class OutputFormat
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
struct TranscodeInputParameters
{
std::filesystem::path filePath;
std::chrono::milliseconds duration{}; // Duration of the audio file
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
};
struct TranscodeOutputParameters
{
std::optional<OutputFormat> format;
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;
std::optional<unsigned> sampleRate;
bool stripMetadata{ true };
};
struct TranscodeParameters
{
TranscodeInputParameters inputParameters;
TranscodeOutputParameters outputParameters;
};
} // namespace lms::audio