diff --git a/.gitignore b/.gitignore index e054ea1..76bda8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ dist/ build/ PySoundFile.egg-info/ .eggs/ +*.egg-info/ +.cache/ +.vscode/ diff --git a/soundfile.py b/soundfile.py index a9b8ec9..a0dc569 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1176,6 +1176,12 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', ---------------- always_2d, fill_value, out See :meth:`.read`. + fill_value : float, optional + See :meth:`.read`. + out : numpy.ndarray or subclass, optional + If `out` is specified, the data is written into the given + array instead of creating a new array. In this case, the + arguments `dtype` and `always_2d` are silently ignored! Examples -------- @@ -1185,33 +1191,47 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', >>> pass # do something with 'block' """ + import numpy as np + if 'r' not in self.mode and '+' not in self.mode: raise RuntimeError("blocks() is not allowed in write-only mode") - if overlap != 0 and not self.seekable(): - raise ValueError("overlap is only allowed for seekable files") - if out is None: if blocksize is None: raise TypeError("One of {blocksize, out} must be specified") + out = self._create_empty_array(blocksize, always_2d, dtype) + copy_out = True else: if blocksize is not None: raise TypeError( "Only one of {blocksize, out} may be specified") blocksize = len(out) + copy_out = False + overlap_memory = None frames = self._check_frames(frames, fill_value) while frames > 0: - if 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 and self.seekable(): - self.seek(-overlap, SEEK_CUR) - frames += overlap - yield block + if overlap_memory is None: + output_offset = 0 + else: + output_offset = len(overlap_memory) + out[:output_offset] = overlap_memory + + toread = min(blocksize - output_offset, frames) + self.read(toread, dtype, always_2d, fill_value, out[output_offset:]) + + if overlap: + if overlap_memory is None: + overlap_memory = np.copy(out[-overlap:]) + else: + overlap_memory[:] = out[-overlap:] + + if blocksize > frames + overlap and fill_value is None: + block = out[:frames + overlap] + else: + block = out + yield np.copy(block) if copy_out else block + frames -= toread def truncate(self, frames=None): """Truncate the file to a given number of frames. diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 703c246..7c1ed6e 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -382,6 +382,17 @@ def test_blocks_with_out(file_stereo_r): list(sf.blocks(filename_stereo, blocksize=3, out=out)) +def test_blocks_inplace_modification(file_stereo_r): + out = np.empty((3, 2)) + blocks = [] + for block in sf.blocks(file_stereo_r, out=out, overlap=1): + blocks.append(np.copy(block)) + block *= 2 + + expected_blocks = [data_stereo[0:3], data_stereo[2:5]] + assert_equal_list_of_arrays(blocks, expected_blocks) + + def test_blocks_mono(): blocks = list(sf.blocks(filename_mono, blocksize=3, dtype='int16', fill_value=0)) @@ -1007,10 +1018,6 @@ def test_write_non_seekable_file(file_w): f.read() assert "frames" in str(excinfo.value) - with pytest.raises(ValueError) as excinfo: - list(f.blocks(blocksize=3, overlap=1)) - assert "overlap" in str(excinfo.value) - data, fs = sf.read(filename_new, dtype='int16') assert np.all(data == data_mono) assert fs == 44100