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