Compare commits

...

47 Commits

Author SHA1 Message Date
Martchus acfb9ef219 Handle TRACKTOTAL/DISCTOTAL/PARTTOTAL fields in Vorbis Comments
* Move those fields into their corresponding
  TRACKNUMBER/DISCNUMBER/PARTNUMBER fields after parsing so they are
  accessible via just one field as PositionInSet which is in line with
  other tag formats and also how other software like VLC expect the total
  to be specified
* NOT implemented yet: Move those fields optionally back into separate
  fields when serializing
2024-02-28 21:36:06 +01:00
Martchus 351e953b83 Avoid empty documentation entry for `EvpMdCtx` 2024-02-28 21:36:06 +01:00
Martchus 8204b2dfde Apply change of `global.h` template 2024-02-27 01:57:17 +01:00
Martchus 8246d30ec5 Update `global.h` via updated template in c++utilities 2024-01-30 23:32:35 +01:00
Martchus d48722f26c Fix unity builds
* Define `CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS` consistently
  with all necassary changes
* Avoid ambiguity between enum members and certain class/struct names
2024-01-30 23:31:15 +01:00
Martchus 45ab6b17b9 Improve various details of documentation 2024-01-23 00:29:11 +01:00
Martchus 6fb34ec3b3 Update copyright date 2024-01-23 00:28:19 +01:00
Martchus 33327390e9 Add Vorbis comment IDs for total number of tracks/disks/parts 2023-12-29 17:12:20 +01:00
Martchus 7f3d4c5751 Add ID3v2 frame definitions for publisher web page and user defined URLs 2023-12-29 17:07:14 +01:00
Martchus b1bca85ef4 Support publisher webpage in Vorbis comments 2023-12-29 16:50:41 +01:00
Martchus 0f669c88a7 Bump minor version 2023-12-29 16:50:19 +01:00
Martchus 1df871870b Map the publisher field for Vorbis comments 2023-12-29 15:57:51 +01:00
Martchus 909a3ee98a Improve comments in Matroska container code 2023-10-31 21:27:17 +01:00
Martchus 5aef3f84ee Bump patch version 2023-10-31 20:07:02 +01:00
Martchus 56ccd3da80 Use auto-syntax in places touched by previous commit consistently 2023-10-31 20:06:38 +01:00
Martchus 9f41c30443 Silence/fix GCC's maybe-uninitialized warnings
* The warning about `bsEnvCount` is actually correct.
* The warning about `lastAtomToBeWritten` might be correct.
* The warning about `relPos` is definitely unjustified because `relPos` is
  only used when `cueRelativePositionElement` is not `nullptr` and `relPos`
  is initialized in that case.
* The warnings about `pos`, `nextPageOffset` and `startOfLastMetaDataBlock`
  are also wrong for similar reasons.
2023-10-31 20:05:13 +01:00
Martchus ef0ab3d8c3 Avoid GCC's stringop-truncation warning
Not copying the termination character here is wanted. Just use
`std::memcpy` to avoid it as the special behavior of `std::strncpy` is not
needed here anyways.
2023-10-31 19:36:12 +01:00
Martchus 0827002183 Fix duration and bitrate calculation of MP3 files via XING header
* Calculate the duration independently of the bitrate which is supposedly
  more accurate
* Fix conversion factor for computing bitrate
* Use real size if it differs from size specified in Xing header; this way
  the bitrate is consistent with MediaInfo and other tools (there is
  possibly still a bug in the way the size is read from the Xing header,
  though)
2023-08-21 12:09:07 +02:00
Martchus f2e97b9899 Fix skipping TOC field of Xing header 2023-08-19 01:09:22 +02:00
Martchus db5e1f2c8c Fix check for presence of frame field in Xing header 2023-08-19 00:44:43 +02:00
Martchus 04795f957f Fix setting writing app name after allowing to preserve it 2023-08-09 23:26:34 +02:00
Martchus c0e9f9bf83 Fix condition for preserving muxing/writing app 2023-08-09 23:17:26 +02:00
Martchus e6bb98d6e6 Improve dealing with muxing/writing application of Matroska files
* Allow reading the current muxing/writing application
* Allow to preserve the original muxing/writing application instead of
  always overriding
