From 29a26963a31a6adc5da5331700434ee4a6b8f702 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 17 Jun 2014 16:49:42 +0200 Subject: [PATCH 1/6] Change default value of start from None to 0 --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index 67f6f0a..496eb69 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -730,7 +730,7 @@ def open(file, mode='r', sample_rate=None, channels=None, def read(file, sample_rate=None, channels=None, subtype=None, endian=None, - format=None, closefd=True, start=None, stop=None, frames=-1, + format=None, closefd=True, start=0, stop=None, frames=-1, dtype='float64', always_2d=True, fill_value=None, out=None): """Read a sound file and return its contents as NumPy array. From 03ac57c78ebccd65b9e1b2f95764b3800429a925 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 17 Jun 2014 16:50:59 +0200 Subject: [PATCH 2/6] Change a RuntimeError to TypeError --- pysoundfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysoundfile.py b/pysoundfile.py index 496eb69..f6d27d0 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -762,7 +762,7 @@ def read(file, sample_rate=None, channels=None, subtype=None, endian=None, """ if frames >= 0 and stop is not None: - raise RuntimeError("Only one of {frames, stop} may be used") + raise TypeError("Only one of {frames, stop} may be used") with SoundFile(file, 'r', sample_rate, channels, subtype, endian, format, closefd) as f: From 4a389fd31ee3df29626753c5db4fb01692930fb8 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 17 Jun 2014 16:52:15 +0200 Subject: [PATCH 3/6] Add blocks() method and function --- pysoundfile.py | 154 ++++++++++++++++++++++++++++++++++--- tests/test_argspec.py | 53 +++++++++---- tests/test_pysoundfile.py | 157 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 340 insertions(+), 24 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index f6d27d0..dd30481 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -1,5 +1,6 @@ import numpy as _np from cffi import FFI as _FFI +from contextlib import closing as _closing from os import SEEK_SET, SEEK_CUR, SEEK_END __version__ = "0.5.0" @@ -624,6 +625,14 @@ def _check_array(self, array): raise ValueError("dtype must be one of %s" % repr([dt.name for dt in _ffi_types])) + def _create_empty_array(self, frames, always_2d, dtype): + # Create an empty array with appropriate shape + if always_2d or self.channels > 1: + shape = frames, self.channels + else: + shape = frames, + return _np.empty(shape, dtype, order='C') + def _read_or_write(self, funcname, array, frames): # Call into libsndfile ffi_type = _ffi_types[array.dtype] @@ -672,11 +681,7 @@ def read(self, frames=-1, dtype='float64', always_2d=True, if frames < 0 or (frames > remaining_frames and fill_value is None): frames = remaining_frames - if always_2d or self.channels > 1: - shape = frames, self.channels - else: - shape = frames, - out = _np.empty(shape, dtype, order='C') + out = self._create_empty_array(frames, always_2d, dtype) else: if frames < 0 or frames > len(out): frames = len(out) @@ -720,6 +725,81 @@ def write(self, data): self._info.frames = self.seek(0, SEEK_END, 'w') self.seek(curr, SEEK_SET, 'w') + def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', + always_2d=True, fill_value=None, out=None): + """Return a generator for block-wise processing. + + By default, the generator returns blocks of the given blocksize + until the end of the file is reached, frames can be used to + stop earlier. + + overlap can be used to rewind a certain number of frames between + blocks, but this is only allowed for mode='r'. + + For the arguments dtype, always_2d, fill_value and out see + SoundFile.read(). + + According to the open mode ('r'/'w'/'rw'), the NumPy array + returned by the generator can be read from and/or written to. + + Warning: If mode='w' and frames is not specified, the + generator runs forever! + + If mode is 'w' or 'rw' and you stop iterating before the + generator is exhausted, you have to call the generator's close() + method in order to write the last block to the file. + + If mode='rw', separate read and write positions are used. See + SoundFile.seek() for how to set them. + Iteration stops when the read position reaches the original end + of the file, regardless if write operations enlarged the file + during iteration. + + Note: When a file is opened with mode='rw', the read position is + at the beginning of the file, the write position at the end. + Use seek(0) to set them both to the beginning of the file. + + """ + if self.mode != 'r' and overlap != 0: + raise TypeError("overlap is only allowed in read mode") + + if out is not None: + if blocksize is not None: + raise TypeError( + "Only one of {blocksize, out} may be specified") + blocksize = len(out) + + if self.mode == 'w': + block = self._create_empty_array(blocksize, always_2d, dtype) + else: + remaining_frames = self.frames - self.seek(0, SEEK_CUR, 'r') + if frames < 0 or (fill_value is None and + frames > remaining_frames): + frames = remaining_frames + + while frames != 0: + if 0 < frames < blocksize: + if fill_value is None: + blocksize = frames + else: + frames = blocksize + frames -= blocksize + + if self.mode != 'w': + block = self.read(blocksize, dtype, always_2d, fill_value, out) + if frames > 0 and overlap != 0: + self.seek(-overlap, SEEK_CUR, 'r') + frames += overlap + elif blocksize < len(block): + block = block[:blocksize] + try: + yield block + except GeneratorExit: + frames = 0 + + if self.mode != 'r': + self.write(block) + def open(file, mode='r', sample_rate=None, channels=None, subtype=None, endian=None, format=None, closefd=True): @@ -766,11 +846,7 @@ def read(file, sample_rate=None, channels=None, subtype=None, endian=None, with SoundFile(file, 'r', sample_rate, channels, subtype, endian, format, closefd) as f: - start, stop, _ = slice(start, stop).indices(f.frames) - if stop < start: - stop = start - if frames < 0: - frames = stop - start + start, frames = _get_read_range(start, stop, frames, f.frames) f.seek(start, SEEK_SET) data = f.read(frames, dtype, always_2d, fill_value, out) return data, f.sample_rate @@ -803,6 +879,64 @@ def write(data, file, sample_rate, f.write(data) +def blocks(file, mode='r', sample_rate=None, channels=None, + subtype=None, endian=None, format=None, closefd=True, + blocksize=None, overlap=0, start=0, stop=None, frames=-1, + dtype='float64', always_2d=True, fill_value=None, out=None): + """Return a generator for block-wise processing. + + Example usage: + + import pysoundfile as sf + for block in sf.blocks('myfile.wav', blocksize=128): + print(block.max()) + # ... or do something more useful with 'block' + + All keyword arguments of SoundFile.blocks() are allowed. + All further arguments are forwarded to open(). + + Both read and write positions are set to start (by default the + beginning of the file) before returning the first block. If you need + to use different read and write positions, use SoundFile.blocks() + (and SoundFile.seek()). + + By default, iteration stops at the end of the file. In write mode, + the generator can be iterated infinitely. Use frames or stop to stop + earlier (or to stop at all). + + If you stop iterating over the generator before it's exhausted, you + should call the generator's close() method in order to properly + close the sound file. + This is especially important when writing to a file. + + """ + if frames >= 0 and stop is not None: + raise TypeError("Only one of {frames, stop} may be used") + + with open(file, mode, sample_rate, channels, + subtype, endian, format, closefd) as f: + if f.mode == 'w': + if start != 0 or stop is not None: + raise TypeError("start and stop are not allowed in write mode") + else: + start, frames = _get_read_range(start, stop, frames, f.frames) + f.seek(start, SEEK_SET) + with _closing(f.blocks(blocksize, overlap, frames, dtype, + always_2d, fill_value, out)) as blocks: + for block in blocks: + yield block + + +def _get_read_range(start, stop, frames, total_frames): + # Calculate start frame and length + start, stop, _ = slice(start, stop).indices(total_frames) + if stop < start: + stop = start + if frames < 0: + frames = stop - start + return start, frames + + def default_subtype(format): """Return default subtype for given format.""" return _default_subtypes.get(str(format).upper()) diff --git a/tests/test_argspec.py b/tests/test_argspec.py index a261002..6832e73 100644 --- a/tests/test_argspec.py +++ b/tests/test_argspec.py @@ -1,4 +1,4 @@ -"""Make sure that arguments of open/read/write don't diverge""" +"""Make sure that arguments of open/read/write don't diverge.""" import pysoundfile as sf from inspect import getargspec @@ -9,12 +9,23 @@ read_function = getargspec(sf.read) read_method = getargspec(sf.SoundFile.read) write_function = getargspec(sf.write) +blocks_function = getargspec(sf.blocks) +blocks_method = getargspec(sf.SoundFile.blocks) def defaults(spec): return dict(zip(reversed(spec.args), reversed(spec.defaults))) +def remove_items(collection, subset): + """From a collection of defaults, remove a subset and return the rest.""" + the_rest = collection.copy() + for arg, default in subset.items(): + assert (arg, the_rest[arg]) == (arg, default) + del the_rest[arg] + return the_rest + + def test_if_open_is_identical_to_init(): assert ['self'] + open.args == init.args assert open.varargs == init.varargs @@ -22,38 +33,52 @@ def test_if_open_is_identical_to_init(): assert open.defaults == init.defaults -def test_read_function(): +def test_read_defaults(): func_defaults = defaults(read_function) meth_defaults = defaults(read_method) open_defaults = defaults(open) - # Not meaningful in read() function: - del open_defaults['mode'] + del open_defaults['mode'] # Not meaningful in read() function: - # Only in read() function: del func_defaults['start'] del func_defaults['stop'] # Same default values as open() and SoundFile.read(): for spec in open_defaults, meth_defaults: - for arg, default in spec.items(): - assert (arg, func_defaults[arg]) == (arg, default) - del func_defaults[arg] + func_defaults = remove_items(func_defaults, spec) assert not func_defaults # No more arguments should be left -def test_write_function(): +def test_write_defaults(): write_defaults = defaults(write_function) open_defaults = defaults(open) - # Same default values as open(): - for arg, default in write_defaults.items(): - assert (arg, open_defaults[arg]) == (arg, default) - del open_defaults[arg] + # Same default values as open() + open_defaults = remove_items(open_defaults, write_defaults) del open_defaults['mode'] # mode is always 'w' del open_defaults['channels'] # Inferred from data del open_defaults['sample_rate'] # Obligatory in write() - assert not open_defaults # No more arguments should be left + + +def test_if_blocks_function_and_method_have_same_defaults(): + func_defaults = defaults(blocks_function) + meth_defaults = defaults(blocks_method) + open_defaults = defaults(open) + + del func_defaults['start'] + del func_defaults['stop'] + + # Same default values as open() and SoundFile.read(): + for spec in open_defaults, meth_defaults: + func_defaults = remove_items(func_defaults, spec) + + assert not func_defaults + + +def test_order_of_blocks_arguments(): + meth_args = blocks_method.args[1:] # remove 'self' + meth_args[2:2] = ['start', 'stop'] + assert blocks_function.args == open.args + meth_args diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index af98c8e..24ae5b6 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -195,6 +195,163 @@ def test_write_function(file_w): assert np.all(data == data_mono) +# ----------------------------------------------------------------------------- +# Test blocks() function +# ----------------------------------------------------------------------------- + + +def assert_equal_list_of_arrays(list1, list2): + """Helper function to assert equality of all list items.""" + for item1, item2 in zip(list1, list2): + assert np.all(item1 == item2) + + +def test_blocks_full_last_block(): + blocks = list(sf.blocks(filename_stereo, blocksize=2)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:4]]) + + +def test_blocks_partial_last_block(): + blocks = list(sf.blocks(filename_stereo, blocksize=3)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:3], data_stereo[3:4]]) + + +def test_blocks_fill_last_block(): + blocks = list(sf.blocks(filename_stereo, blocksize=3, fill_value=0)) + last_block = np.row_stack((data_stereo[3:4], np.zeros((2, 2)))) + assert_equal_list_of_arrays(blocks, [data_stereo[0:3], last_block]) + + +def test_blocks_with_overlap(): + blocks = list(sf.blocks(filename_stereo, blocksize=3, overlap=2)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:3], data_stereo[1:4]]) + + +def test_blocks_with_start(): + blocks = list(sf.blocks(filename_stereo, blocksize=2, start=2)) + assert_equal_list_of_arrays(blocks, [data_stereo[2:4]]) + + +def test_blocks_with_stop(): + blocks = list(sf.blocks(filename_stereo, blocksize=2, stop=2)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:2]]) + + with pytest.raises(TypeError): + list(sf.blocks(filename_stereo, blocksize=2, frames=2, stop=2)) + + +def test_blocks_with_too_large_start(): + blocks = list(sf.blocks(filename_stereo, start=666)) + assert_equal_list_of_arrays(blocks, [[]]) + + +def test_blocks_with_too_large_stop(): + blocks = list(sf.blocks(filename_stereo, blocksize=3, stop=666)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:3], data_stereo[3:4]]) + + +def test_blocks_with_negative_start_and_stop(): + blocks = list(sf.blocks(filename_stereo, blocksize=2, start=-2, stop=-1)) + assert_equal_list_of_arrays(blocks, [data_stereo[-2:-1]]) + + +def test_blocks_with_stop_smaller_than_start(): + blocks = list(sf.blocks(filename_stereo, blocksize=2, start=2, stop=1)) + assert blocks == [] + + +def test_blocks_with_frames(): + blocks = list(sf.blocks(filename_stereo, blocksize=2, frames=3)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:3]]) + + +def test_blocks_with_out(): + out = np.empty((3, 2)) + blocks = list(sf.blocks(filename_stereo, out=out)) + assert blocks[0] is out + # First frame was overwritten by second block: + assert np.all(blocks[0] == [[0.25, -0.25], [0.75, -0.75], [0.5, -0.5]]) + assert blocks[1].base is out + assert np.all(blocks[1] == [[0.25, -0.25]]) + + with pytest.raises(TypeError): + list(sf.blocks(filename_stereo, blocksize=3, out=out)) + + +def test_blocks_mono(): + blocks = list(sf.blocks(filename_mono, blocksize=3, dtype='int16', + always_2d=False, fill_value=0)) + assert_equal_list_of_arrays(blocks, [[0, 1, 2], [-2, -1, 0]]) + + +def test_blocks_write_interrupted(file_w): + for block in sf.blocks(file_w, 'w', 44100, 2, blocksize=len(data_stereo), + format='WAV', subtype='FLOAT'): + block[:] = data_stereo + break # Raises GeneratorExit + data, fs = sf.read(filename_new) + assert fs == 44100 + assert np.all(data == data_stereo) + + +def test_blocks_write_partial_last_block(file_w): + for i, block in enumerate( + sf.blocks(file_w, 'w', 44100, 2, blocksize=3, + format='WAV', subtype='FLOAT', frames=len(data_stereo))): + if i == 0: + block[:] = data_stereo[0:3] + else: + block[:] = data_stereo[3:4] + data, fs = sf.read(filename_new) + assert fs == 44100 + assert np.all(data == data_stereo) + + +def test_blocks_write_with_start(file_w): + with pytest.raises(TypeError): + list(sf.blocks(file_w, 'w', 44100, 2, blocksize=3, + format='WAV', subtype='FLOAT', start=1)) + + +def test_blocks_write_with_stop(file_w): + with pytest.raises(TypeError): + list(sf.blocks(file_w, 'w', 44100, 2, blocksize=3, + format='WAV', subtype='FLOAT', stop=1)) + + +def test_blocks_rw_existing(file_stereo_rw_existing): + for block in sf.blocks(file_stereo_rw_existing, 'rw', blocksize=3): + block[:] = block / 2 + + data, fs = sf.read(tempfilename) + assert fs == 44100 + assert np.all(data == data_stereo / 2) + + +def test_blocks_rw_existing_with_start_and_stop(file_stereo_rw_existing): + for block in sf.blocks(file_stereo_rw_existing, 'rw', blocksize=2, + start=1, stop=-1): + block[:] = block / 2 + + data, fs = sf.read(tempfilename) + assert fs == 44100 + assert np.all(data[:1] == data_stereo[:1]) + assert np.all(data[1:-1] == data_stereo[1:-1] / 2) + assert np.all(data[-1:] == data_stereo[-1:]) + + +def test_blocks_rw_overlap(file_stereo_rw_existing): + with pytest.raises(TypeError): + list(sf.blocks(file_stereo_rw_existing, 'rw', blocksize=3, overlap=1)) + + +def test_blocks_rw_new(file_rw_new): + """There is nothing to yield in a new 'rw' file.""" + blocks = list(sf.blocks(file_rw_new, 'rw', 44100, 2, format='WAV', + blocksize=2, frames=666)) + assert blocks == [] + + # ----------------------------------------------------------------------------- # Test file metadata # ----------------------------------------------------------------------------- From d29c034c8e8da06d5a13ddc8f2a724c5e4b30a05 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 23 Jul 2014 22:01:47 +0200 Subject: [PATCH 4/6] Disallow blocks() in 'w' mode --- pysoundfile.py | 96 +++++++++++---------------------------- tests/test_argspec.py | 6 ++- tests/test_pysoundfile.py | 72 ++++------------------------- 3 files changed, 41 insertions(+), 133 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index dd30481..1acf556 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -734,34 +734,17 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', stop earlier. overlap can be used to rewind a certain number of frames between - blocks, but this is only allowed for mode='r'. + blocks. For the arguments dtype, always_2d, fill_value and out see SoundFile.read(). - According to the open mode ('r'/'w'/'rw'), the NumPy array - returned by the generator can be read from and/or written to. - - Warning: If mode='w' and frames is not specified, the - generator runs forever! - - If mode is 'w' or 'rw' and you stop iterating before the - generator is exhausted, you have to call the generator's close() - method in order to write the last block to the file. - - If mode='rw', separate read and write positions are used. See - SoundFile.seek() for how to set them. - Iteration stops when the read position reaches the original end - of the file, regardless if write operations enlarged the file - during iteration. - - Note: When a file is opened with mode='rw', the read position is - at the beginning of the file, the write position at the end. - Use seek(0) to set them both to the beginning of the file. + If fill_value is not specified, the last block may be smaller + than blocksize. """ - if self.mode != 'r' and overlap != 0: - raise TypeError("overlap is only allowed in read mode") + if self.mode == 'w': + raise RuntimeError("blocks() is not allowed in write mode") if out is not None: if blocksize is not None: @@ -769,36 +752,22 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', "Only one of {blocksize, out} may be specified") blocksize = len(out) - if self.mode == 'w': - block = self._create_empty_array(blocksize, always_2d, dtype) - else: - remaining_frames = self.frames - self.seek(0, SEEK_CUR, 'r') - if frames < 0 or (fill_value is None and - frames > remaining_frames): - frames = remaining_frames + remaining_frames = self.frames - self.seek(0, SEEK_CUR, 'r') + if frames < 0 or (fill_value is None and frames > remaining_frames): + frames = remaining_frames - while frames != 0: - if 0 < frames < blocksize: + while frames > 0: + if frames < blocksize: if fill_value is None: blocksize = frames else: frames = blocksize + block = self.read(blocksize, dtype, always_2d, fill_value, out) frames -= blocksize - - if self.mode != 'w': - block = self.read(blocksize, dtype, always_2d, fill_value, out) - if frames > 0 and overlap != 0: - self.seek(-overlap, SEEK_CUR, 'r') - frames += overlap - elif blocksize < len(block): - block = block[:blocksize] - try: - yield block - except GeneratorExit: - frames = 0 - - if self.mode != 'r': - self.write(block) + if frames > 0: + self.seek(-overlap, SEEK_CUR, 'r') + frames += overlap + yield block def open(file, mode='r', sample_rate=None, channels=None, @@ -879,7 +848,7 @@ def write(data, file, sample_rate, f.write(data) -def blocks(file, mode='r', sample_rate=None, channels=None, +def blocks(file, sample_rate=None, channels=None, subtype=None, endian=None, format=None, closefd=True, blocksize=None, overlap=0, start=0, stop=None, frames=-1, dtype='float64', always_2d=True, fill_value=None, out=None): @@ -895,36 +864,25 @@ def blocks(file, mode='r', sample_rate=None, channels=None, All keyword arguments of SoundFile.blocks() are allowed. All further arguments are forwarded to open(). - Both read and write positions are set to start (by default the - beginning of the file) before returning the first block. If you need - to use different read and write positions, use SoundFile.blocks() - (and SoundFile.seek()). + By default, iteration stops at the end of the file. Use frames or + stop to stop earlier. - By default, iteration stops at the end of the file. In write mode, - the generator can be iterated infinitely. Use frames or stop to stop - earlier (or to stop at all). - - If you stop iterating over the generator before it's exhausted, you - should call the generator's close() method in order to properly - close the sound file. - This is especially important when writing to a file. + If you stop iterating over the generator before it's exhausted, the + sound file is not closed. This is normally not a problem because + the file is opened in read-only mode. To close the file properly, + the generator's close() method can be called. """ if frames >= 0 and stop is not None: raise TypeError("Only one of {frames, stop} may be used") - with open(file, mode, sample_rate, channels, + with open(file, 'r', sample_rate, channels, subtype, endian, format, closefd) as f: - if f.mode == 'w': - if start != 0 or stop is not None: - raise TypeError("start and stop are not allowed in write mode") - else: - start, frames = _get_read_range(start, stop, frames, f.frames) + start, frames = _get_read_range(start, stop, frames, f.frames) f.seek(start, SEEK_SET) - with _closing(f.blocks(blocksize, overlap, frames, dtype, - always_2d, fill_value, out)) as blocks: - for block in blocks: - yield block + for block in f.blocks(blocksize, overlap, frames, + dtype, always_2d, fill_value, out): + yield block def _get_read_range(start, stop, frames, total_frames): diff --git a/tests/test_argspec.py b/tests/test_argspec.py index 6832e73..087fb54 100644 --- a/tests/test_argspec.py +++ b/tests/test_argspec.py @@ -70,8 +70,8 @@ def test_if_blocks_function_and_method_have_same_defaults(): del func_defaults['start'] del func_defaults['stop'] + del open_defaults['mode'] - # Same default values as open() and SoundFile.read(): for spec in open_defaults, meth_defaults: func_defaults = remove_items(func_defaults, spec) @@ -81,4 +81,6 @@ def test_if_blocks_function_and_method_have_same_defaults(): def test_order_of_blocks_arguments(): meth_args = blocks_method.args[1:] # remove 'self' meth_args[2:2] = ['start', 'stop'] - assert blocks_function.args == open.args + meth_args + open_args = open.args[:] + open_args.remove('mode') + assert blocks_function.args == open_args + meth_args diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 24ae5b6..b54fb21 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -284,74 +284,22 @@ def test_blocks_mono(): assert_equal_list_of_arrays(blocks, [[0, 1, 2], [-2, -1, 0]]) -def test_blocks_write_interrupted(file_w): - for block in sf.blocks(file_w, 'w', 44100, 2, blocksize=len(data_stereo), - format='WAV', subtype='FLOAT'): - block[:] = data_stereo - break # Raises GeneratorExit - data, fs = sf.read(filename_new) - assert fs == 44100 - assert np.all(data == data_stereo) - - -def test_blocks_write_partial_last_block(file_w): - for i, block in enumerate( - sf.blocks(file_w, 'w', 44100, 2, blocksize=3, - format='WAV', subtype='FLOAT', frames=len(data_stereo))): - if i == 0: - block[:] = data_stereo[0:3] - else: - block[:] = data_stereo[3:4] - data, fs = sf.read(filename_new) - assert fs == 44100 - assert np.all(data == data_stereo) - - -def test_blocks_write_with_start(file_w): - with pytest.raises(TypeError): - list(sf.blocks(file_w, 'w', 44100, 2, blocksize=3, - format='WAV', subtype='FLOAT', start=1)) - - -def test_blocks_write_with_stop(file_w): - with pytest.raises(TypeError): - list(sf.blocks(file_w, 'w', 44100, 2, blocksize=3, - format='WAV', subtype='FLOAT', stop=1)) - - -def test_blocks_rw_existing(file_stereo_rw_existing): - for block in sf.blocks(file_stereo_rw_existing, 'rw', blocksize=3): - block[:] = block / 2 - - data, fs = sf.read(tempfilename) - assert fs == 44100 - assert np.all(data == data_stereo / 2) - - -def test_blocks_rw_existing_with_start_and_stop(file_stereo_rw_existing): - for block in sf.blocks(file_stereo_rw_existing, 'rw', blocksize=2, - start=1, stop=-1): - block[:] = block / 2 - - data, fs = sf.read(tempfilename) - assert fs == 44100 - assert np.all(data[:1] == data_stereo[:1]) - assert np.all(data[1:-1] == data_stereo[1:-1] / 2) - assert np.all(data[-1:] == data_stereo[-1:]) - - -def test_blocks_rw_overlap(file_stereo_rw_existing): - with pytest.raises(TypeError): - list(sf.blocks(file_stereo_rw_existing, 'rw', blocksize=3, overlap=1)) +def test_blocks_rw_existing(sf_stereo_rw_existing): + blocks = list(sf_stereo_rw_existing.blocks(blocksize=2)) + assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:4]]) -def test_blocks_rw_new(file_rw_new): +def test_blocks_rw_new(sf_stereo_rw_new): """There is nothing to yield in a new 'rw' file.""" - blocks = list(sf.blocks(file_rw_new, 'rw', 44100, 2, format='WAV', - blocksize=2, frames=666)) + blocks = list(sf_stereo_rw_new.blocks(blocksize=2, frames=666)) assert blocks == [] +def test_blocks_write(sf_stereo_w): + with pytest.raises(RuntimeError): + list(sf_stereo_w.blocks(blocksize=2)) + + # ----------------------------------------------------------------------------- # Test file metadata # ----------------------------------------------------------------------------- From a0c5997980bcb18e3b13ce0965199463dbb2ec3b Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 25 Jul 2014 15:56:57 +0200 Subject: [PATCH 5/6] blocks(): fix special case with frames and fill_value --- pysoundfile.py | 7 +++---- tests/test_pysoundfile.py | 7 +++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 1acf556..38cf95e 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -758,10 +758,9 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', while frames > 0: if frames < blocksize: - if fill_value is None: - blocksize = frames - else: - frames = blocksize + if fill_value is not None and out is None: + out = self._create_empty_array(blocksize, always_2d, dtype) + blocksize = frames block = self.read(blocksize, dtype, always_2d, fill_value, out) frames -= blocksize if frames > 0: diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index b54fb21..8c7bedb 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -265,6 +265,13 @@ def test_blocks_with_frames(): assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:3]]) +def test_blocks_with_frames_and_fill_value(): + blocks = list( + sf.blocks(filename_stereo, blocksize=2, frames=3, fill_value=0)) + last_block = np.row_stack((data_stereo[2:3], np.zeros((1, 2)))) + assert_equal_list_of_arrays(blocks, [data_stereo[0:2], last_block]) + + def test_blocks_with_out(): out = np.empty((3, 2)) blocks = list(sf.blocks(filename_stereo, out=out)) From 64e5d559c2b12f03a12b642773be713e53b5b3ab Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sun, 27 Jul 2014 20:55:07 +0200 Subject: [PATCH 6/6] blocks(): check if one of {blocksize, out} is specified --- pysoundfile.py | 5 ++++- tests/test_pysoundfile.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 38cf95e..a897c5f 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -746,7 +746,10 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', if self.mode == 'w': raise RuntimeError("blocks() is not allowed in write mode") - if out is not None: + if out is None: + if blocksize is None: + raise TypeError("One of {blocksize, out} must be specified") + else: if blocksize is not None: raise TypeError( "Only one of {blocksize, out} may be specified") diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 8c7bedb..15eea23 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -206,6 +206,11 @@ def assert_equal_list_of_arrays(list1, list2): assert np.all(item1 == item2) +def test_blocks_without_blocksize(): + with pytest.raises(TypeError): + list(sf.blocks(filename_stereo)) + + def test_blocks_full_last_block(): blocks = list(sf.blocks(filename_stereo, blocksize=2)) assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:4]]) @@ -241,7 +246,7 @@ def test_blocks_with_stop(): def test_blocks_with_too_large_start(): - blocks = list(sf.blocks(filename_stereo, start=666)) + blocks = list(sf.blocks(filename_stereo, blocksize=2, start=666)) assert_equal_list_of_arrays(blocks, [[]])