1"""Stuff to parse AIFF-C and AIFF files.
2
3Unless explicitly stated otherwise, the description below is true
4both for AIFF-C files and AIFF files.
5
6An AIFF-C file has the following structure.
7
8  +-----------------+
9  | FORM            |
10  +-----------------+
11  | <size>          |
12  +----+------------+
13  |    | AIFC       |
14  |    +------------+
15  |    | <chunks>   |
16  |    |    .       |
17  |    |    .       |
18  |    |    .       |
19  +----+------------+
20
21An AIFF file has the string "AIFF" instead of "AIFC".
22
23A chunk consists of an identifier (4 bytes) followed by a size (4 bytes,
24big endian order), followed by the data.  The size field does not include
25the size of the 8 byte header.
26
27The following chunk types are recognized.
28
29  FVER
30      <version number of AIFF-C defining document> (AIFF-C only).
31  MARK
32      <# of markers> (2 bytes)
33      list of markers:
34          <marker ID> (2 bytes, must be > 0)
35          <position> (4 bytes)
36          <marker name> ("pstring")
37  COMM
38      <# of channels> (2 bytes)
39      <# of sound frames> (4 bytes)
40      <size of the samples> (2 bytes)
41      <sampling frequency> (10 bytes, IEEE 80-bit extended
42          floating point)
43      in AIFF-C files only:
44      <compression type> (4 bytes)
45      <human-readable version of compression type> ("pstring")
46  SSND
47      <offset> (4 bytes, not used by this program)
48      <blocksize> (4 bytes, not used by this program)
49      <sound data>
50
51A pstring consists of 1 byte length, a string of characters, and 0 or 1
52byte pad to make the total length even.
53
54Usage.
55
56Reading AIFF files:
57  f = aifc.open(file, 'r')
58where file is either the name of a file or an open file pointer.
59The open file pointer must have methods read(), seek(), and close().
60In some types of audio files, if the setpos() method is not used,
61the seek() method is not necessary.
62
63This returns an instance of a class with the following public methods:
64  getnchannels()  -- returns number of audio channels (1 for
65             mono, 2 for stereo)
66  getsampwidth()  -- returns sample width in bytes
67  getframerate()  -- returns sampling frequency
68  getnframes()    -- returns number of audio frames
69  getcomptype()   -- returns compression type ('NONE' for AIFF files)
70  getcompname()   -- returns human-readable version of
71             compression type ('not compressed' for AIFF files)
72  getparams() -- returns a namedtuple consisting of all of the
73             above in the above order
74  getmarkers()    -- get the list of marks in the audio file or None
75             if there are no marks
76  getmark(id) -- get mark with the specified id (raises an error
77             if the mark does not exist)
78  readframes(n)   -- returns at most n frames of audio
79  rewind()    -- rewind to the beginning of the audio stream
80  setpos(pos) -- seek to the specified position
81  tell()      -- return the current position
82  close()     -- close the instance (make it unusable)
83The position returned by tell(), the position given to setpos() and
84the position of marks are all compatible and have nothing to do with
85the actual position in the file.
86The close() method is called automatically when the class instance
87is destroyed.
88
89Writing AIFF files:
90  f = aifc.open(file, 'w')
91where file is either the name of a file or an open file pointer.
92The open file pointer must have methods write(), tell(), seek(), and
93close().
94
95This returns an instance of a class with the following public methods:
96  aiff()      -- create an AIFF file (AIFF-C default)
97  aifc()      -- create an AIFF-C file
98  setnchannels(n) -- set the number of channels
99  setsampwidth(n) -- set the sample width
100  setframerate(n) -- set the frame rate
101  setnframes(n)   -- set the number of frames
102  setcomptype(type, name)
103          -- set the compression type and the
104             human-readable compression type
105  setparams(tuple)
106          -- set all parameters at once
107  setmark(id, pos, name)
108          -- add specified mark to the list of marks
109  tell()      -- return current position in output file (useful
110             in combination with setmark())
111  writeframesraw(data)
112          -- write audio frames without pathing up the
113             file header
114  writeframes(data)
115          -- write audio frames and patch up the file header
116  close()     -- patch up the file header and close the
117             output file
118You should set the parameters before the first writeframesraw or
119writeframes.  The total number of frames does not need to be set,
120but when it is set to the correct value, the header does not have to
121be patched up.
122It is best to first set all parameters, perhaps possibly the
123compression type, and then write audio frames using writeframesraw.
124When all frames have been written, either call writeframes(b'') or
125close() to patch up the sizes in the header.
126Marks can be added anytime.  If there are any marks, you must call
127close() after all frames have been written.
128The close() method is called automatically when the class instance
129is destroyed.
130
131When a file is opened with the extension '.aiff', an AIFF file is
132written, otherwise an AIFF-C file is written.  This default can be
133changed by calling aiff() or aifc() before the first writeframes or
134writeframesraw.
135"""
136
137import struct
138import builtins
139import warnings
140
141__all__ = ["Error", "open", "openfp"]
142
143class Error(Exception):
144    pass
145
146_AIFC_version = 0xA2805140     # Version 1 of AIFF-C
147
148def _read_long(file):
149    try:
150        return struct.unpack('>l', file.read(4))[0]
151    except struct.error:
152        raise EOFError
153
154def _read_ulong(file):
155    try:
156        return struct.unpack('>L', file.read(4))[0]
157    except struct.error:
158        raise EOFError
159
160def _read_short(file):
161    try:
162        return struct.unpack('>h', file.read(2))[0]
163    except struct.error:
164        raise EOFError
165
166def _read_ushort(file):
167    try:
168        return struct.unpack('>H', file.read(2))[0]
169    except struct.error:
170        raise EOFError
171
172def _read_string(file):
173    length = ord(file.read(1))
174    if length == 0:
175        data = b''
176    else:
177        data = file.read(length)
178    if length & 1 == 0:
179        dummy = file.read(1)
180    return data
181
182_HUGE_VAL = 1.79769313486231e+308 # See <limits.h>
183
184def _read_float(f): # 10 bytes
185    expon = _read_short(f) # 2 bytes
186    sign = 1
187    if expon < 0:
188        sign = -1
189        expon = expon + 0x8000
190    himant = _read_ulong(f) # 4 bytes
191    lomant = _read_ulong(f) # 4 bytes
192    if expon == himant == lomant == 0:
193        f = 0.0
194    elif expon == 0x7FFF:
195        f = _HUGE_VAL
196    else:
197        expon = expon - 16383
198        f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
199    return sign * f
200
201def _write_short(f, x):
202    f.write(struct.pack('>h', x))
203
204def _write_ushort(f, x):
205    f.write(struct.pack('>H', x))
206
207def _write_long(f, x):
208    f.write(struct.pack('>l', x))
209
210def _write_ulong(f, x):
211    f.write(struct.pack('>L', x))
212
213def _write_string(f, s):
214    if len(s) > 255:
215        raise ValueError("string exceeds maximum pstring length")
216    f.write(struct.pack('B', len(s)))
217    f.write(s)
218    if len(s) & 1 == 0:
219        f.write(b'\x00')
220
221def _write_float(f, x):
222    import math
223    if x < 0:
224        sign = 0x8000
225        x = x * -1
226    else:
227        sign = 0
228    if x == 0:
229        expon = 0
230        himant = 0
231        lomant = 0
232    else:
233        fmant, expon = math.frexp(x)
234        if expon > 16384 or fmant >= 1 or fmant != fmant: # Infinity or NaN
235            expon = sign|0x7FFF
236            himant = 0
237            lomant = 0
238        else:                   # Finite
239            expon = expon + 16382
240            if expon < 0:           # denormalized
241                fmant = math.ldexp(fmant, expon)
242                expon = 0
243            expon = expon | sign
244            fmant = math.ldexp(fmant, 32)
245            fsmant = math.floor(fmant)
246            himant = int(fsmant)
247            fmant = math.ldexp(fmant - fsmant, 32)
248            fsmant = math.floor(fmant)
249            lomant = int(fsmant)
250    _write_ushort(f, expon)
251    _write_ulong(f, himant)
252    _write_ulong(f, lomant)
253
254from chunk import Chunk
255from collections import namedtuple
256
257_aifc_params = namedtuple('_aifc_params',
258                          'nchannels sampwidth framerate nframes comptype compname')
259
260_aifc_params.nchannels.__doc__ = 'Number of audio channels (1 for mono, 2 for stereo)'
261_aifc_params.sampwidth.__doc__ = 'Sample width in bytes'
262_aifc_params.framerate.__doc__ = 'Sampling frequency'
263_aifc_params.nframes.__doc__ = 'Number of audio frames'
264_aifc_params.comptype.__doc__ = 'Compression type ("NONE" for AIFF files)'
265_aifc_params.compname.__doc__ = ("""\
266A human-readable version of the compression type
267('not compressed' for AIFF files)""")
268
269
270class Aifc_read:
271    # Variables used in this class:
272    #
273    # These variables are available to the user though appropriate
274    # methods of this class:
275    # _file -- the open file with methods read(), close(), and seek()
276    #       set through the __init__() method
277    # _nchannels -- the number of audio channels
278    #       available through the getnchannels() method
279    # _nframes -- the number of audio frames
280    #       available through the getnframes() method
281    # _sampwidth -- the number of bytes per audio sample
282    #       available through the getsampwidth() method
283    # _framerate -- the sampling frequency
284    #       available through the getframerate() method
285    # _comptype -- the AIFF-C compression type ('NONE' if AIFF)
286    #       available through the getcomptype() method
287    # _compname -- the human-readable AIFF-C compression type
288    #       available through the getcomptype() method
289    # _markers -- the marks in the audio file
290    #       available through the getmarkers() and getmark()
291    #       methods
292    # _soundpos -- the position in the audio stream
293    #       available through the tell() method, set through the
294    #       setpos() method
295    #
296    # These variables are used internally only:
297    # _version -- the AIFF-C version number
298    # _decomp -- the decompressor from builtin module cl
299    # _comm_chunk_read -- 1 iff the COMM chunk has been read
300    # _aifc -- 1 iff reading an AIFF-C file
301    # _ssnd_seek_needed -- 1 iff positioned correctly in audio
302    #       file for readframes()
303    # _ssnd_chunk -- instantiation of a chunk class for the SSND chunk
304    # _framesize -- size of one frame in the file
305
306    _file = None  # Set here since __del__ checks it
307
308    def initfp(self, file):
309        self._version = 0
310        self._convert = None
311        self._markers = []
312        self._soundpos = 0
313        self._file = file
314        chunk = Chunk(file)
315        if chunk.getname() != b'FORM':
316            raise Error('file does not start with FORM id')
317        formdata = chunk.read(4)
318        if formdata == b'AIFF':
319            self._aifc = 0
320        elif formdata == b'AIFC':
321            self._aifc = 1
322        else:
323            raise Error('not an AIFF or AIFF-C file')
324        self._comm_chunk_read = 0
325        while 1:
326            self._ssnd_seek_needed = 1
327            try:
328                chunk = Chunk(self._file)
329            except EOFError:
330                break
331            chunkname = chunk.getname()
332            if chunkname == b'COMM':
333                self._read_comm_chunk(chunk)
334                self._comm_chunk_read = 1
335            elif chunkname == b'SSND':
336                self._ssnd_chunk = chunk
337                dummy = chunk.read(8)
338                self._ssnd_seek_needed = 0
339            elif chunkname == b'FVER':
340                self._version = _read_ulong(chunk)
341            elif chunkname == b'MARK':
342                self._readmark(chunk)
343            chunk.skip()
344        if not self._comm_chunk_read or not self._ssnd_chunk:
345            raise Error('COMM chunk and/or SSND chunk missing')
346
347    def __init__(self, f):
348        if isinstance(f, str):
349            file_object = builtins.open(f, 'rb')
350            try:
351                self.initfp(file_object)
352            except:
353                file_object.close()
354                raise
355        else:
356            # assume it is an open file object already
357            self.initfp(f)
358
359    def __enter__(self):
360        return self
361
362    def __exit__(self, *args):
363        self.close()
364
365    #
366    # User visible methods.
367    #
368    def getfp(self):
369        return self._file
370
371    def rewind(self):
372        self._ssnd_seek_needed = 1
373        self._soundpos = 0
374
375    def close(self):
376        file = self._file
377        if file is not None:
378            self._file = None
379            file.close()
380
381    def tell(self):
382        return self._soundpos
383
384    def getnchannels(self):
385        return self._nchannels
386
387    def getnframes(self):
388        return self._nframes
389
390    def getsampwidth(self):
391        return self._sampwidth
392
393    def getframerate(self):
394        return self._framerate
395
396    def getcomptype(self):
397        return self._comptype
398
399    def getcompname(self):
400        return self._compname
401
402##  def getversion(self):
403##      return self._version
404
405    def getparams(self):
406        return _aifc_params(self.getnchannels(), self.getsampwidth(),
407                            self.getframerate(), self.getnframes(),
408                            self.getcomptype(), self.getcompname())
409
410    def getmarkers(self):
411        if len(self._markers) == 0:
412            return None
413        return self._markers
414
415    def getmark(self, id):
416        for marker in self._markers:
417            if id == marker[0]:
418                return marker
419        raise Error('marker {0!r} does not exist'.format(id))
420
421    def setpos(self, pos):
422        if pos < 0 or pos > self._nframes:
423            raise Error('position not in range')
424        self._soundpos = pos
425        self._ssnd_seek_needed = 1
426
427    def readframes(self, nframes):
428        if self._ssnd_seek_needed:
429            self._ssnd_chunk.seek(0)
430            dummy = self._ssnd_chunk.read(8)
431            pos = self._soundpos * self._framesize
432            if pos:
433                self._ssnd_chunk.seek(pos + 8)
434            self._ssnd_seek_needed = 0
435        if nframes == 0:
436            return b''
437        data = self._ssnd_chunk.read(nframes * self._framesize)
438        if self._convert and data:
439            data = self._convert(data)
440        self._soundpos = self._soundpos + len(data) // (self._nchannels
441                                                        * self._sampwidth)
442        return data
443
444    #
445    # Internal methods.
446    #
447
448    def _alaw2lin(self, data):
449        import audioop
450        return audioop.alaw2lin(data, 2)
451
452    def _ulaw2lin(self, data):
453        import audioop
454        return audioop.ulaw2lin(data, 2)
455
456    def _adpcm2lin(self, data):
457        import audioop
458        if not hasattr(self, '_adpcmstate'):
459            # first time
460            self._adpcmstate = None
461        data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate)
462        return data
463
464    def _read_comm_chunk(self, chunk):
465        self._nchannels = _read_short(chunk)
466        self._nframes = _read_long(chunk)
467        self._sampwidth = (_read_short(chunk) + 7) // 8
468        self._framerate = int(_read_float(chunk))
469        self._framesize = self._nchannels * self._sampwidth
470        if self._aifc:
471            #DEBUG: SGI's soundeditor produces a bad size :-(
472            kludge = 0
473            if chunk.chunksize == 18:
474                kludge = 1
475                warnings.warn('Warning: bad COMM chunk size')
476                chunk.chunksize = 23
477            #DEBUG end
478            self._comptype = chunk.read(4)
479            #DEBUG start
480            if kludge:
481                length = ord(chunk.file.read(1))
482                if length & 1 == 0:
483                    length = length + 1
484                chunk.chunksize = chunk.chunksize + length
485                chunk.file.seek(-1, 1)
486            #DEBUG end
487            self._compname = _read_string(chunk)
488            if self._comptype != b'NONE':
489                if self._comptype == b'G722':
490                    self._convert = self._adpcm2lin
491                elif self._comptype in (b'ulaw', b'ULAW'):
492                    self._convert = self._ulaw2lin
493                elif self._comptype in (b'alaw', b'ALAW'):
494                    self._convert = self._alaw2lin
495                else:
496                    raise Error('unsupported compression type')
497                self._sampwidth = 2
498        else:
499            self._comptype = b'NONE'
500            self._compname = b'not compressed'
501
502    def _readmark(self, chunk):
503        nmarkers = _read_short(chunk)
504        # Some files appear to contain invalid counts.
505        # Cope with this by testing for EOF.
506        try:
507            for i in range(nmarkers):
508                id = _read_short(chunk)
509                pos = _read_long(chunk)
510                name = _read_string(chunk)
511                if pos or name:
512                    # some files appear to have
513                    # dummy markers consisting of
514                    # a position 0 and name ''
515                    self._markers.append((id, pos, name))
516        except EOFError:
517            w = ('Warning: MARK chunk contains only %s marker%s instead of %s' %
518                 (len(self._markers), '' if len(self._markers) == 1 else 's',
519                  nmarkers))
520            warnings.warn(w)
521
522class Aifc_write:
523    # Variables used in this class:
524    #
525    # These variables are user settable through appropriate methods
526    # of this class:
527    # _file -- the open file with methods write(), close(), tell(), seek()
528    #       set through the __init__() method
529    # _comptype -- the AIFF-C compression type ('NONE' in AIFF)
530    #       set through the setcomptype() or setparams() method
531    # _compname -- the human-readable AIFF-C compression type
532    #       set through the setcomptype() or setparams() method
533    # _nchannels -- the number of audio channels
534    #       set through the setnchannels() or setparams() method
535    # _sampwidth -- the number of bytes per audio sample
536    #       set through the setsampwidth() or setparams() method
537    # _framerate -- the sampling frequency
538    #       set through the setframerate() or setparams() method
539    # _nframes -- the number of audio frames written to the header
540    #       set through the setnframes() or setparams() method
541    # _aifc -- whether we're writing an AIFF-C file or an AIFF file
542    #       set through the aifc() method, reset through the
543    #       aiff() method
544    #
545    # These variables are used internally only:
546    # _version -- the AIFF-C version number
547    # _comp -- the compressor from builtin module cl
548    # _nframeswritten -- the number of audio frames actually written
549    # _datalength -- the size of the audio samples written to the header
550    # _datawritten -- the size of the audio samples actually written
551
552    _file = None  # Set here since __del__ checks it
553
554    def __init__(self, f):
555        if isinstance(f, str):
556            file_object = builtins.open(f, 'wb')
557            try:
558                self.initfp(file_object)
559            except:
560                file_object.close()
561                raise
562
563            # treat .aiff file extensions as non-compressed audio
564            if f.endswith('.aiff'):
565                self._aifc = 0
566        else:
567            # assume it is an open file object already
568            self.initfp(f)
569
570    def initfp(self, file):
571        self._file = file
572        self._version = _AIFC_version
573        self._comptype = b'NONE'
574        self._compname = b'not compressed'
575        self._convert = None
576        self._nchannels = 0
577        self._sampwidth = 0
578        self._framerate = 0
579        self._nframes = 0
580        self._nframeswritten = 0
581        self._datawritten = 0
582        self._datalength = 0
583        self._markers = []
584        self._marklength = 0
585        self._aifc = 1      # AIFF-C is default
586
587    def __del__(self):
588        self.close()
589
590    def __enter__(self):
591        return self
592
593    def __exit__(self, *args):
594        self.close()
595
596    #
597    # User visible methods.
598    #
599    def aiff(self):
600        if self._nframeswritten:
601            raise Error('cannot change parameters after starting to write')
602        self._aifc = 0
603
604    def aifc(self):
605        if self._nframeswritten:
606            raise Error('cannot change parameters after starting to write')
607        self._aifc = 1
608
609    def setnchannels(self, nchannels):
610        if self._nframeswritten:
611            raise Error('cannot change parameters after starting to write')
612        if nchannels < 1:
613            raise Error('bad # of channels')
614        self._nchannels = nchannels
615
616    def getnchannels(self):
617        if not self._nchannels:
618            raise Error('number of channels not set')
619        return self._nchannels
620
621    def setsampwidth(self, sampwidth):
622        if self._nframeswritten:
623            raise Error('cannot change parameters after starting to write')
624        if sampwidth < 1 or sampwidth > 4:
625            raise Error('bad sample width')
626        self._sampwidth = sampwidth
627
628    def getsampwidth(self):
629        if not self._sampwidth:
630            raise Error('sample width not set')
631        return self._sampwidth
632
633    def setframerate(self, framerate):
634        if self._nframeswritten:
635            raise Error('cannot change parameters after starting to write')
636        if framerate <= 0:
637            raise Error('bad frame rate')
638        self._framerate = framerate
639
640    def getframerate(self):
641        if not self._framerate:
642            raise Error('frame rate not set')
643        return self._framerate
644
645    def setnframes(self, nframes):
646        if self._nframeswritten:
647            raise Error('cannot change parameters after starting to write')
648        self._nframes = nframes
649
650    def getnframes(self):
651        return self._nframeswritten
652
653    def setcomptype(self, comptype, compname):
654        if self._nframeswritten:
655            raise Error('cannot change parameters after starting to write')
656        if comptype not in (b'NONE', b'ulaw', b'ULAW',
657                            b'alaw', b'ALAW', b'G722'):
658            raise Error('unsupported compression type')
659        self._comptype = comptype
660        self._compname = compname
661
662    def getcomptype(self):
663        return self._comptype
664
665    def getcompname(self):
666        return self._compname
667
668##  def setversion(self, version):
669##      if self._nframeswritten:
670##          raise Error, 'cannot change parameters after starting to write'
671##      self._version = version
672
673    def setparams(self, params):
674        nchannels, sampwidth, framerate, nframes, comptype, compname = params
675        if self._nframeswritten:
676            raise Error('cannot change parameters after starting to write')
677        if comptype not in (b'NONE', b'ulaw', b'ULAW',
678                            b'alaw', b'ALAW', b'G722'):
679            raise Error('unsupported compression type')
680        self.setnchannels(nchannels)
681        self.setsampwidth(sampwidth)
682        self.setframerate(framerate)
683        self.setnframes(nframes)
684        self.setcomptype(comptype, compname)
685
686    def getparams(self):
687        if not self._nchannels or not self._sampwidth or not self._framerate:
688            raise Error('not all parameters set')
689        return _aifc_params(self._nchannels, self._sampwidth, self._framerate,
690                            self._nframes, self._comptype, self._compname)
691
692    def setmark(self, id, pos, name):
693        if id <= 0:
694            raise Error('marker ID must be > 0')
695        if pos < 0:
696            raise Error('marker position must be >= 0')
697        if not isinstance(name, bytes):
698            raise Error('marker name must be bytes')
699        for i in range(len(self._markers)):
700            if id == self._markers[i][0]:
701                self._markers[i] = id, pos, name
702                return
703        self._markers.append((id, pos, name))
704
705    def getmark(self, id):
706        for marker in self._markers:
707            if id == marker[0]:
708                return marker
709        raise Error('marker {0!r} does not exist'.format(id))
710
711    def getmarkers(self):
712        if len(self._markers) == 0:
713            return None
714        return self._markers
715
716    def tell(self):
717        return self._nframeswritten
718
719    def writeframesraw(self, data):
720        if not isinstance(data, (bytes, bytearray)):
721            data = memoryview(data).cast('B')
722        self._ensure_header_written(len(data))
723        nframes = len(data) // (self._sampwidth * self._nchannels)
724        if self._convert:
725            data = self._convert(data)
726        self._file.write(data)
727        self._nframeswritten = self._nframeswritten + nframes
728        self._datawritten = self._datawritten + len(data)
729
730    def writeframes(self, data):
731        self.writeframesraw(data)
732        if self._nframeswritten != self._nframes or \
733              self._datalength != self._datawritten:
734            self._patchheader()
735
736    def close(self):
737        if self._file is None:
738            return
739        try:
740            self._ensure_header_written(0)
741            if self._datawritten & 1:
742                # quick pad to even size
743                self._file.write(b'\x00')
744                self._datawritten = self._datawritten + 1
745            self._writemarkers()
746            if self._nframeswritten != self._nframes or \
747                  self._datalength != self._datawritten or \
748                  self._marklength:
749                self._patchheader()
750        finally:
751            # Prevent ref cycles
752            self._convert = None
753            f = self._file
754            self._file = None
755            f.close()
756
757    #
758    # Internal methods.
759    #
760
761    def _lin2alaw(self, data):
762        import audioop
763        return audioop.lin2alaw(data, 2)
764
765    def _lin2ulaw(self, data):
766        import audioop
767        return audioop.lin2ulaw(data, 2)
768
769    def _lin2adpcm(self, data):
770        import audioop
771        if not hasattr(self, '_adpcmstate'):
772            self._adpcmstate = None
773        data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate)
774        return data
775
776    def _ensure_header_written(self, datasize):
777        if not self._nframeswritten:
778            if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
779                if not self._sampwidth:
780                    self._sampwidth = 2
781                if self._sampwidth != 2:
782                    raise Error('sample width must be 2 when compressing '
783                                'with ulaw/ULAW, alaw/ALAW or G7.22 (ADPCM)')
784            if not self._nchannels:
785                raise Error('# channels not specified')
786            if not self._sampwidth:
787                raise Error('sample width not specified')
788            if not self._framerate:
789                raise Error('sampling rate not specified')
790            self._write_header(datasize)
791
792    def _init_compression(self):
793        if self._comptype == b'G722':
794            self._convert = self._lin2adpcm
795        elif self._comptype in (b'ulaw', b'ULAW'):
796            self._convert = self._lin2ulaw
797        elif self._comptype in (b'alaw', b'ALAW'):
798            self._convert = self._lin2alaw
799
800    def _write_header(self, initlength):
801        if self._aifc and self._comptype != b'NONE':
802            self._init_compression()
803        self._file.write(b'FORM')
804        if not self._nframes:
805            self._nframes = initlength // (self._nchannels * self._sampwidth)
806        self._datalength = self._nframes * self._nchannels * self._sampwidth
807        if self._datalength & 1:
808            self._datalength = self._datalength + 1
809        if self._aifc:
810            if self._comptype in (b'ulaw', b'ULAW', b'alaw', b'ALAW'):
811                self._datalength = self._datalength // 2
812                if self._datalength & 1:
813                    self._datalength = self._datalength + 1
814            elif self._comptype == b'G722':
815                self._datalength = (self._datalength + 3) // 4
816                if self._datalength & 1:
817                    self._datalength = self._datalength + 1
818        try:
819            self._form_length_pos = self._file.tell()
820        except (AttributeError, OSError):
821            self._form_length_pos = None
822        commlength = self._write_form_length(self._datalength)
823        if self._aifc:
824            self._file.write(b'AIFC')
825            self._file.write(b'FVER')
826            _write_ulong(self._file, 4)
827            _write_ulong(self._file, self._version)
828        else:
829            self._file.write(b'AIFF')
830        self._file.write(b'COMM')
831        _write_ulong(self._file, commlength)
832        _write_short(self._file, self._nchannels)
833        if self._form_length_pos is not None:
834            self._nframes_pos = self._file.tell()
835        _write_ulong(self._file, self._nframes)
836        if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
837            _write_short(self._file, 8)
838        else:
839            _write_short(self._file, self._sampwidth * 8)
840        _write_float(self._file, self._framerate)
841        if self._aifc:
842            self._file.write(self._comptype)
843            _write_string(self._file, self._compname)
844        self._file.write(b'SSND')
845        if self._form_length_pos is not None:
846            self._ssnd_length_pos = self._file.tell()
847        _write_ulong(self._file, self._datalength + 8)
848        _write_ulong(self._file, 0)
849        _write_ulong(self._file, 0)
850
851    def _write_form_length(self, datalength):
852        if self._aifc:
853            commlength = 18 + 5 + len(self._compname)
854            if commlength & 1:
855                commlength = commlength + 1
856            verslength = 12
857        else:
858            commlength = 18
859            verslength = 0
860        _write_ulong(self._file, 4 + verslength + self._marklength + \
861                     8 + commlength + 16 + datalength)
862        return commlength
863
864    def _patchheader(self):
865        curpos = self._file.tell()
866        if self._datawritten & 1:
867            datalength = self._datawritten + 1
868            self._file.write(b'\x00')
869        else:
870            datalength = self._datawritten
871        if datalength == self._datalength and \
872              self._nframes == self._nframeswritten and \
873              self._marklength == 0:
874            self._file.seek(curpos, 0)
875            return
876        self._file.seek(self._form_length_pos, 0)
877        dummy = self._write_form_length(datalength)
878        self._file.seek(self._nframes_pos, 0)
879        _write_ulong(self._file, self._nframeswritten)
880        self._file.seek(self._ssnd_length_pos, 0)
881        _write_ulong(self._file, datalength + 8)
882        self._file.seek(curpos, 0)
883        self._nframes = self._nframeswritten
884        self._datalength = datalength
885
886    def _writemarkers(self):
887        if len(self._markers) == 0:
888            return
889        self._file.write(b'MARK')
890        length = 2
891        for marker in self._markers:
892            id, pos, name = marker
893            length = length + len(name) + 1 + 6
894            if len(name) & 1 == 0:
895                length = length + 1
896        _write_ulong(self._file, length)
897        self._marklength = length + 8
898        _write_short(self._file, len(self._markers))
899        for marker in self._markers:
900            id, pos, name = marker
901            _write_short(self._file, id)
902            _write_ulong(self._file, pos)
903            _write_string(self._file, name)
904
905def open(f, mode=None):
906    if mode is None:
907        if hasattr(f, 'mode'):
908            mode = f.mode
909        else:
910            mode = 'rb'
911    if mode in ('r', 'rb'):
912        return Aifc_read(f)
913    elif mode in ('w', 'wb'):
914        return Aifc_write(f)
915    else:
916        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
917
918openfp = open # B/W compatibility
919
920if __name__ == '__main__':
921    import sys
922    if not sys.argv[1:]:
923        sys.argv.append('/usr/demos/data/audio/bach.aiff')
924    fn = sys.argv[1]
925    with open(fn, 'r') as f:
926        print("Reading", fn)
927        print("nchannels =", f.getnchannels())
928        print("nframes   =", f.getnframes())
929        print("sampwidth =", f.getsampwidth())
930        print("framerate =", f.getframerate())
931        print("comptype  =", f.getcomptype())
932        print("compname  =", f.getcompname())
933        if sys.argv[2:]:
934            gn = sys.argv[2]
935            print("Writing", gn)
936            with open(gn, 'w') as g:
937                g.setparams(f.getparams())
938                while 1:
939                    data = f.readframes(1024)
940                    if not data:
941                        break
942                    g.writeframes(data)
943            print("Done.")
944