Files
lms/src/libs/av/impl/AudioFile.cpp
T
2024-04-14 23:54:26 +02:00

337 lines
11 KiB
C++

/*
* 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"
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
}
#include <array>
#include <map>
#include <unordered_map>
#include "core/ILogger.hpp"
#include "core/String.hpp"
namespace lms::av
{
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[0];
else
return "Unknown error";
}
class AudioFileException : public Exception
{
public:
AudioFileException(int avError)
: Exception{ "AudioFileException: " + 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;
}
}
DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
{
switch (codec)
{
case AV_CODEC_ID_MP3: return DecodingCodec::MP3;
case AV_CODEC_ID_AAC: return DecodingCodec::AAC;
case AV_CODEC_ID_AC3: return DecodingCodec::AC3;
case AV_CODEC_ID_VORBIS: return DecodingCodec::VORBIS;
case AV_CODEC_ID_WMAV1: return DecodingCodec::WMAV1;
case AV_CODEC_ID_WMAV2: return DecodingCodec::WMAV2;
case AV_CODEC_ID_FLAC: return DecodingCodec::FLAC;
case AV_CODEC_ID_ALAC: return DecodingCodec::ALAC;
case AV_CODEC_ID_WAVPACK: return DecodingCodec::WAVPACK;
case AV_CODEC_ID_MUSEPACK7: return DecodingCodec::MUSEPACK7;
case AV_CODEC_ID_MUSEPACK8: return DecodingCodec::MUSEPACK8;
case AV_CODEC_ID_APE: return DecodingCodec::APE;
case AV_CODEC_ID_EAC3: return DecodingCodec::EAC3;
case AV_CODEC_ID_MP4ALS: return DecodingCodec::MP4ALS;
case AV_CODEC_ID_OPUS: return DecodingCodec::OPUS;
case AV_CODEC_ID_SHORTEN: return DecodingCodec::SHORTEN;
default:
return DecodingCodec::UNKNOWN;
}
}
}
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
{
return std::make_unique<AudioFile>(p);
}
AudioFile::AudioFile(const std::filesystem::path& p)
: _p{ p }
{
int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
if (error < 0)
{
LMS_LOG(AV, ERROR, "Cannot open " << _p.string() << ": " << averror_to_string(error));
throw AudioFileException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p.string() << ": " << 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.bitrate = _context->bit_rate;
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1000 };
info.name = _context->iformat->name;
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&)> func) const
{
static const std::unordered_map<int, std::string> codecMimeMap =
{
{ AV_CODEC_ID_BMP, "image/x-bmp" },
{ AV_CODEC_ID_GIF, "image/gif" },
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
{ AV_CODEC_ID_PNG, "image/png" },
{ AV_CODEC_ID_PNG, "image/x-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(AV, ERROR, "Skipping stream " << i << " since no codecpar is set");
continue;
}
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(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
}
const AVPacket& pkt{ avstream->attached_pic };
picture.data = reinterpret_cast<const std::byte*>(pkt.data);
picture.dataSize = pkt.size;
func(picture);
}
}
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(AV, 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->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
#endif
res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
assert(!res->codecName.empty()); // doc says it is never NULL
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
return res;
}
std::string_view getMimeType(const std::filesystem::path& fileExtension)
{
// List should be sync with the demuxers shipped in the lms's docker version
// + the _audioFileExtensions in ScanSettings
// std::filesystem::path does not seem to have std::hash specialization on freebsd
static const std::unordered_map<std::string, std::string_view> entries
{
{".mp3", "audio/mpeg"},
{".ogg", "audio/ogg"},
{".oga", "audio/ogg"},
{".opus", "audio/opus"},
{".aac", "audio/aac"},
{".alac", "audio/mp4"},
{".m4a", "audio/mp4"},
{".m4b", "audio/mp4"},
{".flac", "audio/flac"},
{".webm", "audio/webm"},
{".wav", "audio/x-wav"},
{".wma", "audio/x-ms-wma"},
{".ape", "audio/x-monkeys-audio"},
{".mpc", "audio/x-musepack"},
{".shn", "audio/x-shn"},
{".aif", "audio/x-aiff"},
{".aiff", "audio/x-aiff"},
{".m3u", "audio/x-mpegurl"},
{".pls", "audio/x-scpls"},
{".dsf", "audio/dsd"},
{".wv", "audio/x-wavpack"},
{".wvp", "audio/x-wavpack"},
{".mka", "audio/x-matroska"},
};
auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.string())) };
if (it == std::cend(entries))
return "";
return it->second;
}
} // namespace lms::av