Factorized some container/codec handling code + removed the no longer needed transcode output formats mka and webm

This commit is contained in:
emeric
2025-11-30 18:48:04 +01:00
parent c3cd30f248
commit d834f34683
65 changed files with 1424 additions and 1003 deletions
-2
View File
@@ -14,8 +14,6 @@ add_library(lmsaudio STATIC
impl/taglib/ImageReader.cpp
impl/taglib/TagReader.cpp
impl/taglib/Utils.cpp
impl/AudioTypes.cpp
impl/ImageReader.cpp
impl/AudioFileInfoParser.cpp
impl/TagReader.cpp
)
-109
View File
@@ -1,109 +0,0 @@
/*
* 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::DSD:
return "DSD";
case CodecType::EAC3:
return "E-AC3";
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
@@ -1,77 +0,0 @@
/*
* 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
+34 -35
View File
@@ -35,7 +35,6 @@ extern "C"
#include "core/ITraceLogger.hpp"
#include "core/String.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/Exception.hpp"
namespace lms::audio::ffmpeg
@@ -71,83 +70,83 @@ namespace lms::audio::ffmpeg
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
}
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
std::optional<core::media::ContainerType> avdemuxerToContainerType(std::string_view name)
{
if (name == "aiff")
return ContainerType::AIFF;
return core::media::ContainerType::AIFF;
if (name == "ape")
return ContainerType::APE;
return core::media::ContainerType::APE;
if (name.starts_with("asf"))
return ContainerType::ASF;
return core::media::ContainerType::ASF;
if (name == "dsf")
return ContainerType::DSF;
return core::media::ContainerType::DSF;
if (name == "flac")
return ContainerType::FLAC;
return core::media::ContainerType::FLAC;
if (name.find("mp4") != std::string_view::npos)
return ContainerType::MP4;
return core::media::ContainerType::MP4;
if (name.starts_with("mpc"))
return ContainerType::MPC;
return core::media::ContainerType::MPC;
if (name == "mp3")
return ContainerType::MPEG;
return core::media::ContainerType::MPEG;
if (name == "ogg")
return ContainerType::Ogg;
return core::media::ContainerType::Ogg;
if (name == "shn")
return ContainerType::Shorten;
return core::media::ContainerType::Shorten;
if (name == "tta")
return ContainerType::TrueAudio;
return core::media::ContainerType::TrueAudio;
if (name == "wav")
return ContainerType::WAV;
return core::media::ContainerType::WAV;
if (name == "wv")
return ContainerType::WavPack;
return core::media::ContainerType::WavPack;
return std::nullopt;
}
std::optional<CodecType> avcodecToCodecType(AVCodecID codec)
std::optional<core::media::CodecType> avcodecToCodecType(AVCodecID codec)
{
switch (codec)
{
case AV_CODEC_ID_AAC:
return CodecType::AAC;
return core::media::CodecType::AAC;
case AV_CODEC_ID_AC3:
return CodecType::AC3;
return core::media::CodecType::AC3;
case AV_CODEC_ID_ALAC:
return CodecType::ALAC;
return core::media::CodecType::ALAC;
case AV_CODEC_ID_APE:
return CodecType::APE;
return core::media::CodecType::APE;
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;
return core::media::CodecType::DSD;
case AV_CODEC_ID_EAC3:
return CodecType::EAC3;
return core::media::CodecType::EAC3;
case AV_CODEC_ID_FLAC:
return CodecType::FLAC;
return core::media::CodecType::FLAC;
case AV_CODEC_ID_MP3:
return CodecType::MP3;
return core::media::CodecType::MP3;
case AV_CODEC_ID_MP4ALS:
return CodecType::MP4ALS;
return core::media::CodecType::MP4ALS;
case AV_CODEC_ID_MUSEPACK7:
return CodecType::MPC7;
return core::media::CodecType::MPC7;
case AV_CODEC_ID_MUSEPACK8:
return CodecType::MPC8;
return core::media::CodecType::MPC8;
case AV_CODEC_ID_OPUS:
return CodecType::Opus;
return core::media::CodecType::Opus;
case AV_CODEC_ID_SHORTEN:
return CodecType::Shorten;
return core::media::CodecType::Shorten;
case AV_CODEC_ID_VORBIS:
return CodecType::Vorbis;
return core::media::CodecType::Vorbis;
case AV_CODEC_ID_WAVPACK:
return CodecType::WavPack;
return core::media::CodecType::WavPack;
case AV_CODEC_ID_WMALOSSLESS:
return CodecType::WMA9Lossless;
return core::media::CodecType::WMA9Lossless;
case AV_CODEC_ID_WMAPRO:
return CodecType::WMA9Pro;
return core::media::CodecType::WMA9Pro;
case AV_CODEC_ID_WMAV1:
return CodecType::WMA1;
return core::media::CodecType::WMA1;
case AV_CODEC_ID_WMAV2:
return CodecType::WMA2;
return core::media::CodecType::WMA2;
default:
return std::nullopt;
+4 -3
View File
@@ -27,7 +27,8 @@
#include <string>
#include <vector>
#include "audio/AudioTypes.hpp"
#include "core/media/CodecType.hpp"
#include "core/media/ContainerType.hpp"
extern "C"
{
@@ -44,7 +45,7 @@ namespace lms::audio::ffmpeg
struct ContainerInfo
{
std::optional<ContainerType> container;
std::optional<core::media::ContainerType> container;
std::string containerName;
std::optional<std::size_t> bitrate;
@@ -54,7 +55,7 @@ namespace lms::audio::ffmpeg
struct StreamInfo
{
size_t index{};
std::optional<CodecType> codec;
std::optional<core::media::CodecType> codec;
std::string codecName;
std::optional<size_t> bitrate;
+1 -1
View File
@@ -24,7 +24,7 @@
#include "core/ILogger.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "AudioFile.hpp"
+1 -1
View File
@@ -22,7 +22,7 @@
#include <filesystem>
#include <optional>
#include "audio/AudioTypes.hpp"
#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IAudioFileInfoParser.hpp"
+2 -2
View File
@@ -45,9 +45,9 @@ namespace lms::audio::ffmpeg
image.data = picture.data;
image.mimeType = picture.mimeType;
if (metaDataHasKeyword(metaData, "front"))
image.type = Image::Type::FrontCover;
image.type = core::media::ImageType::FrontCover;
else if (metaDataHasKeyword(metaData, "back"))
image.type = Image::Type::BackCover;
image.type = core::media::ImageType::BackCover;
visitor(image);
});
+65 -63
View File
@@ -19,13 +19,14 @@
#include "Transcoder.hpp"
#include <atomic>
#include <filesystem>
#include <iomanip>
#include "core/IChildProcessManager.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "core/media/MimeType.hpp"
#include "audio/Exception.hpp"
#include "audio/TranscodeTypes.hpp"
@@ -42,18 +43,29 @@ 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()
namespace
{
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!" };
}
class FFmpegPath
{
public:
FFmpegPath()
{
path = core::Service<core::IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
if (!std::filesystem::exists(path))
throw Exception{ "File '" + path.string() + "' does not exist!" };
}
const std::filesystem::path& get() const { return path; }
private:
std::filesystem::path path;
};
} // namespace
std::atomic<std::size_t> Transcoder::_nextDebugId;
Transcoder::Transcoder(const TranscodeParameters& parameters)
: _debugId{ globalId++ }
: _debugId{ _nextDebugId++ }
, _inputParams{ parameters.inputParameters }
, _outputParams{ parameters.outputParameters }
{
@@ -64,8 +76,7 @@ namespace lms::audio::ffmpeg
void Transcoder::start()
{
if (ffmpegPath.empty())
init();
static FFmpegPath ffmpegPath;
try
{
@@ -76,15 +87,16 @@ namespace lms::audio::ffmpeg
}
catch (const std::filesystem::filesystem_error& e)
{
// TODO store/raise e.code()
throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
throw IOFileException{ _inputParams.filePath, "Failed to test file existence", e.code() };
}
LOG(INFO, "Transcoding file " << _inputParams.filePath);
std::vector<std::string> args;
args.emplace_back(ffmpegPath.string());
args.emplace_back(ffmpegPath.get().string());
// TODO some codecs have restrictions, take them into account (channel count, sample rate, etc.)
// Make sure:
// - we do not produce anything in the stderr output
@@ -114,7 +126,7 @@ namespace lms::audio::ffmpeg
args.emplace_back("-1");
}
// Skip video flows (including covers)
// Skip video flows (including covers!)
args.emplace_back("-vn");
// Output bitrates
@@ -124,63 +136,68 @@ namespace lms::audio::ffmpeg
args.emplace_back(std::to_string(*_outputParams.bitrate));
}
// Codecs and formats
if (_outputParams.format)
{
switch (*_outputParams.format)
args.emplace_back("-f");
switch (_outputParams.format->container)
{
case OutputFormat::MP3:
args.emplace_back("-f");
case core::media::ContainerType::FLAC:
args.emplace_back("flac");
break;
case core::media::ContainerType::Ogg:
args.emplace_back("ogg");
break;
case core::media::ContainerType::MPEG:
args.emplace_back("mp3");
break;
case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec");
default:
throw Exception{ "Unsupported container type " + std::string{ core::media::containerTypeToString(_outputParams.format->container).str() } };
}
args.emplace_back("-acodec");
switch (_outputParams.format->codec)
{
case core::media::CodecType::MP3:
args.emplace_back("libmp3lame");
break;
case core::media::CodecType::Opus:
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");
case core::media::CodecType::Vorbis:
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");
case core::media::CodecType::FLAC:
args.emplace_back("flac");
break;
default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
throw Exception{ "Unhandled codec type " + std::string{ codecTypeToString(_outputParams.format->codec).str() } };
}
}
args.emplace_back("pipe:1");
LOG(DEBUG, "Dumping args (" << args.size() << ")");
for (const std::string& arg : args)
LOG(DEBUG, "Arg = '" << arg << "'");
if (core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
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);
_childProcess = core::Service<core::IChildProcessManager>::get()->spawnChildProcess(ffmpegPath.get(), args);
}
catch (core::ChildProcessException& exception)
{
throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
throw Exception{ "Cannot execute '" + ffmpegPath.get().string() + "': " + exception.what() };
}
}
@@ -202,25 +219,10 @@ namespace lms::audio::ffmpeg
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 core::media::getMimeType(_outputParams.format->container, _outputParams.format->codec).str();
return "application/octet-stream"; // default, should not happen
return core::media::getMimeType(_inputParams.audioProperties.container, _inputParams.audioProperties.codec).str();
}
bool Transcoder::finished() const
+4 -1
View File
@@ -19,6 +19,8 @@
#pragma once
#include <atomic>
#include "audio/ITranscoder.hpp"
namespace lms::core
@@ -47,7 +49,8 @@ namespace lms::audio::ffmpeg
static void init();
void start();
const std::size_t _debugId{};
static std::atomic<std::size_t> _nextDebugId;
const std::size_t _debugId;
const TranscodeInputParameters _inputParams;
const TranscodeOutputParameters _outputParams;
std::unique_ptr<core::IChildProcess> _childProcess;
+35 -35
View File
@@ -45,7 +45,7 @@
#include "core/ILogger.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IAudioFileInfoParser.hpp"
@@ -104,27 +104,27 @@ namespace lms::audio::taglib
// 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.container = core::media::ContainerType::APE;
audioProperties.codec = core::media::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;
audioProperties.container = core::media::ContainerType::ASF;
switch (asfFile->audioProperties()->codec())
{
case ::TagLib::ASF::Properties::Codec::WMA1:
audioProperties.codec = CodecType::WMA1;
audioProperties.codec = core::media::CodecType::WMA1;
break;
case ::TagLib::ASF::Properties::Codec::WMA2:
audioProperties.codec = CodecType::WMA2;
audioProperties.codec = core::media::CodecType::WMA2;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
audioProperties.codec = CodecType::WMA9Lossless;
audioProperties.codec = core::media::CodecType::WMA9Lossless;
break;
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
audioProperties.codec = CodecType::WMA9Pro;
audioProperties.codec = core::media::CodecType::WMA9Pro;
break;
case ::TagLib::ASF::Properties::Codec::Unknown:
LMS_LOG(AUDIO, DEBUG, "Unhandled ASF codec in " << filePath);
@@ -136,27 +136,27 @@ namespace lms::audio::taglib
#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.container = core::media::ContainerType::DSF;
audioProperties.codec = core::media::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.container = core::media::ContainerType::FLAC;
audioProperties.codec = core::media::CodecType::FLAC;
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
}
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&file) })
{
audioProperties.container = ContainerType::MP4;
audioProperties.container = core::media::ContainerType::MP4;
switch (mp4File->audioProperties()->codec())
{
case ::TagLib::MP4::Properties::Codec::AAC:
audioProperties.codec = CodecType::AAC;
audioProperties.codec = core::media::CodecType::AAC;
break;
case ::TagLib::MP4::Properties::Codec::ALAC:
audioProperties.codec = CodecType::ALAC;
audioProperties.codec = core::media::CodecType::ALAC;
break;
case ::TagLib::MP4::Properties::Codec::Unknown:
LMS_LOG(AUDIO, DEBUG, "Unhandled MP4 codec in " << filePath);
@@ -167,15 +167,15 @@ namespace lms::audio::taglib
}
else if (const auto* mpcFile{ dynamic_cast<const ::TagLib::MPC::File*>(&file) })
{
audioProperties.container = ContainerType::MPC;
audioProperties.container = core::media::ContainerType::MPC;
switch (mpcFile->audioProperties()->mpcVersion())
{
case 7:
audioProperties.codec = CodecType::MPC7;
audioProperties.codec = core::media::CodecType::MPC7;
break;
case 8:
audioProperties.codec = CodecType::MPC8;
audioProperties.codec = core::media::CodecType::MPC8;
break;
default:
LMS_LOG(AUDIO, DEBUG, "Unhandled MPC codec version " << mpcFile->audioProperties()->mpcVersion() << " in " << filePath);
@@ -186,13 +186,13 @@ namespace lms::audio::taglib
{
const auto& properties{ *mpegFile->audioProperties() };
audioProperties.container = ContainerType::MPEG;
audioProperties.container = core::media::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
audioProperties.codec = core::media::CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
#if LMS_TAGLIB_HAS_ADTS
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
audioProperties.codec = CodecType::AAC;
audioProperties.codec = core::media::CodecType::AAC;
#endif
else
{
@@ -202,44 +202,44 @@ namespace lms::audio::taglib
}
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Opus;
audioProperties.container = core::media::ContainerType::Ogg;
audioProperties.codec = core::media::CodecType::Opus;
}
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
{
audioProperties.container = ContainerType::Ogg;
audioProperties.codec = CodecType::Vorbis;
audioProperties.container = core::media::ContainerType::Ogg;
audioProperties.codec = core::media::CodecType::Vorbis;
}
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
{
audioProperties.container = ContainerType::AIFF;
audioProperties.codec = CodecType::PCM;
audioProperties.container = core::media::ContainerType::AIFF;
audioProperties.codec = core::media::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.container = core::media::ContainerType::WAV;
audioProperties.codec = core::media::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.container = core::media::ContainerType::Shorten;
audioProperties.codec = core::media::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.container = core::media::ContainerType::TrueAudio;
audioProperties.codec = core::media::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.container = core::media::ContainerType::WavPack;
audioProperties.codec = core::media::CodecType::WavPack;
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
}
else
+1 -1
View File
@@ -22,7 +22,7 @@
#include <filesystem>
#include <optional>
#include "audio/AudioTypes.hpp"
#include "audio/AudioProperties.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IAudioFileInfoParser.hpp"
#include "audio/IImageReader.hpp"
+74 -74
View File
@@ -44,157 +44,157 @@ namespace lms::audio::taglib
{
namespace
{
Image::Type imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
core::media::ImageType imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
{
switch (type)
{
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
return Image::Type::Other;
return core::media::ImageType::Other;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
return Image::Type::FileIcon;
return core::media::ImageType::FileIcon;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
return core::media::ImageType::OtherFileIcon;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
return Image::Type::FrontCover;
return core::media::ImageType::FrontCover;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
return Image::Type::BackCover;
return core::media::ImageType::BackCover;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
return Image::Type::LeafletPage;
return core::media::ImageType::LeafletPage;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
return Image::Type::Media;
return core::media::ImageType::Media;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
return Image::Type::LeadArtist;
return core::media::ImageType::LeadArtist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
return Image::Type::Artist;
return core::media::ImageType::Artist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
return Image::Type::Conductor;
return core::media::ImageType::Conductor;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
return Image::Type::Band;
return core::media::ImageType::Band;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
return Image::Type::Composer;
return core::media::ImageType::Composer;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
return Image::Type::Lyricist;
return core::media::ImageType::Lyricist;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
return Image::Type::RecordingLocation;
return core::media::ImageType::RecordingLocation;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
return Image::Type::DuringRecording;
return core::media::ImageType::DuringRecording;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
return Image::Type::DuringPerformance;
return core::media::ImageType::DuringPerformance;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
return core::media::ImageType::MovieScreenCapture;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
return Image::Type::ColouredFish;
return core::media::ImageType::ColouredFish;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
return Image::Type::Illustration;
return core::media::ImageType::Illustration;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
return Image::Type::BandLogo;
return core::media::ImageType::BandLogo;
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
return Image::Type::PublisherLogo;
return core::media::ImageType::PublisherLogo;
}
return Image::Type::Unknown;
return core::media::ImageType::Unknown;
}
Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
core::media::ImageType imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
{
switch (type)
{
case ::TagLib::ASF::Picture::Type::Other:
return Image::Type::Other;
return core::media::ImageType::Other;
case ::TagLib::ASF::Picture::Type::FileIcon:
return Image::Type::FileIcon;
return core::media::ImageType::FileIcon;
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
return core::media::ImageType::OtherFileIcon;
case ::TagLib::ASF::Picture::Type::FrontCover:
return Image::Type::FrontCover;
return core::media::ImageType::FrontCover;
case ::TagLib::ASF::Picture::Type::BackCover:
return Image::Type::BackCover;
return core::media::ImageType::BackCover;
case ::TagLib::ASF::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
return core::media::ImageType::LeafletPage;
case ::TagLib::ASF::Picture::Type::Media:
return Image::Type::Media;
return core::media::ImageType::Media;
case ::TagLib::ASF::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
return core::media::ImageType::LeadArtist;
case ::TagLib::ASF::Picture::Type::Artist:
return Image::Type::Artist;
return core::media::ImageType::Artist;
case ::TagLib::ASF::Picture::Type::Conductor:
return Image::Type::Conductor;
return core::media::ImageType::Conductor;
case ::TagLib::ASF::Picture::Type::Band:
return Image::Type::Band;
return core::media::ImageType::Band;
case ::TagLib::ASF::Picture::Type::Composer:
return Image::Type::Composer;
return core::media::ImageType::Composer;
case ::TagLib::ASF::Picture::Type::Lyricist:
return Image::Type::Lyricist;
return core::media::ImageType::Lyricist;
case ::TagLib::ASF::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
return core::media::ImageType::RecordingLocation;
case ::TagLib::ASF::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
return core::media::ImageType::DuringRecording;
case ::TagLib::ASF::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
return core::media::ImageType::DuringPerformance;
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
return core::media::ImageType::MovieScreenCapture;
case ::TagLib::ASF::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
return core::media::ImageType::ColouredFish;
case ::TagLib::ASF::Picture::Type::Illustration:
return Image::Type::Illustration;
return core::media::ImageType::Illustration;
case ::TagLib::ASF::Picture::Type::BandLogo:
return Image::Type::BandLogo;
return core::media::ImageType::BandLogo;
case ::TagLib::ASF::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
return core::media::ImageType::PublisherLogo;
}
return Image::Type::Unknown;
return core::media::ImageType::Unknown;
}
Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
core::media::ImageType imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
{
switch (type)
{
case ::TagLib::FLAC::Picture::Type::Other:
return Image::Type::Other;
return core::media::ImageType::Other;
case ::TagLib::FLAC::Picture::Type::FileIcon:
return Image::Type::FileIcon;
return core::media::ImageType::FileIcon;
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
return core::media::ImageType::OtherFileIcon;
case ::TagLib::FLAC::Picture::Type::FrontCover:
return Image::Type::FrontCover;
return core::media::ImageType::FrontCover;
case ::TagLib::FLAC::Picture::Type::BackCover:
return Image::Type::BackCover;
return core::media::ImageType::BackCover;
case ::TagLib::FLAC::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
return core::media::ImageType::LeafletPage;
case ::TagLib::FLAC::Picture::Type::Media:
return Image::Type::Media;
return core::media::ImageType::Media;
case ::TagLib::FLAC::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
return core::media::ImageType::LeadArtist;
case ::TagLib::FLAC::Picture::Type::Artist:
return Image::Type::Artist;
return core::media::ImageType::Artist;
case ::TagLib::FLAC::Picture::Type::Conductor:
return Image::Type::Conductor;
return core::media::ImageType::Conductor;
case ::TagLib::FLAC::Picture::Type::Band:
return Image::Type::Band;
return core::media::ImageType::Band;
case ::TagLib::FLAC::Picture::Type::Composer:
return Image::Type::Composer;
return core::media::ImageType::Composer;
case ::TagLib::FLAC::Picture::Type::Lyricist:
return Image::Type::Lyricist;
return core::media::ImageType::Lyricist;
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
return core::media::ImageType::RecordingLocation;
case ::TagLib::FLAC::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
return core::media::ImageType::DuringRecording;
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
return core::media::ImageType::DuringPerformance;
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
return core::media::ImageType::MovieScreenCapture;
case ::TagLib::FLAC::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
return core::media::ImageType::ColouredFish;
case ::TagLib::FLAC::Picture::Type::Illustration:
return Image::Type::Illustration;
return core::media::ImageType::Illustration;
case ::TagLib::FLAC::Picture::Type::BandLogo:
return Image::Type::BandLogo;
return core::media::ImageType::BandLogo;
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
return core::media::ImageType::PublisherLogo;
}
return Image::Type::Unknown;
return core::media::ImageType::Unknown;
}
const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format)
@@ -217,14 +217,14 @@ namespace lms::audio::taglib
}
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
core::media::ImageType imageTypeFromAPEPictureType(std::string_view pictureType)
{
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
return Image::Type::FrontCover;
return core::media::ImageType::FrontCover;
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
return Image::Type::BackCover;
return core::media::ImageType::BackCover;
return Image::Type::Unknown;
return core::media::ImageType::Unknown;
}
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
@@ -294,7 +294,7 @@ namespace lms::audio::taglib
image.data = pictureData;
// By convention, consider the first cover art as the front cover
image.type = firstCover ? Image::Type::FrontCover : Image::Type::Unknown;
image.type = firstCover ? core::media::ImageType::FrontCover : core::media::ImageType::Unknown;
firstCover = false;
visitor(image);
+2 -2
View File
@@ -111,7 +111,7 @@ namespace lms::audio::taglib::utils
{
const std::error_code ec{ errno, std::generic_category() };
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
throw IOFileException{ "fopen failed", ec };
throw IOFileException{ p, "fopen failed", ec };
}
int fd{ ::fileno(file) };
@@ -119,7 +119,7 @@ namespace lms::audio::taglib::utils
{
const std::error_code ec{ errno, std::generic_category() };
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
throw IOFileException{ "fileno failed", ec };
throw IOFileException{ p, "fileno failed", ec };
}
return TagLib::FileStream{ fd, true };
@@ -0,0 +1,40 @@
/*
* 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/media/CodecType.hpp"
#include "core/media/ContainerType.hpp"
namespace lms::audio
{
struct AudioProperties
{
core::media::ContainerType container;
core::media::CodecType codec;
std::chrono::milliseconds duration;
unsigned bitrate;
unsigned channelCount;
unsigned sampleRate;
std::optional<unsigned> bitsPerSample;
};
} // namespace lms::audio
@@ -1,85 +0,0 @@
/*
* 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
DSD, // DSD
EAC3,
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
{
ContainerType container;
CodecType codec;
std::chrono::milliseconds duration;
unsigned bitrate;
unsigned channelCount;
unsigned sampleRate;
std::optional<unsigned> bitsPerSample;
};
} // namespace lms::audio
+6 -2
View File
@@ -19,6 +19,7 @@
#pragma once
#include <filesystem>
#include <system_error>
#include "core/Exception.hpp"
@@ -34,15 +35,18 @@ namespace lms::audio
class IOFileException : public Exception
{
public:
IOFileException(std::string_view message, std::error_code err)
: Exception{ std::string{ message } + ": " + err.message() }
IOFileException(const std::filesystem::path& path, std::string_view message, std::error_code err)
: Exception{ "File '" + path.string() + "': " + std::string{ message } + ": " + err.message() }
, _path{ path }
, _err{ err }
{
}
const std::filesystem::path& getPath() const { return _path; }
std::error_code getErrorCode() const { return _err; }
private:
std::filesystem::path _path;
std::error_code _err;
};
+2 -53
View File
@@ -23,69 +23,18 @@
#include <span>
#include <string>
#include "core/LiteralString.hpp"
#include "core/media/ImageType.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 };
core::media::ImageType type{ core::media::ImageType::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:
+15 -13
View File
@@ -23,28 +23,30 @@
#include <filesystem>
#include <optional>
#include "core/media/CodecType.hpp"
#include "core/media/ContainerType.hpp"
#include "audio/AudioProperties.hpp"
namespace lms::audio
{
// TODO deprecate?
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
AudioProperties audioProperties; // properties of the audio file
std::chrono::milliseconds offset{}; // Offset in the audio file to start transcoding from
};
struct TranscodeOutputFormat
{
core::media::ContainerType container;
core::media::CodecType codec;
};
struct TranscodeOutputParameters
{
std::optional<OutputFormat> format;
// Not setting a value here means to use the same value as the input
std::optional<TranscodeOutputFormat> format;
std::optional<unsigned> bitrate;
std::optional<unsigned> bitsPerSample;
std::optional<unsigned> channelCount;