Compare commits

...

66 Commits

Author SHA1 Message Date
Martchus c4e3ae071c Fix casing of Ogg
It is always written as Ogg on https://xiph.org/ogg/doc/index.html (and not
OGG).
2024-06-17 20:47:32 +02:00
Martchus f4a8e2f38f Change more Ogg related code to new coding style 2024-06-17 20:41:27 +02:00
Martchus 3b86aad9b7 Avoid a few magic numbers in Ogg container code 2024-06-17 20:36:42 +02:00
Martchus 436448b68a Improve handling of Ogg page flags
Clear/set the "first/last page of logical bitstream"-flags in pages the
`OggContainer::internalMakeFile()` might modify.
2024-06-17 20:33:28 +02:00
Martchus 3c52e36cc5 Update code for making Ogg files to new coding style 2024-06-17 20:09:45 +02:00
Martchus 986b92bc0e Show padding found in Ogg files after tags 2024-06-16 22:08:28 +02:00
Martchus 75c2bc20b6 Fix typo in documentation of OggPage 2024-06-16 21:36:34 +02:00
Martchus ece59d54a9 Improve warnings in Ogg parser further
* Improve wording
* Check all pages the tag is contained by
* Check "continued"-flag as well
2024-06-16 21:36:10 +02:00
Martchus 43fac8cb67 Adapt tests to recent changes 2024-06-16 20:08:22 +02:00
Martchus 37bab4f353 Fix condition for code specific to `libstdc++` 2024-06-16 01:18:26 +02:00
Martchus 8c27ac0996 Make message about bytes left in segment after Vorbis comment just an info
Mp3tag produces such files and supposedly there's nothing problematic
about it (although also ffmpeg prints a message about it).
2024-06-16 00:48:13 +02:00
Martchus b6b7ab25c2 Fix setting "continued packet" flag of Ogg pages 2024-06-16 00:45:23 +02:00
Martchus e9306c3226 Write cover field as last field in Vorbis comments
This is also how Mp3tag does it.
2024-06-15 23:24:35 +02:00
Martchus 6f61730809 Extend documentation of `OggIterator::nextSegment()` 2024-06-15 22:57:07 +02:00
Martchus 494416f086 Write preferred padding also when making Ogg files 2024-06-15 22:56:39 +02:00
Martchus fcc6dee569 Use correct abs granule pos for OGG pages that don't finish packets
A special value of '-1' (in two's complement) indicates that no packets
finish on a page. Ignoring this does not seem to lead to any real problems
but `oggz-validate` complains about it.
2024-06-15 22:17:49 +02:00
Martchus 0a40932c9b Print warning if abs granule position of pages containing tags is wrong 2024-06-15 20:57:19 +02:00
Martchus e9b11b8d5e Apply clang-format 2024-06-15 19:32:40 +02:00
Martchus 2a340fc880 Write code for adding OggStream in a more compact way 2024-06-15 19:31:24 +02:00
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
67 changed files with 988 additions and 434 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;
@ -154,6 +162,7 @@ public:
VorbisComment *createVorbisComment();
bool removeVorbisComment();
void clearParsingResults();
void reportPaddingSizeChanged(std::uint64_t newPaddingSize);
// methods to get, set object behaviour
const std::string &backupDirectory() const;
@ -184,6 +193,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 +210,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 +238,8 @@ private:
ElementPosition m_tagPosition;
ElementPosition m_indexPosition;
MediaFileHandlingFlags m_fileHandlingFlags;
std::uint64_t m_maxFullParseSize;
std::unique_ptr<MediaFileInfoPrivate> m_p;
};
/*!
@ -293,6 +307,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.
*/
@ -378,6 +401,15 @@ inline const std::vector<std::unique_ptr<Id3v2Tag>> &MediaFileInfo::id3v2Tags()
return m_id3v2Tags;
}
/*!
* \brief Sets the padding size.
* \remarks This is meant to be called from container implementations.
*/
inline void MediaFileInfo::reportPaddingSizeChanged(uint64_t newPaddingSize)
{
m_paddingSize = newPaddingSize;
}
/*!
* \brief Returns the directory used to store backup files.
* \remarks If empty, backup files will be stored in the same directory of the file being modified.
@ -454,7 +486,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 +726,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

@ -10,6 +10,7 @@
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/io/copy.h>
#include <limits>
#include <memory>
using namespace std;
@ -19,7 +20,7 @@ namespace TagParser {
/*!
* \class TagParser::OggVorbisComment
* \brief Specialization of TagParser::VorbisComment for Vorbis comments inside an OGG stream.
* \brief Specialization of TagParser::VorbisComment for Vorbis comments inside an Ogg stream.
*/
std::string_view OggVorbisComment::typeName() const
@ -38,7 +39,7 @@ std::string_view OggVorbisComment::typeName() const
/*!
* \class TagParser::OggContainer
* \brief Implementation of TagParser::AbstractContainer for OGG files.
* \brief Implementation of TagParser::AbstractContainer for Ogg files.
*/
/*!
@ -120,9 +121,9 @@ OggVorbisComment *OggContainer::createTag(const TagTarget &target)
return nullptr;
}
OggVorbisComment *OggContainer::tag(size_t index)
OggVorbisComment *OggContainer::tag(std::size_t index)
{
size_t i = 0;
auto i = std::size_t();
for (const auto &tag : m_tags) {
if (!tag->oggParams().removed) {
if (index == i) {
@ -136,7 +137,7 @@ OggVorbisComment *OggContainer::tag(size_t index)
size_t OggContainer::tagCount() const
{
size_t count = 0;
auto count = std::size_t();
for (const auto &tag : m_tags) {
if (!tag->oggParams().removed) {
++count;
@ -149,7 +150,7 @@ size_t OggContainer::tagCount() const
* \brief Actually just flags the specified \a tag as removed and clears all assigned fields.
*
* This specialization is necessary because removing the tag completely would also
* remove the OGG parameter which are needed when applying changes.
* remove the Ogg parameter which are needed when applying changes.
*
* \remarks Seems like common players aren't able to play Vorbis when no comment is present.
* So do NOT use this method to remove tags from Vorbis, just call Tag::removeAllFields() on \a tag.
@ -170,7 +171,7 @@ bool OggContainer::removeTag(Tag *tag)
* \brief Actually just flags all tags as removed and clears all assigned fields.
*
* This specialization is necessary because completeley removing the tag would also
* remove the OGG parameter which are needed when applying the changes.
* remove the Ogg parameter which are needed when applying the changes.
*
* \remarks Seems like common players aren't able to play Vorbis when no comment is present.
* So do NOT use this method to remove tags from Vorbis, just call removeAllFields() on all tags.
@ -188,8 +189,8 @@ void OggContainer::internalParseHeader(Diagnostics &diag, AbortableProgressFeedb
{
CPP_UTILITIES_UNUSED(progress)
static const string context("parsing OGG bitstream header");
bool pagesSkipped = false, continueFromHere = false;
static const auto context = std::string("parsing Ogg bitstream header");
auto pagesSkipped = false, continueFromHere = false;
// iterate through pages using OggIterator helper class
try {
@ -201,18 +202,17 @@ void OggContainer::internalParseHeader(Diagnostics &diag, AbortableProgressFeedb
if (m_validateChecksums && page.checksum() != OggPage::computeChecksum(stream(), page.startOffset())) {
diag.emplace_back(DiagLevel::Warning,
argsToString(
"The denoted checksum of the OGG page at ", m_iterator.currentSegmentOffset(), " does not match the computed checksum."),
"The denoted checksum of the Ogg page at ", m_iterator.currentSegmentOffset(), " does not match the computed checksum."),
context);
}
OggStream *stream;
std::uint64_t lastNewStreamOffset = 0;
auto lastNewStreamOffset = std::uint64_t();
if (const auto streamIndex = m_streamsBySerialNo.find(page.streamSerialNumber()); streamIndex != m_streamsBySerialNo.end()) {
stream = m_tracks[streamIndex->second].get();
} else {
// new stream serial number recognized -> add new stream
m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size();
m_tracks.emplace_back(make_unique<OggStream>(*this, m_iterator.currentPageIndex()));
stream = m_tracks.back().get();
stream = m_tracks.emplace_back(make_unique<OggStream>(*this, m_iterator.currentPageIndex())).get();
lastNewStreamOffset = page.startOffset();
}
if (!pagesSkipped) {
@ -249,14 +249,14 @@ void OggContainer::internalParseHeader(Diagnostics &diag, AbortableProgressFeedb
} else {
// abort if skipping pages didn't work
diag.emplace_back(DiagLevel::Critical,
"Unable to re-sync after skipping OGG pages in the middle of the file. Try forcing a full parse.", context);
"Unable to re-sync after skipping Ogg pages in the middle of the file. Try forcing a full parse.", context);
return;
}
}
}
} catch (const TruncatedDataException &) {
// thrown when page exceeds max size
diag.emplace_back(DiagLevel::Critical, "The OGG file is truncated.", context);
diag.emplace_back(DiagLevel::Critical, "The Ogg file is truncated.", context);
} catch (const InvalidDataException &) {
// thrown when first 4 byte do not match capture pattern
const auto expectedOffset = m_iterator.currentSegmentOffset();
@ -280,29 +280,62 @@ 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);
m_iterator.setSegmentIndex(params.firstSegmentIndex);
m_iterator.setFilter(m_iterator.currentPage().streamSerialNumber());
const auto startOffset = m_iterator.startOffset();
const auto context = argsToString("parsing tag in Ogg page at ", startOffset);
auto padding = std::uint64_t();
switch (params.streamFormat) {
case GeneralMediaFormat::Vorbis:
comment->parse(m_iterator, VorbisCommentFlags::None, diag);
comment->parse(m_iterator, flags, padding, 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, padding, 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, padding, diag);
break;
default:
diag.emplace_back(DiagLevel::Critical, "Stream format not supported.", "parsing tags from OGG streams");
diag.emplace_back(DiagLevel::Critical, "Stream format not supported.", context);
}
params.lastPageIndex = m_iterator.currentPageIndex();
params.lastSegmentIndex = m_iterator.currentSegmentIndex();
fileInfo().reportPaddingSizeChanged(fileInfo().paddingSize() + padding);
// do a few sanity checks on the continued-flag and absolute granule position as some Ogg demuxers are picky about them
static constexpr auto noPacketsFinishOnPage = std::numeric_limits<std::uint64_t>::max();
if (params.firstPageIndex != params.lastPageIndex) {
const auto pageCount = params.lastPageIndex - params.firstPageIndex;
for (auto i = params.firstPageIndex; i < params.lastPageIndex; ++i) {
if (const auto &page = m_iterator.pages()[i]; page.absoluteGranulePosition() != noPacketsFinishOnPage) {
diag.emplace_back(DiagLevel::Warning,
argsToString("Tag spans over ", pageCount, " pages but absolute granule position of unfinished page at ", page.startOffset(),
" is not set to \"-1\" (it is ", page.absoluteGranulePosition(), ")."),
context);
}
}
for (auto i = params.firstPageIndex + 1; i <= params.lastPageIndex; ++i) {
if (const auto &page = m_iterator.pages()[i]; !page.isContinued()) {
diag.emplace_back(DiagLevel::Warning,
argsToString("The tag is continued in Ogg page at ", page.startOffset(), " but this page is not marked as continued packet."),
context);
}
}
}
if (const auto &page = m_iterator.pages()[params.lastPageIndex]; page.absoluteGranulePosition() == noPacketsFinishOnPage) {
diag.emplace_back(
DiagLevel::Warning, argsToString("Absolute granule position of final page at ", page.startOffset(), " is set to \"-1\"."), context);
}
}
}
@ -326,7 +359,7 @@ void OggContainer::announceComment(std::size_t pageIndex, std::size_t segmentInd
void OggContainer::internalParseTracks(Diagnostics &diag, AbortableProgressFeedback &progress)
{
static const string context("parsing OGG stream");
static const string context("parsing Ogg stream");
for (auto &stream : m_tracks) {
if (progress.isAborted()) {
throw OperationAbortedException();
@ -383,16 +416,19 @@ void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<653
}
default:;
}
for (auto padding = fileInfo().preferredPadding(); padding; --padding) {
buffer.put(0);
}
newSegmentSizes.push_back(static_cast<std::uint32_t>(buffer.tellp() - offset));
}
void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback &progress)
{
const string context("making OGG file");
progress.nextStepOrStop("Prepare for rewriting OGG file ...");
const auto context = std::string("making Ogg file");
progress.nextStepOrStop("Prepare for rewriting Ogg file ...");
parseTags(diag, progress); // tags need to be parsed before the file can be rewritten
string originalPath = fileInfo().path(), backupPath;
NativeFileStream backupStream;
auto originalPath = fileInfo().path(), backupPath = std::string();
auto backupStream = NativeFileStream();
if (fileInfo().saveFilePath().empty()) {
// move current file to temp dir and reopen it as backupStream, recreate original file
@ -421,7 +457,7 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
const auto totalFileSize = fileInfo().size();
try {
progress.nextStepOrStop("Writing OGG pages ...");
progress.nextStepOrStop("Writing Ogg pages ...");
// prepare iterating comments
OggVorbisComment *currentComment;
@ -435,11 +471,13 @@ 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;
static constexpr auto oggPageHeaderSize = 27u;
auto lastPageNewOffset = std::uint64_t();
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;
@ -460,7 +498,7 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
const auto actuallyNextPageOffset = m_iterator.currentPageOffset();
if (actuallyNextPageOffset != nextPageOffset) {
diag.emplace_back(DiagLevel::Critical,
argsToString("Expected OGG page at offset ", nextPageOffset, " but found the next OGG page only at offset ",
argsToString("Expected Ogg page at offset ", nextPageOffset, " but found the next Ogg page only at offset ",
actuallyNextPageOffset, ". Skipped ", (actuallyNextPageOffset - nextPageOffset), " invalid bytes."),
context);
nextPageOffset = actuallyNextPageOffset;
@ -470,14 +508,15 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
} else {
diag.emplace_back(DiagLevel::Critical,
argsToString(
"Expected OGG page at offset ", nextPageOffset, " but could not find any further pages. Skipped the rest of the file."),
"Expected Ogg page at offset ", nextPageOffset, " but could not find any further pages. Skipped the rest of the file."),
context);
break;
}
}
const auto pageSize = currentPage.totalSize();
std::uint32_t &pageSequenceNumber = pageSequenceNumberBySerialNo[currentPage.streamSerialNumber()];
auto &pageSequenceNumber = pageSequenceNumberBySerialNo[currentPage.streamSerialNumber()];
lastPage = &currentPage;
lastPageNewOffset = static_cast<std::uint64_t>(stream().tellp());
nextPageOffset = currentPage.startOffset() + pageSize;
// check whether the Vorbis Comment is present in this Ogg page
@ -485,11 +524,11 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
&& m_iterator.currentPageIndex() <= currentParams->lastPageIndex && !currentPage.segmentSizes().empty()) {
// page needs to be rewritten (not just copied)
// -> write segments to a buffer first
stringstream buffer(ios_base::in | ios_base::out | ios_base::binary);
vector<std::uint32_t> newSegmentSizes;
auto buffer = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
auto newSegmentSizes = std::vector<std::uint32_t>();
newSegmentSizes.reserve(currentPage.segmentSizes().size());
std::uint64_t segmentOffset = m_iterator.currentSegmentOffset();
vector<std::uint32_t>::size_type segmentIndex = 0;
auto segmentOffset = m_iterator.currentSegmentOffset();
auto segmentIndex = std::vector<std::uint32_t>::size_type();
for (const auto segmentSize : currentPage.segmentSizes()) {
if (!segmentSize) {
++segmentIndex;
@ -518,7 +557,7 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
}
} else {
// copy other segments unchanged
backupStream.seekg(static_cast<streamoff>(segmentOffset));
backupStream.seekg(static_cast<std::streamoff>(segmentOffset));
copyHelper.copy(backupStream, buffer, segmentSize);
newSegmentSizes.push_back(segmentSize);
@ -548,22 +587,30 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
auto continuePreviousSegment = false, needsZeroLacingValue = false;
// write pages until all data in the buffer is written
while (newSegmentSizesIterator != newSegmentSizesEnd) {
// write page header
// memorize offset to update checksum later
updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp()));
// copy page header from original file (except for the segment table)
backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp())); // memorize offset to update checksum later
copyHelper.copy(backupStream, stream(), 27); // just copy header from original file
// set continue flag
stream().seekp(-22, ios_base::cur);
stream().put(static_cast<char>(currentPage.headerTypeFlag() & (continuePreviousSegment ? 0xFF : 0xFE)));
copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
// use flags of original page as base and adjust "continued packet"-flag as needed
auto flags = (currentPage.headerTypeFlag() & 0xFE) | (continuePreviousSegment ? 0x01 : 0x00);
continuePreviousSegment = true;
// adjust page sequence number
// ensure "first page of logical bitstream"-flag is cleared for additional pages we need to insert
// ensure "last page of logical bitstream"-flag is cleared for the first page
flags = flags & (newSegmentSizesIterator != newSegmentSizes.cbegin() ? 0xFD : 0xF);
// override flags copied from original file
stream().seekp(-22, ios_base::cur);
stream().put(static_cast<char>(flags));
// update absolute granule position later (8 byte) and keep stream serial number (4 byte)
stream().seekp(12, ios_base::cur);
// adjust page sequence number
writer().writeUInt32LE(pageSequenceNumber);
// skip checksum (4 bytes) and number of page segments (1 byte); those are update later
stream().seekp(5, ios_base::cur);
std::int16_t segmentSizesWritten = 0; // in the current page header only
// write segment sizes as long as there are segment sizes to be written and
// the max number of segment sizes (255) is not exceeded
std::uint32_t currentSize = 0;
auto segmentSizesWritten = std::int16_t(); // in the current page header only
auto currentSize = std::uint32_t();
while ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
while (bytesLeft > 0xFF && segmentSizesWritten < 0xFF) {
stream().put(static_cast<char>(0xFF));
@ -589,15 +636,31 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
}
}
// there are no bytes left in the current segment; remove continue flag
// remove continue flag if there are no bytes left in the current segment
if (!bytesLeft && !needsZeroLacingValue) {
continuePreviousSegment = false;
}
// set the absolute granule postion
if (newSegmentSizesIterator != newSegmentSizesEnd) {
// set absolute granule position to special value "-1" if there are still bytes to be written in the current packet
stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
writer().writeInt64LE(-1);
stream().seekp(12, ios_base::cur);
} else if (currentParams->lastPageIndex != currentParams->firstPageIndex) {
// ensure the written absolute granule position matches the one from the last page of the existing file
backupStream.seekg(static_cast<streamoff>(m_iterator.pages()[currentParams->lastPageIndex].startOffset() + 6));
stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
copyHelper.copy(backupStream, stream(), 8);
stream().seekp(12, ios_base::cur);
} else {
// leave the absolute granule position unchanged
stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
}
// page is full or all segment data has been covered
// -> write segment table size (segmentSizesWritten) and segment data
// -> seek back and write updated page segment number
stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
stream().put(static_cast<char>(segmentSizesWritten));
stream().seekp(segmentSizesWritten, ios_base::cur);
// -> write actual page data
@ -612,11 +675,11 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
// just update page sequence number
backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp())); // memorize offset to update checksum later
copyHelper.copy(backupStream, stream(), 27);
copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
stream().seekp(-9, ios_base::cur);
writer().writeUInt32LE(pageSequenceNumber);
stream().seekp(5, ios_base::cur);
copyHelper.copy(backupStream, stream(), pageSize - 27);
copyHelper.copy(backupStream, stream(), pageSize - oggPageHeaderSize);
} else {
// copy page unchanged
backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
@ -637,9 +700,21 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
}
// close backups stream; reopen new file as readable stream
auto &stream = fileInfo().stream();
backupStream.close();
fileInfo().close();
fileInfo().stream().open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
stream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
// ensure the "last page of logical bitstream"-flag is set on the last Ogg page (in case the last page was written/modified by us)
if (lastPage && lastPageNewOffset) {
const auto offset = static_cast<std::streamoff>(lastPageNewOffset + 5ul);
stream.seekg(offset);
if (const auto flag = stream.get(); !(flag & 0x04)) {
updatedPageOffsets.emplace_back(lastPageNewOffset);
stream.seekp(offset);
stream.put(static_cast<char>(flag | 0x04));
}
}
// update checksums of modified pages
progress.nextStepOrStop("Updating checksums ...");
@ -649,15 +724,15 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
progress.updateStepPercentage(static_cast<std::uint8_t>(offset * 100ul / fileInfo().size()));
progress.stopIfAborted();
}
OggPage::updateChecksum(fileInfo().stream(), offset);
OggPage::updateChecksum(stream, offset);
}
// prevent deferring final write operations (to catch and handle possible errors here)
fileInfo().stream().flush();
stream.flush();
progress.updateStepPercentage(100);
// clear iterator
m_iterator.clear(fileInfo().stream(), startOffset(), fileInfo().size());
m_iterator.clear(stream, startOffset(), fileInfo().size());
} catch (...) {
m_iterator.setStream(fileInfo().stream());

View File

@ -24,7 +24,7 @@ class MediaFileInfo;
class OggContainer;
/*!
* \brief The OggParameter struct holds the OGG parameter for a VorbisComment.
* \brief The OggParameter struct holds the Ogg parameter for a VorbisComment.
*/
struct TAG_PARSER_EXPORT OggParameter {
constexpr OggParameter();
@ -73,7 +73,7 @@ public:
OggVorbisComment();
static constexpr TagType tagType = TagType::OggVorbisComment;
static constexpr std::string_view tagName = "OGG Vorbis comment";
static constexpr std::string_view tagName = "Ogg Vorbis comment";
TagType type() const override;
std::string_view typeName() const override;
bool supportsTarget() const override;
@ -86,7 +86,7 @@ private:
};
/*!
* \brief Constructs a new OGG Vorbis comment.
* \brief Constructs a new Ogg Vorbis comment.
*/
inline OggVorbisComment::OggVorbisComment()
{
@ -110,7 +110,7 @@ inline bool OggVorbisComment::supportsTarget() const
}
/*!
* \brief Returns the OGG parameter for the comment.
* \brief Returns the Ogg parameter for the comment.
*
* Consists of first page index, first segment index, last page index, last segment index and tag index (in this order).
* These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance.
@ -121,7 +121,7 @@ inline OggParameter &OggVorbisComment::oggParams()
}
/*!
* \brief Returns the OGG parameter for the comment.
* \brief Returns the Ogg parameter for the comment.
*
* Consists of first page index, first segment index, last page index, last segment index and tag index (in this order).
* These values are used and managed by the OggContainer class and do not affect the behavior of the VorbisComment instance.
@ -169,7 +169,7 @@ private:
/*!
* \brief Returns whether checksum validation is enabled.
*
* If checksum validation is enabled, the parser will validate the OGG pages by
* If checksum validation is enabled, the parser will validate the Ogg pages by
* checking the CRC32 checksum.
*
* \sa setChecksumValidationEnabled()

View File

@ -14,7 +14,7 @@ namespace TagParser {
/*!
* \class TagParser::OggIterator
* \brief The OggIterator class helps iterating through all segments of an OGG bitstream.
* \brief The OggIterator class helps iterating through all segments of an Ogg bitstream.
*
* If an OggIterator has just been constructed it is invalid. To fetch the first page from
* the stream call the reset() method. The iterator will now point to the first segment of the
@ -22,7 +22,7 @@ namespace TagParser {
*
* To go on call the appropriate methods. Parsing exceptions and IO exceptions might occur during iteration.
*
* The internal buffer of OGG pages might be accessed using the pages() method.
* The internal buffer of Ogg pages might be accessed using the pages() method.
*/
/*!
@ -75,7 +75,7 @@ void OggIterator::nextPage()
}
/*!
* \brief Increases the current position by one segment.
* \brief Increases the current position by one segment. Enters the next page if the current segment is the last one in the current page.
* \remarks The iterator must be valid. The iterator might be invalidated.
*/
void OggIterator::nextSegment()
@ -121,7 +121,7 @@ void OggIterator::previousSegment()
}
/*!
* \brief Reads \a count bytes from the OGG stream and writes it to the specified \a buffer.
* \brief Reads \a count bytes from the Ogg stream and writes it to the specified \a buffer.
* \remarks
* - Might increase the current page index and/or the current segment index.
* - Page headers are skipped (this is the whole purpose of this method).
@ -154,12 +154,12 @@ void OggIterator::read(char *buffer, std::size_t count)
}
/*!
* \brief Reads all bytes from the OGG stream and writes it to the specified \a buffer.
* \brief Reads all bytes from the Ogg stream and writes it to the specified \a buffer.
* \remarks
* - Might increase the current page index and/or the current segment index.
* - Page headers are skipped (this is the whole purpose of this method).
* - Does not write more than \a max bytes to the buffer.
* \returns Returns the number of bytes read from the OGG stream. This might be less than \a max in
* \returns Returns the number of bytes read from the Ogg stream. This might be less than \a max in
* case not that many bytes were available.
* \sa read()
* \sa currentCharacterOffset()
@ -187,7 +187,7 @@ std::size_t OggIterator::readAll(char *buffer, std::size_t max)
}
/*!
* \brief Advances the position of the next character to be read from the OGG stream by \a count bytes.
* \brief Advances the position of the next character to be read from the Ogg stream by \a count bytes.
* \remarks
* - Might increase the current page index and/or the current segment index.
* - Page headers are skipped (this is the whole purpose of this method).
@ -223,7 +223,7 @@ void OggIterator::ignore(std::size_t count)
* the last known page. Hence \a offset must be greater than OggPage::startOffset() + OggPage::totalSize() of the
* last known page. This is checked by the method.
*
* If the OGG capture pattern is not present at \a offset, up to 65307 bytes (max. size of an OGG page) are
* If the Ogg capture pattern is not present at \a offset, up to 65307 bytes (max. size of an Ogg page) are
* skipped. So in a valid stream, this method will always succeed if \a offset is less than the stream size minus
* 65307.
*
@ -256,7 +256,7 @@ bool OggIterator::resyncAt(std::uint64_t offset)
if (lettersFound == 3) {
// capture pattern found
const auto currentOffset = stream().tellg();
// -> try to parse an OGG page at this position
// -> try to parse an Ogg page at this position
try {
m_pages.emplace_back(stream(), static_cast<std::uint64_t>(stream().tellg()) - 4,
bytesAvailable > numeric_limits<std::int32_t>::max() ? numeric_limits<std::int32_t>::max()

View File

@ -120,7 +120,7 @@ inline std::uint64_t OggIterator::streamSize() const
}
/*!
* \brief Returns a vector of containing the OGG pages that have been fetched yet.
* \brief Returns a vector of containing the Ogg pages that have been fetched yet.
*/
inline const std::vector<OggPage> &OggIterator::pages() const
{
@ -128,7 +128,7 @@ inline const std::vector<OggPage> &OggIterator::pages() const
}
/*!
* \brief Returns a vector of containing the OGG pages that have been fetched yet.
* \brief Returns a vector of containing the Ogg pages that have been fetched yet.
*/
inline std::vector<OggPage> &OggIterator::pages()
{
@ -136,7 +136,7 @@ inline std::vector<OggPage> &OggIterator::pages()
}
/*!
* \brief Returns the current OGG page.
* \brief Returns the current Ogg page.
* \remarks Calling this method when the iterator is invalid causes undefined behaviour.
*/
inline const OggPage &OggIterator::currentPage() const
@ -145,7 +145,7 @@ inline const OggPage &OggIterator::currentPage() const
}
/*!
* \brief Returns the start offset of the current OGG page.
* \brief Returns the start offset of the current Ogg page.
* \remarks Calling this method when the iterator is invalid causes undefined behaviour.
*/
inline std::uint64_t OggIterator::currentPageOffset() const

