~ubuntu-branches/ubuntu/maverick/python3.1/maverick

« back to all changes in this revision

Viewing changes to Lib/wave.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-03-23 00:01:27 UTC
  • Revision ID: james.westby@ubuntu.com-20090323000127-5fstfxju4ufrhthq
Tags: upstream-3.1~a1+20090322
ImportĀ upstreamĀ versionĀ 3.1~a1+20090322

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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 builtins
 
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) == b"\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() != b'RIFF':
 
130
            raise Error('file does not start with RIFF id')
 
131
        if self._file.read(4) != b'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 == b'fmt ':
 
143
                self._read_fmt_chunk(chunk)
 
144
                self._fmt_chunk_read = 1
 
145
            elif chunkname == b'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, str):
 
159
            f = builtins.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 b''
 
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_from('<hhllh', chunk.read(14))
 
265
        if wFormatTag == WAVE_FORMAT_PCM:
 
266
            sampwidth = struct.unpack_from('<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, str):
 
303
            f = builtins.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, params):
 
388
        nchannels, sampwidth, framerate, nframes, comptype, compname = params
 
389
        if self._datawritten:
 
390
            raise Error('cannot change parameters after starting to write')
 
391
        self.setnchannels(nchannels)
 
392
        self.setsampwidth(sampwidth)
 
393
        self.setframerate(framerate)
 
394
        self.setnframes(nframes)
 
395
        self.setcomptype(comptype, compname)
 
396
 
 
397
    def getparams(self):
 
398
        if not self._nchannels or not self._sampwidth or not self._framerate:
 
399
            raise Error('not all parameters set')
 
400
        return self._nchannels, self._sampwidth, self._framerate, \
 
401
              self._nframes, self._comptype, self._compname
 
402
 
 
403
    def setmark(self, id, pos, name):
 
404
        raise Error('setmark() not supported')
 
405
 
 
406
    def getmark(self, id):
 
407
        raise Error('no marks')
 
408
 
 
409
    def getmarkers(self):
 
410
        return None
 
411
 
 
412
    def tell(self):
 
413
        return self._nframeswritten
 
414
 
 
415
    def writeframesraw(self, data):
 
416
        self._ensure_header_written(len(data))
 
417
        nframes = len(data) // (self._sampwidth * self._nchannels)
 
418
        if self._convert:
 
419
            data = self._convert(data)
 
420
        if self._sampwidth > 1 and big_endian:
 
421
            import array
 
422
            data = array.array(_array_fmts[self._sampwidth], data)
 
423
            data.byteswap()
 
424
            data.tofile(self._file)
 
425
            self._datawritten = self._datawritten + len(data) * self._sampwidth
 
426
        else:
 
427
            self._file.write(data)
 
428
            self._datawritten = self._datawritten + len(data)
 
429
        self._nframeswritten = self._nframeswritten + nframes
 
430
 
 
431
    def writeframes(self, data):
 
432
        self.writeframesraw(data)
 
433
        if self._datalength != self._datawritten:
 
434
            self._patchheader()
 
435
 
 
436
    def close(self):
 
437
        if self._file:
 
438
            self._ensure_header_written(0)
 
439
            if self._datalength != self._datawritten:
 
440
                self._patchheader()
 
441
            self._file.flush()
 
442
            self._file = None
 
443
        if self._i_opened_the_file:
 
444
            self._i_opened_the_file.close()
 
445
            self._i_opened_the_file = None
 
446
 
 
447
    #
 
448
    # Internal methods.
 
449
    #
 
450
 
 
451
    def _ensure_header_written(self, datasize):
 
452
        if not self._datawritten:
 
453
            if not self._nchannels:
 
454
                raise Error('# channels not specified')
 
455
            if not self._sampwidth:
 
456
                raise Error('sample width not specified')
 
457
            if not self._framerate:
 
458
                raise Error('sampling rate not specified')
 
459
            self._write_header(datasize)
 
460
 
 
461
    def _write_header(self, initlength):
 
462
        self._file.write(b'RIFF')
 
463
        if not self._nframes:
 
464
            self._nframes = initlength / (self._nchannels * self._sampwidth)
 
465
        self._datalength = self._nframes * self._nchannels * self._sampwidth
 
466
        self._form_length_pos = self._file.tell()
 
467
        self._file.write(struct.pack('<l4s4slhhllhh4s',
 
468
            36 + self._datalength, 'WAVE', 'fmt ', 16,
 
469
            WAVE_FORMAT_PCM, self._nchannels, self._framerate,
 
470
            self._nchannels * self._framerate * self._sampwidth,
 
471
            self._nchannels * self._sampwidth,
 
472
            self._sampwidth * 8, 'data'))
 
473
        self._data_length_pos = self._file.tell()
 
474
        self._file.write(struct.pack('<l', self._datalength))
 
475
 
 
476
    def _patchheader(self):
 
477
        if self._datawritten == self._datalength:
 
478
            return
 
479
        curpos = self._file.tell()
 
480
        self._file.seek(self._form_length_pos, 0)
 
481
        self._file.write(struct.pack('<l', 36 + self._datawritten))
 
482
        self._file.seek(self._data_length_pos, 0)
 
483
        self._file.write(struct.pack('<l', self._datawritten))
 
484
        self._file.seek(curpos, 0)
 
485
        self._datalength = self._datawritten
 
486
 
 
487
def open(f, mode=None):
 
488
    if mode is None:
 
489
        if hasattr(f, 'mode'):
 
490
            mode = f.mode
 
491
        else:
 
492
            mode = 'rb'
 
493
    if mode in ('r', 'rb'):
 
494
        return Wave_read(f)
 
495
    elif mode in ('w', 'wb'):
 
496
        return Wave_write(f)
 
497
    else:
 
498
        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
 
499
 
 
500
openfp = open # B/W compatibility