2023-08-08 17:18:02 +02:00
Martchus f5497fb300 Allow setting position/total of PositionInSet 2023-08-01 00:24:28 +02:00
Martchus f7941d442f Fix typo 2023-07-30 14:40:15 +02:00
Martchus 03f9698269 Expose tagDataTypeString() 2023-07-29 16:02:14 +02:00
Martchus 8a6cffad95 Bump minor version 2023-07-29 16:02:14 +02:00
Martchus 54a87cd32c Avoid CMake deprecation warning by bumping version 2023-07-23 20:59:19 +02:00
Martchus 92345027fb Use generic `toInt()` function which relies less on compiler optimizations 2023-05-18 00:52:28 +02:00
Martchus 90cace1e95 Fix computation of effective size when APE tag is present at end of file 2023-05-18 00:41:35 +02:00
Martchus 6eab8b8718 Log the mismatching sizes when XING header mismatches real size 2023-05-18 00:37:59 +02:00
Martchus a5ab3ed1b2 Avoid abusing OggPage header flags to store whether last segment unconcluded 2023-05-16 23:18:27 +02:00
Martchus 5745632af7 Move `maxFullParseSize()` to `MediaFileInfo` as non-static member 2023-05-16 23:11:53 +02:00
Martchus 6f321b7b00 Fix typo in ContainerFormat enum 2023-05-16 23:00:32 +02:00
Martchus 60385aa347 Generalize copy functions in GenericFileElement to eventually use sendfile64()
Since we're still using `container().stream()` in `copyInternal()` this change
is still not really effective.
2023-05-16 22:58:04 +02:00
Martchus c5cd20682d Determine "effective size" via file info instead of track implementations
This allows removing duplicated code from the track implementations to take
APE tags into account.
2023-05-16 22:39:26 +02:00
Martchus 8ad28f857b Allow extending important classes without ABI break
This allows to make ABI breaks less often while still
being able to extend many aspects of the library.
2023-05-16 22:20:48 +02:00
Martchus 6ed968f5e6 Update major version to 12 2023-05-16 22:00:35 +02:00
Martchus a167e0702e Ignore iso_639-2.json 2023-05-11 22:59:09 +02:00
Martchus 405631625f Fix typo 2023-05-08 11:15:53 +02:00
Martchus 6fb16d72eb Avoid unqualified call to `std::strncpy()` 2023-05-07 21:45:47 +02:00
Martchus 111d6190cb Avoid some of the warnings from MSVC 2023-05-07 21:45:15 +02:00
Martchus 0a2b948f26 Detect APE tags, emit according messages and update README
It is likely not worth adding support for APE tags at this point.
However, it still makes sense to acknowledge the presence of them
despite not being actually supported to avoid possible confusion.

With this change, APE tags at the beginning of the file will be
dropped when applying changes as the container offset is updated
when skipping the tag. I suppose that makes sense considering
putting those tags at the beginning is not recommended anyways.
The diag messages and README have been updated accordingly.
2023-05-06 13:04:01 +02:00
Martchus 9e4d221bb1 Bump minor version 2023-05-06 12:51:16 +02:00
Martchus ea6b474e8c Add container format name for ID3v2 2023-05-06 11:12:08 +02:00
Martchus 10a6b10658 Fix typos 2023-05-06 11:07:31 +02:00
Martchus 25166e4cb4 Bump patch version 2023-05-06 11:00:52 +02:00
61 changed files with 768 additions and 330 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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()

View File

@ -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).

View File

@ -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;

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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.

View File

@ -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;
};

View File

@ -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

View File

@ -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;
};
/*!

View File

@ -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

View File

@ -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.
*/

View File

@ -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());

View File

@ -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);
}

View File

@ -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
}

View File

@ -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
}

View File