View File

@ -14,7 +14,7 @@ namespace TagParser {
/*!
* \class TagParser::OggPage
* \brief The OggPage class is used to parse OGG pages.
* \brief The OggPage class is used to parse Ogg pages.
* \sa http://www.xiph.org/ogg/doc/framing.html
* \todo Add field for additional flags in v11.
*/
@ -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,11 +48,12 @@ 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;
};
/*!
* \brief Constructs a new OGG page.
* \brief Constructs a new Ogg page.
*/
inline OggPage::OggPage()
: m_startOffset(0)
@ -63,6 +64,7 @@ inline OggPage::OggPage()
, m_sequenceNumber(0)
, m_checksum(0)
, m_segmentCount(0)
, m_lastSegmentUnconcluded(false)
{
}
@ -106,7 +108,7 @@ inline std::uint8_t OggPage::headerTypeFlag() const
}
/*!
* \brief Returns whether this page is a continued packed (true) or a fresh packed (false).
* \brief Returns whether this page is a continued packet (true) or a fresh packet (false).
*/
inline bool OggPage::isContinued() const
{
@ -134,7 +136,7 @@ inline bool OggPage::isLastPage() const
*/
inline bool OggPage::isLastSegmentUnconcluded() const
{
return m_headerTypeFlag & 0x80;
return m_lastSegmentUnconcluded;
}
/*!

View File

@ -25,7 +25,7 @@ namespace TagParser {
/*!
* \class TagParser::OggStream
* \brief Implementation of TagParser::AbstractTrack for OGG streams.
* \brief Implementation of TagParser::AbstractTrack for Ogg streams.
*/
/*!
@ -50,7 +50,7 @@ void OggStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedback
{
CPP_UTILITIES_UNUSED(progress)
static const string context("parsing OGG page header");
static const string context("parsing Ogg page header");
// read basic information from first page
OggIterator &iterator = m_container.m_iterator;
@ -218,11 +218,11 @@ void OggStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedback
hasCommentHeader = true;
} else {
diag.emplace_back(
DiagLevel::Critical, "OGG page after FLAC-to-Ogg mapping header doesn't contain Vorbis comment.", context);
DiagLevel::Critical, "Ogg page after FLAC-to-Ogg mapping header doesn't contain Vorbis comment.", context);
}
} else {
diag.emplace_back(
DiagLevel::Critical, "No more OGG pages after FLAC-to-Ogg mapping header (Vorbis comment expected).", context);
DiagLevel::Critical, "No more Ogg pages after FLAC-to-Ogg mapping header (Vorbis comment expected).", context);
}
}

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;
@ -83,7 +83,7 @@ void OverallTests::checkFlacTestfile2()
bool gotMessageAboutMissingVorbisComment = false;
for (const auto &msg : m_diag) {
if (msg.level() == DiagLevel::Critical) {
CPPUNIT_ASSERT_EQUAL("OGG page after FLAC-to-Ogg mapping header doesn't contain Vorbis comment."s, msg.message());
CPPUNIT_ASSERT_EQUAL("Ogg page after FLAC-to-Ogg mapping header doesn't contain Vorbis comment."s, msg.message());
gotMessageAboutMissingVorbisComment = true;
continue;
}

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,10 +4,12 @@
#include "../abstracttrack.h"
#include "../tag.h"
#include "../vorbis/vorbiscomment.h"
#include "../vorbis/vorbiscommentfield.h"
#include "../vorbis/vorbiscommentids.h"
#include <c++utilities/io/misc.h>
#include <functional>
#include <algorithm>
using namespace CppUtilities;
@ -135,17 +137,19 @@ void OverallTests::checkOggTestfile3()
return;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("warning present", DiagLevel::Warning, m_diag.level());
for (const auto &msg : m_diag) {
if (msg.level() == DiagLevel::Warning) {
CPPUNIT_ASSERT_EQUAL_MESSAGE("warning due to broken segment termination", "3 bytes left in last segment."s, msg.message());
CPPUNIT_ASSERT_EQUAL_MESSAGE("warning relates to Vorbis comment", "parsing Vorbis comment"s, msg.context());
break;
}
}
CPPUNIT_ASSERT(std::find_if(m_diag.begin(), m_diag.end(), [](const auto &msg) {
return startsWith(msg.message(), "3 bytes left in last segment");
}) != m_diag.end());
CPPUNIT_ASSERT(std::find_if(m_diag.begin(), m_diag.end(), [](const auto &msg) {
return startsWith(msg.message(), "Tag spans over 6 pages but absolute granule position of unfinished page at");
}) != m_diag.end());
CPPUNIT_ASSERT(std::find_if(m_diag.begin(), m_diag.end(), [](const auto &msg) {
return startsWith(msg.message(), "The tag is continued in Ogg page at");
}) != m_diag.end());
}
/*!
* \brief Checks whether test meta data for OGG files has been applied correctly.
* \brief Checks whether test meta data for Ogg files has been applied correctly.
*/
void OverallTests::checkOggTestMetaData()
{
@ -206,7 +210,7 @@ void OverallTests::setOggTestMetaDataCover()
*/
void OverallTests::testOggParsing()
{
cerr << endl << "OGG parser" << endl;
cerr << endl << "Ogg parser" << endl;
m_fileInfo.setForceFullParse(false);
m_tagStatus = TagStatus::Original;
parseFile(testFilePath("mtx-test-data/ogg/qt4dance_medium.ogg"), &OverallTests::checkOggTestfile1);
@ -239,7 +243,7 @@ void OverallTests::testOggMaking()
} else {
testConditions.emplace_back("modifying tag");
}
cerr << endl << "OGG maker - testmode " << m_mode << ": " << joinStrings(testConditions, ", ") << endl;
cerr << endl << "Ogg maker - testmode " << m_mode << ": " << joinStrings(testConditions, ", ") << endl;
// do actual tests
m_tagStatus = (m_mode & RemoveTag) ? TagStatus::Removed : TagStatus::TestMetaDataPresent;
@ -250,3 +254,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,16 +150,80 @@ 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.
*/
template <class StreamType> void VorbisComment::internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag)
template <class StreamType>
void VorbisComment::internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, std::uint64_t &padding, Diagnostics &diag)
{
// prepare parsing
static const string context("parsing Vorbis comment");
@ -167,7 +235,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)
@ -240,13 +308,18 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
}
}
if (bytesRemaining) {
diag.emplace_back(DiagLevel::Warning, argsToString(bytesRemaining, " bytes left in last segment."), context);
diag.emplace_back(DiagLevel::Information, argsToString(bytesRemaining, " bytes left in last segment."), context);
padding += bytesRemaining;
}
}
if (flags & VorbisCommentFlags::ConvertTotalFields) {
convertTotalFields(context, diag);
}
}
/*!
* \brief Parses tag information using the specified OGG \a iterator.
* \brief Parses tag information using the specified Ogg \a iterator.
*
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws TagParser::Failure or a derived exception when a parsing
@ -254,11 +327,24 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
*/
void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, Diagnostics &diag)
{
internalParse(iterator, iterator.streamSize(), flags, diag);
auto padding = std::uint64_t();
internalParse(iterator, iterator.streamSize(), flags, padding, diag);
}
/*!
* \brief Parses tag information using the specified OGG \a iterator.
* \brief Parses tag information using the specified Ogg \a iterator.
*
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws TagParser::Failure or a derived exception when a parsing
* error occurs.
*/
void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, std::uint64_t &padding, Diagnostics &diag)
{
internalParse(iterator, iterator.streamSize(), flags, padding, diag);
}
/*!
* \brief Parses tag information using the specified Ogg \a iterator.
*
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws TagParser::Failure or a derived exception when a parsing
@ -266,9 +352,26 @@ void VorbisComment::parse(OggIterator &iterator, VorbisCommentFlags flags, Diagn
*/
void VorbisComment::parse(istream &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag)
{
internalParse(stream, maxSize, flags, diag);
auto padding = std::uint64_t();
internalParse(stream, maxSize, flags, padding, diag);
}
/// \cond
static std::uint32_t makeField(VorbisCommentField &field, BinaryWriter &writer, VorbisCommentFlags flags, Diagnostics &diag)
{
if (field.value().isEmpty()) {
return 0;
}
try {
if (field.make(writer, flags, diag)) {
return 1;
}
} catch (const Failure &) {
}
return 0;
}
/// \endcond
/*!
* \brief Writes tag information to the specified \a stream.
*
@ -300,17 +403,15 @@ void VorbisComment::make(std::ostream &stream, VorbisCommentFlags flags, Diagnos
writer.writeUInt32LE(0);
// write fields
std::uint32_t fieldsWritten = 0;
static const auto coverId = std::string(VorbisCommentIds::cover());
for (auto &i : fields()) {
VorbisCommentField &field = i.second;
if (!field.value().isEmpty()) {
try {
if (field.make(writer, flags, diag)) {
++fieldsWritten;
}
} catch (const Failure &) {
}
if (i.first != coverId) { // write cover at the end
fieldsWritten += makeField(i.second, writer, flags, diag);
}
}
if (const auto cover = fields().find(coverId); cover != fields().end()) {
fieldsWritten += makeField(cover->second, writer, flags, diag);
}
// write field count
const auto framingByteOffset = stream.tellp();
stream.seekp(fieldCountOffset);

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();
@ -39,6 +42,7 @@ public:
bool setValue(KnownField field, const TagValue &value) override;
void parse(OggIterator &iterator, VorbisCommentFlags flags, Diagnostics &diag);
void parse(OggIterator &iterator, VorbisCommentFlags flags, std::uint64_t &padding, Diagnostics &diag);
void parse(std::istream &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag);
void make(std::ostream &stream, VorbisCommentFlags flags, Diagnostics &diag);
@ -51,7 +55,10 @@ protected:
KnownField internallyGetKnownField(const IdentifierType &id) const;
private:
template <class StreamType> void internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag);
template <class StreamType>
void internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, std::uint64_t &padding, 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

@ -205,7 +205,7 @@ bool VorbisCommentField::make(BinaryWriter &writer, VorbisCommentFlags flags, Di
bufferStream.rdbuf()->pubsetbuf(buffer.get(), requiredSize);
#endif
pictureBlock.make(bufferStream);
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
#if !(defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION))
bufferStream.read(buffer.get(), static_cast<std::streamsize>(requiredSize));
#endif
valueString = encodeBase64(reinterpret_cast<std::uint8_t *>(buffer.get()), requiredSize);

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

View File

@ -186,9 +186,10 @@ void WaveAudioStream::internalParseHeader(Diagnostics &diag, AbortableProgressFe
MpegAudioFrame frame;
frame.parseHeader(m_reader, diag);
MpegAudioFrameStream::addInfo(frame, *this);
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_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_bytesPerSecond = static_cast<std::uint32_t>(m_bitrate * 125);
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bitrate * 128.0));
}