annotate adc2004/wave.py @ 2:86aed1f351e3 tip

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