Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 64 additions & 5 deletions soundfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@
'int16': 'short'
}

_bitrate_modes = {
'CONSTANT': 0,
'AVERAGE': 1,
'VARIABLE': 2,
}

try: # packaged lib (in _soundfile_data which should be on python path)
if _sys.platform == 'darwin':
from platform import machine as _machine
Expand Down Expand Up @@ -290,7 +296,7 @@ def read(file, frames=-1, start=0, stop=None, dtype='float64', always_2d=False,


def write(file, data, samplerate, subtype=None, endian=None, format=None,
closefd=True):
closefd=True, compression_level=None, bitrate_mode=None):
"""Write data to a sound file.

.. note:: If *file* exists, it will be truncated and overwritten!
Expand Down Expand Up @@ -325,6 +331,11 @@ def write(file, data, samplerate, subtype=None, endian=None, format=None,
format, endian, closefd
See `SoundFile`.

compression_level : float, optional
See `libsndfile document <https://github.com/libsndfile/libsndfile/blob/c81375f070f3c6764969a738eacded64f53a076e/docs/command.md>`__.
bitrate_mode : {'CONSTANT', 'AVERAGE', 'VARIABLE'}, optional
See `libsndfile document <https://github.com/libsndfile/libsndfile/blob/c81375f070f3c6764969a738eacded64f53a076e/docs/command.md>`__.

Examples
--------
Write 10 frames of random data to a new file:
Expand All @@ -342,7 +353,7 @@ def write(file, data, samplerate, subtype=None, endian=None, format=None,
channels = data.shape[1]
with SoundFile(file, 'w', samplerate, channels,
subtype, endian, format, closefd) as f:
f.write(data)
f.write(data, compression_level, bitrate_mode)


def blocks(file, blocksize=None, overlap=0, frames=-1, start=0, stop=None,
Expand Down Expand Up @@ -968,7 +979,7 @@ def buffer_read_into(self, buffer, dtype):
frames = self._cdata_io('read', cdata, ctype, frames)
return frames

def write(self, data):
def write(self, data, compression_level=None, bitrate_mode=None):
"""Write audio data from a NumPy array to the file.

Writes a number of frames at the read/write position to the
Expand Down Expand Up @@ -998,6 +1009,12 @@ def write(self, data):
file will then contain ``np.array([42.],
dtype='float32')``.

Other Parameters
----------------
compression_level : float, optional
bitrate_mode : {'CONSTANT', 'AVERAGE', 'VARIABLE'}, optional
See `libsndfile document <https://github.com/libsndfile/libsndfile/blob/c81375f070f3c6764969a738eacded64f53a076e/docs/command.md>`__.

Examples
--------
>>> import numpy as np
Expand All @@ -1015,13 +1032,20 @@ def write(self, data):

"""
import numpy as np

if compression_level is not None:
# needs to be called before set_bitrate_mode
self.set_compression_level(compression_level)
if bitrate_mode is not None:
self.set_bitrate_mode(bitrate_mode)

# no copy is made if data has already the correct memory layout:
data = np.ascontiguousarray(data)
written = self._array_io('write', data, len(data))
assert written == len(data)
self._update_frames(written)

def buffer_write(self, data, dtype):
def buffer_write(self, data, dtype, compression_level=None, bitrate_mode=None):
"""Write audio data from a buffer/bytes object to the file.

Writes the contents of *data* to the file at the current
Expand All @@ -1037,11 +1061,23 @@ def buffer_write(self, data, dtype):
dtype : {'float64', 'float32', 'int32', 'int16'}
The data type of the audio data stored in *data*.

Other Parameters
----------------
compression_level : float, optional
bitrate_mode : {'CONSTANT', 'AVERAGE', 'VARIABLE'}, optional
See `libsndfile document <https://github.com/libsndfile/libsndfile/blob/c81375f070f3c6764969a738eacded64f53a076e/docs/command.md>`__.

See Also
--------
.write, buffer_read

"""
if compression_level is not None:
# needs to be called before set_bitrate_mode
self.set_compression_level(compression_level)
if bitrate_mode is not None:
self.set_bitrate_mode(bitrate_mode)

ctype = self._check_dtype(dtype)
cdata, frames = self._check_buffer(data, ctype)
written = self._cdata_io('write', cdata, ctype, frames)
Expand Down Expand Up @@ -1399,7 +1435,30 @@ def copy_metadata(self):
if data:
strs[strtype] = _ffi.string(data).decode('utf-8', 'replace')
return strs


def set_bitrate_mode(self, bitrate_mode):
"""Call libsndfile's set bitrate mode function."""
assert bitrate_mode in _bitrate_modes