@ -116,13 +116,17 @@ public:
static constexpr std::uint32_t maximumIdLengthSupported();
static constexpr std::uint32_t maximumSizeLengthSupported();
static constexpr std::uint8_t minimumElementSize();
void copyHeader(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
void copyWithoutChilds(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
void copyEntirely(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
template <typename TargetStream = std::ostream>
void copyHeader(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
template <typename TargetStream = std::ostream>
void copyWithoutChilds(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
template <typename TargetStream = std::ostream>
void copyEntirely(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
void makeBuffer();
void discardBuffer();
void copyBuffer(std::ostream &targetStream);
void copyPreferablyFromBuffer(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
template <typename TargetStream = std::ostream> void copyBuffer(TargetStream &targetStream);
template <typename TargetStream = std::ostream>
void copyPreferablyFromBuffer(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
const std::unique_ptr<char[]> &buffer();
ImplementationType *denoteFirstChild(std::uint32_t offset);
@ -139,8 +143,9 @@ protected:
std::unique_ptr<char[]> m_buffer;
private:
template <typename TargetStream = std::ostream>
void copyInternal(
std::ostream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress);
TargetStream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress);
ContainerType *m_container;
bool m_parsed;
@ -840,7 +845,8 @@ void GenericFileElement<ImplementationType>::validateSubsequentElementStructure(
* \brief Writes the header information of the element to the specified \a targetStream.
*/
template <class ImplementationType>
void GenericFileElement<ImplementationType>::copyHeader(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
template <typename TargetStream>
void GenericFileElement<ImplementationType>::copyHeader(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
{
copyInternal(targetStream, startOffset(), headerSize(), diag, progress);
}
@ -849,7 +855,8 @@ void GenericFileElement<ImplementationType>::copyHeader(std::ostream &targetStre
* \brief Writes the element without its children to the specified \a targetStream.
*/
template <class ImplementationType>
void GenericFileElement<ImplementationType>::copyWithoutChilds(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
template <typename TargetStream>
void GenericFileElement<ImplementationType>::copyWithoutChilds(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
{
if (std::uint32_t firstChildOffset = this->firstChildOffset()) {
copyInternal(targetStream, startOffset(), firstChildOffset, diag, progress);
@ -862,7 +869,8 @@ void GenericFileElement<ImplementationType>::copyWithoutChilds(std::ostream &tar
* \brief Writes the entire element including all children to the specified \a targetStream.
*/
template <class ImplementationType>
void GenericFileElement<ImplementationType>::copyEntirely(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
template <typename TargetStream>
void GenericFileElement<ImplementationType>::copyEntirely(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
{
copyInternal(targetStream, startOffset(), totalSize(), diag, progress);
}
@ -890,7 +898,9 @@ template <class ImplementationType> inline void GenericFileElement<Implementatio
* \brief Copies buffered data to \a targetStream.
* \remarks Data must have been buffered using the makeBuffer() method.
*/
template <class ImplementationType> inline void GenericFileElement<ImplementationType>::copyBuffer(std::ostream &targetStream)
template <class ImplementationType>
template <typename TargetStream>
inline void GenericFileElement<ImplementationType>::copyBuffer(TargetStream &targetStream)
{
targetStream.write(m_buffer.get(), static_cast<std::streamsize>(totalSize()));
}
@ -900,8 +910,9 @@ template <class ImplementationType> inline void GenericFileElement<Implementatio
* \remarks So this is copyBuffer() with a fallback to copyEntirely().
*/
template <class ImplementationType>
template <typename TargetStream>
inline void GenericFileElement<ImplementationType>::copyPreferablyFromBuffer(
std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
{
m_buffer ? copyBuffer(targetStream) : copyEntirely(targetStream, diag, progress);
}
@ -923,8 +934,9 @@ template <class ImplementationType> inline const std::unique_ptr<char[]> &Generi
* \sa copyEntireAtomToStream()
*/
template <class ImplementationType>
template <typename TargetStream>
void GenericFileElement<ImplementationType>::copyInternal(
std::ostream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress)
TargetStream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress)
{
// ensure the header has been parsed correctly
try {

View File

@ -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

View File

@ -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);

View File

@ -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");
}

View File

@ -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;
}

View File

@ -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

View File

@ -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
*/

View File

@ -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);
}
/*!

View File

@ -3,6 +3,8 @@
#include "./matroskacontainer.h"
#include "./matroskaid.h"
#include "../mediafileinfo.h"
#include <c++utilities/conversion/binaryconversion.h>
#include <c++utilities/conversion/stringbuilder.h>

View File

@ -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

View File

@ -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.
*/

View File

@ -1,6 +1,8 @@
#include "./matroskacues.h"
#include "./matroskacontainer.h"
#include "../mediafileinfo.h"
#include <c++utilities/conversion/binaryconversion.h>
using namespace std;
@ -76,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()) {

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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");
}

View File

@ -1,5 +1,3 @@
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
#include "./mp4track.h"
#include "./mp4atom.h"
#include "./mp4container.h"

View File

@ -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();

View File

@ -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);
}
/*!

View File

@ -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

View File

@ -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 &params = 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;

View File

@ -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

View File

@ -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;
}
/*!

View File

@ -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));
}

View File

@ -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.
*/

View File

@ -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";
}

View File

@ -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);

View File

@ -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
View File

@ -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;
};

View File

@ -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;
}

View File

@ -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.
*/

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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());
}

View File

@ -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));

View File

@ -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.

View File

@ -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);
}
}
/*!

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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));