From 0d0f47fbd2a74adea4fefa464c1b33b257489bfd Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 4 Mar 2014 18:12:09 +0100 Subject: [PATCH 01/33] Fix __setattr__() In new-style classes the base class __setattr__() has to be called instead of inserting the value into self.__dict__. See http://docs.python.org/2/reference/datamodel.html#customizing-attribute-access --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index d6c7ffe..97f84e8 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -401,7 +401,7 @@ def __setattr__(self, name, value): err = _snd.sf_set_string(self._file, self._snd_strings[name], data) self._handle_error_number(err) else: - self.__dict__[name] = value + super(SoundFile, self).__setattr__(name, value) def __getattr__(self, name): # access text data in the sound file through properties From 25727423bb48af8319ddb1eece2b97aa5e889809 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 3 Mar 2014 20:45:57 +0100 Subject: [PATCH 02/33] Change argument for open mode to 'r'/'w'/'rw' --- pysoundfile.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 97f84e8..3aff42f 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -118,9 +118,9 @@ """) -read_mode = 0x10 -write_mode = 0x20 -read_write_mode = 0x30 +_M_READ = 0x10 +_M_WRITE = 0x20 +_M_RDWR = 0x30 snd_types = { 'WAV': 0x010000, # Microsoft WAV format (little endian default). @@ -216,9 +216,8 @@ class SoundFile(object): Each SoundFile opens one sound file on the disk. This sound file has a specific samplerate, data format and a set number of - channels. Each sound file can be opened in read_mode, write_mode, - or read_write_mode. Note that read_write_mode is unsupported for - some formats. + channels. Each sound file can be opened with mode 'r', 'w', or 'rw'. + Note that 'rw' is unsupported for some formats. Data can be written to the file using write(), or read from the file using read(). Every read and write operation starts at a @@ -247,12 +246,12 @@ class SoundFile(object): """ def __init__(self, name, sample_rate=0, channels=0, format=0, - mode=read_mode, virtual_io=False): + mode='r', virtual_io=False): """Open a new SoundFile. - If a file is only opened in read_mode or in read_write_mode, + If a file is only opened with mode='r' or mode='rw', no sample_rate, channels or file format need to be given. If a - file is opened in write_mode, you must provide a sample_rate, + file is opened with mode='w', you must provide a sample_rate, a number of channels, and a file format. An exception is the RAW data format, which requires these data points for reading as well. @@ -276,7 +275,13 @@ def __init__(self, name, sample_rate=0, channels=0, format=0, if hasattr(format, '__getitem__'): format = _encodeformat(format) info.format = format - self._file_mode = mode + try: + mode_int = {'r': _M_READ, + 'w': _M_WRITE, + 'rw': _M_RDWR}[mode] + except KeyError: + raise ValueError("invalid mode: " + mode) + self.mode = mode if virtual_io: fObj = name @@ -287,11 +292,11 @@ def __init__(self, name, sample_rate=0, channels=0, format=0, self._vio = self._init_vio(fObj) vio = ffi.new("SF_VIRTUAL_IO*", self._vio) self._vio['vio_cdata'] = vio - self._file = _snd.sf_open_virtual(vio, self._file_mode, info, + self._file = _snd.sf_open_virtual(vio, mode_int, info, ffi.NULL) else: filename = ffi.new('char[]', name.encode()) - self._file = _snd.sf_open(filename, self._file_mode, info) + self._file = _snd.sf_open(filename, mode_int, info) self._handle_error() @@ -394,7 +399,7 @@ def _handle_error_number(self, err): def __setattr__(self, name, value): # access text data in the sound file through properties if name in self._snd_strings: - if self._file_mode == read_mode: + if self.mode == 'r': raise RuntimeError("Can not change %s of file in read mode" % name) data = ffi.new('char[]', value.encode()) @@ -451,7 +456,7 @@ def __setitem__(self, frame, data): # access the file as if it where a one-dimensional Numpy # array. Data must be in the form (frames x channels). # Both open slice bounds and negative values are allowed. - if self._file_mode == read_mode: + if self.mode == 'r': raise RuntimeError("Can not write to read-only file") start, stop = self._get_slice_bounds(frame) if stop - start != len(data): @@ -540,7 +545,7 @@ def write(self, data): array. """ - if self._file_mode == read_mode: + if self.mode == 'r': raise RuntimeError("Can not write to read-only file") formats = { np.dtype(np.float64): 'double*', From 2833332c9be6f512e33509aa4d12c5500d9c2179 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 3 Mar 2014 22:36:22 +0100 Subject: [PATCH 03/33] Change file type handling --- pysoundfile.py | 225 +++++++++++++++++++++++++++++-------------------- 1 file changed, 135 insertions(+), 90 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 3aff42f..6e94a1f 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -122,91 +122,125 @@ _M_WRITE = 0x20 _M_RDWR = 0x30 -snd_types = { - 'WAV': 0x010000, # Microsoft WAV format (little endian default). - 'AIFF': 0x020000, # Apple/SGI AIFF format (big endian). - 'AU': 0x030000, # Sun/NeXT AU format (big endian). - 'RAW': 0x040000, # RAW PCM data. - 'PAF': 0x050000, # Ensoniq PARIS file format. - 'SVX': 0x060000, # Amiga IFF / SVX8 / SV16 format. - 'NIST': 0x070000, # Sphere NIST format. - 'VOC': 0x080000, # VOC files. - 'IRCAM': 0x0A0000, # Berkeley/IRCAM/CARL - 'W64': 0x0B0000, # Sonic Foundry's 64 bit RIFF/WAV - 'MAT4': 0x0C0000, # Matlab (tm) V4.2 / GNU Octave 2.0 - 'MAT5': 0x0D0000, # Matlab (tm) V5.0 / GNU Octave 2.1 - 'PVF': 0x0E0000, # Portable Voice Format - 'XI': 0x0F0000, # Fasttracker 2 Extended Instrument - 'HTK': 0x100000, # HMM Tool Kit format - 'SDS': 0x110000, # Midi Sample Dump Standard - 'AVR': 0x120000, # Audio Visual Research - 'WAVEX': 0x130000, # MS WAVE with WAVEFORMATEX - 'SD2': 0x160000, # Sound Designer 2 - 'FLAC': 0x170000, # FLAC lossless file format - 'CAF': 0x180000, # Core Audio File format - 'WVE': 0x190000, # Psion WVE format - 'OGG': 0x200000, # Xiph OGG container - 'MPC2K': 0x210000, # Akai MPC 2000 sampler - 'RF64': 0x220000 # RF64 WAV file +WAV = 0x010000 # Microsoft WAV format (little endian default). +AIFF = 0x020000 # Apple/SGI AIFF format (big endian). +AU = 0x030000 # Sun/NeXT AU format (big endian). +RAW = 0x040000 # RAW PCM data. +PAF = 0x050000 # Ensoniq PARIS file format. +SVX = 0x060000 # Amiga IFF / SVX8 / SV16 format. +NIST = 0x070000 # Sphere NIST format. +VOC = 0x080000 # VOC files. +IRCAM = 0x0A0000 # Berkeley/IRCAM/CARL +W64 = 0x0B0000 # Sonic Foundrys 64 bit RIFF/WAV +MAT4 = 0x0C0000 # Matlab (tm) V4.2 / GNU Octave 2.0 +MAT5 = 0x0D0000 # Matlab (tm) V5.0 / GNU Octave 2.1 +PVF = 0x0E0000 # Portable Voice Format +XI = 0x0F0000 # Fasttracker 2 Extended Instrument +HTK = 0x100000 # HMM Tool Kit format +SDS = 0x110000 # Midi Sample Dump Standard +AVR = 0x120000 # Audio Visual Research +WAVEX = 0x130000 # MS WAVE with WAVEFORMATEX +SD2 = 0x160000 # Sound Designer 2 +FLAC = 0x170000 # FLAC lossless file format +CAF = 0x180000 # Core Audio File format +WVE = 0x190000 # Psion WVE format +OGG = 0x200000 # Xiph OGG container +MPC2K = 0x210000 # Akai MPC 2000 sampler +RF64 = 0x220000 # RF64 WAV file + +PCM_S8 = 0x0001 # Signed 8 bit data +PCM_16 = 0x0002 # Signed 16 bit data +PCM_24 = 0x0003 # Signed 24 bit data +PCM_32 = 0x0004 # Signed 32 bit data +PCM_U8 = 0x0005 # Unsigned 8 bit data (WAV and RAW only) +FLOAT = 0x0006 # 32 bit float data +DOUBLE = 0x0007 # 64 bit float data +ULAW = 0x0010 # U-Law encoded. +ALAW = 0x0011 # A-Law encoded. +IMA_ADPCM = 0x0012 # IMA ADPCM. +MS_ADPCM = 0x0013 # Microsoft ADPCM. +GSM610 = 0x0020 # GSM 6.10 encoding. +VOX_ADPCM = 0x0021 # OKI / Dialogix ADPCM +G721_32 = 0x0030 # 32kbs G721 ADPCM encoding. +G723_24 = 0x0031 # 24kbs G723 ADPCM encoding. +G723_40 = 0x0032 # 40kbs G723 ADPCM encoding. +DWVW_12 = 0x0040 # 12 bit Delta Width Variable Word encoding. +DWVW_16 = 0x0041 # 16 bit Delta Width Variable Word encoding. +DWVW_24 = 0x0042 # 24 bit Delta Width Variable Word encoding. +DWVW_N = 0x0043 # N bit Delta Width Variable Word encoding. +DPCM_8 = 0x0050 # 8 bit differential PCM (XI only) +DPCM_16 = 0x0051 # 16 bit differential PCM (XI only) +VORBIS = 0x0060 # Xiph Vorbis encoding. + +FILE = 0x00000000 # Default file endian-ness. +LITTLE = 0x10000000 # Force little endian-ness. +BIG = 0x20000000 # Force big endian-ness. +CPU = 0x30000000 # Force CPU endian-ness. + +_SUBMASK = 0x0000FFFF +_TYPEMASK = 0x0FFF0000 +_ENDMASK = 0x30000000 + +_format_by_extension = { + 'wav': WAV, + 'aif': AIFF, + 'aiff': AIFF, + 'aifc': AIFF | FLOAT, + 'au': AU, + 'raw': RAW, + 'paf': PAF, + 'svx': SVX, + 'nist': NIST, + 'voc': VOC, + 'ircam': IRCAM, + 'w64': W64, + 'mat4': MAT4, + 'mat': MAT5, + 'pvf': PVF, + 'xi': XI, + 'htk': HTK, + 'sds': SDS, + 'avr': AVR, + 'wavex': WAVEX, + 'sd2': SD2, + 'flac': FLAC, + 'caf': CAF, + 'wve': WVE, + 'ogg': OGG, + 'oga': OGG, + 'mpc2k': MPC2K, + 'rf64': RF64, + #'vox': RAW | VOX_ADPCM, } -snd_subtypes = { - 'PCM_S8': 0x0001, # Signed 8 bit data - 'PCM_16': 0x0002, # Signed 16 bit data - 'PCM_24': 0x0003, # Signed 24 bit data - 'PCM_32': 0x0004, # Signed 32 bit data - 'PCM_U8': 0x0005, # Unsigned 8 bit data (WAV and RAW only) - 'FLOAT': 0x0006, # 32 bit float data - 'DOUBLE': 0x0007, # 64 bit float data - 'ULAW': 0x0010, # U-Law encoded. - 'ALAW': 0x0011, # A-Law encoded. - 'IMA_ADPCM': 0x0012, # IMA ADPCM. - 'MS_ADPCM': 0x0013, # Microsoft ADPCM. - 'GSM610': 0x0020, # GSM 6.10 encoding. - 'VOX_ADPCM': 0x0021, # OKI / Dialogix ADPCM - 'G721_32': 0x0030, # 32kbs G721 ADPCM encoding. - 'G723_24': 0x0031, # 24kbs G723 ADPCM encoding. - 'G723_40': 0x0032, # 40kbs G723 ADPCM encoding. - 'DWVW_12': 0x0040, # 12 bit Delta Width Variable Word encoding. - 'DWVW_16': 0x0041, # 16 bit Delta Width Variable Word encoding. - 'DWVW_24': 0x0042, # 24 bit Delta Width Variable Word encoding. - 'DWVW_N': 0x0043, # N bit Delta Width Variable Word encoding. - 'DPCM_8': 0x0050, # 8 bit differential PCM (XI only) - 'DPCM_16': 0x0051, # 16 bit differential PCM (XI only) - 'VORBIS': 0x0060, # Xiph Vorbis encoding. +_default_subtypes = { + WAV: PCM_16, + AIFF: PCM_16, + AU: PCM_16, + #RAW: # subtype must be explicit! + #PAF: + #SVX: + #NIST: + #VOC: + #IRCAM: + #W64: + MAT4: DOUBLE, + MAT5: DOUBLE, + #PVF: + #XI: + #HTK: + #SDS: + #AVR: + WAVEX: PCM_16, + #SD2: + FLAC: PCM_16, + CAF: PCM_16, + #WVE: + OGG: VORBIS, + #MPC2K: + #RF64: } -snd_endians = { - 'FILE': 0x00000000, # Default file endian-ness. - 'LITTLE': 0x10000000, # Force little endian-ness. - 'BIG': 0x20000000, # Force big endian-ness. - 'CPU': 0x30000000, # Force CPU endian-ness. -} - -wave_file = ('WAV', 'PCM_16', 'FILE') -flac_file = ('FLAC', 'PCM_16', 'FILE') -matlab_file = ('MAT5', 'DOUBLE', 'FILE') -ogg_file = ('OGG', 'VORBIS', 'FILE') - -def _encodeformat(format): - type = snd_types[format[0]] - subtype = snd_subtypes[format[1]] - endianness = snd_endians[format[2]] - return type|subtype|endianness - -def _decodeformat(format): - sub_mask = 0x0000FFFF - type_mask = 0x0FFF0000 - end_mask = 0x30000000 - - def reverse_dict(d): return {value:key for key, value in d.items()} - - type = reverse_dict(snd_types)[format & type_mask] - subtype = reverse_dict(snd_subtypes)[format & sub_mask] - endianness = reverse_dict(snd_endians)[format & end_mask] - - return (type, subtype, endianness) - _snd = ffi.dlopen('sndfile') @@ -245,8 +279,8 @@ class SoundFile(object): """ - def __init__(self, name, sample_rate=0, channels=0, format=0, - mode='r', virtual_io=False): + def __init__(self, name, sample_rate=None, channels=2, format=None, + subtype=None, endian=None, mode='r', virtual_io=False): """Open a new SoundFile. If a file is only opened with mode='r' or mode='rw', @@ -269,12 +303,6 @@ def __init__(self, name, sample_rate=0, channels=0, format=0, ogg_file. """ - info = ffi.new("SF_INFO*") - info.samplerate = sample_rate - info.channels = channels - if hasattr(format, '__getitem__'): - format = _encodeformat(format) - info.format = format try: mode_int = {'r': _M_READ, 'w': _M_WRITE, @@ -282,6 +310,21 @@ def __init__(self, name, sample_rate=0, channels=0, format=0, except KeyError: raise ValueError("invalid mode: " + mode) self.mode = mode + info = ffi.new("SF_INFO*") + if self._file_mode == M_WRITE: + assert sample_rate, "Sample rate must be specified for mode='w'!" + info.samplerate = sample_rate + info.channels = channels + if format is None: + ext = name.rsplit('.', 1)[-1] + format = _format_by_extension[ext.lower()] + assert format & _TYPEMASK, "Invalid format!" + if subtype is None: + subtype = _default_subtypes[format] + assert subtype & _SUBMASK, "Invalid subtype!" + endian = endian or FILE + assert endian == FILE or endian & _ENDMASK, "Invalid endian-ness!" + info.format = format | subtype | endian if virtual_io: fObj = name @@ -303,7 +346,9 @@ def __init__(self, name, sample_rate=0, channels=0, format=0, self.frames = info.frames self.sample_rate = info.samplerate self.channels = info.channels - self.format = _decodeformat(info.format) + self.format = info.format & _TYPEMASK + self.subtype = info.format & _SUBMASK + self.endian = info.format & _ENDMASK self.sections = info.sections self.seekable = info.seekable == 1 From 0ed878facac624e5aa96361f4875940dfe37af8f Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 3 Mar 2014 22:57:16 +0100 Subject: [PATCH 04/33] Change order of SoundFile constructor arguments Moving 'mode' right after 'name' and 'format' after 'subtype' and 'endian'. This way most situations can be handled with positional arguments only, e.g.: f1 = SoundFile('file1.wav') f2 = SoundFile('file2.wav', 'rw') f3 = SoundFile('file3.wav', 'w', 48000, 1, sf.PCM_24) --- pysoundfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 6e94a1f..6b8a3d0 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -279,8 +279,8 @@ class SoundFile(object): """ - def __init__(self, name, sample_rate=None, channels=2, format=None, - subtype=None, endian=None, mode='r', virtual_io=False): + def __init__(self, name, mode='r', sample_rate=None, channels=2, + subtype=None, endian=None, format=None, virtual_io=False): """Open a new SoundFile. If a file is only opened with mode='r' or mode='rw', From d26b657210e92727062074e53cc274d50e2afcdb Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 4 Mar 2014 20:01:57 +0100 Subject: [PATCH 05/33] Improve file type handling for raw files ... and set default channels=None --- pysoundfile.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 6b8a3d0..5bb20f1 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -279,7 +279,7 @@ class SoundFile(object): """ - def __init__(self, name, mode='r', sample_rate=None, channels=2, + def __init__(self, name, mode='r', sample_rate=None, channels=None, subtype=None, endian=None, format=None, virtual_io=False): """Open a new SoundFile. @@ -310,21 +310,26 @@ def __init__(self, name, mode='r', sample_rate=None, channels=2, except KeyError: raise ValueError("invalid mode: " + mode) self.mode = mode + if format is None: + ext = name.rsplit('.', 1)[-1] + format = _format_by_extension.get(ext.lower(), 0x0) info = ffi.new("SF_INFO*") - if self._file_mode == M_WRITE: - assert sample_rate, "Sample rate must be specified for mode='w'!" + if mode == 'w' or format == RAW: + assert sample_rate, \ + "sample_rate must be specified for mode='w' and format=RAW!" info.samplerate = sample_rate + assert channels, \ + "channels must be specified for mode='w' and format=RAW!" info.channels = channels - if format is None: - ext = name.rsplit('.', 1)[-1] - format = _format_by_extension[ext.lower()] - assert format & _TYPEMASK, "Invalid format!" if subtype is None: - subtype = _default_subtypes[format] - assert subtype & _SUBMASK, "Invalid subtype!" + subtype = _default_subtypes.get(format, 0x0) endian = endian or FILE - assert endian == FILE or endian & _ENDMASK, "Invalid endian-ness!" - info.format = format | subtype | endian + format = format | subtype | endian + assert format, "No format specified!" + assert format & _TYPEMASK, "Invalid format!" + assert format & _SUBMASK, "Invalid subtype!" + assert endian == FILE or format & _ENDMASK, "Invalid endian-ness!" + info.format = format if virtual_io: fObj = name From 6ff89dbe31e9d335b12f157848711a9f56bf8cab Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 4 Mar 2014 23:18:44 +0100 Subject: [PATCH 06/33] Change frames=-1 to frames=None --- pysoundfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 5bb20f1..ee8f25e 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -546,13 +546,13 @@ def seek_absolute(self, frames): else: return _snd.sf_seek(self._file, frames, os.SEEK_END) - def read(self, frames=-1, format=np.float32): + def read(self, frames=None, format=np.float32): """Read a number of frames from the file. Reads the given number of frames in the given data format from the current read position. This also advances the read position by the same number of frames. - Use frames=-1 to read until the end of the file. + Use frames=None to read until the end of the file. Returns the read data as a (frames x channels) NumPy array. @@ -574,7 +574,7 @@ def read(self, frames=-1, format=np.float32): } if format not in formats: raise ValueError("Can only read int16, int32, float32 and float64") - if frames == -1: + if frames is None: curr = self.seek(0) frames = self.frames - curr data = ffi.new(formats[format], frames*self.channels) From 29487835e6a8c4899aa18347ba41fefab7eec186 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 3 Mar 2014 23:11:44 +0100 Subject: [PATCH 07/33] Add open() function --- pysoundfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index ee8f25e..6ffa2a7 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -617,3 +617,6 @@ def write(self, data): len(data)) self._handle_error() return written + +def open(*args, **kwargs): + return SoundFile(*args, **kwargs) From beb81b7219e36fa5bab24855df1e9f82d5110159 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 4 Mar 2014 23:24:01 +0100 Subject: [PATCH 08/33] Add read() function --- pysoundfile.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 6ffa2a7..9091bfc 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -620,3 +620,15 @@ def write(self, data): def open(*args, **kwargs): return SoundFile(*args, **kwargs) + +def read(filename, frames=None, start=None, stop=None, **kwargs): + # If frames and stop are both specified, frames takes precedence! + # start and stop accept negative indices. + with open(filename, 'r') as f: + start, stop, _ = slice(start, stop).indices(f.frames) + if frames is None: + frames = max(0, stop - start) + f.seek_absolute(start) + data = f.read(frames, **kwargs) + return data, f.sample_rate + From aa753026e272ac4a62e06a46dedf1b60ec8ada6e Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 11:25:24 +0100 Subject: [PATCH 09/33] Use sf_format_check() to check user-specified arguments --- pysoundfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 9091bfc..5f7f748 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -330,6 +330,8 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, assert format & _SUBMASK, "Invalid subtype!" assert endian == FILE or format & _ENDMASK, "Invalid endian-ness!" info.format = format + assert _snd.sf_format_check(info), \ + "Invalid combination of format, subtype and endian!" if virtual_io: fObj = name From e835cd39ffcbdaeced88a020141b8d47d291009b Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 11:26:00 +0100 Subject: [PATCH 10/33] Raise error if too many arguments are specified --- pysoundfile.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 5f7f748..a8399f9 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -310,6 +310,7 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, except KeyError: raise ValueError("invalid mode: " + mode) self.mode = mode + original_format, original_endian = format, endian if format is None: ext = name.rsplit('.', 1)[-1] format = _format_by_extension.get(ext.lower(), 0x0) @@ -332,6 +333,13 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, info.format = format assert _snd.sf_format_check(info), \ "Invalid combination of format, subtype and endian!" + else: + should_be_none = [sample_rate, channels, subtype, + original_endian, original_format] + if should_be_none != [None] * len(should_be_none): + raise RuntimeError("If mode='r', none of these arguments are " + "allowed: sample_rate, channels, format, " + "subtype, endian") if virtual_io: fObj = name From d8afffd90102b6b4afbe25429e39c89c8d7b22c9 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 4 Mar 2014 23:24:44 +0100 Subject: [PATCH 11/33] Add write() function --- pysoundfile.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index a8399f9..d27d683 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -642,3 +642,15 @@ def read(filename, frames=None, start=None, stop=None, **kwargs): data = f.read(frames, **kwargs) return data, f.sample_rate +def write(data, filename, sample_rate, *args, **kwargs): + # e.g. write(myarray, 'myfile.wav', 44100, sf.FLOAT) + if data.ndim == 1: + channels = 1 + elif data.ndim == 2: + channels = data.shape[1] + else: + raise RuntimeError("Only one- and two-dimensional arrays are allowed!") + frames = data.shape[0] + with open(filename, 'w', sample_rate, channels, *args, **kwargs) as f: + written = f.write(data) + assert frames == written, "Error writing file!" From e35911c35629b1e268d08434163bc56528347bf2 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 14:27:41 +0100 Subject: [PATCH 12/33] Move _snd_strings out of class SoundFile Also, use named constants similar to the ones used in libsndfile (but underscore-prefixed to not clutter the module namespace). --- pysoundfile.py | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index d27d683..f745ab9 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -181,6 +181,17 @@ _TYPEMASK = 0x0FFF0000 _ENDMASK = 0x30000000 +_TITLE = 0x01 +_COPYRIGHT = 0x02 +_SOFTWARE = 0x03 +_ARTIST = 0x04 +_COMMENT = 0x05 +_DATE = 0x06 +_ALBUM = 0x07 +_LICENSE = 0x08 +_TRACKNUMBER = 0x09 +_GENRE = 0x10 + _format_by_extension = { 'wav': WAV, 'aif': AIFF, @@ -241,6 +252,19 @@ #RF64: } +_snd_strings = { + 'title': _TITLE, + 'copyright': _COPYRIGHT, + 'software': _SOFTWARE, + 'artist': _ARTIST, + 'comment': _COMMENT, + 'date': _DATE, + 'album': _ALBUM, + 'license': _LICENSE, + 'tracknumber': _TRACKNUMBER, + 'genre': _GENRE +} + _snd = ffi.dlopen('sndfile') @@ -441,37 +465,22 @@ def _handle_error_number(self, err): err_str = _snd.sf_error_number(err) raise RuntimeError(ffi.string(err_str).decode()) - # these strings are used as properties to access text data n the - # sound file - _snd_strings = { - 'title': 0x01, - 'copyright': 0x02, - 'software': 0x03, - 'artist': 0x04, - 'comment': 0x05, - 'date': 0x06, - 'album': 0x07, - 'license': 0x08, - 'tracknumber': 0x09, - 'genre': 0x10 - } - def __setattr__(self, name, value): # access text data in the sound file through properties - if name in self._snd_strings: + if name in _snd_strings: if self.mode == 'r': raise RuntimeError("Can not change %s of file in read mode" % name) data = ffi.new('char[]', value.encode()) - err = _snd.sf_set_string(self._file, self._snd_strings[name], data) + err = _snd.sf_set_string(self._file, _snd_strings[name], data) self._handle_error_number(err) else: super(SoundFile, self).__setattr__(name, value) def __getattr__(self, name): # access text data in the sound file through properties - if name in self._snd_strings: - data = _snd.sf_get_string(self._file, self._snd_strings[name]) + if name in _snd_strings: + data = _snd.sf_get_string(self._file, _snd_strings[name]) if data == ffi.NULL: return "" else: From c6d1fd118b2c4e36bbd0066feaed46dd5d6aee7a Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 14:47:39 +0100 Subject: [PATCH 13/33] Add _getAttributeNames() (useful for auto-completion) --- pysoundfile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index f745ab9..d60dd0f 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -465,6 +465,11 @@ def _handle_error_number(self, err): err_str = _snd.sf_error_number(err) raise RuntimeError(ffi.string(err_str).decode()) + def _getAttributeNames(self): + # return all possible attributes used in __setattr__ and __getattr__. + # This is useful for auto-completion (e.g. IPython) + return _snd_strings + def __setattr__(self, name, value): # access text data in the sound file through properties if name in _snd_strings: From 0162248b71240b26f50a91f8664e5301038d077a Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 14:50:27 +0100 Subject: [PATCH 14/33] Remove superfluous parentheses in return statement --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index d60dd0f..9decdb5 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -494,7 +494,7 @@ def __getattr__(self, name): raise AttributeError("SoundFile has no attribute %s" % name) def __len__(self): - return(self.frames) + return self.frames def _get_slice_bounds(self, frame): # get start and stop index from slice, asserting step==1 From 1d10fdfa877c358af81bf7e1bc21eae8a63465f7 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 16:42:37 +0100 Subject: [PATCH 15/33] Add properties format_string and subtype_string ... and a module-level function get_format_info() --- pysoundfile.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 9decdb5..523047d 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -116,6 +116,12 @@ SNDFILE* sf_open_virtual (SF_VIRTUAL_IO *sfvirtual, int mode, SF_INFO *sfinfo, void *user_data) ; +typedef struct SF_FORMAT_INFO +{ + int format ; + const char* name ; + const char* extension ; +} SF_FORMAT_INFO ; """) _M_READ = 0x10 @@ -192,6 +198,8 @@ _TRACKNUMBER = 0x09 _GENRE = 0x10 +_GET_FORMAT_INFO = 0x1028 + _format_by_extension = { 'wav': WAV, 'aif': AIFF, @@ -391,6 +399,14 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, self.sections = info.sections self.seekable = info.seekable == 1 + @property + def format_string(self): + return get_format_info(self.format) + + @property + def subtype_string(self): + return get_format_info(self.subtype) + def _init_vio(self, fObj): # Define callbacks here, so they can reference fObj / size @ffi.callback("sf_vio_get_filelen") @@ -668,3 +684,10 @@ def write(data, filename, sample_rate, *args, **kwargs): with open(filename, 'w', sample_rate, channels, *args, **kwargs) as f: written = f.write(data) assert frames == written, "Error writing file!" + +def get_format_info(format): + format_info = ffi.new("struct SF_FORMAT_INFO*") + format_info.format = format + _snd.sf_command(ffi.NULL, _GET_FORMAT_INFO, format_info, + ffi.sizeof("SF_FORMAT_INFO")) + return ffi.string(format_info.name).decode() if format_info.name else "" From d72075fd08a0d252753fe8428aab3e041d396937 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 21:44:52 +0100 Subject: [PATCH 16/33] Add function decode_number() This is just an idea about how to retrieve meaningful strings for format codes etc. Most likely there are much better methods! --- pysoundfile.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 523047d..5bdc345 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -691,3 +691,9 @@ def get_format_info(format): _snd.sf_command(ffi.NULL, _GET_FORMAT_INFO, format_info, ffi.sizeof("SF_FORMAT_INFO")) return ffi.string(format_info.name).decode() if format_info.name else "" + +def decode_number(number): + # e.g. decode_number(myfile.subtype) + for k, v in globals().items(): + if not k.startswith('_') and v == number: + return k From d31a48487c1c6e57bc24b7a22f740794d45c0220 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 22:58:52 +0100 Subject: [PATCH 17/33] Change handling of formats (again) --- pysoundfile.py | 152 ++++++++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 57 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 5bdc345..e7fb814 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -128,60 +128,66 @@ _M_WRITE = 0x20 _M_RDWR = 0x30 -WAV = 0x010000 # Microsoft WAV format (little endian default). -AIFF = 0x020000 # Apple/SGI AIFF format (big endian). -AU = 0x030000 # Sun/NeXT AU format (big endian). -RAW = 0x040000 # RAW PCM data. -PAF = 0x050000 # Ensoniq PARIS file format. -SVX = 0x060000 # Amiga IFF / SVX8 / SV16 format. -NIST = 0x070000 # Sphere NIST format. -VOC = 0x080000 # VOC files. -IRCAM = 0x0A0000 # Berkeley/IRCAM/CARL -W64 = 0x0B0000 # Sonic Foundrys 64 bit RIFF/WAV -MAT4 = 0x0C0000 # Matlab (tm) V4.2 / GNU Octave 2.0 -MAT5 = 0x0D0000 # Matlab (tm) V5.0 / GNU Octave 2.1 -PVF = 0x0E0000 # Portable Voice Format -XI = 0x0F0000 # Fasttracker 2 Extended Instrument -HTK = 0x100000 # HMM Tool Kit format -SDS = 0x110000 # Midi Sample Dump Standard -AVR = 0x120000 # Audio Visual Research -WAVEX = 0x130000 # MS WAVE with WAVEFORMATEX -SD2 = 0x160000 # Sound Designer 2 -FLAC = 0x170000 # FLAC lossless file format -CAF = 0x180000 # Core Audio File format -WVE = 0x190000 # Psion WVE format -OGG = 0x200000 # Xiph OGG container -MPC2K = 0x210000 # Akai MPC 2000 sampler -RF64 = 0x220000 # RF64 WAV file - -PCM_S8 = 0x0001 # Signed 8 bit data -PCM_16 = 0x0002 # Signed 16 bit data -PCM_24 = 0x0003 # Signed 24 bit data -PCM_32 = 0x0004 # Signed 32 bit data -PCM_U8 = 0x0005 # Unsigned 8 bit data (WAV and RAW only) -FLOAT = 0x0006 # 32 bit float data -DOUBLE = 0x0007 # 64 bit float data -ULAW = 0x0010 # U-Law encoded. -ALAW = 0x0011 # A-Law encoded. -IMA_ADPCM = 0x0012 # IMA ADPCM. -MS_ADPCM = 0x0013 # Microsoft ADPCM. -GSM610 = 0x0020 # GSM 6.10 encoding. -VOX_ADPCM = 0x0021 # OKI / Dialogix ADPCM -G721_32 = 0x0030 # 32kbs G721 ADPCM encoding. -G723_24 = 0x0031 # 24kbs G723 ADPCM encoding. -G723_40 = 0x0032 # 40kbs G723 ADPCM encoding. -DWVW_12 = 0x0040 # 12 bit Delta Width Variable Word encoding. -DWVW_16 = 0x0041 # 16 bit Delta Width Variable Word encoding. -DWVW_24 = 0x0042 # 24 bit Delta Width Variable Word encoding. -DWVW_N = 0x0043 # N bit Delta Width Variable Word encoding. -DPCM_8 = 0x0050 # 8 bit differential PCM (XI only) -DPCM_16 = 0x0051 # 16 bit differential PCM (XI only) -VORBIS = 0x0060 # Xiph Vorbis encoding. - -FILE = 0x00000000 # Default file endian-ness. -LITTLE = 0x10000000 # Force little endian-ness. -BIG = 0x20000000 # Force big endian-ness. -CPU = 0x30000000 # Force CPU endian-ness. +_formats = { + 0x010000: 'WAV', # Microsoft WAV format (little endian default). + 0x020000: 'AIFF', # Apple/SGI AIFF format (big endian). + 0x030000: 'AU', # Sun/NeXT AU format (big endian). + 0x040000: 'RAW', # RAW PCM data. + 0x050000: 'PAF', # Ensoniq PARIS file format. + 0x060000: 'SVX', # Amiga IFF / SVX8 / SV16 format. + 0x070000: 'NIST', # Sphere NIST format. + 0x080000: 'VOC', # VOC files. + 0x0A0000: 'IRCAM', # Berkeley/IRCAM/CARL + 0x0B0000: 'W64', # Sonic Foundry's 64 bit RIFF/WAV + 0x0C0000: 'MAT4', # Matlab (tm) V4.2 / GNU Octave 2.0 + 0x0D0000: 'MAT5', # Matlab (tm) V5.0 / GNU Octave 2.1 + 0x0E0000: 'PVF', # Portable Voice Format + 0x0F0000: 'XI', # Fasttracker 2 Extended Instrument + 0x100000: 'HTK', # HMM Tool Kit format + 0x110000: 'SDS', # Midi Sample Dump Standard + 0x120000: 'AVR', # Audio Visual Research + 0x130000: 'WAVEX', # MS WAVE with WAVEFORMATEX + 0x160000: 'SD2', # Sound Designer 2 + 0x170000: 'FLAC', # FLAC lossless file format + 0x180000: 'CAF', # Core Audio File format + 0x190000: 'WVE', # Psion WVE format + 0x200000: 'OGG', # Xiph OGG container + 0x210000: 'MPC2K', # Akai MPC 2000 sampler + 0x220000: 'RF64', # RF64 WAV file +} + +_subtypes = { + 0x0001: 'PCM_S8', # Signed 8 bit data + 0x0002: 'PCM_16', # Signed 16 bit data + 0x0003: 'PCM_24', # Signed 24 bit data + 0x0004: 'PCM_32', # Signed 32 bit data + 0x0005: 'PCM_U8', # Unsigned 8 bit data (WAV and RAW only) + 0x0006: 'FLOAT', # 32 bit float data + 0x0007: 'DOUBLE', # 64 bit float data + 0x0010: 'ULAW', # U-Law encoded. + 0x0011: 'ALAW', # A-Law encoded. + 0x0012: 'IMA_ADPCM', # IMA ADPCM. + 0x0013: 'MS_ADPCM', # Microsoft ADPCM. + 0x0020: 'GSM610', # GSM 6.10 encoding. + 0x0021: 'VOX_ADPCM', # OKI / Dialogix ADPCM + 0x0030: 'G721_32', # 32kbs G721 ADPCM encoding. + 0x0031: 'G723_24', # 24kbs G723 ADPCM encoding. + 0x0032: 'G723_40', # 40kbs G723 ADPCM encoding. + 0x0040: 'DWVW_12', # 12 bit Delta Width Variable Word encoding. + 0x0041: 'DWVW_16', # 16 bit Delta Width Variable Word encoding. + 0x0042: 'DWVW_24', # 24 bit Delta Width Variable Word encoding. + 0x0043: 'DWVW_N', # N bit Delta Width Variable Word encoding. + 0x0050: 'DPCM_8', # 8 bit differential PCM (XI only) + 0x0051: 'DPCM_16', # 16 bit differential PCM (XI only) + 0x0060: 'VORBIS', # Xiph Vorbis encoding. +} + +_endians = { + 0x00000000: 'FILE', # Default file endian-ness. + 0x10000000: 'LITTLE', # Force little endian-ness. + 0x20000000: 'BIG', # Force big endian-ness. + 0x30000000: 'CPU', # Force CPU endian-ness. +} _SUBMASK = 0x0000FFFF _TYPEMASK = 0x0FFF0000 @@ -200,6 +206,27 @@ _GET_FORMAT_INFO = 0x1028 +class FormatType(int): + def __repr__(self): + return _formats.get(self, int.__repr__(self)) + +class SubtypeType(int): + def __repr__(self): + return _subtypes.get(self, int.__repr__(self)) + +class EndianType(int): + def __repr__(self): + return _endians.get(self, int.__repr__(self)) + +for k, v in _formats.items(): + globals()[v] = FormatType(k) + +for k, v in _subtypes.items(): + globals()[v] = SubtypeType(k) + +for k, v in _endians.items(): + globals()[v] = EndianType(k) + _format_by_extension = { 'wav': WAV, 'aif': AIFF, @@ -335,6 +362,7 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, ogg_file. """ + _avoid_format_types(mode, sample_rate, channels) try: mode_int = {'r': _M_READ, 'w': _M_WRITE, @@ -393,9 +421,9 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, self.frames = info.frames self.sample_rate = info.samplerate self.channels = info.channels - self.format = info.format & _TYPEMASK - self.subtype = info.format & _SUBMASK - self.endian = info.format & _ENDMASK + self.format = FormatType(info.format & _TYPEMASK) + self.subtype = SubtypeType(info.format & _SUBMASK) + self.endian = EndianType(info.format & _ENDMASK) self.sections = info.sections self.seekable = info.seekable == 1 @@ -674,6 +702,7 @@ def read(filename, frames=None, start=None, stop=None, **kwargs): def write(data, filename, sample_rate, *args, **kwargs): # e.g. write(myarray, 'myfile.wav', 44100, sf.FLOAT) + _avoid_format_types(sample_rate) if data.ndim == 1: channels = 1 elif data.ndim == 2: @@ -697,3 +726,12 @@ def decode_number(number): for k, v in globals().items(): if not k.startswith('_') and v == number: return k + +def _avoid_format_types(*args): + for arg in args: + if isinstance(arg, FormatType): + raise TypeError("Unexpected FormatType!") + if isinstance(arg, SubtypeType): + raise TypeError("Unexpected SubtypeType!") + if isinstance(arg, EndianType): + raise TypeError("Unexpected EndianType!") From c2e46122cafb161bc5f9ba1e9a6c85022369e46d Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 23:25:06 +0100 Subject: [PATCH 18/33] Proper handling of dtype's --- pysoundfile.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index e7fb814..68ee60b 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -614,7 +614,7 @@ def seek_absolute(self, frames): else: return _snd.sf_seek(self._file, frames, os.SEEK_END) - def read(self, frames=None, format=np.float32): + def read(self, frames=None, dtype=np.float32): """Read a number of frames from the file. Reads the given number of frames in the given data format from @@ -640,15 +640,16 @@ def read(self, frames=None, format=np.float32): np.int32: _snd.sf_readf_int, np.int16: _snd.sf_readf_short } - if format not in formats: + dtype = np.dtype(dtype) + if dtype.type not in formats: raise ValueError("Can only read int16, int32, float32 and float64") if frames is None: curr = self.seek(0) frames = self.frames - curr - data = ffi.new(formats[format], frames*self.channels) - read = readers[format](self._file, data, frames) + data = ffi.new(formats[dtype.type], frames*self.channels) + read = readers[dtype.type](self._file, data, frames) self._handle_error() - np_data = np.frombuffer(ffi.buffer(data), dtype=format, + np_data = np.frombuffer(ffi.buffer(data), dtype=dtype, count=read*self.channels) return np.reshape(np_data, (read, self.channels)) @@ -666,22 +667,23 @@ def write(self, data): if self.mode == 'r': raise RuntimeError("Can not write to read-only file") formats = { - np.dtype(np.float64): 'double*', - np.dtype(np.float32): 'float*', - np.dtype(np.int32): 'int*', - np.dtype(np.int16): 'short*' + np.float64: 'double*', + np.float32: 'float*', + np.int32: 'int*', + np.int16: 'short*' } writers = { - np.dtype(np.float64): _snd.sf_writef_double, - np.dtype(np.float32): _snd.sf_writef_float, - np.dtype(np.int32): _snd.sf_writef_int, - np.dtype(np.int16): _snd.sf_writef_short + np.float64: _snd.sf_writef_double, + np.float32: _snd.sf_writef_float, + np.int32: _snd.sf_writef_int, + np.int16: _snd.sf_writef_short } - if data.dtype not in writers: + if data.dtype.type not in writers: raise ValueError("Data must be int16, int32, float32 or float64") raw_data = ffi.new('char[]', data.flatten().tostring()) - written = writers[data.dtype](self._file, - ffi.cast(formats[data.dtype], raw_data), + written = writers[data.dtype.type](self._file, + ffi.cast( + formats[data.dtype.type], raw_data), len(data)) self._handle_error() return written From ebea05b240cb7f630eaa86e03fbb6b1b440841f8 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 5 Mar 2014 23:48:28 +0100 Subject: [PATCH 19/33] read(): pass dtype to read(), the rest to open() --- pysoundfile.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 68ee60b..423fec1 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -694,12 +694,15 @@ def open(*args, **kwargs): def read(filename, frames=None, start=None, stop=None, **kwargs): # If frames and stop are both specified, frames takes precedence! # start and stop accept negative indices. - with open(filename, 'r') as f: + read_kwargs = {} + if 'dtype' in kwargs: + read_kwargs['dtype'] = kwargs.pop('dtype') + with open(filename, 'r', **kwargs) as f: start, stop, _ = slice(start, stop).indices(f.frames) if frames is None: frames = max(0, stop - start) f.seek_absolute(start) - data = f.read(frames, **kwargs) + data = f.read(frames, **read_kwargs) return data, f.sample_rate def write(data, filename, sample_rate, *args, **kwargs): From ee736abe722e9ec89fa245736250c224dc03d7af Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 6 Mar 2014 14:08:03 +0100 Subject: [PATCH 20/33] Remove code repetition in module setup Also, the quite explicit name _add_formats_to_module_namespace() should now explain what's going on. --- pysoundfile.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 423fec1..31c2061 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -218,14 +218,13 @@ class EndianType(int): def __repr__(self): return _endians.get(self, int.__repr__(self)) -for k, v in _formats.items(): - globals()[v] = FormatType(k) +def _add_formats_to_module_namespace(format_dict, format_type): + for k, v in format_dict.items(): + globals()[v] = format_type(k) -for k, v in _subtypes.items(): - globals()[v] = SubtypeType(k) - -for k, v in _endians.items(): - globals()[v] = EndianType(k) +_add_formats_to_module_namespace(_formats, FormatType) +_add_formats_to_module_namespace(_subtypes, SubtypeType) +_add_formats_to_module_namespace(_endians, EndianType) _format_by_extension = { 'wav': WAV, From 313d3594f4561d36b92500e221811c79046b5590 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 6 Mar 2014 14:11:51 +0100 Subject: [PATCH 21/33] Hide all non-API names in module namespace This is relevant for tools that do instrospection, e.g. for auto-completion in IDEs. --- pysoundfile.py | 107 ++++++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 31c2061..470af3c 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -1,7 +1,6 @@ -import os - -from cffi import FFI -import numpy as np +from cffi import FFI as _FFI +import numpy as _np +import os as _os """PySoundFile is an audio library based on libsndfile, CFFI and Numpy @@ -41,8 +40,8 @@ """ -ffi = FFI() -ffi.cdef(""" +_ffi = _FFI() +_ffi.cdef(""" typedef int64_t sf_count_t ; typedef struct SNDFILE_tag SNDFILE ; @@ -299,7 +298,7 @@ def _add_formats_to_module_namespace(format_dict, format_type): 'genre': _GENRE } -_snd = ffi.dlopen('sndfile') +_snd = _ffi.dlopen('sndfile') class SoundFile(object): @@ -373,7 +372,7 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, if format is None: ext = name.rsplit('.', 1)[-1] format = _format_by_extension.get(ext.lower(), 0x0) - info = ffi.new("SF_INFO*") + info = _ffi.new("SF_INFO*") if mode == 'w' or format == RAW: assert sample_rate, \ "sample_rate must be specified for mode='w' and format=RAW!" @@ -407,12 +406,12 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, msg = 'File-like object must have: "%s"' % attr raise RuntimeError(msg) self._vio = self._init_vio(fObj) - vio = ffi.new("SF_VIRTUAL_IO*", self._vio) + vio = _ffi.new("SF_VIRTUAL_IO*", self._vio) self._vio['vio_cdata'] = vio self._file = _snd.sf_open_virtual(vio, mode_int, info, - ffi.NULL) + _ffi.NULL) else: - filename = ffi.new('char[]', name.encode()) + filename = _ffi.new('char[]', name.encode()) self._file = _snd.sf_open(filename, mode_int, info) self._handle_error() @@ -436,40 +435,40 @@ def subtype_string(self): def _init_vio(self, fObj): # Define callbacks here, so they can reference fObj / size - @ffi.callback("sf_vio_get_filelen") + @_ffi.callback("sf_vio_get_filelen") def vio_get_filelen(user_data): # Streams must set _length or implement __len__ if hasattr(fObj, '_length'): size = fObj._length elif not hasattr(fObj, '__len__'): old_file_position = fObj.tell() - fObj.seek(0, os.SEEK_END) + fObj.seek(0, _os.SEEK_END) size = fObj.tell() - fObj.seek(old_file_position, os.SEEK_SET) + fObj.seek(old_file_position, _os.SEEK_SET) else: size = len(fObj) return size - @ffi.callback("sf_vio_seek") + @_ffi.callback("sf_vio_seek") def vio_seek(offset, whence, user_data): fObj.seek(offset, whence) curr = fObj.tell() return curr - @ffi.callback("sf_vio_read") + @_ffi.callback("sf_vio_read") def vio_read(ptr, count, user_data): - buf = ffi.buffer(ptr, count) + buf = _ffi.buffer(ptr, count) data_read = fObj.readinto(buf) return data_read - @ffi.callback("sf_vio_write") + @_ffi.callback("sf_vio_write") def vio_write(ptr, count, user_data): - buf = ffi.buffer(ptr) + buf = _ffi.buffer(ptr) data = buf[:] length = fObj.write(data) return length - @ffi.callback("sf_vio_tell") + @_ffi.callback("sf_vio_tell") def vio_tell(user_data): return fObj.tell() @@ -506,7 +505,7 @@ def _handle_error_number(self, err): # pretty-print a numerical error code if err != 0: err_str = _snd.sf_error_number(err) - raise RuntimeError(ffi.string(err_str).decode()) + raise RuntimeError(_ffi.string(err_str).decode()) def _getAttributeNames(self): # return all possible attributes used in __setattr__ and __getattr__. @@ -519,7 +518,7 @@ def __setattr__(self, name, value): if self.mode == 'r': raise RuntimeError("Can not change %s of file in read mode" % name) - data = ffi.new('char[]', value.encode()) + data = _ffi.new('char[]', value.encode()) err = _snd.sf_set_string(self._file, _snd_strings[name], data) self._handle_error_number(err) else: @@ -529,10 +528,10 @@ def __getattr__(self, name): # access text data in the sound file through properties if name in _snd_strings: data = _snd.sf_get_string(self._file, _snd_strings[name]) - if data == ffi.NULL: + if data == _ffi.NULL: return "" else: - return ffi.string(data).decode() + return _ffi.string(data).decode() else: raise AttributeError("SoundFile has no attribute %s" % name) @@ -597,7 +596,7 @@ def seek(self, frames): Returns the new absolute read position in frames. """ - return _snd.sf_seek(self._file, frames, os.SEEK_CUR) + return _snd.sf_seek(self._file, frames, _os.SEEK_CUR) def seek_absolute(self, frames): """Set an absolute read position. @@ -609,11 +608,11 @@ def seek_absolute(self, frames): Returns the new absolute read position in frames. """ if frames >= 0: - return _snd.sf_seek(self._file, frames, os.SEEK_SET) + return _snd.sf_seek(self._file, frames, _os.SEEK_SET) else: - return _snd.sf_seek(self._file, frames, os.SEEK_END) + return _snd.sf_seek(self._file, frames, _os.SEEK_END) - def read(self, frames=None, dtype=np.float32): + def read(self, frames=None, dtype='float32'): """Read a number of frames from the file. Reads the given number of frames in the given data format from @@ -628,29 +627,29 @@ def read(self, frames=None, dtype=np.float32): """ formats = { - np.float64: 'double[]', - np.float32: 'float[]', - np.int32: 'int[]', - np.int16: 'short[]' + _np.float64: 'double[]', + _np.float32: 'float[]', + _np.int32: 'int[]', + _np.int16: 'short[]' } readers = { - np.float64: _snd.sf_readf_double, - np.float32: _snd.sf_readf_float, - np.int32: _snd.sf_readf_int, - np.int16: _snd.sf_readf_short + _np.float64: _snd.sf_readf_double, + _np.float32: _snd.sf_readf_float, + _np.int32: _snd.sf_readf_int, + _np.int16: _snd.sf_readf_short } - dtype = np.dtype(dtype) + dtype = _np.dtype(dtype) if dtype.type not in formats: raise ValueError("Can only read int16, int32, float32 and float64") if frames is None: curr = self.seek(0) frames = self.frames - curr - data = ffi.new(formats[dtype.type], frames*self.channels) + data = _ffi.new(formats[dtype.type], frames*self.channels) read = readers[dtype.type](self._file, data, frames) self._handle_error() - np_data = np.frombuffer(ffi.buffer(data), dtype=dtype, + np_data = _np.frombuffer(_ffi.buffer(data), dtype=dtype, count=read*self.channels) - return np.reshape(np_data, (read, self.channels)) + return _np.reshape(np_data, (read, self.channels)) def write(self, data): """Write a number of frames to the file. @@ -666,22 +665,22 @@ def write(self, data): if self.mode == 'r': raise RuntimeError("Can not write to read-only file") formats = { - np.float64: 'double*', - np.float32: 'float*', - np.int32: 'int*', - np.int16: 'short*' + _np.float64: 'double*', + _np.float32: 'float*', + _np.int32: 'int*', + _np.int16: 'short*' } writers = { - np.float64: _snd.sf_writef_double, - np.float32: _snd.sf_writef_float, - np.int32: _snd.sf_writef_int, - np.int16: _snd.sf_writef_short + _np.float64: _snd.sf_writef_double, + _np.float32: _snd.sf_writef_float, + _np.int32: _snd.sf_writef_int, + _np.int16: _snd.sf_writef_short } if data.dtype.type not in writers: raise ValueError("Data must be int16, int32, float32 or float64") - raw_data = ffi.new('char[]', data.flatten().tostring()) + raw_data = _ffi.new('char[]', data.flatten().tostring()) written = writers[data.dtype.type](self._file, - ffi.cast( + _ffi.cast( formats[data.dtype.type], raw_data), len(data)) self._handle_error() @@ -719,11 +718,11 @@ def write(data, filename, sample_rate, *args, **kwargs): assert frames == written, "Error writing file!" def get_format_info(format): - format_info = ffi.new("struct SF_FORMAT_INFO*") + format_info = _ffi.new("struct SF_FORMAT_INFO*") format_info.format = format - _snd.sf_command(ffi.NULL, _GET_FORMAT_INFO, format_info, - ffi.sizeof("SF_FORMAT_INFO")) - return ffi.string(format_info.name).decode() if format_info.name else "" + _snd.sf_command(_ffi.NULL, _GET_FORMAT_INFO, format_info, + _ffi.sizeof("SF_FORMAT_INFO")) + return _ffi.string(format_info.name).decode() if format_info.name else "" def decode_number(number): # e.g. decode_number(myfile.subtype) From 3785edd1a2786ac4549828389a552174686637af Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 6 Mar 2014 14:19:25 +0100 Subject: [PATCH 22/33] Just to be clear: decode_number() wasn't a good idea! There is now a much better (= cleaner and faster) solution involving FormatType, SubtypeType and EndianType. --- pysoundfile.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 470af3c..96ef880 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -724,12 +724,6 @@ def get_format_info(format): _ffi.sizeof("SF_FORMAT_INFO")) return _ffi.string(format_info.name).decode() if format_info.name else "" -def decode_number(number): - # e.g. decode_number(myfile.subtype) - for k, v in globals().items(): - if not k.startswith('_') and v == number: - return k - def _avoid_format_types(*args): for arg in args: if isinstance(arg, FormatType): From 87130b1a4ada582556bada501caef46d53990df0 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 10 Mar 2014 21:32:47 +0100 Subject: [PATCH 23/33] Improve _avoid_format_types() --- pysoundfile.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 96ef880..0ac8fa5 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -360,7 +360,7 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, ogg_file. """ - _avoid_format_types(mode, sample_rate, channels) + _avoid_format_types(sample_rate, channels) try: mode_int = {'r': _M_READ, 'w': _M_WRITE, @@ -725,10 +725,9 @@ def get_format_info(format): return _ffi.string(format_info.name).decode() if format_info.name else "" def _avoid_format_types(*args): + # raise error if one of the arguments has one of the format types. + # This is used to prevent accidentally passing soundfile formats where + # numeric values are expected (especially when using positional arguments). for arg in args: - if isinstance(arg, FormatType): - raise TypeError("Unexpected FormatType!") - if isinstance(arg, SubtypeType): - raise TypeError("Unexpected SubtypeType!") - if isinstance(arg, EndianType): - raise TypeError("Unexpected EndianType!") + if isinstance(arg, (FormatType, SubtypeType, EndianType)): + raise TypeError("%s is not allowed here!" % repr(arg)) From 3abdaf119cbb0ccfd01672a21e85b528ea3cbb6b Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 10 Mar 2014 22:24:56 +0100 Subject: [PATCH 24/33] Change _avoid_format_types() to assertion --- pysoundfile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 0ac8fa5..0d491e4 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -360,7 +360,7 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, ogg_file. """ - _avoid_format_types(sample_rate, channels) + assert _raise_error_if_format_type(sample_rate, channels) try: mode_int = {'r': _M_READ, 'w': _M_WRITE, @@ -705,7 +705,7 @@ def read(filename, frames=None, start=None, stop=None, **kwargs): def write(data, filename, sample_rate, *args, **kwargs): # e.g. write(myarray, 'myfile.wav', 44100, sf.FLOAT) - _avoid_format_types(sample_rate) + assert _raise_error_if_format_type(sample_rate) if data.ndim == 1: channels = 1 elif data.ndim == 2: @@ -724,10 +724,12 @@ def get_format_info(format): _ffi.sizeof("SF_FORMAT_INFO")) return _ffi.string(format_info.name).decode() if format_info.name else "" -def _avoid_format_types(*args): +def _raise_error_if_format_type(*args): # raise error if one of the arguments has one of the format types. - # This is used to prevent accidentally passing soundfile formats where - # numeric values are expected (especially when using positional arguments). + # For use in assertions to prevent accidentally passing soundfile formats + # where numeric values are expected (esp. when using positional arguments). + format_types = (FormatType, SubtypeType, EndianType) for arg in args: - if isinstance(arg, (FormatType, SubtypeType, EndianType)): + if isinstance(arg, format_types): raise TypeError("%s is not allowed here!" % repr(arg)) + return True From f0c7735b817ba6fb90e3e0965570c97a09cb45c0 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 11 Mar 2014 22:41:50 +0100 Subject: [PATCH 25/33] Change mode handling (again), enable file descriptors --- pysoundfile.py | 84 +++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 0d491e4..db68a42 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -114,6 +114,7 @@ } SF_VIRTUAL_IO ; SNDFILE* sf_open_virtual (SF_VIRTUAL_IO *sfvirtual, int mode, SF_INFO *sfinfo, void *user_data) ; +SNDFILE* sf_open_fd (int fd, int mode, SF_INFO *sfinfo, int close_desc) ; typedef struct SF_FORMAT_INFO { @@ -307,8 +308,8 @@ class SoundFile(object): Each SoundFile opens one sound file on the disk. This sound file has a specific samplerate, data format and a set number of - channels. Each sound file can be opened with mode 'r', 'w', or 'rw'. - Note that 'rw' is unsupported for some formats. + channels. Each sound file can be opened with mode 'r', 'w', or 'r+'. + Note that 'r+' is unsupported for some formats. Data can be written to the file using write(), or read from the file using read(). Every read and write operation starts at a @@ -336,11 +337,11 @@ class SoundFile(object): """ - def __init__(self, name, mode='r', sample_rate=None, channels=None, - subtype=None, endian=None, format=None, virtual_io=False): + def __init__(self, file, mode=None, sample_rate=None, channels=None, + subtype=None, endian=None, format=None, closefd=True): """Open a new SoundFile. - If a file is only opened with mode='r' or mode='rw', + If a file is only opened with mode='r' or mode='r+', no sample_rate, channels or file format need to be given. If a file is opened with mode='w', you must provide a sample_rate, a number of channels, and a file format. An exception is the @@ -360,20 +361,27 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, ogg_file. """ - assert _raise_error_if_format_type(sample_rate, channels) - try: - mode_int = {'r': _M_READ, - 'w': _M_WRITE, - 'rw': _M_RDWR}[mode] - except KeyError: - raise ValueError("invalid mode: " + mode) - self.mode = mode + assert _raise_error_if_format_type(file, sample_rate, channels) + + mode = mode or getattr(file, 'mode','r') + mode_chars = set(mode) + mode_chars.add('b') + if mode_chars == set('rb'): + self.mode = _M_READ + elif mode_chars == set('wb'): + self.mode = _M_WRITE + elif mode_chars == set('r+b'): + self.mode = _M_RDWR + else: + raise ValueError("Invalid mode: " + mode) + original_format, original_endian = format, endian - if format is None: - ext = name.rsplit('.', 1)[-1] + if format is None and isinstance(file, str): + ext = file.rsplit('.', 1)[-1] format = _format_by_extension.get(ext.lower(), 0x0) + info = _ffi.new("SF_INFO*") - if mode == 'w' or format == RAW: + if self.mode == _M_WRITE or format == RAW: assert sample_rate, \ "sample_rate must be specified for mode='w' and format=RAW!" info.samplerate = sample_rate @@ -392,28 +400,27 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, assert _snd.sf_format_check(info), \ "Invalid combination of format, subtype and endian!" else: - should_be_none = [sample_rate, channels, subtype, - original_endian, original_format] - if should_be_none != [None] * len(should_be_none): - raise RuntimeError("If mode='r', none of these arguments are " - "allowed: sample_rate, channels, format, " - "subtype, endian") - - if virtual_io: - fObj = name + assert [sample_rate, channels, subtype, original_endian, + original_format] == [None] * 5, \ + "Only allowed if mode='w' or format=RAW: sample_rate, " \ + "channels, format, subtype, endian" + + if isinstance(file, str): + file = _ffi.new('char[]', file.encode()) + self._file = _snd.sf_open(file, self.mode, info) + elif isinstance(file, int): + self._file = _snd.sf_open_fd(file, self.mode, info, closefd) + else: for attr in ('seek', 'read', 'write', 'tell'): - if not hasattr(fObj, attr): - msg = 'File-like object must have: "%s"' % attr + if not hasattr(file, attr): + msg = "file must be a filename, a file descriptor or " \ + "a file-like object with the methods " \ + "'seek()', 'read()', 'write()' and 'tell()'!" raise RuntimeError(msg) - self._vio = self._init_vio(fObj) - vio = _ffi.new("SF_VIRTUAL_IO*", self._vio) - self._vio['vio_cdata'] = vio - self._file = _snd.sf_open_virtual(vio, mode_int, info, + file = self._init_vio(file) + self._vio = _ffi.new("SF_VIRTUAL_IO*", file) + self._file = _snd.sf_open_virtual(self._vio, self.mode, info, _ffi.NULL) - else: - filename = _ffi.new('char[]', name.encode()) - self._file = _snd.sf_open(filename, mode_int, info) - self._handle_error() self.frames = info.frames @@ -424,6 +431,7 @@ def __init__(self, name, mode='r', sample_rate=None, channels=None, self.endian = EndianType(info.format & _ENDMASK) self.sections = info.sections self.seekable = info.seekable == 1 + self.closed = False @property def format_string(self): @@ -515,7 +523,7 @@ def _getAttributeNames(self): def __setattr__(self, name, value): # access text data in the sound file through properties if name in _snd_strings: - if self.mode == 'r': + if self.mode == _M_READ: raise RuntimeError("Can not change %s of file in read mode" % name) data = _ffi.new('char[]', value.encode()) @@ -572,7 +580,7 @@ def __setitem__(self, frame, data): # access the file as if it where a one-dimensional Numpy # array. Data must be in the form (frames x channels). # Both open slice bounds and negative values are allowed. - if self.mode == 'r': + if self.mode == _M_READ: raise RuntimeError("Can not write to read-only file") start, stop = self._get_slice_bounds(frame) if stop - start != len(data): @@ -662,7 +670,7 @@ def write(self, data): array. """ - if self.mode == 'r': + if self.mode == _M_READ: raise RuntimeError("Can not write to read-only file") formats = { _np.float64: 'double*', From bc9a381ca3402c688e3b4b6d99c492d94e3dbb19 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 00:01:48 +0100 Subject: [PATCH 26/33] Change public members to properties --- pysoundfile.py | 83 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index db68a42..fe5a7e4 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -367,11 +367,11 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, mode_chars = set(mode) mode_chars.add('b') if mode_chars == set('rb'): - self.mode = _M_READ + self._mode = _M_READ elif mode_chars == set('wb'): - self.mode = _M_WRITE + self._mode = _M_WRITE elif mode_chars == set('r+b'): - self.mode = _M_RDWR + self._mode = _M_RDWR else: raise ValueError("Invalid mode: " + mode) @@ -380,14 +380,14 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, ext = file.rsplit('.', 1)[-1] format = _format_by_extension.get(ext.lower(), 0x0) - info = _ffi.new("SF_INFO*") - if self.mode == _M_WRITE or format == RAW: + self._info = _ffi.new("SF_INFO*") + if self._mode == _M_WRITE or format == RAW: assert sample_rate, \ "sample_rate must be specified for mode='w' and format=RAW!" - info.samplerate = sample_rate + self._info.samplerate = sample_rate assert channels, \ "channels must be specified for mode='w' and format=RAW!" - info.channels = channels + self._info.channels = channels if subtype is None: subtype = _default_subtypes.get(format, 0x0) endian = endian or FILE @@ -396,8 +396,8 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, assert format & _TYPEMASK, "Invalid format!" assert format & _SUBMASK, "Invalid subtype!" assert endian == FILE or format & _ENDMASK, "Invalid endian-ness!" - info.format = format - assert _snd.sf_format_check(info), \ + self._info.format = format + assert _snd.sf_format_check(self._info), \ "Invalid combination of format, subtype and endian!" else: assert [sample_rate, channels, subtype, original_endian, @@ -407,9 +407,9 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, if isinstance(file, str): file = _ffi.new('char[]', file.encode()) - self._file = _snd.sf_open(file, self.mode, info) + self._file = _snd.sf_open(file, self._mode, self._info) elif isinstance(file, int): - self._file = _snd.sf_open_fd(file, self.mode, info, closefd) + self._file = _snd.sf_open_fd(file, self._mode, self._info, closefd) else: for attr in ('seek', 'read', 'write', 'tell'): if not hasattr(file, attr): @@ -419,28 +419,61 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, raise RuntimeError(msg) file = self._init_vio(file) self._vio = _ffi.new("SF_VIRTUAL_IO*", file) - self._file = _snd.sf_open_virtual(self._vio, self.mode, info, - _ffi.NULL) + self._file = _snd.sf_open_virtual(self._vio, self._mode, + self._info, _ffi.NULL) self._handle_error() - self.frames = info.frames - self.sample_rate = info.samplerate - self.channels = info.channels - self.format = FormatType(info.format & _TYPEMASK) - self.subtype = SubtypeType(info.format & _SUBMASK) - self.endian = EndianType(info.format & _ENDMASK) - self.sections = info.sections - self.seekable = info.seekable == 1 - self.closed = False + @property + def closed(self): + return self._file is None + + @property + def mode(self): + return {_M_READ: 'r', _M_WRITE: 'w', _M_RDWR: 'r+'}[self._mode] + + @property + def frames(self): + curr = self.seek(0) + frames = _snd.sf_seek(self._file, 0, _os.SEEK_END) + self.seek_absolute(curr) + return frames + + @property + def sample_rate(self): + return self._info.samplerate + + @property + def channels(self): + return self._info.channels + + @property + def format(self): + return FormatType(self._info.format & _TYPEMASK) @property def format_string(self): return get_format_info(self.format) + @property + def subtype(self): + return SubtypeType(self._info.format & _SUBMASK) + @property def subtype_string(self): return get_format_info(self.subtype) + @property + def endian(self): + return EndianType(self._info.format & _ENDMASK) + + @property + def sections(self): + return self._info.sections + + @property + def seekable(self): + return self._info.seekable == 1 + def _init_vio(self, fObj): # Define callbacks here, so they can reference fObj / size @_ffi.callback("sf_vio_get_filelen") @@ -523,7 +556,7 @@ def _getAttributeNames(self): def __setattr__(self, name, value): # access text data in the sound file through properties if name in _snd_strings: - if self.mode == _M_READ: + if self._mode == _M_READ: raise RuntimeError("Can not change %s of file in read mode" % name) data = _ffi.new('char[]', value.encode()) @@ -580,7 +613,7 @@ def __setitem__(self, frame, data): # access the file as if it where a one-dimensional Numpy # array. Data must be in the form (frames x channels). # Both open slice bounds and negative values are allowed. - if self.mode == _M_READ: + if self._mode == _M_READ: raise RuntimeError("Can not write to read-only file") start, stop = self._get_slice_bounds(frame) if stop - start != len(data): @@ -670,7 +703,7 @@ def write(self, data): array. """ - if self.mode == _M_READ: + if self._mode == _M_READ: raise RuntimeError("Can not write to read-only file") formats = { _np.float64: 'double*', From 128ddffae978012d52612692ba4de2c74ec0cbc3 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 11:36:16 +0100 Subject: [PATCH 27/33] Add a missing space --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index fe5a7e4..e6641c8 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -363,7 +363,7 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, """ assert _raise_error_if_format_type(file, sample_rate, channels) - mode = mode or getattr(file, 'mode','r') + mode = mode or getattr(file, 'mode', 'r') mode_chars = set(mode) mode_chars.add('b') if mode_chars == set('rb'): From ffa0c83039d31063d0a5c94ea008e8046334cd1c Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 11:46:06 +0100 Subject: [PATCH 28/33] Make properties much more concise --- pysoundfile.py | 62 ++++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index e6641c8..03b624e 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -423,57 +423,33 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, self._info, _ffi.NULL) self._handle_error() - @property - def closed(self): - return self._file is None - - @property - def mode(self): - return {_M_READ: 'r', _M_WRITE: 'w', _M_RDWR: 'r+'}[self._mode] + sample_rate = property(lambda self: self._info.samplerate) + channels = property(lambda self: self._info.channels) + format = property(lambda self: FormatType(self._info.format & _TYPEMASK)) + subtype = property(lambda self: SubtypeType(self._info.format & _SUBMASK)) + endian = property(lambda self: EndianType(self._info.format & _ENDMASK)) + format_string = property(lambda self: get_format_info(self.format)) + subtype_string = property(lambda self: get_format_info(self.subtype)) + sections = property(lambda self: self._info.sections) + seekable = property(lambda self: self._info.seekable == 1) + closed = property(lambda self: self._file is None) + mode = property(lambda self: {_M_READ: 'r', + _M_WRITE: 'w', + _M_RDWR: 'r+'}[self._mode]) @property def frames(self): + """Return number of frames by seeking to the end of the file. + + This should give the right result even after write operations. + It doesn't work, however, if the file is closed. + + """ curr = self.seek(0) frames = _snd.sf_seek(self._file, 0, _os.SEEK_END) self.seek_absolute(curr) return frames - @property - def sample_rate(self): - return self._info.samplerate - - @property - def channels(self): - return self._info.channels - - @property - def format(self): - return FormatType(self._info.format & _TYPEMASK) - - @property - def format_string(self): - return get_format_info(self.format) - - @property - def subtype(self): - return SubtypeType(self._info.format & _SUBMASK) - - @property - def subtype_string(self): - return get_format_info(self.subtype) - - @property - def endian(self): - return EndianType(self._info.format & _ENDMASK) - - @property - def sections(self): - return self._info.sections - - @property - def seekable(self): - return self._info.seekable == 1 - def _init_vio(self, fObj): # Define callbacks here, so they can reference fObj / size @_ffi.callback("sf_vio_get_filelen") From 5a4d76efb67645989e6e7dfef1e2c2588260b021 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 14:52:45 +0100 Subject: [PATCH 29/33] Seek overhaul --- pysoundfile.py | 98 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 03b624e..c13a704 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -314,7 +314,7 @@ class SoundFile(object): Data can be written to the file using write(), or read from the file using read(). Every read and write operation starts at a certain position in the file. Reading N frames will change this - position by N frames as well. Alternatively, seek() and + position by N frames as well. Alternatively, seek_relative() and seek_absolute() can be used to set the current position to a frame index offset from the current position, the start of the file, or the end of the file, respectively. @@ -445,7 +445,7 @@ def frames(self): It doesn't work, however, if the file is closed. """ - curr = self.seek(0) + curr = self.seek_relative(0) frames = _snd.sf_seek(self._file, 0, _os.SEEK_END) self.seek_absolute(curr) return frames @@ -576,7 +576,7 @@ def __getitem__(self, frame): "SoundFile can only be accessed in one or two dimensions") frame, second_frame = frame start, stop = self._get_slice_bounds(frame) - curr = self.seek(0) + curr = self.seek_relative(0) self.seek_absolute(start) data = self.read(stop - start) self.seek_absolute(curr) @@ -596,7 +596,7 @@ def __setitem__(self, frame, data): raise IndexError( "Could not fit data of length %i into slice of length %i" % (len(data), stop - start)) - curr = self.seek(0) + curr = self.seek_relative(0) self.seek_absolute(start) self.write(data) self.seek_absolute(curr) @@ -606,28 +606,88 @@ def flush(self): """Write unwritten data to disk.""" _snd.sf_write_sync(self._file) - def seek(self, frames): - """Set the read position relative to the current position. + def _check_seek_arguments(self, both, w, r): + if self._mode == _M_WRITE: + raise RuntimeError("Seeking is not possible if mode='w'!") + if [both, w, r] == [None] * 3: + raise ValueError("At least one argument needed!") + assert both is None or r is None, "Either both or r must be None!" + + original_r, original_w = r, w + try: + r, w = both + both = None + assert original_r is None and original_w is None, \ + "If both has two items, r and w must be None!" + except (TypeError, ValueError): + pass # ignore errors and continue + + if (r, w) == (None, None) and self._mode != _M_READ: + w = both + if r is None: + r = both + + if self._mode == _M_READ and w is not None: + raise ValueError("Trying to set write position in read mode!") + if self._mode == _M_WRITE and r is not None: + raise ValueError("Trying to set read position in write mode!") + + # at least one of (r, w) is not None! + return r, w + + def seek_relative(self, both=None, w=None, r=None): + """Set the read/write positions relative to the current position. Positive values will fast-forward. Negative values will rewind. - Returns the new absolute read position in frames. + Returns the new absolute read/write positions in frames. + """ - return _snd.sf_seek(self._file, frames, _os.SEEK_CUR) + r, w = self._check_seek_arguments(both, w, r) + + def seek_helper(frames, flag=0x0): + offset = _snd.sf_seek(self._file, frames, _os.SEEK_CUR | flag) + if offset < 0: + raise IndexError("Cannot seek to %d" % frames) + return offset + + if w is None: + return seek_helper(r, _M_READ) + elif r is None: + return seek_helper(w, _M_WRITE) + else: + return seek_helper(r, _M_READ), seek_helper(w, _M_WRITE) - def seek_absolute(self, frames): - """Set an absolute read position. + def seek_absolute(self, both=None, w=None, r=None): + """Set absolute read/write positions. - Positive values will set the read position to the given frame - index. Negative values will set the read position to the given + Positive values will set the read/write position to the given frame + index. Negative values will set the read/write position to the given index counted from the end of the file. - Returns the new absolute read position in frames. + Returns the new absolute read/write positions in frames. + """ - if frames >= 0: - return _snd.sf_seek(self._file, frames, _os.SEEK_SET) + r, w = self._check_seek_arguments(both, w, r) + + def seek_helper(frames, flag=0x0): + if frames >= 0: + offset = _snd.sf_seek(self._file, frames, _os.SEEK_SET | flag) + else: + offset = _snd.sf_seek(self._file, frames, _os.SEEK_END | flag) + if offset < 0: + raise IndexError("Cannot seek to %d" % frames) + return offset + + if w is None: + return seek_helper(r, _M_READ) + elif r is None: + return seek_helper(w, _M_WRITE) + elif r == w: + offset = seek_helper(r) + return offset, offset else: - return _snd.sf_seek(self._file, frames, _os.SEEK_END) + return seek_helper(r, _M_READ), seek_helper(w, _M_WRITE) def read(self, frames=None, dtype='float32'): """Read a number of frames from the file. @@ -643,6 +703,8 @@ def read(self, frames=None, dtype='float32'): smaller NumPy array will be returned. """ + if self._mode == _M_WRITE: + raise RuntimeError("Can not read if mode='w'!") formats = { _np.float64: 'double[]', _np.float32: 'float[]', @@ -659,7 +721,7 @@ def read(self, frames=None, dtype='float32'): if dtype.type not in formats: raise ValueError("Can only read int16, int32, float32 and float64") if frames is None: - curr = self.seek(0) + curr = self.seek_relative(0) frames = self.frames - curr data = _ffi.new(formats[dtype.type], frames*self.channels) read = readers[dtype.type](self._file, data, frames) @@ -680,7 +742,7 @@ def write(self, data): """ if self._mode == _M_READ: - raise RuntimeError("Can not write to read-only file") + raise RuntimeError("Can not write if mode='r'!") formats = { _np.float64: 'double*', _np.float32: 'float*', From 29a06f1ecffbe3b43b96a30ddecb3d0a41cf43e4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 16:13:44 +0100 Subject: [PATCH 30/33] Fix frames property Special treatment for modes 'w' and 'r+'. --- pysoundfile.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index c13a704..e6329b1 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -422,7 +422,11 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, self._file = _snd.sf_open_virtual(self._vio, self._mode, self._info, _ffi.NULL) self._handle_error() + if self._mode == _M_WRITE: + # this is not set by libsndfile: + self._info.frames = 0 + frames = property(lambda self: self._info.frames) sample_rate = property(lambda self: self._info.samplerate) channels = property(lambda self: self._info.channels) format = property(lambda self: FormatType(self._info.format & _TYPEMASK)) @@ -437,19 +441,6 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, _M_WRITE: 'w', _M_RDWR: 'r+'}[self._mode]) - @property - def frames(self): - """Return number of frames by seeking to the end of the file. - - This should give the right result even after write operations. - It doesn't work, however, if the file is closed. - - """ - curr = self.seek_relative(0) - frames = _snd.sf_seek(self._file, 0, _os.SEEK_END) - self.seek_absolute(curr) - return frames - def _init_vio(self, fObj): # Define callbacks here, so they can reference fObj / size @_ffi.callback("sf_vio_get_filelen") @@ -763,6 +754,15 @@ def write(self, data): formats[data.dtype.type], raw_data), len(data)) self._handle_error() + + if self._mode == _M_RDWR: + curr = self.seek_relative(0) + self._info.frames = _snd.sf_seek(self._file, 0, _os.SEEK_END) + self.seek_absolute(curr) + else: + # in mode='w', seeking is not possible + self._info.frames += written + return written def open(*args, **kwargs): From b00d0521e1a2e4d44ae31d2c20f2ae20479627a7 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 17:04:39 +0100 Subject: [PATCH 31/33] Fix wrong use of seek_relative() --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index e6329b1..979c9e3 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -712,7 +712,7 @@ def read(self, frames=None, dtype='float32'): if dtype.type not in formats: raise ValueError("Can only read int16, int32, float32 and float64") if frames is None: - curr = self.seek_relative(0) + curr = self.seek_relative(r=0) frames = self.frames - curr data = _ffi.new(formats[dtype.type], frames*self.channels) read = readers[dtype.type](self._file, data, frames) From 187bb86cf31d0fc330d9a20c58ce7bea3d67b3ff Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 12 Mar 2014 17:48:54 +0100 Subject: [PATCH 32/33] Add 'name' property --- pysoundfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 979c9e3..471d41d 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -405,6 +405,7 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, "Only allowed if mode='w' or format=RAW: sample_rate, " \ "channels, format, subtype, endian" + self._name = file if isinstance(file, str): file = _ffi.new('char[]', file.encode()) self._file = _snd.sf_open(file, self._mode, self._info) @@ -417,6 +418,7 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, "a file-like object with the methods " \ "'seek()', 'read()', 'write()' and 'tell()'!" raise RuntimeError(msg) + self._name = str(file) file = self._init_vio(file) self._vio = _ffi.new("SF_VIRTUAL_IO*", file) self._file = _snd.sf_open_virtual(self._vio, self._mode, @@ -426,6 +428,7 @@ def __init__(self, file, mode=None, sample_rate=None, channels=None, # this is not set by libsndfile: self._info.frames = 0 + name = property(lambda self: self._name) frames = property(lambda self: self._info.frames) sample_rate = property(lambda self: self._info.samplerate) channels = property(lambda self: self._info.channels) From 08cb76bb72265f84af99949594ea16168fcaf996 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 14 Mar 2014 11:25:33 +0100 Subject: [PATCH 33/33] Add new default subtypes Mostly PCM_16, as supported according to the table at http://www.mega-nerd.com/libsndfile/ --- pysoundfile.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 471d41d..8989bd2 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -258,26 +258,27 @@ def _add_formats_to_module_namespace(format_dict, format_type): #'vox': RAW | VOX_ADPCM, } +# see http://www.mega-nerd.com/libsndfile/ for supported subtypes _default_subtypes = { WAV: PCM_16, AIFF: PCM_16, AU: PCM_16, #RAW: # subtype must be explicit! - #PAF: - #SVX: - #NIST: - #VOC: - #IRCAM: - #W64: + PAF: PCM_16, + SVX: PCM_16, + NIST: PCM_16, + VOC: PCM_16, + IRCAM: PCM_16, + W64: PCM_16, MAT4: DOUBLE, MAT5: DOUBLE, - #PVF: - #XI: - #HTK: + PVF: PCM_16, + XI: DPCM_16, + HTK: PCM_16, #SDS: #AVR: WAVEX: PCM_16, - #SD2: + SD2: PCM_16, FLAC: PCM_16, CAF: PCM_16, #WVE: