"""SoundFile is an audio library based on libsndfile, CFFI and NumPy. Sound files can be read or written directly using the functions :func:`read` and :func:`write`. To read a sound file in a block-wise fashion, use :func:`blocks`. Alternatively, sound files can be opened as :class:`SoundFile` objects. For further information, see http://pysoundfile.readthedocs.org/. """ __version__ = "0.10.3" 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 } _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 '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. '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). } _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', } _ffi_types = { 'float64': 'double', 'float32': 'float', 'int32': 'int', 'int16': 'short' } try: _libname = _find_library('sndfile') if _libname is None: raise OSError('sndfile library not found') _snd = _ffi.dlopen(_libname) except OSError: if _sys.platform == 'darwin': _libname = 'libsndfile.dylib' elif _sys.platform == 'win32': from platform import architecture as _architecture _libname = 'libsndfile' + _architecture()[0] + '.dll' else: raise # hack for packaging tools like cx_Freeze, which # compress all scripts into a zip file # which causes __file__ to be inside this zip file _path = _os.path.dirname(_os.path.abspath(__file__)) while not _os.path.isdir(_path): _path = _os.path.abspath(_os.path.join(_path, '..')) _snd = _ffi.dlopen(_os.path.join( _path, '_soundfile_data', _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 :class:`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 :class:`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 :class:`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 :func:`default_subtype` for the default value and :func:`available_subtypes` for all possible values. Other Parameters ---------------- format, endian, closefd See :class:`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 :class:`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 :func:`read`. dtype : {'float64', 'float32', 'int32', 'int16'}, optional See :func:`read`. always_2d, fill_value, out See :func:`read`. samplerate, channels, format, subtype, endian, closefd See :class:`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) 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 (http://pysoundfile.readthedocs.org/). """ 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 :class:`SoundFile` object is a *context manager*, which means if used in a "with" statement, :meth:`.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 :func:`default_subtype`). See :func:`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 :func:`available_formats` for all possible values. closefd : bool, optional Whether to close the file descriptor on :meth:`.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/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 :meth:`.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. numpy.ndarray or type(out) 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 :meth:`.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 :meth:`.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 :meth:`.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 :meth:`.read`. fill_value : float, optional See :meth:`.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 RuntimeError("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: raise RuntimeError("Error truncating the file") self._info.frames = frames def flush(self): """Write unwritten data to the file system. Data written with :meth:`.write` is not immediately written to the file system but buffered in memory to be written at a later time. Calling :meth:`.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)) _error_check(_snd.sf_error(file_ptr), "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 RuntimeError("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 _error_check(err, prefix=""): """Pretty-print a numerical error code if there is an error.""" if err != 0: err_str = _snd.sf_error_number(err) raise RuntimeError(prefix + _ffi.string(err_str).decode('utf-8', 'replace')) 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, ])