pax_global_header00006660000000000000000000000064132020744150014510gustar00rootroot0000000000000052 comment=033e11b163b9d3a85bc53a37e58066d31feda31f SoundFile-0.10.0/000077500000000000000000000000001320207441500134565ustar00rootroot00000000000000SoundFile-0.10.0/.gitignore000066400000000000000000000001321320207441500154420ustar00rootroot00000000000000*.pyc __pycache__/ dist/ build/ PySoundFile.egg-info/ .eggs/ *.egg-info/ .cache/ .vscode/ SoundFile-0.10.0/.gitmodules000066400000000000000000000001601320207441500156300ustar00rootroot00000000000000[submodule "_soundfile_data"] path = _soundfile_data url = https://github.com/bastibe/libsndfile-binaries.git SoundFile-0.10.0/.travis.yml000066400000000000000000000007151320207441500155720ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "3.2" - "3.3" - "3.4" - "3.5" - "3.6" - "nightly" - "pypy-5.3.1" - "pypy-5.4.1" # TODO: Enable pypy3 once NumPy is working there, see # https://bitbucket.org/pypy/pypy/issues/1567/ #- "pypy3" addons: apt: packages: - libsndfile1 install: - "if [[ $TRAVIS_PYTHON_VERSION == pypy* ]]; then pip install git+https://bitbucket.org/pypy/numpy.git ; fi" script: - python setup.py test SoundFile-0.10.0/CONTRIBUTING.rst000066400000000000000000000037331320207441500161250ustar00rootroot00000000000000Contributing ------------ If you find bugs, errors, omissions or other things that need improvement, please create an issue or a pull request at https://github.com/bastibe/PySoundFile/. Contributions are always welcome! Testing ^^^^^^^ If you fix a bug, you should add a test that exposes the bug (to avoid future regressions), if you add a feature, you should add tests for it as well. To run the tests, use:: python setup.py test This uses py.test_; if you haven't installed it already, it will be downloaded and installed for you. .. _py.test: http://pytest.org/ .. note:: There is a `known problem`_ that prohibits the use of file descriptors on Windows if the libsndfile DLL was compiled with a different compiler than the Python interpreter. Unfortunately, this is typically the case if the packaged DLLs are used. To skip the tests which utilize file descriptors, use:: python setup.py test --pytest-args="-knot\ fd" .. _known problem: http://www.mega-nerd.com/libsndfile/api.html#open_fd Coverage ^^^^^^^^ If you want to measure code coverage, you can use coverage.py_. Just install it with:: pip install coverage --user ... and run it with:: coverage run --source soundfile.py -m py.test coverage html The resulting HTML files will be written to the ``htmlcov/`` directory. You can even check `branch coverage`_:: coverage run --branch --source soundfile.py -m py.test coverage html .. _coverage.py: http://nedbatchelder.com/code/coverage/ .. _branch coverage: http://nedbatchelder.com/code/coverage/branch.html Documentation ^^^^^^^^^^^^^ If you make changes to the documentation, you can re-create the HTML pages on your local system using Sphinx_. .. _Sphinx: http://sphinx-doc.org/ You can install it and a few other necessary packages with:: pip install -r doc/requirements.txt --user To create the HTML pages, use:: python setup.py build_sphinx The generated files will be available in the directory ``build/sphinx/html/``. SoundFile-0.10.0/LICENSE000066400000000000000000000027431320207441500144710ustar00rootroot00000000000000Copyright (c) 2013, Bastian Bechtold All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of PySoundFile nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SoundFile-0.10.0/MANIFEST.in000066400000000000000000000002041320207441500152100ustar00rootroot00000000000000include LICENSE include soundfile_build.py recursive-include doc *.py *.rst *.txt Makefile recursive-include tests *.py *.wav *.raw SoundFile-0.10.0/README.rst000066400000000000000000000216211320207441500151470ustar00rootroot00000000000000SoundFile ========= `SoundFile `__ is an audio library based on libsndfile, CFFI and NumPy. Full documentation is available on http://pysoundfile.readthedocs.org/. SoundFile can read and write sound files. File reading/writing is supported through `libsndfile `__, which is a free, cross-platform, open-source (LGPL) library for reading and writing many different sampled sound file formats that runs on many platforms including Windows, OS X, and Unix. It is accessed through `CFFI `__, which is a foreign function interface for Python calling C code. CFFI is supported for CPython 2.6+, 3.x and PyPy 2.0+. SoundFile represents audio data as NumPy arrays. | SoundFile is BSD licensed (BSD 3-Clause License). | (c) 2013, Bastian Bechtold Breaking Changes ---------------- SoundFile has evolved rapidly during the last few releases. Most notably, we changed the import name from ``import pysoundfile`` to ``import soundfile`` in 0.7. In 0.6, we cleaned up many small inconsistencies, particularly in the the ordering and naming of function arguments and the removal of the indexing interface. In 0.8.0, we changed the default value of ``always_2d`` from ``True`` to ``False``. Also, the order of arguments of the ``write`` function changed from ``write(data, file, ...)`` to ``write(file, data, ...)``. In 0.9.0, we changed the ``ctype`` arguments of the ``buffer_*`` methods to ``dtype``, using the Numpy ``dtype`` notation. The old ``ctype`` arguments still work, but are now officially deprecated. Installation ------------ SoundFile depends on the Python packages CFFI and NumPy, and the system library libsndfile. In a modern Python, you can use ``pip install soundfile`` to download and install the latest release of SoundFile and its dependencies. On Windows and OS X, this will also install the library libsndfile. On Linux, you need to install libsndfile using your distribution's package manager, for example ``sudo apt-get install libsndfile1``. If you are running on an unusual platform or if you are using an older version of Python, you might need to install NumPy and CFFI separately, for example using the Anaconda_ package manager or the `Unofficial Windows Binaries for Python Extension Packages `_. .. _Anaconda: https://www.continuum.io/downloads Read/Write Functions -------------------- Data can be written to the file using `soundfile.write()`, or read from the file using `soundfile.read()`. SoundFile can open all file formats that `libsndfile supports `__, for example WAV, FLAC, OGG and MAT files (see `Known Issues `__ below about writing OGG files). Here is an example for a program that reads a wave file and copies it into an FLAC file: .. code:: python import soundfile as sf data, samplerate = sf.read('existing_file.wav') sf.write('new_file.flac', data, samplerate) Block Processing ---------------- Sound files can also be read in short, optionally overlapping blocks with `soundfile.blocks()`. For example, this calculates the signal level for each block of a long file: .. code:: python import numpy as np import soundfile as sf rms = [np.sqrt(np.mean(block**2)) for block in sf.blocks('myfile.wav', blocksize=1024, overlap=512)] SoundFile Objects ----------------- Sound files can also be opened as `soundfile.SoundFile` objects. Every SoundFile has a specific sample rate, data format and a set number of channels. If a file is opened, it is kept open for as long as the SoundFile object exists. The file closes when the object is garbage collected, but you should use the `soundfile.SoundFile.close()` method or the context manager to close the file explicitly: .. code:: python import soundfile as sf with sf.SoundFile('myfile.wav', 'r+') as f: while f.tell() < f.frames: pos = f.tell() data = f.read(1024) f.seek(pos) f.write(data*2) All data access uses frames as index. A frame is one discrete time-step in the sound file. Every frame contains as many samples as there are channels in the file. RAW Files --------- Pysoundfile can usually auto-detect the file type of sound files. This is not possible for RAW files, though: .. code:: python import soundfile as sf data, samplerate = sf.read('myfile.raw', channels=1, samplerate=44100, subtype='FLOAT') Note that on x86, this defaults to ``endian='LITTLE'``. If you are reading big endian data (mostly old PowerPC/6800-based files), you have to set ``endian='BIG'`` accordingly. You can write RAW files in a similar way, but be advised that in most cases, a more expressive format is better and should be used instead. Virtual IO ---------- If you have an open file-like object, Pysoundfile can open it just like regular files: .. code:: python import soundfile as sf with open('filename.flac', 'rb') as f: data, samplerate = sf.read(f) Here is an example using an HTTP request: .. code:: python import io import soundfile as sf from urllib.request import urlopen url = "http://tinyurl.com/shepard-risset" data, samplerate = sf.read(io.BytesIO(urlopen(url).read())) Note that the above example only works with Python 3.x. For Python 2.x support, replace the third line with: .. code:: python from urllib2 import urlopen Known Issues ------------ Writing to OGG files can result in empty files with certain versions of libsndfile. See `#130 `__ for news on this issue. News ---- 2013-08-27 V0.1.0 Bastian Bechtold: Initial prototype. A simple wrapper for libsndfile in Python 2013-08-30 V0.2.0 Bastian Bechtold: Bugfixes and more consistency with PySoundCard 2013-08-30 V0.2.1 Bastian Bechtold: Bugfixes 2013-09-27 V0.3.0 Bastian Bechtold: Added binary installer for Windows, and context manager 2013-11-06 V0.3.1 Bastian Bechtold: Switched from distutils to setuptools for easier installation 2013-11-29 V0.4.0 Bastian Bechtold: Thanks to David Blewett, now with Virtual IO! 2013-12-08 V0.4.1 Bastian Bechtold: Thanks to Xidorn Quan, FLAC files are not float32 any more. 2014-02-26 V0.5.0 Bastian Bechtold: Thanks to Matthias Geier, improved seeking and a flush() method. 2015-01-19 V0.6.0 Bastian Bechtold: A big, big thank you to Matthias Geier, who did most of the work! - Switched to ``float64`` as default data type. - Function arguments changed for consistency. - Added unit tests. - Added global ``read()``, ``write()``, ``blocks()`` convenience functions. - Documentation overhaul and hosting on readthedocs. - Added ``'x'`` open mode. - Added ``tell()`` method. - Added ``__repr__()`` method. 2015-04-12 V0.7.0 Bastian Bechtold: Again, thanks to Matthias Geier for all of his hard work, but also Nils Werner and Whistler7 for their many suggestions and help. - Renamed ``import pysoundfile`` to ``import soundfile``. - Installation through pip wheels that contain the necessary libraries for OS X and Windows. - Removed ``exclusive_creation`` argument to ``write``. - Added ``truncate()`` method. 2015-10-20 V0.8.0 Bastian Bechtold: Again, Matthias Geier contributed a whole lot of hard work to this release. - Changed the default value of ``always_2d`` from ``True`` to ``False``. - Numpy is now optional, and only loaded for ``read`` and ``write``. - Added ``SoundFile.buffer_read`` and ``SoundFile.buffer_read_into`` and ``SoundFile.buffer_write``, which read/write raw data without involving Numpy. - Added ``info`` function that returns metadata of a sound file. - Changed the argument order of the ``write`` function from ``write(data, file, ...)`` to ``write(file, data, ...)`` And many more minor bug fixes. 2017-02-02 V0.9.0 Bastian Bechtold: Thank you, Matthias Geier, Tomas Garcia, and Todd, for contributions for this release. - Adds support for ALAC files. - Adds new member ``__libsndfile_version__`` - Adds number of frames to ``info`` class - Adds ``dtype`` argument to ``buffer_*`` methods - Deprecates ``ctype`` argument to ``buffer_*`` methods - Adds official support for Python 3.6 And some minor bug fixes. 2017-11-12 V0.10.0 Bastian Bechtold: Thank you, Matthias Geier, Toni Barth, Jon Peirce, Till Hoffmann, and Tomas Garcia, for contributions to this release. - Should now work with cx_freeze. - Several documentation fixes in the README. - Removes deprecated ``ctype`` argument in favor of ``dtype`` in ``buffer_*()``. - Adds ``SoundFile.frames`` in favor of now-deprecated ``__len__()``. - Improves performance of ``blocks`` and ``SoundFile.blocks()``. - Improves import time by using CFFI's out of line mode. SoundFile-0.10.0/_soundfile_data/000077500000000000000000000000001320207441500165765ustar00rootroot00000000000000SoundFile-0.10.0/doc/000077500000000000000000000000001320207441500142235ustar00rootroot00000000000000SoundFile-0.10.0/doc/Makefile000066400000000000000000000151761320207441500156750ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PySoundFile.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PySoundFile.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PySoundFile" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PySoundFile" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." SoundFile-0.10.0/doc/conf.py000066400000000000000000000214531320207441500155270ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # PySoundFile documentation build configuration file, created by # sphinx-quickstart on Sun Sep 21 19:26:48 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os from subprocess import check_output # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', # support for NumPy-style docstrings ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'PySoundFile' copyright = '2015, Bastian Bechtold, Matthias Geier' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. #version = 0.0 # The full version, including alpha/beta/rc tags. try: release = check_output(['git', 'describe', '--tags', '--always']) release = release.decode().strip() except Exception: release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'PySoundFiledoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'PySoundFile.tex', 'PySoundFile Documentation', 'Bastian Bechtold, Matthias Geier', 'howto'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pysoundfile', 'PySoundFile Documentation', ['Bastian Bechtold, Matthias Geier'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'PySoundFile', 'PySoundFile Documentation', 'Bastian Bechtold, Matthias Geier', 'PySoundFile', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False autodoc_member_order = 'bysource' autoclass_content = 'init' napoleon_use_rtype = False napoleon_include_private_with_doc = False napoleon_include_special_with_doc = False # Fake imports to avoid actually loading NumPy and libsndfile import fake_numpy sys.modules['numpy'] = sys.modules['fake_numpy'] import fake__soundfile sys.modules['_soundfile'] = sys.modules['fake__soundfile'] SoundFile-0.10.0/doc/fake__soundfile.py000066400000000000000000000004351320207441500177140ustar00rootroot00000000000000"""Mock module for Sphinx autodoc.""" class ffi(object): def dlopen(self, _): return self def string(self, _): return b'not implemented' def sf_version_string(self): return NotImplemented SFC_GET_FORMAT_INFO = NotImplemented ffi = ffi() SoundFile-0.10.0/doc/fake_numpy.py000066400000000000000000000001201320207441500167240ustar00rootroot00000000000000"""Mock module for Sphinx autodoc.""" def dtype(_): return NotImplemented SoundFile-0.10.0/doc/index.rst000066400000000000000000000003521320207441500160640ustar00rootroot00000000000000.. default-role:: py:obj .. include:: ../README.rst .. include:: ../CONTRIBUTING.rst .. default-role:: API Documentation ================= .. automodule:: soundfile :members: :undoc-members: Index ===== * :ref:`genindex` SoundFile-0.10.0/doc/requirements.txt000066400000000000000000000000341320207441500175040ustar00rootroot00000000000000Jinja2 Pygments Sphinx>=1.3 SoundFile-0.10.0/setup.py000066400000000000000000000070711320207441500151750ustar00rootroot00000000000000#!/usr/bin/env python import os from platform import architecture from setuptools import setup from setuptools.command.test import test as TestCommand import sys PYTHON_INTERPRETERS = '.'.join([ 'cp26', 'cp27', 'cp32', 'cp33', 'cp34', 'cp35', 'cp36', 'pp27', 'pp32', 'pp33', ]) MACOSX_VERSIONS = '.'.join([ 'macosx_10_5_x86_64', 'macosx_10_6_intel', 'macosx_10_9_intel', 'macosx_10_9_x86_64', ]) # environment variables for cross-platform package creation platform = os.environ.get('PYSOUNDFILE_PLATFORM', sys.platform) architecture0 = os.environ.get('PYSOUNDFILE_ARCHITECTURE', architecture()[0]) if platform == 'darwin': libname = 'libsndfile.dylib' elif platform == 'win32': libname = 'libsndfile' + architecture0 + '.dll' else: libname = None if libname and os.path.isdir('_soundfile_data'): packages = ['_soundfile_data'] package_data = {'_soundfile_data': [libname, 'COPYING']} zip_safe = False else: packages = None package_data = None zip_safe = True class PyTest(TestCommand): user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = [] def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.pytest_args) sys.exit(errno) cmdclass = {'test': PyTest} try: from wheel.bdist_wheel import bdist_wheel except ImportError: pass else: class bdist_wheel_half_pure(bdist_wheel): """Create OS-dependent, but Python-independent wheels.""" def get_tag(self): pythons = 'py2.py3.' + PYTHON_INTERPRETERS if platform == 'darwin': oses = MACOSX_VERSIONS elif platform == 'win32': if architecture0 == '32bit': oses = 'win32' else: oses = 'win_amd64' else: pythons = 'py2.py3' oses = 'any' return pythons, 'none', oses cmdclass['bdist_wheel'] = bdist_wheel_half_pure setup( name='SoundFile', version='0.10.0', description='An audio library based on libsndfile, CFFI and NumPy', author='Bastian Bechtold', author_email='basti@bastibe.de', url='https://github.com/bastibe/PySoundFile', keywords=['audio', 'libsndfile'], py_modules=['soundfile'], packages=packages, package_data=package_data, zip_safe=zip_safe, license='BSD 3-Clause License', setup_requires=["cffi>=1.0"], install_requires=['cffi>=1.0'], cffi_modules=["soundfile_build.py:ffibuilder"], extras_require={'numpy': ['numpy']}, platforms='any', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', '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', ], long_description=open('README.rst').read(), tests_require=['pytest'], cmdclass=cmdclass, ) SoundFile-0.10.0/soundfile.py000066400000000000000000001565731320207441500160410ustar00rootroot00000000000000"""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.0" 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: _snd = _ffi.dlopen(_find_library('sndfile')) 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) 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 """ 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}".format( sorted(_ffi_types.keys()))) 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, ]) SoundFile-0.10.0/soundfile_build.py000066400000000000000000000120151320207441500171760ustar00rootroot00000000000000import sys from cffi import FFI ffibuilder = FFI() ffibuilder.set_source("_soundfile", None) ffibuilder.cdef(""" enum { SF_FORMAT_SUBMASK = 0x0000FFFF, SF_FORMAT_TYPEMASK = 0x0FFF0000, SF_FORMAT_ENDMASK = 0x30000000 } ; enum { SFC_GET_LIB_VERSION = 0x1000, SFC_GET_LOG_INFO = 0x1001, SFC_GET_FORMAT_INFO = 0x1028, SFC_GET_FORMAT_MAJOR_COUNT = 0x1030, SFC_GET_FORMAT_MAJOR = 0x1031, SFC_GET_FORMAT_SUBTYPE_COUNT = 0x1032, SFC_GET_FORMAT_SUBTYPE = 0x1033, SFC_FILE_TRUNCATE = 0x1080, SFC_SET_CLIPPING = 0x10C0, SFC_SET_SCALE_FLOAT_INT_READ = 0x1014, SFC_SET_SCALE_INT_FLOAT_WRITE = 0x1015, } ; enum { SF_FALSE = 0, SF_TRUE = 1, /* Modes for opening files. */ SFM_READ = 0x10, SFM_WRITE = 0x20, SFM_RDWR = 0x30, } ; typedef int64_t sf_count_t ; typedef struct SNDFILE_tag SNDFILE ; typedef struct SF_INFO { sf_count_t frames ; /* Used to be called samples. Changed to avoid confusion. */ int samplerate ; int channels ; int format ; int sections ; int seekable ; } SF_INFO ; SNDFILE* sf_open (const char *path, int mode, SF_INFO *sfinfo) ; int sf_format_check (const SF_INFO *info) ; sf_count_t sf_seek (SNDFILE *sndfile, sf_count_t frames, int whence) ; int sf_command (SNDFILE *sndfile, int cmd, void *data, int datasize) ; int sf_error (SNDFILE *sndfile) ; const char* sf_strerror (SNDFILE *sndfile) ; const char* sf_error_number (int errnum) ; int sf_perror (SNDFILE *sndfile) ; int sf_error_str (SNDFILE *sndfile, char* str, size_t len) ; int sf_close (SNDFILE *sndfile) ; void sf_write_sync (SNDFILE *sndfile) ; sf_count_t sf_read_short (SNDFILE *sndfile, short *ptr, sf_count_t items) ; sf_count_t sf_read_int (SNDFILE *sndfile, int *ptr, sf_count_t items) ; sf_count_t sf_read_float (SNDFILE *sndfile, float *ptr, sf_count_t items) ; sf_count_t sf_read_double (SNDFILE *sndfile, double *ptr, sf_count_t items) ; /* Note: Data ptr argument types are declared as void* here in order to avoid an implicit cast warning. (gh183). */ sf_count_t sf_readf_short (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_readf_int (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_readf_float (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_readf_double (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_write_short (SNDFILE *sndfile, short *ptr, sf_count_t items) ; sf_count_t sf_write_int (SNDFILE *sndfile, int *ptr, sf_count_t items) ; sf_count_t sf_write_float (SNDFILE *sndfile, float *ptr, sf_count_t items) ; sf_count_t sf_write_double (SNDFILE *sndfile, double *ptr, sf_count_t items) ; /* Note: The argument types were changed to void* in order to allow writing bytes in SoundFile.buffer_write() */ sf_count_t sf_writef_short (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_writef_int (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_writef_float (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_writef_double (SNDFILE *sndfile, void *ptr, sf_count_t frames) ; sf_count_t sf_read_raw (SNDFILE *sndfile, void *ptr, sf_count_t bytes) ; sf_count_t sf_write_raw (SNDFILE *sndfile, void *ptr, sf_count_t bytes) ; const char* sf_get_string (SNDFILE *sndfile, int str_type) ; int sf_set_string (SNDFILE *sndfile, int str_type, const char* str) ; const char * sf_version_string (void) ; typedef sf_count_t (*sf_vio_get_filelen) (void *user_data) ; typedef sf_count_t (*sf_vio_seek) (sf_count_t offset, int whence, void *user_data) ; typedef sf_count_t (*sf_vio_read) (void *ptr, sf_count_t count, void *user_data) ; typedef sf_count_t (*sf_vio_write) (const void *ptr, sf_count_t count, void *user_data) ; typedef sf_count_t (*sf_vio_tell) (void *user_data) ; typedef struct SF_VIRTUAL_IO { sf_count_t (*get_filelen) (void *user_data) ; sf_count_t (*seek) (sf_count_t offset, int whence, void *user_data) ; sf_count_t (*read) (void *ptr, sf_count_t count, void *user_data) ; sf_count_t (*write) (const void *ptr, sf_count_t count, void *user_data) ; sf_count_t (*tell) (void *user_data) ; } SF_VIRTUAL_IO ; SNDFILE* sf_open_virtual (SF_VIRTUAL_IO *sfvirtual, int mode, SF_INFO *sfinfo, void *user_data) ; SNDFILE* sf_open_fd (int fd, int mode, SF_INFO *sfinfo, int close_desc) ; typedef struct SF_FORMAT_INFO { int format ; const char* name ; const char* extension ; } SF_FORMAT_INFO ; """) if sys.platform == 'win32': ffibuilder.cdef(""" SNDFILE* sf_wchar_open (LPCWSTR wpath, int mode, SF_INFO *sfinfo) ; """) if __name__ == "__main__": ffibuilder.compile(verbose=True) SoundFile-0.10.0/tests/000077500000000000000000000000001320207441500146205ustar00rootroot00000000000000SoundFile-0.10.0/tests/generate_soundfiles.py000077500000000000000000000030531320207441500212230ustar00rootroot00000000000000#!/usr/bin/env python import struct def uint32(number): return struct.pack('€¾SoundFile-0.10.0/tests/test_argspec.py000066400000000000000000000046521320207441500176640ustar00rootroot00000000000000"""Make sure that arguments of open/read/write don't diverge.""" import pytest import soundfile as sf import sys pytestmark = pytest.mark.skipif(sys.version_info < (3, 3), reason="signature module requires Python 3.3") def defaults(func): from inspect import signature return dict((k, v) for k, v in signature(func).parameters.items() if v.default is not v.empty) def remove_items(collection, subset): """From a collection of defaults, remove a subset and return the rest.""" the_rest = collection.copy() for name, param in subset.items(): assert (name, the_rest[name].default) == (name, param.default) del the_rest[name] return the_rest def test_read_defaults(): func_defaults = defaults(sf.read) meth_defaults = defaults(sf.SoundFile.read) init_defaults = defaults(sf.SoundFile.__init__) del init_defaults['mode'] # mode is always 'r' del func_defaults['start'] del func_defaults['stop'] # Same default values as SoundFile.__init__() and SoundFile.read(): for spec in init_defaults, meth_defaults: func_defaults = remove_items(func_defaults, spec) assert not func_defaults # No more arguments should be left def test_write_defaults(): write_defaults = defaults(sf.write) init_defaults = defaults(sf.SoundFile.__init__) # Same default values as SoundFile.__init__() init_defaults = remove_items(init_defaults, write_defaults) del init_defaults['mode'] # mode is always 'x' or 'w' del init_defaults['channels'] # Inferred from data del init_defaults['samplerate'] # Obligatory in write() assert not init_defaults # No more arguments should be left def test_if_blocks_function_and_method_have_same_defaults(): func_defaults = defaults(sf.blocks) meth_defaults = defaults(sf.SoundFile.blocks) init_defaults = defaults(sf.SoundFile.__init__) del func_defaults['start'] del func_defaults['stop'] del init_defaults['mode'] for spec in init_defaults, meth_defaults: func_defaults = remove_items(func_defaults, spec) assert not func_defaults def test_order_of_blocks_arguments(): from inspect import signature # remove 'self': meth_args = list(signature(sf.SoundFile.blocks).parameters)[1:] meth_args[3:3] = ['start', 'stop'] func_args = list(signature(sf.blocks).parameters) assert func_args[:10] == ['file'] + meth_args SoundFile-0.10.0/tests/test_pysoundfile.py000066400000000000000000001033411320207441500205740ustar00rootroot00000000000000import soundfile as sf import numpy as np import os import io import shutil import pytest import cffi import sys # floating point data is typically limited to the interval [-1.0, 1.0], # but smaller/larger values are supported as well data_stereo = np.array([[1.75, -1.75], [1.0, -1.0], [0.5, -0.5], [0.25, -0.25]]) data_mono = np.array([0, 1, 2, -2, -1], dtype='int16') filename_stereo = 'tests/stereo.wav' filename_mono = 'tests/mono.wav' filename_raw = 'tests/mono.raw' filename_new = 'tests/delme.please' open_variants = 'name', 'fd', 'obj' xfail_from_buffer = pytest.mark.xfail(cffi.__version_info__ < (0, 9), reason="from_buffer() since CFFI 0.9") def _file_existing(request, filename, fdarg, objarg=None): if request.param == 'name': return filename elif request.param == 'fd': fd = os.open(filename, fdarg) def finalizer(): with pytest.raises(OSError): os.close(fd) request.addfinalizer(finalizer) return fd elif request.param == 'obj': obj = open(filename, objarg, buffering=False) request.addfinalizer(obj.close) return obj def _file_new(request, fdarg, objarg=None): filename = filename_new request.addfinalizer(lambda: os.remove(filename)) return _file_existing(request, filename, fdarg, objarg) def _file_copy(request, filename, fdarg, objarg=None): shutil.copy(filename, filename_new) request.addfinalizer(lambda: os.remove(filename_new)) return _file_existing(request, filename_new, fdarg, objarg) @pytest.fixture(params=open_variants) def file_stereo_r(request): return _file_existing(request, filename_stereo, os.O_RDONLY, 'rb') @pytest.fixture(params=open_variants) def file_mono_r(request): return _file_existing(request, filename_mono, os.O_RDONLY, 'rb') @pytest.fixture(params=open_variants) def file_w(request): return _file_new(request, os.O_CREAT | os.O_WRONLY, 'wb') @pytest.fixture(params=open_variants) def file_stereo_rplus(request): return _file_copy(request, filename_stereo, os.O_RDWR, 'r+b') @pytest.fixture(params=['obj']) def file_obj_stereo_rplus(request): return _file_copy(request, filename_stereo, os.O_RDWR, 'r+b') @pytest.fixture(params=['obj']) def file_obj_w(request): return _file_new(request, os.O_CREAT | os.O_WRONLY, 'wb') @pytest.fixture(params=open_variants) def file_wplus(request): return _file_new(request, os.O_CREAT | os.O_RDWR, 'w+b') @pytest.yield_fixture def file_inmemory(): with io.BytesIO() as f: yield f @pytest.yield_fixture def sf_stereo_r(file_stereo_r): with sf.SoundFile(file_stereo_r) as f: yield f @pytest.yield_fixture def sf_stereo_w(file_w): with sf.SoundFile(file_w, 'w', 44100, 2, format='WAV') as f: yield f @pytest.yield_fixture def sf_stereo_rplus(file_stereo_rplus): with sf.SoundFile(file_stereo_rplus, 'r+') as f: yield f @pytest.yield_fixture def sf_stereo_wplus(file_wplus): with sf.SoundFile(file_wplus, 'w+', 44100, 2, format='WAV', subtype='FLOAT') as f: yield f # ----------------------------------------------------------------------------- # Test read() function # ----------------------------------------------------------------------------- def test_if_read_returns_float64_data(file_stereo_r): data, fs = sf.read(file_stereo_r) assert fs == 44100 assert np.all(data == data_stereo) assert data.dtype == np.float64 def test_read_float32(file_stereo_r): data, fs = sf.read(file_stereo_r, dtype='float32') assert np.all(data == data_stereo) assert data.dtype == np.float32 def test_read_int16(file_mono_r): data, fs = sf.read(file_mono_r, dtype='int16') assert np.all(data == data_mono) assert data.dtype == np.int16 def test_read_int32(file_mono_r): data, fs = sf.read(file_mono_r, dtype='int32') assert np.all(data // 2**16 == data_mono) assert data.dtype == np.int32 def test_read_into_out(file_stereo_r): out = np.empty((3, 2), dtype='float64') data, fs = sf.read(file_stereo_r, out=out) assert data is out assert np.all(data == data_stereo[:3]) def test_if_read_into_malformed_out_fails(file_stereo_r): out = np.empty((2, 3), dtype='float64') with pytest.raises(ValueError): sf.read(file_stereo_r, out=out) def test_if_read_into_out_with_too_many_dimensions_fails(file_stereo_r): out = np.empty((3, 2, 1), dtype='float64') with pytest.raises(ValueError): sf.read(file_stereo_r, out=out) def test_if_read_into_zero_len_out_works(file_stereo_r): out = np.empty((0, 2), dtype='float64') data, fs = sf.read(file_stereo_r, out=out) assert data is out assert len(out) == 0 def test_read_into_non_contiguous_out(file_stereo_r): out = np.empty(data_stereo.shape[::-1], dtype='float64') if getattr(sys, 'pypy_version_info', (999,)) < (2, 6): # The test for C-contiguous doesn't work with PyPy 2.5.0 sf.read(file_stereo_r, out=out.T) else: with pytest.raises(ValueError) as excinfo: sf.read(file_stereo_r, out=out.T) assert "C-contiguous" in str(excinfo.value) def test_read_into_out_with_invalid_dtype(file_stereo_r): out = np.empty((3, 2), dtype='int64') with pytest.raises(ValueError) as excinfo: sf.read(file_stereo_r, out=out) assert "dtype must be one of" in str(excinfo.value) def test_read_mono(file_mono_r): data, fs = sf.read(file_mono_r, dtype='int16') assert data.ndim == 1 assert np.all(data == data_mono) def test_if_read_mono_with_always2d_returns_2d_array(file_mono_r): data, fs = sf.read(file_mono_r, dtype='int16', always_2d=True) assert data.ndim == 2 assert np.all(data == data_mono.reshape(-1, 1)) def test_read_mono_into_1d_out(file_mono_r): out = np.empty(len(data_mono), dtype='int16') data, fs = sf.read(file_mono_r, out=out) assert data is out assert np.all(data == data_mono) def test_read_mono_into_2d_out(file_mono_r): out = np.empty((len(data_mono), 1), dtype='int16') data, fs = sf.read(file_mono_r, out=out) assert data is out assert np.all(data == data_mono.reshape(-1, 1)) def test_read_non_existing_file(): with pytest.raises(RuntimeError) as excinfo: sf.read("i_do_not_exist.wav") assert "Error opening 'i_do_not_exist.wav'" in str(excinfo.value) # ----------------------------------------------------------------------------- # Test write() function # ----------------------------------------------------------------------------- # The read() function is tested above, we assume here that it is working. def test_write_float_data_to_float_file(file_inmemory): sf.write(file_inmemory, data_stereo, 44100, format='WAV', subtype='FLOAT') file_inmemory.seek(0) read, fs = sf.read(file_inmemory) assert np.all(read == data_stereo) assert fs == 44100 def test_write_float_data_to_pcm_file(file_inmemory): float_to_clipped_int16 = [ (-1.0 - 2**-15, -2**15 ), (-1.0 , -2**15 ), (-1.0 + 2**-15, -2**15 + 1), ( 0.0 , 0 ), ( 1.0 - 2**-14, 2**15 - 2), ( 1.0 - 2**-15, 2**15 - 1), ( 1.0 , 2**15 - 1), ] written, expected = zip(*float_to_clipped_int16) sf.write(file_inmemory, written, 44100, format='WAV', subtype='PCM_16') file_inmemory.seek(0) read, fs = sf.read(file_inmemory, dtype='int16') assert np.all(read == expected) assert fs == 44100 def test_write_int_data_to_pcm_file(file_inmemory): sf.write(file_inmemory, data_mono, 44100, format='WAV') file_inmemory.seek(0) read, fs = sf.read(file_inmemory, dtype='int16') assert fs == 44100 assert np.all(read == data_mono) def test_write_int_data_to_float_file(file_inmemory): """This is a very uncommon use case.""" sf.write(file_inmemory, data_mono, 44100, format='WAV', subtype='FLOAT') file_inmemory.seek(0) read, fs = sf.read(file_inmemory, always_2d=False, dtype='float32') assert np.all(read == data_mono) assert fs == 44100 @pytest.mark.parametrize("filename", ["wav", ".wav", "wav.py"]) def test_write_with_unknown_extension(filename): with pytest.raises(TypeError) as excinfo: sf.write(filename, [0.0], 44100) assert "file extension" in str(excinfo.value) # ----------------------------------------------------------------------------- # Test blocks() function # ----------------------------------------------------------------------------- def assert_equal_list_of_arrays(list1, list2): """Helper function to assert equality of all list items.""" for item1, item2 in zip(list1, list2): assert np.all(item1 == item2) def test_blocks_without_blocksize(): with pytest.raises(TypeError): list(sf.blocks(filename_stereo)) def test_blocks_full_last_block(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2)) assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:4]]) def test_blocks_partial_last_block(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=3)) assert_equal_list_of_arrays(blocks, [data_stereo[0:3], data_stereo[3:4]]) def test_blocks_fill_last_block(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=3, fill_value=0)) last_block = np.row_stack((data_stereo[3:4], np.zeros((2, 2)))) assert_equal_list_of_arrays(blocks, [data_stereo[0:3], last_block]) def test_blocks_with_overlap(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=3, overlap=2)) assert_equal_list_of_arrays(blocks, [data_stereo[0:3], data_stereo[1:4]]) def test_blocks_with_start(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2, start=2)) assert_equal_list_of_arrays(blocks, [data_stereo[2:4]]) def test_blocks_with_stop(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2, stop=2)) assert_equal_list_of_arrays(blocks, [data_stereo[0:2]]) with pytest.raises(TypeError): list(sf.blocks(filename_stereo, blocksize=2, frames=2, stop=2)) def test_blocks_with_too_large_start(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2, start=666)) assert_equal_list_of_arrays(blocks, [[]]) def test_blocks_with_too_large_stop(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=3, stop=666)) assert_equal_list_of_arrays(blocks, [data_stereo[0:3], data_stereo[3:4]]) def test_blocks_with_negative_start_and_stop(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2, start=-2, stop=-1)) assert_equal_list_of_arrays(blocks, [data_stereo[-2:-1]]) def test_blocks_with_stop_smaller_than_start(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2, start=2, stop=1)) assert blocks == [] def test_blocks_with_frames(file_stereo_r): blocks = list(sf.blocks(file_stereo_r, blocksize=2, frames=3)) assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:3]]) def test_blocks_with_frames_and_fill_value(file_stereo_r): blocks = list( sf.blocks(file_stereo_r, blocksize=2, frames=3, fill_value=0)) last_block = np.row_stack((data_stereo[2:3], np.zeros((1, 2)))) assert_equal_list_of_arrays(blocks, [data_stereo[0:2], last_block]) def test_blocks_with_out(file_stereo_r): out = np.empty((3, 2)) blocks = list(sf.blocks(file_stereo_r, out=out)) assert blocks[0] is out # First frame was overwritten by second block: assert np.all(blocks[0] == data_stereo[[3, 1, 2]]) assert blocks[1].base is out assert np.all(blocks[1] == data_stereo[[3]]) with pytest.raises(TypeError): list(sf.blocks(filename_stereo, blocksize=3, out=out)) def test_blocks_inplace_modification(file_stereo_r): out = np.empty((3, 2)) blocks = [] for block in sf.blocks(file_stereo_r, out=out, overlap=1): blocks.append(np.copy(block)) block *= 2 expected_blocks = [data_stereo[0:3], data_stereo[2:5]] assert_equal_list_of_arrays(blocks, expected_blocks) def test_blocks_mono(): blocks = list(sf.blocks(filename_mono, blocksize=3, dtype='int16', fill_value=0)) assert_equal_list_of_arrays(blocks, [[0, 1, 2], [-2, -1, 0]]) def test_blocks_rplus(sf_stereo_rplus): blocks = list(sf_stereo_rplus.blocks(blocksize=2)) assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:4]]) def test_blocks_wplus(sf_stereo_wplus): """There is nothing to yield in a 'w+' file.""" blocks = list(sf_stereo_wplus.blocks(blocksize=2, frames=666)) assert blocks == [] def test_blocks_write(sf_stereo_w): with pytest.raises(RuntimeError): list(sf_stereo_w.blocks(blocksize=2)) # ----------------------------------------------------------------------------- # Test SoundFile.__init__() # ----------------------------------------------------------------------------- def test_open_bytes_filename(): with sf.SoundFile(filename_stereo.encode()) as f: assert np.all(f.read() == data_stereo) def test_open_with_invalid_file(): with pytest.raises(TypeError) as excinfo: sf.SoundFile(3.1415) assert "Invalid file" in str(excinfo.value) def test_open_with_invalid_mode(): with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_stereo, 42) assert "Invalid mode: 42" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: sf.SoundFile(filename_stereo, 'rr') assert "Invalid mode: 'rr'" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: sf.SoundFile(filename_stereo, 'rw') assert "exactly one of 'xrw'" in str(excinfo.value) def test_open_with_more_invalid_arguments(): with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, 'w', 3.1415, 2, format='WAV') assert "integer" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, 'w', 44100, 3.1415, format='WAV') assert "integer" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: sf.SoundFile(filename_new, 'w', 44100, 2, format='WAF') assert "Unknown format" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: sf.SoundFile(filename_new, 'w', 44100, 2, 'PCM16', format='WAV') assert "Unknown subtype" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, 'w', 44100, 2, 666, format='WAV') assert "Invalid subtype" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: sf.SoundFile(filename_new, 'w', 44100, 2, endian='BOTH', format='WAV') assert "Unknown endian-ness" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, 'w', 44100, 2, endian=True, format='WAV') assert "Invalid endian-ness" in str(excinfo.value) def test_open_r_and_rplus_with_too_many_arguments(): for mode in 'r', 'r+': with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_stereo, mode, samplerate=44100) assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_stereo, mode, channels=2) assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_stereo, mode, format='WAV') assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_stereo, mode, subtype='FLOAT') assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_stereo, mode, endian='FILE') assert "Not allowed" in str(excinfo.value) def test_open_w_and_wplus_with_too_few_arguments(): for mode in 'w', 'w+': with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, mode, samplerate=44100, channels=2) assert "No format specified" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, mode, samplerate=44100, format='WAV') assert "channels" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: sf.SoundFile(filename_new, mode, channels=2, format='WAV') assert "samplerate" in str(excinfo.value) 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) with open(filename_stereo, 'rb') as fobj: with sf.SoundFile(fobj, mode=None) as f: assert f.mode == 'rb' def test_open_with_mode_is_x(): with pytest.raises(OSError) as excinfo: sf.SoundFile(filename_stereo, 'x', 44100, 2) assert "exists" in str(excinfo.value) with pytest.raises(OSError) as excinfo: sf.SoundFile(filename_stereo, 'x+', 44100, 2) assert "exists" in str(excinfo.value) @pytest.mark.parametrize("mode", ['w', 'w+']) def test_if_open_with_mode_w_truncates(file_stereo_rplus, mode): with sf.SoundFile(file_stereo_rplus, mode, 48000, 6, format='AIFF') as f: pass with sf.SoundFile(filename_new) as f: if isinstance(file_stereo_rplus, str): assert f.samplerate == 48000 assert f.channels == 6 assert f.format == 'AIFF' assert f.frames == 0 else: # This doesn't really work for file descriptors and file objects pass class LimitedFile(object): def __init__(self, file, attrs): for attr in attrs: setattr(self, attr, getattr(file, attr)) @pytest.mark.parametrize("readmethod", ['read', 'readinto']) def test_virtual_io_readonly(file_obj_stereo_rplus, readmethod): limitedfile = LimitedFile(file_obj_stereo_rplus, ['seek', 'tell', readmethod]) data, fs = sf.read(limitedfile) assert fs == 44100 assert np.all(data == data_stereo) def test_virtual_io_writeonly(file_obj_w): limitedfile = LimitedFile(file_obj_w, ['seek', 'tell', 'write']) sf.write(limitedfile, [0.5], 48000, format='WAV') data, fs = sf.read(filename_new) assert fs == 48000 assert data == [0.5] VIRTUAL_IO_ATTRS = 'seek', 'tell', 'read', 'write' @pytest.mark.parametrize("missing", VIRTUAL_IO_ATTRS) def test_virtual_io_missing_attr(file_obj_stereo_rplus, missing): attrs = list(VIRTUAL_IO_ATTRS) goodfile = LimitedFile(file_obj_stereo_rplus, attrs) success = sf.SoundFile(goodfile, 'r+') attrs.remove(missing) badfile = LimitedFile(file_obj_stereo_rplus, attrs) with pytest.raises(TypeError) as excinfo: sf.SoundFile(badfile, 'r+') assert "Invalid file" in str(excinfo.value) assert np.all(success.read() == data_stereo) # ----------------------------------------------------------------------------- # Test file metadata # ----------------------------------------------------------------------------- def test_file_content(sf_stereo_r): assert np.all(data_stereo == sf_stereo_r.read()) def test_file_attributes_in_read_mode(sf_stereo_r): if isinstance(sf_stereo_r.name, str): assert sf_stereo_r.name == filename_stereo elif not isinstance(sf_stereo_r.name, int): assert sf_stereo_r.name.name == filename_stereo assert sf_stereo_r.mode == 'r' assert sf_stereo_r.samplerate == 44100 assert sf_stereo_r.channels == 2 assert sf_stereo_r.format == 'WAV' assert sf_stereo_r.subtype == 'FLOAT' assert sf_stereo_r.endian == 'FILE' assert sf_stereo_r.format_info == 'WAV (Microsoft)' assert sf_stereo_r.subtype_info == '32 bit float' assert sf_stereo_r.sections == 1 assert sf_stereo_r.closed is False assert sf_stereo_r.seekable() is True assert sf_stereo_r.frames == len(data_stereo) def test__repr__(sf_stereo_r): assert repr(sf_stereo_r) == ("SoundFile({0.name!r}, mode='r', " "samplerate=44100, channels=2, " "format='WAV', subtype='FLOAT', " "endian='FILE')").format(sf_stereo_r) def test_extra_info(sf_stereo_r): assert 'WAVE_FORMAT_IEEE_FLOAT' in sf_stereo_r.extra_info def test_mode_should_be_in_write_mode(sf_stereo_w): assert sf_stereo_w.mode == 'w' assert sf_stereo_w.frames == 0 def test_mode_should_be_in_readwrite_mode(sf_stereo_rplus): assert sf_stereo_rplus.mode == 'r+' def test_file_truthiness(file_w): with sf.SoundFile(file_w, 'w', 44100, 2, format='WAV') as f: assert f # ----------------------------------------------------------------------------- # Test seek/tell # ----------------------------------------------------------------------------- def test_seek_in_read_mode(sf_stereo_r): assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 0 assert sf_stereo_r.tell() == 0 assert sf_stereo_r.seek(2) == 2 assert sf_stereo_r.tell() == 2 assert sf_stereo_r.seek(2, sf.SEEK_CUR) == 4 assert sf_stereo_r.seek(-2, sf.SEEK_END) == len(data_stereo) - 2 with pytest.raises(RuntimeError): sf_stereo_r.seek(666) with pytest.raises(RuntimeError): sf_stereo_r.seek(-666) def test_seek_in_write_mode(sf_stereo_w): assert sf_stereo_w.seek(0, sf.SEEK_CUR) == 0 assert sf_stereo_w.tell() == 0 assert sf_stereo_w.seek(2) == 2 assert sf_stereo_w.tell() == 2 def test_seek_in_rplus_mode(sf_stereo_rplus): assert sf_stereo_rplus.seek(0, sf.SEEK_CUR) == 0 assert sf_stereo_rplus.tell() == 0 assert sf_stereo_rplus.seek(2) == 2 assert sf_stereo_rplus.seek(0, sf.SEEK_CUR) == 2 assert sf_stereo_rplus.tell() == 2 @pytest.mark.parametrize("use_default", [True, False]) def test_truncate(file_stereo_rplus, use_default): if isinstance(file_stereo_rplus, (str, int)): with sf.SoundFile(file_stereo_rplus, 'r+', closefd=False) as f: if use_default: f.seek(2) f.truncate() else: f.truncate(2) assert f.tell() == 2 assert f.frames == 2 if isinstance(file_stereo_rplus, int): os.lseek(file_stereo_rplus, 0, os.SEEK_SET) data, fs = sf.read(file_stereo_rplus) assert np.all(data == data_stereo[:2]) assert fs == 44100 else: # file objects don't support truncate() with sf.SoundFile(file_stereo_rplus, 'r+', closefd=False) as f: with pytest.raises(RuntimeError) as excinfo: f.truncate() assert "Error truncating" in str(excinfo.value) # ----------------------------------------------------------------------------- # Test read # ----------------------------------------------------------------------------- def test_read_write_only(sf_stereo_w): with pytest.raises(RuntimeError): sf_stereo_w.read(2) def test_read_should_read_data_and_advance_read_pointer(sf_stereo_r): data = sf_stereo_r.read(2) assert np.all(data == data_stereo[:2]) assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 2 def test_read_n_frames_should_return_n_frames(sf_stereo_r): assert len(sf_stereo_r.read(2)) == 2 def test_read_all_frames_should_read_all_remaining_frames(sf_stereo_r): sf_stereo_r.seek(-2, sf.SEEK_END) assert np.all(sf_stereo_r.read() == data_stereo[-2:]) def test_read_over_end_should_return_only_remaining_frames(sf_stereo_r): sf_stereo_r.seek(-2, sf.SEEK_END) assert np.all(sf_stereo_r.read(4) == data_stereo[-2:]) def test_read_over_end_with_fill_should_reaturn_asked_frames(sf_stereo_r): sf_stereo_r.seek(-2, sf.SEEK_END) data = sf_stereo_r.read(4, fill_value=0) assert np.all(data[:2] == data_stereo[-2:]) assert np.all(data[2:] == 0) assert len(data) == 4 def test_read_into_out_over_end_should_return_shorter_data_and_write_into_out( sf_stereo_r): out = np.ones((4, sf_stereo_r.channels), dtype='float64') sf_stereo_r.seek(-2, sf.SEEK_END) data = sf_stereo_r.read(out=out) assert np.all(data[:2] == out[:2]) assert np.all(data[2:] == 1) assert out.shape == (4, sf_stereo_r.channels) assert data.shape == (2, sf_stereo_r.channels) def test_read_into_out_over_end_with_fill_should_return_full_data_and_write_into_out( sf_stereo_r): out = np.ones((4, sf_stereo_r.channels), dtype='float64') sf_stereo_r.seek(-2, sf.SEEK_END) data = sf_stereo_r.read(out=out, fill_value=0) assert np.all(data == out) assert np.all(data[2:] == 0) assert out.shape == (4, sf_stereo_r.channels) # ----------------------------------------------------------------------------- # Test buffer read # ----------------------------------------------------------------------------- def test_buffer_read(sf_stereo_r): buf = sf_stereo_r.buffer_read(2, dtype='float64') assert len(buf) == 2 * 2 * 8 assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 2 data = np.frombuffer(buf, dtype='float64').reshape(-1, 2) assert np.all(data == data_stereo[:2]) buf = sf_stereo_r.buffer_read(dtype='float32') assert len(buf) == 2 * 2 * 4 assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 4 data = np.frombuffer(buf, dtype='float32').reshape(-1, 2) assert np.all(data == data_stereo[2:]) buf = sf_stereo_r.buffer_read(dtype='float32') assert len(buf) == 0 buf = sf_stereo_r.buffer_read(666, dtype='float32') assert len(buf) == 0 with pytest.raises(ValueError) as excinfo: sf_stereo_r.buffer_read(dtype='int8') assert "dtype must be one of" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: sf_stereo_r.buffer_read() assert "dtype must be one of" in str(excinfo.value) @xfail_from_buffer def test_buffer_read_into(sf_stereo_r): out = np.ones((3, 2)) frames = sf_stereo_r.buffer_read_into(out, dtype='float64') assert frames == 3 assert np.all(out == data_stereo[:3]) assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 3 frames = sf_stereo_r.buffer_read_into(out, dtype='float64') assert frames == 1 assert np.all(out[:1] == data_stereo[3:]) assert sf_stereo_r.seek(0, sf.SEEK_CUR) == 4 # ----------------------------------------------------------------------------- # Test write # ----------------------------------------------------------------------------- def test_write_to_read_only_file_should_fail(sf_stereo_r): with pytest.raises(RuntimeError): sf_stereo_r.write(data_stereo) def test_if_write_advances_write_pointer(sf_stereo_w): position = sf_stereo_w.seek(0, sf.SEEK_CUR) sf_stereo_w.write(data_stereo) assert sf_stereo_w.seek(0, sf.SEEK_CUR) == position + len(data_stereo) def test_write_flush_should_write_to_disk(sf_stereo_w): sf_stereo_w.flush() size = os.path.getsize(filename_new) sf_stereo_w.write(data_stereo) sf_stereo_w.flush() assert os.path.getsize(filename_new) == size + data_stereo.size * 2 def test_wplus_read_written_data(sf_stereo_wplus): sf_stereo_wplus.write(data_stereo) assert sf_stereo_wplus.seek(0, sf.SEEK_CUR) == len(data_stereo) sf_stereo_wplus.seek(0) assert np.all(sf_stereo_wplus.read() == data_stereo) assert sf_stereo_wplus.seek(0, sf.SEEK_CUR) == len(data_stereo) sf_stereo_wplus.close() data, fs = sf.read(filename_new) assert np.all(data == data_stereo) def test_rplus_append_data(sf_stereo_rplus): sf_stereo_rplus.seek(0, sf.SEEK_END) sf_stereo_rplus.write(data_stereo / 2) sf_stereo_rplus.close() data, fs = sf.read(filename_new) assert np.all(data[:len(data_stereo)] == data_stereo) assert np.all(data[len(data_stereo):] == data_stereo / 2) # ----------------------------------------------------------------------------- # Test buffer write # ----------------------------------------------------------------------------- @xfail_from_buffer def test_buffer_write(sf_stereo_w): buf = np.array([[1, 2], [-1, -2]], dtype='int16') sf_stereo_w.buffer_write(buf, dtype='int16') sf_stereo_w.close() data, fs = sf.read(filename_new, dtype='int16') assert np.all(data == buf) assert fs == 44100 def test_buffer_write_with_bytes(sf_stereo_w): b = b"\x01\x00\xFF\xFF\xFF\x00\x00\xFF" sf_stereo_w.buffer_write(b, dtype='int16') sf_stereo_w.close() data, fs = sf.read(filename_new, dtype='int16') assert np.all(data == [[1, -1], [255, -256]]) assert fs == 44100 @xfail_from_buffer def test_buffer_write_with_wrong_size(sf_stereo_w): buf = np.array([1, 2, 3], dtype='int16') with pytest.raises(ValueError) as excinfo: sf_stereo_w.buffer_write(buf, dtype='int16') assert "multiple of frame size" in str(excinfo.value) # ----------------------------------------------------------------------------- # Other tests # ----------------------------------------------------------------------------- def test_context_manager_should_open_and_close_file(file_stereo_r): with sf.SoundFile(file_stereo_r) as f: assert not f.closed assert f.closed def test_closing_should_close_file(file_stereo_r): f = sf.SoundFile(file_stereo_r) assert not f.closed f.close() assert f.closed def test_anything_on_closed_file(file_stereo_r): with sf.SoundFile(file_stereo_r) as f: pass with pytest.raises(RuntimeError) as excinfo: f.seek(0) assert "closed" in str(excinfo.value) def test_file_attributes_should_save_to_disk(file_w): with sf.SoundFile(file_w, 'w', 44100, 2, format='WAV') as f: f.title = 'testing' with sf.SoundFile(filename_new) as f: assert f.title == 'testing' def test_non_file_attributes_should_not_save_to_disk(file_w): with sf.SoundFile(file_w, 'w', 44100, 2, format='WAV') as f: f.foobar = 'testing' with sf.SoundFile(filename_new) as f: with pytest.raises(AttributeError): f.foobar def test_getAttributeNames(sf_stereo_r): names = sf_stereo_r._getAttributeNames() assert 'artist' in names assert 'genre' in names def test_read_int_data_from_float_file(file_inmemory): """This is a very uncommon use case.""" unnormalized_float_to_clipped_int16 = [ (-2.0**15 - 1 , -2**15), (-2.0**15 , -2**15), (-2.0**15 + 1 , -2**15 + 1), (-1.0 , -1), (-0.51 , -1), (-0.5 , 0), ( 0.0 , 0), ( 0.5 , 0), ( 0.51 , 1), ( 1.0 , 1), ( 2.0**15 - 2 , 2**15 - 2), ( 2.0**15 - 1 , 2**15 - 1), ( 2.0**15 , 2**15 - 1), ] file_data, expected = zip(*unnormalized_float_to_clipped_int16) sf.write(file_inmemory, file_data, 44100, format='WAV', subtype='FLOAT') file_inmemory.seek(0) read, fs = sf.read(file_inmemory, always_2d=False, dtype='int16') assert np.all(read == expected) assert fs == 44100 def test_libsndfile_version(): assert '.' in sf.__libsndfile_version__ # ----------------------------------------------------------------------------- # RAW tests # ----------------------------------------------------------------------------- def test_read_raw_files_should_read_data(): with sf.SoundFile(filename_raw, 'r', 44100, 1, 'PCM_16') as f: assert np.all(f.read(dtype='int16') == data_mono) def test_read_raw_files_with_too_few_arguments_should_fail(): with pytest.raises(TypeError): # missing everything sf.SoundFile(filename_raw) with pytest.raises(TypeError): # missing subtype sf.SoundFile(filename_raw, samplerate=44100, channels=2) with pytest.raises(TypeError): # missing channels sf.SoundFile(filename_raw, samplerate=44100, subtype='PCM_16') with pytest.raises(TypeError): # missing samplerate sf.SoundFile(filename_raw, channels=2, subtype='PCM_16') def test_available_formats(): formats = sf.available_formats() assert 'WAV' in formats assert 'OGG' in formats assert 'FLAC' in formats def test_available_subtypes(): subtypes = sf.available_subtypes() assert 'PCM_24' in subtypes assert 'FLOAT' in subtypes assert 'VORBIS' in subtypes subtypes = sf.available_subtypes('WAV') assert 'PCM_24' in subtypes assert 'FLOAT' in subtypes assert 'VORBIS' not in subtypes subtypes = sf.available_subtypes('nonsense') assert subtypes == {} def test_default_subtype(): assert sf.default_subtype('FLAC') == 'PCM_16' assert sf.default_subtype('RAW') is None with pytest.raises(ValueError) as excinfo: sf.default_subtype('nonsense') assert str(excinfo.value) == "Unknown format: 'nonsense'" with pytest.raises(TypeError) as excinfo: sf.default_subtype(666) assert str(excinfo.value) == "Invalid format: 666" # ----------------------------------------------------------------------------- # Test non-seekable files # ----------------------------------------------------------------------------- def test_write_non_seekable_file(file_w): with sf.SoundFile(file_w, 'w', 44100, 1, format='XI') as f: assert not f.seekable() assert f.frames == 0 f.write(data_mono) assert f.frames == len(data_mono) with pytest.raises(RuntimeError) as excinfo: f.seek(2) assert "unseekable" in str(excinfo.value) with sf.SoundFile(filename_new) as f: assert not f.seekable() assert f.frames == len(data_mono) data = f.read(3, dtype='int16') assert np.all(data == data_mono[:3]) data = f.read(666, dtype='int16') assert np.all(data == data_mono[3:]) with pytest.raises(RuntimeError) as excinfo: f.seek(2) assert "unseekable" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: f.read() assert "frames" in str(excinfo.value) data, fs = sf.read(filename_new, dtype='int16') assert np.all(data == data_mono) assert fs == 44100 with pytest.raises(ValueError) as excinfo: sf.read(filename_new, start=3) assert "start is only allowed for seekable files" in str(excinfo.value)