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