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