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