1#!/usr/bin/python 2 3# Audio Tools, a module and set of tools for manipulating audio data 4# Copyright (C) 2007-2014 Brian Langenberger 5 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 20from audiotools import MetaData, Image 21from audiotools.image import image_metrics 22import sys 23 24# M4A atoms are typically laid on in the file as follows: 25# ftyp 26# mdat 27# moov/ 28# +mvhd 29# +iods 30# +trak/ 31# +-tkhd 32# +-mdia/ 33# +--mdhd 34# +--hdlr 35# +--minf/ 36# +---smhd 37# +---dinf/ 38# +----dref 39# +---stbl/ 40# +----stsd 41# +----stts 42# +----stsz 43# +----stsc 44# +----stco 45# +----ctts 46# +udta/ 47# +-meta 48# 49# Where atoms ending in / are container atoms and the rest are leaf atoms. 50# 'mdat' is where the file's audio stream is stored 51# the rest are various bits of metadata 52 53 54def parse_sub_atoms(data_size, reader, parsers): 55 """data size is the length of the parent atom's data 56 reader is a BitstreamReader 57 parsers is a dict of leaf_name->parser() 58 where parser is defined as: 59 parser(leaf_name, leaf_data_size, BitstreamReader, parsers) 60 as a sort of recursive parsing handler 61 """ 62 63 leaf_atoms = [] 64 65 while data_size > 0: 66 (leaf_size, leaf_name) = reader.parse("32u 4b") 67 leaf_atoms.append( 68 parsers.get(leaf_name, M4A_Leaf_Atom).parse( 69 leaf_name, 70 leaf_size - 8, 71 reader.substream(leaf_size - 8), 72 parsers)) 73 data_size -= leaf_size 74 75 return leaf_atoms 76 77# build(), parse() and size() work on atom data 78# but not the atom's size and name values 79 80 81class M4A_Tree_Atom(object): 82 def __init__(self, name, leaf_atoms): 83 """name should be a 4 byte string 84 85 children should be a list of M4A_Tree_Atoms or M4A_Leaf_Atoms""" 86 87 # assert((name is None) or isinstance(name, bytes)) 88 89 self.name = name 90 self.leaf_atoms = leaf_atoms 91 92 def copy(self): 93 """returns a newly copied instance of this atom 94 and new instances of any sub-atoms it contains""" 95 96 return M4A_Tree_Atom(self.name, [leaf.copy() for leaf in self]) 97 98 def __repr__(self): 99 return "M4A_Tree_Atom(%s, %s)" % \ 100 (repr(self.name), repr(self.leaf_atoms)) 101 102 def __eq__(self, atom): 103 for attr in ["name", "leaf_atoms"]: 104 if ((not hasattr(atom, attr)) or (getattr(self, attr) != 105 getattr(atom, attr))): 106 return False 107 else: 108 return True 109 110 def __iter__(self): 111 for leaf in self.leaf_atoms: 112 yield leaf 113 114 def __getitem__(self, atom_name): 115 return self.get_child(atom_name) 116 117 def get_child(self, atom_name): 118 """returns the first instance of the given child atom 119 raises KeyError if the child is not found""" 120 121 # assert(isinstance(atom_name, bytes)) 122 123 for leaf in self: 124 if leaf.name == atom_name: 125 return leaf 126 else: 127 raise KeyError(atom_name) 128 129 def has_child(self, atom_name): 130 """returns True if the given atom name 131 is an immediate child of this atom""" 132 133 # assert(isinstance(atom_name, bytes)) 134 135 for leaf in self: 136 if leaf.name == atom_name: 137 return True 138 else: 139 return False 140 141 def add_child(self, atom_obj): 142 """adds the given child atom to this container""" 143 144 self.leaf_atoms.append(atom_obj) 145 146 def remove_child(self, atom_name): 147 """removes the first instance of the given atom from this container""" 148 149 # assert(isinstance(atom_name, bytes)) 150 151 new_leaf_atoms = [] 152 data_deleted = False 153 for leaf_atom in self: 154 if (leaf_atom.name == atom_name) and (not data_deleted): 155 data_deleted = True 156 else: 157 new_leaf_atoms.append(leaf_atom) 158 159 self.leaf_atoms = new_leaf_atoms 160 161 def replace_child(self, atom_obj): 162 """replaces the first instance of the given atom's name 163 with the given atom""" 164 165 new_leaf_atoms = [] 166 data_replaced = False 167 for leaf_atom in self: 168 if (leaf_atom.name == atom_obj.name) and (not data_replaced): 169 new_leaf_atoms.append(atom_obj) 170 data_replaced = True 171 else: 172 new_leaf_atoms.append(leaf_atom) 173 174 self.leaf_atoms = new_leaf_atoms 175 176 def child_offset(self, *child_path): 177 """given a path to the given child atom 178 returns its offset within this parent 179 180 raises KeyError if the child cannot be found""" 181 182 offset = 0 183 next_child = child_path[0] 184 for leaf_atom in self: 185 if leaf_atom.name == next_child: 186 if len(child_path) > 1: 187 return (offset + 8 + 188 leaf_atom.child_offset(*(child_path[1:]))) 189 else: 190 return offset 191 else: 192 offset += (8 + leaf_atom.size()) 193 else: 194 raise KeyError(next_child) 195 196 @classmethod 197 def parse(cls, name, data_size, reader, parsers): 198 """given a 4 byte name, data_size int, BitstreamReader 199 and dict of {"atom":handler} sub-parsers, 200 returns an atom of this class""" 201 202 return cls(name, parse_sub_atoms(data_size, reader, parsers)) 203 204 def build(self, writer): 205 """writes the atom to the given BitstreamWriter 206 not including its 64-bit size / name header""" 207 208 for sub_atom in self: 209 writer.build("32u 4b", (sub_atom.size() + 8, sub_atom.name)) 210 sub_atom.build(writer) 211 212 def size(self): 213 """returns the atom's size 214 not including its 64-bit size / name header""" 215 216 return sum([8 + sub_atom.size() for sub_atom in self]) 217 218 219class M4A_Leaf_Atom(object): 220 def __init__(self, name, data): 221 """name should be a 4 byte string 222 223 data should be a binary string of atom data""" 224 225 # assert(isinstance(name, bytes)) 226 # assert(isinstance(data, bytes)) 227 228 self.name = name 229 self.data = data 230 231 def copy(self): 232 """returns a newly copied instance of this atom 233 and new instances of any sub-atoms it contains""" 234 235 return M4A_Leaf_Atom(self.name, self.data) 236 237 def __repr__(self): 238 return "M4A_Leaf_Atom(%s, %s)" % \ 239 (repr(self.name), repr(self.data)) 240 241 def __eq__(self, atom): 242 for attr in ["name", "data"]: 243 if ((not hasattr(atom, attr)) or (getattr(self, attr) != 244 getattr(atom, attr))): 245 return False 246 else: 247 return True 248 249 if sys.version_info[0] >= 3: 250 def __str__(self): 251 return self.__unicode__() 252 else: 253 def __str__(self): 254 return self.__unicode__().encode('utf-8') 255 256 def __unicode__(self): 257 # FIXME - should make this more informative, if possible 258 259 from audiotools import hex_string 260 261 return hex_string(self.data[0:20]) 262 263 def raw_info(self): 264 """returns a line of human-readable information about the atom""" 265 266 from audiotools import hex_string 267 268 if len(self.data) > 20: 269 return u"%s : %s\u2026" % \ 270 (self.name.decode('ascii', 'replace'), 271 hex_string(self.data[0:20])) 272 else: 273 return u"%s : %s" % \ 274 (self.name.decode('ascii', 'replace'), 275 hex_string(self.data)) 276 277 @classmethod 278 def parse(cls, name, data_size, reader, parsers): 279 """given a 4 byte name, data_size int, BitstreamReader 280 and dict of {"atom":handler} sub-parsers, 281 returns an atom of this class""" 282 283 return cls(name, reader.read_bytes(data_size)) 284 285 def build(self, writer): 286 """writes the atom to the given BitstreamWriter 287 not including its 64-bit size / name header""" 288 289 writer.write_bytes(self.data) 290 291 def size(self): 292 """returns the atom's size 293 not including its 64-bit size / name header""" 294 295 return len(self.data) 296 297 298class M4A_FTYP_Atom(M4A_Leaf_Atom): 299 def __init__(self, major_brand, major_brand_version, compatible_brands): 300 # assert(isinstance(major_brand, bytes)) 301 # for b in compatible_brands: 302 # assert(isinstance(b, bytes)) 303 304 self.name = b'ftyp' 305 self.major_brand = major_brand 306 self.major_brand_version = major_brand_version 307 self.compatible_brands = compatible_brands 308 309 def __repr__(self): 310 return "M4A_FTYP_Atom(%s, %s, %s)" % \ 311 (repr(self.major_brand), 312 repr(self.major_brand_version), 313 repr(self.compatible_brands)) 314 315 @classmethod 316 def parse(cls, name, data_size, reader, parsers): 317 """given a 4 byte name, data_size int, BitstreamReader 318 and dict of {"atom":handler} sub-parsers, 319 returns an atom of this class""" 320 321 assert(name == b'ftyp') 322 return cls(reader.read_bytes(4), 323 reader.read(32), 324 [reader.read_bytes(4) 325 for i in range((data_size - 8) // 4)]) 326 327 def build(self, writer): 328 """writes the atom to the given BitstreamWriter 329 not including its 64-bit size / name header""" 330 331 writer.build("4b 32u %d* 4b" % (len(self.compatible_brands)), 332 [self.major_brand, 333 self.major_brand_version] + 334 self.compatible_brands) 335 336 def size(self): 337 """returns the atom's size 338 not including its 64-bit size / name header""" 339 340 return 4 + 4 + (4 * len(self.compatible_brands)) 341 342 343class M4A_MVHD_Atom(M4A_Leaf_Atom): 344 def __init__(self, version, flags, created_utc_date, modified_utc_date, 345 time_scale, duration, playback_speed, user_volume, 346 geometry_matrices, qt_preview, qt_still_poster, 347 qt_selection_time, qt_current_time, next_track_id): 348 self.name = b'mvhd' 349 self.version = version 350 self.flags = flags 351 self.created_utc_date = created_utc_date 352 self.modified_utc_date = modified_utc_date 353 self.time_scale = time_scale 354 self.duration = duration 355 self.playback_speed = playback_speed 356 self.user_volume = user_volume 357 self.geometry_matrices = geometry_matrices 358 self.qt_preview = qt_preview 359 self.qt_still_poster = qt_still_poster 360 self.qt_selection_time = qt_selection_time 361 self.qt_current_time = qt_current_time 362 self.next_track_id = next_track_id 363 364 @classmethod 365 def parse(cls, name, data_size, reader, parsers): 366 """given a 4 byte name, data_size int, BitstreamReader 367 and dict of {"atom":handler} sub-parsers, 368 returns an atom of this class""" 369 370 assert(name == b'mvhd') 371 (version, flags) = reader.parse("8u 24u") 372 373 if version == 0: 374 atom_format = "32u 32u 32u 32u 32u 16u 10P" 375 else: 376 atom_format = "64U 64U 32u 64U 32u 16u 10P" 377 (created_utc_date, 378 modified_utc_date, 379 time_scale, 380 duration, 381 playback_speed, 382 user_volume) = reader.parse(atom_format) 383 384 geometry_matrices = reader.parse("32u" * 9) 385 386 (qt_preview, 387 qt_still_poster, 388 qt_selection_time, 389 qt_current_time, 390 next_track_id) = reader.parse("64U 32u 64U 32u 32u") 391 392 return cls(version=version, 393 flags=flags, 394 created_utc_date=created_utc_date, 395 modified_utc_date=modified_utc_date, 396 time_scale=time_scale, 397 duration=duration, 398 playback_speed=playback_speed, 399 user_volume=user_volume, 400 geometry_matrices=geometry_matrices, 401 qt_preview=qt_preview, 402 qt_still_poster=qt_still_poster, 403 qt_selection_time=qt_selection_time, 404 qt_current_time=qt_current_time, 405 next_track_id=next_track_id) 406 407 def __repr__(self): 408 return "MVHD_Atom(%s)" % ( 409 ",".join(map(repr, 410 [self.version, self.flags, 411 self.created_utc_date, self.modified_utc_date, 412 self.time_scale, self.duration, self.playback_speed, 413 self.user_volume, self.geometry_matrices, 414 self.qt_preview, self.qt_still_poster, 415 self.qt_selection_time, self.qt_current_time, 416 self.next_track_id]))) 417 418 def build(self, writer): 419 """writes the atom to the given BitstreamWriter 420 not including its 64-bit size / name header""" 421 422 writer.build("8u 24u", (self.version, self.flags)) 423 424 if self.version == 0: 425 atom_format = "32u 32u 32u 32u 32u 16u 10P" 426 else: 427 atom_format = "64U 64U 32u 64U 32u 16u 10P" 428 429 writer.build(atom_format, 430 (self.created_utc_date, self.modified_utc_date, 431 self.time_scale, self.duration, 432 self.playback_speed, self.user_volume)) 433 434 writer.build("9* 32u", self.geometry_matrices) 435 436 writer.build("64U 32u 64U 32u 32u", 437 (self.qt_preview, self.qt_still_poster, 438 self.qt_selection_time, self.qt_current_time, 439 self.next_track_id)) 440 441 def size(self): 442 """returns the atom's size 443 not including its 64-bit size / name header""" 444 445 if self.version == 0: 446 return 100 447 else: 448 return 112 449 450 451class M4A_TKHD_Atom(M4A_Leaf_Atom): 452 def __init__(self, version, track_in_poster, track_in_preview, 453 track_in_movie, track_enabled, created_utc_date, 454 modified_utc_date, track_id, duration, video_layer, 455 qt_alternate, volume, geometry_matrices, 456 video_width, video_height): 457 self.name = b'tkhd' 458 self.version = version 459 self.track_in_poster = track_in_poster 460 self.track_in_preview = track_in_preview 461 self.track_in_movie = track_in_movie 462 self.track_enabled = track_enabled 463 self.created_utc_date = created_utc_date 464 self.modified_utc_date = modified_utc_date 465 self.track_id = track_id 466 self.duration = duration 467 self.video_layer = video_layer 468 self.qt_alternate = qt_alternate 469 self.volume = volume 470 self.geometry_matrices = geometry_matrices 471 self.video_width = video_width 472 self.video_height = video_height 473 474 def __repr__(self): 475 return "M4A_TKHD_Atom(%s)" % ( 476 ",".join(map(repr, 477 [self.version, self.track_in_poster, 478 self.track_in_preview, self.track_in_movie, 479 self.track_enabled, self.created_utc_date, 480 self.modified_utc_date, self.track_id, 481 self.duration, self.video_layer, self.qt_alternate, 482 self.volume, self.geometry_matrices, 483 self.video_width, self.video_height]))) 484 485 @classmethod 486 def parse(cls, name, data_size, reader, parsers): 487 """given a 4 byte name, data_size int, BitstreamReader 488 and dict of {"atom":handler} sub-parsers, 489 returns an atom of this class""" 490 491 (version, 492 track_in_poster, 493 track_in_preview, 494 track_in_movie, 495 track_enabled) = reader.parse("8u 20p 1u 1u 1u 1u") 496 497 if version == 0: 498 atom_format = "32u 32u 32u 4P 32u 8P 16u 16u 16u 2P" 499 else: 500 atom_format = "64U 64U 32u 4P 64U 8P 16u 16u 16u 2P" 501 (created_utc_date, 502 modified_utc_date, 503 track_id, 504 duration, 505 video_layer, 506 qt_alternate, 507 volume) = reader.parse(atom_format) 508 509 geometry_matrices = reader.parse("9* 32u") 510 (video_width, video_height) = reader.parse("32u 32u") 511 512 return cls(version=version, 513 track_in_poster=track_in_poster, 514 track_in_preview=track_in_preview, 515 track_in_movie=track_in_movie, 516 track_enabled=track_enabled, 517 created_utc_date=created_utc_date, 518 modified_utc_date=modified_utc_date, 519 track_id=track_id, 520 duration=duration, 521 video_layer=video_layer, 522 qt_alternate=qt_alternate, 523 volume=volume, 524 geometry_matrices=geometry_matrices, 525 video_width=video_width, 526 video_height=video_height) 527 528 def build(self, writer): 529 """writes the atom to the given BitstreamWriter 530 not including its 64-bit size / name header""" 531 532 writer.build("8u 20p 1u 1u 1u 1u", 533 (self.version, self.track_in_poster, 534 self.track_in_preview, self.track_in_movie, 535 self.track_enabled)) 536 if self.version == 0: 537 atom_format = "32u 32u 32u 4P 32u 8P 16u 16u 16u 2P" 538 else: 539 atom_format = "64U 64U 32u 4P 64U 8P 16u 16u 16u 2P" 540 writer.build(atom_format, 541 (self.created_utc_date, self.modified_utc_date, 542 self.track_id, self.duration, self.video_layer, 543 self.qt_alternate, self.volume)) 544 writer.build("9* 32u", self.geometry_matrices) 545 writer.build("32u 32u", (self.video_width, self.video_height)) 546 547 def size(self): 548 """returns the atom's size 549 not including its 64-bit size / name header""" 550 551 if self.version == 0: 552 return 84 553 else: 554 return 96 555 556 557class M4A_MDHD_Atom(M4A_Leaf_Atom): 558 def __init__(self, version, flags, created_utc_date, modified_utc_date, 559 sample_rate, track_length, language, quality): 560 self.name = b'mdhd' 561 self.version = version 562 self.flags = flags 563 self.created_utc_date = created_utc_date 564 self.modified_utc_date = modified_utc_date 565 self.sample_rate = sample_rate 566 self.track_length = track_length 567 self.language = language 568 self.quality = quality 569 570 def __repr__(self): 571 return "M4A_MDHD_Atom(%s)" % \ 572 (",".join(map(repr, 573 [self.version, self.flags, self.created_utc_date, 574 self.modified_utc_date, self.sample_rate, 575 self.track_length, self.language, self.quality]))) 576 577 @classmethod 578 def parse(cls, name, data_size, reader, parsers): 579 """given a 4 byte name, data_size int, BitstreamReader 580 and dict of {"atom":handler} sub-parsers, 581 returns an atom of this class""" 582 583 assert(name == b'mdhd') 584 (version, flags) = reader.parse("8u 24u") 585 if version == 0: 586 atom_format = "32u 32u 32u 32u" 587 else: 588 atom_format = "64U 64U 32u 64U" 589 (created_utc_date, 590 modified_utc_date, 591 sample_rate, 592 track_length) = reader.parse(atom_format) 593 language = reader.parse("1p 5u 5u 5u") 594 quality = reader.read(16) 595 596 return cls(version=version, 597 flags=flags, 598 created_utc_date=created_utc_date, 599 modified_utc_date=modified_utc_date, 600 sample_rate=sample_rate, 601 track_length=track_length, 602 language=language, 603 quality=quality) 604 605 def build(self, writer): 606 """writes the atom to the given BitstreamWriter 607 not including its 64-bit size / name header""" 608 609 writer.build("8u 24u", (self.version, self.flags)) 610 if self.version == 0: 611 atom_format = "32u 32u 32u 32u" 612 else: 613 atom_format = "64U 64U 32u 64U" 614 writer.build(atom_format, 615 (self.created_utc_date, self.modified_utc_date, 616 self.sample_rate, self.track_length)) 617 writer.build("1p 5u 5u 5u", self.language) 618 writer.write(16, self.quality) 619 620 def size(self): 621 """returns the atom's size 622 not including its 64-bit size / name header""" 623 624 if self.version == 0: 625 return 24 626 else: 627 return 36 628 629 630class M4A_SMHD_Atom(M4A_Leaf_Atom): 631 def __init__(self, version, flags, audio_balance): 632 self.name = b'smhd' 633 self.version = version 634 self.flags = flags 635 self.audio_balance = audio_balance 636 637 def __repr__(self): 638 return "M4A_SMHD_Atom(%s)" % \ 639 (",".join(map(repr, (self.version, 640 self.flags, 641 self.audio_balance)))) 642 643 @classmethod 644 def parse(cls, name, data_size, reader, parsers): 645 """given a 4 byte name, data_size int, BitstreamReader 646 and dict of {"atom":handler} sub-parsers, 647 returns an atom of this class""" 648 649 return cls(*reader.parse("8u 24u 16u 16p")) 650 651 def build(self, writer): 652 """writes the atom to the given BitstreamWriter 653 not including its 64-bit size / name header""" 654 655 writer.build("8u 24u 16u 16p", 656 (self.version, self.flags, self.audio_balance)) 657 658 def size(self): 659 """returns the atom's size 660 not including its 64-bit size / name header""" 661 662 return 8 663 664 665class M4A_DREF_Atom(M4A_Leaf_Atom): 666 def __init__(self, version, flags, references): 667 self.name = b'dref' 668 self.version = version 669 self.flags = flags 670 self.references = references 671 672 def __repr__(self): 673 return "M4A_DREF_Atom(%s)" % \ 674 (",".join(map(repr, (self.version, 675 self.flags, 676 self.references)))) 677 678 @classmethod 679 def parse(cls, name, data_size, reader, parsers): 680 """given a 4 byte name, data_size int, BitstreamReader 681 and dict of {"atom":handler} sub-parsers, 682 returns an atom of this class""" 683 684 (version, flags, reference_count) = reader.parse("8u 24u 32u") 685 references = [] 686 for i in range(reference_count): 687 (leaf_size, leaf_name) = reader.parse("32u 4b") 688 references.append( 689 M4A_Leaf_Atom.parse( 690 leaf_name, leaf_size - 8, 691 reader.substream(leaf_size - 8), {})) 692 return cls(version, flags, references) 693 694 def build(self, writer): 695 """writes the atom to the given BitstreamWriter 696 not including its 64-bit size / name header""" 697 698 writer.build("8u 24u 32u", (self.version, 699 self.flags, 700 len(self.references))) 701 702 for reference_atom in self.references: 703 writer.build("32u 4b", (reference_atom.size() + 8, 704 reference_atom.name)) 705 reference_atom.build(writer) 706 707 def size(self): 708 """returns the atom's size 709 not including its 64-bit size / name header""" 710 711 return 8 + sum([reference_atom.size() + 8 712 for reference_atom in self.references]) 713 714 715class M4A_STSD_Atom(M4A_Leaf_Atom): 716 def __init__(self, version, flags, descriptions): 717 self.name = b'stsd' 718 self.version = version 719 self.flags = flags 720 self.descriptions = descriptions 721 722 def __repr__(self): 723 return "M4A_STSD_Atom(%s, %s, %s)" % \ 724 (repr(self.version), repr(self.flags), repr(self.descriptions)) 725 726 @classmethod 727 def parse(cls, name, data_size, reader, parsers): 728 """given a 4 byte name, data_size int, BitstreamReader 729 and dict of {"atom":handler} sub-parsers, 730 returns an atom of this class""" 731 732 (version, flags, description_count) = reader.parse("8u 24u 32u") 733 descriptions = [] 734 for i in range(description_count): 735 (leaf_size, leaf_name) = reader.parse("32u 4b") 736 descriptions.append( 737 parsers.get(leaf_name, M4A_Leaf_Atom).parse( 738 leaf_name, 739 leaf_size - 8, 740 reader.substream(leaf_size - 8), 741 parsers)) 742 return cls(version=version, 743 flags=flags, 744 descriptions=descriptions) 745 746 def build(self, writer): 747 """writes the atom to the given BitstreamWriter 748 not including its 64-bit size / name header""" 749 750 writer.build("8u 24u 32u", (self.version, 751 self.flags, 752 len(self.descriptions))) 753 754 for description_atom in self.descriptions: 755 writer.build("32u 4b", (description_atom.size() + 8, 756 description_atom.name)) 757 description_atom.build(writer) 758 759 def size(self): 760 """returns the atom's size 761 not including its 64-bit size / name header""" 762 763 return 8 + sum([8 + description_atom.size() 764 for description_atom in self.descriptions]) 765 766 767class M4A_STTS_Atom(M4A_Leaf_Atom): 768 def __init__(self, version, flags, times): 769 self.name = b'stts' 770 self.version = version 771 self.flags = flags 772 self.times = times 773 774 def __repr__(self): 775 return "M4A_STTS_Atom(%s, %s, %s)" % \ 776 (repr(self.version), repr(self.flags), repr(self.times)) 777 778 @classmethod 779 def parse(cls, name, data_size, reader, parsers): 780 """given a 4 byte name, data_size int, BitstreamReader 781 and dict of {"atom":handler} sub-parsers, 782 returns an atom of this class""" 783 784 (version, flags) = reader.parse("8u 24u") 785 return cls(version=version, 786 flags=flags, 787 times=[tuple(reader.parse("32u 32u")) 788 for i in range(reader.read(32))]) 789 790 def build(self, writer): 791 """writes the atom to the given BitstreamWriter 792 not including its 64-bit size / name header""" 793 794 writer.build("8u 24u 32u", (self.version, self.flags, len(self.times))) 795 for time in self.times: 796 writer.build("32u 32u", time) 797 798 def size(self): 799 """returns the atom's size 800 not including its 64-bit size / name header""" 801 802 return 8 + (8 * len(self.times)) 803 804 805class M4A_STSC_Atom(M4A_Leaf_Atom): 806 def __init__(self, version, flags, blocks): 807 self.name = b'stsc' 808 self.version = version 809 self.flags = flags 810 self.blocks = blocks 811 812 def __repr__(self): 813 return "M4A_STSC_Atom(%s, %s, %s)" % \ 814 (repr(self.version), repr(self.flags), repr(self.blocks)) 815 816 @classmethod 817 def parse(cls, name, data_size, reader, parsers): 818 """given a 4 byte name, data_size int, BitstreamReader 819 and dict of {"atom":handler} sub-parsers, 820 returns an atom of this class""" 821 822 (version, flags) = reader.parse("8u 24u") 823 return cls(version=version, 824 flags=flags, 825 blocks=[tuple(reader.parse("32u 32u 32u")) 826 for i in range(reader.read(32))]) 827 828 def build(self, writer): 829 """writes the atom to the given BitstreamWriter 830 not including its 64-bit size / name header""" 831 832 writer.build("8u 24u 32u", 833 (self.version, self.flags, len(self.blocks))) 834 for block in self.blocks: 835 writer.build("32u 32u 32u", block) 836 837 def size(self): 838 """returns the atom's size 839 not including its 64-bit size / name header""" 840 841 return 8 + (12 * len(self.blocks)) 842 843 844class M4A_STSZ_Atom(M4A_Leaf_Atom): 845 def __init__(self, version, flags, byte_size, block_sizes): 846 self.name = b'stsz' 847 self.version = version 848 self.flags = flags 849 self.byte_size = byte_size 850 self.block_sizes = block_sizes 851 852 def __repr__(self): 853 return "M4A_STSZ_Atom(%s, %s, %s, %s)" % \ 854 (repr(self.version), repr(self.flags), repr(self.byte_size), 855 repr(self.block_sizes)) 856 857 @classmethod 858 def parse(cls, name, data_size, reader, parsers): 859 """given a 4 byte name, data_size int, BitstreamReader 860 and dict of {"atom":handler} sub-parsers, 861 returns an atom of this class""" 862 863 (version, flags, byte_size) = reader.parse("8u 24u 32u") 864 return cls(version=version, 865 flags=flags, 866 byte_size=byte_size, 867 block_sizes=[reader.read(32) for i in 868 range(reader.read(32))]) 869 870 def build(self, writer): 871 """writes the atom to the given BitstreamWriter 872 not including its 64-bit size / name header""" 873 874 writer.build("8u 24u 32u 32u", (self.version, 875 self.flags, 876 self.byte_size, 877 len(self.block_sizes))) 878 for size in self.block_sizes: 879 writer.write(32, size) 880 881 def size(self): 882 """returns the atom's size 883 not including its 64-bit size / name header""" 884 885 return 12 + (4 * len(self.block_sizes)) 886 887 888class M4A_STCO_Atom(M4A_Leaf_Atom): 889 def __init__(self, version, flags, offsets): 890 self.name = b'stco' 891 self.version = version 892 self.flags = flags 893 self.offsets = offsets 894 895 def __repr__(self): 896 return "M4A_STCO_Atom(%s, %s, %s)" % \ 897 (self.version, self.flags, self.offsets) 898 899 @classmethod 900 def parse(cls, name, data_size, reader, parsers): 901 """given a 4 byte name, data_size int, BitstreamReader 902 and dict of {"atom":handler} sub-parsers, 903 returns an atom of this class""" 904 905 assert(name == b"stco") 906 (version, flags, offset_count) = reader.parse("8u 24u 32u") 907 return cls(version, flags, 908 [reader.read(32) for i in range(offset_count)]) 909 910 def build(self, writer): 911 """writes the atom to the given BitstreamWriter 912 not including its 64-bit size / name header""" 913 914 writer.build("8u 24u 32u", (self.version, self.flags, 915 len(self.offsets))) 916 for offset in self.offsets: 917 writer.write(32, offset) 918 919 def size(self): 920 """returns the atom's size 921 not including its 64-bit size / name header""" 922 923 return 8 + (4 * len(self.offsets)) 924 925 926class M4A_ALAC_Atom(M4A_Leaf_Atom): 927 def __init__(self, reference_index, qt_version, qt_revision_level, 928 qt_vendor, channels, bits_per_sample, qt_compression_id, 929 audio_packet_size, sample_rate, sub_alac): 930 # assert(isinstance(qt_vendor, bytes)) 931 932 self.name = b'alac' 933 self.reference_index = reference_index 934 self.qt_version = qt_version 935 self.qt_revision_level = qt_revision_level 936 self.qt_vendor = qt_vendor 937 self.channels = channels 938 self.bits_per_sample = bits_per_sample 939 self.qt_compression_id = qt_compression_id 940 self.audio_packet_size = audio_packet_size 941 self.sample_rate = sample_rate 942 self.sub_alac = sub_alac 943 944 def __repr__(self): 945 return "M4A_ALAC_Atom(%s)" % \ 946 ",".join(map(repr, [self.reference_index, 947 self.qt_version, 948 self.qt_revision_level, 949 self.qt_vendor, 950 self.channels, 951 self.bits_per_sample, 952 self.qt_compression_id, 953 self.audio_packet_size, 954 self.sample_rate, 955 self.sub_alac])) 956 957 @classmethod 958 def parse(cls, name, data_size, reader, parsers): 959 """given a 4 byte name, data_size int, BitstreamReader 960 and dict of {"atom":handler} sub-parsers, 961 returns an atom of this class""" 962 963 (reference_index, 964 qt_version, 965 qt_revision_level, 966 qt_vendor, 967 channels, 968 bits_per_sample, 969 qt_compression_id, 970 audio_packet_size, 971 sample_rate) = reader.parse( 972 "6P 16u 16u 16u 4b 16u 16u 16u 16u 32u") 973 (sub_alac_size, sub_alac_name) = reader.parse("32u 4b") 974 sub_alac = M4A_SUB_ALAC_Atom.parse(sub_alac_name, 975 sub_alac_size - 8, 976 reader.substream(sub_alac_size - 8), 977 {}) 978 return cls(reference_index=reference_index, 979 qt_version=qt_version, 980 qt_revision_level=qt_revision_level, 981 qt_vendor=qt_vendor, 982 channels=channels, 983 bits_per_sample=bits_per_sample, 984 qt_compression_id=qt_compression_id, 985 audio_packet_size=audio_packet_size, 986 sample_rate=sample_rate, 987 sub_alac=sub_alac) 988 989 def build(self, writer): 990 """writes the atom to the given BitstreamWriter 991 not including its 64-bit size / name header""" 992 993 writer.build("6P 16u 16u 16u 4b 16u 16u 16u 16u 32u", 994 (self.reference_index, 995 self.qt_version, 996 self.qt_revision_level, 997 self.qt_vendor, 998 self.channels, 999 self.bits_per_sample, 1000 self.qt_compression_id, 1001 self.audio_packet_size, 1002 self.sample_rate)) 1003 writer.build("32u 4b", (self.sub_alac.size() + 8, 1004 self.sub_alac.name)) 1005 self.sub_alac.build(writer) 1006 1007 def size(self): 1008 """returns the atom's size 1009 not including its 64-bit size / name header""" 1010 1011 return 28 + 8 + self.sub_alac.size() 1012 1013 1014class M4A_SUB_ALAC_Atom(M4A_Leaf_Atom): 1015 def __init__(self, max_samples_per_frame, bits_per_sample, 1016 history_multiplier, initial_history, maximum_k, 1017 channels, unknown, max_coded_frame_size, bitrate, 1018 sample_rate): 1019 self.name = b'alac' 1020 self.max_samples_per_frame = max_samples_per_frame 1021 self.bits_per_sample = bits_per_sample 1022 self.history_multiplier = history_multiplier 1023 self.initial_history = initial_history 1024 self.maximum_k = maximum_k 1025 self.channels = channels 1026 self.unknown = unknown 1027 self.max_coded_frame_size = max_coded_frame_size 1028 self.bitrate = bitrate 1029 self.sample_rate = sample_rate 1030 1031 def __repr__(self): 1032 return "M4A_SUB_ALAC_Atom(%s)" % \ 1033 (",".join(map(repr, [self.max_samples_per_frame, 1034 self.bits_per_sample, 1035 self.history_multiplier, 1036 self.initial_history, 1037 self.maximum_k, 1038 self.channels, 1039 self.unknown, 1040 self.max_coded_frame_size, 1041 self.bitrate, 1042 self.sample_rate]))) 1043 1044 @classmethod 1045 def parse(cls, name, data_size, reader, parsers): 1046 """given a 4 byte name, data_size int, BitstreamReader 1047 and dict of {"atom":handler} sub-parsers, 1048 returns an atom of this class""" 1049 1050 return cls( 1051 *reader.parse( 1052 "4P 32u 8p 8u 8u 8u 8u 8u 16u 32u 32u 32u")) 1053 1054 def build(self, writer): 1055 """writes the atom to the given BitstreamWriter 1056 not including its 64-bit size / name header""" 1057 1058 writer.build("4P 32u 8p 8u 8u 8u 8u 8u 16u 32u 32u 32u", 1059 (self.max_samples_per_frame, 1060 self.bits_per_sample, 1061 self.history_multiplier, 1062 self.initial_history, 1063 self.maximum_k, 1064 self.channels, 1065 self.unknown, 1066 self.max_coded_frame_size, 1067 self.bitrate, 1068 self.sample_rate)) 1069 1070 def size(self): 1071 """returns the atom's size 1072 not including its 64-bit size / name header""" 1073 1074 return 28 1075 1076 1077class M4A_META_Atom(MetaData, M4A_Tree_Atom): 1078 UNICODE_ATTRIB_TO_ILST = {"track_name": b"\xa9nam", 1079 "album_name": b"\xa9alb", 1080 "artist_name": b"\xa9ART", 1081 "composer_name": b"\xa9wrt", 1082 "copyright": b"cprt", 1083 "year": b"\xa9day", 1084 "comment": b"\xa9cmt"} 1085 1086 INT_ATTRIB_TO_ILST = {"track_number": b"trkn", 1087 "album_number": b"disk"} 1088 1089 TOTAL_ATTRIB_TO_ILST = {"track_total": b"trkn", 1090 "album_total": b"disk"} 1091 1092 def __init__(self, version, flags, leaf_atoms): 1093 M4A_Tree_Atom.__init__(self, b"meta", leaf_atoms) 1094 MetaData.__setattr__(self, "version", version) 1095 MetaData.__setattr__(self, "flags", flags) 1096 1097 def __repr__(self): 1098 return "M4A_META_Atom(%s, %s, %s)" % \ 1099 (repr(self.version), repr(self.flags), repr(self.leaf_atoms)) 1100 1101 def has_ilst_atom(self): 1102 """returns True if this atom contains an ILST sub-atom""" 1103 1104 for a in self.leaf_atoms: 1105 if a.name == b'ilst': 1106 return True 1107 else: 1108 return False 1109 1110 def ilst_atom(self): 1111 """returns the first ILST sub-atom, or None""" 1112 1113 for a in self.leaf_atoms: 1114 if a.name == b'ilst': 1115 return a 1116 else: 1117 return None 1118 1119 def add_ilst_atom(self): 1120 """place new ILST atom after the first HDLR atom, if any""" 1121 1122 for (index, atom) in enumerate(self.leaf_atoms): 1123 if atom.name == b'hdlr': 1124 self.leaf_atoms.insert(index, M4A_Tree_Atom(b'ilst', [])) 1125 break 1126 else: 1127 self.leaf_atoms.append(M4A_Tree_Atom(b'ilst', [])) 1128 1129 def raw_info(self): 1130 """returns a Unicode string of low-level MetaData information 1131 1132 whereas __unicode__ is meant to contain complete information 1133 at a very high level 1134 raw_info() should be more developer-specific and with 1135 very little adjustment or reordering to the data itself 1136 """ 1137 1138 from os import linesep 1139 1140 if self.has_ilst_atom(): 1141 comment_lines = [u"M4A:"] 1142 1143 for atom in self.ilst_atom(): 1144 if hasattr(atom, "raw_info_lines"): 1145 comment_lines.extend(atom.raw_info_lines()) 1146 else: 1147 comment_lines.append(u"%s : (%d bytes)" % 1148 (atom.name.decode('ascii', 'replace'), 1149 atom.size())) 1150 1151 return linesep.join(comment_lines) 1152 else: 1153 return u"" 1154 1155 @classmethod 1156 def parse(cls, name, data_size, reader, parsers): 1157 """given a 4 byte name, data_size int, BitstreamReader 1158 and dict of {"atom":handler} sub-parsers, 1159 returns an atom of this class""" 1160 1161 assert(name == b"meta") 1162 (version, flags) = reader.parse("8u 24u") 1163 return cls(version, flags, 1164 parse_sub_atoms(data_size - 4, reader, parsers)) 1165 1166 def build(self, writer): 1167 """writes the atom to the given BitstreamWriter 1168 not including its 64-bit size / name header""" 1169 1170 writer.build("8u 24u", (self.version, self.flags)) 1171 for sub_atom in self: 1172 writer.build("32u 4b", (sub_atom.size() + 8, sub_atom.name)) 1173 sub_atom.build(writer) 1174 1175 def size(self): 1176 """returns the atom's size 1177 not including its 64-bit size / name header""" 1178 1179 return 4 + sum([8 + sub_atom.size() for sub_atom in self]) 1180 1181 def __getattr__(self, attr): 1182 if attr in self.UNICODE_ATTRIB_TO_ILST: 1183 if self.has_ilst_atom(): 1184 try: 1185 return self.ilst_atom()[ 1186 self.UNICODE_ATTRIB_TO_ILST[attr]][b'data'].__unicode__() 1187 except KeyError: 1188 return None 1189 else: 1190 return None 1191 elif attr in self.INT_ATTRIB_TO_ILST: 1192 if self.has_ilst_atom(): 1193 try: 1194 return self.ilst_atom()[ 1195 self.INT_ATTRIB_TO_ILST[attr]][b'data'].number() 1196 except KeyError: 1197 return None 1198 else: 1199 return None 1200 elif attr in self.TOTAL_ATTRIB_TO_ILST: 1201 if self.has_ilst_atom(): 1202 try: 1203 return self.ilst_atom()[ 1204 self.TOTAL_ATTRIB_TO_ILST[attr]][b'data'].total() 1205 except KeyError: 1206 return None 1207 else: 1208 return None 1209 elif attr in self.FIELDS: 1210 return None 1211 else: 1212 raise AttributeError(attr) 1213 1214 def __setattr__(self, attr, value): 1215 def new_data_atom(attribute, value): 1216 if attribute in self.UNICODE_ATTRIB_TO_ILST: 1217 return M4A_ILST_Unicode_Data_Atom(0, 1, value.encode('utf-8')) 1218 elif attribute == "track_number": 1219 return M4A_ILST_TRKN_Data_Atom(int(value), 0) 1220 elif attribute == "track_total": 1221 return M4A_ILST_TRKN_Data_Atom(0, int(value)) 1222 elif attribute == "album_number": 1223 return M4A_ILST_DISK_Data_Atom(int(value), 0) 1224 elif attribute == "album_total": 1225 return M4A_ILST_DISK_Data_Atom(0, int(value)) 1226 else: 1227 raise ValueError(value) 1228 1229 def replace_data_atom(attribute, parent_atom, value): 1230 new_leaf_atoms = [] 1231 data_replaced = False 1232 for leaf_atom in parent_atom.leaf_atoms: 1233 if (leaf_atom.name == b'data') and (not data_replaced): 1234 if attribute == "track_number": 1235 new_leaf_atoms.append( 1236 M4A_ILST_TRKN_Data_Atom(int(value), 1237 leaf_atom.track_total)) 1238 elif attribute == "track_total": 1239 new_leaf_atoms.append( 1240 M4A_ILST_TRKN_Data_Atom(leaf_atom.track_number, 1241 int(value))) 1242 elif attribute == "album_number": 1243 new_leaf_atoms.append( 1244 M4A_ILST_DISK_Data_Atom(int(value), 1245 leaf_atom.disk_total)) 1246 elif attribute == "album_total": 1247 new_leaf_atoms.append( 1248 M4A_ILST_DISK_Data_Atom(leaf_atom.disk_number, 1249 int(value))) 1250 else: 1251 new_leaf_atoms.append(new_data_atom(attribute, value)) 1252 1253 data_replaced = True 1254 else: 1255 new_leaf_atoms.append(leaf_atom) 1256 1257 parent_atom.leaf_atoms = new_leaf_atoms 1258 1259 if value is None: 1260 return delattr(self, attr) 1261 1262 ilst_leaf = self.UNICODE_ATTRIB_TO_ILST.get( 1263 attr, 1264 self.INT_ATTRIB_TO_ILST.get( 1265 attr, 1266 self.TOTAL_ATTRIB_TO_ILST.get( 1267 attr, 1268 None))) 1269 1270 if ilst_leaf is not None: 1271 if not self.has_ilst_atom(): 1272 self.add_ilst_atom() 1273 1274 # an ilst atom is present, so check its sub-atoms 1275 for ilst_atom in self.ilst_atom(): 1276 if ilst_atom.name == ilst_leaf: 1277 # atom already present, so adjust its data sub-atom 1278 replace_data_atom(attr, ilst_atom, value) 1279 break 1280 else: 1281 # atom not present, so append new parent and data sub-atom 1282 self.ilst_atom().add_child( 1283 M4A_ILST_Leaf_Atom(ilst_leaf, 1284 [new_data_atom(attr, value)])) 1285 else: 1286 # attribute is not an atom, so pass it through 1287 MetaData.__setattr__(self, attr, value) 1288 1289 def __delattr__(self, attr): 1290 if self.has_ilst_atom(): 1291 ilst_atom = self.ilst_atom() 1292 1293 if attr in self.UNICODE_ATTRIB_TO_ILST: 1294 ilst_atom.leaf_atoms = [ 1295 atom for atom in ilst_atom if 1296 atom.name != self.UNICODE_ATTRIB_TO_ILST[attr]] 1297 elif attr == "track_number": 1298 if self.track_total is None: 1299 # if track_number and track_total are both 0 1300 # remove trkn atom 1301 ilst_atom.leaf_atoms = [ 1302 atom for atom in ilst_atom if 1303 atom.name != b"trkn"] 1304 else: 1305 self.track_number = 0 1306 elif attr == "track_total": 1307 if self.track_number is None: 1308 # if track_number and track_total are both 0 1309 # remove trkn atom 1310 ilst_atom.leaf_atoms = [ 1311 atom for atom in ilst_atom if 1312 atom.name != b"trkn"] 1313 else: 1314 self.track_total = 0 1315 elif attr == "album_number": 1316 if self.album_total is None: 1317 # if album_number and album_total are both 0 1318 # remove disk atom 1319 ilst_atom.leaf_atoms = [ 1320 atom for atom in ilst_atom if 1321 atom.name != b"disk"] 1322 else: 1323 self.album_number = 0 1324 elif attr == "album_total": 1325 if self.album_number is None: 1326 # if album_number and album_total are both 0 1327 # remove disk atom 1328 ilst_atom.leaf_atoms = [ 1329 atom for atom in ilst_atom if 1330 atom.name != b"disk"] 1331 else: 1332 self.album_total = 0 1333 else: 1334 MetaData.__delattr__(self, attr) 1335 1336 def images(self): 1337 """returns a list of embedded Image objects""" 1338 1339 if self.has_ilst_atom(): 1340 return [atom[b'data'] for atom in self.ilst_atom() 1341 if ((atom.name == b'covr') and (atom.has_child(b'data')))] 1342 else: 1343 return [] 1344 1345 def add_image(self, image): 1346 """embeds an Image object in this metadata""" 1347 1348 def not_cover(atom): 1349 return not ((atom.name == b'covr') and (atom.has_child(b'data'))) 1350 1351 if not self.has_ilst_atom(): 1352 self.add_ilst_atom() 1353 1354 ilst_atom = self.ilst_atom() 1355 1356 # filter out old cover image before adding new one 1357 ilst_atom.leaf_atoms = ( 1358 [atom for atom in ilst_atom if not_cover(atom)] + 1359 [M4A_ILST_Leaf_Atom(b'covr', [M4A_ILST_COVR_Data_Atom.converted( 1360 image)])]) 1361 1362 def delete_image(self, image): 1363 """deletes an Image object from this metadata""" 1364 1365 if self.has_ilst_atom(): 1366 ilst_atom = self.ilst_atom() 1367 1368 ilst_atom.leaf_atoms = [ 1369 atom for atom in ilst_atom if 1370 not ((atom.name == b'covr') and 1371 (atom.has_child(b'data')) and 1372 (atom[b'data'].data == image.data))] 1373 1374 @classmethod 1375 def converted(cls, metadata): 1376 """converts metadata from another class to this one, if necessary 1377 1378 takes a MetaData-compatible object (or None) 1379 and returns a new MetaData subclass with the data fields converted""" 1380 1381 if metadata is None: 1382 return None 1383 elif isinstance(metadata, cls): 1384 return cls(metadata.version, 1385 metadata.flags, 1386 [leaf.copy() for leaf in metadata]) 1387 1388 ilst_atoms = [ 1389 M4A_ILST_Leaf_Atom( 1390 cls.UNICODE_ATTRIB_TO_ILST[attrib], 1391 [M4A_ILST_Unicode_Data_Atom(0, 1, value.encode('utf-8'))]) 1392 for (attrib, value) in metadata.filled_fields() 1393 if (attrib in cls.UNICODE_ATTRIB_TO_ILST)] 1394 1395 if (((metadata.track_number is not None) or 1396 (metadata.track_total is not None))): 1397 ilst_atoms.append( 1398 M4A_ILST_Leaf_Atom( 1399 b'trkn', 1400 [M4A_ILST_TRKN_Data_Atom(metadata.track_number if 1401 (metadata.track_number 1402 is not None) else 0, 1403 metadata.track_total if 1404 (metadata.track_total 1405 is not None) else 0)])) 1406 1407 if (((metadata.album_number is not None) or 1408 (metadata.album_total is not None))): 1409 ilst_atoms.append( 1410 M4A_ILST_Leaf_Atom( 1411 b'disk', 1412 [M4A_ILST_DISK_Data_Atom(metadata.album_number if 1413 (metadata.album_number 1414 is not None) else 0, 1415 metadata.album_total if 1416 (metadata.album_total 1417 is not None) else 0)])) 1418 1419 if len(metadata.front_covers()) > 0: 1420 ilst_atoms.append( 1421 M4A_ILST_Leaf_Atom( 1422 b'covr', 1423 [M4A_ILST_COVR_Data_Atom.converted( 1424 metadata.front_covers()[0])])) 1425 1426 ilst_atoms.append( 1427 M4A_ILST_Leaf_Atom( 1428 b'cpil', 1429 [M4A_Leaf_Atom(b'data', 1430 b'\x00\x00\x00\x15\x00\x00\x00\x00\x01')])) 1431 1432 return cls(0, 0, [M4A_HDLR_Atom(0, 0, b'\x00\x00\x00\x00', 1433 b'mdir', b'appl', 0, 0, b'', 0), 1434 M4A_Tree_Atom(b'ilst', ilst_atoms), 1435 M4A_FREE_Atom(1024)]) 1436 1437 @classmethod 1438 def supports_images(self): 1439 """returns True""" 1440 1441 return True 1442 1443 def clean(self): 1444 """returns a new MetaData object that's been cleaned of problems 1445 1446 any fixes performed are appended to fixes_performed as Unicode""" 1447 1448 fixes_performed = [] 1449 1450 def cleaned_atom(atom): 1451 # numerical fields are stored in bytes, 1452 # so no leading zeroes are possible 1453 1454 # image fields don't store metadata, 1455 # so no field problems are possible there either 1456 1457 from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE, 1458 CLEAN_REMOVE_EMPTY_TAG) 1459 1460 if atom.name in self.UNICODE_ATTRIB_TO_ILST.values(): 1461 text = atom[b'data'].data.decode('utf-8') 1462 fix1 = text.rstrip() 1463 if fix1 != text: 1464 fixes_performed.append( 1465 CLEAN_REMOVE_TRAILING_WHITESPACE % 1466 {"field": atom.name.lstrip(b'\xa9').decode('ascii')}) 1467 fix2 = fix1.lstrip() 1468 if fix2 != fix1: 1469 from audiotools.text import CLEAN_REMOVE_LEADING_WHITESPACE 1470 fixes_performed.append( 1471 CLEAN_REMOVE_LEADING_WHITESPACE % 1472 {"field": atom.name.lstrip(b'\xa9').decode('ascii')}) 1473 if len(fix2) > 0: 1474 return M4A_ILST_Leaf_Atom( 1475 atom.name, 1476 [M4A_ILST_Unicode_Data_Atom(0, 1, 1477 fix2.encode('utf-8'))]) 1478 else: 1479 fixes_performed.append( 1480 CLEAN_REMOVE_EMPTY_TAG % 1481 {"field": atom.name.lstrip(b'\xa9').decode('ascii')}) 1482 return None 1483 else: 1484 return atom 1485 1486 if self.has_ilst_atom(): 1487 return (M4A_META_Atom( 1488 self.version, 1489 self.flags, 1490 [M4A_Tree_Atom(b'ilst', 1491 [atom for atom in 1492 map(cleaned_atom, self.ilst_atom()) 1493 if atom is not None])]), 1494 fixes_performed) 1495 else: 1496 # if no ilst atom, return a copy of the meta atom as-is 1497 return (M4A_META_Atom( 1498 self.version, 1499 self.flags, 1500 [M4A_Tree_Atom(b'ilst', 1501 [atom.copy() for atom in self.ilst_atom()])]), 1502 []) 1503 1504 1505class M4A_ILST_Leaf_Atom(M4A_Tree_Atom): 1506 def copy(self): 1507 """returns a newly copied instance of this atom 1508 and new instances of any sub-atoms it contains""" 1509 1510 return M4A_ILST_Leaf_Atom(self.name, [leaf.copy() for leaf in self]) 1511 1512 def __repr__(self): 1513 return "M4A_ILST_Leaf_Atom(%s, %s)" % \ 1514 (repr(self.name), repr(self.leaf_atoms)) 1515 1516 @classmethod 1517 def parse(cls, name, data_size, reader, parsers): 1518 """given a 4 byte name, data_size int, BitstreamReader 1519 and dict of {"atom":handler} sub-parsers, 1520 returns an atom of this class""" 1521 1522 return cls( 1523 name, 1524 parse_sub_atoms(data_size, reader, 1525 {b"data": {b"\xa9alb": M4A_ILST_Unicode_Data_Atom, 1526 b"\xa9ART": M4A_ILST_Unicode_Data_Atom, 1527 b"\xa9cmt": M4A_ILST_Unicode_Data_Atom, 1528 b"cprt": M4A_ILST_Unicode_Data_Atom, 1529 b"\xa9day": M4A_ILST_Unicode_Data_Atom, 1530 b"\xa9grp": M4A_ILST_Unicode_Data_Atom, 1531 b"\xa9nam": M4A_ILST_Unicode_Data_Atom, 1532 b"\xa9too": M4A_ILST_Unicode_Data_Atom, 1533 b"\xa9wrt": M4A_ILST_Unicode_Data_Atom, 1534 b'aART': M4A_ILST_Unicode_Data_Atom, 1535 b"covr": M4A_ILST_COVR_Data_Atom, 1536 b"trkn": M4A_ILST_TRKN_Data_Atom, 1537 b"disk": M4A_ILST_DISK_Data_Atom 1538 }.get(name, M4A_Leaf_Atom)})) 1539 1540 if sys.version_info[0] >= 3: 1541 def __str__(self): 1542 return self.__unicode__() 1543 else: 1544 def __str__(self): 1545 return self.__unicode__().encode('utf-8') 1546 1547 def __unicode__(self): 1548 try: 1549 return [l for l in self.leaf_atoms 1550 if l.name == b'data'][0].__unicode__() 1551 except IndexError: 1552 return u"" 1553 1554 def raw_info_lines(self): 1555 """yields lines of human-readable information about the atom""" 1556 1557 for leaf_atom in self.leaf_atoms: 1558 name = self.name.replace(b"\xa9", b" ").decode('ascii') 1559 if hasattr(leaf_atom, "raw_info"): 1560 yield u"%s : %s" % (name, leaf_atom.raw_info()) 1561 else: 1562 yield u"%s : %s" % (name, repr(leaf_atom)) # FIXME 1563 1564 1565class M4A_ILST_Unicode_Data_Atom(M4A_Leaf_Atom): 1566 def __init__(self, type, flags, data): 1567 # assert(isinstance(data, bytes)) 1568 1569 self.name = b"data" 1570 self.type = type 1571 self.flags = flags 1572 self.data = data 1573 1574 def copy(self): 1575 """returns a newly copied instance of this atom 1576 and new instances of any sub-atoms it contains""" 1577 1578 return M4A_ILST_Unicode_Data_Atom(self.type, self.flags, self.data) 1579 1580 def __repr__(self): 1581 return "M4A_ILST_Unicode_Data_Atom(%s, %s, %s)" % \ 1582 (repr(self.type), repr(self.flags), repr(self.data)) 1583 1584 def __eq__(self, atom): 1585 for attr in ["type", "flags", "data"]: 1586 if ((not hasattr(atom, attr)) or (getattr(self, attr) != 1587 getattr(atom, attr))): 1588 return False 1589 else: 1590 return True 1591 1592 def raw_info(self): 1593 """returns a line of human-readable information about the atom""" 1594 1595 return self.data.decode('utf-8') 1596 1597 @classmethod 1598 def parse(cls, name, data_size, reader, parsers): 1599 """given a 4 byte name, data_size int, BitstreamReader 1600 and dict of {"atom":handler} sub-parsers, 1601 returns an atom of this class""" 1602 1603 assert(name == b"data") 1604 (type, flags) = reader.parse("8u 24u 32p") 1605 return cls(type, flags, reader.read_bytes(data_size - 8)) 1606 1607 def build(self, writer): 1608 """writes the atom to the given BitstreamWriter 1609 not including its 64-bit size / name header""" 1610 1611 writer.build("8u 24u 32p %db" % (len(self.data)), 1612 (self.type, self.flags, self.data)) 1613 1614 def size(self): 1615 """returns the atom's size 1616 not including its 64-bit size / name header""" 1617 1618 return 8 + len(self.data) 1619 1620 if sys.version_info[0] >= 3: 1621 def __str__(self): 1622 return self.__unicode__() 1623 else: 1624 def __str__(self): 1625 return self.__unicode__().encode('utf-8') 1626 1627 def __unicode__(self): 1628 return self.data.decode('utf-8') 1629 1630 1631class M4A_ILST_TRKN_Data_Atom(M4A_Leaf_Atom): 1632 def __init__(self, track_number, track_total): 1633 self.name = b"data" 1634 self.track_number = track_number 1635 self.track_total = track_total 1636 1637 def copy(self): 1638 """returns a newly copied instance of this atom 1639 and new instances of any sub-atoms it contains""" 1640 1641 return M4A_ILST_TRKN_Data_Atom(self.track_number, self.track_total) 1642 1643 def __repr__(self): 1644 return "M4A_ILST_TRKN_Data_Atom(%d, %d)" % \ 1645 (self.track_number, self.track_total) 1646 1647 def __eq__(self, atom): 1648 for attr in ["track_number", "track_total"]: 1649 if ((not hasattr(atom, attr)) or (getattr(self, attr) != 1650 getattr(atom, attr))): 1651 return False 1652 else: 1653 return True 1654 1655 if sys.version_info[0] >= 3: 1656 def __str__(self): 1657 return self.__unicode__() 1658 else: 1659 def __str__(self): 1660 return self.__unicode__().encode('utf-8') 1661 1662 def __unicode__(self): 1663 if self.track_total > 0: 1664 return u"%d/%d" % (self.track_number, self.track_total) 1665 else: 1666 return u"%d" % (self.track_number,) 1667 1668 def raw_info(self): 1669 """returns a line of human-readable information about the atom""" 1670 1671 return u"%d/%d" % (self.track_number, self.track_total) 1672 1673 @classmethod 1674 def parse(cls, name, data_size, reader, parsers): 1675 """given a 4 byte name, data_size int, BitstreamReader 1676 and dict of {"atom":handler} sub-parsers, 1677 returns an atom of this class""" 1678 1679 assert(name == b"data") 1680 # FIXME - handle mis-sized TRKN data atoms 1681 return cls(*reader.parse("64p 16p 16u 16u 16p")) 1682 1683 def build(self, writer): 1684 """writes the atom to the given BitstreamWriter 1685 not including its 64-bit size / name header""" 1686 1687 writer.build("64p 16p 16u 16u 16p", 1688 (self.track_number, self.track_total)) 1689 1690 def size(self): 1691 """returns the atom's size 1692 not including its 64-bit size / name header""" 1693 1694 return 16 1695 1696 def number(self): 1697 """returns this atom's track_number field 1698 or None if the field is 0""" 1699 1700 if self.track_number != 0: 1701 return self.track_number 1702 else: 1703 return None 1704 1705 def total(self): 1706 """returns this atom's track_total field 1707 or None if the field is 0""" 1708 1709 if self.track_total != 0: 1710 return self.track_total 1711 else: 1712 return None 1713 1714 1715class M4A_ILST_DISK_Data_Atom(M4A_Leaf_Atom): 1716 def __init__(self, disk_number, disk_total): 1717 self.name = b"data" 1718 self.disk_number = disk_number 1719 self.disk_total = disk_total 1720 1721 def copy(self): 1722 """returns a newly copied instance of this atom 1723 and new instances of any sub-atoms it contains""" 1724 1725 return M4A_ILST_DISK_Data_Atom(self.disk_number, self.disk_total) 1726 1727 def __repr__(self): 1728 return "M4A_ILST_DISK_Data_Atom(%d, %d)" % \ 1729 (self.disk_number, self.disk_total) 1730 1731 def __eq__(self, atom): 1732 for attr in ["disk_number", "disk_total"]: 1733 if ((not hasattr(atom, attr)) or (getattr(self, attr) != 1734 getattr(atom, attr))): 1735 return False 1736 else: 1737 return True 1738 1739 if sys.version_info[0] >= 3: 1740 def __str__(self): 1741 return self.__unicode__() 1742 else: 1743 def __str__(self): 1744 return self.__unicode__().encode('utf-8') 1745 1746 def __unicode__(self): 1747 if self.disk_total > 0: 1748 return u"%d/%d" % (self.disk_number, self.disk_total) 1749 else: 1750 return u"%d" % (self.disk_number,) 1751 1752 def raw_info(self): 1753 """returns a line of human-readable information about the atom""" 1754 1755 return u"%d/%d" % (self.disk_number, self.disk_total) 1756 1757 @classmethod 1758 def parse(cls, name, data_size, reader, parsers): 1759 """given a 4 byte name, data_size int, BitstreamReader 1760 and dict of {"atom":handler} sub-parsers, 1761 returns an atom of this class""" 1762 1763 assert(name == b"data") 1764 # FIXME - handle mis-sized DISK data atoms 1765 return cls(*reader.parse("64p 16p 16u 16u")) 1766 1767 def build(self, writer): 1768 """writes the atom to the given BitstreamWriter 1769 not including its 64-bit size / name header""" 1770 1771 writer.build("64p 16p 16u 16u", 1772 (self.disk_number, self.disk_total)) 1773 1774 def size(self): 1775 """returns the atom's size 1776 not including its 64-bit size / name header""" 1777 1778 return 14 1779 1780 def number(self): 1781 """returns this atom's disc_number field""" 1782 1783 if self.disk_number != 0: 1784 return self.disk_number 1785 else: 1786 return None 1787 1788 def total(self): 1789 """returns this atom's disk_total field""" 1790 1791 if self.disk_total != 0: 1792 return self.disk_total 1793 else: 1794 return None 1795 1796 1797class M4A_ILST_COVR_Data_Atom(Image, M4A_Leaf_Atom): 1798 def __init__(self, version, flags, image_data): 1799 # assert(isinstance(image_data, bytes)) 1800 1801 self.version = version 1802 self.flags = flags 1803 self.name = b"data" 1804 1805 img = image_metrics(image_data) 1806 Image.__init__(self, 1807 data=image_data, 1808 mime_type=img.mime_type, 1809 width=img.width, 1810 height=img.height, 1811 color_depth=img.bits_per_pixel, 1812 color_count=img.color_count, 1813 description=u"", 1814 type=0) 1815 1816 def copy(self): 1817 """returns a newly copied instance of this atom 1818 and new instances of any sub-atoms it contains""" 1819 1820 return M4A_ILST_COVR_Data_Atom(self.version, self.flags, self.data) 1821 1822 def __repr__(self): 1823 return "M4A_ILST_COVR_Data_Atom(%s, %s, ...)" % \ 1824 (self.version, self.flags) 1825 1826 def raw_info(self): 1827 """returns a line of human-readable information about the atom""" 1828 1829 from audiotools import hex_string 1830 1831 if len(self.data) > 20: 1832 return (u"(%d bytes) %s\u2026" % (len(self.data), 1833 hex_string(self.data[0:20]))) 1834 else: 1835 return (u"(%d bytes) %s" % (len(self.data), hex_string(self.data))) 1836 1837 @classmethod 1838 def parse(cls, name, data_size, reader, parsers): 1839 """given a 4 byte name, data_size int, BitstreamReader 1840 and dict of {"atom":handler} sub-parsers, 1841 returns an atom of this class""" 1842 1843 assert(name == b"data") 1844 (version, flags) = reader.parse("8u 24u 32p") 1845 return cls(version, flags, reader.read_bytes(data_size - 8)) 1846 1847 def build(self, writer): 1848 """writes the atom to the given BitstreamWriter 1849 not including its 64-bit size / name header""" 1850 1851 writer.build("8u 24u 32p %db" % (len(self.data)), 1852 (self.version, self.flags, self.data)) 1853 1854 def size(self): 1855 """returns the atom's size 1856 not including its 64-bit size / name header""" 1857 1858 return 8 + len(self.data) 1859 1860 @classmethod 1861 def converted(cls, image): 1862 """given an Image-compatible object, 1863 returns a new M4A_ILST_COVR_Data_Atom object""" 1864 1865 return cls(0, 0, image.data) 1866 1867 1868class M4A_HDLR_Atom(M4A_Leaf_Atom): 1869 def __init__(self, version, flags, qt_type, qt_subtype, 1870 qt_manufacturer, qt_reserved_flags, qt_reserved_flags_mask, 1871 component_name, padding_size): 1872 # assert(isinstance(qt_type, bytes)) 1873 # assert(isinstance(qt_subtype, bytes)) 1874 # assert(isinstance(qt_manufacturer, bytes)) 1875 1876 self.name = b'hdlr' 1877 self.version = version 1878 self.flags = flags 1879 self.qt_type = qt_type 1880 self.qt_subtype = qt_subtype 1881 self.qt_manufacturer = qt_manufacturer 1882 self.qt_reserved_flags = qt_reserved_flags 1883 self.qt_reserved_flags_mask = qt_reserved_flags_mask 1884 self.component_name = component_name 1885 self.padding_size = padding_size 1886 1887 def copy(self): 1888 """returns a newly copied instance of this atom 1889 and new instances of any sub-atoms it contains""" 1890 1891 return M4A_HDLR_Atom(self.version, 1892 self.flags, 1893 self.qt_type, 1894 self.qt_subtype, 1895 self.qt_manufacturer, 1896 self.qt_reserved_flags, 1897 self.qt_reserved_flags_mask, 1898 self.component_name, 1899 self.padding_size) 1900 1901 def __repr__(self): 1902 return "M4A_HDLR_Atom(%s, %s, %s, %s, %s, %s, %s, %s, %d)" % \ 1903 (self.version, self.flags, repr(self.qt_type), 1904 repr(self.qt_subtype), repr(self.qt_manufacturer), 1905 self.qt_reserved_flags, self.qt_reserved_flags_mask, 1906 repr(self.component_name), self.padding_size) 1907 1908 @classmethod 1909 def parse(cls, name, data_size, reader, parsers): 1910 """given a 4 byte name, data_size int, BitstreamReader 1911 and dict of {"atom":handler} sub-parsers, 1912 returns an atom of this class""" 1913 1914 assert(name == b'hdlr') 1915 (version, 1916 flags, 1917 qt_type, 1918 qt_subtype, 1919 qt_manufacturer, 1920 qt_reserved_flags, 1921 qt_reserved_flags_mask) = reader.parse( 1922 "8u 24u 4b 4b 4b 32u 32u") 1923 component_name = reader.read_bytes(reader.read(8)) 1924 return cls(version, flags, qt_type, qt_subtype, 1925 qt_manufacturer, qt_reserved_flags, 1926 qt_reserved_flags_mask, component_name, 1927 data_size - len(component_name) - 25) 1928 1929 def build(self, writer): 1930 """writes the atom to the given BitstreamWriter 1931 not including its 64-bit size / name header""" 1932 1933 writer.build("8u 24u 4b 4b 4b 32u 32u 8u %db %dP" % 1934 (len(self.component_name), 1935 self.padding_size), 1936 (self.version, 1937 self.flags, 1938 self.qt_type, 1939 self.qt_subtype, 1940 self.qt_manufacturer, 1941 self.qt_reserved_flags, 1942 self.qt_reserved_flags_mask, 1943 len(self.component_name), 1944 self.component_name)) 1945 1946 def size(self): 1947 """returns the atom's size 1948 not including its 64-bit size / name header""" 1949 1950 return 25 + len(self.component_name) + self.padding_size 1951 1952 1953class M4A_FREE_Atom(M4A_Leaf_Atom): 1954 def __init__(self, bytes): 1955 self.name = b"free" 1956 self.bytes = bytes 1957 1958 def copy(self): 1959 """returns a newly copied instance of this atom 1960 and new instances of any sub-atoms it contains""" 1961 1962 return M4A_FREE_Atom(self.bytes) 1963 1964 def __repr__(self): 1965 return "M4A_FREE_Atom(%d)" % (self.bytes) 1966 1967 @classmethod 1968 def parse(cls, name, data_size, reader, parsers): 1969 """given a 4 byte name, data_size int, BitstreamReader 1970 and dict of {"atom":handler} sub-parsers, 1971 returns an atom of this class""" 1972 1973 assert(name == b"free") 1974 reader.skip_bytes(data_size) 1975 return cls(data_size) 1976 1977 def build(self, writer): 1978 """writes the atom to the given BitstreamWriter 1979 not including its 64-bit size / name header""" 1980 1981 writer.write_bytes(b"\x00" * self.bytes) 1982 1983 def size(self): 1984 """returns the atom's size 1985 not including its 64-bit size / name header""" 1986 1987 return self.bytes 1988