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 namedtuple 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(b'') 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 collections import namedtuple
107 import warnings
108
109 warnings._deprecated(__name__, remove=(3, 13))
110
111
112 _sunau_params = namedtuple('_sunau_params',
113 'nchannels sampwidth framerate nframes comptype compname')
114
115 # from <multimedia/audio_filehdr.h>
116 AUDIO_FILE_MAGIC = 0x2e736e64
117 AUDIO_FILE_ENCODING_MULAW_8 = 1
118 AUDIO_FILE_ENCODING_LINEAR_8 = 2
119 AUDIO_FILE_ENCODING_LINEAR_16 = 3
120 AUDIO_FILE_ENCODING_LINEAR_24 = 4
121 AUDIO_FILE_ENCODING_LINEAR_32 = 5
122 AUDIO_FILE_ENCODING_FLOAT = 6
123 AUDIO_FILE_ENCODING_DOUBLE = 7
124 AUDIO_FILE_ENCODING_ADPCM_G721 = 23
125 AUDIO_FILE_ENCODING_ADPCM_G722 = 24
126 AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25
127 AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26
128 AUDIO_FILE_ENCODING_ALAW_8 = 27
129
130 # from <multimedia/audio_hdr.h>
131 AUDIO_UNKNOWN_SIZE = 0xFFFFFFFF # ((unsigned)(~0))
132
133 _simple_encodings = [AUDIO_FILE_ENCODING_MULAW_8,
134 AUDIO_FILE_ENCODING_LINEAR_8,
135 AUDIO_FILE_ENCODING_LINEAR_16,
136 AUDIO_FILE_ENCODING_LINEAR_24,
137 AUDIO_FILE_ENCODING_LINEAR_32,
138 AUDIO_FILE_ENCODING_ALAW_8]
139
140 class ESC[4;38;5;81mError(ESC[4;38;5;149mException):
141 pass
142
143 def _read_u32(file):
144 x = 0
145 for i in range(4):
146 byte = file.read(1)
147 if not byte:
148 raise EOFError
149 x = x*256 + ord(byte)
150 return x
151
152 def _write_u32(file, x):
153 data = []
154 for i in range(4):
155 d, m = divmod(x, 256)
156 data.insert(0, int(m))
157 x = d
158 file.write(bytes(data))
159
160 class ESC[4;38;5;81mAu_read:
161
162 def __init__(self, f):
163 if isinstance(f, str):
164 import builtins
165 f = builtins.open(f, 'rb')
166 self._opened = True
167 else:
168 self._opened = False
169 self.initfp(f)
170
171 def __del__(self):
172 if self._file:
173 self.close()
174
175 def __enter__(self):
176 return self
177
178 def __exit__(self, *args):
179 self.close()
180
181 def initfp(self, file):
182 self._file = file
183 self._soundpos = 0
184 magic = int(_read_u32(file))
185 if magic != AUDIO_FILE_MAGIC:
186 raise Error('bad magic number')
187 self._hdr_size = int(_read_u32(file))
188 if self._hdr_size < 24:
189 raise Error('header size too small')
190 if self._hdr_size > 100:
191 raise Error('header size ridiculously large')
192 self._data_size = _read_u32(file)
193 if self._data_size != AUDIO_UNKNOWN_SIZE:
194 self._data_size = int(self._data_size)
195 self._encoding = int(_read_u32(file))
196 if self._encoding not in _simple_encodings:
197 raise Error('encoding not (yet) supported')
198 if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8,
199 AUDIO_FILE_ENCODING_ALAW_8):
200 self._sampwidth = 2
201 self._framesize = 1
202 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_8:
203 self._framesize = self._sampwidth = 1
204 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16:
205 self._framesize = self._sampwidth = 2
206 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24:
207 self._framesize = self._sampwidth = 3
208 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32:
209 self._framesize = self._sampwidth = 4
210 else:
211 raise Error('unknown encoding')
212 self._framerate = int(_read_u32(file))
213 self._nchannels = int(_read_u32(file))
214 if not self._nchannels:
215 raise Error('bad # of channels')
216 self._framesize = self._framesize * self._nchannels
217 if self._hdr_size > 24:
218 self._info = file.read(self._hdr_size - 24)
219 self._info, _, _ = self._info.partition(b'\0')
220 else:
221 self._info = b''
222 try:
223 self._data_pos = file.tell()
224 except (AttributeError, OSError):
225 self._data_pos = None
226
227 def getfp(self):
228 return self._file
229
230 def getnchannels(self):
231 return self._nchannels
232
233 def getsampwidth(self):
234 return self._sampwidth
235
236 def getframerate(self):
237 return self._framerate
238
239 def getnframes(self):
240 if self._data_size == AUDIO_UNKNOWN_SIZE:
241 return AUDIO_UNKNOWN_SIZE
242 if self._encoding in _simple_encodings:
243 return self._data_size // self._framesize
244 return 0 # XXX--must do some arithmetic here
245
246 def getcomptype(self):
247 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
248 return 'ULAW'
249 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
250 return 'ALAW'
251 else:
252 return 'NONE'
253
254 def getcompname(self):
255 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
256 return 'CCITT G.711 u-law'
257 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
258 return 'CCITT G.711 A-law'
259 else:
260 return 'not compressed'
261
262 def getparams(self):
263 return _sunau_params(self.getnchannels(), self.getsampwidth(),
264 self.getframerate(), self.getnframes(),
265 self.getcomptype(), self.getcompname())
266
267 def getmarkers(self):
268 return None
269
270 def getmark(self, id):
271 raise Error('no marks')
272
273 def readframes(self, nframes):
274 if self._encoding in _simple_encodings:
275 if nframes == AUDIO_UNKNOWN_SIZE:
276 data = self._file.read()
277 else:
278 data = self._file.read(nframes * self._framesize)
279 self._soundpos += len(data) // self._framesize
280 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
281 with warnings.catch_warnings():
282 warnings.simplefilter('ignore', category=DeprecationWarning)
283 import audioop
284 data = audioop.ulaw2lin(data, self._sampwidth)
285 return data
286 return None # XXX--not implemented yet
287
288 def rewind(self):
289 if self._data_pos is None:
290 raise OSError('cannot seek')
291 self._file.seek(self._data_pos)
292 self._soundpos = 0
293
294 def tell(self):
295 return self._soundpos
296
297 def setpos(self, pos):
298 if pos < 0 or pos > self.getnframes():
299 raise Error('position not in range')
300 if self._data_pos is None:
301 raise OSError('cannot seek')
302 self._file.seek(self._data_pos + pos * self._framesize)
303 self._soundpos = pos
304
305 def close(self):
306 file = self._file
307 if file:
308 self._file = None
309 if self._opened:
310 file.close()
311
312 class ESC[4;38;5;81mAu_write:
313
314 def __init__(self, f):
315 if isinstance(f, str):
316 import builtins
317 f = builtins.open(f, 'wb')
318 self._opened = True
319 else:
320 self._opened = False
321 self.initfp(f)
322
323 def __del__(self):
324 if self._file:
325 self.close()
326 self._file = None
327
328 def __enter__(self):
329 return self
330
331 def __exit__(self, *args):
332 self.close()
333
334 def initfp(self, file):
335 self._file = file
336 self._framerate = 0
337 self._nchannels = 0
338 self._sampwidth = 0
339 self._framesize = 0
340 self._nframes = AUDIO_UNKNOWN_SIZE
341 self._nframeswritten = 0
342 self._datawritten = 0
343 self._datalength = 0
344 self._info = b''
345 self._comptype = 'ULAW' # default is U-law
346
347 def setnchannels(self, nchannels):
348 if self._nframeswritten:
349 raise Error('cannot change parameters after starting to write')
350 if nchannels not in (1, 2, 4):
351 raise Error('only 1, 2, or 4 channels supported')
352 self._nchannels = nchannels
353
354 def getnchannels(self):
355 if not self._nchannels:
356 raise Error('number of channels not set')
357 return self._nchannels
358
359 def setsampwidth(self, sampwidth):
360 if self._nframeswritten:
361 raise Error('cannot change parameters after starting to write')
362 if sampwidth not in (1, 2, 3, 4):
363 raise Error('bad sample width')
364 self._sampwidth = sampwidth
365
366 def getsampwidth(self):
367 if not self._framerate:
368 raise Error('sample width not specified')
369 return self._sampwidth
370
371 def setframerate(self, framerate):
372 if self._nframeswritten:
373 raise Error('cannot change parameters after starting to write')
374 self._framerate = framerate
375
376 def getframerate(self):
377 if not self._framerate:
378 raise Error('frame rate not set')
379 return self._framerate
380
381 def setnframes(self, nframes):
382 if self._nframeswritten:
383 raise Error('cannot change parameters after starting to write')
384 if nframes < 0:
385 raise Error('# of frames cannot be negative')
386 self._nframes = nframes
387
388 def getnframes(self):
389 return self._nframeswritten
390
391 def setcomptype(self, type, name):
392 if type in ('NONE', 'ULAW'):
393 self._comptype = type
394 else:
395 raise Error('unknown compression type')
396
397 def getcomptype(self):
398 return self._comptype
399
400 def getcompname(self):
401 if self._comptype == 'ULAW':
402 return 'CCITT G.711 u-law'
403 elif self._comptype == 'ALAW':
404 return 'CCITT G.711 A-law'
405 else:
406 return 'not compressed'
407
408 def setparams(self, params):
409 nchannels, sampwidth, framerate, nframes, comptype, compname = params
410 self.setnchannels(nchannels)
411 self.setsampwidth(sampwidth)
412 self.setframerate(framerate)
413 self.setnframes(nframes)
414 self.setcomptype(comptype, compname)
415
416 def getparams(self):
417 return _sunau_params(self.getnchannels(), self.getsampwidth(),
418 self.getframerate(), self.getnframes(),
419 self.getcomptype(), self.getcompname())
420
421 def tell(self):
422 return self._nframeswritten
423
424 def writeframesraw(self, data):
425 if not isinstance(data, (bytes, bytearray)):
426 data = memoryview(data).cast('B')
427 self._ensure_header_written()
428 if self._comptype == 'ULAW':
429 with warnings.catch_warnings():
430 warnings.simplefilter('ignore', category=DeprecationWarning)
431 import audioop
432 data = audioop.lin2ulaw(data, self._sampwidth)
433 nframes = len(data) // self._framesize
434 self._file.write(data)
435 self._nframeswritten = self._nframeswritten + nframes
436 self._datawritten = self._datawritten + len(data)
437
438 def writeframes(self, data):
439 self.writeframesraw(data)
440 if self._nframeswritten != self._nframes or \
441 self._datalength != self._datawritten:
442 self._patchheader()
443
444 def close(self):
445 if self._file:
446 try:
447 self._ensure_header_written()
448 if self._nframeswritten != self._nframes or \
449 self._datalength != self._datawritten:
450 self._patchheader()
451 self._file.flush()
452 finally:
453 file = self._file
454 self._file = None
455 if self._opened:
456 file.close()
457
458 #
459 # private methods
460 #
461
462 def _ensure_header_written(self):
463 if not self._nframeswritten:
464 if not self._nchannels:
465 raise Error('# of channels not specified')
466 if not self._sampwidth:
467 raise Error('sample width not specified')
468 if not self._framerate:
469 raise Error('frame rate not specified')
470 self._write_header()
471
472 def _write_header(self):
473 if self._comptype == 'NONE':
474 if self._sampwidth == 1:
475 encoding = AUDIO_FILE_ENCODING_LINEAR_8
476 self._framesize = 1
477 elif self._sampwidth == 2:
478 encoding = AUDIO_FILE_ENCODING_LINEAR_16
479 self._framesize = 2
480 elif self._sampwidth == 3:
481 encoding = AUDIO_FILE_ENCODING_LINEAR_24
482 self._framesize = 3
483 elif self._sampwidth == 4:
484 encoding = AUDIO_FILE_ENCODING_LINEAR_32
485 self._framesize = 4
486 else:
487 raise Error('internal error')
488 elif self._comptype == 'ULAW':
489 encoding = AUDIO_FILE_ENCODING_MULAW_8
490 self._framesize = 1
491 else:
492 raise Error('internal error')
493 self._framesize = self._framesize * self._nchannels
494 _write_u32(self._file, AUDIO_FILE_MAGIC)
495 header_size = 25 + len(self._info)
496 header_size = (header_size + 7) & ~7
497 _write_u32(self._file, header_size)
498 if self._nframes == AUDIO_UNKNOWN_SIZE:
499 length = AUDIO_UNKNOWN_SIZE
500 else:
501 length = self._nframes * self._framesize
502 try:
503 self._form_length_pos = self._file.tell()
504 except (AttributeError, OSError):
505 self._form_length_pos = None
506 _write_u32(self._file, length)
507 self._datalength = length
508 _write_u32(self._file, encoding)
509 _write_u32(self._file, self._framerate)
510 _write_u32(self._file, self._nchannels)
511 self._file.write(self._info)
512 self._file.write(b'\0'*(header_size - len(self._info) - 24))
513
514 def _patchheader(self):
515 if self._form_length_pos is None:
516 raise OSError('cannot seek')
517 self._file.seek(self._form_length_pos)
518 _write_u32(self._file, self._datawritten)
519 self._datalength = self._datawritten
520 self._file.seek(0, 2)
521
522 def open(f, mode=None):
523 if mode is None:
524 if hasattr(f, 'mode'):
525 mode = f.mode
526 else:
527 mode = 'rb'
528 if mode in ('r', 'rb'):
529 return Au_read(f)
530 elif mode in ('w', 'wb'):
531 return Au_write(f)
532 else:
533 raise Error("mode must be 'r', 'rb', 'w', or 'wb'")