pointer_bitrate_mode = _ffi.new("int[1]")
pointer_bitrate_mode[0] = _bitrate_modes[bitrate_mode]
err = _snd.sf_command(self._file, _snd.SFC_SET_BITRATE_MODE, pointer_bitrate_mode, _ffi.sizeof(pointer_bitrate_mode))
if err != _snd.SF_TRUE:
err = _snd.sf_error(self._file)
raise LibsndfileError(err, f"Error set bitrate mode {bitrate_mode}")


def set_compression_level(self, compression_level):
"""Call libsndfile's set compression level function."""
if not (0 <= compression_level <= 1):
raise ValueError("Compression level must be in range [0..1]")

pointer_compression_level = _ffi.new("double[1]")
pointer_compression_level[0] = compression_level
err = _snd.sf_command(self._file, _snd.SFC_SET_COMPRESSION_LEVEL, pointer_compression_level, _ffi.sizeof(pointer_compression_level))
if err != _snd.SF_TRUE:
err = _snd.sf_error(self._file)
raise LibsndfileError(err, f"Error set compression level {compression_level}")


def _error_check(err, prefix=""):
Expand Down
8 changes: 8 additions & 0 deletions soundfile_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

SFC_SET_SCALE_FLOAT_INT_READ = 0x1014,
SFC_SET_SCALE_INT_FLOAT_WRITE = 0x1015,

SFC_SET_COMPRESSION_LEVEL = 0x1301,
SFC_SET_BITRATE_MODE = 0x1305,
} ;

enum
Expand All @@ -38,6 +41,11 @@
SFM_READ = 0x10,
SFM_WRITE = 0x20,
SFM_RDWR = 0x30,

/* Modes for bitrate. */
SF_BITRATE_MODE_CONSTANT = 0,
SF_BITRATE_MODE_AVERAGE = 1,
SF_BITRATE_MODE_VARIABLE = 2,
} ;

typedef int64_t sf_count_t ;
Expand Down
4 changes: 4 additions & 0 deletions tests/test_argspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ def test_write_defaults():
write_defaults = defaults(sf.write)
init_defaults = defaults(sf.SoundFile.__init__)

# Only write values
del write_defaults['compression_level'] # compression_level is [0, 1] or None
del write_defaults['bitrate_mode'] # bitrate_mode is 'CONSTANT' or 'AVERAGE' or 'VARIABLE' or None

# Same default values as SoundFile.__init__()
init_defaults = remove_items(init_defaults, write_defaults)

Expand Down
54 changes: 54 additions & 0 deletions tests/test_soundfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
filename_mono = 'tests/mono.wav'
filename_raw = 'tests/mono.raw'
filename_new = 'tests/delme.please'
filename_mp3 = 'tests/stereo.mp3'
filename_flac = 'tests/stereo.flac'


if sys.version_info >= (3, 6):
Expand Down Expand Up @@ -295,6 +297,58 @@ def test_write_with_unknown_extension(filename):
assert "file extension" in str(excinfo.value)


def test_write_mp3_compression():
sr = 44100
sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=0, bitrate_mode='CONSTANT')
constant_0_size = os.path.getsize(filename_mp3)

sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=0, bitrate_mode='VARIABLE')
variable_0_size = os.path.getsize(filename_mp3)
assert variable_0_size < constant_0_size

sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=0, bitrate_mode='AVERAGE')
average_0_size = os.path.getsize(filename_mp3)
assert (average_0_size < variable_0_size < constant_0_size)

sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=0.999, bitrate_mode='CONSTANT')
constant_1_size= os.path.getsize(filename_mp3)
assert constant_1_size < constant_0_size

sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=0.999, bitrate_mode='VARIABLE')
variable_1_size = os.path.getsize(filename_mp3)
assert constant_1_size <variable_1_size < constant_0_size

sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=0.999, bitrate_mode='AVERAGE')
average_1_size = os.path.getsize(filename_mp3)
assert constant_1_size < average_1_size < constant_0_size

# This test case should be OK, but an exception is raised at libsndfile<=1.2.2.
with pytest.raises(RuntimeError) as excinfo:
sf.write(filename_mp3, data_stereo, sr, format='MP3', subtype='MPEG_LAYER_III',
compression_level=1, bitrate_mode='VARIABLE')
assert "compression" in str(excinfo.value)


def test_write_flac_compression():
sr = 44100
# Compression requires a certain size
data_stereo = np.random.random((sr, 1))
data_stereo = np.concat([data_stereo, -data_stereo], axis=1)

sf.write(filename_flac, data_stereo, sr, format='FLAC', subtype='PCM_16', compression_level=0)
low_compression_size = os.path.getsize(filename_flac)

sf.write(filename_flac, data_stereo, sr, format='FLAC', subtype='PCM_16', compression_level=1)
high_compression_size = os.path.getsize(filename_flac)
assert high_compression_size < low_compression_size


# -----------------------------------------------------------------------------
# Test blocks() function
# -----------------------------------------------------------------------------
Expand Down