source: python/trunk/Lib/sunau.py@ 393

Last change on this file since 393 was 391, checked in by dmik, 11 years ago

python: Merge vendor 2.7.6 to trunk.

  • Property svn:eol-style set to native
File size: 16.8 KB
Line 
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
Note: See TracBrowser for help on using the repository browser.