Compare commits
47 Commits
Author | SHA1 | Date |
---|---|---|
Martchus | acfb9ef219 | |
Martchus | 351e953b83 | |
Martchus | 8204b2dfde | |
Martchus | 8246d30ec5 | |
Martchus | d48722f26c | |
Martchus | 45ab6b17b9 | |
Martchus | 6fb34ec3b3 | |
Martchus | 33327390e9 | |
Martchus | 7f3d4c5751 | |
Martchus | b1bca85ef4 | |
Martchus | 0f669c88a7 | |
Martchus | 1df871870b | |
Martchus | 909a3ee98a | |
Martchus | 5aef3f84ee | |
Martchus | 56ccd3da80 | |
Martchus | 9f41c30443 | |
Martchus | ef0ab3d8c3 | |
Martchus | 0827002183 | |
Martchus | f2e97b9899 | |
Martchus | db5e1f2c8c | |
Martchus | 04795f957f | |
Martchus | c0e9f9bf83 | |
Martchus | e6bb98d6e6 | |
Martchus | f5497fb300 | |
Martchus | f7941d442f | |
Martchus | 03f9698269 | |
Martchus | 8a6cffad95 | |
Martchus | 54a87cd32c | |
Martchus | 92345027fb | |
Martchus | 90cace1e95 | |
Martchus | 6eab8b8718 | |
Martchus | a5ab3ed1b2 | |
Martchus | 5745632af7 | |
Martchus | 6f321b7b00 | |
Martchus | 60385aa347 | |
Martchus | c5cd20682d | |
Martchus | 8ad28f857b | |
Martchus | 6ed968f5e6 | |
Martchus | a167e0702e | |
Martchus | 405631625f | |
Martchus | 6fb16d72eb | |
Martchus | 111d6190cb | |
Martchus | 0a2b948f26 | |
Martchus | 9e4d221bb1 | |
Martchus | ea6b474e8c | |
Martchus | 10a6b10658 | |
Martchus | 25166e4cb4 |
|
@ -42,3 +42,7 @@ Makefile*
|
|||
|
||||
# clang-format
|
||||
/.clang-format
|
||||
|
||||
# file with language codes one might store in the a local checkout
|
||||
iso_639-2.json
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.3.0 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.17.0 FATAL_ERROR)
|
||||
|
||||
# meta data
|
||||
project(tagparser)
|
||||
|
@ -8,8 +8,8 @@ set(META_APP_NAME "Tag Parser")
|
|||
set(META_APP_AUTHOR "Martchus")
|
||||
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
|
||||
set(META_APP_DESCRIPTION "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags")
|
||||
set(META_VERSION_MAJOR 11)
|
||||
set(META_VERSION_MINOR 6)
|
||||
set(META_VERSION_MAJOR 12)
|
||||
set(META_VERSION_MINOR 2)
|
||||
set(META_VERSION_PATCH 0)
|
||||
set(META_REQUIRED_CPP_UNIT_VERSION 1.14.0)
|
||||
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)
|
||||
|
@ -185,8 +185,9 @@ set(RES_FILES "${LANGUAGE_HEADER_ISO_639_2}")
|
|||
set(CONFIGURATION_PACKAGE_SUFFIX
|
||||
""
|
||||
CACHE STRING "sets the suffix for find_package() calls to packages configured via c++utilities")
|
||||
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.19.0 REQUIRED)
|
||||
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.21.0 REQUIRED)
|
||||
use_cpp_utilities(VISIBILITY PUBLIC)
|
||||
list(APPEND META_PRIVATE_COMPILE_DEFINITIONS CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS)
|
||||
|
||||
# link against a possibly required extra library for std::filesystem
|
||||
use_standard_filesystem()
|
||||
|
|
12
README.md
12
README.md
|
@ -7,11 +7,19 @@ The tag library can read and write the following tag formats:
|
|||
* iTunes-style MP4/M4A tags (MP4-DASH is supported)
|
||||
* ID3v1 and ID3v2 tags
|
||||
* conversion between ID3v1 and different versions of ID3v2 is possible
|
||||
* mainly for use in MP3 files but can be added to any kind of file
|
||||
* Vorbis, Opus and FLAC comments in Ogg streams
|
||||
* cover art via "METADATA_BLOCK_PICTURE" is supported
|
||||
* Vorbis comments and "METADATA_BLOCK_PICTURE" in raw FLAC streams
|
||||
* Matroska/WebM tags and attachments
|
||||
|
||||
Further remarks:
|
||||
|
||||
* Unsupported file contents (such as unsupported tag formats) are *generally* preserved as-is.
|
||||
* Note that APE tags are *not* supported. APE tags in the beginning of a file are strongly
|
||||
unrecommended and thus discarded when applying changes. APE tags at the end of the file
|
||||
are preserved as-is when applying changes.
|
||||
|
||||
## File layout options
|
||||
### Tag position
|
||||
The library allows you to choose whether tags should be placed at the beginning or at
|
||||
|
@ -59,7 +67,7 @@ The library is aware of different text encodings and can convert between differe
|
|||
|
||||
### Further documentation
|
||||
For more examples check out the command line interface of [Tag Editor](https://github.com/Martchus/tageditor).
|
||||
API documentation can be generated using Doxygen with `make tagparser_apidoc`.
|
||||
API documentation can be generated using Doxygen with `cmake --build … --target tagparser_apidoc`.
|
||||
|
||||
## Bugs, stability
|
||||
Bugs can be reported on GitHub.
|
||||
|
@ -86,6 +94,6 @@ the ["Building this straight"](https://github.com/Martchus/tageditor#building-th
|
|||
More TODOs are tracked in the [issue section at GitHub](https://github.com/Martchus/tagparser/issues).
|
||||
|
||||
## Copyright notice and license
|
||||
Copyright © 2015-2023 Marius Kittler
|
||||
Copyright © 2015-2024 Marius Kittler
|
||||
|
||||
All code is licensed under [GPL-2-or-later](LICENSE).
|
||||
|
|
|
@ -955,7 +955,7 @@ std::int16_t AacFrameElementParser::sbrHuffmanDec(SbrHuffTab table)
|
|||
|
||||
void AacFrameElementParser::parseSbrGrid(std::shared_ptr<AacSbrInfo> &sbr, std::uint8_t channel)
|
||||
{
|
||||
std::uint8_t tmp, bsEnvCount;
|
||||
std::uint8_t tmp, bsEnvCount = 0;
|
||||
//byte bsRelCount0, bsRelCount1;
|
||||
switch ((sbr->bsFrameClass[channel] = m_reader.readBits<std::uint8_t>(2))) {
|
||||
using namespace BsFrameClasses;
|
||||
|
|
|
@ -14,6 +14,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractAttachmentPrivate struct contains private fields of the AbstractAttachment class.
|
||||
struct AbstractAttachmentPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::StreamDataBlock
|
||||
* \brief The StreamDataBlock class is a reference to a certain data block of a stream.
|
||||
|
@ -127,6 +130,23 @@ FileDataBlock::~FileDataBlock()
|
|||
* \brief The AbstractAttachment class parses and stores attachment information.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new attachment.
|
||||
*/
|
||||
AbstractAttachment::AbstractAttachment()
|
||||
: m_id(0)
|
||||
, m_isDataFromFile(false)
|
||||
, m_ignored(false)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys the attachment.
|
||||
*/
|
||||
AbstractAttachment::~AbstractAttachment()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns a label for the track.
|
||||
*/
|
||||
|
|
|
@ -102,6 +102,8 @@ inline const MediaFileInfo *FileDataBlock::fileInfo() const
|
|||
return m_fileInfo.get();
|
||||
}
|
||||
|
||||
struct AbstractAttachmentPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractAttachment {
|
||||
public:
|
||||
const std::string &description() const;
|
||||
|
@ -123,7 +125,8 @@ public:
|
|||
bool isEmpty() const;
|
||||
|
||||
protected:
|
||||
AbstractAttachment();
|
||||
explicit AbstractAttachment();
|
||||
virtual ~AbstractAttachment();
|
||||
|
||||
private:
|
||||
std::string m_description;
|
||||
|
@ -131,20 +134,11 @@ private:
|
|||
std::string m_mimeType;
|
||||
std::uint64_t m_id;
|
||||
std::unique_ptr<StreamDataBlock> m_data;
|
||||
std::unique_ptr<AbstractAttachmentPrivate> m_p;
|
||||
bool m_isDataFromFile;
|
||||
bool m_ignored;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new attachment.
|
||||
*/
|
||||
inline AbstractAttachment::AbstractAttachment()
|
||||
: m_id(0)
|
||||
, m_isDataFromFile(false)
|
||||
, m_ignored(false)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns a description of the attachment.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractChapterPrivate struct contains private fields of the AbstractChapter class.
|
||||
struct AbstractChapterPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::AbstractChapter
|
||||
* \brief The AbstractChapter class parses chapter information.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -12,6 +13,7 @@ namespace TagParser {
|
|||
|
||||
class AbortableProgressFeedback;
|
||||
class Diagnostics;
|
||||
struct AbstractChapterPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractChapter {
|
||||
public:
|
||||
|
@ -41,6 +43,7 @@ protected:
|
|||
CppUtilities::TimeSpan m_startTime;
|
||||
CppUtilities::TimeSpan m_endTime;
|
||||
std::vector<std::uint64_t> m_tracks;
|
||||
std::unique_ptr<AbstractChapterPrivate> m_p;
|
||||
bool m_hidden;
|
||||
bool m_enabled;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,11 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractContainerPrivate struct contains private fields of the AbstractContainer class.
|
||||
struct AbstractContainerPrivate {
|
||||
std::vector<std::string> muxingApps, writingApps;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \class TagParser::AbstractContainer
|
||||
* \brief The AbstractContainer class provides an interface and common functionality to parse and make a certain container format.
|
||||
|
@ -472,6 +477,40 @@ bool AbstractContainer::supportsTitle() const
|
|||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the muxing applications specified as meta-data.
|
||||
*/
|
||||
const std::vector<std::string> &AbstractContainer::muxingApplications() const
|
||||
{
|
||||
static const auto empty = std::vector<std::string>();
|
||||
return m_p ? m_p->muxingApps : empty;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the muxing applications specified as meta-data.
|
||||
*/
|
||||
std::vector<std::string> &AbstractContainer::muxingApplications()
|
||||
{
|
||||
return p()->muxingApps;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the writing applications specified as meta-data.
|
||||
*/
|
||||
const std::vector<std::string> &AbstractContainer::writingApplications() const
|
||||
{
|
||||
static const auto empty = std::vector<std::string>();
|
||||
return m_p ? m_p->writingApps : empty;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the writing applications specified as meta-data.
|
||||
*/
|
||||
std::vector<std::string> &AbstractContainer::writingApplications()
|
||||
{
|
||||
return p()->writingApps;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the number of segments.
|
||||
*/
|
||||
|
@ -499,4 +538,15 @@ void AbstractContainer::reset()
|
|||
m_titles.clear();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the private data for the container.
|
||||
*/
|
||||
std::unique_ptr<AbstractContainerPrivate> &AbstractContainer::p()
|
||||
{
|
||||
if (!m_p) {
|
||||
m_p = std::make_unique<AbstractContainerPrivate>();
|
||||
}
|
||||
return m_p;
|
||||
}
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <c++utilities/io/binarywriter.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
namespace CppUtilities {
|
||||
class BinaryReader;
|
||||
|
@ -25,6 +26,7 @@ class AbstractChapter;
|
|||
class AbstractAttachment;
|
||||
class Diagnostics;
|
||||
class AbortableProgressFeedback;
|
||||
struct AbstractContainerPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractContainer {
|
||||
public:
|
||||
|
@ -78,6 +80,8 @@ public:
|
|||
const std::vector<std::string> &titles() const;
|
||||
void setTitle(std::string_view title, std::size_t segmentIndex = 0);
|
||||
virtual bool supportsTitle() const;
|
||||
const std::vector<std::string> &muxingApplications() const;
|
||||
const std::vector<std::string> &writingApplications() const;
|
||||
virtual std::size_t segmentCount() const;
|
||||
CppUtilities::TimeSpan duration() const;
|
||||
CppUtilities::DateTime creationTime() const;
|
||||
|
@ -95,6 +99,8 @@ protected:
|
|||
virtual void internalParseChapters(Diagnostics &diag, AbortableProgressFeedback &progress);
|
||||
virtual void internalParseAttachments(Diagnostics &diag, AbortableProgressFeedback &progress);
|
||||
virtual void internalMakeFile(Diagnostics &diag, AbortableProgressFeedback &progress);
|
||||
std::vector<std::string> &muxingApplications();
|
||||
std::vector<std::string> &writingApplications();
|
||||
|
||||
std::uint64_t m_version;
|
||||
std::uint64_t m_readVersion;
|
||||
|
@ -115,10 +121,13 @@ protected:
|
|||
bool m_attachmentsParsed;
|
||||
|
||||
private:
|
||||
std::unique_ptr<AbstractContainerPrivate> &p();
|
||||
|
||||
std::uint64_t m_startOffset;
|
||||
std::iostream *m_stream;
|
||||
CppUtilities::BinaryReader m_reader;
|
||||
CppUtilities::BinaryWriter m_writer;
|
||||
std::unique_ptr<AbstractContainerPrivate> m_p;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
|
|
@ -11,6 +11,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractTrackPrivate struct contains private fields of the AbstractTrack class.
|
||||
struct AbstractTrackPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::AbstractTrack
|
||||
* \brief The AbstractTrack class parses and stores technical information about
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <c++utilities/misc/flagenumclass.h>
|
||||
|
||||
#include <iosfwd>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
|
@ -104,6 +105,8 @@ CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(TagParser, TagParser::TrackFlags)
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
struct AbstractTrackPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractTrack {
|
||||
friend class MpegAudioFrameStream;
|
||||
friend class WaveAudioStream;
|
||||
|
@ -129,6 +132,7 @@ public:
|
|||
MediaType mediaType() const;
|
||||
std::string_view mediaTypeName() const;
|
||||
std::uint64_t size() const;
|
||||
void setSize(std::uint64_t size);
|
||||
std::uint32_t trackNumber() const;
|
||||
void setTrackNumber(std::uint32_t trackNumber);
|
||||
std::uint64_t id() const;
|
||||
|
@ -232,6 +236,7 @@ protected:
|
|||
AlphaMode m_alphaMode;
|
||||
DisplayUnit m_displayUnit;
|
||||
AspectRatioType m_aspectRatioType;
|
||||
std::unique_ptr<AbstractTrackPrivate> m_p;
|
||||
|
||||
private:
|
||||
std::string makeDescription(bool verbose) const;
|
||||
|
@ -392,6 +397,19 @@ inline std::uint64_t AbstractTrack::size() const
|
|||
return m_size;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the size in bytes.
|
||||
* \remarks
|
||||
* This is used by MediaFileInfo to set the track size for certain types of tracks before invoking the parsing.
|
||||
* If you use this a class derived from AbstractTrack directly you may want to do the same if not the entire
|
||||
* input stream is supposed to be considered part of the track and the parser would otherwise assume that (like
|
||||
* the parser of MpegAudioFrameStream might do).
|
||||
*/
|
||||
inline void AbstractTrack::setSize(std::uint64_t size)
|
||||
{
|
||||
m_size = size;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the track number if known; otherwise returns 0.
|
||||
*/
|
||||
|
|
|
@ -24,15 +24,8 @@ void AdtsStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedbac
|
|||
if (!m_istream) {
|
||||
throw NoDataFoundException();
|
||||
}
|
||||
// get size
|
||||
m_istream->seekg(-128, ios_base::end);
|
||||
if (m_reader.readUInt24BE() == 0x544147) {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) - 3u - m_startOffset;
|
||||
} else {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) + 125u - m_startOffset;
|
||||
}
|
||||
m_istream->seekg(static_cast<streamoff>(m_startOffset), ios_base::beg);
|
||||
// parse frame header
|
||||
m_istream->seekg(static_cast<std::streamoff>(m_startOffset), ios_base::beg);
|
||||
m_firstFrame.parseHeader(m_reader);
|
||||
m_format = Mpeg4AudioObjectIds::idToMediaFormat(m_firstFrame.mpeg4AudioObjectId());
|
||||
m_channelCount = Mpeg4ChannelConfigs::channelCount(m_channelConfig = m_firstFrame.mpeg4ChannelConfig());
|
||||
|
|
|
@ -42,12 +42,13 @@ void example()
|
|||
// code.
|
||||
// - Parsing a file can be expensive if the file is big or the disk IO is slow. You might want to
|
||||
// run it in a separate thread.
|
||||
// - At this point the parser does not make much use of the progress object.
|
||||
// - At this point the parser does not make much use of the progress object (in contrast to applyChanges()
|
||||
// shown below).
|
||||
fileInfo.parseContainerFormat(diag, progress);
|
||||
fileInfo.parseTags(diag, progress);
|
||||
fileInfo.parseAttachments(diag, progress);
|
||||
fileInfo.parseChapters(diag, progress);
|
||||
fileInfo.parseEverything(diag, progress); // just use that one if you want all over the above
|
||||
fileInfo.parseEverything(diag, progress); // just use that one if you want all of the above
|
||||
|
||||
// get tag as an object derived from the Tag class
|
||||
// notes:
|
||||
|
@ -55,7 +56,7 @@ void example()
|
|||
// fileInfo.createAppropriateTags(…) to create tags as needed.
|
||||
auto tag = fileInfo.tags().at(0);
|
||||
|
||||
// extract a field value and convert it to UTF-8 std::string (toString() might throw ConversionException)
|
||||
// extract a field value and convert it to a UTF-8 std::string (toString() might throw ConversionException)
|
||||
auto title = tag->value(TagParser::KnownField::Title).toString(TagParser::TagTextEncoding::Utf8);
|
||||
|
||||
// change a field value using an encoding suitable for the tag format
|
||||
|
@ -84,6 +85,5 @@ void example()
|
|||
// - Use progress.tryToAbort() from another thread or an interrupt handler to abort gracefully without leaving
|
||||
// the file in an inconsistent state.
|
||||
// - Be sure everything has been parsed before as the library needs to be aware of the whole file structure.
|
||||
fileInfo.parseEverything(diag, progress);
|
||||
fileInfo.applyChanges(diag, progress);
|
||||
}
|
||||
|
|
|
@ -115,7 +115,11 @@ void FlacStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedbac
|
|||
m_vorbisComment = make_unique<VorbisComment>();
|
||||
}
|
||||
try {
|
||||
m_vorbisComment->parse(*m_istream, header.dataSize(), VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
auto flags = VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte;
|
||||
if (m_mediaFileInfo.fileHandlingFlags() & MediaFileHandlingFlags::ConvertTotalFields) {
|
||||
flags += VorbisCommentFlags::ConvertTotalFields;
|
||||
}
|
||||
m_vorbisComment->parse(*m_istream, header.dataSize(), flags, diag);
|
||||
} catch (const Failure &) {
|
||||
// error is logged via notifications, just continue with the next metadata block
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ void FlacToOggMappingHeader::parseHeader(OggIterator &iterator)
|
|||
constexpr auto idSize = 0x05, mappingHeaderSize = 0x0D, blockHeaderSize = 0x04, streamInfoSize = 0x22;
|
||||
char buff[mappingHeaderSize + blockHeaderSize + streamInfoSize - idSize];
|
||||
iterator.read(buff, idSize);
|
||||
if (*buff != 0x7Fu || BE::toUInt32(buff + 1) != 0x464C4143u) {
|
||||
if (*buff != 0x7Fu || BE::toInt<std::uint32_t>(buff + 1) != 0x464C4143u) {
|
||||
throw InvalidDataException(); // not FLAC-to-Ogg mapping header
|
||||
}
|
||||
iterator.read(buff, sizeof(buff));
|
||||
|
@ -35,8 +35,8 @@ void FlacToOggMappingHeader::parseHeader(OggIterator &iterator)
|
|||
// parse FLAC-to-Ogg mapping header
|
||||
m_majorVersion = static_cast<std::uint8_t>(*(buff + 0x00));
|
||||
m_minorVersion = static_cast<std::uint8_t>(*(buff + 0x01));
|
||||
m_headerCount = BE::toUInt16(buff + 0x02);
|
||||
if (BE::toUInt32(buff + 0x04) != 0x664C6143u) {
|
||||
m_headerCount = BE::toInt<std::uint16_t>(buff + 0x02);
|
||||
if (BE::toInt<std::uint32_t>(buff + 0x04) != 0x664C6143u) {
|
||||
throw InvalidDataException(); // native FLAC signature not present
|
||||
}
|
||||
|
||||
|
|
|
@ -944,7 +944,7 @@ void GenericFileElement<ImplementationType>::copyInternal(
|
|||
} catch (const Failure &) {
|
||||
throw InvalidDataException();
|
||||
}
|
||||
auto &stream = container().fileInfo().stream();
|
||||
auto &stream = container().stream();
|
||||
stream.seekg(static_cast<std::streamoff>(startOffset), std::ios_base::beg);
|
||||
CppUtilities::CopyHelper<0x10000> copyHelper;
|
||||
if (progress) {
|
||||
|
|
1
global.h
1
global.h
|
@ -4,6 +4,7 @@
|
|||
#ifndef TAG_PARSER_GLOBAL
|
||||
#define TAG_PARSER_GLOBAL
|
||||
|
||||
#include "tagparser-definitions.h"
|
||||
#include <c++utilities/application/global.h>
|
||||
|
||||
#ifdef TAG_PARSER_STATIC
|
||||
|
|
|
@ -934,7 +934,7 @@ tuple<const char *, size_t, const char *> Id3v2Frame::parseSubstring(
|
|||
case TagTextEncoding::Utf16BigEndian:
|
||||
case TagTextEncoding::Utf16LittleEndian: {
|
||||
if (bufferSize >= 2) {
|
||||
switch (LE::toUInt16(buffer)) {
|
||||
switch (LE::toInt<std::uint16_t>(buffer)) {
|
||||
case 0xFEFF:
|
||||
if (encoding == TagTextEncoding::Utf16BigEndian) {
|
||||
diag.emplace_back(DiagLevel::Critical,
|
||||
|
@ -1002,9 +1002,9 @@ void Id3v2Frame::parseBom(const char *buffer, std::size_t maxSize, TagTextEncodi
|
|||
switch (encoding) {
|
||||
case TagTextEncoding::Utf16BigEndian:
|
||||
case TagTextEncoding::Utf16LittleEndian:
|
||||
if ((maxSize >= 2) && (BE::toUInt16(buffer) == 0xFFFE)) {
|
||||
if ((maxSize >= 2) && (BE::toInt<std::uint16_t>(buffer) == 0xFFFE)) {
|
||||
encoding = TagTextEncoding::Utf16LittleEndian;
|
||||
} else if ((maxSize >= 2) && (BE::toUInt16(buffer) == 0xFEFF)) {
|
||||
} else if ((maxSize >= 2) && (BE::toInt<std::uint16_t>(buffer) == 0xFEFF)) {
|
||||
encoding = TagTextEncoding::Utf16BigEndian;
|
||||
}
|
||||
break;
|
||||
|
@ -1172,7 +1172,7 @@ void Id3v2Frame::makeLegacyPicture(
|
|||
} else {
|
||||
imageFormat = "UND";
|
||||
}
|
||||
strncpy(++offset, imageFormat, 3);
|
||||
std::memcpy(++offset, imageFormat, 3);
|
||||
|
||||
// write picture type
|
||||
*(offset += 3) = static_cast<char>(typeInfo);
|
||||
|
|
|
@ -338,7 +338,7 @@ inline Id3v2Frame::IdentifierType Id3v2Frame::fieldIdFromString(std::string_view
|
|||
case 3:
|
||||
return CppUtilities::BE::toUInt24(idString.data());
|
||||
case 4:
|
||||
return CppUtilities::BE::toUInt32(idString.data());
|
||||
return CppUtilities::BE::toInt<std::uint32_t>(idString.data());
|
||||
default:
|
||||
throw CppUtilities::ConversionException("ID3v2 ID must be 3 or 4 chars");
|
||||
}
|
||||
|
|
|
@ -85,6 +85,10 @@ std::uint32_t convertToShortId(std::uint32_t id)
|
|||
return sRating;
|
||||
case lISRC:
|
||||
return sISRC;
|
||||
case lPublisherWebpage:
|
||||
return sPublisherWebpage;
|
||||
case lUserDefinedURL:
|
||||
return sUserDefinedURL;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
@ -155,6 +159,10 @@ std::uint32_t convertToLongId(std::uint32_t id)
|
|||
return lRating;
|
||||
case sISRC:
|
||||
return lISRC;
|
||||
case sPublisherWebpage:
|
||||
return lPublisherWebpage;
|
||||
case sUserDefinedURL:
|
||||
return lUserDefinedURL;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ enum KnownValue : std::uint32_t {
|
|||
lMood = 0x544D4F4F, /**< TMOO */
|
||||
lISRC = 0x54535243, /**< TSRC */
|
||||
lUserDefinedText = 0x54585858, /**< TXXX */
|
||||
lPublisherWebpage = 0x57505542, /**< WPUB */
|
||||
lUserDefinedURL = 0x57585858, /**< WXXX */
|
||||
|
||||
sAlbum = 0x54414c, /**< ?TAL */
|
||||
sArtist = 0x545031, /**< ?TP1 */
|
||||
|
@ -82,6 +84,8 @@ enum KnownValue : std::uint32_t {
|
|||
sCopyright = 0x544352, /**< TCR */
|
||||
sISRC = 0x545243, /**< TRC */
|
||||
sUserDefinedText = 0x545858, /**< ?TXX */
|
||||
sPublisherWebpage = 0x575042, /**< ?WPB */
|
||||
sUserDefinedURL = 0x575858, /**< ?WXX */
|
||||
};
|
||||
|
||||
TAG_PARSER_EXPORT std::uint32_t convertToShortId(std::uint32_t id);
|
||||
|
@ -117,6 +121,14 @@ constexpr bool isTextFrame(std::uint32_t id)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether the specified \a id is a URL frame id.
|
||||
*/
|
||||
constexpr bool isUrlFrame(std::uint32_t id)
|
||||
{
|
||||
return (id & 0xFF000000u) == 0x57000000u && (id != Id3v2FrameIds::lUserDefinedURL);
|
||||
}
|
||||
|
||||
} // namespace Id3v2FrameIds
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace TagParser {
|
|||
|
||||
/*!
|
||||
* \class TagParser::IvfStream
|
||||
* \brief Implementation of TagParser::AbstractTrack for ADTS streams.
|
||||
* \brief Implementation of TagParser::AbstractTrack for IVF streams.
|
||||
* \sa https://wiki.multimedia.cx/index.php/IVF
|
||||
*/
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ void EbmlElement::internalParse(Diagnostics &diag)
|
|||
continue; // try again
|
||||
}
|
||||
reader().read(buf + (maximumIdLengthSupported() - m_idLength), m_idLength);
|
||||
m_id = BE::toUInt32(buf);
|
||||
m_id = BE::toInt<std::uint32_t>(buf);
|
||||
|
||||
// check whether this element is actually a sibling of one of its parents rather then a child
|
||||
// (might be the case if the parent's size is unknown and hence assumed to be the max file size)
|
||||
|
@ -172,7 +172,7 @@ void EbmlElement::internalParse(Diagnostics &diag)
|
|||
reader().read(buf + (maximumSizeLengthSupported() - m_sizeLength), m_sizeLength);
|
||||
// xor the first byte in buffer which has been read from the file with mask
|
||||
*(buf + (maximumSizeLengthSupported() - m_sizeLength)) ^= static_cast<char>(mask);
|
||||
m_dataSize = BE::toUInt64(buf);
|
||||
m_dataSize = BE::toInt<std::uint64_t>(buf);
|
||||
// check if element is truncated
|
||||
if (totalSize() > maxTotalSize()) {
|
||||
if (m_idLength + m_sizeLength > maxTotalSize()) { // header truncated
|
||||
|
@ -242,7 +242,7 @@ std::uint64_t EbmlElement::readUInteger()
|
|||
const auto bytesToSkip = maxBytesToRead - min(dataSize(), maxBytesToRead);
|
||||
stream().seekg(static_cast<streamoff>(dataOffset()), ios_base::beg);
|
||||
stream().read(buff + bytesToSkip, static_cast<streamoff>(sizeof(buff) - bytesToSkip));
|
||||
return BE::toUInt64(buff);
|
||||
return BE::toInt<std::uint64_t>(buff);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -35,8 +35,6 @@ namespace TagParser {
|
|||
* \brief Implementation of GenericContainer<MediaFileInfo, MatroskaTag, MatroskaTrack, EbmlElement>.
|
||||
*/
|
||||
|
||||
std::uint64_t MatroskaContainer::m_maxFullParseSize = 0x3200000; // FIXME v11: move to MediaFileInfo
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new container for the specified \a fileInfo at the specified \a startOffset.
|
||||
*/
|
||||
|
@ -84,13 +82,13 @@ void MatroskaContainer::reset()
|
|||
*/
|
||||
void MatroskaContainer::validateIndex(Diagnostics &diag, AbortableProgressFeedback &progress)
|
||||
{
|
||||
static const string context("validating Matroska file index (cues)");
|
||||
bool cuesElementsFound = false;
|
||||
static const auto context = std::string("validating Matroska file index (cues)");
|
||||
auto cuesElementsFound = false;
|
||||
if (m_firstElement) {
|
||||
unordered_set<EbmlElement::IdentifierType> ids;
|
||||
bool cueTimeFound = false, cueTrackPositionsFound = false;
|
||||
unique_ptr<EbmlElement> clusterElement;
|
||||
std::uint64_t pos, prevClusterSize = 0, currentOffset = 0;
|
||||
auto ids = std::unordered_set<EbmlElement::IdentifierType>();
|
||||
auto cueTimeFound = false, cueTrackPositionsFound = false;
|
||||
auto clusterElement = std::unique_ptr<EbmlElement>();
|
||||
auto pos = std::uint64_t(), prevClusterSize = std::uint64_t(), currentOffset = std::uint64_t();
|
||||
// iterate through all segments
|
||||
for (EbmlElement *segmentElement = m_firstElement->siblingById(MatroskaIds::Segment, diag); segmentElement;
|
||||
segmentElement = segmentElement->siblingById(MatroskaIds::Segment, diag)) {
|
||||
|
@ -564,7 +562,7 @@ void MatroskaContainer::internalParseHeader(Diagnostics &diag, AbortableProgress
|
|||
}
|
||||
}
|
||||
// -> stop if tracks and tags have been found or the file exceeds the max. size to fully process
|
||||
if (((!m_tracksElements.empty() && !m_tagsElements.empty()) || fileInfo().size() > m_maxFullParseSize)
|
||||
if (((!m_tracksElements.empty() && !m_tagsElements.empty()) || fileInfo().size() > fileInfo().maxFullParseSize())
|
||||
&& !m_segmentInfoElements.empty()) {
|
||||
goto finish;
|
||||
}
|
||||
|
@ -629,6 +627,12 @@ void MatroskaContainer::parseSegmentInfo(Diagnostics &diag)
|
|||
case MatroskaIds::TimeCodeScale:
|
||||
timeScale = subElement->readUInteger();
|
||||
break;
|
||||
case MatroskaIds::MuxingApp:
|
||||
muxingApplications().emplace_back(subElement->readString());
|
||||
break;
|
||||
case MatroskaIds::WrittingApp:
|
||||
writingApplications().emplace_back(subElement->readString());
|
||||
break;
|
||||
}
|
||||
subElement = subElement->nextSibling();
|
||||
}
|
||||
|
@ -834,7 +838,7 @@ struct SegmentData {
|
|||
MatroskaCuePositionUpdater cuesUpdater;
|
||||
/// \brief size of the "SegmentInfo"-element
|
||||
std::uint64_t infoDataSize;
|
||||
/// \brief cluster sizes
|
||||
/// \brief cluster sizes, needed because cluster elements are not necessarily copied as-is so they're size might change
|
||||
std::vector<std::uint64_t> clusterSizes;
|
||||
/// \brief first "Cluster"-element (original file)
|
||||
EbmlElement *firstClusterElement;
|
||||
|
@ -944,13 +948,18 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
const std::uint64_t ebmlHeaderSize = 4 + EbmlElement::calculateSizeDenotationLength(ebmlHeaderDataSize) + ebmlHeaderDataSize;
|
||||
|
||||
// calculate size of "WritingLib"-element
|
||||
constexpr std::string_view muxingAppName = APP_NAME " v" APP_VERSION;
|
||||
constexpr std::uint64_t muxingAppElementTotalSize = 2 + 1 + muxingAppName.size();
|
||||
const auto &muxingApps = const_cast<const MatroskaContainer *>(this)->muxingApplications();
|
||||
const auto muxingAppName = (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::PreserveMuxingApplication && !muxingApps.empty())
|
||||
? std::string_view(muxingApps.front())
|
||||
: std::string_view(APP_NAME " v" APP_VERSION);
|
||||
const auto muxingAppElementTotalSize = std::uint64_t(2 + 1 + muxingAppName.size());
|
||||
|
||||
// calculate size of "WritingApp"-element
|
||||
const std::uint64_t writingAppElementDataSize
|
||||
= fileInfo().writingApplication().empty() ? muxingAppName.size() : fileInfo().writingApplication().size();
|
||||
const std::uint64_t writingAppElementTotalSize = 2 + 1 + writingAppElementDataSize;
|
||||
const auto writingApps = const_cast<const MatroskaContainer *>(this)->writingApplications();
|
||||
const auto writingAppName = (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::PreserveWritingApplication && !writingApps.empty())
|
||||
? std::string_view(writingApps.front())
|
||||
: std::string_view(fileInfo().writingApplication().empty() ? muxingAppName : std::string_view(fileInfo().writingApplication()));
|
||||
const auto writingAppElementTotalSize = std::uint64_t(2 + 1 + writingAppName.size());
|
||||
|
||||
try {
|
||||
// calculate size of "Tags"-element
|
||||
|
@ -1219,7 +1228,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
progress.updateStep("Calculating cluster offsets ...");
|
||||
}
|
||||
|
||||
// decided whether it is necessary to rewrite the entire file (if not already rewriting)
|
||||
// decide whether it is necessary to rewrite the entire file (if not already rewriting)
|
||||
if (!rewriteRequired) {
|
||||
// find first "Cluster"-element
|
||||
if ((level1Element = segment.firstClusterElement)) {
|
||||
|
@ -1357,7 +1366,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
goto calculateSegmentData;
|
||||
}
|
||||
} else {
|
||||
// if rewrite is required, pretend writing the remaining elements to compute total segment size
|
||||
// if rewrite is required, pretend writing the remaining elements to compute total segment size and cluster sizes
|
||||
|
||||
// pretend writing "Void"-element (only if there is at least one "Cluster"-element in the segment)
|
||||
if (!segmentIndex && rewriteRequired && (level1Element = level0Element->childById(MatroskaIds::Cluster, diag))) {
|
||||
|
@ -1639,8 +1648,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
}
|
||||
// -> write "MuxingApp"- and "WritingApp"-element
|
||||
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::MuxingApp, muxingAppName);
|
||||
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::WrittingApp,
|
||||
fileInfo().writingApplication().empty() ? muxingAppName : fileInfo().writingApplication());
|
||||
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::WrittingApp, writingAppName);
|
||||
}
|
||||
|
||||
// write "Tracks"-element
|
||||
|
|
|
@ -31,8 +31,6 @@ public:
|
|||
std::uint64_t maxSizeLength() const;
|
||||
const std::vector<std::unique_ptr<MatroskaSeekInfo>> &seekInfos() const;
|
||||
|
||||
static std::uint64_t maxFullParseSize();
|
||||
void setMaxFullParseSize(std::uint64_t maxFullParseSize);
|
||||
const std::vector<std::unique_ptr<MatroskaEditionEntry>> &editionEntires() const;
|
||||
MatroskaChapter *chapter(std::size_t index) override;
|
||||
std::size_t chapterCount() const override;
|
||||
|
@ -72,7 +70,6 @@ private:
|
|||
std::vector<std::unique_ptr<MatroskaEditionEntry>> m_editionEntries;
|
||||
std::vector<std::unique_ptr<MatroskaAttachment>> m_attachments;
|
||||
std::size_t m_segmentCount;
|
||||
static std::uint64_t m_maxFullParseSize;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -99,33 +96,6 @@ inline const std::vector<std::unique_ptr<MatroskaSeekInfo>> &MatroskaContainer::
|
|||
return m_seekInfos;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the maximal file size for a "full parse" in byte.
|
||||
*
|
||||
* The "Tags" element (which holds the tag information) is commonly at the end of a Matroska file. Hence the
|
||||
* parser needs to walk through the entire file to find the tag information if no "SeekHead" element is present
|
||||
* which might causes long loading times. To avoid this a maximal file size for a "full parse" can be specified.
|
||||
* The disadvantage is that the parser relies on the presence of a SeekHead element on larger files to retrieve
|
||||
* tag information.
|
||||
*
|
||||
* The default value is 50 MiB.
|
||||
*
|
||||
* \sa setMaxFullParseSize()
|
||||
*/
|
||||
inline std::uint64_t MatroskaContainer::maxFullParseSize()
|
||||
{
|
||||
return m_maxFullParseSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the maximal file size for a "full parse" in byte.
|
||||
* \sa maxFullParseSize()
|
||||
*/
|
||||
inline void MatroskaContainer::setMaxFullParseSize(std::uint64_t maxFullParseSize)
|
||||
{
|
||||
m_maxFullParseSize = maxFullParseSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the edition entries.
|
||||
*/
|
||||
|
|
|
@ -78,7 +78,7 @@ void MatroskaCuePositionUpdater::parse(EbmlElement *cuesElement, Diagnostics &di
|
|||
cuePointElementSize += cuePointChild->totalSize();
|
||||
break;
|
||||
case MatroskaIds::CueTrackPositions:
|
||||
cueTrackPositionsElementSize = 0;
|
||||
cueTrackPositionsElementSize = relPos = 0;
|
||||
cueRelativePositionElement = cueClusterPositionElement = nullptr;
|
||||
for (EbmlElement *cueTrackPositionsChild = cuePointChild->firstChild(); cueTrackPositionsChild;
|
||||
cueTrackPositionsChild = cueTrackPositionsChild->nextSibling()) {
|
||||
|
|
|
@ -122,7 +122,7 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "track number";
|
||||
case TrackUID:
|
||||
return "unique track id";
|
||||
case TrackType:
|
||||
case TrackEntryIds::TrackType:
|
||||
return "track type";
|
||||
case TrackAudio:
|
||||
return "audio track";
|
||||
|
@ -192,7 +192,7 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "video display width";
|
||||
case DisplayHeight:
|
||||
return "video display height";
|
||||
case DisplayUnit:
|
||||
case TrackVideoIds::DisplayUnit:
|
||||
return "video display unit";
|
||||
case PixelWidth:
|
||||
return "video pixel width";
|
||||
|
@ -208,9 +208,9 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "video pixel crop right";
|
||||
case FlagInterlaced:
|
||||
return "video flag interlaced";
|
||||
case StereoMode:
|
||||
case TrackVideoIds::StereoMode:
|
||||
return "video stereo mode";
|
||||
case AspectRatioType:
|
||||
case TrackVideoIds::AspectRatioType:
|
||||
return "video aspect ratio type";
|
||||
case ColorSpace:
|
||||
return "video color space";
|
||||
|
@ -276,7 +276,7 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "content encryption signature hash algorithmus";
|
||||
|
||||
// IDs in the Tags master
|
||||
case Tag:
|
||||
case TagsIds::Tag:
|
||||
return "tag";
|
||||
|
||||
// IDs in the Tag master
|
||||
|
@ -569,7 +569,7 @@ MatroskaElementLevel matroskaIdLevel(std::uint32_t matroskaId)
|
|||
case CuePoint:
|
||||
case AttachedFile:
|
||||
case EditionEntry:
|
||||
case Tag:
|
||||
case TagsIds::Tag:
|
||||
return MatroskaElementLevel::Level2;
|
||||
case SeekID:
|
||||
case SeekPosition:
|
||||
|
@ -588,7 +588,7 @@ MatroskaElementLevel matroskaIdLevel(std::uint32_t matroskaId)
|
|||
case Slices:
|
||||
case TrackNumber:
|
||||
case TrackUID:
|
||||
case TrackType:
|
||||
case TrackEntryIds::TrackType:
|
||||
case TrackFlagEnabled:
|
||||
case TrackFlagDefault:
|
||||
case TrackFlagForced:
|
||||
|
|
|
@ -43,7 +43,7 @@ enum SeekIds { SeekID = 0x53AB, SeekPosition = 0x53AC };
|
|||
enum SegmentInfoIds {
|
||||
TimeCodeScale = 0x2AD7B1,
|
||||
Duration = 0x4489,
|
||||
WrittingApp = 0x5741,
|
||||
WrittingApp = 0x5741, // TODOv13: change to WritingApp
|
||||
MuxingApp = 0x4D80,
|
||||
DateUTC = 0x4461,
|
||||
SegmentUID = 0x73A4,
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
#include <c++utilities/io/path.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
|
@ -59,6 +60,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The MediaFileInfoPrivate struct contains private fields of the MediaFileInfo class.
|
||||
struct MediaFileInfoPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::MediaFileInfo
|
||||
* \brief The MediaFileInfo class allows to read and write tag information providing
|
||||
|
@ -80,6 +84,7 @@ MediaFileInfo::MediaFileInfo(std::string &&path)
|
|||
, m_containerFormat(ContainerFormat::Unknown)
|
||||
, m_containerOffset(0)
|
||||
, m_paddingSize(0)
|
||||
, m_effectiveSize(0)
|
||||
, m_fileStructureFlags(MediaFileStructureFlags::None)
|
||||
, m_tracksParsingStatus(ParsingStatus::NotParsedYet)
|
||||
, m_tagsParsingStatus(ParsingStatus::NotParsedYet)
|
||||
|
@ -92,6 +97,7 @@ MediaFileInfo::MediaFileInfo(std::string &&path)
|
|||
, m_indexPosition(ElementPosition::BeforeData)
|
||||
, m_fileHandlingFlags(MediaFileHandlingFlags::ForceRewrite | MediaFileHandlingFlags::ForceTagPosition | MediaFileHandlingFlags::ForceIndexPosition
|
||||
| MediaFileHandlingFlags::NormalizeKnownTagFieldIds | MediaFileHandlingFlags::PreserveRawTimingValues)
|
||||
, m_maxFullParseSize(0x3200000)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -189,7 +195,7 @@ startParsingSignature:
|
|||
|
||||
// parse signature
|
||||
switch ((m_containerFormat = parseSignature(buff, sizeof(buff)))) {
|
||||
case ContainerFormat::Id2v2Tag:
|
||||
case ContainerFormat::Id3v2Tag:
|
||||
// save position of ID3v2 tag
|
||||
m_actualId3v2TagOffsets.push_back(m_containerOffset);
|
||||
if (m_actualId3v2TagOffsets.size() == 2) {
|
||||
|
@ -201,7 +207,7 @@ startParsingSignature:
|
|||
stream().read(buff, 5);
|
||||
|
||||
// set the container offset to skip ID3v2 header
|
||||
m_containerOffset += toNormalInt(BE::toUInt32(buff + 1)) + 10;
|
||||
m_containerOffset += toNormalInt(BE::toInt<std::uint32_t>(buff + 1)) + 10;
|
||||
if ((*buff) & 0x10) {
|
||||
// footer present
|
||||
m_containerOffset += 10;
|
||||
|
@ -254,6 +260,23 @@ startParsingSignature:
|
|||
static_cast<OggContainer *>(m_container.get())->setChecksumValidationEnabled(isForcingFullParse());
|
||||
break;
|
||||
case ContainerFormat::Unknown:
|
||||
case ContainerFormat::ApeTag:
|
||||
// skip APE tag if the specified size makes sense at all
|
||||
if (m_containerFormat == ContainerFormat::ApeTag) {
|
||||
if (const auto apeEnd = m_containerOffset + 32 + LE::toUInt32(buff + 12); apeEnd <= static_cast<std::streamoff>(size())) {
|
||||
// take record of APE tag
|
||||
diag.emplace_back(DiagLevel::Critical,
|
||||
argsToString("Found an APE tag at the beginning of the file at offset ", m_containerOffset,
|
||||
". This tag format is not supported and the tag will therefore be ignored. It will NOT be preserved when saving as "
|
||||
"placing an APE tag at the beginning of a file is strongly unrecommended."),
|
||||
context);
|
||||
// continue reading signature
|
||||
m_containerOffset = apeEnd;
|
||||
goto startParsingSignature;
|
||||
}
|
||||
m_containerFormat = ContainerFormat::Unknown;
|
||||
}
|
||||
|
||||
// check for magic numbers at odd offsets
|
||||
// -> check for tar (magic number at offset 0x101)
|
||||
if (size() > 0x107) {
|
||||
|
@ -334,6 +357,13 @@ void MediaFileInfo::parseTracks(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
default:
|
||||
throw NotImplementedException();
|
||||
}
|
||||
if (m_containerFormat != ContainerFormat::Flac) {
|
||||
// ensure the effective size has been determined
|
||||
// note: This is not required for FLAC and should also be avoided as parseTags() will invoke
|
||||
// parseTracks() when dealing with FLAC files.
|
||||
parseTags(diag, progress);
|
||||
m_singleTrack->setSize(m_effectiveSize);
|
||||
}
|
||||
m_singleTrack->parseHeader(diag, progress);
|
||||
|
||||
// take padding for some "single-track" formats into account
|
||||
|
@ -378,12 +408,14 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
static const string context("parsing tag");
|
||||
|
||||
// check for ID3v1 tag
|
||||
if (size() >= 128) {
|
||||
auto effectiveSize = static_cast<std::streamoff>(size());
|
||||
if (effectiveSize >= 128) {
|
||||
m_id3v1Tag = make_unique<Id3v1Tag>();
|
||||
try {
|
||||
stream().seekg(-128, ios_base::end);
|
||||
stream().seekg(effectiveSize - 128, std::ios_base::beg);
|
||||
m_id3v1Tag->parse(stream(), diag);
|
||||
m_fileStructureFlags += MediaFileStructureFlags::ActualExistingId3v1Tag;
|
||||
effectiveSize -= 128;
|
||||
} catch (const NoDataFoundException &) {
|
||||
m_id3v1Tag.reset();
|
||||
} catch (const OperationAbortedException &) {
|
||||
|
@ -395,6 +427,31 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
}
|
||||
}
|
||||
|
||||
// check for APE tag at the end of the file (APE tags a the beginning are already covered when parsing the container format)
|
||||
if (constexpr auto apeHeaderSize = 32; effectiveSize >= apeHeaderSize) {
|
||||
const auto footerOffset = effectiveSize - apeHeaderSize;
|
||||
char buffer[apeHeaderSize];
|
||||
stream().seekg(footerOffset, std::ios_base::beg);
|
||||
stream().read(buffer, sizeof(buffer));
|
||||
if (BE::toInt<std::uint64_t>(buffer) == 0x4150455441474558ul /* APETAGEX */) {
|
||||
// take record of APE tag
|
||||
const auto tagSize = static_cast<std::streamoff>(LE::toInt<std::uint32_t>(buffer + 12));
|
||||
const auto flags = LE::toInt<std::uint32_t>(buffer + 20);
|
||||
// subtract tag size (footer size and contents) from effective size
|
||||
if (tagSize <= effectiveSize) {
|
||||
effectiveSize -= tagSize;
|
||||
}
|
||||
// subtract header size (not included in tag size) from effective size if flags indicate presence of header
|
||||
if ((flags & 0x80000000u) && (apeHeaderSize <= effectiveSize)) {
|
||||
effectiveSize -= apeHeaderSize;
|
||||
}
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("Found an APE tag at the end of the file at offset ", (footerOffset - tagSize),
|
||||
". This tag format is not supported and the tag will therefore be ignored. It will be preserved when saving as-is."),
|
||||
context);
|
||||
}
|
||||
}
|
||||
|
||||
// check for ID3v2 tags: the offsets of the ID3v2 tags have already been parsed when parsing the container format
|
||||
m_id3v2Tags.clear();
|
||||
for (const auto offset : m_actualId3v2TagOffsets) {
|
||||
|
@ -415,6 +472,9 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
m_id3v2Tags.emplace_back(id3v2Tag.release());
|
||||
}
|
||||
|
||||
// compute effective size
|
||||
m_effectiveSize = static_cast<std::uint64_t>(effectiveSize - m_containerOffset);
|
||||
|
||||
// check for tags in tracks (FLAC only) or via container object
|
||||
try {
|
||||
if (m_containerFormat == ContainerFormat::Flac) {
|
||||
|
@ -1642,9 +1702,9 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
progress.updateStep(flacStream ? "Updating FLAC tags ..." : "Updating ID3v2 tags ...");
|
||||
|
||||
// prepare ID3v2 tags
|
||||
vector<Id3v2TagMaker> makers;
|
||||
auto makers = std::vector<Id3v2TagMaker>();
|
||||
makers.reserve(m_id3v2Tags.size());
|
||||
std::uint64_t tagsSize = 0;
|
||||
auto tagsSize = std::uint64_t();
|
||||
for (auto &tag : m_id3v2Tags) {
|
||||
try {
|
||||
makers.emplace_back(tag->prepareMaking(diag));
|
||||
|
@ -1654,10 +1714,10 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
}
|
||||
|
||||
// determine stream offset and make track/format specific metadata
|
||||
std::uint32_t streamOffset; // where the actual stream starts
|
||||
stringstream flacMetaData(ios_base::in | ios_base::out | ios_base::binary);
|
||||
flacMetaData.exceptions(ios_base::badbit | ios_base::failbit);
|
||||
std::streamoff startOfLastMetaDataBlock;
|
||||
auto streamOffset = std::uint32_t(); // where the actual stream starts
|
||||
auto flacMetaData = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
|
||||
flacMetaData.exceptions(std::ios_base::badbit | std::ios_base::failbit);
|
||||
auto startOfLastMetaDataBlock = std::streamoff();
|
||||
if (flacStream) {
|
||||
// if it is a raw FLAC stream, make FLAC metadata
|
||||
startOfLastMetaDataBlock = flacStream->makeHeader(flacMetaData, diag);
|
||||
|
@ -1669,8 +1729,8 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
}
|
||||
|
||||
// check whether rewrite is required
|
||||
bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
|
||||
size_t padding = 0;
|
||||
auto rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
|
||||
auto padding = std::size_t();
|
||||
if (!rewriteRequired) {
|
||||
// rewriting is not forced and new tag is not too big for available space
|
||||
// -> calculate new padding
|
||||
|
|
|
@ -64,6 +64,11 @@ enum class MediaFileHandlingFlags : std::uint64_t {
|
|||
ForceIndexPosition = (1 << 3), /**< enforces the index position when applying changes, see remarks of MediaFileInfo::setIndexPosition() */
|
||||
NormalizeKnownTagFieldIds = (1 << 4), /**< normalizes known tag field IDs when parsing to match the tag specification's recommendations */
|
||||
PreserveRawTimingValues = (1 << 8), /**< preverves raw timing values (so far only used when making MP4 tracks) */
|
||||
PreserveMuxingApplication = (1 << 9), /**< preverves the muxing application (so far only used when making Matroska container) */
|
||||
PreserveWritingApplication = (1 << 10), /**< preverves the writing application (so far only used when making Matroska container) */
|
||||
ConvertTotalFields = (1 << 11), /**< ensures fields usually holding PositionInSet values such as KnownField::TrackPosition are actually
|
||||
stored as such (and *not* as two separate fields for the position and total values); currently only relevant for Vorbis Comments
|
||||
\sa VorbisCommentFlags::ConvertTotalFields */
|
||||
};
|
||||
|
||||
} // namespace TagParser
|
||||
|
@ -73,6 +78,8 @@ CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(TagParser, TagParser::MediaFileHandlingFlags)
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
struct MediaFileInfoPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT MediaFileInfo : public BasicFileInfo {
|
||||
public:
|
||||
// constructor, destructor
|
||||
|
@ -103,9 +110,10 @@ public:
|
|||
std::string_view mimeType() const;
|
||||
std::uint64_t containerOffset() const;
|
||||
std::uint64_t paddingSize() const;
|
||||
std::uint64_t effectiveSize() const;
|
||||
AbstractContainer *container() const;
|
||||
ParsingStatus containerParsingStatus() const;
|
||||
// ... the capters
|
||||
// ... the chapters
|
||||
ParsingStatus chaptersParsingStatus() const;
|
||||
std::vector<AbstractChapter *> chapters() const;
|
||||
bool areChaptersSupported() const;
|
||||
|
@ -184,6 +192,8 @@ public:
|
|||
void setIndexPosition(ElementPosition indexPosition);
|
||||
bool forceIndexPosition() const;
|
||||
void setForceIndexPosition(bool forceTagPosition);
|
||||
std::uint64_t maxFullParseSize() const;
|
||||
void setMaxFullParseSize(std::uint64_t maxFullParseSize);
|
||||
|
||||
protected:
|
||||
void invalidated() override;
|
||||
|
@ -199,6 +209,7 @@ private:
|
|||
ContainerFormat m_containerFormat;
|
||||
std::streamoff m_containerOffset;
|
||||
std::uint64_t m_paddingSize;
|
||||
std::uint64_t m_effectiveSize;
|
||||
std::vector<std::streamoff> m_actualId3v2TagOffsets;
|
||||
std::unique_ptr<AbstractContainer> m_container;
|
||||
MediaFileStructureFlags m_fileStructureFlags;
|
||||
|
@ -226,6 +237,8 @@ private:
|
|||
ElementPosition m_tagPosition;
|
||||
ElementPosition m_indexPosition;
|
||||
MediaFileHandlingFlags m_fileHandlingFlags;
|
||||
std::uint64_t m_maxFullParseSize;
|
||||
std::unique_ptr<MediaFileInfoPrivate> m_p;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -293,6 +306,15 @@ inline std::uint64_t MediaFileInfo::paddingSize() const
|
|||
return m_paddingSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the "effective size" of the file if know; otherwise returns 0.
|
||||
* \remarks This is the size of the file minus tags at the beginning and the end.
|
||||
*/
|
||||
inline std::uint64_t MediaFileInfo::effectiveSize() const
|
||||
{
|
||||
return m_effectiveSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether tag information has been parsed yet.
|
||||
*/
|
||||
|
@ -454,7 +476,9 @@ inline const std::string &MediaFileInfo::writingApplication() const
|
|||
|
||||
/*!
|
||||
* \brief Sets the writing application as container-level meta-data. Put the name of your application here.
|
||||
* \remarks Might not be used (depends on the format).
|
||||
* \remarks
|
||||
* - Currently only used when making Matroska files.
|
||||
* - The assigned value is ignored when MediaFileHandlingFlags::PreserveWritingApplication is set.
|
||||
*/
|
||||
inline void MediaFileInfo::setWritingApplication(std::string_view writingApplication)
|
||||
{
|
||||
|
@ -692,6 +716,33 @@ inline void MediaFileInfo::setForceIndexPosition(bool forceIndexPosition)
|
|||
CppUtilities::modFlagEnum(m_fileHandlingFlags, MediaFileHandlingFlags::ForceIndexPosition, forceIndexPosition);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the maximal file size for a "full parse" in byte.
|
||||
* \remarks
|
||||
* So far this is Matroska-specific: The "Tags" element (which holds the tag information) is commonly at the end
|
||||
* of a Matroska file. Hence the parser needs to walk through the entire file to find the tag information if no
|
||||
* "SeekHead" element is present which might causes long loading times. To avoid this a maximal file size for a
|
||||
* "full parse" can be specified. The disadvantage is that the parser relies on the presence of a SeekHead element
|
||||
* on larger files to retrieve tag information.
|
||||
*
|
||||
* The default value is 50 MiB.
|
||||
*
|
||||
* \sa setMaxFullParseSize()
|
||||
*/
|
||||
inline std::uint64_t MediaFileInfo::maxFullParseSize() const
|
||||
{
|
||||
return m_maxFullParseSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the maximal file size for a "full parse" in byte.
|
||||
* \sa maxFullParseSize()
|
||||
*/
|
||||
inline void MediaFileInfo::setMaxFullParseSize(std::uint64_t maxFullParseSize)
|
||||
{
|
||||
m_maxFullParseSize = maxFullParseSize;
|
||||
}
|
||||
|
||||
} // namespace TagParser
|
||||
|
||||
#endif // TAG_PARSER_MEDIAINFO_H
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
|
||||
|
||||
#include "./mp4container.h"
|
||||
#include "./mp4ids.h"
|
||||
|
||||
|
@ -269,7 +267,7 @@ void Mp4Container::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
|
||||
// find relevant atoms in original file
|
||||
Mp4Atom *fileTypeAtom, *progressiveDownloadInfoAtom, *movieAtom, *firstMediaDataAtom, *firstMovieFragmentAtom /*, *userDataAtom*/;
|
||||
Mp4Atom *level0Atom, *level1Atom, *level2Atom, *lastAtomToBeWritten;
|
||||
Mp4Atom *level0Atom, *level1Atom, *level2Atom, *lastAtomToBeWritten = nullptr;
|
||||
try {
|
||||
// file type atom (mandatory)
|
||||
if ((fileTypeAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::FileType, diag))) {
|
||||
|
@ -749,7 +747,7 @@ calculatePadding:
|
|||
|
||||
// increase total chunk count and size
|
||||
totalChunkCount += track->chunkCount();
|
||||
totalMediaDataSize += accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), 0ul);
|
||||
totalMediaDataSize += std::accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), static_cast<std::uint64_t>(0u));
|
||||
}
|
||||
|
||||
// write media data chunk-by-chunk
|
||||
|
|
|
@ -257,7 +257,7 @@ inline Mp4TagField::IdentifierType Mp4TagField::fieldIdFromString(std::string_vi
|
|||
const auto latin1 = CppUtilities::convertUtf8ToLatin1(idString.data(), idString.size());
|
||||
switch (latin1.second) {
|
||||
case 4:
|
||||
return CppUtilities::BE::toUInt32(latin1.first.get());
|
||||
return CppUtilities::BE::toInt<std::uint32_t>(latin1.first.get());
|
||||
default:
|
||||
throw CppUtilities::ConversionException("MP4 ID must be exactly 4 chars");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
|
||||
|
||||
#include "./mp4track.h"
|
||||
#include "./mp4atom.h"
|
||||
#include "./mp4container.h"
|
||||
|
|
|
@ -74,7 +74,7 @@ void MpegAudioFrame::parseHeader(BinaryReader &reader, Diagnostics &diag)
|
|||
m_xingBytesfield = reader.readUInt32BE();
|
||||
}
|
||||
if (isXingTocFieldPresent()) {
|
||||
reader.stream()->seekg(64, ios_base::cur);
|
||||
reader.stream()->seekg(0x64, ios_base::cur);
|
||||
}
|
||||
if (isXingQualityIndicatorFieldPresent()) {
|
||||
m_xingQualityIndicator = reader.readUInt32BE();
|
||||
|
|
|
@ -174,7 +174,7 @@ constexpr XingHeaderFlags MpegAudioFrame::xingHeaderFlags() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingFramefieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasFramesField) == XingHeaderFlags::HasFramesField) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasFramesField) == XingHeaderFlags::HasFramesField);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -182,7 +182,7 @@ constexpr bool MpegAudioFrame::isXingFramefieldPresent() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingBytesfieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasFramesField) == XingHeaderFlags::HasFramesField) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasBytesField) == XingHeaderFlags::HasBytesField);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -190,7 +190,7 @@ constexpr bool MpegAudioFrame::isXingBytesfieldPresent() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingTocFieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasTocField) == XingHeaderFlags::HasTocField) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasTocField) == XingHeaderFlags::HasTocField);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -198,7 +198,7 @@ constexpr bool MpegAudioFrame::isXingTocFieldPresent() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingQualityIndicatorFieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasQualityIndicator) == XingHeaderFlags::HasQualityIndicator) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasQualityIndicator) == XingHeaderFlags::HasQualityIndicator);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "../exceptions.h"
|
||||
#include "../mediaformat.h"
|
||||
|
||||
#include <c++utilities/conversion/stringbuilder.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
using namespace std;
|
||||
|
@ -35,20 +37,13 @@ void MpegAudioFrameStream::internalParseHeader(Diagnostics &diag, AbortableProgr
|
|||
if (!m_istream) {
|
||||
throw NoDataFoundException();
|
||||
}
|
||||
// get size
|
||||
m_istream->seekg(-128, ios_base::end);
|
||||
if (m_reader.readUInt24BE() == 0x544147) {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) - 3u - m_startOffset;
|
||||
} else {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) + 125u - m_startOffset;
|
||||
}
|
||||
m_istream->seekg(static_cast<streamoff>(m_startOffset), ios_base::beg);
|
||||
// parse frames until the first valid, non-empty frame is reached
|
||||
m_istream->seekg(static_cast<std::streamoff>(m_startOffset), ios_base::beg);
|
||||
for (size_t invalidByteskipped = 0; m_frames.size() < 200 && invalidByteskipped <= 0x600u;) {
|
||||
MpegAudioFrame &frame = invalidByteskipped > 0 ? m_frames.back() : m_frames.emplace_back();
|
||||
try {
|
||||
frame.parseHeader(m_reader, diag);
|
||||
} catch (const InvalidDataException &e) {
|
||||
} catch (const InvalidDataException &) {
|
||||
if (++invalidByteskipped > 1) {
|
||||
diag.pop_back();
|
||||
}
|
||||
|
@ -73,18 +68,24 @@ void MpegAudioFrameStream::internalParseHeader(Diagnostics &diag, AbortableProgr
|
|||
const MpegAudioFrame &frame = m_frames.back();
|
||||
addInfo(frame, *this);
|
||||
if (frame.isXingBytesfieldPresent()) {
|
||||
std::uint32_t xingSize = frame.xingBytesfield();
|
||||
if (m_size && xingSize != m_size) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
"Real length of MPEG audio frames is not in accordance with value provided by Xing header. The Xing header value will be used.",
|
||||
context);
|
||||
const auto xingSize = frame.xingBytesfield();
|
||||
if (!m_size) {
|
||||
m_size = xingSize;
|
||||
} else if (xingSize != m_size) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("Real size of MPEG audio frames (", m_size, " byte) is not in accordance with value provided by Xing header (", xingSize,
|
||||
" byte). The real size will be used."),
|
||||
context);
|
||||
}
|
||||
}
|
||||
m_bitrate = frame.isXingFramefieldPresent() ? ((static_cast<double>(m_size) * 8.0)
|
||||
/ (static_cast<double>(frame.xingFrameCount() * frame.sampleCount()) / static_cast<double>(frame.samplingFrequency())) / 1024.0)
|
||||
: frame.bitrate();
|
||||
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bytesPerSecond = static_cast<std::uint32_t>(m_bitrate * 125)));
|
||||
if (frame.isXingFramefieldPresent()) {
|
||||
const auto duration = static_cast<double>(frame.xingFrameCount() * frame.sampleCount()) / static_cast<double>(frame.samplingFrequency());
|
||||
m_bitrate = static_cast<double>(m_size) / duration / 125.0;
|
||||
m_duration = TimeSpan::fromSeconds(duration);
|
||||
} else {
|
||||
m_bitrate = frame.bitrate();
|
||||
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bytesPerSecond = static_cast<std::uint32_t>(m_bitrate * 125)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -280,6 +280,10 @@ void OggContainer::internalParseTags(Diagnostics &diag, AbortableProgressFeedbac
|
|||
{
|
||||
// tracks needs to be parsed before because tags are stored at stream level
|
||||
parseTracks(diag, progress);
|
||||
auto flags = VorbisCommentFlags::None;
|
||||
if (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::ConvertTotalFields) {
|
||||
flags += VorbisCommentFlags::ConvertTotalFields;
|
||||
}
|
||||
for (auto &comment : m_tags) {
|
||||
OggParameter ¶ms = comment->oggParams();
|
||||
m_iterator.setPageIndex(params.firstPageIndex);
|
||||
|
@ -287,16 +291,16 @@ void OggContainer::internalParseTags(Diagnostics &diag, AbortableProgressFeedbac
|
|||
m_iterator.setFilter(m_iterator.currentPage().streamSerialNumber());
|
||||
switch (params.streamFormat) {
|
||||
case GeneralMediaFormat::Vorbis:
|
||||
comment->parse(m_iterator, VorbisCommentFlags::None, diag);
|
||||
comment->parse(m_iterator, flags, diag);
|
||||
break;
|
||||
case GeneralMediaFormat::Opus:
|
||||
// skip header (has already been detected by OggStream)
|
||||
m_iterator.ignore(8);
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
break;
|
||||
case GeneralMediaFormat::Flac:
|
||||
m_iterator.ignore(4);
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
break;
|
||||
default:
|
||||
diag.emplace_back(DiagLevel::Critical, "Stream format not supported.", "parsing tags from OGG streams");
|
||||
|
@ -435,11 +439,11 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
}
|
||||
|
||||
// define misc variables
|
||||
CopyHelper<65307> copyHelper;
|
||||
vector<std::uint64_t> updatedPageOffsets;
|
||||
const OggPage *lastPage = nullptr;
|
||||
std::uint64_t nextPageOffset;
|
||||
unordered_map<std::uint32_t, std::uint32_t> pageSequenceNumberBySerialNo;
|
||||
auto copyHelper = CopyHelper<65307>();
|
||||
auto updatedPageOffsets = std::vector<std::uint64_t>();
|
||||
auto nextPageOffset = std::uint64_t();
|
||||
auto pageSequenceNumberBySerialNo = std::unordered_map<std::uint32_t, std::uint32_t>();
|
||||
|
||||
// iterate through all pages of the original file
|
||||
auto updateTick = 0u;
|
||||
|
|
|
@ -62,7 +62,7 @@ void OggPage::parseHeader(istream &stream, std::uint64_t startOffset, std::int32
|
|||
if (++i < m_segmentCount && entry < 0xFF) {
|
||||
m_segmentSizes.emplace_back(0);
|
||||
} else if (i == m_segmentCount && entry == 0xFF) {
|
||||
m_headerTypeFlag |= 0x80; // FIXME v11: don't abuse header type flags
|
||||
m_lastSegmentUnconcluded = true;
|
||||
}
|
||||
}
|
||||
// check whether the maximum size is exceeded
|
||||
|
|
|
@ -48,6 +48,7 @@ private:
|
|||
std::uint32_t m_sequenceNumber;
|
||||
std::uint32_t m_checksum;
|
||||
std::uint8_t m_segmentCount;
|
||||
bool m_lastSegmentUnconcluded;
|
||||
std::vector<std::uint32_t> m_segmentSizes;
|
||||
};
|
||||
|
||||
|
@ -63,6 +64,7 @@ inline OggPage::OggPage()
|
|||
, m_sequenceNumber(0)
|
||||
, m_checksum(0)
|
||||
, m_segmentCount(0)
|
||||
, m_lastSegmentUnconcluded(false)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,7 @@ inline bool OggPage::isLastPage() const
|
|||
*/
|
||||
inline bool OggPage::isLastSegmentUnconcluded() const
|
||||
{
|
||||
return m_headerTypeFlag & 0x80;
|
||||
return m_lastSegmentUnconcluded;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -25,15 +25,15 @@ void OpusIdentificationHeader::parseHeader(OggIterator &iterator)
|
|||
{
|
||||
char buff[19 - 8];
|
||||
iterator.read(buff, 8);
|
||||
if (BE::toUInt64(buff) != 0x4F70757348656164u) {
|
||||
if (BE::toInt<std::uint64_t>(buff) != 0x4F70757348656164u) {
|
||||
throw InvalidDataException(); // not Opus identification header
|
||||
}
|
||||
iterator.read(buff, sizeof(buff));
|
||||
m_version = static_cast<std::uint8_t>(*(buff));
|
||||
m_channels = static_cast<std::uint8_t>(*(buff + 1));
|
||||
m_preSkip = LE::toUInt16(buff + 2);
|
||||
m_preSkip = LE::toInt<std::uint16_t>(buff + 2);
|
||||
m_sampleRate = LE::toUInt32(buff + 4);
|
||||
m_outputGain = LE::toUInt16(buff + 8);
|
||||
m_outputGain = LE::toInt<std::uint16_t>(buff + 8);
|
||||
m_channelMap = static_cast<std::uint8_t>(*(buff + 10));
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,9 @@ public:
|
|||
PositionInSet(const StringType &numericString);
|
||||
|
||||
constexpr std::int32_t position() const;
|
||||
void setPosition(std::int32_t position);
|
||||
constexpr std::int32_t total() const;
|
||||
void setTotal(std::int32_t total);
|
||||
constexpr bool isNull() const;
|
||||
constexpr bool operator==(const PositionInSet &other) const;
|
||||
|
||||
|
@ -80,6 +82,14 @@ constexpr inline std::int32_t PositionInSet::position() const
|
|||
return m_position;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the element position of the current instance.
|
||||
*/
|
||||
inline void PositionInSet::setPosition(int32_t position)
|
||||
{
|
||||
m_position = position;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the total element count of the current instance.
|
||||
*/
|
||||
|
@ -88,6 +98,14 @@ constexpr inline std::int32_t PositionInSet::total() const
|
|||
return m_total;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the total element count of the current instance.
|
||||
*/
|
||||
inline void PositionInSet::setTotal(int32_t total)
|
||||
{
|
||||
m_total = total;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether both the element position and total element count is 0.
|
||||
*/
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace TagParser {
|
|||
* \brief Holds 64-bit signatures.
|
||||
*/
|
||||
enum Sig64 : std::uint64_t {
|
||||
ApeTag = 0x4150455441474558ul, // APETAGEX
|
||||
Ar = 0x213C617263683E0A,
|
||||
Asf1 = 0x3026B2758E66CF11ul,
|
||||
Asf2 = 0xA6D900AA0062CE6Cul,
|
||||
|
@ -114,18 +115,20 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
// read signature
|
||||
std::uint64_t sig = 0;
|
||||
if (buffer.size() >= 8) {
|
||||
sig = BE::toUInt64(buffer.data());
|
||||
sig = BE::toInt<std::uint64_t>(buffer.data());
|
||||
} else if (buffer.size() >= 4) {
|
||||
sig = BE::toUInt32(buffer.data());
|
||||
sig = BE::toInt<std::uint32_t>(buffer.data());
|
||||
sig <<= 4;
|
||||
} else if (buffer.size() >= 2) {
|
||||
sig = BE::toUInt16(buffer.data());
|
||||
sig = BE::toInt<std::uint16_t>(buffer.data());
|
||||
sig <<= 6;
|
||||
} else {
|
||||
return ContainerFormat::Unknown;
|
||||
}
|
||||
// return corresponding container format
|
||||
switch (sig) { // check 64-bit signatures
|
||||
case ApeTag:
|
||||
return ContainerFormat::ApeTag;
|
||||
case Ar:
|
||||
return ContainerFormat::Ar;
|
||||
case Asf1:
|
||||
|
@ -191,9 +194,9 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
case PhotoshopDocument:
|
||||
return ContainerFormat::PhotoshopDocument;
|
||||
case Riff:
|
||||
if (buffer.size() >= 16 && BE::toUInt64(buffer.data() + 8) == Sig64::RiffAvi) {
|
||||
if (buffer.size() >= 16 && BE::toInt<std::uint64_t>(buffer.data() + 8) == Sig64::RiffAvi) {
|
||||
return ContainerFormat::RiffAvi;
|
||||
} else if (buffer.size() >= 12 && BE::toUInt32(buffer.data() + 8) == RiffWave) {
|
||||
} else if (buffer.size() >= 12 && BE::toInt<std::uint32_t>(buffer.data() + 8) == RiffWave) {
|
||||
return ContainerFormat::RiffWave;
|
||||
} else {
|
||||
return ContainerFormat::Riff;
|
||||
|
@ -226,7 +229,7 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
case Gzip:
|
||||
return ContainerFormat::Gzip;
|
||||
case Id3v2:
|
||||
return ContainerFormat::Id2v2Tag;
|
||||
return ContainerFormat::Id3v2Tag;
|
||||
case Utf8Text:
|
||||
return ContainerFormat::Utf8Text;
|
||||
}
|
||||
|
@ -488,6 +491,10 @@ std::string_view containerFormatName(ContainerFormat containerFormat)
|
|||
return "Audio Interchange File Format";
|
||||
case ContainerFormat::Zstd:
|
||||
return "Zstandard compressed file";
|
||||
case ContainerFormat::Id3v2Tag:
|
||||
return "ID3v2 tag";
|
||||
case ContainerFormat::ApeTag:
|
||||
return "APE tag";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ enum class ContainerFormat : unsigned int {
|
|||
Gif87a, /**< Graphics Interchange Format (1987) */
|
||||
Gif89a, /**< Graphics Interchange Format (1989) */
|
||||
Gzip, /**< gzip compressed file */
|
||||
Id2v2Tag, /**< file holding an ID2v2 tag only */
|
||||
Id3v2Tag, /**< file holding an ID3v2 tag only */
|
||||
Ivf, /**< IVF (simple file format that transports raw VP8/VP9/AV1 data) */
|
||||
JavaClassFile, /**< Java class file */
|
||||
Jpeg, /**< JPEG File Interchange Format */
|
||||
|
@ -67,6 +67,7 @@ enum class ContainerFormat : unsigned int {
|
|||
Zip, /**< ZIP archive */
|
||||
Aiff, /**< Audio Interchange File Format */
|
||||
Zstd, /**< Zstandard-compressed data */
|
||||
ApeTag, /**< APE tag */
|
||||
};
|
||||
|
||||
TAG_PARSER_EXPORT ContainerFormat parseSignature(const char *buffer, std::size_t bufferSize);
|
||||
|
|
3
tag.cpp
3
tag.cpp
|
@ -4,6 +4,9 @@ using namespace std;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The TagPrivate struct contains private fields of the Tag class.
|
||||
struct TagPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::Tag
|
||||
* \brief The Tag class is used to store, read and write tag information.
|
||||
|
|
7
tag.h
7
tag.h
|
@ -8,6 +8,7 @@
|
|||
#include <c++utilities/io/binaryreader.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
|
@ -125,6 +126,7 @@ enum class KnownField : unsigned int {
|
|||
ProductionCopyright, /** production copyright */
|
||||
License, /** license */
|
||||
TermsOfUse, /** terms of use */
|
||||
PublisherWebpage, /** the publisher's official webpage */
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -135,7 +137,7 @@ constexpr KnownField firstKnownField = KnownField::Title;
|
|||
/*!
|
||||
* \brief The last valid entry in the TagParser::KnownField enum.
|
||||
*/
|
||||
constexpr KnownField lastKnownField = KnownField::TermsOfUse;
|
||||
constexpr KnownField lastKnownField = KnownField::PublisherWebpage;
|
||||
|
||||
/*!
|
||||
* \brief The number of valid entries in the TagParser::KnownField enum.
|
||||
|
@ -160,6 +162,8 @@ constexpr KnownField nextKnownField(KnownField field)
|
|||
return isKnownFieldDeprecated(next) ? nextKnownField(next) : next;
|
||||
}
|
||||
|
||||
struct TagPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT Tag {
|
||||
public:
|
||||
virtual ~Tag();
|
||||
|
@ -199,6 +203,7 @@ protected:
|
|||
|
||||
std::string m_version;
|
||||
std::uint64_t m_size;
|
||||
std::unique_ptr<TagPrivate> m_p;
|
||||
TagTarget m_target;
|
||||
};
|
||||
|
||||
|
|
147
tagvalue.cpp
147
tagvalue.cpp
|
@ -22,6 +22,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The TagValuePrivate struct contains private fields of the TagValue class.
|
||||
struct TagValuePrivate {};
|
||||
|
||||
/*!
|
||||
* \brief Returns the string representation of the specified \a dataType.
|
||||
*/
|
||||
|
@ -98,7 +101,7 @@ pair<const char *, float> encodingParameter(TagTextEncoding tagTextEncoding)
|
|||
*
|
||||
* Values of the type TagDataType::Text can be differently encoded.
|
||||
* - See TagParser::TagTextEncoding for a list of encodings supported by this library.
|
||||
* - Tag formats usually only support a subset of these encodings. The serializers for the varoius tag
|
||||
* - Tag formats usually only support a subset of these encodings. The serializers for the various tag
|
||||
* formats provided by this library will keep the encoding if possible and otherwise convert the assigned
|
||||
* text to an encoding supported by the tag format on the fly. Note that ID3v1 does not specify which
|
||||
* encodings are supported (or unsupported) so the serializer will just write text data as-is.
|
||||
|
@ -145,6 +148,140 @@ TagValue::TagValue(const TagValue &other)
|
|||
}
|
||||
}
|
||||
|
||||
TagValue::TagValue(TagValue &&other) = default;
|
||||
|
||||
/*!
|
||||
* \brief Constructs an empty TagValue.
|
||||
*/
|
||||
TagValue::TagValue()
|
||||
: m_size(0)
|
||||
, m_type(TagDataType::Undefined)
|
||||
, m_encoding(TagTextEncoding::Latin1)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textSize Specifies the size of \a text. (The actual number of bytes, not the number of characters.)
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textSize, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned. This string must be null-terminated.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(const char *text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
{
|
||||
assignText(text, std::strlen(text), textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(const std::string &text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(std::string_view text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys the TagValue.
|
||||
*/
|
||||
TagValue::~TagValue()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue with a copy of the given \a data.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Strips the BOM of the specified \a data if \a type is TagDataType::Text.
|
||||
*/
|
||||
TagValue::TagValue(const char *data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
if (type == TagDataType::Text) {
|
||||
stripBom(data, m_size, encoding);
|
||||
}
|
||||
m_ptr = std::make_unique<char[]>(m_size);
|
||||
std::copy(data, data + m_size, m_ptr.get());
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding with the given \a data.
|
||||
*
|
||||
* The \a data is not copied. It is moved.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Does not strip the BOM so for consistency the caller must ensure there is no BOM present.
|
||||
*/
|
||||
TagValue::TagValue(std::unique_ptr<char[]> &&data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
m_ptr = std::move(data);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Assigns the value of another TagValue to the current instance.
|
||||
*/
|
||||
|
@ -170,6 +307,8 @@ TagValue &TagValue::operator=(const TagValue &other)
|
|||
return *this;
|
||||
}
|
||||
|
||||
TagValue &TagValue::operator=(TagValue &&other) = default;
|
||||
|
||||
/// \cond
|
||||
TagTextEncoding pickUtfEncoding(TagTextEncoding encoding1, TagTextEncoding encoding2)
|
||||
{
|
||||
|
@ -604,7 +743,7 @@ TimeSpan TagValue::toTimeSpan() const
|
|||
switch (m_size) {
|
||||
case sizeof(std::uint64_t): {
|
||||
const auto ticks = *(reinterpret_cast<std::uint64_t *>(m_ptr.get()));
|
||||
if (ticks < std::numeric_limits<std::int64_t>::max()) {
|
||||
if (ticks < static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
|
||||
return TimeSpan(static_cast<std::int64_t>(ticks));
|
||||
}
|
||||
}
|
||||
|
@ -1221,13 +1360,13 @@ void TagValue::stripBom(const char *&text, size_t &length, TagTextEncoding encod
|
|||
}
|
||||
break;
|
||||
case TagTextEncoding::Utf16LittleEndian:
|
||||
if ((length >= 2) && (LE::toUInt16(text) == 0xFEFF)) {
|
||||
if ((length >= 2) && (LE::toInt<std::uint16_t>(text) == 0xFEFF)) {
|
||||
text += 2;
|
||||
length -= 2;
|
||||
}
|
||||
break;
|
||||
case TagTextEncoding::Utf16BigEndian:
|
||||
if ((length >= 2) && (BE::toUInt16(text) == 0xFEFF)) {
|
||||
if ((length >= 2) && (BE::toInt<std::uint16_t>(text) == 0xFEFF)) {
|
||||
text += 2;
|
||||
length -= 2;
|
||||
}
|
||||
|
|
141
tagvalue.h
141
tagvalue.h
|
@ -131,6 +131,8 @@ enum class TagDataType : unsigned int {
|
|||
DateTimeExpression, /**< date time expression, see CppUtilities::DateTimeExpression */
|
||||
};
|
||||
|
||||
TAG_PARSER_EXPORT std::string_view tagDataTypeString(TagDataType dataType);
|
||||
|
||||
/*!
|
||||
* \brief The TagValueComparisionOption enum specifies options for TagValue::compareTo().
|
||||
*/
|
||||
|
@ -140,6 +142,8 @@ enum class TagValueComparisionFlags : unsigned int {
|
|||
IgnoreMetaData = 0x2, /**< do *not* take meta-data like description and MIME-types into account */
|
||||
};
|
||||
|
||||
struct TagValuePrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT TagValue {
|
||||
public:
|
||||
// constructor, destructor
|
||||
|
@ -164,12 +168,12 @@ public:
|
|||
explicit TagValue(CppUtilities::TimeSpan value);
|
||||
explicit TagValue(const Popularity &value);
|
||||
TagValue(const TagValue &other);
|
||||
TagValue(TagValue &&other) = default;
|
||||
TagValue(TagValue &&other);
|
||||
~TagValue();
|
||||
|
||||
// operators
|
||||
TagValue &operator=(const TagValue &other);
|
||||
TagValue &operator=(TagValue &&other) = default;
|
||||
TagValue &operator=(TagValue &&other);
|
||||
bool operator==(const TagValue &other) const;
|
||||
bool operator!=(const TagValue &other) const;
|
||||
operator bool() const;
|
||||
|
@ -260,90 +264,9 @@ private:
|
|||
TagTextEncoding m_encoding;
|
||||
TagTextEncoding m_descEncoding;
|
||||
TagValueFlags m_flags;
|
||||
std::unique_ptr<TagValuePrivate> m_p;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Constructs an empty TagValue.
|
||||
*/
|
||||
inline TagValue::TagValue()
|
||||
: m_size(0)
|
||||
, m_type(TagDataType::Undefined)
|
||||
, m_encoding(TagTextEncoding::Latin1)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys the TagValue.
|
||||
*/
|
||||
inline TagValue::~TagValue()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textSize Specifies the size of \a text. (The actual number of bytes, not the number of characters.)
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textSize, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned. This string must be null-terminated.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(const char *text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
{
|
||||
assignText(text, std::strlen(text), textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(const std::string &text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(std::string_view text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding the given integer \a value.
|
||||
*/
|
||||
|
@ -360,56 +283,6 @@ inline TagParser::TagValue::TagValue(std::uint64_t value)
|
|||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue with a copy of the given \a data.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Strips the BOM of the specified \a data if \a type is TagDataType::Text.
|
||||
*/
|
||||
inline TagValue::TagValue(const char *data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
if (type == TagDataType::Text) {
|
||||
stripBom(data, m_size, encoding);
|
||||
}
|
||||
m_ptr = std::make_unique<char[]>(m_size);
|
||||
std::copy(data, data + m_size, m_ptr.get());
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding with the given \a data.
|
||||
*
|
||||
* The \a data is not copied. It is moved.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Does not strip the BOM so for consistency the caller must ensure there is no BOM present.
|
||||
*/
|
||||
inline TagValue::TagValue(std::unique_ptr<char[]> &&data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
m_ptr = std::move(data);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given PositionInSet \a value.
|
||||
*/
|
||||
|
|
|
@ -50,6 +50,7 @@ class OverallTests : public TestFixture {
|
|||
CPPUNIT_TEST(testFlacMaking);
|
||||
CPPUNIT_TEST(testMkvMakingWithDifferentSettings);
|
||||
CPPUNIT_TEST(testMkvMakingNestedTags);
|
||||
CPPUNIT_TEST(testVorbisCommentFieldHandling);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
|
@ -122,6 +123,7 @@ public:
|
|||
void testMp3Making();
|
||||
void testOggMaking();
|
||||
void testFlacMaking();
|
||||
void testVorbisCommentFieldHandling();
|
||||
|
||||
private:
|
||||
MediaFileInfo m_fileInfo;
|
||||
|
|
|
@ -35,7 +35,7 @@ void OverallTests::checkFlacTestfile1()
|
|||
CPPUNIT_ASSERT_EQUAL("1998"s, tags.front()->value(KnownField::RecordDate).toString());
|
||||
CPPUNIT_ASSERT(tags.front()->value(KnownField::Comment).isEmpty());
|
||||
//CPPUNIT_ASSERT(tags.front()->value(KnownField::Cover).dataSize() == 0x58f3);
|
||||
//CPPUNIT_ASSERT(BE::toUInt64(tags.front()->value(KnownField::Cover).dataPointer()) == 0xFFD8FFE000104A46);
|
||||
//CPPUNIT_ASSERT(BE::toInt<std::uint64_t>(tags.front()->value(KnownField::Cover).dataPointer()) == 0xFFD8FFE000104A46);
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(3, 4), tags.front()->value(KnownField::TrackPosition).toPositionInSet());
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(1, 1), tags.front()->value(KnownField::DiskPosition).toPositionInSet());
|
||||
break;
|
||||
|
|
|
@ -37,7 +37,7 @@ enum TestFlag {
|
|||
void OverallTests::checkMkvTestfile1()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1) + TimeSpan::fromSeconds(27) + TimeSpan::fromMilliseconds(336), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1.0) + TimeSpan::fromSeconds(27.0) + TimeSpan::fromMilliseconds(336.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -86,7 +86,7 @@ void OverallTests::checkMkvTestfile1()
|
|||
void OverallTests::checkMkvTestfile2()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47) + TimeSpan::fromMilliseconds(509), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47.0) + TimeSpan::fromMilliseconds(509.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -135,7 +135,7 @@ void OverallTests::checkMkvTestfile2()
|
|||
void OverallTests::checkMkvTestfile3()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(49) + TimeSpan::fromMilliseconds(64), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(49.0) + TimeSpan::fromMilliseconds(64.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -244,7 +244,7 @@ void OverallTests::checkMkvTestfile4()
|
|||
void OverallTests::checkMkvTestfile5()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(46) + TimeSpan::fromMilliseconds(665), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(46.0) + TimeSpan::fromMilliseconds(665.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(11_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -298,7 +298,7 @@ void OverallTests::checkMkvTestfile5()
|
|||
void OverallTests::checkMkvTestfile6()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1) + TimeSpan::fromSeconds(27) + TimeSpan::fromMilliseconds(336), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1.0) + TimeSpan::fromSeconds(27.0) + TimeSpan::fromMilliseconds(336.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -348,7 +348,7 @@ void OverallTests::checkMkvTestfile6()
|
|||
void OverallTests::checkMkvTestfile7()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(37) + TimeSpan::fromMilliseconds(43), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(37.0) + TimeSpan::fromMilliseconds(43.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -408,7 +408,7 @@ void OverallTests::checkMkvTestfile7()
|
|||
void OverallTests::checkMkvTestfile8()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47) + TimeSpan::fromMilliseconds(341), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47.0) + TimeSpan::fromMilliseconds(341.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -459,7 +459,7 @@ void OverallTests::checkMkvTestfile8()
|
|||
void OverallTests::checkMkvTestfileHandbrakeChapters()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(27) + TimeSpan::fromMilliseconds(569), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(27.0) + TimeSpan::fromMilliseconds(569.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
|
|
@ -209,7 +209,7 @@ void OverallTests::checkMp4Testfile4()
|
|||
CPPUNIT_ASSERT_EQUAL("1998"s, tags.front()->value(KnownField::RecordDate).toString());
|
||||
CPPUNIT_ASSERT(tags.front()->value(KnownField::Comment).isEmpty());
|
||||
CPPUNIT_ASSERT_EQUAL(0x58f3_st, tags.front()->value(KnownField::Cover).dataSize());
|
||||
CPPUNIT_ASSERT_EQUAL(0xFFD8FFE000104A46ul, BE::toUInt64(tags.front()->value(KnownField::Cover).dataPointer()));
|
||||
CPPUNIT_ASSERT_EQUAL(0xFFD8FFE000104A46ul, BE::toInt<std::uint64_t>(tags.front()->value(KnownField::Cover).dataPointer()));
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(3, 4), tags.front()->value(KnownField::TrackPosition).toPositionInSet());
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(1, 1), tags.front()->value(KnownField::DiskPosition).toPositionInSet());
|
||||
break;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include "../abstracttrack.h"
|
||||
#include "../tag.h"
|
||||
#include "../vorbis/vorbiscomment.h"
|
||||
#include "../vorbis/vorbiscommentfield.h"
|
||||
#include "../vorbis/vorbiscommentids.h"
|
||||
|
||||
#include <c++utilities/io/misc.h>
|
||||
|
||||
|
@ -250,3 +252,52 @@ void OverallTests::testOggMaking()
|
|||
makeFile(workingCopyPath("ogg/noise-without-cover.opus"), modifyRoutineCover, &OverallTests::checkOggTestfile3);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests the Vorbis Comment specifc handling of certain fields done in VorbisComment::convertTotalFields().
|
||||
*/
|
||||
void OverallTests::testVorbisCommentFieldHandling()
|
||||
{
|
||||
const auto context = std::string();
|
||||
const auto trackNumberFieldId = std::string(VorbisCommentIds::trackNumber());
|
||||
const auto trackTotalFieldId = std::string(VorbisCommentIds::trackTotal());
|
||||
const auto diskNumberFieldId = std::string(VorbisCommentIds::diskNumber());
|
||||
const auto diskTotalFieldId = std::string(VorbisCommentIds::diskTotal());
|
||||
|
||||
auto diag = Diagnostics();
|
||||
auto vc = VorbisComment();
|
||||
auto trackNumber = VorbisCommentField(trackNumberFieldId, TagValue(5));
|
||||
auto trackTotal = VorbisCommentField(trackTotalFieldId, TagValue(20));
|
||||
auto &fields = vc.fields();
|
||||
fields.insert(std::make_pair(trackNumberFieldId, std::move(trackNumber)));
|
||||
fields.insert(std::make_pair(trackTotalFieldId, std::move(trackTotal)));
|
||||
vc.convertTotalFields(context, diag);
|
||||
|
||||
const auto convertedValues = vc.values(trackNumberFieldId);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("the two fileds have been combined into one", 1_st, fields.size());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("there is exactly one track number value", 1_st, convertedValues.size());
|
||||
const auto convertedTrackNumber = convertedValues.front()->toPositionInSet();
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(5, 20), convertedTrackNumber);
|
||||
CPPUNIT_ASSERT_EQUAL(0_st, diag.size());
|
||||
|
||||
auto diskNumber = VorbisCommentField(diskNumberFieldId, TagValue("invalid pos"));
|
||||
auto diskTotal = VorbisCommentField(diskTotalFieldId, TagValue("invalid total"));
|
||||
auto diskTotal2 = VorbisCommentField(diskTotalFieldId, TagValue(42));
|
||||
fields.insert(std::make_pair(diskNumberFieldId, std::move(diskNumber)));
|
||||
fields.insert(std::make_pair(diskTotalFieldId, std::move(diskTotal)));
|
||||
fields.insert(std::make_pair(diskTotalFieldId, std::move(diskTotal2)));
|
||||
vc.convertTotalFields(context, diag);
|
||||
|
||||
const auto newDiskNumberValues = vc.values(diskNumberFieldId);
|
||||
const auto newDiskTotalValues = vc.values(diskTotalFieldId);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("invalid fields have not been combined", 4_st, fields.size());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("invalid disk position has been preserved and valid disk total converted", 2_st, newDiskNumberValues.size());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("invalid disk total has been preserved", 1_st, newDiskTotalValues.size());
|
||||
const auto preservedDiskNumber = newDiskNumberValues[0]->toString();
|
||||
const auto convertedDiskTotal = newDiskNumberValues[1]->toPositionInSet();
|
||||
const auto preservedDiskTotal = newDiskTotalValues[0]->toString();
|
||||
CPPUNIT_ASSERT_EQUAL("invalid pos"s, preservedDiskNumber);
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(0, 42), convertedDiskTotal);
|
||||
CPPUNIT_ASSERT_EQUAL("invalid total"s, preservedDiskTotal);
|
||||
CPPUNIT_ASSERT_EQUAL(3_st, diag.size());
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ void TagValueTests::testPositionInSet()
|
|||
|
||||
void TagValueTests::testTimeSpan()
|
||||
{
|
||||
const TimeSpan fiveMinutes(TimeSpan::fromMinutes(5));
|
||||
const TimeSpan fiveMinutes(TimeSpan::fromMinutes(5.0));
|
||||
TagValue timeSpan;
|
||||
timeSpan.assignTimeSpan(fiveMinutes);
|
||||
CPPUNIT_ASSERT_EQUAL(timeSpan, TagValue(timeSpan));
|
||||
|
|
|
@ -82,6 +82,7 @@ struct TestFile {
|
|||
{ "ogg/example-cover.png", { "897e1a2d0cfb79c1fe5068108bb34610c3758bd0b9a7e90c1702c4e6972e0801" } },
|
||||
};
|
||||
|
||||
/// \cond
|
||||
struct EvpMdCtx {
|
||||
EvpMdCtx()
|
||||
: handle(EVP_MD_CTX_new())
|
||||
|
@ -95,6 +96,7 @@ struct EvpMdCtx {
|
|||
}
|
||||
EVP_MD_CTX *handle;
|
||||
};
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Computes the SHA-256 checksums for the file using OpenSSL.
|
||||
|
|
|
@ -107,6 +107,10 @@ VorbisComment::IdentifierType VorbisComment::internallyGetFieldId(KnownField fie
|
|||
return std::string(rating());
|
||||
case KnownField::Bpm:
|
||||
return std::string(bpm());
|
||||
case KnownField::Publisher:
|
||||
return std::string(publisher());
|
||||
case KnownField::PublisherWebpage:
|
||||
return std::string(publisherWebpage());
|
||||
default:
|
||||
return std::string();
|
||||
}
|
||||
|
@ -146,12 +150,75 @@ KnownField VorbisComment::internallyGetKnownField(const IdentifierType &id) cons
|
|||
{ isrc(), KnownField::ISRC },
|
||||
{ rating(), KnownField::Rating },
|
||||
{ bpm(), KnownField::Bpm },
|
||||
{ publisher(), KnownField::Publisher },
|
||||
{ publisherWebpage(), KnownField::PublisherWebpage },
|
||||
});
|
||||
// clang-format on
|
||||
const auto knownField(fieldMap.find(id));
|
||||
return knownField != fieldMap.cend() ? knownField->second : KnownField::Invalid;
|
||||
}
|
||||
|
||||
/// \cond
|
||||
void VorbisComment::extendPositionInSetField(std::string_view field, std::string_view totalField, const std::string &diagContext, Diagnostics &diag)
|
||||
{
|
||||
auto totalValues = std::vector<std::int32_t>();
|
||||
auto fieldsIter = fields().equal_range(std::string(totalField));
|
||||
auto fieldsDist = std::distance(fieldsIter.first, fieldsIter.second);
|
||||
if (!fieldsDist) {
|
||||
return;
|
||||
}
|
||||
totalValues.reserve(static_cast<std::size_t>(fieldsDist));
|
||||
for (; fieldsIter.first != fieldsIter.second;) {
|
||||
try {
|
||||
totalValues.emplace_back(fieldsIter.first->second.value().toInteger());
|
||||
fields().erase(fieldsIter.first++);
|
||||
} catch (const ConversionException &e) {
|
||||
diag.emplace_back(DiagLevel::Warning, argsToString("Unable to parse \"", totalField, "\" as integer: ", e.what()), diagContext);
|
||||
totalValues.emplace_back(0);
|
||||
++fieldsIter.first;
|
||||
}
|
||||
}
|
||||
|
||||
auto totalIter = totalValues.begin(), totalEnd = totalValues.end();
|
||||
for (fieldsIter = fields().equal_range(std::string(field)); fieldsIter.first != fieldsIter.second && totalIter != totalEnd;
|
||||
++fieldsIter.first, ++totalIter) {
|
||||
auto &v = fieldsIter.first->second.value();
|
||||
try {
|
||||
auto p = v.toPositionInSet();
|
||||
if (p.total() && p.total() != *totalIter) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("The \"", totalField, "\" field value (", *totalIter, ") does not match \"", field, "\" field value (", p.total(),
|
||||
"). Discarding the former in favor of the latter."),
|
||||
diagContext);
|
||||
} else {
|
||||
p.setTotal(*totalIter);
|
||||
v.assignPosition(p);
|
||||
}
|
||||
} catch (const ConversionException &e) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("Unable to parse \"", field, "\" as position in set for incorporating \"", totalField, "\": ", e.what()), diagContext);
|
||||
}
|
||||
}
|
||||
if (totalIter != totalEnd) {
|
||||
diag.emplace_back(
|
||||
DiagLevel::Warning, argsToString("Vorbis Comment contains more \"", totalField, "\" fields than \"", field, "\" fields."), diagContext);
|
||||
}
|
||||
for (; totalIter != totalEnd; ++totalIter) {
|
||||
fields().insert(std::make_pair(field, VorbisCommentField(std::string(field), TagValue(PositionInSet(0, *totalIter)))));
|
||||
}
|
||||
}
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Converts TRACKTOTAL/DISCTOTAL/PARTTOTAL to be included in the TRACKNUMBER/DISCNUMBER/PARTNUMBER fields instead.
|
||||
*/
|
||||
void VorbisComment::convertTotalFields(const std::string &diagContext, Diagnostics &diag)
|
||||
{
|
||||
extendPositionInSetField(VorbisCommentIds::trackNumber(), VorbisCommentIds::trackTotal(), diagContext, diag);
|
||||
extendPositionInSetField(VorbisCommentIds::diskNumber(), VorbisCommentIds::diskTotal(), diagContext, diag);
|
||||
extendPositionInSetField(VorbisCommentIds::partNumber(), VorbisCommentIds::partTotal(), diagContext, diag);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Internal implementation for parsing.
|
||||
*/
|
||||
|
@ -167,7 +234,7 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
|
|||
if (!skipSignature) {
|
||||
CHECK_MAX_SIZE(7)
|
||||
stream.read(sig, 7);
|
||||
skipSignature = (BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
|
||||
skipSignature = (BE::toInt<std::uint64_t>(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
|
||||
}
|
||||
if (skipSignature) {
|
||||
// read vendor (length prefixed string)
|
||||
|
@ -243,6 +310,10 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
|
|||
diag.emplace_back(DiagLevel::Warning, argsToString(bytesRemaining, " bytes left in last segment."), context);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & VorbisCommentFlags::ConvertTotalFields) {
|
||||
convertTotalFields(context, diag);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
#include "../fieldbasedtag.h"
|
||||
#include "../mediaformat.h"
|
||||
|
||||
class OverallTests;
|
||||
|
||||
namespace TagParser {
|
||||
|
||||
class OggIterator;
|
||||
|
@ -24,6 +26,7 @@ public:
|
|||
|
||||
class TAG_PARSER_EXPORT VorbisComment : public FieldMapBasedTag<VorbisComment> {
|
||||
friend class FieldMapBasedTag<VorbisComment>;
|
||||
friend class ::OverallTests;
|
||||
|
||||
public:
|
||||
VorbisComment();
|
||||
|
@ -52,6 +55,8 @@ protected:
|
|||
|
||||
private:
|
||||
template <class StreamType> void internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag);
|
||||
void extendPositionInSetField(std::string_view field, std::string_view totalField, const std::string &diagContext, Diagnostics &diag);
|
||||
void convertTotalFields(const std::string &diagContext, Diagnostics &diag);
|
||||
|
||||
private:
|
||||
TagValue m_vendor;
|
||||
|
|
|
@ -19,7 +19,8 @@ enum class VorbisCommentFlags : std::uint8_t {
|
|||
None = 0x0, /**< Regular parsing/making. */
|
||||
NoSignature = 0x1, /**< Skips the signature when parsing and making. */
|
||||
NoFramingByte = 0x2, /**< Doesn't expect the framing bit to be present when parsing; does not make the framing bit when making. */
|
||||
NoCovers = 0x4 /**< Skips all covers when making. */
|
||||
NoCovers = 0x4, /**< Skips all covers when making. */
|
||||
ConvertTotalFields = 0x8, /**< Converts TRACKTOTAL/DISCTOTAL/PARTTOTAL to be included in the TRACKNUMBER/DISCNUMBER/PARTNUMBER fields. */
|
||||
};
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -9,7 +9,12 @@ namespace TagParser {
|
|||
|
||||
/*!
|
||||
* \brief Encapsulates Vorbis comment field names.
|
||||
* \sa See https://xiph.org/vorbis/doc/v-comment.html for the upstream documentation of the field names.
|
||||
* \sa
|
||||
* - See https://xiph.org/vorbis/doc/v-comment.html for the upstream documentation of the field names.
|
||||
* - See https://wiki.xiph.org/Field_names for an additional proposal that is most notably introducing
|
||||
* `DISCNUMBER` and `…TOTAL` fields.
|
||||
* - See https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping for further conventions and a
|
||||
* comparision with other formats.
|
||||
*/
|
||||
namespace VorbisCommentIds {
|
||||
|
||||
|
@ -17,10 +22,18 @@ constexpr TAG_PARSER_EXPORT std::string_view trackNumber()
|
|||
{
|
||||
return "TRACKNUMBER";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view trackTotal()
|
||||
{
|
||||
return "TRACKTOTAL";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view diskNumber()
|
||||
{
|
||||
return "DISCNUMBER";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view diskTotal()
|
||||
{
|
||||
return "DISCTOTAL";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view part()
|
||||
{
|
||||
return "PART";
|
||||
|
@ -29,6 +42,10 @@ constexpr TAG_PARSER_EXPORT std::string_view partNumber()
|
|||
{
|
||||
return "PARTNUMBER";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view partTotal()
|
||||
{
|
||||
return "PARTTOTAL";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view title()
|
||||
{
|
||||
return "TITLE";
|
||||
|
@ -189,6 +206,10 @@ constexpr TAG_PARSER_EXPORT std::string_view bpm()
|
|||
{
|
||||
return "BPM";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view publisherWebpage()
|
||||
{
|
||||
return "WWWPUBLISHER";
|
||||
}
|
||||
|
||||
} // namespace VorbisCommentIds
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ void VorbisIdentificationHeader::parseHeader(OggIterator &iterator)
|
|||
{
|
||||
char buff[30 - 7];
|
||||
iterator.read(buff, 7);
|
||||
if ((BE::toUInt64(buff) & 0xffffffffffffff00u) != 0x01766F7262697300u) {
|
||||
if ((BE::toInt<std::uint64_t>(buff) & 0xffffffffffffff00u) != 0x01766F7262697300u) {
|
||||
throw InvalidDataException(); // not Vorbis identification header
|
||||
}
|
||||
iterator.read(buff, sizeof(buff));
|
||||
|
|
Loading…
Reference in New Issue