Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-SoundFile for
openSUSE:Factory checked in at 2026-08-24 12:10:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-SoundFile (Old)
and /work/SRC/openSUSE:Factory/.python-SoundFile.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-SoundFile"
Mon Aug 24 12:10:40 2026 rev:12 rq:1373168 version:0.14.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-SoundFile/python-SoundFile.changes
2026-08-22 21:38:17.832841060 +0200
+++
/work/SRC/openSUSE:Factory/.python-SoundFile.new.1258/python-SoundFile.changes
2026-08-24 12:16:04.647516344 +0200
@@ -1,0 +2,16 @@
+Sun Aug 23 05:20:16 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.14.0:
+ * Added type annotations throughout the public API
+ * Added a licensing note to the wheel
+ * Fixed a race condition when opening files concurrently
+ * Fixed regressions in the test suite
+ * Dropped support for Python 3.9 and older
+ * Added ARM64 support on Windows
+- Add python-typing_extensions dependency, newly required at
+ runtime by the type annotations
+- Drop 0001-Fix-libsndfile-versioning.patch: openSUSE builds
+ libsndfile without experimental code, so sf_version_string()
+ never carries the "-exp" suffix the patch stripped
+
+-------------------------------------------------------------------
Old:
----
0001-Fix-libsndfile-versioning.patch
soundfile-0.13.1.tar.gz
New:
----
soundfile-0.14.0.tar.gz
----------(Old B)----------
Old: runtime by the type annotations
- Drop 0001-Fix-libsndfile-versioning.patch: openSUSE builds
libsndfile without experimental code, so sf_version_string()
----------(Old E)----------
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-SoundFile.spec ++++++
--- /var/tmp/diff_new_pack.5MXv6M/_old 2026-08-24 12:16:05.892560956 +0200
+++ /var/tmp/diff_new_pack.5MXv6M/_new 2026-08-24 12:16:05.897561136 +0200
@@ -6,7 +6,7 @@
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
-# file, is the same license as the pristine package itself (unless the
+# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
@@ -18,20 +18,19 @@
%{?sle15_python_module_pythons}
Name: python-SoundFile
-Version: 0.13.1
+Version: 0.14.0
Release: 0
Summary: An audio library based on libsndfile, CFFI and NumPy
License: BSD-3-Clause
URL: https://github.com/bastibe/python-soundfile
Source:
https://files.pythonhosted.org/packages/source/s/soundfile/soundfile-%{version}.tar.gz
Source99: python-SoundFile.rpmlintrc
-# PATCH-FIX-OPENSUSE 0001-Fix-libsndfile-versioning.patch [email protected] --
strip optional -exp suffix from libsndfile version string
-Patch0: 0001-Fix-libsndfile-versioning.patch
BuildRequires: %{python_module cffi >= 1.0}
BuildRequires: %{python_module numpy}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest}
BuildRequires: %{python_module setuptools}
+BuildRequires: %{python_module typing_extensions}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: pkgconfig
@@ -41,6 +40,7 @@
Requires: libsndfile1
Requires: python-cffi >= 1.0
Requires: python-numpy
+Requires: python-typing_extensions
Obsoletes: python-PySoundFile < %{version}
Provides: python-PySoundFile = %{version}
Provides: python-soundfile = %{version}
++++++ soundfile-0.13.1.tar.gz -> soundfile-0.14.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/PKG-INFO
new/soundfile-0.14.0/PKG-INFO
--- old/soundfile-0.13.1/PKG-INFO 2025-01-25 10:09:37.205288400 +0100
+++ new/soundfile-0.14.0/PKG-INFO 2026-06-06 10:55:45.166059300 +0200
@@ -1,10 +1,10 @@
Metadata-Version: 2.1
Name: soundfile
-Version: 0.13.1
+Version: 0.14.0
Summary: An audio library based on libsndfile, CFFI and NumPy
Home-page: https://github.com/bastibe/python-soundfile
Author: Bastian Bechtold
-Author-email: [email protected]
+Author-email: [email protected]
License: BSD 3-Clause License
Keywords: audio,libsndfile
Platform: any
@@ -16,14 +16,15 @@
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Multimedia :: Sound/Audio
+Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: cffi>=1.0
Requires-Dist: numpy
+Requires-Dist: typing-extensions
Provides-Extra: numpy
python-soundfile
@@ -270,25 +271,44 @@
Controlling bitrate mode and compression level
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-For some audio formats, you can control the bitrate and compression level.
+For some audio formats, you can control the bitrate and compression level.
-`compression_level` is a float between 0 and 1, with 1 being the highest
compression,
+`compression_level` is a float between 0 and 1, with 1 being the highest
compression,
and `bitrate_mode` is 'VARIABLE', 'CONSTANT', or 'AVERAGE'.
.. code:: python
import soundfile as sf
-
+
# for example, this uncompressed 5 minute wav file with 32 kHz sample rate
is 18 Mb
- data, samplerate = sf.read('5min_32kHz.wav')
-
+ data, samplerate = sf.read('5min_32kHz.wav')
+
# maximum mp3 compression results in 1.1 Mb file, with either CONSTANT or
VARIABLE bit rate
- sf.write('max_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=.99)
+ sf.write('max_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=.99)
sf.write('max_compression_cbr.mp3', data, samplerate,
bitrate_mode='CONSTANT', compression_level=.99)
-
+
# minimum mp3 compression results in 3.5 Mb file
sf.write('min_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=0)
+Thread Safety
+-------------
+
+The ``soundfile`` module is a thin wrapper around the low-level ``libsndfile``
C
+library. As such, ``soundfile`` provides the same thread safety guarantees as
+the underlying C library. See `this libsndfile issue
+<https://github.com/libsndfile/libsndfile/issues/279>`_ for more details.
+
+In short, multithreaded use is safe as long as handles for readers or writers
+are not shared between threads. It is safe to concurrently open and read the
+same file through unique per-thread handles. It is not safe to concurrently
+write to the same file or share reader or writer handles. You may see garbage
+results or crashes if you do this.
+
+See the advice in the Free-threaded Python guide on `thread-safe Python
+programming
+<https://py-free-threading.github.io/porting/#multithreaded-python-programming>`_
+for suggestions on how to synchronize access to a thread-unsafe object.
+
Known Issues
------------
@@ -436,3 +456,13 @@
Thank you, Brian McFee and Guy Illes
- Fixed regression in blocks
+
+2026-06-06 V0.14.0 Bastian Bechtold
+ Thank you GesonAnko, Trevor Gamblin, Andreas Karatzas, Harish RS, Hunter
Hogan
+
+ - Added type annotations
+ - Added Licensing note to wheel
+ - Fixed race condition when opening files concurrently
+ - Fixed regressions in test suite
+ - Removed support for Python <= 3.9
+ - Added ARM64 support for Windows
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/README.rst
new/soundfile-0.14.0/README.rst
--- old/soundfile-0.13.1/README.rst 2025-01-25 10:04:43.000000000 +0100
+++ new/soundfile-0.14.0/README.rst 2026-06-06 10:55:35.000000000 +0200
@@ -242,25 +242,44 @@
Controlling bitrate mode and compression level
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-For some audio formats, you can control the bitrate and compression level.
+For some audio formats, you can control the bitrate and compression level.
-`compression_level` is a float between 0 and 1, with 1 being the highest
compression,
+`compression_level` is a float between 0 and 1, with 1 being the highest
compression,
and `bitrate_mode` is 'VARIABLE', 'CONSTANT', or 'AVERAGE'.
.. code:: python
import soundfile as sf
-
+
# for example, this uncompressed 5 minute wav file with 32 kHz sample rate
is 18 Mb
- data, samplerate = sf.read('5min_32kHz.wav')
-
+ data, samplerate = sf.read('5min_32kHz.wav')
+
# maximum mp3 compression results in 1.1 Mb file, with either CONSTANT or
VARIABLE bit rate
- sf.write('max_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=.99)
+ sf.write('max_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=.99)
sf.write('max_compression_cbr.mp3', data, samplerate,
bitrate_mode='CONSTANT', compression_level=.99)
-
+
# minimum mp3 compression results in 3.5 Mb file
sf.write('min_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=0)
+Thread Safety
+-------------
+
+The ``soundfile`` module is a thin wrapper around the low-level ``libsndfile``
C
+library. As such, ``soundfile`` provides the same thread safety guarantees as
+the underlying C library. See `this libsndfile issue
+<https://github.com/libsndfile/libsndfile/issues/279>`_ for more details.
+
+In short, multithreaded use is safe as long as handles for readers or writers
+are not shared between threads. It is safe to concurrently open and read the
+same file through unique per-thread handles. It is not safe to concurrently
+write to the same file or share reader or writer handles. You may see garbage
+results or crashes if you do this.
+
+See the advice in the Free-threaded Python guide on `thread-safe Python
+programming
+<https://py-free-threading.github.io/porting/#multithreaded-python-programming>`_
+for suggestions on how to synchronize access to a thread-unsafe object.
+
Known Issues
------------
@@ -407,4 +426,14 @@
2025-01-25 V0.13.1 Bastian Bechtold
Thank you, Brian McFee and Guy Illes
- - Fixed regression in blocks
\ No newline at end of file
+ - Fixed regression in blocks
+
+2026-06-06 V0.14.0 Bastian Bechtold
+ Thank you GesonAnko, Trevor Gamblin, Andreas Karatzas, Harish RS, Hunter
Hogan
+
+ - Added type annotations
+ - Added Licensing note to wheel
+ - Fixed race condition when opening files concurrently
+ - Fixed regressions in test suite
+ - Removed support for Python <= 3.9
+ - Added ARM64 support for Windows
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/setup.py
new/soundfile-0.14.0/setup.py
--- old/soundfile-0.13.1/setup.py 2025-01-25 10:02:16.000000000 +0100
+++ new/soundfile-0.14.0/setup.py 2026-06-06 09:32:23.000000000 +0200
@@ -25,8 +25,8 @@
libname = None
if libname and os.path.isdir('_soundfile_data'):
- packages = ['_soundfile_data']
- package_data = {'_soundfile_data': [libname, 'COPYING']}
+ packages = ['_soundfile_data', 'licensing']
+ package_data = {'_soundfile_data': [libname, 'COPYING'], 'licensing':
['license_notes.md']}
zip_safe = False
else:
packages = None
@@ -87,7 +87,7 @@
version=soundfile_version,
description='An audio library based on libsndfile, CFFI and NumPy',
author='Bastian Bechtold',
- author_email='[email protected]',
+ author_email='[email protected]',
url='https://github.com/bastibe/python-soundfile',
keywords=['audio', 'libsndfile'],
py_modules=['soundfile'],
@@ -95,8 +95,9 @@
package_data=package_data,
zip_safe=zip_safe,
license='BSD 3-Clause License',
+ python_requires=">=3.10",
setup_requires=["cffi>=1.0"],
- install_requires=['cffi>=1.0', 'numpy'],
+ install_requires=['cffi>=1.0', 'numpy', 'typing-extensions'],
cffi_modules=["soundfile_build.py:ffibuilder"],
extras_require={'numpy': []}, # This option is no longer relevant, but the
empty entry must be left in to avoid breaking old build scripts.
platforms='any',
@@ -109,7 +110,6 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 2',
'Programming Language :: Python :: Implementation :: PyPy',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Multimedia :: Sound/Audio',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/soundfile.egg-info/PKG-INFO
new/soundfile-0.14.0/soundfile.egg-info/PKG-INFO
--- old/soundfile-0.13.1/soundfile.egg-info/PKG-INFO 2025-01-25
10:09:37.000000000 +0100
+++ new/soundfile-0.14.0/soundfile.egg-info/PKG-INFO 2026-06-06
10:55:45.000000000 +0200
@@ -1,10 +1,10 @@
Metadata-Version: 2.1
Name: soundfile
-Version: 0.13.1
+Version: 0.14.0
Summary: An audio library based on libsndfile, CFFI and NumPy
Home-page: https://github.com/bastibe/python-soundfile
Author: Bastian Bechtold
-Author-email: [email protected]
+Author-email: [email protected]
License: BSD 3-Clause License
Keywords: audio,libsndfile
Platform: any
@@ -16,14 +16,15 @@
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Multimedia :: Sound/Audio
+Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: cffi>=1.0
Requires-Dist: numpy
+Requires-Dist: typing-extensions
Provides-Extra: numpy
python-soundfile
@@ -270,25 +271,44 @@
Controlling bitrate mode and compression level
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-For some audio formats, you can control the bitrate and compression level.
+For some audio formats, you can control the bitrate and compression level.
-`compression_level` is a float between 0 and 1, with 1 being the highest
compression,
+`compression_level` is a float between 0 and 1, with 1 being the highest
compression,
and `bitrate_mode` is 'VARIABLE', 'CONSTANT', or 'AVERAGE'.
.. code:: python
import soundfile as sf
-
+
# for example, this uncompressed 5 minute wav file with 32 kHz sample rate
is 18 Mb
- data, samplerate = sf.read('5min_32kHz.wav')
-
+ data, samplerate = sf.read('5min_32kHz.wav')
+
# maximum mp3 compression results in 1.1 Mb file, with either CONSTANT or
VARIABLE bit rate
- sf.write('max_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=.99)
+ sf.write('max_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=.99)
sf.write('max_compression_cbr.mp3', data, samplerate,
bitrate_mode='CONSTANT', compression_level=.99)
-
+
# minimum mp3 compression results in 3.5 Mb file
sf.write('min_compression_vbr.mp3', data, samplerate,
bitrate_mode='VARIABLE', compression_level=0)
+Thread Safety
+-------------
+
+The ``soundfile`` module is a thin wrapper around the low-level ``libsndfile``
C
+library. As such, ``soundfile`` provides the same thread safety guarantees as
+the underlying C library. See `this libsndfile issue
+<https://github.com/libsndfile/libsndfile/issues/279>`_ for more details.
+
+In short, multithreaded use is safe as long as handles for readers or writers
+are not shared between threads. It is safe to concurrently open and read the
+same file through unique per-thread handles. It is not safe to concurrently
+write to the same file or share reader or writer handles. You may see garbage
+results or crashes if you do this.
+
+See the advice in the Free-threaded Python guide on `thread-safe Python
+programming
+<https://py-free-threading.github.io/porting/#multithreaded-python-programming>`_
+for suggestions on how to synchronize access to a thread-unsafe object.
+
Known Issues
------------
@@ -436,3 +456,13 @@
Thank you, Brian McFee and Guy Illes
- Fixed regression in blocks
+
+2026-06-06 V0.14.0 Bastian Bechtold
+ Thank you GesonAnko, Trevor Gamblin, Andreas Karatzas, Harish RS, Hunter
Hogan
+
+ - Added type annotations
+ - Added Licensing note to wheel
+ - Fixed race condition when opening files concurrently
+ - Fixed regressions in test suite
+ - Removed support for Python <= 3.9
+ - Added ARM64 support for Windows
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/soundfile.egg-info/requires.txt
new/soundfile-0.14.0/soundfile.egg-info/requires.txt
--- old/soundfile-0.13.1/soundfile.egg-info/requires.txt 2025-01-25
10:09:37.000000000 +0100
+++ new/soundfile-0.14.0/soundfile.egg-info/requires.txt 2026-06-06
10:55:45.000000000 +0200
@@ -1,4 +1,5 @@
cffi>=1.0
numpy
+typing-extensions
[numpy]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/soundfile.py
new/soundfile-0.14.0/soundfile.py
--- old/soundfile-0.13.1/soundfile.py 2025-01-25 10:04:53.000000000 +0100
+++ new/soundfile-0.14.0/soundfile.py 2026-06-06 10:55:35.000000000 +0200
@@ -8,21 +8,29 @@
For further information, see https://python-soundfile.readthedocs.io/.
"""
-__version__ = "0.13.1"
+__version__ = "0.14.0"
import os as _os
import sys as _sys
-from os import SEEK_SET, SEEK_CUR, SEEK_END
+import threading as _threading
+from collections.abc import Generator
from ctypes.util import find_library as _find_library
-from _soundfile import ffi as _ffi
+from os import SEEK_CUR, SEEK_END, SEEK_SET
+from typing import Any, BinaryIO, Final, Literal, TypeAlias
+
+import numpy
+from typing_extensions import Self
-try:
- _unicode = unicode # doesn't exist in Python 3.x
-except NameError:
- _unicode = str
+from _soundfile import ffi as _ffi
+FileDescriptorOrPath: TypeAlias = str | int | BinaryIO | _os.PathLike[Any]
+AudioData: TypeAlias = numpy.ndarray[tuple[int, ...],
numpy.dtype[numpy.float32 | numpy.float64 | numpy.int32 | numpy.int16]]
+AudioData_2d: TypeAlias = numpy.ndarray[tuple[int, int],
numpy.dtype[numpy.float32 | numpy.float64 | numpy.int32 | numpy.int16]]
+dtype_str: TypeAlias = Literal['float64', 'float32', 'int32', 'int16']
+_snd: Any
+_ffi: Any
-_str_types = {
+_str_types: Final[dict[str, int]] = {
'title': 0x01,
'copyright': 0x02,
'software': 0x03,
@@ -35,7 +43,7 @@
'genre': 0x10,
}
-_formats = {
+_formats: Final[dict[str, int]] = {
'WAV': 0x010000, # Microsoft WAV format (little endian default).
'AIFF': 0x020000, # Apple/SGI AIFF format (big endian).
'AU': 0x030000, # Sun/NeXT AU format (big endian).
@@ -64,7 +72,7 @@
'MP3': 0x230000, # MPEG-1/2 audio stream
}
-_subtypes = {
+_subtypes: Final[dict[str, int]] = {
'PCM_S8': 0x0001, # Signed 8 bit data
'PCM_16': 0x0002, # Signed 16 bit data
'PCM_24': 0x0003, # Signed 24 bit data
@@ -101,7 +109,7 @@
'MPEG_LAYER_III': 0x0082, # MPEG-2 Audio Layer III.
}
-_endians = {
+_endians: Final[dict[str, int]] = {
'FILE': 0x00000000, # Default file endian-ness.
'LITTLE': 0x10000000, # Force little endian-ness.
'BIG': 0x20000000, # Force big endian-ness.
@@ -109,7 +117,7 @@
}
# libsndfile doesn't specify default subtypes, these are somehow arbitrary:
-_default_subtypes = {
+_default_subtypes: Final[dict[str, str]] = {
'WAV': 'PCM_16',
'AIFF': 'PCM_16',
'AU': 'PCM_16',
@@ -138,14 +146,14 @@
'MP3': 'MPEG_LAYER_III',
}
-_ffi_types = {
+_ffi_types: Final[dict[str, str]] = {
'float64': 'double',
'float32': 'float',
'int32': 'int',
'int16': 'short'
}
-_bitrate_modes = {
+_bitrate_modes: Final[dict[str, int]] = {
'CONSTANT': 0,
'AVERAGE': 1,
'VARIABLE': 2,
@@ -156,19 +164,17 @@
from platform import machine as _machine
_packaged_libname = 'libsndfile_' + _machine() + '.dylib'
elif _sys.platform == 'win32':
- from platform import architecture as _architecture
- from platform import machine as _machine
- # this check can not be completed correctly: for x64 binaries running
on
- # arm64 Windows report the same values as arm64 binaries. For now,
neither
- # numpy nor cffi are available for arm64, so we can safely assume we're
- # in x86 land:
- if _architecture()[0] == '64bit':
+ import sysconfig as _sysconfig
+
+ _win_machine = _sysconfig.get_platform()
+ if _win_machine == 'win-arm64':
+ _packaged_libname = 'libsndfile_arm64.dll'
+ elif _win_machine == 'win-amd64':
_packaged_libname = 'libsndfile_x64.dll'
- elif _architecture()[0] == '32bit':
+ elif _win_machine == 'win32':
_packaged_libname = 'libsndfile_x86.dll'
else:
- raise OSError('no packaged library for Windows {} {}'
- .format(_architecture(), _machine()))
+ raise OSError(f'no packaged library for Windows {_win_machine}')
elif _sys.platform == 'linux':
from platform import machine as _machine
if _machine() in ["aarch64", "aarch64_be", "armv8b", "armv8l"]:
@@ -216,9 +222,11 @@
__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):
+def read(file: FileDescriptorOrPath, frames: int = -1, start: int = 0, stop:
int | None = None, dtype: dtype_str = 'float64',
+ always_2d: bool = False, fill_value: float | None = None, out:
AudioData | AudioData_2d | None = None,
+ samplerate: int | None = None, channels: int | None = None, format:
str | None = None, subtype: str | None = None,
+ endian: str | None = None, closefd: bool = True) -> tuple[AudioData |
AudioData_2d, int]:
+
"""Provide audio data from a sound file as NumPy array.
By default, the whole file is read from the beginning, but the
@@ -309,8 +317,12 @@
return data, f.samplerate
-def write(file, data, samplerate, subtype=None, endian=None, format=None,
- closefd=True, compression_level=None, bitrate_mode=None):
+
+def write(file: FileDescriptorOrPath, data: AudioData, samplerate: int,
+ subtype: str | None = None, endian: str | None = None,
+ format: str | None = None, closefd: bool = True,
+ compression_level: float | None = None,
+ bitrate_mode: str | None = None) -> None:
"""Write data to a sound file.
.. note:: If *file* exists, it will be truncated and overwritten!
@@ -365,11 +377,14 @@
compression_level, bitrate_mode) 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):
+def blocks(file: FileDescriptorOrPath, blocksize: int | None = None,
+ overlap: int = 0, frames: int = -1, start: int = 0,
+ stop: int | None = None, dtype: dtype_str = 'float64',
+ always_2d: bool = False, fill_value: float | None = None,
+ out: AudioData | AudioData_2d | None = None, samplerate: int | None
= None,
+ channels: int | None = None, format: str | None = None,
+ subtype: str | None = None, endian: str | None = None,
+ closefd: bool = True) -> Generator[AudioData, None, None] |
Generator[AudioData_2d, None, None]:
"""Return a generator for block-wise reading.
By default, iteration starts at the beginning and stops at the end
@@ -420,64 +435,62 @@
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
+ yield from f.blocks(blocksize, overlap, frames, dtype, always_2d,
fill_value, out)
-class _SoundFileInfo(object):
+class _SoundFileInfo:
"""Information about a SoundFile"""
def __init__(self, file, verbose):
- self.verbose = verbose
+ self.verbose: bool = 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
+ self.name: str | int | Any = f.name
+ self.samplerate: int = f.samplerate
+ self.channels: int = f.channels
+ self.frames: int = f.frames
+ self.duration: float = float(self.frames)/f.samplerate
+ self.format: str = f.format
+ self.subtype: str = f.subtype
+ self.endian: str = f.endian
+ self.format_info: str = f.format_info
+ self.subtype_info: str = f.subtype_info
+ self.sections: int = f.sections
+ self.extra_info: str = 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)
+ duration = f"{hours:.0g}:{minutes:02.0g}:{seconds:05.3f} h"
elif minutes >= 1:
- duration = "{0:02.0g}:{1:05.3f} min".format(minutes, seconds)
+ duration = f"{minutes:02.0g}:{seconds:05.3f} min"
elif seconds <= 1:
- duration = "{0:d} samples".format(self.frames)
+ duration = f"{self.frames:d} samples"
else:
- duration = "{0:.3f} s".format(seconds)
+ duration = f"{seconds:.3f} s"
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}]"])
+ [f"{self.name}",
+ f"samplerate: {self.samplerate} Hz",
+ f"channels: {self.channels}",
+ f"duration: {self._duration_str}",
+ f"format: {self.format_info} [{self.format}]",
+ f"subtype: {self.subtype_info} [{self.subtype}]"])
if self.verbose:
+ indented_extra_info = ("\n"+"
"*4).join(self.extra_info.split("\n"))
info += "\n".join(
- ["\nendian: {0.endian}",
- "sections: {0.sections}",
- "frames: {0.frames}",
+ [f"\nendian: {self.endian}",
+ f"sections: {self.sections}",
+ f"frames: {self.frames}",
'extra_info: """',
- ' {1}"""'])
- indented_extra_info = ("\n"+" "*4).join(self.extra_info.split("\n"))
- return info.format(self, indented_extra_info)
+ f' {indented_extra_info}"""'])
+ return info
-def info(file, verbose=False):
+def info(file: FileDescriptorOrPath, verbose: bool = False) -> _SoundFileInfo:
"""Returns an object with information about a `SoundFile`.
Parameters
@@ -488,7 +501,7 @@
return _SoundFileInfo(file, verbose)
-def available_formats():
+def available_formats() -> dict[str, str]:
"""Return a dictionary of available major formats.
Examples
@@ -509,7 +522,7 @@
_snd.SFC_GET_FORMAT_MAJOR))
-def available_subtypes(format=None):
+def available_subtypes(format: str | None = None) -> dict[str, str]:
"""Return a dictionary of available subtypes.
Parameters
@@ -528,11 +541,12 @@
"""
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))
+ return {subtype: name for subtype, name in subtypes
+ if format is None or check_format(format, subtype)}
-def check_format(format, subtype=None, endian=None):
+def check_format(format: str, subtype: str | None = None,
+ endian: str | None = None) -> bool:
"""Check if the combination of format/subtype/endian is valid.
Examples
@@ -550,7 +564,7 @@
return False
-def default_subtype(format):
+def default_subtype(format: str) -> str | None:
"""Return the default subtype for a given format.
Examples
@@ -566,7 +580,7 @@
return _default_subtypes.get(format.upper())
-class SoundFile(object):
+class SoundFile:
"""A sound file.
For more documentation see the __init__() docstring (which is also
@@ -574,9 +588,12 @@
"""
- def __init__(self, file, mode='r', samplerate=None, channels=None,
- subtype=None, endian=None, format=None, closefd=True,
- compression_level=None, bitrate_mode=None):
+ def __init__(self, file: FileDescriptorOrPath, mode: str | None = 'r',
+ samplerate: int | None = None, channels: int | None = None,
+ subtype: str | None = None, endian: str | None = None,
+ format: str | None = None, closefd: bool = True,
+ compression_level: float | None = None,
+ bitrate_mode: str | None = None) -> None:
"""Open a sound file.
If a file is opened with `mode` ``'r'`` (the default) or
@@ -651,7 +668,7 @@
(highest compression level).
See `libsndfile document
<https://github.com/libsndfile/libsndfile/blob/c81375f070f3c6764969a738eacded64f53a076e/docs/command.md>`__.
bitrate_mode : {'CONSTANT', 'AVERAGE', 'VARIABLE'}, optional
- The bitrate mode on 'write()'.
+ The bitrate mode on 'write()'.
See `libsndfile document
<https://github.com/libsndfile/libsndfile/blob/c81375f070f3c6764969a738eacded64f53a076e/docs/command.md>`__.
Examples
@@ -675,12 +692,13 @@
>>> 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
+ if isinstance(file, _os.PathLike):
+ file = _os.fspath(file)
self._name = file
if mode is None:
mode = getattr(file, 'mode', None)
+ if mode is None:
+ raise TypeError("Can not get `mode` from file. provided `mode`
is None.") # Raises ValueError explicitly for type checking.
mode_int = _check_mode(mode)
self._mode = mode
self._compression_level = compression_level
@@ -693,7 +711,7 @@
self.seek(0)
_snd.sf_command(self._file, _snd.SFC_SET_CLIPPING, _ffi.NULL,
_snd.SF_TRUE)
-
+
# set compression setting
if self._compression_level is not None:
# needs to be called before set_bitrate_mode
@@ -750,26 +768,26 @@
# avoid confusion if something goes wrong before assigning self._file:
_file = None
- def __repr__(self):
- compression_setting = (",
compression_level={0}".format(self.compression_level)
+ def __repr__(self) -> str:
+ compression_setting = (f", compression_level={self.compression_level}"
if self.compression_level is not None else "")
- compression_setting += (",
bitrate_mode='{0}'".format(self.bitrate_mode)
+ compression_setting += (f", bitrate_mode='{self.bitrate_mode}'"
if self.bitrate_mode is not None else "")
- 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}{1})".format(self, compression_setting))
+ return (f"SoundFile({self.name!r}, mode={self.mode!r}, "
+ f"samplerate={self.samplerate}, channels={self.channels}, "
+ f"format={self.format!r}, subtype={self.subtype!r}, "
+ f"endian={self.endian!r}{compression_setting})")
- def __del__(self):
+ def __del__(self) -> None:
self.close()
- def __enter__(self):
+ def __enter__(self) -> Self:
return self
- def __exit__(self, *args):
+ def __exit__(self, *args: Any) -> None:
self.close()
- def __setattr__(self, name, value):
+ def __setattr__(self, name: str, value: Any) -> None:
"""Write text meta-data in the sound file through properties."""
if name in _str_types:
self._check_if_closed()
@@ -779,7 +797,7 @@
else:
object.__setattr__(self, name, value)
- def __getattr__(self, name):
+ def __getattr__(self, name: str) -> Any:
"""Read text meta-data in the sound file through properties."""
if name in _str_types:
self._check_if_closed()
@@ -787,28 +805,28 @@
return _ffi.string(data).decode('utf-8', 'replace') if data else ""
else:
raise AttributeError(
- "'SoundFile' object has no attribute {0!r}".format(name))
+ f"'SoundFile' object has no attribute {name!r}")
- def __len__(self):
+ def __len__(self) -> int:
# 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):
+ def __bool__(self) -> bool:
# Note: This is temporary until __len__ is removed, afterwards it
# can (and should) be removed without change of behavior
return True
- def __nonzero__(self):
+ def __nonzero__(self) -> bool:
# 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):
+ def seekable(self) -> bool:
"""Return True if the file supports seeking."""
return self._info.seekable == _snd.SF_TRUE
- def seek(self, frames, whence=SEEK_SET):
+ def seek(self, frames: int, whence: int = SEEK_SET) -> int:
"""Set the read/write position.
Parameters
@@ -849,12 +867,14 @@
_error_check(self._errorcode)
return position
- def tell(self):
+ def tell(self) -> int:
"""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):
+
+ def read(self, frames: int = -1, dtype: dtype_str = 'float64',
+ always_2d: bool = False, fill_value: float | None = None,
+ out: AudioData | AudioData_2d | None = None) -> AudioData |
AudioData_2d:
"""Read from the file and return data as NumPy array.
Reads the given number of frames in the given data format
@@ -947,7 +967,8 @@
out[frames:] = fill_value
return out
- def buffer_read(self, frames=-1, dtype=None):
+
+ def buffer_read(self, frames: int = -1, dtype: dtype_str | None = None) ->
memoryview:
"""Read from the file and return data as buffer object.
Reads the given number of *frames* in the given data format
@@ -982,7 +1003,7 @@
assert read_frames == frames
return _ffi.buffer(cdata)
- def buffer_read_into(self, buffer, dtype):
+ def buffer_read_into(self, buffer: bytearray | memoryview | Any, dtype:
dtype_str) -> int:
"""Read from the file into a given buffer object.
Fills the given *buffer* with frames in the given data format
@@ -1015,7 +1036,7 @@
frames = self._cdata_io('read', cdata, ctype, frames)
return frames
- def write(self, data):
+ def write(self, data: AudioData) -> None:
"""Write audio data from a NumPy array to the file.
Writes a number of frames at the read/write position to the
@@ -1069,7 +1090,7 @@
assert written == len(data)
self._update_frames(written)
- def buffer_write(self, data, dtype):
+ def buffer_write(self, data: bytes, dtype: dtype_str) -> None:
"""Write audio data from a buffer/bytes object to the file.
Writes the contents of *data* to the file at the current
@@ -1096,8 +1117,10 @@
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):
+ def blocks(self, blocksize: int | None = None, overlap: int = 0,
+ frames: int = -1, dtype: dtype_str = 'float64',
+ always_2d: bool = False, fill_value: float | None = None,
+ out: AudioData | AudioData_2d | None = None) ->
Generator[AudioData, None, None] | Generator[AudioData_2d, None, None]:
"""Return a generator for block-wise reading.
By default, the generator yields blocks of the given
@@ -1189,7 +1212,7 @@
yield np.copy(block) if copy_out else block
frames -= toread
- def truncate(self, frames=None):
+ def truncate(self, frames: int | None = None) -> None:
"""Truncate the file to a given number of frames.
After this command, the read/write position will be at the new
@@ -1213,7 +1236,7 @@
raise LibsndfileError(err, "Error truncating the file")
self._info.frames = frames
- def flush(self):
+ def flush(self) -> None:
"""Write unwritten data to the file system.
Data written with `write()` is not immediately written to
@@ -1227,7 +1250,7 @@
self._check_if_closed()
_snd.sf_write_sync(self._file)
- def close(self):
+ def close(self) -> None:
"""Close the file. Can be called multiple times."""
if not self.closed:
# be sure to flush data to disk before closing the file
@@ -1236,33 +1259,41 @@
self._file = None
_error_check(err)
+ # sf_error(NULL) returns a global (non-thread-safe) error code.
+ # When an sf_open* call fails (returns NULL), we must call
+ # sf_error(NULL) to retrieve the error code, but another thread
+ # may clear the global error between our open and our sf_error.
+ # This lock serialises the open+sf_error(NULL) pair to prevent that.
+ _sf_error_lock = _threading.Lock()
+
def _open(self, file, mode_int, closefd):
"""Call the appropriate sf_open*() function from libsndfile."""
- if isinstance(file, (_unicode, bytes)):
+ if isinstance(file, (str, bytes)):
if _os.path.isfile(file):
if 'x' in self.mode:
- raise OSError("File exists: {0!r}".format(self.name))
+ raise OSError(f"File exists: {self.name!r}")
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 isinstance(file, str):
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)
+ openfunction = lambda file, mode_int, info: _snd.sf_open_fd(file,
mode_int, 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)
+ openfunction = lambda file, mode_int, info:
_snd.sf_open_virtual(self._init_virtual_io(file),
+ mode_int, 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))
+ raise TypeError(f"Invalid file: {self.name!r}")
+
+ with self._sf_error_lock:
+ file_ptr = openfunction(file, mode_int, self._info)
+ if file_ptr == _ffi.NULL:
+ err = _snd.sf_error(file_ptr)
+ raise LibsndfileError(err, prefix=f"Error opening
{self.name!r}: ")
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.
@@ -1376,16 +1407,15 @@
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))
+ raise ValueError(f"dtype must be one of
{sorted(_ffi_types.keys())!r} and not {dtype!r}")
def _array_io(self, action, array, frames):
"""Check array and call low-level IO function."""
if array.ndim not in (1,2):
- raise ValueError("Invalid shape: {0!r} ({1})".format(array.shape,
"0 dimensions not supported" if array.ndim < 1 else "too many dimensions"))
+ raise ValueError(f"Invalid shape: {array.shape!r} ({'0 dimensions
not supported' if array.ndim < 1 else 'too many dimensions'})")
array_channels = 1 if array.ndim == 1 else array.shape[1]
if array_channels != self.channels:
- raise ValueError("Invalid shape: {0!r} (Expected {1} channels, got
{2})".format(array.shape, self.channels, array_channels))
+ raise ValueError(f"Invalid shape: {array.shape!r} (Expected
{self.channels} channels, got {array_channels})")
if not array.flags.c_contiguous:
raise ValueError("Data must be C-contiguous")
ctype = self._check_dtype(array.dtype.name)
@@ -1397,6 +1427,7 @@
"""Call one of libsndfile's read/write functions."""
assert ctype in _ffi_types.values()
self._check_if_closed()
+ curr = 0
if self.seekable():
curr = self.tell()
func = getattr(_snd, 'sf_' + action + 'f_' + ctype)
@@ -1431,7 +1462,7 @@
self.seek(start, SEEK_SET)
return frames
- def copy_metadata(self):
+ def copy_metadata(self) -> dict[str, str]:
"""Get all metadata present in this SoundFile
Returns
@@ -1448,7 +1479,7 @@
if data:
strs[strtype] = _ffi.string(data).decode('utf-8', 'replace')
return strs
-
+
def _set_bitrate_mode(self, bitrate_mode):
"""Call libsndfile's set bitrate mode function."""
assert bitrate_mode in _bitrate_modes
@@ -1460,7 +1491,7 @@
err = _snd.sf_error(self._file)
raise LibsndfileError(err, f"Error set bitrate mode
{bitrate_mode}")
-
+
def _set_compression_level(self, compression_level):
"""Call libsndfile's set compression level function."""
if not (0 <= compression_level <= 1):
@@ -1487,21 +1518,21 @@
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))
+ f"No default subtype for major format {format!r}")
+ elif not isinstance(subtype, str):
+ raise TypeError(f"Invalid subtype: {subtype!r}")
try:
result |= _subtypes[subtype.upper()]
except KeyError:
- raise ValueError("Unknown subtype: {0!r}".format(subtype))
+ raise ValueError(f"Unknown subtype: {subtype!r}")
if endian is None:
endian = 'FILE'
- elif not isinstance(endian, (_unicode, str)):
- raise TypeError("Invalid endian-ness: {0!r}".format(endian))
+ elif not isinstance(endian, str):
+ raise TypeError(f"Invalid endian-ness: {endian!r}")
try:
result |= _endians[endian.upper()]
except KeyError:
- raise ValueError("Unknown endian-ness: {0!r}".format(endian))
+ raise ValueError(f"Unknown endian-ness: {endian!r}")
info = _ffi.new("SF_INFO*")
info.format = result
@@ -1514,11 +1545,11 @@
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))
+ if not isinstance(mode, str):
+ raise TypeError(f"Invalid mode: {mode!r}")
mode_set = set(mode)
if mode_set.difference('xrwb+') or len(mode) > len(mode_set):
- raise ValueError("Invalid mode: {0!r}".format(mode))
+ raise ValueError(f"Invalid mode: {mode!r}")
if len(mode_set.intersection('xrw')) != 1:
raise ValueError("mode must contain exactly one of 'xrw'")
@@ -1537,7 +1568,7 @@
original_format = format
if format is None:
format = _get_format_from_filename(file, mode)
- assert isinstance(format, (_unicode, str))
+ assert isinstance(format, str)
else:
_check_format(format)
@@ -1577,8 +1608,8 @@
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))
+ raise TypeError(f"No format specified and unable to get format from "
+ f"file extension: {file!r}")
return format
@@ -1613,12 +1644,12 @@
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))
+ if not isinstance(format_str, str):
+ raise TypeError(f"Invalid format: {format_str!r}")
try:
format_int = _formats[format_str.upper()]
except KeyError:
- raise ValueError("Unknown format: {0!r}".format(format_str))
+ raise ValueError(f"Unknown format: {format_str!r}")
return format_int
@@ -1653,13 +1684,13 @@
code
libsndfile internal error number.
"""
- def __init__(self, code, prefix=""):
+ def __init__(self, code: int, prefix: str = "") -> None:
SoundFileRuntimeError.__init__(self, code, prefix)
self.code = code
self.prefix = prefix
@property
- def error_string(self):
+ def error_string(self) -> str:
"""Raw libsndfile error message."""
if self.code:
err_str = _snd.sf_error_number(self.code)
@@ -1670,5 +1701,5 @@
# See https://github.com/erikd/libsndfile/issues/610 for details.
return "(Garbled error message from libsndfile)"
- def __str__(self):
+ def __str__(self) -> str:
return self.prefix + self.error_string
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soundfile-0.13.1/tests/test_soundfile.py
new/soundfile-0.14.0/tests/test_soundfile.py
--- old/soundfile-0.13.1/tests/test_soundfile.py 2025-01-25
10:02:16.000000000 +0100
+++ new/soundfile-0.14.0/tests/test_soundfile.py 2026-04-22
08:56:52.000000000 +0200
@@ -46,8 +46,12 @@
fd = os.open(filename, fdarg)
def finalizer():
- with pytest.raises(OSError):
+ # On Windows Server 2025, os.close() on an already-closed fd does
+ # not raise OSError, so we close it silently to avoid resource
leaks.
+ try:
os.close(fd)
+ except OSError:
+ pass
request.addfinalizer(finalizer)
return fd
@@ -598,7 +602,7 @@
def test_open_with_mode_is_none():
with pytest.raises(TypeError) as excinfo:
sf.SoundFile(filename_stereo, mode=None)
- assert "Invalid mode: None" in str(excinfo.value)
+ assert "Can not get `mode` from file. provided `mode` is None." in
str(excinfo.value)
with open(filename_stereo, 'rb') as fobj:
with sf.SoundFile(fobj, mode=None) as f:
assert f.mode == 'rb'