Spaces:
Sleeping
Sleeping
"""python-soundfile is an audio library based on libsndfile, CFFI and NumPy. | |
Sound files can be read or written directly using the functions | |
`read()` and `write()`. | |
To read a sound file in a block-wise fashion, use `blocks()`. | |
Alternatively, sound files can be opened as `SoundFile` objects. | |
For further information, see https://python-soundfile.readthedocs.io/. | |
""" | |
__version__ = "0.12.1" | |
import os as _os | |
import sys as _sys | |
from os import SEEK_SET, SEEK_CUR, SEEK_END | |
from ctypes.util import find_library as _find_library | |
from _soundfile import ffi as _ffi | |
try: | |
_unicode = unicode # doesn't exist in Python 3.x | |
except NameError: | |
_unicode = str | |
_str_types = { | |
'title': 0x01, | |
'copyright': 0x02, | |
'software': 0x03, | |
'artist': 0x04, | |
'comment': 0x05, | |
'date': 0x06, | |
'album': 0x07, | |
'license': 0x08, | |
'tracknumber': 0x09, | |
'genre': 0x10, | |
} | |
_formats = { | |
'WAV': 0x010000, # Microsoft WAV format (little endian default). | |
'AIFF': 0x020000, # Apple/SGI AIFF format (big endian). | |
'AU': 0x030000, # Sun/NeXT AU format (big endian). | |
'RAW': 0x040000, # RAW PCM data. | |
'PAF': 0x050000, # Ensoniq PARIS file format. | |
'SVX': 0x060000, # Amiga IFF / SVX8 / SV16 format. | |
'NIST': 0x070000, # Sphere NIST format. | |
'VOC': 0x080000, # VOC files. | |
'IRCAM': 0x0A0000, # Berkeley/IRCAM/CARL | |
'W64': 0x0B0000, # Sonic Foundry's 64 bit RIFF/WAV | |
'MAT4': 0x0C0000, # Matlab (tm) V4.2 / GNU Octave 2.0 | |
'MAT5': 0x0D0000, # Matlab (tm) V5.0 / GNU Octave 2.1 | |
'PVF': 0x0E0000, # Portable Voice Format | |
'XI': 0x0F0000, # Fasttracker 2 Extended Instrument | |
'HTK': 0x100000, # HMM Tool Kit format | |
'SDS': 0x110000, # Midi Sample Dump Standard | |
'AVR': 0x120000, # Audio Visual Research | |
'WAVEX': 0x130000, # MS WAVE with WAVEFORMATEX | |
'SD2': 0x160000, # Sound Designer 2 | |
'FLAC': 0x170000, # FLAC lossless file format | |
'CAF': 0x180000, # Core Audio File format | |
'WVE': 0x190000, # Psion WVE format | |
'OGG': 0x200000, # Xiph OGG container | |
'MPC2K': 0x210000, # Akai MPC 2000 sampler | |
'RF64': 0x220000, # RF64 WAV file | |
'MP3': 0x230000, # MPEG-1/2 audio stream | |
} | |
_subtypes = { | |
'PCM_S8': 0x0001, # Signed 8 bit data | |
'PCM_16': 0x0002, # Signed 16 bit data | |
'PCM_24': 0x0003, # Signed 24 bit data | |
'PCM_32': 0x0004, # Signed 32 bit data | |
'PCM_U8': 0x0005, # Unsigned 8 bit data (WAV and RAW only) | |
'FLOAT': 0x0006, # 32 bit float data | |
'DOUBLE': 0x0007, # 64 bit float data | |
'ULAW': 0x0010, # U-Law encoded. | |
'ALAW': 0x0011, # A-Law encoded. | |
'IMA_ADPCM': 0x0012, # IMA ADPCM. | |
'MS_ADPCM': 0x0013, # Microsoft ADPCM. | |
'GSM610': 0x0020, # GSM 6.10 encoding. | |
'VOX_ADPCM': 0x0021, # OKI / Dialogix ADPCM | |
'NMS_ADPCM_16': 0x0022, # 16kbs NMS G721-variant encoding. | |
'NMS_ADPCM_24': 0x0023, # 24kbs NMS G721-variant encoding. | |
'NMS_ADPCM_32': 0x0024, # 32kbs NMS G721-variant encoding. | |
'G721_32': 0x0030, # 32kbs G721 ADPCM encoding. | |
'G723_24': 0x0031, # 24kbs G723 ADPCM encoding. | |
'G723_40': 0x0032, # 40kbs G723 ADPCM encoding. | |
'DWVW_12': 0x0040, # 12 bit Delta Width Variable Word encoding. | |
'DWVW_16': 0x0041, # 16 bit Delta Width Variable Word encoding. | |
'DWVW_24': 0x0042, # 24 bit Delta Width Variable Word encoding. | |
'DWVW_N': 0x0043, # N bit Delta Width Variable Word encoding. | |
'DPCM_8': 0x0050, # 8 bit differential PCM (XI only) | |
'DPCM_16': 0x0051, # 16 bit differential PCM (XI only) | |
'VORBIS': 0x0060, # Xiph Vorbis encoding. | |
'OPUS': 0x0064, # Xiph/Skype Opus encoding. | |
'ALAC_16': 0x0070, # Apple Lossless Audio Codec (16 bit). | |
'ALAC_20': 0x0071, # Apple Lossless Audio Codec (20 bit). | |
'ALAC_24': 0x0072, # Apple Lossless Audio Codec (24 bit). | |
'ALAC_32': 0x0073, # Apple Lossless Audio Codec (32 bit). | |
'MPEG_LAYER_I': 0x0080, # MPEG-1 Audio Layer I. | |
'MPEG_LAYER_II': 0x0081, # MPEG-1 Audio Layer II. | |
'MPEG_LAYER_III': 0x0082, # MPEG-2 Audio Layer III. | |
} | |
_endians = { | |
'FILE': 0x00000000, # Default file endian-ness. | |
'LITTLE': 0x10000000, # Force little endian-ness. | |
'BIG': 0x20000000, # Force big endian-ness. | |
'CPU': 0x30000000, # Force CPU endian-ness. | |
} | |
# libsndfile doesn't specify default subtypes, these are somehow arbitrary: | |
_default_subtypes = { | |
'WAV': 'PCM_16', | |
'AIFF': 'PCM_16', | |
'AU': 'PCM_16', | |
# 'RAW': # subtype must be explicit! | |
'PAF': 'PCM_16', | |
'SVX': 'PCM_16', | |
'NIST': 'PCM_16', | |
'VOC': 'PCM_16', | |
'IRCAM': 'PCM_16', | |
'W64': 'PCM_16', | |
'MAT4': 'DOUBLE', | |
'MAT5': 'DOUBLE', | |
'PVF': 'PCM_16', | |
'XI': 'DPCM_16', | |
'HTK': 'PCM_16', | |
'SDS': 'PCM_16', | |
'AVR': 'PCM_16', | |
'WAVEX': 'PCM_16', | |
'SD2': 'PCM_16', | |
'FLAC': 'PCM_16', | |
'CAF': 'PCM_16', | |
'WVE': 'ALAW', | |
'OGG': 'VORBIS', | |
'MPC2K': 'PCM_16', | |
'RF64': 'PCM_16', | |
'MP3': 'MPEG_LAYER_III', | |
} | |
_ffi_types = { | |
'float64': 'double', | |
'float32': 'float', | |
'int32': 'int', | |
'int16': 'short' | |
} | |
try: # packaged lib (in _soundfile_data which should be on python path) | |
if _sys.platform == 'darwin': | |
from platform import machine as _machine | |
_packaged_libname = 'libsndfile_' + _machine() + '.dylib' | |
elif _sys.platform == 'win32': | |
from platform import architecture as _architecture | |
_packaged_libname = 'libsndfile_' + _architecture()[0] + '.dll' | |
elif _sys.platform == 'linux': | |
from platform import machine as _machine | |
_packaged_libname = 'libsndfile_' + _machine() + '.so' | |
else: | |
raise OSError('no packaged library for this platform') | |
import _soundfile_data # ImportError if this doesn't exist | |
_path = _os.path.dirname(_soundfile_data.__file__) # TypeError if __file__ is None | |
_full_path = _os.path.join(_path, _packaged_libname) | |
_snd = _ffi.dlopen(_full_path) # OSError if file doesn't exist or can't be loaded | |
except (OSError, ImportError, TypeError): | |
try: # system-wide libsndfile: | |
_libname = _find_library('sndfile') | |
if _libname is None: | |
raise OSError('sndfile library not found using ctypes.util.find_library') | |
_snd = _ffi.dlopen(_libname) | |
except OSError: | |
# Try explicit file name, if the general does not work (e.g. on nixos) | |
if _sys.platform == 'darwin': | |
_explicit_libname = 'libsndfile.dylib' | |
elif _sys.platform == 'win32': | |
_explicit_libname = 'libsndfile.dll' | |
elif _sys.platform == 'linux': | |
_explicit_libname = 'libsndfile.so' | |
else: | |
raise | |
# Homebrew on Apple M1 uses a `/opt/homebrew/lib` instead of | |
# `/usr/local/lib`. We are making sure we pick that up. | |
from platform import machine as _machine | |
if _sys.platform == 'darwin' and _machine() == 'arm64': | |
_hbrew_path = '/opt/homebrew/lib/' if _os.path.isdir('/opt/homebrew/lib/') \ | |
else '/usr/local/lib/' | |
_snd = _ffi.dlopen(_os.path.join(_hbrew_path, _explicit_libname)) | |
else: | |
_snd = _ffi.dlopen(_explicit_libname) | |
__libsndfile_version__ = _ffi.string(_snd.sf_version_string()).decode('utf-8', 'replace') | |
if __libsndfile_version__.startswith('libsndfile-'): | |
__libsndfile_version__ = __libsndfile_version__[len('libsndfile-'):] | |
def read(file, frames=-1, start=0, stop=None, dtype='float64', always_2d=False, | |
fill_value=None, out=None, samplerate=None, channels=None, | |
format=None, subtype=None, endian=None, closefd=True): | |
"""Provide audio data from a sound file as NumPy array. | |
By default, the whole file is read from the beginning, but the | |
position to start reading can be specified with *start* and the | |
number of frames to read can be specified with *frames*. | |
Alternatively, a range can be specified with *start* and *stop*. | |
If there is less data left in the file than requested, the rest of | |
the frames are filled with *fill_value*. | |
If no *fill_value* is specified, a smaller array is returned. | |
Parameters | |
---------- | |
file : str or int or file-like object | |
The file to read from. See `SoundFile` for details. | |
frames : int, optional | |
The number of frames to read. If *frames* is negative, the whole | |
rest of the file is read. Not allowed if *stop* is given. | |
start : int, optional | |
Where to start reading. A negative value counts from the end. | |
stop : int, optional | |
The index after the last frame to be read. A negative value | |
counts from the end. Not allowed if *frames* is given. | |
dtype : {'float64', 'float32', 'int32', 'int16'}, optional | |
Data type of the returned array, by default ``'float64'``. | |
Floating point audio data is typically in the range from | |
``-1.0`` to ``1.0``. Integer data is in the range from | |
``-2**15`` to ``2**15-1`` for ``'int16'`` and from ``-2**31`` to | |
``2**31-1`` for ``'int32'``. | |
.. note:: Reading int values from a float file will *not* | |
scale the data to [-1.0, 1.0). If the file contains | |
``np.array([42.6], dtype='float32')``, you will read | |
``np.array([43], dtype='int32')`` for ``dtype='int32'``. | |
Returns | |
------- | |
audiodata : `numpy.ndarray` or type(out) | |
A two-dimensional (frames x channels) NumPy array is returned. | |
If the sound file has only one channel, a one-dimensional array | |
is returned. Use ``always_2d=True`` to return a two-dimensional | |
array anyway. | |
If *out* was specified, it is returned. If *out* has more | |
frames than available in the file (or if *frames* is smaller | |
than the length of *out*) and no *fill_value* is given, then | |
only a part of *out* is overwritten and a view containing all | |
valid frames is returned. | |
samplerate : int | |
The sample rate of the audio file. | |
Other Parameters | |
---------------- | |
always_2d : bool, optional | |
By default, reading a mono sound file will return a | |
one-dimensional array. With ``always_2d=True``, audio data is | |
always returned as a two-dimensional array, even if the audio | |
file has only one channel. | |
fill_value : float, optional | |
If more frames are requested than available in the file, the | |
rest of the output is be filled with *fill_value*. If | |
*fill_value* is not specified, a smaller array is 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 arguments | |
*dtype* and *always_2d* are silently ignored! If *frames* is | |
not given, it is obtained from the length of *out*. | |
samplerate, channels, format, subtype, endian, closefd | |
See `SoundFile`. | |
Examples | |
-------- | |
>>> import soundfile as sf | |
>>> data, samplerate = sf.read('stereo_file.wav') | |
>>> data | |
array([[ 0.71329652, 0.06294799], | |
[-0.26450912, -0.38874483], | |
... | |
[ 0.67398441, -0.11516333]]) | |
>>> samplerate | |
44100 | |
""" | |
with SoundFile(file, 'r', samplerate, channels, | |
subtype, endian, format, closefd) as f: | |
frames = f._prepare_read(start, stop, frames) | |
data = f.read(frames, dtype, always_2d, fill_value, out) | |
return data, f.samplerate | |
def write(file, data, samplerate, subtype=None, endian=None, format=None, | |
closefd=True): | |
"""Write data to a sound file. | |
.. note:: If *file* exists, it will be truncated and overwritten! | |
Parameters | |
---------- | |
file : str or int or file-like object | |
The file to write to. See `SoundFile` for details. | |
data : array_like | |
The data to write. Usually two-dimensional (frames x channels), | |
but one-dimensional *data* can be used for mono files. | |
Only the data types ``'float64'``, ``'float32'``, ``'int32'`` | |
and ``'int16'`` are supported. | |
.. note:: The data type of *data* does **not** select the data | |
type of the written file. Audio data will be | |
converted to the given *subtype*. Writing int values | |
to a float file will *not* scale the values to | |
[-1.0, 1.0). If you write the value ``np.array([42], | |
dtype='int32')``, to a ``subtype='FLOAT'`` file, the | |
file will then contain ``np.array([42.], | |
dtype='float32')``. | |
samplerate : int | |
The sample rate of the audio data. | |
subtype : str, optional | |
See `default_subtype()` for the default value and | |
`available_subtypes()` for all possible values. | |
Other Parameters | |
---------------- | |
format, endian, closefd | |
See `SoundFile`. | |
Examples | |
-------- | |
Write 10 frames of random data to a new file: | |
>>> import numpy as np | |
>>> import soundfile as sf | |
>>> sf.write('stereo_file.wav', np.random.randn(10, 2), 44100, 'PCM_24') | |
""" | |
import numpy as np | |
data = np.asarray(data) | |
if data.ndim == 1: | |
channels = 1 | |
else: | |
channels = data.shape[1] | |
with SoundFile(file, 'w', samplerate, channels, | |
subtype, endian, format, closefd) as f: | |
f.write(data) | |
def blocks(file, blocksize=None, overlap=0, frames=-1, start=0, stop=None, | |
dtype='float64', always_2d=False, fill_value=None, out=None, | |
samplerate=None, channels=None, | |
format=None, subtype=None, endian=None, closefd=True): | |
"""Return a generator for block-wise reading. | |
By default, iteration starts at the beginning and stops at the end | |
of the file. Use *start* to start at a later position and *frames* | |
or *stop* to stop earlier. | |
If you stop iterating over the generator before it's exhausted, | |
the sound file is not closed. This is normally not a problem | |
because the file is opened in read-only mode. To close the file | |
properly, the generator's ``close()`` method can be called. | |
Parameters | |
---------- | |
file : str or int or file-like object | |
The file to read from. See `SoundFile` for details. | |
blocksize : int | |
The number of frames to read per block. | |
Either this or *out* must be given. | |
overlap : int, optional | |
The number of frames to rewind between each block. | |
Yields | |
------ | |
`numpy.ndarray` or type(out) | |
Blocks of audio data. | |
If *out* was given, and the requested frames are not an integer | |
multiple of the length of *out*, and no *fill_value* was given, | |
the last block will be a smaller view into *out*. | |
Other Parameters | |
---------------- | |
frames, start, stop | |
See `read()`. | |
dtype : {'float64', 'float32', 'int32', 'int16'}, optional | |
See `read()`. | |
always_2d, fill_value, out | |
See `read()`. | |
samplerate, channels, format, subtype, endian, closefd | |
See `SoundFile`. | |
Examples | |
-------- | |
>>> import soundfile as sf | |
>>> for block in sf.blocks('stereo_file.wav', blocksize=1024): | |
>>> pass # do something with 'block' | |
""" | |
with SoundFile(file, 'r', samplerate, channels, | |
subtype, endian, format, closefd) as f: | |
frames = f._prepare_read(start, stop, frames) | |
for block in f.blocks(blocksize, overlap, frames, | |
dtype, always_2d, fill_value, out): | |
yield block | |
class _SoundFileInfo(object): | |
"""Information about a SoundFile""" | |
def __init__(self, file, verbose): | |
self.verbose = verbose | |
with SoundFile(file) as f: | |
self.name = f.name | |
self.samplerate = f.samplerate | |
self.channels = f.channels | |
self.frames = f.frames | |
self.duration = float(self.frames)/f.samplerate | |
self.format = f.format | |
self.subtype = f.subtype | |
self.endian = f.endian | |
self.format_info = f.format_info | |
self.subtype_info = f.subtype_info | |
self.sections = f.sections | |
self.extra_info = f.extra_info | |
def _duration_str(self): | |
hours, rest = divmod(self.duration, 3600) | |
minutes, seconds = divmod(rest, 60) | |
if hours >= 1: | |
duration = "{0:.0g}:{1:02.0g}:{2:05.3f} h".format(hours, minutes, seconds) | |
elif minutes >= 1: | |
duration = "{0:02.0g}:{1:05.3f} min".format(minutes, seconds) | |
elif seconds <= 1: | |
duration = "{0:d} samples".format(self.frames) | |
else: | |
duration = "{0:.3f} s".format(seconds) | |
return duration | |
def __repr__(self): | |
info = "\n".join( | |
["{0.name}", | |
"samplerate: {0.samplerate} Hz", | |
"channels: {0.channels}", | |
"duration: {0._duration_str}", | |
"format: {0.format_info} [{0.format}]", | |
"subtype: {0.subtype_info} [{0.subtype}]"]) | |
if self.verbose: | |
info += "\n".join( | |
["\nendian: {0.endian}", | |
"sections: {0.sections}", | |
"frames: {0.frames}", | |
'extra_info: """', | |
' {1}"""']) | |
indented_extra_info = ("\n"+" "*4).join(self.extra_info.split("\n")) | |
return info.format(self, indented_extra_info) | |
def info(file, verbose=False): | |
"""Returns an object with information about a `SoundFile`. | |
Parameters | |
---------- | |
verbose : bool | |
Whether to print additional information. | |
""" | |
return _SoundFileInfo(file, verbose) | |
def available_formats(): | |
"""Return a dictionary of available major formats. | |
Examples | |
-------- | |
>>> import soundfile as sf | |
>>> sf.available_formats() | |
{'FLAC': 'FLAC (FLAC Lossless Audio Codec)', | |
'OGG': 'OGG (OGG Container format)', | |
'WAV': 'WAV (Microsoft)', | |
'AIFF': 'AIFF (Apple/SGI)', | |
... | |
'WAVEX': 'WAVEX (Microsoft)', | |
'RAW': 'RAW (header-less)', | |
'MAT5': 'MAT5 (GNU Octave 2.1 / Matlab 5.0)'} | |
""" | |
return dict(_available_formats_helper(_snd.SFC_GET_FORMAT_MAJOR_COUNT, | |
_snd.SFC_GET_FORMAT_MAJOR)) | |
def available_subtypes(format=None): | |
"""Return a dictionary of available subtypes. | |
Parameters | |
---------- | |
format : str | |
If given, only compatible subtypes are returned. | |
Examples | |
-------- | |
>>> import soundfile as sf | |
>>> sf.available_subtypes('FLAC') | |
{'PCM_24': 'Signed 24 bit PCM', | |
'PCM_16': 'Signed 16 bit PCM', | |
'PCM_S8': 'Signed 8 bit PCM'} | |
""" | |
subtypes = _available_formats_helper(_snd.SFC_GET_FORMAT_SUBTYPE_COUNT, | |
_snd.SFC_GET_FORMAT_SUBTYPE) | |
return dict((subtype, name) for subtype, name in subtypes | |
if format is None or check_format(format, subtype)) | |
def check_format(format, subtype=None, endian=None): | |
"""Check if the combination of format/subtype/endian is valid. | |
Examples | |
-------- | |
>>> import soundfile as sf | |
>>> sf.check_format('WAV', 'PCM_24') | |
True | |
>>> sf.check_format('FLAC', 'VORBIS') | |
False | |
""" | |
try: | |
return bool(_format_int(format, subtype, endian)) | |
except (ValueError, TypeError): | |
return False | |
def default_subtype(format): | |
"""Return the default subtype for a given format. | |
Examples | |
-------- | |
>>> import soundfile as sf | |
>>> sf.default_subtype('WAV') | |
'PCM_16' | |
>>> sf.default_subtype('MAT5') | |
'DOUBLE' | |
""" | |
_check_format(format) | |
return _default_subtypes.get(format.upper()) | |
class SoundFile(object): | |
"""A sound file. | |
For more documentation see the __init__() docstring (which is also | |
used for the online documentation (https://python-soundfile.readthedocs.io/). | |
""" | |
def __init__(self, file, mode='r', samplerate=None, channels=None, | |
subtype=None, endian=None, format=None, closefd=True): | |
"""Open a sound file. | |
If a file is opened with `mode` ``'r'`` (the default) or | |
``'r+'``, no sample rate, channels or file format need to be | |
given because the information is obtained from the file. An | |
exception is the ``'RAW'`` data format, which always requires | |
these data points. | |
File formats consist of three case-insensitive strings: | |
* a *major format* which is by default obtained from the | |
extension of the file name (if known) and which can be | |
forced with the format argument (e.g. ``format='WAVEX'``). | |
* a *subtype*, e.g. ``'PCM_24'``. Most major formats have a | |
default subtype which is used if no subtype is specified. | |
* an *endian-ness*, which doesn't have to be specified at all in | |
most cases. | |
A `SoundFile` object is a *context manager*, which means | |
if used in a "with" statement, `close()` is automatically | |
called when reaching the end of the code block inside the "with" | |
statement. | |
Parameters | |
---------- | |
file : str or int or file-like object | |
The file to open. This can be a file name, a file | |
descriptor or a Python file object (or a similar object with | |
the methods ``read()``/``readinto()``, ``write()``, | |
``seek()`` and ``tell()``). | |
mode : {'r', 'r+', 'w', 'w+', 'x', 'x+'}, optional | |
Open mode. Has to begin with one of these three characters: | |
``'r'`` for reading, ``'w'`` for writing (truncates *file*) | |
or ``'x'`` for writing (raises an error if *file* already | |
exists). Additionally, it may contain ``'+'`` to open | |
*file* for both reading and writing. | |
The character ``'b'`` for *binary mode* is implied because | |
all sound files have to be opened in this mode. | |
If *file* is a file descriptor or a file-like object, | |
``'w'`` doesn't truncate and ``'x'`` doesn't raise an error. | |
samplerate : int | |
The sample rate of the file. If `mode` contains ``'r'``, | |
this is obtained from the file (except for ``'RAW'`` files). | |
channels : int | |
The number of channels of the file. | |
If `mode` contains ``'r'``, this is obtained from the file | |
(except for ``'RAW'`` files). | |
subtype : str, sometimes optional | |
The subtype of the sound file. If `mode` contains ``'r'``, | |
this is obtained from the file (except for ``'RAW'`` | |
files), if not, the default value depends on the selected | |
`format` (see `default_subtype()`). | |
See `available_subtypes()` for all possible subtypes for | |
a given `format`. | |
endian : {'FILE', 'LITTLE', 'BIG', 'CPU'}, sometimes optional | |
The endian-ness of the sound file. If `mode` contains | |
``'r'``, this is obtained from the file (except for | |
``'RAW'`` files), if not, the default value is ``'FILE'``, | |
which is correct in most cases. | |
format : str, sometimes optional | |
The major format of the sound file. If `mode` contains | |
``'r'``, this is obtained from the file (except for | |
``'RAW'`` files), if not, the default value is determined | |
from the file extension. See `available_formats()` for | |
all possible values. | |
closefd : bool, optional | |
Whether to close the file descriptor on `close()`. Only | |
applicable if the *file* argument is a file descriptor. | |
Examples | |
-------- | |
>>> from soundfile import SoundFile | |
Open an existing file for reading: | |
>>> myfile = SoundFile('existing_file.wav') | |
>>> # do something with myfile | |
>>> myfile.close() | |
Create a new sound file for reading and writing using a with | |
statement: | |
>>> with SoundFile('new_file.wav', 'x+', 44100, 2) as myfile: | |
>>> # do something with myfile | |
>>> # ... | |
>>> assert not myfile.closed | |
>>> # myfile.close() is called automatically at the end | |
>>> assert myfile.closed | |
""" | |
# resolve PathLike objects (see PEP519 for details): | |
# can be replaced with _os.fspath(file) for Python >= 3.6 | |
file = file.__fspath__() if hasattr(file, '__fspath__') else file | |
self._name = file | |
if mode is None: | |
mode = getattr(file, 'mode', None) | |
mode_int = _check_mode(mode) | |
self._mode = mode | |
self._info = _create_info_struct(file, mode, samplerate, channels, | |
format, subtype, endian) | |
self._file = self._open(file, mode_int, closefd) | |
if set(mode).issuperset('r+') and self.seekable(): | |
# Move write position to 0 (like in Python file objects) | |
self.seek(0) | |
_snd.sf_command(self._file, _snd.SFC_SET_CLIPPING, _ffi.NULL, | |
_snd.SF_TRUE) | |
name = property(lambda self: self._name) | |
"""The file name of the sound file.""" | |
mode = property(lambda self: self._mode) | |
"""The open mode the sound file was opened with.""" | |
samplerate = property(lambda self: self._info.samplerate) | |
"""The sample rate of the sound file.""" | |
frames = property(lambda self: self._info.frames) | |
"""The number of frames in the sound file.""" | |
channels = property(lambda self: self._info.channels) | |
"""The number of channels in the sound file.""" | |
format = property( | |
lambda self: _format_str(self._info.format & _snd.SF_FORMAT_TYPEMASK)) | |
"""The major format of the sound file.""" | |
subtype = property( | |
lambda self: _format_str(self._info.format & _snd.SF_FORMAT_SUBMASK)) | |
"""The subtype of data in the the sound file.""" | |
endian = property( | |
lambda self: _format_str(self._info.format & _snd.SF_FORMAT_ENDMASK)) | |
"""The endian-ness of the data in the sound file.""" | |
format_info = property( | |
lambda self: _format_info(self._info.format & | |
_snd.SF_FORMAT_TYPEMASK)[1]) | |
"""A description of the major format of the sound file.""" | |
subtype_info = property( | |
lambda self: _format_info(self._info.format & | |
_snd.SF_FORMAT_SUBMASK)[1]) | |
"""A description of the subtype of the sound file.""" | |
sections = property(lambda self: self._info.sections) | |
"""The number of sections of the sound file.""" | |
closed = property(lambda self: self._file is None) | |
"""Whether the sound file is closed or not.""" | |
_errorcode = property(lambda self: _snd.sf_error(self._file)) | |
"""A pending sndfile error code.""" | |
def extra_info(self): | |
"""Retrieve the log string generated when opening the file.""" | |
info = _ffi.new("char[]", 2**14) | |
_snd.sf_command(self._file, _snd.SFC_GET_LOG_INFO, | |
info, _ffi.sizeof(info)) | |
return _ffi.string(info).decode('utf-8', 'replace') | |
# avoid confusion if something goes wrong before assigning self._file: | |
_file = None | |
def __repr__(self): | |
return ("SoundFile({0.name!r}, mode={0.mode!r}, " | |
"samplerate={0.samplerate}, channels={0.channels}, " | |
"format={0.format!r}, subtype={0.subtype!r}, " | |
"endian={0.endian!r})".format(self)) | |
def __del__(self): | |
self.close() | |
def __enter__(self): | |
return self | |
def __exit__(self, *args): | |
self.close() | |
def __setattr__(self, name, value): | |
"""Write text meta-data in the sound file through properties.""" | |
if name in _str_types: | |
self._check_if_closed() | |
err = _snd.sf_set_string(self._file, _str_types[name], | |
value.encode()) | |
_error_check(err) | |
else: | |
object.__setattr__(self, name, value) | |
def __getattr__(self, name): | |
"""Read text meta-data in the sound file through properties.""" | |
if name in _str_types: | |
self._check_if_closed() | |
data = _snd.sf_get_string(self._file, _str_types[name]) | |
return _ffi.string(data).decode('utf-8', 'replace') if data else "" | |
else: | |
raise AttributeError( | |
"'SoundFile' object has no attribute {0!r}".format(name)) | |
def __len__(self): | |
# Note: This is deprecated and will be removed at some point, | |
# see https://github.com/bastibe/python-soundfile/issues/199 | |
return self._info.frames | |
def __bool__(self): | |
# Note: This is temporary until __len__ is removed, afterwards it | |
# can (and should) be removed without change of behavior | |
return True | |
def __nonzero__(self): | |
# Note: This is only for compatibility with Python 2 and it shall be | |
# removed at the same time as __bool__(). | |
return self.__bool__() | |
def seekable(self): | |
"""Return True if the file supports seeking.""" | |
return self._info.seekable == _snd.SF_TRUE | |
def seek(self, frames, whence=SEEK_SET): | |
"""Set the read/write position. | |
Parameters | |
---------- | |
frames : int | |
The frame index or offset to seek. | |
whence : {SEEK_SET, SEEK_CUR, SEEK_END}, optional | |
By default (``whence=SEEK_SET``), *frames* are counted from | |
the beginning of the file. | |
``whence=SEEK_CUR`` seeks from the current position | |
(positive and negative values are allowed for *frames*). | |
``whence=SEEK_END`` seeks from the end (use negative value | |
for *frames*). | |
Returns | |
------- | |
int | |
The new absolute read/write position in frames. | |
Examples | |
-------- | |
>>> from soundfile import SoundFile, SEEK_END | |
>>> myfile = SoundFile('stereo_file.wav') | |
Seek to the beginning of the file: | |
>>> myfile.seek(0) | |
0 | |
Seek to the end of the file: | |
>>> myfile.seek(0, SEEK_END) | |
44100 # this is the file length | |
""" | |
self._check_if_closed() | |
position = _snd.sf_seek(self._file, frames, whence) | |
_error_check(self._errorcode) | |
return position | |
def tell(self): | |
"""Return the current read/write position.""" | |
return self.seek(0, SEEK_CUR) | |
def read(self, frames=-1, dtype='float64', always_2d=False, | |
fill_value=None, out=None): | |
"""Read from the file and return data as NumPy array. | |
Reads the given number of frames in the given data format | |
starting at the current read/write position. This advances the | |
read/write position by the same number of frames. | |
By default, all frames from the current read/write position to | |
the end of the file are returned. | |
Use `seek()` to move the current read/write position. | |
Parameters | |
---------- | |
frames : int, optional | |
The number of frames to read. If ``frames < 0``, the whole | |
rest of the file is read. | |
dtype : {'float64', 'float32', 'int32', 'int16'}, optional | |
Data type of the returned array, by default ``'float64'``. | |
Floating point audio data is typically in the range from | |
``-1.0`` to ``1.0``. Integer data is in the range from | |
``-2**15`` to ``2**15-1`` for ``'int16'`` and from | |
``-2**31`` to ``2**31-1`` for ``'int32'``. | |
.. note:: Reading int values from a float file will *not* | |
scale the data to [-1.0, 1.0). If the file contains | |
``np.array([42.6], dtype='float32')``, you will read | |
``np.array([43], dtype='int32')`` for | |
``dtype='int32'``. | |
Returns | |
------- | |
audiodata : `numpy.ndarray` or type(out) | |
A two-dimensional NumPy (frames x channels) array is | |
returned. If the sound file has only one channel, a | |
one-dimensional array is returned. Use ``always_2d=True`` | |
to return a two-dimensional array anyway. | |
If *out* was specified, it is returned. If *out* has more | |
frames than available in the file (or if *frames* is | |
smaller than the length of *out*) and no *fill_value* is | |
given, then only a part of *out* is overwritten and a view | |
containing all valid frames is returned. | |
Other Parameters | |
---------------- | |
always_2d : bool, optional | |
By default, reading a mono sound file will return a | |
one-dimensional array. With ``always_2d=True``, audio data | |
is always returned as a two-dimensional array, even if the | |
audio file has only one channel. | |
fill_value : float, optional | |
If more frames are requested than available in the file, | |
the rest of the output is be filled with *fill_value*. If | |
*fill_value* is not specified, a smaller array is | |
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 | |
arguments *dtype* and *always_2d* are silently ignored! If | |
*frames* is not given, it is obtained from the length of | |
*out*. | |
Examples | |
-------- | |
>>> from soundfile import SoundFile | |
>>> myfile = SoundFile('stereo_file.wav') | |
Reading 3 frames from a stereo file: | |
>>> myfile.read(3) | |
array([[ 0.71329652, 0.06294799], | |
[-0.26450912, -0.38874483], | |
[ 0.67398441, -0.11516333]]) | |
>>> myfile.close() | |
See Also | |
-------- | |
buffer_read, .write | |
""" | |
if out is None: | |
frames = self._check_frames(frames, fill_value) | |
out = self._create_empty_array(frames, always_2d, dtype) | |
else: | |
if frames < 0 or frames > len(out): | |
frames = len(out) | |
frames = self._array_io('read', out, frames) | |
if len(out) > frames: | |
if fill_value is None: | |
out = out[:frames] | |
else: | |
out[frames:] = fill_value | |
return out | |
def buffer_read(self, frames=-1, dtype=None): | |
"""Read from the file and return data as buffer object. | |
Reads the given number of *frames* in the given data format | |
starting at the current read/write position. This advances the | |
read/write position by the same number of frames. | |
By default, all frames from the current read/write position to | |
the end of the file are returned. | |
Use `seek()` to move the current read/write position. | |
Parameters | |
---------- | |
frames : int, optional | |
The number of frames to read. If ``frames < 0``, the whole | |
rest of the file is read. | |
dtype : {'float64', 'float32', 'int32', 'int16'} | |
Audio data will be converted to the given data type. | |
Returns | |
------- | |
buffer | |
A buffer containing the read data. | |
See Also | |
-------- | |
buffer_read_into, .read, buffer_write | |
""" | |
frames = self._check_frames(frames, fill_value=None) | |
ctype = self._check_dtype(dtype) | |
cdata = _ffi.new(ctype + '[]', frames * self.channels) | |
read_frames = self._cdata_io('read', cdata, ctype, frames) | |
assert read_frames == frames | |
return _ffi.buffer(cdata) | |
def buffer_read_into(self, buffer, dtype): | |
"""Read from the file into a given buffer object. | |
Fills the given *buffer* with frames in the given data format | |
starting at the current read/write position (which can be | |
changed with `seek()`) until the buffer is full or the end | |
of the file is reached. This advances the read/write position | |
by the number of frames that were read. | |
Parameters | |
---------- | |
buffer : writable buffer | |
Audio frames from the file are written to this buffer. | |
dtype : {'float64', 'float32', 'int32', 'int16'} | |
The data type of *buffer*. | |
Returns | |
------- | |
int | |
The number of frames that were read from the file. | |
This can be less than the size of *buffer*. | |
The rest of the buffer is not filled with meaningful data. | |
See Also | |
-------- | |
buffer_read, .read | |
""" | |
ctype = self._check_dtype(dtype) | |
cdata, frames = self._check_buffer(buffer, ctype) | |
frames = self._cdata_io('read', cdata, ctype, frames) | |
return frames | |
def write(self, data): | |
"""Write audio data from a NumPy array to the file. | |
Writes a number of frames at the read/write position to the | |
file. This also advances the read/write position by the same | |
number of frames and enlarges the file if necessary. | |
Note that writing int values to a float file will *not* scale | |
the values to [-1.0, 1.0). If you write the value | |
``np.array([42], dtype='int32')``, to a ``subtype='FLOAT'`` | |
file, the file will then contain ``np.array([42.], | |
dtype='float32')``. | |
Parameters | |
---------- | |
data : array_like | |
The data to write. Usually two-dimensional (frames x | |
channels), but one-dimensional *data* can be used for mono | |
files. Only the data types ``'float64'``, ``'float32'``, | |
``'int32'`` and ``'int16'`` are supported. | |
.. note:: The data type of *data* does **not** select the | |
data type of the written file. Audio data will be | |
converted to the given *subtype*. Writing int values | |
to a float file will *not* scale the values to | |
[-1.0, 1.0). If you write the value ``np.array([42], | |
dtype='int32')``, to a ``subtype='FLOAT'`` file, the | |
file will then contain ``np.array([42.], | |
dtype='float32')``. | |
Examples | |
-------- | |
>>> import numpy as np | |
>>> from soundfile import SoundFile | |
>>> myfile = SoundFile('stereo_file.wav') | |
Write 10 frames of random data to a new file: | |
>>> with SoundFile('stereo_file.wav', 'w', 44100, 2, 'PCM_24') as f: | |
>>> f.write(np.random.randn(10, 2)) | |
See Also | |
-------- | |
buffer_write, .read | |
""" | |
import numpy as np | |
# 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): | |
"""Write audio data from a buffer/bytes object to the file. | |
Writes the contents of *data* to the file at the current | |
read/write position. | |
This also advances the read/write position by the number of | |
frames that were written and enlarges the file if necessary. | |
Parameters | |
---------- | |
data : buffer or bytes | |
A buffer or bytes object containing the audio data to be | |
written. | |
dtype : {'float64', 'float32', 'int32', 'int16'} | |
The data type of the audio data stored in *data*. | |
See Also | |
-------- | |
.write, buffer_read | |
""" | |
ctype = self._check_dtype(dtype) | |
cdata, frames = self._check_buffer(data, ctype) | |
written = self._cdata_io('write', cdata, ctype, frames) | |
assert written == frames | |
self._update_frames(written) | |
def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', | |
always_2d=False, fill_value=None, out=None): | |
"""Return a generator for block-wise reading. | |
By default, the generator yields blocks of the given | |
*blocksize* (using a given *overlap*) until the end of the file | |
is reached; *frames* can be used to stop earlier. | |
Parameters | |
---------- | |
blocksize : int | |
The number of frames to read per block. Either this or *out* | |
must be given. | |
overlap : int, optional | |
The number of frames to rewind between each block. | |
frames : int, optional | |
The number of frames to read. | |
If ``frames < 0``, the file is read until the end. | |
dtype : {'float64', 'float32', 'int32', 'int16'}, optional | |
See `read()`. | |
Yields | |
------ | |
`numpy.ndarray` or type(out) | |
Blocks of audio data. | |
If *out* was given, and the requested frames are not an | |
integer multiple of the length of *out*, and no | |
*fill_value* was given, the last block will be a smaller | |
view into *out*. | |
Other Parameters | |
---------------- | |
always_2d, fill_value, out | |
See `read()`. | |
fill_value : float, optional | |
See `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! | |
Examples | |
-------- | |
>>> from soundfile import SoundFile | |
>>> with SoundFile('stereo_file.wav') as f: | |
>>> for block in f.blocks(blocksize=1024): | |
>>> pass # do something with 'block' | |
""" | |
import numpy as np | |
if 'r' not in self.mode and '+' not in self.mode: | |
raise SoundFileRuntimeError("blocks() is not allowed in write-only mode") | |
if out is None: | |
if blocksize is None: | |
raise TypeError("One of {blocksize, out} must be specified") | |
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 | |
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 | |
def truncate(self, frames=None): | |
"""Truncate the file to a given number of frames. | |
After this command, the read/write position will be at the new | |
end of the file. | |
Parameters | |
---------- | |
frames : int, optional | |
Only the data before *frames* is kept, the rest is deleted. | |
If not specified, the current read/write position is used. | |
""" | |
if frames is None: | |
frames = self.tell() | |
err = _snd.sf_command(self._file, _snd.SFC_FILE_TRUNCATE, | |
_ffi.new("sf_count_t*", frames), | |
_ffi.sizeof("sf_count_t")) | |
if err: | |
# get the actual error code | |
err = _snd.sf_error(self._file) | |
raise LibsndfileError(err, "Error truncating the file") | |
self._info.frames = frames | |
def flush(self): | |
"""Write unwritten data to the file system. | |
Data written with `write()` is not immediately written to | |
the file system but buffered in memory to be written at a later | |
time. Calling `flush()` makes sure that all changes are | |
actually written to the file system. | |
This has no effect on files opened in read-only mode. | |
""" | |
self._check_if_closed() | |
_snd.sf_write_sync(self._file) | |
def close(self): | |
"""Close the file. Can be called multiple times.""" | |
if not self.closed: | |
# be sure to flush data to disk before closing the file | |
self.flush() | |
err = _snd.sf_close(self._file) | |
self._file = None | |
_error_check(err) | |
def _open(self, file, mode_int, closefd): | |
"""Call the appropriate sf_open*() function from libsndfile.""" | |
if isinstance(file, (_unicode, bytes)): | |
if _os.path.isfile(file): | |
if 'x' in self.mode: | |
raise OSError("File exists: {0!r}".format(self.name)) | |
elif set(self.mode).issuperset('w+'): | |
# truncate the file, because SFM_RDWR doesn't: | |
_os.close(_os.open(file, _os.O_WRONLY | _os.O_TRUNC)) | |
openfunction = _snd.sf_open | |
if isinstance(file, _unicode): | |
if _sys.platform == 'win32': | |
openfunction = _snd.sf_wchar_open | |
else: | |
file = file.encode(_sys.getfilesystemencoding()) | |
file_ptr = openfunction(file, mode_int, self._info) | |
elif isinstance(file, int): | |
file_ptr = _snd.sf_open_fd(file, mode_int, self._info, closefd) | |
elif _has_virtual_io_attrs(file, mode_int): | |
file_ptr = _snd.sf_open_virtual(self._init_virtual_io(file), | |
mode_int, self._info, _ffi.NULL) | |
else: | |
raise TypeError("Invalid file: {0!r}".format(self.name)) | |
if file_ptr == _ffi.NULL: | |
# get the actual error code | |
err = _snd.sf_error(file_ptr) | |
raise LibsndfileError(err, prefix="Error opening {0!r}: ".format(self.name)) | |
if mode_int == _snd.SFM_WRITE: | |
# Due to a bug in libsndfile version <= 1.0.25, frames != 0 | |
# when opening a named pipe in SFM_WRITE mode. | |
# See http://github.com/erikd/libsndfile/issues/77. | |
self._info.frames = 0 | |
# This is not necessary for "normal" files (because | |
# frames == 0 in this case), but it doesn't hurt, either. | |
return file_ptr | |
def _init_virtual_io(self, file): | |
"""Initialize callback functions for sf_open_virtual().""" | |
def vio_get_filelen(user_data): | |
curr = file.tell() | |
file.seek(0, SEEK_END) | |
size = file.tell() | |
file.seek(curr, SEEK_SET) | |
return size | |
def vio_seek(offset, whence, user_data): | |
file.seek(offset, whence) | |
return file.tell() | |
def vio_read(ptr, count, user_data): | |
# first try readinto(), if not available fall back to read() | |
try: | |
buf = _ffi.buffer(ptr, count) | |
data_read = file.readinto(buf) | |
except AttributeError: | |
data = file.read(count) | |
data_read = len(data) | |
buf = _ffi.buffer(ptr, data_read) | |
buf[0:data_read] = data | |
return data_read | |
def vio_write(ptr, count, user_data): | |
buf = _ffi.buffer(ptr, count) | |
data = buf[:] | |
written = file.write(data) | |
# write() returns None for file objects in Python <= 2.7: | |
if written is None: | |
written = count | |
return written | |
def vio_tell(user_data): | |
return file.tell() | |
# Note: the callback functions must be kept alive! | |
self._virtual_io = {'get_filelen': vio_get_filelen, | |
'seek': vio_seek, | |
'read': vio_read, | |
'write': vio_write, | |
'tell': vio_tell} | |
return _ffi.new("SF_VIRTUAL_IO*", self._virtual_io) | |
def _getAttributeNames(self): | |
"""Return all attributes used in __setattr__ and __getattr__. | |
This is useful for auto-completion (e.g. IPython). | |
""" | |
return _str_types | |
def _check_if_closed(self): | |
"""Check if the file is closed and raise an error if it is. | |
This should be used in every method that uses self._file. | |
""" | |
if self.closed: | |
raise SoundFileRuntimeError("I/O operation on closed file") | |
def _check_frames(self, frames, fill_value): | |
"""Reduce frames to no more than are available in the file.""" | |
if self.seekable(): | |
remaining_frames = self.frames - self.tell() | |
if frames < 0 or (frames > remaining_frames and | |
fill_value is None): | |
frames = remaining_frames | |
elif frames < 0: | |
raise ValueError("frames must be specified for non-seekable files") | |
return frames | |
def _check_buffer(self, data, ctype): | |
"""Convert buffer to cdata and check for valid size.""" | |
assert ctype in _ffi_types.values() | |
if not isinstance(data, bytes): | |
data = _ffi.from_buffer(data) | |
frames, remainder = divmod(len(data), | |
self.channels * _ffi.sizeof(ctype)) | |
if remainder: | |
raise ValueError("Data size must be a multiple of frame size") | |
return data, frames | |
def _create_empty_array(self, frames, always_2d, dtype): | |
"""Create an empty array with appropriate shape.""" | |
import numpy as np | |
if always_2d or self.channels > 1: | |
shape = frames, self.channels | |
else: | |
shape = frames, | |
return np.empty(shape, dtype, order='C') | |
def _check_dtype(self, dtype): | |
"""Check if dtype string is valid and return ctype string.""" | |
try: | |
return _ffi_types[dtype] | |
except KeyError: | |
raise ValueError("dtype must be one of {0!r} and not {1!r}".format( | |
sorted(_ffi_types.keys()), dtype)) | |
def _array_io(self, action, array, frames): | |
"""Check array and call low-level IO function.""" | |
if (array.ndim not in (1, 2) or | |
array.ndim == 1 and self.channels != 1 or | |
array.ndim == 2 and array.shape[1] != self.channels): | |
raise ValueError("Invalid shape: {0!r}".format(array.shape)) | |
if not array.flags.c_contiguous: | |
raise ValueError("Data must be C-contiguous") | |
ctype = self._check_dtype(array.dtype.name) | |
assert array.dtype.itemsize == _ffi.sizeof(ctype) | |
cdata = _ffi.cast(ctype + '*', array.__array_interface__['data'][0]) | |
return self._cdata_io(action, cdata, ctype, frames) | |
def _cdata_io(self, action, data, ctype, frames): | |
"""Call one of libsndfile's read/write functions.""" | |
assert ctype in _ffi_types.values() | |
self._check_if_closed() | |
if self.seekable(): | |
curr = self.tell() | |
func = getattr(_snd, 'sf_' + action + 'f_' + ctype) | |
frames = func(self._file, data, frames) | |
_error_check(self._errorcode) | |
if self.seekable(): | |
self.seek(curr + frames, SEEK_SET) # Update read & write position | |
return frames | |
def _update_frames(self, written): | |
"""Update self.frames after writing.""" | |
if self.seekable(): | |
curr = self.tell() | |
self._info.frames = self.seek(0, SEEK_END) | |
self.seek(curr, SEEK_SET) | |
else: | |
self._info.frames += written | |
def _prepare_read(self, start, stop, frames): | |
"""Seek to start frame and calculate length.""" | |
if start != 0 and not self.seekable(): | |
raise ValueError("start is only allowed for seekable files") | |
if frames >= 0 and stop is not None: | |
raise TypeError("Only one of {frames, stop} may be used") | |
start, stop, _ = slice(start, stop).indices(self.frames) | |
if stop < start: | |
stop = start | |
if frames < 0: | |
frames = stop - start | |
if self.seekable(): | |
self.seek(start, SEEK_SET) | |
return frames | |
def copy_metadata(self): | |
"""Get all metadata present in this SoundFile | |
Returns | |
------- | |
metadata: dict[str, str] | |
A dict with all metadata. Possible keys are: 'title', 'copyright', | |
'software', 'artist', 'comment', 'date', 'album', 'license', | |
'tracknumber' and 'genre'. | |
""" | |
strs = {} | |
for strtype, strid in _str_types.items(): | |
data = _snd.sf_get_string(self._file, strid) | |
if data: | |
strs[strtype] = _ffi.string(data).decode('utf-8', 'replace') | |
return strs | |
def _error_check(err, prefix=""): | |
"""Raise LibsndfileError if there is an error.""" | |
if err != 0: | |
raise LibsndfileError(err, prefix=prefix) | |
def _format_int(format, subtype, endian): | |
"""Return numeric ID for given format|subtype|endian combo.""" | |
result = _check_format(format) | |
if subtype is None: | |
subtype = default_subtype(format) | |
if subtype is None: | |
raise TypeError( | |
"No default subtype for major format {0!r}".format(format)) | |
elif not isinstance(subtype, (_unicode, str)): | |
raise TypeError("Invalid subtype: {0!r}".format(subtype)) | |
try: | |
result |= _subtypes[subtype.upper()] | |
except KeyError: | |
raise ValueError("Unknown subtype: {0!r}".format(subtype)) | |
if endian is None: | |
endian = 'FILE' | |
elif not isinstance(endian, (_unicode, str)): | |
raise TypeError("Invalid endian-ness: {0!r}".format(endian)) | |
try: | |
result |= _endians[endian.upper()] | |
except KeyError: | |
raise ValueError("Unknown endian-ness: {0!r}".format(endian)) | |
info = _ffi.new("SF_INFO*") | |
info.format = result | |
info.channels = 1 | |
if _snd.sf_format_check(info) == _snd.SF_FALSE: | |
raise ValueError( | |
"Invalid combination of format, subtype and endian") | |
return result | |
def _check_mode(mode): | |
"""Check if mode is valid and return its integer representation.""" | |
if not isinstance(mode, (_unicode, str)): | |
raise TypeError("Invalid mode: {0!r}".format(mode)) | |
mode_set = set(mode) | |
if mode_set.difference('xrwb+') or len(mode) > len(mode_set): | |
raise ValueError("Invalid mode: {0!r}".format(mode)) | |
if len(mode_set.intersection('xrw')) != 1: | |
raise ValueError("mode must contain exactly one of 'xrw'") | |
if '+' in mode_set: | |
mode_int = _snd.SFM_RDWR | |
elif 'r' in mode_set: | |
mode_int = _snd.SFM_READ | |
else: | |
mode_int = _snd.SFM_WRITE | |
return mode_int | |
def _create_info_struct(file, mode, samplerate, channels, | |
format, subtype, endian): | |
"""Check arguments and create SF_INFO struct.""" | |
original_format = format | |
if format is None: | |
format = _get_format_from_filename(file, mode) | |
assert isinstance(format, (_unicode, str)) | |
else: | |
_check_format(format) | |
info = _ffi.new("SF_INFO*") | |
if 'r' not in mode or format.upper() == 'RAW': | |
if samplerate is None: | |
raise TypeError("samplerate must be specified") | |
info.samplerate = samplerate | |
if channels is None: | |
raise TypeError("channels must be specified") | |
info.channels = channels | |
info.format = _format_int(format, subtype, endian) | |
else: | |
if any(arg is not None for arg in ( | |
samplerate, channels, original_format, subtype, endian)): | |
raise TypeError("Not allowed for existing files (except 'RAW'): " | |
"samplerate, channels, format, subtype, endian") | |
return info | |
def _get_format_from_filename(file, mode): | |
"""Return a format string obtained from file (or file.name). | |
If file already exists (= read mode), an empty string is returned on | |
error. If not, an exception is raised. | |
The return type will always be str or unicode (even if | |
file/file.name is a bytes object). | |
""" | |
format = '' | |
file = getattr(file, 'name', file) | |
try: | |
# This raises an exception if file is not a (Unicode/byte) string: | |
format = _os.path.splitext(file)[-1][1:] | |
# Convert bytes to unicode (raises AttributeError on Python 3 str): | |
format = format.decode('utf-8', 'replace') | |
except Exception: | |
pass | |
if format.upper() not in _formats and 'r' not in mode: | |
raise TypeError("No format specified and unable to get format from " | |
"file extension: {0!r}".format(file)) | |
return format | |
def _format_str(format_int): | |
"""Return the string representation of a given numeric format.""" | |
for dictionary in _formats, _subtypes, _endians: | |
for k, v in dictionary.items(): | |
if v == format_int: | |
return k | |
else: | |
return 'n/a' | |
def _format_info(format_int, format_flag=_snd.SFC_GET_FORMAT_INFO): | |
"""Return the ID and short description of a given format.""" | |
format_info = _ffi.new("SF_FORMAT_INFO*") | |
format_info.format = format_int | |
_snd.sf_command(_ffi.NULL, format_flag, format_info, | |
_ffi.sizeof("SF_FORMAT_INFO")) | |
name = format_info.name | |
return (_format_str(format_info.format), | |
_ffi.string(name).decode('utf-8', 'replace') if name else "") | |
def _available_formats_helper(count_flag, format_flag): | |
"""Helper for available_formats() and available_subtypes().""" | |
count = _ffi.new("int*") | |
_snd.sf_command(_ffi.NULL, count_flag, count, _ffi.sizeof("int")) | |
for format_int in range(count[0]): | |
yield _format_info(format_int, format_flag) | |
def _check_format(format_str): | |
"""Check if `format_str` is valid and return format ID.""" | |
if not isinstance(format_str, (_unicode, str)): | |
raise TypeError("Invalid format: {0!r}".format(format_str)) | |
try: | |
format_int = _formats[format_str.upper()] | |
except KeyError: | |
raise ValueError("Unknown format: {0!r}".format(format_str)) | |
return format_int | |
def _has_virtual_io_attrs(file, mode_int): | |
"""Check if file has all the necessary attributes for virtual IO.""" | |
readonly = mode_int == _snd.SFM_READ | |
writeonly = mode_int == _snd.SFM_WRITE | |
return all([ | |
hasattr(file, 'seek'), | |
hasattr(file, 'tell'), | |
hasattr(file, 'write') or readonly, | |
hasattr(file, 'read') or hasattr(file, 'readinto') or writeonly, | |
]) | |
class SoundFileError(Exception): | |
"""Base class for all soundfile-specific errors.""" | |
pass | |
class SoundFileRuntimeError(SoundFileError, RuntimeError): | |
"""soundfile module runtime error. | |
Errors that used to be `RuntimeError`.""" | |
pass | |
class LibsndfileError(SoundFileRuntimeError): | |
"""libsndfile errors. | |
Attributes | |
---------- | |
code | |
libsndfile internal error number. | |
""" | |
def __init__(self, code, prefix=""): | |
SoundFileRuntimeError.__init__(self, code, prefix) | |
self.code = code | |
self.prefix = prefix | |
def error_string(self): | |
"""Raw libsndfile error message.""" | |
if self.code: | |
err_str = _snd.sf_error_number(self.code) | |
return _ffi.string(err_str).decode('utf-8', 'replace') | |
else: | |
# Due to race conditions, if used concurrently, sf_error() may | |
# return 0 (= no error) even if an error has happened. | |
# See https://github.com/erikd/libsndfile/issues/610 for details. | |
return "(Garbled error message from libsndfile)" | |
def __str__(self): | |
return self.prefix + self.error_string | |