diff --git a/Lib/aifc.py b/Lib/aifc.py index 2d897be8001..ee1f8f05c96 100644 --- a/Lib/aifc.py +++ b/Lib/aifc.py @@ -40,7 +40,7 @@ # (2 bytes) # (10 bytes, IEEE 80-bit extended # floating point) -# if AIFF-C files only: +# in AIFF-C files only: # (4 bytes) # ("pstring") # SSND @@ -58,9 +58,9 @@ # or # f = aifc.openfp(filep, 'r') # where file is the name of a file and filep is an open file pointer. -# The open file pointer must have methods read(), seek(), and -# close(). In some types of audio files, if the setpos() method is -# not used, the seek() method is not necessary. +# The open file pointer must have methods read(), seek(), and close(). +# In some types of audio files, if the setpos() method is 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 @@ -85,6 +85,8 @@ # The position returned by tell(), the position given to setpos() and # the position of marks are all compatible and have nothing to do with # the actual postion in the file. +# The close() method is called automatically when the class instance +# is destroyed. # # Writing AIFF files: # f = aifc.open(file, 'w') @@ -122,11 +124,13 @@ # 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 the write audio frames using writeframesraw. +# compression type, and then write audio frames using writeframesraw. # When all frames have been written, either call writeframes('') or # close() to patch up the sizes in the header. # Marks can be added anytime. If there are any marks, ypu must call # close() after all frames have been written. +# The close() method is called automatically when the class instance +# is destroyed. # # When a file is opened with the extension '.aiff', an AIFF file is # written, otherwise an AIFF-C file is written. This default can be @@ -347,7 +351,7 @@ class Aifc_read: # methods # _soundpos -- the position in the audio stream # available through the tell() method, set through the - # tell() method + # setpos() method # # These variables are used internally only: # _version -- the AIFF-C version number @@ -362,6 +366,7 @@ class Aifc_read: self._file = file self._version = 0 self._decomp = None + self._convert = None self._markers = [] self._soundpos = 0 form = self._file.read(4) @@ -433,6 +438,10 @@ class Aifc_read: def init(self, filename): return self.initfp(builtin.open(filename, 'r')) + def __del__(self): + if self._file: + self.close() + # # User visible methods. # @@ -475,8 +484,9 @@ class Aifc_read: ## return self._version def getparams(self): - return self._nchannels, self._sampwidth, self._framerate, \ - self._nframes, self._comptype, self._compname + return self.getnchannels(), self.getsampwidth(), \ + self.getframerate(), self.getnframes(), \ + self.getcomptype(), self.getcompname() def getmarkers(self): if len(self._markers) == 0: @@ -506,16 +516,24 @@ class Aifc_read: if nframes == 0: return '' data = self._ssnd_chunk.read(nframes * self._framesize) - if self._decomp and data: - dummy = self._decomp.SetParam(CL.FRAME_BUFFER_SIZE, \ - len(data) * 2) - data = self._decomp.Decompress(len(data) / self._nchannels, data) + if self._convert and data: + data = self._convert(data) self._soundpos = self._soundpos + len(data) / (self._nchannels * self._sampwidth) return data # # Internal methods. # + def _decomp_data(self, data): + dummy = self._decomp.SetParam(CL.FRAME_BUFFER_SIZE, + len(data) * 2) + return self._decomp.Decompress(len(data) / self._nchannels, + data) + + def _ulaw2lin(self, data): + import audioop + return audioop.ulaw2lin(data, 2) + def _read_comm_chunk(self, chunk): nchannels = _read_short(chunk) self._nchannels = _convert1(nchannels, _nchannelslist) @@ -547,6 +565,14 @@ class Aifc_read: try: import cl, CL except ImportError: + if self._comptype == 'ULAW': + try: + import audioop + self._convert = self._ulaw2lin + self._framesize = self._framesize / 2 + return + except ImportError: + pass raise Error, 'cannot read compressed AIFF-C files' if self._comptype == 'ULAW': scheme = CL.G711_ULAW @@ -557,6 +583,7 @@ class Aifc_read: else: raise Error, 'unsupported compression type' self._decomp = cl.OpenDecompressor(scheme) + self._convert = self._decomp_data else: self._comptype = 'NONE' self._compname = 'not compressed' @@ -622,6 +649,7 @@ class Aifc_write: self._comptype = 'NONE' self._compname = 'not compressed' self._comp = None + self._convert = None self._nchannels = 0 self._sampwidth = 0 self._framerate = 0 @@ -634,6 +662,10 @@ class Aifc_write: self._aifc = 1 # AIFF-C is default return self + def __del__(self): + if self._file: + self.close() + # # User visible methods. # @@ -752,15 +784,14 @@ class Aifc_write: return None return self._markers + def tell(self): + return self._nframeswritten + def writeframesraw(self, data): self._ensure_header_written(len(data)) nframes = len(data) / (self._sampwidth * self._nchannels) - if self._comp: - dummy = self._comp.SetParam(CL.FRAME_BUFFER_SIZE, \ - len(data)) - dummy = self._comp.SetParam(CL.COMPRESSED_BUFFER_SIZE,\ - len(data)) - data = self._comp.Compress(nframes, data) + if self._convert: + data = self._convert(data) self._file.write(data) self._nframeswritten = self._nframeswritten + nframes self._datawritten = self._datawritten + len(data) @@ -791,6 +822,15 @@ class Aifc_write: # # Internal methods. # + def _comp_data(self, data): + dum = self._comp.SetParam(CL.FRAME_BUFFER_SIZE, len(data)) + dum = self._comp.SetParam(CL.COMPRESSED_BUFFER_SIZE, len(data)) + return self._comp.Compress(nframes, data) + + def _lin2ulaw(self, data): + import audioop + return audioop.lin2ulaw(data, 2) + def _ensure_header_written(self, datasize): if not self._nframeswritten: if self._comptype in ('ULAW', 'ALAW'): @@ -806,37 +846,48 @@ class Aifc_write: raise Error, 'sampling rate not specified' self._write_header(datasize) + def _init_compression(self): + try: + import cl, CL + except ImportError: + if self._comptype == 'ULAW': + try: + import audioop + self._convert = self._lin2ulaw + return + except ImportError: + pass + raise Error, 'cannot write compressed AIFF-C files' + if self._comptype == 'ULAW': + scheme = CL.G711_ULAW + elif self._comptype == 'ALAW': + scheme = CL.G711_ALAW + else: + raise Error, 'unsupported compression type' + self._comp = cl.OpenCompressor(scheme) + params = [CL.ORIGINAL_FORMAT, 0, \ + CL.BITS_PER_COMPONENT, 0, \ + CL.FRAME_RATE, self._framerate, \ + CL.FRAME_BUFFER_SIZE, 100, \ + CL.COMPRESSED_BUFFER_SIZE, 100] + if self._nchannels == AL.MONO: + params[1] = CL.MONO + else: + params[1] = CL.STEREO_INTERLEAVED + if self._sampwidth == AL.SAMPLE_8: + params[3] = 8 + elif self._sampwidth == AL.SAMPLE_16: + params[3] = 16 + else: + params[3] = 24 + self._comp.SetParams(params) + # the compressor produces a header which we ignore + dummy = self._comp.Compress(0, '') + self._convert = self._comp_data + def _write_header(self, initlength): if self._aifc and self._comptype != 'NONE': - try: - import cl, CL - except ImportError: - raise Error, 'cannot write compressed AIFF-C files' - if self._comptype == 'ULAW': - scheme = CL.G711_ULAW - elif self._comptype == 'ALAW': - scheme = CL.G711_ALAW - else: - raise Error, 'unsupported compression type' - self._comp = cl.OpenCompressor(scheme) - params = [CL.ORIGINAL_FORMAT, 0, \ - CL.BITS_PER_COMPONENT, 0, \ - CL.FRAME_RATE, self._framerate, \ - CL.FRAME_BUFFER_SIZE, 100, \ - CL.COMPRESSED_BUFFER_SIZE, 100] - if self._nchannels == AL.MONO: - params[1] = CL.MONO - else: - params[1] = CL.STEREO_INTERLEAVED - if self._sampwidth == AL.SAMPLE_8: - params[3] = 8 - elif self._sampwidth == AL.SAMPLE_16: - params[3] = 16 - else: - params[3] = 24 - self._comp.SetParams(params) - # the compressor produces a header which we ignore - dummy = self._comp.Compress(0, '') + self._init_compression() self._file.write('FORM') if not self._nframes: self._nframes = initlength / (self._nchannels * self._sampwidth) diff --git a/Lib/sunau.py b/Lib/sunau.py new file mode 100644 index 00000000000..1acebd03c0f --- /dev/null +++ b/Lib/sunau.py @@ -0,0 +1,471 @@ +# Stuff to parse Sun and NeXT audio files. +# +# An audio 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 +# parameter 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 = au.open(file, 'r') +# or +# f = au.openfp(filep, 'r') +# where file is the name of a file and filep is 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' for AIFF files) +# getcompname() -- returns human-readable version of +# compression type ('not compressed' for AIFF files) +# getparams() -- returns a tuple 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 postion in the +# file. +# The close() method is called automatically when the class instance +# is destroyed. +# +# Writing audio files: +# f = au.open(file, 'w') +# or +# f = au.openfp(filep, 'w') +# where file is the name of a file and filep is 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(nchannels, sampwidth, framerate, nframes, comptype, compname) +# -- 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('') or +# close() to patch up the sizes in the header. +# The close() method is called automatically when the class instance +# is destroyed. + +# from +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 +AUDIO_UNKNOWN_SIZE = 0xFFFFFFFFL # ((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] + +def _read_u32(file): + x = 0L + for i in range(4): + byte = file.read(1) + if 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, m) + x = d + for i in range(4): + file.write(chr(int(data[i]))) + +class Au_read: + 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 rediculously 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_LINEAR_8, + AUDIO_FILE_ENCODING_ALAW_8): + self._sampwidth = 2 + self._framesize = 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)) + self._framesize = self._framesize * self._nchannels + if self._hdr_size > 24: + self._info = file.read(self._hdr_size - 24) + for i in range(len(self._info)): + if self._info[i] == '\0': + self._info = self._info[:i] + break + else: + self._info = '' + return self + + def init(self, filename): + import builtin + return self.initfp(builtin.open(filename, 'r')) + + def __del__(self): + if self._file: + self.close() + + 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 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._sampwidth * self._nchannels) + 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): + self._soundpos = 0 + self._file.seek(self._hdr_size) + + def tell(self): + return self._soundpos + + def setpos(self, pos): + if pos < 0 or pos > self.getnframes(): + raise Error, 'position not in range' + self._file.seek(pos * self._framesize + self._hdr_size) + self._soundpos = pos + + def close(self): + self._file.close() + self._file = None + +class Au_write: + def init(self, filename): + import builtin + return self.initfp(builtin.open(filename, 'w')) + + 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 = '' + self._comptype = 'ULAW' # default is U-law + return self + + def __del__(self): + if self._file: + self.close() + + 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, 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, (nchannels, sampwidth, framerate, nframes, comptype, compname)): + self.setnchannels(nchannels) + self.setsampwidth(sampwidth) + self.setframerate(framerate) + self.setnframes(nframes) + self.setcomptype(comptype, compname) + + def getparams(self): + return self.getnchannels(), self.getsampwidth(), \ + self.getframerate(), self.getnframes(), \ + self.getcomptype(), self.getcompname() + + def tell(self): + return self._nframeswritten + + def writeframesraw(self, data): + self._ensure_header_written() + nframes = len(data) / self._framesize + if self._comptype == 'ULAW': + import audioop + data = audioop.lin2ulaw(data, self._sampwidth) + 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): + self._ensure_header_written() + if self._nframeswritten != self._nframes or \ + self._datalength != self._datawritten: + self._patchheader() + self._file.close() + self._file = None + + # + # 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 == 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 + _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('\0'*(header_size - len(self._info) - 24)) + + def _patchheader(self): + self._file.seek(8) + _write_u32(self._file, self._datawritten) + self._datalength = self._datawritten + self._file.seek(0, 2) + +def open(filename, mode): + if mode == 'r': + return Au_read().init(filename) + elif mode == 'w': + return Au_write().init(filename) + else: + raise Error, "mode must be 'r' or 'w'" + +def openfp(filep, mode): + if mode == 'r': + return Au_read().initfp(filep) + elif mode == 'w': + return Au_write().initfp(filep) + else: + raise Error, "mode must be 'r' or 'w'"