1"""Stuff to parse AIFF-C and AIFF files. 2 3Unless explicitly stated otherwise, the description below is true 4both for AIFF-C files and AIFF files. 5 6An 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 21An AIFF file has the string "AIFF" instead of "AIFC". 22 23A chunk consists of an identifier (4 bytes) followed by a size (4 bytes, 24big endian order), followed by the data. The size field does not include 25the size of the 8 byte header. 26 27The 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 51A pstring consists of 1 byte length, a string of characters, and 0 or 1 52byte pad to make the total length even. 53 54Usage. 55 56Reading AIFF files: 57 f = aifc.open(file, 'r') 58where file is either the name of a file or an open file pointer. 59The open file pointer must have methods read(), seek(), and close(). 60In some types of audio files, if the setpos() method is not used, 61the seek() method is not necessary. 62 63This 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) 83The position returned by tell(), the position given to setpos() and 84the position of marks are all compatible and have nothing to do with 85the actual position in the file. 86The close() method is called automatically when the class instance 87is destroyed. 88 89Writing AIFF files: 90 f = aifc.open(file, 'w') 91where file is either the name of a file or an open file pointer. 92The open file pointer must have methods write(), tell(), seek(), and 93close(). 94 95This 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 118You should set the parameters before the first writeframesraw or 119writeframes. The total number of frames does not need to be set, 120but when it is set to the correct value, the header does not have to 121be patched up. 122It is best to first set all parameters, perhaps possibly the 123compression type, and then write audio frames using writeframesraw. 124When all frames have been written, either call writeframes(b'') or 125close() to patch up the sizes in the header. 126Marks can be added anytime. If there are any marks, you must call 127close() after all frames have been written. 128The close() method is called automatically when the class instance 129is destroyed. 130 131When a file is opened with the extension '.aiff', an AIFF file is 132written, otherwise an AIFF-C file is written. This default can be 133changed by calling aiff() or aifc() before the first writeframes or 134writeframesraw. 135""" 136 137import struct 138import builtins 139import warnings 140 141__all__ = ["Error", "open", "openfp"] 142 143class Error(Exception): 144 pass 145 146_AIFC_version = 0xA2805140 # Version 1 of AIFF-C 147 148def _read_long(file): 149 try: 150 return struct.unpack('>l', file.read(4))[0] 151 except struct.error: 152 raise EOFError from None 153 154def _read_ulong(file): 155 try: 156 return struct.unpack('>L', file.read(4))[0] 157 except struct.error: 158 raise EOFError from None 159 160def _read_short(file): 161 try: 162 return struct.unpack('>h', file.read(2))[0] 163 except struct.error: 164 raise EOFError from None 165 166def _read_ushort(file): 167 try: 168 return struct.unpack('>H', file.read(2))[0] 169 except struct.error: 170 raise EOFError from None 171 172def _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 184def _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 201def _write_short(f, x): 202 f.write(struct.pack('>h', x)) 203 204def _write_ushort(f, x): 205 f.write(struct.pack('>H', x)) 206 207def _write_long(f, x): 208 f.write(struct.pack('>l', x)) 209 210def _write_ulong(f, x): 211 f.write(struct.pack('>L', x)) 212 213def _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 221def _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 254from chunk import Chunk 255from 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__ = ("""\ 266A human-readable version of the compression type 267('not compressed' for AIFF files)""") 268 269 270class 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 self._ssnd_chunk = None 326 while 1: 327 self._ssnd_seek_needed = 1 328 try: 329 chunk = Chunk(self._file) 330 except EOFError: 331 break 332 chunkname = chunk.getname() 333 if chunkname == b'COMM': 334 self._read_comm_chunk(chunk) 335 self._comm_chunk_read = 1 336 elif chunkname == b'SSND': 337 self._ssnd_chunk = chunk 338 dummy = chunk.read(8) 339 self._ssnd_seek_needed = 0 340 elif chunkname == b'FVER': 341 self._version = _read_ulong(chunk) 342 elif chunkname == b'MARK': 343 self._readmark(chunk) 344 chunk.skip() 345 if not self._comm_chunk_read or not self._ssnd_chunk: 346 raise Error('COMM chunk and/or SSND chunk missing') 347 348 def __init__(self, f): 349 if isinstance(f, str): 350 file_object = builtins.open(f, 'rb') 351 try: 352 self.initfp(file_object) 353 except: 354 file_object.close() 355 raise 356 else: 357 # assume it is an open file object already 358 self.initfp(f) 359 360 def __enter__(self): 361 return self 362 363 def __exit__(self, *args): 364 self.close() 365 366 # 367 # User visible methods. 368 # 369 def getfp(self): 370 return self._file 371 372 def rewind(self): 373 self._ssnd_seek_needed = 1 374 self._soundpos = 0 375 376 def close(self): 377 file = self._file 378 if file is not None: 379 self._file = None 380 file.close() 381 382 def tell(self): 383 return self._soundpos 384 385 def getnchannels(self): 386 return self._nchannels 387 388 def getnframes(self): 389 return self._nframes 390 391 def getsampwidth(self): 392 return self._sampwidth 393 394 def getframerate(self): 395 return self._framerate 396 397 def getcomptype(self): 398 return self._comptype 399 400 def getcompname(self): 401 return self._compname 402 403## def getversion(self): 404## return self._version 405 406 def getparams(self): 407 return _aifc_params(self.getnchannels(), self.getsampwidth(), 408 self.getframerate(), self.getnframes(), 409 self.getcomptype(), self.getcompname()) 410 411 def getmarkers(self): 412 if len(self._markers) == 0: 413 return None 414 return self._markers 415 416 def getmark(self, id): 417 for marker in self._markers: 418 if id == marker[0]: 419 return marker 420 raise Error('marker {0!r} does not exist'.format(id)) 421 422 def setpos(self, pos): 423 if pos < 0 or pos > self._nframes: 424 raise Error('position not in range') 425 self._soundpos = pos 426 self._ssnd_seek_needed = 1 427 428 def readframes(self, nframes): 429 if self._ssnd_seek_needed: 430 self._ssnd_chunk.seek(0) 431 dummy = self._ssnd_chunk.read(8) 432 pos = self._soundpos * self._framesize 433 if pos: 434 self._ssnd_chunk.seek(pos + 8) 435 self._ssnd_seek_needed = 0 436 if nframes == 0: 437 return b'' 438 data = self._ssnd_chunk.read(nframes * self._framesize) 439 if self._convert and data: 440 data = self._convert(data) 441 self._soundpos = self._soundpos + len(data) // (self._nchannels 442 * self._sampwidth) 443 return data 444 445 # 446 # Internal methods. 447 # 448 449 def _alaw2lin(self, data): 450 import audioop 451 return audioop.alaw2lin(data, 2) 452 453 def _ulaw2lin(self, data): 454 import audioop 455 return audioop.ulaw2lin(data, 2) 456 457 def _adpcm2lin(self, data): 458 import audioop 459 if not hasattr(self, '_adpcmstate'): 460 # first time 461 self._adpcmstate = None 462 data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate) 463 return data 464 465 def _read_comm_chunk(self, chunk): 466 self._nchannels = _read_short(chunk) 467 self._nframes = _read_long(chunk) 468 self._sampwidth = (_read_short(chunk) + 7) // 8 469 self._framerate = int(_read_float(chunk)) 470 if self._sampwidth <= 0: 471 raise Error('bad sample width') 472 if self._nchannels <= 0: 473 raise Error('bad # of channels') 474 self._framesize = self._nchannels * self._sampwidth 475 if self._aifc: 476 #DEBUG: SGI's soundeditor produces a bad size :-( 477 kludge = 0 478 if chunk.chunksize == 18: 479 kludge = 1 480 warnings.warn('Warning: bad COMM chunk size') 481 chunk.chunksize = 23 482 #DEBUG end 483 self._comptype = chunk.read(4) 484 #DEBUG start 485 if kludge: 486 length = ord(chunk.file.read(1)) 487 if length & 1 == 0: 488 length = length + 1 489 chunk.chunksize = chunk.chunksize + length 490 chunk.file.seek(-1, 1) 491 #DEBUG end 492 self._compname = _read_string(chunk) 493 if self._comptype != b'NONE': 494 if self._comptype == b'G722': 495 self._convert = self._adpcm2lin 496 elif self._comptype in (b'ulaw', b'ULAW'): 497 self._convert = self._ulaw2lin 498 elif self._comptype in (b'alaw', b'ALAW'): 499 self._convert = self._alaw2lin 500 else: 501 raise Error('unsupported compression type') 502 self._sampwidth = 2 503 else: 504 self._comptype = b'NONE' 505 self._compname = b'not compressed' 506 507 def _readmark(self, chunk): 508 nmarkers = _read_short(chunk) 509 # Some files appear to contain invalid counts. 510 # Cope with this by testing for EOF. 511 try: 512 for i in range(nmarkers): 513 id = _read_short(chunk) 514 pos = _read_long(chunk) 515 name = _read_string(chunk) 516 if pos or name: 517 # some files appear to have 518 # dummy markers consisting of 519 # a position 0 and name '' 520 self._markers.append((id, pos, name)) 521 except EOFError: 522 w = ('Warning: MARK chunk contains only %s marker%s instead of %s' % 523 (len(self._markers), '' if len(self._markers) == 1 else 's', 524 nmarkers)) 525 warnings.warn(w) 526 527class Aifc_write: 528 # Variables used in this class: 529 # 530 # These variables are user settable through appropriate methods 531 # of this class: 532 # _file -- the open file with methods write(), close(), tell(), seek() 533 # set through the __init__() method 534 # _comptype -- the AIFF-C compression type ('NONE' in AIFF) 535 # set through the setcomptype() or setparams() method 536 # _compname -- the human-readable AIFF-C compression type 537 # set through the setcomptype() or setparams() method 538 # _nchannels -- the number of audio channels 539 # set through the setnchannels() or setparams() method 540 # _sampwidth -- the number of bytes per audio sample 541 # set through the setsampwidth() or setparams() method 542 # _framerate -- the sampling frequency 543 # set through the setframerate() or setparams() method 544 # _nframes -- the number of audio frames written to the header 545 # set through the setnframes() or setparams() method 546 # _aifc -- whether we're writing an AIFF-C file or an AIFF file 547 # set through the aifc() method, reset through the 548 # aiff() method 549 # 550 # These variables are used internally only: 551 # _version -- the AIFF-C version number 552 # _comp -- the compressor from builtin module cl 553 # _nframeswritten -- the number of audio frames actually written 554 # _datalength -- the size of the audio samples written to the header 555 # _datawritten -- the size of the audio samples actually written 556 557 _file = None # Set here since __del__ checks it 558 559 def __init__(self, f): 560 if isinstance(f, str): 561 file_object = builtins.open(f, 'wb') 562 try: 563 self.initfp(file_object) 564 except: 565 file_object.close() 566 raise 567 568 # treat .aiff file extensions as non-compressed audio 569 if f.endswith('.aiff'): 570 self._aifc = 0 571 else: 572 # assume it is an open file object already 573 self.initfp(f) 574 575 def initfp(self, file): 576 self._file = file 577 self._version = _AIFC_version 578 self._comptype = b'NONE' 579 self._compname = b'not compressed' 580 self._convert = None 581 self._nchannels = 0 582 self._sampwidth = 0 583 self._framerate = 0 584 self._nframes = 0 585 self._nframeswritten = 0 586 self._datawritten = 0 587 self._datalength = 0 588 self._markers = [] 589 self._marklength = 0 590 self._aifc = 1 # AIFF-C is default 591 592 def __del__(self): 593 self.close() 594 595 def __enter__(self): 596 return self 597 598 def __exit__(self, *args): 599 self.close() 600 601 # 602 # User visible methods. 603 # 604 def aiff(self): 605 if self._nframeswritten: 606 raise Error('cannot change parameters after starting to write') 607 self._aifc = 0 608 609 def aifc(self): 610 if self._nframeswritten: 611 raise Error('cannot change parameters after starting to write') 612 self._aifc = 1 613 614 def setnchannels(self, nchannels): 615 if self._nframeswritten: 616 raise Error('cannot change parameters after starting to write') 617 if nchannels < 1: 618 raise Error('bad # of channels') 619 self._nchannels = nchannels 620 621 def getnchannels(self): 622 if not self._nchannels: 623 raise Error('number of channels not set') 624 return self._nchannels 625 626 def setsampwidth(self, sampwidth): 627 if self._nframeswritten: 628 raise Error('cannot change parameters after starting to write') 629 if sampwidth < 1 or sampwidth > 4: 630 raise Error('bad sample width') 631 self._sampwidth = sampwidth 632 633 def getsampwidth(self): 634 if not self._sampwidth: 635 raise Error('sample width not set') 636 return self._sampwidth 637 638 def setframerate(self, framerate): 639 if self._nframeswritten: 640 raise Error('cannot change parameters after starting to write') 641 if framerate <= 0: 642 raise Error('bad frame rate') 643 self._framerate = framerate 644 645 def getframerate(self): 646 if not self._framerate: 647 raise Error('frame rate not set') 648 return self._framerate 649 650 def setnframes(self, nframes): 651 if self._nframeswritten: 652 raise Error('cannot change parameters after starting to write') 653 self._nframes = nframes 654 655 def getnframes(self): 656 return self._nframeswritten 657 658 def setcomptype(self, comptype, compname): 659 if self._nframeswritten: 660 raise Error('cannot change parameters after starting to write') 661 if comptype not in (b'NONE', b'ulaw', b'ULAW', 662 b'alaw', b'ALAW', b'G722'): 663 raise Error('unsupported compression type') 664 self._comptype = comptype 665 self._compname = compname 666 667 def getcomptype(self): 668 return self._comptype 669 670 def getcompname(self): 671 return self._compname 672 673## def setversion(self, version): 674## if self._nframeswritten: 675## raise Error, 'cannot change parameters after starting to write' 676## self._version = version 677 678 def setparams(self, params): 679 nchannels, sampwidth, framerate, nframes, comptype, compname = params 680 if self._nframeswritten: 681 raise Error('cannot change parameters after starting to write') 682 if comptype not in (b'NONE', b'ulaw', b'ULAW', 683 b'alaw', b'ALAW', b'G722'): 684 raise Error('unsupported compression type') 685 self.setnchannels(nchannels) 686 self.setsampwidth(sampwidth) 687 self.setframerate(framerate) 688 self.setnframes(nframes) 689 self.setcomptype(comptype, compname) 690 691 def getparams(self): 692 if not self._nchannels or not self._sampwidth or not self._framerate: 693 raise Error('not all parameters set') 694 return _aifc_params(self._nchannels, self._sampwidth, self._framerate, 695 self._nframes, self._comptype, self._compname) 696 697 def setmark(self, id, pos, name): 698 if id <= 0: 699 raise Error('marker ID must be > 0') 700 if pos < 0: 701 raise Error('marker position must be >= 0') 702 if not isinstance(name, bytes): 703 raise Error('marker name must be bytes') 704 for i in range(len(self._markers)): 705 if id == self._markers[i][0]: 706 self._markers[i] = id, pos, name 707 return 708 self._markers.append((id, pos, name)) 709 710 def getmark(self, id): 711 for marker in self._markers: 712 if id == marker[0]: 713 return marker 714 raise Error('marker {0!r} does not exist'.format(id)) 715 716 def getmarkers(self): 717 if len(self._markers) == 0: 718 return None 719 return self._markers 720 721 def tell(self): 722 return self._nframeswritten 723 724 def writeframesraw(self, data): 725 if not isinstance(data, (bytes, bytearray)): 726 data = memoryview(data).cast('B') 727 self._ensure_header_written(len(data)) 728 nframes = len(data) // (self._sampwidth * self._nchannels) 729 if self._convert: 730 data = self._convert(data) 731 self._file.write(data) 732 self._nframeswritten = self._nframeswritten + nframes 733 self._datawritten = self._datawritten + len(data) 734 735 def writeframes(self, data): 736 self.writeframesraw(data) 737 if self._nframeswritten != self._nframes or \ 738 self._datalength != self._datawritten: 739 self._patchheader() 740 741 def close(self): 742 if self._file is None: 743 return 744 try: 745 self._ensure_header_written(0) 746 if self._datawritten & 1: 747 # quick pad to even size 748 self._file.write(b'\x00') 749 self._datawritten = self._datawritten + 1 750 self._writemarkers() 751 if self._nframeswritten != self._nframes or \ 752 self._datalength != self._datawritten or \ 753 self._marklength: 754 self._patchheader() 755 finally: 756 # Prevent ref cycles 757 self._convert = None 758 f = self._file 759 self._file = None 760 f.close() 761 762 # 763 # Internal methods. 764 # 765 766 def _lin2alaw(self, data): 767 import audioop 768 return audioop.lin2alaw(data, 2) 769 770 def _lin2ulaw(self, data): 771 import audioop 772 return audioop.lin2ulaw(data, 2) 773 774 def _lin2adpcm(self, data): 775 import audioop 776 if not hasattr(self, '_adpcmstate'): 777 self._adpcmstate = None 778 data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate) 779 return data 780 781 def _ensure_header_written(self, datasize): 782 if not self._nframeswritten: 783 if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'): 784 if not self._sampwidth: 785 self._sampwidth = 2 786 if self._sampwidth != 2: 787 raise Error('sample width must be 2 when compressing ' 788 'with ulaw/ULAW, alaw/ALAW or G7.22 (ADPCM)') 789 if not self._nchannels: 790 raise Error('# channels not specified') 791 if not self._sampwidth: 792 raise Error('sample width not specified') 793 if not self._framerate: 794 raise Error('sampling rate not specified') 795 self._write_header(datasize) 796 797 def _init_compression(self): 798 if self._comptype == b'G722': 799 self._convert = self._lin2adpcm 800 elif self._comptype in (b'ulaw', b'ULAW'): 801 self._convert = self._lin2ulaw 802 elif self._comptype in (b'alaw', b'ALAW'): 803 self._convert = self._lin2alaw 804 805 def _write_header(self, initlength): 806 if self._aifc and self._comptype != b'NONE': 807 self._init_compression() 808 self._file.write(b'FORM') 809 if not self._nframes: 810 self._nframes = initlength // (self._nchannels * self._sampwidth) 811 self._datalength = self._nframes * self._nchannels * self._sampwidth 812 if self._datalength & 1: 813 self._datalength = self._datalength + 1 814 if self._aifc: 815 if self._comptype in (b'ulaw', b'ULAW', b'alaw', b'ALAW'): 816 self._datalength = self._datalength // 2 817 if self._datalength & 1: 818 self._datalength = self._datalength + 1 819 elif self._comptype == b'G722': 820 self._datalength = (self._datalength + 3) // 4 821 if self._datalength & 1: 822 self._datalength = self._datalength + 1 823 try: 824 self._form_length_pos = self._file.tell() 825 except (AttributeError, OSError): 826 self._form_length_pos = None 827 commlength = self._write_form_length(self._datalength) 828 if self._aifc: 829 self._file.write(b'AIFC') 830 self._file.write(b'FVER') 831 _write_ulong(self._file, 4) 832 _write_ulong(self._file, self._version) 833 else: 834 self._file.write(b'AIFF') 835 self._file.write(b'COMM') 836 _write_ulong(self._file, commlength) 837 _write_short(self._file, self._nchannels) 838 if self._form_length_pos is not None: 839 self._nframes_pos = self._file.tell() 840 _write_ulong(self._file, self._nframes) 841 if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'): 842 _write_short(self._file, 8) 843 else: 844 _write_short(self._file, self._sampwidth * 8) 845 _write_float(self._file, self._framerate) 846 if self._aifc: 847 self._file.write(self._comptype) 848 _write_string(self._file, self._compname) 849 self._file.write(b'SSND') 850 if self._form_length_pos is not None: 851 self._ssnd_length_pos = self._file.tell() 852 _write_ulong(self._file, self._datalength + 8) 853 _write_ulong(self._file, 0) 854 _write_ulong(self._file, 0) 855 856 def _write_form_length(self, datalength): 857 if self._aifc: 858 commlength = 18 + 5 + len(self._compname) 859 if commlength & 1: 860 commlength = commlength + 1 861 verslength = 12 862 else: 863 commlength = 18 864 verslength = 0 865 _write_ulong(self._file, 4 + verslength + self._marklength + \ 866 8 + commlength + 16 + datalength) 867 return commlength 868 869 def _patchheader(self): 870 curpos = self._file.tell() 871 if self._datawritten & 1: 872 datalength = self._datawritten + 1 873 self._file.write(b'\x00') 874 else: 875 datalength = self._datawritten 876 if datalength == self._datalength and \ 877 self._nframes == self._nframeswritten and \ 878 self._marklength == 0: 879 self._file.seek(curpos, 0) 880 return 881 self._file.seek(self._form_length_pos, 0) 882 dummy = self._write_form_length(datalength) 883 self._file.seek(self._nframes_pos, 0) 884 _write_ulong(self._file, self._nframeswritten) 885 self._file.seek(self._ssnd_length_pos, 0) 886 _write_ulong(self._file, datalength + 8) 887 self._file.seek(curpos, 0) 888 self._nframes = self._nframeswritten 889 self._datalength = datalength 890 891 def _writemarkers(self): 892 if len(self._markers) == 0: 893 return 894 self._file.write(b'MARK') 895 length = 2 896 for marker in self._markers: 897 id, pos, name = marker 898 length = length + len(name) + 1 + 6 899 if len(name) & 1 == 0: 900 length = length + 1 901 _write_ulong(self._file, length) 902 self._marklength = length + 8 903 _write_short(self._file, len(self._markers)) 904 for marker in self._markers: 905 id, pos, name = marker 906 _write_short(self._file, id) 907 _write_ulong(self._file, pos) 908 _write_string(self._file, name) 909 910def open(f, mode=None): 911 if mode is None: 912 if hasattr(f, 'mode'): 913 mode = f.mode 914 else: 915 mode = 'rb' 916 if mode in ('r', 'rb'): 917 return Aifc_read(f) 918 elif mode in ('w', 'wb'): 919 return Aifc_write(f) 920 else: 921 raise Error("mode must be 'r', 'rb', 'w', or 'wb'") 922 923def openfp(f, mode=None): 924 warnings.warn("aifc.openfp is deprecated since Python 3.7. " 925 "Use aifc.open instead.", DeprecationWarning, stacklevel=2) 926 return open(f, mode=mode) 927 928if __name__ == '__main__': 929 import sys 930 if not sys.argv[1:]: 931 sys.argv.append('/usr/demos/data/audio/bach.aiff') 932 fn = sys.argv[1] 933 with open(fn, 'r') as f: 934 print("Reading", fn) 935 print("nchannels =", f.getnchannels()) 936 print("nframes =", f.getnframes()) 937 print("sampwidth =", f.getsampwidth()) 938 print("framerate =", f.getframerate()) 939 print("comptype =", f.getcomptype()) 940 print("compname =", f.getcompname()) 941 if sys.argv[2:]: 942 gn = sys.argv[2] 943 print("Writing", gn) 944 with open(gn, 'w') as g: 945 g.setparams(f.getparams()) 946 while 1: 947 data = f.readframes(1024) 948 if not data: 949 break 950 g.writeframes(data) 951 print("Done.") 952