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
+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