PageRenderTime 28ms CodeModel.GetById 8ms app.highlight 16ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/sunau.py

http://unladen-swallow.googlecode.com/
Python | 474 lines | 433 code | 16 blank | 25 comment | 31 complexity | bc7d91aa872eb3deb5a261b18d3c6da4 MD5 | raw file
  1"""Stuff to parse Sun and NeXT audio files.
  2
  3An audio file consists of a header followed by the data.  The structure
  4of 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
 23The magic word consists of the 4 characters '.snd'.  Apart from the
 24info field, all header fields are 4 bytes in size.  They are all
 2532-bit unsigned integers encoded in big-endian byte order.
 26
 27The header size really gives the start of the data.
 28The data size is the physical size of the data.  From the other
 29parameters the number of frames can be calculated.
 30The encoding gives the way in which audio samples are encoded.
 31Possible values are listed below.
 32The info field currently consists of an ASCII string giving a
 33human-readable description of the audio file.  The info field is
 34padded with NUL bytes to the header size.
 35
 36Usage.
 37
 38Reading audio files:
 39        f = sunau.open(file, 'r')
 40where file is either the name of a file or an open file pointer.
 41The open file pointer must have methods read(), seek(), and close().
 42When the setpos() and rewind() methods are not used, the seek()
 43method is not  necessary.
 44
 45This 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)
 65The position returned by tell() and the position given to setpos()
 66are compatible and have nothing to do with the actual position in the
 67file.
 68The close() method is called automatically when the class instance
 69is destroyed.
 70
 71Writing audio files:
 72        f = sunau.open(file, 'w')
 73where file is either the name of a file or an open file pointer.
 74The open file pointer must have methods write(), tell(), seek(), and
 75close().
 76
 77This 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
 94You should set the parameters before the first writeframesraw or
 95writeframes.  The total number of frames does not need to be set,
 96but when it is set to the correct value, the header does not have to
 97be patched up.
 98It is best to first set all parameters, perhaps possibly the
 99compression type, and then write audio frames using writeframesraw.
100When all frames have been written, either call writeframes('') or
101close() to patch up the sizes in the header.
102The close() method is called automatically when the class instance
103is destroyed.
104"""
105
106# from <multimedia/audio_filehdr.h>
107AUDIO_FILE_MAGIC = 0x2e736e64
108AUDIO_FILE_ENCODING_MULAW_8 = 1
109AUDIO_FILE_ENCODING_LINEAR_8 = 2
110AUDIO_FILE_ENCODING_LINEAR_16 = 3
111AUDIO_FILE_ENCODING_LINEAR_24 = 4
112AUDIO_FILE_ENCODING_LINEAR_32 = 5
113AUDIO_FILE_ENCODING_FLOAT = 6
114AUDIO_FILE_ENCODING_DOUBLE = 7
115AUDIO_FILE_ENCODING_ADPCM_G721 = 23
116AUDIO_FILE_ENCODING_ADPCM_G722 = 24
117AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25
118AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26
119AUDIO_FILE_ENCODING_ALAW_8 = 27
120
121# from <multimedia/audio_hdr.h>
122AUDIO_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
131class Error(Exception):
132    pass
133
134def _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
143def _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
152class 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
281class 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, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
368        self.setnchannels(nchannels)
369        self.setsampwidth(sampwidth)
370        self.setframerate(framerate)
371        self.setnframes(nframes)
372        self.setcomptype(comptype, compname)
373
374    def getparams(self):
375        return self.getnchannels(), self.getsampwidth(), \
376                  self.getframerate(), self.getnframes(), \
377                  self.getcomptype(), self.getcompname()
378
379    def tell(self):
380        return self._nframeswritten
381
382    def writeframesraw(self, data):
383        self._ensure_header_written()
384        nframes = len(data) / self._framesize
385        if self._comptype == 'ULAW':
386            import audioop
387            data = audioop.lin2ulaw(data, self._sampwidth)
388        self._file.write(data)
389        self._nframeswritten = self._nframeswritten + nframes
390        self._datawritten = self._datawritten + len(data)
391
392    def writeframes(self, data):
393        self.writeframesraw(data)
394        if self._nframeswritten != self._nframes or \
395                  self._datalength != self._datawritten:
396            self._patchheader()
397
398    def close(self):
399        self._ensure_header_written()
400        if self._nframeswritten != self._nframes or \
401                  self._datalength != self._datawritten:
402            self._patchheader()
403        self._file.flush()
404        self._file = None
405
406    #
407    # private methods
408    #
409
410    def _ensure_header_written(self):
411        if not self._nframeswritten:
412            if not self._nchannels:
413                raise Error, '# of channels not specified'
414            if not self._sampwidth:
415                raise Error, 'sample width not specified'
416            if not self._framerate:
417                raise Error, 'frame rate not specified'
418            self._write_header()
419
420    def _write_header(self):
421        if self._comptype == 'NONE':
422            if self._sampwidth == 1:
423                encoding = AUDIO_FILE_ENCODING_LINEAR_8
424                self._framesize = 1
425            elif self._sampwidth == 2:
426                encoding = AUDIO_FILE_ENCODING_LINEAR_16
427                self._framesize = 2
428            elif self._sampwidth == 4:
429                encoding = AUDIO_FILE_ENCODING_LINEAR_32
430                self._framesize = 4
431            else:
432                raise Error, 'internal error'
433        elif self._comptype == 'ULAW':
434            encoding = AUDIO_FILE_ENCODING_MULAW_8
435            self._framesize = 1
436        else:
437            raise Error, 'internal error'
438        self._framesize = self._framesize * self._nchannels
439        _write_u32(self._file, AUDIO_FILE_MAGIC)
440        header_size = 25 + len(self._info)
441        header_size = (header_size + 7) & ~7
442        _write_u32(self._file, header_size)
443        if self._nframes == AUDIO_UNKNOWN_SIZE:
444            length = AUDIO_UNKNOWN_SIZE
445        else:
446            length = self._nframes * self._framesize
447        _write_u32(self._file, length)
448        self._datalength = length
449        _write_u32(self._file, encoding)
450        _write_u32(self._file, self._framerate)
451        _write_u32(self._file, self._nchannels)
452        self._file.write(self._info)
453        self._file.write('\0'*(header_size - len(self._info) - 24))
454
455    def _patchheader(self):
456        self._file.seek(8)
457        _write_u32(self._file, self._datawritten)
458        self._datalength = self._datawritten
459        self._file.seek(0, 2)
460
461def open(f, mode=None):
462    if mode is None:
463        if hasattr(f, 'mode'):
464            mode = f.mode
465        else:
466            mode = 'rb'
467    if mode in ('r', 'rb'):
468        return Au_read(f)
469    elif mode in ('w', 'wb'):
470        return Au_write(f)
471    else:
472        raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"
473
474openfp = open