From 48caf74140fc743697dbac299794ae1902a5b4e1 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 17 Sep 2015 19:32:31 +0200 Subject: [PATCH 1/3] Remove obsolete method _get_slice_bounds() --- soundfile.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/soundfile.py b/soundfile.py index c902696..ba5fe16 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1085,17 +1085,6 @@ def _check_if_closed(self): if self.closed: raise ValueError("I/O operation on closed file") - def _get_slice_bounds(self, frame): - # get start and stop index from slice, asserting step==1 - if not isinstance(frame, slice): - frame = slice(frame, frame + 1) - start, stop, step = frame.indices(len(self)) - if step != 1: - raise RuntimeError("Step size must be 1") - if start > stop: - stop = start - return start, stop - def _check_frames(self, frames, fill_value): # Check if frames is larger than the remaining frames in the file if self.seekable(): From b078a3887bd91789b5f93d05fa88fee36dfe63dc Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 17 Sep 2015 19:38:06 +0200 Subject: [PATCH 2/3] Change 2 comments to proper method docstrings ... and move one operator from after to before the line break. --- soundfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/soundfile.py b/soundfile.py index ba5fe16..df2cd35 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1086,11 +1086,11 @@ def _check_if_closed(self): raise ValueError("I/O operation on closed file") def _check_frames(self, frames, fill_value): - # Check if frames is larger than the remaining frames in the file + """Reduce frames to no more than are available in the file.""" if self.seekable(): remaining_frames = len(self) - self.tell() - if frames < 0 or (frames > remaining_frames - and fill_value is None): + if frames < 0 or (frames > remaining_frames and + fill_value is None): frames = remaining_frames elif frames < 0: raise ValueError("frames must be specified for non-seekable files") @@ -1135,7 +1135,7 @@ def _read_or_write(self, funcname, array, frames): return frames def _prepare_read(self, start, stop, frames): - # Seek to start frame and calculate length + """Seek to start frame and calculate length.""" if start != 0 and not self.seekable(): raise ValueError("start is only allowed for seekable files") if frames >= 0 and stop is not None: From 357ef040e2494753f9a431e56052757ad2bcc51f Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 17 Sep 2015 19:17:45 +0200 Subject: [PATCH 3/3] Add buffer_read() et al., make NumPy optional --- setup.py | 3 +- soundfile.py | 211 +++++++++++++++++++++++++++++--------- tests/test_pysoundfile.py | 57 +++++++++- 3 files changed, 219 insertions(+), 52 deletions(-) diff --git a/setup.py b/setup.py index 05e37df..f4c0f49 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,8 @@ def get_tag(self): packages=packages, package_data=package_data, license='BSD 3-Clause License', - install_requires=['numpy', 'cffi>=0.6'], + install_requires=['cffi>=0.9'], + extras_require={'numpy': ['numpy']}, platforms='any', classifiers=[ 'Development Status :: 3 - Alpha', diff --git a/soundfile.py b/soundfile.py index df2cd35..861b3a9 100644 --- a/soundfile.py +++ b/soundfile.py @@ -10,7 +10,6 @@ """ __version__ = "0.7.0" -import numpy as _np import os as _os import sys as _sys from cffi import FFI as _FFI @@ -99,10 +98,12 @@ sf_count_t sf_write_float (SNDFILE *sndfile, float *ptr, sf_count_t items) ; sf_count_t sf_write_double (SNDFILE *sndfile, double *ptr, sf_count_t items) ; -sf_count_t sf_writef_short (SNDFILE *sndfile, short *ptr, sf_count_t frames) ; -sf_count_t sf_writef_int (SNDFILE *sndfile, int *ptr, sf_count_t frames) ; -sf_count_t sf_writef_float (SNDFILE *sndfile, float *ptr, sf_count_t frames) ; -sf_count_t sf_writef_double (SNDFILE *sndfile, double *ptr, sf_count_t frames) ; +/* Note: The argument types were changed to void* in order to allow + writing bytes in SoundFile.buffer_write() */ +sf_count_t sf_writef_short (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; +sf_count_t sf_writef_int (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; +sf_count_t sf_writef_float (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; +sf_count_t sf_writef_double (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_read_raw (SNDFILE *sndfile, void *ptr, sf_count_t bytes) ; sf_count_t sf_write_raw (SNDFILE *sndfile, void *ptr, sf_count_t bytes) ; @@ -244,10 +245,10 @@ } _ffi_types = { - _np.dtype('float64'): 'double', - _np.dtype('float32'): 'float', - _np.dtype('int32'): 'int', - _np.dtype('int16'): 'short' + 'float64': 'double', + 'float32': 'float', + 'int32': 'int', + 'int16': 'short' } try: @@ -395,7 +396,8 @@ def write(data, file, samplerate, subtype=None, endian=None, format=None, >>> sf.write(np.random.randn(10, 2), 'stereo_file.wav', 44100, 'PCM_24') """ - data = _np.asarray(data) + import numpy as np + data = np.asarray(data) if data.ndim == 1: channels = 1 else: @@ -825,6 +827,10 @@ def read(self, frames=-1, dtype='float64', always_2d=True, [ 0.67398441, -0.11516333]]) >>> myfile.close() + See Also + -------- + buffer_read, .write + """ if out is None: frames = self._check_frames(frames, fill_value) @@ -832,22 +838,82 @@ def read(self, frames=-1, dtype='float64', always_2d=True, else: if frames < 0 or frames > len(out): frames = len(out) - if not out.flags.c_contiguous: - raise ValueError("out must be C-contiguous") - - self._check_array(out) - frames = self._read_or_write('sf_readf_', out, frames) - + frames = self._array_io('read', out, frames) if len(out) > frames: if fill_value is None: out = out[:frames] else: out[frames:] = fill_value - return out + def buffer_read(self, frames=-1, ctype='double'): + """Read from the file and return data as buffer object. + + Reads the given number of frames in the given data format + starting at the current read/write position. This advances the + read/write position by the same number of frames. + By default, all frames from the current read/write position to + the end of the file are returned. + Use :meth:`.seek` to move the current read/write position. + + Parameters + ---------- + frames : int, optional + The number of frames to read. If `frames < 0`, the whole + rest of the file is read. + ctype : {'double', 'float', 'int', 'short'}, optional + Audio data will be converted to the given C data type. + + Returns + ------- + buffer + A buffer containing the read data. + + See Also + -------- + buffer_read_into, .read, buffer_write + + """ + frames = self._check_frames(frames, fill_value=None) + cdata = _ffi.new(ctype + '[]', frames * self.channels) + read_frames = self._cdata_io('read', cdata, ctype, frames) + assert read_frames == frames + return _ffi.buffer(cdata) + + def buffer_read_into(self, buffer, ctype='double'): + """Read from the file into a buffer object. + + Reads the given number of frames in the given data format + starting at the current read/write position. This advances the + read/write position by the same number of frames. + By default, all frames from the current read/write position to + the end of the file are returned. + Use :meth:`.seek` to move the current read/write position. + + Parameters + ---------- + out : writable buffer, optional + If specified, audio data from the file is written to this + buffer instead of a newly created buffer. + + Returns + ------- + int + The number of frames that were read from the file. + This can be less than the size of `buffer`. + The rest of the buffer is not filled with meaningful data. + + See Also + -------- + buffer_read, .read + + """ + cdata, frames = self._check_buffer(buffer, ctype) + frames = self._cdata_io('read', cdata, ctype, frames) + return frames + def write(self, data): - """Write audio data to the file. + """Write audio data from a NumPy array to the file. Writes a number of frames at the read/write position to the file. This also advances the read/write position by the same @@ -858,20 +924,42 @@ def write(self, data): data : array_like See :func:`write`. + See Also + -------- + buffer_write, .read + """ + import numpy as np # no copy is made if data has already the correct memory layout: - data = _np.ascontiguousarray(data) - - self._check_array(data) - written = self._read_or_write('sf_writef_', data, len(data)) + data = np.ascontiguousarray(data) + written = self._array_io('write', data, len(data)) assert written == len(data) + self._update_len(written) - if self.seekable(): - curr = self.tell() - self._info.frames = self.seek(0, SEEK_END) - self.seek(curr, SEEK_SET) - else: - self._info.frames += written + def buffer_write(self, data, ctype): + """Write audio data from a buffer/bytes object to the file. + + Writes a number of frames at the read/write position to the + file. This also advances the read/write position by the same + number of frames and enlarges the file if necessary. + + Parameters + ---------- + data : buffer or bytes + A buffer object or bytes containing the audio data to be + written. + ctype : {'double', 'float', 'int', 'short'}, optional + The data type of the audio data stored in `buffer`. + + See Also + -------- + .write, buffer_read + + """ + cdata, frames = self._check_buffer(data, ctype) + written = self._cdata_io('write', cdata, ctype, frames) + assert written == frames + self._update_len(written) def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', always_2d=True, fill_value=None, out=None): @@ -1096,44 +1184,67 @@ def _check_frames(self, frames, fill_value): raise ValueError("frames must be specified for non-seekable files") return frames - def _check_array(self, array): - """Do some error checking.""" - if (array.ndim not in (1, 2) or - array.ndim == 1 and self.channels != 1 or - array.ndim == 2 and array.shape[1] != self.channels): - raise ValueError("Invalid shape: {0!r}".format(array.shape)) - - if array.dtype not in _ffi_types: - raise ValueError("dtype must be one of {0!r}".format( - sorted(dt.name for dt in _ffi_types))) + def _check_buffer(self, data, ctype): + """Convert buffer to cdata and check for valid size.""" + if isinstance(data, bytes): + size = len(data) + else: + data = _ffi.from_buffer(data) + size = _ffi.sizeof(data) + frames, remainder = divmod(size, self.channels * _ffi.sizeof(ctype)) + if remainder: + raise ValueError("Data size must be a multiple of frame size") + return data, frames def _create_empty_array(self, frames, always_2d, dtype): """Create an empty array with appropriate shape.""" + import numpy as np if always_2d or self.channels > 1: shape = frames, self.channels else: shape = frames, - return _np.empty(shape, dtype, order='C') + return np.empty(shape, dtype, order='C') - def _read_or_write(self, funcname, array, frames): - """Call into libsndfile.""" + def _array_io(self, action, array, frames): + """Check array and call low-level IO function.""" + if (array.ndim not in (1, 2) or + array.ndim == 1 and self.channels != 1 or + array.ndim == 2 and array.shape[1] != self.channels): + raise ValueError("Invalid shape: {0!r}".format(array.shape)) + if not array.flags.c_contiguous: + raise ValueError("Data must be C-contiguous") + try: + ctype = _ffi_types[array.dtype.name] + except KeyError: + raise ValueError("dtype must be one of {0!r}".format( + sorted(_ffi_types.keys()))) + assert array.dtype.itemsize == _ffi.sizeof(ctype) + cdata = _ffi.cast(ctype + '*', array.__array_interface__['data'][0]) + return self._cdata_io(action, cdata, ctype, frames) + + def _cdata_io(self, action, data, ctype, frames): + """Call one of libsndfile's read/write functions.""" + if ctype not in _ffi_types.values(): + raise ValueError("Unsupported data type: {0!r}".format(ctype)) self._check_if_closed() - - ffi_type = _ffi_types[array.dtype] - assert array.flags.c_contiguous - assert array.dtype.itemsize == _ffi.sizeof(ffi_type) - assert array.size >= frames * self.channels - if self.seekable(): curr = self.tell() - func = getattr(_snd, funcname + ffi_type) - ptr = _ffi.cast(ffi_type + '*', array.__array_interface__['data'][0]) - frames = func(self._file, ptr, frames) + func = getattr(_snd, 'sf_' + action + 'f_' + ctype) + frames = func(self._file, data, frames) _error_check(self._errorcode) if self.seekable(): self.seek(curr + frames, SEEK_SET) # Update read & write position return frames + def _update_len(self, written): + """Update len(self) after writing.""" + if self.seekable(): + curr = self.tell() + self._info.frames = self.seek(0, SEEK_END) + self.seek(curr, SEEK_SET) + else: + self._info.frames += written + def _prepare_read(self, start, stop, frames): """Seek to start frame and calculate length.""" if start != 0 and not self.seekable(): diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 70d6786..cfad320 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -626,7 +626,6 @@ def test_read_should_read_data_and_advance_read_pointer(sf_stereo_r): assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 2 - def test_read_n_frames_should_return_n_frames(sf_stereo_r): assert len(sf_stereo_r.read(2)) == 2 @@ -669,6 +668,39 @@ def test_read_into_out_over_end_with_fill_should_return_full_data_and_write_into assert np.all(data[2:] == 0) assert out.shape == (4, sf_stereo_r.channels) +# ----------------------------------------------------------------------------- +# Test buffer read +# ----------------------------------------------------------------------------- + + +def test_buffer_read(sf_stereo_r): + buf = sf_stereo_r.buffer_read(2) + assert len(buf) == 2 * 2 * 8 + assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 2 + data = np.frombuffer(buf, dtype='float64').reshape(-1, 2) + assert np.all(data == data_stereo[:2]) + buf = sf_stereo_r.buffer_read(ctype='float') + assert len(buf) == 2 * 2 * 4 + assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 4 + data = np.frombuffer(buf, dtype='float32').reshape(-1, 2) + assert np.all(data == data_stereo[2:]) + buf = sf_stereo_r.buffer_read() + assert len(buf) == 0 + buf = sf_stereo_r.buffer_read(666) + assert len(buf) == 0 + + +def test_buffer_read_into(sf_stereo_r): + out = np.ones((3, 2)) + frames = sf_stereo_r.buffer_read_into(out) + assert frames == 3 + assert np.all(out == data_stereo[:3]) + assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 3 + frames = sf_stereo_r.buffer_read_into(out) + assert frames == 1 + assert np.all(out[:1] == data_stereo[3:]) + assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 4 + # ----------------------------------------------------------------------------- # Test write @@ -714,6 +746,29 @@ def test_rplus_append_data(sf_stereo_rplus): assert np.all(data[len(data_stereo):] == data_stereo / 2) +# ----------------------------------------------------------------------------- +# Test buffer write +# ----------------------------------------------------------------------------- + + +def test_buffer_write(sf_stereo_w): + buf = np.array([[1, 2], [-1, -2]], dtype='int16') + sf_stereo_w.buffer_write(buf, 'short') + sf_stereo_w.close() + data, fs = sf.read(filename_new, dtype='int16') + assert np.all(data == buf) + assert fs == 44100 + + +def test_buffer_write_with_bytes(sf_stereo_w): + b = b"\x01\x00\xFF\xFF\xFF\x00\x00\xFF" + sf_stereo_w.buffer_write(b, 'short') + sf_stereo_w.close() + data, fs = sf.read(filename_new, dtype='int16') + assert np.all(data == [[1, -1], [255, -256]]) + assert fs == 44100 + + # ----------------------------------------------------------------------------- # Other tests # -----------------------------------------------------------------------------