Home | History | Annotate | Download | only in python2.7
      1 """Stuff to parse AIFF-C and AIFF files.
      2 
      3 Unless explicitly stated otherwise, the description below is true
      4 both for AIFF-C files and AIFF files.
      5 
      6 An 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 
     21 An AIFF file has the string "AIFF" instead of "AIFC".
     22 
     23 A chunk consists of an identifier (4 bytes) followed by a size (4 bytes,
     24 big endian order), followed by the data.  The size field does not include
     25 the size of the 8 byte header.
     26 
     27 The 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 
     51 A pstring consists of 1 byte length, a string of characters, and 0 or 1
     52 byte pad to make the total length even.
     53 
     54 Usage.
     55 
     56 Reading AIFF files:
     57   f = aifc.open(file, 'r')
     58 where file is either the name of a file or an open file pointer.
     59 The open file pointer must have methods read(), seek(), and close().
     60 In some types of audio files, if the setpos() method is not used,
     61 the seek() method is not necessary.
     62 
     63 This 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)
     83 The position returned by tell(), the position given to setpos() and
     84 the position of marks are all compatible and have nothing to do with
     85 the actual position in the file.
     86 The close() method is called automatically when the class instance
     87 is destroyed.
     88 
     89 Writing AIFF files:
     90   f = aifc.open(file, 'w')
     91 where file is either the name of a file or an open file pointer.
     92 The open file pointer must have methods write(), tell(), seek(), and
     93 close().
     94 
     95 This 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
    118 You should set the parameters before the first writeframesraw or
    119 writeframes.  The total number of frames does not need to be set,
    120 but when it is set to the correct value, the header does not have to
    121 be patched up.
    122 It is best to first set all parameters, perhaps possibly the
    123 compression type, and then write audio frames using writeframesraw.
    124 When all frames have been written, either call writeframes('') or
    125 close() to patch up the sizes in the header.
    126 Marks can be added anytime.  If there are any marks, ypu must call
    127 close() after all frames have been written.
    128 The close() method is called automatically when the class instance
    129 is destroyed.
    130 
    131 When a file is opened with the extension '.aiff', an AIFF file is
    132 written, otherwise an AIFF-C file is written.  This default can be
    133 changed by calling aiff() or aifc() before the first writeframes or
    134 writeframesraw.
    135 """
    136 
    137 import struct
    138 import __builtin__
    139 
    140 __all__ = ["Error","open","openfp"]
    141 
    142 class Error(Exception):
    143     pass
    144 
    145 _AIFC_version = 0xA2805140L     # Version 1 of AIFF-C
    146 
    147 def _read_long(file):
    148     try:
    149         return struct.unpack('>l', file.read(4))[0]
    150     except struct.error:
    151         raise EOFError
    152 
    153 def _read_ulong(file):
    154     try:
    155         return struct.unpack('>L', file.read(4))[0]
    156     except struct.error:
    157         raise EOFError
    158 
    159 def _read_short(file):
    160     try:
    161         return struct.unpack('>h', file.read(2))[0]
    162     except struct.error:
    163         raise EOFError
    164 
    165 def _read_ushort(file):
    166     try:
    167         return struct.unpack('>H', file.read(2))[0]
    168     except struct.error:
    169         raise EOFError
    170 
    171 def _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 
    183 def _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 
    200 def _write_short(f, x):
    201     f.write(struct.pack('>h', x))
    202 
    203 def _write_ushort(f, x):
    204     f.write(struct.pack('>H', x))
    205 
    206 def _write_long(f, x):
    207     f.write(struct.pack('>l', x))
    208 
    209 def _write_ulong(f, x):
    210     f.write(struct.pack('>L', x))
    211 
    212 def _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 
    220 def _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 
    253 from chunk import Chunk
    254 
    255 class 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 
    533 class 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 
    935 def 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 
    948 openfp = open # B/W compatibility
    949 
    950 if __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