From 7ff99b01d6eba637164d35137a6f5e3ad3caa7c8 Mon Sep 17 00:00:00 2001 From: Till Hoffmann Date: Tue, 3 Oct 2017 16:16:18 +0100 Subject: [PATCH 1/4] Update SoundFile.blocks. --- .gitignore | 3 +++ soundfile.py | 33 ++++++++++++++++++++------------- tests/test_pysoundfile.py | 6 +----- 3 files changed, 24 insertions(+), 18 deletions(-) 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..a3dabd8 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1188,30 +1188,37 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', 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") + # Allocate memory + 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 + # Specify an offset for the output array + offset = 0 + # Get the number of remaining frames 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 + n = min(blocksize - offset, frames) + self.read(n, dtype, always_2d, fill_value, out[offset:]) + block = out[:min(blocksize, frames + overlap)] if fill_value is None else out + if copy_out: + import numpy as np + yield np.copy(block) + else: + yield block + frames -= n + # Copy the end of the block to the beginning of the next + if overlap: + offset = overlap + out[:overlap] = out[-overlap:] 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..15127b5 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -371,7 +371,7 @@ def test_blocks_with_frames_and_fill_value(file_stereo_r): def test_blocks_with_out(file_stereo_r): out = np.empty((3, 2)) blocks = list(sf.blocks(file_stereo_r, out=out)) - assert blocks[0] is out + assert blocks[0].base is out # First frame was overwritten by second block: assert np.all(blocks[0] == data_stereo[[3, 1, 2]]) @@ -1007,10 +1007,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 From 1d8f3812e218dedb5fc07d3a41bced5f333271ce Mon Sep 17 00:00:00 2001 From: Till Hoffmann Date: Wed, 4 Oct 2017 10:17:01 +0100 Subject: [PATCH 2/4] Improve variable naming and slicing. --- soundfile.py | 17 ++++++++--------- tests/test_pysoundfile.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/soundfile.py b/soundfile.py index a3dabd8..73b11f8 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1185,6 +1185,8 @@ 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") @@ -1206,15 +1208,12 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', # Get the number of remaining frames frames = self._check_frames(frames, fill_value) while frames > 0: - n = min(blocksize - offset, frames) - self.read(n, dtype, always_2d, fill_value, out[offset:]) - block = out[:min(blocksize, frames + overlap)] if fill_value is None else out - if copy_out: - import numpy as np - yield np.copy(block) - else: - yield block - frames -= n + toread = min(blocksize - offset, frames) + self.read(toread, dtype, always_2d, fill_value, out[offset:]) + block = out[:frames + overlap] if blocksize > frames + overlap and fill_value is None \ + else out + yield np.copy(block) if copy_out else block + frames -= toread # Copy the end of the block to the beginning of the next if overlap: offset = overlap diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 15127b5..3b91a92 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -371,7 +371,7 @@ def test_blocks_with_frames_and_fill_value(file_stereo_r): def test_blocks_with_out(file_stereo_r): out = np.empty((3, 2)) blocks = list(sf.blocks(file_stereo_r, out=out)) - assert blocks[0].base is out + assert blocks[0] is out # First frame was overwritten by second block: assert np.all(blocks[0] == data_stereo[[3, 1, 2]]) From eb2ea63c65de873daa11774c156c8467f6900257 Mon Sep 17 00:00:00 2001 From: Till Hoffmann Date: Sun, 8 Oct 2017 13:05:38 +0100 Subject: [PATCH 3/4] Style changes. --- soundfile.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/soundfile.py b/soundfile.py index 73b11f8..83ff24b 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1176,6 +1176,15 @@ 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! If + `frames` is not given, it is obtained from the length of + `out`. If `overlap` is not 0, `out` should not be modified + in-place. Examples -------- @@ -1193,7 +1202,6 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', if out is None: if blocksize is None: raise TypeError("One of {blocksize, out} must be specified") - # Allocate memory out = self._create_empty_array(blocksize, always_2d, dtype) copy_out = True else: @@ -1203,20 +1211,20 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', blocksize = len(out) copy_out = False - # Specify an offset for the output array - offset = 0 - # Get the number of remaining frames + output_offset = 0 frames = self._check_frames(frames, fill_value) while frames > 0: - toread = min(blocksize - offset, frames) - self.read(toread, dtype, always_2d, fill_value, out[offset:]) - block = out[:frames + overlap] if blocksize > frames + overlap and fill_value is None \ - else out + toread = min(blocksize - output_offset, frames) + self.read(toread, dtype, always_2d, fill_value, out[output_offset:]) + 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 # Copy the end of the block to the beginning of the next if overlap: - offset = overlap + output_offset = overlap out[:overlap] = out[-overlap:] def truncate(self, frames=None): From 49cebbf015492631207eac36abf4a689511acf97 Mon Sep 17 00:00:00 2001 From: Till Hoffmann Date: Thu, 12 Oct 2017 10:33:47 +0100 Subject: [PATCH 4/4] Ensure in-place modifications do not cause unexpected behaviour. --- soundfile.py | 24 +++++++++++++++--------- tests/test_pysoundfile.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/soundfile.py b/soundfile.py index 83ff24b..a0dc569 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1181,10 +1181,7 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', 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! If - `frames` is not given, it is obtained from the length of - `out`. If `overlap` is not 0, `out` should not be modified - in-place. + arguments `dtype` and `always_2d` are silently ignored! Examples -------- @@ -1211,21 +1208,30 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', blocksize = len(out) copy_out = False - output_offset = 0 + overlap_memory = None frames = self._check_frames(frames, fill_value) while frames > 0: + 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 - # Copy the end of the block to the beginning of the next - if overlap: - output_offset = overlap - out[:overlap] = out[-overlap:] 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 3b91a92..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))