blob: b49a5dafae506d83d00718c7cacd04432416a9d8 [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
Guido van Rossumbb189db1998-04-23 21:40:02 +000051# getcomptype() -- returns compression type ('NONE' or 'ULAW')
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +000052# getcompname() -- returns human-readable version of
Guido van Rossumbb189db1998-04-23 21:40:02 +000053# compression type ('not compressed' matches 'NONE')
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +000054# 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
Guido van Rossumbb189db1998-04-23 21:40:02 +000085# setparams(tuple)-- set all parameters at once
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +000086# 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# from <multimedia/audio_filehdr.h>
106AUDIO_FILE_MAGIC = 0x2e736e64
107AUDIO_FILE_ENCODING_MULAW_8 = 1
108AUDIO_FILE_ENCODING_LINEAR_8 = 2
109AUDIO_FILE_ENCODING_LINEAR_16 = 3
110AUDIO_FILE_ENCODING_LINEAR_24 = 4
111AUDIO_FILE_ENCODING_LINEAR_32 = 5
112AUDIO_FILE_ENCODING_FLOAT = 6
113AUDIO_FILE_ENCODING_DOUBLE = 7
114AUDIO_FILE_ENCODING_ADPCM_G721 = 23
115AUDIO_FILE_ENCODING_ADPCM_G722 = 24
116AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25
117AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26
118AUDIO_FILE_ENCODING_ALAW_8 = 27
119
120# from <multimedia/audio_hdr.h>
121AUDIO_UNKNOWN_SIZE = 0xFFFFFFFFL # ((unsigned)(~0))
122
123_simple_encodings = [AUDIO_FILE_ENCODING_MULAW_8,
124 AUDIO_FILE_ENCODING_LINEAR_8,
125 AUDIO_FILE_ENCODING_LINEAR_16,
126 AUDIO_FILE_ENCODING_LINEAR_24,
127 AUDIO_FILE_ENCODING_LINEAR_32,
128 AUDIO_FILE_ENCODING_ALAW_8]
129
Sjoerd Mullenderf33a69f1995-08-14 07:49:51 +0000130Error = 'sunau.Error'
131
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000132def _read_u32(file):
133 x = 0L
134 for i in range(4):
135 byte = file.read(1)
136 if byte == '':
137 raise EOFError
138 x = x*256 + ord(byte)
139 return x
140
141def _write_u32(file, x):
142 data = []
143 for i in range(4):
144 d, m = divmod(x, 256)
145 data.insert(0, m)
146 x = d
147 for i in range(4):
148 file.write(chr(int(data[i])))
149
150class Au_read:
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000151
152 def __init__(self, f):
153 if type(f) == type(''):
Guido van Rossum3db6ebc1994-01-28 09:59:35 +0000154 import __builtin__
Guido van Rossum2013ba41999-02-05 20:55:16 +0000155 f = __builtin__.open(f, 'rb')
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000156 self.initfp(f)
157
158 def __del__(self):
159 if self._file:
160 self.close()
161
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000162 def initfp(self, file):
163 self._file = file
164 self._soundpos = 0
165 magic = int(_read_u32(file))
166 if magic != AUDIO_FILE_MAGIC:
167 raise Error, 'bad magic number'
168 self._hdr_size = int(_read_u32(file))
169 if self._hdr_size < 24:
170 raise Error, 'header size too small'
171 if self._hdr_size > 100:
172 raise Error, 'header size rediculously large'
173 self._data_size = _read_u32(file)
174 if self._data_size != AUDIO_UNKNOWN_SIZE:
175 self._data_size = int(self._data_size)
176 self._encoding = int(_read_u32(file))
177 if self._encoding not in _simple_encodings:
178 raise Error, 'encoding not (yet) supported'
179 if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8,
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000180 AUDIO_FILE_ENCODING_ALAW_8):
181 self._sampwidth = 2
182 self._framesize = 1
Guido van Rossum5ebeea01999-02-05 19:59:27 +0000183 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_8:
184 self._framesize = self._sampwidth = 1
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000185 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16:
186 self._framesize = self._sampwidth = 2
187 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24:
188 self._framesize = self._sampwidth = 3
189 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32:
190 self._framesize = self._sampwidth = 4
191 else:
192 raise Error, 'unknown encoding'
193 self._framerate = int(_read_u32(file))
194 self._nchannels = int(_read_u32(file))
195 self._framesize = self._framesize * self._nchannels
196 if self._hdr_size > 24:
197 self._info = file.read(self._hdr_size - 24)
198 for i in range(len(self._info)):
199 if self._info[i] == '\0':
200 self._info = self._info[:i]
201 break
202 else:
203 self._info = ''
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000204
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000205 def getfp(self):
206 return self._file
207
208 def getnchannels(self):
209 return self._nchannels
210
211 def getsampwidth(self):
212 return self._sampwidth
213
214 def getframerate(self):
215 return self._framerate
216
217 def getnframes(self):
218 if self._data_size == AUDIO_UNKNOWN_SIZE:
219 return AUDIO_UNKNOWN_SIZE
220 if self._encoding in _simple_encodings:
221 return self._data_size / self._framesize
222 return 0 # XXX--must do some arithmetic here
223
224 def getcomptype(self):
225 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
226 return 'ULAW'
227 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
228 return 'ALAW'
229 else:
230 return 'NONE'
231
232 def getcompname(self):
233 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
234 return 'CCITT G.711 u-law'
235 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
236 return 'CCITT G.711 A-law'
237 else:
238 return 'not compressed'
239
240 def getparams(self):
241 return self.getnchannels(), self.getsampwidth(), \
242 self.getframerate(), self.getnframes(), \
243 self.getcomptype(), self.getcompname()
244
245 def getmarkers(self):
246 return None
247
248 def getmark(self, id):
249 raise Error, 'no marks'
250
251 def readframes(self, nframes):
252 if self._encoding in _simple_encodings:
253 if nframes == AUDIO_UNKNOWN_SIZE:
254 data = self._file.read()
255 else:
Sjoerd Mullenderffe94901994-01-28 09:56:05 +0000256 data = self._file.read(nframes * self._framesize * self._nchannels)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000257 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
258 import audioop
259 data = audioop.ulaw2lin(data, self._sampwidth)
260 return data
261 return None # XXX--not implemented yet
262
263 def rewind(self):
264 self._soundpos = 0
265 self._file.seek(self._hdr_size)
266
267 def tell(self):
268 return self._soundpos
269
270 def setpos(self, pos):
271 if pos < 0 or pos > self.getnframes():
272 raise Error, 'position not in range'
273 self._file.seek(pos * self._framesize + self._hdr_size)
274 self._soundpos = pos
275
276 def close(self):
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000277 self._file = None
278
279class Au_write:
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000280
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000281 def __init__(self, f):
282 if type(f) == type(''):
Guido van Rossum3db6ebc1994-01-28 09:59:35 +0000283 import __builtin__
Guido van Rossum2013ba41999-02-05 20:55:16 +0000284 f = __builtin__.open(f, 'wb')
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000285 self.initfp(f)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000286
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000287 def __del__(self):
288 if self._file:
289 self.close()
290
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000291 def initfp(self, file):
292 self._file = file
293 self._framerate = 0
294 self._nchannels = 0
295 self._sampwidth = 0
296 self._framesize = 0
297 self._nframes = AUDIO_UNKNOWN_SIZE
298 self._nframeswritten = 0
299 self._datawritten = 0
300 self._datalength = 0
301 self._info = ''
302 self._comptype = 'ULAW' # default is U-law
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000303
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000304 def setnchannels(self, nchannels):
305 if self._nframeswritten:
306 raise Error, 'cannot change parameters after starting to write'
307 if nchannels not in (1, 2, 4):
308 raise Error, 'only 1, 2, or 4 channels supported'
309 self._nchannels = nchannels
310
311 def getnchannels(self):
312 if not self._nchannels:
313 raise Error, 'number of channels not set'
314 return self._nchannels
315
316 def setsampwidth(self, sampwidth):
317 if self._nframeswritten:
318 raise Error, 'cannot change parameters after starting to write'
319 if sampwidth not in (1, 2, 4):
320 raise Error, 'bad sample width'
321 self._sampwidth = sampwidth
322
323 def getsampwidth(self):
324 if not self._framerate:
325 raise Error, 'sample width not specified'
326 return self._sampwidth
327
328 def setframerate(self, framerate):
329 if self._nframeswritten:
330 raise Error, 'cannot change parameters after starting to write'
331 self._framerate = framerate
332
333 def getframerate(self):
334 if not self._framerate:
335 raise Error, 'frame rate not set'
336 return self._framerate
337
338 def setnframes(self, nframes):
339 if self._nframeswritten:
340 raise Error, 'cannot change parameters after starting to write'
341 if nframes < 0:
342 raise Error, '# of frames cannot be negative'
343 self._nframes = nframes
344
345 def getnframes(self):
346 return self._nframeswritten
347
348 def setcomptype(self, type, name):
349 if type in ('NONE', 'ULAW'):
350 self._comptype = type
351 else:
352 raise Error, 'unknown compression type'
353
354 def getcomptype(self):
355 return self._comptype
356
357 def getcompname(self):
358 if self._comptype == 'ULAW':
359 return 'CCITT G.711 u-law'
360 elif self._comptype == 'ALAW':
361 return 'CCITT G.711 A-law'
362 else:
363 return 'not compressed'
364
365 def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
366 self.setnchannels(nchannels)
367 self.setsampwidth(sampwidth)
368 self.setframerate(framerate)
369 self.setnframes(nframes)
370 self.setcomptype(comptype, compname)
371
372 def getparams(self):
373 return self.getnchannels(), self.getsampwidth(), \
374 self.getframerate(), self.getnframes(), \
375 self.getcomptype(), self.getcompname()
376
377 def tell(self):
378 return self._nframeswritten
379
380 def writeframesraw(self, data):
381 self._ensure_header_written()
382 nframes = len(data) / self._framesize
383 if self._comptype == 'ULAW':
384 import audioop
385 data = audioop.lin2ulaw(data, self._sampwidth)
386 self._file.write(data)
387 self._nframeswritten = self._nframeswritten + nframes
388 self._datawritten = self._datawritten + len(data)
389
390 def writeframes(self, data):
391 self.writeframesraw(data)
392 if self._nframeswritten != self._nframes or \
393 self._datalength != self._datawritten:
394 self._patchheader()
395
396 def close(self):
397 self._ensure_header_written()
398 if self._nframeswritten != self._nframes or \
399 self._datalength != self._datawritten:
400 self._patchheader()
Sjoerd Mullenderad7324c1993-12-16 14:02:44 +0000401 self._file.flush()
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000402 self._file = None
403
404 #
405 # private methods
406 #
Sjoerd Mullender2a451411993-12-20 09:36:01 +0000407
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000408 def _ensure_header_written(self):
409 if not self._nframeswritten:
410 if not self._nchannels:
411 raise Error, '# of channels not specified'
412 if not self._sampwidth:
413 raise Error, 'sample width not specified'
414 if not self._framerate:
415 raise Error, 'frame rate not specified'
416 self._write_header()
417
418 def _write_header(self):
419 if self._comptype == 'NONE':
420 if self._sampwidth == 1:
421 encoding = AUDIO_FILE_ENCODING_LINEAR_8
422 self._framesize = 1
423 elif self._sampwidth == 2:
424 encoding = AUDIO_FILE_ENCODING_LINEAR_16
425 self._framesize = 2
426 elif self._sampwidth == 4:
427 encoding = AUDIO_FILE_ENCODING_LINEAR_32
428 self._framesize = 4
429 else:
430 raise Error, 'internal error'
431 elif self._comptype == 'ULAW':
432 encoding = AUDIO_FILE_ENCODING_MULAW_8
433 self._framesize = 1
434 else:
435 raise Error, 'internal error'
436 self._framesize = self._framesize * self._nchannels
437 _write_u32(self._file, AUDIO_FILE_MAGIC)
438 header_size = 25 + len(self._info)
439 header_size = (header_size + 7) & ~7
440 _write_u32(self._file, header_size)
441 if self._nframes == AUDIO_UNKNOWN_SIZE:
442 length = AUDIO_UNKNOWN_SIZE
443 else:
444 length = self._nframes * self._framesize
445 _write_u32(self._file, length)
446 self._datalength = length
447 _write_u32(self._file, encoding)
448 _write_u32(self._file, self._framerate)
449 _write_u32(self._file, self._nchannels)
450 self._file.write(self._info)
451 self._file.write('\0'*(header_size - len(self._info) - 24))
452
453 def _patchheader(self):
454 self._file.seek(8)
455 _write_u32(self._file, self._datawritten)
456 self._datalength = self._datawritten
457 self._file.seek(0, 2)
458
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000459def open(f, mode):
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000460 if mode == 'r':
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000461 return Au_read(f)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000462 elif mode == 'w':
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000463 return Au_write(f)
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000464 else:
465 raise Error, "mode must be 'r' or 'w'"
466
Guido van Rossum7bc817d1993-12-17 15:25:27 +0000467openfp = open