94 Commits
v2.1.1 ... v2.3

Author SHA1 Message Date
Urs Fleisch
1b94b93762 Version 2.3 2026-05-10 15:25:51 +02:00
HomerJau
8511827fa1 Skip Matroska Cues with AudioProperties::Fast and read-only mode
When the file is opened in read-only mode, it will not be written and
the Cues do not have to be updated. Skipping the Cues will make the
reading of large Matroska files over network filesystems (SMB/NFS)
faster.
2026-05-09 09:25:14 +02:00
Urs Fleisch
b02ff63916 Fix -Wconversion size_t to unsigned int warning 2026-05-09 08:28:54 +02:00
HomerJau
f1e8dac084 [Matroska: Follow chained SeekHead entries when parsing segment metadata
Some muxers — notably MakeMKV, and mkvmerge in certain configurations — write a small primary seekHead at the start of the segment that contains a single entry referencing a secondary seekHead near the end of the file. The secondary seekHead carries the actual entries for info, tracks, tags, chapters, and attachments.
2026-05-08 05:17:23 +02:00
Luc Schrijvers
d1460b6fbf Build fix for Haiku's fcntl.h which can't be found in sys/fcntl.h 2026-05-07 18:32:19 +02:00
Urs Fleisch
43190d30ed Prepare 2.3 release 2026-05-04 13:02:17 +02:00
Urs Fleisch
4c43f1c577 Matroska: Provide different WriteStyle to trade-off size/speed
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 <copilot@github.com>
2026-05-04 12:55:17 +02:00
Ryan Francesconi
59ed19d12f [WAV] Decode iXML as UTF-8
The iXML chunk in BWF/WAV files is specified as UTF-8 (per the EBU
Tech 3285 supplement and the iXML spec). The reader was constructing
the String without an encoding hint, which falls back to Latin-1 and
mangles any non-ASCII bytes (e.g. Unicode in <NOTE>, <PROJECT>, or
<TRACK_LIST> entries written by Sound Devices, Zaxcom, etc.).
2026-05-01 06:32:09 +02:00
Ryan Francesconi
1e7bdae284 [FLAC] Add iXML and BEXT support via APPLICATION blocks
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.
2026-05-01 06:31:50 +02:00
HomerJau
e07b956fda [Matroska] Allow Orphaned Chapter Reading (when Chapter has no EditionID)
Fix: Handle orphan ChapterAtom elements not wrapped in EditionEntry

The Matroska specification requires every ChapterAtom to be inside an
EditionEntry. However, some muxers (older FFmpeg versions, some streaming
tools) produce files with ChapterAtom elements directly under Chapters,
without an EditionEntry wrapper.
MKVToolNix and FFmpeg both handle this case gracefully by treating orphan
atoms as belonging to an implicit default edition. Previously, TagLib
silently ignored these chapters, returning an empty ChapterEditionList.

This change:
- Collects orphan ChapterAtom elements encountered directly under Chapters
- Wraps them in an implicit default edition (UID = 0, isDefault = true,
  isOrdered = false) so they are exposed through the existing
  chapterEditionList() API
- Extracts the atom-parsing logic into a private parseChapterAtom() helper
  to avoid code duplication between the two call sites

No existing behavior is changed - files that already conform to the spec
(chapters inside an EditionEntry) parse identically.
2026-04-26 08:24:19 +02:00
Urs Fleisch
5e1cb4081d Limit MP4 atom sibling count at top level (#1344) 2026-04-26 07:16:53 +02:00
Urs Fleisch
e7e4f0958c Merge pull requests #1325 #1343 from ryanfrancesconi MP4 chapterlist
ryanfrancesconi/feature/mp4-chapterlist
ryanfrancesconi/fix/qt-chapter-orphaned-mdat
2026-04-26 07:15:40 +02:00
Urs Fleisch
497c040f04 Set MP4 chapters only if modified
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.
2026-04-25 11:46:51 +02:00
Ryan Francesconi
05c2c8671e MP4: Add test coverage for chapter unicode, empty titles, and format independence
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.
2026-04-23 12:19:27 -07:00
Ryan Francesconi
85b6a9eb93 MP4: Guard against deleting shared mdat on QT chapter remove
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.
2026-04-23 12:14:00 -07:00
Ryan Francesconi
5c70f0071f MP4: Add regression test for orphaned mdat on QT chapter remove
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.
2026-04-23 11:03:23 -07:00
Ryan Francesconi
ae171ee237 MP4: Remove orphaned mdat when removing QT chapter track
write() appends a new mdat at EOF to hold chapter text samples but the
removal code (both remove() and the replace-existing path in write())
only deleted the chapter trak and tref atoms from inside moov.  Each
add/remove cycle left the previous chapter mdat behind, causing orphaned
mdat atoms to accumulate.

Fix: extract a removeQTChapterTrack() helper that performs all three
removals atomically.  Before deleting the chapter trak, the helper reads
the first stco chunk offset (which points 8 bytes past the chapter mdat
header) to locate the mdat.  After removing the trak and tref (both
inside moov, which precedes the mdat at EOF), it adjusts the mdat offset
by -(chapterLen + trefLen) and removes the atom, leaving no orphaned data.
2026-04-23 11:03:23 -07:00
Urs Fleisch
78c7208bc9 Integrate MP4 chapters into MP4::File 2026-04-23 11:03:23 -07:00
Urs Fleisch
0df52e3993 Apply stco/co64 bounds fix from PR #1333 to MP4 chapter code
The updateChunkOffsets() function in mp4qtchapterlist.cpp and
mp4chapterlist.cpp is duplicated code from mp4tag.cpp and needs
the patch from mp4tag.cpp too.
2026-04-23 11:03:23 -07:00
Ryan Francesconi
ba2441b378 corrected nanosecond unit change -> milliseconds
taglib/mp4/mp4chapterlist.h
• start​Time doc comment: 100​-nanosecond units → milliseconds

taglib/mp4/mp4chapterlist.cpp
• render​Chpl​Data: from​Long​Long(ch​.start​Time) → from​Long​Long(ch​.start​Time * 10000​LL)
• parse​Chpl​Data: ch​.start​Time = start​Time → ch​.start​Time = start​Time100ns / 10000​LL

taglib/mp4/mp4qtchapterlist.cpp
• read: current​Time * 10000000​.0 / timescale → current​Time * 1000​.0 / timescale
• build​Stts lambda: time100ns * timescale / 10000000​.0 → time​Ms * timescale / 1000​.0

tests/test_mp4.cpp
• All start​Time assignments and assertions divided by 10,000 (e.g. 300000000​LL → 30000​LL)
2026-04-23 11:03:23 -07:00
Ryan Francesconi
c5ea13bb34 overloads for read, write, remove
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 Tag​Lib::​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 chpl​Header​Size = 9 constant; replaced the minimum-size guard in parse​Chpl​Data 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 test​Chapter​List​File​API and test​QTChapter​List​File​API — 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)
2026-04-23 11:03:23 -07:00
Ryan Francesconi
4a73d73b20 MP4: Add QuickTime-style chapter track support
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.
2026-04-23 11:03:23 -07:00
Ryan Francesconi
9c56f191e5 MP4: Add Nero-style chapter marker support
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.
2026-04-23 11:03:23 -07:00
Urs Fleisch
77f6b9add5 Drop zero size ID3v2 frames but accept tag (#437) 2026-04-20 15:11:33 +02:00
Urs Fleisch
a64e7543f8 Fix DSD/DSF signed integer issues (#1332) 2026-04-20 15:08:37 +02:00
Felipe
d466b72eea docs: Some improvements to the documentation (#1337)
Make MP4 AtomDataType descriptions visible in the generated documentation.
Convert the ID3v2 text frame listing into a table.
Convert the shorten `fileType()` documentation into a table.
Fix some typos.
Add link to specification in `EventType` for consistency with other headers.
2026-04-13 20:05:53 +02:00
Urs Fleisch
c3a0e1d0a2 Matroska: Use seek head for faster element lookup (#1321)
Limit scan for Matroska seek head to 512 KB in ReadStyle::Fast

---------

Co-authored-by: tolriq <git@leetzone.org>
2026-04-13 19:58:52 +02:00
Ryan Francesconi
13751f5a6b Fix/shorten rice golomb k bounds (#1335)
* Shorten: Reject out-of-range k in getRiceGolombCode

k values outside [0, 31] cause undefined behavior: a left shift by 32
on int32_t (UB in C++) when bitsAvailable reaches 32 after a buffer
refill. Guard against this at the top of getRiceGolombCode and return
false (invalid file) for any k outside the valid range.

* Shorten: Reject out-of-range k in getRiceGolombCode

k values outside [0, 31] cause undefined behavior: a left shift by 32
on int32_t (UB in C++) when bitsAvailable reaches 32 after a buffer
refill. Guard against this at the top of getRiceGolombCode and return
false (invalid file) for any k outside the valid range.
2026-04-09 14:03:36 -06:00
Urs Fleisch
4da5ac2de4 Fix writing too many offsets when updating MP4 stco/co64 atoms (#1332)
This will fix a DoS with a crafted MP4 file causing too many offsets
to be written when updating the stco or co64 tables in MP4 files.

Credits for the discovery of this bug go to Yuen Ying Ng (Ruth)
(Cyber Security Researcher at PwC Hong Kong).
2026-04-08 20:53:59 +02:00
Urs Fleisch
193091fe2e Fix unbounded recursion in EBML/Matroska MasterElement and MP4 atoms (#1326)
Credits for fix and reporting go to https://github.com/ericliu-12.
2026-04-08 20:52:58 +02:00
Ryan Francesconi
5d63187a8b MP4: Fix data race in ItemFactory lazy map initialization (#1331)
Concurrent calls to propertyKeyForName() and handlerTypeForName() (e.g.
via batchMap during import) could race on the isEmpty() guard used for
first-call lazy initialization.

Replace isEmpty() guards with std::call_once / std::once_flag so that
each map is initialized exactly once in a thread-safe manner. Using
call_once (rather than eager construction in the base class constructor)
preserves virtual dispatch, allowing ItemFactory subclasses to override
nameHandlerMap() and namePropertyMap() correctly.

Both property maps are initialized together in a single once_flag since
nameForPropertyKey is derived from namePropertyMap.
2026-04-04 17:52:54 +02:00
Ryan Francesconi
f32b503f56 Fix bitrate calculation unit errors in ADTS and MP4 ESDS parsers (#1330)
mpegheader.cpp: ADTS bitrate divided by 1024 (binary kilo) instead of
1000 (decimal kilo), causing ~2.4% underreporting for all AAC streams.

mp4properties.cpp: ESDS averageBitrate double-rounded via both +500 and
+0.5 before int cast, causing standard bitrates (128000, 192000, etc.)
to read 1 kbps too high.
2026-04-04 16:34:37 +02:00
Ryan Francesconi
d6a2134cf3 Clamp oversized RIFF chunk to available bytes instead of rejecting it (#1329)
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.
2026-04-04 12:47:49 +02:00
Ryan Francesconi
abadbb6768 Add BEXT and iXML chunk support to WAV files (#1323)
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.
2026-04-04 12:14:34 +02:00
Daniel
49510e7d5a Move MPEG check to end of content-based detection (#1319)
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.
2026-04-04 08:01:41 +02:00
Daniel
7f2f2ddcaf Add tests for FileRef content-based detection via ByteVectorStream (#1318)
This covers all 18 formats supported by the content-based detection.
2026-04-04 07:43:56 +02:00
Urs Fleisch
0368c0239a Pin submodule utfcpp to tag v4.0.9 (#1315)
git submodule init
git submodule update --remote
(cd 3rdparty/utfcpp && git checkout v4.0.9)
git add 3rdparty/utfcpp
git commit -m 'Pin submodule utfcpp to tag v4.0.9'
2026-03-31 20:04:11 +02:00
Felipe
9411bb161f Opus: Read output gain (#1320) 2026-03-31 11:14:44 -05:00
Urs Fleisch
78298769de Version 2.2.1 2026-03-07 06:41:13 +01:00
Urs Fleisch
c43d2b3fc1 Avoid duplicates in StringList Matroska::Tag::complexPropertyKeys()
When using for example

examples/tagwriter -C GENRE \
"name=GENRE,targetTypeValue=50,value=Soft Rock;name=GENRE,targetTypeValue=50,value=Classic Rock" \
path/to/file.mka

the GENRE key was included twice and tagreader displayed the two genre
tags twice.
2026-02-28 07:51:04 +01:00
Urs Fleisch
3db0d48f4b Support edition, chapter and attachment UIDs in Matroska simple tags (#1311) 2026-02-24 06:53:23 +01:00
Urs Fleisch
de6e2b15c8 Version 2.2 2026-02-18 05:15:14 +01:00
Urs Fleisch
b7f0225f0d Do not allow unknown frames embedded in CTOC and CHAP (#1306)
This prevents the parsing of frames with specially constructed recursive
frame hierarchies from taking an extremely long time.
2026-02-15 08:38:14 +01:00
tsdgeos
f4117f873c wavpack: Fix infinite loop when reading broken files (#1304)
A crafted file can have blockSamples set to 0 and a blockSize so big
that when adding 8 it overflows and offset is 0 so it goes back to the
same position and loops forever
2026-02-04 17:31:59 +01:00
Damien Plisson
cf86f20b36 Add DSD information to WavPack properties (#1303) 2026-02-04 17:31:03 +01:00
Urs Fleisch
74d93db166 Update changelog for v2.2beta 2026-02-01 12:08:59 +01:00
Stephen Booth
397b6c1de3 Add check for ID3v2 frame data length (#1300)
Also fix some wrong frame sizes in the unit tests.

---------

Co-authored-by: Urs Fleisch <ufleisch@users.sourceforge.net>
2026-01-31 08:24:54 +01:00
Stephen Booth
51f431c96a Verify the ID3v2 version and revision are not 0xFF (#1301) 2026-01-31 08:21:17 +01:00
Antoine Colombier
11e3eb05bd MP4: Add support for NI STEM (#1299) 2026-01-25 13:33:54 +01:00
Urs Fleisch
2c01b63433 Merge pull request #1149 from complexlogic/matroska
Another Matroska Attempt
2026-01-24 15:27:22 +01:00
Urs Fleisch
d83d751443 Matroska: Fix build with older macOS and 32-bit Android compilers
When building for macOS < 10.14, the API for std::optional and
std::variant is restricted

    error: 'value' is unavailable: introduced in macOS 10.14
    error: 'get<..>' is unavailable: introduced in macOS 10.14

There was also an issue with Android armeabi-v7a where long is not
64 bit and a static assertion failed.
2026-01-24 14:59:10 +01:00
Urs Fleisch
f4e7a742c3 Improve Matroska API
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.
2026-01-24 14:59:10 +01:00
Urs Fleisch
68a514f4a0 Matroska: Avoid inlined header methods, only install public headers 2026-01-24 14:59:10 +01:00
Urs Fleisch
607cd5bdf4 Matroska: Fix issues reported by static analysis, formatting, docs 2026-01-24 14:59:10 +01:00
Urs Fleisch
b625be5690 Support for Matroska chapters 2026-01-24 14:59:10 +01:00
Urs Fleisch
d2bf7e72d2 Unit tests for Matroska 2026-01-24 14:59:10 +01:00
Urs Fleisch
280d6306d2 Update property mapping documentation for Matroska 2026-01-24 14:59:10 +01:00
Urs Fleisch
aef78068b3 Render when tags are modified 2026-01-24 14:59:10 +01:00
Urs Fleisch
81815e1091 Include doc type and version in properties 2026-01-24 14:59:10 +01:00
Urs Fleisch
b9122afaca Preserve track UID in simple tags
Some encoders write track specific DURATION tags, which should not be
removed.
2026-01-24 14:59:10 +01:00
Urs Fleisch
1d3a375765 Use segment title as fallback for returned tag title
Some applications like handbrake store the title only in the segment
info element.
2026-01-24 14:59:10 +01:00
Urs Fleisch
241b3b2921 Remove offset listeners, they are not used 2026-01-24 14:59:10 +01:00
Urs Fleisch
48104959b2 Integrate cues, fix element rendering and resizing 2026-01-24 14:59:10 +01:00
Urs Fleisch
c817cc0a47 tagwriter: Support setting of complex properties
A complex property can be set with

-C <complex-property-key> <key1=val1,key2=val2,...>

The second parameter can be set to "" to delete complex properties with the
given key. The set complex property values, a simple shorthand syntax can be
used. Multiple maps are separated by ';', values within a map are assigned
with key=value and separated by a ','. Types are automatically detected,
double quotes can be used to force a string. A ByteVector can be constructed
from the contents of a file with the path is given after "file://". There is
no escape, but hex codes are supported, e.g. "\x2C" to include a ',' and \x3B
to include a ';'.

Examples:

Set a GEOB frame in an ID3v2 tag:
examples/tagwriter -C GENERALOBJECT \
  'data=file://file.bin,description=My description,fileName=file.bin,mimeType=application/octet-stream' \
  file.mp3

Set an APIC frame in an ID3v2 tag (same as -p file.jpg 'My description'):
examples/tagwriter -C PICTURE \
  'data=file://file.jpg,description=My description,pictureType=Front Cover,mimeType=image/jpeg' \
  file.mp3

Set an attached file in a Matroska file:
examples/tagwriter -C file.bin \
  'fileName=file.bin,data=file://file.bin,mimeType=application/octet-stream' \
  file.mka

Set simple tag with target type in a Matroska file:
examples/tagwriter -C PART_NUMBER \
  name=PART_NUMBER,targetTypeValue=20,value=2 file.mka

Set simple tag with binary value in a Matroska file:
examples/tagwriter -C BINARY \
  name=BINARY,data=file://file.bin,targetTypeValue=60 file.mka
2026-01-24 14:59:10 +01:00
Urs Fleisch
7a5a10102e Set Matroska tags which are not in the property map using complex properties
Also all attached files can be accessed and modified using complex properties.
2026-01-24 14:59:10 +01:00
Urs Fleisch
d47d28f0f8 Fix and simplify Matroska simple tag
Avoid use of raw pointers, fix property interface.
2026-01-24 14:59:10 +01:00
Urs Fleisch
3566b00596 Clean up attachments
Avoid use of raw pointers.
2026-01-24 14:59:10 +01:00
Urs Fleisch
98bc68d16e Implement complex properties for Matroska 2026-01-24 14:59:10 +01:00
Urs Fleisch
8d02484804 Replace raw pointers by smart pointers
Also improve type safety and consistency.
2026-01-24 14:59:10 +01:00
Urs Fleisch
cfade6d082 Implement Matroska audio properties 2026-01-24 14:59:10 +01:00
Urs Fleisch
f7ab162514 Make tags and properties more flexible 2026-01-24 14:59:10 +01:00
Urs Fleisch
5a6f1f96f8 Apply TagLib code formatting, fix static analysis issues 2026-01-24 14:59:10 +01:00
Urs Fleisch
6fa3db1e51 C bindings, fileref, fix warnings 2026-01-24 14:59:10 +01:00
Urs Fleisch
4546c00417 Compile time configuration WITH_MATROSKA 2026-01-24 14:59:10 +01:00
complexlogic
f94843614f Initial cues work 2026-01-24 14:59:10 +01:00
complexlogic
975eaac3ca Fix seekhead 2026-01-24 14:59:10 +01:00
complexlogic
6d019a894c Initial attachments support 2026-01-24 14:59:10 +01:00
complexlogic
6342f00e8b Support tag language 2026-01-24 14:59:10 +01:00
complexlogic
b4e79a4a27 Fix Clang build 2026-01-24 14:59:10 +01:00
complexlogic
80837485cf Added write support 2026-01-24 14:59:10 +01:00
complexlogic
47e9b9a17c Initial matroska support 2026-01-24 14:59:10 +01:00
Stefan Brüns
8c7d3368da Fix reading of last page in ogg stream
When trying to read a packet from the last page the readPages() method
would return false for all packets on the last page.

Move the lastPage check just before trying to create the next page, and
after the packetIndex check of the last fetched page.
2026-01-18 20:08:09 +01:00
Stefan Brüns
b4a4b42254 Avoid corrupting an (invalid) FLAC Ogg file without Vorbis comment
FLAC in Ogg must have a Vorbis comment Metadata block, but in case
the file still has none avoid overwriting the first packet, i.e.
the Streaminfo Metadata block.
2026-01-18 20:08:09 +01:00
Stefan Brüns
70c6a0c75f Set last header flag in FLAC Metadata block type field
In case the Vorbis comment block is the last one the last block flag
has to be set.

As the other metadata blocks are kept as is, and in original order,
and no other metadata blocks are added/inserted, these can be kept
as is.

Also warn if the header scan loop detects a frame sync sequence (which
is the head of the first non-header packet). Previously, this was
detected as the last header packet, but streamStart and streamLength
are unused, and all packets but the vorbis comment packet are just copied
in order.

See taglib#1297
2026-01-18 20:08:09 +01:00
Stefan Brüns
c67e434939 Do not warn when seeing FLAC picture block in Ogg files 2026-01-18 20:08:09 +01:00
Urs Fleisch
5d44f3eeac MP4: Support 64-bit atoms on Windows
The check for the size of long is no longer needed because since
TagLib 2.0 the related fields are 64-bit on Windows too.

Related to https://bugs.kde.org/show_bug.cgi?id=513531
2026-01-11 20:24:33 +01:00
dependabot[bot]
9c042984d2 Bump actions/checkout from 5 to 6 (#1294)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 06:57:09 -06:00
Rosen Penev
49be371caa std::accumulate conversion
Signed-off-by: Rosen Penev <rosenp@gmail.com>
2025-10-25 13:00:24 +02:00
Andreas Sturmlechner
cbb3de6836 Fix cmake_minimum_required version range typo
While CMake appears to accept the ".." notation, it does not correctly
apply the policy_max version in that case.

Amends ab31d11c8d

Signed-off-by: Andreas Sturmlechner <asturm@gentoo.org>
2025-10-22 16:37:21 +02:00
Rosen Penev
11efd2dd10 std::none_of conversion
Signed-off-by: Rosen Penev <rosenp@gmail.com>
2025-10-21 07:17:13 +02:00
Andreas Sturmlechner
ab31d11c8d Raise CMake minimum version to 3.10..3.31 range
CMake 3.10 was released in 2017.

Amends 967aaf7af2, fixes warning since CMake-3.31:

CMake Deprecation Warning at CMakeLists.txt:1 (cmake_minimum_required):
  Compatibility with CMake < 3.10 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
  to tell CMake that the project requires at least <min> but has been updated
  to work with policies introduced by <max> or earlier.

Signed-off-by: Andreas Sturmlechner <asturm@gentoo.org>
2025-10-18 17:00:09 +02:00
Artem Umerov
3c917caf52 Fix std::ostream not defined on Android NDK 29.0.14033849
Fix std::ostream not defined on Android NDK 29.0.14033849
2025-09-04 22:16:59 +02:00
dependabot[bot]
fa96a6458e Bump actions/checkout from 4 to 5 (#1283)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 07:03:51 -05:00
norma
e831f0929f Fix Unicode property keys in C bindings
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).
2025-07-04 09:43:34 +02:00
146 changed files with 15205 additions and 365 deletions

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Ubuntu
run: sudo apt install -y libcppunit-dev libutfcpp-dev zlib1g-dev

View File

@@ -1,3 +1,42 @@
TagLib 2.3 (May 10, 2026)
=========================
* MP4: Support for chapters (Nero and QuickTime).
* WAV: Support for BEXT and iXML chunks.
* FLAC: Support for BEXT and iXML application blocks.
* Opus: New audio property `outputGain()`.
* Speed up Matroska reading by using seek head for element lookup.
* Speed up Matroska writing by offering multiple write style modes.
* More tolerant handling of files with oversized RIFF chunks, zero size ID3v2
frames and Matroska chapters without edition.
* Avoid wrong content-based detection as MPEG files.
* Fix bitrate calculations for MPEG ADTS and MP4 ESDS.
* Fix data race with multi-threaded use of `MP4::ItemFactory`.
* Fix unbounded recursion in EBML/Matroska `MasterElement` and MP4 atoms.
* Limit number of MP4 atoms at top level.
* Fix writing too many offsets when updating MP4 stco/co64 atoms.
* Fix k bounds in Shorten Rice-Golomb coding.
TagLib 2.2.1 (Mar 7, 2026)
==========================
* Support edition, chapter and attachment UIDs in Matroska simple tags.
* Avoid duplicates in Matroska complex property keys.
TagLib 2.2 (Feb 18, 2026)
=========================
* Support for Matroska (MKA, MKV) and WebM files.
* Support for NI STEM in MP4 files.
* New method isDsd() in WavPack Properties.
* Stricter verification of ID3v2 frames.
* Fix setting the last header flag in Ogg FLAC files.
* Fix reading of the last page in Ogg streams.
* Avoid corrupting invalid Ogg FLAC files without Vorbis comment.
* Windows: Support MP4 files with 64-bit atoms.
* Fix use of property keys with non-ASCII characters in C bindings.
* Fix building with Android NDK 29.
TagLib 2.1.1 (June 30, 2025)
============================

View File

@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5.0 FATAL_ERROR)
cmake_minimum_required(VERSION 3.10...3.31)
project(taglib)
@@ -12,7 +12,7 @@ include(CMakePackageConfigHelpers)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
if(APPLE)
option(BUILD_FRAMEWORK "Build an OS X framework" OFF)
option(BUILD_FRAMEWORK "Build a macOS framework" OFF)
if(BUILD_FRAMEWORK)
set(BUILD_SHARED_LIBS ON)
#set(CMAKE_MACOSX_RPATH 1)
@@ -92,14 +92,15 @@ endif()
# Minor version: increase it if you add ABI compatible features.
# Patch version: increase it for bug fix releases.
set(TAGLIB_SOVERSION_MAJOR 2)
set(TAGLIB_SOVERSION_MINOR 1)
set(TAGLIB_SOVERSION_PATCH 1)
set(TAGLIB_SOVERSION_MINOR 3)
set(TAGLIB_SOVERSION_PATCH 0)
include(ConfigureChecks.cmake)
option(WITH_APE "Build with APE, MPC, WavPack" ON)
option(WITH_ASF "Build with ASF" ON)
option(WITH_DSF "Build with DSF" ON)
option(WITH_MATROSKA "Build with Matroska" ON)
option(WITH_MOD "Build with Tracker modules" ON)
option(WITH_MP4 "Build with MP4" ON)
option(WITH_RIFF "Build with AIFF, RIFF, WAV" ON)
@@ -197,6 +198,9 @@ endif()
if(WITH_DSF)
set(TAGLIB_WITH_DSF TRUE)
endif()
if(WITH_MATROSKA)
set(TAGLIB_WITH_MATROSKA TRUE)
endif()
if(WITH_MOD)
set(TAGLIB_WITH_MOD TRUE)
endif()

View File

@@ -63,12 +63,13 @@ and ID3 tags cannot be disabled. The following CMake options are available:
| `WITH_APE` | Build with APE, MPC, WavPack (default ON) |
| `WITH_ASF` | Build with ASF (default ON) |
| `WITH_DSF` | Build with DSF (default ON) |
| `WITH_MATROSKA` | Build with Matroska, WebM (default ON) |
| `WITH_MOD` | Build with Tracker modules (default ON) |
| `WITH_MP4` | Build with MP4 (default ON) |
| `WITH_RIFF` | Build with AIFF, RIFF, WAV (default ON) |
| `WITH_SHORTEN` | Build with Shorten (default ON) |
| `WITH_TRUEAUDIO` | Build with TrueAudio (default ON) |
| `WITH_VORBIS` | Build with Vorbis, FLAC, Ogg, Opus (default ON) |
| `WITH_VORBIS` | Build with FLAC, Ogg, Opus, Speex (default ON) |
Note that disabling formats will remove exported symbols from the library and
thus break binary compatibility. These options should therefore only be used

View File

@@ -7,10 +7,11 @@
https://taglib.org/
TagLib is a library for reading and editing the metadata of several
popular audio formats. Currently, it supports both ID3v1 and ID3v2
for MP3 files, [Ogg Vorbis][] comments and ID3 tags
in [FLAC][], MPC, Speex, WavPack, TrueAudio, WAV, AIFF, MP4, APE, ASF,
DSF, DFF and AAC files.
popular audio formats. Currently, it supports various metadata containers such
as [ID3v1][], [ID3v2][] and [Vorbis][] comments for MP3, MP4, AAC,
[Ogg][], [Opus][], [FLAC][], [Speex][], [APE][], [MPC][], [WavPack][],
[WAV][], [AIFF][], [TrueAudio][], [Matroska][], [WebM][], ASF, WMA, DSF, DFF and
tracker (MOD, XM, S3M, IT) files.
TagLib is distributed under the [GNU Lesser General Public License][]
(LGPL) and [Mozilla Public License][] (MPL). Essentially that means that
@@ -18,7 +19,20 @@ it may be used in proprietary applications, but if changes are made to
TagLib they must be contributed back to the project. Please review the
licenses if you are considering using TagLib in your project.
[Ogg Vorbis]: https://xiph.org/vorbis/
[ID3v1]: https://id3.org/ID3v1
[ID3v2]: https://id3.org/Home
[Vorbis]: https://xiph.org/vorbis/
[Ogg]: https://www.xiph.org/ogg/
[Opus]: https://opus-codec.org/
[FLAC]: https://xiph.org/flac/
[Speex]: https://www.speex.org/
[APE]: https://www.monkeysaudio.com/
[MPC]: https://musepack.net/
[WavPack]: https://www.wavpack.com/
[WAV]: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
[AIFF]: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/AIFF.html
[TrueAudio]: https://sourceforge.net/projects/tta/
[Matroska]: https://www.matroska.org/
[WebM]: https://www.webmproject.org/
[GNU Lesser General Public License]: https://www.gnu.org/licenses/lgpl.html
[Mozilla Public License]: https://www.mozilla.org/MPL/MPL-1.1.html

View File

@@ -63,6 +63,12 @@ if(WITH_SHORTEN)
${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/shorten
)
endif()
if(WITH_MATROSKA)
set(tag_c_HDR_DIRS ${tag_c_HDR_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/matroska
${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/matroska/ebml
)
endif()
include_directories(${tag_c_HDR_DIRS})
set(tag_c_HDRS tag_c.h)

View File

@@ -80,6 +80,9 @@
#ifdef TAGLIB_WITH_SHORTEN
#include "shortenfile.h"
#endif
#ifdef TAGLIB_WITH_MATROSKA
#include "matroskafile.h"
#endif
using namespace TagLib;
@@ -241,6 +244,11 @@ TagLib_File *taglib_file_new_type_any_char(const T *filename, TagLib_File_Type t
case TagLib_File_SHORTEN:
file = new Shorten::File(filename);
break;
#endif
#ifdef TAGLIB_WITH_MATROSKA
case TagLib_File_MATROSKA:
file = new Matroska::File(filename);
break;
#endif
default:
break;
@@ -469,12 +477,13 @@ void _taglib_property_set(TagLib_File *file, const char* prop, const char* value
return;
auto tfile = reinterpret_cast<FileRef *>(file);
auto propStr = charArrayToString(prop);
PropertyMap map = tfile->tag()->properties();
if(value) {
auto property = map.find(prop);
auto property = map.find(propStr);
if(property == map.end()) {
map.insert(prop, StringList(charArrayToString(value)));
map.insert(propStr, StringList(charArrayToString(value)));
}
else {
if(append) {
@@ -486,7 +495,7 @@ void _taglib_property_set(TagLib_File *file, const char* prop, const char* value
}
}
else {
map.erase(prop);
map.erase(propStr);
}
tfile->setProperties(map);
@@ -531,7 +540,7 @@ char **taglib_property_get(const TagLib_File *file, const char *prop)
const PropertyMap map = reinterpret_cast<const FileRef *>(file)->properties();
auto property = map.find(prop);
auto property = map.find(charArrayToString(prop));
if(property == map.end())
return NULL;
@@ -573,16 +582,17 @@ bool _taglib_complex_property_set(
return false;
auto tfile = reinterpret_cast<FileRef *>(file);
auto keyStr = charArrayToString(key);
if(value == NULL) {
return tfile->setComplexProperties(key, {});
return tfile->setComplexProperties(keyStr, {});
}
VariantMap map;
const TagLib_Complex_Property_Attribute** attrPtr = value;
while(*attrPtr) {
const TagLib_Complex_Property_Attribute *attr = *attrPtr;
String attrKey(attr->key);
auto attrKey = charArrayToString(attr->key);
switch(attr->value.type) {
case TagLib_Variant_Void:
map.insert(attrKey, Variant());
@@ -627,8 +637,8 @@ bool _taglib_complex_property_set(
++attrPtr;
}
return append ? tfile->setComplexProperties(key, tfile->complexProperties(key).append(map))
: tfile->setComplexProperties(key, {map});
return append ? tfile->setComplexProperties(keyStr, tfile->complexProperties(keyStr).append(map))
: tfile->setComplexProperties(keyStr, {map});
}
} // namespace
@@ -676,7 +686,7 @@ TagLib_Complex_Property_Attribute*** taglib_complex_property_get(
return NULL;
}
const auto variantMaps = reinterpret_cast<const FileRef *>(file)->complexProperties(key);
const auto variantMaps = reinterpret_cast<const FileRef *>(file)->complexProperties(charArrayToString(key));
if(variantMaps.isEmpty()) {
return NULL;
}

View File

@@ -132,7 +132,8 @@ typedef enum {
TagLib_File_Opus,
TagLib_File_DSF,
TagLib_File_DSDIFF,
TagLib_File_SHORTEN
TagLib_File_SHORTEN,
TagLib_File_MATROSKA
} TagLib_File_Type;
/*!

View File

@@ -2,6 +2,7 @@ include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/../taglib
${CMAKE_CURRENT_SOURCE_DIR}/../taglib/toolkit
${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ape
${CMAKE_CURRENT_SOURCE_DIR}/../taglib/matroska
${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg
${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v1
${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v2
@@ -38,6 +39,18 @@ target_link_libraries(framelist tag)
add_executable(strip-id3v1 strip-id3v1.cpp)
target_link_libraries(strip-id3v1 tag)
if(WITH_MATROSKA)
########### next target ###############
add_executable(matroskareader matroskareader.cpp)
target_link_libraries(matroskareader tag)
########### next target ###############
add_executable(matroskawriter matroskawriter.cpp)
target_link_libraries(matroskawriter tag)
endif()
install(TARGETS tagreader tagreader_c tagwriter framelist strip-id3v1
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}

139
examples/matroskareader.cpp Normal file
View File

@@ -0,0 +1,139 @@
#include <cstdio>
#include "matroskafile.h"
#include "matroskatag.h"
#include "matroskasimpletag.h"
#include "matroskaattachments.h"
#include "matroskaattachedfile.h"
#include "matroskachapters.h"
#include "matroskachapteredition.h"
#include "tstring.h"
#include "tutils.h"
#include "tbytevector.h"
#define GREEN_TEXT(s) "" s ""
#define PRINT_PRETTY(label, value) printf(" " GREEN_TEXT(label) ": %s\n", value)
int main(int argc, char *argv[])
{
if(argc != 2) {
printf("Usage: matroskareader FILE\n");
return 1;
}
TagLib::Matroska::File file(argv[1]);
if(!file.isValid()) {
printf("File is not valid\n");
return 1;
}
auto tag = dynamic_cast<TagLib::Matroska::Tag*>(file.tag());
if(!tag) {
printf("File has no tag\n");
return 0;
}
const TagLib::Matroska::SimpleTagsList &list = tag->simpleTagsList();
printf("Found %u tag(s):\n", list.size());
for(const TagLib::Matroska::SimpleTag &t : list) {
PRINT_PRETTY("Tag Name", t.name().toCString(true));
if(t.type() == TagLib::Matroska::SimpleTag::StringType)
PRINT_PRETTY("Tag Value", t.toString().toCString(true));
else if(t.type() == TagLib::Matroska::SimpleTag::BinaryType)
PRINT_PRETTY("Tag Value",
TagLib::Utils::formatString("Binary with size %i", t.toByteVector().size()).toCString(false)
);
auto targetTypeValue = static_cast<unsigned int>(t.targetTypeValue());
PRINT_PRETTY("Target Type Value",
targetTypeValue == 0 ? "None" : TagLib::Utils::formatString("%i", targetTypeValue).toCString(false)
);
if(auto trackUid = t.trackUid()) {
PRINT_PRETTY("Track UID",
TagLib::Utils::formatString("%llu",trackUid).toCString(false)
);
}
if(auto editionUid = t.editionUid()) {
PRINT_PRETTY("Edition UID",
TagLib::Utils::formatString("%llu",editionUid).toCString(false)
);
}
if(auto chapterUid = t.chapterUid()) {
PRINT_PRETTY("Chapter UID",
TagLib::Utils::formatString("%llu",chapterUid).toCString(false)
);
}
if(auto attachmentUid = t.attachmentUid()) {
PRINT_PRETTY("Attachment UID",
TagLib::Utils::formatString("%llu",attachmentUid).toCString(false)
);
}
const TagLib::String &language = t.language();
PRINT_PRETTY("Language", !language.isEmpty() ? language.toCString(false) : "Not set");
printf("\n");
}
TagLib::Matroska::Attachments *attachments = file.attachments();
if(attachments) {
const TagLib::Matroska::Attachments::AttachedFileList &list = attachments->attachedFileList();
printf("Found %u attachment(s)\n", list.size());
for(const auto &attachedFile : list) {
PRINT_PRETTY("Filename", attachedFile.fileName().toCString(true));
const TagLib::String &description = attachedFile.description();
PRINT_PRETTY("Description", !description.isEmpty() ? description.toCString(true) : "None");
const TagLib::String &mediaType = attachedFile.mediaType();
PRINT_PRETTY("Media Type", !mediaType.isEmpty() ? mediaType.toCString(false) : "None");
PRINT_PRETTY("Data Size",
TagLib::Utils::formatString("%u byte(s)", attachedFile.data().size()).toCString(false)
);
PRINT_PRETTY("UID",
TagLib::Utils::formatString("%llu", attachedFile.uid()).toCString(false)
);
}
}
else
printf("File has no attachments\n");
TagLib::Matroska::Chapters *chapters = file.chapters();
if(chapters) {
printf("Chapters:\n");
const TagLib::Matroska::Chapters::ChapterEditionList &editions = chapters->chapterEditionList();
for(const auto &edition : editions) {
if(edition.uid()) {
PRINT_PRETTY("Edition UID", TagLib::Utils::formatString("%llu", edition.uid())
.toCString(false));
}
PRINT_PRETTY("Edition Flags", TagLib::Utils::formatString("default=%d, ordered=%d",
edition.isDefault(), edition.isOrdered())
.toCString(false));
printf("\n");
for(const auto &chapter : edition.chapterList()) {
PRINT_PRETTY("Chapter UID", TagLib::Utils::formatString("%llu", chapter.uid())
.toCString(false));
PRINT_PRETTY("Chapter flags", TagLib::Utils::formatString("hidden=%d", chapter.isHidden())
.toCString(false));
PRINT_PRETTY("Start-End", TagLib::Utils::formatString("%llu-%llu",
chapter.timeStart(), chapter.timeEnd()).toCString(false));
for(const auto &display : chapter.displayList()) {
PRINT_PRETTY("Display", display.string().toCString(false));
PRINT_PRETTY("Language", !display.language().isEmpty()
? display.language().toCString(false) : "Not set");
}
printf("\n");
}
}
}
else
printf("File has no chapters\n");
if(auto properties = dynamic_cast<const TagLib::Matroska::Properties *>(file.audioProperties())) {
printf("Properties:\n");
PRINT_PRETTY("Doc Type", properties->docType().toCString(false));
PRINT_PRETTY("Doc Type Version", TagLib::String::number(properties->docTypeVersion()).toCString(false));
PRINT_PRETTY("Codec Name", properties->codecName().toCString(true));
PRINT_PRETTY("Bitrate", TagLib::String::number(properties->bitrate()).toCString(false));
PRINT_PRETTY("Sample Rate", TagLib::String::number(properties->sampleRate()).toCString(false));
PRINT_PRETTY("Channels", TagLib::String::number(properties->channels()).toCString(false));
PRINT_PRETTY("Length [ms]", TagLib::String::number(properties->lengthInMilliseconds()).toCString(false));
}
return 0;
}

View File

@@ -0,0 +1,45 @@
#include <cstdio>
#include "matroskafile.h"
#include "matroskatag.h"
#include "matroskasimpletag.h"
#include "matroskaattachments.h"
#include "matroskaattachedfile.h"
#include "tfilestream.h"
#include "tstring.h"
#include "tutils.h"
int main(int argc, char *argv[])
{
if(argc != 3) {
printf("Usage: matroskawriter FILE ARTWORK\n");
return 1;
}
TagLib::Matroska::File file(argv[1]);
if(!file.isValid()) {
printf("File is not valid\n");
return 1;
}
auto tag = file.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(TagLib::Matroska::SimpleTag(
"Test Name 1", TagLib::String("Test Value 1"),
TagLib::Matroska::SimpleTag::TargetTypeValue::Track, "en"));
tag->addSimpleTag(TagLib::Matroska::SimpleTag(
"Test Name 2", TagLib::String("Test Value 2"),
TagLib::Matroska::SimpleTag::TargetTypeValue::Album));
tag->setTitle("Test title");
tag->setArtist("Test artist");
tag->setYear(1969);
TagLib::FileStream image(argv[2]);
auto data = image.readBlock(image.length());
auto attachments = file.attachments(true);
attachments->addAttachedFile(TagLib::Matroska::AttachedFile(
data, "cover.jpg", "image/jpeg"));
file.save();
return 0;
}

View File

@@ -117,4 +117,3 @@ int main(int argc, char *argv[])
}
return 0;
}

View File

@@ -71,6 +71,7 @@ void usage()
std::cout << " -R <tagname> <tagvalue>" << std::endl;
std::cout << " -I <tagname> <tagvalue>" << std::endl;
std::cout << " -D <tagname>" << std::endl;
std::cout << " -C <complex-property-key> <key1=val1,key2=val2,...>" << std::endl;
std::cout << " -p <picturefile> <description> (\"\" \"\" to remove)" << std::endl;
std::cout << std::endl;
@@ -95,6 +96,102 @@ void checkForRejectedProperties(const TagLib::PropertyMap &tags)
}
}
/*!
* Create a list of variant maps from a string.
* The shorthand syntax in the string is kept simple, but should be sufficient
* for testing. Multiple maps are separated by ';', values within a map are
* assigned with key=value and separated by a ','. Types are detected, use
* double quotes to force a string. A ByteVector can be constructed from the
* contents of a file, the path is given after "file://". There is no escape
* character, use hex codes for ',' (\x2C) or ';' (\x3B).
*/
TagLib::List<TagLib::VariantMap> parseComplexPropertyValues(const TagLib::String &str)
{
if(str.isEmpty() || str == "\"\"" || str == "''") {
return {};
}
TagLib::List<TagLib::VariantMap> values;
const auto valueStrs = str.split(";");
for(const auto &valueStr : valueStrs) {
TagLib::VariantMap value;
const auto keyValStrs = valueStr.split(",");
for(const auto &keyValStr : keyValStrs) {
if(int equalPos = keyValStr.find('='); equalPos != -1) {
TagLib::String key = keyValStr.substr(0, equalPos);
TagLib::String valStr = keyValStr.substr(equalPos + 1);
bool hasDot = false;
bool hasNonNumeric = false;
bool hasSign = false;
for(auto it = valStr.cbegin(); it != valStr.cend(); ++it) {
if(it == valStr.cbegin() && (*it == '-' || *it == '+')) {
hasSign = true;
}
else if(*it == '.') {
hasDot = true;
}
else if(*it < '0' || *it > '9') {
hasNonNumeric = true;
}
}
TagLib::Variant val;
if(valStr == "null") {
// keep empty variant
}
else if(valStr == "true" || valStr == "false") {
val = TagLib::Variant(valStr == "true");
}
else if(!hasNonNumeric && hasDot) {
val = TagLib::Variant(std::stod(valStr.to8Bit()));
}
else if(!hasNonNumeric && hasSign) {
val = valStr.toLongLong(nullptr);
}
else if(!hasNonNumeric) {
val = valStr.toULongLong(nullptr);
}
else if(valStr.startsWith("file://")) {
auto filePath = valStr.substr(7 );
if(isFile(filePath.toCString())) {
std::ifstream fs;
fs.open(filePath.toCString(), std::ios::in | std::ios::binary);
std::stringstream buffer;
buffer << fs.rdbuf();
fs.close();
TagLib::String buf(buffer.str());
val = TagLib::Variant(buf.data(TagLib::String::Latin1));
}
else {
std::cout << filePath.toCString() << " not found." << std::endl;
val = TagLib::Variant(TagLib::ByteVector());
}
}
else {
int len = valStr.size();
if(len >= 2 && valStr[0] == '"' && valStr[len - 1] == '"') {
valStr = valStr.substr(1, len - 2);
}
int hexPos = 0;
while((hexPos = valStr.find("\\x", hexPos)) != -1) {
char ch;
bool ok;
if(static_cast<int>(valStr.length()) < hexPos + 4 ||
(ch = static_cast<char>(
valStr.substr(hexPos + 2, 2).toLongLong(&ok, 16)), !ok)) {
break;
}
valStr = valStr.substr(0, hexPos) + ch + valStr.substr(hexPos + 4);
++hexPos;
}
val = TagLib::Variant(valStr);
}
value.insert(key, val);
}
}
values.append(value);
}
return values;
}
int main(int argc, char *argv[])
{
TagLib::List<TagLib::FileRef> fileList;
@@ -170,6 +267,19 @@ int main(int argc, char *argv[])
checkForRejectedProperties(f.setProperties(map));
break;
}
case 'C': {
if(i + 2 < argc) {
numArgsConsumed = 3;
if(!value.isEmpty()) {
TagLib::List<TagLib::VariantMap> values = parseComplexPropertyValues(argv[i + 2]);
f.setComplexProperties(value, values);
}
}
else {
usage();
}
break;
}
case 'p': {
if(i + 2 < argc) {
numArgsConsumed = 3;

View File

@@ -64,8 +64,15 @@ if(WITH_SHORTEN)
${CMAKE_CURRENT_SOURCE_DIR}/shorten
)
endif()
if(WITH_MATROSKA)
set(tag_HDR_DIRS ${tag_HDR_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/matroska
${CMAKE_CURRENT_SOURCE_DIR}/matroska/ebml
)
endif()
include_directories(${tag_HDR_DIRS})
set(tag_PRIVATE_HDRS)
set(tag_HDRS
tag.h
fileref.h
@@ -187,7 +194,12 @@ if(WITH_MP4)
mp4/mp4item.h
mp4/mp4properties.h
mp4/mp4coverart.h
mp4/mp4stem.h
mp4/mp4itemfactory.h
mp4/mp4chapter.h
mp4/mp4chapterholder.h
mp4/mp4nerochapterlist.h
mp4/mp4qtchapterlist.h
)
endif()
if(WITH_MOD)
@@ -220,6 +232,42 @@ if(WITH_SHORTEN)
shorten/shortentag.h
)
endif()
if(WITH_MATROSKA)
set(tag_HDRS ${tag_HDRS}
matroska/matroskaattachedfile.h
matroska/matroskaattachments.h
matroska/matroskachapter.h
matroska/matroskachapteredition.h
matroska/matroskachapters.h
matroska/matroskaelement.h
matroska/matroskafile.h
matroska/matroskaproperties.h
matroska/matroskasimpletag.h
matroska/matroskatag.h
matroska/matroskawritestyle.h
)
set(tag_PRIVATE_HDRS ${tag_PRIVATE_HDRS}
matroska/ebml/ebmlbinaryelement.h
matroska/ebml/ebmlelement.h
matroska/ebml/ebmlmasterelement.h
matroska/ebml/ebmlmkattachments.h
matroska/ebml/ebmlmkchapters.h
matroska/ebml/ebmlmkcues.h
matroska/ebml/ebmlmkseekhead.h
matroska/ebml/ebmlmksegment.h
matroska/ebml/ebmlmkinfo.h
matroska/ebml/ebmlmktracks.h
matroska/ebml/ebmlmktags.h
matroska/ebml/ebmlstringelement.h
matroska/ebml/ebmluintelement.h
matroska/ebml/ebmlfloatelement.h
matroska/ebml/ebmlutils.h
matroska/ebml/ebmlvoidelement.h
matroska/matroskacues.h
matroska/matroskaseekhead.h
matroska/matroskasegment.h
)
endif()
set(mpeg_SRCS
mpeg/mpegfile.cpp
@@ -327,7 +375,11 @@ if(WITH_MP4)
mp4/mp4item.cpp
mp4/mp4properties.cpp
mp4/mp4coverart.cpp
mp4/mp4stem.cpp
mp4/mp4itemfactory.cpp
mp4/mp4chapter.cpp
mp4/mp4nerochapterlist.cpp
mp4/mp4qtchapterlist.cpp
)
endif()
@@ -410,6 +462,43 @@ if(WITH_SHORTEN)
)
endif()
if(WITH_MATROSKA)
set(matroska_SRCS
matroska/matroskaattachedfile.cpp
matroska/matroskaattachments.cpp
matroska/matroskachapter.cpp
matroska/matroskachapteredition.cpp
matroska/matroskachapters.cpp
matroska/matroskacues.cpp
matroska/matroskaelement.cpp
matroska/matroskafile.cpp
matroska/matroskaproperties.cpp
matroska/matroskaseekhead.cpp
matroska/matroskasegment.cpp
matroska/matroskasimpletag.cpp
matroska/matroskatag.cpp
)
set(ebml_SRCS
matroska/ebml/ebmlbinaryelement.cpp
matroska/ebml/ebmlelement.cpp
matroska/ebml/ebmlmasterelement.cpp
matroska/ebml/ebmlmkattachments.cpp
matroska/ebml/ebmlmkchapters.cpp
matroska/ebml/ebmlmkcues.cpp
matroska/ebml/ebmlmkseekhead.cpp
matroska/ebml/ebmlmksegment.cpp
matroska/ebml/ebmlmkinfo.cpp
matroska/ebml/ebmlmktracks.cpp
matroska/ebml/ebmlmktags.cpp
matroska/ebml/ebmlstringelement.cpp
matroska/ebml/ebmluintelement.cpp
matroska/ebml/ebmlfloatelement.cpp
matroska/ebml/ebmlutils.cpp
matroska/ebml/ebmlvoidelement.cpp
)
endif()
set(toolkit_SRCS
toolkit/tstring.cpp
toolkit/tstringlist.cpp
@@ -433,7 +522,7 @@ set(tag_LIB_SRCS
${vorbis_SRCS} ${oggflacs_SRCS} ${mpc_SRCS} ${ape_SRCS} ${toolkit_SRCS} ${flacs_SRCS}
${wavpack_SRCS} ${speex_SRCS} ${trueaudio_SRCS} ${riff_SRCS} ${aiff_SRCS} ${wav_SRCS}
${asf_SRCS} ${mp4_SRCS} ${mod_SRCS} ${s3m_SRCS} ${it_SRCS} ${xm_SRCS} ${opus_SRCS}
${dsf_SRCS} ${dsdiff_SRCS} ${shorten_SRCS}
${dsf_SRCS} ${dsdiff_SRCS} ${shorten_SRCS} ${matroska_SRCS} ${ebml_SRCS}
tag.cpp
tagunion.cpp
fileref.cpp
@@ -441,7 +530,7 @@ set(tag_LIB_SRCS
tagutils.cpp
)
add_library(tag ${tag_LIB_SRCS} ${tag_HDRS})
add_library(tag ${tag_LIB_SRCS} ${tag_HDRS} ${tag_PRIVATE_HDRS})
target_include_directories(tag INTERFACE
$<INSTALL_INTERFACE:include>

View File

@@ -172,7 +172,7 @@ namespace TagLib {
void setReadOnly(bool readOnly);
/*!
* Return \c true if the item is read-only.
* Returns \c true if the item is read-only.
*/
bool isReadOnly() const;

View File

@@ -65,12 +65,8 @@ namespace
if(name.size() != 4)
return false;
for(int i = 0; i < 4; i++) {
if(name[i] < 32 || name[i] > 126)
return false;
}
return true;
return std::none_of(name.cbegin(), name.cend(),
[](unsigned char c) { return c < 32 || c > 126; });
}
enum {
@@ -316,7 +312,7 @@ void DSDIFF::File::removeRootChunk(unsigned int i)
unsigned long long chunkSize = d->chunks[i].size + d->chunks[i].padding + 12;
d->size -= chunkSize;
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
removeBlock(d->chunks[i].offset - 12, chunkSize);
@@ -350,7 +346,7 @@ void DSDIFF::File::setRootChunkData(unsigned int i, const ByteVector &data)
// First we update the global size
d->size += ((data.size() + 1) & ~1) - (d->chunks[i].size + d->chunks[i].padding);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
// Now update the specific chunk
@@ -387,7 +383,7 @@ void DSDIFF::File::setRootChunkData(const ByteVector &name, const ByteVector &da
// First we update the global size
d->size += (offset & 1) + ((data.size() + 1) & ~1) + 12;
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
// Now add the chunk to the file
const unsigned long long fileLength = length();
@@ -414,12 +410,12 @@ void DSDIFF::File::removeChildChunk(unsigned int i, unsigned int childChunkNum)
unsigned long long removedChunkTotalSize = childChunks[i].size + childChunks[i].padding + 12;
d->size -= removedChunkTotalSize;
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
// Update child chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size -= removedChunkTotalSize;
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
insert(ByteVector::fromULongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
// Remove the chunk
@@ -466,13 +462,13 @@ void DSDIFF::File::setChildChunkData(unsigned int i,
d->size += ((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
// And the PROP chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size +=
((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
insert(ByteVector::fromULongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
@@ -542,13 +538,13 @@ void DSDIFF::File::setChildChunkData(const ByteVector &name,
// First we update the global size
d->size += (offset & 1) + ((data.size() + 1) & ~1) + 12;
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
// And the child chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size += (offset & 1)
+ ((data.size() + 1) & ~1) + 12;
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
insert(ByteVector::fromULongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
@@ -610,14 +606,14 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
bool bigEndian = d->endianness == BigEndian;
d->type = readBlock(4);
d->size = readBlock(8).toLongLong(bigEndian);
d->size = readBlock(8).toULongLong(bigEndian);
d->format = readBlock(4);
// + 12: chunk header at least, fix for additional junk bytes
while(tell() + 12 <= length()) {
ByteVector chunkName = readBlock(4);
unsigned long long chunkSize = readBlock(8).toLongLong(bigEndian);
unsigned long long chunkSize = readBlock(8).toULongLong(bigEndian);
if(!isValidChunkID(chunkName)) {
debug("DSDIFF::File::read() -- Chunk '" + chunkName + "' has invalid ID");
@@ -670,14 +666,14 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
}
else if(d->chunks[i].name == "DST ") {
// Now decode the chunks inside the DST chunk to read the DST Frame Information one
long long dstChunkEnd = d->chunks[i].offset + d->chunks[i].size;
unsigned long long dstChunkEnd = d->chunks[i].offset + d->chunks[i].size;
seek(d->chunks[i].offset);
audioDataSizeinBytes = d->chunks[i].size;
while(tell() + 12 <= dstChunkEnd) {
while(static_cast<unsigned long long>(tell()) + 12 <= dstChunkEnd) {
ByteVector dstChunkName = readBlock(4);
long long dstChunkSize = readBlock(8).toLongLong(bigEndian);
unsigned long long dstChunkSize = readBlock(8).toULongLong(bigEndian);
if(!isValidChunkID(dstChunkName)) {
debug("DSDIFF::File::read() -- DST Chunk '" + dstChunkName + "' has invalid ID");
@@ -685,7 +681,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
break;
}
if(static_cast<long long>(tell()) + dstChunkSize > dstChunkEnd) {
if(tell() + dstChunkSize > dstChunkEnd) {
debug("DSDIFF::File::read() -- DST Chunk '" + dstChunkName
+ "' has invalid size (larger than the DST chunk)");
setValid(false);
@@ -712,14 +708,14 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
}
}
else if(d->chunks[i].name == "PROP") {
d->childChunkIndex[PROPChunk] = i;
d->childChunkIndex[PROPChunk] = static_cast<int>(i);
// Now decodes the chunks inside the PROP chunk
long long propChunkEnd = d->chunks[i].offset + d->chunks[i].size;
unsigned long long propChunkEnd = d->chunks[i].offset + d->chunks[i].size;
// +4 to remove the 'SND ' marker at beginning of 'PROP' chunk
seek(d->chunks[i].offset + 4);
while(tell() + 12 <= propChunkEnd) {
while(static_cast<unsigned long long>(tell()) + 12 <= propChunkEnd) {
ByteVector propChunkName = readBlock(4);
long long propChunkSize = readBlock(8).toLongLong(bigEndian);
unsigned long long propChunkSize = readBlock(8).toULongLong(bigEndian);
if(!isValidChunkID(propChunkName)) {
debug("DSDIFF::File::read() -- PROP Chunk '" + propChunkName + "' has invalid ID");
@@ -727,7 +723,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
break;
}
if(static_cast<long long>(tell()) + propChunkSize > propChunkEnd) {
if(tell() + propChunkSize > propChunkEnd) {
debug("DSDIFF::File::read() -- PROP Chunk '" + propChunkName
+ "' has invalid size (larger than the PROP chunk)");
setValid(false);
@@ -755,17 +751,17 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
}
}
else if(d->chunks[i].name == "DIIN") {
d->childChunkIndex[DIINChunk] = i;
d->childChunkIndex[DIINChunk] = static_cast<int>(i);
d->hasDiin = true;
// Now decode the chunks inside the DIIN chunk
long long diinChunkEnd = d->chunks[i].offset + d->chunks[i].size;
unsigned long long diinChunkEnd = d->chunks[i].offset + d->chunks[i].size;
seek(d->chunks[i].offset);
while(tell() + 12 <= diinChunkEnd) {
while(static_cast<unsigned long long>(tell()) + 12 <= diinChunkEnd) {
ByteVector diinChunkName = readBlock(4);
long long diinChunkSize = readBlock(8).toLongLong(bigEndian);
unsigned long long diinChunkSize = readBlock(8).toULongLong(bigEndian);
if(!isValidChunkID(diinChunkName)) {
debug("DSDIFF::File::read() -- DIIN Chunk '" + diinChunkName + "' has invalid ID");
@@ -773,7 +769,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
break;
}
if(static_cast<long long>(tell()) + diinChunkSize > diinChunkEnd) {
if(tell() + diinChunkSize > diinChunkEnd) {
debug("DSDIFF::File::read() -- DIIN Chunk '" + diinChunkName
+ "' has invalid size (larger than the DIIN chunk)");
setValid(false);
@@ -829,7 +825,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
if(d->childChunks[PROPChunk][i].name == "ID3 " ||
d->childChunks[PROPChunk][i].name == "id3 ") {
if(d->hasID3v2) {
d->duplicateID3V2chunkIndex = i;
d->duplicateID3V2chunkIndex = static_cast<int>(i);
// ID3V2 tag has already been found at root level
continue;
}
@@ -917,7 +913,7 @@ void DSDIFF::File::writeChunk(const ByteVector &name, const ByteVector &data,
combined.append(ByteVector(leadingPadding, '\x00'));
combined.append(name);
combined.append(ByteVector::fromLongLong(data.size(), d->endianness == BigEndian));
combined.append(ByteVector::fromULongLong(data.size(), d->endianness == BigEndian));
combined.append(data);
if((data.size() & 0x01) != 0)
combined.append('\x00');

View File

@@ -46,8 +46,8 @@ public:
FilePrivate &operator=(const FilePrivate &) = delete;
const ID3v2::FrameFactory *ID3v2FrameFactory;
long long fileSize = 0;
long long metadataOffset = 0;
unsigned long long fileSize = 0;
unsigned long long metadataOffset = 0;
std::unique_ptr<Properties> properties;
std::unique_ptr<ID3v2::Tag> tag;
};
@@ -116,17 +116,17 @@ bool DSF::File::save(ID3v2::Version version)
// Three things must be updated: the file size, the tag data, and the metadata offset
if(d->tag->isEmpty()) {
long long newFileSize = d->metadataOffset ? d->metadataOffset : d->fileSize;
unsigned long long newFileSize = d->metadataOffset ? d->metadataOffset : d->fileSize;
// Update the file size
if(d->fileSize != newFileSize) {
insert(ByteVector::fromLongLong(newFileSize, false), 12, 8);
insert(ByteVector::fromULongLong(newFileSize, false), 12, 8);
d->fileSize = newFileSize;
}
// Update the metadata offset to 0 since there is no longer a tag
if(d->metadataOffset) {
insert(ByteVector::fromLongLong(0ULL, false), 20, 8);
insert(ByteVector::fromULongLong(0ULL, false), 20, 8);
d->metadataOffset = 0;
}
@@ -136,19 +136,19 @@ bool DSF::File::save(ID3v2::Version version)
else {
ByteVector tagData = d->tag->render(version);
long long newMetadataOffset = d->metadataOffset ? d->metadataOffset : d->fileSize;
long long newFileSize = newMetadataOffset + tagData.size();
long long oldTagSize = d->fileSize - newMetadataOffset;
unsigned long long newMetadataOffset = d->metadataOffset ? d->metadataOffset : d->fileSize;
unsigned long long newFileSize = newMetadataOffset + tagData.size();
unsigned long long oldTagSize = d->fileSize - newMetadataOffset;
// Update the file size
if(d->fileSize != newFileSize) {
insert(ByteVector::fromLongLong(newFileSize, false), 12, 8);
insert(ByteVector::fromULongLong(newFileSize, false), 12, 8);
d->fileSize = newFileSize;
}
// Update the metadata offset
if(d->metadataOffset != newMetadataOffset) {
insert(ByteVector::fromLongLong(newMetadataOffset, false), 20, 8);
insert(ByteVector::fromULongLong(newMetadataOffset, false), 20, 8);
d->metadataOffset = newMetadataOffset;
}
@@ -175,7 +175,7 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
long long dsdHeaderSize = readBlock(8).toLongLong(false);
unsigned long long dsdHeaderSize = readBlock(8).toULongLong(false);
// Integrity check
if(dsdHeaderSize != 28) {
@@ -184,16 +184,16 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
d->fileSize = readBlock(8).toLongLong(false);
d->fileSize = readBlock(8).toULongLong(false);
// File is malformed or corrupted, allow trailing garbage
if(d->fileSize > length()) {
if(d->fileSize > static_cast<unsigned long long>(length())) {
debug("DSF::File::read() -- File is corrupted wrong length");
setValid(false);
return;
}
d->metadataOffset = readBlock(8).toLongLong(false);
d->metadataOffset = readBlock(8).toULongLong(false);
// File is malformed or corrupted
if(d->metadataOffset > d->fileSize) {
@@ -210,7 +210,7 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
long long fmtHeaderSize = readBlock(8).toLongLong(false);
unsigned long long fmtHeaderSize = readBlock(8).toULongLong(false);
if(fmtHeaderSize != 52) {
debug("DSF::File::read() -- File is corrupted, wrong FMT header size");
setValid(false);

View File

@@ -77,6 +77,9 @@
#ifdef TAGLIB_WITH_SHORTEN
#include "shortenfile.h"
#endif
#ifdef TAGLIB_WITH_MATROSKA
#include "matroskafile.h"
#endif
using namespace TagLib;
@@ -220,6 +223,10 @@ namespace
else if(ext == "SHN")
file = new Shorten::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_MATROSKA
else if(ext == "MKA" || ext == "MKV" || ext == "WEBM")
file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
// if file is not valid, leave it to content-based detection.
@@ -239,8 +246,7 @@ namespace
{
File *file = nullptr;
if(MPEG::File::isSupported(stream))
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
if(false);
#ifdef TAGLIB_WITH_VORBIS
else if(Ogg::Vorbis::File::isSupported(stream))
file = new Ogg::Vorbis::File(stream, readAudioProperties, audioPropertiesStyle);
@@ -289,6 +295,12 @@ namespace
else if(Shorten::File::isSupported(stream))
file = new Shorten::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_MATROSKA
else if(Matroska::File::isSupported(stream))
file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
else if(MPEG::File::isSupported(stream))
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
// isSupported() only does a quick check, so double check the file here.
@@ -513,6 +525,11 @@ StringList FileRef::defaultFileExtensions()
#ifdef TAGLIB_WITH_SHORTEN
l.append("shn");
#endif
#ifdef TAGLIB_WITH_MATROSKA
l.append("mkv");
l.append("mka");
l.append("webm");
#endif
return l;
}

View File

@@ -70,6 +70,8 @@ public:
std::unique_ptr<Properties> properties;
ByteVector xiphCommentData;
String iXMLData;
ByteVector bextData;
List<FLAC::MetadataBlock *> blocks;
offset_t flacStart { 0 };
@@ -241,6 +243,52 @@ bool FLAC::File::save()
d->xiphCommentData = xiphComment()->render(false);
// Drop any APPLICATION blocks we recognize as iXML or bext from the block
// list. Recognized blocks were normally extracted to d->iXMLData /
// d->bextData during scan() and never added here, but this also catches
// entries inserted after scan() (defensive).
for(auto it = d->blocks.begin(); it != d->blocks.end();) {
if((*it)->code() == MetadataBlock::Application) {
const ByteVector blockData = (*it)->render();
if(blockData.size() >= 4) {
const ByteVector appId = blockData.mid(0, 4);
ByteVector innerId;
if(appId == "riff" && blockData.size() >= 12)
innerId = blockData.mid(4, 4);
else if(appId == "iXML" || appId == "bext")
innerId = appId;
if(innerId == "iXML" || innerId == "bext") {
delete *it;
it = d->blocks.erase(it);
continue;
}
}
}
++it;
}
// Append fresh APPLICATION/"riff" blocks for iXML and bext if non-empty.
// Per FLAC foreign-metadata convention the payload is a RIFF chunk:
// <4 byte FOURCC><4 byte LE size><data>.
if(!d->iXMLData.isEmpty()) {
const ByteVector xml = d->iXMLData.data(String::UTF8);
ByteVector payload;
payload.append("riff");
payload.append("iXML");
payload.append(ByteVector::fromUInt(xml.size(), false));
payload.append(xml);
d->blocks.append(new UnknownMetadataBlock(MetadataBlock::Application, payload));
}
if(!d->bextData.isEmpty()) {
ByteVector payload;
payload.append("riff");
payload.append("bext");
payload.append(ByteVector::fromUInt(d->bextData.size(), false));
payload.append(d->bextData);
d->blocks.append(new UnknownMetadataBlock(MetadataBlock::Application, payload));
}
// Replace metadata blocks
MetadataBlock *commentBlock =
@@ -433,6 +481,26 @@ void FLAC::File::removePictures()
}
}
String FLAC::File::iXMLData() const
{
return d->iXMLData;
}
void FLAC::File::setiXMLData(const String &data)
{
d->iXMLData = data;
}
ByteVector FLAC::File::BEXTData() const
{
return d->bextData;
}
void FLAC::File::setBEXTData(const ByteVector &data)
{
d->bextData = data;
}
void FLAC::File::strip(int tags)
{
if(tags & ID3v1)
@@ -462,6 +530,16 @@ bool FLAC::File::hasID3v2Tag() const
return d->ID3v2Location >= 0;
}
bool FLAC::File::hasiXMLData() const
{
return !d->iXMLData.isEmpty();
}
bool FLAC::File::hasBEXTData() const
{
return !d->bextData.isEmpty();
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
@@ -613,6 +691,49 @@ void FLAC::File::scan()
else if(blockType == MetadataBlock::Padding) {
// Skip all padding blocks.
}
else if(blockType == MetadataBlock::Application && data.size() >= 4) {
// APPLICATION block (RFC 9639 § 8.4):
// <4 bytes> big-endian application ID (ASCII FOURCC in practice)
// <n bytes> application-defined data
//
// We recognize two conventions for carrying RIFF iXML / bext metadata:
// 1. App ID "riff" — IANA-registered FLAC foreign-metadata wrapper.
// Payload is a RIFF chunk: <4 byte FOURCC><4 byte LE size><data>.
// 2. App ID "iXML" or "bext" — direct, used by some third-party tools
// (e.g. Sequoia). Payload is the chunk data verbatim.
//
// Other application IDs (and "riff" wrapping FOURCCs we don't recognize)
// fall through to UnknownMetadataBlock so they round-trip unchanged.
const ByteVector appId = data.mid(0, 4);
ByteVector innerId;
ByteVector innerData;
if(appId == "riff" && data.size() >= 12) {
innerId = data.mid(4, 4);
const unsigned int innerSize = data.toUInt(8U, false);
innerData = data.mid(12, innerSize);
}
else if(appId == "iXML" || appId == "bext") {
innerId = appId;
innerData = data.mid(4);
}
if(innerId == "iXML") {
if(d->iXMLData.isEmpty())
d->iXMLData = String(innerData, String::UTF8);
else
debug("FLAC::File::scan() -- multiple iXML blocks found, discarding");
}
else if(innerId == "bext") {
if(d->bextData.isEmpty())
d->bextData = innerData;
else
debug("FLAC::File::scan() -- multiple BEXT blocks found, discarding");
}
else {
block = new UnknownMetadataBlock(blockType, data);
}
}
else {
block = new UnknownMetadataBlock(blockType, data);
}

View File

@@ -296,6 +296,52 @@ namespace TagLib {
*/
void addPicture(Picture *picture);
/*!
* Returns the raw iXML data as a String. Empty if no iXML metadata
* is present. Read from an APPLICATION metadata block (RFC 9639 § 8.4)
* carrying either the FLAC foreign-metadata application ID "riff"
* (with an iXML RIFF chunk as payload) or the direct application ID
* "iXML" used by some third-party tools.
*
* \see setiXMLData()
* \see hasiXMLData()
*/
String iXMLData() const;
/*!
* Sets the iXML data. Pass an empty string to remove the iXML
* APPLICATION block on save. On save, the data is written using the
* FLAC foreign-metadata convention: an APPLICATION block with
* application ID "riff" wrapping an iXML RIFF chunk.
*
* \see iXMLData()
* \see hasiXMLData()
*/
void setiXMLData(const String &data);
/*!
* Returns the raw BEXT (Broadcast Audio Extension) data as a
* ByteVector. Empty if no BEXT metadata is present. Read from an
* APPLICATION metadata block (RFC 9639 § 8.4) carrying either the FLAC
* foreign-metadata application ID "riff" (with a bext RIFF chunk as
* payload) or the direct application ID "bext".
*
* \see setBEXTData()
* \see hasBEXTData()
*/
ByteVector BEXTData() const;
/*!
* Sets the BEXT data. Pass an empty ByteVector to remove the BEXT
* APPLICATION block on save. On save, the data is written using the
* FLAC foreign-metadata convention: an APPLICATION block with
* application ID "riff" wrapping a bext RIFF chunk.
*
* \see BEXTData()
* \see hasBEXTData()
*/
void setBEXTData(const ByteVector &data);
/*!
* This will remove the tags that match the OR-ed together TagTypes from
* the file. By default it removes all tags.
@@ -332,6 +378,22 @@ namespace TagLib {
*/
bool hasID3v2Tag() const;
/*!
* Returns whether or not the file on disk actually has iXML data
* stored in an APPLICATION metadata block.
*
* \see iXMLData()
*/
bool hasiXMLData() const;
/*!
* Returns whether or not the file on disk actually has BEXT data
* stored in an APPLICATION metadata block.
*
* \see BEXTData()
*/
bool hasBEXTData() const;
/*!
* Returns whether or not the given \a stream can be opened as a FLAC
* file.

View File

@@ -87,7 +87,7 @@ namespace TagLib {
int bitsPerSample() const;
/*!
* Return the number of sample frames.
* Returns the number of sample frames.
*/
unsigned long long sampleFrames() const;

View File

@@ -0,0 +1,70 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlbinaryelement.h"
#include "ebmlutils.h"
#include "tfile.h"
#include "tdebug.h"
using namespace TagLib;
EBML::BinaryElement::BinaryElement(Id id, int sizeLength, offset_t dataSize):
Element(id, sizeLength, dataSize)
{
}
EBML::BinaryElement::BinaryElement(Id id, int sizeLength, offset_t dataSize, offset_t):
Element(id, sizeLength, dataSize)
{
}
EBML::BinaryElement::BinaryElement(Id id):
Element(id, 0, 0)
{
}
const ByteVector& EBML::BinaryElement::getValue() const
{
return value;
}
void EBML::BinaryElement::setValue(const ByteVector& val)
{
value = val;
}
bool EBML::BinaryElement::read(File &file)
{
value = file.readBlock(dataSize);
if(value.size() != dataSize) {
debug("Failed to read binary element");
return false;
}
return true;
}
ByteVector EBML::BinaryElement::render()
{
ByteVector buffer = renderId();
dataSize = value.size();
buffer.append(renderVINT(dataSize, 0));
buffer.append(value);
return buffer;
}

View File

@@ -0,0 +1,51 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLBINARYELEMENT_H
#define TAGLIB_EBMLBINARYELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlelement.h"
#include "tbytevector.h"
namespace TagLib {
class File;
namespace EBML {
class BinaryElement : public Element
{
public:
BinaryElement(Id id, int sizeLength, offset_t dataSize);
BinaryElement(Id id, int sizeLength, offset_t dataSize, offset_t);
explicit BinaryElement(Id id);
const ByteVector &getValue() const;
void setValue(const ByteVector &val);
bool read(File &file) override;
ByteVector render() override;
private:
ByteVector value;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,216 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlelement.h"
#include "ebmlvoidelement.h"
#include "ebmlmasterelement.h"
#include "ebmlbinaryelement.h"
#include "ebmlfloatelement.h"
#include "ebmlmkseekhead.h"
#include "ebmlmksegment.h"
#include "ebmlmktags.h"
#include "ebmlmkattachments.h"
#include "ebmlmkchapters.h"
#include "ebmlmktracks.h"
#include "ebmlstringelement.h"
#include "ebmluintelement.h"
#include "ebmlutils.h"
#include "tfile.h"
#include "tdebug.h"
#include "tutils.h"
using namespace TagLib;
#define RETURN_ELEMENT_FOR_CASE(eid) \
case (eid): return make_unique_element<eid>(id, sizeLength, dataSize, offset)
std::unique_ptr<EBML::Element> EBML::Element::factory(File &file)
{
// Get the element ID
const offset_t offset = file.tell();
unsigned int uintId = readId(file);
if(!uintId) {
debug("Failed to parse EMBL ElementID");
return nullptr;
}
// Get the size length and data length
const auto &[sizeLength, dataSize] = readVINT(file);
if(!sizeLength)
return nullptr;
// Return the subclass
// The enum switch without default will give us a warning if an ID is missing
auto id = static_cast<Id>(uintId);
switch(id) {
RETURN_ELEMENT_FOR_CASE(Id::EBMLHeader);
RETURN_ELEMENT_FOR_CASE(Id::DocType);
RETURN_ELEMENT_FOR_CASE(Id::DocTypeVersion);
RETURN_ELEMENT_FOR_CASE(Id::MkSegment);
RETURN_ELEMENT_FOR_CASE(Id::MkInfo);
RETURN_ELEMENT_FOR_CASE(Id::MkTracks);
RETURN_ELEMENT_FOR_CASE(Id::MkTags);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachments);
RETURN_ELEMENT_FOR_CASE(Id::MkTag);
RETURN_ELEMENT_FOR_CASE(Id::MkTagTargets);
RETURN_ELEMENT_FOR_CASE(Id::MkSimpleTag);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFile);
RETURN_ELEMENT_FOR_CASE(Id::MkSeek);
RETURN_ELEMENT_FOR_CASE(Id::MkTrackEntry);
RETURN_ELEMENT_FOR_CASE(Id::MkAudio);
RETURN_ELEMENT_FOR_CASE(Id::MkTagName);
RETURN_ELEMENT_FOR_CASE(Id::MkTagString);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileName);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileDescription);
RETURN_ELEMENT_FOR_CASE(Id::MkTagLanguage);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileMediaType);
RETURN_ELEMENT_FOR_CASE(Id::MkCodecID);
RETURN_ELEMENT_FOR_CASE(Id::MkTagTargetTypeValue);
RETURN_ELEMENT_FOR_CASE(Id::MkTagTrackUID);
RETURN_ELEMENT_FOR_CASE(Id::MkTagEditionUID);
RETURN_ELEMENT_FOR_CASE(Id::MkTagChapterUID);
RETURN_ELEMENT_FOR_CASE(Id::MkTagAttachmentUID);
RETURN_ELEMENT_FOR_CASE(Id::MkTagsLanguageDefault);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileUID);
RETURN_ELEMENT_FOR_CASE(Id::MkSeekPosition);
RETURN_ELEMENT_FOR_CASE(Id::MkTimestampScale);
RETURN_ELEMENT_FOR_CASE(Id::MkBitDepth);
RETURN_ELEMENT_FOR_CASE(Id::MkChannels);
RETURN_ELEMENT_FOR_CASE(Id::MkAttachedFileData);
RETURN_ELEMENT_FOR_CASE(Id::MkSeekID);
RETURN_ELEMENT_FOR_CASE(Id::MkDuration);
RETURN_ELEMENT_FOR_CASE(Id::MkTitle);
RETURN_ELEMENT_FOR_CASE(Id::MkSamplingFrequency);
RETURN_ELEMENT_FOR_CASE(Id::MkSeekHead);
RETURN_ELEMENT_FOR_CASE(Id::VoidElement);
RETURN_ELEMENT_FOR_CASE(Id::MkCluster);
RETURN_ELEMENT_FOR_CASE(Id::MkCodecState);
RETURN_ELEMENT_FOR_CASE(Id::MkTagBinary);
RETURN_ELEMENT_FOR_CASE(Id::MkCues);
RETURN_ELEMENT_FOR_CASE(Id::MkCuePoint);
RETURN_ELEMENT_FOR_CASE(Id::MkCueTime);
RETURN_ELEMENT_FOR_CASE(Id::MkCueTrackPositions);
RETURN_ELEMENT_FOR_CASE(Id::MkCueTrack);
RETURN_ELEMENT_FOR_CASE(Id::MkCueClusterPosition);
RETURN_ELEMENT_FOR_CASE(Id::MkCueRelativePosition);
RETURN_ELEMENT_FOR_CASE(Id::MkCueDuration);
RETURN_ELEMENT_FOR_CASE(Id::MkCueBlockNumber);
RETURN_ELEMENT_FOR_CASE(Id::MkCueCodecState);
RETURN_ELEMENT_FOR_CASE(Id::MkCueReference);
RETURN_ELEMENT_FOR_CASE(Id::MkCueRefTime);
RETURN_ELEMENT_FOR_CASE(Id::MkChapters);
RETURN_ELEMENT_FOR_CASE(Id::MkEditionEntry);
RETURN_ELEMENT_FOR_CASE(Id::MkEditionUID);
RETURN_ELEMENT_FOR_CASE(Id::MkEditionFlagDefault);
RETURN_ELEMENT_FOR_CASE(Id::MkEditionFlagOrdered);
RETURN_ELEMENT_FOR_CASE(Id::MkChapterAtom);
RETURN_ELEMENT_FOR_CASE(Id::MkChapterUID);
RETURN_ELEMENT_FOR_CASE(Id::MkChapterTimeStart);
RETURN_ELEMENT_FOR_CASE(Id::MkChapterTimeEnd);
RETURN_ELEMENT_FOR_CASE(Id::MkChapterFlagHidden);
RETURN_ELEMENT_FOR_CASE(Id::MkChapterDisplay);
RETURN_ELEMENT_FOR_CASE(Id::MkChapString);
RETURN_ELEMENT_FOR_CASE(Id::MkChapLanguage);
}
return std::make_unique<Element>(id, sizeLength, dataSize);
}
unsigned int EBML::Element::readId(File &file)
{
auto buffer = file.readBlock(1);
if(buffer.size() != 1) {
debug("Failed to read VINT size");
return 0;
}
const unsigned int numBytes = VINTSizeLength<4>(*buffer.begin());
if(!numBytes)
return 0;
if(numBytes > 1)
buffer.append(file.readBlock(numBytes - 1));
if(buffer.size() != numBytes) {
debug("Failed to read VINT data");
return 0;
}
return buffer.toUInt(true);
}
EBML::Element::Element(Id id, int sizeLength, offset_t dataSize):
id(id), sizeLength(sizeLength), dataSize(dataSize)
{
}
EBML::Element::Element(Id id, int sizeLength, offset_t dataSize, offset_t):
id(id), sizeLength(sizeLength), dataSize(dataSize)
{
}
EBML::Element::~Element() = default;
bool EBML::Element::read(File& file)
{
skipData(file);
return true;
}
void EBML::Element::skipData(File &file)
{
file.seek(dataSize, File::Position::Current);
}
EBML::Element::Id EBML::Element::getId() const
{
return id;
}
offset_t EBML::Element::headSize() const
{
return idSize(id) + sizeLength;
}
offset_t EBML::Element::getSize() const
{
return headSize() + dataSize;
}
int EBML::Element::getSizeLength() const
{
return sizeLength;
}
int64_t EBML::Element::getDataSize() const
{
return dataSize;
}
ByteVector EBML::Element::render()
{
ByteVector buffer = renderId();
buffer.append(renderVINT(0, 0));
return buffer;
}
ByteVector EBML::Element::renderId() const
{
const int numBytes = idSize(id);
static const auto byteOrder = Utils::systemByteOrder();
const auto uintId = static_cast<uint32_t>(id);
uint32_t data = byteOrder == Utils::LittleEndian ? Utils::byteSwap(uintId) : uintId;
return ByteVector(reinterpret_cast<char *>(&data) + (4 - numBytes), numBytes);
}

View File

@@ -0,0 +1,255 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLELEMENT_H
#define TAGLIB_EBMLELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include <cstdint>
#include <memory>
#include "taglib.h"
namespace TagLib
{
class File;
class ByteVector;
namespace EBML {
class Element
{
public:
enum class Id : unsigned int
{
EBMLHeader = 0x1A45DFA3,
DocType = 0x4282,
DocTypeVersion = 0x4287,
VoidElement = 0xEC,
MkSegment = 0x18538067,
MkTags = 0x1254C367,
MkTag = 0x7373,
MkTagTargets = 0x63C0,
MkTagTargetTypeValue = 0x68CA,
MkTagTrackUID = 0x63C5,
MkTagEditionUID = 0x63C9,
MkTagChapterUID = 0x63C4,
MkTagAttachmentUID = 0x63C6,
MkSimpleTag = 0x67C8,
MkTagName = 0x45A3,
MkTagLanguage = 0x447A,
MkTagBinary = 0x4485,
MkTagString = 0x4487,
MkTagsTagLanguage = 0x447A,
MkTagsLanguageDefault = 0x4484,
MkAttachments = 0x1941A469,
MkAttachedFile = 0x61A7,
MkAttachedFileDescription = 0x467E,
MkAttachedFileName = 0x466E,
MkAttachedFileMediaType = 0x4660,
MkAttachedFileData = 0x465C,
MkAttachedFileUID = 0x46AE,
MkSeekHead = 0x114D9B74,
MkSeek = 0x4DBB,
MkSeekID = 0x53AB,
MkSeekPosition = 0x53AC,
MkCluster = 0x1F43B675,
MkCodecState = 0xA4,
MkCues = 0x1C53BB6B,
MkCuePoint = 0xBB,
MkCueTime = 0xB3,
MkCueTrackPositions = 0xB7,
MkCueTrack = 0xF7,
MkCueClusterPosition = 0xF1,
MkCueRelativePosition = 0xF0,
MkCueDuration = 0xB2,
MkCueBlockNumber = 0x5378,
MkCueCodecState = 0xEA,
MkCueReference = 0xDB,
MkCueRefTime = 0x96,
MkInfo = 0x1549A966,
MkTimestampScale = 0x2AD7B1,
MkDuration = 0x4489,
MkTitle = 0x7BA9,
MkTracks = 0x1654AE6B,
MkTrackEntry = 0xAE,
MkCodecID = 0x86,
MkAudio = 0xE1,
MkSamplingFrequency = 0xB5,
MkBitDepth = 0x6264,
MkChannels = 0x9F,
MkChapters = 0x1043A770,
MkEditionEntry = 0x45B9,
MkEditionUID = 0x45BC,
MkEditionFlagDefault = 0x45DB,
MkEditionFlagOrdered = 0x45DD,
MkChapterAtom = 0xB6,
MkChapterUID = 0x73C4,
MkChapterTimeStart = 0x91,
MkChapterTimeEnd = 0x92,
MkChapterFlagHidden = 0x98,
MkChapterDisplay = 0x80,
MkChapString = 0x85,
MkChapLanguage = 0x437C,
};
Element(Id id, int sizeLength, offset_t dataSize);
Element(Id id, int sizeLength, offset_t dataSize, offset_t);
virtual ~Element();
virtual bool read(File &file);
void skipData(File &file);
Id getId() const;
offset_t headSize() const;
offset_t getSize() const;
int getSizeLength() const;
int64_t getDataSize() const;
ByteVector renderId() const;
virtual ByteVector render();
static std::unique_ptr<Element> factory(File &file);
static unsigned int readId(File &file);
protected:
Id id;
int sizeLength;
offset_t dataSize;
};
// Template specializations to ensure that elements for the different IDs
// are consistently created and cast. They provide a mapping between IDs
// and Element subclasses. The switch in factory() makes sure that the
// template for all IDs are instantiated, i.e. that every ID has its Element
// subclass mapped.
class MasterElement;
class UIntElement;
class BinaryElement;
class FloatElement;
class MkSegment;
class MkInfo;
class MkTracks;
class MkTags;
class MkAttachments;
class MkSeekHead;
class MkChapters;
class MkCues;
class VoidElement;
class UTF8StringElement;
class Latin1StringElement;
template <Element::Id ID>
struct GetElementTypeById;
template <> struct GetElementTypeById<Element::Id::EBMLHeader> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::DocType> { using type = Latin1StringElement; };
template <> struct GetElementTypeById<Element::Id::DocTypeVersion> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkSegment> { using type = MkSegment; };
template <> struct GetElementTypeById<Element::Id::MkInfo> { using type = MkInfo; };
template <> struct GetElementTypeById<Element::Id::MkTracks> { using type = MkTracks; };
template <> struct GetElementTypeById<Element::Id::MkTags> { using type = MkTags; };
template <> struct GetElementTypeById<Element::Id::MkAttachments> { using type = MkAttachments; };
template <> struct GetElementTypeById<Element::Id::MkTag> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkTagTargets> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkSimpleTag> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkAttachedFile> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkSeek> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkTrackEntry> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkAudio> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkCuePoint> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkCueTrackPositions> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkCueReference> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkCluster> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkCues> { using type = MkCues; };
template <> struct GetElementTypeById<Element::Id::MkTagName> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkTagString> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkAttachedFileName> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkAttachedFileDescription> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkTagLanguage> { using type = Latin1StringElement; };
template <> struct GetElementTypeById<Element::Id::MkAttachedFileMediaType> { using type = Latin1StringElement; };
template <> struct GetElementTypeById<Element::Id::MkCodecID> { using type = Latin1StringElement; };
template <> struct GetElementTypeById<Element::Id::MkTagTargetTypeValue> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkTagTrackUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkTagEditionUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkTagChapterUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkTagAttachmentUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkAttachedFileUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkSeekPosition> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkTimestampScale> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkBitDepth> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChannels> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueTime> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueTrack> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueClusterPosition> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueRelativePosition> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueDuration> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueBlockNumber> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueCodecState> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkCueRefTime> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkTagsLanguageDefault> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkAttachedFileData> { using type = BinaryElement; };
template <> struct GetElementTypeById<Element::Id::MkSeekID> { using type = BinaryElement; };
template <> struct GetElementTypeById<Element::Id::MkTagBinary> { using type = BinaryElement; };
template <> struct GetElementTypeById<Element::Id::MkCodecState> { using type = BinaryElement; };
template <> struct GetElementTypeById<Element::Id::MkDuration> { using type = FloatElement; };
template <> struct GetElementTypeById<Element::Id::MkTitle> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkSamplingFrequency> { using type = FloatElement; };
template <> struct GetElementTypeById<Element::Id::MkSeekHead> { using type = MkSeekHead; };
template <> struct GetElementTypeById<Element::Id::MkChapters> { using type = MkChapters; };
template <> struct GetElementTypeById<Element::Id::MkEditionEntry> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkEditionUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkEditionFlagDefault> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkEditionFlagOrdered> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterAtom> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterUID> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterTimeStart> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterTimeEnd> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterFlagHidden> { using type = UIntElement; };
template <> struct GetElementTypeById<Element::Id::MkChapterDisplay> { using type = MasterElement; };
template <> struct GetElementTypeById<Element::Id::MkChapString> { using type = UTF8StringElement; };
template <> struct GetElementTypeById<Element::Id::MkChapLanguage> { using type = Latin1StringElement; };
template <> struct GetElementTypeById<Element::Id::VoidElement> { using type = VoidElement; };
template <Element::Id ID, typename T=typename GetElementTypeById<ID>::type>
const T *element_cast(const std::unique_ptr<Element> &ptr)
{
return static_cast<const T *>(ptr.get());
}
template <Element::Id ID, typename T=typename GetElementTypeById<ID>::type>
std::unique_ptr<T> element_cast(std::unique_ptr<Element> &&ptr)
{
return std::unique_ptr<T>(static_cast<T *>(ptr.release()));
}
template <Element::Id ID, typename T=typename GetElementTypeById<ID>::type>
std::unique_ptr<T> make_unique_element(Element::Id id, int sizeLength, offset_t dataSize, offset_t offset)
{
return std::make_unique<T>(id, sizeLength, dataSize, offset);
}
template <Element::Id ID, typename T=typename GetElementTypeById<ID>::type>
std::unique_ptr<T> make_unique_element()
{
return std::make_unique<T>(ID, 0, 0, 0);
}
}
}
#endif
#endif

View File

@@ -0,0 +1,109 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlfloatelement.h"
#include "ebmlutils.h"
#include "tbytevector.h"
#include "tfile.h"
#include "tdebug.h"
using namespace TagLib;
EBML::FloatElement::FloatElement(Id id, int sizeLength, offset_t dataSize):
Element(id, sizeLength, dataSize)
{
}
EBML::FloatElement::FloatElement(Id id, int sizeLength, offset_t dataSize, offset_t):
Element(id, sizeLength, dataSize)
{
}
EBML::FloatElement::FloatElement(Id id):
FloatElement(id, 0, 0)
{
}
EBML::FloatElement::FloatVariantType EBML::FloatElement::getValue() const
{ return value; }
double EBML::FloatElement::getValueAsDouble(double defaultValue) const
{
if(std::holds_alternative<double>(value)) {
// get_if() used instead of get() to support restricted compilers
return *std::get_if<double>(&value);
}
if(std::holds_alternative<float>(value)) {
// get_if() used instead of get() to support restricted compilers
return *std::get_if<float>(&value);
}
return defaultValue;
}
void EBML::FloatElement::setValue(FloatVariantType val)
{
value = val;
}
bool EBML::FloatElement::read(File &file)
{
const ByteVector buffer = file.readBlock(dataSize);
if(buffer.size() != dataSize) {
debug("Failed to read EBML Float element");
return false;
}
if(dataSize == 0) {
value = std::monostate();
}
else if(dataSize == 4) {
value = buffer.toFloat32BE(0);
}
else if(dataSize == 8) {
value = buffer.toFloat64BE(0);
}
else {
debug("Invalid size for EBML Float element");
return false;
}
return true;
}
ByteVector EBML::FloatElement::render()
{
ByteVector data;
if(std::holds_alternative<double>(value)) {
// get_if() used instead of get() to support restricted compilers
data = ByteVector::fromFloat64BE(*std::get_if<double>(&value));
}
else if(std::holds_alternative<float>(value)) {
// get_if() used instead of get() to support restricted compilers
data = ByteVector::fromFloat32BE(*std::get_if<float>(&value));
}
ByteVector buffer = renderId();
buffer.append(renderVINT(data.size(), 0));
buffer.append(data);
return buffer;
}

View File

@@ -0,0 +1,59 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLFLOATELEMENT_H
#define TAGLIB_EBMLFLOATELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include <variant>
#include "ebmlelement.h"
namespace TagLib {
class File;
namespace EBML {
class FloatElement : public Element
{
public:
using FloatVariantType = std::variant<std::monostate, float, double>;
FloatElement(Id id, int sizeLength, offset_t dataSize);
FloatElement(Id id, int sizeLength, offset_t dataSize, offset_t);
explicit FloatElement(Id id);
FloatVariantType getValue() const;
double getValueAsDouble(double defaultValue = 0.0) const;
void setValue(FloatVariantType val);
bool read(File &file) override;
ByteVector render() override;
private:
FloatVariantType value;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,144 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmasterelement.h"
#include "ebmlvoidelement.h"
#include "ebmlutils.h"
#include "tdebug.h"
#include "tfile.h"
using namespace TagLib;
EBML::MasterElement::MasterElement(Id id, int sizeLength, offset_t dataSize, offset_t offset):
Element(id, sizeLength, dataSize), offset(offset)
{
}
EBML::MasterElement::MasterElement(Id id):
Element(id, 0, 0), offset(0)
{
}
EBML::MasterElement::~MasterElement() = default;
offset_t EBML::MasterElement::getOffset() const
{
return offset;
}
void EBML::MasterElement::appendElement(std::unique_ptr<Element> &&element)
{
elements.push_back(std::move(element));
}
std::list<std::unique_ptr<EBML::Element>>::iterator EBML::MasterElement::begin()
{
return elements.begin();
}
std::list<std::unique_ptr<EBML::Element>>::iterator EBML::MasterElement::end()
{
return elements.end();
}
std::list<std::unique_ptr<EBML::Element>>::const_iterator EBML::MasterElement::begin() const
{
return elements.begin();
}
std::list<std::unique_ptr<EBML::Element>>::const_iterator EBML::MasterElement::end() const
{
return elements.end();
}
std::list<std::unique_ptr<EBML::Element>>::const_iterator EBML::MasterElement::cbegin() const
{
return elements.cbegin();
}
std::list<std::unique_ptr<EBML::Element>>::const_iterator EBML::MasterElement::cend() const
{
return elements.cend();
}
offset_t EBML::MasterElement::getPadding() const
{
return padding;
}
void EBML::MasterElement::setPadding(offset_t numBytes)
{
padding = numBytes;
}
offset_t EBML::MasterElement::getMinRenderSize() const
{
return minRenderSize;
}
void EBML::MasterElement::setMinRenderSize(offset_t minimumSize)
{
minRenderSize = minimumSize;
}
bool EBML::MasterElement::read(File &file, int depth)
{
static constexpr int MAX_EBML_DEPTH = 64;
if(depth > MAX_EBML_DEPTH) {
debug("EBML: Maximum nesting depth exceeded");
return false;
}
const offset_t maxOffset = file.tell() + dataSize;
std::unique_ptr<Element> element;
while((element = findNextElement(file, maxOffset))) {
if(auto master = dynamic_cast<MasterElement *>(element.get())) {
if(!master->read(file, depth + 1))
return false;
}
else {
if(!element->read(file))
return false;
}
elements.push_back(std::move(element));
}
return file.tell() == maxOffset;
}
bool EBML::MasterElement::read(File &file)
{
return read(file, 0);
}
ByteVector EBML::MasterElement::render()
{
ByteVector buffer = renderId();
ByteVector data;
for(const auto &element : elements)
data.append(element->render());
dataSize = data.size();
buffer.append(renderVINT(dataSize, 0));
buffer.append(data);
if(minRenderSize) {
if(const auto bufferSize = buffer.size();
minRenderSize >= bufferSize + MIN_VOID_ELEMENT_SIZE)
buffer.append(VoidElement::renderSize(minRenderSize - bufferSize));
}
return buffer;
}

View File

@@ -0,0 +1,70 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMASTERELEMENT_H
#define TAGLIB_EBMLMASTERELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include <list>
#include "ebmlelement.h"
#include "taglib.h"
namespace TagLib
{
class ByteVector;
namespace EBML {
class MasterElement : public Element
{
public:
MasterElement(Id id, int sizeLength, offset_t dataSize, offset_t offset);
explicit MasterElement(Id id);
~MasterElement() override;
offset_t getOffset() const;
bool read(File &file) override;
ByteVector render() override;
void appendElement(std::unique_ptr<Element> &&element);
std::list<std::unique_ptr<Element>>::iterator begin();
std::list<std::unique_ptr<Element>>::iterator end();
std::list<std::unique_ptr<Element>>::const_iterator begin() const;
std::list<std::unique_ptr<Element>>::const_iterator end() const;
std::list<std::unique_ptr<Element>>::const_iterator cbegin() const;
std::list<std::unique_ptr<Element>>::const_iterator cend() const;
offset_t getPadding() const;
void setPadding(offset_t numBytes);
offset_t getMinRenderSize() const;
void setMinRenderSize(offset_t minimumSize);
protected:
bool read(File &file, int depth);
offset_t offset;
offset_t padding = 0;
offset_t minRenderSize = 0;
std::list<std::unique_ptr<Element>> elements;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,81 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmkattachments.h"
#include "ebmlstringelement.h"
#include "ebmluintelement.h"
#include "ebmlbinaryelement.h"
#include "matroskaattachments.h"
#include "matroskaattachedfile.h"
using namespace TagLib;
EBML::MkAttachments::MkAttachments(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkAttachments, sizeLength, dataSize, offset)
{
}
EBML::MkAttachments::MkAttachments(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkAttachments, sizeLength, dataSize, offset)
{
}
EBML::MkAttachments::MkAttachments():
MasterElement(Id::MkAttachments, 0, 0, 0)
{
}
std::unique_ptr<Matroska::Attachments> EBML::MkAttachments::parse() const
{
auto attachments = std::make_unique<Matroska::Attachments>();
attachments->setOffset(offset);
attachments->setSize(getSize() + padding);
for(const auto &element : elements) {
if(element->getId() != Id::MkAttachedFile)
continue;
const String *filename = nullptr;
const String *description = nullptr;
const String *mediaType = nullptr;
const ByteVector *data = nullptr;
Matroska::AttachedFile::UID uid = 0;
const auto attachedFile = element_cast<Id::MkAttachedFile>(element);
for(const auto &attachedFileChild : *attachedFile) {
if(const Id id = attachedFileChild->getId(); id == Id::MkAttachedFileName)
filename = &element_cast<Id::MkAttachedFileName>(attachedFileChild)->getValue();
else if(id == Id::MkAttachedFileData)
data = &element_cast<Id::MkAttachedFileData>(attachedFileChild)->getValue();
else if(id == Id::MkAttachedFileDescription)
description = &element_cast<Id::MkAttachedFileDescription>(attachedFileChild)->getValue();
else if(id == Id::MkAttachedFileMediaType)
mediaType = &element_cast<Id::MkAttachedFileMediaType>(attachedFileChild)->getValue();
else if(id == Id::MkAttachedFileUID)
uid = element_cast<Id::MkAttachedFileUID>(attachedFileChild)->getValue();
}
if(!(filename && data))
continue;
attachments->addAttachedFile(Matroska::AttachedFile(
*data, *filename, mediaType ? *mediaType : String(),
uid, description ? *description : String()));
}
return attachments;
}

View File

@@ -0,0 +1,47 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKATTACHMENTS_H
#define TAGLIB_EBMLMKATTACHMENTS_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Attachments;
}
namespace EBML {
class MkAttachments : public MasterElement
{
public:
MkAttachments(int sizeLength, offset_t dataSize, offset_t offset);
MkAttachments(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkAttachments();
std::unique_ptr<Matroska::Attachments> parse() const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,146 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmkchapters.h"
#include "ebmlstringelement.h"
#include "ebmluintelement.h"
#include "matroskachapters.h"
#include "matroskachapteredition.h"
using namespace TagLib;
namespace {
Matroska::Chapter parseChapterAtom(
const std::unique_ptr<EBML::Element> &atomElement)
{
Matroska::Chapter::UID chapterUid = 0;
Matroska::Chapter::Time chapterTimeStart = 0;
Matroska::Chapter::Time chapterTimeEnd = 0;
List<Matroska::Chapter::Display> chapterDisplays;
bool chapterHidden = false;
const auto chapterAtom = EBML::element_cast<EBML::Element::Id::MkChapterAtom>(atomElement);
for(const auto &chapterChild : *chapterAtom) {
if(const EBML::Element::Id cid = chapterChild->getId(); cid == EBML::Element::Id::MkChapterUID)
chapterUid = EBML::element_cast<EBML::Element::Id::MkChapterUID>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterTimeStart)
chapterTimeStart = EBML::element_cast<EBML::Element::Id::MkChapterTimeStart>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterTimeEnd)
chapterTimeEnd = EBML::element_cast<EBML::Element::Id::MkChapterTimeEnd>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterFlagHidden)
chapterHidden = EBML::element_cast<EBML::Element::Id::MkChapterFlagHidden>(chapterChild)->getValue() != 0;
else if (cid == EBML::Element::Id::MkChapterDisplay) {
const auto display = EBML::element_cast<EBML::Element::Id::MkChapterDisplay>(chapterChild);
String displayString;
String displayLanguage;
for(const auto &displayChild : *display) {
if (const EBML::Element::Id did = displayChild->getId(); did == EBML::Element::Id::MkChapString)
displayString = EBML::element_cast<EBML::Element::Id::MkChapString>(displayChild)->getValue();
else if(did == EBML::Element::Id::MkChapLanguage)
displayLanguage = EBML::element_cast<EBML::Element::Id::MkChapLanguage>(displayChild)->getValue();
}
if(!displayString.isEmpty()) {
chapterDisplays.append(Matroska::Chapter::Display(displayString, displayLanguage));
}
}
}
return Matroska::Chapter(chapterTimeStart, chapterTimeEnd, chapterDisplays,
chapterUid, chapterHidden);
}
} // namespae
EBML::MkChapters::MkChapters(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkChapters, sizeLength, dataSize, offset)
{
}
EBML::MkChapters::MkChapters(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkChapters, sizeLength, dataSize, offset)
{
}
EBML::MkChapters::MkChapters() :
MasterElement(Id::MkChapters, 0, 0, 0)
{
}
std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
{
auto chapters = std::make_unique<Matroska::Chapters>();
chapters->setOffset(offset);
chapters->setSize(getSize() + padding);
// Collect any orphan ChapterAtom elements not wrapped in an EditionEntry.
// The Matroska spec requires ChapterAtom to be inside an EditionEntry, but
// some muxers produce files with ChapterAtom directly under Chapters.
// MKVToolNix and FFmpeg handle this case by treating the orphan atoms as
// belonging to an implicit default edition.
List<Matroska::Chapter> orphanChapters;
for(const auto &element : elements) {
if(element->getId() == Id::MkChapterAtom) {
if(auto chapter = parseChapterAtom(element); chapter.uid()) {
orphanChapters.append(chapter);
}
continue;
}
if(element->getId() != Id::MkEditionEntry)
continue;
List<Matroska::Chapter> editionChapters;
Matroska::ChapterEdition::UID editionUid = 0;
bool editionIsDefault = false;
bool editionIsOrdered = false;
const auto edition = element_cast<Id::MkEditionEntry>(element);
for(const auto &editionChild : *edition) {
if(const Id id = editionChild->getId(); id == Id::MkEditionUID)
editionUid = element_cast<Id::MkEditionUID>(editionChild)->getValue();
else if(id == Id::MkEditionFlagDefault)
editionIsDefault = element_cast<Id::MkEditionFlagDefault>(editionChild)->getValue() != 0;
else if(id == Id::MkEditionFlagOrdered)
editionIsOrdered = element_cast<Id::MkEditionFlagOrdered>(editionChild)->getValue() != 0;
else if(id == Id::MkChapterAtom) {
if(auto chapter = parseChapterAtom(editionChild); chapter.uid()) {
editionChapters.append(chapter);
}
}
}
if(!editionChapters.isEmpty()) {
chapters->addChapterEdition(Matroska::ChapterEdition(
editionChapters, editionIsDefault, editionIsOrdered, editionUid));
}
}
// If orphan chapters were found, wrap them in an implicit default edition
// so they are not silently lost.
if (!orphanChapters.isEmpty()) {
chapters->addChapterEdition(Matroska::ChapterEdition(
orphanChapters, true, false, 0));
}
return chapters;
}

View File

@@ -0,0 +1,52 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKCHAPTERS_H
#define TAGLIB_EBMLMKCHAPTERS_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Chapters;
}
namespace EBML {
class MkChapters : public MasterElement
{
public:
MkChapters(int sizeLength, offset_t dataSize, offset_t offset);
MkChapters(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkChapters();
std::unique_ptr<Matroska::Chapters> parse() const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,89 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmkcues.h"
#include "ebmluintelement.h"
#include "matroskacues.h"
using namespace TagLib;
EBML::MkCues::MkCues(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkCues, sizeLength, dataSize, offset)
{
}
EBML::MkCues::MkCues(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkCues, sizeLength, dataSize, offset)
{
}
EBML::MkCues::MkCues():
MasterElement(Id::MkCues, 0, 0, 0)
{
}
std::unique_ptr<Matroska::Cues> EBML::MkCues::parse(offset_t segmentDataOffset) const
{
auto cues = std::make_unique<Matroska::Cues>(segmentDataOffset);
cues->setOffset(offset);
cues->setSize(getSize());
cues->setID(static_cast<Matroska::Element::ID>(id));
for(const auto &cuesChild : elements) {
if(cuesChild->getId() != Id::MkCuePoint)
continue;
const auto cuePointElement = element_cast<Id::MkCuePoint>(cuesChild);
auto cuePoint = std::make_unique<Matroska::CuePoint>();
for(const auto &cuePointChild : *cuePointElement) {
if(const Id id = cuePointChild->getId(); id == Id::MkCueTime)
cuePoint->setTime(element_cast<Id::MkCueTime>(cuePointChild)->getValue());
else if(id == Id::MkCueTrackPositions) {
auto cueTrack = std::make_unique<Matroska::CueTrack>();
const auto cueTrackElement = element_cast<Id::MkCueTrackPositions>(cuePointChild);
for(const auto &cueTrackChild : *cueTrackElement) {
if(const Id trackId = cueTrackChild->getId(); trackId == Id::MkCueTrack)
cueTrack->setTrackNumber(element_cast<Id::MkCueTrack>(cueTrackChild)->getValue());
else if(trackId == Id::MkCueClusterPosition)
cueTrack->setClusterPosition(element_cast<Id::MkCueClusterPosition>(cueTrackChild)->getValue());
else if(trackId == Id::MkCueRelativePosition)
cueTrack->setRelativePosition(element_cast<Id::MkCueRelativePosition>(cueTrackChild)->getValue());
else if(trackId == Id::MkCueDuration)
cueTrack->setDuration(element_cast<Id::MkCueDuration>(cueTrackChild)->getValue());
else if(trackId == Id::MkCueBlockNumber)
cueTrack->setBlockNumber(element_cast<Id::MkCueBlockNumber>(cueTrackChild)->getValue());
else if(trackId == Id::MkCueCodecState)
cueTrack->setCodecState(element_cast<Id::MkCueCodecState>(cueTrackChild)->getValue());
else if(trackId == Id::MkCueReference) {
const auto cueReference = element_cast<Id::MkCueReference>(cueTrackChild);
for(const auto &cueReferenceChild : *cueReference) {
if(cueReferenceChild->getId() != Id::MkCueRefTime)
continue;
cueTrack->addReferenceTime(element_cast<Id::MkCueRefTime>(cueReferenceChild)->getValue());
}
}
}
cuePoint->addCueTrack(std::move(cueTrack));
}
}
cues->addCuePoint(std::move(cuePoint));
}
return cues;
}

View File

@@ -0,0 +1,47 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKCUES_H
#define TAGLIB_EBMLMKCUES_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Cues;
}
namespace EBML {
class MkCues : public MasterElement
{
public:
MkCues(int sizeLength, offset_t dataSize, offset_t offset);
MkCues(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkCues();
std::unique_ptr<Matroska::Cues> parse(offset_t segmentDataOffset) const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,72 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmkinfo.h"
#include "ebmlstringelement.h"
#include "ebmluintelement.h"
#include "ebmlfloatelement.h"
#include "matroskaproperties.h"
using namespace TagLib;
EBML::MkInfo::MkInfo(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkInfo, sizeLength, dataSize, offset)
{
}
EBML::MkInfo::MkInfo(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkInfo, sizeLength, dataSize, offset)
{
}
EBML::MkInfo::MkInfo():
MasterElement(Id::MkInfo, 0, 0, 0)
{
}
void EBML::MkInfo::parse(Matroska::Properties *properties) const
{
if(!properties)
return;
unsigned long long timestampScale = 1000000;
double duration = 0.0;
String title;
for(const auto &element : elements) {
if(const Id id = element->getId(); id == Id::MkTimestampScale) {
timestampScale = element_cast<Id::MkTimestampScale>(element)->getValue();
}
else if(id == Id::MkDuration) {
duration = element_cast<Id::MkDuration>(element)->getValueAsDouble();
}
else if(id == Id::MkTitle) {
title = element_cast<Id::MkTitle>(element)->getValue();
}
}
properties->setLengthInMilliseconds(
static_cast<int>(duration * static_cast<double>(timestampScale) / 1000000.0));
properties->setTitle(title);
}

View File

@@ -0,0 +1,52 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKINFO_H
#define TAGLIB_EBMLMKINFO_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Properties;
}
namespace EBML {
class MkInfo : public MasterElement
{
public:
MkInfo(int sizeLength, offset_t dataSize, offset_t offset);
MkInfo(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkInfo();
void parse(Matroska::Properties * properties) const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,73 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmkseekhead.h"
#include "matroskaseekhead.h"
#include "ebmluintelement.h"
#include "ebmlbinaryelement.h"
using namespace TagLib;
EBML::MkSeekHead::MkSeekHead(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkSeekHead, sizeLength, dataSize, offset)
{
}
EBML::MkSeekHead::MkSeekHead(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkSeekHead, sizeLength, dataSize, offset)
{
}
EBML::MkSeekHead::MkSeekHead():
MasterElement(Id::MkSeekHead, 0, 0, 0)
{
}
std::unique_ptr<Matroska::SeekHead> EBML::MkSeekHead::parse(offset_t segmentDataOffset) const
{
auto seekHead = std::make_unique<Matroska::SeekHead>(segmentDataOffset);
seekHead->setOffset(offset);
seekHead->setSize(getSize() + padding);
for(const auto &element : elements) {
if(element->getId() != Id::MkSeek)
continue;
const auto seekElement = element_cast<Id::MkSeek>(element);
Matroska::Element::ID entryId = 0;
offset_t offset = 0;
for(const auto &seekElementChild : *seekElement) {
if(const Id id = seekElementChild->getId(); id == Id::MkSeekID && !entryId) {
if(auto data = element_cast<Id::MkSeekID>(seekElementChild)->getValue();
data.size() == 4)
entryId = data.toUInt(true);
}
else if(id == Id::MkSeekPosition && !offset)
offset = element_cast<Id::MkSeekPosition>(seekElementChild)->getValue();
}
if(entryId && offset)
seekHead->addEntry(entryId, offset);
else {
seekHead.reset();
return nullptr;
}
}
return seekHead;
}

View File

@@ -0,0 +1,47 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKSEEKHEAD_H
#define TAGLIB_EBMLMKSEEKHEAD_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class SeekHead;
}
namespace EBML {
class MkSeekHead : public MasterElement
{
public:
MkSeekHead(int sizeLength, offset_t dataSize, offset_t offset);
MkSeekHead(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkSeekHead();
std::unique_ptr<Matroska::SeekHead> parse(offset_t segmentDataOffset) const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,296 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmksegment.h"
#include "ebmlutils.h"
#include "matroskafile.h"
#include "matroskatag.h"
#include "matroskaattachments.h"
#include "matroskachapters.h"
#include "matroskacues.h"
#include "matroskaseekhead.h"
#include "matroskasegment.h"
using namespace TagLib;
namespace {
template <EBML::Element::Id Id, typename ElementType>
std::unique_ptr<ElementType> readElementAt(File &file,
offset_t offset,
offset_t maxOffset)
{
if(offset < 0 || offset >= maxOffset) {
return nullptr;
}
file.seek(offset);
auto element = EBML::Element::factory(file);
if(!element || element->getId() != Id) {
return nullptr;
}
auto typed = EBML::element_cast<Id>(std::move(element));
if(!typed || !typed->read(file)) {
return nullptr;
}
return typed;
}
} // namespace
EBML::MkSegment::MkSegment(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkSegment, sizeLength, dataSize, offset)
{
}
EBML::MkSegment::MkSegment(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkSegment, sizeLength, dataSize, offset)
{
}
EBML::MkSegment::~MkSegment() = default;
offset_t EBML::MkSegment::segmentDataOffset() const
{
return offset + idSize(id) + sizeLength;
}
bool EBML::MkSegment::read(File &file)
{
return readLimited(file, dataSize);
}
bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
{
const offset_t filePos = file.tell();
const offset_t maxOffset = filePos + dataSize;
const offset_t maxScanOffset = filePos + std::min(scanLimit, dataSize);
// When scanLimit is less than dataSize, the caller has requested a
// fast/limited scan (e.g. AudioProperties::Fast). In that case and if the
// file has been opened in read-only mode, we skip parsing the Cues element,
// which can be tens of MB on large files, causing severe slowdowns over
// network filesystems, and do not have to be updated in read-only mode.
const bool skipCues = file.readOnly() && scanLimit < dataSize;
MasterElement *pendingPaddingTarget = nullptr;
offset_t accumulatedPadding = 0;
std::unique_ptr<Element> element;
while((element = findNextElement(file, maxScanOffset))) {
if(const Id id = element->getId(); id == Id::MkSeekHead) {
seekHead = element_cast<Id::MkSeekHead>(std::move(element));
if(!seekHead->read(file))
return false;
// We have a seek head, let's use it for faster access to the other elements
if(const auto elementAfterSeekHead = findNextElement(file, maxScanOffset);
elementAfterSeekHead && elementAfterSeekHead->getId() == Id::VoidElement)
seekHead->setPadding(elementAfterSeekHead->getSize());
const offset_t segDataOffset = segmentDataOffset();
const auto matroskaSeekHead = parseSeekHead();
const auto accumulateVoidPadding = [&](MasterElement *target) {
offset_t accPadding = 0;
while(const auto next = findNextElement(file, maxOffset)) {
if(next->getId() != Id::VoidElement)
break;
accPadding += next->getSize();
next->skipData(file);
}
if(accPadding > 0)
target->setPadding(accPadding);
};
// Build a work list of seek entries. Some muxers (e.g. MakeMKV,
// mkvmerge) write a small primary SeekHead at the start of the segment
// that only references a secondary SeekHead at the end of the file,
// which in turn lists Info / Tracks / Tags / Chapters / Attachments.
// Follow such MkSeekHead -> MkSeekHead chains so the real entries are
// not silently dropped.
List<std::pair<unsigned int, offset_t>> entries =
matroskaSeekHead->entryList();
// Guard against pathological / circular chains.
int chainedSeekHeadsFollowed = 0;
constexpr int MAX_CHAINED_SEEKHEADS = 8;
for(unsigned int i = 0; i < entries.size(); ++i) {
const auto &[idValue, relativeOffset] = entries[i];
const offset_t absoluteOffset = segDataOffset + relativeOffset;
switch(static_cast<Id>(idValue)) {
case Id::MkSeekHead: {
if(chainedSeekHeadsFollowed++ >= MAX_CHAINED_SEEKHEADS)
break;
auto chained = readElementAt<Id::MkSeekHead, MkSeekHead>(
file, absoluteOffset, maxOffset);
if(!chained)
break;
if(const auto parsed = chained->parse(segDataOffset)) {
for(const auto &entry : parsed->entryList())
entries.append(entry);
}
break;
}
case Id::MkCues:
if(!skipCues) {
if(!((cues = readElementAt<Id::MkCues, MkCues>(
file, absoluteOffset, maxOffset))))
return false;
}
break;
case Id::MkInfo:
if(!((info = readElementAt<Id::MkInfo, MkInfo>(
file, absoluteOffset, maxOffset))))
return false;
break;
case Id::MkTracks:
if(!((tracks = readElementAt<Id::MkTracks, MkTracks>(
file, absoluteOffset, maxOffset))))
return false;
break;
case Id::MkTags:
if(!((tags = readElementAt<Id::MkTags, MkTags>(
file, absoluteOffset, maxOffset))))
return false;
accumulateVoidPadding(tags.get());
break;
case Id::MkAttachments:
if(!((attachments = readElementAt<Id::MkAttachments, MkAttachments>(
file, absoluteOffset, maxOffset))))
return false;
accumulateVoidPadding(attachments.get());
break;
case Id::MkChapters:
if(!((chapters = readElementAt<Id::MkChapters, MkChapters>(
file, absoluteOffset, maxOffset))))
return false;
accumulateVoidPadding(chapters.get());
break;
default:
break;
}
}
return true;
}
else if(id == Id::VoidElement) {
if(pendingPaddingTarget) {
accumulatedPadding += element->getSize();
pendingPaddingTarget->setPadding(accumulatedPadding);
}
element->skipData(file);
}
else if(id == Id::MkCues) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
if(!skipCues) {
cues = element_cast<Id::MkCues>(std::move(element));
if(!cues->read(file))
return false;
}
else {
element->skipData(file);
}
}
else if(id == Id::MkInfo) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
info = element_cast<Id::MkInfo>(std::move(element));
if(!info->read(file))
return false;
}
else if(id == Id::MkTracks) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
tracks = element_cast<Id::MkTracks>(std::move(element));
if(!tracks->read(file))
return false;
}
else if(id == Id::MkTags) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
tags = element_cast<Id::MkTags>(std::move(element));
if(!tags->read(file))
return false;
pendingPaddingTarget = tags.get();
}
else if(id == Id::MkAttachments) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
attachments = element_cast<Id::MkAttachments>(std::move(element));
if(!attachments->read(file))
return false;
pendingPaddingTarget = attachments.get();
}
else if(id == Id::MkChapters) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
chapters = element_cast<Id::MkChapters>(std::move(element));
if(!chapters->read(file))
return false;
pendingPaddingTarget = chapters.get();
}
else {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
element->skipData(file);
}
}
return true;
}
std::unique_ptr<Matroska::Tag> EBML::MkSegment::parseTag() const
{
return tags ? tags->parse() : nullptr;
}
std::unique_ptr<Matroska::Attachments> EBML::MkSegment::parseAttachments() const
{
return attachments ? attachments->parse() : nullptr;
}
std::unique_ptr<Matroska::Chapters> EBML::MkSegment::parseChapters() const
{
return chapters ? chapters->parse() : nullptr;
}
std::unique_ptr<Matroska::SeekHead> EBML::MkSegment::parseSeekHead() const
{
return seekHead ? seekHead->parse(segmentDataOffset()) : nullptr;
}
std::unique_ptr<Matroska::Cues> EBML::MkSegment::parseCues() const
{
return cues ? cues->parse(segmentDataOffset()) : nullptr;
}
std::unique_ptr<Matroska::Segment> EBML::MkSegment::parseSegment() const
{
return std::make_unique<Matroska::Segment>(sizeLength, dataSize, offset + idSize(id));
}
void EBML::MkSegment::parseInfo(Matroska::Properties *properties) const
{
if(info) {
info->parse(properties);
}
}
void EBML::MkSegment::parseTracks(Matroska::Properties *properties) const
{
if(tracks) {
tracks->parse(properties);
}
}

View File

@@ -0,0 +1,77 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKSEGMENT_H
#define TAGLIB_EBMLMKSEGMENT_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "ebmlmktags.h"
#include "ebmlmkattachments.h"
#include "ebmlmkchapters.h"
#include "ebmlmkseekhead.h"
#include "ebmlmkcues.h"
#include "ebmlmkinfo.h"
#include "ebmlmktracks.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Tag;
class Attachments;
class Chapters;
class SeekHead;
class Segment;
}
namespace EBML {
class MkSegment : public MasterElement
{
public:
MkSegment(int sizeLength, offset_t dataSize, offset_t offset);
MkSegment(Id, int sizeLength, offset_t dataSize, offset_t offset);
~MkSegment() override;
offset_t segmentDataOffset() const;
bool read(File &file) override;
bool readLimited(File &file, offset_t scanLimit);
std::unique_ptr<Matroska::Tag> parseTag() const;
std::unique_ptr<Matroska::Attachments> parseAttachments() const;
std::unique_ptr<Matroska::Chapters> parseChapters() const;
std::unique_ptr<Matroska::SeekHead> parseSeekHead() const;
std::unique_ptr<Matroska::Cues> parseCues() const;
std::unique_ptr<Matroska::Segment> parseSegment() const;
void parseInfo(Matroska::Properties *properties) const;
void parseTracks(Matroska::Properties *properties) const;
private:
std::unique_ptr<MkTags> tags;
std::unique_ptr<MkAttachments> attachments;
std::unique_ptr<MkChapters> chapters;
std::unique_ptr<MkSeekHead> seekHead;
std::unique_ptr<MkCues> cues;
std::unique_ptr<MkInfo> info;
std::unique_ptr<MkTracks> tracks;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,131 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmktags.h"
#include "ebmlbinaryelement.h"
#include "ebmluintelement.h"
#include "ebmlstringelement.h"
#include "matroskatag.h"
#include "matroskasimpletag.h"
#include "tlist.h"
using namespace TagLib;
EBML::MkTags::MkTags(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkTags, sizeLength, dataSize, offset)
{
}
EBML::MkTags::MkTags(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkTags, sizeLength, dataSize, offset)
{
}
EBML::MkTags::MkTags():
MasterElement(Id::MkTags, 0, 0, 0)
{
}
std::unique_ptr<Matroska::Tag> EBML::MkTags::parse() const
{
auto mTag = std::make_unique<Matroska::Tag>();
mTag->setOffset(offset);
mTag->setSize(getSize() + padding);
mTag->setID(static_cast<Matroska::Element::ID>(id));
// Loop through each <Tag> element
for(const auto &tagsChild : elements) {
if(tagsChild->getId() != Id::MkTag)
continue;
const auto tag = element_cast<Id::MkTag>(tagsChild);
List<const MasterElement *> simpleTags;
const MasterElement *targets = nullptr;
// Identify the <Targets> element and the <SimpleTag> elements
for(const auto &tagChild : *tag) {
if(const Id tagChildId = tagChild->getId(); !targets && tagChildId == Id::MkTagTargets)
targets = element_cast<Id::MkTagTargets>(tagChild);
else if(tagChildId == Id::MkSimpleTag)
simpleTags.append(element_cast<Id::MkSimpleTag>(tagChild));
}
// Parse the <Targets> element
Matroska::SimpleTag::TargetTypeValue targetTypeValue = Matroska::SimpleTag::TargetTypeValue::None;
unsigned long long trackUid = 0;
unsigned long long edtionUid = 0;
unsigned long long chapterUid = 0;
unsigned long long attachmentUid = 0;
if(targets) {
for(const auto &targetsChild : *targets) {
if(const Id id = targetsChild->getId(); id == Id::MkTagTargetTypeValue
&& targetTypeValue == Matroska::SimpleTag::TargetTypeValue::None) {
targetTypeValue = static_cast<Matroska::SimpleTag::TargetTypeValue>(
element_cast<Id::MkTagTargetTypeValue>(targetsChild)->getValue()
);
}
else if(id == Id::MkTagTrackUID) {
trackUid = element_cast<Id::MkTagTrackUID>(targetsChild)->getValue();
}
else if(id == Id::MkTagEditionUID) {
edtionUid = element_cast<Id::MkTagEditionUID>(targetsChild)->getValue();
}
else if(id == Id::MkTagChapterUID) {
chapterUid = element_cast<Id::MkTagChapterUID>(targetsChild)->getValue();
}
else if(id == Id::MkTagAttachmentUID) {
attachmentUid = element_cast<Id::MkTagAttachmentUID>(targetsChild)->getValue();
}
}
}
// Parse each <SimpleTag>
for(const auto simpleTag : simpleTags) {
const String *tagValueString = nullptr;
const ByteVector *tagValueBinary = nullptr;
String tagName;
String language;
bool defaultLanguageFlag = true;
for(const auto &simpleTagChild : *simpleTag) {
if(const Id id = simpleTagChild->getId(); id == Id::MkTagName && tagName.isEmpty())
tagName = element_cast<Id::MkTagName>(simpleTagChild)->getValue();
else if(id == Id::MkTagString && !tagValueString)
tagValueString = &element_cast<Id::MkTagString>(simpleTagChild)->getValue();
else if(id == Id::MkTagBinary && !tagValueBinary)
tagValueBinary = &element_cast<Id::MkTagBinary>(simpleTagChild)->getValue();
else if(id == Id::MkTagsTagLanguage && language.isEmpty())
language = element_cast<Id::MkTagsTagLanguage>(simpleTagChild)->getValue();
else if(id == Id::MkTagsLanguageDefault)
defaultLanguageFlag = element_cast<Id::MkTagsLanguageDefault>(simpleTagChild)->getValue() ? true : false;
}
if(tagName.isEmpty() || (tagValueString && tagValueBinary) || (!tagValueString && !tagValueBinary))
continue;
mTag->addSimpleTag(tagValueString
? Matroska::SimpleTag(tagName, *tagValueString,
targetTypeValue, language, defaultLanguageFlag,
trackUid, edtionUid, chapterUid, attachmentUid)
: Matroska::SimpleTag(tagName, *tagValueBinary,
targetTypeValue, language, defaultLanguageFlag,
trackUid, edtionUid, chapterUid, attachmentUid));
}
}
return mTag;
}

View File

@@ -0,0 +1,47 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKTAGS_H
#define TAGLIB_EBMLMKTAGS_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Tag;
}
namespace EBML {
class MkTags : public MasterElement
{
public:
MkTags(int sizeLength, offset_t dataSize, offset_t offset);
MkTags(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkTags();
std::unique_ptr<Matroska::Tag> parse() const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,86 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlmktracks.h"
#include "ebmlstringelement.h"
#include "ebmluintelement.h"
#include "ebmlfloatelement.h"
#include "matroskaproperties.h"
using namespace TagLib;
EBML::MkTracks::MkTracks(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkTracks, sizeLength, dataSize, offset)
{
}
EBML::MkTracks::MkTracks(Id, int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkTracks, sizeLength, dataSize, offset)
{
}
EBML::MkTracks::MkTracks():
MasterElement(Id::MkTracks, 0, 0, 0)
{
}
void EBML::MkTracks::parse(Matroska::Properties *properties) const
{
if(!properties)
return;
for(const auto &element : elements) {
if(element->getId() != Id::MkTrackEntry)
continue;
String codecId;
double samplingFrequency = 0.0;
unsigned long long bitDepth = 0;
unsigned long long channels = 0;
const auto trackEntry = element_cast<Id::MkTrackEntry>(element);
for(const auto &trackEntryChild : *trackEntry) {
if(const Id trackEntryChildId = trackEntryChild->getId(); trackEntryChildId == Id::MkCodecID)
codecId = element_cast<Id::MkCodecID>(trackEntryChild)->getValue();
else if(trackEntryChildId == Id::MkAudio) {
const auto audio = element_cast<Id::MkAudio>(trackEntryChild);
for(const auto &audioChild : *audio) {
if(const Id audioChildId = audioChild->getId(); audioChildId == Id::MkSamplingFrequency)
samplingFrequency = element_cast<Id::MkSamplingFrequency>(audioChild)->getValueAsDouble();
else if(audioChildId == Id::MkBitDepth)
bitDepth = element_cast<Id::MkBitDepth>(audioChild)->getValue();
else if(audioChildId == Id::MkChannels)
channels = element_cast<Id::MkChannels>(audioChild)->getValue();
}
}
}
if(bitDepth || channels) {
properties->setSampleRate(static_cast<int>(samplingFrequency));
properties->setBitsPerSample(static_cast<int>(bitDepth));
properties->setChannels(static_cast<int>(channels));
properties->setCodecName(codecId);
return;
}
}
}

View File

@@ -0,0 +1,52 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLMKTRACKS_H
#define TAGLIB_EBMLMKTRACKS_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlmasterelement.h"
#include "taglib.h"
namespace TagLib {
namespace Matroska {
class Properties;
}
namespace EBML {
class MkTracks : public MasterElement
{
public:
MkTracks(int sizeLength, offset_t dataSize, offset_t offset);
MkTracks(Id, int sizeLength, offset_t dataSize, offset_t offset);
MkTracks();
void parse(Matroska::Properties *properties) const;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,100 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlstringelement.h"
#include <string>
#include "tfile.h"
#include "tbytevector.h"
#include "tdebug.h"
#include "ebmlutils.h"
using namespace TagLib;
EBML::StringElement::StringElement(
String::Type stringEncoding, Id id, int sizeLength, offset_t dataSize):
Element(id, sizeLength, dataSize), encoding(stringEncoding)
{
}
const String& EBML::StringElement::getValue() const
{
return value;
}
void EBML::StringElement::setValue(const String& val)
{
value = val;
}
bool EBML::StringElement::read(File &file)
{
ByteVector buffer = file.readBlock(dataSize);
if(buffer.size() != dataSize) {
debug("Failed to read string");
return false;
}
// The EBML strings aren't supposed to be null-terminated,
// but we'll check for it and strip the null terminator if found
if(const int nullByte = buffer.find('\0'); nullByte >= 0)
buffer = ByteVector(buffer.data(), nullByte);
value = String(buffer, encoding);
return true;
}
ByteVector EBML::StringElement::render()
{
ByteVector buffer = renderId();
const std::string string = value.to8Bit(encoding == String::UTF8);
dataSize = string.size();
buffer.append(renderVINT(dataSize, 0));
buffer.append(ByteVector(string.data(), static_cast<unsigned int>(dataSize)));
return buffer;
}
EBML::UTF8StringElement::UTF8StringElement(Id id, int sizeLength, offset_t dataSize):
StringElement(String::UTF8, id, sizeLength, dataSize)
{
}
EBML::UTF8StringElement::UTF8StringElement(Id id, int sizeLength, offset_t dataSize, offset_t):
UTF8StringElement(id, sizeLength, dataSize)
{
}
EBML::UTF8StringElement::UTF8StringElement(Id id):
UTF8StringElement(id, 0, 0)
{
}
EBML::Latin1StringElement::Latin1StringElement(Id id, int sizeLength, offset_t dataSize):
StringElement(String::Latin1, id, sizeLength, dataSize)
{
}
EBML::Latin1StringElement::Latin1StringElement(Id id, int sizeLength, offset_t dataSize, offset_t):
Latin1StringElement(id, sizeLength, dataSize)
{
}
EBML::Latin1StringElement::Latin1StringElement(Id id):
Latin1StringElement(id, 0, 0)
{
}

View File

@@ -0,0 +1,65 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLSTRINGELEMENT_H
#define TAGLIB_EBMLSTRINGELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlelement.h"
#include "tstring.h"
namespace TagLib {
class File;
class ByteVector;
namespace EBML {
class StringElement : public Element
{
public:
StringElement(String::Type stringEncoding, Id id, int sizeLength, offset_t dataSize);
const String &getValue() const;
void setValue(const String &val);
bool read(File &file) override;
ByteVector render() override;
private:
String value;
String::Type encoding;
};
class UTF8StringElement : public StringElement {
public:
UTF8StringElement(Id id, int sizeLength, offset_t dataSize);
UTF8StringElement(Id id, int sizeLength, offset_t dataSize, offset_t);
explicit UTF8StringElement(Id id);
};
class Latin1StringElement : public StringElement {
public:
Latin1StringElement(Id id, int sizeLength, offset_t dataSize);
Latin1StringElement(Id id, int sizeLength, offset_t dataSize, offset_t);
explicit Latin1StringElement(Id id);
};
}
}
#endif
#endif

View File

@@ -0,0 +1,95 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmluintelement.h"
#include "ebmlutils.h"
#include "tbytevector.h"
#include "tfile.h"
#include "tutils.h"
#include "tdebug.h"
using namespace TagLib;
EBML::UIntElement::UIntElement(Id id, int sizeLength, offset_t dataSize):
Element(id, sizeLength, dataSize)
{
}
EBML::UIntElement::UIntElement(Id id, int sizeLength, offset_t dataSize, offset_t):
Element(id, sizeLength, dataSize)
{
}
EBML::UIntElement::UIntElement(Id id):
UIntElement(id, 0, 0)
{
}
unsigned long long EBML::UIntElement::getValue() const
{
return value;
}
void EBML::UIntElement::setValue(unsigned long long val)
{
value = val;
}
bool EBML::UIntElement::read(File &file)
{
const ByteVector buffer = file.readBlock(dataSize);
if(buffer.size() != dataSize) {
debug("Failed to read EBML Uint element");
return false;
}
value = buffer.toULongLong(true);
return true;
}
ByteVector EBML::UIntElement::render()
{
int dataSize = 0;
if(value <= 0xFFull)
dataSize = 1;
else if(value <= 0xFFFFull)
dataSize = 2;
else if(value <= 0xFFFFFFull)
dataSize = 3;
else if(value <= 0xFFFFFFFFull)
dataSize = 4;
else if(value <= 0xFFFFFFFFFFull)
dataSize = 5;
else if(value <= 0xFFFFFFFFFFFFull)
dataSize = 6;
else if(value <= 0xFFFFFFFFFFFFFFull)
dataSize = 7;
else if(value <= 0xFFFFFFFFFFFFFFFFull)
dataSize = 8;
ByteVector buffer = renderId();
buffer.append(renderVINT(dataSize, 0));
uint64_t val = value;
static const auto byteOrder = Utils::systemByteOrder();
if(byteOrder == Utils::LittleEndian)
val = Utils::byteSwap(val);
buffer.append(ByteVector(reinterpret_cast<char *>(&val) + (sizeof(val) - dataSize), dataSize));
return buffer;
}

View File

@@ -0,0 +1,50 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLUINTELEMENT_H
#define TAGLIB_EBMLUINTELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlelement.h"
namespace TagLib {
class File;
namespace EBML {
class UIntElement : public Element
{
public:
UIntElement(Id id, int sizeLength, offset_t dataSize);
UIntElement(Id id, int sizeLength, offset_t dataSize, offset_t);
explicit UIntElement(Id id);
unsigned long long getValue() const;
void setValue(unsigned long long val);
bool read(File &file) override;
ByteVector render() override;
private:
unsigned long long value = 0;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,122 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlutils.h"
#include <random>
#include "tbytevector.h"
#include "matroskafile.h"
#include "tutils.h"
#include "tdebug.h"
using namespace TagLib;
std::unique_ptr<EBML::Element> EBML::findElement(File &file, Element::Id id, offset_t maxOffset)
{
std::unique_ptr<Element> element;
while(file.tell() < maxOffset) {
element = Element::factory(file);
if(!element || element->getId() == id)
return element;
element->skipData(file);
element.reset();
}
return element;
}
std::unique_ptr<EBML::Element> EBML::findNextElement(File &file, offset_t maxOffset)
{
return file.tell() < maxOffset ? Element::factory(file) : nullptr;
}
template <int maxSizeLength>
unsigned int EBML::VINTSizeLength(uint8_t firstByte)
{
static_assert(maxSizeLength >= 1 && maxSizeLength <= 8);
if(!firstByte) {
debug("VINT with greater than 8 bytes not allowed");
return 0;
}
uint8_t mask = 0b10000000;
unsigned int numBytes = 1;
while(!(mask & firstByte)) {
numBytes++;
mask >>= 1;
}
if(numBytes > maxSizeLength) {
debug(Utils::formatString("VINT size length exceeds %i bytes", maxSizeLength));
return 0;
}
return numBytes;
}
namespace TagLib::EBML {
template unsigned int VINTSizeLength<4>(uint8_t firstByte);
template unsigned int VINTSizeLength<8>(uint8_t firstByte);
}
std::pair<unsigned int, uint64_t> EBML::readVINT(File &file)
{
auto buffer = file.readBlock(1);
if(buffer.size() != 1) {
debug("Failed to read VINT size");
return {0, 0};
}
unsigned int numBytes = VINTSizeLength<8>(*buffer.begin());
if(!numBytes)
return {0, 0};
if(numBytes > 1)
buffer.append(file.readBlock(numBytes - 1));
const int bitsToShift = static_cast<int>(sizeof(uint64_t) * 8) - 7 * numBytes;
const uint64_t mask = 0xFFFFFFFFFFFFFFFF >> bitsToShift;
return { numBytes, buffer.toULongLong(true) & mask };
}
std::pair<unsigned int, uint64_t> EBML::parseVINT(const ByteVector &buffer)
{
if(buffer.isEmpty())
return {0, 0};
unsigned int numBytes = VINTSizeLength<8>(*buffer.begin());
if(!numBytes)
return {0, 0};
const int bitsToShift = static_cast<int>(sizeof(uint64_t) * 8) - 7 * numBytes;
const uint64_t mask = 0xFFFFFFFFFFFFFFFF >> bitsToShift;
return { numBytes, buffer.toULongLong(true) & mask };
}
ByteVector EBML::renderVINT(uint64_t number, int minSizeLength)
{
const int numBytes = std::max(minSizeLength, minSize(number));
number |= 1ULL << (numBytes * 7);
static const auto byteOrder = Utils::systemByteOrder();
if(byteOrder == Utils::LittleEndian)
number = Utils::byteSwap(number);
return ByteVector(reinterpret_cast<char *>(&number) + (sizeof(number) - numBytes), numBytes);
}
unsigned long long EBML::randomUID()
{
static std::random_device device;
static std::mt19937 generator(device());
static std::uniform_int_distribution<unsigned long long> distribution;
return distribution(generator);
}

View File

@@ -0,0 +1,78 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLUTILS_H
#define TAGLIB_EBMLUTILS_H
#ifndef DO_NOT_DOCUMENT
#include <utility>
#include "taglib.h"
#include "ebmlelement.h"
namespace TagLib {
class File;
class ByteVector;
namespace EBML {
std::unique_ptr<Element> findElement(File &file, Element::Id id, offset_t maxOffset);
std::unique_ptr<Element> findNextElement(File &file, offset_t maxOffset);
template <int maxSizeLength>
unsigned int VINTSizeLength(uint8_t firstByte);
std::pair<unsigned int, uint64_t> readVINT(File &file);
std::pair<unsigned int, uint64_t> parseVINT(const ByteVector &buffer);
ByteVector renderVINT(uint64_t number, int minSizeLength);
unsigned long long randomUID();
constexpr int minSize(uint64_t data)
{
if(data <= 0x7Fu)
return 1;
if(data <= 0x3FFFu)
return 2;
if(data <= 0x1FFFFFu)
return 3;
if(data <= 0xFFFFFFFu)
return 4;
if(data <= 0x7FFFFFFFFu)
return 5;
return 0;
}
constexpr int idSize(Element::Id id)
{
const auto uintId = static_cast<unsigned int>(id);
if(uintId <= 0xFF)
return 1;
if(uintId <= 0xFFFF)
return 2;
if(uintId <= 0xFFFFFF)
return 3;
return 4;
}
}
}
#endif
#endif

View File

@@ -0,0 +1,73 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "ebmlvoidelement.h"
#include <algorithm>
#include "ebmlutils.h"
#include "tbytevector.h"
using namespace TagLib;
EBML::VoidElement::VoidElement(int sizeLength, offset_t dataSize):
Element(Id::VoidElement, sizeLength, dataSize)
{
}
EBML::VoidElement::VoidElement(Id, int sizeLength, offset_t dataSize, offset_t):
Element(Id::VoidElement, sizeLength, dataSize)
{
}
EBML::VoidElement::VoidElement():
Element(Id::VoidElement, 0, 0)
{
}
ByteVector EBML::VoidElement::render()
{
offset_t bytesNeeded = targetSize;
ByteVector buffer = renderId();
bytesNeeded -= buffer.size();
sizeLength = static_cast<int>(std::min(bytesNeeded, static_cast<offset_t>(8)));
bytesNeeded -= sizeLength;
dataSize = bytesNeeded;
buffer.append(renderVINT(dataSize, sizeLength));
if(dataSize)
buffer.append(ByteVector(static_cast<unsigned int>(dataSize), 0));
return buffer;
}
offset_t EBML::VoidElement::getTargetSize() const
{
return targetSize;
}
void EBML::VoidElement::setTargetSize(offset_t size)
{
this->targetSize = std::max(size, MIN_VOID_ELEMENT_SIZE);
}
ByteVector EBML::VoidElement::renderSize(offset_t targetSize)
{
VoidElement element;
element.setTargetSize(targetSize);
return element.render();
}

View File

@@ -0,0 +1,51 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_EBMLVOIDELEMENT_H
#define TAGLIB_EBMLVOIDELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include "ebmlelement.h"
namespace TagLib {
class File;
namespace EBML {
inline constexpr offset_t MIN_VOID_ELEMENT_SIZE = 2;
class VoidElement : public Element
{
public:
VoidElement(int sizeLength, offset_t dataSize);
VoidElement(Id, int sizeLength, offset_t dataSize, offset_t);
VoidElement();
ByteVector render() override;
offset_t getTargetSize() const;
void setTargetSize(offset_t size);
static ByteVector renderSize(offset_t targetSize);
private:
offset_t targetSize = MIN_VOID_ELEMENT_SIZE;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,99 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskaattachedfile.h"
#include "tbytevector.h"
using namespace TagLib;
class Matroska::AttachedFile::AttachedFilePrivate
{
public:
AttachedFilePrivate(const ByteVector &data, const String &fileName,
const String &mediaType, UID uid, const String &description) :
fileName(fileName), description(description), mediaType(mediaType),
data(data), uid(uid) {}
~AttachedFilePrivate() = default;
String fileName;
String description;
String mediaType;
ByteVector data;
UID uid = 0;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::AttachedFile::AttachedFile(const ByteVector &data,
const String &fileName, const String &mediaType, UID uid,
const String &description) :
d(std::make_unique<AttachedFilePrivate>(data, fileName, mediaType, uid, description))
{
}
Matroska::AttachedFile::AttachedFile(const AttachedFile &other) :
d(std::make_unique<AttachedFilePrivate>(*other.d))
{
}
Matroska::AttachedFile::AttachedFile(AttachedFile &&other) noexcept = default;
Matroska::AttachedFile::~AttachedFile() = default;
Matroska::AttachedFile &Matroska::AttachedFile::operator=(AttachedFile &&other) noexcept = default;
Matroska::AttachedFile &Matroska::AttachedFile::operator=(const AttachedFile &other)
{
AttachedFile(other).swap(*this);
return *this;
}
void Matroska::AttachedFile::swap(AttachedFile &other) noexcept
{
using std::swap;
swap(d, other.d);
}
const String &Matroska::AttachedFile::fileName() const
{
return d->fileName;
}
const String &Matroska::AttachedFile::description() const
{
return d->description;
}
const String &Matroska::AttachedFile::mediaType() const
{
return d->mediaType;
}
const ByteVector &Matroska::AttachedFile::data() const
{
return d->data;
}
Matroska::AttachedFile::UID Matroska::AttachedFile::uid() const
{
return d->uid;
}

View File

@@ -0,0 +1,110 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKAATTACHEDFILE_H
#define TAGLIB_MATROSKAATTACHEDFILE_H
#include <memory>
#include "tstring.h"
#include "taglib_export.h"
namespace TagLib {
class String;
class ByteVector;
namespace Matroska {
//! Attached file embedded into a Matroska file.
class TAGLIB_EXPORT AttachedFile
{
public:
//! Unique identifier.
using UID = unsigned long long;
/*!
* Construct an attached file.
*/
AttachedFile(const ByteVector &data, const String &fileName,
const String &mediaType, UID uid = 0,
const String &description = String());
/*!
* Construct an attached file as a copy of \a other.
*/
AttachedFile(const AttachedFile &other);
/*!
* Construct an attached file moving from \a other.
*/
AttachedFile(AttachedFile &&other) noexcept;
/*!
* Destroys this attached file.
*/
~AttachedFile();
/*!
* Copies the contents of \a other into this object.
*/
AttachedFile &operator=(const AttachedFile &other);
/*!
* Moves the contents of \a other into this object.
*/
AttachedFile &operator=(AttachedFile &&other) noexcept;
/*!
* Exchanges the content of the object with the content of \a other.
*/
void swap(AttachedFile &other) noexcept;
/*!
* Returns the filename of the attached file.
*/
const String &fileName() const;
/*!
* Returns the human-friendly description for the attached file.
*/
const String &description() const;
/*!
* Returns the media type of the attached file.
*/
const String &mediaType() const;
/*!
* Returns the data of the attached file.
*/
const ByteVector &data() const;
/*!
* Returns the UID of the attached file.
*/
UID uid() const;
private:
class AttachedFilePrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<AttachedFilePrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,150 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskaattachments.h"
#include "matroskaattachedfile.h"
#include "ebmlmkattachments.h"
#include "ebmlmasterelement.h"
#include "ebmlstringelement.h"
#include "ebmlbinaryelement.h"
#include "ebmluintelement.h"
#include "ebmlutils.h"
#include "tlist.h"
#include "tbytevector.h"
using namespace TagLib;
class Matroska::Attachments::AttachmentsPrivate
{
public:
AttachmentsPrivate() = default;
~AttachmentsPrivate() = default;
AttachmentsPrivate(const AttachmentsPrivate &) = delete;
AttachmentsPrivate &operator=(const AttachmentsPrivate &) = delete;
AttachedFileList files;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::Attachments::Attachments() :
Element(static_cast<ID>(EBML::Element::Id::MkAttachments)),
d(std::make_unique<AttachmentsPrivate>())
{
}
Matroska::Attachments::~Attachments() = default;
void Matroska::Attachments::addAttachedFile(const AttachedFile &file)
{
d->files.append(file);
setNeedsRender(true);
}
void Matroska::Attachments::removeAttachedFile(unsigned long long uid)
{
const auto it = std::find_if(d->files.begin(), d->files.end(),
[uid](const AttachedFile &file) {
return file.uid() == uid;
});
if(it != d->files.end()) {
d->files.erase(it);
setNeedsRender(true);
}
}
void Matroska::Attachments::clear()
{
d->files.clear();
setNeedsRender(true);
}
const Matroska::Attachments::AttachedFileList &Matroska::Attachments::attachedFileList() const
{
return d->files;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
Matroska::Attachments::AttachedFileList &Matroska::Attachments::attachedFiles()
{
setNeedsRender(true);
return d->files;
}
ByteVector Matroska::Attachments::renderInternal()
{
if(d->files.isEmpty()) {
// Avoid writing an Attachments element without AttachedFile element.
return {};
}
EBML::MkAttachments attachments;
for(const auto &attachedFile : std::as_const(d->files)) {
auto attachedFileElement = EBML::make_unique_element<EBML::Element::Id::MkAttachedFile>();
// Filename
auto fileNameElement = EBML::make_unique_element<EBML::Element::Id::MkAttachedFileName>();
fileNameElement->setValue(attachedFile.fileName());
attachedFileElement->appendElement(std::move(fileNameElement));
// Media/MIME type
auto mediaTypeElement =
EBML::make_unique_element<EBML::Element::Id::MkAttachedFileMediaType>();
mediaTypeElement->setValue(attachedFile.mediaType());
attachedFileElement->appendElement(std::move(mediaTypeElement));
// Description
if(const String &description = attachedFile.description(); !description.isEmpty()) {
auto descriptionElement =
EBML::make_unique_element<EBML::Element::Id::MkAttachedFileDescription>();
descriptionElement->setValue(description);
attachedFileElement->appendElement(std::move(descriptionElement));
}
// Data
auto dataElement = EBML::make_unique_element<EBML::Element::Id::MkAttachedFileData>();
dataElement->setValue(attachedFile.data());
attachedFileElement->appendElement(std::move(dataElement));
// UID
auto uidElement = EBML::make_unique_element<EBML::Element::Id::MkAttachedFileUID>();
AttachedFile::UID uid = attachedFile.uid();
if(!uid)
uid = EBML::randomUID();
uidElement->setValue(uid);
attachedFileElement->appendElement(std::move(uidElement));
attachments.appendElement(std::move(attachedFileElement));
}
// Pad to the previous size so the element keeps its slot in the file,
// unless this element is the trailing element of the segment in
// AvoidInsert mode -- shrinking from the end never inserts anything.
if(writeStyle() != WriteStyle::Compact &&
!(writeStyle() == WriteStyle::AvoidInsert && isTrailingInSegment())) {
const auto beforeSize = sizeRenderedOrWritten();
if(beforeSize > 0)
attachments.setMinRenderSize(beforeSize);
}
return attachments.render();
}

View File

@@ -0,0 +1,83 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKAATTACHMENTS_H
#define TAGLIB_MATROSKAATTACHMENTS_H
#include <memory>
#include "taglib_export.h"
#include "tlist.h"
#include "matroskaelement.h"
namespace TagLib {
class File;
namespace EBML {
class MkAttachments;
}
namespace Matroska {
class AttachedFile;
class File;
//! Collection of attached files.
class TAGLIB_EXPORT Attachments
#ifndef DO_NOT_DOCUMENT
: private Element
#endif
{
public:
//! List of attached files.
using AttachedFileList = List<AttachedFile>;
//! Construct attachments.
Attachments();
//! Destroy attachments.
virtual ~Attachments();
//! Add an attached file.
void addAttachedFile(const AttachedFile &file);
//! Remove an attached file.
void removeAttachedFile(unsigned long long uid);
//! Remove all attached files.
void clear();
//! Get list of all attached files.
const AttachedFileList &attachedFileList() const;
private:
friend class EBML::MkAttachments;
friend class File;
class AttachmentsPrivate;
// private Element implementation
ByteVector renderInternal() override;
AttachedFileList &attachedFiles();
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<AttachmentsPrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,157 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskachapter.h"
#include "tstring.h"
#include "tbytevector.h"
using namespace TagLib;
class Matroska::Chapter::Display::DisplayPrivate
{
public:
DisplayPrivate() = default;
~DisplayPrivate() = default;
String string;
String language;
};
class Matroska::Chapter::ChapterPrivate
{
public:
ChapterPrivate() = default;
~ChapterPrivate() = default;
UID uid = 0;
Time timeStart = 0;
Time timeEnd = 0;
List<Display> displayList;
bool hidden = false;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::Chapter::Chapter(Time timeStart, Time timeEnd,
const List<Display> &displayList, UID uid, bool hidden) :
d(std::make_unique<ChapterPrivate>())
{
d->uid = uid;
d->timeStart = timeStart;
d->timeEnd = timeEnd;
d->displayList = displayList;
d->hidden = hidden;
}
Matroska::Chapter::Chapter(const Chapter &other) :
d(std::make_unique<ChapterPrivate>(*other.d))
{
}
Matroska::Chapter::Chapter(Chapter &&other) noexcept = default;
Matroska::Chapter::~Chapter() = default;
Matroska::Chapter &Matroska::Chapter::operator=(Chapter &&other) noexcept = default;
Matroska::Chapter &Matroska::Chapter::operator=(const Chapter &other)
{
Chapter(other).swap(*this);
return *this;
}
void Matroska::Chapter::swap(Chapter &other) noexcept
{
using std::swap;
swap(d, other.d);
}
Matroska::Chapter::UID Matroska::Chapter::uid() const
{
return d->uid;
}
Matroska::Chapter::Time Matroska::Chapter::timeStart() const
{
return d->timeStart;
}
Matroska::Chapter::Time Matroska::Chapter::timeEnd() const
{
return d->timeEnd;
}
bool Matroska::Chapter::isHidden() const
{
return d->hidden;
}
const List<Matroska::Chapter::Display> &Matroska::Chapter::displayList() const
{
return d->displayList;
}
Matroska::Chapter::Display::Display(const String &string, const String &language) :
d(std::make_unique<DisplayPrivate>())
{
d->string = string;
d->language = language;
}
Matroska::Chapter::Display::Display(const Display &other) :
d(std::make_unique<DisplayPrivate>(*other.d))
{
}
Matroska::Chapter::Display::Display(Display &&other) noexcept = default;
Matroska::Chapter::Display::~Display() = default;
Matroska::Chapter::Display &Matroska::Chapter::Display::operator=(const Display &other)
{
Display(other).swap(*this);
return *this;
}
Matroska::Chapter::Display &Matroska::Chapter::Display::operator=(
Display &&other) noexcept = default;
void Matroska::Chapter::Display::swap(Display &other) noexcept
{
using std::swap;
swap(d, other.d);
}
const String &Matroska::Chapter::Display::string() const
{
return d->string;
}
const String &Matroska::Chapter::Display::language() const
{
return d->language;
}

View File

@@ -0,0 +1,178 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKACHAPTER_H
#define TAGLIB_MATROSKACHAPTER_H
#include <memory>
#include "taglib_export.h"
#include "tlist.h"
namespace TagLib {
class String;
class ByteVector;
namespace EBML {
class MkChapters;
}
namespace Matroska {
//! Matroska chapter.
class TAGLIB_EXPORT Chapter
{
public:
//! Unique identifier.
using UID = unsigned long long;
//! Timestamp in nanoseconds.
using Time = unsigned long long;
/*!
* Contains all possible strings to use for the chapter display.
*/
class TAGLIB_EXPORT Display
{
public:
/*!
* Construct a chapter display.
*/
Display(const String &string, const String &language);
/*!
* Construct a chapter display as a copy of \a other.
*/
Display(const Display &other);
/*!
* Construct a chapter display moving from \a other.
*/
Display(Display &&other) noexcept;
/*!
* Destroys this chapter display.
*/
~Display();
/*!
* Copies the contents of \a other into this object.
*/
Display &operator=(const Display &other);
/*!
* Moves the contents of \a other into this object.
*/
Display &operator=(Display &&other) noexcept;
/*!
* Exchanges the content of the object with the content of \a other.
*/
void swap(Display &other) noexcept;
/*!
* Returns string representing the chapter.
*/
const String &string() const;
/*!
* Returns language corresponding to the string.
*/
const String &language() const;
private:
class DisplayPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<DisplayPrivate> d;
};
/*!
* Construct a chapter.
*/
Chapter(Time timeStart, Time timeEnd, const List<Display> &displayList,
UID uid, bool hidden = false);
/*!
* Construct a chapter as a copy of \a other.
*/
Chapter(const Chapter &other);
/*!
* Construct a chapter moving from \a other.
*/
Chapter(Chapter &&other) noexcept;
/*!
* Destroys this chapter.
*/
~Chapter();
/*!
* Copies the contents of \a other into this object.
*/
Chapter &operator=(const Chapter &other);
/*!
* Moves the contents of \a other into this object.
*/
Chapter &operator=(Chapter &&other) noexcept;
/*!
* Exchanges the content of the object with the content of \a other.
*/
void swap(Chapter &other) noexcept;
/*!
* Returns the UID of the chapter.
*/
UID uid() const;
/*!
* Returns the timestamp of the start of the chapter in nanoseconds.
*/
Time timeStart() const;
/*!
* Returns the timestamp of the start of the chapter in nanoseconds.
*/
Time timeEnd() const;
/*!
* Check if chapter is hidden.
*/
bool isHidden() const;
/*!
* Returns strings with language.
*/
const List<Display> &displayList() const;
private:
class ChapterPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChapterPrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,102 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskachapter.h"
#include "matroskachapteredition.h"
#include "tstring.h"
#include "tbytevector.h"
#include "tlist.h"
using namespace TagLib;
class Matroska::ChapterEdition::ChapterEditionPrivate
{
public:
ChapterEditionPrivate() = default;
~ChapterEditionPrivate() = default;
List<Chapter> chapters;
UID uid = 0;
bool flagDefault = false;
bool flagOrdered = false;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::ChapterEdition::ChapterEdition(const List<Chapter> &chapterList,
bool isDefault, bool isOrdered, UID uid) :
d(std::make_unique<ChapterEditionPrivate>())
{
d->chapters = chapterList;
d->uid = uid;
d->flagDefault = isDefault;
d->flagOrdered = isOrdered;
}
Matroska::ChapterEdition::ChapterEdition(const ChapterEdition &other) :
d(std::make_unique<ChapterEditionPrivate>(*other.d))
{
}
Matroska::ChapterEdition::ChapterEdition(ChapterEdition &&other) noexcept = default;
Matroska::ChapterEdition::~ChapterEdition() = default;
Matroska::ChapterEdition &Matroska::ChapterEdition::operator=(
ChapterEdition &&other) noexcept = default;
Matroska::ChapterEdition &Matroska::ChapterEdition::operator=(const ChapterEdition &other)
{
ChapterEdition(other).swap(*this);
return *this;
}
void Matroska::ChapterEdition::swap(ChapterEdition &other) noexcept
{
using std::swap;
swap(d, other.d);
}
Matroska::ChapterEdition::UID Matroska::ChapterEdition::uid() const
{
return d->uid;
}
bool Matroska::ChapterEdition::isDefault() const
{
return d->flagDefault;
}
bool Matroska::ChapterEdition::isOrdered() const
{
return d->flagOrdered;
}
const List<Matroska::Chapter> &Matroska::ChapterEdition::chapterList() const
{
return d->chapters;
}

View File

@@ -0,0 +1,108 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKACHAPTEREDITION_H
#define TAGLIB_MATROSKACHAPTEREDITION_H
#include "matroskachapter.h"
namespace TagLib {
class String;
class ByteVector;
namespace Matroska {
//! Edition of chapters.
class TAGLIB_EXPORT ChapterEdition
{
public:
//! Unique identifier.
using UID = unsigned long long;
/*!
* Construct an edition.
*/
ChapterEdition(const List<Chapter> &chapterList,
bool isDefault, bool isOrdered = false, UID uid = 0);
/*!
* Construct an edition as a copy of \a other.
*/
ChapterEdition(const ChapterEdition &other);
/*!
* Construct an edition moving from \a other.
*/
ChapterEdition(ChapterEdition &&other) noexcept;
/*!
* Destroys this edition.
*/
~ChapterEdition();
/*!
* Copies the contents of \a other into this object.
*/
ChapterEdition &operator=(const ChapterEdition &other);
/*!
* Moves the contents of \a other into this object.
*/
ChapterEdition &operator=(ChapterEdition &&other) noexcept;
/*!
* Exchanges the content of the object with the content of \a other.
*/
void swap(ChapterEdition &other) noexcept;
/*!
* Returns the UID of the edition.
*/
UID uid() const;
/*!
* Check if this edition should be used as the default one.
*/
bool isDefault() const;
/*!
* Check if the chapters can be defined multiple times and the order to
* play them is enforced.
*/
bool isOrdered() const;
/*!
* Get the list of all chapters.
*/
const List<Chapter> &chapterList() const;
private:
class ChapterEditionPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChapterEditionPrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,160 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskachapters.h"
#include "matroskachapteredition.h"
#include "ebmlstringelement.h"
#include "ebmlbinaryelement.h"
#include "ebmlmkchapters.h"
#include "ebmluintelement.h"
#include "ebmlutils.h"
#include "tlist.h"
#include "tbytevector.h"
using namespace TagLib;
class Matroska::Chapters::ChaptersPrivate
{
public:
ChaptersPrivate() = default;
~ChaptersPrivate() = default;
ChaptersPrivate(const ChaptersPrivate &) = delete;
ChaptersPrivate &operator=(const ChaptersPrivate &) = delete;
ChapterEditionList editions;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::Chapters::Chapters() :
Element(static_cast<ID>(EBML::Element::Id::MkChapters)),
d(std::make_unique<ChaptersPrivate>())
{
}
Matroska::Chapters::~Chapters() = default;
void Matroska::Chapters::addChapterEdition(const ChapterEdition &edition)
{
d->editions.append(edition);
setNeedsRender(true);
}
void Matroska::Chapters::removeChapterEdition(unsigned long long uid)
{
const auto it = std::find_if(d->editions.begin(), d->editions.end(),
[uid](const ChapterEdition &file) {
return file.uid() == uid;
});
if(it != d->editions.end()) {
d->editions.erase(it);
setNeedsRender(true);
}
}
void Matroska::Chapters::clear()
{
d->editions.clear();
setNeedsRender(true);
}
const Matroska::Chapters::ChapterEditionList &Matroska::Chapters::chapterEditionList() const
{
return d->editions;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
ByteVector Matroska::Chapters::renderInternal()
{
if(d->editions.isEmpty()) {
// Avoid writing a Chapters element without ChapterEdition element.
return {};
}
EBML::MkChapters chapters;
for(const auto &chapterEdition : std::as_const(d->editions)) {
auto chapterEditionElement = EBML::make_unique_element<EBML::Element::Id::MkEditionEntry>();
if(const auto uid = chapterEdition.uid()) {
auto uidElement = EBML::make_unique_element<EBML::Element::Id::MkEditionUID>();
uidElement->setValue(uid);
chapterEditionElement->appendElement(std::move(uidElement));
}
auto defaultElement = EBML::make_unique_element<EBML::Element::Id::MkEditionFlagDefault>();
defaultElement->setValue(chapterEdition.isDefault());
chapterEditionElement->appendElement(std::move(defaultElement));
auto orderedElement = EBML::make_unique_element<EBML::Element::Id::MkEditionFlagOrdered>();
orderedElement->setValue(chapterEdition.isOrdered());
chapterEditionElement->appendElement(std::move(orderedElement));
for(const auto &chapter : chapterEdition.chapterList()) {
auto chapterElement = EBML::make_unique_element<EBML::Element::Id::MkChapterAtom>();
auto cuidElement = EBML::make_unique_element<EBML::Element::Id::MkChapterUID>();
const auto cuid = chapter.uid();
cuidElement->setValue(cuid ? cuid : EBML::randomUID());
chapterElement->appendElement(std::move(cuidElement));
auto timeStartElement = EBML::make_unique_element<EBML::Element::Id::MkChapterTimeStart>();
timeStartElement->setValue(chapter.timeStart());
chapterElement->appendElement(std::move(timeStartElement));
auto timeEndElement = EBML::make_unique_element<EBML::Element::Id::MkChapterTimeEnd>();
timeEndElement->setValue(chapter.timeEnd());
chapterElement->appendElement(std::move(timeEndElement));
auto hiddenElement = EBML::make_unique_element<EBML::Element::Id::MkChapterFlagHidden>();
hiddenElement->setValue(chapter.isHidden());
chapterElement->appendElement(std::move(hiddenElement));
for(const auto &display : chapter.displayList()) {
auto displayElement = EBML::make_unique_element<EBML::Element::Id::MkChapterDisplay>();
auto stringElement = EBML::make_unique_element<EBML::Element::Id::MkChapString>();
stringElement->setValue(display.string());
displayElement->appendElement(std::move(stringElement));
auto languageElement = EBML::make_unique_element<EBML::Element::Id::MkChapLanguage>();
languageElement->setValue(display.language());
displayElement->appendElement(std::move(languageElement));
chapterElement->appendElement(std::move(displayElement));
}
chapterEditionElement->appendElement(std::move(chapterElement));
}
chapters.appendElement(std::move(chapterEditionElement));
}
// Pad to the previous size so the element keeps its slot in the file,
// unless this element is the trailing element of the segment in
// AvoidInsert mode -- shrinking from the end never inserts anything.
if(writeStyle() != WriteStyle::Compact &&
!(writeStyle() == WriteStyle::AvoidInsert && isTrailingInSegment())) {
const auto beforeSize = sizeRenderedOrWritten();
if(beforeSize > 0)
chapters.setMinRenderSize(beforeSize);
}
return chapters.render();
}

View File

@@ -0,0 +1,87 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKACHAPTERS_H
#define TAGLIB_MATROSKACHAPTERS_H
#include <memory>
#include "taglib_export.h"
#include "tlist.h"
#include "matroskaelement.h"
namespace TagLib {
class File;
namespace EBML {
class MkChapters;
}
namespace Matroska {
class ChapterEdition;
class File;
//! Collection of chapter editions.
class TAGLIB_EXPORT Chapters
#ifndef DO_NOT_DOCUMENT
: private Element
#endif
{
public:
//! List of chapter editions.
using ChapterEditionList = List<ChapterEdition>;
//! Construct chapters.
Chapters();
//! Destroy chapters.
virtual ~Chapters();
//! Add a chapter edition.
void addChapterEdition(const ChapterEdition &edition);
//! Remove a chapter edition.
void removeChapterEdition(unsigned long long uid);
//! Remove all chapter editions.
void clear();
//! Get list of all chapter editions.
const ChapterEditionList &chapterEditionList() const;
private:
friend class EBML::MkChapters;
friend class File;
class ChaptersPrivate;
// private Element implementation
ByteVector renderInternal() override;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChaptersPrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,312 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskacues.h"
#include "ebmlelement.h"
#include "ebmlmkcues.h"
#include "ebmlmasterelement.h"
#include "ebmluintelement.h"
#include "tlist.h"
#include "tdebug.h"
#include "tfile.h"
using namespace TagLib;
Matroska::Cues::Cues(offset_t segmentDataOffset) :
Element(static_cast<ID>(EBML::Element::Id::MkCues)),
segmentDataOffset(segmentDataOffset)
{
setNeedsRender(false);
}
Matroska::Cues::~Cues() = default;
ByteVector Matroska::Cues::renderInternal()
{
const auto beforeSize = sizeRenderedOrWritten();
EBML::MkCues cues;
cues.setMinRenderSize(beforeSize);
for(const auto &cuePoint : cuePoints) {
auto cuePointElement = EBML::make_unique_element<EBML::Element::Id::MkCuePoint>();
auto timestamp = EBML::make_unique_element<EBML::Element::Id::MkCueTime>();
timestamp->setValue(cuePoint->getTime());
cuePointElement->appendElement(std::move(timestamp));
const auto &trackList = cuePoint->cueTrackList();
for(const auto &cueTrack : trackList) {
auto cueTrackElement = EBML::make_unique_element<EBML::Element::Id::MkCueTrackPositions>();
// Track number
auto trackNumber = EBML::make_unique_element<EBML::Element::Id::MkCueTrack>();
trackNumber->setValue(cueTrack->getTrackNumber());
cueTrackElement->appendElement(std::move(trackNumber));
// Cluster position
auto clusterPosition = EBML::make_unique_element<EBML::Element::Id::MkCueClusterPosition>();
clusterPosition->setValue(cueTrack->getClusterPosition());
cueTrackElement->appendElement(std::move(clusterPosition));
// Relative position, optional
if(cueTrack->getRelativePosition().has_value()) {
auto relativePosition = EBML::make_unique_element<EBML::Element::Id::MkCueRelativePosition>();
// operator*() used instead of value() to support restricted compilers
relativePosition->setValue(*cueTrack->getRelativePosition());
cueTrackElement->appendElement(std::move(relativePosition));
}
// Duration, optional
if(cueTrack->getDuration().has_value()) {
auto duration = EBML::make_unique_element<EBML::Element::Id::MkCueDuration>();
// operator*() used instead of value() to support restricted compilers
duration->setValue(*cueTrack->getDuration());
cueTrackElement->appendElement(std::move(duration));
}
// Block number, optional
if(cueTrack->getBlockNumber().has_value()) {
auto blockNumber = EBML::make_unique_element<EBML::Element::Id::MkCueBlockNumber>();
// operator*() used instead of value() to support restricted compilers
blockNumber->setValue(*cueTrack->getBlockNumber());
cueTrackElement->appendElement(std::move(blockNumber));
}
// Codec state, not in version 1
if(cueTrack->getCodecState().has_value()) {
auto codecState = EBML::make_unique_element<EBML::Element::Id::MkCueCodecState>();
// operator*() used instead of value() to support restricted compilers
codecState->setValue(*cueTrack->getCodecState());
cueTrackElement->appendElement(std::move(codecState));
}
// Reference times
if(auto referenceTimes = cueTrack->referenceTimes(); !referenceTimes.isEmpty()) {
auto cueReference = EBML::make_unique_element<EBML::Element::Id::MkCueReference>();
for(const auto reference : referenceTimes) {
auto refTime = EBML::make_unique_element<EBML::Element::Id::MkCueRefTime>();
refTime->setValue(reference);
cueReference->appendElement(std::move(refTime));
}
cueTrackElement->appendElement(std::move(cueReference));
}
cuePointElement->appendElement(std::move(cueTrackElement));
}
cues.appendElement(std::move(cuePointElement));
}
return cues.render();
}
void Matroska::Cues::write(TagLib::File &file)
{
if(!data().isEmpty())
Element::write(file);
}
bool Matroska::Cues::sizeChanged(Element &caller, offset_t delta)
{
// Adjust own offset
if(!Element::sizeChanged(caller, delta))
return false;
const offset_t offset = caller.offset() - segmentDataOffset;
for(const auto &cuePoint : cuePoints) {
if(cuePoint->adjustOffset(offset, delta)) {
setNeedsRender(true);
}
}
return true;
}
bool Matroska::Cues::isValid(TagLib::File &file) const
{
for(const auto &cuePoint : cuePoints) {
if(!cuePoint->isValid(file, segmentDataOffset))
return false;
}
return true;
}
void Matroska::Cues::addCuePoint(std::unique_ptr<CuePoint> &&cuePoint)
{
cuePoints.push_back(std::move(cuePoint));
}
const Matroska::Cues::CuePointList &Matroska::Cues::cuePointList()
{
return cuePoints;
}
Matroska::CuePoint::CuePoint() = default;
Matroska::CuePoint::~CuePoint() = default;
bool Matroska::CuePoint::isValid(TagLib::File &file, offset_t segmentDataOffset) const
{
for(const auto &track : cueTracks) {
if(!track->isValid(file, segmentDataOffset))
return false;
}
return true;
}
void Matroska::CuePoint::addCueTrack(std::unique_ptr<CueTrack> &&cueTrack)
{
cueTracks.push_back(std::move(cueTrack));
}
const Matroska::CuePoint::CueTrackList &Matroska::CuePoint::cueTrackList() const
{
return cueTracks;
}
void Matroska::CuePoint::setTime(Time timestamp)
{
time = timestamp;
}
Matroska::CuePoint::Time Matroska::CuePoint::getTime() const
{
return time;
}
bool Matroska::CuePoint::adjustOffset(offset_t offset, offset_t delta)
{
bool ret = false;
for(const auto &cueTrack : cueTracks)
ret |= cueTrack->adjustOffset(offset, delta);
return ret;
}
Matroska::CueTrack::CueTrack() = default;
Matroska::CueTrack::~CueTrack() = default;
bool Matroska::CueTrack::isValid(TagLib::File &file, offset_t segmentDataOffset) const
{
if(!trackNumber) {
debug("Cue track number not set");
return false;
}
if(!clusterPosition) {
debug("Cue track cluster position not set");
return false;
}
file.seek(segmentDataOffset + clusterPosition);
if(EBML::Element::readId(file) != static_cast<unsigned int>(EBML::Element::Id::MkCluster)) {
debug("No cluster found at position");
return false;
}
if(codecState.has_value() && *codecState != 0) {
// operator*() used instead of value() to support restricted compilers
file.seek(segmentDataOffset + *codecState);
if(EBML::Element::readId(file) != static_cast<unsigned int>(EBML::Element::Id::MkCodecState)) {
debug("No codec state found at position");
return false;
}
}
return true;
}
void Matroska::CueTrack::setTrackNumber(unsigned long long trackNr)
{
trackNumber = trackNr;
}
unsigned long long Matroska::CueTrack::getTrackNumber() const
{
return trackNumber;
}
void Matroska::CueTrack::setClusterPosition(offset_t clusterPos)
{
clusterPosition = clusterPos;
}
offset_t Matroska::CueTrack::getClusterPosition() const
{
return clusterPosition;
}
void Matroska::CueTrack::setRelativePosition(std::optional<offset_t> relativePos)
{
relativePosition = relativePos;
}
std::optional<offset_t> Matroska::CueTrack::getRelativePosition() const
{
return relativePosition;
}
void Matroska::CueTrack::setCodecState(std::optional<offset_t> codecStatePos)
{
codecState = codecStatePos;
}
std::optional<offset_t> Matroska::CueTrack::getCodecState() const
{
return codecState;
}
void Matroska::CueTrack::setBlockNumber(std::optional<unsigned long long> blockNr)
{
blockNumber = blockNr;
}
std::optional<unsigned long long> Matroska::CueTrack::getBlockNumber() const
{
return blockNumber;
}
void Matroska::CueTrack::setDuration(std::optional<unsigned long long> segmentTicks)
{
duration = segmentTicks;
}
std::optional<unsigned long long> Matroska::CueTrack::getDuration() const
{
return duration;
}
void Matroska::CueTrack::addReferenceTime(unsigned long long refTime)
{
refTimes.append(refTime);
}
const Matroska::CueTrack::ReferenceTimeList &Matroska::CueTrack::referenceTimes() const
{
return refTimes;
}
bool Matroska::CueTrack::adjustOffset(offset_t offset, offset_t delta)
{
bool ret = false;
if(clusterPosition > offset) {
clusterPosition += delta;
ret = true;
}
// operator*() used instead of value() to support restricted compilers
if(offset_t codecStateValue;
codecState.has_value() && (codecStateValue = *codecState) != 0 &&
codecStateValue > offset) {
codecState = codecStateValue + delta;
ret = true;
}
return ret;
}

View File

@@ -0,0 +1,116 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKACUES_H
#define TAGLIB_MATROSKACUES_H
#ifndef DO_NOT_DOCUMENT
#include <optional>
#include "tlist.h"
#include "matroskaelement.h"
namespace TagLib {
class File;
namespace EBML {
class MkCues;
}
namespace Matroska {
class CuePoint;
class CueTrack;
class Cues : public Element
{
public:
using CuePointList = std::list<std::unique_ptr<CuePoint>>;
explicit Cues(offset_t segmentDataOffset);
~Cues() override;
bool isValid(TagLib::File &file) const;
void addCuePoint(std::unique_ptr<CuePoint> &&cuePoint);
const CuePointList &cuePointList();
bool sizeChanged(Element &caller, offset_t delta) override;
void write(TagLib::File &file) override;
private:
friend class EBML::MkCues;
ByteVector renderInternal() override;
CuePointList cuePoints;
const offset_t segmentDataOffset;
};
class CuePoint
{
public:
using CueTrackList = std::list<std::unique_ptr<CueTrack>>;
using Time = unsigned long long;
CuePoint();
~CuePoint();
bool isValid(TagLib::File &file, offset_t segmentDataOffset) const;
void addCueTrack(std::unique_ptr<CueTrack> &&cueTrack);
const CueTrackList &cueTrackList() const;
void setTime(Time timestamp);
Time getTime() const;
bool adjustOffset(offset_t offset, offset_t delta);
private:
CueTrackList cueTracks;
Time time = 0;
};
class CueTrack
{
public:
using ReferenceTimeList = List<unsigned long long>;
CueTrack();
~CueTrack();
bool isValid(TagLib::File &file, offset_t segmentDataOffset) const;
void setTrackNumber(unsigned long long trackNr);
unsigned long long getTrackNumber() const;
void setClusterPosition(offset_t clusterPos);
offset_t getClusterPosition() const;
void setRelativePosition(std::optional<offset_t> relativePos);
std::optional<offset_t> getRelativePosition() const;
void setCodecState(std::optional<offset_t> codecStatePos);
std::optional<offset_t> getCodecState() const;
void setBlockNumber(std::optional<unsigned long long> blockNr);
std::optional<unsigned long long> getBlockNumber() const;
void setDuration(std::optional<unsigned long long> segmentTicks);
std::optional<unsigned long long> getDuration() const;
void addReferenceTime(unsigned long long refTime);
const ReferenceTimeList &referenceTimes() const;
bool adjustOffset(offset_t offset, offset_t delta);
private:
unsigned long long trackNumber = 0;
offset_t clusterPosition = 0;
std::optional<offset_t> relativePosition;
std::optional<unsigned long long> blockNumber;
std::optional<unsigned long long> duration;
std::optional<offset_t> codecState;
ReferenceTimeList refTimes;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,240 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskaelement.h"
#include <memory>
#include "tlist.h"
#include "tfile.h"
#include "tbytevector.h"
#include "ebmlvoidelement.h"
using namespace TagLib;
class Matroska::Element::ElementPrivate
{
public:
ElementPrivate() = default;
~ElementPrivate() = default;
ElementPrivate(const ElementPrivate &) = delete;
ElementPrivate &operator=(const ElementPrivate &) = delete;
offset_t size = 0;
offset_t offset = 0;
ID id = 0;
ByteVector data;
List<Element *> sizeListeners;
// The default write() implementation will delete an unrendered element,
// therefore rendering is required by default and needs to be explicitly set
// using setNeedsRender(false) together with overriding the write() method.
bool needsRender = true;
WriteStyle writeStyle = WriteStyle::Compact;
bool isLastElement = true;
bool isTrailingInSegment = false;
offset_t appendOffset = 0;
// Populated during render() for AvoidInsert+grow+non-last: the offset and
// original size of the slot that should be overwritten with a Void element.
offset_t voidAtOffset = 0;
offset_t voidAtSize = 0;
};
Matroska::Element::Element(ID id) :
e(std::make_unique<ElementPrivate>())
{
e->id = id;
}
Matroska::Element::~Element() = default;
offset_t Matroska::Element::size() const
{
return e->size;
}
offset_t Matroska::Element::offset() const
{
return e->offset;
}
void Matroska::Element::setData(const ByteVector &data)
{
e->data = data;
}
const ByteVector &Matroska::Element::data() const
{
return e->data;
}
void Matroska::Element::setOffset(offset_t offset)
{
e->offset = offset;
}
void Matroska::Element::adjustOffset(offset_t delta)
{
e->offset += delta;
}
void Matroska::Element::setSize(offset_t size)
{
e->size = size;
}
Matroska::Element::ID Matroska::Element::id() const
{
return e->id;
}
void Matroska::Element::addSizeListener(Element *element)
{
e->sizeListeners.append(element);
}
void Matroska::Element::addSizeListeners(const List<Element *> &elements)
{
e->sizeListeners.append(elements);
}
void Matroska::Element::setID(ID id)
{
e->id = id;
}
bool Matroska::Element::render()
{
if(!needsRender())
return true;
const auto beforeSize = sizeRenderedOrWritten();
const auto data = renderInternal();
setNeedsRender(false);
if(const auto afterSize = data.size(); afterSize != beforeSize) {
if(e->writeStyle == WriteStyle::AvoidInsert && !e->isLastElement
&& afterSize > beforeSize && beforeSize > 0) {
// Record old slot for void-overwrite, move element to end of segment.
e->voidAtOffset = e->offset;
e->voidAtSize = beforeSize;
e->offset = e->appendOffset;
// Notify listeners that a new element of afterSize bytes appeared at
// appendOffset (which is past all other elements, so no offset shifts).
if(!emitSizeChanged(static_cast<offset_t>(afterSize))) {
return false;
}
// Update appendOffset for any subsequent AvoidInsert-grow in this round.
e->appendOffset += static_cast<offset_t>(afterSize);
}
else {
if(!emitSizeChanged(afterSize - beforeSize)) {
return false;
}
}
}
setData(data);
return true;
}
void Matroska::Element::setNeedsRender(bool needsRender)
{
e->needsRender = needsRender;
}
bool Matroska::Element::needsRender() const
{
return e->needsRender;
}
bool Matroska::Element::emitSizeChanged(offset_t delta)
{
for(const auto element : e->sizeListeners) {
if(!element->sizeChanged(*this, delta))
return false;
}
return true;
}
bool Matroska::Element::sizeChanged(Element &caller, offset_t delta)
{
// The equal case is needed when multiple new elements are added
// (e.g. Attachments and Tags), they will start with the same offset
// are updated via size change handling.
if(caller.offset() <= e->offset && caller.id() != e->id) {
e->offset += delta;
}
return true;
}
offset_t Matroska::Element::sizeRenderedOrWritten() const
{
const offset_t dataSize = e->data.size();
return dataSize != 0 ? dataSize : e->size;
}
void Matroska::Element::setWriteStyle(WriteStyle style)
{
e->writeStyle = style;
}
Matroska::WriteStyle Matroska::Element::writeStyle() const
{
return e->writeStyle;
}
void Matroska::Element::setIsLastElement(bool isLast)
{
e->isLastElement = isLast;
}
void Matroska::Element::setAppendOffset(offset_t appendOffset)
{
e->appendOffset = appendOffset;
}
void Matroska::Element::setIsTrailingInSegment(bool isTrailing)
{
e->isTrailingInSegment = isTrailing;
}
bool Matroska::Element::isTrailingInSegment() const
{
return e->isTrailingInSegment;
}
bool Matroska::Element::wasMoved() const
{
// voidAtSize is set when the element was moved during render().
// After write() it is cleared, but the caller checks before write().
return e->voidAtOffset != 0 || e->voidAtSize != 0;
}
void Matroska::Element::write(File &file)
{
if(e->voidAtSize > 0) {
// AvoidInsert: overwrite the old slot with a Void element.
const auto voidData = EBML::VoidElement::renderSize(e->voidAtSize);
file.insert(voidData, e->voidAtOffset, e->voidAtSize);
e->voidAtOffset = 0;
// The element was moved to a new position (end of segment),
// so there are no existing bytes to replace at the new offset.
e->size = 0;
e->voidAtSize = 0;
}
file.insert(e->data, e->offset, e->size);
e->size = e->data.size();
}

View File

@@ -0,0 +1,86 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKAELEMENT_H
#define TAGLIB_MATROSKAELEMENT_H
#ifndef DO_NOT_DOCUMENT
#include <memory>
#include "taglib_export.h"
#include "taglib.h"
#include "tlist.h"
#include "matroskawritestyle.h"
namespace TagLib {
class File;
class ByteVector;
namespace Matroska {
class TAGLIB_EXPORT Element
{
public:
using ID = unsigned int;
explicit Element(ID id);
virtual ~Element();
offset_t size() const;
offset_t offset() const;
ID id() const;
void setOffset(offset_t offset);
void adjustOffset(offset_t delta);
void setSize(offset_t size);
void setID(ID id);
virtual bool render();
void setNeedsRender(bool needsRender);
bool needsRender() const;
void setData(const ByteVector &data);
const ByteVector &data() const;
virtual void write(TagLib::File &file);
void addSizeListener(Element *element);
void addSizeListeners(const List<Element *> &elements);
bool emitSizeChanged(offset_t delta);
virtual bool sizeChanged(Element &caller, offset_t delta);
void setWriteStyle(WriteStyle style);
WriteStyle writeStyle() const;
void setIsLastElement(bool isLast);
void setAppendOffset(offset_t appendOffset);
bool wasMoved() const;
//! Mark this element as the trailing element of the segment (no other
//! element follows it in the file). Trailing elements may shrink even
//! in non-Compact write styles because no offsets need to be preserved.
void setIsTrailingInSegment(bool isTrailing);
bool isTrailingInSegment() const;
protected:
offset_t sizeRenderedOrWritten() const;
private:
virtual ByteVector renderInternal() = 0;
class ElementPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ElementPrivate> e;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,695 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskafile.h"
#include <memory>
#include "matroskatag.h"
#include "matroskaattachments.h"
#include "matroskaattachedfile.h"
#include "matroskachapter.h"
#include "matroskachapteredition.h"
#include "matroskachapters.h"
#include "matroskaseekhead.h"
#include "matroskacues.h"
#include "matroskasegment.h"
#include "ebmlutils.h"
#include "ebmlelement.h"
#include "ebmlstringelement.h"
#include "ebmluintelement.h"
#include "ebmlmksegment.h"
#include "tlist.h"
#include "tdebug.h"
#include "tagutils.h"
#include "tpropertymap.h"
using namespace TagLib;
class Matroska::File::FilePrivate
{
public:
FilePrivate() = default;
~FilePrivate() = default;
FilePrivate(const FilePrivate &) = delete;
FilePrivate &operator=(const FilePrivate &) = delete;
std::unique_ptr<Tag> tag;
std::unique_ptr<Attachments> attachments;
std::unique_ptr<Chapters> chapters;
std::unique_ptr<SeekHead> seekHead;
std::unique_ptr<Cues> cues;
std::unique_ptr<Segment> segment;
std::unique_ptr<Properties> properties;
};
////////////////////////////////////////////////////////////////////////////////
// static members
////////////////////////////////////////////////////////////////////////////////
bool Matroska::File::isSupported(IOStream *stream)
{
const ByteVector id = Utils::readHeader(stream, 4, false);
return id.startsWith("\x1A\x45\xDF\xA3");
}
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::File::File(FileName file, bool readProperties,
Properties::ReadStyle readStyle) :
TagLib::File(file),
d(std::make_unique<FilePrivate>())
{
if(!isOpen()) {
debug("Failed to open matroska file");
setValid(false);
return;
}
read(readProperties, readStyle);
}
Matroska::File::File(IOStream *stream, bool readProperties,
Properties::ReadStyle readStyle) :
TagLib::File(stream),
d(std::make_unique<FilePrivate>())
{
if(!isOpen()) {
debug("Failed to open matroska file");
setValid(false);
return;
}
read(readProperties, readStyle);
}
Matroska::File::~File() = default;
Matroska::Properties *Matroska::File::audioProperties() const
{
return d->properties.get();
}
Tag *Matroska::File::tag() const
{
return tag(true);
}
Matroska::Tag *Matroska::File::tag(bool create) const
{
if(!d->tag && create) {
d->tag = std::make_unique<Tag>();
if(d->properties) {
d->tag->setSegmentTitle(d->properties->title());
}
}
return d->tag.get();
}
PropertyMap Matroska::File::properties() const
{
return d->tag ? d->tag->properties() : PropertyMap();
}
void Matroska::File::removeUnsupportedProperties(const StringList &properties)
{
if(d->tag) {
d->tag->removeUnsupportedProperties(properties);
}
}
PropertyMap Matroska::File::setProperties(const PropertyMap &properties)
{
if(!d->tag) {
d->tag = std::make_unique<Tag>();
}
return d->tag->setProperties(properties);
}
namespace {
constexpr offset_t FAST_SCAN_LIMIT = static_cast<offset_t>(512 * 1024);
String keyForAttachedFile(const Matroska::AttachedFile &attachedFile)
{
if(attachedFile.mediaType().startsWith("image/")) {
return "PICTURE";
}
if(!attachedFile.fileName().isEmpty()) {
return attachedFile.fileName();
}
if(!attachedFile.mediaType().isEmpty()) {
return attachedFile.mediaType();
}
return String::fromULongLong(attachedFile.uid());
}
bool keyMatchesAttachedFile(const String &key, const Matroska::AttachedFile &attachedFile)
{
return !key.isEmpty() && (
(key == "PICTURE" && attachedFile.mediaType().startsWith("image/")) ||
key == attachedFile.fileName() ||
key == attachedFile.mediaType() ||
key == String::fromULongLong(attachedFile.uid())
);
}
}
StringList Matroska::File::complexPropertyKeys() const
{
StringList keys = TagLib::File::complexPropertyKeys();
if(d->attachments) {
const auto &attachedFiles = d->attachments->attachedFileList();
for(const auto &attachedFile : attachedFiles) {
if(String key = keyForAttachedFile(attachedFile);
!key.isEmpty() && !keys.contains(key)) {
keys.append(key);
}
}
}
if(d->chapters && !d->chapters->chapterEditionList().isEmpty()) {
keys.append("CHAPTERS");
}
return keys;
}
List<VariantMap> Matroska::File::complexProperties(const String &key) const
{
List<VariantMap> props = TagLib::File::complexProperties(key);
if(key.upper() == "CHAPTERS") {
if(d->chapters) {
for(const auto &edition : d->chapters->chapterEditionList()) {
VariantMap property;
if(const auto uid = edition.uid()) {
property.insert("uid", uid);
}
if(const auto isDefault = edition.isDefault()) {
property.insert("isDefault", isDefault);
}
if(const auto isOrdered = edition.isOrdered()) {
property.insert("isOrdered", isOrdered);
}
if(auto chapters = edition.chapterList(); !chapters.isEmpty()) {
VariantList chaps;
for(const auto &chapter : chapters) {
VariantMap chap;
if(const auto uid = chapter.uid()) {
chap.insert("uid", uid);
}
if(const auto isHidden = chapter.isHidden()) {
chap.insert("isHidden", isHidden);
}
chap.insert("timeStart", chapter.timeStart());
if(const auto timeEnd = chapter.timeEnd()) {
chap.insert("timeEnd", timeEnd);
}
if(auto displays = chapter.displayList(); !displays.isEmpty()) {
VariantList disps;
for(const auto &display : displays) {
VariantMap disp;
if(auto str = display.string(); !str.isEmpty()) {
disp.insert("string", str);
}
if(auto language = display.language(); !language.isEmpty()) {
disp.insert("language", language);
}
disps.append(disp);
}
chap.insert("displays", disps);
}
chaps.append(chap);
}
property.insert("chapters", chaps);
}
props.append(property);
}
}
}
if(d->attachments) {
const auto &attachedFiles = d->attachments->attachedFileList();
for(const auto &attachedFile : attachedFiles) {
if(keyMatchesAttachedFile(key, attachedFile)) {
VariantMap property;
property.insert("data", attachedFile.data());
property.insert("mimeType", attachedFile.mediaType());
property.insert("description", attachedFile.description());
property.insert("fileName", attachedFile.fileName());
property.insert("uid", attachedFile.uid());
props.append(property);
}
}
}
return props;
}
bool Matroska::File::setComplexProperties(const String &key, const List<VariantMap> &value)
{
if(TagLib::File::setComplexProperties(key, value)) {
return true;
}
if(key.upper() == "CHAPTERS") {
chapters(true)->clear();
for(const auto &ed : value) {
List<Chapter> editionChapters;
const auto chaps = ed.value("chapters").toList();
for(const auto &chapVar : chaps) {
auto chap = chapVar.toMap();
const auto disps = chap.value("displays").toList();
List<Chapter::Display> chapterDisplays;
for(const auto &dispVar : disps) {
auto disp = dispVar.toMap();
chapterDisplays.append(Chapter::Display(
disp.value("string").toString(),
disp.value("language").toString()));
}
editionChapters.append(Chapter(
chap.value("timeStart").toULongLong(),
chap.value("timeEnd").toULongLong(),
chapterDisplays,
chap.value("uid", 0ULL).toULongLong(),
chap.value("isHidden", false).toBool()));
}
d->chapters->addChapterEdition(ChapterEdition(
editionChapters,
ed.value("isDefault", false).toBool(),
ed.value("isOrdered", false).toBool(),
ed.value("uid", 0ULL).toULongLong()));
}
return true;
}
List<AttachedFile> &files = attachments(true)->attachedFiles();
for(auto it = files.begin(); it != files.end();) {
if(keyMatchesAttachedFile(key, *it)) {
it = files.erase(it);
}
else {
++it;
}
}
for(const auto &property : value) {
if(property.isEmpty())
continue;
auto mimeType = property.value("mimeType").value<String>();
auto data = property.value("data").value<ByteVector>();
auto fileName = property.value("fileName").value<String>();
auto uid = property.value("uid").value<unsigned long long>();
bool ok;
if(key.upper() == "PICTURE" && !mimeType.startsWith("image/")) {
mimeType = data.startsWith("\x89PNG\x0d\x0a\x1a\x0a")
? "image/png" : "image/jpeg";
}
else if(mimeType.isEmpty() && key.find("/") != -1) {
mimeType = key;
}
else if(fileName.isEmpty() && key.find(".") != -1) {
fileName = key;
}
else if(unsigned long long uidKey;
!uid && ((uidKey = key.toULongLong(&ok))) && ok) {
uid = uidKey;
}
if(fileName.isEmpty() && !mimeType.isEmpty()) {
const int slashPos = mimeType.rfind('/');
String ext = mimeType.substr(slashPos + 1);
if(ext == "jpeg") {
ext = "jpg";
}
fileName = "attachment." + ext;
}
if(!mimeType.isEmpty() && !fileName.isEmpty()) {
d->attachments->addAttachedFile(AttachedFile(
data, fileName, mimeType, uid,
property.value("description").value<String>()));
}
}
return true;
}
Matroska::Attachments *Matroska::File::attachments(bool create) const
{
if(!d->attachments && create)
d->attachments = std::make_unique<Attachments>();
return d->attachments.get();
}
Matroska::Chapters *Matroska::File::chapters(bool create) const
{
if(!d->chapters && create)
d->chapters = std::make_unique<Chapters>();
return d->chapters.get();
}
void Matroska::File::read(bool readProperties, Properties::ReadStyle readStyle)
{
const offset_t fileLength = length();
// Find the EBML Header
const auto head = EBML::element_cast<EBML::Element::Id::EBMLHeader>(
EBML::Element::factory(*this));
if(!head || head->getId() != EBML::Element::Id::EBMLHeader) {
debug("Failed to find EBML head");
setValid(false);
return;
}
if(readProperties) {
head->read(*this);
}
else {
head->skipData(*this);
}
offset_t maxOffset = fileLength - tell();
if (readStyle == Properties::ReadStyle::Fast && maxOffset > FAST_SCAN_LIMIT) {
maxOffset = FAST_SCAN_LIMIT;
}
// Find the Matroska segment in the file
const std::unique_ptr<EBML::MkSegment> segment(
EBML::element_cast<EBML::Element::Id::MkSegment>(
EBML::findElement(*this, EBML::Element::Id::MkSegment, maxOffset)
)
);
if(!segment) {
debug("Failed to find Matroska segment");
setValid(false);
return;
}
// Read the segment into memory from file
d->segment = segment->parseSegment();
maxOffset = segment->getDataSize();
if (readStyle == Properties::ReadStyle::Fast && maxOffset > FAST_SCAN_LIMIT) {
maxOffset = FAST_SCAN_LIMIT;
}
if(!segment->readLimited(*this, maxOffset)) {
debug("Failed to read segment");
setValid(false);
return;
}
// Parse the elements
d->seekHead = segment->parseSeekHead();
d->cues = segment->parseCues();
d->tag = segment->parseTag();
d->attachments = segment->parseAttachments();
d->chapters = segment->parseChapters();
if(readProperties) {
d->properties = std::make_unique<Properties>(this);
for(const auto &element : *head) {
if(const auto id = element->getId(); id == EBML::Element::Id::DocType) {
d->properties->setDocType(
EBML::element_cast<EBML::Element::Id::DocType>(element)->getValue());
}
else if(id == EBML::Element::Id::DocTypeVersion) {
d->properties->setDocTypeVersion(static_cast<int>(
EBML::element_cast<EBML::Element::Id::DocTypeVersion>(element)->getValue()));
}
}
segment->parseInfo(d->properties.get());
segment->parseTracks(d->properties.get());
if(d->tag) {
d->tag->setSegmentTitle(d->properties->title());
}
}
if(readStyle == AudioProperties::Accurate &&
((d->seekHead && !d->seekHead->isValid(*this)) ||
(d->cues && !d->cues->isValid(*this)))) {
setValid(false);
return;
}
setValid(true);
}
bool Matroska::File::save()
{
return save(WriteStyle::Compact);
}
bool Matroska::File::save(WriteStyle writeStyle)
{
if(readOnly()) {
debug("Matroska::File::save() -- File is read only.");
return false;
}
if(!isValid()) {
debug("Matroska::File::save() -- File is not valid.");
return false;
}
// Do not create new attachments, chapters or tags and corresponding
// seek head entries if only empty objects were created.
if(d->chapters && d->chapters->chapterEditionList().isEmpty() &&
d->chapters->size() == 0 && d->chapters->offset() == 0 &&
d->chapters->data().isEmpty()) {
d->chapters.reset();
}
if(d->attachments && d->attachments->attachedFileList().isEmpty() &&
d->attachments->size() == 0 && d->attachments->offset() == 0 &&
d->attachments->data().isEmpty()) {
d->attachments.reset();
}
if(d->tag && d->tag->isEmpty() &&
d->tag->size() == 0 && d->tag->offset() == 0 &&
d->tag->data().isEmpty()) {
d->tag.reset();
}
List<Element *> renderList;
List<Element *> newElements;
// List of all possible elements we can write
List<Element *> elements {
d->chapters.get(),
d->attachments.get(),
d->tag.get()
};
/* Build render list. New elements will be added
* to the end of the file. For new elements,
* the order is from least likely to change,
* to most likely to change:
* 1. Chapters
* 2. Attachments
* 3. Tags
*/
for(auto element : elements) {
if(!element)
continue;
if(element->size())
renderList.append(element);
else {
element->setOffset(length());
newElements.append(element);
}
}
if(renderList.isEmpty() && newElements.isEmpty())
return true;
auto sortAscending = [](const auto a, const auto b) { return a->offset() < b->offset(); };
renderList.sort(sortAscending);
renderList.append(newElements);
// Configure write style on each data element. Determines whether elements
// may be padded (DoNotShrink/AvoidInsert) or moved to the end (AvoidInsert).
// New elements (no prior size) are always written compactly.
if(writeStyle != WriteStyle::Compact) {
// Determine which existing data element has the highest file offset
// (i.e., is "last" among the data elements, before cues/seekHead/segment).
// New elements always go after existing ones and are treated as compact.
const Element *lastDataElement = nullptr;
for(const auto element : renderList) {
if(element->size() > 0)
lastDataElement = element;
}
// For AvoidInsert: an existing data element (Tags, Chapters, Attachments)
// located before the LAST Cluster must not be grown in-place. Doing so
// would shift later clusters and invalidate their cue positions. Such
// elements are voided at their original position and appended at the
// end of the segment instead. The boundary is the maximum cluster offset
// (derived from cue-point cluster positions). If no cue points are
// available, the Cues element offset is used as a safe upper bound
// (Cues are always after the last Cluster). A value of 0 means
// "no boundary" any offset compares >= 0, so the boundary check is
// a no-op in non-AvoidInsert modes.
offset_t audioBoundary = 0;
if(writeStyle == WriteStyle::AvoidInsert && d->cues) {
const offset_t segDataOffset = d->segment->dataOffset();
for(const auto &cp : d->cues->cuePointList()) {
for(const auto &ct : cp->cueTrackList()) {
audioBoundary = std::max(audioBoundary,
segDataOffset + ct->getClusterPosition());
}
}
if(audioBoundary == 0)
audioBoundary = d->cues->offset();
}
for(const auto element : renderList) {
if(element->size() > 0) {
element->setWriteStyle(writeStyle);
// An element is "last" only if it has the highest data-element
// offset AND sits past the last cluster. The latter is always true
// when audioBoundary == 0 (DoNotShrink, or AvoidInsert without cues).
element->setIsLastElement(element == lastDataElement
&& element->offset() >= audioBoundary);
}
}
// For AvoidInsert: identify the segment-trailing element (highest offset
// among data elements, Cues, SeekHead). The trailing element may shrink
// without padding -- there is nothing after it whose offset would shift,
// so a trailing void would be wasted space.
if(writeStyle == WriteStyle::AvoidInsert) {
Element *trailing = nullptr;
offset_t maxOffset = 0;
const auto consider = [&](Element *e) {
if(e && e->size() > 0 && e->offset() > maxOffset) {
maxOffset = e->offset();
trailing = e;
}
};
for(const auto element : renderList)
consider(element);
consider(d->cues.get());
consider(d->seekHead.get());
if(trailing)
trailing->setIsTrailingInSegment(true);
}
}
// Add our new elements to the Seek Head (if the file has one)
if(d->seekHead) {
const auto segmentDataOffset = d->segment->dataOffset();
for(const auto element : newElements)
d->seekHead->addEntry(element->id(), element->offset() - segmentDataOffset);
d->seekHead->sort();
}
// Set up listeners, add seek head and segment length to the end
for(auto it = renderList.begin(); it != renderList.end(); ++it) {
for(auto it2 = std::next(it); it2 != renderList.end(); ++it2)
(*it)->addSizeListener(*it2);
if(d->cues)
(*it)->addSizeListener(d->cues.get());
if(d->seekHead)
(*it)->addSizeListener(d->seekHead.get());
(*it)->addSizeListener(d->segment.get());
}
if(d->cues) {
renderList.append(d->cues.get());
d->cues->addSizeListeners(renderList);
if(d->seekHead) {
d->cues->addSizeListener(d->seekHead.get());
}
d->cues->addSizeListener(d->segment.get());
}
if(d->seekHead) {
renderList.append(d->seekHead.get());
d->seekHead->addSizeListeners(renderList);
d->seekHead->addSizeListener(d->segment.get());
}
d->segment->addSizeListeners(renderList);
renderList.append(d->segment.get());
// Render the elements.
// Because size changes of elements can cause segment offset updates and
// size changes in other elements, we might need multiple rounds until no more
// element needs rendering.
int renderRound = 0;
bool rendering = true;
while(rendering && renderRound < 5) {
rendering = false;
// Initialize appendOffset for AvoidInsert elements at the start of each round.
if(writeStyle == WriteStyle::AvoidInsert) {
const offset_t appendOffset = d->segment->endOffset();
for(const auto element : renderList)
element->setAppendOffset(appendOffset);
}
for(const auto element : renderList) {
if(element->needsRender()) {
rendering = true;
if(!element->render()) {
return false;
}
}
}
++renderRound;
}
// For AvoidInsert: elements that were moved during rendering may have
// stale offsets if in-place elements grew after the move was computed.
// Re-assign their offsets sequentially from the correct position.
if(writeStyle == WriteStyle::AvoidInsert) {
// Collect moved elements in render order (= ascending original-offset order
// = order they appear in renderList before any re-sort).
List<Element *> movedElements;
offset_t totalMovedSize = 0;
for(const auto element : renderList) {
if(element->wasMoved()) {
movedElements.append(element);
totalMovedSize += static_cast<offset_t>(element->data().size());
}
}
if(!movedElements.isEmpty()) {
// The segment end includes in-place growths AND all moved element sizes.
// The moved elements start right after all in-place content.
offset_t appendAt = d->segment->endOffset() - totalMovedSize;
for(const auto element : movedElements) {
element->setOffset(appendAt);
appendAt += static_cast<offset_t>(element->data().size());
}
}
}
// For elements that were moved to the end by AvoidInsert, update their
// seek head entry to reflect the new file position.
if(writeStyle == WriteStyle::AvoidInsert && d->seekHead) {
const offset_t segDataOffset = d->segment->dataOffset();
for(const auto element : renderList) {
if(element->wasMoved()) {
d->seekHead->updateEntry(element->id(), element->offset() - segDataOffset);
}
}
// Re-render the seekHead (and anything it affects) after updating entries.
// The seekHead slot was pre-padded, so this should not cause size changes.
d->seekHead->setNeedsRender(true);
for(const auto element : renderList) {
if(element->needsRender()) {
if(!element->render())
return false;
}
}
}
// Write out to file
renderList.sort(sortAscending);
for(const auto element : renderList)
element->write(*this);
return true;
}

View File

@@ -0,0 +1,194 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKAFILE_H
#define TAGLIB_MATROSKAFILE_H
#include "taglib_export.h"
#include "tfile.h"
#include "matroskaproperties.h"
#include "matroskawritestyle.h"
//! An implementation of Matroska metadata
namespace TagLib::Matroska {
class Tag;
class Attachments;
class Chapters;
//! An implementation of TagLib::File with Matroska specific methods
/*!
* Implementation of TagLib::File for Matroska.
*/
class TAGLIB_EXPORT File : public TagLib::File
{
public:
/*!
* Constructs a Matroska file from \a file. If \a readProperties is \c true the
* file's audio properties will also be read.
*
* If \a readStyle is \c Accurate all seek head and cues segment positions
* are verified for the isValid() state of the file.
*/
explicit File(FileName file, bool readProperties = true,
Properties::ReadStyle readStyle = Properties::Average);
/*!
* Constructs a Matroska file from \a stream. If \a readProperties is \c true the
* file's audio properties will also be read.
*
* If \a readStyle is \c Accurate all seek head and cues segment positions
* are verified for the isValid() state of the file.
*/
explicit File(IOStream *stream, bool readProperties = true,
Properties::ReadStyle readStyle = Properties::Average);
/*!
* Destroys this instance of the File.
*/
~File() override;
File(const File &) = delete;
File &operator=(const File &) = delete;
/*!
* Returns a pointer to the tag of the file.
*
* It will create a tag if one does not exist and returns a valid pointer.
*
* \note The tag <b>is still</b> owned by the Matroska::File and should not
* be deleted by the user. It will be deleted when the file (object) is
* destroyed.
*/
TagLib::Tag *tag() const override;
/*!
* Returns a pointer to the Matroska tag of the file.
*
* If \a create is \c false this may return a null pointer if there is no tag.
* If \a create is \c true it will create a tag if one does not exist and
* returns a valid pointer.
*
* \note The tag <b>is still</b> owned by the Matroska::File and should not
* be deleted by the user. It will be deleted when the file (object)
* destroyed.
*/
Tag *tag(bool create) const;
/*!
* Implements the reading part of the unified property interface.
*/
PropertyMap properties() const override;
void removeUnsupportedProperties(const StringList &properties) override;
/*!
* Implements the writing part of the unified tag dictionary interface.
*/
PropertyMap setProperties(const PropertyMap &) override;
/*!
* Returns the keys for attached files, "PICTURE" for images, the media
* type, file name or UID for other attached files.
* The names of the binary simple tags are included too.
*/
StringList complexPropertyKeys() const override;
/*!
* Get the pictures stored in the attachments as complex properties
* for \a key "PICTURE". Other attached files can be retrieved, by
* media type, file name or UID.
* The attached files are returned as maps with keys "data", "mimeType",
* "description", "fileName, "uid".
* Binary simple tags can be retrieved as maps with keys "data", "name",
* "targetTypeValue", "language", "defaultLanguage".
*/
List<VariantMap> complexProperties(const String &key) const override;
/*!
* Set attached files as complex properties \a value, e.g. pictures for
* \a key "PICTURE" with the maps in \a value having keys "data", "mimeType",
* "description", "fileName, "uid". For other attached files, the mime type,
* file name or UID can be used as the \a key.
* Maps with keys "name" (with the same value as \a key) and "data" are
* stored as binary simple tags with additional keys "targetTypeValue",
* "language", "defaultLanguage".
*/
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
/*!
* Returns the Matroska::Properties for this file. If no audio properties
* were read then this will return a null pointer.
*/
Properties *audioProperties() const override;
/*!
* Save the file.
*
* This returns \c true if the save was successful.
*/
bool save() override;
/*!
* Save the file with the specified write style.
*
* This returns \c true if the save was successful.
*/
bool save(WriteStyle style);
/*!
* Returns a pointer to the attachments of the file.
*
* If \a create is \c false this may return a null pointer if there are no
* attachments.
* If \a create is \c true it will create attachments if none exist and
* returns a valid pointer.
*/
Attachments *attachments(bool create = false) const;
/*!
* Returns a pointer to the chapters of the file.
*
* If \a create is \c false this may return a null pointer if there are no
* chapters.
* If \a create is \c true it will create chapters if none exist and
* returns a valid pointer.
*/
Chapters *chapters(bool create = false) const;
/*!
* Returns whether or not the given \a stream can be opened as a Matroska
* file.
*
* \note This method is designed to do a quick check. The result may
* not necessarily be correct.
*/
static bool isSupported(IOStream *stream);
private:
void read(bool readProperties, Properties::ReadStyle readStyle);
class FilePrivate;
friend class Properties;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<FilePrivate> d;
};
}
#endif

View File

@@ -0,0 +1,155 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskaproperties.h"
#include "matroskafile.h"
using namespace TagLib;
class Matroska::Properties::PropertiesPrivate
{
public:
explicit PropertiesPrivate(File *file) : file(file) {}
~PropertiesPrivate() = default;
PropertiesPrivate(const PropertiesPrivate &) = delete;
PropertiesPrivate &operator=(const PropertiesPrivate &) = delete;
File *file;
String codecName;
String title;
String docType;
int docTypeVersion { 0 };
int length { 0 };
int bitrate { -1 };
int sampleRate { 0 };
int channels { 0 };
int bitsPerSample { 0 };
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::Properties::Properties(File *file, ReadStyle style) :
AudioProperties(style),
d(std::make_unique<PropertiesPrivate>(file))
{
}
Matroska::Properties::~Properties() = default;
int Matroska::Properties::lengthInMilliseconds() const
{
return d->length;
}
int Matroska::Properties::bitrate() const
{
if(d->bitrate == -1) {
d->bitrate = d->length != 0 ? static_cast<int>(d->file->length() * 8 / d->length) : 0;
}
return d->bitrate;
}
int Matroska::Properties::sampleRate() const
{
return d->sampleRate;
}
int Matroska::Properties::channels() const
{
return d->channels;
}
int Matroska::Properties::bitsPerSample() const
{
return d->bitsPerSample;
}
String Matroska::Properties::docType() const
{
return d->docType;
}
int Matroska::Properties::docTypeVersion() const
{
return d->docTypeVersion;
}
String Matroska::Properties::codecName() const
{
return d->codecName;
}
String Matroska::Properties::title() const
{
return d->title;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
void Matroska::Properties::setLengthInMilliseconds(int length)
{
d->length = length;
}
void Matroska::Properties::setSampleRate(int sampleRate)
{
d->sampleRate = sampleRate;
}
void Matroska::Properties::setChannels(int channels)
{
d->channels = channels;
}
void Matroska::Properties::setBitsPerSample(int bitsPerSample)
{
d->bitsPerSample = bitsPerSample;
}
void Matroska::Properties::setDocType(const String &docType)
{
d->docType = docType;
}
void Matroska::Properties::setDocTypeVersion(int docTypeVersion)
{
d->docTypeVersion = docTypeVersion;
}
void Matroska::Properties::setCodecName(const String &codecName)
{
d->codecName = codecName;
}
void Matroska::Properties::setTitle(const String &title)
{
d->title = title;
}

View File

@@ -0,0 +1,127 @@
/***************************************************************************
copyright : (C) 2025 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKAPROPERTIES_H
#define TAGLIB_MATROSKAPROPERTIES_H
#include "taglib_export.h"
#include "audioproperties.h"
namespace TagLib::EBML {
class MkTracks;
class MkInfo;
}
namespace TagLib::Matroska {
class File;
//! An implementation of Matroska audio properties
class TAGLIB_EXPORT Properties : public AudioProperties
{
public:
/*!
* Creates an instance of Matroska::Properties.
*/
explicit Properties(File *file, ReadStyle style = Average);
/*!
* Destroys this Matroska::Properties instance.
*/
~Properties() override;
Properties(const Properties &) = delete;
Properties &operator=(const Properties &) = delete;
/*!
* Returns the length of the file in milliseconds.
*
* \see lengthInSeconds()
*/
int lengthInMilliseconds() const override;
/*!
* Returns the average bit rate of the file in kb/s.
*/
int bitrate() const override;
/*!
* Returns the sample rate in Hz.
*/
int sampleRate() const override;
/*!
* Returns the number of audio channels.
*/
int channels() const override;
/*!
* Returns the number of bits per audio sample.
*/
int bitsPerSample() const;
/*!
* Returns the EBML doc type, "matroska" or "webm".
*/
String docType() const;
/*!
* Returns the EBML doc type version, typical values are 2 or 4.
*/
int docTypeVersion() const;
/*!
* Returns the concrete codec name, for example "A_MPEG/L3"
* used in the file if available, otherwise an empty string.
*/
String codecName() const;
/*!
* Returns the general name of the segment.
* Some applications store the title of the file here, but players should
* prioritize the tag title over the segment title.
*/
String title() const;
private:
class PropertiesPrivate;
friend class EBML::MkInfo;
friend class EBML::MkTracks;
friend class File;
void setLengthInMilliseconds(int length);
void setSampleRate(int sampleRate);
void setChannels(int channels);
void setBitsPerSample(int bitsPerSample);
void setDocType(const String &docType);
void setDocTypeVersion(int docTypeVersion);
void setCodecName(const String &codecName);
void setTitle(const String &title);
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<PropertiesPrivate> d;
};
}
#endif

View File

@@ -0,0 +1,148 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskaseekhead.h"
#include "ebmlmkseekhead.h"
#include "ebmlbinaryelement.h"
#include "ebmluintelement.h"
#include "ebmlmasterelement.h"
#include "tfile.h"
#include "tutils.h"
#include "tdebug.h"
using namespace TagLib;
Matroska::SeekHead::SeekHead(offset_t segmentDataOffset) :
Element(static_cast<ID>(EBML::Element::Id::MkSeekHead)),
segmentDataOffset(segmentDataOffset)
{
setNeedsRender(false);
}
Matroska::SeekHead::~SeekHead() = default;
bool Matroska::SeekHead::isValid(TagLib::File &file) const
{
bool result = true;
for(const auto &[id, offset] : entries) {
file.seek(segmentDataOffset + offset);
if(EBML::Element::readId(file) != id) {
debug(Utils::formatString("No ID %x found at seek position", id));
result = false;
}
}
return result;
}
void Matroska::SeekHead::addEntry(const Element &element)
{
entries.append({element.id(), element.offset()});
setNeedsRender(true);
}
void Matroska::SeekHead::addEntry(ID id, offset_t offset)
{
entries.append({id, offset});
setNeedsRender(true);
}
void Matroska::SeekHead::updateEntry(ID id, offset_t newOffset)
{
for(auto &entry : entries) {
if(entry.first == id) {
entry.second = newOffset;
setNeedsRender(true);
return;
}
}
}
const List<std::pair<unsigned int, offset_t>> &Matroska::SeekHead::entryList() const
{
return entries;
}
ByteVector Matroska::SeekHead::renderInternal()
{
const auto beforeSize = sizeRenderedOrWritten();
EBML::MkSeekHead seekHead;
seekHead.setMinRenderSize(beforeSize);
for(const auto &[id, position] : entries) {
auto seekElement = EBML::make_unique_element<EBML::Element::Id::MkSeek>();
auto idElement = EBML::make_unique_element<EBML::Element::Id::MkSeekID>();
idElement->setValue(ByteVector::fromUInt(id, true));
seekElement->appendElement(std::move(idElement));
auto positionElement = EBML::make_unique_element<EBML::Element::Id::MkSeekPosition>();
positionElement->setValue(static_cast<unsigned long long>(position));
seekElement->appendElement(std::move(positionElement));
seekHead.appendElement(std::move(seekElement));
}
return seekHead.render();
}
void Matroska::SeekHead::write(File &file)
{
if(!data().isEmpty())
Element::write(file);
}
void Matroska::SeekHead::sort()
{
entries.sort([](const auto &a, const auto &b) { return a.second < b.second; });
}
bool Matroska::SeekHead::sizeChanged(Element &caller, offset_t delta)
{
ID callerID = caller.id();
if(callerID == static_cast<ID>(EBML::Element::Id::MkSegment)) {
adjustOffset(delta);
return true;
}
// The equal case is needed when multiple new elements are added
// (e.g. Attachments and Tags), they will start with the same offset
// and are updated via size change handling.
offset_t offset = caller.offset() - segmentDataOffset;
auto it = entries.begin();
while(it != entries.end()) {
it = std::find_if(it,
entries.end(),
[offset, callerID](const auto &a) {
return a.second >= offset && a.first != callerID;
}
);
if(it != entries.end()) {
it->second += delta;
setNeedsRender(true);
++it;
}
}
if(caller.data().isEmpty() && caller.size() + delta == 0) {
// The caller element is removed, remove it from the seek head.
it = std::find_if(entries.begin(), entries.end(),
[callerID](const auto &a){ return a.first == callerID; });
if(it != entries.end()) {
entries.erase(it);
}
}
return true;
}

View File

@@ -0,0 +1,57 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKASEEKHEAD_H
#define TAGLIB_MATROSKASEEKHEAD_H
#ifndef DO_NOT_DOCUMENT
#include "matroskaelement.h"
#include "tlist.h"
namespace TagLib {
class File;
class ByteVector;
namespace Matroska {
class SeekHead : public Element
{
public:
explicit SeekHead(offset_t segmentDataOffset);
~SeekHead() override;
bool isValid(TagLib::File &file) const;
void addEntry(const Element &element);
void addEntry(ID id, offset_t offset);
void updateEntry(ID id, offset_t offset);
const List<std::pair<unsigned int, offset_t>> &entryList() const;
void write(TagLib::File &file) override;
void sort();
bool sizeChanged(Element &caller, offset_t delta) override;
private:
ByteVector renderInternal() override;
List<std::pair<unsigned int, offset_t>> entries;
const offset_t segmentDataOffset;
};
}
}
#endif
#endif

View File

@@ -0,0 +1,76 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskasegment.h"
#include "ebmlutils.h"
#include "tbytevector.h"
using namespace TagLib;
Matroska::Segment::Segment(offset_t sizeLength, offset_t dataSize, offset_t lengthOffset) :
Element(static_cast<ID>(EBML::Element::Id::MkSegment)),
sizeLength(sizeLength), dataSize(dataSize)
{
setOffset(lengthOffset);
setSize(sizeLength);
}
Matroska::Segment::~Segment() = default;
ByteVector Matroska::Segment::renderInternal()
{
return EBML::renderVINT(dataSize, static_cast<int>(sizeLength));
}
bool Matroska::Segment::render()
{
const auto beforeSize = sizeLength;
auto data = renderInternal();
setNeedsRender(false);
if(auto afterSize = data.size(); afterSize != beforeSize) {
sizeLength = 8;
data = renderInternal();
setNeedsRender(false);
afterSize = data.size();
if(!emitSizeChanged(afterSize - beforeSize)) {
return false;
}
}
setData(data);
return true;
}
bool Matroska::Segment::sizeChanged(Element &, offset_t delta)
{
dataSize += delta;
setNeedsRender(true);
return true;
}
offset_t Matroska::Segment::dataOffset() const
{
return offset() + sizeLength;
}
offset_t Matroska::Segment::endOffset() const
{
return dataOffset() + dataSize;
}

View File

@@ -0,0 +1,47 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKASEGMENT_H
#define TAGLIB_MATROSKASEGMENT_H
#ifndef DO_NOT_DOCUMENT
#include "matroskaelement.h"
namespace TagLib::Matroska {
class Segment : public Element
{
public:
Segment(offset_t sizeLength, offset_t dataSize, offset_t lengthOffset);
~Segment() override;
bool render() override;
bool sizeChanged(Element &caller, offset_t delta) override;
offset_t dataOffset() const;
offset_t endOffset() const;
private:
ByteVector renderInternal() override;
offset_t sizeLength;
offset_t dataSize;
};
}
#endif
#endif

View File

@@ -0,0 +1,188 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskasimpletag.h"
#include <variant>
#include "matroskatag.h"
#include "tbytevector.h"
using namespace TagLib;
class Matroska::SimpleTag::SimpleTagPrivate
{
public:
SimpleTagPrivate(const String &name, const String &value,
TargetTypeValue targetTypeValue, const String &language, bool defaultLanguage,
unsigned long long trackUid, unsigned long long editionUid,
unsigned long long chapterUid, unsigned long long attachmentUid) :
value(value), name(name), language(language), trackUid(trackUid),
editionUid(editionUid), chapterUid(chapterUid), attachmentUid(attachmentUid),
targetTypeValue(targetTypeValue), defaultLanguageFlag(defaultLanguage) {}
SimpleTagPrivate(const String &name, const ByteVector &value,
TargetTypeValue targetTypeValue, const String &language, bool defaultLanguage,
unsigned long long trackUid, unsigned long long editionUid,
unsigned long long chapterUid, unsigned long long attachmentUid) :
value(value), name(name), language(language), trackUid(trackUid),
editionUid(editionUid), chapterUid(chapterUid), attachmentUid(attachmentUid),
targetTypeValue(targetTypeValue), defaultLanguageFlag(defaultLanguage) {}
const std::variant<String, ByteVector> value;
const String name;
const String language;
const unsigned long long trackUid;
const unsigned long long editionUid;
const unsigned long long chapterUid;
const unsigned long long attachmentUid;
const TargetTypeValue targetTypeValue;
const bool defaultLanguageFlag;
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
Matroska::SimpleTag::SimpleTag(const String &name, const String &value,
TargetTypeValue targetTypeValue,
const String &language, bool defaultLanguage,
unsigned long long trackUid) :
d(std::make_unique<SimpleTagPrivate>(name, value, targetTypeValue,
language, defaultLanguage, trackUid, 0, 0, 0))
{
}
Matroska::SimpleTag::SimpleTag(const String &name, const String &value,
TargetTypeValue targetTypeValue,
const String &language, bool defaultLanguage,
unsigned long long trackUid,
unsigned long long editionUid,
unsigned long long chapterUid,
unsigned long long attachmentUid) :
d(std::make_unique<SimpleTagPrivate>(name, value, targetTypeValue,
language, defaultLanguage, trackUid, editionUid, chapterUid, attachmentUid))
{
}
Matroska::SimpleTag::SimpleTag(const String &name, const ByteVector &value,
TargetTypeValue targetTypeValue,
const String &language, bool defaultLanguage,
unsigned long long trackUid) :
d(std::make_unique<SimpleTagPrivate>(name, value, targetTypeValue,
language, defaultLanguage, trackUid, 0, 0, 0))
{
}
Matroska::SimpleTag::SimpleTag(const String &name, const ByteVector &value,
TargetTypeValue targetTypeValue,
const String &language, bool defaultLanguage,
unsigned long long trackUid,
unsigned long long editionUid,
unsigned long long chapterUid,
unsigned long long attachmentUid) :
d(std::make_unique<SimpleTagPrivate>(name, value, targetTypeValue,
language, defaultLanguage, trackUid, editionUid, chapterUid, attachmentUid))
{
}
Matroska::SimpleTag::SimpleTag(const SimpleTag &other) :
d(std::make_unique<SimpleTagPrivate>(*other.d))
{
}
Matroska::SimpleTag::SimpleTag(SimpleTag &&other) noexcept = default;
Matroska::SimpleTag::~SimpleTag() = default;
Matroska::SimpleTag &Matroska::SimpleTag::operator=(SimpleTag &&other) noexcept = default;
Matroska::SimpleTag &Matroska::SimpleTag::operator=(const SimpleTag &other)
{
SimpleTag(other).swap(*this);
return *this;
}
void Matroska::SimpleTag::swap(SimpleTag &other) noexcept
{
using std::swap;
swap(d, other.d);
}
Matroska::SimpleTag::TargetTypeValue Matroska::SimpleTag::targetTypeValue() const
{
return d->targetTypeValue;
}
const String &Matroska::SimpleTag::name() const
{
return d->name;
}
const String &Matroska::SimpleTag::language() const
{
return d->language;
}
bool Matroska::SimpleTag::defaultLanguageFlag() const
{
return d->defaultLanguageFlag;
}
unsigned long long Matroska::SimpleTag::trackUid() const
{
return d->trackUid;
}
unsigned long long Matroska::SimpleTag::editionUid() const
{
return d->editionUid;
}
unsigned long long Matroska::SimpleTag::chapterUid() const
{
return d->chapterUid;
}
unsigned long long Matroska::SimpleTag::attachmentUid() const
{
return d->attachmentUid;
}
Matroska::SimpleTag::ValueType Matroska::SimpleTag::type() const
{
return std::holds_alternative<ByteVector>(d->value) ? BinaryType : StringType;
}
String Matroska::SimpleTag::toString() const
{
if(std::holds_alternative<String>(d->value)) {
// get_if() used instead of get() to support restricted compilers
return *std::get_if<String>(&d->value);
}
return {};
}
ByteVector Matroska::SimpleTag::toByteVector() const
{
if(std::holds_alternative<ByteVector>(d->value)) {
// get_if() used instead of get() to support restricted compilers
return *std::get_if<ByteVector>(&d->value);
}
return {};
}

View File

@@ -0,0 +1,185 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKASIMPLETAG_H
#define TAGLIB_MATROSKASIMPLETAG_H
#include <memory>
#include "tstring.h"
namespace TagLib {
class String;
class ByteVector;
namespace Matroska {
//! Attribute of Matroska metadata.
class TAGLIB_EXPORT SimpleTag
{
public:
//! Specifies the level of other elements the tag value applies to.
enum TargetTypeValue {
None = 0, //!< Empty or omitted, everything in the segment
Shot = 10, //!< Shot
Subtrack = 20, //!< Subtrack / movement / scene
Track = 30, //!< Track / song / chapter
Part = 40, //!< Part / session
Album = 50, //!< Album / opera / concert / movie / episode
Edition = 60, //!< Edition / issue / volume / opus / season / sequel
Collection = 70 //!< Collection
};
//! The types the value can have.
enum ValueType {
StringType = 0, //!< Item contains text information coded in UTF-8
BinaryType = 1 //!< Item contains binary information
};
/*!
* Construct a string simple tag.
*/
SimpleTag(const String &name, const String &value,
TargetTypeValue targetTypeValue = None,
const String &language = String(), bool defaultLanguage = true,
unsigned long long trackUid = 0);
/*!
* Construct a string simple tag.
*/
// BIC: merge with constructor above
SimpleTag(const String& name, const String& value, TargetTypeValue targetTypeValue, const String& language,
bool defaultLanguage, unsigned long long trackUid, unsigned long long editionUid,
unsigned long long chapterUid = 0, unsigned long long attachmentUid = 0);
/*!
* Construct a binary simple tag.
*/
SimpleTag(const String &name, const ByteVector &value,
TargetTypeValue targetTypeValue = None,
const String &language = String(), bool defaultLanguage = true,
unsigned long long trackUid = 0);
/*!
* Construct a binary simple tag.
*/
// BIC: merge with constructor above
SimpleTag(const String& name, const ByteVector& value, TargetTypeValue targetTypeValue, const String& language,
bool defaultLanguage, unsigned long long trackUid, unsigned long long editionUid,
unsigned long long chapterUid = 0, unsigned long long attachmentUid = 0);
/*!
* Construct a simple tag as a copy of \a other.
*/
SimpleTag(const SimpleTag &other);
/*!
* Construct a simple tag moving from \a other.
*/
SimpleTag(SimpleTag &&other) noexcept;
/*!
* Destroys this simple tag.
*/
~SimpleTag();
/*!
* Copies the contents of \a other into this item.
*/
SimpleTag &operator=(const SimpleTag &other);
/*!
* Moves the contents of \a other into this item.
*/
SimpleTag &operator=(SimpleTag &&other) noexcept;
/*!
* Exchanges the content of the simple tag with the content of \a other.
*/
void swap(SimpleTag &other) noexcept;
/*!
* Returns the name of the simple tag.
*/
const String &name() const;
/*!
* Returns the logical level of the target.
*/
TargetTypeValue targetTypeValue() const;
/*!
* Returns the language of the tag.
*/
const String &language() const;
/*!
* Returns if this is the default/original language to use for the tag.
*/
bool defaultLanguageFlag() const;
/*!
* Returns the UID that identifies the track that the tags belong to,
* zero if not defined, the tag applies to all tracks
*/
unsigned long long trackUid() const;
/*!
* Returns the UID that identifies the edition that the tags belong to,
* zero if not defined, the tag applies to all editions
*/
unsigned long long editionUid() const;
/*!
* Returns the UID that identifies the chapter that the tags belong to,
* zero if not defined, the tag applies to all chapters
*/
unsigned long long chapterUid() const;
/*!
* Returns the UID that identifies the attachment that the tags belong to,
* zero if not defined, the tag applies to all attachments
*/
unsigned long long attachmentUid() const;
/*!
* Returns the type of the value.
*/
ValueType type() const;
/*!
* Returns the StringType value.
*/
String toString() const;
/*!
* Returns the BinaryType value.
*/
ByteVector toByteVector() const;
private:
class SimpleTagPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<SimpleTagPrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,670 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "matroskatag.h"
#include <algorithm>
#include <array>
#include <tuple>
#include "ebmlmasterelement.h"
#include "ebmlstringelement.h"
#include "ebmlbinaryelement.h"
#include "ebmlmktags.h"
#include "ebmluintelement.h"
#include "ebmlutils.h"
#include "tpropertymap.h"
using namespace TagLib;
class Matroska::Tag::TagPrivate
{
public:
TagPrivate() = default;
~TagPrivate() = default;
bool setTag(const String &key, const String &value);
String getTag(const String &key) const;
template <typename T>
int removeSimpleTags(T &&p)
{
auto &list = tags;
int numRemoved = 0;
for(auto it = list.begin(); it != list.end();) {
it = std::find_if(it, list.end(), std::forward<T>(p));
if(it != list.end()) {
it = list.erase(it);
numRemoved++;
}
}
return numRemoved;
}
template <typename T>
SimpleTagsList findSimpleTags(T &&p)
{
auto &list = tags;
for(auto it = list.begin(); it != list.end();) {
it = std::find_if(it, list.end(), std::forward<T>(p));
if(it != list.end()) {
list.append(*it);
++it;
}
}
return list;
}
SimpleTagsList tags;
ByteVector data;
String segmentTitle;
};
Matroska::Tag::Tag() :
Element(static_cast<ID>(EBML::Element::Id::MkTags)),
d(std::make_unique<TagPrivate>())
{
}
Matroska::Tag::~Tag() = default;
void Matroska::Tag::addSimpleTag(const SimpleTag &tag)
{
d->tags.append(tag);
setNeedsRender(true);
}
void Matroska::Tag::addSimpleTags(const SimpleTagsList& simpleTags)
{
d->tags.append(simpleTags);
setNeedsRender(true);
}
void Matroska::Tag::insertSimpleTag(unsigned int index, const SimpleTag &tag)
{
if(index < d->tags.size()) {
auto it = d->tags.begin();
std::advance(it, index);
d->tags.insert(it, tag);
}
else {
d->tags.append(tag);
}
setNeedsRender(true);
}
void Matroska::Tag::removeSimpleTag(unsigned int index)
{
if(index < d->tags.size()) {
auto it = d->tags.begin();
std::advance(it, index);
d->tags.erase(it);
setNeedsRender(true);
}
}
void Matroska::Tag::removeSimpleTag(const String &name,
SimpleTag::TargetTypeValue targetTypeValue,
unsigned long long trackUid)
{
removeSimpleTag(name, targetTypeValue, trackUid, 0, 0, 0);
}
void Matroska::Tag::removeSimpleTag(const String& name,
SimpleTag::TargetTypeValue targetTypeValue,
unsigned long long trackUid, unsigned long long editionUid,
unsigned long long chapterUid, unsigned long long attachmentUid)
{
const auto it = std::find_if(d->tags.begin(), d->tags.end(),
[&name, targetTypeValue, trackUid, editionUid, chapterUid, attachmentUid](
const SimpleTag &t) {
return t.name() == name && t.targetTypeValue() == targetTypeValue &&
t.trackUid() == trackUid && t.editionUid() == editionUid &&
t.chapterUid() == chapterUid && t.attachmentUid() == attachmentUid;
}
);
if(it != d->tags.end()) {
d->tags.erase(it);
setNeedsRender(true);
}
}
void Matroska::Tag::clearSimpleTags()
{
d->tags.clear();
setNeedsRender(true);
}
const Matroska::SimpleTagsList &Matroska::Tag::simpleTagsList() const
{
return d->tags;
}
void Matroska::Tag::setSegmentTitle(const String &title)
{
d->segmentTitle = title;
}
bool Matroska::Tag::setTag(const String &key, const String &value)
{
const bool found = d->setTag(key, value);
if(found) {
setNeedsRender(true);
}
return found;
}
void Matroska::Tag::setTitle(const String &s)
{
setTag("TITLE", s);
}
void Matroska::Tag::setArtist(const String &s)
{
setTag("ARTIST", s);
}
void Matroska::Tag::setAlbum(const String &s)
{
setTag("ALBUM", s);
}
void Matroska::Tag::setComment(const String &s)
{
setTag("COMMENT", s);
}
void Matroska::Tag::setGenre(const String &s)
{
setTag("GENRE", s);
}
void Matroska::Tag::setYear(unsigned int i)
{
setTag("DATE", i != 0 ? String::number(i) : String());
}
void Matroska::Tag::setTrack(unsigned int i)
{
setTag("TRACKNUMBER", i != 0 ? String::number(i) : String());
}
String Matroska::Tag::title() const
{
String s = d->getTag("TITLE");
if(s.isEmpty()) {
return d->segmentTitle;
}
return s;
}
String Matroska::Tag::artist() const
{
return d->getTag("ARTIST");
}
String Matroska::Tag::album() const
{
return d->getTag("ALBUM");
}
String Matroska::Tag::comment() const
{
return d->getTag("COMMENT");
}
String Matroska::Tag::genre() const
{
return d->getTag("GENRE");
}
unsigned int Matroska::Tag::year() const
{
const auto value = d->getTag("DATE");
if(value.isEmpty())
return 0;
auto list = value.split("-");
return static_cast<unsigned int>(list.front().toInt());
}
unsigned int Matroska::Tag::track() const
{
const auto value = d->getTag("TRACKNUMBER");
if(value.isEmpty())
return 0;
auto list = value.split("-");
return static_cast<unsigned int>(list.front().toInt());
}
bool Matroska::Tag::isEmpty() const
{
return d->tags.isEmpty();
}
ByteVector Matroska::Tag::renderInternal()
{
if(d->tags.isEmpty()) {
// Avoid writing a Tags element without Tag element.
return {};
}
EBML::MkTags tags;
List<SimpleTagsList> targetList;
// Build target-based list
for(const auto &tag : std::as_const(d->tags)) {
auto targetTypeValue = tag.targetTypeValue();
auto trackUid = tag.trackUid();
auto editionUid = tag.editionUid();
auto chapterUid = tag.chapterUid();
auto attachmentUid = tag.attachmentUid();
auto it = std::find_if(targetList.begin(),
targetList.end(),
[&](const auto &list) {
const auto &simpleTag = list.front();
return simpleTag.targetTypeValue() == targetTypeValue &&
simpleTag.trackUid() == trackUid &&
simpleTag.editionUid() == editionUid &&
simpleTag.chapterUid() == chapterUid &&
simpleTag.attachmentUid() == attachmentUid;
}
);
if(it == targetList.end()) {
SimpleTagsList list;
list.append(tag);
targetList.append(list);
}
else
it->append(tag);
}
for(const auto &list : targetList) {
const auto &frontTag = list.front();
const auto targetTypeValue = frontTag.targetTypeValue();
const auto trackUid = frontTag.trackUid();
const auto editionUid = frontTag.editionUid();
const auto chapterUid = frontTag.chapterUid();
const auto attachmentUid = frontTag.attachmentUid();
auto tag = EBML::make_unique_element<EBML::Element::Id::MkTag>();
// Build <Tag Targets> element
auto targets = EBML::make_unique_element<EBML::Element::Id::MkTagTargets>();
if(targetTypeValue != SimpleTag::TargetTypeValue::None) {
auto element = EBML::make_unique_element<EBML::Element::Id::MkTagTargetTypeValue>();
element->setValue(static_cast<unsigned int>(targetTypeValue));
targets->appendElement(std::move(element));
}
if(trackUid != 0) {
auto element = EBML::make_unique_element<EBML::Element::Id::MkTagTrackUID>();
element->setValue(trackUid);
targets->appendElement(std::move(element));
}
if(editionUid != 0) {
auto element = EBML::make_unique_element<EBML::Element::Id::MkTagEditionUID>();
element->setValue(editionUid);
targets->appendElement(std::move(element));
}
if(chapterUid != 0) {
auto element = EBML::make_unique_element<EBML::Element::Id::MkTagChapterUID>();
element->setValue(chapterUid);
targets->appendElement(std::move(element));
}
if(attachmentUid != 0) {
auto element = EBML::make_unique_element<EBML::Element::Id::MkTagAttachmentUID>();
element->setValue(attachmentUid);
targets->appendElement(std::move(element));
}
tag->appendElement(std::move(targets));
// Build <Simple Tag> element
for(const auto &simpleTag : list) {
auto t = EBML::make_unique_element<EBML::Element::Id::MkSimpleTag>();
auto tagName = EBML::make_unique_element<EBML::Element::Id::MkTagName>();
tagName->setValue(simpleTag.name());
t->appendElement(std::move(tagName));
// Tag Value
if(simpleTag.type() == SimpleTag::StringType) {
auto tagValue = EBML::make_unique_element<EBML::Element::Id::MkTagString>();
tagValue->setValue(simpleTag.toString());
t->appendElement(std::move(tagValue));
}
else if(simpleTag.type() == SimpleTag::BinaryType) {
auto tagValue = EBML::make_unique_element<EBML::Element::Id::MkTagBinary>();
tagValue->setValue(simpleTag.toByteVector());
t->appendElement(std::move(tagValue));
}
// Language
auto language = EBML::make_unique_element<EBML::Element::Id::MkTagsTagLanguage>();
const String &lang = simpleTag.language();
language->setValue(!lang.isEmpty() ? lang : "und");
t->appendElement(std::move(language));
// Default language flag
auto dlf = EBML::make_unique_element<EBML::Element::Id::MkTagsLanguageDefault>();
dlf->setValue(simpleTag.defaultLanguageFlag() ? 1 : 0);
t->appendElement(std::move(dlf));
tag->appendElement(std::move(t));
}
tags.appendElement(std::move(tag));
}
// Pad to the previous size so the element keeps its slot in the file,
// unless this element is the trailing element of the segment in
// AvoidInsert mode -- shrinking from the end never inserts anything,
// so the trailing void would be wasted space.
if(writeStyle() != WriteStyle::Compact &&
!(writeStyle() == WriteStyle::AvoidInsert && isTrailingInSegment())) {
const auto beforeSize = sizeRenderedOrWritten();
if(beforeSize > 0)
tags.setMinRenderSize(beforeSize);
}
return tags.render();
}
namespace
{
// PropertyMap key, Tag name, Target type value, strict
// If the key is the same as the name and the target type value is Track,
// no translation is needed because this is the default mapping.
// Therefore, keys like TITLE, ARTIST, GENRE, COMMENT, etc. are omitted
// unless they shall have priority over higher level tags with the same name
// when no target type value is given. The strict boolean marks
// entries which shall not be mapped without correct target type value.
// For offical tags, see https://www.matroska.org/technical/tagging.html
constexpr std::array simpleTagsTranslation {
std::tuple("TITLE", "TITLE", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("ALBUM", "TITLE", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("ARTIST", "ARTIST", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("ALBUMARTIST", "ARTIST", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("TRACKNUMBER", "PART_NUMBER", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("DISCNUMBER", "PART_NUMBER", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("TRACKTOTAL", "TOTAL_PARTS", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("DISCTOTAL", "TOTAL_PARTS", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("DATE", "DATE_RECORDED", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("TITLESORT", "TITLESORT", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("ALBUMSORT", "TITLESORT", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("ARTISTSORT", "ARTISTSORT", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("ALBUMARTISTSORT", "ARTISTSORT", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("MEDIA", "ORIGINAL_MEDIA_TYPE", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("LABEL", "LABEL_CODE", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("CATALOGNUMBER", "CATALOG_NUMBER", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("DJMIXER", "MIXED_BY", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("REMIXER", "REMIXED_BY", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("INITIALKEY", "INITIAL_KEY", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("RELEASEDATE", "DATE_RELEASED", Matroska::SimpleTag::TargetTypeValue::Album, false),
std::tuple("ENCODINGTIME", "DATE_ENCODED", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("TAGGINGDATE", "DATE_TAGGED", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("ENCODEDBY", "ENCODER", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("ENCODING", "ENCODER_SETTINGS", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("OWNER", "PURCHASE_OWNER", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_GAIN", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_GAIN", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("REPLAYGAIN_TRACK_PEAK", "REPLAYGAIN_PEAK", Matroska::SimpleTag::TargetTypeValue::Track, false),
std::tuple("REPLAYGAIN_ALBUM_PEAK", "REPLAYGAIN_PEAK", Matroska::SimpleTag::TargetTypeValue::Album, true),
std::tuple("MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ_ALBUMARTISTID", Matroska::SimpleTag::TargetTypeValue::Album, false),
std::tuple("MUSICBRAINZ_ALBUMID", "MUSICBRAINZ_ALBUMID", Matroska::SimpleTag::TargetTypeValue::Album, false),
std::tuple("MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ_RELEASEGROUPID", Matroska::SimpleTag::TargetTypeValue::Album, false),
};
std::tuple<String, Matroska::SimpleTag::TargetTypeValue, bool> translateKey(const String &key)
{
auto it = std::find_if(simpleTagsTranslation.cbegin(),
simpleTagsTranslation.cend(),
[&key](const auto &t) { return key == std::get<0>(t); }
);
if(it != simpleTagsTranslation.end())
return { std::get<1>(*it), std::get<2>(*it), std::get<3>(*it) };
if(!key.isEmpty())
return { key, Matroska::SimpleTag::TargetTypeValue::Track, false };
return { String(), Matroska::SimpleTag::TargetTypeValue::None, false };
}
String translateTag(const String &name, Matroska::SimpleTag::TargetTypeValue targetTypeValue)
{
const auto it = std::find_if(simpleTagsTranslation.cbegin(),
simpleTagsTranslation.cend(),
[&name, targetTypeValue](const auto &t) {
return name == std::get<1>(t)
&& (targetTypeValue == std::get<2>(t) ||
(targetTypeValue == Matroska::SimpleTag::TargetTypeValue::None
&& !std::get<3>(t)));
}
);
return it != simpleTagsTranslation.end()
? String(std::get<0>(*it), String::UTF8)
: targetTypeValue == Matroska::SimpleTag::TargetTypeValue::Track ||
targetTypeValue == Matroska::SimpleTag::TargetTypeValue::None
? name
: String();
}
}
bool Matroska::Tag::TagPrivate::setTag(const String &key, const String &value)
{
const auto tpl = translateKey(key);
// Workaround Clang issue - no lambda capture of structured bindings
const String &name = std::get<0>(tpl);
auto targetTypeValue = std::get<1>(tpl);
if(name.isEmpty())
return false;
removeSimpleTags(
[&name, targetTypeValue] (const auto &t) {
return t.name() == name
&& t.targetTypeValue() == targetTypeValue;
}
);
if(!value.isEmpty()) {
tags.append(SimpleTag(name, value, targetTypeValue));
}
return true;
}
String Matroska::Tag::TagPrivate::getTag(const String &key) const
{
const auto tpl = translateKey(key);
// Workaround Clang issue - no lambda capture of structured bindings
const String &name = std::get<0>(tpl);
auto targetTypeValue = std::get<1>(tpl);
bool strict = std::get<2>(tpl);
if(name.isEmpty())
return {};
const auto it = std::find_if(tags.begin(), tags.end(),
[&name, targetTypeValue, strict] (const SimpleTag &t) {
return t.name() == name
&& t.type() == SimpleTag::StringType
&& (t.targetTypeValue() == targetTypeValue ||
(t.targetTypeValue() == SimpleTag::TargetTypeValue::None && !strict))
&& t.trackUid() == 0 && t.editionUid() == 0 && t.chapterUid() == 0
&& t.attachmentUid() == 0;
}
);
return it != tags.end() ? it->toString() : String();
}
PropertyMap Matroska::Tag::properties() const
{
PropertyMap properties;
for(const auto &simpleTag : std::as_const(d->tags)) {
if(simpleTag.type() == SimpleTag::StringType && simpleTag.trackUid() == 0 &&
simpleTag.editionUid() == 0 && simpleTag.chapterUid() == 0 &&
simpleTag.attachmentUid() == 0) {
if(String key = translateTag(simpleTag.name(), simpleTag.targetTypeValue());
!key.isEmpty())
properties[key].append(simpleTag.toString());
else
properties.addUnsupportedData(simpleTag.name());
}
}
return properties;
}
PropertyMap Matroska::Tag::setProperties(const PropertyMap &propertyMap)
{
// Remove all simple tags which would be returned in properties()
for(auto it = d->tags.begin(); it != d->tags.end();) {
if(it->type() == SimpleTag::StringType &&
it->trackUid() == 0 && it->editionUid() == 0 &&
it->chapterUid() == 0 && it->attachmentUid() == 0 &&
!translateTag(it->name(), it->targetTypeValue()).isEmpty()) {
it = d->tags.erase(it);
setNeedsRender(true);
}
else {
++it;
}
}
// Add the new properties
PropertyMap unsupportedProperties;
for(const auto &[key, values] : propertyMap) {
for(const auto &value : values) {
if(auto [name, targetTypeValue, _] = translateKey(key);
!name.isEmpty()) {
d->tags.append(SimpleTag(name, value, targetTypeValue));
setNeedsRender(true);
}
else {
unsupportedProperties[key] = values;
}
}
}
return unsupportedProperties;
}
void Matroska::Tag::removeUnsupportedProperties(const StringList &properties)
{
if(d->removeSimpleTags(
[&properties](const SimpleTag &t) {
return properties.contains(t.name());
}) > 0) {
setNeedsRender(true);
}
}
StringList Matroska::Tag::complexPropertyKeys() const
{
StringList keys;
for(const SimpleTag &t : std::as_const(d->tags)) {
if((t.type() != SimpleTag::StringType ||
t.trackUid() != 0 || t.editionUid() != 0 ||
t.chapterUid() != 0 || t.attachmentUid() != 0 ||
translateTag(t.name(), t.targetTypeValue()).isEmpty()) &&
!keys.contains(t.name())) {
keys.append(t.name());
}
}
return keys;
}
List<VariantMap> Matroska::Tag::complexProperties(const String &key) const
{
List<VariantMap> props;
if(key.upper() != "PICTURE") { // Pictures are handled at the file level
for(const SimpleTag &t : std::as_const(d->tags)) {
if(t.name() == key &&
(t.type() != SimpleTag::StringType ||
t.trackUid() != 0 || t.editionUid() != 0 ||
t.chapterUid() != 0 || t.attachmentUid() != 0 ||
translateTag(t.name(), t.targetTypeValue()).isEmpty())) {
VariantMap property;
if(t.type() != SimpleTag::StringType) {
property.insert("data", t.toByteVector());
}
else {
property.insert("value", t.toString());
}
property.insert("name", t.name());
if(t.targetTypeValue() != SimpleTag::TargetTypeValue::None) {
property.insert("targetTypeValue", t.targetTypeValue());
}
if(t.trackUid()) {
property.insert("trackUid", t.trackUid());
}
if(t.editionUid()) {
property.insert("editionUid", t.editionUid());
}
if(t.chapterUid()) {
property.insert("chapterUid", t.chapterUid());
}
if(t.attachmentUid()) {
property.insert("attachmentUid", t.attachmentUid());
}
property.insert("language", t.language());
property.insert("defaultLanguage", t.defaultLanguageFlag());
props.append(property);
}
}
}
return props;
}
bool Matroska::Tag::setComplexProperties(const String &key, const List<VariantMap> &value)
{
if(key.upper() == "PICTURE") {
// Pictures are handled at the file level
return false;
}
if(d->removeSimpleTags(
[&key](const SimpleTag &t) {
return t.name() == key &&
(t.type() != SimpleTag::StringType ||
t.trackUid() != 0 || t.editionUid() != 0 ||
t.chapterUid() != 0 || t.attachmentUid() != 0 ||
translateTag(t.name(), t.targetTypeValue()).isEmpty());
}) > 0) {
setNeedsRender(true);
}
bool result = false;
for(const auto &property : value) {
if(property.value("name").value<String>() == key &&
(property.contains("data") || property.contains("value") )) {
SimpleTag::TargetTypeValue targetTypeValue;
switch(Variant targetTypeValueVar = property.value("targetTypeValue", 0);
targetTypeValueVar.type()) {
case Variant::UInt:
targetTypeValue = static_cast<SimpleTag::TargetTypeValue>(targetTypeValueVar.value<unsigned int>());
break;
case Variant::LongLong:
targetTypeValue = static_cast<SimpleTag::TargetTypeValue>(targetTypeValueVar.value<long long>());
break;
case Variant::ULongLong:
targetTypeValue = static_cast<SimpleTag::TargetTypeValue>(targetTypeValueVar.value<unsigned long long>());
break;
default:
targetTypeValue = static_cast<SimpleTag::TargetTypeValue>(targetTypeValueVar.value<int>());
}
auto language = property.value("language").value<String>();
const bool defaultLanguage = property.value("defaultLanguage", true).value<bool>();
const auto trackUid = property.value("trackUid", 0ULL).value<unsigned long long>();
const auto editionUid = property.value("editionUid", 0ULL).value<unsigned long long>();
const auto chapterUid = property.value("chapterUid", 0ULL).value<unsigned long long>();
const auto attachmentUid = property.value("attachmentUid", 0ULL).value<unsigned long long>();
d->tags.append(property.contains("data")
? SimpleTag(key, property.value("data").value<ByteVector>(),
targetTypeValue, language, defaultLanguage, trackUid,
editionUid, chapterUid, attachmentUid)
: SimpleTag(key, property.value("value").value<String>(),
targetTypeValue, language, defaultLanguage, trackUid,
editionUid, chapterUid, attachmentUid));
setNeedsRender(true);
result = true;
}
}
return result;
}

View File

@@ -0,0 +1,158 @@
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKATAG_H
#define TAGLIB_MATROSKATAG_H
#include <memory>
#include "tag.h"
#include "tlist.h"
#include "matroskaelement.h"
#include "matroskasimpletag.h"
namespace TagLib {
class File;
namespace EBML {
class MkTags;
}
namespace Matroska {
//! List of tag attributes.
using SimpleTagsList = List<SimpleTag>;
//! Matroska tag implementation.
class TAGLIB_EXPORT Tag : public TagLib::Tag
#ifndef DO_NOT_DOCUMENT
, private Element
#endif
{
public:
/*!
* Constructs a Matroska tag.
*/
Tag();
~Tag() override;
String title() const override;
String artist() const override;
String album() const override;
String comment() const override;
String genre() const override;
unsigned int year() const override;
unsigned int track() const override;
void setTitle(const String &s) override;
void setArtist(const String &s) override;
void setAlbum(const String &s) override;
void setComment(const String &s) override;
void setGenre(const String &s) override;
void setYear(unsigned int i) override;
void setTrack(unsigned int i) override;
bool isEmpty() const override;
PropertyMap properties() const override;
PropertyMap setProperties(const PropertyMap &propertyMap) override;
void removeUnsupportedProperties(const StringList &properties) override;
/*!
* Returns the names of the binary simple tags.
*/
StringList complexPropertyKeys() const override;
/*!
* Get the binary simple tags as maps with keys "data", "name",
* "targetTypeValue", "language", "defaultLanguage".
* The attached files such as pictures with key "PICTURE" are available
* with Matroska::File::complexProperties().
*/
List<VariantMap> complexProperties(const String &key) const override;
/*!
* Set the binary simple tags as maps with keys "data", "name",
* "targetTypeValue", "language", "defaultLanguage".
* The attached files such as pictures with key "PICTURE" can be set
* with Matroska::File::setComplexProperties().
*
* Returns \c true if \c key can be stored as binary simple tags.
*/
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
/*!
* Add a tag attribute.
*/
void addSimpleTag(const SimpleTag &tag);
/*!
* Add multiple tag attributes.
*/
void addSimpleTags(const SimpleTagsList &simpleTags);
/*!
* Insert a tag attribute at position \a index.
*/
void insertSimpleTag(unsigned int index, const SimpleTag &tag);
/*!
* Remove a tag attribute at position \a index.
*/
void removeSimpleTag(unsigned int index);
/*!
* Remove a tag attribute.
*/
void removeSimpleTag(const String &name, SimpleTag::TargetTypeValue targetTypeValue,
unsigned long long trackUid = 0);
/*!
* Remove a tag attribute.
*/
// BIC: Merge with the method above
void removeSimpleTag(const String &name, SimpleTag::TargetTypeValue targetTypeValue,
unsigned long long trackUid, unsigned long long editionUid,
unsigned long long chapterUid = 0, unsigned long long attachmentUid = 0);
/*!
* Remove all tag attributes.
*/
void clearSimpleTags();
/*!
* Get list of all tag attributes.
*/
const SimpleTagsList &simpleTagsList() const;
private:
friend class File;
friend class EBML::MkTags;
class TagPrivate;
bool setTag(const String &key, const String &value);
void setSegmentTitle(const String &title);
// private Element implementation
ByteVector renderInternal() override;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<TagPrivate> d;
};
}
}
#endif

View File

@@ -0,0 +1,49 @@
/***************************************************************************
copyright : (C) 2026 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MATROSKAWRITESTYLE_H
#define TAGLIB_MATROSKAWRITESTYLE_H
namespace TagLib::Matroska {
/*!
* Controls the trade-off between file size and write speed when saving.
* Mode of writing tags, attachments and chapters to the file.
* For very large files and/or slow (network) filesystems, using
* \c AvoidInsert will reduce write time significantly.
*/
enum class WriteStyle {
//! Write tags, attachments and chapters as compact as possible (default).
Compact,
//! Do not shrink elements; add void padding when content gets smaller.
//! Allow inserts when content gets larger.
DoNotShrink,
//! Like \c 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.
AvoidInsert
};
}
#endif //TAGLIB_MATROSKAWRITESTYLE_H

View File

@@ -37,7 +37,7 @@ namespace {
constexpr std::array containers {
"moov", "udta", "mdia", "meta", "ilst",
"stbl", "minf", "moof", "traf", "trak",
"stsd"
"stsd", "stem"
};
} // namespace
@@ -51,7 +51,7 @@ public:
AtomList children;
};
MP4::Atom::Atom(File *file)
MP4::Atom::Atom(File *file, int depth)
: d(std::make_unique<AtomPrivate>(file->tell()))
{
d->children.setAutoDelete(true);
@@ -74,17 +74,7 @@ MP4::Atom::Atom(File *file)
}
else if(d->length == 1) {
// The atom has a 64-bit length.
if(const long long longLength = file->readBlock(8).toLongLong();
longLength <= LONG_MAX) {
// The actual length fits in long. That's always the case if long is 64-bit.
d->length = static_cast<long>(longLength);
}
else {
debug("MP4: 64-bit atoms are not supported");
d->length = 0;
file->seek(0, File::End);
return;
}
d->length = file->readBlock(8).toLongLong();
}
if(d->length < 8 || d->length > file->length() - d->offset) {
@@ -96,6 +86,11 @@ MP4::Atom::Atom(File *file)
d->name = header.mid(4, 4);
if(d->name == "stem") {
file->seek(d->length - 8, File::Current);
return;
}
for(auto c : containers) {
if(d->name == c) {
if(d->name == "meta") {
@@ -114,8 +109,13 @@ MP4::Atom::Atom(File *file)
else if(d->name == "stsd") {
file->seek(8, File::Current);
}
static constexpr int MAX_MP4_ATOM_DEPTH = 64;
if(depth > MAX_MP4_ATOM_DEPTH) {
debug("MP4: Maximum nesting depth exceeded");
return;
}
while(file->tell() < d->offset + d->length) {
auto child = new MP4::Atom(file);
auto child = new MP4::Atom(file, depth + 1);
d->children.append(child);
if(child->d->length == 0)
return;
@@ -127,6 +127,11 @@ MP4::Atom::Atom(File *file)
file->seek(d->offset + d->length);
}
MP4::Atom::Atom(File *file)
: Atom(file, 0)
{
}
MP4::Atom::~Atom() = default;
MP4::Atom *
@@ -217,6 +222,8 @@ public:
MP4::Atoms::Atoms(File *file) :
d(std::make_unique<AtomsPrivate>())
{
static constexpr int MAX_MP4_ATOM_COUNT_PER_LEVEL = 5000;
d->atoms.setAutoDelete(true);
file->seek(0, File::End);
@@ -227,6 +234,13 @@ MP4::Atoms::Atoms(File *file) :
d->atoms.append(atom);
if (atom->length() == 0)
break;
if(d->atoms.size() > MAX_MP4_ATOM_COUNT_PER_LEVEL) {
debug("MP4: Maximum atom count exceeded");
// Make sure the file is detected as invalid.
d->atoms.clear();
break;
}
}
}

View File

@@ -35,27 +35,48 @@ namespace TagLib {
namespace MP4 {
enum AtomDataType {
TypeImplicit = 0, // for use with tags for which no type needs to be indicated because only one type is allowed
TypeUTF8 = 1, // without any count or null terminator
TypeUTF16 = 2, // also known as UTF-16BE
TypeSJIS = 3, // deprecated unless it is needed for special Japanese characters
TypeHTML = 6, // the HTML file header specifies which HTML version
TypeXML = 7, // the XML header must identify the DTD or schemas
TypeUUID = 8, // also known as GUID; stored as 16 bytes in binary (valid as an ID)
TypeISRC = 9, // stored as UTF-8 text (valid as an ID)
TypeMI3P = 10, // stored as UTF-8 text (valid as an ID)
TypeGIF = 12, // (deprecated) a GIF image
TypeJPEG = 13, // a JPEG image
TypePNG = 14, // a PNG image
TypeURL = 15, // absolute, in UTF-8 characters
TypeDuration = 16, // in milliseconds, 32-bit integer
TypeDateTime = 17, // in UTC, counting seconds since midnight, January 1, 1904; 32 or 64-bits
TypeGenred = 18, // a list of enumerated values
TypeInteger = 21, // a signed big-endian integer with length one of { 1,2,3,4,8 } bytes
TypeRIAAPA = 24, // RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, 8-bit integer
TypeUPC = 25, // Universal Product Code, in text UTF-8 format (valid as an ID)
TypeBMP = 27, // Windows bitmap image
TypeUndefined = 255 // undefined
//! For use with tags for which no type needs to be indicated because only one type is allowed
TypeImplicit = 0,
//! Without any count or null terminator
TypeUTF8 = 1,
//! Also known as UTF-16BE
TypeUTF16 = 2,
//! Deprecated unless it is needed for special Japanese characters
TypeSJIS = 3,
//! The HTML file header specifies which HTML version
TypeHTML = 6,
//! The XML header must identify the DTD or schemas
TypeXML = 7,
//! Also known as GUID; stored as 16 bytes in binary (valid as an ID)
TypeUUID = 8,
//! Stored as UTF-8 text (valid as an ID)
TypeISRC = 9,
//! Stored as UTF-8 text (valid as an ID)
TypeMI3P = 10,
//! (Deprecated) A GIF image
TypeGIF = 12,
//! A JPEG image
TypeJPEG = 13,
//! A PNG image
TypePNG = 14,
//! Absolute, in UTF-8 characters
TypeURL = 15,
//! In milliseconds, 32-bit integer
TypeDuration = 16,
//! In UTC, counting seconds since midnight, January 1, 1904; 32 or 64-bits
TypeDateTime = 17,
//! A list of enumerated values
TypeGenred = 18,
//! A signed big-endian integer with length one of { 1,2,3,4,8 } bytes
TypeInteger = 21,
//! RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, 8-bit integer
TypeRIAAPA = 24,
//! Universal Product Code, in text UTF-8 format (valid as an ID)
TypeUPC = 25,
//! Windows bitmap image
TypeBMP = 27,
//! Undefined
TypeUndefined = 255
};
#ifndef DO_NOT_DOCUMENT
@@ -89,6 +110,9 @@ namespace TagLib {
const ByteVector &name() const;
const AtomList &children() const;
protected:
Atom(File *file, int depth);
private:
class AtomPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE

89
taglib/mp4/mp4chapter.cpp Normal file
View File

@@ -0,0 +1,89 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "mp4chapter.h"
#include "tstring.h"
using namespace TagLib;
class MP4::Chapter::ChapterPrivate
{
public:
ChapterPrivate() = default;
~ChapterPrivate() = default;
String title;
long long startTime {0};
};
MP4::Chapter::Chapter(const String &title, long long startTime) :
d(std::make_unique<ChapterPrivate>())
{
d->title = title;
d->startTime = startTime;
}
MP4::Chapter::Chapter(const Chapter &other) :
d(std::make_unique<ChapterPrivate>(*other.d))
{
}
MP4::Chapter::Chapter(Chapter &&other) noexcept = default;
MP4::Chapter::Chapter::~Chapter() = default;
MP4::Chapter &MP4::Chapter::Chapter::operator=(const Chapter &other)
{
Chapter(other).swap(*this);
return *this;
}
MP4::Chapter &MP4::Chapter::Chapter::operator=(
Chapter &&other) noexcept = default;
bool MP4::Chapter::operator==(const Chapter &other) const
{
return title() == other.title() && startTime() == other.startTime();
}
bool MP4::Chapter::operator!=(const Chapter &other) const
{
return !(*this == other);
}
void MP4::Chapter::swap(Chapter &other) noexcept
{
using std::swap;
swap(d, other.d);
}
const String &MP4::Chapter::title() const
{
return d->title;
}
long long MP4::Chapter::startTime() const
{
return d->startTime;
}

108
taglib/mp4/mp4chapter.h Normal file
View File

@@ -0,0 +1,108 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MP4CHAPTER_H
#define TAGLIB_MP4CHAPTER_H
#include <memory>
#include "taglib_export.h"
#include "tlist.h"
namespace TagLib {
class String;
namespace MP4 {
/*!
* A single Nero-style chapter marker.
*/
class TAGLIB_EXPORT Chapter {
public:
/*!
* Construct a chapter.
*/
Chapter(const String &title, long long startTime);
/*!
* Construct a chapter as a copy of \a other.
*/
Chapter(const Chapter &other);
/*!
* Construct a chapter moving from \a other.
*/
Chapter(Chapter &&other) noexcept;
/*!
* Destroys this chapter.
*/
~Chapter();
/*!
* Copies the contents of \a other into this object.
*/
Chapter &operator=(const Chapter &other);
/*!
* Moves the contents of \a other into this object.
*/
Chapter &operator=(Chapter &&other) noexcept;
/*!
* Returns \c true if the chapter and \a other contain the same data.
*/
bool operator==(const Chapter &other) const;
/*!
* Returns \c true if the chapter and \a other differ in data.
*/
bool operator!=(const Chapter &other) const;
/*!
* Exchanges the content of the object with the content of \a other.
*/
void swap(Chapter &other) noexcept;
/*!
* Returns the title representing the chapter.
*/
const String &title() const;
/*!
* Returns the start time in milliseconds.
*/
long long startTime() const;
private:
class ChapterPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChapterPrivate> d;
};
//! List of chapters.
using ChapterList = List<Chapter>;
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -0,0 +1,126 @@
/**************************************************************************
copyright : (C) 2006 by Urs Fleisch
email : ufleisch@users.sourceforge.net
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MP4CHAPTERHOLDER_H
#define TAGLIB_MP4CHAPTERHOLDER_H
#include "mp4chapter.h"
namespace TagLib {
class File;
namespace MP4 {
/*!
* Base class to hold chapters and store modified state.
*/
class ChapterHolder {
public:
/*!
* Get list of chapters.
*/
ChapterList chapters() const { return chapterList; }
/*!
* Set list of chapters.
*/
void setChapters(const ChapterList &chapters) { chapterList = chapters; }
/*!
* Returns \c true if the list of chapters has been modified.
*/
bool isModified() const { return modified; }
/*!
* Set if the contained chapters are modified.
*/
void setModified(bool chaptersModified) { modified = chaptersModified; }
protected:
ChapterList chapterList;
bool modified = false;
};
/*!
* Lazily fetch list of chapters.
* @tparam T class derived from ChapterHolder and implementing read(File *)
* @param holder unique pointer to holder, initially null
* @param file file with chapters
* @return list of chapters, empty if no chapters found.
*/
template <typename T>
ChapterList getChaptersLazy(std::unique_ptr<T> &holder, TagLib::File *file)
{
if (!holder) {
holder = std::make_unique<T>();
holder->read(file);
}
return holder->chapters();
}
/*!
* Lazily set a list of chapters.
* @tparam T class derived from ChapterHolder
* @param holder unique pointer to holder, initially null
* @param chapters list of chapters to set
*/
template <typename T>
void setChaptersLazy(std::unique_ptr<T> &holder, const ChapterList& chapters)
{
if (!holder) {
holder = std::make_unique<T>();
// The chapters have not been read before, so we do not know their
// current state and mark them as modified. Otherwise, the check below
// would not set the chapters if they are empty.
holder->setModified(true);
}
if(holder->isModified() || holder->chapters() != chapters) {
holder->setChapters(chapters);
holder->setModified(true);
}
}
/*!
* Save a list of chapters if it has been modified.
* @tparam T class derived from ChapterHolder and implementing write(File *)
* @param holder unique pointer to holder, initially null
* @param file file with chapters
* @return true if write successful or not modified.
*/
template <typename T>
bool saveChaptersIfModified(std::unique_ptr<T> &holder, TagLib::File *file)
{
if(holder && holder->isModified()) {
if(holder->write(file)) {
holder->setModified(false);
return true;
}
return false;
}
return true;
}
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -30,6 +30,8 @@
#include "tagutils.h"
#include "mp4itemfactory.h"
#include "mp4nerochapterlist.h"
#include "mp4qtchapterlist.h"
using namespace TagLib;
@@ -48,6 +50,8 @@ public:
std::unique_ptr<MP4::Tag> tag;
std::unique_ptr<MP4::Atoms> atoms;
std::unique_ptr<MP4::Properties> properties;
std::unique_ptr<MP4::NeroChapterList> neroChapterList;
std::unique_ptr<MP4::QtChapterList> qtChapterList;
};
////////////////////////////////////////////////////////////////////////////////
@@ -111,6 +115,26 @@ MP4::Properties *MP4::File::audioProperties() const
return d->properties.get();
}
MP4::ChapterList MP4::File::neroChapters()
{
return getChaptersLazy(d->neroChapterList, this);
}
void MP4::File::setNeroChapters(const ChapterList& chapters)
{
setChaptersLazy(d->neroChapterList, chapters);
}
MP4::ChapterList MP4::File::qtChapters()
{
return getChaptersLazy(d->qtChapterList, this);
}
void MP4::File::setQtChapters(const ChapterList& chapters)
{
setChaptersLazy(d->qtChapterList, chapters);
}
void
MP4::File::read(bool readProperties)
{
@@ -148,7 +172,9 @@ MP4::File::save()
return false;
}
return d->tag->save();
return d->tag->save() &&
saveChaptersIfModified(d->neroChapterList, this) &&
saveChaptersIfModified(d->qtChapterList, this);
}
bool

View File

@@ -31,6 +31,7 @@
#include "mp4tag.h"
#include "tag.h"
#include "mp4properties.h"
#include "mp4chapter.h"
namespace TagLib {
//! An implementation of MP4 (AAC, ALAC, ...) metadata
@@ -130,6 +131,26 @@ namespace TagLib {
*/
Properties *audioProperties() const override;
/*!
* Returns the Nero style chapters for this file.
*/
ChapterList neroChapters();
/*!
* Sets the Nero style chapters for this file.
*/
void setNeroChapters(const ChapterList &chapters);
/*!
* Returns the QuickTime chapters for this file.
*/
ChapterList qtChapters();
/*!
* Sets the QuickTime style chapters for this file.
*/
void setQtChapters(const ChapterList &chapters);
/*!
* Save the file.
*

View File

@@ -44,6 +44,7 @@ public:
StringList m_stringList;
ByteVectorList m_byteVectorList;
MP4::CoverArtList m_coverArtList;
MP4::Stem m_stem;
};
MP4::Item::Item() :
@@ -130,6 +131,13 @@ MP4::Item::Item(const MP4::CoverArtList &value) :
d->m_coverArtList = value;
}
MP4::Item::Item(const MP4::Stem &value) :
d(std::make_shared<ItemPrivate>())
{
d->type = Type::Stem;
d->m_stem = value;
}
void MP4::Item::setAtomDataType(MP4::AtomDataType type)
{
d->atomDataType = type;
@@ -194,6 +202,12 @@ MP4::Item::toCoverArtList() const
return d->m_coverArtList;
}
MP4::Stem
MP4::Item::toStem() const
{
return d->m_stem;
}
bool
MP4::Item::isValid() const
{
@@ -234,6 +248,8 @@ bool MP4::Item::operator==(const Item &other) const
return toByteVectorList() == other.toByteVectorList();
case Type::CoverArtList:
return toCoverArtList() == other.toCoverArtList();
case Type::Stem:
return toStem() == other.toStem();
}
}
return false;

View File

@@ -29,6 +29,7 @@
#include "tstringlist.h"
#include "taglib_export.h"
#include "mp4coverart.h"
#include "mp4stem.h"
namespace TagLib {
namespace MP4 {
@@ -49,7 +50,8 @@ namespace TagLib {
LongLong,
StringList,
ByteVectorList,
CoverArtList
CoverArtList,
Stem,
};
struct IntPair {
@@ -80,6 +82,7 @@ namespace TagLib {
Item(const StringList &value);
Item(const ByteVectorList &value);
Item(const CoverArtList &value);
Item(const Stem &value);
void setAtomDataType(AtomDataType type);
AtomDataType atomDataType() const;
@@ -93,6 +96,7 @@ namespace TagLib {
StringList toStringList() const;
ByteVectorList toByteVectorList() const;
CoverArtList toCoverArtList() const;
Stem toStem() const;
bool isValid() const;

View File

@@ -25,6 +25,7 @@
#include "mp4itemfactory.h"
#include <mutex>
#include <utility>
#include "tbytevector.h"
@@ -47,6 +48,8 @@ public:
NameHandlerMap handlerTypeForName;
Map<ByteVector, String> propertyKeyForName;
Map<String, ByteVector> nameForPropertyKey;
mutable std::once_flag handlerMapOnce;
mutable std::once_flag propertyMapsOnce;
};
ItemFactory ItemFactory::factory;
@@ -87,6 +90,8 @@ std::pair<String, Item> ItemFactory::parseItem(
return parseGnre(atom, data);
case ItemHandlerType::Covr:
return parseCovr(atom, data);
case ItemHandlerType::Stem:
return parseStem(atom, data);
case ItemHandlerType::TextImplicit:
return parseText(atom, data, -1);
case ItemHandlerType::Text:
@@ -128,6 +133,8 @@ ByteVector ItemFactory::renderItem(
return renderInt(name, item);
case ItemHandlerType::Covr:
return renderCovr(name, item);
case ItemHandlerType::Stem:
return renderStem(name, item);
case ItemHandlerType::TextImplicit:
return renderText(name, item, TypeImplicit);
case ItemHandlerType::Text:
@@ -175,8 +182,8 @@ std::pair<ByteVector, Item> ItemFactory::itemFromProperty(
case ItemHandlerType::TextImplicit:
case ItemHandlerType::Text:
return {name, values};
case ItemHandlerType::Covr:
case ItemHandlerType::Stem:
debug("MP4: Invalid item \"" + name + "\" for property");
break;
case ItemHandlerType::Unknown:
@@ -222,6 +229,7 @@ std::pair<String, StringList> ItemFactory::itemToProperty(
return {key, item.toStringList()};
case ItemHandlerType::Covr:
case ItemHandlerType::Stem:
debug("MP4: Invalid item \"" + itemName + "\" for property");
break;
case ItemHandlerType::Unknown:
@@ -234,9 +242,11 @@ std::pair<String, StringList> ItemFactory::itemToProperty(
String ItemFactory::propertyKeyForName(const ByteVector &name) const
{
if(d->propertyKeyForName.isEmpty()) {
std::call_once(d->propertyMapsOnce, [this] {
d->propertyKeyForName = namePropertyMap();
}
for(const auto &[k, t] : std::as_const(d->propertyKeyForName))
d->nameForPropertyKey[t] = k;
});
String key = d->propertyKeyForName.value(name);
if(key.isEmpty() && name.startsWith(freeFormPrefix)) {
key = name.mid(std::size(freeFormPrefix) - 1);
@@ -246,14 +256,11 @@ String ItemFactory::propertyKeyForName(const ByteVector &name) const
ByteVector ItemFactory::nameForPropertyKey(const String &key) const
{
if(d->nameForPropertyKey.isEmpty()) {
if(d->propertyKeyForName.isEmpty()) {
d->propertyKeyForName = namePropertyMap();
}
for(const auto &[k, t] : std::as_const(d->propertyKeyForName)) {
std::call_once(d->propertyMapsOnce, [this] {
d->propertyKeyForName = namePropertyMap();
for(const auto &[k, t] : std::as_const(d->propertyKeyForName))
d->nameForPropertyKey[t] = k;
}
}
});
ByteVector name = d->nameForPropertyKey.value(key);
if(name.isEmpty() && !key.isEmpty()) {
const auto &firstChar = key[0];
@@ -303,6 +310,7 @@ ItemFactory::NameHandlerMap ItemFactory::nameHandlerMap() const
{"akID", ItemHandlerType::Byte},
{"gnre", ItemHandlerType::Gnre},
{"covr", ItemHandlerType::Covr},
{"stem", ItemHandlerType::Stem},
{"purl", ItemHandlerType::TextImplicit},
{"egid", ItemHandlerType::TextImplicit},
};
@@ -311,9 +319,9 @@ ItemFactory::NameHandlerMap ItemFactory::nameHandlerMap() const
ItemFactory::ItemHandlerType ItemFactory::handlerTypeForName(
const ByteVector &name) const
{
if(d->handlerTypeForName.isEmpty()) {
std::call_once(d->handlerMapOnce, [this] {
d->handlerTypeForName = nameHandlerMap();
}
});
auto type = d->handlerTypeForName.value(name, ItemHandlerType::Unknown);
if (type == ItemHandlerType::Unknown && name.size() == 4) {
type = ItemHandlerType::Text;
@@ -633,6 +641,12 @@ std::pair<String, Item> ItemFactory::parseCovr(
};
}
std::pair<String, Item> ItemFactory::parseStem(
const MP4::Atom *atom, const ByteVector &data)
{
return {atom->name(), Item(Stem(data))};
}
ByteVector ItemFactory::renderAtom(
const ByteVector &name, const ByteVector &data)
@@ -742,6 +756,13 @@ ByteVector ItemFactory::renderCovr(
return renderAtom(name, data);
}
ByteVector ItemFactory::renderStem(
const ByteVector &name, const MP4::Item &item)
{
auto data = item.toStem().data();
return renderAtom(name, data);
}
ByteVector ItemFactory::renderFreeForm(
const String &name, const MP4::Item &item)
{

View File

@@ -142,6 +142,7 @@ namespace TagLib {
Byte,
Gnre,
Covr,
Stem,
TextImplicit,
Text
};
@@ -214,6 +215,8 @@ namespace TagLib {
const MP4::Atom *atom, const ByteVector &bytes);
static std::pair<String, Item> parseCovr(
const MP4::Atom *atom, const ByteVector &data);
static std::pair<String, Item> parseStem(
const MP4::Atom *atom, const ByteVector &data);
// Functions used by renderItem() to render atom data for items.
static ByteVector renderAtom(
@@ -242,6 +245,8 @@ namespace TagLib {
const ByteVector &name, const MP4::Item &item);
static ByteVector renderCovr(
const ByteVector &name, const MP4::Item &item);
static ByteVector renderStem(
const ByteVector &name, const MP4::Item &item);
private:
static ItemFactory factory;

View File

@@ -0,0 +1,320 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "mp4nerochapterlist.h"
#include <algorithm>
#include "tdebug.h"
#include "mp4file.h"
#include "mp4atom.h"
using namespace TagLib;
namespace
{
ByteVector renderAtom(const ByteVector &name, const ByteVector &data)
{
return ByteVector::fromUInt(data.size() + 8) + name + data;
}
// Update parent atom sizes along a path when child size changes by delta.
// Mirrors MP4::Tag::updateParents().
void updateParentSizes(TagLib::File *file, const MP4::AtomList &path,
offset_t delta, int ignore = 0)
{
if(static_cast<int>(path.size()) <= ignore)
return;
auto itEnd = path.end();
std::advance(itEnd, 0 - ignore);
for(auto it = path.begin(); it != itEnd; ++it) {
file->seek((*it)->offset());
if(const long size = file->readBlock(4).toUInt(); size == 1) {
// 64-bit size
file->seek(4, TagLib::File::Current);
const long long longSize = file->readBlock(8).toLongLong();
file->seek((*it)->offset() + 8);
file->writeBlock(ByteVector::fromLongLong(longSize + delta));
}
else {
// 32-bit size
file->seek((*it)->offset());
file->writeBlock(ByteVector::fromUInt(static_cast<unsigned int>(size + delta)));
}
}
}
// Update stco/co64/tfhd chunk offsets when file content shifts.
// Mirrors MP4::Tag::updateOffsets().
void updateChunkOffsets(TagLib::File *file, const MP4::Atoms *atoms,
offset_t delta, offset_t offset)
{
if(const MP4::Atom *moov = atoms->find("moov")) {
const MP4::AtomList stco = moov->findall("stco", true);
for(const auto &atom : stco) {
if(atom->offset() > offset)
atom->addToOffset(delta);
file->seek(atom->offset() + 12);
ByteVector data = file->readBlock(atom->length() - 12);
unsigned int count = data.toUInt();
file->seek(atom->offset() + 16);
unsigned int pos = 4;
const unsigned int maxPos = data.size() - 4;
while(count-- && pos <= maxPos) {
auto o = static_cast<offset_t>(data.toUInt(pos));
if(o > offset)
o += delta;
file->writeBlock(ByteVector::fromUInt(static_cast<unsigned int>(o)));
pos += 4;
}
}
const MP4::AtomList co64 = moov->findall("co64", true);
for(const auto &atom : co64) {
if(atom->offset() > offset)
atom->addToOffset(delta);
file->seek(atom->offset() + 12);
ByteVector data = file->readBlock(atom->length() - 12);
unsigned int count = data.toUInt();
file->seek(atom->offset() + 16);
unsigned int pos = 4;
const unsigned int maxPos = data.size() - 8;
while(count-- && pos <= maxPos) {
long long o = data.toLongLong(pos);
if(o > offset)
o += delta;
file->writeBlock(ByteVector::fromLongLong(o));
pos += 8;
}
}
}
if(const MP4::Atom *moof = atoms->find("moof")) {
const MP4::AtomList tfhd = moof->findall("tfhd", true);
for(const auto &atom : tfhd) {
if(atom->offset() > offset)
atom->addToOffset(delta);
file->seek(atom->offset() + 9);
ByteVector data = file->readBlock(atom->length() - 9);
if(const unsigned int flags = data.toUInt(0, 3, true);
flags & 1) {
long long o = data.toLongLong(7U);
if(o > offset)
o += delta;
file->seek(atom->offset() + 16);
file->writeBlock(ByteVector::fromLongLong(o));
}
}
}
}
// Build the binary payload for a chpl atom (version 1).
ByteVector renderChplData(const MP4::ChapterList &chapters)
{
const unsigned int count = std::min(chapters.size(), 255U);
ByteVector data;
// Version (1 byte) + flags (3 bytes) + reserved (4 bytes)
data.append(static_cast<char>(0x01)); // version 1
data.append(ByteVector(3, '\0')); // flags
data.append(ByteVector(4, '\0')); // reserved
// Chapter count
data.append(static_cast<char>(count & 0xFF));
unsigned int i = 0;
for(const auto &ch : chapters) {
if(i++ >= count)
break;
// Start time: 8 bytes big-endian, on-disk format is 100-nanosecond units
data.append(ByteVector::fromLongLong(ch.startTime() * 10000LL));
// Title: 1-byte length + UTF-8 bytes (max 255 bytes)
ByteVector titleBytes = ch.title().data(String::UTF8);
const unsigned int titleLen = std::min(titleBytes.size(), 255U);
data.append(static_cast<char>(titleLen & 0xFF));
if(titleLen > 0)
data.append(titleBytes.mid(0, titleLen));
}
return data;
}
// Parse the binary content of a chpl atom into a ChapterList.
MP4::ChapterList parseChplData(const ByteVector &data)
{
MP4::ChapterList chapters;
// Minimum: version(1) + flags(3) + count(1) = 5 bytes (version 0 layout)
if(data.size() < 5)
return chapters;
unsigned int pos = 0;
const auto version = static_cast<unsigned char>(data[pos++]);
// Skip flags (3 bytes)
pos += 3;
// Version 1 has 4 reserved bytes
if(version >= 1)
pos += 4;
if(pos >= data.size())
return chapters;
const unsigned int count = static_cast<unsigned char>(data[pos++]);
for(unsigned int i = 0; i < count && pos + 9 <= data.size(); ++i) {
const long long startTime100ns = data.toLongLong(pos);
pos += 8;
const unsigned int titleLen = static_cast<unsigned char>(data[pos++]);
String title;
if(titleLen > 0 && pos + titleLen <= data.size()) {
title = String(data.mid(pos, titleLen), String::UTF8);
pos += titleLen;
}
chapters.append(MP4::Chapter(title, startTime100ns / 10000LL));
}
return chapters;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
bool MP4::NeroChapterList::read(TagLib::File *file)
{
const Atoms atoms(file);
const Atom *chpl = atoms.find("moov", "udta", "chpl");
modified = false;
chapterList.clear();
if(chpl) {
// Read the atom content (skip 8-byte atom header)
file->seek(chpl->offset() + 8);
const ByteVector data = file->readBlock(chpl->length() - 8);
chapterList = parseChplData(data);
return true;
}
return false;
}
bool MP4::NeroChapterList::write(TagLib::File *file)
{
// Writing an empty list is equivalent to removing the chapters.
if(chapterList.isEmpty())
return remove(file);
const Atoms atoms(file);
if(!atoms.find("moov")) {
debug("MP4ChapterList::write() -- No moov atom found");
return false;
}
const ByteVector chplPayload = renderChplData(chapterList);
const ByteVector chplAtom = renderAtom("chpl", chplPayload);
if(const Atom *existingChpl = atoms.find("moov", "udta", "chpl")) {
// Replace existing chpl atom
const offset_t offset = existingChpl->offset();
const offset_t oldLength = existingChpl->length();
const offset_t delta = static_cast<offset_t>(chplAtom.size()) - oldLength;
file->insert(chplAtom, offset, oldLength);
if(delta != 0) {
// Update parent sizes: moov and udta
const AtomList parentPath = atoms.path("moov", "udta", "chpl");
updateParentSizes(file, parentPath, delta, 1); // ignore chpl itself
updateChunkOffsets(file, &atoms, delta, offset);
}
}
else {
// Need to insert a new chpl atom
if(AtomList udtaPath = atoms.path("moov", "udta"); udtaPath.size() == 2) {
// udta exists -- insert chpl at the beginning of udta's content
const offset_t insertOffset = udtaPath.back()->offset() + 8;
file->insert(chplAtom, insertOffset, 0);
updateParentSizes(file, udtaPath, chplAtom.size());
updateChunkOffsets(file, &atoms, chplAtom.size(), insertOffset);
}
else {
// No udta -- insert udta + chpl at the beginning of moov's content
const ByteVector udtaAtom = renderAtom("udta", chplAtom);
AtomList moovPath = atoms.path("moov");
if(moovPath.isEmpty()) {
debug("MP4ChapterList::write() -- No moov atom in path");
return false;
}
const offset_t insertOffset = moovPath.back()->offset() + 8;
file->insert(udtaAtom, insertOffset, 0);
updateParentSizes(file, moovPath, udtaAtom.size());
updateChunkOffsets(file, &atoms, udtaAtom.size(), insertOffset);
}
}
modified = false;
return true;
}
bool MP4::NeroChapterList::remove(TagLib::File *file)
{
const Atoms atoms(file);
chapterList.clear();
modified = false;
const Atom *chpl = atoms.find("moov", "udta", "chpl");
if(!chpl) {
// No chpl atom -- nothing to remove
return true;
}
const offset_t offset = chpl->offset();
const offset_t length = chpl->length();
file->removeBlock(offset, length);
// Update parent sizes with negative delta
const AtomList parentPath = atoms.path("moov", "udta", "chpl");
updateParentSizes(file, parentPath, -length, 1); // ignore chpl itself
updateChunkOffsets(file, &atoms, -length, offset);
return true;
}

View File

@@ -0,0 +1,66 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MP4CHAPTERLIST_H
#define TAGLIB_MP4CHAPTERLIST_H
#include "mp4chapterholder.h"
namespace TagLib {
class File;
namespace MP4 {
/*!
* Reads, writes, and removes Nero-style chapter markers (chpl atom)
* from MP4 files. Operates independently of MP4::Tag -- the chpl atom
* lives at moov/udta/chpl, a sibling of the metadata ilst path.
*/
class NeroChapterList : public ChapterHolder
{
public:
/*!
* Reads chapter markers from the already-opened \a file.
* Returns \c false if the file has no chpl atom.
*/
bool read(TagLib::File *file);
/*!
* Writes chapter markers to the already-opened \a file,
* replacing any existing chpl atom.
* The chapter count is capped at 255 (Nero format limit).
* Returns \c true on success.
*/
bool write(TagLib::File *file);
/*!
* Removes the chpl atom from the already-opened \a file.
* Returns \c true on success, or if no chpl atom exists.
*/
bool remove(TagLib::File *file);
};
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -224,7 +224,7 @@ MP4::Properties::read(File *file, const Atoms *atoms)
pos += 10;
if(const unsigned int bitrateValue = data.toUInt(pos);
bitrateValue != 0 || d->length <= 0) {
d->bitrate = static_cast<int>((bitrateValue + 500) / 1000.0 + 0.5);
d->bitrate = static_cast<int>(bitrateValue / 1000.0 + 0.5);
}
else {
d->bitrate = static_cast<int>(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MP4QTCHAPTERLIST_H
#define TAGLIB_MP4QTCHAPTERLIST_H
#include "mp4chapterholder.h"
namespace TagLib {
class File;
namespace MP4 {
/*!
* Reads, writes, and removes QuickTime-style chapter tracks from MP4
* files. A QT chapter track is a disabled text track (\c hdlr type
* \c "text") referenced by a \c chap track-reference in the audio
* track's \c tref box. This format is understood by QuickTime,
* iTunes, Final Cut, Logic, DaVinci Resolve, Twisted Wave, and most
* other Apple/macOS software.
*
* The existing \c MP4ChapterList class handles Nero-style \c chpl
* atoms, which are a different (and less widely supported) chapter
* format.
*
* Chapter times use the same 100-nanosecond unit convention as
* \c MP4ChapterList so that existing \c Chapter / \c ChapterList
* types can be shared.
*/
class QtChapterList : public ChapterHolder
{
public:
/*!
* Reads chapter markers from the QuickTime chapter track in the
* already-opened \a file.
* Returns \c false if the file has no chapter track.
*/
bool read(TagLib::File *file);
/*!
* Writes chapter markers as a QuickTime chapter track to the
* already-opened \a file, replacing any existing chapter track.
* Returns \c true on success.
*/
bool write(TagLib::File *file);
/*!
* Removes the QuickTime chapter track and its \c tref/chap
* reference from the already-opened \a file.
* Returns \c true on success, or if no chapter track exists.
*/
bool remove(TagLib::File *file);
};
} // namespace MP4
} // namespace TagLib
#endif

64
taglib/mp4/mp4stem.cpp Normal file
View File

@@ -0,0 +1,64 @@
/**************************************************************************
copyright : (C) 2026 by Antoine Colombier
email : antoine@mixxx.org
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "mp4stem.h"
using namespace TagLib;
MP4::Stem::Stem(const ByteVector &data) :
d(std::make_shared<StemPrivate>())
{
d->data = data;
}
MP4::Stem::Stem() = default;
MP4::Stem::Stem(const Stem &) = default;
MP4::Stem &MP4::Stem::operator=(const Stem &) = default;
void
MP4::Stem::swap(Stem &item) noexcept
{
using std::swap;
swap(d, item.d);
}
MP4::Stem::~Stem() = default;
ByteVector
MP4::Stem::data() const
{
return d->data;
}
bool MP4::Stem::operator==(const Stem &other) const
{
return data() == other.data();
}
bool MP4::Stem::operator!=(const Stem &other) const
{
return !(*this == other);
}

78
taglib/mp4/mp4stem.h Normal file
View File

@@ -0,0 +1,78 @@
/**************************************************************************
copyright : (C) 2026 by Antoine Colombier
email : antoine@mixxx.org
**************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#ifndef TAGLIB_MP4STEM_H
#define TAGLIB_MP4STEM_H
#include "tbytevector.h"
#include "taglib_export.h"
namespace TagLib::MP4 {
//! STEM
class StemPrivate
{
public:
ByteVector data;
};
class TAGLIB_EXPORT Stem
{
public:
Stem();
explicit Stem(const ByteVector &data);
~Stem();
Stem(const Stem &item);
/*!
* Copies the contents of \a item into this Stem.
*/
Stem &operator=(const Stem &item);
/*!
* Exchanges the content of the Stem with the content of \a item.
*/
void swap(Stem &item) noexcept;
//! The Stem data
ByteVector data() const;
/*!
* Returns \c true if the Stem and \a other contain the same data.
*/
bool operator==(const Stem &other) const;
/*!
* Returns \c true if the Stem and \a other have different data.
*/
bool operator!=(const Stem &other) const;
private:
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::shared_ptr<StemPrivate> d;
};
} // namespace TagLib::MP4
#endif

View File

@@ -32,6 +32,7 @@
#include "mp4itemfactory.h"
#include "mp4atom.h"
#include "mp4coverart.h"
#include "mp4stem.h"
using namespace TagLib;
@@ -65,15 +66,22 @@ MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms,
d->atoms = atoms;
const MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst");
if(!ilst) {
//debug("Atom moov.udta.meta.ilst not found.");
return;
if(ilst) {
for(const auto &atom : ilst->children()) {
file->seek(atom->offset() + 8);
ByteVector data = d->file->readBlock(atom->length() - 8);
if(const auto &[name, itm] = d->factory->parseItem(atom, data);
itm.isValid()) {
addItem(name, itm);
}
}
}
for(const auto &atom : ilst->children()) {
file->seek(atom->offset() + 8);
ByteVector data = d->file->readBlock(atom->length() - 8);
if(const auto &[name, itm] = d->factory->parseItem(atom, data);
const MP4::Atom *stem = atoms->find("moov", "udta", "stem");
if(stem) {
file->seek(stem->offset() + 8);
ByteVector data = d->file->readBlock(stem->length() - 8);
if(const auto &[name, itm] = d->factory->parseItem(stem, data);
itm.isValid()) {
addItem(name, itm);
}
@@ -100,18 +108,33 @@ MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data) const
bool
MP4::Tag::save()
{
ByteVector data;
ByteVector ilstData, stemData;
for(const auto &[name, itm] : std::as_const(d->items)) {
data.append(d->factory->renderItem(name, itm));
if(name == "stem"){
stemData.append(d->factory->renderItem(name, itm));
} else {
ilstData.append(d->factory->renderItem(name, itm));
}
}
data = renderAtom("ilst", data);
ilstData = renderAtom("ilst", ilstData);
AtomList path = d->atoms->path("moov", "udta", "meta", "ilst");
if(path.size() == 4) {
saveExisting(data, path);
saveExisting(ilstData, path);
}
else {
saveNew(data);
ByteVector metaData = renderAtom("meta", ByteVector(4, '\0') +
renderAtom("hdlr", ByteVector(8, '\0') + ByteVector("mdirappl") +
ByteVector(9, '\0')) +
ilstData + padIlst(ilstData));
saveNew(metaData);
}
path = d->atoms->path("moov", "udta", "stem");
if(path.size() == 3) {
saveExisting(stemData, path);
} else if (!stemData.isEmpty()) {
saveNew(stemData);
}
return true;
@@ -127,6 +150,11 @@ MP4::Tag::strip()
saveExisting(ByteVector(), path);
}
path = d->atoms->path("moov", "udta", "stem");
if(path.size() == 3) {
saveExisting(ByteVector(), path);
}
return true;
}
@@ -172,7 +200,8 @@ MP4::Tag::updateOffsets(offset_t delta, offset_t offset)
unsigned int count = data.toUInt();
d->file->seek(atom->offset() + 16);
unsigned int pos = 4;
while(count--) {
const unsigned int maxPos = data.size() - 4;
while(count-- && pos <= maxPos) {
auto o = static_cast<offset_t>(data.toUInt(pos));
if(o > offset) {
o += delta;
@@ -192,7 +221,8 @@ MP4::Tag::updateOffsets(offset_t delta, offset_t offset)
unsigned int count = data.toUInt();
d->file->seek(atom->offset() + 16);
unsigned int pos = 4;
while(count--) {
const unsigned int maxPos = data.size() - 8;
while(count-- && pos <= maxPos) {
long long o = data.toLongLong(pos);
if(o > offset) {
o += delta;
@@ -227,11 +257,6 @@ MP4::Tag::updateOffsets(offset_t delta, offset_t offset)
void
MP4::Tag::saveNew(ByteVector data)
{
data = renderAtom("meta", ByteVector(4, '\0') +
renderAtom("hdlr", ByteVector(8, '\0') + ByteVector("mdirappl") +
ByteVector(9, '\0')) +
data + padIlst(data));
AtomList path = d->atoms->path("moov", "udta");
if(path.size() != 2) {
path = d->atoms->path("moov");
@@ -510,13 +535,17 @@ StringList MP4::Tag::complexPropertyKeys() const
if(d->items.contains("covr")) {
keys.append("PICTURE");
}
if(d->items.contains("stem")) {
keys.append("STEM");
}
return keys;
}
List<VariantMap> MP4::Tag::complexProperties(const String &key) const
{
List<VariantMap> props;
if(const String uppercaseKey = key.upper(); uppercaseKey == "PICTURE") {
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
const CoverArtList pictures = d->items.value("covr").toCoverArtList();
for(const CoverArt &picture : pictures) {
String mimeType = "image/";
@@ -543,12 +572,20 @@ List<VariantMap> MP4::Tag::complexProperties(const String &key) const
props.append(property);
}
}
else if(uppercaseKey == "STEM" && d->items.contains("stem")) {
const Stem stem = d->items.value("stem").toStem();
VariantMap property;
property.insert("manifest", stem.data());
props.append(property);
}
return props;
}
bool MP4::Tag::setComplexProperties(const String &key, const List<VariantMap> &value)
{
if(const String uppercaseKey = key.upper(); uppercaseKey == "PICTURE") {
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
CoverArtList pictures;
for(const auto &property : value) {
auto mimeType = property.value("mimeType").value<String>();
@@ -568,6 +605,13 @@ bool MP4::Tag::setComplexProperties(const String &key, const List<VariantMap> &v
}
d->items["covr"] = pictures;
}
else if(uppercaseKey == "STEM") {
if (!value.isEmpty()) {
d->items["stem"] = Stem(value.front().value("manifest").value<ByteVector>());
} else {
d->items.erase("stem");
}
}
else {
return false;
}

Some files were not shown because too many files have changed in this diff Show More