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