From a3295f7fe118c82ed53257625966c07964adab30 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 13 Mar 2014 23:03:03 +0100 Subject: [PATCH 1/2] Change mode handling Now the integer mode constants are called READ, WRITE and RDWR. They are still in the module namespace, but now they have their own type _ModeType (derived from int). Alternatively, 'r'/'w'/'rw' and 'READ'/'WRITE'/'RDWR' (case-insensitive) can be used in the SoundFile constructor. This includes the strings suggested by @bastibe in #14. The strings 'r+'/'w+'/'a'/'a+'/... from the standard Python file objects are not used, because their behaviour is different from the modes in libsndfile which would lead to confusion. The mode is now the second constructor argument, right after the file name. This way, 'rw' can be used as positional parameter: f = SoundFile('myfile.wav', 'rw') --- pysoundfile.py | 66 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index d6c7ffe..d78a36a 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -118,9 +118,11 @@ """) -read_mode = 0x10 -write_mode = 0x20 -read_write_mode = 0x30 +_open_modes = { + 0x10: 'READ', + 0x20: 'WRITE', + 0x30: 'RDWR' +} snd_types = { 'WAV': 0x010000, # Microsoft WAV format (little endian default). @@ -207,6 +209,18 @@ def reverse_dict(d): return {value:key for key, value in d.items()} return (type, subtype, endianness) + +class _ModeType(int): + def __repr__(self): + return _open_modes.get(self, int.__repr__(self)) + + +def _add_constants_to_module_namespace(constants_dict, constants_type): + for k, v in constants_dict.items(): + globals()[v] = constants_type(k) + +_add_constants_to_module_namespace(_open_modes, _ModeType) + _snd = ffi.dlopen('sndfile') @@ -216,9 +230,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 one of the modes + READ/WRITE/RDWR. Note that RDWR 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 @@ -246,17 +259,21 @@ class SoundFile(object): """ - def __init__(self, name, sample_rate=0, channels=0, format=0, - mode=read_mode, virtual_io=False): + def __init__(self, name, mode=READ, sample_rate=0, channels=0, format=0, + 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 opened with mode READ or WRITE, 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 RDWR, 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. + Instead of the library constants READ/WRITE/RDWR you can also + use the (case-insensitive) strings 'r'/'w'/'rw' or + 'READ'/'WRITE'/'RDWR'. + File formats consist of three parts: - one of the file types from snd_types - one of the data types from snd_subtypes @@ -276,7 +293,17 @@ 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 + + if isinstance(mode, str): + try: + mode = {'read': READ, 'r': READ, + 'write': WRITE, 'w': WRITE, + 'rdwr': RDWR, 'rw': RDWR}[mode.lower()] + except KeyError: + pass + if not isinstance(mode, _ModeType): + raise ValueError("Invalid mode: %s" % repr(mode)) + self.mode = mode if virtual_io: fObj = name @@ -287,11 +314,10 @@ 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, - ffi.NULL) + self._file = _snd.sf_open_virtual(vio, mode, 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, info) self._handle_error() @@ -394,7 +420,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 == READ: raise RuntimeError("Can not change %s of file in read mode" % name) data = ffi.new('char[]', value.encode()) @@ -451,8 +477,8 @@ 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: - raise RuntimeError("Can not write to read-only file") + if self.mode == READ: + raise RuntimeError("Cannot write to file opened in READ mode!") start, stop = self._get_slice_bounds(frame) if stop - start != len(data): raise IndexError( @@ -505,6 +531,8 @@ def read(self, frames=-1, format=np.float32): smaller NumPy array will be returned. """ + if self.mode == WRITE: + raise RuntimeError("Cannot read from file opened in WRITE mode!") formats = { np.float64: 'double[]', np.float32: 'float[]', @@ -540,8 +568,8 @@ def write(self, data): array. """ - if self._file_mode == read_mode: - raise RuntimeError("Can not write to read-only file") + if self.mode == READ: + raise RuntimeError("Cannot write to file opened in READ mode!") formats = { np.dtype(np.float64): 'double*', np.dtype(np.float32): 'float*', From e3431f09210ecdb462fb7bafb53aa17ddf9fa6d9 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 13 Mar 2014 23:53:01 +0100 Subject: [PATCH 2/2] Remove seek_absolute(), re-introduce whence Closes #10. --- pysoundfile.py | 57 ++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index d78a36a..36a212b 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 +from os import SEEK_SET, SEEK_CUR, SEEK_END """PySoundFile is an audio library based on libsndfile, CFFI and Numpy @@ -236,8 +235,8 @@ 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 - seek_absolute() can be used to set the current position to a frame + position by N frames as well. Alternatively, seek() + 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. @@ -337,9 +336,9 @@ def vio_get_filelen(user_data): size = fObj._length elif not hasattr(fObj, '__len__'): old_file_position = fObj.tell() - fObj.seek(0, os.SEEK_END) + fObj.seek(0, SEEK_END) size = fObj.tell() - fObj.seek(old_file_position, os.SEEK_SET) + fObj.seek(old_file_position, SEEK_SET) else: size = len(fObj) return size @@ -464,10 +463,10 @@ 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) - self.seek_absolute(start) + curr = self.seek(0, SEEK_CUR | READ) + self.seek(start, SEEK_SET | READ) data = self.read(stop - start) - self.seek_absolute(curr) + self.seek(curr, SEEK_SET | READ) if second_frame: return data[(slice(None), second_frame)] else: @@ -484,38 +483,36 @@ 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) - self.seek_absolute(start) + curr = self.seek(0, SEEK_CUR | WRITE) + self.seek(start, SEEK_SET | WRITE) self.write(data) - self.seek_absolute(curr) + self.seek(curr, SEEK_SET | WRITE) return data 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 seek(self, frames, whence=SEEK_SET): + """Set the read and/or write position. - Positive values will fast-forward. Negative values will rewind. + By default (whence=SEEK_SET), frames are counted from the + beginning of the file. SEEK_CUR seeks from the current position + (positive and negative values are allowed). + SEEK_END seeks from the end (use negative values). - Returns the new absolute read position in frames. - """ - return _snd.sf_seek(self._file, frames, os.SEEK_CUR) - - def seek_absolute(self, frames): - """Set an absolute read position. + In RDWR mode, the whence argument can be combined (using + logical or) with READ or WRITE in order to set only the read + or write position, respectively (e.g. SEEK_SET | WRITE). - Positive values will set the read position to the given frame - index. Negative values will set the read position to the given - index counted from the end of the file. + To set the read/write position to the beginning of the file, + use seek(0), to set it to right after the last frame, + e.g. for appending new data, use seek(0, SEEK_END). - Returns the new absolute read position in frames. + Returns the new absolute read position in frames or a negative + value on error. """ - if frames >= 0: - 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, whence) def read(self, frames=-1, format=np.float32): """Read a number of frames from the file.