1# -*- coding: utf-8 -*-
2
3import os
4import struct
5import subprocess
6
7from mutagen._compat import cBytesIO, PY3, text_type, PY2, izip
8from tests import TestCase, DATA_DIR, get_temp_copy
9from mutagen.mp4 import (MP4, Atom, Atoms, MP4Tags, MP4Info, delete, MP4Cover,
10                         MP4MetadataError, MP4FreeForm, error, AtomDataType,
11                         AtomError, _item_sort_key, MP4StreamInfoError)
12from mutagen.mp4._util import parse_full_atom
13from mutagen.mp4._as_entry import AudioSampleEntry, ASEntryError
14from mutagen._util import cdata
15
16
17class TAtom(TestCase):
18
19    def test_no_children(self):
20        fileobj = cBytesIO(b"\x00\x00\x00\x08atom")
21        atom = Atom(fileobj)
22        self.failUnlessRaises(KeyError, atom.__getitem__, "test")
23
24    def test_length_1(self):
25        fileobj = cBytesIO(b"\x00\x00\x00\x01atom"
26                           b"\x00\x00\x00\x00\x00\x00\x00\x10" + b"\x00" * 16)
27        atom = Atom(fileobj)
28        self.failUnlessEqual(atom.length, 16)
29        self.failUnlessEqual(atom.datalength, 0)
30
31    def test_length_64bit_less_than_16(self):
32        fileobj = cBytesIO(b"\x00\x00\x00\x01atom"
33                           b"\x00\x00\x00\x00\x00\x00\x00\x08" + b"\x00" * 8)
34        self.assertRaises(AtomError, Atom, fileobj)
35
36    def test_length_less_than_8(self):
37        fileobj = cBytesIO(b"\x00\x00\x00\x02atom")
38        self.assertRaises(AtomError, Atom, fileobj)
39
40    def test_truncated(self):
41        self.assertRaises(AtomError, Atom, cBytesIO(b"\x00"))
42        self.assertRaises(AtomError, Atom, cBytesIO(b"\x00\x00\x00\x01atom"))
43
44    def test_render_too_big(self):
45        class TooBig(bytes):
46            def __len__(self):
47                return 1 << 32
48        data = TooBig(b"test")
49        try:
50            len(data)
51        except OverflowError:
52            # Py_ssize_t is still only 32 bits on this system.
53            self.failUnlessRaises(OverflowError, Atom.render, b"data", data)
54        else:
55            data = Atom.render(b"data", data)
56            self.failUnlessEqual(len(data), 4 + 4 + 8 + 4)
57
58    def test_non_top_level_length_0_is_invalid(self):
59        data = cBytesIO(struct.pack(">I4s", 0, b"whee"))
60        self.assertRaises(AtomError, Atom, data, level=1)
61
62    def test_length_0(self):
63        fileobj = cBytesIO(b"\x00\x00\x00\x00atom" + 40 * b"\x00")
64        atom = Atom(fileobj)
65        self.failUnlessEqual(fileobj.tell(), 48)
66        self.failUnlessEqual(atom.length, 48)
67        self.failUnlessEqual(atom.datalength, 40)
68
69    def test_length_0_container(self):
70        data = cBytesIO(struct.pack(">I4s", 0, b"moov") +
71                        Atom.render(b"data", b"whee"))
72        atom = Atom(data)
73        self.failUnlessEqual(len(atom.children), 1)
74        self.failUnlessEqual(atom.length, 20)
75        self.failUnlessEqual(atom.children[-1].length, 12)
76
77    def test_read(self):
78        payload = 8 * b"\xff"
79        fileobj = cBytesIO(b"\x00\x00\x00\x10atom" + payload)
80        atom = Atom(fileobj)
81        ok, data = atom.read(fileobj)
82        self.assertTrue(ok)
83        self.assertEqual(data, payload)
84
85        payload = 7 * b"\xff"
86        fileobj = cBytesIO(b"\x00\x00\x00\x10atom" + payload)
87        atom = Atom(fileobj)
88        ok, data = atom.read(fileobj)
89        self.assertFalse(ok)
90        self.assertEqual(data, payload)
91
92
93class TAtoms(TestCase):
94    filename = os.path.join(DATA_DIR, "has-tags.m4a")
95
96    def setUp(self):
97        with open(self.filename, "rb") as h:
98            self.atoms = Atoms(h)
99
100    def test_getitem(self):
101        self.failUnless(self.atoms[b"moov"])
102        self.failUnless(self.atoms[b"moov.udta"])
103        self.failUnlessRaises(KeyError, self.atoms.__getitem__, b"whee")
104
105    def test_contains(self):
106        self.failUnless(b"moov" in self.atoms)
107        self.failUnless(b"moov.udta" in self.atoms)
108        self.failUnless(b"whee" not in self.atoms)
109
110    def test_name(self):
111        self.failUnlessEqual(self.atoms.atoms[0].name, b"ftyp")
112
113    def test_children(self):
114        self.failUnless(self.atoms.atoms[2].children)
115
116    def test_no_children(self):
117        self.failUnless(self.atoms.atoms[0].children is None)
118
119    def test_extra_trailing_data(self):
120        data = cBytesIO(Atom.render(b"data", b"whee") + b"\x00\x00")
121        self.failUnless(Atoms(data))
122
123    def test_repr(self):
124        repr(self.atoms)
125
126
127class TMP4Info(TestCase):
128
129    def test_no_soun(self):
130        self.failUnlessRaises(
131            error, self.test_mdhd_version_1, b"vide")
132
133    def test_mdhd_version_1(self, soun=b"soun"):
134        mdhd = Atom.render(b"mdhd", (b"\x01\x00\x00\x00" + b"\x00" * 16 +
135                                     b"\x00\x00\x00\x02" +  # 2 Hz
136                                     b"\x00\x00\x00\x00\x00\x00\x00\x10"))
137        hdlr = Atom.render(b"hdlr", b"\x00" * 8 + soun)
138        mdia = Atom.render(b"mdia", mdhd + hdlr)
139        trak = Atom.render(b"trak", mdia)
140        moov = Atom.render(b"moov", trak)
141        fileobj = cBytesIO(moov)
142        atoms = Atoms(fileobj)
143        info = MP4Info(atoms, fileobj)
144        self.failUnlessEqual(info.length, 8)
145
146    def test_multiple_tracks(self):
147        hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"whee")
148        mdia = Atom.render(b"mdia", hdlr)
149        trak1 = Atom.render(b"trak", mdia)
150        mdhd = Atom.render(b"mdhd", (b"\x01\x00\x00\x00" + b"\x00" * 16 +
151                                     b"\x00\x00\x00\x02" +  # 2 Hz
152                                     b"\x00\x00\x00\x00\x00\x00\x00\x10"))
153        hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"soun")
154        mdia = Atom.render(b"mdia", mdhd + hdlr)
155        trak2 = Atom.render(b"trak", mdia)
156        moov = Atom.render(b"moov", trak1 + trak2)
157        fileobj = cBytesIO(moov)
158        atoms = Atoms(fileobj)
159        info = MP4Info(atoms, fileobj)
160        self.failUnlessEqual(info.length, 8)
161
162    def test_no_tracks(self):
163        moov = Atom.render(b"moov", b"")
164        fileobj = cBytesIO(moov)
165        atoms = Atoms(fileobj)
166        with self.assertRaises(MP4StreamInfoError):
167            MP4Info(atoms, fileobj)
168
169
170class TMP4Tags(TestCase):
171
172    def wrap_ilst(self, data):
173        ilst = Atom.render(b"ilst", data)
174        meta = Atom.render(b"meta", b"\x00" * 4 + ilst)
175        data = Atom.render(b"moov", Atom.render(b"udta", meta))
176        fileobj = cBytesIO(data)
177        return MP4Tags(Atoms(fileobj), fileobj)
178
179    def test_parse_multiple_atoms(self):
180        # while we don't write multiple values as multiple atoms
181        # still read them
182        # https://github.com/quodlibet/mutagen/issues/165
183        data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"foo")
184        grp1 = Atom.render(b"\xa9grp", data)
185        data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"bar")
186        grp2 = Atom.render(b"\xa9grp", data)
187        tags = self.wrap_ilst(grp1 + grp2)
188        self.assertEqual(tags["\xa9grp"], [u"foo", u"bar"])
189
190    def test_purl(self):
191        # purl can have 0 or 1 flags (implicit or utf8)
192        data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"foo")
193        purl = Atom.render(b"purl", data)
194        tags = self.wrap_ilst(purl)
195        self.failUnlessEqual(tags["purl"], ["foo"])
196
197        data = Atom.render(b"data", b"\x00\x00\x00\x00" + b"\x00" * 4 + b"foo")
198        purl = Atom.render(b"purl", data)
199        tags = self.wrap_ilst(purl)
200        self.failUnlessEqual(tags["purl"], ["foo"])
201
202        # invalid flag
203        data = Atom.render(b"data", b"\x00\x00\x00\x03" + b"\x00" * 4 + b"foo")
204        purl = Atom.render(b"purl", data)
205        tags = self.wrap_ilst(purl)
206        self.assertFalse("purl" in tags)
207
208        self.assertTrue("purl" in tags._failed_atoms)
209
210        # invalid utf8
211        data = Atom.render(
212            b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"\xff")
213        purl = Atom.render(b"purl", data)
214        tags = self.wrap_ilst(purl)
215        self.assertFalse("purl" in tags)
216
217    def test_genre(self):
218        data = Atom.render(b"data", b"\x00" * 8 + b"\x00\x01")
219        genre = Atom.render(b"gnre", data)
220        tags = self.wrap_ilst(genre)
221        self.failIf("gnre" in tags)
222        self.failUnlessEqual(tags["\xa9gen"], ["Blues"])
223
224    def test_empty_cpil(self):
225        cpil = Atom.render(b"cpil", Atom.render(b"data", b"\x00" * 8))
226        tags = self.wrap_ilst(cpil)
227        self.assertFalse("cpil" in tags)
228
229    def test_genre_too_big(self):
230        data = Atom.render(b"data", b"\x00" * 8 + b"\x01\x00")
231        genre = Atom.render(b"gnre", data)
232        tags = self.wrap_ilst(genre)
233        self.failIf("gnre" in tags)
234        self.failIf("\xa9gen" in tags)
235
236    def test_strips_unknown_types(self):
237        data = Atom.render(b"data", b"\x00" * 8 + b"whee")
238        foob = Atom.render(b"foob", data)
239        tags = self.wrap_ilst(foob)
240        self.failIf(tags)
241
242    def test_strips_bad_unknown_types(self):
243        data = Atom.render(b"datA", b"\x00" * 8 + b"whee")
244        foob = Atom.render(b"foob", data)
245        tags = self.wrap_ilst(foob)
246        self.failIf(tags)
247
248    def test_bad_covr(self):
249        data = Atom.render(
250            b"foob", b"\x00\x00\x00\x0E" + b"\x00" * 4 + b"whee")
251        covr = Atom.render(b"covr", data)
252        tags = self.wrap_ilst(covr)
253        self.assertFalse(tags)
254
255    def test_covr_blank_format(self):
256        data = Atom.render(
257            b"data", b"\x00\x00\x00\x00" + b"\x00" * 4 + b"whee")
258        covr = Atom.render(b"covr", data)
259        tags = self.wrap_ilst(covr)
260        self.failUnlessEqual(
261            MP4Cover.FORMAT_JPEG, tags["covr"][0].imageformat)
262
263    def test_render_bool(self):
264        self.failUnlessEqual(
265            MP4Tags()._MP4Tags__render_bool('pgap', True),
266            b"\x00\x00\x00\x19pgap\x00\x00\x00\x11data"
267            b"\x00\x00\x00\x15\x00\x00\x00\x00\x01"
268        )
269        self.failUnlessEqual(
270            MP4Tags()._MP4Tags__render_bool('pgap', False),
271            b"\x00\x00\x00\x19pgap\x00\x00\x00\x11data"
272            b"\x00\x00\x00\x15\x00\x00\x00\x00\x00"
273        )
274
275    def test_render_integer_min_size(self):
276        render_int = MP4Tags()._MP4Tags__render_integer
277
278        data = render_int('stik', [42], 1)
279        tags = self.wrap_ilst(data)
280        assert tags['stik'] == [42]
281
282        assert len(render_int('stik', [42], 2)) == len(data) + 1
283        assert len(render_int('stik', [42], 4)) == len(data) + 3
284        assert len(render_int('stik', [42], 8)) == len(data) + 7
285
286    def test_render_text(self):
287        self.failUnlessEqual(
288            MP4Tags()._MP4Tags__render_text(
289                'purl', ['http://foo/bar.xml'], 0),
290            b"\x00\x00\x00*purl\x00\x00\x00\"data\x00\x00\x00\x00\x00\x00"
291            b"\x00\x00http://foo/bar.xml"
292        )
293        self.failUnlessEqual(
294            MP4Tags()._MP4Tags__render_text(
295                'aART', [u'\u0041lbum Artist']),
296            b"\x00\x00\x00$aART\x00\x00\x00\x1cdata\x00\x00\x00\x01\x00\x00"
297            b"\x00\x00\x41lbum Artist"
298        )
299        self.failUnlessEqual(
300            MP4Tags()._MP4Tags__render_text(
301                'aART', [u'Album Artist', u'Whee']),
302            b"\x00\x00\x008aART\x00\x00\x00\x1cdata\x00\x00\x00\x01\x00\x00"
303            b"\x00\x00Album Artist\x00\x00\x00\x14data\x00\x00\x00\x01\x00"
304            b"\x00\x00\x00Whee"
305        )
306
307    def test_render_data(self):
308        self.failUnlessEqual(
309            MP4Tags()._MP4Tags__render_data('aART', 0, 1, [b'whee']),
310            b"\x00\x00\x00\x1caART"
311            b"\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee"
312        )
313        self.failUnlessEqual(
314            MP4Tags()._MP4Tags__render_data('aART', 0, 2, [b'whee', b'wee']),
315            b"\x00\x00\x00/aART"
316            b"\x00\x00\x00\x14data\x00\x00\x00\x02\x00\x00\x00\x00whee"
317            b"\x00\x00\x00\x13data\x00\x00\x00\x02\x00\x00\x00\x00wee"
318        )
319
320    def test_bad_text_data(self):
321        data = Atom.render(b"datA", b"\x00\x00\x00\x01\x00\x00\x00\x00whee")
322        data = Atom.render(b"aART", data)
323        tags = self.wrap_ilst(data)
324        self.assertFalse(tags)
325
326    def test_bad_cprt(self):
327        data = Atom.render(b"cprt", b"\x00\x00\x00#data\x00")
328        tags = self.wrap_ilst(data)
329        self.assertFalse(tags)
330
331    def test_parse_tmpo(self):
332        for d, v in [(b"\x01", 1), (b"\x01\x02", 258),
333                     (b"\x01\x02\x03", 66051), (b"\x01\x02\x03\x04", 16909060),
334                     (b"\x01\x02\x03\x04\x05\x06\x07\x08", 72623859790382856)]:
335            data = Atom.render(
336                b"data", b"\x00\x00\x00\x15" + b"\x00\x00\x00\x00" + d)
337            tmpo = Atom.render(b"tmpo", data)
338            tags = self.wrap_ilst(tmpo)
339            assert tags["tmpo"][0] == v
340
341    def test_write_back_bad_atoms(self):
342        # write a broken atom and try to load it
343        data = Atom.render(b"datA", b"\x00\x00\x00\x01\x00\x00\x00\x00wheeee")
344        data = Atom.render(b"aART", data)
345        tags = self.wrap_ilst(data)
346        self.assertFalse(tags)
347
348        # save it into an existing mp4
349        original = os.path.join(DATA_DIR, "has-tags.m4a")
350        filename = get_temp_copy(original)
351        try:
352            delete(filename)
353
354            # it should still end up in the file
355            tags.save(filename)
356            with open(filename, "rb") as h:
357                self.assertTrue(b"wheeee" in h.read())
358
359            # if we define our own aART throw away the broken one
360            tags["aART"] = ["new"]
361            tags.save(filename)
362            with open(filename, "rb") as h:
363                self.assertFalse(b"wheeee" in h.read())
364
365            # add the broken one back and delete all tags including
366            # the broken one
367            del tags["aART"]
368            tags.save(filename)
369            with open(filename, "rb") as h:
370                self.assertTrue(b"wheeee" in h.read())
371            delete(filename)
372            with open(filename, "rb") as h:
373                self.assertFalse(b"wheeee" in h.read())
374        finally:
375            os.unlink(filename)
376
377    def test_render_freeform(self):
378        data = (
379            b"\x00\x00\x00a----"
380            b"\x00\x00\x00\"mean\x00\x00\x00\x00net.sacredchao.Mutagen"
381            b"\x00\x00\x00\x10name\x00\x00\x00\x00test"
382            b"\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee"
383            b"\x00\x00\x00\x13data\x00\x00\x00\x01\x00\x00\x00\x00wee"
384        )
385
386        key = '----:net.sacredchao.Mutagen:test'
387        self.failUnlessEqual(
388            MP4Tags()._MP4Tags__render_freeform(key, [b'whee', b'wee']), data)
389
390    def test_parse_freeform(self):
391        double_data = (
392            b"\x00\x00\x00a----"
393            b"\x00\x00\x00\"mean\x00\x00\x00\x00net.sacredchao.Mutagen"
394            b"\x00\x00\x00\x10name\x00\x00\x00\x00test"
395            b"\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee"
396            b"\x00\x00\x00\x13data\x00\x00\x00\x01\x00\x00\x00\x00wee"
397        )
398
399        key = '----:net.sacredchao.Mutagen:test'
400        double_atom = \
401            MP4Tags()._MP4Tags__render_freeform(key, [b'whee', b'wee'])
402
403        tags = self.wrap_ilst(double_data)
404        self.assertTrue(key in tags)
405        self.assertEqual(tags[key], [b'whee', b'wee'])
406
407        tags2 = self.wrap_ilst(double_atom)
408        self.assertEqual(tags, tags2)
409
410    def test_multi_freeform(self):
411        # merge multiple freeform tags with the same key
412        mean = Atom.render(b"mean", b"\x00" * 4 + b"net.sacredchao.Mutagen")
413        name = Atom.render(b"name", b"\x00" * 4 + b"foo")
414
415        data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"bar")
416        result = Atom.render(b"----", mean + name + data)
417        data = Atom.render(
418            b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"quux")
419        result += Atom.render(b"----", mean + name + data)
420        tags = self.wrap_ilst(result)
421        values = tags["----:net.sacredchao.Mutagen:foo"]
422        self.assertEqual(values[0], b"bar")
423        self.assertEqual(values[1], b"quux")
424
425    def test_bad_freeform(self):
426        mean = Atom.render(b"mean", b"net.sacredchao.Mutagen")
427        name = Atom.render(b"name", b"empty test key")
428        bad_freeform = Atom.render(b"----", b"\x00" * 4 + mean + name)
429        tags = self.wrap_ilst(bad_freeform)
430        self.assertFalse(tags)
431
432    def test_pprint_non_text_list(self):
433        tags = MP4Tags()
434        tags["tmpo"] = [120, 121]
435        tags["trkn"] = [(1, 2), (3, 4)]
436        tags.pprint()
437
438    def test_freeform_data(self):
439        # https://github.com/quodlibet/mutagen/issues/103
440        key = "----:com.apple.iTunes:Encoding Params"
441        value = (b"vers\x00\x00\x00\x01acbf\x00\x00\x00\x01brat\x00\x01\xf4"
442                 b"\x00cdcv\x00\x01\x05\x04")
443
444        data = (b"\x00\x00\x00\x1cmean\x00\x00\x00\x00com.apple.iTunes\x00\x00"
445                b"\x00\x1bname\x00\x00\x00\x00Encoding Params\x00\x00\x000data"
446                b"\x00\x00\x00\x00\x00\x00\x00\x00vers\x00\x00\x00\x01acbf\x00"
447                b"\x00\x00\x01brat\x00\x01\xf4\x00cdcv\x00\x01\x05\x04")
448
449        tags = self.wrap_ilst(Atom.render(b"----", data))
450        v = tags[key][0]
451        self.failUnlessEqual(v, value)
452        self.failUnlessEqual(v.dataformat, AtomDataType.IMPLICIT)
453
454        data = MP4Tags()._MP4Tags__render_freeform(key, v)
455        v = self.wrap_ilst(data)[key][0]
456        self.failUnlessEqual(v.dataformat, AtomDataType.IMPLICIT)
457
458        data = MP4Tags()._MP4Tags__render_freeform(key, value)
459        v = self.wrap_ilst(data)[key][0]
460        self.failUnlessEqual(v.dataformat, AtomDataType.UTF8)
461
462
463class TMP4(TestCase):
464
465    def setUp(self):
466        self.filename = get_temp_copy(self.original)
467        self.audio = MP4(self.filename)
468
469    def tearDown(self):
470        os.unlink(self.filename)
471
472
473class TMP4Mixin(object):
474
475    def faad(self):
476        if not have_faad:
477            return
478        self.assertEqual(call_faad("-w", self.filename), 0)
479
480    def test_set_inval(self):
481        self.assertRaises(TypeError, self.audio.__setitem__, "\xa9nam", 42)
482
483    def test_score(self):
484        fileobj = open(self.filename, "rb")
485        header = fileobj.read(128)
486        self.failUnless(MP4.score(self.filename, fileobj, header))
487        fileobj.close()
488
489    def test_channels(self):
490        self.failUnlessEqual(self.audio.info.channels, 2)
491
492    def test_sample_rate(self):
493        self.failUnlessEqual(self.audio.info.sample_rate, 44100)
494
495    def test_bits_per_sample(self):
496        self.failUnlessEqual(self.audio.info.bits_per_sample, 16)
497
498    def test_bitrate(self):
499        self.failUnlessEqual(self.audio.info.bitrate, 2914)
500
501    def test_length(self):
502        self.failUnlessAlmostEqual(3.7, self.audio.info.length, 1)
503
504    def test_kind(self):
505        self.assertEqual(self.audio.info.codec, u'mp4a.40.2')
506
507    def test_padding(self):
508        self.audio["\xa9nam"] = u"wheeee" * 10
509        self.audio.save()
510        size1 = os.path.getsize(self.audio.filename)
511        self.audio["\xa9nam"] = u"wheeee" * 11
512        self.audio.save()
513        size2 = os.path.getsize(self.audio.filename)
514        self.failUnless(size1, size2)
515
516    def test_padding_2(self):
517        self.audio["\xa9nam"] = u"wheeee" * 10
518        self.audio.save()
519
520        # Reorder "free" and "ilst" atoms
521        with open(self.audio.filename, "rb+") as fileobj:
522            atoms = Atoms(fileobj)
523            meta = atoms[b"moov", b"udta", b"meta"]
524            meta_length1 = meta.length
525            ilst = meta[b"ilst", ]
526            free = meta[b"free", ]
527            self.failUnlessEqual(ilst.offset + ilst.length, free.offset)
528            fileobj.seek(ilst.offset)
529            ilst_data = fileobj.read(ilst.length)
530            fileobj.seek(free.offset)
531            free_data = fileobj.read(free.length)
532            fileobj.seek(ilst.offset)
533            fileobj.write(free_data + ilst_data)
534
535        with open(self.audio.filename, "rb+") as fileobj:
536            atoms = Atoms(fileobj)
537            meta = atoms[b"moov", b"udta", b"meta"]
538            ilst = meta[b"ilst", ]
539            free = meta[b"free", ]
540            self.failUnlessEqual(free.offset + free.length, ilst.offset)
541
542        # Save the file
543        self.audio["\xa9nam"] = u"wheeee" * 11
544        self.audio.save()
545
546        # Check the order of "free" and "ilst" atoms
547        with open(self.audio.filename, "rb+") as fileobj:
548            atoms = Atoms(fileobj)
549
550        meta = atoms[b"moov", b"udta", b"meta"]
551        ilst = meta[b"ilst", ]
552        free = meta[b"free", ]
553        self.failUnlessEqual(meta.length, meta_length1)
554        self.failUnlessEqual(ilst.offset + ilst.length, free.offset)
555
556    def set_key(self, key, value, result=None, faad=True):
557        self.audio[key] = value
558        self.audio.save()
559        audio = MP4(self.audio.filename)
560        self.failUnless(key in audio)
561        self.failUnlessEqual(audio[key], result or value)
562        if faad:
563            self.faad()
564
565    def test_unicode(self):
566        try:
567            self.set_key('\xa9nam', [b'\xe3\x82\x8a\xe3\x81\x8b'],
568                         result=[u'\u308a\u304b'])
569        except TypeError:
570            if not PY3:
571                raise
572
573    def test_preserve_freeform(self):
574        self.set_key('----:net.sacredchao.Mutagen:test key',
575                     [MP4FreeForm(b'woooo', 142, 42)])
576
577    def test_invalid_text(self):
578        self.assertRaises(
579            TypeError, self.audio.__setitem__, '\xa9nam', [b'\xff'])
580
581    def test_save_text(self):
582        self.set_key('\xa9nam', [u"Some test name"])
583
584    def test_save_texts(self):
585        self.set_key('\xa9nam', [u"Some test name", u"One more name"])
586
587    def test_freeform(self):
588        self.set_key('----:net.sacredchao.Mutagen:test key', [b"whee"])
589
590    def test_freeform_2(self):
591        self.set_key(
592            '----:net.sacredchao.Mutagen:test key', b"whee", [b"whee"])
593
594    def test_freeforms(self):
595        self.set_key(
596            '----:net.sacredchao.Mutagen:test key', [b"whee", b"uhh"])
597
598    def test_freeform_bin(self):
599        self.set_key('----:net.sacredchao.Mutagen:test key', [
600            MP4FreeForm(b'woooo', AtomDataType.UTF8),
601            MP4FreeForm(b'hoooo', AtomDataType.IMPLICIT),
602            MP4FreeForm(b'boooo'),
603        ])
604
605    def test_tracknumber(self):
606        self.set_key('trkn', [(1, 10)])
607        self.set_key('trkn', [(1, 10), (5, 20)], faad=False)
608        self.set_key('trkn', [])
609
610    def test_disk(self):
611        self.set_key('disk', [(18, 0)])
612        self.set_key('disk', [(1, 10), (5, 20)], faad=False)
613        self.set_key('disk', [])
614
615    def test_tracknumber_too_small(self):
616        self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(-1, 0)])
617        self.failUnlessRaises(
618            ValueError, self.set_key, 'trkn', [(2 ** 18, 1)])
619
620    def test_disk_too_small(self):
621        self.failUnlessRaises(ValueError, self.set_key, 'disk', [(-1, 0)])
622        self.failUnlessRaises(
623            ValueError, self.set_key, 'disk', [(2 ** 18, 1)])
624
625    def test_tracknumber_wrong_size(self):
626        self.failUnlessRaises(ValueError, self.set_key, 'trkn', (1,))
627        self.failUnlessRaises(ValueError, self.set_key, 'trkn', (1, 2, 3,))
628        self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(1,)])
629        self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(1, 2, 3,)])
630
631    def test_disk_wrong_size(self):
632        self.failUnlessRaises(ValueError, self.set_key, 'disk', [(1,)])
633        self.failUnlessRaises(ValueError, self.set_key, 'disk', [(1, 2, 3,)])
634
635    def test_tempo(self):
636        self.set_key('tmpo', [150])
637        self.set_key('tmpo', [])
638        self.set_key('tmpo', [0])
639        self.set_key('tmpo', [cdata.int16_min])
640        self.set_key('tmpo', [cdata.int32_min])
641        self.set_key('tmpo', [cdata.int64_min])
642        self.set_key('tmpo', [cdata.int16_max])
643        self.set_key('tmpo', [cdata.int32_max])
644        self.set_key('tmpo', [cdata.int64_max])
645
646    def test_various_int(self):
647        keys = [
648            "stik", "rtng", "plID", "cnID", "geID", "atID", "sfID",
649            "cmID", "akID", "tvsn", "tves",
650        ]
651
652        for key in keys:
653            self.set_key(key, [])
654            self.set_key(key, [0])
655            self.set_key(key, [1])
656            self.set_key(key, [cdata.int64_max])
657
658    def test_movements(self):
659        self.set_key('shwm', [1])
660        self.set_key('\xa9mvc', [42])
661        self.set_key('\xa9mvi', [24])
662        self.set_key('\xa9mvn', [u"movement"])
663        self.set_key('\xa9wrk', [u"work"])
664
665    def test_tempos(self):
666        self.set_key('tmpo', [160, 200], faad=False)
667
668    def test_tempo_invalid(self):
669        for badvalue in [
670                [cdata.int64_max + 1], [cdata.int64_min - 1], 10, "foo"]:
671            self.failUnlessRaises(ValueError, self.set_key, 'tmpo', badvalue)
672
673    def test_compilation(self):
674        self.set_key('cpil', True)
675
676    def test_compilation_false(self):
677        self.set_key('cpil', False)
678
679    def test_gapless(self):
680        self.set_key('pgap', True)
681
682    def test_gapless_false(self):
683        self.set_key('pgap', False)
684
685    def test_podcast(self):
686        self.set_key('pcst', True)
687
688    def test_podcast_false(self):
689        self.set_key('pcst', False)
690
691    def test_cover(self):
692        self.set_key('covr', [b'woooo'])
693
694    def test_cover_png(self):
695        self.set_key('covr', [
696            MP4Cover(b'woooo', MP4Cover.FORMAT_PNG),
697            MP4Cover(b'hoooo', MP4Cover.FORMAT_JPEG),
698        ])
699
700    def test_podcast_url(self):
701        self.set_key('purl', ['http://pdl.warnerbros.com/wbie/'
702                              'justiceleagueheroes/audio/JLH_EA.xml'])
703
704    def test_episode_guid(self):
705        self.set_key('catg', ['falling-star-episode-1'])
706
707    def test_pprint(self):
708        self.failUnless(self.audio.pprint())
709        self.assertTrue(isinstance(self.audio.pprint(), text_type))
710
711    def test_pprint_binary(self):
712        self.audio["covr"] = [b"\x00\xa9garbage"]
713        self.failUnless(self.audio.pprint())
714
715    def test_pprint_pair(self):
716        self.audio["cpil"] = (1, 10)
717        self.failUnless("cpil=(1, 10)" in self.audio.pprint())
718
719    def test_delete(self):
720        self.audio.delete()
721        audio = MP4(self.audio.filename)
722        self.failIf(audio.tags)
723        self.faad()
724
725    def test_module_delete(self):
726        delete(self.filename)
727        audio = MP4(self.audio.filename)
728        self.failIf(audio.tags)
729        self.faad()
730
731    def test_reads_unknown_text(self):
732        self.set_key("foob", [u"A test"])
733
734    def __read_offsets(self, filename):
735        fileobj = open(filename, 'rb')
736        atoms = Atoms(fileobj)
737        moov = atoms[b'moov']
738        samples = []
739        for atom in moov.findall(b'stco', True):
740            fileobj.seek(atom.offset + 12)
741            data = fileobj.read(atom.length - 12)
742            fmt = ">%dI" % cdata.uint_be(data[:4])
743            offsets = struct.unpack(fmt, data[4:])
744            for offset in offsets:
745                fileobj.seek(offset)
746                samples.append(fileobj.read(8))
747        for atom in moov.findall(b'co64', True):
748            fileobj.seek(atom.offset + 12)
749            data = fileobj.read(atom.length - 12)
750            fmt = ">%dQ" % cdata.uint_be(data[:4])
751            offsets = struct.unpack(fmt, data[4:])
752            for offset in offsets:
753                fileobj.seek(offset)
754                samples.append(fileobj.read(8))
755        try:
756            for atom in atoms[b"moof"].findall(b'tfhd', True):
757                data = fileobj.read(atom.length - 9)
758                flags = cdata.uint_be(b"\x00" + data[:3])
759                if flags & 1:
760                    offset = cdata.ulonglong_be(data[7:15])
761                    fileobj.seek(offset)
762                    samples.append(fileobj.read(8))
763        except KeyError:
764            pass
765        fileobj.close()
766        return samples
767
768    def test_update_offsets(self):
769        aa = self.__read_offsets(self.original)
770        self.audio["\xa9nam"] = "wheeeeeeee"
771        self.audio.save()
772        bb = self.__read_offsets(self.filename)
773        for a, b in izip(aa, bb):
774            self.failUnlessEqual(a, b)
775
776    def test_mime(self):
777        self.failUnless("audio/mp4" in self.audio.mime)
778
779    def test_set_init_padding_zero(self):
780        if self.audio.tags is None:
781            self.audio.add_tags()
782        self.audio.save(padding=lambda x: 0)
783        self.assertEqual(MP4(self.audio.filename)._padding, 0)
784
785    def test_set_init_padding_large(self):
786        if self.audio.tags is None:
787            self.audio.add_tags()
788        self.audio.save(padding=lambda x: 5000)
789        self.assertEqual(MP4(self.audio.filename)._padding, 5000)
790
791    def test_set_various_padding(self):
792        if self.audio.tags is None:
793            self.audio.add_tags()
794        for i in [0, 1, 2, 3, 1024, 983, 5000, 0, 1]:
795            self.audio.save(padding=lambda x: i)
796            self.assertEqual(MP4(self.audio.filename)._padding, i)
797            self.faad()
798
799
800class TMP4HasTagsMixin(TMP4Mixin):
801    def test_save_simple(self):
802        self.audio.save()
803        self.faad()
804
805    def test_shrink(self):
806        self.audio.clear()
807        self.audio.save()
808        audio = MP4(self.audio.filename)
809        self.failIf(audio.tags)
810
811    def test_too_short(self):
812        fileobj = open(self.audio.filename, "rb")
813        try:
814            atoms = Atoms(fileobj)
815            ilst = atoms[b"moov.udta.meta.ilst"]
816            # fake a too long atom length
817            ilst.children[0].length += 10000000
818            self.failUnlessRaises(MP4MetadataError, MP4Tags, atoms, fileobj)
819        finally:
820            fileobj.close()
821
822    def test_has_tags(self):
823        self.failUnless(self.audio.tags)
824
825    def test_not_my_file(self):
826        # should raise something like "Not a MP4 file"
827        self.failUnlessRaisesRegexp(
828            error, "MP4", MP4, os.path.join(DATA_DIR, "empty.ogg"))
829
830    def test_delete_remove_padding(self):
831        self.audio.clear()
832        self.audio.tags['foob'] = u"foo"
833        self.audio.save(padding=lambda x: 0)
834        filesize = os.path.getsize(self.audio.filename)
835        self.audio.delete()
836        self.assertTrue(os.path.getsize(self.audio.filename) < filesize)
837
838
839class TMP4Datatypes(TMP4, TMP4HasTagsMixin):
840    original = os.path.join(DATA_DIR, "has-tags.m4a")
841
842    def test_has_freeform(self):
843        key = "----:com.apple.iTunes:iTunNORM"
844        self.failUnless(key in self.audio.tags)
845        ff = self.audio.tags[key]
846        self.failUnlessEqual(ff[0].dataformat, AtomDataType.UTF8)
847        self.failUnlessEqual(ff[0].version, 0)
848
849    def test_has_covr(self):
850        self.failUnless('covr' in self.audio.tags)
851        covr = self.audio.tags['covr']
852        self.failUnlessEqual(len(covr), 2)
853        self.failUnlessEqual(covr[0].imageformat, MP4Cover.FORMAT_PNG)
854        self.failUnlessEqual(covr[1].imageformat, MP4Cover.FORMAT_JPEG)
855
856    def test_pprint(self):
857        text = self.audio.tags.pprint().splitlines()
858        self.assertTrue(u"©ART=Test Artist" in text)
859
860    def test_get_padding(self):
861        self.assertEqual(self.audio._padding, 1634)
862
863
864class TMP4CovrWithName(TMP4, TMP4Mixin):
865    # http://bugs.musicbrainz.org/ticket/5894
866    original = os.path.join(DATA_DIR, "covr-with-name.m4a")
867
868    def test_has_covr(self):
869        self.failUnless('covr' in self.audio.tags)
870        covr = self.audio.tags['covr']
871        self.failUnlessEqual(len(covr), 2)
872        self.failUnlessEqual(covr[0].imageformat, MP4Cover.FORMAT_PNG)
873        self.failUnlessEqual(covr[1].imageformat, MP4Cover.FORMAT_JPEG)
874
875
876class TMP4HasTags64Bit(TMP4, TMP4HasTagsMixin):
877    original = os.path.join(DATA_DIR, "truncated-64bit.mp4")
878
879    def test_has_covr(self):
880        pass
881
882    def test_bitrate(self):
883        self.failUnlessEqual(self.audio.info.bitrate, 128000)
884
885    def test_length(self):
886        self.failUnlessAlmostEqual(0.325, self.audio.info.length, 3)
887
888    def faad(self):
889        # This is only half a file, so FAAD segfaults. Can't test. :(
890        pass
891
892
893class TMP4NoTagsM4A(TMP4, TMP4Mixin):
894    original = os.path.join(DATA_DIR, "no-tags.m4a")
895
896    def test_no_tags(self):
897        self.failUnless(self.audio.tags is None)
898
899    def test_add_tags(self):
900        self.audio.add_tags()
901        self.failUnlessRaises(error, self.audio.add_tags)
902
903
904class TMP4NoTags3G2(TMP4, TMP4Mixin):
905    original = os.path.join(DATA_DIR, "no-tags.3g2")
906
907    def test_no_tags(self):
908        self.failUnless(self.audio.tags is None)
909
910    def test_sample_rate(self):
911        self.failUnlessEqual(self.audio.info.sample_rate, 22050)
912
913    def test_bitrate(self):
914        self.failUnlessEqual(self.audio.info.bitrate, 32000)
915
916    def test_length(self):
917        self.failUnlessAlmostEqual(15, self.audio.info.length, 1)
918
919
920class TMP4UpdateParents64Bit(TestCase):
921    original = os.path.join(DATA_DIR, "64bit.mp4")
922
923    def setUp(self):
924        self.filename = get_temp_copy(self.original)
925
926    def test_update_parents(self):
927        with open(self.filename, "rb") as fileobj:
928            atoms = Atoms(fileobj)
929            self.assertEqual(77, atoms.atoms[0].length)
930            self.assertEqual(61, atoms.atoms[0].children[0].length)
931            tags = MP4Tags(atoms, fileobj)
932            tags['pgap'] = True
933            tags.save(self.filename, padding=lambda x: 0)
934
935        with open(self.filename, "rb") as fileobj:
936            atoms = Atoms(fileobj)
937            # original size + 'pgap' size + padding
938            self.assertEqual(77 + 25 + 8, atoms.atoms[0].length)
939            self.assertEqual(61 + 25 + 8, atoms.atoms[0].children[0].length)
940
941    def tearDown(self):
942        os.unlink(self.filename)
943
944
945class TMP4ALAC(TestCase):
946    original = os.path.join(DATA_DIR, "alac.m4a")
947
948    def setUp(self):
949        self.audio = MP4(self.original)
950
951    def test_channels(self):
952        self.failUnlessEqual(self.audio.info.channels, 2)
953
954    def test_sample_rate(self):
955        self.failUnlessEqual(self.audio.info.sample_rate, 44100)
956
957    def test_bits_per_sample(self):
958        self.failUnlessEqual(self.audio.info.bits_per_sample, 16)
959
960    def test_length(self):
961        self.failUnlessAlmostEqual(3.7, self.audio.info.length, 1)
962
963    def test_bitrate(self):
964        self.assertEqual(self.audio.info.bitrate, 2764)
965
966    def test_kind(self):
967        self.assertEqual(self.audio.info.codec, u'alac')
968
969
970class TMP4Misc(TestCase):
971
972    def test_no_audio_tracks(self):
973        data = Atom.render(b"moov", Atom.render(b"udta", b""))
974        fileobj = cBytesIO(data)
975        audio = MP4(fileobj)
976        assert audio.info
977        assert audio.pprint()
978        info = audio.info
979        assert isinstance(info.bitrate, int)
980        assert isinstance(info.length, float)
981        assert isinstance(info.channels, int)
982        assert isinstance(info.sample_rate, int)
983        assert isinstance(info.bits_per_sample, int)
984        assert isinstance(info.codec, text_type)
985        assert isinstance(info.codec_description, text_type)
986
987    def test_parse_full_atom(self):
988        p = parse_full_atom(b"\x01\x02\x03\x04\xff")
989        self.assertEqual(p, (1, 131844, b'\xff'))
990
991        self.assertRaises(ValueError, parse_full_atom, b"\x00\x00\x00")
992
993    def test_sort_items(self):
994        items = [
995            ("\xa9nam", ["foo"]),
996            ("gnre", ["fo"]),
997            ("----", ["123"]),
998            ("----", ["1234"]),
999        ]
1000
1001        sorted_items = sorted(items, key=lambda kv: _item_sort_key(*kv))
1002        self.assertEqual(sorted_items, items)
1003
1004
1005class TMP4Freeform(TestCase):
1006
1007    def test_cmp(self):
1008        self.assertReallyEqual(
1009            MP4FreeForm(b'woooo', 142, 42), MP4FreeForm(b'woooo', 142, 42))
1010        self.assertReallyNotEqual(
1011            MP4FreeForm(b'woooo', 142, 43), MP4FreeForm(b'woooo', 142, 42))
1012        self.assertReallyNotEqual(
1013            MP4FreeForm(b'woooo', 143, 42), MP4FreeForm(b'woooo', 142, 42))
1014        self.assertReallyNotEqual(
1015            MP4FreeForm(b'wooox', 142, 42), MP4FreeForm(b'woooo', 142, 42))
1016
1017    def test_cmp_bytes(self):
1018        self.assertReallyEqual(MP4FreeForm(b'woooo'), b"woooo")
1019        self.assertReallyNotEqual(MP4FreeForm(b'woooo'), b"foo")
1020        if PY2:
1021            self.assertReallyEqual(MP4FreeForm(b'woooo'), u"woooo")
1022            self.assertReallyNotEqual(MP4FreeForm(b'woooo'), u"foo")
1023
1024
1025class TMP4Cover(TestCase):
1026
1027    def test_cmp(self):
1028        self.assertReallyEqual(
1029            MP4Cover(b'woooo', 142), MP4Cover(b'woooo', 142))
1030        self.assertReallyNotEqual(
1031            MP4Cover(b'woooo', 143), MP4Cover(b'woooo', 142))
1032        self.assertReallyNotEqual(
1033            MP4Cover(b'woooo', 142), MP4Cover(b'wooox', 142))
1034
1035    def test_cmp_bytes(self):
1036        self.assertReallyEqual(MP4Cover(b'woooo'), b"woooo")
1037        self.assertReallyNotEqual(MP4Cover(b'woooo'), b"foo")
1038        if PY2:
1039            self.assertReallyEqual(MP4Cover(b'woooo'), u"woooo")
1040            self.assertReallyNotEqual(MP4Cover(b'woooo'), u"foo")
1041
1042
1043class TMP4AudioSampleEntry(TestCase):
1044
1045    def test_alac(self):
1046        # an exampe where the channel count in the alac cookie is right
1047        # but the SampleEntry is wrong
1048        atom_data = (
1049            b'\x00\x00\x00Halac\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00'
1050            b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\x1f@\x00'
1051            b'\x00\x00\x00\x00$alac\x00\x00\x00\x00\x00\x00\x10\x00\x00\x10'
1052            b'(\n\x0e\x01\x00\xff\x00\x00P\x01\x00\x00\x00\x00\x00\x00\x1f@')
1053
1054        fileobj = cBytesIO(atom_data)
1055        atom = Atom(fileobj)
1056        entry = AudioSampleEntry(atom, fileobj)
1057        self.assertEqual(entry.bitrate, 0)
1058        self.assertEqual(entry.channels, 1)
1059        self.assertEqual(entry.codec, "alac")
1060        self.assertEqual(entry.codec_description, "ALAC")
1061        self.assertEqual(entry.sample_rate, 8000)
1062
1063    def test_alac_2(self):
1064        # an example where the samplerate is only correct in the cookie,
1065        # also contains a bitrate
1066        atom_data = (
1067            b'\x00\x00\x00Halac\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00'
1068            b'\x00\x00\x00\x00\x00\x00\x02\x00\x18\x00\x00\x00\x00X\x88\x00'
1069            b'\x00\x00\x00\x00$alac\x00\x00\x00\x00\x00\x00\x10\x00\x00\x18'
1070            b'(\n\x0e\x02\x00\xff\x00\x00F/\x00%2\xd5\x00\x01X\x88')
1071
1072        fileobj = cBytesIO(atom_data)
1073        atom = Atom(fileobj)
1074        entry = AudioSampleEntry(atom, fileobj)
1075        self.assertEqual(entry.bitrate, 2437845)
1076        self.assertEqual(entry.channels, 2)
1077        self.assertEqual(entry.codec, "alac")
1078        self.assertEqual(entry.codec_description, "ALAC")
1079        self.assertEqual(entry.sample_rate, 88200)
1080
1081    def test_pce(self):
1082        atom_data = (
1083            b'\x00\x00\x00dmp4a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00'
1084            b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\xbb\x80'
1085            b'\x00\x00\x00\x00\x00@esds\x00\x00\x00\x00\x03\x80\x80\x80/\x00'
1086            b'\x00\x00\x04\x80\x80\x80!@\x15\x00\x15\x00\x00\x03\xed\xaa\x00'
1087            b'\x03k\x00\x05\x80\x80\x80\x0f+\x01\x88\x02\xc4\x04\x90,\x10\x8c'
1088            b'\x80\x00\x00\xed@\x06\x80\x80\x80\x01\x02')
1089
1090        fileobj = cBytesIO(atom_data)
1091        atom = Atom(fileobj)
1092        entry = AudioSampleEntry(atom, fileobj)
1093
1094        self.assertEqual(entry.bitrate, 224000)
1095        self.assertEqual(entry.channels, 8)
1096        self.assertEqual(entry.codec_description, "AAC LC+SBR")
1097        self.assertEqual(entry.codec, "mp4a.40.2")
1098        self.assertEqual(entry.sample_rate, 48000)
1099        self.assertEqual(entry.sample_size, 16)
1100
1101    def test_sbr_ps_sig_1(self):
1102        atom_data = (
1103            b"\x00\x00\x00\\mp4a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00"
1104            b"\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\xbb\x80\x00"
1105            b"\x00\x00\x00\x008esds\x00\x00\x00\x00\x03\x80\x80\x80'\x00\x00"
1106            b"\x00\x04\x80\x80\x80\x19@\x15\x00\x03\x00\x00\x00\xe9j\x00\x00"
1107            b"\xda\xc0\x05\x80\x80\x80\x07\x13\x08V\xe5\x9dH\x80\x06\x80\x80"
1108            b"\x80\x01\x02")
1109
1110        fileobj = cBytesIO(atom_data)
1111        atom = Atom(fileobj)
1112        entry = AudioSampleEntry(atom, fileobj)
1113
1114        self.assertEqual(entry.bitrate, 56000)
1115        self.assertEqual(entry.channels, 2)
1116        self.assertEqual(entry.codec_description, "AAC LC+SBR+PS")
1117        self.assertEqual(entry.codec, "mp4a.40.2")
1118        self.assertEqual(entry.sample_rate, 48000)
1119        self.assertEqual(entry.sample_size, 16)
1120
1121        self.assertTrue(isinstance(entry.codec, text_type))
1122        self.assertTrue(isinstance(entry.codec_description, text_type))
1123
1124    def test_als(self):
1125        atom_data = (
1126            b'\x00\x00\x00\x9dmp4a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00'
1127            b'\x00\x00\x00\x00\x00\x00\x02\x00\x00\x10\x00\x00\x00\x00\x07'
1128            b'\xd0\x00\x00\x00\x00\x00yesds\x00\x00\x00\x00\x03k\x00\x00\x00'
1129            b'\x04c@\x15\x10\xe7\xe6\x00W\xcbJ\x00W\xcbJ\x05T\xf8\x9e\x00\x0f'
1130            b'\xa0\x00ALS\x00\x00\x00\x07\xd0\x00\x00\x0c\t\x01\xff$O\xff\x00'
1131            b'g\xff\xfc\x80\x00\x00\x00,\x00\x00\x00\x00RIFF$$0\x00WAVEfmt '
1132            b'\x10\x00\x00\x00\x01\x00\x00\x02\xd0\x07\x00\x00\x00@\x1f\x00'
1133            b'\x00\x04\x10\x00data\x00$0\x00\xf6\xceF+\x06\x01\x02')
1134
1135        fileobj = cBytesIO(atom_data)
1136        atom = Atom(fileobj)
1137        entry = AudioSampleEntry(atom, fileobj)
1138
1139        self.assertEqual(entry.bitrate, 5753674)
1140        self.assertEqual(entry.channels, 512)
1141        self.assertEqual(entry.codec_description, "ALS")
1142        self.assertEqual(entry.codec, "mp4a.40.36")
1143        self.assertEqual(entry.sample_rate, 2000)
1144        self.assertEqual(entry.sample_size, 16)
1145
1146    def test_ac3(self):
1147        atom_data = (
1148            b'\x00\x00\x00/ac-3\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00'
1149            b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00V"\x00\x00'
1150            b'\x00\x00\x00\x0bdac3R\t\x00')
1151
1152        fileobj = cBytesIO(atom_data)
1153        atom = Atom(fileobj)
1154        entry = AudioSampleEntry(atom, fileobj)
1155
1156        self.assertEqual(entry.bitrate, 128000)
1157        self.assertEqual(entry.channels, 1)
1158        self.assertEqual(entry.codec_description, "AC-3")
1159        self.assertEqual(entry.codec, "ac-3")
1160        self.assertEqual(entry.sample_rate, 22050)
1161        self.assertEqual(entry.sample_size, 16)
1162
1163        self.assertTrue(isinstance(entry.codec, text_type))
1164        self.assertTrue(isinstance(entry.codec_description, text_type))
1165
1166    def test_samr(self):
1167        # parsing not implemented, values are wrong but at least it loads.
1168        # should be Mono 7.95kbps 8KHz
1169        atom_data = (
1170            b'\x00\x00\x005samr\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00'
1171            b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\x1f@\x00'
1172            b'\x00\x00\x00\x00\x11damrFFMP\x00\x81\xff\x00\x01')
1173
1174        fileobj = cBytesIO(atom_data)
1175        atom = Atom(fileobj)
1176        entry = AudioSampleEntry(atom, fileobj)
1177
1178        self.assertEqual(entry.bitrate, 0)
1179        self.assertEqual(entry.channels, 2)
1180        self.assertEqual(entry.codec_description, "SAMR")
1181        self.assertEqual(entry.codec, "samr")
1182        self.assertEqual(entry.sample_rate, 8000)
1183        self.assertEqual(entry.sample_size, 16)
1184
1185        self.assertTrue(isinstance(entry.codec, text_type))
1186        self.assertTrue(isinstance(entry.codec_description, text_type))
1187
1188    def test_error(self):
1189        fileobj = cBytesIO(b"\x00" * 20)
1190        atom = Atom(fileobj)
1191        self.assertRaises(ASEntryError, AudioSampleEntry, atom, fileobj)
1192
1193
1194def call_faad(*args):
1195    with open(os.devnull, 'wb') as null:
1196        return subprocess.call(
1197            ["faad"] + list(args),
1198            stdout=null, stderr=subprocess.STDOUT)
1199
1200have_faad = True
1201try:
1202    call_faad()
1203except OSError:
1204    have_faad = False
1205    print("WARNING: Skipping FAAD reference tests.")
1206