From 60f2fcdba527ad609eb67677e88b5b5244ada8fa Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 21:42:14 +0200 Subject: [PATCH 01/11] Move dictionary for string types out of SoundFile class --- pysoundfile.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 36a212b..5899dbf 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -123,6 +123,19 @@ 0x30: 'RDWR' } +_str_types = { + 'title': 0x01, + 'copyright': 0x02, + 'software': 0x03, + 'artist': 0x04, + 'comment': 0x05, + 'date': 0x06, + 'album': 0x07, + 'license': 0x08, + 'tracknumber': 0x09, + 'genre': 0x10, +} + snd_types = { 'WAV': 0x010000, # Microsoft WAV format (little endian default). 'AIFF': 0x020000, # Apple/SGI AIFF format (big endian). @@ -401,37 +414,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 _str_types: if self.mode == READ: 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, _str_types[name], data) self._handle_error_number(err) else: self.__dict__[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 _str_types: + data = _snd.sf_get_string(self._file, _str_types[name]) if data == ffi.NULL: return "" else: From 0134103987226e25b6686ec8e1fd7015fde50a46 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 21:46:14 +0200 Subject: [PATCH 02/11] 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 5899dbf..2dab166 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -438,7 +438,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 62edcba7c0c3cfc5bee622411cc311fe29d0547f Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 21:48:04 +0200 Subject: [PATCH 03/11] 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 2dab166..f04a7f6 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -424,7 +424,7 @@ def __setattr__(self, name, value): err = _snd.sf_set_string(self._file, _str_types[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 be5aee4a32b43ab6af11332704de2d95129aada1 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 21:50:59 +0200 Subject: [PATCH 04/11] Fix a seek-related bug --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index f04a7f6..bbf08ef 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -543,7 +543,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: - curr = self.seek(0) + curr = self.seek(0, SEEK_CUR | READ) frames = self.frames - curr data = ffi.new(formats[format], frames*self.channels) read = readers[format](self._file, data, frames) From 90e16d4b0cf03476dc7233e105931f82b59f8d90 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 21:53:02 +0200 Subject: [PATCH 05/11] Read whole file if frames is negative ... in contrast to if frames == -1 This corresponds to the behaviour of standard Python file objects. --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index bbf08ef..eb390fa 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -542,7 +542,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 < 0: curr = self.seek(0, SEEK_CUR | READ) frames = self.frames - curr data = ffi.new(formats[format], frames*self.channels) From e01b7ab63a8fea69ce614e0456ad529e9675edb2 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 22:05:08 +0200 Subject: [PATCH 06/11] Add close() method Closes #14 --- pysoundfile.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index eb390fa..0fd6d60 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -340,6 +340,11 @@ def __init__(self, name, mode=READ, sample_rate=0, channels=0, format=0, self.sections = info.sections self.seekable = info.seekable == 1 + closed = property(lambda self: self._file is None) + + # avoid confusion if something goes wrong before assigning self._file: + _file = None + def _init_vio(self, fObj): # Define callbacks here, so they can reference fObj / size @ffi.callback("sf_vio_get_filelen") @@ -389,22 +394,17 @@ def vio_tell(user_data): return vio def __del__(self): - # be sure to flush data to disk before closing the file - if self._file: - _snd.sf_write_sync(self._file) - err = _snd.sf_close(self._file) - self._handle_error_number(err) - self._file = None + self.close() def __enter__(self): return self - def __exit__(self, type, value, tb): - # flush remaining data to disk and close file - self.__del__() + def __exit__(self, *args): + self.close() def _handle_error(self): # this checks the error flag of the SNDFILE* structure + self._check_if_closed() err = _snd.sf_error(self._file) self._handle_error_number(err) @@ -414,9 +414,16 @@ def _handle_error_number(self, err): err_str = _snd.sf_error_number(err) raise RuntimeError(ffi.string(err_str).decode()) + def _check_if_closed(self): + # check if the file is closed and raise an error if it is. + # This should be used in every method that tries to access self._file. + if self.closed: + raise ValueError("I/O operation on closed file") + def __setattr__(self, name, value): # access text data in the sound file through properties if name in _str_types: + self._check_if_closed() if self.mode == READ: raise RuntimeError("Can not change %s of file in read mode" % name) @@ -429,6 +436,7 @@ def __setattr__(self, name, value): def __getattr__(self, name): # access text data in the sound file through properties if name in _str_types: + self._check_if_closed() data = _snd.sf_get_string(self._file, _str_types[name]) if data == ffi.NULL: return "" @@ -489,8 +497,18 @@ def __setitem__(self, frame, data): def flush(self): """Write unwritten data to disk.""" + self._check_if_closed() _snd.sf_write_sync(self._file) + def close(self): + """Close the file. Can be called multiple times.""" + if not self.closed: + # be sure to flush data to disk before closing the file + self.flush() + err = _snd.sf_close(self._file) + self._file = None + self._handle_error_number(err) + def seek(self, frames, whence=SEEK_SET): """Set the read and/or write position. @@ -510,6 +528,7 @@ def seek(self, frames, whence=SEEK_SET): Returns the new absolute read position in frames or a negative value on error. """ + self._check_if_closed() return _snd.sf_seek(self._file, frames, whence) def read(self, frames=-1, format=np.float32): @@ -526,6 +545,7 @@ def read(self, frames=-1, format=np.float32): smaller NumPy array will be returned. """ + self._check_if_closed() if self.mode == WRITE: raise RuntimeError("Cannot read from file opened in WRITE mode!") formats = { @@ -563,6 +583,7 @@ def write(self, data): array. """ + self._check_if_closed() if self.mode == READ: raise RuntimeError("Cannot write to file opened in READ mode!") formats = { From fc956e93cd5e464573e51778fe19179806dd1426 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 22:16:18 +0200 Subject: [PATCH 07/11] Return float64 by default Closes #17 --- pysoundfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 0fd6d60..2581a5b 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -29,7 +29,7 @@ At the same time, SoundFiles act as sequence types, so you can use slices to read or write data as well. Since there is no way of specifying data formats for slices, the SoundFile will always return -float32 data for those. +float64 data for those. Note that you need to have libsndfile installed in order to use PySoundFile. On Windows, you need to rename the library to @@ -256,7 +256,7 @@ class SoundFile(object): Alternatively, slices can be used to access data at arbitrary positions in the file. Note that slices currently only work on frame indices, not channels. The quickest way to read in a whole - file as a float32 NumPy array is in fact SoundFile('filename')[:]. + file as a float64 NumPy array is in fact SoundFile('filename')[:]. All data access uses frames as index. A frame is one discrete time-step in the sound file. Every frame contains as many samples @@ -531,7 +531,7 @@ def seek(self, frames, whence=SEEK_SET): self._check_if_closed() return _snd.sf_seek(self._file, frames, whence) - def read(self, frames=-1, format=np.float32): + def read(self, frames=-1, format=np.float64): """Read a number of frames from the file. Reads the given number of frames in the given data format from From 58fbaa5201cc72449f7cfb71c0f224062792605a Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 22:21:55 +0200 Subject: [PATCH 08/11] Fix missing 'count' in vio_write() --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index 2581a5b..9765acd 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -375,7 +375,7 @@ def vio_read(ptr, count, user_data): @ffi.callback("sf_vio_write") def vio_write(ptr, count, user_data): - buf = ffi.buffer(ptr) + buf = ffi.buffer(ptr, count) data = buf[:] length = fObj.write(data) return length From 17db9714c542d54de47595bc3e9f049171bf8bc7 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 22:26:32 +0200 Subject: [PATCH 09/11] Add _getAttributeNames() This is useful for auto-completion in IDEs. --- pysoundfile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pysoundfile.py b/pysoundfile.py index 9765acd..fcb0776 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -414,6 +414,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 _str_types + def _check_if_closed(self): # check if the file is closed and raise an error if it is. # This should be used in every method that tries to access self._file. From 0bb8bd6b238b39a589008613c93ae929308d8a90 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 22:31:14 +0200 Subject: [PATCH 10/11] Make __getattr__() more compact --- pysoundfile.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index fcb0776..6b25089 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -443,12 +443,9 @@ def __getattr__(self, name): if name in _str_types: self._check_if_closed() data = _snd.sf_get_string(self._file, _str_types[name]) - if data == ffi.NULL: - return "" - else: - return ffi.string(data).decode() + return ffi.string(data).decode() if data else "" else: - raise AttributeError("SoundFile has no attribute %s" % name) + raise AttributeError("SoundFile has no attribute %s" % repr(name)) def __len__(self): return self.frames From 47f003f4ba0e1920aacaca69d9b07846593bb423 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Apr 2014 22:35:06 +0200 Subject: [PATCH 11/11] Remove exclamation marks from error messages --- pysoundfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 6b25089..70616dd 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -456,7 +456,7 @@ def _get_slice_bounds(self, frame): frame = slice(frame, frame + 1) start, stop, step = frame.indices(len(self)) if step != 1: - raise RuntimeError("Step size must be 1!") + raise RuntimeError("Step size must be 1") if start > stop: stop = start return start, stop @@ -485,7 +485,7 @@ def __setitem__(self, frame, data): # array. Data must be in the form (frames x channels). # Both open slice bounds and negative values are allowed. if self.mode == READ: - raise RuntimeError("Cannot write to file opened in READ mode!") + raise RuntimeError("Cannot write to file opened in READ mode") start, stop = self._get_slice_bounds(frame) if stop - start != len(data): raise IndexError( @@ -549,7 +549,7 @@ def read(self, frames=-1, format=np.float64): """ self._check_if_closed() if self.mode == WRITE: - raise RuntimeError("Cannot read from file opened in WRITE mode!") + raise RuntimeError("Cannot read from file opened in WRITE mode") formats = { np.float64: 'double[]', np.float32: 'float[]', @@ -587,7 +587,7 @@ def write(self, data): """ self._check_if_closed() if self.mode == READ: - raise RuntimeError("Cannot write to file opened in READ mode!") + raise RuntimeError("Cannot write to file opened in READ mode") formats = { np.dtype(np.float64): 'double*', np.dtype(np.float32): 'float*',