Skip to content

Commit 0bc2c57

Browse files
committed
🎶 QuickTime audio stsd parsing
Support QuickTime-specific audio stsd fields.
1 parent 2568f55 commit 0bc2c57

File tree

7 files changed

+58
-34
lines changed

7 files changed

+58
-34
lines changed

boxes.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,24 @@ def fmt(color_16bit: int):
132132
def parse_audio_sample_entry_contents(btype: str, ps: Parser, version: int):
133133
assert version <= 1, 'invalid version'
134134

135-
if version == 0:
136-
ps.reserved('reserved_1_2', ps.bytes(2))
137-
else:
138-
ps.reserved('entry_version', ps.int(2), 1)
139-
ps.reserved('reserved_1', ps.bytes(6))
135+
ps.field('entry_version', entry_version := ps.int(2), default=version)
136+
if entry_version >= 2:
137+
raise AssertionError(f'Unsupported audio sample entry version: {entry_version}')
138+
ps.field('revision_level', ps.int(2), default=0)
139+
ps.field('vendor', ps.int(4), default=0)
140140

141141
ps.field('channelcount', ps.int(2), default=(2 if version == 0 else None))
142142
ps.field('samplesize', ps.int(2), default=16)
143-
ps.reserved('pre_defined_1', ps.int(2))
144-
ps.reserved('reserved_2', ps.int(2))
143+
ps.field('compression_id', ps.sint(2), default=0)
144+
ps.field('packet_size', ps.int(2), default=0)
145145
ps.field('samplerate', ps.fixed16(), default=(None if version == 0 else 1))
146146

147+
if entry_version == 1:
148+
ps.field("samples_per_packet", ps.int(4), default=0)
149+
ps.field("bytes_per_packet", ps.int(4), default=0)
150+
ps.field("bytes_per_frame", ps.int(4), default=0)
151+
ps.field("bytes_per_sample", ps.int(4), default=0)
152+
147153
parse_boxes(ps)
148154

149155
def parse_text_sample_entry_contents(btype: str, ps: Parser, version: int):
@@ -945,6 +951,7 @@ def parse_data_box(ps: Parser):
945951
ps.field_dump('value')
946952

947953
def parse_udta_box(ps: Parser):
954+
parse_boxes(ps)
948955
# From the QuickTime spec:
949956
# > For historical reasons, the data list is optionally terminated by a
950957
# > 32-bit integer set to 0.

descriptors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def parse_DecoderConfigDescriptor_descriptor(ps: Parser):
115115
with ps.bits(4) as br:
116116
ps.field('streamType', br.read(6), describe=format_stream_type)
117117
ps.field('upStream', br.bit())
118-
ps.reserved('reserved', br.read(1), 1)
118+
ps.reserved('reserved', br.read(1), 1) # not always 1, unknown why
119119
ps.field('bufferSizeDB', br.read(24))
120120
ps.field('maxBitrate', ps.int(4))
121121
ps.field('avgBitrate', ps.int(4))

mp4parser.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def parse_box_header(ps: Parser):
376376
start = ps.pos
377377
length = ps.int(4)
378378
btype = ps.fourcc()
379-
assert btype.isprintable(), f'invalid type {repr(btype)}'
379+
assert btype.isprintable() or btype == '\x00\x00\x00\x00', f'invalid type {repr(btype)}'
380380

381381
last_box, large_size = False, False
382382
if length == 0:
@@ -395,7 +395,7 @@ def parse_box_header(ps: Parser):
395395
def parse_boxes(ps: Parser, contents_fn: Optional[Callable[[str, Parser], T]]=None) -> List[T]:
396396
result = []
397397
with ps.in_list():
398-
while not ps.ended:
398+
while ps.remaining > 4: # accept TerminatorBox but specifically avoid reading null trailers here
399399
with ps.in_list_item():
400400
result.append(parse_box(ps, contents_fn or parse_contents))
401401
return result
@@ -415,13 +415,15 @@ def parse_box(ps: Parser, contents_fn: Callable[[str, Parser], T]) -> T:
415415
if large_size:
416416
offset_text = ' (large size)' + offset_text
417417
type_label = btype
418-
if len(btype) != 4: # it's a UUID
418+
if btype == '\x00\x00\x00\x00':
419+
type_label = '00 00 00 00'
420+
elif len(btype) != 4: # it's a UUID
419421
type_label = f'UUID {btype}'
420422
ps.print(ansi_bold(f'[{type_label}]') + name_text + offset_text + length_text, header=True)
421423
with ps.subparser(length) as data, data.handle_errors():
422424
return contents_fn(btype, data)
423425

