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 from None
153
154def _read_ulong(file):
155    try:
156        return struct.unpack('>L', file.read(4))[0]
157    except struct.error:
158        raise EOFError from None
159
160def _read_short(file):
161    try:
162        return struct.unpack('>h', file.read(2))[0]
163    except struct.error:
164        raise EOFError from None
165
166def _read_ushort(file):
167    try:
168        return struct.unpack('>H', file.read(2))[0]
169    except struct.error:
170        raise EOFError from None
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        self._ssnd_chunk = None
326        while 1:
327            self._ssnd_seek_needed = 1
328            try:
329                chunk = Chunk(self._file)
330            except EOFError:
331                break
332            chunkname = chunk.getname()
333            if chunkname == b'COMM':
334                self._read_comm_chunk(chunk)
335                self._comm_chunk_read = 1
336            elif chunkname == b'SSND':
337                self._ssnd_chunk = chunk
338                dummy = chunk.read(8)
339                self._ssnd_seek_needed = 0
340            elif chunkname == b'FVER':
341                self._version = _read_ulong(chunk)
342            elif chunkname == b'MARK':
343                self._readmark(chunk)
344            chunk.skip()
345        if not self._comm_chunk_read or not self._ssnd_chunk:
346            raise Error('COMM chunk and/or SSND chunk missing')
347
348    def __init__(self, f):
349        if isinstance(f, str):
350            file_object = builtins.open(f, 'rb')
351            try:
352                self.initfp(file_object)
353            except:
354                file_object.close()
355                raise
356        else:
357            # assume it is an open file object already
358            self.initfp(f)
359
360    def __enter__(self):
361        return self
362
363    def __exit__(self, *args):
364        self.close()
365
366    #
367    # User visible methods.
368    #
369    def getfp(self):
370        return self._file
371
372    def rewind(self):
373        self._ssnd_seek_needed = 1
374        self._soundpos = 0
375
376    def close(self):
377        file = self._file
378        if file is not None:
379            self._file = None
380            file.close()
381
382    def tell(self):
383        return self._soundpos
384
385    def getnchannels(self):
386        return self._nchannels
387
388    def getnframes(self):
389        return self._nframes
390
391    def getsampwidth(self):
392        return self._sampwidth
393
394    def getframerate(self):
395        return self._framerate
396
397    def getcomptype(self):
398        return self._comptype
399
400    def getcompname(self):
401        return self._compname
402
403##  def getversion(self):
404##      return self._version
405
406    def getparams(self):
407        return _aifc_params(self.getnchannels(), self.getsampwidth(),
408                            self.getframerate(), self.getnframes(),
409                            self.getcomptype(), self.getcompname())
410
411    def getmarkers(self):
412        if len(self._markers) == 0:
413            return None
414        return self._markers
415
416    def getmark(self, id):
417        for marker in self._markers:
418            if id == marker[0]:
419                return marker
420        raise Error('marker {0!r} does not exist'.format(id))
421
422    def setpos(self, pos):
423        if pos < 0 or pos > self._nframes:
424            raise Error('position not in range')
425        self._soundpos = pos
426        self._ssnd_seek_needed = 1
427
428    def readframes(self, nframes):
429        if self._ssnd_seek_needed:
430            self._ssnd_chunk.seek(0)
431            dummy = self._ssnd_chunk.read(8)
432            pos = self._soundpos * self._framesize
433            if pos:
434                self._ssnd_chunk.seek(pos + 8)
435            self._ssnd_seek_needed = 0
436        if nframes == 0:
437            return b''
438        data = self._ssnd_chunk.read(nframes * self._framesize)
439        if self._convert and data:
440            data = self._convert(data)
441        self._soundpos = self._soundpos + len(data) // (self._nchannels
442                                                        * self._sampwidth)
443        return data
444
445    #
446    # Internal methods.
447    #
448
449    def _alaw2lin(self, data):
450        import audioop
451        return audioop.alaw2lin(data, 2)
452
453    def _ulaw2lin(self, data):
454        import audioop
455        return audioop.ulaw2lin(data, 2)
456
457    def _adpcm2lin(self, data):
458        import audioop
459        if not hasattr(self, '_adpcmstate'):
460            # first time
461            self._adpcmstate = None
462        data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate)
463        return data
464
465    def _read_comm_chunk(self, chunk):
466        self._nchannels = _read_short(chunk)
467        self._nframes = _read_long(chunk)
468        self._sampwidth = (_read_short(chunk) + 7) // 8
469        self._framerate = int(_read_float(chunk))
470        if self._sampwidth <= 0:
471            raise Error('bad sample width')
472        if self._nchannels <= 0:
473            raise Error('bad # of channels')
474        self._framesize = self._nchannels * self._sampwidth
475        if self._aifc:
476            #DEBUG: SGI's soundeditor produces a bad size :-(
477            kludge = 0
478            if chunk.chunksize == 18:
479                kludge = 1
480                warnings.warn('Warning: bad COMM chunk size')
481                chunk.chunksize = 23
482            #DEBUG end
483            self._comptype = chunk.read(4)
484            #DEBUG start
485            if kludge:
486                length = ord(chunk.file.read(1))
487                if length & 1 == 0:
488                    length = length + 1
489                chunk.chunksize = chunk.chunksize + length
490                chunk.file.seek(-1, 1)
491            #DEBUG end
492            self._compname = _read_string(chunk)
493            if self._comptype != b'NONE':
494                if self._comptype == b'G722':
495                    self._convert = self._adpcm2lin
496                elif self._comptype in (b'ulaw', b'ULAW'):
497                    self._convert = self._ulaw2lin
498                elif self._comptype in (b'alaw', b'ALAW'):
499                    self._convert = self._alaw2lin
500                else:
501                    raise Error('unsupported compression type')
502                self._sampwidth = 2
503        else:
504            self._comptype = b'NONE'
505            self._compname = b'not compressed'
506
507    def _readmark(self, chunk):
508        nmarkers = _read_short(chunk)
509        # Some files appear to contain invalid counts.
510        # Cope with this by testing for EOF.
511        try:
512            for i in range(nmarkers):
513                id = _read_short(chunk)
514                pos = _read_long(chunk)
515                name = _read_string(chunk)
516                if pos or name:
517                    # some files appear to have
518                    # dummy markers consisting of
519                    # a position 0 and name ''
520                    self._markers.append((id, pos, name))
521        except EOFError:
522            w = ('Warning: MARK chunk contains only %s marker%s instead of %s' %
523                 (len(self._markers), '' if len(self._markers) == 1 else 's',
524                  nmarkers))
525            warnings.warn(w)
526
527class Aifc_write:
528    # Variables used in this class:
529    #
530    # These variables are user settable through appropriate methods
531    # of this class:
532    # _file -- the open file with methods write(), close(), tell(), seek()
533    #       set through the __init__() method
534    # _comptype -- the AIFF-C compression type ('NONE' in AIFF)
535    #       set through the setcomptype() or setparams() method
536    # _compname -- the human-readable AIFF-C compression type
537    #       set through the setcomptype() or setparams() method
538    # _nchannels -- the number of audio channels
539    #       set through the setnchannels() or setparams() method
540    # _sampwidth -- the number of bytes per audio sample
541    #       set through the setsampwidth() or setparams() method
542    # _framerate -- the sampling frequency
543    #       set through the setframerate() or setparams() method
544    # _nframes -- the number of audio frames written to the header
545    #       set through the setnframes() or setparams() method
546    # _aifc -- whether we're writing an AIFF-C file or an AIFF file
547    #       set through the aifc() method, reset through the
548    #       aiff() method
549    #
550    # These variables are used internally only:
551    # _version -- the AIFF-C version number
552    # _comp -- the compressor from builtin module cl
553    # _nframeswritten -- the number of audio frames actually written
554    # _datalength -- the size of the audio samples written to the header
555    # _datawritten -- the size of the audio samples actually written
556
557    _file = None  # Set here since __del__ checks it
558
559    def __init__(self, f):
560        if isinstance(f, str):
561            file_object = builtins.open(f, 'wb')
562            try:
563                self.initfp(file_object)
564            except:
565                file_object.close()
566                raise
567
568            # treat .aiff file extensions as non-compressed audio
569            if f.endswith('.aiff'):
570                self._aifc = 0
571        else:
572            # assume it is an open file object already
573            self.initfp(f)
574
575    def initfp(self, file):
576        self._file = file
577        self._version = _AIFC_version
578        self._comptype = b'NONE'
579        self._compname = b'not compressed'
580        self._convert = None
581        self._nchannels = 0
582        self._sampwidth = 0
583        self._framerate = 0
584        self._nframes = 0
585        self._nframeswritten = 0
586        self._datawritten = 0
587        self._datalength = 0
588        self._markers = []
589        self._marklength = 0
590        self._aifc = 1      # AIFF-C is default
591
592    def __del__(self):
593        self.close()
594
595    def __enter__(self):
596        return self
597
598    def __exit__(self, *args):
599        self.close()
600
601    #
602    # User visible methods.
603    #
604    def aiff(self):
605        if self._nframeswritten:
606            raise Error('cannot change parameters after starting to write')
607        self._aifc = 0
608
609    def aifc(self):
610        if self._nframeswritten:
611            raise Error('cannot change parameters after starting to write')
612        self._aifc = 1
613
614    def setnchannels(self, nchannels):
615        if self._nframeswritten:
616            raise Error('cannot change parameters after starting to write')
617        if nchannels < 1:
618            raise Error('bad # of channels')
619        self._nchannels = nchannels
620
621    def getnchannels(self):
622        if not self._nchannels:
623            raise Error('number of channels not set')
624        return self._nchannels
625
626    def setsampwidth(self, sampwidth):
627        if self._nframeswritten:
628            raise Error('cannot change parameters after starting to write')
629        if sampwidth < 1 or sampwidth > 4:
630            raise Error('bad sample width')
631        self._sampwidth = sampwidth
632
633    def getsampwidth(self):
634        if not self._sampwidth:
635            raise Error('sample width not set')
636        return self._sampwidth
637
638    def setframerate(self, framerate):
639        if self._nframeswritten:
640            raise Error('cannot change parameters after starting to write')
641        if framerate <= 0:
642            raise Error('bad frame rate')
643        self._framerate = framerate
644
645    def getframerate(self):
646        if not self._framerate:
647            raise Error('frame rate not set')
648        return self._framerate
649
650    def setnframes(self, nframes):
651        if self._nframeswritten:
652            raise Error('cannot change parameters after starting to write')
653        self._nframes = nframes
654
655    def getnframes(self):
656        return self._nframeswritten
657
658    def setcomptype(self, comptype, compname):
659        if self._nframeswritten:
660            raise Error('cannot change parameters after starting to write')
661        if comptype not in (b'NONE', b'ulaw', b'ULAW',
662                            b'alaw', b'ALAW', b'G722'):
663            raise Error('unsupported compression type')
664        self._comptype = comptype
665        self._compname = compname
666
667    def getcomptype(self):
668        return self._comptype
669
670    def getcompname(self):
671        return self._compname
672
673##  def setversion(self, version):
674##      if self._nframeswritten:
675##          raise Error, 'cannot change parameters after starting to write'
676##      self._version = version
677
678    def setparams(self, params):
679        nchannels, sampwidth, framerate, nframes, comptype, compname = params
680        if self._nframeswritten:
681            raise Error('cannot change parameters after starting to write')
682        if comptype not in (b'NONE', b'ulaw', b'ULAW',
683                            b'alaw', b'ALAW', b'G722'):
684            raise Error('unsupported compression type')
685        self.setnchannels(nchannels)
686        self.setsampwidth(sampwidth)
687        self.setframerate(framerate)
688        self.setnframes(nframes)
689        self.setcomptype(comptype, compname)
690
691    def getparams(self):
692        if not self._nchannels or not self._sampwidth or not self._framerate:
693            raise Error('not all parameters set')
694        return _aifc_params(self._nchannels, self._sampwidth, self._framerate,
695                            self._nframes, self._comptype, self._compname)
696
697    def setmark(self, id, pos, name):
698        if id <= 0:
699            raise Error('marker ID must be > 0')
700        if pos < 0:
701            raise Error('marker position must be >= 0')
702        if not isinstance(name, bytes):
703            raise Error('marker name must be bytes')
704        for i in range(len(self._markers)):
705            if id == self._markers[i][0]:
706                self._markers[i] = id, pos, name
707                return
708        self._markers.append((id, pos, name))
709
710    def getmark(self, id):
711        for marker in self._markers:
712            if id == marker[0]:
713                return marker
714        raise Error('marker {0!r} does not exist'.format(id))
715
716    def getmarkers(self):
717        if len(self._markers) == 0:
718            return None
719        return self._markers
720
721    def tell(self):
722        return self._nframeswritten
723
724    def writeframesraw(self, data):
725        if not isinstance(data, (bytes, bytearray)):
726            data = memoryview(data).cast('B')
727        self._ensure_header_written(len(data))
728        nframes = len(data) // (self._sampwidth * self._nchannels)
729        if self._convert:
730            data = self._convert(data)
731        self._file.write(data)
732        self._nframeswritten = self._nframeswritten + nframes
733        self._datawritten = self._datawritten + len(data)
734
735    def writeframes(self, data):
736        self.writeframesraw(data)
737        if self._nframeswritten != self._nframes or \
738              self._datalength != self._datawritten:
739            self._patchheader()
740
741    def close(self):
742        if self._file is None:
743            return
744        try:
745            self._ensure_header_written(0)
746            if self._datawritten & 1:
747                # quick pad to even size
748                self._file.write(b'\x00')
749                self._datawritten = self._datawritten + 1
750            self._writemarkers()
751            if self._nframeswritten != self._nframes or \
752                  self._datalength != self._datawritten or \
753                  self._marklength:
754                self._patchheader()
755        finally:
756            # Prevent ref cycles
757            self._convert = None
758            f = self._file
759            self._file = None
760            f.close()
761
762    #
763    # Internal methods.
764    #
765
766    def _lin2alaw(self, data):
767        import audioop
768        return audioop.lin2alaw(data, 2)
769
770    def _lin2ulaw(self, data):
771        import audioop
772        return audioop.lin2ulaw(data, 2)
773
774    def _lin2adpcm(self, data):
775        import audioop
776        if not hasattr(self, '_adpcmstate'):
777            self._adpcmstate = None
778        data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate)
779        return data
780
781    def _ensure_header_written(self, datasize):
782        if not self._nframeswritten:
783            if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
784                if not self._sampwidth:
785                    self._sampwidth = 2
786                if self._sampwidth != 2:
787                    raise Error('sample width must be 2 when compressing '
788                                'with ulaw/ULAW, alaw/ALAW or G7.22 (ADPCM)')
789            if not self._nchannels:
790                raise Error('# channels not specified')
791            if not self._sampwidth:
792                raise Error('sample width not specified')
793            if not self._framerate:
794                raise Error('sampling rate not specified')
795            self._write_header(datasize)
796
797    def _init_compression(self):
798        if self._comptype == b'G722':
799            self._convert = self._lin2adpcm
800        elif self._comptype in (b'ulaw', b'ULAW'):
801            self._convert = self._lin2ulaw
802        elif self._comptype in (b'alaw', b'ALAW'):
803            self._convert = self._lin2alaw
804
805    def _write_header(self, initlength):
806        if self._aifc and self._comptype != b'NONE':
807            self._init_compression()
808        self._file.write(b'FORM')
809        if not self._nframes:
810            self._nframes = initlength // (self._nchannels * self._sampwidth)
811        self._datalength = self._nframes * self._nchannels * self._sampwidth
812        if self._datalength & 1:
813            self._datalength = self._datalength + 1
814        if self._aifc:
815            if self._comptype in (b'ulaw', b'ULAW', b'alaw', b'ALAW'):
816                self._datalength = self._datalength // 2
817                if self._datalength & 1:
818                    self._datalength = self._datalength + 1
819            elif self._comptype == b'G722':
820                self._datalength = (self._datalength + 3) // 4
821                if self._datalength & 1:
822                    self._datalength = self._datalength + 1
823        try:
824            self._form_length_pos = self._file.tell()
825        except (AttributeError, OSError):
826            self._form_length_pos = None
827        commlength = self._write_form_length(self._datalength)
828        if self._aifc:
829            self._file.write(b'AIFC')
830            self._file.write(b'FVER')
831            _write_ulong(self._file, 4)
832            _write_ulong(self._file, self._version)
833        else:
834            self._file.write(b'AIFF')
835        self._file.write(b'COMM')
836        _write_ulong(self._file, commlength)
837        _write_short(self._file, self._nchannels)
838        if self._form_length_pos is not None:
839            self._nframes_pos = self._file.tell()
840        _write_ulong(self._file, self._nframes)
841        if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
842            _write_short(self._file, 8)
843        else:
844            _write_short(self._file, self._sampwidth * 8)
845        _write_float(self._file, self._framerate)
846        if self._aifc:
847            self._file.write(self._comptype)
848            _write_string(self._file, self._compname)
849        self._file.write(b'SSND')
850        if self._form_length_pos is not None:
851            self._ssnd_length_pos = self._file.tell()
852        _write_ulong(self._file, self._datalength + 8)
853        _write_ulong(self._file, 0)
854        _write_ulong(self._file, 0)
855
856    def _write_form_length(self, datalength):
857        if self._aifc:
858            commlength = 18 + 5 + len(self._compname)
859            if commlength & 1:
860                commlength = commlength + 1
861            verslength = 12
862        else:
863            commlength = 18
864            verslength = 0
865        _write_ulong(self._file, 4 + verslength + self._marklength + \
866                     8 + commlength + 16 + datalength)
867        return commlength
868
869    def _patchheader(self):
870        curpos = self._file.tell()
871        if self._datawritten & 1:
872            datalength = self._datawritten + 1
873            self._file.write(b'\x00')
874        else:
875            datalength = self._datawritten
876        if datalength == self._datalength and \
877              self._nframes == self._nframeswritten and \
878              self._marklength == 0:
879            self._file.seek(curpos, 0)
880            return
881        self._file.seek(self._form_length_pos, 0)
882        dummy = self._write_form_length(datalength)
883        self._file.seek(self._nframes_pos, 0)
884        _write_ulong(self._file, self._nframeswritten)
885        self._file.seek(self._ssnd_length_pos, 0)
886        _write_ulong(self._file, datalength + 8)
887        self._file.seek(curpos, 0)
888        self._nframes = self._nframeswritten
889        self._datalength = datalength
890
891    def _writemarkers(self):
892        if len(self._markers) == 0:
893            return
894        self._file.write(b'MARK')
895        length = 2
896        for marker in self._markers:
897            id, pos, name = marker
898            length = length + len(name) + 1 + 6
899            if len(name) & 1 == 0:
900                length = length + 1
901        _write_ulong(self._file, length)
902        self._marklength = length + 8
903        _write_short(self._file, len(self._markers))
904        for marker in self._markers:
905            id, pos, name = marker
906            _write_short(self._file, id)
907            _write_ulong(self._file, pos)
908            _write_string(self._file, name)
909
910def open(f, mode=None):
911    if mode is None:
912        if hasattr(f, 'mode'):
913            mode = f.mode
914        else:
915            mode = 'rb'
916    if mode in ('r', 'rb'):
917        return Aifc_read(f)
918    elif mode in ('w', 'wb'):
919        return Aifc_write(f)
920    else:
921        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
922
923def openfp(f, mode=None):
924    warnings.warn("aifc.openfp is deprecated since Python 3.7. "
925                  "Use aifc.open instead.", DeprecationWarning, stacklevel=2)
926    return open(f, mode=mode)
927
928if __name__ == '__main__':
929    import sys
930    if not sys.argv[1:]:
931        sys.argv.append('/usr/demos/data/audio/bach.aiff')
932    fn = sys.argv[1]
933    with open(fn, 'r') as f:
934        print("Reading", fn)
935        print("nchannels =", f.getnchannels())
936        print("nframes   =", f.getnframes())
937        print("sampwidth =", f.getsampwidth())
938        print("framerate =", f.getframerate())
939        print("comptype  =", f.getcomptype())
940        print("compname  =", f.getcompname())
941        if sys.argv[2:]:
942            gn = sys.argv[2]
943            print("Writing", gn)
944            with open(gn, 'w') as g:
945                g.setparams(f.getparams())
946                while 1:
947                    data = f.readframes(1024)
948                    if not data:
949                        break
950                    g.writeframes(data)
951            print("Done.")
952