123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- """Stuff to parse Sun and NeXT audio files.
- An audio file consists of a header followed by the data. The structure
- of the header is as follows.
- +---------------+
- | magic word |
- +---------------+
- | header size |
- +---------------+
- | data size |
- +---------------+
- | encoding |
- +---------------+
- | sample rate |
- +---------------+
- | # of channels |
- +---------------+
- | info |
- | |
- +---------------+
- The magic word consists of the 4 characters '.snd'. Apart from the
- info field, all header fields are 4 bytes in size. They are all
- 32-bit unsigned integers encoded in big-endian byte order.
- The header size really gives the start of the data.
- The data size is the physical size of the data. From the other
- parameters the number of frames can be calculated.
- The encoding gives the way in which audio samples are encoded.
- Possible values are listed below.
- The info field currently consists of an ASCII string giving a
- human-readable description of the audio file. The info field is
- padded with NUL bytes to the header size.
- Usage.
- Reading audio files:
- f = sunau.open(file, 'r')
- where file is either the name of a file or an open file pointer.
- The open file pointer must have methods read(), seek(), and close().
- When the setpos() and rewind() methods are not used, the seek()
- method is not necessary.
- This returns an instance of a class with the following public methods:
- getnchannels() -- returns number of audio channels (1 for
- mono, 2 for stereo)
- getsampwidth() -- returns sample width in bytes
- getframerate() -- returns sampling frequency
- getnframes() -- returns number of audio frames
- getcomptype() -- returns compression type ('NONE' or 'ULAW')
- getcompname() -- returns human-readable version of
- compression type ('not compressed' matches 'NONE')
- getparams() -- returns a namedtuple consisting of all of the
- above in the above order
- getmarkers() -- returns None (for compatibility with the
- aifc module)
- getmark(id) -- raises an error since the mark does not
- exist (for compatibility with the aifc module)
- readframes(n) -- returns at most n frames of audio
- rewind() -- rewind to the beginning of the audio stream
- setpos(pos) -- seek to the specified position
- tell() -- return the current position
- close() -- close the instance (make it unusable)
- The position returned by tell() and the position given to setpos()
- are compatible and have nothing to do with the actual position in the
- file.
- The close() method is called automatically when the class instance
- is destroyed.
- Writing audio files:
- f = sunau.open(file, 'w')
- where file is either the name of a file or an open file pointer.
- The open file pointer must have methods write(), tell(), seek(), and
- close().
- This returns an instance of a class with the following public methods:
- setnchannels(n) -- set the number of channels
- setsampwidth(n) -- set the sample width
- setframerate(n) -- set the frame rate
- setnframes(n) -- set the number of frames
- setcomptype(type, name)
- -- set the compression type and the
- human-readable compression type
- setparams(tuple)-- set all parameters at once
- tell() -- return current position in output file
- writeframesraw(data)
- -- write audio frames without pathing up the
- file header
- writeframes(data)
- -- write audio frames and patch up the file header
- close() -- patch up the file header and close the
- output file
- You should set the parameters before the first writeframesraw or
- writeframes. The total number of frames does not need to be set,
- but when it is set to the correct value, the header does not have to
- be patched up.
- It is best to first set all parameters, perhaps possibly the
- compression type, and then write audio frames using writeframesraw.
- When all frames have been written, either call writeframes(b'') or
- close() to patch up the sizes in the header.
- The close() method is called automatically when the class instance
- is destroyed.
- """
- from collections import namedtuple
- _sunau_params = namedtuple('_sunau_params',
- 'nchannels sampwidth framerate nframes comptype compname')
- # from <multimedia/audio_filehdr.h>
- AUDIO_FILE_MAGIC = 0x2e736e64
- AUDIO_FILE_ENCODING_MULAW_8 = 1
- AUDIO_FILE_ENCODING_LINEAR_8 = 2
- AUDIO_FILE_ENCODING_LINEAR_16 = 3
- AUDIO_FILE_ENCODING_LINEAR_24 = 4
- AUDIO_FILE_ENCODING_LINEAR_32 = 5
- AUDIO_FILE_ENCODING_FLOAT = 6
- AUDIO_FILE_ENCODING_DOUBLE = 7
- AUDIO_FILE_ENCODING_ADPCM_G721 = 23
- AUDIO_FILE_ENCODING_ADPCM_G722 = 24
- AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25
- AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26
- AUDIO_FILE_ENCODING_ALAW_8 = 27
- # from <multimedia/audio_hdr.h>
- AUDIO_UNKNOWN_SIZE = 0xFFFFFFFF # ((unsigned)(~0))
- _simple_encodings = [AUDIO_FILE_ENCODING_MULAW_8,
- AUDIO_FILE_ENCODING_LINEAR_8,
- AUDIO_FILE_ENCODING_LINEAR_16,
- AUDIO_FILE_ENCODING_LINEAR_24,
- AUDIO_FILE_ENCODING_LINEAR_32,
- AUDIO_FILE_ENCODING_ALAW_8]
- class Error(Exception):
- pass
- def _read_u32(file):
- x = 0
- for i in range(4):
- byte = file.read(1)
- if not byte:
- raise EOFError
- x = x*256 + ord(byte)
- return x
- def _write_u32(file, x):
- data = []
- for i in range(4):
- d, m = divmod(x, 256)
- data.insert(0, int(m))
- x = d
- file.write(bytes(data))
- class Au_read:
- def __init__(self, f):
- if type(f) == type(''):
- import builtins
- f = builtins.open(f, 'rb')
- self._opened = True
- else:
- self._opened = False
- self.initfp(f)
- def __del__(self):
- if self._file:
- self.close()
- def __enter__(self):
- return self
- def __exit__(self, *args):
- self.close()
- def initfp(self, file):
- self._file = file
- self._soundpos = 0
- magic = int(_read_u32(file))
- if magic != AUDIO_FILE_MAGIC:
- raise Error('bad magic number')
- self._hdr_size = int(_read_u32(file))
- if self._hdr_size < 24:
- raise Error('header size too small')
- if self._hdr_size > 100:
- raise Error('header size ridiculously large')
- self._data_size = _read_u32(file)
- if self._data_size != AUDIO_UNKNOWN_SIZE:
- self._data_size = int(self._data_size)
- self._encoding = int(_read_u32(file))
- if self._encoding not in _simple_encodings:
- raise Error('encoding not (yet) supported')
- if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8,
- AUDIO_FILE_ENCODING_ALAW_8):
- self._sampwidth = 2
- self._framesize = 1
- elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_8:
- self._framesize = self._sampwidth = 1
- elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16:
- self._framesize = self._sampwidth = 2
- elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24:
- self._framesize = self._sampwidth = 3
- elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32:
- self._framesize = self._sampwidth = 4
- else:
- raise Error('unknown encoding')
- self._framerate = int(_read_u32(file))
- self._nchannels = int(_read_u32(file))
- if not self._nchannels:
- raise Error('bad # of channels')
- self._framesize = self._framesize * self._nchannels
- if self._hdr_size > 24:
- self._info = file.read(self._hdr_size - 24)
- self._info, _, _ = self._info.partition(b'\0')
- else:
- self._info = b''
- try:
- self._data_pos = file.tell()
- except (AttributeError, OSError):
- self._data_pos = None
- def getfp(self):
- return self._file
- def getnchannels(self):
- return self._nchannels
- def getsampwidth(self):
- return self._sampwidth
- def getframerate(self):
- return self._framerate
- def getnframes(self):
- if self._data_size == AUDIO_UNKNOWN_SIZE:
- return AUDIO_UNKNOWN_SIZE
- if self._encoding in _simple_encodings:
- return self._data_size // self._framesize
- return 0 # XXX--must do some arithmetic here
- def getcomptype(self):
- if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
- return 'ULAW'
- elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
- return 'ALAW'
- else:
- return 'NONE'
- def getcompname(self):
- if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
- return 'CCITT G.711 u-law'
- elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8:
- return 'CCITT G.711 A-law'
- else:
- return 'not compressed'
- def getparams(self):
- return _sunau_params(self.getnchannels(), self.getsampwidth(),
- self.getframerate(), self.getnframes(),
- self.getcomptype(), self.getcompname())
- def getmarkers(self):
- return None
- def getmark(self, id):
- raise Error('no marks')
- def readframes(self, nframes):
- if self._encoding in _simple_encodings:
- if nframes == AUDIO_UNKNOWN_SIZE:
- data = self._file.read()
- else:
- data = self._file.read(nframes * self._framesize)
- self._soundpos += len(data) // self._framesize
- if self._encoding == AUDIO_FILE_ENCODING_MULAW_8:
- import audioop
- data = audioop.ulaw2lin(data, self._sampwidth)
- return data
- return None # XXX--not implemented yet
- def rewind(self):
- if self._data_pos is None:
- raise OSError('cannot seek')
- self._file.seek(self._data_pos)
- self._soundpos = 0
- def tell(self):
- return self._soundpos
- def setpos(self, pos):
- if pos < 0 or pos > self.getnframes():
- raise Error('position not in range')
- if self._data_pos is None:
- raise OSError('cannot seek')
- self._file.seek(self._data_pos + pos * self._framesize)
- self._soundpos = pos
- def close(self):
- file = self._file
- if file:
- self._file = None
- if self._opened:
- file.close()
- class Au_write:
- def __init__(self, f):
- if type(f) == type(''):
- import builtins
- f = builtins.open(f, 'wb')
- self._opened = True
- else:
- self._opened = False
- self.initfp(f)
- def __del__(self):
- if self._file:
- self.close()
- self._file = None
- def __enter__(self):
- return self
- def __exit__(self, *args):
- self.close()
- def initfp(self, file):
- self._file = file
- self._framerate = 0
- self._nchannels = 0
- self._sampwidth = 0
- self._framesize = 0
- self._nframes = AUDIO_UNKNOWN_SIZE
- self._nframeswritten = 0
- self._datawritten = 0
- self._datalength = 0
- self._info = b''
- self._comptype = 'ULAW' # default is U-law
- def setnchannels(self, nchannels):
- if self._nframeswritten:
- raise Error('cannot change parameters after starting to write')
- if nchannels not in (1, 2, 4):
- raise Error('only 1, 2, or 4 channels supported')
- self._nchannels = nchannels
- def getnchannels(self):
- if not self._nchannels:
- raise Error('number of channels not set')
- return self._nchannels
- def setsampwidth(self, sampwidth):
- if self._nframeswritten:
- raise Error('cannot change parameters after starting to write')
- if sampwidth not in (1, 2, 3, 4):
- raise Error('bad sample width')
- self._sampwidth = sampwidth
- def getsampwidth(self):
- if not self._framerate:
- raise Error('sample width not specified')
- return self._sampwidth
- def setframerate(self, framerate):
- if self._nframeswritten:
- raise Error('cannot change parameters after starting to write')
- self._framerate = framerate
- def getframerate(self):
- if not self._framerate:
- raise Error('frame rate not set')
- return self._framerate
- def setnframes(self, nframes):
- if self._nframeswritten:
- raise Error('cannot change parameters after starting to write')
- if nframes < 0:
- raise Error('# of frames cannot be negative')
- self._nframes = nframes
- def getnframes(self):
- return self._nframeswritten
- def setcomptype(self, type, name):
- if type in ('NONE', 'ULAW'):
- self._comptype = type
- else:
- raise Error('unknown compression type')
- def getcomptype(self):
- return self._comptype
- def getcompname(self):
- if self._comptype == 'ULAW':
- return 'CCITT G.711 u-law'
- elif self._comptype == 'ALAW':
- return 'CCITT G.711 A-law'
- else:
- return 'not compressed'
- def setparams(self, params):
- nchannels, sampwidth, framerate, nframes, comptype, compname = params
- self.setnchannels(nchannels)
- self.setsampwidth(sampwidth)
- self.setframerate(framerate)
- self.setnframes(nframes)
- self.setcomptype(comptype, compname)
- def getparams(self):
- return _sunau_params(self.getnchannels(), self.getsampwidth(),
- self.getframerate(), self.getnframes(),
- self.getcomptype(), self.getcompname())
- def tell(self):
- return self._nframeswritten
- def writeframesraw(self, data):
- if not isinstance(data, (bytes, bytearray)):
- data = memoryview(data).cast('B')
- self._ensure_header_written()
- if self._comptype == 'ULAW':
- import audioop
- data = audioop.lin2ulaw(data, self._sampwidth)
- nframes = len(data) // self._framesize
- self._file.write(data)
- self._nframeswritten = self._nframeswritten + nframes
- self._datawritten = self._datawritten + len(data)
- def writeframes(self, data):
- self.writeframesraw(data)
- if self._nframeswritten != self._nframes or \
- self._datalength != self._datawritten:
- self._patchheader()
- def close(self):
- if self._file:
- try:
- self._ensure_header_written()
- if self._nframeswritten != self._nframes or \
- self._datalength != self._datawritten:
- self._patchheader()
- self._file.flush()
- finally:
- file = self._file
- self._file = None
- if self._opened:
- file.close()
- #
- # private methods
- #
- def _ensure_header_written(self):
- if not self._nframeswritten:
- if not self._nchannels:
- raise Error('# of channels not specified')
- if not self._sampwidth:
- raise Error('sample width not specified')
- if not self._framerate:
- raise Error('frame rate not specified')
- self._write_header()
- def _write_header(self):
- if self._comptype == 'NONE':
- if self._sampwidth == 1:
- encoding = AUDIO_FILE_ENCODING_LINEAR_8
- self._framesize = 1
- elif self._sampwidth == 2:
- encoding = AUDIO_FILE_ENCODING_LINEAR_16
- self._framesize = 2
- elif self._sampwidth == 3:
- encoding = AUDIO_FILE_ENCODING_LINEAR_24
- self._framesize = 3
- elif self._sampwidth == 4:
- encoding = AUDIO_FILE_ENCODING_LINEAR_32
- self._framesize = 4
- else:
- raise Error('internal error')
- elif self._comptype == 'ULAW':
- encoding = AUDIO_FILE_ENCODING_MULAW_8
- self._framesize = 1
- else:
- raise Error('internal error')
- self._framesize = self._framesize * self._nchannels
- _write_u32(self._file, AUDIO_FILE_MAGIC)
- header_size = 25 + len(self._info)
- header_size = (header_size + 7) & ~7
- _write_u32(self._file, header_size)
- if self._nframes == AUDIO_UNKNOWN_SIZE:
- length = AUDIO_UNKNOWN_SIZE
- else:
- length = self._nframes * self._framesize
- try:
- self._form_length_pos = self._file.tell()
- except (AttributeError, OSError):
- self._form_length_pos = None
- _write_u32(self._file, length)
- self._datalength = length
- _write_u32(self._file, encoding)
- _write_u32(self._file, self._framerate)
- _write_u32(self._file, self._nchannels)
- self._file.write(self._info)
- self._file.write(b'\0'*(header_size - len(self._info) - 24))
- def _patchheader(self):
- if self._form_length_pos is None:
- raise OSError('cannot seek')
- self._file.seek(self._form_length_pos)
- _write_u32(self._file, self._datawritten)
- self._datalength = self._datawritten
- self._file.seek(0, 2)
- def open(f, mode=None):
- if mode is None:
- if hasattr(f, 'mode'):
- mode = f.mode
- else:
- mode = 'rb'
- if mode in ('r', 'rb'):
- return Au_read(f)
- elif mode in ('w', 'wb'):
- return Au_write(f)
- else:
- raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
|