424-
nesting_boxes = { 'moov', 'trak', 'mdia', 'minf', 'dinf', 'stbl', 'mvex', 'moof', 'traf', 'mfra', 'meco', 'edts', 'udta', 'sinf', 'schi', 'gmhd', 'cmov' }
426+
nesting_boxes = { 'moov', 'trak', 'mdia', 'minf', 'dinf', 'stbl', 'mvex', 'moof', 'traf', 'mfra', 'meco', 'edts', 'udta', 'sinf', 'schi', 'gmhd', 'cmov', 'wave' }
425427
# metadata?
426428
nesting_boxes |= { 'aART', 'trkn', 'covr', '----' }
427429

parser_tables.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@
331331
'gmhd': ('BaseMediaInformationHeaderBox', 'Box'),
332332
'gmin': ('BaseMediaInfoBox', 'Box'),
333333
'ftab': ('FontTableBox', 'Box'),
334+
'twos': ('TwosComplementAudioSampleEntryBox', 'AudioSampleEntry'),
335+
'wave': ('siDecompressionParamBox', 'Box'),
336+
'\x00\x00\x00\x00': ('TerminatorBox', 'Box'),
334337
'cmov': ('CompressedMovieBox', 'Box'),
335338
'dcom': ('DataCompressionBox', 'Box'),
336339
'cmvd': ('CompressedMovieDataBox', 'Box'),

tests/dv84.mov.txt

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
colour_type = 'nclc'
128128
data = @ 0x3722c0 .. 0x3722c6 (6)
129129
00 09 00 12 00 09 ......
130-
ERROR: unexpected EOF (needed 4, got 0)
130+
ERROR: 4 unparsed trailing bytes
131131

