|
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 |