• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        try:
207            self._data_pos = file.tell()
208        except (AttributeError, IOError):
209            self._data_pos = None
210
211    def getfp(self):
212        return self._file
213
214    def getnchannels(self):
215        return self._nchannels
216
217    def getsampwidth(self):
218        return self._sampwidth
219
220    def getframerate(self):
221        return self._framerate
222
223    def getnframes(self):
224        if self._data_size == AUDIO_UNKNOWN_SIZE:
225            return AUDIO_UNKNOWN_SIZE
226        if self._encoding in _simple_encodings:
227            return self._data_size // self._framesize
228        return 0                # XXX--must do some arithmetic here
229
230    def getcomptype(self):
231        if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
232            return 'ULAW'
233        elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
234            return 'ALAW'
235        else:
236            return 'NONE'
237
238    def getcompname(self):
239        if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
240            return 'CCITT G.711 u-law'
241        elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
242            return 'CCITT G.711 A-law'
243        else:
244            return 'not compressed'
245
246    def getparams(self):
247        return self.getnchannels(), self.getsampwidth(), \
248                  self.getframerate(), self.getnframes(), \
249                  self.getcomptype(), self.getcompname()
250
251    def getmarkers(self):
252        return None
253
254    def getmark(self, id):
255        raise Error, 'no marks'
256
257    def readframes(self, nframes):
258        if self._encoding in _simple_encodings:
259            if nframes == AUDIO_UNKNOWN_SIZE:
260                data = self._file.read()
261            else:
262                data = self._file.read(nframes * self._framesize)
263            self._soundpos += len(data) // self._framesize
264            if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
265                import audioop
266                data = audioop.ulaw2lin(data, self._sampwidth)
267            return data
268        return None             # XXX--not implemented yet
269
270    def rewind(self):
271        if self._data_pos is None:
272            raise IOError('cannot seek')
273        self._file.seek(self._data_pos)
274        self._soundpos = 0
275
276    def tell(self):
277        return self._soundpos
278
279    def setpos(self, pos):
280        if pos < 0 or pos > self.getnframes():
281            raise Error, 'position not in range'
282        if self._data_pos is None:
283            raise IOError('cannot seek')
284        self._file.seek(self._data_pos + pos * self._framesize)
285        self._soundpos = pos
286
287    def close(self):
288        self._file = None
289
290class Au_write:
291
292    def __init__(self, f):
293        if type(f) == type(''):
294            import __builtin__
295            f = __builtin__.open(f, 'wb')
296        self.initfp(f)
297
298    def __del__(self):
299        if self._file:
300            self.close()
301
302    def initfp(self, file):
303        self._file = file
304        self._framerate = 0
305        self._nchannels = 0
306        self._sampwidth = 0
307        self._framesize = 0
308        self._nframes = AUDIO_UNKNOWN_SIZE
309        self._nframeswritten = 0
310        self._datawritten = 0
311        self._datalength = 0
312        self._info = ''
313        self._comptype = 'ULAW' # default is U-law
314
315    def setnchannels(self, nchannels):
316        if self._nframeswritten:
317            raise Error, 'cannot change parameters after starting to write'
318        if nchannels not in (1, 2, 4):
319            raise Error, 'only 1, 2, or 4 channels supported'
320        self._nchannels = nchannels
321
322    def getnchannels(self):
323        if not self._nchannels:
324            raise Error, 'number of channels not set'
325        return self._nchannels
326
327    def setsampwidth(self, sampwidth):
328        if self._nframeswritten:
329            raise Error, 'cannot change parameters after starting to write'
330        if sampwidth not in (1, 2, 4):
331            raise Error, 'bad sample width'
332        self._sampwidth = sampwidth
333
334    def getsampwidth(self):
335        if not self._framerate:
336            raise Error, 'sample width not specified'
337        return self._sampwidth
338
339    def setframerate(self, framerate):
340        if self._nframeswritten:
341            raise Error, 'cannot change parameters after starting to write'
342        self._framerate = framerate
343
344    def getframerate(self):
345        if not self._framerate:
346            raise Error, 'frame rate not set'
347        return self._framerate
348
349    def setnframes(self, nframes):
350        if self._nframeswritten:
351            raise Error, 'cannot change parameters after starting to write'
352        if nframes < 0:
353            raise Error, '# of frames cannot be negative'
354        self._nframes = nframes
355
356    def getnframes(self):
357        return self._nframeswritten
358
359    def setcomptype(self, type, name):
360        if type in ('NONE', 'ULAW'):
361            self._comptype = type
362        else:
363            raise Error, 'unknown compression type'
364
365    def getcomptype(self):
366        return self._comptype
367
368    def getcompname(self):
369        if self._comptype == 'ULAW':
370            return 'CCITT G.711 u-law'
371        elif self._comptype == 'ALAW':
372            return 'CCITT G.711 A-law'
373        else:
374            return 'not compressed'
375
376    def setparams(self, params):
377        nchannels, sampwidth, framerate, nframes, comptype, compname = params
378        self.setnchannels(nchannels)
379        self.setsampwidth(sampwidth)
380        self.setframerate(framerate)
381        self.setnframes(nframes)
382        self.setcomptype(comptype, compname)
383
384    def getparams(self):
385        return self.getnchannels(), self.getsampwidth(), \
386                  self.getframerate(), self.getnframes(), \
387                  self.getcomptype(), self.getcompname()
388
389    def tell(self):
390        return self._nframeswritten
391
392    def writeframesraw(self, data):
393        self._ensure_header_written()
394        if self._comptype == 'ULAW':
395            import audioop
396            data = audioop.lin2ulaw(data, self._sampwidth)
397        nframes = len(data) // self._framesize
398        self._file.write(data)
399        self._nframeswritten = self._nframeswritten + nframes
400        self._datawritten = self._datawritten + len(data)
401
402    def writeframes(self, data):
403        self.writeframesraw(data)
404        if self._nframeswritten != self._nframes or \
405                  self._datalength != self._datawritten:
406            self._patchheader()
407
408    def close(self):
409        if self._file:
410            try:
411                self._ensure_header_written()
412                if self._nframeswritten != self._nframes or \
413                        self._datalength != self._datawritten:
414                    self._patchheader()
415                self._file.flush()
416            finally:
417                self._file = None
418
419    #
420    # private methods
421    #
422
423    def _ensure_header_written(self):
424        if not self._nframeswritten:
425            if not self._nchannels:
426                raise Error, '# of channels not specified'
427            if not self._sampwidth:
428                raise Error, 'sample width not specified'
429            if not self._framerate:
430                raise Error, 'frame rate not specified'
431            self._write_header()
432
433    def _write_header(self):
434        if self._comptype == 'NONE':
435            if self._sampwidth == 1:
436                encoding = AUDIO_FILE_ENCODING_LINEAR_8
437                self._framesize = 1
438            elif self._sampwidth == 2:
439                encoding = AUDIO_FILE_ENCODING_LINEAR_16
440                self._framesize = 2
441            elif self._sampwidth == 4:
442                encoding = AUDIO_FILE_ENCODING_LINEAR_32
443                self._framesize = 4
444            else:
445                raise Error, 'internal error'
446        elif self._comptype == 'ULAW':
447            encoding = AUDIO_FILE_ENCODING_MULAW_8
448            self._framesize = 1
449        else:
450            raise Error, 'internal error'
451        self._framesize = self._framesize * self._nchannels
452        _write_u32(self._file, AUDIO_FILE_MAGIC)
453        header_size = 25 + len(self._info)
454        header_size = (header_size + 7) & ~7
455        _write_u32(self._file, header_size)
456        if self._nframes == AUDIO_UNKNOWN_SIZE:
457            length = AUDIO_UNKNOWN_SIZE
458        else:
459            length = self._nframes * self._framesize
460        try:
461            self._form_length_pos = self._file.tell()
462        except (AttributeError, IOError):
463            self._form_length_pos = None
464        _write_u32(self._file, length)
465        self._datalength = length
466        _write_u32(self._file, encoding)
467        _write_u32(self._file, self._framerate)
468        _write_u32(self._file, self._nchannels)
469        self._file.write(self._info)
470        self._file.write('\0'*(header_size - len(self._info) - 24))
471
472    def _patchheader(self):
473        if self._form_length_pos is None:
474            raise IOError('cannot seek')
475        self._file.seek(self._form_length_pos)
476        _write_u32(self._file, self._datawritten)
477        self._datalength = self._datawritten
478        self._file.seek(0, 2)
479
480def open(f, mode=None):
481    if mode is None:
482        if hasattr(f, 'mode'):
483            mode = f.mode
484        else:
485            mode = 'rb'
486    if mode in ('r', 'rb'):
487        return Au_read(f)
488    elif mode in ('w', 'wb'):
489        return Au_write(f)
490    else:
491        raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"
492
493openfp = open
494