Added a fallback to ffmpeg parser in case of audio properties are not properly decoded, ref #781

This commit is contained in:
emeric
2025-11-20 23:15:07 +01:00
parent 162d910904
commit d69827c2a2
35 changed files with 736 additions and 318 deletions
+101 -48
View File
@@ -20,21 +20,22 @@
#include "AudioFile.hpp"
#include <array>
#include <cstdio>
#include <unordered_map>
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
#include <libavutil/log.h>
}
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/Exception.hpp"
namespace lms::audio::ffmpeg
{
@@ -50,25 +51,23 @@ namespace lms::audio::ffmpeg
return "Unknown error";
}
class AudioFileException : public AudioFileParsingException
class AvException : public Exception
{
public:
AudioFileException(int avError)
: AudioFileParsingException{ averror_to_string(avError) }
AvException(int avError)
: Exception{ averror_to_string(avError) }
{
}
};
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
void extractMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
{
if (!dictionnary)
return;
AVDictionaryEntry* tag = NULL;
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
{
const AVDictionaryEntry* tag{ NULL };
while ((tag = av_dict_iterate(dictionnary, tag)))
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
}
}
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
@@ -107,58 +106,112 @@ namespace lms::audio::ffmpeg
{
switch (codec)
{
case AV_CODEC_ID_MP3:
return CodecType::MP3;
case AV_CODEC_ID_AAC:
return CodecType::AAC;
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_AC3:
return CodecType::AC3;
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_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;
case AV_CODEC_ID_EAC3:
return CodecType::EAC3;
case AV_CODEC_ID_FLAC:
return CodecType::FLAC;
case AV_CODEC_ID_MP3:
return CodecType::MP3;
case AV_CODEC_ID_MP4ALS:
return CodecType::MP4ALS;
case AV_CODEC_ID_MUSEPACK7:
return CodecType::MPC7;
case AV_CODEC_ID_MUSEPACK8:
return CodecType::MPC8;
case AV_CODEC_ID_OPUS:
return CodecType::Opus;
case AV_CODEC_ID_SHORTEN:
return CodecType::Shorten;
case AV_CODEC_ID_VORBIS:
return CodecType::Vorbis;
case AV_CODEC_ID_WAVPACK:
return CodecType::WavPack;
case AV_CODEC_ID_WMALOSSLESS:
return CodecType::WMA9Lossless;
case AV_CODEC_ID_WMAPRO:
return CodecType::WMA9Pro;
case AV_CODEC_ID_WMAV1:
return CodecType::WMA1;
case AV_CODEC_ID_WMAV2:
return CodecType::WMA2;
default:
return std::nullopt;
}
}
core::LiteralString avLogLevelToStr(int level)
{
switch (level)
{
case AV_LOG_TRACE:
return "trace";
case AV_LOG_DEBUG:
return "debug";
case AV_LOG_VERBOSE:
return "verbose";
case AV_LOG_INFO:
return "info";
case AV_LOG_WARNING:
return "warning";
case AV_LOG_ERROR:
return "error";
case AV_LOG_FATAL:
return "fatal";
case AV_LOG_PANIC:
return "panic";
default:
return "unknown";
}
}
void avLogCallback(void*, int level, const char* fmt, va_list vl)
{
if (!core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
return;
if (level > AV_LOG_WARNING)
return;
std::array<char, 256> buffer{ 0 };
std::vsnprintf(buffer.data(), buffer.size(), fmt, vl);
LMS_LOG(AUDIO, DEBUG, "ffmpeg [" << avLogLevelToStr(level) << "] " << buffer.data());
}
class AvInitializer
{
public:
AvInitializer()
{
::av_log_set_callback(avLogCallback);
}
};
} // namespace
AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p }
{
static AvInitializer init;
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 };
throw AvException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
@@ -166,7 +219,7 @@ namespace lms::audio::ffmpeg
{
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
avformat_close_input(&_context);
throw AudioFileException{ error };
throw AvException{ error };
}
}
@@ -193,11 +246,11 @@ namespace lms::audio::ffmpeg
return info;
}
AudioFile::MetadataMap AudioFile::getMetaData() const
AudioFile::MetadataMap AudioFile::extractMetaData() const
{
MetadataMap res;
getMetaDataFromDictionnary(_context->metadata, res);
extractMetaDataFromDictionnary(_context->metadata, res);
// HACK for OGG files
// If we did not find tags, search metadata in streams
@@ -205,7 +258,7 @@ namespace lms::audio::ffmpeg
{
for (std::size_t i{}; i < _context->nb_streams; ++i)
{
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
extractMetaDataFromDictionnary(_context->streams[i]->metadata, res);
if (!res.empty())
break;
@@ -266,7 +319,7 @@ namespace lms::audio::ffmpeg
return false;
}
void AudioFile::visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const
void AudioFile::visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const
{
static const std::unordered_map<int, std::string> codecMimeMap{
{ AV_CODEC_ID_BMP, "image/bmp" },
@@ -286,14 +339,14 @@ namespace lms::audio::ffmpeg
if (avstream->codecpar == nullptr)
{
LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
LMS_LOG(AUDIO, WARNING, "Skipping stream " << i << " since no codecpar is set");
continue;
}
MetadataMap metadata;
getMetaDataFromDictionnary(avstream->metadata, metadata);
extractMetaDataFromDictionnary(avstream->metadata, metadata);
Picture picture;
PictureView picture;
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
if (itMime != codecMimeMap.end())
@@ -303,10 +356,10 @@ namespace lms::audio::ffmpeg
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
LMS_LOG(AUDIO, WARNING, "AVCodecID" << avstream->codecpar->codec_id << " (" << ::avcodec_get_name(avstream->codecpar->codec_id) << ") not handled in mime type conversion");
}
const AVPacket& pkt{ avstream->attached_pic };
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);
@@ -325,7 +378,7 @@ namespace lms::audio::ffmpeg
if (!avstream->codecpar)
{
LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
LMS_LOG(AUDIO, WARNING, "Skipping stream " << streamIndex << " since no codecpar is set");
return res;
}
+3 -3
View File
@@ -36,7 +36,7 @@ extern "C"
namespace lms::audio::ffmpeg
{
struct Picture
struct PictureView
{
std::string mimeType;
std::span<const std::byte> data; // valid as long as IAudioFile exists
@@ -75,12 +75,12 @@ namespace lms::audio::ffmpeg
const std::filesystem::path& getPath() const;
ContainerInfo getContainerInfo() const;
MetadataMap getMetaData() const;
MetadataMap extractMetaData() 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;
void visitAttachedPictures(std::function<void(const PictureView&, const MetadataMap&)> func) const;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
+50 -25
View File
@@ -19,6 +19,8 @@
#include "AudioFileInfo.hpp"
#include "core/ILogger.hpp"
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
@@ -30,65 +32,88 @@ namespace lms::audio::ffmpeg
{
namespace
{
AudioProperties computeAudioProperties(const AudioFile& audioFile)
std::optional<AudioProperties> computeAudioProperties(const AudioFile& audioFile)
{
AudioProperties audioProperties;
std::optional<AudioProperties> audioProperties;
const auto containerInfo{ audioFile.getContainerInfo() };
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
if (!bestStreamInfo)
throw AudioFileParsingException{ "Cannot find best audio stream" };
{
LMS_LOG(AUDIO, DEBUG, "Cannot find best audio stream in " << audioFile.getPath());
return audioProperties;
}
if (!containerInfo.container)
throw AudioFileParsingException{ "Unhandled container type '" + containerInfo.containerName + "'" };
{
LMS_LOG(AUDIO, DEBUG, "Unhandled container '" << containerInfo.containerName << "' in " << audioFile.getPath());
return audioProperties;
}
if (!bestStreamInfo->codec)
throw AudioFileParsingException{ "Unhandled codec type '" + bestStreamInfo->codecName + "'" };
{
LMS_LOG(AUDIO, DEBUG, "Unhandled codec '" << bestStreamInfo->codecName << "' in " << audioFile.getPath());
return audioProperties;
}
if (!bestStreamInfo->bitrate || *bestStreamInfo->bitrate == 0)
throw AudioFileParsingException{ "Cannot determine bitrate" };
{
LMS_LOG(AUDIO, DEBUG, "Cannot determine bitrate in " << audioFile.getPath());
return audioProperties;
}
if (!bestStreamInfo->channelCount || *bestStreamInfo->channelCount == 0)
throw AudioFileParsingException{ "Cannot determine channel count" };
{
LMS_LOG(AUDIO, DEBUG, "Cannot determine channel count in " << audioFile.getPath());
return audioProperties;
}
if (!bestStreamInfo->sampleRate || *bestStreamInfo->sampleRate == 0)
throw AudioFileParsingException{ "Cannot determine sample rate" };
{
LMS_LOG(AUDIO, DEBUG, "Cannot determine sample rate in " << audioFile.getPath());
return audioProperties;
}
audioProperties.container = *containerInfo.container;
audioProperties.duration = containerInfo.duration;
audioProperties.codec = *bestStreamInfo->codec;
audioProperties.bitrate = *bestStreamInfo->bitrate;
audioProperties.channelCount = *bestStreamInfo->channelCount;
audioProperties.sampleRate = *bestStreamInfo->sampleRate;
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
audioProperties.emplace();
audioProperties->container = *containerInfo.container;
audioProperties->duration = containerInfo.duration;
audioProperties->codec = *bestStreamInfo->codec;
audioProperties->bitrate = *bestStreamInfo->bitrate;
audioProperties->channelCount = *bestStreamInfo->channelCount;
audioProperties->sampleRate = *bestStreamInfo->sampleRate;
audioProperties->bitsPerSample = bestStreamInfo->bitsPerSample;
return audioProperties;
}
} // namespace
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions)
: _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) }
, _audioProperties{ computeAudioProperties(*_audioFile) }
{
if (parseOptions.readTags)
_tagReader = std::make_unique<TagReader>(*_audioFile, parseOptions.enableExtraDebugLogs);
if (parseOptions.readImages)
_imageReader = std::make_unique<ImageReader>(*_audioFile);
}
AudioFileInfo::~AudioFileInfo() = default;
const AudioProperties& AudioFileInfo::getAudioProperties() const
const AudioProperties* AudioFileInfo::getAudioProperties() const
{
return *_audioProperties;
return _audioProperties.has_value() ? &_audioProperties.value() : nullptr;
}
const IImageReader& AudioFileInfo::getImageReader() const
const IImageReader* AudioFileInfo::getImageReader() const
{
return *_imageReader;
return _imageReader.get();
}
const ITagReader& AudioFileInfo::getTagReader() const
const ITagReader* AudioFileInfo::getTagReader() const
{
return *_tagReader;
return _tagReader.get();
}
} // namespace lms::audio::ffmpeg
+9 -6
View File
@@ -20,9 +20,11 @@
#pragma once
#include <filesystem>
#include <optional>
#include "audio/AudioTypes.hpp"
#include "audio/IAudioFileInfo.hpp"
#include "audio/IAudioFileInfoParser.hpp"
namespace lms::audio::ffmpeg
{
@@ -33,18 +35,19 @@ namespace lms::audio::ffmpeg
class AudioFileInfo final : public IAudioFileInfo
{
public:
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
~AudioFileInfo();
AudioFileInfo(const std::filesystem::path& filePath, const AudioFileInfoParseOptions& parseOptions);
~AudioFileInfo() override;
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;
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;
const std::optional<AudioProperties> _audioProperties;
std::unique_ptr<TagReader> _tagReader;
std::unique_ptr<ImageReader> _imageReader;
};
@@ -0,0 +1,38 @@
/*
* 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 "AudioFileInfoParser.hpp"
#include <memory>
#include "AudioFileInfo.hpp"
#include "Utils.hpp"
namespace lms::audio::ffmpeg
{
std::unique_ptr<IAudioFileInfo> AudioFileInfoParser::parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions) const
{
return std::make_unique<AudioFileInfo>(p, parseOptions);
}
std::span<const std::filesystem::path> AudioFileInfoParser::getSupportedExtensions() const
{
return utils::getSupportedExtensions();
}
} // namespace lms::audio::ffmpeg
@@ -0,0 +1,32 @@
/*
* 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/IAudioFileInfoParser.hpp"
namespace lms::audio::ffmpeg
{
class AudioFileInfoParser : public IAudioFileInfoParser
{
private:
std::unique_ptr<IAudioFileInfo> parse(const std::filesystem::path& p, const AudioFileInfoParseOptions& parseOptions = AudioFileInfoParseOptions{}) const override;
std::span<const std::filesystem::path> getSupportedExtensions() const override;
};
} // namespace lms::audio::ffmpeg
+1 -2
View File
@@ -40,7 +40,7 @@ namespace lms::audio::ffmpeg
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) {
_audioFile.visitAttachedPictures([&](const PictureView& picture, const AudioFile::MetadataMap& metaData) {
Image image;
image.data = picture.data;
image.mimeType = picture.mimeType;
@@ -52,5 +52,4 @@ namespace lms::audio::ffmpeg
visitor(image);
});
}
} // namespace lms::audio::ffmpeg
+1 -1
View File
@@ -145,7 +145,7 @@ namespace lms::audio::ffmpeg
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
: _audioFile{ audioFile }
, _metaDataMap{ audioFile.getMetaData() }
, _metaDataMap{ audioFile.extractMetaData() }
{
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{