]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/wave.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / wave.py
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         try:
163             self.initfp(f)
164         except:
165             if self._i_opened_the_file:
166                 f.close()
167             raise
168
169     def __del__(self):
170         self.close()
171     #
172     # User visible methods.
173     #
174     def getfp(self):
175         return self._file
176
177     def rewind(self):
178         self._data_seek_needed = 1
179         self._soundpos = 0
180
181     def close(self):
182         if self._i_opened_the_file:
183             self._i_opened_the_file.close()
184             self._i_opened_the_file = None
185         self._file = None
186
187     def tell(self):
188         return self._soundpos
189
190     def getnchannels(self):
191         return self._nchannels
192
193     def getnframes(self):
194         return self._nframes
195
196     def getsampwidth(self):
197         return self._sampwidth
198
199     def getframerate(self):
200         return self._framerate
201
202     def getcomptype(self):
203         return self._comptype
204
205     def getcompname(self):
206         return self._compname
207
208     def getparams(self):
209         return self.getnchannels(), self.getsampwidth(), \
210                self.getframerate(), self.getnframes(), \
211                self.getcomptype(), self.getcompname()
212
213     def getmarkers(self):
214         return None
215
216     def getmark(self, id):
217         raise Error, 'no marks'
218
219     def setpos(self, pos):
220         if pos < 0 or pos > self._nframes:
221             raise Error, 'position not in range'
222         self._soundpos = pos
223         self._data_seek_needed = 1
224
225     def readframes(self, nframes):
226         if self._data_seek_needed:
227             self._data_chunk.seek(0, 0)
228             pos = self._soundpos * self._framesize
229             if pos:
230                 self._data_chunk.seek(pos, 0)
231             self._data_seek_needed = 0
232         if nframes == 0:
233             return ''
234         if self._sampwidth > 1 and big_endian:
235             # unfortunately the fromfile() method does not take
236             # something that only looks like a file object, so
237             # we have to reach into the innards of the chunk object
238             import array
239             chunk = self._data_chunk
240             data = array.array(_array_fmts[self._sampwidth])
241             nitems = nframes * self._nchannels
242             if nitems * self._sampwidth > chunk.chunksize - chunk.size_read:
243                 nitems = (chunk.chunksize - chunk.size_read) / self._sampwidth
244             data.fromfile(chunk.file.file, nitems)
245             # "tell" data chunk how much was read
246             chunk.size_read = chunk.size_read + nitems * self._sampwidth
247             # do the same for the outermost chunk
248             chunk = chunk.file
249             chunk.size_read = chunk.size_read + nitems * self._sampwidth
250             data.byteswap()
251             data = data.tostring()
252         else:
253             data = self._data_chunk.read(nframes * self._framesize)
254         if self._convert and data:
255             data = self._convert(data)
256         self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth)
257         return data
258
259     #
260     # Internal methods.
261     #
262
263     def _read_fmt_chunk(self, chunk):
264         wFormatTag, self._nchannels, self._framerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack('<hhllh', chunk.read(14))
265         if wFormatTag == WAVE_FORMAT_PCM:
266             sampwidth = struct.unpack('<h', chunk.read(2))[0]
267             self._sampwidth = (sampwidth + 7) // 8
268         else:
269             raise Error, 'unknown format: %r' % (wFormatTag,)
270         self._framesize = self._nchannels * self._sampwidth
271         self._comptype = 'NONE'
272         self._compname = 'not compressed'
273
274 class Wave_write:
275     """Variables used in this class:
276
277     These variables are user settable through appropriate methods
278     of this class:
279     _file -- the open file with methods write(), close(), tell(), seek()
280               set through the __init__() method
281     _comptype -- the AIFF-C compression type ('NONE' in AIFF)
282               set through the setcomptype() or setparams() method
283     _compname -- the human-readable AIFF-C compression type
284               set through the setcomptype() or setparams() method
285     _nchannels -- the number of audio channels
286               set through the setnchannels() or setparams() method
287     _sampwidth -- the number of bytes per audio sample
288               set through the setsampwidth() or setparams() method
289     _framerate -- the sampling frequency
290               set through the setframerate() or setparams() method
291     _nframes -- the number of audio frames written to the header
292               set through the setnframes() or setparams() method
293
294     These variables are used internally only:
295     _datalength -- the size of the audio samples written to the header
296     _nframeswritten -- the number of frames actually written
297     _datawritten -- the size of the audio samples actually written
298     """
299
300     def __init__(self, f):
301         self._i_opened_the_file = None
302         if isinstance(f, basestring):
303             f = __builtin__.open(f, 'wb')
304             self._i_opened_the_file = f
305         try:
306             self.initfp(f)
307         except:
308             if self._i_opened_the_file:
309                 f.close()
310             raise
311
312     def initfp(self, file):
313         self._file = file
314         self._convert = None
315         self._nchannels = 0
316         self._sampwidth = 0
317         self._framerate = 0
318         self._nframes = 0
319         self._nframeswritten = 0
320         self._datawritten = 0
321         self._datalength = 0
322
323     def __del__(self):
324         self.close()
325
326     #
327     # User visible methods.
328     #
329     def setnchannels(self, nchannels):
330         if self._datawritten:
331             raise Error, 'cannot change parameters after starting to write'
332         if nchannels < 1:
333             raise Error, 'bad # of channels'
334         self._nchannels = nchannels
335
336     def getnchannels(self):
337         if not self._nchannels:
338             raise Error, 'number of channels not set'
339         return self._nchannels
340
341     def setsampwidth(self, sampwidth):
342         if self._datawritten:
343             raise Error, 'cannot change parameters after starting to write'
344         if sampwidth < 1 or sampwidth > 4:
345             raise Error, 'bad sample width'
346         self._sampwidth = sampwidth
347
348     def getsampwidth(self):
349         if not self._sampwidth:
350             raise Error, 'sample width not set'
351         return self._sampwidth
352
353     def setframerate(self, framerate):
354         if self._datawritten:
355             raise Error, 'cannot change parameters after starting to write'
356         if framerate <= 0:
357             raise Error, 'bad frame rate'
358         self._framerate = framerate
359
360     def getframerate(self):
361         if not self._framerate:
362             raise Error, 'frame rate not set'
363         return self._framerate
364
365     def setnframes(self, nframes):
366         if self._datawritten:
367             raise Error, 'cannot change parameters after starting to write'
368         self._nframes = nframes
369
370     def getnframes(self):
371         return self._nframeswritten
372
373     def setcomptype(self, comptype, compname):
374         if self._datawritten:
375             raise Error, 'cannot change parameters after starting to write'
376         if comptype not in ('NONE',):
377             raise Error, 'unsupported compression type'
378         self._comptype = comptype
379         self._compname = compname
380
381     def getcomptype(self):
382         return self._comptype
383
384     def getcompname(self):
385         return self._compname
386
387     def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
388         if self._datawritten:
389             raise Error, 'cannot change parameters after starting to write'
390         self.setnchannels(nchannels)
391         self.setsampwidth(sampwidth)
392         self.setframerate(framerate)
393         self.setnframes(nframes)
394         self.setcomptype(comptype, compname)
395
396     def getparams(self):
397         if not self._nchannels or not self._sampwidth or not self._framerate:
398             raise Error, 'not all parameters set'
399         return self._nchannels, self._sampwidth, self._framerate, \
400               self._nframes, self._comptype, self._compname
401
402     def setmark(self, id, pos, name):
403         raise Error, 'setmark() not supported'
404
405     def getmark(self, id):
406         raise Error, 'no marks'
407
408     def getmarkers(self):
409         return None
410
411     def tell(self):
412         return self._nframeswritten
413
414     def writeframesraw(self, data):
415         self._ensure_header_written(len(data))
416         nframes = len(data) // (self._sampwidth * self._nchannels)
417         if self._convert:
418             data = self._convert(data)
419         if self._sampwidth > 1 and big_endian:
420             import array
421             data = array.array(_array_fmts[self._sampwidth], data)
422             data.byteswap()
423             data.tofile(self._file)
424             self._datawritten = self._datawritten + len(data) * self._sampwidth
425         else:
426             self._file.write(data)
427             self._datawritten = self._datawritten + len(data)
428         self._nframeswritten = self._nframeswritten + nframes
429
430     def writeframes(self, data):
431         self.writeframesraw(data)
432         if self._datalength != self._datawritten:
433             self._patchheader()
434
435     def close(self):
436         if self._file:
437             self._ensure_header_written(0)
438             if self._datalength != self._datawritten:
439                 self._patchheader()
440             self._file.flush()
441             self._file = None
442         if self._i_opened_the_file:
443             self._i_opened_the_file.close()
444             self._i_opened_the_file = None
445
446     #
447     # Internal methods.
448     #
449
450     def _ensure_header_written(self, datasize):
451         if not self._datawritten:
452             if not self._nchannels:
453                 raise Error, '# channels not specified'
454             if not self._sampwidth:
455                 raise Error, 'sample width not specified'
456             if not self._framerate:
457                 raise Error, 'sampling rate not specified'
458             self._write_header(datasize)
459
460     def _write_header(self, initlength):
461         self._file.write('RIFF')
462         if not self._nframes:
463             self._nframes = initlength / (self._nchannels * self._sampwidth)
464         self._datalength = self._nframes * self._nchannels * self._sampwidth
465         self._form_length_pos = self._file.tell()
466         self._file.write(struct.pack('<l4s4slhhllhh4s',
467             36 + self._datalength, 'WAVE', 'fmt ', 16,
468             WAVE_FORMAT_PCM, self._nchannels, self._framerate,
469             self._nchannels * self._framerate * self._sampwidth,
470             self._nchannels * self._sampwidth,
471             self._sampwidth * 8, 'data'))
472         self._data_length_pos = self._file.tell()
473         self._file.write(struct.pack('<l', self._datalength))
474
475     def _patchheader(self):
476         if self._datawritten == self._datalength:
477             return
478         curpos = self._file.tell()
479         self._file.seek(self._form_length_pos, 0)
480         self._file.write(struct.pack('<l', 36 + self._datawritten))
481         self._file.seek(self._data_length_pos, 0)
482         self._file.write(struct.pack('<l', self._datawritten))
483         self._file.seek(curpos, 0)
484         self._datalength = self._datawritten
485
486 def open(f, mode=None):
487     if mode is None:
488         if hasattr(f, 'mode'):
489             mode = f.mode
490         else:
491             mode = 'rb'
492     if mode in ('r', 'rb'):
493         return Wave_read(f)
494     elif mode in ('w', 'wb'):
495         return Wave_write(f)
496     else:
497         raise Error, "mode must be 'r', 'rb', 'w', or 'wb'"
498
499 openfp = open # B/W compatibility