132132
00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 ................
133133
00 00 02 00 00 00 02 00 07 80 04 38 00 48 00 00 ...........8.H..
@@ -262,19 +262,35 @@
262262
[stsd] SampleDescription @ 0x372ac1, 0x372ac9 .. 0x372b60 (151)
263263
[mp4a] MP4AudioSampleEntry @ 0x372ad1, 0x372ad9 .. 0x372b60 (135)
264264
data_reference_index = 1
265-
invalid reserved_1_2: b'\x00\x01'
266-
invalid pre_defined_1: 65534
265+
entry_version = 1
266+
compression_id = -2
267267
samplerate = 44100.0
268-
ERROR: invalid type '\x00\x00\x00\x01'
269-
270-
00 00 00 00 00 00 00 01 00 01 00 00 00 00 00 00 ................
271-
00 02 00 10 ff fe 00 00 ac 44 00 00 00 00 04 00 .........D......
272-
00 00 00 01 00 00 00 02 00 00 00 02 00 00 00 5b ...............[
273-
77 61 76 65 00 00 00 0c 66 72 6d 61 6d 70 34 61 wave....frmamp4a
274-
00 00 00 0c 6d 70 34 61 00 00 00 00 00 00 00 33 ....mp4a.......3
275-
65 73 64 73 00 00 00 00 03 80 80 80 22 00 00 00 esds........"...
276-
04 80 80 80 14 40 14 00 18 00 00 02 ee 00 00 00 .....@..........
277-
...
268+
samples_per_packet = 1024
269+
bytes_per_packet = 1
270+
bytes_per_frame = 2
271+
bytes_per_sample = 2
272+
[wave] siDecompressionParam @ 0x372b05, 0x372b0d .. 0x372b60 (83)
273+
[frma] OriginalFormat @ 0x372b0d, 0x372b15 .. 0x372b19 (4)
274+
data_format = 'mp4a'
275+
[mp4a] MP4AudioSampleEntry @ 0x372b19, 0x372b21 .. 0x372b25 (4)
276+
00 00 00 00 ....
277+
[esds] ESD @ 0x372b25, 0x372b2d .. 0x372b58 (43)
278+
[3] ES_Descriptor -> BaseDescriptor (4 length bytes)
279+
ES_ID = 0
280+
streamPriority = 0
281+
[4] DecoderConfigDescriptor -> BaseDescriptor (4 length bytes)
282+
objectTypeIndication = 64 (AAC)
283+
streamType = 5 (AudioStream)
284+
upStream = False
285+
invalid reserved: 0
286+
bufferSizeDB = 6144
287+
maxBitrate = 192000
288+
avgBitrate = 0
289+
[5] DecoderSpecificInfo -> BaseDescriptor (4 length bytes)
290+
12 10 ..
291+
[6] SLConfigDescriptor -> BaseDescriptor (4 length bytes)
292+
predefined = 2 (Reserved for use in MP4 files)
293+
[00 00 00 00] Terminator @ 0x372b58, 0x372b60 .. 0x372b60 (0)
278294
[stts] TimeToSample @ 0x372b60, 0x372b68 .. 0x372b78 (16)
279295
entry_count = 1
280296
[entry 0] [sample = 1, time = 0] sample_count = 146, sample_delta = 1024
@@ -528,7 +544,8 @@
528544
(1016 empty bytes)
529545
[meta] Meta @ 0x373d66, 0x373d6e .. 0x3742c6 (1368)
530546
flags = 000022
531-
ERROR: invalid type '\x00\x00\x00\x00'
547+
[00 00 00 00] Terminator @ 0x373d72, 0x373d7a .. 0x689ba9e4 (1751411818)
548+
ERROR: unexpected EOF (needed 1751411818, got 1356)
532549

533550
00 00 00 22 68 64 6c 72 00 00 00 00 00 00 00 00 ..."hdlr........
534551
6d 64 74 61 00 00 00 00 00 00 00 00 00 00 00 00 mdta............

tests/mangled_pascal_strings.mov.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
00 00 00 01 ....
162162
[stbl] SampleTable @ 0xdb4, 0xdbc .. 0xf1c (352)
163163
[stsd] SampleDescription @ 0xdbc, 0xdc4 .. 0xe00 (60)
164-
[twos] @ 0xdcc, 0xdd4 .. 0xe00 (44)
164+
[twos] TwosComplementAudioSampleEntry @ 0xdcc, 0xdd4 .. 0xe00 (44)
165165
data_reference_index = 1
166166
sample_entry_version = 1
167167
temporal_quality = 65544

tests/quicktime3_sample_smc_pcm_s8.mov.txt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,12 @@
165165
00 00 00 01 ....
166166
[stbl] SampleTable @ 0xdb4, 0xdbc .. 0xf1c (352)
167167
[stsd] SampleDescription @ 0xdbc, 0xdc4 .. 0xe00 (60)
168-
[twos] @ 0xdcc, 0xdd4 .. 0xe00 (44)
168+
[twos] TwosComplementAudioSampleEntry @ 0xdcc, 0xdd4 .. 0xe00 (44)
169169
data_reference_index = 1
170-
invalid reserved_1_2: b'\x00\x01'
170+
entry_version = 1
171171
channelcount = 1
172172
samplesize = 8
173173
samplerate = 11025.0
174-
ERROR: invalid type '\x00\x00\x00\x00'
175-
176-
00 00 00 00 00 00 00 01 00 01 00 00 00 00 00 00 ................
177-
00 01 00 08 00 00 00 00 2b 11 00 00 00 00 00 00 ........+.......
178-
00 00 00 00 00 00 00 00 00 00 00 00 ............
179174
[stts] TimeToSample @ 0xe00, 0xe08 .. 0xe18 (16)
180175
entry_count = 1
181176
[entry 0] [sample = 1, time = 0] sample_count = 68355, sample_delta = 1

0 commit comments

Comments
 (0)