diff --git a/README.md b/README.md deleted file mode 100644 index dc1f7ee..0000000 --- a/README.md +++ /dev/null @@ -1,147 +0,0 @@ -PySoundFile -=========== - -PySoundFile is an audio library based on libsndfile, CFFI and Numpy - -PySoundFile can read and write sound files. File reading/writing is -supported through [libsndfile][], which is a free, cross-platform, -open-source 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 foreight -function interface for Python calling C code. CFFI is supported for -CPython 2.6+, 3.x and PyPy 2.0+. PySoundFile represents audio data as -NumPy arrays. - -[libsndfile]: http://www.mega-nerd.com/libsndfile/ -[CFFI]: http://cffi.readthedocs.org/ - -PySoundFile is BSD licensed. -(c) 2013, Bastian Bechtold - -Installation ------------- - -On the Python side, you need to have CFFI and Numpy in order to use -PySoundFile. Additionally, You need the library libsndfile installed on -your computer. On Unix, use your package manager to install libsndfile. -Then just install PySoundFile using pip or `python setup.py install`. - -If you are running Windows, I recommend using [WinPython][] or some -similar distribution. This should set you up with Numpy. However, you -also need CFFI and it's dependency, PyCParser. A good place to get -these are the [Unofficial Windows Binaries for Python][pybuilds]. -Having installed those, you can download the Windows installers for -PySoundFile: - -[PySoundFile-0.5.0.win-amd64-py2.7](https://github.com/bastibe/PySoundFile/releases/download/0.5.0/PySoundFile-0.5.0.win-amd64-py2.7.exe) -[PySoundFile-0.5.0.win-amd64-py3.3](https://github.com/bastibe/PySoundFile/releases/download/0.5.0/PySoundFile-0.5.0.win-amd64-py3.3.exe) -[PySoundFile-0.5.0.win32-py2.7](https://github.com/bastibe/PySoundFile/releases/download/0.5.0/PySoundFile-0.5.0.win32-py2.7.exe) -[PySoundFile-0.5.0.win32-py3.3](https://github.com/bastibe/PySoundFile/releases/download/0.5.0/PySoundFile-0.5.0.win32-py3.3.exe) - -[WinPython]: https://code.google.com/p/winpython/ -[pybuilds]: http://www.lfd.uci.edu/~gohlke/pythonlibs/ - -Usage ------ - -Each SoundFile can either open a sound file on the disk, or a -file-like object (using `libsndfile`'s [virtual file interface][vio]). -Every sound file has a specific samplerate, data format and a set -number of channels. Each sound file can be opened in `read_mode`, -`write_mode`, or `read_write_mode`. Note that `read_write_mode` is -unsupported for some formats. - -You can read and write any file that [`libsndfile`][formats] can open. -This includes Microsoft WAV, OGG, FLAC and Matlab MAT files. Different -variants of these can be built by ORing `snd_types`, `snd_subtypes` -and `snd_endians` or using the predefined formats `wave_file`, -`flac_file`, `matlab_file` and `ogg_file`. Note that specifying the -format is only necessary when writing. - -If a file on disk is opened, it is kept open for as long as the -SoundFile object exists and closes automatically when it goes out of -scope. Alternatively, the SoundFile object can be used as a context -manager, which closes the file when it exits. - -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. - -[vio]: http://www.mega-nerd.com/libsndfile/api.html#open_virtual -[formats]: http://www.mega-nerd.com/libsndfile/#Features - -### Read/Write Functions - -Data can be written to the file using `write()`, or read from the -file using `read()`. Every read and write operation starts at a -certain position in the file. Reading N frames will change this -position by N frames as well. Alternatively, `seek()`, and -`seek_absolute()`, can be used to set the current position to a -frame index offset from the current position, the start of the file, -or the end of the file, respectively. - -Here is an example for a program that reads a wave file and copies it -into an ogg-vorbis file: - -```python -from pysoundfile import SoundFile - -wave = SoundFile('existing_file.wav') -ogg = SoundFile('new_file.ogg', sample_rate=wave.sample_rate, - channels=wave.channels, format=ogg_file, - mode=write_mode) - -data = wave.read(1024) -while len(data) > 0: - ogg.write(data) - data = wave.read(1024) -``` - -### Sequence Interface - -Alternatively, slices can be used to access data at arbitrary -positions in the file. If you index in two dimensions, you can select -single channels of a multi-channel file. - -Here is an example of reading in a whole wave file into a NumPy array: - -```python -from pysoundfile import SoundFile - -wave = SoundFile('existing_file.wav')[:] -``` - -### Virtual IO - -If you have an open file-like object, you can use something -similar to this to decode it: - -```python -from io import BytesIO -from pysoundfile import SoundFile -fObj = BytesIO(open('filename.flac', 'rb').read()) -flac = SoundFile(fObj, virtual_io=True) -``` - -Here is an example using an HTTP request: -```python -from io import BytesIO -from pysoundfile import SoundFile -import requests - -fObj = BytesIO() -response = requests.get('http://www.example.com/my.flac', stream=True) -for data in response.iter_content(4096): - if data: - fObj.write(data) -fObj.seek(0) -flac = SoundFile(fObj, virtual_io=True) -``` - -### Accessing Text Data - -In addition to audio data, there are a number of text fields in every -sound file. In particular, you can set a title, a copyright notice, a -software description, the artist name, a comment, a date, the album -name, a license, a tracknumber and a genre. Note however, that not all -of these fields are supported for every file format. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bc9eaeb --- /dev/null +++ b/README.rst @@ -0,0 +1,112 @@ +PySoundFile +=========== + +PySoundFile is an audio library based on libsndfile, CFFI and Numpy + +PySoundFile can read and write sound files. File reading/writing is +supported through `libsndfile `__, +which is a free, cross-platform, open-source 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+. PySoundFile represents audio data as NumPy arrays. + +| PySoundFile is BSD licensed. +| (c) 2013, Bastian Bechtold + +Installation +------------ + +On the Python side, you need to have CFFI and Numpy in order to use +PySoundFile. Additionally, You need the library libsndfile installed on +your computer. On Unix, use your package manager to install libsndfile. +Then just install PySoundFile using pip or ``python setup.py install``. + +If you are running Windows, I recommend using +`WinPython `__ or some similar +distribution. This should set you up with Numpy. However, you also need +CFFI and it's dependency, PyCParser. A good place to get these are the +`Unofficial Windows Binaries for +Python `__. Having installed +those, you can download the Windows installers for PySoundFile: + +| `PySoundFile-0.5.0.win-amd64-py2.7 `__ +| `PySoundFile-0.5.0.win-amd64-py3.3 `__ +| `PySoundFile-0.5.0.win32-py2.7 `__ +| `PySoundFile-0.5.0.win32-py3.3 `__ + +Usage +----- + +Each SoundFile can either open a sound file on the disk, or a file-like +object (using ``libsndfile``'s `virtual file +interface `__). +Every sound file has a specific samplerate, data format and a set number +of channels. + +You can read and write any file that +`libsndfile `__ can +open. This includes Microsoft WAV, OGG, FLAC and Matlab MAT files. + +If a file on disk is opened, it is kept open for as long as the +SoundFile object exists and closes automatically when it goes out of +scope. Alternatively, the SoundFile object can be used as a context +manager, which closes the file when it exits. + +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. + +Read/Write Functions +~~~~~~~~~~~~~~~~~~~~ + +Data can be written to the file using ``write()``, or read from the +file using ``read()``. + +Here is an example for a program that reads a wave file and copies it +into an ogg-vorbis file: + +.. code:: python + + import pysoundfile as sf + + data, samplerate = sf.read('existing_file.wav') + sf.write(data, 'new_file.ogg', samplerate=samplerate) + +Virtual IO +~~~~~~~~~~ + +If you have an open file-like object, you can use something similar to +this to decode it: + +.. code:: python + + from pysoundfile import SoundFile + with SoundFile('filename.flac', 'rb') as fObj: + data, samplerate = sf.read(fObj) + +Here is an example using an HTTP request: + +.. code:: python + + from io import BytesIO + import pysoundfile as sf + import requests + + fObj = BytesIO() + response = requests.get('http://www.example.com/my.flac', stream=True) + for data in response.iter_content(4096): + if data: + fObj.write(data) + fObj.seek(0) + data, samplerate = sf.read(fObj) + +Accessing Text Data +~~~~~~~~~~~~~~~~~~~ + +In addition to audio data, there are a number of text fields in every +sound file. In particular, you can set a title, a copyright notice, a +software description, the artist name, a comment, a date, the album +name, a license, a tracknumber and a genre. Note however, that not all +of these fields are supported for every file format. diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..0ed7dd4 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,177 @@ +# 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." diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..3dfe0e2 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,277 @@ +#!/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 + +# 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.0' + +# 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.mathjax', + 'sphinx.ext.viewcode', + 'sphinxcontrib.napoleon', +] + +# 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 = '2014, 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.5.0' +# The full version, including alpha/beta/rc tags. +release = '0.5.0' + +# 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 = 'default' + +# 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', 'manual'), +] + +# 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_cffi +sys.modules['cffi'] = sys.modules['fake_cffi'] diff --git a/doc/fake_cffi.py b/doc/fake_cffi.py new file mode 100644 index 0000000..66ed279 --- /dev/null +++ b/doc/fake_cffi.py @@ -0,0 +1,11 @@ +"""Mock module for Sphinx autodoc.""" + + +class FFI(object): + def cdef(self, _): + pass + + def dlopen(self, _): + return self + + SFC_GET_FORMAT_INFO = NotImplemented diff --git a/doc/fake_numpy.py b/doc/fake_numpy.py new file mode 100644 index 0000000..9279494 --- /dev/null +++ b/doc/fake_numpy.py @@ -0,0 +1,5 @@ +"""Mock module for Sphinx autodoc.""" + + +def dtype(_): + return NotImplemented diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..872f87f --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,13 @@ +.. include:: ../README.rst + +API Documentation +================= + +.. automodule:: pysoundfile + :members: + :undoc-members: + +Index +===== + +* :ref:`genindex` diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..e1b3da6 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,4 @@ +jinja2 +pygments +sphinx +sphinxcontrib-napoleon diff --git a/pysoundfile.py b/pysoundfile.py index 028d5da..949e59e 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -1,3 +1,15 @@ +"""PySoundFile 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.rtfd.org/. + +""" +__version__ = "0.5.0" + import numpy as _np from cffi import FFI as _FFI from os import SEEK_SET, SEEK_CUR, SEEK_END @@ -7,46 +19,6 @@ except ImportError: import __builtin__ as _builtins # for Python < 3.0 -__version__ = "0.5.0" - -"""PySoundFile is an audio library based on libsndfile, CFFI and Numpy - -PySoundFile can read and write sound files. File reading/writing is -supported through libsndfile[1], which is a free, cross-platform, -open-source 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[2], which is a foreight -function interface for Python calling C code. CFFI is supported for -CPython 2.6+, 3.x and PyPy 2.0+. PySoundFile represents audio data as -NumPy arrays. - -[1]: http://www.mega-nerd.com/libsndfile/ -[2]: http://cffi.readthedocs.org/ - -Every sound file is represented as a SoundFile object. SoundFiles can -be created for reading, writing, or both. Each SoundFile has a -samplerate, a number of channels, and a file format. These can not be -changed at runtime. - -A SoundFile has methods for reading and writing data to/from the file. -Even though every sound file has a fixed file format, reading and -writing is possible in four different NumPy formats: int16, int32, -float32 and float64. - -At the same time, SoundFiles act as sequence types, so you can use -slices to read or write data as well. Since there is no way of -specifying data formats for slices, the SoundFile will always return -float64 data for those. - -Note that you need to have libsndfile installed in order to use -PySoundFile. On Windows, you need to rename the library to -"sndfile.dll". - -PySoundFile is BSD licensed. -(c) 2013, Bastian Bechtold - -""" - _ffi = _FFI() _ffi.cdef(""" enum @@ -272,38 +244,296 @@ _snd = _ffi.dlopen('sndfile') +def read(file, frames=-1, start=0, stop=None, dtype='float64', always_2d=True, + 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'``. + + Returns + ------- + audiodata : numpy.ndarray or type(out) + A two-dimensional NumPy array is returned, where the channels + are stored along the first dimension, i.e. as columns. + A two-dimensional array is returned even if the sound file has + only one channel. Use ``always_2d=False`` to return a + one-dimensional array in this case. + + 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, audio data is always returned as a two-dimensional + array, even if the audio file has only one channel. + With ``always_2d=False``, reading a mono sound file will return + a one-dimensional array. + 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 pysoundfile 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 + + """ + if frames >= 0 and stop is not None: + raise TypeError("Only one of {frames, stop} may be used") + + with SoundFile(file, 'r', samplerate, channels, + subtype, endian, format, closefd) as f: + start, frames = _get_read_range(frames, start, stop, f.frames) + f.seek(start, SEEK_SET) + data = f.read(frames, dtype, always_2d, fill_value, out) + return data, f.samplerate + + +def write(data, file, 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 + ---------- + data : array_like + The data to write. Usually two-dimensional (channels x frames), + 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`. + + file : str or int or file-like object + The file to write to. See :class:`SoundFile` for details. + 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 file: + + >>> import numpy as np + >>> import pysoundfile as sf + >>> sf.write(np.random.randn(10, 2), 'stereo_file.wav', 44100, 'PCM_24') + + """ + 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=True, 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 pysoundfile as sf + >>> for block in sf.blocks('stereo_file.wav', blocksize=1024): + >>> pass # do something with 'block' + + """ + if frames >= 0 and stop is not None: + raise TypeError("Only one of {frames, stop} may be used") + + with SoundFile(file, 'r', samplerate, channels, + subtype, endian, format, closefd) as f: + start, frames = _get_read_range(frames, start, stop, f.frames) + f.seek(start, SEEK_SET) + for block in f.blocks(blocksize, overlap, frames, + dtype, always_2d, fill_value, out): + yield block + + +def available_formats(): + """Return a dictionary of available major formats. + + Examples + -------- + >>> import pysoundfile 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 pysoundfile 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 format_check(format, subtype)) + + +def format_check(format, subtype=None, endian=None): + """Check if the combination of format/subtype/endian is valid. + + Examples + -------- + >>> import pysoundfile as sf + >>> sf.format_check('WAV', 'PCM_24') + True + >>> sf.format_check('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 pysoundfile as sf + >>> sf.default_subtype('WAV') + 'PCM_16' + >>> sf.default_subtype('MAT5') + 'DOUBLE' + + """ + return _default_subtypes.get(str(format).upper()) + + class SoundFile(object): - """SoundFile handles reading and writing to sound files. - - Each SoundFile opens one sound file on the disk. This sound file - has a specific samplerate, data format and a set number of - channels. Each sound file can be opened for reading, for writing or - both. Note that the latter is unsupported for some formats. - - Data can be written to the file using write(), or read from the - file using read(). Every read and write operation starts at a - certain position in the file. Reading N frames will change this - position by N frames as well. Alternatively, seek() - can be used to set the current position to a frame - index offset from the current position, the start of the file, or - the end of the file, respectively. - - Alternatively, slices can be used to access data at arbitrary - positions in the file. Note that slices currently only work on - frame indices, not channels. The quickest way to read in a whole - file as a float64 NumPy array is in fact SoundFile('filename')[:]. - - 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. - - In addition to audio data, there are a number of text fields in - every sound file. In particular, you can set a title, a copyright - notice, a software description, the artist name, a comment, a - date, the album name, a license, a tracknumber and a genre. Note - however, that not all of these fields are supported for every file - format. + """A sound file. + + For more documentation see the __init__() docstring (which is also + used for the online documentation (http://pysoundfile.rtfd.org/). """ @@ -311,25 +541,94 @@ 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 samplerate, channels or file format need to be given. If a - file is opened with another mode, you must provide a samplerate, - a number of channels, and a file format. An exception is the - RAW data format, which requires these data points for reading - as well. + 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": 'FILE' (default), 'LITTLE', 'BIG' or 'CPU'. - In most cases this doesn't have to be specified. - - The functions available_formats() and available_subtypes() can - be used to obtain a list of all avaliable major formats and - subtypes, respectively. + + * 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) or + ``'x'`` for writing (but fail if already existing). + Additionally, it may contain ``'+'`` to open a 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. + + .. note:: The modes containing ``'x'`` are only available + since Python 3.3! + + 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 pysoundfile 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 """ if mode is None: @@ -393,91 +692,45 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, self._handle_error() if modes.issuperset('r+') and self.seekable(): - # Move write pointer to 0 (like in Python file objects) + # Move write position to 0 (like in Python file objects) self.seek(0) 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.""" frames = property(lambda self: self._info.frames) + """The number of frames in the sound file.""" samplerate = property(lambda self: self._info.samplerate) + """The sample rate of 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.""" # avoid confusion if something goes wrong before assigning self._file: _file = None _filestream = None - def seekable(self): - """Return True if the file supports seeking.""" - return self._info.seekable == _snd.SF_TRUE - - def _init_virtual_io(self, file): - @_ffi.callback("sf_vio_get_filelen") - def vio_get_filelen(user_data): - # first try __len__(), if not available fall back to seek()/tell() - try: - size = len(file) - except TypeError: - 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 __del__(self): self.close() @@ -487,31 +740,8 @@ def __enter__(self): def __exit__(self, *args): self.close() - def _handle_error(self): - # this checks the error flag of the SNDFILE* structure - self._check_if_closed() - err = _snd.sf_error(self._file) - self._handle_error_number(err) - - def _handle_error_number(self, err): - # pretty-print a numerical error code - if err != 0: - err_str = _snd.sf_error_number(err) - raise RuntimeError(_ffi.string(err_str).decode()) - - def _getAttributeNames(self): - # return all possible 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 tries to access self._file. - if self.closed: - raise ValueError("I/O operation on closed file") - def __setattr__(self, name, value): - # access text data in the sound file through properties + """Write text meta-data in the sound file through properties.""" if name in _str_types: self._check_if_closed() data = _ffi.new('char[]', value.encode()) @@ -521,7 +751,7 @@ def __setattr__(self, name, value): super(SoundFile, self).__setattr__(name, value) def __getattr__(self, name): - # access text data in the sound file through properties + """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]) @@ -532,17 +762,6 @@ def __getattr__(self, name): def __len__(self): return self.frames - def _get_slice_bounds(self, frame): - # get start and stop index from slice, asserting step==1 - if not isinstance(frame, slice): - frame = slice(frame, frame + 1) - start, stop, step = frame.indices(len(self)) - if step != 1: - raise RuntimeError("Step size must be 1") - if start > stop: - stop = start - return start, stop - def __getitem__(self, frame): # access the file as if it where a Numpy array. The data is # returned as numpy array. @@ -577,108 +796,92 @@ def __setitem__(self, frame, data): self.seek(curr, SEEK_SET) return data - def flush(self): - """Write unwritten data to disk.""" - 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 - if self._filestream: - self._filestream.close() - self._handle_error_number(err) + 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 and/or write position. - - By default (whence=SEEK_SET), frames are counted from the - beginning of the file. SEEK_CUR seeks from the current position - (positive and negative values are allowed). - SEEK_END seeks from the end (use negative values). + """Set the read/write position. - If the file is opened in 'rw' mode, both read and write position - are set to the same value by default. - Use which='r' or which='w' to set only the read position or the - write position, respectively. + 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`). - To set the read/write position to the beginning of the file, - use seek(0), to set it to right after the last frame, - e.g. for appending new data, use seek(0, SEEK_END). + Returns + ------- + int + The new absolute read/write position in frames, or a + negative value on error. - Returns the new absolute read/write position in frames or a - negative value on error. + Examples + -------- + >>> from pysoundfile import SoundFile, SEEK_END + >>> myfile = SoundFile('stereo_file.wav') - """ - self._check_if_closed() - return _snd.sf_seek(self._file, frames, whence) + Seek to the beginning of the file: - def _check_array(self, array): - # Do some error checking - 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: %s" % repr(array.shape)) + >>> myfile.seek(0) + 0 - if array.dtype not in _ffi_types: - raise ValueError("dtype must be one of %s" % - repr([dt.name for dt in _ffi_types])) + Seek to the end of the file: - def _create_empty_array(self, frames, always_2d, dtype): - # Create an empty array with appropriate shape - if always_2d or self.channels > 1: - shape = frames, self.channels - else: - shape = frames, - return _np.empty(shape, dtype, order='C') + >>> myfile.seek(0, SEEK_END) + 44100 # this is the file length - def _read_or_write(self, funcname, array, frames): - # Call into libsndfile + """ self._check_if_closed() - - ffi_type = _ffi_types[array.dtype] - assert array.flags.c_contiguous - assert array.dtype.itemsize == _ffi.sizeof(ffi_type) - assert array.size >= frames * self.channels - - if self.seekable(): - curr = self.seek(0, SEEK_CUR) - func = getattr(_snd, funcname + ffi_type) - ptr = _ffi.cast(ffi_type + '*', array.ctypes.data) - frames = func(self._file, ptr, frames) - self._handle_error() - if self.seekable(): - self.seek(curr + frames, SEEK_SET) # Update read & write position - return frames + return _snd.sf_seek(self._file, frames, whence) def read(self, frames=-1, dtype='float64', always_2d=True, fill_value=None, out=None): - """Read a number of frames from the file. - - Reads the given number of frames in the given data format from - the current read position. This also advances the read - position by the same number of frames. - Use frames=-1 to read until the end of the file. - - A two-dimensional NumPy array is returned, where the channels - are stored along the first dimension, i.e. as columns. - A two-dimensional array is returned even if the sound file has - only one channel. Use always_2d=False to return a - one-dimensional array in this case. - - If out is specified, the data is written into the given NumPy - array. In this case, the arguments dtype and always_2d are - silently ignored! - - If there is less data left in the file than requested, the rest - of the frames are filled with fill_value. If fill_value=None, a - smaller array is returned. - If out is given, only a part of it is overwritten and a view - containing all valid frames is returned. + """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 + See :func:`read`. + + Returns + ------- + numpy.ndarray or type(out) + The read data; either a new array or `out` or a view into + `out`. See :func:`read` for details. + + Other Parameters + ---------------- + always_2d, fill_value, out + See :func:`read`. + + Examples + -------- + >>> from pysoundfile 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() """ if out is None: @@ -705,14 +908,16 @@ def read(self, frames=-1, dtype='float64', always_2d=True, return out def write(self, data): - """Write a number of frames to the file. + """Write audio data to the file. - Writes a number of frames to the current write position in the - file. This also advances the write position by the same number - of frames and enlarges the file if necessary. + 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. - The data must be provided as a (frames x channels) NumPy - array or as one-dimensional array for mono signals. + Parameters + ---------- + data : array_like + See :func:`write`. """ # no copy is made if data has already the correct memory layout: @@ -728,20 +933,34 @@ def write(self, data): def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', always_2d=True, fill_value=None, out=None): - """Return a generator for block-wise processing. - - By default, the generator returns blocks of the given blocksize - until the end of the file is reached, frames can be used to - stop earlier. - - overlap can be used to rewind a certain number of frames between - blocks. - - For the arguments dtype, always_2d, fill_value and out see - SoundFile.read(). - - If fill_value is not specified, the last block may be smaller - than blocksize. + """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 < 1``, the file is read until the end. + dtype : {'float64', 'float32', 'int32', 'int16'}, optional + See :func:`read`. + + Yields + ------ + numpy.ndarray or type(out) + Blocks of audio data. See :func:`blocks` for details. + + Other Parameters + ---------------- + always_2d, fill_value, out + See :func:`read`. """ if 'r' not in self.mode and '+' not in self.mode: @@ -772,124 +991,167 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', frames += overlap yield block + def flush(self): + """Write unwritten data to the file system. -def open(file, mode='r', samplerate=None, channels=None, - subtype=None, endian=None, format=None, closefd=True): - return SoundFile(file, mode, samplerate, channels, - subtype, endian, format, closefd) - -open.__doc__ = SoundFile.__init__.__doc__ - + 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. -def read(file, samplerate=None, channels=None, subtype=None, endian=None, - format=None, closefd=True, start=0, stop=None, frames=-1, - dtype='float64', always_2d=True, fill_value=None, out=None): - """Read a sound file and return its contents as NumPy array. + This has no effect on files opened in read-only mode. - The number of frames to read can be specified with frames, the - position to start reading can be specified with start. - By default, the whole file is read from the beginning. - Alternatively, a range can be specified with start and stop. - Both start and stop accept negative indices to specify positions - relative to the end of the file. + """ + self._check_if_closed() + _snd.sf_write_sync(self._file) - A two-dimensional NumPy array is returned, where the channels are - stored along the first dimension, i.e. as columns. - A two-dimensional array is returned even if the sound file has only - one channel. - Use always_2d=False to return a one-dimensional array in this case. + 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 + if self._filestream: + self._filestream.close() + self._handle_error_number(err) - If out is specified, the data is written into the given NumPy array. - In this case, the arguments frames, dtype and always_2d are silently - ignored! + 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): + # first try __len__(), if not available fall back to seek()/tell() + try: + size = len(file) + except TypeError: + curr = file.tell() + file.seek(0, SEEK_END) + size = file.tell() + file.seek(curr, SEEK_SET) + return size - If there is less data left in the file than requested, the rest of - the frames are filled with fill_value. If fill_value=None, a smaller - array is returned. - If out is given, only a part of it is overwritten and a view - containing all valid frames is returned. + @_ffi.callback("sf_vio_seek") + def vio_seek(offset, whence, user_data): + file.seek(offset, whence) + return file.tell() - The keyword arguments samplerate, channels, format, subtype and - endian are only needed for 'RAW' files. See open() for details. + @_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 - """ - if frames >= 0 and stop is not None: - raise TypeError("Only one of {frames, stop} may be used") + @_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 - with SoundFile(file, 'r', samplerate, channels, - subtype, endian, format, closefd) as f: - start, frames = _get_read_range(start, stop, frames, f.frames) - f.seek(start, SEEK_SET) - data = f.read(frames, dtype, always_2d, fill_value, out) - return data, f.samplerate + @_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} -def write(data, file, samplerate, - subtype=None, endian=None, format=None, closefd=True): - """Write data from a NumPy array into a sound file. + return _ffi.new("SF_VIRTUAL_IO*", self._virtual_io) - If file exists, it will be overwritten! + def _handle_error(self): + """Check the error flag of the SNDFILE* structure.""" + self._check_if_closed() + err = _snd.sf_error(self._file) + self._handle_error_number(err) - If data is one-dimensional, a mono file is written. - For two-dimensional data, the columns are interpreted as channels. + def _handle_error_number(self, err): + """Pretty-print a numerical error code.""" + if err != 0: + err_str = _snd.sf_error_number(err) + raise RuntimeError(_ffi.string(err_str).decode()) - All further arguments are forwarded to open(). + def _getAttributeNames(self): + """Return all attributes used in __setattr__ and __getattr__. - Example usage: + This is useful for auto-completion (e.g. IPython). - import pysoundfile as sf - sf.write(myarray, 'myfile.wav', 44100, 'PCM_24') + """ + return _str_types - """ - data = _np.asarray(data) - if data.ndim == 1: - channels = 1 - else: - channels = data.shape[1] - with open(file, 'w', samplerate, channels, - subtype, endian, format, closefd) as f: - f.write(data) + 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. -def blocks(file, samplerate=None, channels=None, - subtype=None, endian=None, format=None, closefd=True, - blocksize=None, overlap=0, start=0, stop=None, frames=-1, - dtype='float64', always_2d=True, fill_value=None, out=None): - """Return a generator for block-wise processing. + """ + if self.closed: + raise ValueError("I/O operation on closed file") - Example usage: + def _get_slice_bounds(self, frame): + # get start and stop index from slice, asserting step==1 + if not isinstance(frame, slice): + frame = slice(frame, frame + 1) + start, stop, step = frame.indices(len(self)) + if step != 1: + raise RuntimeError("Step size must be 1") + if start > stop: + stop = start + return start, stop - import pysoundfile as sf - for block in sf.blocks('myfile.wav', blocksize=128): - print(block.max()) - # ... or do something more useful with 'block' + def _check_array(self, array): + """Do some error checking.""" + 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: %s" % repr(array.shape)) - All keyword arguments of SoundFile.blocks() are allowed. - All further arguments are forwarded to open(). + if array.dtype not in _ffi_types: + raise ValueError("dtype must be one of %s" % + repr([dt.name for dt in _ffi_types])) - By default, iteration stops at the end of the file. Use frames or - stop to stop earlier. + def _create_empty_array(self, frames, always_2d, dtype): + """Create an empty array with appropriate shape.""" + if always_2d or self.channels > 1: + shape = frames, self.channels + else: + shape = frames, + return _np.empty(shape, dtype, order='C') - 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. + def _read_or_write(self, funcname, array, frames): + """Call into libsndfile.""" + self._check_if_closed() - """ - if frames >= 0 and stop is not None: - raise TypeError("Only one of {frames, stop} may be used") + ffi_type = _ffi_types[array.dtype] + assert array.flags.c_contiguous + assert array.dtype.itemsize == _ffi.sizeof(ffi_type) + assert array.size >= frames * self.channels - with open(file, 'r', samplerate, channels, - subtype, endian, format, closefd) as f: - start, frames = _get_read_range(start, stop, frames, f.frames) - f.seek(start, SEEK_SET) - for block in f.blocks(blocksize, overlap, frames, - dtype, always_2d, fill_value, out): - yield block + if self.seekable(): + curr = self.seek(0, SEEK_CUR) + func = getattr(_snd, funcname + ffi_type) + ptr = _ffi.cast(ffi_type + '*', array.ctypes.data) + frames = func(self._file, ptr, frames) + self._handle_error() + if self.seekable(): + self.seek(curr + frames, SEEK_SET) # Update read & write position + return frames -def _get_read_range(start, stop, frames, total_frames): - # Calculate start frame and length +def _get_read_range(frames, start, stop, total_frames): + """Calculate start frame and length.""" start, stop, _ = slice(start, stop).indices(total_frames) if stop < start: stop = start @@ -898,13 +1160,8 @@ def _get_read_range(start, stop, frames, total_frames): return start, frames -def default_subtype(format): - """Return default subtype for given format.""" - return _default_subtypes.get(str(format).upper()) - - def _format_int(format, subtype, endian): - # Return numeric format ID for given format|subtype|endian combo + """Return numeric ID for given format|subtype|endian combo.""" try: result = _formats[str(format).upper()] except KeyError: @@ -933,16 +1190,8 @@ def _format_int(format, subtype, endian): return result -def format_check(format, subtype=None, endian=None): - """Check if the combination of format/subtype/endian is valid.""" - try: - return bool(_format_int(format, subtype, endian)) - except (ValueError, TypeError): - return False - - def _format_str(format_int): - # Return the string representation of a given numeric format + """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: @@ -951,7 +1200,7 @@ def _format_str(format_int): def _format_info(format_int, format_flag=_snd.SFC_GET_FORMAT_INFO): - # Return the ID and short description of a given format. + """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, @@ -962,26 +1211,8 @@ def _format_info(format_int, format_flag=_snd.SFC_GET_FORMAT_INFO): def _available_formats_helper(count_flag, format_flag): - # Generator function used in available_formats() and available_subtypes() + """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 available_formats(): - """Return a dictionary of available major formats.""" - 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. - - If format is specified, only compatible subtypes are returned. - - """ - 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 format_check(format, subtype)) diff --git a/setup.py b/setup.py index cf3d793..d54e48b 100644 --- a/setup.py +++ b/setup.py @@ -40,25 +40,5 @@ 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Multimedia :: Sound/Audio' ], - long_description=''' - PySoundFile can read and write sound files. - - PySoundFile can read and write sound files. File reading/writing is - supported through libsndfile_, which is a free, cross-platform, - open-source 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+. PySoundFile represents audio data as - NumPy arrays. - - You must have libsndfile installed in order to use PySoundFile. - - .. _libsndfile: http://www.mega-nerd.com/libsndfile/ - .. _CFFI: http://cffi.readthedocs.org/ - - Note that you need to have libsndfile installed in order to use - PySoundFile. On Windows, you need to rename the library to - "sndfile.dll". - - ''') + long_description=open('README.rst').read(), +) diff --git a/tests/test_argspec.py b/tests/test_argspec.py index 32b950d..9aa987d 100644 --- a/tests/test_argspec.py +++ b/tests/test_argspec.py @@ -4,7 +4,6 @@ from inspect import getargspec -open = getargspec(sf.open) init = getargspec(sf.SoundFile.__init__) read_function = getargspec(sf.read) read_method = getargspec(sf.SoundFile.read) @@ -26,25 +25,18 @@ def remove_items(collection, subset): return the_rest -def test_if_open_is_identical_to_init(): - assert ['self'] + open.args == init.args - assert open.varargs == init.varargs - assert open.keywords == init.keywords - assert open.defaults == init.defaults - - def test_read_defaults(): func_defaults = defaults(read_function) meth_defaults = defaults(read_method) - open_defaults = defaults(open) + init_defaults = defaults(init) - del open_defaults['mode'] # Not meaningful in read() function: + del init_defaults['mode'] # Not meaningful in read() function: del func_defaults['start'] del func_defaults['stop'] - # Same default values as open() and SoundFile.read(): - for spec in open_defaults, meth_defaults: + # 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 @@ -52,35 +44,34 @@ def test_read_defaults(): def test_write_defaults(): write_defaults = defaults(write_function) - open_defaults = defaults(open) + init_defaults = defaults(init) - # Same default values as open() - open_defaults = remove_items(open_defaults, write_defaults) + # Same default values as SoundFile.__init__() + init_defaults = remove_items(init_defaults, write_defaults) - del open_defaults['mode'] # mode is always 'w' - del open_defaults['channels'] # Inferred from data - del open_defaults['samplerate'] # Obligatory in write() - assert not open_defaults # No more arguments should be left + del init_defaults['mode'] # mode is always '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(blocks_function) meth_defaults = defaults(blocks_method) - open_defaults = defaults(open) + init_defaults = defaults(init) del func_defaults['start'] del func_defaults['stop'] - del open_defaults['mode'] + del init_defaults['mode'] - for spec in open_defaults, meth_defaults: + for spec in init_defaults, meth_defaults: func_defaults = remove_items(func_defaults, spec) assert not func_defaults def test_order_of_blocks_arguments(): + # Only the first few are checked meth_args = blocks_method.args[1:] # remove 'self' - meth_args[2:2] = ['start', 'stop'] - open_args = open.args[:] - open_args.remove('mode') - assert blocks_function.args == open_args + meth_args + meth_args[3:3] = ['start', 'stop'] + assert blocks_function.args[:10] == ['file'] + meth_args diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 775ec15..21e88a3 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -80,26 +80,26 @@ def file_wplus(request): @pytest.yield_fixture def sf_stereo_r(file_stereo_r): - with sf.open(file_stereo_r) as f: + with sf.SoundFile(file_stereo_r) as f: yield f @pytest.yield_fixture def sf_stereo_w(file_w): - with sf.open(file_w, 'w', 44100, 2, format='WAV') as f: + 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.open(file_stereo_rplus, 'r+') as f: + with sf.SoundFile(file_stereo_rplus, 'r+') as f: yield f @pytest.yield_fixture def sf_stereo_wplus(file_wplus): - with sf.open(file_wplus, 'w+', 44100, 2, - format='WAV', subtype='FLOAT') as f: + with sf.SoundFile(file_wplus, 'w+', 44100, 2, + format='WAV', subtype='FLOAT') as f: yield f @@ -316,65 +316,65 @@ def test_blocks_write(sf_stereo_w): # ----------------------------------------------------------------------------- -# Test open() +# Test SoundFile.__init__() # ----------------------------------------------------------------------------- def test_open_with_invalid_file(): with pytest.raises(TypeError) as excinfo: - sf.open(3.1415) + sf.SoundFile(3.1415) assert "Invalid file" in str(excinfo.value) def test_open_with_invalid_mode(): with pytest.raises(TypeError) as excinfo: - sf.open(filename_stereo, 42) + sf.SoundFile(filename_stereo, 42) assert "Invalid mode: 42" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - sf.open(filename_stereo, 'rr') + sf.SoundFile(filename_stereo, 'rr') assert "Invalid mode: 'rr'" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - sf.open(filename_stereo, 'rw') + 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.open(filename_new, 'w', samplerate=3.1415, channels=2) + sf.SoundFile(filename_new, 'w', samplerate=3.1415, channels=2) assert "integer" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename_new, 'w', samplerate=44100, channels=3.1415) + sf.SoundFile(filename_new, 'w', samplerate=44100, channels=3.1415) assert "integer" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - sf.open(filename_new, 'w', 44100, 2, format='WAF') + sf.SoundFile(filename_new, 'w', 44100, 2, format='WAF') assert "Invalid format string" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - sf.open(filename_new, 'w', 44100, 2, subtype='PCM16') + sf.SoundFile(filename_new, 'w', 44100, 2, subtype='PCM16') assert "Invalid subtype string" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - sf.open(filename_new, 'w', 44100, 2, endian='BOTH') + sf.SoundFile(filename_new, 'w', 44100, 2, endian='BOTH') assert "Invalid endian-ness" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - sf.open(filename_stereo, closefd=False) + sf.SoundFile(filename_stereo, closefd=False) assert "closefd=False" 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.open(filename_stereo, mode, samplerate=44100) + sf.SoundFile(filename_stereo, mode, samplerate=44100) assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename_stereo, mode, channels=2) + sf.SoundFile(filename_stereo, mode, channels=2) assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename_stereo, mode, format='WAV') + sf.SoundFile(filename_stereo, mode, format='WAV') assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename_stereo, mode, subtype='FLOAT') + sf.SoundFile(filename_stereo, mode, subtype='FLOAT') assert "Not allowed" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename_stereo, mode, endian='FILE') + sf.SoundFile(filename_stereo, mode, endian='FILE') assert "Not allowed" in str(excinfo.value) @@ -382,31 +382,31 @@ def test_open_w_and_wplus_with_too_few_arguments(): filename = 'not_existing.xyz' for mode in 'w', 'w+': with pytest.raises(TypeError) as excinfo: - sf.open(filename, mode, samplerate=44100, channels=2) + sf.SoundFile(filename, mode, samplerate=44100, channels=2) assert "No format specified" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename, mode, samplerate=44100, format='WAV') + sf.SoundFile(filename, mode, samplerate=44100, format='WAV') assert "channels" in str(excinfo.value) with pytest.raises(TypeError) as excinfo: - sf.open(filename, mode, channels=2, format='WAV') + sf.SoundFile(filename, 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.open(filename_stereo, mode=None) + sf.SoundFile(filename_stereo, mode=None) assert "Invalid mode: None" in str(excinfo.value) with open(filename_stereo, 'rb') as fobj: - with sf.open(fobj, mode=None) as f: + with sf.SoundFile(fobj, mode=None) as f: assert f.mode == 'rb' @pytest.mark.skipif(PY2, reason="mode='x' not supported in Python 2") def test_open_with_mode_is_x(): with pytest.raises(FileExistsError): - sf.open(filename_stereo, 'x', 44100, 2) + sf.SoundFile(filename_stereo, 'x', 44100, 2) with pytest.raises(FileExistsError): - sf.open(filename_stereo, 'x+', 44100, 2) + sf.SoundFile(filename_stereo, 'x+', 44100, 2) # ----------------------------------------------------------------------------- @@ -597,30 +597,30 @@ def test_rplus_append_data(sf_stereo_rplus): def test_context_manager_should_open_and_close_file(): - with sf.open(filename_stereo) as f: + with sf.SoundFile(filename_stereo) as f: assert not f.closed assert f.closed def test_closing_should_close_file(): - f = sf.open(filename_stereo) + f = sf.SoundFile(filename_stereo) assert not f.closed f.close() assert f.closed def test_file_attributes_should_save_to_disk(): - with sf.open(filename_new, 'w', 44100, 2, format='WAV') as f: + with sf.SoundFile(filename_new, 'w', 44100, 2, format='WAV') as f: f.title = 'testing' - with sf.open(filename_new) as f: + with sf.SoundFile(filename_new) as f: assert f.title == 'testing' os.remove(filename_new) def test_non_file_attributes_should_not_save_to_disk(): - with sf.open(filename_new, 'w', 44100, 2, format='WAV') as f: + with sf.SoundFile(filename_new, 'w', 44100, 2, format='WAV') as f: f.foobar = 'testing' - with sf.open(filename_new) as f: + with sf.SoundFile(filename_new) as f: with pytest.raises(AttributeError): f.foobar os.remove(filename_new) @@ -632,16 +632,16 @@ def test_non_file_attributes_should_not_save_to_disk(): def test_read_raw_files_should_read_data(): - with sf.open(filename_raw, 'r', 44100, 1, 'PCM_16') as f: + 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.open(filename_raw) + sf.SoundFile(filename_raw) with pytest.raises(TypeError): # missing subtype - sf.open(filename_raw, samplerate=44100, channels=2) + sf.SoundFile(filename_raw, samplerate=44100, channels=2) with pytest.raises(TypeError): # missing channels - sf.open(filename_raw, samplerate=44100, subtype='PCM_16') + sf.SoundFile(filename_raw, samplerate=44100, subtype='PCM_16') with pytest.raises(TypeError): # missing samplerate - sf.open(filename_raw, channels=2, subtype='PCM_16') + sf.SoundFile(filename_raw, channels=2, subtype='PCM_16')