├── .coveragerc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yml ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── README.md ├── conf.py ├── constants.rst ├── index.rst └── requirements.txt ├── examples ├── devices.py ├── play.py ├── record.py └── sine.py ├── pysoundio ├── __init__.py ├── builder │ └── soundio.py ├── constants.py └── pysoundio.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_pysoundio.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | relative_files = True 3 | concurrency = thread 4 | source = pysoundio 5 | 6 | omit = 7 | *__init__.py 8 | */tests/* 9 | .tox/* 10 | setup.py 11 | pysoundio/builder/*.py 12 | 13 | [report] 14 | omit = 15 | *__init__.py 16 | */tests/* 17 | .tox/* 18 | setup.py 19 | pysoundio/builder/*.py 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | 13 | linux: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [ '3.6', '3.7', '3.8', '3.9' ] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | submodules: recursive 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | sudo apt-get update -y 30 | sudo apt-get install alsa pulseaudio 31 | python -m pip install --upgrade pip 32 | pip install coverage 33 | - name: Run tests 34 | run: coverage run setup.py test 35 | - name: coveralls 36 | uses: AndreMiras/coveralls-python-action@develop 37 | with: 38 | parallel: true 39 | flag-name: unittest 40 | 41 | macos: 42 | runs-on: macos-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | with: 46 | submodules: recursive 47 | - name: Run tests 48 | run: python3 setup.py test 49 | 50 | windows: 51 | runs-on: windows-latest 52 | steps: 53 | - uses: actions/checkout@v2 54 | with: 55 | submodules: recursive 56 | - name: Run tests 57 | run: python setup.py test 58 | 59 | coveralls_finish: 60 | needs: linux 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: coveralls 64 | uses: AndreMiras/coveralls-python-action@develop 65 | with: 66 | parallel-finished: true 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyd 4 | build 5 | dist 6 | pysoundio.egg-info/ 7 | _soundio*.so 8 | *.wav 9 | _build 10 | _static 11 | .coveralls.yml 12 | .coverage 13 | *.c 14 | *.o 15 | .DS_Store 16 | coverage.xml 17 | .tox -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pysoundio/libraries"] 2 | path = pysoundio/libraries 3 | url = git@github.com:joextodd/libsoundio-binaries.git 4 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF 13 | formats: 14 | - pdf 15 | 16 | # Optionally set the version of Python and requirements required to build your docs 17 | python: 18 | version: 3.7 19 | install: 20 | - requirements: docs/requirements.txt 21 | 22 | submodules: 23 | exclude: all -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ---------- 3 | 4 | **v2.0.0** 5 | 6 | * Updated to use CFFI 7 | * Improved performance 8 | * Bundled in libraries for macOS, Linux, Windows and Raspbian 9 | * Fixes issue where multiple instances cause a crash 10 | * Added command line scripts for examples 11 | 12 | **v1.1.0** 13 | 14 | * Added support for libsoundio v2.0.0 15 | * Added support for Windows (thanks @cameronmaske) 16 | * Fixes for malloc errors 17 | 18 | **v1.0.0** 19 | 20 | * Initial release -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | If you find any bugs or other things that need improvement, 5 | please create an issue or a pull request at 6 | https://github.com/joextodd/pysoundio/. 7 | Contributions are always welcome! 8 | 9 | You should get the latest version from GitHub_:: 10 | 11 | git clone https://github.com/joextodd/pysoundio.git 12 | cd pysoundio 13 | 14 | .. _GitHub: https://github.com/joextodd/pysoundio/ 15 | 16 | To install the package for development, first build the library 17 | 18 | python3 pysoundio/builder/soundio.py 19 | 20 | and then install with pip. 21 | 22 | pip3 install . 23 | 24 | Before submitting a pull request, make sure all tests are passing, 25 | and all of the example scripts run as they as should. 26 | 27 | If you make changes to the documentation, you can locally re-create the HTML 28 | pages using Sphinx_. 29 | You can install it and the read the docs theme with:: 30 | 31 | pip3 install -r docs/requirements.txt 32 | 33 | To create the HTML pages, use:: 34 | 35 | python3 setup.py build_sphinx 36 | 37 | The generated files will be available in the directory ``docs/_build/html``. 38 | 39 | .. _Sphinx: http://sphinx-doc.org/ 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Joe Todd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.rst 3 | include examples/play.py 4 | include examples/devices.py 5 | include examples/record.py 6 | include examples/sine.py 7 | include tests/__init__.py 8 | include tests/test_pysoundio.py 9 | include pysoundio/builder/soundio.py 10 | include pysoundio/libraries/include/soundio/* 11 | include pysoundio/libraries/darwin/* 12 | include pysoundio/libraries/linux/* 13 | include pysoundio/libraries/win32/* 14 | include pysoundio/libraries/win64/* 15 | include pysoundio/libraries/rpiv6/* 16 | include pysoundio/libraries/rpiv7/* 17 | exclude pysoundio/libraries/.git 18 | exclude **/.DS_Store 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PySoundIo 2 | ========= 3 | 4 | .. image:: https://github.com/joextodd/pysoundio/workflows/tests/badge.svg 5 | :target: https://github.com/joextodd/pysoundio/workflows/tests 6 | .. image:: https://coveralls.io/repos/github/joextodd/pysoundio/badge.svg 7 | :target: https://coveralls.io/github/joextodd/pysoundio 8 | .. image:: https://readthedocs.org/projects/pysoundio/badge/?version=latest 9 | :target: http://pysoundio.readthedocs.io/en/latest/?badge=latest 10 | 11 | 12 | A simple Pythonic interface for `libsoundio `_. 13 | 14 | libsoundio is a robust, cross-platform solution for real-time audio. It performs 15 | no buffering or processing on your behalf, instead exposing the raw power of the 16 | underlying backend. 17 | 18 | 19 | Installation 20 | ------------ 21 | 22 | You can use pip to download and install the latest release with a single command. :: 23 | 24 | pip3 install pysoundio 25 | 26 | 27 | Examples 28 | -------- 29 | 30 | See examples directory. 31 | 32 | Some of the examples require `PySoundFile `_ :: 33 | 34 | pip3 install soundfile 35 | 36 | On Windows and OS X, this will also install the library libsndfile. On Linux you will need 37 | to install the library as well. 38 | 39 | * Ubuntu / Debian :: 40 | 41 | apt-get install libsndfile1 42 | 43 | 44 | :download:`devices.py <../examples/devices.py>` 45 | 46 | List the available input and output devices on the system and their properties. :: 47 | 48 | python devices.py 49 | 50 | 51 | :download:`record.py <../examples/record.py>` 52 | 53 | Records data from microphone and saves to a wav file. 54 | Supports specifying backend, device, sample rate, block size. :: 55 | 56 | python record.py out.wav --device 0 --rate 44100 57 | 58 | 59 | :download:`play.py <../examples/play.py>` 60 | 61 | Plays a wav file through the speakers. 62 | Supports specifying backend, device, block size. :: 63 | 64 | python play.py in.wav --device 0 65 | 66 | 67 | :download:`sine.py <../examples/sine.py>` 68 | 69 | Plays a sine wave through the speakers. 70 | Supports specifying backend, device, sample rate, block size. :: 71 | 72 | python sine.py --freq 442 73 | 74 | 75 | Testing 76 | ------- 77 | 78 | To run the test suite. :: 79 | 80 | tox -r 81 | 82 | 83 | Advanced 84 | -------- 85 | 86 | If you wish to use your own build of libsoundio (perhaps you want Jack enabled) 87 | then build from source and install it globally and reinstall pysoundio. 88 | 89 | Note: PySoundIo only works with libsoundio versions >= 1.1.0 90 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pysoundio 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/README.md: -------------------------------------------------------------------------------- 1 | This directory is used by read the docs 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('.')) 18 | sys.path.insert(0, os.path.abspath('..')) 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'pysoundio' 23 | copyright = '2021, Joe Todd' 24 | author = 'Joe Todd' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '2.0.0' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | needs_sphinx = '1.3' # for napoleon 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.todo', 44 | 'sphinx.ext.coverage', 45 | 'sphinx.ext.viewcode', 46 | 'sphinx.ext.napoleon', # support for NumPy-style docstrings 47 | ] 48 | 49 | napoleon_google_docstring = False 50 | napoleon_numpy_docstring = True 51 | napoleon_include_private_with_doc = False 52 | napoleon_include_special_with_doc = False 53 | napoleon_use_admonition_for_examples = False 54 | napoleon_use_admonition_for_notes = False 55 | napoleon_use_admonition_for_references = False 56 | napoleon_use_ivar = False 57 | napoleon_use_param = False 58 | napoleon_use_rtype = False 59 | 60 | # Add any paths that contain templates here, relative to this directory. 61 | templates_path = ['_templates'] 62 | 63 | # The suffix(es) of source filenames. 64 | # You can specify multiple suffix as a list of string: 65 | # 66 | # source_suffix = ['.rst', '.md'] 67 | source_suffix = '.rst' 68 | 69 | # The master toctree document. 70 | master_doc = 'index' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This pattern also affects html_static_path and html_extra_path . 82 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | 88 | # -- Options for HTML output ------------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = 'sphinx_rtd_theme' 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | # 99 | # html_theme_options = {} 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ['_static'] 105 | 106 | # Custom sidebar templates, must be a dictionary that maps document names 107 | # to template names. 108 | # 109 | # The default sidebars (for documents that don't match any pattern) are 110 | # defined by theme itself. Builtin themes are using these templates by 111 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 112 | # 'searchbox.html']``. 113 | # 114 | # html_sidebars = {} 115 | 116 | 117 | # -- Options for HTMLHelp output --------------------------------------------- 118 | 119 | # Output file base name for HTML help builder. 120 | htmlhelp_basename = 'pysoundiodoc' 121 | 122 | 123 | # -- Options for LaTeX output ------------------------------------------------ 124 | 125 | latex_elements = { 126 | # The paper size ('letterpaper' or 'a4paper'). 127 | # 128 | # 'papersize': 'letterpaper', 129 | 130 | # The font size ('10pt', '11pt' or '12pt'). 131 | # 132 | # 'pointsize': '10pt', 133 | 134 | # Additional stuff for the LaTeX preamble. 135 | # 136 | # 'preamble': '', 137 | 138 | # Latex figure (float) alignment 139 | # 140 | # 'figure_align': 'htbp', 141 | } 142 | 143 | # Grouping the document tree into LaTeX files. List of tuples 144 | # (source start file, target name, title, 145 | # author, documentclass [howto, manual, or own class]). 146 | latex_documents = [ 147 | (master_doc, 'pysoundio.tex', 'pysoundio Documentation', 148 | 'Joe Todd', 'manual'), 149 | ] 150 | 151 | 152 | # -- Options for manual page output ------------------------------------------ 153 | 154 | # One entry per manual page. List of tuples 155 | # (source start file, name, description, authors, manual section). 156 | man_pages = [ 157 | (master_doc, 'pysoundio', 'pysoundio Documentation', 158 | [author], 1) 159 | ] 160 | 161 | 162 | # -- Options for Texinfo output ---------------------------------------------- 163 | 164 | # Grouping the document tree into Texinfo files. List of tuples 165 | # (source start file, target name, title, author, 166 | # dir menu entry, description, category) 167 | texinfo_documents = [ 168 | (master_doc, 'pysoundio', 'pysoundio Documentation', 169 | author, 'pysoundio', 'One line description of project.', 170 | 'Miscellaneous'), 171 | ] 172 | 173 | 174 | # -- Extension configuration ------------------------------------------------- 175 | 176 | # -- Options for todo extension ---------------------------------------------- 177 | 178 | # If true, `todo` and `todoList` produce output, else they produce nothing. 179 | todo_include_todos = False -------------------------------------------------------------------------------- /docs/constants.rst: -------------------------------------------------------------------------------- 1 | Backends 2 | -------- 3 | 4 | =================================== =============================================================== 5 | Value Backend Description 6 | =================================== =============================================================== 7 | pysoundio.SoundIoBackendJack JACK Audio 8 | pysoundio.SoundIoBackendPulseAudio Pulse Audio 9 | pysoundio.SoundIoBackendAlsa ALSA Audio 10 | pysoundio.SoundIoBackendCoreAudio Core Audio 11 | pysoundio.SoundIoBackendWasapi WASAPI Audio 12 | pysoundio.SoundIoBackendDummy Dummy backend 13 | =================================== =============================================================== 14 | 15 | 16 | Formats 17 | ------- 18 | 19 | ================================= ==================================================================== 20 | Value Format Description 21 | ================================= ==================================================================== 22 | pysoundio.SoundIoFormatS8 Signed 8 bit 23 | pysoundio.SoundIoFormatU8 Unsigned 8 bit 24 | pysoundio.SoundIoFormatS16LE Signed 16 bit Little Endian 25 | pysoundio.SoundIoFormatS16BE Signed 16 bit Big Endian 26 | pysoundio.SoundIoFormatU16LE Unsigned 16 bit Little Endian 27 | pysoundio.SoundIoFormatU16BE Unsigned 16 bit Little Endian 28 | pysoundio.SoundIoFormatS24LE Signed 24 bit Little Endian using low three bytes in 32-bit word 29 | pysoundio.SoundIoFormatS24BE Signed 24 bit Big Endian using low three bytes in 32-bit word 30 | pysoundio.SoundIoFormatU24LE Unsigned 24 bit Little Endian using low three bytes in 32-bit word 31 | pysoundio.SoundIoFormatU24BE Unsigned 24 bit Big Endian using low three bytes in 32-bit word 32 | pysoundio.SoundIoFormatS32LE Signed 32 bit Little Endian 33 | pysoundio.SoundIoFormatS32BE Signed 32 bit Big Endian 34 | pysoundio.SoundIoFormatU32LE Unsigned 32 bit Little Endian 35 | pysoundio.SoundIoFormatU32BE Unsigned 32 bit Big Endian 36 | pysoundio.SoundIoFormatFloat32LE Float 32 bit Little Endian, Range -1.0 to 1.0 37 | pysoundio.SoundIoFormatFloat32BE Float 32 bit Big Endian, Range -1.0 to 1.0 38 | pysoundio.SoundIoFormatFloat64LE Float 64 bit Little Endian, Range -1.0 to 1.0 39 | pysoundio.SoundIoFormatFloat64BE Float 64 bit Big Endian, Range -1.0 to 1.0 40 | ================================= ==================================================================== 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. default-role:: py:obj 2 | 3 | .. include:: ../README.rst 4 | 5 | .. only:: html 6 | 7 | .. include:: ../CONTRIBUTING.rst 8 | 9 | .. include:: ./constants.rst 10 | 11 | API Reference 12 | ------------- 13 | 14 | .. autoclass:: pysoundio.PySoundIo 15 | :members: 16 | :undoc-members: 17 | :inherited-members: 18 | 19 | .. only:: html 20 | 21 | .. include:: ../CHANGELOG.rst 22 | 23 | Index 24 | ===== 25 | 26 | * :ref:`genindex` 27 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | Sphinx 3 | sphinx-rtd-theme -------------------------------------------------------------------------------- /examples/devices.py: -------------------------------------------------------------------------------- 1 | """ 2 | devices.py 3 | 4 | List the available input and output devices on the system and their properties. 5 | """ 6 | import argparse 7 | 8 | from pysoundio import PySoundIo 9 | 10 | 11 | def print_devices(devices): 12 | for i, device in enumerate(devices): 13 | msg = '* ' if device['is_default'] else ' ' 14 | msg += str(i) + ' - ' + device['name'] 15 | print(msg) 16 | print('\tsample rate:') 17 | print('\t default: {}Hz'.format(device['sample_rates']['current'])) 18 | print('\t available: {}'.format( 19 | ', '.join([str(d['max']) + 'Hz' for d in device['sample_rates']['available']]))) 20 | print('\tformat: {}'.format( 21 | ', '.join([str(d) for d in device['formats']['available']]))) 22 | print('\tlayouts: {}'.format(device['layouts']['current']['name'])) 23 | print('\t available: {}'.format( 24 | ', '.join([str(d['name']) for d in device['layouts']['available']]))) 25 | print('\tsoftware latency:') 26 | print('\t min: {}s, max: {}s, current: {}s'.format( 27 | round(device['software_latency_min'], 4), 28 | round(device['software_latency_max'], 4), 29 | round(device['software_latency_current'], 4))) 30 | print("") 31 | 32 | 33 | def get_args(): 34 | parser = argparse.ArgumentParser( 35 | description='PySoundIo list devices example', 36 | epilog='List the available input and output devices' 37 | ) 38 | parser.add_argument('--backend', type=int, help='Backend to use (optional)') 39 | args = parser.parse_args() 40 | return args 41 | 42 | 43 | def main(): 44 | args = get_args() 45 | pysoundio = PySoundIo(backend=args.backend) 46 | input_devices, output_devices = pysoundio.list_devices() 47 | print('\n' + '-' * 20 + ' Input Devices ' + '-' * 20 + '\n') 48 | print_devices(input_devices) 49 | 50 | print('\n' + '-' * 20 + ' Output Devices ' + '-' * 20 + '\n') 51 | print_devices(output_devices) 52 | 53 | pysoundio.close() 54 | 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /examples/play.py: -------------------------------------------------------------------------------- 1 | """ 2 | play.py 3 | 4 | Stream a wav file to the default output device. 5 | Supports specifying backend, device and block size. 6 | 7 | Requires soundfile 8 | pip3 install soundfile 9 | 10 | http://pysoundfile.readthedocs.io/ 11 | """ 12 | import argparse 13 | import time 14 | 15 | import soundfile as sf 16 | from pysoundio import ( 17 | PySoundIo, 18 | SoundIoFormatFloat32LE, 19 | ) 20 | 21 | 22 | class Player: 23 | 24 | def __init__(self, infile, backend=None, output_device=None, block_size=None): 25 | 26 | data, rate = sf.read( 27 | infile, 28 | dtype='float32', 29 | always_2d=True 30 | ) 31 | self.idx = 0 32 | self.stream = data.tobytes() 33 | self.block_size = block_size 34 | 35 | self.total_blocks = len(data) 36 | self.timer = self.total_blocks / float(rate) 37 | 38 | self.num_channels = data.shape[1] 39 | self.sample_size = data.dtype.itemsize 40 | 41 | self.pysoundio = PySoundIo(backend=backend) 42 | self.pysoundio.start_output_stream( 43 | device_id=output_device, 44 | channels=self.num_channels, 45 | sample_rate=rate, 46 | block_size=self.block_size, 47 | dtype=SoundIoFormatFloat32LE, 48 | write_callback=self.callback 49 | ) 50 | 51 | print('%s:\n' % infile) 52 | print(' Channels: %d' % data.shape[1]) 53 | print(' Sample rate: %dHz' % rate) 54 | print('') 55 | 56 | def close(self): 57 | self.pysoundio.close() 58 | 59 | def callback(self, data, length): 60 | num_bytes = length * self.sample_size * self.num_channels 61 | data[:] = self.stream[self.idx:self.idx+num_bytes] 62 | self.idx += num_bytes 63 | 64 | 65 | def get_args(): 66 | parser = argparse.ArgumentParser( 67 | description='PySoundIo audio player example', 68 | epilog='Play a wav file over the default output device' 69 | ) 70 | parser.add_argument('infile', help='WAV output file name') 71 | parser.add_argument('--backend', type=int, help='Backend to use (optional)') 72 | parser.add_argument('--blocksize', type=int, default=4096, help='Block size (optional)') 73 | parser.add_argument('--device', type=int, help='Output device id (optional)') 74 | args = parser.parse_args() 75 | return args 76 | 77 | 78 | def main(): 79 | args = get_args() 80 | player = Player(args.infile, args.backend, args.device, args.blocksize) 81 | print('Playing...') 82 | print('CTRL-C to exit') 83 | 84 | try: 85 | time.sleep(player.timer) 86 | except KeyboardInterrupt: 87 | print('Exiting...') 88 | 89 | player.close() 90 | 91 | 92 | if __name__ == '__main__': 93 | main() 94 | -------------------------------------------------------------------------------- /examples/record.py: -------------------------------------------------------------------------------- 1 | """ 2 | record.py 3 | 4 | Stream the default input device and save to wav file. 5 | Supports specifying backend, device, sample rate, block size. 6 | 7 | Requires soundfile 8 | pip3 install soundfile 9 | 10 | http://pysoundfile.readthedocs.io/ 11 | """ 12 | import argparse 13 | import time 14 | 15 | import soundfile as sf 16 | from pysoundio import ( 17 | PySoundIo, 18 | SoundIoFormatFloat32LE, 19 | ) 20 | 21 | 22 | class Record: 23 | 24 | def __init__(self, outfile, backend=None, input_device=None, 25 | sample_rate=None, block_size=None, channels=None): 26 | self.wav_file = sf.SoundFile( 27 | outfile, mode='w', channels=channels, 28 | samplerate=sample_rate 29 | ) 30 | self.pysoundio = PySoundIo(backend=backend) 31 | self.pysoundio.start_input_stream( 32 | device_id=input_device, 33 | channels=channels, 34 | sample_rate=sample_rate, 35 | block_size=block_size, 36 | dtype=SoundIoFormatFloat32LE, 37 | read_callback=self.callback 38 | ) 39 | 40 | def close(self): 41 | self.pysoundio.close() 42 | self.wav_file.close() 43 | 44 | def callback(self, data, length): 45 | self.wav_file.buffer_write(data, dtype='float32') 46 | 47 | 48 | def get_args(): 49 | parser = argparse.ArgumentParser( 50 | description='PySoundIo audio record example', 51 | epilog='Stream the input device and save to wav file' 52 | ) 53 | parser.add_argument('outfile', help='WAV output file name') 54 | parser.add_argument('--backend', type=int, help='Backend to use (optional)') 55 | parser.add_argument('--blocksize', type=int, default=4096, help='Block size (optional)') 56 | parser.add_argument('--rate', type=int, default=44100, help='Sample rate (optional)') 57 | parser.add_argument('--channels', type=int, default=1, help='Mono or stereo (optional)') 58 | parser.add_argument('--device', type=int, help='Input device id (optional)') 59 | args = parser.parse_args() 60 | return args 61 | 62 | 63 | def main(): 64 | args = get_args() 65 | record = Record(args.outfile, args.backend, args.device, args.rate, args.blocksize, args.channels) 66 | print('Recording...') 67 | print('CTRL-C to exit') 68 | 69 | try: 70 | while True: 71 | time.sleep(1) 72 | except KeyboardInterrupt: 73 | print('Exiting...') 74 | 75 | record.close() 76 | 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /examples/sine.py: -------------------------------------------------------------------------------- 1 | """ 2 | sine.py 3 | 4 | Play a sine wave over the default output device. 5 | Supports specifying backend, device, sample rate, block size. 6 | 7 | """ 8 | import array as ar 9 | import argparse 10 | import math 11 | import time 12 | 13 | from pysoundio import ( 14 | PySoundIo, 15 | SoundIoFormatFloat32LE, 16 | ) 17 | 18 | 19 | class Player: 20 | 21 | def __init__(self, freq=None, backend=None, output_device=None, 22 | sample_rate=None, block_size=None): 23 | self.pysoundio = PySoundIo(backend=backend) 24 | 25 | self.freq = float(freq) 26 | self.seconds_offset = 0.0 27 | self.radians_per_second = self.freq * 2.0 * math.pi 28 | self.seconds_per_frame = 1.0 / sample_rate 29 | 30 | self.pysoundio.start_output_stream( 31 | device_id=output_device, 32 | channels=1, 33 | sample_rate=sample_rate, 34 | block_size=block_size, 35 | dtype=SoundIoFormatFloat32LE, 36 | write_callback=self.callback 37 | ) 38 | 39 | def close(self): 40 | self.pysoundio.close() 41 | 42 | def callback(self, data, length): 43 | indata = ar.array('f', [0] * length) 44 | for i in range(0, length): 45 | indata[i] = math.sin( 46 | (self.seconds_offset + i * self.seconds_per_frame) * self.radians_per_second) 47 | data[:] = indata.tobytes() 48 | self.seconds_offset += self.seconds_per_frame * length 49 | 50 | 51 | def get_args(): 52 | parser = argparse.ArgumentParser( 53 | description='PySoundIo sine wave output example', 54 | epilog='Play a sine wave over the default output device' 55 | ) 56 | parser.add_argument('--freq', default=442.0, help='Note frequency (optional)') 57 | parser.add_argument('--backend', type=int, help='Backend to use (optional)') 58 | parser.add_argument('--blocksize', type=int, default=4096, help='Block size (optional)') 59 | parser.add_argument('--rate', type=int, default=44100, help='Sample rate (optional)') 60 | parser.add_argument('--device', type=int, help='Output device id (optional)') 61 | args = parser.parse_args() 62 | return args 63 | 64 | 65 | def main(): 66 | args = get_args() 67 | player = Player(args.freq, args.backend, args.device, args.rate, args.blocksize) 68 | print('Playing...') 69 | print('CTRL-C to exit') 70 | 71 | try: 72 | while True: 73 | time.sleep(1) 74 | except KeyboardInterrupt: 75 | print('Exiting...') 76 | 77 | player.close() 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /pysoundio/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Joe Todd 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | # SOFTWARE. 21 | 22 | import os 23 | import platform 24 | 25 | from cffi import FFI 26 | 27 | if platform.system() == 'Windows': 28 | ffi = FFI() 29 | base_path = os.path.dirname(os.path.abspath(__file__)) 30 | if platform.architecture()[0] == '32bit': 31 | soundio = ffi.dlopen(os.path.join(base_path, 'libraries', 'win32', 'libsoundio.dll')) 32 | elif platform.architecture()[0] == '64bit': 33 | soundio = ffi.dlopen(os.path.join(base_path, 'libraries', 'win64', 'libsoundio.dll')) 34 | 35 | from pysoundio._soundio.lib import ( # noqa: F401 36 | SoundIoBackendNone, SoundIoBackendJack, SoundIoBackendPulseAudio, 37 | SoundIoBackendAlsa, SoundIoBackendCoreAudio, SoundIoBackendWasapi, 38 | SoundIoBackendDummy, 39 | SoundIoFormatS8, SoundIoFormatU8, SoundIoFormatS16LE, 40 | SoundIoFormatS16BE, SoundIoFormatU16LE, SoundIoFormatU16BE, 41 | SoundIoFormatS24LE, SoundIoFormatS24BE, SoundIoFormatU24LE, 42 | SoundIoFormatU24BE, SoundIoFormatS32LE, SoundIoFormatS32BE, 43 | SoundIoFormatU32LE, SoundIoFormatU32BE, 44 | SoundIoFormatFloat32LE, SoundIoFormatFloat32BE, SoundIoFormatFloat64LE, 45 | SoundIoFormatFloat64BE, SoundIoFormatInvalid 46 | ) 47 | 48 | from .constants import ( # noqa: F401 49 | BACKENDS, 50 | FORMATS 51 | ) 52 | from .pysoundio import PySoundIo, PySoundIoError # noqa: F401 53 | -------------------------------------------------------------------------------- /pysoundio/builder/soundio.py: -------------------------------------------------------------------------------- 1 | """ 2 | soundio.py 3 | 4 | CFFI builder for PySoundIO 5 | """ 6 | 7 | import os 8 | import cffi 9 | import subprocess 10 | import platform 11 | 12 | build_kwargs = { 13 | 'libraries': [], 14 | 'include_dirs': ['./pysoundio/libraries/include'], 15 | 'library_dirs': [], 16 | } 17 | builder_path = os.path.dirname(os.path.realpath(__file__)) 18 | package_path = os.path.abspath(os.path.join(builder_path, os.pardir)) 19 | 20 | if platform.system() == 'Darwin': 21 | build_kwargs['libraries'].append('soundio.2.0.0') 22 | build_kwargs['library_dirs'].append('/usr/local/lib') 23 | build_kwargs['library_dirs'].append(os.path.join(package_path, 'libraries', 'darwin')) 24 | build_kwargs['extra_link_args'] = ['-Wl,-rpath,@loader_path/libraries/darwin'] 25 | 26 | elif platform.system() == 'Linux': 27 | if os.uname()[4].startswith('arm'): 28 | cpuinfo = subprocess.check_output(['cat', '/proc/cpuinfo']).decode() 29 | if 'neon' in cpuinfo: 30 | architecture = 'rpiv7' 31 | else: 32 | architecture = 'rpiv6' 33 | else: 34 | architecture = 'linux' 35 | 36 | build_kwargs['libraries'].append('soundio-2.0.0') 37 | build_kwargs['library_dirs'].append('/usr/local/lib') 38 | build_kwargs['library_dirs'].append(os.path.join(package_path, 'libraries', architecture)) 39 | build_kwargs['extra_link_args'] = ['-Wl,-rpath,$ORIGIN/libraries/' + architecture] 40 | 41 | elif platform.system() == 'Windows': 42 | build_kwargs['libraries'].append('soundio') 43 | if platform.architecture()[0] == '32bit': 44 | build_kwargs['library_dirs'].append(os.path.join(package_path, 'libraries', 'win32')) 45 | elif platform.architecture()[0] == '64bit': 46 | build_kwargs['library_dirs'].append(os.path.join(package_path, 'libraries', 'win64')) 47 | else: 48 | raise RuntimeError('Windows architecture %s is not supported' % platform.architecture()[0]) 49 | else: 50 | raise RuntimeError('%s platform is not supported' % platform.system()) 51 | 52 | ffibuilder = cffi.FFI() 53 | ffibuilder.set_source( 54 | "pysoundio._soundio", 55 | """ 56 | #include 57 | """, 58 | **build_kwargs 59 | ) 60 | ffibuilder.cdef(""" 61 | 62 | // ENUMS 63 | enum SoundIoError { 64 | SoundIoErrorNone, 65 | SoundIoErrorNoMem, 66 | SoundIoErrorInitAudioBackend, 67 | SoundIoErrorSystemResources, 68 | SoundIoErrorOpeningDevice, 69 | SoundIoErrorNoSuchDevice, 70 | SoundIoErrorInvalid, 71 | SoundIoErrorBackendUnavailable, 72 | SoundIoErrorStreaming, 73 | SoundIoErrorIncompatibleDevice, 74 | SoundIoErrorNoSuchClient, 75 | SoundIoErrorIncompatibleBackend, 76 | SoundIoErrorBackendDisconnected, 77 | SoundIoErrorInterrupted, 78 | SoundIoErrorUnderflow, 79 | SoundIoErrorEncodingString, 80 | }; 81 | 82 | enum SoundIoChannelId { 83 | SoundIoChannelIdInvalid, 84 | 85 | SoundIoChannelIdFrontLeft, ///< First of the more commonly supported ids. 86 | SoundIoChannelIdFrontRight, 87 | SoundIoChannelIdFrontCenter, 88 | SoundIoChannelIdLfe, 89 | SoundIoChannelIdBackLeft, 90 | SoundIoChannelIdBackRight, 91 | SoundIoChannelIdFrontLeftCenter, 92 | SoundIoChannelIdFrontRightCenter, 93 | SoundIoChannelIdBackCenter, 94 | SoundIoChannelIdSideLeft, 95 | SoundIoChannelIdSideRight, 96 | SoundIoChannelIdTopCenter, 97 | SoundIoChannelIdTopFrontLeft, 98 | SoundIoChannelIdTopFrontCenter, 99 | SoundIoChannelIdTopFrontRight, 100 | SoundIoChannelIdTopBackLeft, 101 | SoundIoChannelIdTopBackCenter, 102 | SoundIoChannelIdTopBackRight, ///< Last of the more commonly supported ids. 103 | 104 | SoundIoChannelIdBackLeftCenter, ///< First of the less commonly supported ids. 105 | SoundIoChannelIdBackRightCenter, 106 | SoundIoChannelIdFrontLeftWide, 107 | SoundIoChannelIdFrontRightWide, 108 | SoundIoChannelIdFrontLeftHigh, 109 | SoundIoChannelIdFrontCenterHigh, 110 | SoundIoChannelIdFrontRightHigh, 111 | SoundIoChannelIdTopFrontLeftCenter, 112 | SoundIoChannelIdTopFrontRightCenter, 113 | SoundIoChannelIdTopSideLeft, 114 | SoundIoChannelIdTopSideRight, 115 | SoundIoChannelIdLeftLfe, 116 | SoundIoChannelIdRightLfe, 117 | SoundIoChannelIdLfe2, 118 | SoundIoChannelIdBottomCenter, 119 | SoundIoChannelIdBottomLeftCenter, 120 | SoundIoChannelIdBottomRightCenter, 121 | 122 | /// Mid/side recording 123 | SoundIoChannelIdMsMid, 124 | SoundIoChannelIdMsSide, 125 | 126 | /// first order ambisonic channels 127 | SoundIoChannelIdAmbisonicW, 128 | SoundIoChannelIdAmbisonicX, 129 | SoundIoChannelIdAmbisonicY, 130 | SoundIoChannelIdAmbisonicZ, 131 | 132 | /// X-Y Recording 133 | SoundIoChannelIdXyX, 134 | SoundIoChannelIdXyY, 135 | 136 | SoundIoChannelIdHeadphonesLeft, ///< First of the "other" channel ids 137 | SoundIoChannelIdHeadphonesRight, 138 | SoundIoChannelIdClickTrack, 139 | SoundIoChannelIdForeignLanguage, 140 | SoundIoChannelIdHearingImpaired, 141 | SoundIoChannelIdNarration, 142 | SoundIoChannelIdHaptic, 143 | SoundIoChannelIdDialogCentricMix, ///< Last of the "other" channel ids 144 | 145 | SoundIoChannelIdAux, 146 | SoundIoChannelIdAux0, 147 | SoundIoChannelIdAux1, 148 | SoundIoChannelIdAux2, 149 | SoundIoChannelIdAux3, 150 | SoundIoChannelIdAux4, 151 | SoundIoChannelIdAux5, 152 | SoundIoChannelIdAux6, 153 | SoundIoChannelIdAux7, 154 | SoundIoChannelIdAux8, 155 | SoundIoChannelIdAux9, 156 | SoundIoChannelIdAux10, 157 | SoundIoChannelIdAux11, 158 | SoundIoChannelIdAux12, 159 | SoundIoChannelIdAux13, 160 | SoundIoChannelIdAux14, 161 | SoundIoChannelIdAux15, 162 | }; 163 | 164 | /// Built-in channel layouts for convenience. 165 | enum SoundIoChannelLayoutId { 166 | SoundIoChannelLayoutIdMono, 167 | SoundIoChannelLayoutIdStereo, 168 | SoundIoChannelLayoutId2Point1, 169 | SoundIoChannelLayoutId3Point0, 170 | SoundIoChannelLayoutId3Point0Back, 171 | SoundIoChannelLayoutId3Point1, 172 | SoundIoChannelLayoutId4Point0, 173 | SoundIoChannelLayoutIdQuad, 174 | SoundIoChannelLayoutIdQuadSide, 175 | SoundIoChannelLayoutId4Point1, 176 | SoundIoChannelLayoutId5Point0Back, 177 | SoundIoChannelLayoutId5Point0Side, 178 | SoundIoChannelLayoutId5Point1, 179 | SoundIoChannelLayoutId5Point1Back, 180 | SoundIoChannelLayoutId6Point0Side, 181 | SoundIoChannelLayoutId6Point0Front, 182 | SoundIoChannelLayoutIdHexagonal, 183 | SoundIoChannelLayoutId6Point1, 184 | SoundIoChannelLayoutId6Point1Back, 185 | SoundIoChannelLayoutId6Point1Front, 186 | SoundIoChannelLayoutId7Point0, 187 | SoundIoChannelLayoutId7Point0Front, 188 | SoundIoChannelLayoutId7Point1, 189 | SoundIoChannelLayoutId7Point1Wide, 190 | SoundIoChannelLayoutId7Point1WideBack, 191 | SoundIoChannelLayoutIdOctagonal, 192 | }; 193 | 194 | enum SoundIoBackend { 195 | SoundIoBackendNone, 196 | SoundIoBackendJack, 197 | SoundIoBackendPulseAudio, 198 | SoundIoBackendAlsa, 199 | SoundIoBackendCoreAudio, 200 | SoundIoBackendWasapi, 201 | SoundIoBackendDummy, 202 | }; 203 | 204 | enum SoundIoDeviceAim { 205 | SoundIoDeviceAimInput, ///< capture / recording 206 | SoundIoDeviceAimOutput, ///< playback 207 | }; 208 | 209 | enum SoundIoFormat { 210 | SoundIoFormatInvalid, 211 | SoundIoFormatS8, ///< Signed 8 bit 212 | SoundIoFormatU8, ///< Unsigned 8 bit 213 | SoundIoFormatS16LE, ///< Signed 16 bit Little Endian 214 | SoundIoFormatS16BE, ///< Signed 16 bit Big Endian 215 | SoundIoFormatU16LE, ///< Unsigned 16 bit Little Endian 216 | SoundIoFormatU16BE, ///< Unsigned 16 bit Big Endian 217 | SoundIoFormatS24LE, ///< Signed 24 bit Little Endian using low three bytes in 32-bit word 218 | SoundIoFormatS24BE, ///< Signed 24 bit Big Endian using low three bytes in 32-bit word 219 | SoundIoFormatU24LE, ///< Unsigned 24 bit Little Endian using low three bytes in 32-bit word 220 | SoundIoFormatU24BE, ///< Unsigned 24 bit Big Endian using low three bytes in 32-bit word 221 | SoundIoFormatS32LE, ///< Signed 32 bit Little Endian 222 | SoundIoFormatS32BE, ///< Signed 32 bit Big Endian 223 | SoundIoFormatU32LE, ///< Unsigned 32 bit Little Endian 224 | SoundIoFormatU32BE, ///< Unsigned 32 bit Big Endian 225 | SoundIoFormatFloat32LE, ///< Float 32 bit Little Endian, Range -1.0 to 1.0 226 | SoundIoFormatFloat32BE, ///< Float 32 bit Big Endian, Range -1.0 to 1.0 227 | SoundIoFormatFloat64LE, ///< Float 64 bit Little Endian, Range -1.0 to 1.0 228 | SoundIoFormatFloat64BE, ///< Float 64 bit Big Endian, Range -1.0 to 1.0 229 | }; 230 | 231 | // STRUCTURES 232 | 233 | struct SoundIoChannelLayout { 234 | const char *name; 235 | int channel_count; 236 | enum SoundIoChannelId channels[24]; 237 | }; 238 | 239 | struct SoundIoSampleRateRange { 240 | int min; 241 | int max; 242 | }; 243 | 244 | struct SoundIoChannelArea { 245 | char *ptr; 246 | int step; 247 | }; 248 | 249 | struct SoundIo { 250 | void *userdata; 251 | void (*on_devices_change)(struct SoundIo *); 252 | void (*on_backend_disconnect)(struct SoundIo *, int err); 253 | void (*on_events_signal)(struct SoundIo *); 254 | 255 | enum SoundIoBackend current_backend; 256 | const char *app_name; 257 | 258 | void (*emit_rtprio_warning)(void); 259 | void (*jack_info_callback)(const char *msg); 260 | void (*jack_error_callback)(const char *msg); 261 | }; 262 | 263 | struct SoundIoDevice { 264 | struct SoundIo *soundio; 265 | char *id; 266 | char *name; 267 | 268 | enum SoundIoDeviceAim aim; 269 | struct SoundIoChannelLayout *layouts; 270 | int layout_count; 271 | struct SoundIoChannelLayout current_layout; 272 | 273 | enum SoundIoFormat *formats; 274 | int format_count; 275 | enum SoundIoFormat current_format; 276 | struct SoundIoSampleRateRange *sample_rates; 277 | int sample_rate_count; 278 | int sample_rate_current; 279 | double software_latency_min; 280 | double software_latency_max; 281 | double software_latency_current; 282 | 283 | bool is_raw; 284 | int ref_count; 285 | 286 | int probe_error; 287 | }; 288 | 289 | struct SoundIoOutStream { 290 | struct SoundIoDevice *device; 291 | enum SoundIoFormat format; 292 | 293 | int sample_rate; 294 | struct SoundIoChannelLayout layout; 295 | 296 | double software_latency; 297 | float volume; 298 | void *userdata; 299 | void (*write_callback)(struct SoundIoOutStream *, int frame_count_min, int frame_count_max); 300 | void (*underflow_callback)(struct SoundIoOutStream *); 301 | void (*error_callback)(struct SoundIoOutStream *, int err); 302 | 303 | const char *name; 304 | bool non_terminal_hint; 305 | 306 | int bytes_per_frame; 307 | int bytes_per_sample; 308 | 309 | int layout_error; 310 | }; 311 | 312 | struct SoundIoInStream { 313 | struct SoundIoDevice *device; 314 | enum SoundIoFormat format; 315 | 316 | int sample_rate; 317 | struct SoundIoChannelLayout layout; 318 | 319 | double software_latency; 320 | void *userdata; 321 | void (*read_callback)(struct SoundIoInStream *, int frame_count_min, int frame_count_max); 322 | void (*overflow_callback)(struct SoundIoInStream *); 323 | void (*error_callback)(struct SoundIoInStream *, int err); 324 | 325 | const char *name; 326 | bool non_terminal_hint; 327 | 328 | int bytes_per_frame; 329 | int bytes_per_sample; 330 | 331 | int layout_error; 332 | }; 333 | 334 | // CALLBACKS 335 | extern "Python" void _write_callback(struct SoundIoOutStream *, int, int); 336 | extern "Python" void _underflow_callback(struct SoundIoOutStream *); 337 | extern "Python" void _output_error_callback(struct SoundIoOutStream *, int); 338 | extern "Python" void _read_callback(struct SoundIoInStream *, int, int); 339 | extern "Python" void _overflow_callback(struct SoundIoInStream *); 340 | extern "Python" void _input_error_callback(struct SoundIoInStream *, int); 341 | 342 | // STATIC 343 | const char *soundio_version_string(void); 344 | int soundio_version_major(void); 345 | int soundio_version_minor(void); 346 | int soundio_version_patch(void); 347 | 348 | // CONSTRUCTOR / DESTRUCTOR 349 | struct SoundIo *soundio_create(void); 350 | void soundio_destroy(struct SoundIo *soundio); 351 | 352 | // CONFIGURATION 353 | int soundio_connect(struct SoundIo *soundio); 354 | int soundio_connect_backend(struct SoundIo *soundio, enum SoundIoBackend backend); 355 | void soundio_disconnect(struct SoundIo *soundio); 356 | 357 | const char *soundio_strerror(int error); 358 | const char *soundio_backend_name(enum SoundIoBackend backend); 359 | 360 | int soundio_backend_count(struct SoundIo *soundio); 361 | enum SoundIoBackend soundio_get_backend(struct SoundIo *soundio, int index); 362 | 363 | bool soundio_have_backend(enum SoundIoBackend backend); 364 | 365 | void soundio_flush_events(struct SoundIo *soundio); 366 | void soundio_wait_events(struct SoundIo *soundio); 367 | void soundio_wakeup(struct SoundIo *soundio); 368 | 369 | void soundio_force_device_scan(struct SoundIo *soundio); 370 | 371 | // CHANNEL LAYOUTS 372 | bool soundio_channel_layout_equal( 373 | const struct SoundIoChannelLayout *a, 374 | const struct SoundIoChannelLayout *b); 375 | 376 | const char *soundio_get_channel_name(enum SoundIoChannelId id); 377 | enum SoundIoChannelId soundio_parse_channel_id(const char *str, int str_len); 378 | 379 | int soundio_channel_layout_builtin_count(void); 380 | const struct SoundIoChannelLayout *soundio_channel_layout_get_builtin(int index); 381 | 382 | const struct SoundIoChannelLayout *soundio_channel_layout_get_default(int channel_count); 383 | int soundio_channel_layout_find_channel( 384 | const struct SoundIoChannelLayout *layout, enum SoundIoChannelId channel); 385 | 386 | bool soundio_channel_layout_detect_builtin(struct SoundIoChannelLayout *layout); 387 | const struct SoundIoChannelLayout *soundio_best_matching_channel_layout( 388 | const struct SoundIoChannelLayout *preferred_layouts, int preferred_layout_count, 389 | const struct SoundIoChannelLayout *available_layouts, int available_layout_count); 390 | 391 | void soundio_sort_channel_layouts(struct SoundIoChannelLayout *layouts, int layout_count); 392 | 393 | // SAMPLE FORMATS 394 | int soundio_get_bytes_per_sample(enum SoundIoFormat format); 395 | const char * soundio_format_string(enum SoundIoFormat format); 396 | 397 | // DEVICES 398 | int soundio_input_device_count(struct SoundIo *soundio); 399 | int soundio_output_device_count(struct SoundIo *soundio); 400 | 401 | struct SoundIoDevice *soundio_get_input_device(struct SoundIo *soundio, int index); 402 | struct SoundIoDevice *soundio_get_output_device(struct SoundIo *soundio, int index); 403 | 404 | int soundio_default_input_device_index(struct SoundIo *soundio); 405 | 406 | int soundio_default_output_device_index(struct SoundIo *soundio); 407 | 408 | void soundio_device_ref(struct SoundIoDevice *device); 409 | void soundio_device_unref(struct SoundIoDevice *device); 410 | 411 | bool soundio_device_equal( 412 | const struct SoundIoDevice *a, 413 | const struct SoundIoDevice *b); 414 | 415 | void soundio_device_sort_channel_layouts(struct SoundIoDevice *device); 416 | 417 | bool soundio_device_supports_format(struct SoundIoDevice *device, 418 | enum SoundIoFormat format); 419 | 420 | bool soundio_device_supports_layout(struct SoundIoDevice *device, 421 | const struct SoundIoChannelLayout *layout); 422 | 423 | bool soundio_device_supports_sample_rate(struct SoundIoDevice *device, 424 | int sample_rate); 425 | 426 | int soundio_device_nearest_sample_rate(struct SoundIoDevice *device, 427 | int sample_rate); 428 | 429 | // OUTPUT STREAMS 430 | struct SoundIoOutStream *soundio_outstream_create(struct SoundIoDevice *device); 431 | void soundio_outstream_destroy(struct SoundIoOutStream *outstream); 432 | 433 | int soundio_outstream_open(struct SoundIoOutStream *outstream); 434 | int soundio_outstream_start(struct SoundIoOutStream *outstream); 435 | 436 | int soundio_outstream_begin_write(struct SoundIoOutStream *outstream, 437 | struct SoundIoChannelArea **areas, int *frame_count); 438 | 439 | int soundio_outstream_end_write(struct SoundIoOutStream *outstream); 440 | int soundio_outstream_clear_buffer(struct SoundIoOutStream *outstream); 441 | 442 | int soundio_outstream_pause(struct SoundIoOutStream *outstream, bool pause); 443 | 444 | int soundio_outstream_get_latency(struct SoundIoOutStream *outstream, 445 | double *out_latency); 446 | 447 | int soundio_outstream_set_volume(struct SoundIoOutStream *outstream, 448 | double volume); 449 | 450 | // INPUT STREAMS 451 | struct SoundIoInStream *soundio_instream_create(struct SoundIoDevice *device); 452 | void soundio_instream_destroy(struct SoundIoInStream *instream); 453 | 454 | int soundio_instream_open(struct SoundIoInStream *instream); 455 | 456 | int soundio_instream_start(struct SoundIoInStream *instream); 457 | 458 | int soundio_instream_begin_read(struct SoundIoInStream *instream, 459 | struct SoundIoChannelArea **areas, int *frame_count); 460 | int soundio_instream_end_read(struct SoundIoInStream *instream); 461 | 462 | int soundio_instream_pause(struct SoundIoInStream *instream, bool pause); 463 | 464 | int soundio_instream_get_latency(struct SoundIoInStream *instream, 465 | double *out_latency); 466 | 467 | // RING BUFFERS 468 | struct SoundIoRingBuffer; 469 | 470 | struct SoundIoRingBuffer *soundio_ring_buffer_create(struct SoundIo *soundio, int requested_capacity); 471 | void soundio_ring_buffer_destroy(struct SoundIoRingBuffer *ring_buffer); 472 | 473 | int soundio_ring_buffer_capacity(struct SoundIoRingBuffer *ring_buffer); 474 | 475 | char *soundio_ring_buffer_write_ptr(struct SoundIoRingBuffer *ring_buffer); 476 | void soundio_ring_buffer_advance_write_ptr(struct SoundIoRingBuffer *ring_buffer, int count); 477 | 478 | char *soundio_ring_buffer_read_ptr(struct SoundIoRingBuffer *ring_buffer); 479 | void soundio_ring_buffer_advance_read_ptr(struct SoundIoRingBuffer *ring_buffer, int count); 480 | 481 | int soundio_ring_buffer_fill_count(struct SoundIoRingBuffer *ring_buffer); 482 | 483 | int soundio_ring_buffer_free_count(struct SoundIoRingBuffer *ring_buffer); 484 | 485 | void soundio_ring_buffer_clear(struct SoundIoRingBuffer *ring_buffer); 486 | 487 | """) 488 | 489 | if __name__ == "__main__": 490 | ffibuilder.compile(verbose=True, debug=False) 491 | -------------------------------------------------------------------------------- /pysoundio/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | constants.py 3 | 4 | Constants and enumerations. 5 | """ 6 | 7 | from pysoundio._soundio import lib as _lib 8 | 9 | DEFAULT_RING_BUFFER_DURATION = 30 # secs 10 | 11 | BACKENDS = { 12 | _lib.SoundIoBackendNone: 'SoundIoBackendNone', 13 | _lib.SoundIoBackendPulseAudio: 'SoundIoBackendPulseAudio', 14 | _lib.SoundIoBackendJack: 'SoundIoBackendJack', 15 | _lib.SoundIoBackendAlsa: 'SoundIoBackendAlsa', 16 | _lib.SoundIoBackendCoreAudio: 'SoundIoBackendCoreAudio', 17 | _lib.SoundIoBackendWasapi: 'SoundIoBackendWasapi', 18 | _lib.SoundIoBackendDummy: 'SoundIoBackendDummy', 19 | } 20 | 21 | FORMATS = { 22 | _lib.SoundIoFormatS8: 'SoundIoFormatS8', 23 | _lib.SoundIoFormatU8: 'SoundIoFormatU8', 24 | _lib.SoundIoFormatS16LE: 'SoundIoFormatS16LE', 25 | _lib.SoundIoFormatS16BE: 'SoundIoFormatS16BE', 26 | _lib.SoundIoFormatU16LE: 'SoundIoFormatU16LE', 27 | _lib.SoundIoFormatU16BE: 'SoundIoFormatU16BE', 28 | _lib.SoundIoFormatS24LE: 'SoundIoFormatS24LE', 29 | _lib.SoundIoFormatS24BE: 'SoundIoFormatS24BE', 30 | _lib.SoundIoFormatU24LE: 'SoundIoFormatU24LE', 31 | _lib.SoundIoFormatU24BE: 'SoundIoFormatU24BE', 32 | _lib.SoundIoFormatS32LE: 'SoundIoFormatS32LE', 33 | _lib.SoundIoFormatS32BE: 'SoundIoFormatS32BE', 34 | _lib.SoundIoFormatU32LE: 'SoundIoFormatU32LE', 35 | _lib.SoundIoFormatU32BE: 'SoundIoFormatU32BE', 36 | _lib.SoundIoFormatFloat32LE: 'SoundIoFormatFloat32LE', 37 | _lib.SoundIoFormatFloat32BE: 'SoundIoFormatFloat32BE', 38 | _lib.SoundIoFormatFloat64LE: 'SoundIoFormatFloat64LE', 39 | _lib.SoundIoFormatFloat64BE: 'SoundIoFormatFloat64BE', 40 | _lib.SoundIoFormatInvalid: 'SoundIoFormatInvalid' 41 | } 42 | 43 | PRIORITISED_FORMATS = [ 44 | _lib.SoundIoFormatFloat32LE, 45 | _lib.SoundIoFormatFloat32BE, 46 | _lib.SoundIoFormatS32LE, 47 | _lib.SoundIoFormatS32BE, 48 | _lib.SoundIoFormatS24LE, 49 | _lib.SoundIoFormatS24BE, 50 | _lib.SoundIoFormatS16LE, 51 | _lib.SoundIoFormatS16BE, 52 | _lib.SoundIoFormatFloat64LE, 53 | _lib.SoundIoFormatFloat64BE, 54 | _lib.SoundIoFormatU32LE, 55 | _lib.SoundIoFormatU32BE, 56 | _lib.SoundIoFormatU24LE, 57 | _lib.SoundIoFormatU24BE, 58 | _lib.SoundIoFormatU16LE, 59 | _lib.SoundIoFormatU16BE, 60 | _lib.SoundIoFormatS8, 61 | _lib.SoundIoFormatU8, 62 | _lib.SoundIoFormatInvalid, 63 | ] 64 | 65 | PRIORITISED_SAMPLE_RATES = [ 66 | 48000, 67 | 44100, 68 | 96000, 69 | 24000, 70 | 0, 71 | ] 72 | 73 | ARRAY_FORMATS = { 74 | _lib.SoundIoFormatS8: 'b', 75 | _lib.SoundIoFormatU8: 'B', 76 | _lib.SoundIoFormatS16LE: 'h', 77 | _lib.SoundIoFormatS16BE: 'h', 78 | _lib.SoundIoFormatU16LE: 'H', 79 | _lib.SoundIoFormatU16BE: 'H', 80 | _lib.SoundIoFormatS24LE: None, 81 | _lib.SoundIoFormatS24BE: None, 82 | _lib.SoundIoFormatU24LE: None, 83 | _lib.SoundIoFormatU24BE: None, 84 | _lib.SoundIoFormatS32LE: 'l', 85 | _lib.SoundIoFormatS32BE: 'l', 86 | _lib.SoundIoFormatU32LE: 'L', 87 | _lib.SoundIoFormatU32BE: 'L', 88 | _lib.SoundIoFormatFloat32LE: 'f', 89 | _lib.SoundIoFormatFloat32BE: 'f', 90 | _lib.SoundIoFormatFloat64LE: 'd', 91 | _lib.SoundIoFormatFloat64BE: 'd' 92 | } 93 | 94 | SOUNDFILE_FORMATS = { 95 | _lib.SoundIoFormatS8: None, 96 | _lib.SoundIoFormatU8: None, 97 | _lib.SoundIoFormatS16LE: 'int16', 98 | _lib.SoundIoFormatS16BE: 'int16', 99 | _lib.SoundIoFormatU16LE: None, 100 | _lib.SoundIoFormatU16BE: None, 101 | _lib.SoundIoFormatS24LE: None, 102 | _lib.SoundIoFormatS24BE: None, 103 | _lib.SoundIoFormatU24LE: None, 104 | _lib.SoundIoFormatU24BE: None, 105 | _lib.SoundIoFormatS32LE: 'int32', 106 | _lib.SoundIoFormatS32BE: 'int32', 107 | _lib.SoundIoFormatU32LE: None, 108 | _lib.SoundIoFormatU32BE: None, 109 | _lib.SoundIoFormatFloat32LE: 'float32', 110 | _lib.SoundIoFormatFloat32BE: 'float32', 111 | _lib.SoundIoFormatFloat64LE: 'float64', 112 | _lib.SoundIoFormatFloat64BE: 'float64' 113 | } 114 | -------------------------------------------------------------------------------- /pysoundio/pysoundio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pysoundio.py 4 | 5 | Play and Record Sound in Python using libsoundio 6 | 7 | libsoundio is a C library for cross-platform audio input and output. 8 | It is suitable for real-time and consumer software. 9 | 10 | """ 11 | import logging 12 | import threading 13 | import time 14 | 15 | from pysoundio._soundio import ffi as _ffi 16 | from pysoundio._soundio import lib as _lib 17 | from pysoundio import constants 18 | 19 | 20 | class PySoundIoError(Exception): 21 | pass 22 | 23 | 24 | class _InputProcessingThread(threading.Thread): 25 | 26 | def __init__(self, parent, *args, **kwargs): 27 | super().__init__(*args, **kwargs) 28 | 29 | self.buffer = parent.input['buffer'] 30 | self.callback = parent.input['read_callback'] 31 | self.bytes_per_frame = parent.input['bytes_per_frame'] 32 | 33 | self.daemon = True 34 | self.running = True 35 | self.start() 36 | 37 | def run(self): 38 | """ 39 | When there is data ready in the input buffer, 40 | pass it to the user callback. 41 | """ 42 | while self.running: 43 | fill_bytes = _lib.soundio_ring_buffer_fill_count(self.buffer) 44 | if fill_bytes > 0: 45 | read_buf = _lib.soundio_ring_buffer_read_ptr(self.buffer) 46 | data = bytearray(fill_bytes) 47 | _ffi.memmove(data, read_buf, fill_bytes) 48 | if self.callback: 49 | self.callback(data=data, length=int(fill_bytes / self.bytes_per_frame)) 50 | _lib.soundio_ring_buffer_advance_read_ptr(self.buffer, fill_bytes) 51 | time.sleep(0.001) 52 | 53 | def stop(self): 54 | self.running = False 55 | 56 | 57 | class _OutputProcessingThread(threading.Thread): 58 | 59 | def __init__(self, parent, *args, **kwargs): 60 | super().__init__(*args, **kwargs) 61 | 62 | self.buffer = parent.output['buffer'] 63 | self.callback = parent.output['write_callback'] 64 | self.bytes_per_frame = parent.output['bytes_per_frame'] 65 | self.sample_rate = parent.output['sample_rate'] 66 | self.block_size = parent.output['block_size'] 67 | 68 | self.to_read = 0 69 | self.running = True 70 | self.daemon = True 71 | self.start() 72 | 73 | def run(self): 74 | """ 75 | Request output data from user callback when there is 76 | free space in the buffer. 77 | """ 78 | while self.running: 79 | if self.to_read > 0: 80 | data = bytearray(self.block_size * self.bytes_per_frame) 81 | free_bytes = _lib.soundio_ring_buffer_free_count(self.buffer) 82 | if free_bytes > len(data): 83 | if self.callback: 84 | self.callback(data=data, length=self.block_size) 85 | 86 | write_buf = _lib.soundio_ring_buffer_write_ptr(self.buffer) 87 | _ffi.memmove(write_buf, data, len(data)) 88 | _lib.soundio_ring_buffer_advance_write_ptr(self.buffer, len(data)) 89 | 90 | with threading.Lock(): 91 | self.to_read -= 1 92 | time.sleep(0.001) 93 | 94 | def stop(self): 95 | self.running = False 96 | 97 | 98 | class PySoundIo: 99 | 100 | def __init__(self, backend=None): 101 | """ 102 | Initialise PySoundIo. 103 | Connect to a specific backend, or the default. 104 | 105 | Parameters 106 | ---------- 107 | backend: (SoundIoBackend) see `Backends`_. (optional) 108 | """ 109 | self.backend = backend 110 | 111 | self.input = {'device': None, 'stream': None, 'buffer': None, 'read_callback': None, 'thread': None} 112 | self.output = {'device': None, 'stream': None, 'buffer': None, 'write_callback': None, 'thread': None} 113 | 114 | self.logger = logging.getLogger(__name__) 115 | 116 | self._soundio = _lib.soundio_create() 117 | if not self._soundio: 118 | raise PySoundIoError('Out of memory') 119 | 120 | if backend: 121 | self._check(_lib.soundio_connect_backend(self._soundio, backend)) 122 | else: 123 | self._check(_lib.soundio_connect(self._soundio)) 124 | 125 | self._userdata = _ffi.new_handle(self) 126 | self.flush() 127 | 128 | def close(self): 129 | """ 130 | Clean up allocated memory 131 | Close libsoundio connections 132 | """ 133 | self.logger.info('Closing down threads...') 134 | 135 | if self.input['thread']: 136 | self.input['thread'].stop() 137 | while self.input['thread'].is_alive(): 138 | time.sleep(0.001) 139 | if self.output['thread']: 140 | self.output['thread'].stop() 141 | while self.output['thread'].is_alive(): 142 | time.sleep(0.001) 143 | 144 | self.logger.info('Closing down streams...') 145 | if self.input['stream']: 146 | _lib.soundio_instream_destroy(self.input['stream']) 147 | del self.input['stream'] 148 | if self.output['stream']: 149 | _lib.soundio_outstream_destroy(self.output['stream']) 150 | del self.output['stream'] 151 | 152 | if self.input['buffer']: 153 | _lib.soundio_ring_buffer_destroy(self.input['buffer']) 154 | del self.input['buffer'] 155 | if self.output['buffer']: 156 | _lib.soundio_ring_buffer_destroy(self.output['buffer']) 157 | del self.output['buffer'] 158 | 159 | if self.input['device']: 160 | _lib.soundio_device_unref(self.input['device']) 161 | del self.input['device'] 162 | if self.output['device']: 163 | _lib.soundio_device_unref(self.output['device']) 164 | del self.output['device'] 165 | 166 | if self._soundio: 167 | _lib.soundio_disconnect(self._soundio) 168 | _lib.soundio_destroy(self._soundio) 169 | del self._soundio 170 | 171 | def flush(self): 172 | """ 173 | Atomically update information for all connected devices. 174 | """ 175 | _lib.soundio_flush_events(self._soundio) 176 | 177 | @property 178 | def version(self): 179 | """ 180 | Returns the current version of libsoundio 181 | """ 182 | return _ffi.string(_lib.soundio_version_string()).decode() 183 | 184 | def _check(self, code): 185 | """ 186 | Returns an error message associated with the return code 187 | """ 188 | if code != _lib.SoundIoErrorNone: 189 | raise PySoundIoError(_ffi.string(_lib.soundio_strerror(code)).decode()) 190 | 191 | @property 192 | def backend_count(self): 193 | """ 194 | Returns the number of available backends. 195 | """ 196 | return _lib.soundio_backend_count(self._soundio) 197 | 198 | def get_default_input_device(self): 199 | """ 200 | Returns default input device 201 | 202 | Returns 203 | ------- 204 | PySoundIoDevice input device 205 | 206 | Raises 207 | ------ 208 | PySoundIoError if the input device is not available 209 | """ 210 | device_id = _lib.soundio_default_input_device_index(self._soundio) 211 | return self.get_input_device(device_id) 212 | 213 | def get_input_device(self, device_id): 214 | """ 215 | Return an input device by index 216 | 217 | Parameters 218 | ---------- 219 | device_id: (int) input device index 220 | 221 | Returns 222 | ------- 223 | PySoundIoDevice input device 224 | 225 | Raises 226 | ------ 227 | PySoundIoError if an invalid device id is used, or device is unavailable 228 | """ 229 | if device_id < 0 or device_id > _lib.soundio_input_device_count(self._soundio): 230 | raise PySoundIoError('Invalid input device id') 231 | self.input['device'] = _lib.soundio_get_input_device(self._soundio, device_id) 232 | return self.input['device'] 233 | 234 | def get_default_output_device(self): 235 | """ 236 | Returns default output device 237 | 238 | Returns 239 | ------- 240 | PySoundIoDevice output device 241 | 242 | Raises 243 | ------ 244 | PySoundIoError if the output device is not available 245 | """ 246 | device_id = _lib.soundio_default_output_device_index(self._soundio) 247 | return self.get_output_device(device_id) 248 | 249 | def get_output_device(self, device_id): 250 | """ 251 | Return an output device by index 252 | 253 | Parameters 254 | ---------- 255 | device_id: (int) output device index 256 | 257 | Returns 258 | ------- 259 | PySoundIoDevice output device 260 | 261 | Raises 262 | ------ 263 | PySoundIoError if an invalid device id is used, or device is unavailable 264 | """ 265 | if device_id < 0 or device_id > _lib.soundio_output_device_count(self._soundio): 266 | raise PySoundIoError('Invalid output device id') 267 | self.output['device'] = _lib.soundio_get_output_device(self._soundio, device_id) 268 | return self.output['device'] 269 | 270 | def list_devices(self): 271 | """ 272 | Return a list of available devices 273 | 274 | Returns 275 | ------- 276 | (list)(dict) containing information on available input / output devices. 277 | """ 278 | output_count = _lib.soundio_output_device_count(self._soundio) 279 | input_count = _lib.soundio_input_device_count(self._soundio) 280 | 281 | default_output = _lib.soundio_default_output_device_index(self._soundio) 282 | default_input = _lib.soundio_default_input_device_index(self._soundio) 283 | 284 | input_devices = [] 285 | output_devices = [] 286 | 287 | for i in range(0, input_count): 288 | device = _lib.soundio_get_input_device(self._soundio, i) 289 | input_devices.append({ 290 | 'id': _ffi.string(device.id).decode(), 291 | 'name': _ffi.string(device.name).decode(), 292 | 'is_raw': device.is_raw, 293 | 'is_default': default_input == i, 294 | 'sample_rates': self.get_sample_rates(device), 295 | 'formats': self.get_formats(device), 296 | 'layouts': self.get_layouts(device), 297 | 'software_latency_min': device.software_latency_min, 298 | 'software_latency_max': device.software_latency_max, 299 | 'software_latency_current': device.software_latency_current, 300 | 'probe_error': PySoundIoError( 301 | _ffi.string(_lib.soundio_strerror(device.probe_error)).decode() 302 | if device.probe_error else None) 303 | }) 304 | _lib.soundio_device_unref(device) 305 | 306 | for i in range(0, output_count): 307 | device = _lib.soundio_get_output_device(self._soundio, i) 308 | output_devices.append({ 309 | 'id': _ffi.string(device.id).decode(), 310 | 'name': _ffi.string(device.name).decode(), 311 | 'is_raw': device.is_raw, 312 | 'is_default': default_output == i, 313 | 'sample_rates': self.get_sample_rates(device), 314 | 'formats': self.get_formats(device), 315 | 'layouts': self.get_layouts(device), 316 | 'software_latency_min': device.software_latency_min, 317 | 'software_latency_max': device.software_latency_max, 318 | 'software_latency_current': device.software_latency_current, 319 | 'probe_error': PySoundIoError( 320 | _ffi.string(_lib.soundio_strerror(device.probe_error)).decode() 321 | if device.probe_error else None) 322 | }) 323 | _lib.soundio_device_unref(device) 324 | 325 | self.logger.info('%d devices found' % (input_count + output_count)) 326 | return (input_devices, output_devices) 327 | 328 | def get_layouts(self, device): 329 | """ 330 | Return a list of available layouts for a device 331 | 332 | Parameters 333 | ---------- 334 | device: (SoundIoDevice) device object 335 | 336 | Returns 337 | ------- 338 | (dict) Dictionary of available channel layouts for a device 339 | """ 340 | current = device.current_layout 341 | layouts = { 342 | 'current': { 343 | 'name': _ffi.string(current.name).decode() if current.name else 'None' 344 | }, 345 | 'available': [] 346 | } 347 | for idx in range(0, device.layout_count): 348 | layouts['available'].append({ 349 | 'name': (_ffi.string(device.layouts[idx].name).decode() if 350 | device.layouts[idx].name else 'None'), 351 | 'channel_count': device.layouts[idx].channel_count 352 | }) 353 | return layouts 354 | 355 | def get_sample_rates(self, device): 356 | """ 357 | Return a list of available sample rates for a device 358 | 359 | Parameters 360 | ---------- 361 | device: (SoundIoDevice) device object 362 | 363 | Returns 364 | ------- 365 | (dict) Dictionary of available sample rates for a device 366 | """ 367 | sample_rates = {'current': device.sample_rate_current, 'available': []} 368 | for s in range(0, device.sample_rate_count): 369 | sample_rates['available'].append({ 370 | 'min': device.sample_rates[s].min, 371 | 'max': device.sample_rates[s].max 372 | }) 373 | return sample_rates 374 | 375 | def get_formats(self, device): 376 | """ 377 | Return a list of available formats for a device 378 | 379 | Parameters 380 | ---------- 381 | device: (SoundIoDevice) device object 382 | 383 | Returns 384 | ------- 385 | (dict) Dictionary of available formats for a device 386 | """ 387 | formats = {'current': device.current_format, 'available': []} 388 | for r in range(0, device.format_count): 389 | formats['available'].append(constants.FORMATS[device.formats[r]]) 390 | return formats 391 | 392 | def supports_sample_rate(self, device, rate): 393 | """ 394 | Check the sample rate is supported by the selected device. 395 | 396 | Parameters 397 | ---------- 398 | device: (SoundIoDevice) device object 399 | rate (int): sample rate 400 | 401 | Returns 402 | ------- 403 | (bool) True if sample rate is supported for this device 404 | """ 405 | return bool(_lib.soundio_device_supports_sample_rate(device, rate)) 406 | 407 | def get_default_sample_rate(self, device): 408 | """ 409 | Get the best sample rate. 410 | 411 | Parameters 412 | ---------- 413 | device: (SoundIoDevice) device object 414 | 415 | Returns 416 | ------- 417 | (int) The best available sample rate 418 | """ 419 | sample_rate = None 420 | for rate in constants.PRIORITISED_SAMPLE_RATES: 421 | if self.supports_sample_rate(device, rate): 422 | sample_rate = rate 423 | break 424 | if not sample_rate: 425 | sample_rate = device.sample_rates.max 426 | return sample_rate 427 | 428 | def supports_format(self, device, format): 429 | """ 430 | Check the format is supported by the selected device. 431 | 432 | Parameters 433 | ---------- 434 | device: (SoundIoDevice) device object 435 | format: (SoundIoFormat) see `Formats`_. 436 | 437 | Returns 438 | ------- 439 | (bool) True if the format is supported for this device 440 | """ 441 | return bool(_lib.soundio_device_supports_format(device, format)) 442 | 443 | def get_default_format(self, device): 444 | """ 445 | Get the best format value. 446 | 447 | Parameters 448 | ---------- 449 | device: (SoundIoDevice) device object 450 | 451 | Returns 452 | ------ 453 | (SoundIoFormat) The best available format 454 | """ 455 | dtype = _lib.SoundIoFormatInvalid 456 | for fmt in constants.PRIORITISED_FORMATS: 457 | if self.supports_format(device, fmt): 458 | dtype = fmt 459 | break 460 | if dtype == _lib.SoundIoFormatInvalid: 461 | raise PySoundIoError('Incompatible sample formats') 462 | return dtype 463 | 464 | def sort_channel_layouts(self, device): 465 | """ 466 | Sorts channel layouts by channel count, descending 467 | 468 | Parameters 469 | ---------- 470 | device: (SoundIoDevice) device object 471 | """ 472 | _lib.soundio_device_sort_channel_layouts(device) 473 | 474 | def _get_default_layout(self, channels): 475 | """ 476 | Get default builtin channel layout for the given number of channels 477 | 478 | Parameters 479 | ---------- 480 | channel_count: (int) desired number of channels 481 | """ 482 | return _lib.soundio_channel_layout_get_default(channels) 483 | 484 | def get_bytes_per_frame(self, format, channels): 485 | """ 486 | Get the number of bytes per frame 487 | 488 | Parameters 489 | ---------- 490 | format: (SoundIoFormat) format 491 | channels: (int) number of channels 492 | 493 | Returns 494 | ------- 495 | (int) number of bytes per frame 496 | """ 497 | return _lib.soundio_get_bytes_per_sample(format) * channels 498 | 499 | def get_bytes_per_sample(self, format): 500 | """ 501 | Get the number of bytes per sample 502 | 503 | Parameters 504 | ---------- 505 | format: (SoundIoFormat) format 506 | 507 | Returns 508 | ------- 509 | (int) number of bytes per sample 510 | """ 511 | return _lib.soundio_get_bytes_per_sample(format) 512 | 513 | def get_bytes_per_second(self, format, channels, sample_rate): 514 | """ 515 | Get the number of bytes per second 516 | 517 | Parameters 518 | ---------- 519 | format: (SoundIoFormat) format 520 | channels (int) number of channels 521 | sample_rate (int) sample rate 522 | 523 | Returns 524 | ------- 525 | (int) number of bytes per second 526 | """ 527 | return self.get_bytes_per_frame(format, channels) * sample_rate 528 | 529 | def _create_input_ring_buffer(self, capacity): 530 | """ 531 | Creates ring buffer with the capacity to hold 30 seconds of data, 532 | by default. 533 | """ 534 | self.input['buffer'] = _lib.soundio_ring_buffer_create(self._soundio, capacity) 535 | return self.input['buffer'] 536 | 537 | def _create_output_ring_buffer(self, capacity): 538 | """ 539 | Creates ring buffer with the capacity to hold 30 seconds of data, 540 | by default. 541 | """ 542 | self.output['buffer'] = _lib.soundio_ring_buffer_create(self._soundio, capacity) 543 | return self.output['buffer'] 544 | 545 | def _create_input_stream(self): 546 | """ 547 | Allocates memory and sets defaults for input stream 548 | """ 549 | self.input['stream'] = _lib.soundio_instream_create(self.input['device']) 550 | if not self.input['stream']: 551 | raise PySoundIoError('Out of memory') 552 | 553 | self.input['stream'].userdata = self._userdata 554 | self.input['stream'].read_callback = _lib._read_callback 555 | self.input['stream'].overflow_callback = _lib._overflow_callback 556 | self.input['stream'].error_callback = _lib._input_error_callback 557 | 558 | layout = self._get_default_layout(self.input['channels']) 559 | if layout: 560 | self.input['stream'].layout = layout[0] 561 | else: 562 | raise RuntimeError('Failed to find a channel layout for %d channels' % self.input['channels']) 563 | 564 | self.input['stream'].format = self.input['format'] 565 | self.input['stream'].sample_rate = self.input['sample_rate'] 566 | if self.input['block_size']: 567 | self.input['stream'].software_latency = float(self.input['block_size']) / self.input['sample_rate'] 568 | 569 | return self.input['stream'] 570 | 571 | def _open_input_stream(self): 572 | """ 573 | Open an input stream. 574 | """ 575 | self._check(_lib.soundio_instream_open(self.input['stream'])) 576 | 577 | def _start_input_stream(self): 578 | """ 579 | Start an input stream running. 580 | """ 581 | self._check(_lib.soundio_instream_start(self.input['stream'])) 582 | 583 | def pause_input_stream(self, pause): 584 | """ 585 | Pause input stream 586 | 587 | Parameters 588 | ---------- 589 | pause: (bool) True to pause, False to unpause 590 | """ 591 | self._check(_lib.soundio_instream_pause(self.input['stream'], pause)) 592 | 593 | def get_input_latency(self, out_latency): 594 | """ 595 | Obtain the number of seconds that the next frame of sound 596 | being captured will take to arrive in the buffer, 597 | plus the amount of time that is represented in the buffer. 598 | 599 | Parameters 600 | ---------- 601 | out_latency: (float) output latency in seconds 602 | """ 603 | c_latency = _ffi.new('double *', out_latency) 604 | return _lib.soundio_instream_get_latency(self.input['stream'], c_latency) 605 | 606 | def start_input_stream(self, device_id=None, 607 | sample_rate=None, dtype=None, 608 | block_size=None, channels=None, 609 | read_callback=None, overflow_callback=None): 610 | """ 611 | Creates input stream, and sets parameters. Then allocates 612 | a ring buffer and starts the stream. 613 | 614 | The read callback is called in an audio processing thread, 615 | when a block of data is read from the microphone. Data is 616 | passed from the ring buffer to the callback to process. 617 | 618 | Parameters 619 | ---------- 620 | device_id: (int) input device id 621 | sample_rate: (int) desired sample rate (optional) 622 | dtype: (SoundIoFormat) desired format, see `Formats`_. (optional) 623 | block_size: (int) desired block size (optional) 624 | channels: (int) number of channels [1: mono, 2: stereo] (optional) 625 | read_callback: (fn) function to call with data, the function must have 626 | the arguments data and length. See record example 627 | overflow_callback: (fn) function to call if data is not being read fast enough 628 | 629 | Raises 630 | ------ 631 | PySoundIoError if any invalid parameters are used 632 | 633 | Notes 634 | ----- 635 | An example read callback 636 | 637 | .. code-block:: python 638 | :linenos: 639 | 640 | # Note: `length` is the number of samples per channel 641 | def read_callback(data: bytearray, length: int): 642 | wav.write(data) 643 | 644 | Overflow callback example 645 | 646 | .. code-block:: python 647 | :linenos: 648 | 649 | def overflow_callback(): 650 | print('buffer overflow') 651 | """ 652 | self.input['sample_rate'] = sample_rate 653 | self.input['format'] = dtype 654 | self.input['block_size'] = block_size 655 | self.input['channels'] = channels 656 | self.input['read_callback'] = read_callback 657 | self.input['overflow_callback'] = overflow_callback 658 | 659 | if device_id is not None: 660 | self.input['device'] = self.get_input_device(device_id) 661 | else: 662 | self.input['device'] = self.get_default_input_device() 663 | 664 | self.logger.info('Input Device: %s' % _ffi.string(self.input['device'].name).decode()) 665 | self.sort_channel_layouts(self.input['device']) 666 | 667 | if self.input['sample_rate']: 668 | if not self.supports_sample_rate(self.input['device'], self.input['sample_rate']): 669 | raise PySoundIoError('Invalid sample rate: %d' % self.input['sample_rate']) 670 | else: 671 | self.input['sample_rate'] = self.get_default_sample_rate(self.input['device']) 672 | 673 | if self.input['format']: 674 | if not self.supports_format(self.input['device'], self.input['format']): 675 | raise PySoundIoError('Invalid format: %s interleaved' % 676 | (_ffi.string(_lib.soundio_format_string(self.input['format'])).decode())) 677 | else: 678 | self.input['format'] = self.get_default_format(self.input['device']) 679 | 680 | self._create_input_stream() 681 | self._open_input_stream() 682 | self.input['bytes_per_frame'] = self.get_bytes_per_frame(self.input['format'], channels) 683 | capacity = int(constants.DEFAULT_RING_BUFFER_DURATION * 684 | self.input['stream'].sample_rate * self.input['bytes_per_frame']) 685 | self._create_input_ring_buffer(capacity) 686 | 687 | if self.input['stream'].layout_error: 688 | raise RuntimeError('Layout error') 689 | 690 | layout_name = _ffi.string(self.input['stream'].layout.name).decode() 691 | self.logger.info('Created input stream with a %s layout', layout_name) 692 | 693 | self.input['thread'] = _InputProcessingThread(parent=self) 694 | self._start_input_stream() 695 | self.flush() 696 | 697 | def _create_output_stream(self): 698 | """ 699 | Allocates memory and sets defaults for output stream 700 | """ 701 | self.output['stream'] = _lib.soundio_outstream_create(self.output['device']) 702 | if not self.output['stream']: 703 | raise PySoundIoError('Out of memory') 704 | 705 | self.output['stream'].userdata = self._userdata 706 | self.output['stream'].write_callback = _lib._write_callback 707 | self.output['stream'].underflow_callback = _lib._underflow_callback 708 | self.output['stream'].error_callback = _lib._output_error_callback 709 | 710 | layout = self._get_default_layout(self.output['channels']) 711 | if layout: 712 | self.output['stream'].layout = layout[0] 713 | else: 714 | raise RuntimeError('Failed to find a channel layout for %d channels' % self.output['channels']) 715 | 716 | self.output['stream'].format = self.output['format'] 717 | self.output['stream'].sample_rate = self.output['sample_rate'] 718 | if self.output['block_size']: 719 | self.output['stream'].software_latency = float(self.output['block_size']) / self.output['sample_rate'] 720 | 721 | return self.output['stream'] 722 | 723 | def _open_output_stream(self): 724 | """ 725 | Open an output stream. 726 | """ 727 | self._check(_lib.soundio_outstream_open(self.output['stream'])) 728 | self.output['block_size'] = int(self.output['stream'].software_latency / self.output['sample_rate']) 729 | 730 | def _start_output_stream(self): 731 | """ 732 | Start an output stream running. 733 | """ 734 | self._check(_lib.soundio_outstream_start(self.output['stream'])) 735 | 736 | def pause_output_stream(self, pause): 737 | """ 738 | Pause output stream 739 | 740 | Parameters 741 | ---------- 742 | pause: (bool) True to pause, False to unpause 743 | """ 744 | self._check(_lib.soundio_outstream_pause(self.output['stream'], pause)) 745 | 746 | def _clear_output_buffer(self): 747 | """ 748 | Clear the output buffer 749 | """ 750 | if self.output['buffer']: 751 | _lib.soundio_ring_buffer_clear(self.output['buffer']) 752 | 753 | def get_output_latency(self, out_latency): 754 | """ 755 | Obtain the total number of seconds that the next frame written 756 | will take to become audible. 757 | 758 | Parameters 759 | ---------- 760 | out_latency: (float) output latency in seconds 761 | """ 762 | c_latency = _ffi.new('double *', out_latency) 763 | return _lib.soundio_outstream_get_latency(self.output['stream'], c_latency) 764 | 765 | def start_output_stream(self, device_id=None, 766 | sample_rate=None, dtype=None, 767 | block_size=None, channels=None, 768 | write_callback=None, underflow_callback=None): 769 | """ 770 | Creates output stream, and sets parameters. Then allocates 771 | a ring buffer and starts the stream. 772 | 773 | The write callback is called in an audio processing thread, 774 | when a block of data should be passed to the speakers. Data is 775 | added to the ring buffer to process. 776 | 777 | Parameters 778 | ---------- 779 | device_id: (int) output device id 780 | sample_rate: (int) desired sample rate (optional) 781 | dtype: (SoundIoFormat) desired format, see `Formats`_. (optional) 782 | block_size: (int) desired block size (optional) 783 | channels: (int) number of channels [1: mono, 2: stereo] (optional) 784 | write_callback: (fn) function to call with data, the function must have 785 | the arguments data and length. 786 | underflow_callback: (fn) function to call if data is not being written fast enough 787 | 788 | Raises 789 | ------ 790 | PySoundIoError if any invalid parameters are used 791 | 792 | Notes 793 | ----- 794 | An example write callback 795 | 796 | .. code-block:: python 797 | :linenos: 798 | 799 | # Note: `length` is the number of samples per channel 800 | def write_callback(data: bytearray, length: int): 801 | outdata = ar.array('f', [0] * length) 802 | for value in outdata: 803 | outdata = 1.0 804 | data[:] = outdata.tostring() 805 | 806 | Underflow callback example 807 | 808 | .. code-block:: python 809 | :linenos: 810 | 811 | def underflow_callback(): 812 | print('buffer underflow') 813 | """ 814 | self.output['sample_rate'] = sample_rate 815 | self.output['format'] = dtype 816 | self.output['block_size'] = block_size 817 | self.output['channels'] = channels 818 | self.output['write_callback'] = write_callback 819 | self.output['underflow_callback'] = underflow_callback 820 | 821 | if device_id is not None: 822 | self.output['device'] = self.get_output_device(device_id) 823 | else: 824 | self.output['device'] = self.get_default_output_device() 825 | 826 | self.logger.info('Input Device: %s' % _ffi.string(self.output['device'].name).decode()) 827 | self.sort_channel_layouts(self.output['device']) 828 | 829 | if self.output['sample_rate']: 830 | if not self.supports_sample_rate(self.output['device'], self.output['sample_rate']): 831 | raise PySoundIoError('Invalid sample rate: %d' % self.output['sample_rate']) 832 | else: 833 | self.output['sample_rate'] = self.get_default_sample_rate(self.output['device']) 834 | 835 | if self.output['format']: 836 | if not self.supports_format(self.output['device'], self.output['format']): 837 | raise PySoundIoError('Invalid format: %s interleaved' % 838 | (_ffi.string(_lib.soundio_format_string(self.output['format'])).decode())) 839 | else: 840 | self.output['format'] = self.get_default_format(self.output['device']) 841 | 842 | self._create_output_stream() 843 | self._open_output_stream() 844 | self.output['bytes_per_frame'] = self.get_bytes_per_frame(self.output['format'], channels) 845 | capacity = int(constants.DEFAULT_RING_BUFFER_DURATION * 846 | self.output['stream'].sample_rate * self.output['bytes_per_frame']) 847 | self._create_output_ring_buffer(capacity) 848 | self._clear_output_buffer() 849 | 850 | if self.output['stream'].layout_error: 851 | raise RuntimeError('Layout error') 852 | 853 | layout_name = _ffi.string(self.output['stream'].layout.name).decode() 854 | self.logger.info('Created output stream with a %s layout', layout_name) 855 | 856 | self.output['thread'] = _OutputProcessingThread(parent=self) 857 | self._start_output_stream() 858 | self.flush() 859 | 860 | 861 | @_ffi.def_extern() 862 | def _write_callback(output_stream, 863 | frame_count_min, 864 | frame_count_max): 865 | """ 866 | Called internally when the output requires some data 867 | """ 868 | self = _ffi.from_handle(output_stream.userdata) 869 | 870 | frame_count = 0 871 | read_ptr = _lib.soundio_ring_buffer_read_ptr(self.output['buffer']) 872 | fill_bytes = _lib.soundio_ring_buffer_fill_count(self.output['buffer']) 873 | fill_count = fill_bytes / output_stream.bytes_per_frame 874 | 875 | read_count = min(frame_count_max, fill_count) 876 | frames_left = read_count 877 | 878 | if frame_count_min > fill_count: 879 | frames_left = frame_count_min 880 | while frames_left > 0: 881 | frame_count = frames_left 882 | if frame_count <= 0: 883 | return 884 | 885 | frame_count_ptr = _ffi.new('int *', frame_count) 886 | areas_ptr = _ffi.new('struct SoundIoChannelArea **') 887 | self._check( 888 | _lib.soundio_outstream_begin_write(output_stream, 889 | areas_ptr, 890 | frame_count_ptr) 891 | ) 892 | if frame_count_ptr[0] <= 0: 893 | return 894 | 895 | num_bytes = output_stream.bytes_per_sample * output_stream.layout.channel_count * frame_count_ptr[0] 896 | fill_bytes = bytearray(b'\x00' * num_bytes) 897 | _ffi.memmove(areas_ptr[0][0].ptr, fill_bytes, num_bytes) 898 | 899 | self._check(_lib.soundio_outstream_end_write(output_stream)) 900 | frames_left -= frame_count_ptr[0] 901 | 902 | while frames_left > 0: 903 | frame_count = int(frames_left) 904 | frame_count_ptr = _ffi.new('int *', frame_count) 905 | areas_ptr = _ffi.new('struct SoundIoChannelArea **') 906 | self._check( 907 | _lib.soundio_outstream_begin_write(output_stream, areas_ptr, frame_count_ptr) 908 | ) 909 | if frame_count_ptr[0] <= 0: 910 | break 911 | 912 | num_bytes = output_stream.bytes_per_sample * output_stream.layout.channel_count * frame_count_ptr[0] 913 | _ffi.memmove(areas_ptr[0][0].ptr, read_ptr, num_bytes) 914 | read_ptr += num_bytes 915 | 916 | self._check(_lib.soundio_outstream_end_write(output_stream)) 917 | frames_left -= frame_count_ptr[0] 918 | 919 | _lib.soundio_ring_buffer_advance_read_ptr( 920 | self.output['buffer'], 921 | int(read_count * output_stream.bytes_per_frame) 922 | ) 923 | 924 | with threading.Lock(): 925 | self.output['thread'].block_size = frame_count_max 926 | self.output['thread'].to_read += 1 927 | 928 | 929 | @_ffi.def_extern() 930 | def _underflow_callback(output_stream): 931 | """ 932 | Called internally when the sound device runs out of 933 | buffered audio data to play. 934 | """ 935 | logger = logging.getLogger(__name__) 936 | logger.error('Output underflow') 937 | 938 | self = _ffi.from_handle(output_stream.userdata) 939 | if self.output['underflow_callback']: 940 | self.output['underflow_callback']() 941 | 942 | 943 | @_ffi.def_extern() 944 | def _output_error_callback(output_stream, 945 | error_code): 946 | """ 947 | Called internally when an error occurs in the 948 | output stream. 949 | """ 950 | logger = logging.getLogger(__name__) 951 | logger.error(_ffi.string(_lib.soundio_strerror(error_code)).decode()) 952 | 953 | 954 | @_ffi.def_extern() 955 | def _read_callback(input_stream, 956 | frame_count_min, 957 | frame_count_max): 958 | """ 959 | Called internally when there is input data available. 960 | """ 961 | self = _ffi.from_handle(input_stream.userdata) 962 | 963 | write_ptr = _lib.soundio_ring_buffer_write_ptr(self.input['buffer']) 964 | free_bytes = _lib.soundio_ring_buffer_free_count(self.input['buffer']) 965 | 966 | free_count = free_bytes / input_stream.bytes_per_frame 967 | 968 | if free_count < frame_count_min: 969 | logger = logging.getLogger(__name__) 970 | logger.critical('Ring buffer overflow') 971 | 972 | write_frames = min(free_count, frame_count_max) 973 | frames_left = write_frames 974 | 975 | while True: 976 | frame_count = frames_left 977 | frame_count_ptr = _ffi.new('int *', int(frame_count)) 978 | areas_ptr = _ffi.new('struct SoundIoChannelArea **') 979 | 980 | self._check( 981 | _lib.soundio_instream_begin_read(input_stream, 982 | areas_ptr, 983 | frame_count_ptr) 984 | ) 985 | if not frame_count_ptr[0]: 986 | break 987 | if not areas_ptr[0]: 988 | # Due to an overflow there is a hole. 989 | # Fill the ring buffer with silence for the size of the hole. 990 | fill = bytearray(b'\x00' * frame_count_ptr[0] * input_stream.bytes_per_frame) 991 | _ffi.memmove(write_ptr, fill, len(fill)) 992 | else: 993 | num_bytes = input_stream.bytes_per_sample * input_stream.layout.channel_count * frame_count_ptr[0] 994 | _ffi.memmove(write_ptr, areas_ptr[0][0].ptr, num_bytes) 995 | write_ptr += num_bytes 996 | 997 | self._check(_lib.soundio_instream_end_read(input_stream)) 998 | frames_left -= frame_count_ptr[0] 999 | if frames_left <= 0: 1000 | break 1001 | 1002 | advance_bytes = int(write_frames * input_stream.bytes_per_frame) 1003 | _lib.soundio_ring_buffer_advance_write_ptr(self.input['buffer'], advance_bytes) 1004 | 1005 | 1006 | @_ffi.def_extern() 1007 | def _overflow_callback(input_stream): 1008 | """ 1009 | Called internally when the sound device buffer is full, 1010 | yet there is more captured audio to put in it. 1011 | """ 1012 | logger = logging.getLogger(__name__) 1013 | logger.error('Input overflow') 1014 | 1015 | self = _ffi.from_handle(input_stream.userdata) 1016 | if self.input['overflow_callback']: 1017 | self.input['overflow_callback']() 1018 | 1019 | 1020 | @_ffi.def_extern() 1021 | def _input_error_callback(input_stream, 1022 | error_code): 1023 | """ 1024 | Called internally when an error occurs in the 1025 | input stream. 1026 | """ 1027 | logger = logging.getLogger(__name__) 1028 | logger.error(_ffi.string(_lib.soundio_strerror(error_code)).decode()) 1029 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [build_sphinx] 5 | source-dir = docs 6 | build-dir = docs/_build 7 | all_files = 1 8 | 9 | [upload_sphinx] 10 | upload-dir = docs/_build/html 11 | 12 | [flake8] 13 | max-line-length = 120 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | PySoundIo 3 | 4 | A robust, cross-platform solution for real-time audio. 5 | """ 6 | from setuptools import setup 7 | 8 | __version__ = '2.0.0' 9 | 10 | setup( 11 | name='pysoundio', 12 | version=__version__, 13 | description='Python wrapper for libsoundio', 14 | long_description='A robust, cross-platform solution for real-time audio', 15 | license='MIT', 16 | author='Joe Todd', 17 | author_email='joextodd@gmail.com', 18 | url='http://pysoundio.readthedocs.io/en/latest/', 19 | download_url='https://github.com/joextodd/pysoundio/archive/' + __version__ + '.tar.gz', 20 | include_package_data=True, 21 | packages=['pysoundio', 'examples'], 22 | setup_requires=['cffi>=1.4.0'], 23 | install_requires=['cffi>=1.4.0'], 24 | python_requires='>=3.6', 25 | cffi_modules=['pysoundio/builder/soundio.py:ffibuilder'], 26 | test_suite='tests', 27 | entry_points={ 28 | 'console_scripts': [ 29 | 'pysio_devices = examples.devices:main', 30 | 'pysio_play = examples.play:main', 31 | 'pysio_record = examples.record:main', 32 | 'pysio_sine = examples.sine:main', 33 | ], 34 | }, 35 | keywords=['audio', 'sound', 'stream'], 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Intended Audience :: Developers', 39 | 'Intended Audience :: Science/Research', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Programming Language :: Python :: 3.8', 45 | 'Programming Language :: Python :: 3.9', 46 | 'Topic :: Multimedia :: Sound/Audio', 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joextodd/pysoundio/c8962ae74f6bec056c2991bd5c927ca28cf128a2/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_pysoundio.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_pysoundio.py 3 | 4 | PySoundIo Test Suite 5 | """ 6 | import platform 7 | import threading 8 | import time 9 | import unittest 10 | 11 | import pysoundio 12 | from pysoundio._soundio import lib as _lib 13 | from pysoundio._soundio import ffi as _ffi 14 | 15 | 16 | class TestPySoundIo(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.sio = pysoundio.PySoundIo( 20 | backend=pysoundio.SoundIoBackendDummy) 21 | 22 | self.sio.input['channels'] = 2 23 | self.sio.input['sample_rate'] = 44100 24 | self.sio.input['format'] = pysoundio.SoundIoFormatFloat32LE 25 | self.sio.input['block_size'] = None 26 | 27 | self.sio.output['channels'] = 2 28 | self.sio.output['sample_rate'] = 44100 29 | self.sio.output['format'] = pysoundio.SoundIoFormatFloat32LE 30 | self.sio.output['block_size'] = None 31 | 32 | self.stream_kwargs = { 33 | 'device_id': 0, 34 | 'sample_rate': 44100, 35 | 'block_size': 128, 36 | 'dtype': pysoundio.SoundIoFormatFloat32LE, 37 | 'channels': 2, 38 | } 39 | 40 | self.to_read = False 41 | self.callback_called = False 42 | 43 | def tearDown(self): 44 | self.sio.close() 45 | 46 | # - Device API 47 | 48 | def test_version(self): 49 | self.assertIsInstance(self.sio.version, str) 50 | self.assertEqual(len(self.sio.version), 5) 51 | 52 | def test_backend_count(self): 53 | self.assertIsInstance(self.sio.backend_count, int) 54 | 55 | def test_multiple_instances(self): 56 | self.sio2 = pysoundio.PySoundIo() 57 | self.sio2.close() 58 | 59 | def test__check(self): 60 | with self.assertRaises(pysoundio.PySoundIoError): 61 | self.sio._check(_lib.SoundIoErrorNoMem) 62 | 63 | def test_get_default_input_device(self): 64 | self.sio.input['device'] = self.sio.get_default_input_device() 65 | self.assertIsNotNone(self.sio.input['device']) 66 | 67 | def test_get_input_device(self): 68 | self.sio.input['device'] = self.sio.get_input_device(0) 69 | self.assertIsNotNone(self.sio.input['device']) 70 | 71 | def test_invalid_input_device(self): 72 | with self.assertRaises(pysoundio.PySoundIoError): 73 | self.sio.get_input_device(100) 74 | 75 | def test_get_default_output_device(self): 76 | self.sio.output['device'] = self.sio.get_default_output_device() 77 | self.assertIsNotNone(self.sio.output['device']) 78 | 79 | def test_get_output_device(self): 80 | self.sio.output['device'] = self.sio.get_output_device(0) 81 | self.assertIsNotNone(self.sio.output['device']) 82 | 83 | def test_invalid_output_device(self): 84 | with self.assertRaises(pysoundio.PySoundIoError): 85 | self.sio.get_output_device(100) 86 | 87 | def test_get_layouts(self): 88 | self.sio.input['device'] = self.sio.get_default_input_device() 89 | layouts = self.sio.get_layouts(self.sio.input['device']) 90 | self.assertIsInstance(layouts, dict) 91 | self.assertIn('current', layouts) 92 | self.assertIn('available', layouts) 93 | self.assertTrue(len(layouts['available']) > 0) 94 | self.assertIn('name', layouts['available'][0]) 95 | self.assertIn('channel_count', layouts['available'][0]) 96 | 97 | def test_get_sample_rates(self): 98 | self.sio.input['device'] = self.sio.get_default_input_device() 99 | rates = self.sio.get_sample_rates(self.sio.input['device']) 100 | self.assertIsInstance(rates, dict) 101 | self.assertIn('current', rates) 102 | self.assertIn('available', rates) 103 | self.assertTrue(len(rates['available']) > 0) 104 | self.assertIn('min', rates['available'][0]) 105 | self.assertIn('max', rates['available'][0]) 106 | 107 | def test_get_formats(self): 108 | self.sio.input['device'] = self.sio.get_default_input_device() 109 | formats = self.sio.get_formats(self.sio.input['device']) 110 | self.assertIsInstance(formats, dict) 111 | self.assertIn('current', formats) 112 | self.assertIn('available', formats) 113 | self.assertTrue(len(formats['available']) > 0) 114 | 115 | def test_list_devices(self): 116 | input_devices, output_devices = self.sio.list_devices() 117 | self.assertIsInstance(input_devices, list) 118 | self.assertIsInstance(output_devices, list) 119 | self.assertTrue(len(input_devices) > 0) 120 | self.assertTrue(len(output_devices) > 0) 121 | self.assertIsInstance(input_devices[0], dict) 122 | self.assertIsInstance(output_devices[0], dict) 123 | for device in [input_devices[0], output_devices[0]]: 124 | self.assertIn('id', device) 125 | self.assertIn('name', device) 126 | self.assertIn('is_raw', device) 127 | self.assertIn('sample_rates', device) 128 | self.assertIn('formats', device) 129 | self.assertIn('layouts', device) 130 | self.assertIn('software_latency_min', device) 131 | self.assertIn('software_latency_max', device) 132 | self.assertIn('software_latency_current', device) 133 | 134 | def test_supports_sample_rate(self): 135 | self.sio.input['device'] = self.sio.get_input_device(0) 136 | self.assertTrue(self.sio.supports_sample_rate(self.sio.input['device'], 44100)) 137 | 138 | def test_get_default_sample_rate(self): 139 | self.sio.input['device'] = self.sio.get_input_device(0) 140 | self.assertIsInstance(self.sio.get_default_sample_rate(self.sio.input['device']), int) 141 | 142 | def test_get_default_sample_rate_max(self): 143 | sample_rates = pysoundio.constants.PRIORITISED_SAMPLE_RATES 144 | pysoundio.constants.PRIORITISED_SAMPLE_RATES = [0] 145 | self.sio.input['device'] = self.sio.get_input_device(0) 146 | self.assertIsInstance(self.sio.get_default_sample_rate(self.sio.input['device']), int) 147 | pysoundio.constants.PRIORITISED_SAMPLE_RATES = sample_rates 148 | 149 | def test_supports_format(self): 150 | self.sio.input['device'] = self.sio.get_input_device(0) 151 | self.assertTrue(self.sio.supports_format(self.sio.input['device'], pysoundio.SoundIoFormatFloat32LE)) 152 | 153 | def test_get_default_format(self): 154 | self.sio.input['device'] = self.sio.get_input_device(0) 155 | self.assertIsInstance(self.sio.get_default_format(self.sio.input['device']), int) 156 | 157 | def test_get_default_format_invalid(self): 158 | formats = pysoundio.constants.PRIORITISED_FORMATS 159 | pysoundio.constants.PRIORITISED_FORMATS = [] 160 | self.sio.input['device'] = self.sio.get_input_device(0) 161 | with self.assertRaises(pysoundio.PySoundIoError): 162 | self.assertIsInstance(self.sio.get_default_format(self.sio.input['device']), int) 163 | pysoundio.constants.PRIORITISED_FORMATS = formats 164 | 165 | def test_get_default_layout(self): 166 | self.assertIsNotNone(self.sio._get_default_layout(2)) 167 | 168 | def test_bytes_per_frame(self): 169 | self.assertEqual(self.sio.get_bytes_per_frame( 170 | pysoundio.SoundIoFormatFloat32LE, 2), 8) 171 | 172 | def test_bytes_per_sample(self): 173 | self.assertEqual(self.sio.get_bytes_per_sample( 174 | pysoundio.SoundIoFormatFloat32LE), 4) 175 | 176 | def test_bytes_per_second(self): 177 | self.assertEqual(self.sio.get_bytes_per_second( 178 | pysoundio.SoundIoFormatFloat32LE, 1, 44100), 176400) 179 | 180 | # - Input Stream API 181 | 182 | def fill_input_buffer(self): 183 | data = bytearray(b'\x00' * 4096 * 8) 184 | _lib.soundio_ring_buffer_write_ptr(self.sio.input['buffer']) 185 | _lib.soundio_ring_buffer_advance_write_ptr(self.sio.input['buffer'], len(data)) 186 | 187 | def test__create_input_ring_buffer(self): 188 | capacity = 44100 * 8 189 | self.sio.input['buffer'] = self.sio._create_input_ring_buffer(capacity) 190 | self.assertIsNotNone(self.sio.input['buffer']) 191 | 192 | def test_get_input_latency(self): 193 | self.sio.start_input_stream(**self.stream_kwargs) 194 | self.fill_input_buffer() 195 | self.assertIsInstance(self.sio.get_input_latency(0.2), int) 196 | 197 | def test__create_invalid_input_stream(self): 198 | with self.assertRaises(RuntimeError): 199 | self.sio.input['device'] = self.sio.get_default_input_device() 200 | self.sio.input['channels'] = 25 201 | self.sio._create_input_stream() 202 | 203 | def read_callback(self, data, length): 204 | self.callback_called = True 205 | 206 | @unittest.skipIf(platform.system() == 'Windows', 'not on Windows') 207 | def test_read_callback(self): 208 | self.sio.start_input_stream(**self.stream_kwargs, 209 | read_callback=self.read_callback) 210 | self.assertIsNotNone(self.sio.input['stream']) 211 | time.sleep(0.01) 212 | self.assertTrue(self.callback_called) 213 | 214 | def overflow_callback(self): 215 | self.callback_called = True 216 | 217 | @unittest.skipIf(platform.system() == 'Windows', 'not on Windows') 218 | def test_overflow_callback(self): 219 | pysoundio.constants.DEFAULT_RING_BUFFER_DURATION = 0.1 220 | self.sio.start_input_stream(**self.stream_kwargs, 221 | overflow_callback=self.overflow_callback) 222 | self.assertIsNotNone(self.sio.input['stream']) 223 | self.sio.input['thread'].stop() 224 | time.sleep(0.15) 225 | self.assertTrue(self.callback_called) 226 | pysoundio.constants.DEFAULT_RING_BUFFER_DURATION = 1 227 | 228 | def test_pause_input_stream(self): 229 | self.sio.start_input_stream(**self.stream_kwargs) 230 | self.fill_input_buffer() 231 | self.sio.pause_input_stream(True) 232 | 233 | def test_start_input_invalid_rate(self): 234 | with self.assertRaises(pysoundio.PySoundIoError): 235 | self.stream_kwargs['sample_rate'] = 10000000 236 | self.sio.start_input_stream(**self.stream_kwargs) 237 | 238 | def test_start_input_default_rate(self): 239 | del self.stream_kwargs['sample_rate'] 240 | self.sio.start_input_stream(**self.stream_kwargs) 241 | self.fill_input_buffer() 242 | self.assertIsNotNone(self.sio.input['stream']) 243 | self.assertIsInstance(self.sio.input['sample_rate'], int) 244 | 245 | def test_start_input_invalid_format(self): 246 | with self.assertRaises(pysoundio.PySoundIoError): 247 | self.stream_kwargs['dtype'] = 50 248 | self.sio.start_input_stream(**self.stream_kwargs) 249 | 250 | def test_start_input_default_format(self): 251 | del self.stream_kwargs['dtype'] 252 | self.sio.start_input_stream(**self.stream_kwargs) 253 | self.fill_input_buffer() 254 | self.assertIsNotNone(self.sio.input['stream']) 255 | self.assertIsInstance(self.sio.input['format'], int) 256 | 257 | # -- Output Stream API 258 | 259 | def test__create_output_ring_buffer(self): 260 | capacity = 44100 * 8 261 | self.assertIsNotNone(self.sio._create_output_ring_buffer(capacity)) 262 | 263 | def test__create_invalid_output_stream(self): 264 | with self.assertRaises(RuntimeError): 265 | self.sio.output['device'] = self.sio.get_default_output_device() 266 | self.sio.output['channels'] = 25 267 | self.sio._create_output_stream() 268 | 269 | def test__open_output_stream(self): 270 | self.sio.output['device'] = self.sio.get_default_output_device() 271 | self.sio.output['stream'] = self.sio._create_output_stream() 272 | self.sio._open_output_stream() 273 | self.assertIsNotNone(self.sio.output['block_size']) 274 | 275 | def test__open_output_stream_blocksize(self): 276 | self.sio.block_size = 4096 277 | self.sio.output['device'] = self.sio.get_default_output_device() 278 | self.sio.output['stream'] = self.sio._create_output_stream() 279 | self.sio._open_output_stream() 280 | self.assertIsNotNone(self.sio.output['block_size']) 281 | 282 | def test_clear_output_buffer(self): 283 | capacity = 44100 * 8 284 | self.sio.output['buffer'] = self.sio._create_output_ring_buffer(capacity) 285 | self.assertIsNotNone(self.sio.output['buffer']) 286 | self.sio._clear_output_buffer() 287 | 288 | def test_get_output_latency(self): 289 | self.sio.start_output_stream(**self.stream_kwargs) 290 | self.assertIsInstance(self.sio.get_output_latency(0.2), int) 291 | 292 | def test_pause_output_stream(self): 293 | self.sio.start_output_stream(**self.stream_kwargs) 294 | self.sio.pause_output_stream(True) 295 | 296 | def test_start_output_stream(self): 297 | self.sio.start_output_stream(**self.stream_kwargs) 298 | self.assertIsNotNone(self.sio.output['stream']) 299 | 300 | def test_start_output_invalid_rate(self): 301 | with self.assertRaises(pysoundio.PySoundIoError): 302 | self.stream_kwargs['sample_rate'] = 10000000 303 | self.sio.start_output_stream(**self.stream_kwargs) 304 | 305 | def test_start_output_default_rate(self): 306 | self.sio.start_output_stream( 307 | dtype=pysoundio.SoundIoFormatFloat32LE, 308 | channels=2) 309 | self.assertIsNotNone(self.sio.output['stream']) 310 | self.assertIsInstance(self.sio.output['sample_rate'], int) 311 | 312 | def test_start_output_invalid_format(self): 313 | with self.assertRaises(pysoundio.PySoundIoError): 314 | self.stream_kwargs['dtype'] = 50 315 | self.sio.start_output_stream(**self.stream_kwargs) 316 | 317 | def test_start_output_default_format(self): 318 | del self.stream_kwargs['dtype'] 319 | self.sio.start_output_stream(**self.stream_kwargs) 320 | self.assertIsNotNone(self.sio.output['stream']) 321 | self.assertIsInstance(self.sio.output['format'], int) 322 | 323 | def write_callback(self, data, length): 324 | self.callback_called = True 325 | 326 | @unittest.skipIf(platform.system() == 'Windows', 'not on Windows') 327 | def test_write_callback(self): 328 | self.sio.start_output_stream( 329 | **self.stream_kwargs, 330 | write_callback=self.write_callback) 331 | self.assertIsNotNone(self.sio.output['stream']) 332 | time.sleep(0.01) 333 | self.assertTrue(self.callback_called) 334 | 335 | def underflow_callback(self): 336 | self.callback_called = True 337 | 338 | @unittest.skipIf(platform.system() == 'Windows', 'not on Windows') 339 | def test_underflow_callback(self): 340 | self.sio.start_output_stream( 341 | **self.stream_kwargs, 342 | underflow_callback=self.underflow_callback 343 | ) 344 | self.assertIsNotNone(self.sio.output['stream']) 345 | time.sleep(0.01) 346 | self.assertTrue(self.callback_called) 347 | 348 | def test_internal_write_callback_initial(self): 349 | self.sio.output['device'] = self.sio.get_default_output_device() 350 | self.sio._create_output_stream() 351 | self.sio.output['stream'].bytes_per_frame = self.sio.get_bytes_per_frame( 352 | self.stream_kwargs['dtype'], self.stream_kwargs['channels']) 353 | capacity = int(pysoundio.constants.DEFAULT_RING_BUFFER_DURATION * 354 | self.sio.output['stream'].sample_rate * self.sio.output['stream'].bytes_per_frame) 355 | self.sio._create_output_ring_buffer(capacity) 356 | self.assertIsNotNone(self.sio.output['stream']) 357 | self.sio._open_output_stream() 358 | self.sio._start_output_stream() 359 | time.sleep(0.1) 360 | self.sio.output['thread'] = self 361 | self.sio.output['thread'].stop = lambda: None 362 | self.sio.output['thread'].is_alive = lambda: None 363 | pysoundio.pysoundio._write_callback( 364 | self.sio.output['stream'], 365 | self.stream_kwargs['block_size'], 366 | self.stream_kwargs['block_size'], 367 | ) 368 | 369 | def test_internal_write_callback(self): 370 | self.sio.output['device'] = self.sio.get_default_output_device() 371 | self.sio._create_output_stream() 372 | self.sio.output['stream'].bytes_per_frame = self.sio.get_bytes_per_frame( 373 | self.stream_kwargs['dtype'], self.stream_kwargs['channels']) 374 | capacity = int(pysoundio.constants.DEFAULT_RING_BUFFER_DURATION * 375 | self.sio.output['stream'].sample_rate * self.sio.output['stream'].bytes_per_frame) 376 | self.sio._create_output_ring_buffer(capacity) 377 | self.assertIsNotNone(self.sio.output['stream']) 378 | self.sio._open_output_stream() 379 | self.sio._start_output_stream() 380 | time.sleep(0.1) 381 | self.sio.output['thread'] = self 382 | self.sio.output['thread'].stop = lambda: None 383 | self.sio.output['thread'].is_alive = lambda: None 384 | data = bytearray(1024) 385 | write_buf = _lib.soundio_ring_buffer_write_ptr(self.sio.output['buffer']) 386 | _ffi.memmove(write_buf, data, len(data)) 387 | _lib.soundio_ring_buffer_advance_write_ptr(self.sio.output['buffer'], len(data)) 388 | pysoundio.pysoundio._write_callback( 389 | self.sio.output['stream'], 390 | int(self.stream_kwargs['block_size']), 391 | int(self.stream_kwargs['block_size']) 392 | ) 393 | 394 | def test_internal_underflow_callback(self): 395 | self.sio.start_output_stream(**self.stream_kwargs) 396 | self.sio.output['underflow_callback'] = self.underflow_callback 397 | self.sio.output['thread'].stop() 398 | pysoundio.pysoundio._underflow_callback(self.sio.output['stream']) 399 | self.assertTrue(self.callback_called) 400 | 401 | def test_internal_output_error_callback(self): 402 | self.sio.start_output_stream(**self.stream_kwargs) 403 | self.sio.output['thread'].stop() 404 | pysoundio.pysoundio._output_error_callback( 405 | self.sio.output['stream'], _lib.SoundIoErrorNoMem) 406 | 407 | def test_internal_read_callback_initial(self): 408 | self.sio.input['device'] = self.sio.get_default_input_device() 409 | self.sio._create_input_stream() 410 | self.sio.input['stream'].bytes_per_frame = self.sio.get_bytes_per_frame( 411 | self.stream_kwargs['dtype'], self.stream_kwargs['channels']) 412 | capacity = int(pysoundio.constants.DEFAULT_RING_BUFFER_DURATION * 413 | self.sio.input['stream'].sample_rate * self.sio.input['stream'].bytes_per_frame) 414 | self.sio._create_input_ring_buffer(capacity) 415 | self.assertIsNotNone(self.sio.input['stream']) 416 | self.sio._open_input_stream() 417 | self.sio._start_input_stream() 418 | pysoundio.pysoundio._read_callback( 419 | self.sio.input['stream'], 420 | 0, 421 | 0 422 | ) 423 | 424 | # def test_internal_read_callback_overflow(self): 425 | # self.sio.input['device'] = self.sio.get_default_input_device() 426 | # self.sio._create_input_stream() 427 | # self.sio.input['stream'].bytes_per_frame = self.sio.get_bytes_per_frame( 428 | # self.stream_kwargs['dtype'], self.stream_kwargs['channels']) 429 | # capacity = int(pysoundio.constants.DEFAULT_RING_BUFFER_DURATION * 430 | # self.sio.input['stream'].sample_rate * self.sio.input['stream'].bytes_per_frame) 431 | # self.sio._create_input_ring_buffer(capacity) 432 | # self.assertIsNotNone(self.sio.input['stream']) 433 | # self.sio._open_input_stream() 434 | # self.sio._start_input_stream() 435 | # # _lib.soundio_ring_buffer_advance_read_ptr(self.sio.input['buffer'], 1024) 436 | # pysoundio.pysoundio._read_callback( 437 | # self.sio.input['stream'], 438 | # self.stream_kwargs['block_size'], 439 | # self.stream_kwargs['block_size'] 440 | # ) 441 | 442 | def test_internal_overflow_callback(self): 443 | self.sio.start_input_stream(**self.stream_kwargs) 444 | self.sio.input['overflow_callback'] = self.overflow_callback 445 | self.sio.input['thread'].stop() 446 | pysoundio.pysoundio._overflow_callback(self.sio.input['stream']) 447 | self.assertTrue(self.callback_called) 448 | 449 | def test_internal_input_error_callback(self): 450 | self.sio.start_input_stream(**self.stream_kwargs) 451 | self.sio.input['thread'].stop() 452 | pysoundio.pysoundio._input_error_callback( 453 | self.sio.input['stream'], _lib.SoundIoErrorNoMem) 454 | 455 | 456 | class TestInputProcessing(unittest.TestCase): 457 | 458 | def setUp(self): 459 | self.sio = pysoundio.PySoundIo( 460 | backend=pysoundio.SoundIoBackendDummy) 461 | self.callback_called = False 462 | 463 | def tearDown(self): 464 | self.sio.close() 465 | 466 | def callback(self, data, length): 467 | self.callback_called = True 468 | 469 | def test_read_callback(self): 470 | self.sio.start_input_stream( 471 | sample_rate=44100, 472 | dtype=pysoundio.SoundIoFormatFloat32LE, 473 | channels=2, 474 | block_size=4096, 475 | read_callback=self.callback) 476 | self.assertIsNotNone(self.sio.input['stream']) 477 | 478 | data = bytearray(b'\x00' * 4096 * 4 * 2) 479 | _lib.soundio_ring_buffer_write_ptr(self.sio.input['buffer']) 480 | _lib.soundio_ring_buffer_advance_write_ptr(self.sio.input['buffer'], len(data)) 481 | 482 | thread = pysoundio.pysoundio._InputProcessingThread(parent=self.sio) 483 | threading.Timer(0.1, thread.stop).start() 484 | thread.run() 485 | self.assertTrue(self.callback_called) 486 | 487 | 488 | class TestOutputProcessing(unittest.TestCase): 489 | 490 | def setUp(self): 491 | self.sio = pysoundio.PySoundIo( 492 | backend=pysoundio.SoundIoBackendDummy) 493 | self.sio.testing = True 494 | self.callback_called = False 495 | 496 | def tearDown(self): 497 | self.sio.close() 498 | 499 | def callback(self, data, length): 500 | self.callback_called = True 501 | 502 | def test_write_callback(self): 503 | self.sio.start_output_stream( 504 | sample_rate=44100, 505 | dtype=pysoundio.SoundIoFormatFloat32LE, 506 | channels=2, 507 | block_size=4096, 508 | write_callback=self.callback) 509 | self.assertIsNotNone(self.sio.output['stream']) 510 | thread = pysoundio.pysoundio._OutputProcessingThread(parent=self.sio) 511 | threading.Timer(0.1, thread.stop).start() 512 | thread.run() 513 | self.assertTrue(self.callback_called) 514 | 515 | if __name__ == '__main__': 516 | unittest.main(failfast=True) 517 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, py39 3 | 4 | [testenv] 5 | deps = pytest==6.2.2 6 | pytest-cov==2.11.1 7 | commands = pytest --cov=. --cov-config .coveragerc --cov-report=xml --maxfail=1 8 | --------------------------------------------------------------------------------