Source code for soundfile

"""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-'):]


[docs] 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
[docs] 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)
[docs] 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 @property 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)
[docs] 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)
[docs] 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))
[docs] 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))
[docs] 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
[docs] 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())
[docs] 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.""" @property 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__()
[docs] def seekable(self): """Return True if the file supports seeking.""" return self._info.seekable == _snd.SF_TRUE
[docs] 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
[docs] def tell(self): """Return the current read/write position.""" return self.seek(0, SEEK_CUR)
[docs] 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
[docs] 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)
[docs] 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
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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
[docs] 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)
[docs] 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().""" @_ffi.callback("sf_vio_get_filelen") def vio_get_filelen(user_data): curr = file.tell() file.seek(0, SEEK_END) size = file.tell() file.seek(curr, SEEK_SET) return size @_ffi.callback("sf_vio_seek") def vio_seek(offset, whence, user_data): file.seek(offset, whence) return file.tell() @_ffi.callback("sf_vio_read") 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 @_ffi.callback("sf_vio_write") 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 @_ffi.callback("sf_vio_tell") 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
[docs] 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, ])
[docs] class SoundFileError(Exception): """Base class for all soundfile-specific errors.""" pass
[docs] class SoundFileRuntimeError(SoundFileError, RuntimeError): """soundfile module runtime error. Errors that used to be `RuntimeError`.""" pass
[docs] 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 @property 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