Skip to content

Commit

Permalink
Add buffer_read() et al., make NumPy optional
Browse files Browse the repository at this point in the history
  • Loading branch information
mgeier committed Sep 26, 2015
1 parent b078a38 commit 357ef04
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 52 deletions.
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
211 changes: 161 additions & 50 deletions soundfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) ;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -825,29 +827,93 @@ 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)
out = self._create_empty_array(frames, always_2d, dtype)
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
Expand All @@ -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):
Expand Down Expand Up @@ -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():
Expand Down
57 changes: 56 additions & 1 deletion tests/test_pysoundfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
# -----------------------------------------------------------------------------
Expand Down

0 comments on commit 357ef04

Please sign in to comment.