Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """Stuff to parse WAVE files. |
2 | ||
3 | Usage. | |
4 | ||
5 | Reading WAVE files: | |
6 | f = wave.open(file, 'r') | |
7 | where file is either the name of a file or an open file pointer. | |
8 | The open file pointer must have methods read(), seek(), and close(). | |
9 | When the setpos() and rewind() methods are not used, the seek() | |
10 | method is not necessary. | |
11 | ||
12 | This returns an instance of a class with the following public methods: | |
13 | getnchannels() -- returns number of audio channels (1 for | |
14 | mono, 2 for stereo) | |
15 | getsampwidth() -- returns sample width in bytes | |
16 | getframerate() -- returns sampling frequency | |
17 | getnframes() -- returns number of audio frames | |
18 | getcomptype() -- returns compression type ('NONE' for linear samples) | |
19 | getcompname() -- returns human-readable version of | |
20 | compression type ('not compressed' linear samples) | |
21 | getparams() -- returns a tuple consisting of all of the | |
22 | above in the above order | |
23 | getmarkers() -- returns None (for compatibility with the | |
24 | aifc module) | |
25 | getmark(id) -- raises an error since the mark does not | |
26 | exist (for compatibility with the aifc module) | |
27 | readframes(n) -- returns at most n frames of audio | |
28 | rewind() -- rewind to the beginning of the audio stream | |
29 | setpos(pos) -- seek to the specified position | |
30 | tell() -- return the current position | |
31 | close() -- close the instance (make it unusable) | |
32 | The position returned by tell() and the position given to setpos() | |
33 | are compatible and have nothing to do with the actual position in the | |
34 | file. | |
35 | The close() method is called automatically when the class instance | |
36 | is destroyed. | |
37 | ||
38 | Writing WAVE files: | |
39 | f = wave.open(file, 'w') | |
40 | where file is either the name of a file or an open file pointer. | |
41 | The open file pointer must have methods write(), tell(), seek(), and | |
42 | close(). | |
43 | ||
44 | This returns an instance of a class with the following public methods: | |
45 | setnchannels(n) -- set the number of channels | |
46 | setsampwidth(n) -- set the sample width | |
47 | setframerate(n) -- set the frame rate | |
48 | setnframes(n) -- set the number of frames | |
49 | setcomptype(type, name) | |
50 | -- set the compression type and the | |
51 | human-readable compression type | |
52 | setparams(tuple) | |
53 | -- set all parameters at once | |
54 | tell() -- return current position in output file | |
55 | writeframesraw(data) | |
56 | -- write audio frames without pathing up the | |
57 | file header | |
58 | writeframes(data) | |
59 | -- write audio frames and patch up the file header | |
60 | close() -- patch up the file header and close the | |
61 | output file | |
62 | You should set the parameters before the first writeframesraw or | |
63 | writeframes. The total number of frames does not need to be set, | |
64 | but when it is set to the correct value, the header does not have to | |
65 | be patched up. | |
66 | It is best to first set all parameters, perhaps possibly the | |
67 | compression type, and then write audio frames using writeframesraw. | |
68 | When all frames have been written, either call writeframes('') or | |
69 | close() to patch up the sizes in the header. | |
70 | The close() method is called automatically when the class instance | |
71 | is destroyed. | |
72 | """ | |
73 | ||
74 | import __builtin__ | |
75 | ||
76 | __all__ = ["open", "openfp", "Error"] | |
77 | ||
78 | class Error(Exception): | |
79 | pass | |
80 | ||
81 | WAVE_FORMAT_PCM = 0x0001 | |
82 | ||
83 | _array_fmts = None, 'b', 'h', None, 'l' | |
84 | ||
85 | # Determine endian-ness | |
86 | import struct | |
87 | if struct.pack("h", 1) == "\000\001": | |
88 | big_endian = 1 | |
89 | else: | |
90 | big_endian = 0 | |
91 | ||
92 | from chunk import Chunk | |
93 | ||
94 | class Wave_read: | |
95 | """Variables used in this class: | |
96 | ||
97 | These variables are available to the user though appropriate | |
98 | methods of this class: | |
99 | _file -- the open file with methods read(), close(), and seek() | |
100 | set through the __init__() method | |
101 | _nchannels -- the number of audio channels | |
102 | available through the getnchannels() method | |
103 | _nframes -- the number of audio frames | |
104 | available through the getnframes() method | |
105 | _sampwidth -- the number of bytes per audio sample | |
106 | available through the getsampwidth() method | |
107 | _framerate -- the sampling frequency | |
108 | available through the getframerate() method | |
109 | _comptype -- the AIFF-C compression type ('NONE' if AIFF) | |
110 | available through the getcomptype() method | |
111 | _compname -- the human-readable AIFF-C compression type | |
112 | available through the getcomptype() method | |
113 | _soundpos -- the position in the audio stream | |
114 | available through the tell() method, set through the | |
115 | setpos() method | |
116 | ||
117 | These variables are used internally only: | |
118 | _fmt_chunk_read -- 1 iff the FMT chunk has been read | |
119 | _data_seek_needed -- 1 iff positioned correctly in audio | |
120 | file for readframes() | |
121 | _data_chunk -- instantiation of a chunk class for the DATA chunk | |
122 | _framesize -- size of one frame in the file | |
123 | """ | |
124 | ||
125 | def initfp(self, file): | |
126 | self._convert = None | |
127 | self._soundpos = 0 | |
128 | self._file = Chunk(file, bigendian = 0) | |
129 | if self._file.getname() != 'RIFF': | |
130 | raise Error, 'file does not start with RIFF id' | |
131 | if self._file.read(4) != 'WAVE': | |
132 | raise Error, 'not a WAVE file' | |
133 | self._fmt_chunk_read = 0 | |
134 | self._data_chunk = None | |
135 | while 1: | |
136 | self._data_seek_needed = 1 | |
137 | try: | |
138 | chunk = Chunk(self._file, bigendian = 0) | |
139 | except EOFError: | |
140 | break | |
141 | chunkname = chunk.getname() | |
142 | if chunkname == 'fmt ': | |
143 | self._read_fmt_chunk(chunk) | |
144 | self._fmt_chunk_read = 1 | |
145 | elif chunkname == 'data': | |
146 | if not self._fmt_chunk_read: | |
147 | raise Error, 'data chunk before fmt chunk' | |
148 | self._data_chunk = chunk | |
149 | self._nframes = chunk.chunksize // self._framesize | |
150 | self._data_seek_needed = 0 | |
151 | break | |
152 | chunk.skip() | |
153 | if not self._fmt_chunk_read or not self._data_chunk: | |
154 | raise Error, 'fmt chunk and/or data chunk missing' | |
155 | ||
156 | def __init__(self, f): | |
157 | self._i_opened_the_file = None | |
158 | if isinstance(f, basestring): | |
159 | f = __builtin__.open(f, 'rb') | |
160 | self._i_opened_the_file = f | |
161 | # else, assume it is an open file object already | |
162 | self.initfp(f) | |
163 | ||
164 | def __del__(self): | |
165 | self.close() | |
166 | # | |
167 | # User visible methods. | |
168 | # | |
169 | def getfp(self): | |
170 | return self._file | |
171 | ||
172 | def rewind(self): | |
173 | self._data_seek_needed = 1 | |
174 | self._soundpos = 0 | |
175 | ||
176 | def close(self): | |
177 | if self._i_opened_the_file: | |
178 | self._i_opened_the_file.close() | |
179 | self._i_opened_the_file = None | |
180 | self._file = None | |
181 | ||
182 | def tell(self): | |
183 | return self._soundpos | |
184 | ||
185 | def getnchannels(self): | |
186 | return self._nchannels | |
187 | ||
188 | def getnframes(self): | |
189 | return self._nframes | |
190 | ||
191 | def getsampwidth(self): | |
192 | return self._sampwidth | |
193 | ||
194 | def getframerate(self): | |
195 | return self._framerate | |
196 | ||
197 | def getcomptype(self): | |
198 | return self._comptype | |
199 | ||
200 | def getcompname(self): | |
201 | return self._compname | |
202 | ||
203 | def getparams(self): | |
204 | return self.getnchannels(), self.getsampwidth(), \ | |
205 | self.getframerate(), self.getnframes(), \ | |
206 | self.getcomptype(), self.getcompname() | |
207 | ||
208 | def getmarkers(self): | |
209 | return None | |
210 | ||
211 | def getmark(self, id): | |
212 | raise Error, 'no marks' | |
213 | ||
214 | def setpos(self, pos): | |
215 | if pos < 0 or pos > self._nframes: | |
216 | raise Error, 'position not in range' | |
217 | self._soundpos = pos | |
218 | self._data_seek_needed = 1 | |
219 | ||
220 | def readframes(self, nframes): | |
221 | if self._data_seek_needed: | |
222 | self._data_chunk.seek(0, 0) | |
223 | pos = self._soundpos * self._framesize | |
224 | if pos: | |
225 | self._data_chunk.seek(pos, 0) | |
226 | self._data_seek_needed = 0 | |
227 | if nframes == 0: | |
228 | return '' | |
229 | if self._sampwidth > 1 and big_endian: | |
230 | # unfortunately the fromfile() method does not take | |
231 | # something that only looks like a file object, so | |
232 | # we have to reach into the innards of the chunk object | |
233 | import array | |
234 | chunk = self._data_chunk | |
235 | data = array.array(_array_fmts[self._sampwidth]) | |
236 | nitems = nframes * self._nchannels | |
237 | if nitems * self._sampwidth > chunk.chunksize - chunk.size_read: | |
238 | nitems = (chunk.chunksize - chunk.size_read) / self._sampwidth | |
239 | data.fromfile(chunk.file.file, nitems) | |
240 | # "tell" data chunk how much was read | |
241 | chunk.size_read = chunk.size_read + nitems * self._sampwidth | |
242 | # do the same for the outermost chunk | |
243 | chunk = chunk.file | |
244 | chunk.size_read = chunk.size_read + nitems * self._sampwidth | |
245 | data.byteswap() | |
246 | data = data.tostring() | |
247 | else: | |
248 | data = self._data_chunk.read(nframes * self._framesize) | |
249 | if self._convert and data: | |
250 | data = self._convert(data) | |
251 | self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth) | |
252 | return data | |
253 | ||
254 | # | |
255 | # Internal methods. | |
256 | # | |
257 | ||
258 | def _read_fmt_chunk(self, chunk): | |
259 | wFormatTag, self._nchannels, self._framerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack('<hhllh', chunk.read(14)) | |
260 | if wFormatTag == WAVE_FORMAT_PCM: | |
261 | sampwidth = struct.unpack('<h', chunk.read(2))[0] | |
262 | self._sampwidth = (sampwidth + 7) // 8 | |
263 | else: | |
264 | raise Error, 'unknown format: %r' % (wFormatTag,) | |
265 | self._framesize = self._nchannels * self._sampwidth | |
266 | self._comptype = 'NONE' | |
267 | self._compname = 'not compressed' | |
268 | ||
269 | class Wave_write: | |
270 | """Variables used in this class: | |
271 | ||
272 | These variables are user settable through appropriate methods | |
273 | of this class: | |
274 | _file -- the open file with methods write(), close(), tell(), seek() | |
275 | set through the __init__() method | |
276 | _comptype -- the AIFF-C compression type ('NONE' in AIFF) | |
277 | set through the setcomptype() or setparams() method | |
278 | _compname -- the human-readable AIFF-C compression type | |
279 | set through the setcomptype() or setparams() method | |
280 | _nchannels -- the number of audio channels | |
281 | set through the setnchannels() or setparams() method | |
282 | _sampwidth -- the number of bytes per audio sample | |
283 | set through the setsampwidth() or setparams() method | |
284 | _framerate -- the sampling frequency | |
285 | set through the setframerate() or setparams() method | |
286 | _nframes -- the number of audio frames written to the header | |
287 | set through the setnframes() or setparams() method | |
288 | ||
289 | These variables are used internally only: | |
290 | _datalength -- the size of the audio samples written to the header | |
291 | _nframeswritten -- the number of frames actually written | |
292 | _datawritten -- the size of the audio samples actually written | |
293 | """ | |
294 | ||
295 | def __init__(self, f): | |
296 | self._i_opened_the_file = None | |
297 | if isinstance(f, basestring): | |
298 | f = __builtin__.open(f, 'wb') | |
299 | self._i_opened_the_file = f | |
300 | self.initfp(f) | |
301 | ||
302 | def initfp(self, file): | |
303 | self._file = file | |
304 | self._convert = None | |
305 | self._nchannels = 0 | |
306 | self._sampwidth = 0 | |
307 | self._framerate = 0 | |
308 | self._nframes = 0 | |
309 | self._nframeswritten = 0 | |
310 | self._datawritten = 0 | |
311 | self._datalength = 0 | |
312 | ||
313 | def __del__(self): | |
314 | self.close() | |
315 | ||
316 | # | |
317 | # User visible methods. | |
318 | # | |
319 | def setnchannels(self, nchannels): | |
320 | if self._datawritten: | |
321 | raise Error, 'cannot change parameters after starting to write' | |
322 | if nchannels < 1: | |
323 | raise Error, 'bad # of channels' | |
324 | self._nchannels = nchannels | |
325 | ||
326 | def getnchannels(self): | |
327 | if not self._nchannels: | |
328 | raise Error, 'number of channels not set' | |
329 | return self._nchannels | |
330 | ||
331 | def setsampwidth(self, sampwidth): | |
332 | if self._datawritten: | |
333 | raise Error, 'cannot change parameters after starting to write' | |
334 | if sampwidth < 1 or sampwidth > 4: | |
335 | raise Error, 'bad sample width' | |
336 | self._sampwidth = sampwidth | |
337 | ||
338 | def getsampwidth(self): | |
339 | if not self._sampwidth: | |
340 | raise Error, 'sample width not set' | |
341 | return self._sampwidth | |
342 | ||
343 | def setframerate(self, framerate): | |
344 | if self._datawritten: | |
345 | raise Error, 'cannot change parameters after starting to write' | |
346 | if framerate <= 0: | |
347 | raise Error, 'bad frame rate' | |
348 | self._framerate = framerate | |
349 | ||
350 | def getframerate(self): | |
351 | if not self._framerate: | |
352 | raise Error, 'frame rate not set' | |
353 | return self._framerate | |
354 | ||
355 | def setnframes(self, nframes): | |
356 | if self._datawritten: | |
357 | raise Error, 'cannot change parameters after starting to write' | |
358 | self._nframes = nframes | |
359 | ||
360 | def getnframes(self): | |
361 | return self._nframeswritten | |
362 | ||
363 | def setcomptype(self, comptype, compname): | |
364 | if self._datawritten: | |
365 | raise Error, 'cannot change parameters after starting to write' | |
366 | if comptype not in ('NONE',): | |
367 | raise Error, 'unsupported compression type' | |
368 | self._comptype = comptype | |
369 | self._compname = compname | |
370 | ||
371 | def getcomptype(self): | |
372 | return self._comptype | |
373 | ||
374 | def getcompname(self): | |
375 | return self._compname | |
376 | ||
377 | def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)): | |
378 | if self._datawritten: | |
379 | raise Error, 'cannot change parameters after starting to write' | |
380 | self.setnchannels(nchannels) | |
381 | self.setsampwidth(sampwidth) | |
382 | self.setframerate(framerate) | |
383 | self.setnframes(nframes) | |
384 | self.setcomptype(comptype, compname) | |
385 | ||
386 | def getparams(self): | |
387 | if not self._nchannels or not self._sampwidth or not self._framerate: | |
388 | raise Error, 'not all parameters set' | |
389 | return self._nchannels, self._sampwidth, self._framerate, \ | |
390 | self._nframes, self._comptype, self._compname | |
391 | ||
392 | def setmark(self, id, pos, name): | |
393 | raise Error, 'setmark() not supported' | |
394 | ||
395 | def getmark(self, id): | |
396 | raise Error, 'no marks' | |
397 | ||
398 | def getmarkers(self): | |
399 | return None | |
400 | ||
401 | def tell(self): | |
402 | return self._nframeswritten | |
403 | ||
404 | def writeframesraw(self, data): | |
405 | self._ensure_header_written(len(data)) | |
406 | nframes = len(data) // (self._sampwidth * self._nchannels) | |
407 | if self._convert: | |
408 | data = self._convert(data) | |
409 | if self._sampwidth > 1 and big_endian: | |
410 | import array | |
411 | data = array.array(_array_fmts[self._sampwidth], data) | |
412 | data.byteswap() | |
413 | data.tofile(self._file) | |
414 | self._datawritten = self._datawritten + len(data) * self._sampwidth | |
415 | else: | |
416 | self._file.write(data) | |
417 | self._datawritten = self._datawritten + len(data) | |
418 | self._nframeswritten = self._nframeswritten + nframes | |
419 | ||
420 | def writeframes(self, data): | |
421 | self.writeframesraw(data) | |
422 | if self._datalength != self._datawritten: | |
423 | self._patchheader() | |
424 | ||
425 | def close(self): | |
426 | if self._file: | |
427 | self._ensure_header_written(0) | |
428 | if self._datalength != self._datawritten: | |
429 | self._patchheader() | |
430 | self._file.flush() | |
431 | self._file = None | |
432 | if self._i_opened_the_file: | |
433 | self._i_opened_the_file.close() | |
434 | self._i_opened_the_file = None | |
435 | ||
436 | # | |
437 | # Internal methods. | |
438 | # | |
439 | ||
440 | def _ensure_header_written(self, datasize): | |
441 | if not self._datawritten: | |
442 | if not self._nchannels: | |
443 | raise Error, '# channels not specified' | |
444 | if not self._sampwidth: | |
445 | raise Error, 'sample width not specified' | |
446 | if not self._framerate: | |
447 | raise Error, 'sampling rate not specified' | |
448 | self._write_header(datasize) | |
449 | ||
450 | def _write_header(self, initlength): | |
451 | self._file.write('RIFF') | |
452 | if not self._nframes: | |
453 | self._nframes = initlength / (self._nchannels * self._sampwidth) | |
454 | self._datalength = self._nframes * self._nchannels * self._sampwidth | |
455 | self._form_length_pos = self._file.tell() | |
456 | self._file.write(struct.pack('<l4s4slhhllhh4s', | |
457 | 36 + self._datalength, 'WAVE', 'fmt ', 16, | |
458 | WAVE_FORMAT_PCM, self._nchannels, self._framerate, | |
459 | self._nchannels * self._framerate * self._sampwidth, | |
460 | self._nchannels * self._sampwidth, | |
461 | self._sampwidth * 8, 'data')) | |
462 | self._data_length_pos = self._file.tell() | |
463 | self._file.write(struct.pack('<l', self._datalength)) | |
464 | ||
465 | def _patchheader(self): | |
466 | if self._datawritten == self._datalength: | |
467 | return | |
468 | curpos = self._file.tell() | |
469 | self._file.seek(self._form_length_pos, 0) | |
470 | self._file.write(struct.pack('<l', 36 + self._datawritten)) | |
471 | self._file.seek(self._data_length_pos, 0) | |
472 | self._file.write(struct.pack('<l', self._datawritten)) | |
473 | self._file.seek(curpos, 0) | |
474 | self._datalength = self._datawritten | |
475 | ||
476 | def open(f, mode=None): | |
477 | if mode is None: | |
478 | if hasattr(f, 'mode'): | |
479 | mode = f.mode | |
480 | else: | |
481 | mode = 'rb' | |
482 | if mode in ('r', 'rb'): | |
483 | return Wave_read(f) | |
484 | elif mode in ('w', 'wb'): | |
485 | return Wave_write(f) | |
486 | else: | |
487 | raise Error, "mode must be 'r', 'rb', 'w', or 'wb'" | |
488 | ||
489 | openfp = open # B/W compatibility |