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 tuple 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('') or
125close() to patch up the sizes in the header.
126Marks can be added anytime.  If there are any marks, ypu 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 __builtin__
139
140__all__ = ["Error","open","openfp"]
141
142class Error(Exception):
143    pass
144
145_AIFC_version = 0xA2805140L     # Version 1 of AIFF-C
146
147def _read_long(file):
148    try:
149        return struct.unpack('>l', file.read(4))[0]
150    except struct.error:
151        raise EOFError
152
153def _read_ulong(file):
154    try:
155        return struct.unpack('>L', file.read(4))[0]
156    except struct.error:
157        raise EOFError
158
159def _read_short(file):
160    try:
161        return struct.unpack('>h', file.read(2))[0]
162    except struct.error:
163        raise EOFError
164
165def _read_string(file):
166    length = ord(file.read(1))
167    if length == 0:
168        data = ''
169    else:
170        data = file.read(length)
171    if length & 1 == 0:
172        dummy = file.read(1)
173    return data
174
175_HUGE_VAL = 1.79769313486231e+308 # See <limits.h>
176
177def _read_float(f): # 10 bytes
178    expon = _read_short(f) # 2 bytes
179    sign = 1
180    if expon < 0:
181        sign = -1
182        expon = expon + 0x8000
183    himant = _read_ulong(f) # 4 bytes
184    lomant = _read_ulong(f) # 4 bytes
185    if expon == himant == lomant == 0:
186        f = 0.0
187    elif expon == 0x7FFF:
188        f = _HUGE_VAL
189    else:
190        expon = expon - 16383
191        f = (himant * 0x100000000L + lomant) * pow(2.0, expon - 63)
192    return sign * f
193
194def _write_short(f, x):
195    f.write(struct.pack('>h', x))
196
197def _write_long(f, x):
198    f.write(struct.pack('>L', x))
199
200def _write_string(f, s):
201    if len(s) > 255:
202        raise ValueError("string exceeds maximum pstring length")
203    f.write(chr(len(s)))
204    f.write(s)
205    if len(s) & 1 == 0:
206        f.write(chr(0))
207
208def _write_float(f, x):
209    import math
210    if x < 0:
211        sign = 0x8000
212        x = x * -1
213    else:
214        sign = 0
215    if x == 0:
216        expon = 0
217        himant = 0
218        lomant = 0
219    else:
220        fmant, expon = math.frexp(x)
221        if expon > 16384 or fmant >= 1:     # Infinity or NaN
222            expon = sign|0x7FFF
223            himant = 0
224            lomant = 0
225        else:                   # Finite
226            expon = expon + 16382
227            if expon < 0:           # denormalized
228                fmant = math.ldexp(fmant, expon)
229                expon = 0
230            expon = expon | sign
231            fmant = math.ldexp(fmant, 32)
232            fsmant = math.floor(fmant)
233            himant = long(fsmant)
234            fmant = math.ldexp(fmant - fsmant, 32)
235            fsmant = math.floor(fmant)
236            lomant = long(fsmant)
237    _write_short(f, expon)
238    _write_long(f, himant)
239    _write_long(f, lomant)
240
241from chunk import Chunk
242
243class Aifc_read:
244    # Variables used in this class:
245    #
246    # These variables are available to the user though appropriate
247    # methods of this class:
248    # _file -- the open file with methods read(), close(), and seek()
249    #       set through the __init__() method
250    # _nchannels -- the number of audio channels
251    #       available through the getnchannels() method
252    # _nframes -- the number of audio frames
253    #       available through the getnframes() method
254    # _sampwidth -- the number of bytes per audio sample
255    #       available through the getsampwidth() method
256    # _framerate -- the sampling frequency
257    #       available through the getframerate() method
258    # _comptype -- the AIFF-C compression type ('NONE' if AIFF)
259    #       available through the getcomptype() method
260    # _compname -- the human-readable AIFF-C compression type
261    #       available through the getcomptype() method
262    # _markers -- the marks in the audio file
263    #       available through the getmarkers() and getmark()
264    #       methods
265    # _soundpos -- the position in the audio stream
266    #       available through the tell() method, set through the
267    #       setpos() method
268    #
269    # These variables are used internally only:
270    # _version -- the AIFF-C version number
271    # _decomp -- the decompressor from builtin module cl
272    # _comm_chunk_read -- 1 iff the COMM chunk has been read
273    # _aifc -- 1 iff reading an AIFF-C file
274    # _ssnd_seek_needed -- 1 iff positioned correctly in audio
275    #       file for readframes()
276    # _ssnd_chunk -- instantiation of a chunk class for the SSND chunk
277    # _framesize -- size of one frame in the file
278
279    def initfp(self, file):
280        self._version = 0
281        self._decomp = None
282        self._convert = None
283        self._markers = []
284        self._soundpos = 0
285        self._file = file
286        chunk = Chunk(file)
287        if chunk.getname() != 'FORM':
288            raise Error, 'file does not start with FORM id'
289        formdata = chunk.read(4)
290        if formdata == 'AIFF':
291            self._aifc = 0
292        elif formdata == 'AIFC':
293            self._aifc = 1
294        else:
295            raise Error, 'not an AIFF or AIFF-C file'
296        self._comm_chunk_read = 0
297        while 1:
298            self._ssnd_seek_needed = 1
299            try:
300                chunk = Chunk(self._file)
301            except EOFError:
302                break
303            chunkname = chunk.getname()
304            if chunkname == 'COMM':
305                self._read_comm_chunk(chunk)
306                self._comm_chunk_read = 1
307            elif chunkname == 'SSND':
308                self._ssnd_chunk = chunk
309                dummy = chunk.read(8)
310                self._ssnd_seek_needed = 0
311            elif chunkname == 'FVER':
312                self._version = _read_ulong(chunk)
313            elif chunkname == 'MARK':
314                self._readmark(chunk)
315            chunk.skip()
316        if not self._comm_chunk_read or not self._ssnd_chunk:
317            raise Error, 'COMM chunk and/or SSND chunk missing'
318        if self._aifc and self._decomp:
319            import cl
320            params = [cl.ORIGINAL_FORMAT, 0,
321                  cl.BITS_PER_COMPONENT, self._sampwidth * 8,
322                  cl.FRAME_RATE, self._framerate]
323            if self._nchannels == 1:
324                params[1] = cl.MONO
325            elif self._nchannels == 2:
326                params[1] = cl.STEREO_INTERLEAVED
327            else:
328                raise Error, 'cannot compress more than 2 channels'
329            self._decomp.SetParams(params)
330
331    def __init__(self, f):
332        if type(f) == type(''):
333            f = __builtin__.open(f, 'rb')
334        # else, assume it is an open file object already
335        self.initfp(f)
336
337    #
338    # User visible methods.
339    #
340    def getfp(self):
341        return self._file
342
343    def rewind(self):
344        self._ssnd_seek_needed = 1
345        self._soundpos = 0
346
347    def close(self):
348        if self._decomp:
349            self._decomp.CloseDecompressor()
350            self._decomp = None
351        self._file.close()
352
353    def tell(self):
354        return self._soundpos
355
356    def getnchannels(self):
357        return self._nchannels
358
359    def getnframes(self):
360        return self._nframes
361
362    def getsampwidth(self):
363        return self._sampwidth
364
365    def getframerate(self):
366        return self._framerate
367
368    def getcomptype(self):
369        return self._comptype
370
371    def getcompname(self):
372        return self._compname
373
374##  def getversion(self):
375##      return self._version
376
377    def getparams(self):
378        return self.getnchannels(), self.getsampwidth(), \
379              self.getframerate(), self.getnframes(), \
380              self.getcomptype(), self.getcompname()
381
382    def getmarkers(self):
383        if len(self._markers) == 0:
384            return None
385        return self._markers
386
387    def getmark(self, id):
388        for marker in self._markers:
389            if id == marker[0]:
390                return marker
391        raise Error, 'marker %r does not exist' % (id,)
392
393    def setpos(self, pos):
394        if pos < 0 or pos > self._nframes:
395            raise Error, 'position not in range'
396        self._soundpos = pos
397        self._ssnd_seek_needed = 1
398
399    def readframes(self, nframes):
400        if self._ssnd_seek_needed:
401            self._ssnd_chunk.seek(0)
402            dummy = self._ssnd_chunk.read(8)
403            pos = self._soundpos * self._framesize
404            if pos:
405                self._ssnd_chunk.seek(pos + 8)
406            self._ssnd_seek_needed = 0
407        if nframes == 0:
408            return ''
409        data = self._ssnd_chunk.read(nframes * self._framesize)
410        if self._convert and data:
411            data = self._convert(data)
412        self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth)
413        return data
414
415    #
416    # Internal methods.
417    #
418
419    def _decomp_data(self, data):
420        import cl
421        dummy = self._decomp.SetParam(cl.FRAME_BUFFER_SIZE,
422                          len(data) * 2)
423        return self._decomp.Decompress(len(data) // self._nchannels,
424                           data)
425
426    def _ulaw2lin(self, data):
427        import audioop
428        return audioop.ulaw2lin(data, 2)
429
430    def _adpcm2lin(self, data):
431        import audioop
432        if not hasattr(self, '_adpcmstate'):
433            # first time
434            self._adpcmstate = None
435        data, self._adpcmstate = audioop.adpcm2lin(data, 2,
436                               self._adpcmstate)
437        return data
438
439    def _read_comm_chunk(self, chunk):
440        self._nchannels = _read_short(chunk)
441        self._nframes = _read_long(chunk)
442        self._sampwidth = (_read_short(chunk) + 7) // 8
443        self._framerate = int(_read_float(chunk))
444        self._framesize = self._nchannels * self._sampwidth
445        if self._aifc:
446            #DEBUG: SGI's soundeditor produces a bad size :-(
447            kludge = 0
448            if chunk.chunksize == 18:
449                kludge = 1
450                print 'Warning: bad COMM chunk size'
451                chunk.chunksize = 23
452            #DEBUG end
453            self._comptype = chunk.read(4)
454            #DEBUG start
455            if kludge:
456                length = ord(chunk.file.read(1))
457                if length & 1 == 0:
458                    length = length + 1
459                chunk.chunksize = chunk.chunksize + length
460                chunk.file.seek(-1, 1)
461            #DEBUG end
462            self._compname = _read_string(chunk)
463            if self._comptype != 'NONE':
464                if self._comptype == 'G722':
465                    try:
466                        import audioop
467                    except ImportError:
468                        pass
469                    else:
470                        self._convert = self._adpcm2lin
471                        self._framesize = self._framesize // 4
472                        return
473                # for ULAW and ALAW try Compression Library
474                try:
475                    import cl
476                except ImportError:
477                    if self._comptype == 'ULAW':
478                        try:
479                            import audioop
480                            self._convert = self._ulaw2lin
481                            self._framesize = self._framesize // 2
482                            return
483                        except ImportError:
484                            pass
485                    raise Error, 'cannot read compressed AIFF-C files'
486                if self._comptype == 'ULAW':
487                    scheme = cl.G711_ULAW
488                    self._framesize = self._framesize // 2
489                elif self._comptype == 'ALAW':
490                    scheme = cl.G711_ALAW
491                    self._framesize = self._framesize // 2
492                else:
493                    raise Error, 'unsupported compression type'
494                self._decomp = cl.OpenDecompressor(scheme)
495                self._convert = self._decomp_data
496        else:
497            self._comptype = 'NONE'
498            self._compname = 'not compressed'
499
500    def _readmark(self, chunk):
501        nmarkers = _read_short(chunk)
502        # Some files appear to contain invalid counts.
503        # Cope with this by testing for EOF.
504        try:
505            for i in range(nmarkers):
506                id = _read_short(chunk)
507                pos = _read_long(chunk)
508                name = _read_string(chunk)
509                if pos or name:
510                    # some files appear to have
511                    # dummy markers consisting of
512                    # a position 0 and name ''
513                    self._markers.append((id, pos, name))
514        except EOFError:
515            print 'Warning: MARK chunk contains only',
516            print len(self._markers),
517            if len(self._markers) == 1: print 'marker',
518            else: print 'markers',
519            print 'instead of', nmarkers
520
521class Aifc_write:
522    # Variables used in this class:
523    #
524    # These variables are user settable through appropriate methods
525    # of this class:
526    # _file -- the open file with methods write(), close(), tell(), seek()
527    #       set through the __init__() method
528    # _comptype -- the AIFF-C compression type ('NONE' in AIFF)
529    #       set through the setcomptype() or setparams() method
530    # _compname -- the human-readable AIFF-C compression type
531    #       set through the setcomptype() or setparams() method
532    # _nchannels -- the number of audio channels
533    #       set through the setnchannels() or setparams() method
534    # _sampwidth -- the number of bytes per audio sample
535    #       set through the setsampwidth() or setparams() method
536    # _framerate -- the sampling frequency
537    #       set through the setframerate() or setparams() method
538    # _nframes -- the number of audio frames written to the header
539    #       set through the setnframes() or setparams() method
540    # _aifc -- whether we're writing an AIFF-C file or an AIFF file
541    #       set through the aifc() method, reset through the
542    #       aiff() method
543    #
544    # These variables are used internally only:
545    # _version -- the AIFF-C version number
546    # _comp -- the compressor from builtin module cl
547    # _nframeswritten -- the number of audio frames actually written
548    # _datalength -- the size of the audio samples written to the header
549    # _datawritten -- the size of the audio samples actually written
550
551    def __init__(self, f):
552        if type(f) == type(''):
553            filename = f
554            f = __builtin__.open(f, 'wb')
555        else:
556            # else, assume it is an open file object already
557            filename = '???'
558        self.initfp(f)
559        if filename[-5:] == '.aiff':
560            self._aifc = 0
561        else:
562            self._aifc = 1
563
564    def initfp(self, file):
565        self._file = file
566        self._version = _AIFC_version
567        self._comptype = 'NONE'
568        self._compname = 'not compressed'
569        self._comp = None
570        self._convert = None
571        self._nchannels = 0
572        self._sampwidth = 0
573        self._framerate = 0
574        self._nframes = 0
575        self._nframeswritten = 0
576        self._datawritten = 0
577        self._datalength = 0
578        self._markers = []
579        self._marklength = 0
580        self._aifc = 1      # AIFF-C is default
581
582    def __del__(self):
583        if self._file:
584            self.close()
585
586    #
587    # User visible methods.
588    #
589    def aiff(self):
590        if self._nframeswritten:
591            raise Error, 'cannot change parameters after starting to write'
592        self._aifc = 0
593
594    def aifc(self):
595        if self._nframeswritten:
596            raise Error, 'cannot change parameters after starting to write'
597        self._aifc = 1
598
599    def setnchannels(self, nchannels):
600        if self._nframeswritten:
601            raise Error, 'cannot change parameters after starting to write'
602        if nchannels < 1:
603            raise Error, 'bad # of channels'
604        self._nchannels = nchannels
605
606    def getnchannels(self):
607        if not self._nchannels:
608            raise Error, 'number of channels not set'
609        return self._nchannels
610
611    def setsampwidth(self, sampwidth):
612        if self._nframeswritten:
613            raise Error, 'cannot change parameters after starting to write'
614        if sampwidth < 1 or sampwidth > 4:
615            raise Error, 'bad sample width'
616        self._sampwidth = sampwidth
617
618    def getsampwidth(self):
619        if not self._sampwidth:
620            raise Error, 'sample width not set'
621        return self._sampwidth
622
623    def setframerate(self, framerate):
624        if self._nframeswritten:
625            raise Error, 'cannot change parameters after starting to write'
626        if framerate <= 0:
627            raise Error, 'bad frame rate'
628        self._framerate = framerate
629
630    def getframerate(self):
631        if not self._framerate:
632            raise Error, 'frame rate not set'
633        return self._framerate
634
635    def setnframes(self, nframes):
636        if self._nframeswritten:
637            raise Error, 'cannot change parameters after starting to write'
638        self._nframes = nframes
639
640    def getnframes(self):
641        return self._nframeswritten
642
643    def setcomptype(self, comptype, compname):
644        if self._nframeswritten:
645            raise Error, 'cannot change parameters after starting to write'
646        if comptype not in ('NONE', 'ULAW', 'ALAW', 'G722'):
647            raise Error, 'unsupported compression type'
648        self._comptype = comptype
649        self._compname = compname
650
651    def getcomptype(self):
652        return self._comptype
653
654    def getcompname(self):
655        return self._compname
656
657##  def setversion(self, version):
658##      if self._nframeswritten:
659##          raise Error, 'cannot change parameters after starting to write'
660##      self._version = version
661
662    def setparams(self, info):
663        nchannels, sampwidth, framerate, nframes, comptype, compname = info
664        if self._nframeswritten:
665            raise Error, 'cannot change parameters after starting to write'
666        if comptype not in ('NONE', 'ULAW', 'ALAW', 'G722'):
667            raise Error, 'unsupported compression type'
668        self.setnchannels(nchannels)
669        self.setsampwidth(sampwidth)
670        self.setframerate(framerate)
671        self.setnframes(nframes)
672        self.setcomptype(comptype, compname)
673
674    def getparams(self):
675        if not self._nchannels or not self._sampwidth or not self._framerate:
676            raise Error, 'not all parameters set'
677        return self._nchannels, self._sampwidth, self._framerate, \
678              self._nframes, self._comptype, self._compname
679
680    def setmark(self, id, pos, name):
681        if id <= 0:
682            raise Error, 'marker ID must be > 0'
683        if pos < 0:
684            raise Error, 'marker position must be >= 0'
685        if type(name) != type(''):
686            raise Error, 'marker name must be a string'
687        for i in range(len(self._markers)):
688            if id == self._markers[i][0]:
689                self._markers[i] = id, pos, name
690                return
691        self._markers.append((id, pos, name))
692
693    def getmark(self, id):
694        for marker in self._markers:
695            if id == marker[0]:
696                return marker
697        raise Error, 'marker %r does not exist' % (id,)
698
699    def getmarkers(self):
700        if len(self._markers) == 0:
701            return None
702        return self._markers
703
704    def tell(self):
705        return self._nframeswritten
706
707    def writeframesraw(self, data):
708        self._ensure_header_written(len(data))
709        nframes = len(data) // (self._sampwidth * self._nchannels)
710        if self._convert:
711            data = self._convert(data)
712        self._file.write(data)
713        self._nframeswritten = self._nframeswritten + nframes
714        self._datawritten = self._datawritten + len(data)
715
716    def writeframes(self, data):
717        self.writeframesraw(data)
718        if self._nframeswritten != self._nframes or \
719              self._datalength != self._datawritten:
720            self._patchheader()
721
722    def close(self):
723        self._ensure_header_written(0)
724        if self._datawritten & 1:
725            # quick pad to even size
726            self._file.write(chr(0))
727            self._datawritten = self._datawritten + 1
728        self._writemarkers()
729        if self._nframeswritten != self._nframes or \
730              self._datalength != self._datawritten or \
731              self._marklength:
732            self._patchheader()
733        if self._comp:
734            self._comp.CloseCompressor()
735            self._comp = None
736        # Prevent ref cycles
737        self._convert = None
738        self._file.close()
739
740    #
741    # Internal methods.
742    #
743
744    def _comp_data(self, data):
745        import cl
746        dummy = self._comp.SetParam(cl.FRAME_BUFFER_SIZE, len(data))
747        dummy = self._comp.SetParam(cl.COMPRESSED_BUFFER_SIZE, len(data))
748        return self._comp.Compress(self._nframes, data)
749
750    def _lin2ulaw(self, data):
751        import audioop
752        return audioop.lin2ulaw(data, 2)
753
754    def _lin2adpcm(self, data):
755        import audioop
756        if not hasattr(self, '_adpcmstate'):
757            self._adpcmstate = None
758        data, self._adpcmstate = audioop.lin2adpcm(data, 2,
759                               self._adpcmstate)
760        return data
761
762    def _ensure_header_written(self, datasize):
763        if not self._nframeswritten:
764            if self._comptype in ('ULAW', 'ALAW'):
765                if not self._sampwidth:
766                    self._sampwidth = 2
767                if self._sampwidth != 2:
768                    raise Error, 'sample width must be 2 when compressing with ULAW or ALAW'
769            if self._comptype == 'G722':
770                if not self._sampwidth:
771                    self._sampwidth = 2
772                if self._sampwidth != 2:
773                    raise Error, 'sample width must be 2 when compressing with G7.22 (ADPCM)'
774            if not self._nchannels:
775                raise Error, '# channels not specified'
776            if not self._sampwidth:
777                raise Error, 'sample width not specified'
778            if not self._framerate:
779                raise Error, 'sampling rate not specified'
780            self._write_header(datasize)
781
782    def _init_compression(self):
783        if self._comptype == 'G722':
784            self._convert = self._lin2adpcm
785            return
786        try:
787            import cl
788        except ImportError:
789            if self._comptype == 'ULAW':
790                try:
791                    import audioop
792                    self._convert = self._lin2ulaw
793                    return
794                except ImportError:
795                    pass
796            raise Error, 'cannot write compressed AIFF-C files'
797        if self._comptype == 'ULAW':
798            scheme = cl.G711_ULAW
799        elif self._comptype == 'ALAW':
800            scheme = cl.G711_ALAW
801        else:
802            raise Error, 'unsupported compression type'
803        self._comp = cl.OpenCompressor(scheme)
804        params = [cl.ORIGINAL_FORMAT, 0,
805              cl.BITS_PER_COMPONENT, self._sampwidth * 8,
806              cl.FRAME_RATE, self._framerate,
807              cl.FRAME_BUFFER_SIZE, 100,
808              cl.COMPRESSED_BUFFER_SIZE, 100]
809        if self._nchannels == 1:
810            params[1] = cl.MONO
811        elif self._nchannels == 2:
812            params[1] = cl.STEREO_INTERLEAVED
813        else:
814            raise Error, 'cannot compress more than 2 channels'
815        self._comp.SetParams(params)
816        # the compressor produces a header which we ignore
817        dummy = self._comp.Compress(0, '')
818        self._convert = self._comp_data
819
820    def _write_header(self, initlength):
821        if self._aifc and self._comptype != 'NONE':
822            self._init_compression()
823        self._file.write('FORM')
824        if not self._nframes:
825            self._nframes = initlength // (self._nchannels * self._sampwidth)
826        self._datalength = self._nframes * self._nchannels * self._sampwidth
827        if self._datalength & 1:
828            self._datalength = self._datalength + 1
829        if self._aifc:
830            if self._comptype in ('ULAW', 'ALAW'):
831                self._datalength = self._datalength // 2
832                if self._datalength & 1:
833                    self._datalength = self._datalength + 1
834            elif self._comptype == 'G722':
835                self._datalength = (self._datalength + 3) // 4
836                if self._datalength & 1:
837                    self._datalength = self._datalength + 1
838        self._form_length_pos = self._file.tell()
839        commlength = self._write_form_length(self._datalength)
840        if self._aifc:
841            self._file.write('AIFC')
842            self._file.write('FVER')
843            _write_long(self._file, 4)
844            _write_long(self._file, self._version)
845        else:
846            self._file.write('AIFF')
847        self._file.write('COMM')
848        _write_long(self._file, commlength)
849        _write_short(self._file, self._nchannels)
850        self._nframes_pos = self._file.tell()
851        _write_long(self._file, self._nframes)
852        _write_short(self._file, self._sampwidth * 8)
853        _write_float(self._file, self._framerate)
854        if self._aifc:
855            self._file.write(self._comptype)
856            _write_string(self._file, self._compname)
857        self._file.write('SSND')
858        self._ssnd_length_pos = self._file.tell()
859        _write_long(self._file, self._datalength + 8)
860        _write_long(self._file, 0)
861        _write_long(self._file, 0)
862
863    def _write_form_length(self, datalength):
864        if self._aifc:
865            commlength = 18 + 5 + len(self._compname)
866            if commlength & 1:
867                commlength = commlength + 1
868            verslength = 12
869        else:
870            commlength = 18
871            verslength = 0
872        _write_long(self._file, 4 + verslength + self._marklength + \
873                    8 + commlength + 16 + datalength)
874        return commlength
875
876    def _patchheader(self):
877        curpos = self._file.tell()
878        if self._datawritten & 1:
879            datalength = self._datawritten + 1
880            self._file.write(chr(0))
881        else:
882            datalength = self._datawritten
883        if datalength == self._datalength and \
884              self._nframes == self._nframeswritten and \
885              self._marklength == 0:
886            self._file.seek(curpos, 0)
887            return
888        self._file.seek(self._form_length_pos, 0)
889        dummy = self._write_form_length(datalength)
890        self._file.seek(self._nframes_pos, 0)
891        _write_long(self._file, self._nframeswritten)
892        self._file.seek(self._ssnd_length_pos, 0)
893        _write_long(self._file, datalength + 8)
894        self._file.seek(curpos, 0)
895        self._nframes = self._nframeswritten
896        self._datalength = datalength
897
898    def _writemarkers(self):
899        if len(self._markers) == 0:
900            return
901        self._file.write('MARK')
902        length = 2
903        for marker in self._markers:
904            id, pos, name = marker
905            length = length + len(name) + 1 + 6
906            if len(name) & 1 == 0:
907                length = length + 1
908        _write_long(self._file, length)
909        self._marklength = length + 8
910        _write_short(self._file, len(self._markers))
911        for marker in self._markers:
912            id, pos, name = marker
913            _write_short(self._file, id)
914            _write_long(self._file, pos)
915            _write_string(self._file, name)
916
917def open(f, mode=None):
918    if mode is None:
919        if hasattr(f, 'mode'):
920            mode = f.mode
921        else:
922            mode = 'rb'
923    if mode in ('r', 'rb'):
924        return Aifc_read(f)
925    elif mode in ('w', 'wb'):
926        return Aifc_write(f)
927    else:
928        raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"
929
930openfp = open # B/W compatibility
931
932if __name__ == '__main__':
933    import sys
934    if not sys.argv[1:]:
935        sys.argv.append('/usr/demos/data/audio/bach.aiff')
936    fn = sys.argv[1]
937    f = open(fn, 'r')
938    print "Reading", fn
939    print "nchannels =", f.getnchannels()
940    print "nframes   =", f.getnframes()
941    print "sampwidth =", f.getsampwidth()
942    print "framerate =", f.getframerate()
943    print "comptype  =", f.getcomptype()
944    print "compname  =", f.getcompname()
945    if sys.argv[2:]:
946        gn = sys.argv[2]
947        print "Writing", gn
948        g = open(gn, 'w')
949        g.setparams(f.getparams())
950        while 1:
951            data = f.readframes(1024)
952            if not data:
953                break
954            g.writeframes(data)
955        g.close()
956        f.close()
957        print "Done."
958