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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user