1 """Stuff to parse AIFF-C and AIFF files. 2 3 Unless explicitly stated otherwise, the description below is true 4 both for AIFF-C files and AIFF files. 5 6 An AIFF-C file has the following structure. 7 8 +-----------------+ 9 | FORM | 10 +-----------------+ 11 | <size> | 12 +----+------------+ 13 | | AIFC | 14 | +------------+ 15 | | <chunks> | 16 | | . | 17 | | . | 18 | | . | 19 +----+------------+ 20 21 An AIFF file has the string "AIFF" instead of "AIFC". 22 23 A chunk consists of an identifier (4 bytes) followed by a size (4 bytes, 24 big endian order), followed by the data. The size field does not include 25 the size of the 8 byte header. 26 27 The following chunk types are recognized. 28 29 FVER 30 <version number of AIFF-C defining document> (AIFF-C only). 31 MARK 32 <# of markers> (2 bytes) 33 list of markers: 34 <marker ID> (2 bytes, must be > 0) 35 <position> (4 bytes) 36 <marker name> ("pstring") 37 COMM 38 <# of channels> (2 bytes) 39 <# of sound frames> (4 bytes) 40 <size of the samples> (2 bytes) 41 <sampling frequency> (10 bytes, IEEE 80-bit extended 42 floating point) 43 in AIFF-C files only: 44 <compression type> (4 bytes) 45 <human-readable version of compression type> ("pstring") 46 SSND 47 <offset> (4 bytes, not used by this program) 48 <blocksize> (4 bytes, not used by this program) 49 <sound data> 50 51 A pstring consists of 1 byte length, a string of characters, and 0 or 1 52 byte pad to make the total length even. 53 54 Usage. 55 56 Reading AIFF files: 57 f = aifc.open(file, 'r') 58 where file is either the name of a file or an open file pointer. 59 The open file pointer must have methods read(), seek(), and close(). 60 In some types of audio files, if the setpos() method is not used, 61 the seek() method is not necessary. 62 63 This returns an instance of a class with the following public methods: 64 getnchannels() -- returns number of audio channels (1 for 65 mono, 2 for stereo) 66 getsampwidth() -- returns sample width in bytes 67 getframerate() -- returns sampling frequency 68 getnframes() -- returns number of audio frames 69 getcomptype() -- returns compression type ('NONE' for AIFF files) 70 getcompname() -- returns human-readable version of 71 compression type ('not compressed' for AIFF files) 72 getparams() -- returns a namedtuple consisting of all of the 73 above in the above order 74 getmarkers() -- get the list of marks in the audio file or None 75 if there are no marks 76 getmark(id) -- get mark with the specified id (raises an error 77 if the mark does not exist) 78 readframes(n) -- returns at most n frames of audio 79 rewind() -- rewind to the beginning of the audio stream 80 setpos(pos) -- seek to the specified position 81 tell() -- return the current position 82 close() -- close the instance (make it unusable) 83 The position returned by tell(), the position given to setpos() and 84 the position of marks are all compatible and have nothing to do with 85 the actual position in the file. 86 The close() method is called automatically when the class instance 87 is destroyed. 88 89 Writing AIFF files: 90 f = aifc.open(file, 'w') 91 where file is either the name of a file or an open file pointer. 92 The open file pointer must have methods write(), tell(), seek(), and 93 close(). 94 95 This returns an instance of a class with the following public methods: 96 aiff() -- create an AIFF file (AIFF-C default) 97 aifc() -- create an AIFF-C file 98 setnchannels(n) -- set the number of channels 99 setsampwidth(n) -- set the sample width 100 setframerate(n) -- set the frame rate 101 setnframes(n) -- set the number of frames 102 setcomptype(type, name) 103 -- set the compression type and the 104 human-readable compression type 105 setparams(tuple) 106 -- set all parameters at once 107 setmark(id, pos, name) 108 -- add specified mark to the list of marks 109 tell() -- return current position in output file (useful 110 in combination with setmark()) 111 writeframesraw(data) 112 -- write audio frames without pathing up the 113 file header 114 writeframes(data) 115 -- write audio frames and patch up the file header 116 close() -- patch up the file header and close the 117 output file 118 You should set the parameters before the first writeframesraw or 119 writeframes. The total number of frames does not need to be set, 120 but when it is set to the correct value, the header does not have to 121 be patched up. 122 It is best to first set all parameters, perhaps possibly the 123 compression type, and then write audio frames using writeframesraw. 124 When all frames have been written, either call writeframes(b'') or 125 close() to patch up the sizes in the header. 126 Marks can be added anytime. If there are any marks, you must call 127 close() after all frames have been written. 128 The close() method is called automatically when the class instance 129 is destroyed. 130 131 When a file is opened with the extension '.aiff', an AIFF file is 132 written, otherwise an AIFF-C file is written. This default can be 133 changed by calling aiff() or aifc() before the first writeframes or 134 writeframesraw. 135 """ 136 137 import struct 138 import builtins 139 import warnings 140 141 __all__ = ["Error", "open", "openfp"] 142 143 class Error(Exception): 144 pass 145 146 _AIFC_version = 0xA2805140 # Version 1 of AIFF-C 147 148 def _read_long(file): 149 try: 150 return struct.unpack('>l', file.read(4))[0] 151 except struct.error: 152 raise EOFError 153 154 def _read_ulong(file): 155 try: 156 return struct.unpack('>L', file.read(4))[0] 157 except struct.error: 158 raise EOFError 159 160 def _read_short(file): 161 try: 162 return struct.unpack('>h', file.read(2))[0] 163 except struct.error: 164 raise EOFError 165 166 def _read_ushort(file): 167 try: 168 return struct.unpack('>H', file.read(2))[0] 169 except struct.error: 170 raise EOFError 171 172 def _read_string(file): 173 length = ord(file.read(1)) 174 if length == 0: 175 data = b'' 176 else: 177 data = file.read(length) 178 if length & 1 == 0: 179 dummy = file.read(1) 180 return data 181 182 _HUGE_VAL = 1.79769313486231e+308 # See <limits.h> 183 184 def _read_float(f): # 10 bytes 185 expon = _read_short(f) # 2 bytes 186 sign = 1 187 if expon < 0: 188 sign = -1 189 expon = expon + 0x8000 190 himant = _read_ulong(f) # 4 bytes 191 lomant = _read_ulong(f) # 4 bytes 192 if expon == himant == lomant == 0: 193 f = 0.0 194 elif expon == 0x7FFF: 195 f = _HUGE_VAL 196 else: 197 expon = expon - 16383 198 f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63) 199 return sign * f 200 201 def _write_short(f, x): 202 f.write(struct.pack('>h', x)) 203 204 def _write_ushort(f, x): 205 f.write(struct.pack('>H', x)) 206 207 def _write_long(f, x): 208 f.write(struct.pack('>l', x)) 209 210 def _write_ulong(f, x): 211 f.write(struct.pack('>L', x)) 212 213 def _write_string(f, s): 214 if len(s) > 255: 215 raise ValueError("string exceeds maximum pstring length") 216 f.write(struct.pack('B', len(s))) 217 f.write(s) 218 if len(s) & 1 == 0: 219 f.write(b'\x00') 220 221 def _write_float(f, x): 222 import math 223 if x < 0: 224 sign = 0x8000 225 x = x * -1 226 else: 227 sign = 0 228 if x == 0: 229 expon = 0 230 himant = 0 231 lomant = 0 232 else: 233 fmant, expon = math.frexp(x) 234 if expon > 16384 or fmant >= 1 or fmant != fmant: # Infinity or NaN 235 expon = sign|0x7FFF 236 himant = 0 237 lomant = 0 238 else: # Finite 239 expon = expon + 16382 240 if expon < 0: # denormalized 241 fmant = math.ldexp(fmant, expon) 242 expon = 0 243 expon = expon | sign 244 fmant = math.ldexp(fmant, 32) 245 fsmant = math.floor(fmant) 246 himant = int(fsmant) 247 fmant = math.ldexp(fmant - fsmant, 32) 248 fsmant = math.floor(fmant) 249 lomant = int(fsmant) 250 _write_ushort(f, expon) 251 _write_ulong(f, himant) 252 _write_ulong(f, lomant) 253 254 from chunk import Chunk 255 from collections import namedtuple 256 257 _aifc_params = namedtuple('_aifc_params', 258 'nchannels sampwidth framerate nframes comptype compname') 259 260 _aifc_params.nchannels.__doc__ = 'Number of audio channels (1 for mono, 2 for stereo)' 261 _aifc_params.sampwidth.__doc__ = 'Sample width in bytes' 262 _aifc_params.framerate.__doc__ = 'Sampling frequency' 263 _aifc_params.nframes.__doc__ = 'Number of audio frames' 264 _aifc_params.comptype.__doc__ = 'Compression type ("NONE" for AIFF files)' 265 _aifc_params.compname.__doc__ = ("""\ 266 A human-readable version of the compression type 267 ('not compressed' for AIFF files)""") 268 269 270 class Aifc_read: 271 # Variables used in this class: 272 # 273 # These variables are available to the user though appropriate 274 # methods of this class: 275 # _file -- the open file with methods read(), close(), and seek() 276 # set through the __init__() method 277 # _nchannels -- the number of audio channels 278 # available through the getnchannels() method 279 # _nframes -- the number of audio frames 280 # available through the getnframes() method 281 # _sampwidth -- the number of bytes per audio sample 282 # available through the getsampwidth() method 283 # _framerate -- the sampling frequency 284 # available through the getframerate() method 285 # _comptype -- the AIFF-C compression type ('NONE' if AIFF) 286 # available through the getcomptype() method 287 # _compname -- the human-readable AIFF-C compression type 288 # available through the getcomptype() method 289 # _markers -- the marks in the audio file 290 # available through the getmarkers() and getmark() 291 # methods 292 # _soundpos -- the position in the audio stream 293 # available through the tell() method, set through the 294 # setpos() method 295 # 296 # These variables are used internally only: 297 # _version -- the AIFF-C version number 298 # _decomp -- the decompressor from builtin module cl 299 # _comm_chunk_read -- 1 iff the COMM chunk has been read 300 # _aifc -- 1 iff reading an AIFF-C file 301 # _ssnd_seek_needed -- 1 iff positioned correctly in audio 302 # file for readframes() 303 # _ssnd_chunk -- instantiation of a chunk class for the SSND chunk 304 # _framesize -- size of one frame in the file 305 306 _file = None # Set here since __del__ checks it 307 308 def initfp(self, file): 309 self._version = 0 310 self._convert = None 311 self._markers = [] 312 self._soundpos = 0 313 self._file = file 314 chunk = Chunk(file) 315 if chunk.getname() != b'FORM': 316 raise Error('file does not start with FORM id') 317 formdata = chunk.read(4) 318 if formdata == b'AIFF': 319 self._aifc = 0 320 elif formdata == b'AIFC': 321 self._aifc = 1 322 else: 323 raise Error('not an AIFF or AIFF-C file') 324 self._comm_chunk_read = 0 325 while 1: 326 self._ssnd_seek_needed = 1 327 try: 328 chunk = Chunk(self._file) 329 except EOFError: 330 break 331 chunkname = chunk.getname() 332 if chunkname == b'COMM': 333 self._read_comm_chunk(chunk) 334 self._comm_chunk_read = 1 335 elif chunkname == b'SSND': 336 self._ssnd_chunk = chunk 337 dummy = chunk.read(8) 338 self._ssnd_seek_needed = 0 339 elif chunkname == b'FVER': 340 self._version = _read_ulong(chunk) 341 elif chunkname == b'MARK': 342 self._readmark(chunk) 343 chunk.skip() 344 if not self._comm_chunk_read or not self._ssnd_chunk: 345 raise Error('COMM chunk and/or SSND chunk missing') 346 347 def __init__(self, f): 348 if isinstance(f, str): 349 file_object = builtins.open(f, 'rb') 350 try: 351 self.initfp(file_object) 352 except: 353 file_object.close() 354 raise 355 else: 356 # assume it is an open file object already 357 self.initfp(f) 358 359 def __enter__(self): 360 return self 361 362 def __exit__(self, *args): 363 self.close() 364 365 # 366 # User visible methods. 367 # 368 def getfp(self): 369 return self._file 370 371 def rewind(self): 372 self._ssnd_seek_needed = 1 373 self._soundpos = 0 374 375 def close(self): 376 file = self._file 377 if file is not None: 378 self._file = None 379 file.close() 380 381 def tell(self): 382 return self._soundpos 383 384 def getnchannels(self): 385 return self._nchannels 386 387 def getnframes(self): 388 return self._nframes 389 390 def getsampwidth(self): 391 return self._sampwidth 392 393 def getframerate(self): 394 return self._framerate 395 396 def getcomptype(self): 397 return self._comptype 398 399 def getcompname(self): 400 return self._compname 401 402 ## def getversion(self): 403 ## return self._version 404 405 def getparams(self): 406 return _aifc_params(self.getnchannels(), self.getsampwidth(), 407 self.getframerate(), self.getnframes(), 408 self.getcomptype(), self.getcompname()) 409 410 def getmarkers(self): 411 if len(self._markers) == 0: 412 return None 413 return self._markers 414 415 def getmark(self, id): 416 for marker in self._markers: 417 if id == marker[0]: 418 return marker 419 raise Error('marker {0!r} does not exist'.format(id)) 420 421 def setpos(self, pos): 422 if pos < 0 or pos > self._nframes: 423 raise Error('position not in range') 424 self._soundpos = pos 425 self._ssnd_seek_needed = 1 426 427 def readframes(self, nframes): 428 if self._ssnd_seek_needed: 429 self._ssnd_chunk.seek(0) 430 dummy = self._ssnd_chunk.read(8) 431 pos = self._soundpos * self._framesize 432 if pos: 433 self._ssnd_chunk.seek(pos + 8) 434 self._ssnd_seek_needed = 0 435 if nframes == 0: 436 return b'' 437 data = self._ssnd_chunk.read(nframes * self._framesize) 438 if self._convert and data: 439 data = self._convert(data) 440 self._soundpos = self._soundpos + len(data) // (self._nchannels 441 * self._sampwidth) 442 return data 443 444 # 445 # Internal methods. 446 # 447 448 def _alaw2lin(self, data): 449 import audioop 450 return audioop.alaw2lin(data, 2) 451 452 def _ulaw2lin(self, data): 453 import audioop 454 return audioop.ulaw2lin(data, 2) 455 456 def _adpcm2lin(self, data): 457 import audioop 458 if not hasattr(self, '_adpcmstate'): 459 # first time 460 self._adpcmstate = None 461 data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate) 462 return data 463 464 def _read_comm_chunk(self, chunk): 465 self._nchannels = _read_short(chunk) 466 self._nframes = _read_long(chunk) 467 self._sampwidth = (_read_short(chunk) + 7) // 8 468 self._framerate = int(_read_float(chunk)) 469 self._framesize = self._nchannels * self._sampwidth 470 if self._aifc: 471 #DEBUG: SGI's soundeditor produces a bad size :-( 472 kludge = 0 473 if chunk.chunksize == 18: 474 kludge = 1 475 warnings.warn('Warning: bad COMM chunk size') 476 chunk.chunksize = 23 477 #DEBUG end 478 self._comptype = chunk.read(4) 479 #DEBUG start 480 if kludge: 481 length = ord(chunk.file.read(1)) 482 if length & 1 == 0: 483 length = length + 1 484 chunk.chunksize = chunk.chunksize + length 485 chunk.file.seek(-1, 1) 486 #DEBUG end 487 self._compname = _read_string(chunk) 488 if self._comptype != b'NONE': 489 if self._comptype == b'G722': 490 self._convert = self._adpcm2lin 491 elif self._comptype in (b'ulaw', b'ULAW'): 492 self._convert = self._ulaw2lin 493 elif self._comptype in (b'alaw', b'ALAW'): 494 self._convert = self._alaw2lin 495 else: 496 raise Error('unsupported compression type') 497 self._sampwidth = 2 498 else: 499 self._comptype = b'NONE' 500 self._compname = b'not compressed' 501 502 def _readmark(self, chunk): 503 nmarkers = _read_short(chunk) 504 # Some files appear to contain invalid counts. 505 # Cope with this by testing for EOF. 506 try: 507 for i in range(nmarkers): 508 id = _read_short(chunk) 509 pos = _read_long(chunk) 510 name = _read_string(chunk) 511 if pos or name: 512 # some files appear to have 513 # dummy markers consisting of 514 # a position 0 and name '' 515 self._markers.append((id, pos, name)) 516 except EOFError: 517 w = ('Warning: MARK chunk contains only %s marker%s instead of %s' % 518 (len(self._markers), '' if len(self._markers) == 1 else 's', 519 nmarkers)) 520 warnings.warn(w) 521 522 class Aifc_write: 523 # Variables used in this class: 524 # 525 # These variables are user settable through appropriate methods 526 # of this class: 527 # _file -- the open file with methods write(), close(), tell(), seek() 528 # set through the __init__() method 529 # _comptype -- the AIFF-C compression type ('NONE' in AIFF) 530 # set through the setcomptype() or setparams() method 531 # _compname -- the human-readable AIFF-C compression type 532 # set through the setcomptype() or setparams() method 533 # _nchannels -- the number of audio channels 534 # set through the setnchannels() or setparams() method 535 # _sampwidth -- the number of bytes per audio sample 536 # set through the setsampwidth() or setparams() method 537 # _framerate -- the sampling frequency 538 # set through the setframerate() or setparams() method 539 # _nframes -- the number of audio frames written to the header 540 # set through the setnframes() or setparams() method 541 # _aifc -- whether we're writing an AIFF-C file or an AIFF file 542 # set through the aifc() method, reset through the 543 # aiff() method 544 # 545 # These variables are used internally only: 546 # _version -- the AIFF-C version number 547 # _comp -- the compressor from builtin module cl 548 # _nframeswritten -- the number of audio frames actually written 549 # _datalength -- the size of the audio samples written to the header 550 # _datawritten -- the size of the audio samples actually written 551 552 _file = None # Set here since __del__ checks it 553 554 def __init__(self, f): 555 if isinstance(f, str): 556 file_object = builtins.open(f, 'wb') 557 try: 558 self.initfp(file_object) 559 except: 560 file_object.close() 561 raise 562 563 # treat .aiff file extensions as non-compressed audio 564 if f.endswith('.aiff'): 565 self._aifc = 0 566 else: 567 # assume it is an open file object already 568 self.initfp(f) 569 570 def initfp(self, file): 571 self._file = file 572 self._version = _AIFC_version 573 self._comptype = b'NONE' 574 self._compname = b'not compressed' 575 self._convert = None 576 self._nchannels = 0 577 self._sampwidth = 0 578 self._framerate = 0 579 self._nframes = 0 580 self._nframeswritten = 0 581 self._datawritten = 0 582 self._datalength = 0 583 self._markers = [] 584 self._marklength = 0 585 self._aifc = 1 # AIFF-C is default 586 587 def __del__(self): 588 self.close() 589 590 def __enter__(self): 591 return self 592 593 def __exit__(self, *args): 594 self.close() 595 596 # 597 # User visible methods. 598 # 599 def aiff(self): 600 if self._nframeswritten: 601 raise Error('cannot change parameters after starting to write') 602 self._aifc = 0 603 604 def aifc(self): 605 if self._nframeswritten: 606 raise Error('cannot change parameters after starting to write') 607 self._aifc = 1 608 609 def setnchannels(self, nchannels): 610 if self._nframeswritten: 611 raise Error('cannot change parameters after starting to write') 612 if nchannels < 1: 613 raise Error('bad # of channels') 614 self._nchannels = nchannels 615 616 def getnchannels(self): 617 if not self._nchannels: 618 raise Error('number of channels not set') 619 return self._nchannels 620 621 def setsampwidth(self, sampwidth): 622 if self._nframeswritten: 623 raise Error('cannot change parameters after starting to write') 624 if sampwidth < 1 or sampwidth > 4: 625 raise Error('bad sample width') 626 self._sampwidth = sampwidth 627 628 def getsampwidth(self): 629 if not self._sampwidth: 630 raise Error('sample width not set') 631 return self._sampwidth 632 633 def setframerate(self, framerate): 634 if self._nframeswritten: 635 raise Error('cannot change parameters after starting to write') 636 if framerate <= 0: 637 raise Error('bad frame rate') 638 self._framerate = framerate 639 640 def getframerate(self): 641 if not self._framerate: 642 raise Error('frame rate not set') 643 return self._framerate 644 645 def setnframes(self, nframes): 646 if self._nframeswritten: 647 raise Error('cannot change parameters after starting to write') 648 self._nframes = nframes 649 650 def getnframes(self): 651 return self._nframeswritten 652 653 def setcomptype(self, comptype, compname): 654 if self._nframeswritten: 655 raise Error('cannot change parameters after starting to write') 656 if comptype not in (b'NONE', b'ulaw', b'ULAW', 657 b'alaw', b'ALAW', b'G722'): 658 raise Error('unsupported compression type') 659 self._comptype = comptype 660 self._compname = compname 661 662 def getcomptype(self): 663 return self._comptype 664 665 def getcompname(self): 666 return self._compname 667 668 ## def setversion(self, version): 669 ## if self._nframeswritten: 670 ## raise Error, 'cannot change parameters after starting to write' 671 ## self._version = version 672 673 def setparams(self, params): 674 nchannels, sampwidth, framerate, nframes, comptype, compname = params 675 if self._nframeswritten: 676 raise Error('cannot change parameters after starting to write') 677 if comptype not in (b'NONE', b'ulaw', b'ULAW', 678 b'alaw', b'ALAW', b'G722'): 679 raise Error('unsupported compression type') 680 self.setnchannels(nchannels) 681 self.setsampwidth(sampwidth) 682 self.setframerate(framerate) 683 self.setnframes(nframes) 684 self.setcomptype(comptype, compname) 685 686 def getparams(self): 687 if not self._nchannels or not self._sampwidth or not self._framerate: 688 raise Error('not all parameters set') 689 return _aifc_params(self._nchannels, self._sampwidth, self._framerate, 690 self._nframes, self._comptype, self._compname) 691 692 def setmark(self, id, pos, name): 693 if id <= 0: 694 raise Error('marker ID must be > 0') 695 if pos < 0: 696 raise Error('marker position must be >= 0') 697 if not isinstance(name, bytes): 698 raise Error('marker name must be bytes') 699 for i in range(len(self._markers)): 700 if id == self._markers[i][0]: 701 self._markers[i] = id, pos, name 702 return 703 self._markers.append((id, pos, name)) 704 705 def getmark(self, id): 706 for marker in self._markers: 707 if id == marker[0]: 708 return marker 709 raise Error('marker {0!r} does not exist'.format(id)) 710 711 def getmarkers(self): 712 if len(self._markers) == 0: 713 return None 714 return self._markers 715 716 def tell(self): 717 return self._nframeswritten 718 719 def writeframesraw(self, data): 720 if not isinstance(data, (bytes, bytearray)): 721 data = memoryview(data).cast('B') 722 self._ensure_header_written(len(data)) 723 nframes = len(data) // (self._sampwidth * self._nchannels) 724 if self._convert: 725 data = self._convert(data) 726 self._file.write(data) 727 self._nframeswritten = self._nframeswritten + nframes 728 self._datawritten = self._datawritten + len(data) 729 730 def writeframes(self, data): 731 self.writeframesraw(data) 732 if self._nframeswritten != self._nframes or \ 733 self._datalength != self._datawritten: 734 self._patchheader() 735 736 def close(self): 737 if self._file is None: 738 return 739 try: 740 self._ensure_header_written(0) 741 if self._datawritten & 1: 742 # quick pad to even size 743 self._file.write(b'\x00') 744 self._datawritten = self._datawritten + 1 745 self._writemarkers() 746 if self._nframeswritten != self._nframes or \ 747 self._datalength != self._datawritten or \ 748 self._marklength: 749 self._patchheader() 750 finally: 751 # Prevent ref cycles 752 self._convert = None 753 f = self._file 754 self._file = None 755 f.close() 756 757 # 758 # Internal methods. 759 # 760 761 def _lin2alaw(self, data): 762 import audioop 763 return audioop.lin2alaw(data, 2) 764 765 def _lin2ulaw(self, data): 766 import audioop 767 return audioop.lin2ulaw(data, 2) 768 769 def _lin2adpcm(self, data): 770 import audioop 771 if not hasattr(self, '_adpcmstate'): 772 self._adpcmstate = None 773 data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate) 774 return data 775 776 def _ensure_header_written(self, datasize): 777 if not self._nframeswritten: 778 if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'): 779 if not self._sampwidth: 780 self._sampwidth = 2 781 if self._sampwidth != 2: 782 raise Error('sample width must be 2 when compressing ' 783 'with ulaw/ULAW, alaw/ALAW or G7.22 (ADPCM)') 784 if not self._nchannels: 785 raise Error('# channels not specified') 786 if not self._sampwidth: 787 raise Error('sample width not specified') 788 if not self._framerate: 789 raise Error('sampling rate not specified') 790 self._write_header(datasize) 791 792 def _init_compression(self): 793 if self._comptype == b'G722': 794 self._convert = self._lin2adpcm 795 elif self._comptype in (b'ulaw', b'ULAW'): 796 self._convert = self._lin2ulaw 797 elif self._comptype in (b'alaw', b'ALAW'): 798 self._convert = self._lin2alaw 799 800 def _write_header(self, initlength): 801 if self._aifc and self._comptype != b'NONE': 802 self._init_compression() 803 self._file.write(b'FORM') 804 if not self._nframes: 805 self._nframes = initlength // (self._nchannels * self._sampwidth) 806 self._datalength = self._nframes * self._nchannels * self._sampwidth 807 if self._datalength & 1: 808 self._datalength = self._datalength + 1 809 if self._aifc: 810 if self._comptype in (b'ulaw', b'ULAW', b'alaw', b'ALAW'): 811 self._datalength = self._datalength // 2 812 if self._datalength & 1: 813 self._datalength = self._datalength + 1 814 elif self._comptype == b'G722': 815 self._datalength = (self._datalength + 3) // 4 816 if self._datalength & 1: 817 self._datalength = self._datalength + 1 818 try: 819 self._form_length_pos = self._file.tell() 820 except (AttributeError, OSError): 821 self._form_length_pos = None 822 commlength = self._write_form_length(self._datalength) 823 if self._aifc: 824 self._file.write(b'AIFC') 825 self._file.write(b'FVER') 826 _write_ulong(self._file, 4) 827 _write_ulong(self._file, self._version) 828 else: 829 self._file.write(b'AIFF') 830 self._file.write(b'COMM') 831 _write_ulong(self._file, commlength) 832 _write_short(self._file, self._nchannels) 833 if self._form_length_pos is not None: 834 self._nframes_pos = self._file.tell() 835 _write_ulong(self._file, self._nframes) 836 if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'): 837 _write_short(self._file, 8) 838 else: 839 _write_short(self._file, self._sampwidth * 8) 840 _write_float(self._file, self._framerate) 841 if self._aifc: 842 self._file.write(self._comptype) 843 _write_string(self._file, self._compname) 844 self._file.write(b'SSND') 845 if self._form_length_pos is not None: 846 self._ssnd_length_pos = self._file.tell() 847 _write_ulong(self._file, self._datalength + 8) 848 _write_ulong(self._file, 0) 849 _write_ulong(self._file, 0) 850 851 def _write_form_length(self, datalength): 852 if self._aifc: 853 commlength = 18 + 5 + len(self._compname) 854 if commlength & 1: 855 commlength = commlength + 1 856 verslength = 12 857 else: 858 commlength = 18 859 verslength = 0 860 _write_ulong(self._file, 4 + verslength + self._marklength + \ 861 8 + commlength + 16 + datalength) 862 return commlength 863 864 def _patchheader(self): 865 curpos = self._file.tell() 866 if self._datawritten & 1: 867 datalength = self._datawritten + 1 868 self._file.write(b'\x00') 869 else: 870 datalength = self._datawritten 871 if datalength == self._datalength and \ 872 self._nframes == self._nframeswritten and \ 873 self._marklength == 0: 874 self._file.seek(curpos, 0) 875 return 876 self._file.seek(self._form_length_pos, 0) 877 dummy = self._write_form_length(datalength) 878 self._file.seek(self._nframes_pos, 0) 879 _write_ulong(self._file, self._nframeswritten) 880 self._file.seek(self._ssnd_length_pos, 0) 881 _write_ulong(self._file, datalength + 8) 882 self._file.seek(curpos, 0) 883 self._nframes = self._nframeswritten 884 self._datalength = datalength 885 886 def _writemarkers(self): 887 if len(self._markers) == 0: 888 return 889 self._file.write(b'MARK') 890 length = 2 891 for marker in self._markers: 892 id, pos, name = marker 893 length = length + len(name) + 1 + 6 894 if len(name) & 1 == 0: 895 length = length + 1 896 _write_ulong(self._file, length) 897 self._marklength = length + 8 898 _write_short(self._file, len(self._markers)) 899 for marker in self._markers: 900 id, pos, name = marker 901 _write_short(self._file, id) 902 _write_ulong(self._file, pos) 903 _write_string(self._file, name) 904 905 def open(f, mode=None): 906 if mode is None: 907 if hasattr(f, 'mode'): 908 mode = f.mode 909 else: 910 mode = 'rb' 911 if mode in ('r', 'rb'): 912 return Aifc_read(f) 913 elif mode in ('w', 'wb'): 914 return Aifc_write(f) 915 else: 916 raise Error("mode must be 'r', 'rb', 'w', or 'wb'") 917 918 openfp = open # B/W compatibility 919 920 if __name__ == '__main__': 921 import sys 922 if not sys.argv[1:]: 923 sys.argv.append('/usr/demos/data/audio/bach.aiff') 924 fn = sys.argv[1] 925 with open(fn, 'r') as f: 926 print("Reading", fn) 927 print("nchannels =", f.getnchannels()) 928 print("nframes =", f.getnframes()) 929 print("sampwidth =", f.getsampwidth()) 930 print("framerate =", f.getframerate()) 931 print("comptype =", f.getcomptype()) 932 print("compname =", f.getcompname()) 933 if sys.argv[2:]: 934 gn = sys.argv[2] 935 print("Writing", gn) 936 with open(gn, 'w') as g: 937 g.setparams(f.getparams()) 938 while 1: 939 data = f.readframes(1024) 940 if not data: 941 break 942 g.writeframes(data) 943 print("Done.") 944