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