]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/sunau.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / sunau.py
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, (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
461 def 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
474 openfp = open