diff --git a/soundfile.py b/soundfile.py index 0bf3a6e..dc13ef7 100644 --- a/soundfile.py +++ b/soundfile.py @@ -934,7 +934,8 @@ def read(self, frames: int = -1, dtype: dtype_str = 'float64', 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 -------- @@ -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) @@ -1421,6 +1423,8 @@ 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: + 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 new file mode 100644 index 0000000..96d3762 --- /dev/null +++ b/tests/test_byteorder.py @@ -0,0 +1,77 @@ +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]) +@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) + 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)) + 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) + 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_rejected_without_mutation( + 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)) + 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