Home | History | Annotate | Download | only in python2.7
      1 """Stuff to parse Sun and NeXT audio files.
      2 
      3 An audio file consists of a header followed by the data.  The structure
      4 of the header is as follows.
      5 
      6         +---------------+
      7         | magic word    |
      8         +---------------+
      9         | header size   |
     10         +---------------+
     11         | data size     |
     12         +---------------+
     13         | encoding      |
     14         +---------------+
     15         | sample rate   |
     16         +---------------+
     17         | # of channels |
     18         +---------------+
     19         | info          |
     20         |               |
     21         +---------------+
     22 
     23 The magic word consists of the 4 characters '.snd'.  Apart from the
     24 info field, all header fields are 4 bytes in size.  They are all
     25 32-bit unsigned integers encoded in big-endian byte order.
     26 
     27 The header size really gives the start of the data.
     28 The data size is the physical size of the data.  From the other
     29 parameters the number of frames can be calculated.
     30 The encoding gives the way in which audio samples are encoded.
     31 Possible values are listed below.
     32 The info field currently consists of an ASCII string giving a
     33 human-readable description of the audio file.  The info field is
     34 padded with NUL bytes to the header size.
     35 
     36 Usage.
     37 
     38 Reading audio files:
     39         f = sunau.open(file, 'r')
     40 where file is either the name of a file or an open file pointer.
     41 The open file pointer must have methods read(), seek(), and close().
     42 When the setpos() and rewind() methods are not used, the seek()
     43 method is not  necessary.
     44 
     45 This returns an instance of a class with the following public methods:
     46         getnchannels()  -- returns number of audio channels (1 for
     47                            mono, 2 for stereo)
     48         getsampwidth()  -- returns sample width in bytes
     49         getframerate()  -- returns sampling frequency
     50         getnframes()    -- returns number of audio frames
     51         getcomptype()   -- returns compression type ('NONE' or 'ULAW')
     52         getcompname()   -- returns human-readable version of
     53                            compression type ('not compressed' matches 'NONE')
     54         getparams()     -- returns a tuple consisting of all of the
     55                            above in the above order
     56         getmarkers()    -- returns None (for compatibility with the
     57                            aifc module)
     58         getmark(id)     -- raises an error since the mark does not
     59                            exist (for compatibility with the aifc module)
     60         readframes(n)   -- returns at most n frames of audio
     61         rewind()        -- rewind to the beginning of the audio stream
     62         setpos(pos)     -- seek to the specified position
     63         tell()          -- return the current position
     64         close()         -- close the instance (make it unusable)
     65 The position returned by tell() and the position given to setpos()
     66 are compatible and have nothing to do with the actual position in the
     67 file.
     68 The close() method is called automatically when the class instance
     69 is destroyed.
     70 
     71 Writing audio files:
     72         f = sunau.open(file, 'w')
     73 where file is either the name of a file or an open file pointer.
     74 The open file pointer must have methods write(), tell(), seek(), and
     75 close().
     76 
     77 This returns an instance of a class with the following public methods:
     78         setnchannels(n) -- set the number of channels
     79         setsampwidth(n) -- set the sample width
     80         setframerate(n) -- set the frame rate
     81         setnframes(n)   -- set the number of frames
     82         setcomptype(type, name)
     83                         -- set the compression type and the
     84                            human-readable compression type
     85         setparams(tuple)-- set all parameters at once
     86         tell()          -- return current position in output file
     87         writeframesraw(data)
     88                         -- write audio frames without pathing up the
     89                            file header
     90         writeframes(data)
     91                         -- write audio frames and patch up the file header
     92         close()         -- patch up the file header and close the
     93                            output file
     94 You should set the parameters before the first writeframesraw or
     95 writeframes.  The total number of frames does not need to be set,
     96 but when it is set to the correct value, the header does not have to
     97 be patched up.
     98 It is best to first set all parameters, perhaps possibly the
     99 compression type, and then write audio frames using writeframesraw.
    100 When all frames have been written, either call writeframes('') or
    101 close() to patch up the sizes in the header.
    102 The close() method is called automatically when the class instance
    103 is destroyed.
    104 """
    105 
    106 # from <multimedia/audio_filehdr.h>
    107 AUDIO_FILE_MAGIC = 0x2e736e64
    108 AUDIO_FILE_ENCODING_MULAW_8 = 1
    109 AUDIO_FILE_ENCODING_LINEAR_8 = 2
    110 AUDIO_FILE_ENCODING_LINEAR_16 = 3
    111 AUDIO_FILE_ENCODING_LINEAR_24 = 4
    112 AUDIO_FILE_ENCODING_LINEAR_32 = 5
    113 AUDIO_FILE_ENCODING_FLOAT = 6
    114 AUDIO_FILE_ENCODING_DOUBLE = 7
    115 AUDIO_FILE_ENCODING_ADPCM_G721 = 23
    116 AUDIO_FILE_ENCODING_ADPCM_G722 = 24
    117 AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25
    118 AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26
    119 AUDIO_FILE_ENCODING_ALAW_8 = 27
    120 
    121 # from <multimedia/audio_hdr.h>
    122 AUDIO_UNKNOWN_SIZE = 0xFFFFFFFFL        # ((unsigned)(~0))
    123 
    124 _simple_encodings = [AUDIO_FILE_ENCODING_MULAW_8,
    125                      AUDIO_FILE_ENCODING_LINEAR_8,
    126                      AUDIO_FILE_ENCODING_LINEAR_16,
    127                      AUDIO_FILE_ENCODING_LINEAR_24,
    128                      AUDIO_FILE_ENCODING_LINEAR_32,
    129                      AUDIO_FILE_ENCODING_ALAW_8]
    130 
    131 class Error(Exception):
    132     pass
    133 
    134 def _read_u32(file):
    135     x = 0L
    136     for i in range(4):
    137         byte = file.read(1)
    138         if byte == '':
    139             raise EOFError
    140         x = x*256 + ord(byte)
    141     return x
    142 
    143 def _write_u32(file, x):
    144     data = []
    145     for i in range(4):
    146         d, m = divmod(x, 256)
    147         data.insert(0, m)
    148         x = d
    149     for i in range(4):
    150         file.write(chr(int(data[i])))
    151 
    152 class Au_read:
    153 
    154     def __init__(self, f):
    155         if type(f) == type(''):
    156             import __builtin__
    157             f = __builtin__.open(f, 'rb')
    158         self.initfp(f)
    159 
    160     def __del__(self):
    161         if self._file:
    162             self.close()
    163 
    164     def initfp(self, file):
    165         self._file = file
    166         self._soundpos = 0
    167         magic = int(_read_u32(file))
    168         if magic != AUDIO_FILE_MAGIC:
    169             raise Error, 'bad magic number'
    170         self._hdr_size = int(_read_u32(file))
    171         if self._hdr_size < 24:
    172             raise Error, 'header size too small'
    173         if self._hdr_size > 100:
    174             raise Error, 'header size ridiculously large'
    175         self._data_size = _read_u32(file)
    176         if self._data_size != AUDIO_UNKNOWN_SIZE:
    177             self._data_size = int(self._data_size)
    178         self._encoding = int(_read_u32(file))
    179         if self._encoding not in _simple_encodings:
    180             raise Error, 'encoding not (yet) supported'
    181         if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8,
    182                   AUDIO_FILE_ENCODING_ALAW_8):
    183             self._sampwidth = 2
    184             self._framesize = 1
    185         elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_8:
    186             self._framesize = self._sampwidth = 1
    187         elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16:
    188             self._framesize = self._sampwidth = 2
    189         elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24:
    190             self._framesize = self._sampwidth = 3
    191         elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32:
    192             self._framesize = self._sampwidth = 4
    193         else:
    194             raise Error, 'unknown encoding'
    195         self._framerate = int(_read_u32(file))
    196         self._nchannels = int(_read_u32(file))
    197         self._framesize = self._framesize * self._nchannels
    198         if self._hdr_size > 24:
    199             self._info = file.read(self._hdr_size - 24)
    200             for i in range(len(self._info)):
    201                 if self._info[i] == '\0':
    202                     self._info = self._info[:i]
    203                     break
    204         else:
    205             self._info = ''
    206 
    207     def getfp(self):
    208         return self._file
    209 
    210     def getnchannels(self):
    211         return self._nchannels
    212 
    213     def getsampwidth(self):
    214         return self._sampwidth
    215 
    216     def getframerate(self):
    217         return self._framerate
    218 
    219     def getnframes(self):
    220         if self._data_size == AUDIO_UNKNOWN_SIZE:
    221             return AUDIO_UNKNOWN_SIZE
    222         if self._encoding in _simple_encodings:
    223             return self._data_size / self._framesize
    224         return 0                # XXX--must do some arithmetic here
    225 
    226     def getcomptype(self):
    227         if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
    228             return 'ULAW'
    229         elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
    230             return 'ALAW'
    231         else:
    232             return 'NONE'
    233 
    234     def getcompname(self):
    235         if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
    236             return 'CCITT G.711 u-law'
    237         elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
    238             return 'CCITT G.711 A-law'
    239         else:
    240             return 'not compressed'
    241 
    242     def getparams(self):
    243         return self.getnchannels(), self.getsampwidth(), \
    244                   self.getframerate(), self.getnframes(), \
    245                   self.getcomptype(), self.getcompname()
    246 
    247     def getmarkers(self):
    248         return None
    249 
    250     def getmark(self, id):
    251         raise Error, 'no marks'
    252 
    253     def readframes(self, nframes):
    254         if self._encoding in _simple_encodings:
    255             if nframes == AUDIO_UNKNOWN_SIZE:
    256                 data = self._file.read()
    257             else:
    258                 data = self._file.read(nframes * self._framesize * self._nchannels)
    259             if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
    260                 import audioop
    261                 data = audioop.ulaw2lin(data, self._sampwidth)
    262             return data
    263         return None             # XXX--not implemented yet
    264 
    265     def rewind(self):
    266         self._soundpos = 0
    267         self._file.seek(self._hdr_size)
    268 
    269     def tell(self):
    270         return self._soundpos
    271 
    272     def setpos(self, pos):
    273         if pos < 0 or pos > self.getnframes():
    274             raise Error, 'position not in range'
    275         self._file.seek(pos * self._framesize + self._hdr_size)
    276         self._soundpos = pos
    277 
    278     def close(self):
    279         self._file = None
    280 
    281 class Au_write:
    282 
    283     def __init__(self, f):
    284         if type(f) == type(''):
    285             import __builtin__
    286             f = __builtin__.open(f, 'wb')
    287         self.initfp(f)
    288 
    289     def __del__(self):
    290         if self._file:
    291             self.close()
    292 
    293     def initfp(self, file):
    294         self._file = file
    295         self._framerate = 0
    296         self._nchannels = 0
    297         self._sampwidth = 0
    298         self._framesize = 0
    299         self._nframes = AUDIO_UNKNOWN_SIZE
    300         self._nframeswritten = 0
    301         self._datawritten = 0
    302         self._datalength = 0
    303         self._info = ''
    304         self._comptype = 'ULAW' # default is U-law
    305 
    306     def setnchannels(self, nchannels):
    307         if self._nframeswritten:
    308             raise Error, 'cannot change parameters after starting to write'
    309         if nchannels not in (1, 2, 4):
    310             raise Error, 'only 1, 2, or 4 channels supported'
    311         self._nchannels = nchannels
    312 
    313     def getnchannels(self):
    314         if not self._nchannels:
    315             raise Error, 'number of channels not set'
    316         return self._nchannels
    317 
    318     def setsampwidth(self, sampwidth):
    319         if self._nframeswritten:
    320             raise Error, 'cannot change parameters after starting to write'
    321         if sampwidth not in (1, 2, 4):
    322             raise Error, 'bad sample width'
    323         self._sampwidth = sampwidth
    324 
    325     def getsampwidth(self):
    326         if not self._framerate:
    327             raise Error, 'sample width not specified'
    328         return self._sampwidth
    329 
    330     def setframerate(self, framerate):
    331         if self._nframeswritten:
    332             raise Error, 'cannot change parameters after starting to write'
    333         self._framerate = framerate
    334 
    335     def getframerate(self):
    336         if not self._framerate:
    337             raise Error, 'frame rate not set'
    338         return self._framerate
    339 
    340     def setnframes(self, nframes):
    341         if self._nframeswritten:
    342             raise Error, 'cannot change parameters after starting to write'
    343         if nframes < 0:
    344             raise Error, '# of frames cannot be negative'
    345         self._nframes = nframes
    346 
    347     def getnframes(self):
    348         return self._nframeswritten
    349 
    350     def setcomptype(self, type, name):
    351         if type in ('NONE', 'ULAW'):
    352             self._comptype = type
    353         else:
    354             raise Error, 'unknown compression type'
    355 
    356     def getcomptype(self):
    357         return self._comptype
    358 
    359     def getcompname(self):
    360         if self._comptype == 'ULAW':
    361             return 'CCITT G.711 u-law'
    362         elif self._comptype == 'ALAW':
    363             return 'CCITT G.711 A-law'
    364         else:
    365             return 'not compressed'
    366 
    367     def setparams(self, params):
    368         nchannels, sampwidth, framerate, nframes, comptype, compname = params
    369         self.setnchannels(nchannels)
    370         self.setsampwidth(sampwidth)
    371         self.setframerate(framerate)
    372         self.setnframes(nframes)
    373         self.setcomptype(comptype, compname)
    374 
    375     def getparams(self):
    376         return self.getnchannels(), self.getsampwidth(), \
    377                   self.getframerate(), self.getnframes(), \
    378                   self.getcomptype(), self.getcompname()
    379 
    380     def tell(self):
    381         return self._nframeswritten
    382 
    383     def writeframesraw(self, data):
    384         self._ensure_header_written()
    385         nframes = len(data) / self._framesize
    386         if self._comptype == 'ULAW':
    387             import audioop
    388             data = audioop.lin2ulaw(data, self._sampwidth)
    389         self._file.write(data)
    390         self._nframeswritten = self._nframeswritten + nframes
    391         self._datawritten = self._datawritten + len(data)
    392 
    393     def writeframes(self, data):
    394         self.writeframesraw(data)
    395         if self._nframeswritten != self._nframes or \
    396                   self._datalength != self._datawritten:
    397             self._patchheader()
    398 
    399     def close(self):
    400         self._ensure_header_written()
    401         if self._nframeswritten != self._nframes or \
    402                   self._datalength != self._datawritten:
    403             self._patchheader()
    404         self._file.flush()
    405         self._file = None
    406 
    407     #
    408     # private methods
    409     #
    410 
    411     def _ensure_header_written(self):
    412         if not self._nframeswritten:
    413             if not self._nchannels:
    414                 raise Error, '# of channels not specified'
    415             if not self._sampwidth:
    416                 raise Error, 'sample width not specified'
    417             if not self._framerate:
    418                 raise Error, 'frame rate not specified'
    419             self._write_header()
    420 
    421     def _write_header(self):
    422         if self._comptype == 'NONE':
    423             if self._sampwidth == 1:
    424                 encoding = AUDIO_FILE_ENCODING_LINEAR_8
    425                 self._framesize = 1
    426             elif self._sampwidth == 2:
    427                 encoding = AUDIO_FILE_ENCODING_LINEAR_16
    428                 self._framesize = 2
    429             elif self._sampwidth == 4:
    430                 encoding = AUDIO_FILE_ENCODING_LINEAR_32
    431                 self._framesize = 4
    432             else:
    433                 raise Error, 'internal error'
    434         elif self._comptype == 'ULAW':
    435             encoding = AUDIO_FILE_ENCODING_MULAW_8
    436             self._framesize = 1
    437         else:
    438             raise Error, 'internal error'
    439         self._framesize = self._framesize * self._nchannels
    440         _write_u32(self._file, AUDIO_FILE_MAGIC)
    441         header_size = 25 + len(self._info)
    442         header_size = (header_size + 7) & ~7
    443         _write_u32(self._file, header_size)
    444         if self._nframes == AUDIO_UNKNOWN_SIZE:
    445             length = AUDIO_UNKNOWN_SIZE
    446         else:
    447             length = self._nframes * self._framesize
    448         _write_u32(self._file, length)
    449         self._datalength = length
    450         _write_u32(self._file, encoding)
    451         _write_u32(self._file, self._framerate)
    452         _write_u32(self._file, self._nchannels)
    453         self._file.write(self._info)
    454         self._file.write('\0'*(header_size - len(self._info) - 24))
    455 
    456     def _patchheader(self):
    457         self._file.seek(8)
    458         _write_u32(self._file, self._datawritten)
    459         self._datalength = self._datawritten
    460         self._file.seek(0, 2)
    461 
    462 def open(f, mode=None):
    463     if mode is None:
    464         if hasattr(f, 'mode'):
    465             mode = f.mode
    466         else:
    467             mode = 'rb'
    468     if mode in ('r', 'rb'):
    469         return Au_read(f)
    470     elif mode in ('w', 'wb'):
    471         return Au_write(f)
    472     else:
    473         raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"
    474 
    475 openfp = open
    476