Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions soundfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand Down Expand Up @@ -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)
Comment thread
bastibe marked this conversation as resolved.
data = np.ascontiguousarray(data, dtype=data.dtype.newbyteorder('='))
written = self._array_io('write', data, len(data))
assert written == len(data)
self._update_frames(written)
Expand Down Expand Up @@ -1421,6 +1423,8 @@ def _array_io(self, action, array, frames):
raise ValueError("Data must be C-contiguous")
Comment thread
bastibe marked this conversation as resolved.
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)

Expand Down
77 changes: 77 additions & 0 deletions tests/test_byteorder.py
Original file line number Diff line number Diff line change
@@ -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
Loading