blob: aeac20eb3b8e820c420479fb7a9a54e8b3f261c8 [file] [log] [blame]
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +00001# Stuff to parse Sun and NeXT audio files.
2#
3# An audio 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# parameter 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:
Sjoerd Mullenderb513c741994-02-03 14:19:21 +000039# f = sunau.open(file, 'r')
Sjoerd Mullender2a451411993-12-20 09:36:01 +000040# where file is either the name of a file or an open file pointer.
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +000041# 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' for AIFF files)
52# getcompname() -- returns human-readable version of
53# compression type ('not compressed' for AIFF files)
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 postion in the
67# file.
68# The close() method is called automatically when the class instance
69# is destroyed.
70#
71# Writing audio files:
Sjoerd Mullenderb513c741994-02-03 14:19:21 +000072# f = sunau.open(file, 'w')
Sjoerd Mullender2a451411993-12-20 09:36:01 +000073# where file is either the name of a file or an open file pointer.
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +000074# 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(nchannels, sampwidth, framerate, nframes, comptype, compname)
86# -- set all parameters at once
87# tell() -- return current position in output file
88# writeframesraw(data)
89# -- write audio frames without pathing up the
90# file header
91# writeframes(data)
92# -- write audio frames and patch up the file header
93# close() -- patch up the file header and close the
94# output file
95# You should set the parameters before the first writeframesraw or
96# writeframes. The total number of frames does not need to be set,
97# but when it is set to the correct value, the header does not have to
98# be patched up.
99# It is best to first set all parameters, perhaps possibly the
100# compression type, and then write audio frames using writeframesraw.
101# When all frames have been written, either call writeframes('') or
102# close() to patch up the sizes in the header.
103# The close() method is called automatically when the class instance
104# is destroyed.
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
Sjoerd Mullenderf33a69f1995-08-14 07:49:51 +0000131Error = 'sunau.Error'
132
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000133def _read_u32(file):
134 x = 0L
135 for i in range(4):
136 byte = file.read(1)
137 if byte == '':
138 raise EOFError
139 x = x*256 + ord(byte)
140 return x
141
142def _write_u32(file, x):
143 data = []
144 for i in range(4):
145 d, m = divmod(x, 256)
146 data.insert(0, m)
147 x = d
148 for i in range(4):
149 file.write(chr(int(data[i])))
150
151class Au_read:
Guido van Rossumd7abed31996-08-20 20:40:07 +0000152## access _file, _soundpos, _hdr_size, _data_size, _encoding, \
153## _sampwidth, _framesize, _framerate, _nchannels, \
154## _info: private
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000155
156 def __init__(self, f):
157 if type(f) == type(''):
Guido van Rossum3db6ebc1994-01-28 09:59:35 +0000158 import __builtin__
159 f = __builtin__.open(f, 'r')
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000160 self.initfp(f)
161
162 def __del__(self):
163 if self._file:
164 self.close()
165
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000166 def initfp(self, file):
167 self._file = file
168 self._soundpos = 0
169 magic = int(_read_u32(file))
170 if magic != AUDIO_FILE_MAGIC:
171 raise Error, 'bad magic number'
172 self._hdr_size = int(_read_u32(file))
173 if self._hdr_size < 24:
174 raise Error, 'header size too small'
175 if self._hdr_size > 100:
176 raise Error, 'header size rediculously large'
177 self._data_size = _read_u32(file)
178 if self._data_size != AUDIO_UNKNOWN_SIZE:
179 self._data_size = int(self._data_size)
180 self._encoding = int(_read_u32(file))
181 if self._encoding not in _simple_encodings:
182 raise Error, 'encoding not (yet) supported'
183 if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8,
184 AUDIO_FILE_ENCODING_LINEAR_8,
185 AUDIO_FILE_ENCODING_ALAW_8):
186 self._sampwidth = 2
187 self._framesize = 1
188 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16:
189 self._framesize = self._sampwidth = 2
190 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24:
191 self._framesize = self._sampwidth = 3
192 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32:
193 self._framesize = self._sampwidth = 4
194 else:
195 raise Error, 'unknown encoding'
196 self._framerate = int(_read_u32(file))
197 self._nchannels = int(_read_u32(file))
198 self._framesize = self._framesize * self._nchannels
199 if self._hdr_size > 24:
200 self._info = file.read(self._hdr_size - 24)
201 for i in range(len(self._info)):
202 if self._info[i] == '\0':
203 self._info = self._info[:i]
204 break
205 else:
206 self._info = ''
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000207
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000208 def getfp(self):
209 return self._file
210
211 def getnchannels(self):
212 return self._nchannels
213
214 def getsampwidth(self):
215 return self._sampwidth
216
217 def getframerate(self):
218 return self._framerate
219
220 def getnframes(self):
221 if self._data_size == AUDIO_UNKNOWN_SIZE:
222 return AUDIO_UNKNOWN_SIZE
223 if self._encoding in _simple_encodings:
224 return self._data_size / self._framesize
225 return 0 # XXX--must do some arithmetic here
226
227 def getcomptype(self):
228 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
229 return 'ULAW'
230 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
231 return 'ALAW'
232 else:
233 return 'NONE'
234
235 def getcompname(self):
236 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
237 return 'CCITT G.711 u-law'
238 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
239 return 'CCITT G.711 A-law'
240 else:
241 return 'not compressed'
242
243 def getparams(self):
244 return self.getnchannels(), self.getsampwidth(), \
245 self.getframerate(), self.getnframes(), \
246 self.getcomptype(), self.getcompname()
247
248 def getmarkers(self):
249 return None
250
251 def getmark(self, id):
252 raise Error, 'no marks'
253
254 def readframes(self, nframes):
255 if self._encoding in _simple_encodings:
256 if nframes == AUDIO_UNKNOWN_SIZE:
257 data = self._file.read()
258 else:
Sjoerd Mullenderffe94901994-01-28 09:56:05 +0000259 data = self._file.read(nframes * self._framesize * self._nchannels)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000260 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
261 import audioop
262 data = audioop.ulaw2lin(data, self._sampwidth)
263 return data
264 return None # XXX--not implemented yet
265
266 def rewind(self):
267 self._soundpos = 0
268 self._file.seek(self._hdr_size)
269
270 def tell(self):
271 return self._soundpos
272
273 def setpos(self, pos):
274 if pos < 0 or pos > self.getnframes():
275 raise Error, 'position not in range'
276 self._file.seek(pos * self._framesize + self._hdr_size)
277 self._soundpos = pos
278
279 def close(self):
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000280 self._file = None
281
282class Au_write:
Guido van Rossumd7abed31996-08-20 20:40:07 +0000283## access _file, _framerate, _nchannels, _sampwidth, _framesize, \
284## _nframes, _nframeswritten, _datawritten, _info, \
285## _comptype: private
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000286
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000287 def __init__(self, f):
288 if type(f) == type(''):
Guido van Rossum3db6ebc1994-01-28 09:59:35 +0000289 import __builtin__
290 f = __builtin__.open(f, 'w')
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000291 self.initfp(f)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000292
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000293 def __del__(self):
294 if self._file:
295 self.close()
296
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000297 def initfp(self, file):
298 self._file = file
299 self._framerate = 0
300 self._nchannels = 0
301 self._sampwidth = 0
302 self._framesize = 0
303 self._nframes = AUDIO_UNKNOWN_SIZE
304 self._nframeswritten = 0
305 self._datawritten = 0
306 self._datalength = 0
307 self._info = ''
308 self._comptype = 'ULAW' # default is U-law
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000309
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000310 def setnchannels(self, nchannels):
311 if self._nframeswritten:
312 raise Error, 'cannot change parameters after starting to write'
313 if nchannels not in (1, 2, 4):
314 raise Error, 'only 1, 2, or 4 channels supported'
315 self._nchannels = nchannels
316
317 def getnchannels(self):
318 if not self._nchannels:
319 raise Error, 'number of channels not set'
320 return self._nchannels
321
322 def setsampwidth(self, sampwidth):
323 if self._nframeswritten:
324 raise Error, 'cannot change parameters after starting to write'
325 if sampwidth not in (1, 2, 4):
326 raise Error, 'bad sample width'
327 self._sampwidth = sampwidth
328
329 def getsampwidth(self):
330 if not self._framerate:
331 raise Error, 'sample width not specified'
332 return self._sampwidth
333
334 def setframerate(self, framerate):
335 if self._nframeswritten:
336 raise Error, 'cannot change parameters after starting to write'
337 self._framerate = framerate
338
339 def getframerate(self):
340 if not self._framerate:
341 raise Error, 'frame rate not set'
342 return self._framerate
343
344 def setnframes(self, nframes):
345 if self._nframeswritten:
346 raise Error, 'cannot change parameters after starting to write'
347 if nframes < 0:
348 raise Error, '# of frames cannot be negative'
349 self._nframes = nframes
350
351 def getnframes(self):
352 return self._nframeswritten
353
354 def setcomptype(self, type, name):
355 if type in ('NONE', 'ULAW'):
356 self._comptype = type
357 else:
358 raise Error, 'unknown compression type'
359
360 def getcomptype(self):
361 return self._comptype
362
363 def getcompname(self):
364 if self._comptype == 'ULAW':
365 return 'CCITT G.711 u-law'
366 elif self._comptype == 'ALAW':
367 return 'CCITT G.711 A-law'
368 else:
369 return 'not compressed'
370
371 def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
372 self.setnchannels(nchannels)
373 self.setsampwidth(sampwidth)
374 self.setframerate(framerate)
375 self.setnframes(nframes)
376 self.setcomptype(comptype, compname)
377
378 def getparams(self):
379 return self.getnchannels(), self.getsampwidth(), \
380 self.getframerate(), self.getnframes(), \
381 self.getcomptype(), self.getcompname()
382
383 def tell(self):
384 return self._nframeswritten
385
386 def writeframesraw(self, data):
387 self._ensure_header_written()
388 nframes = len(data) / self._framesize
389 if self._comptype == 'ULAW':
390 import audioop
391 data = audioop.lin2ulaw(data, self._sampwidth)
392 self._file.write(data)
393 self._nframeswritten = self._nframeswritten + nframes
394 self._datawritten = self._datawritten + len(data)
395
396 def writeframes(self, data):
397 self.writeframesraw(data)
398 if self._nframeswritten != self._nframes or \
399 self._datalength != self._datawritten:
400 self._patchheader()
401
402 def close(self):
403 self._ensure_header_written()
404 if self._nframeswritten != self._nframes or \
405 self._datalength != self._datawritten:
406 self._patchheader()
Sjoerd Mullenderad7324c1993-12-16 14:02:44 +0000407 self._file.flush()
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000408 self._file = None
409
410 #
411 # private methods
412 #
Guido van Rossumd7abed31996-08-20 20:40:07 +0000413## if 0: access *: private
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000414
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000415 def _ensure_header_written(self):
416 if not self._nframeswritten:
417 if not self._nchannels:
418 raise Error, '# of channels not specified'
419 if not self._sampwidth:
420 raise Error, 'sample width not specified'
421 if not self._framerate:
422 raise Error, 'frame rate not specified'
423 self._write_header()
424
425 def _write_header(self):
426 if self._comptype == 'NONE':
427 if self._sampwidth == 1:
428 encoding = AUDIO_FILE_ENCODING_LINEAR_8
429 self._framesize = 1
430 elif self._sampwidth == 2:
431 encoding = AUDIO_FILE_ENCODING_LINEAR_16
432 self._framesize = 2
433 elif self._sampwidth == 4:
434 encoding = AUDIO_FILE_ENCODING_LINEAR_32
435 self._framesize = 4
436 else:
437 raise Error, 'internal error'
438 elif self._comptype == 'ULAW':
439 encoding = AUDIO_FILE_ENCODING_MULAW_8
440 self._framesize = 1
441 else:
442 raise Error, 'internal error'
443 self._framesize = self._framesize * self._nchannels
444 _write_u32(self._file, AUDIO_FILE_MAGIC)
445 header_size = 25 + len(self._info)
446 header_size = (header_size + 7) & ~7
447 _write_u32(self._file, header_size)
448 if self._nframes == AUDIO_UNKNOWN_SIZE:
449 length = AUDIO_UNKNOWN_SIZE
450 else:
451 length = self._nframes * self._framesize
452 _write_u32(self._file, length)
453 self._datalength = length
454 _write_u32(self._file, encoding)
455 _write_u32(self._file, self._framerate)
456 _write_u32(self._file, self._nchannels)
457 self._file.write(self._info)
458 self._file.write('\0'*(header_size - len(self._info) - 24))
459
460 def _patchheader(self):
461 self._file.seek(8)
462 _write_u32(self._file, self._datawritten)
463 self._datalength = self._datawritten
464 self._file.seek(0, 2)
465
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000466def open(f, mode):
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000467 if mode == 'r':
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000468 return Au_read(f)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000469 elif mode == 'w':
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000470 return Au_write(f)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000471 else:
472 raise Error, "mode must be 'r' or 'w'"
473
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000474openfp = open