Matroska::Tag::title() falls back to \Segment\Info\Title when a file has
no TITLE simple tag. That title was only extracted inside read()'s
readProperties branch, because Info is also where the audio properties
come from, so opening a file with readAudioProperties = false left the
tag with an empty title:
FileRef(path, true ).tag()->title(); // "handbrake"
FileRef(path, false).tag()->title(); // ""
Reading tags without audio properties is the documented fast path for
scanning a library, so this silently emptied the title for exactly the
callers who opted into it, and it made tag content depend on an
audio-properties flag in a way no other format does.
Store the segment title on the file itself, read regardless of
readProperties, and use it from both read() and tag(). The Info element
is already resident by then, so no additional I/O is involved and
audioProperties() still returns null when properties were not requested.
A trak may hold at most one tref. setQtChapters() appended a second one for
the chap reference instead of joining the atom already present, which happens
whenever the audio track references another track -- a timecode track, as in
camera recordings.
Parsers differ on the result: the lenient keep reading the track, the strict
discard it entirely. A file written this way therefore still reads correctly
in TagLib while presenting as having no audio elsewhere, and anything that
remuxes it from a stricter parser's track list writes an audioless copy back
to disk.
Removal follows the same rule inverted: only the chap box is taken out when
the tref is shared, and the tref itself goes only when chap was its sole
child. It also now looks for the tref that actually contains chap rather than
the first one on the track, so a file already carrying two is repaired rather
than stripped of the wrong reference.
Reading the tags of a file materialised every attachment, so a fast read of
a file with cover art cost as much as the art is large, even though the
caller may never ask for it. Measured with a counting IOStream over
tests/data/no-tags.mka with an attachment behind its seek head, read with
AudioProperties::Fast:
attachment | read before | read now
----------------------------------
128 KiB | 131350 B | 278 B
512 KiB | 524544 B | 256 B
4 MiB | 4194563 B | 259 B
The number of read calls is unchanged (183, 164, 164 before; 182, 163, 163
now), so this is volume, not round trips, and what remains does not grow
with the payload.
MkAttachedFileData is now a DeferredBinaryElement, which registers the offset
of its data and skips over it. Matroska::File::attachments() reads the data
before handing the attachments out, so callers see no difference.
save() renders the attachments from the attached files, which would write an
empty attachment for data that was never requested. It therefore loads the
data before writing.
Co-authored-by: Claude Opus 5 <[email protected]>
Element::factory() rejects any element whose declared size runs past the bound it is given, and
read() passes the Fast scan limit as that bound. A Segment spans practically the whole file, so
under ReadStyle::Fast every Matroska over 512 KiB is rejected and no tags are read:
EBML: datasize too great: 1003369 > (524288 - 52)
Failed to find Matroska segment
The limit is readLimited()'s, which already applies it to skip Cues and to bound the walk over a
segment with no usable SeekHead. The lookup only needs the file length.
Correctly handle offsets and scan limits:
- maxOffset: Maximum offset from the beginning of the file; the end of
the element must be before this offset.
- scanLimit: Offset from the current file position until which scanning
for elements is allowed. Normally, elements are scanned up to the end
of the enclosing master element or the end of the file, but in Fast
reading mode, it is limited to FAST_SCAN_LIMIT, which is 512 kB.
- maxScanOffset: scanLimit from the current file position
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Urs Fleisch <[email protected]>
Ogg::File::readPages() read pages from all logical bitstreams and
indexed their packets into one global list. In a multiplexed file the
Theora and Vorbis packets got interleaved, so packet 0 was a Theora
header rather than the expected Vorbis type-3 comment header.
Before reading packets, the Vorbis reader now selects the Vorbis
logical bitstream (the one whose first packet is the Vorbis
identification header), so it reads the correct stream regardless of
position. Packet parsing is scoped to the selected bitstream, so
packets from other codecs in the same file are ignored.
tests/data/multiplex.ogg is generated using
ffmpeg -hide_banner -y \
-f lavfi -i "color=c=navy:s=500x500:r=1:d=2" \
-f lavfi -i "sine=frequency=440:sample_rate=48000:duration=2" \
-map 0:v -map 1:a \
-c:v libtheora -q:v 3 \
-c:a libvorbis -ac 2 -q:a 2 \
-flags +bitexact -fflags +bitexact \
-metadata:s:a:0 TITLE="Paper Lights" \
-metadata:s:a:0 encoder= -metadata:s:v:0 encoder= \
-f ogg tests/data/multiplex.ogg
Frame::fieldData() (taglib/mpeg/id3v2/id3v2frame.cpp) discarded any
ID3v2.4 frame whose declared size (from the header) no longer matched
its actual buffer size after per-frame unsynchronisation was decoded by
FrameFactory::prepareFrameHeader(), silently emptying frames like
TIT2/TPE1/TALB. Clamp the declared length to what's actually available
instead of discarding the frame, only bailing out if the frame's data
offset itself doesn't fit.
Adds testUnsynchDecodeID3v24Frame() to tests/test_id3v2.cpp, covering a
frame with its own per-frame Unsynchronisation flag (as opposed to the
tag-wide flag already covered by testUnsynchDecode()), using new fixture
tests/data/unsynch24.id3.
In matroska an element data size of a VINT with all bits 1 means the
data size is unknown. Unknown data size can only apply to Master
Elements.
Unknown sized elements are described in
https://datatracker.ietf.org/doc/rfc8794/ section 6.2
It gives the following 5 conditions for detecting the end of an
unknown sized element:
* Any EBML Element that is a valid Parent Element of the Unknown-
Sized Element according to the EBML Schema, Global Elements
excluded.
* Any valid EBML Element according to the EBML Schema, Global
Elements excluded, that is not a Descendant Element of the
Unknown-Sized Element but shares a common direct parent, such as a
Top-Level Element.
* Any EBML Element that is a valid Root Element according to the
EBML Schema, Global Elements excluded.
* The end of the Parent Element with a known size has been reached.
* The end of the EBML Document, either when reaching the end of the
file or because a new EBML Header started.
In this patch we use the higher level maxOffset to determine
the maximum data size for the element, which matches the fourth
condition, but is incomplete without the other four methods.
As only Segment and Cluster elements of Matroska files are allowed
to use unknown size length and TagLib does not process Cluster
elements, this should be sufficient.
---------
Signed-off-by: Anthony Brandon <[email protected]>
Co-authored-by: Urs Fleisch <[email protected]>
cnID must be longlong instead of int as Apple Music cnID values can now
exceed the range of a 32-bit integer and require 64-bit aka longlong.
---------
Co-authored-by: Urs Fleisch <[email protected]>
hasiXMLData() / hasBEXTData() were implemented as !data.isEmpty()
checks, which conflated in-memory payload with on-disk block presence.
That caused two wrong answers:
* setiXMLData("foo") on a file with no iXML block made hasiXMLData()
return true immediately, before save().
* A FLAC file carrying an iXML APPLICATION block with empty payload
round-tripped fine, but hasiXMLData() reported false.
Switch to the same model RIFF::WAV::File already uses: explicit
hasiXML / hasBEXT bool flags on FilePrivate, set during scan() when
the APPLICATION block is recognised, updated during save() after the
block is (re)written or omitted, and returned verbatim by the
accessors. New regression test pins down the before/after-save and
empty-block cases.
Refs: https://github.com/taglib/taglib/issues/1362
A new Matroska::File::save(WriteStyle style) overload is provided to
control how tags, attachments and chapters are written to the file.
- Compact: Write tags, attachments and chapters as compact as possible.
This is the default mode.
- DoNotShrink: Do not shrink elements; add void padding when content
gets smaller. Allow inserts when content gets larger.
- AvoidInsert: Like DoNotShrink but also avoid inserts for non-last
elements: replace a growing non-last element with a void of the old
size and append the new element at the end of the segment.
For very large files and/or slow (network) filesystems, using this
mode will reduce write time significantly.
Co-authored-by: Copilot <[email protected]>
Adds 6 public methods on FLAC::File mirroring RIFF::WAV::File's existing
iXML/BEXT API: iXMLData/setiXMLData/hasiXMLData and the BEXT equivalents.
Reads APPLICATION blocks (RFC 9639 § 8.4) carrying either the IANA-
registered "riff" foreign-metadata wrapper or the direct "iXML" / "bext"
application IDs used by some third-party tools (e.g. Sequoia). Writes
the spec-blessed "riff"-wrapped form. Unrecognized application IDs and
"riff"-wrapped chunks other than iXML/bext (e.g. "fmt ", "JUNK") flow
through unmodified, so existing files round-trip without churn.
Test coverage: read direct + riff-wrapped for both iXML and BEXT,
write+reread round-trip, empty-clears-block, and an unknown-application-
block preservation guard.
An equality operator is added for the chapters. The chapters are
only written to the file if they were really modified, so just
reading the chapters without modifying them will not affect
the save operation.
Six new tests exercise corners of the chapter implementation that the
orphaned-mdat fix did not reach:
testQTChapterListUnicodeTitles / testChapterListUnicodeTitles --
Round-trip Japanese, German (umlaut), and Russian titles through the
QT text-sample serialisation and the Nero length-prefixed UTF-8 path
respectively. These are separate paths in the code and benefit from
separate coverage.
testQTChapterListEmptyTitleStripped --
A multi-chapter list whose first entry is empty at t=0 matches the QT
dummy-marker pattern; read() must drop it. Test documents the rule so
a regression is immediately detectable.
testQTChapterListSingleEmptyTitleNotStripped --
The stripping rule only applies when size > 1. A single empty-title
chapter at t=0 is valid and must be preserved.
testNeroAndQTChaptersAreIndependent --
Both formats can coexist; removing one leaves the other intact.
Validates the lazy saveChaptersIfModified contract in mp4file.cpp.
testNeroChaptersAloneWhenNoQT --
Writing one format must not create atoms for the other.
All 47 MP4 tests pass.
The previous fix for orphaned chapter mdats assumed the chapter text
mdat was dedicated and derived its location from stco[0] - 8. In
audiobooks that co-locate chapter text at the start of the primary
audio mdat (stco[0] == audioMdat.offset + 8), that arithmetic lands
on the audio mdat header, the "mdat" signature check passes, and the
full audio payload gets removed -- shrinking a 484 MB audiobook to
5.4 MB.
Fix: resolve the chapter mdat by finding the top-level mdat whose
data range contains stco[0], then re-parse after the trak/tref
removals and confirm no other track's stco/co64 points into that
mdat before deleting it. Shared mdats are left intact; the dead
chapter text bytes remain as harmless padding.
Add a regression test that writes a chapter track, patches its
stco[0] to point into the primary audio mdat (simulating the
audiobook layout), removes the chapter track, and verifies the
audio mdat is byte-identical afterwards.
Adds testQTChapterListNoOrphanedMdat which performs three add/remove
cycles and asserts that the top-level mdat count is identical before and
after. Without the fix, each cycle leaves an orphaned mdat at EOF, so
three cycles produce originalCount + 3 atoms.
Uses TagLib's own MP4::Atoms parser as the primary check, with
AtomicParsley as an optional cross-validation when installed.
Changes made
mp4chapterlist.h
• Added (MP4::File*) overloads for read, write, remove
• Replaced broken class File; forward declaration with #include "mp4file.h" (fixed a subtle C++ name-resolution linker bug where Atoms(File*) resolved to MP4::File* instead of TagLib::File*)
mp4chapterlist.cpp
• Refactored: path-based overloads are now thin wrappers that delegate to file-based overloads
• File-based overloads construct Atoms locally — no Atoms* in the public API
• Removed chplHeaderSize = 9 constant; replaced the minimum-size guard in parseChplData with a correct 5-byte check (the old constant was version-1 specific and would reject valid version-0 atoms)
mp4qtchapterlist.h
• Added (MP4::File*) overloads for read, write, remove
• Removed Atoms* parameters entirely from the public API
mp4qtchapterlist.cpp
• Same refactor: path-based overloads delegate; file-based overloads construct Atoms locally
• Added empty-chapter guard: write(MP4::File*, {}) delegates to remove(file) instead of writing a 0-sample chapter track
tests/test_mp4.cpp
• Added testChapterListFileAPI and testQTChapterListFileAPI — exercise the full write/read/remove cycle via the file-based API
• Updated test bodies to use the simplified (MP4::File*) API (no MP4::Atoms construction in test code)
QuickTime-style chapter tracks are the native chapter format for
Apple's ecosystem. They use a disabled text track (hdlr type "text")
referenced by a chap track-reference in the audio track's tref box.
This format is recognized by QuickTime, iTunes/Music, Final Cut Pro,
Logic Pro, DaVinci Resolve, VLC, and most other MP4/M4A players. It
is also the format that AVFoundation reads natively via
AVAssetChapterMetadataGroup.
The implementation produces output that matches ffmpeg's chapter track
structure byte-for-byte: per-sample stts entries (required by
AVFoundation), encd atoms for UTF-8 text encoding, edts/elst edit
lists, gmhd with gmin+text media information, and disabled tkhd flags
(track_in_movie only).
Key behaviors:
- write() inserts tref + chapter trak as a single contiguous block,
then appends text samples in an mdat atom at EOF
- Handles non-zero first chapter times by prepending a dummy chapter
at time 0 (stripped on read)
- Overwrite support: removes existing chapter track before writing
- Preserves existing metadata tags and audio data integrity
- Uses timescale=1000 (milliseconds) for chapter track timing
7 new tests covering write/read round-trip, remove, overwrite, tag
preservation, empty file read, timestamp precision, and non-zero
first chapter handling.
Implement read/write/remove of Nero-style chapter markers (chpl atom)
in MP4 files. The chpl atom lives at moov/udta/chpl, storing up to 255
chapter entries with 100-nanosecond timestamps and UTF-8 titles.
Includes CppUnit tests covering round-trip read/write, remove, tag
preservation, and reading from files with no chapters.
Some encoders write a valid data chunk but with a slightly too-large
declared chunkSize, or place the data chunk beyond the declared RIFF
boundary. The previous behaviour called break, abandoning all remaining
chunks and making the file appear empty to taglib.
Lenient parsers (ffmpeg, QuickTime) handle this case by clamping the
chunk size to the bytes that actually remain in the file. Adopt the
same strategy: when chunkSize would exceed the file length, clamp it
and continue parsing rather than stopping early.
Read, write, and remove Broadcast Audio Extension (BEXT, EBU Tech 3285)
and iXML metadata chunks in WAV files. BEXT is widely used in broadcast
and professional audio for originator, description, time reference, and
loudness metadata. iXML is used by field recorders and DAWs for scene,
take, and track metadata.
MPEG::File::isSupported() scans for frame sync bytes that can appear
in other files, causing them to be misidentified as MP3.
This also includes a test with such a file.
Make AttachedFile immutable. This is consistent with SimpleTag and
Chapter and avoids using attached files which do not have all required
attributes.
Provide methods to insert and remove a single simple tag, so that
they can be modified without setting all of them while still not
exposing internal lists to the API.
Use DATE_RECORDED instead of DATE_RELEASED for year() and the "DATE"
property. This is more consistent with other tag formats, e.g. for ID3v2
"TDRC" is used, which is the recording time.
The C bindings would convert a char* to String using the default
constructor, which uses the Latin1 encoding, breaking when a key
contains a Unicode character (e.g. an ID3v2 comment description).
The involvement/involvee pairs which are supported for TIPL properties
(ARRANGER, ENGINEER, PRODUCER, DJ-MIX, MIX) are left in the TIPL
frame, other pairs are moved to a TMCL frame. This will result in a
consistent behavior for both ID3v2.3 and ID3v2.4 tags produced by
MusicBrainz Picard.
The following user-settable values for CMake are supported:
- TESTS_DIR: Tests directory, is path to unit test data when 'data' is
appended. Can be used to run the unit tests on a target.
- TESTS_TMPDIR: Directory for temporary files created during unit tests,
system tmpdir is used if undefined. Has to be defined on systems
without global temporary directory.
* Add Shorten (SHN) support
* Add `<cmath>` include and use `std::log2`
* Use `uintptr_t` for buffer size calculations
* Work around `byteSwap` not using fixed width types
* Remove four-character codes
* Attempt to fix `static_assert`
* Revert previous commit
* Update `read_uint`* functions
* Use ByteVector for byte swaps
* Use different ByteVector ctor
* Rework variable-length input to use ByteVector
* Rename some variables
* Naming and formatting cleanup
* Add basic Shorten tests
* Rename a constant
* Rename `internalFileType` to `fileType`
* Add documentation on `fileType` meaning
* Add DO_NOT_DOCUMENT guard
* Fix shadowVariable issues reported by cppcheck
cppcheck --enable=all --inline-suppr \
--suppress=noExplicitConstructor --suppress=unusedFunction \
--suppress=missingIncludeSystem --project=compile_commands.json
* Formatting cleanup
* More explicit types
Reason for these changes: getRiceGolombCode(k, uInt32CodeSize) was
called with int k for uint32_t& argument.
There was also a warning from MSVC for line 299:
warning C4267: 'argument': conversion from 'size_t' to 'int'
* Additional explicit types
* Rename `SHN` namespace to `Shorten`
Also rename files to match
---------
Co-authored-by: Urs Fleisch <[email protected]>