From 323f68dabd00801ae30222381407e5b98712489b Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Sun, 11 Oct 2026 21:40:34 -0700 Subject: [PATCH 1/3] Preserve NumPy sample values across non-native byte orders --- soundfile.py | 6 ++++ tests/test_byteorder.py | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/test_byteorder.py diff --git a/soundfile.py b/soundfile.py index 0bf3a6e..a488b8e 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1421,6 +1421,12 @@ def _array_io(self, action, array, frames): raise ValueError("Data must be C-contiguous") ctype = self._check_dtype(array.dtype.name) assert array.dtype.itemsize == _ffi.sizeof(ctype) + if not array.dtype.isnative: + native_array = array.astype(array.dtype.newbyteorder('=')) + processed = self._array_io(action, native_array, frames) + if action == 'read': + array[:processed] = native_array[:processed] + return processed cdata = _ffi.cast(ctype + '*', array.__array_interface__['data'][0]) return self._cdata_io(action, cdata, ctype, frames) diff --git a/tests/test_byteorder.py b/tests/test_byteorder.py new file mode 100644 index 0000000..d26e98c --- /dev/null +++ b/tests/test_byteorder.py @@ -0,0 +1,71 @@ +import io +import sys + +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +import soundfile as sf + + +@pytest.mark.parametrize( + 'dtype,subtype', + [ + ('int16', 'PCM_16'), + ('int32', 'PCM_32'), + ('float32', 'FLOAT'), + ('float64', 'DOUBLE'), + ], +) +@pytest.mark.parametrize('channels', [1, 2]) +def test_write_non_native_endian_preserves_samples(dtype, subtype, channels): + order = '>' if sys.byteorder == 'little' else '<' + values = ( + np.array([1, 256, -2345, 42], dtype=dtype) + if dtype.startswith('int') + else np.array([0.25, 0.5, -0.75, 0.125], dtype=dtype) + ) + if channels == 2: + values = np.column_stack((values, -values)) + swapped = values.astype(np.dtype(dtype).newbyteorder(order)) + snapshot = swapped.tobytes() + output = io.BytesIO() + sf.write(output, swapped, 8000, format='WAV', subtype=subtype) + output.seek(0) + decoded, _ = sf.read(output, dtype=dtype) + assert_array_equal(decoded, values) + assert swapped.tobytes() == snapshot + + +@pytest.mark.parametrize( + 'dtype,subtype', + [ + ('int16', 'PCM_16'), + ('int32', 'PCM_32'), + ('float32', 'FLOAT'), + ('float64', 'DOUBLE'), + ], +) +@pytest.mark.parametrize('channels', [1, 2]) +@pytest.mark.parametrize('fill', [None, 17]) +def test_read_non_native_endian_out_preserves_values_and_tail( + dtype, subtype, channels, fill +): + order = '>' if sys.byteorder == 'little' else '<' + values = ( + np.array([1, 256, -2345, 42], dtype=dtype) + if dtype.startswith('int') + else np.array([0.25, 0.5, -0.75, 0.125], dtype=dtype) + ) + if channels == 2: + values = np.column_stack((values, -values)) + output = io.BytesIO() + sf.write(output, values, 8000, format='WAV', subtype=subtype) + output.seek(0) + shape = (6,) if channels == 1 else (6, 2) + out = np.full(shape, 23, dtype=np.dtype(dtype).newbyteorder(order)) + decoded, _ = sf.read(output, out=out, fill_value=fill) + assert_array_equal(decoded[:4], values) + assert np.shares_memory(decoded, out) + assert_array_equal(out[4:], 23 if fill is None else fill) + assert decoded.dtype == out.dtype From a8857641aea1db6a9ab7b63e32c32390cdac2c31 Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Mon, 12 Oct 2026 12:19:26 -0700 Subject: [PATCH 2/3] Handle byte order alongside input contiguity and output validation --- soundfile.py | 12 +++++------- tests/test_byteorder.py | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/soundfile.py b/soundfile.py index a488b8e..f63874e 100644 --- a/soundfile.py +++ b/soundfile.py @@ -931,7 +931,8 @@ def read(self, frames: int = -1, dtype: dtype_str = 'float64', returned. 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 + array instead of creating a new array. The array must be + C-contiguous and have native byte order. 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*. @@ -1086,7 +1087,8 @@ def write(self, data: AudioData) -> None: import numpy as np # no copy is made if data has already the correct memory layout: - data = np.ascontiguousarray(data) + data = np.asarray(data) + data = np.ascontiguousarray(data, dtype=data.dtype.newbyteorder('=')) written = self._array_io('write', data, len(data)) assert written == len(data) self._update_frames(written) @@ -1422,11 +1424,7 @@ def _array_io(self, action, array, frames): ctype = self._check_dtype(array.dtype.name) assert array.dtype.itemsize == _ffi.sizeof(ctype) if not array.dtype.isnative: - native_array = array.astype(array.dtype.newbyteorder('=')) - processed = self._array_io(action, native_array, frames) - if action == 'read': - array[:processed] = native_array[:processed] - return processed + raise ValueError("Data must have native byte order") cdata = _ffi.cast(ctype + '*', array.__array_interface__['data'][0]) return self._cdata_io(action, cdata, ctype, frames) diff --git a/tests/test_byteorder.py b/tests/test_byteorder.py index d26e98c..96d3762 100644 --- a/tests/test_byteorder.py +++ b/tests/test_byteorder.py @@ -18,7 +18,8 @@ ], ) @pytest.mark.parametrize('channels', [1, 2]) -def test_write_non_native_endian_preserves_samples(dtype, subtype, channels): +@pytest.mark.parametrize('strided', [False, True]) +def test_write_non_native_endian_preserves_samples(dtype, subtype, channels, strided): order = '>' if sys.byteorder == 'little' else '<' values = ( np.array([1, 256, -2345, 42], dtype=dtype) @@ -28,6 +29,10 @@ def test_write_non_native_endian_preserves_samples(dtype, subtype, channels): if channels == 2: values = np.column_stack((values, -values)) swapped = values.astype(np.dtype(dtype).newbyteorder(order)) + if strided: + backing = np.empty((len(values) * 2,) + values.shape[1:], dtype=swapped.dtype) + backing[::2] = swapped + swapped = backing[::2] snapshot = swapped.tobytes() output = io.BytesIO() sf.write(output, swapped, 8000, format='WAV', subtype=subtype) @@ -48,7 +53,7 @@ def test_write_non_native_endian_preserves_samples(dtype, subtype, channels): ) @pytest.mark.parametrize('channels', [1, 2]) @pytest.mark.parametrize('fill', [None, 17]) -def test_read_non_native_endian_out_preserves_values_and_tail( +def test_read_non_native_endian_out_rejected_without_mutation( dtype, subtype, channels, fill ): order = '>' if sys.byteorder == 'little' else '<' @@ -64,8 +69,9 @@ def test_read_non_native_endian_out_preserves_values_and_tail( output.seek(0) shape = (6,) if channels == 1 else (6, 2) out = np.full(shape, 23, dtype=np.dtype(dtype).newbyteorder(order)) - decoded, _ = sf.read(output, out=out, fill_value=fill) - assert_array_equal(decoded[:4], values) - assert np.shares_memory(decoded, out) - assert_array_equal(out[4:], 23 if fill is None else fill) - assert decoded.dtype == out.dtype + snapshot = out.tobytes() + with sf.SoundFile(output) as file: + with pytest.raises(ValueError, match='native byte order'): + file.read(out=out, fill_value=fill) + assert file.tell() == 0 + assert out.tobytes() == snapshot From ff663f0425076e6a475e4430b54150d3f5698267 Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Tue, 13 Oct 2026 11:57:08 -0700 Subject: [PATCH 3/3] Keep output-buffer documentation in logical sentence order --- soundfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/soundfile.py b/soundfile.py index f63874e..dc13ef7 100644 --- a/soundfile.py +++ b/soundfile.py @@ -931,11 +931,11 @@ def read(self, frames: int = -1, dtype: dtype_str = 'float64', returned. out : `numpy.ndarray` or subclass, optional If *out* is specified, the data is written into the given - array instead of creating a new array. The array must be - C-contiguous and have native byte order. In this case, the + 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*. + *out*. The array must be C-contiguous and have native byte + order. Examples --------