blob: 2cb35a7ebdcb789d123a57aacbc332318b341a09 [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:
39# f = au.open(file, 'r')
40# or
41# f = au.openfp(filep, 'r')
42# where file is the name of a file and filep is an open file pointer.
43# The open file pointer must have methods read(), seek(), and close().
44# When the setpos() and rewind() methods are not used, the seek()
45# method is not necessary.
46#
47# This returns an instance of a class with the following public methods:
48# getnchannels() -- returns number of audio channels (1 for
49# mono, 2 for stereo)
50# getsampwidth() -- returns sample width in bytes
51# getframerate() -- returns sampling frequency
52# getnframes() -- returns number of audio frames
53# getcomptype() -- returns compression type ('NONE' for AIFF files)
54# getcompname() -- returns human-readable version of
55# compression type ('not compressed' for AIFF files)
56# getparams() -- returns a tuple consisting of all of the
57# above in the above order
58# getmarkers() -- returns None (for compatibility with the
59# aifc module)
60# getmark(id) -- raises an error since the mark does not
61# exist (for compatibility with the aifc module)
62# readframes(n) -- returns at most n frames of audio
63# rewind() -- rewind to the beginning of the audio stream
64# setpos(pos) -- seek to the specified position
65# tell() -- return the current position
66# close() -- close the instance (make it unusable)
67# The position returned by tell() and the position given to setpos()
68# are compatible and have nothing to do with the actual postion in the
69# file.
70# The close() method is called automatically when the class instance
71# is destroyed.
72#
73# Writing audio files:
74# f = au.open(file, 'w')
75# or
76# f = au.openfp(filep, 'w')
77# where file is the name of a file and filep is an open file pointer.
78# The open file pointer must have methods write(), tell(), seek(), and
79# close().
80#
81# This returns an instance of a class with the following public methods:
82# setnchannels(n) -- set the number of channels
83# setsampwidth(n) -- set the sample width
84# setframerate(n) -- set the frame rate
85# setnframes(n) -- set the number of frames
86# setcomptype(type, name)
87# -- set the compression type and the
88# human-readable compression type
89# setparams(nchannels, sampwidth, framerate, nframes, comptype, compname)
90# -- set all parameters at once
91# tell() -- return current position in output file
92# writeframesraw(data)
93# -- write audio frames without pathing up the
94# file header
95# writeframes(data)
96# -- write audio frames and patch up the file header
97# close() -- patch up the file header and close the
98# output file
99# You should set the parameters before the first writeframesraw or
100# writeframes. The total number of frames does not need to be set,
101# but when it is set to the correct value, the header does not have to
102# be patched up.
103# It is best to first set all parameters, perhaps possibly the
104# compression type, and then write audio frames using writeframesraw.
105# When all frames have been written, either call writeframes('') or
106# close() to patch up the sizes in the header.
107# The close() method is called automatically when the class instance
108# is destroyed.
109
110# from <multimedia/audio_filehdr.h>
111AUDIO_FILE_MAGIC = 0x2e736e64
112AUDIO_FILE_ENCODING_MULAW_8 = 1
113AUDIO_FILE_ENCODING_LINEAR_8 = 2
114AUDIO_FILE_ENCODING_LINEAR_16 = 3
115AUDIO_FILE_ENCODING_LINEAR_24 = 4
116AUDIO_FILE_ENCODING_LINEAR_32 = 5
117AUDIO_FILE_ENCODING_FLOAT = 6
118AUDIO_FILE_ENCODING_DOUBLE = 7
119AUDIO_FILE_ENCODING_ADPCM_G721 = 23
120AUDIO_FILE_ENCODING_ADPCM_G722 = 24
121AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25
122AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26
123AUDIO_FILE_ENCODING_ALAW_8 = 27
124
125# from <multimedia/audio_hdr.h>
126AUDIO_UNKNOWN_SIZE = 0xFFFFFFFFL # ((unsigned)(~0))
127
128_simple_encodings = [AUDIO_FILE_ENCODING_MULAW_8,
129 AUDIO_FILE_ENCODING_LINEAR_8,
130 AUDIO_FILE_ENCODING_LINEAR_16,
131 AUDIO_FILE_ENCODING_LINEAR_24,
132 AUDIO_FILE_ENCODING_LINEAR_32,
133 AUDIO_FILE_ENCODING_ALAW_8]
134
135def _read_u32(file):
136 x = 0L
137 for i in range(4):
138 byte = file.read(1)
139 if byte == '':
140 raise EOFError
141 x = x*256 + ord(byte)
142 return x
143
144def _write_u32(file, x):
145 data = []
146 for i in range(4):
147 d, m = divmod(x, 256)
148 data.insert(0, m)
149 x = d
150 for i in range(4):
151 file.write(chr(int(data[i])))
152
153class Au_read:
154 def initfp(self, file):
155 self._file = file
156 self._soundpos = 0
157 magic = int(_read_u32(file))
158 if magic != AUDIO_FILE_MAGIC:
159 raise Error, 'bad magic number'
160 self._hdr_size = int(_read_u32(file))
161 if self._hdr_size < 24:
162 raise Error, 'header size too small'
163 if self._hdr_size > 100:
164 raise Error, 'header size rediculously large'
165 self._data_size = _read_u32(file)
166 if self._data_size != AUDIO_UNKNOWN_SIZE:
167 self._data_size = int(self._data_size)
168 self._encoding = int(_read_u32(file))
169 if self._encoding not in _simple_encodings:
170 raise Error, 'encoding not (yet) supported'
171 if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8,
172 AUDIO_FILE_ENCODING_LINEAR_8,
173 AUDIO_FILE_ENCODING_ALAW_8):
174 self._sampwidth = 2
175 self._framesize = 1
176 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16:
177 self._framesize = self._sampwidth = 2
178 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24:
179 self._framesize = self._sampwidth = 3
180 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32:
181 self._framesize = self._sampwidth = 4
182 else:
183 raise Error, 'unknown encoding'
184 self._framerate = int(_read_u32(file))
185 self._nchannels = int(_read_u32(file))
186 self._framesize = self._framesize * self._nchannels
187 if self._hdr_size > 24:
188 self._info = file.read(self._hdr_size - 24)
189 for i in range(len(self._info)):
190 if self._info[i] == '\0':
191 self._info = self._info[:i]
192 break
193 else:
194 self._info = ''
195 return self
196
197 def init(self, filename):
198 import builtin
199 return self.initfp(builtin.open(filename, 'r'))
200
201 def __del__(self):
202 if self._file:
203 self.close()
204
205 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:
256 data = self._file.read(nframes * self._sampwidth * self._nchannels)
257 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:
280 def init(self, filename):
281 import builtin
282 return self.initfp(builtin.open(filename, 'w'))
283
284 def initfp(self, file):
285 self._file = file
286 self._framerate = 0
287 self._nchannels = 0
288 self._sampwidth = 0
289 self._framesize = 0
290 self._nframes = AUDIO_UNKNOWN_SIZE
291 self._nframeswritten = 0
292 self._datawritten = 0
293 self._datalength = 0
294 self._info = ''
295 self._comptype = 'ULAW' # default is U-law
296 return self
297
298 def __del__(self):
299 if self._file:
300 self.close()
301
302 def setnchannels(self, nchannels):
303 if self._nframeswritten:
304 raise Error, 'cannot change parameters after starting to write'
305 if nchannels not in (1, 2, 4):
306 raise Error, 'only 1, 2, or 4 channels supported'
307 self._nchannels = nchannels
308
309 def getnchannels(self):
310 if not self._nchannels:
311 raise Error, 'number of channels not set'
312 return self._nchannels
313
314 def setsampwidth(self, sampwidth):
315 if self._nframeswritten:
316 raise Error, 'cannot change parameters after starting to write'
317 if sampwidth not in (1, 2, 4):
318 raise Error, 'bad sample width'
319 self._sampwidth = sampwidth
320
321 def getsampwidth(self):
322 if not self._framerate:
323 raise Error, 'sample width not specified'
324 return self._sampwidth
325
326 def setframerate(self, framerate):
327 if self._nframeswritten:
328 raise Error, 'cannot change parameters after starting to write'
329 self._framerate = framerate
330
331 def getframerate(self):
332 if not self._framerate:
333 raise Error, 'frame rate not set'
334 return self._framerate
335
336 def setnframes(self, nframes):
337 if self._nframeswritten:
338 raise Error, 'cannot change parameters after starting to write'
339 if nframes < 0:
340 raise Error, '# of frames cannot be negative'
341 self._nframes = nframes
342
343 def getnframes(self):
344 return self._nframeswritten
345
346 def setcomptype(self, type, name):
347 if type in ('NONE', 'ULAW'):
348 self._comptype = type
349 else:
350 raise Error, 'unknown compression type'
351
352 def getcomptype(self):
353 return self._comptype
354
355 def getcompname(self):
356 if self._comptype == 'ULAW':
357 return 'CCITT G.711 u-law'
358 elif self._comptype == 'ALAW':
359 return 'CCITT G.711 A-law'
360 else:
361 return 'not compressed'
362
363 def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
364 self.setnchannels(nchannels)
365 self.setsampwidth(sampwidth)
366 self.setframerate(framerate)
367 self.setnframes(nframes)
368 self.setcomptype(comptype, compname)
369
370 def getparams(self):
371 return self.getnchannels(), self.getsampwidth(), \
372 self.getframerate(), self.getnframes(), \
373 self.getcomptype(), self.getcompname()
374
375 def tell(self):
376 return self._nframeswritten
377
378 def writeframesraw(self, data):
379 self._ensure_header_written()
380 nframes = len(data) / self._framesize
381 if self._comptype == 'ULAW':
382 import audioop
383 data = audioop.lin2ulaw(data, self._sampwidth)
384 self._file.write(data)
385 self._nframeswritten = self._nframeswritten + nframes
386 self._datawritten = self._datawritten + len(data)
387
388 def writeframes(self, data):
389 self.writeframesraw(data)
390 if self._nframeswritten != self._nframes or \
391 self._datalength != self._datawritten:
392 self._patchheader()
393
394 def close(self):
395 self._ensure_header_written()
396 if self._nframeswritten != self._nframes or \
397 self._datalength != self._datawritten:
398 self._patchheader()
Sjoerd Mullenderad7324c1993-12-16 14:02:44 +0000399 self._file.flush()
Sjoerd Mullender43bf0bc1993-12-13 11:42:39 +0000400 self._file = None
401
402 #
403 # private methods
404 #
405 def _ensure_header_written(self):
406 if not self._nframeswritten:
407 if not self._nchannels:
408 raise Error, '# of channels not specified'
409 if not self._sampwidth:
410 raise Error, 'sample width not specified'
411 if not self._framerate:
412 raise Error, 'frame rate not specified'
413 self._write_header()
414
415 def _write_header(self):
416 if self._comptype == 'NONE':
417 if self._sampwidth == 1:
418 encoding = AUDIO_FILE_ENCODING_LINEAR_8
419 self._framesize = 1
420 elif self._sampwidth == 2:
421 encoding = AUDIO_FILE_ENCODING_LINEAR_16
422 self._framesize = 2
423 elif self._sampwidth == 4:
424 encoding = AUDIO_FILE_ENCODING_LINEAR_32
425 self._framesize = 4
426 else:
427 raise Error, 'internal error'
428 elif self._comptype == 'ULAW':
429 encoding = AUDIO_FILE_ENCODING_MULAW_8
430 self._framesize = 1
431 else:
432 raise Error, 'internal error'
433 self._framesize = self._framesize * self._nchannels
434 _write_u32(self._file, AUDIO_FILE_MAGIC)
435 header_size = 25 + len(self._info)
436 header_size = (header_size + 7) & ~7
437 _write_u32(self._file, header_size)
438 if self._nframes == AUDIO_UNKNOWN_SIZE:
439 length = AUDIO_UNKNOWN_SIZE
440 else:
441 length = self._nframes * self._framesize
442 _write_u32(self._file, length)
443 self._datalength = length
444 _write_u32(self._file, encoding)
445 _write_u32(self._file, self._framerate)
446 _write_u32(self._file, self._nchannels)
447 self._file.write(self._info)
448 self._file.write('\0'*(header_size - len(self._info) - 24))
449
450 def _patchheader(self):
451 self._file.seek(8)
452 _write_u32(self._file, self._datawritten)
453 self._datalength = self._datawritten
454 self._file.seek(0, 2)
455
456def open(filename, mode):
457 if mode == 'r':
458 return Au_read().init(filename)
459 elif mode == 'w':
460 return Au_write().init(filename)
461 else:
462 raise Error, "mode must be 'r' or 'w'"
463
464def openfp(filep, mode):
465 if mode == 'r':
466 return Au_read().initfp(filep)
467 elif mode == 'w':
468 return Au_write().initfp(filep)
469 else:
470 raise Error, "mode must be 'r' or 'w'"