Compare commits
31 Commits
Author | SHA1 | Date |
---|---|---|
Derek | f749f2187e | |
Derek | 2ee560056e | |
Hubert Pham | 7090e25bcb | |
Hubert Pham | 045ebc7767 | |
Hubert Pham | 653fdfeb87 | |
Hubert Pham | 281a8f32e7 | |
Hubert Pham | 39de78a74a | |
Hubert Pham | 2e696d98c0 | |
Hubert Pham | 833ebc0b14 | |
Hubert Pham | fe528b2050 | |
Hubert Pham | 8285b9bc5c | |
Hubert Pham | a0f6d13628 | |
Hubert Pham | 47a80684e1 | |
Hubert Pham | 439327bcd7 | |
Hubert Pham | c9bc0193b0 | |
Hubert Pham | 884cedae7f | |
Hubert Pham | 50e08d41d4 | |
Hubert Pham | 7a61080270 | |
Hubert Pham | e59fa4a24e | |
Hubert Pham | 870016f6d1 | |
Hubert Pham | bff409df19 | |
Hubert Pham | da8c238eee | |
Hubert Pham | dce064b428 | |
Hubert Pham | ab5e5b775e | |
Hubert Pham | 5a4da7d870 | |
Hubert Pham | 0e5eb7cb9d | |
Hubert Pham | 3e6adb4588 | |
Hubert Pham | d786cc59a2 | |
Hubert Pham | ebea06b12c | |
Hubert Pham | 1783aaf9bc | |
Hubert Pham | 53ab5eb107 |
48
CHANGELOG
48
CHANGELOG
|
@ -1,3 +1,50 @@
|
|||
2017-03-18 Hubert Pham <hubert@mit.edu>
|
||||
|
||||
PyAudio 0.2.11
|
||||
|
||||
- Fix use-after-free memory issue in callback handler.
|
||||
|
||||
Thanks to both Blaise Potard and Matthias Schaff for their patches!
|
||||
|
||||
- Fix docstring for get_output_latency().
|
||||
|
||||
Thanks to Timothy Port for finding the issue!
|
||||
|
||||
2017-01-10 Hubert Pham <hubert@mit.edu>
|
||||
|
||||
PyAudio 0.2.10
|
||||
|
||||
- Release the GIL during PortAudio I/O calls to avoid potential deadlock.
|
||||
|
||||
Thanks to Michael Graczyk for submitting a patch!
|
||||
|
||||
- Add a few automated unit tests.
|
||||
|
||||
2015-10-18 Hubert Pham <hubert@mit.edu>
|
||||
|
||||
PyAudio 0.2.9
|
||||
|
||||
- Fix overflow error handling logic for pa_read_stream.
|
||||
|
||||
Stream.read takes an additional parameter that specifies whether
|
||||
an exception is raised on audio buffer overflow, for parity with
|
||||
Stream.write. Includes relevant bug fixes in the C module logic.
|
||||
|
||||
Thanks to Tony Jacobson for submitting a patch!
|
||||
|
||||
- Fix IOError arguments.
|
||||
|
||||
IOError exceptions previously had values in the strerror and errno fields
|
||||
swapped, which is now corrected.
|
||||
|
||||
Thanks to Sami Liedes for the report!
|
||||
|
||||
- Miscellaneous updates.
|
||||
|
||||
Python library surfaces issues with importing low-level C module.
|
||||
Code formatting update.
|
||||
Updates to examples for Python 3 compatibility.
|
||||
|
||||
2014-02-16 Hubert Pham <hubert@mit.edu>
|
||||
|
||||
PyAudio 0.2.8
|
||||
|
@ -96,4 +143,3 @@
|
|||
2008-02-12 Justin Mazzola Paluska <jmp@mit.edu>
|
||||
|
||||
- Initial version of debian packaging.
|
||||
|
||||
|
|
96
INSTALL
96
INSTALL
|
@ -8,81 +8,83 @@ platforms:
|
|||
* General UNIX Guide: (GNU/Linux, Mac OS X, Cygwin)
|
||||
* Microsoft Windows (native)
|
||||
|
||||
Generally speaking, you must first install the PortAudio v19 library
|
||||
before building PyAudio.
|
||||
|
||||
Generally speaking, installation involves building the PortAudio v19
|
||||
library and then building PyAudio.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
General UNIX Guide (GNU/Linux, Mac OS X, Cygwin)
|
||||
----------------------------------------------------------------------
|
||||
|
||||
1. Build and install PortAudio, i.e.:
|
||||
1. Use a package manager to install PortAudio v19.
|
||||
|
||||
To build PortAudio from source instead, extract the source and run:
|
||||
|
||||
% ./configure
|
||||
% make
|
||||
% make install # you may need to be root
|
||||
|
||||
(Or better yet, use your package manager to install PortAudio v19)
|
||||
|
||||
2. Extract PyAudio; to build and install, run:
|
||||
2. Extract PyAudio. To build and install, run:
|
||||
|
||||
% python setup.py install
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Microsoft Windows
|
||||
----------------------------------------------------------------------
|
||||
|
||||
If you are targeting native Win32 Python, you will need either
|
||||
Microsoft Visual Studio or MinGW (via Cygwin). Here are compilation
|
||||
hints for using MinGW under the Cygwin build environment.
|
||||
Targeting native Win32 Python will require either Microsoft Visual
|
||||
Studio or MinGW (via Cygwin). Here are compilation hints for using
|
||||
MinGW under the Cygwin build environment.
|
||||
|
||||
Note: I've only tested this under Cygwin's build environment. Your
|
||||
mileage may vary in other environments (i.e., compiling PortAudio with
|
||||
MinGW's compiler and environment).
|
||||
MinGW's compiler).
|
||||
|
||||
(If you have instructions for building PyAudio using Visual Studio,
|
||||
I'd love to hear about it.)
|
||||
1. Install cygwin's gcc and mingw packages.
|
||||
|
||||
1. Download PortAudio to ./portaudio-v19 in this directory
|
||||
and build. When running configure, be sure to use ``-mno-cygwin``
|
||||
(under cygwin) to generate native Win32 binaries:
|
||||
2. Download PortAudio and build. When running configure, be sure to
|
||||
specify the MinGW compiler (via a CC environment variable) to
|
||||
generate native Win32 binaries:
|
||||
|
||||
% cd ./portaudio-v19
|
||||
% CFLAGS="-mno-cygwin" LDFLAGS="-mno-cygwin" ./configure
|
||||
% make
|
||||
% cd ..
|
||||
% CC=i686-w64-mingw32-gcc ./configure --enable-static --with-pic
|
||||
% make
|
||||
|
||||
2. To build PyAudio, run (from this directory):
|
||||
3. Before building PyAudio, apply a few necessary modifications:
|
||||
|
||||
% python setup.py build --static-link -cmingw32
|
||||
a. Python distutils calls ``gcc'' to build the C extension, so
|
||||
temporarily move your MinGW compiler to /usr/bin/gcc.
|
||||
|
||||
b. Modify Python's Lib/distutils/cygwincompiler.py so that
|
||||
mscvr900.dll is not included in the build. See:
|
||||
http://bugs.python.org/issue16472.
|
||||
|
||||
Both Python 2.7 and Python 3+ require similar modification.
|
||||
|
||||
c. For some versions of Python (e.g., Python 2.7 32-bit), it is
|
||||
necessary to further modify Python's
|
||||
Lib/distutils/cygwincompiler.py and remove references to
|
||||
-cmingw32, a flag which is no longer supported.
|
||||
See http://hg.python.org/cpython/rev/6b89176f1be5/.
|
||||
|
||||
d. For some versions of 64-bit Python 3 (e.g., Python 3.2, 3.3, 3.4),
|
||||
it is necessary to generate .a archive of the Python DLL.
|
||||
See https://bugs.python.org/issue20785. Example for Python 3.4:
|
||||
|
||||
% cd /path/to/Python34-x64/libs/
|
||||
% gendef /path/to/Windows/System32/python34.dll
|
||||
% dlltool --as-flags=--64 -m i386:x64-64 -k --output-lib libpython34.a \
|
||||
--input-def python34.def
|
||||
|
||||
4. To build PyAudio, run:
|
||||
|
||||
% PORTAUDIO_PATH=/path/to/portaudio_tree /path/to/win/python \
|
||||
setup.py build --static-link -cmingw32
|
||||
|
||||
Be sure to invoke the native Win32 python rather than cygwin's
|
||||
python. The --static-link option statically links in the PortAudio
|
||||
library to the PyAudio module, which is probably the best way to go
|
||||
on Windows.
|
||||
library to the PyAudio module.
|
||||
|
||||
From: http://boodebr.org/main/python/build-windows-extensions
|
||||
5. To install PyAudio:
|
||||
|
||||
Update: 2008-09-10
|
||||
% python setup.py install --skip-build
|
||||
|
||||
Recent versions of Cygwin binutils have version numbers that are
|
||||
breaking the version number parsing, resulting in errors like:
|
||||
ValueError: invalid version number '2.18.50.20080625'
|
||||
|
||||
To fix this, edit distutils/version.py. At line 100, replace:
|
||||
|
||||
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
|
||||
re.VERBOSE)
|
||||
|
||||
with
|
||||
|
||||
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? (\. (\d+))?$',
|
||||
re.VERBOSE)
|
||||
|
||||
3. To install PyAudio:
|
||||
|
||||
% python setup.py install --skip-build
|
||||
|
||||
The --skip-build option prevents Python from searching your system
|
||||
for Visual Studio and the .NET framework.
|
||||
Or create a Python wheel and install using pip.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
include src/*.c src/*.h src/*.py
|
||||
include Makefile CHANGELOG INSTALL MANIFEST.in
|
||||
recursive-include test *.py *.c
|
||||
graft docs
|
||||
recursive-include examples *.py
|
||||
recursive-include tests *.py
|
||||
graft sphinx
|
||||
|
|
7
Makefile
7
Makefile
|
@ -2,7 +2,7 @@
|
|||
|
||||
.PHONY: docs clean build
|
||||
|
||||
VERSION := 0.2.8
|
||||
VERSION := 0.2.11
|
||||
PYTHON ?= python
|
||||
BUILD_ARGS ?=
|
||||
SPHINX ?= sphinx-build
|
||||
|
@ -11,7 +11,8 @@ PYTHON_BUILD_DIR:=$(shell $(PYTHON) -c "import distutils.util; import sys; print
|
|||
BUILD_DIR:=lib.$(PYTHON_BUILD_DIR)
|
||||
BUILD_STAMP:=$(BUILD_DIR)/build
|
||||
SRCFILES := src/*.c src/*.h src/*.py
|
||||
EXAMPLES := test/*.py
|
||||
EXAMPLES := examples/*.py
|
||||
TESTS := tests/*.py
|
||||
|
||||
what:
|
||||
@echo "make targets:"
|
||||
|
@ -43,5 +44,5 @@ docs: build
|
|||
######################################################################
|
||||
# Source Tarball
|
||||
######################################################################
|
||||
tarball: docs $(SRCFILES) $(EXAMPLES) MANIFEST.in
|
||||
tarball: $(SRCFILES) $(EXAMPLES) $(TESTS) MANIFEST.in
|
||||
@$(PYTHON) setup.py sdist
|
||||
|
|
5
README
5
README
|
@ -1,5 +1,5 @@
|
|||
======================================================================
|
||||
PyAudio v0.2.8: Python Bindings for PortAudio.
|
||||
PyAudio v0.2.12: Python Bindings for PortAudio.
|
||||
======================================================================
|
||||
|
||||
See: http://people.csail.mit.edu/hubert/pyaudio/
|
||||
|
@ -14,7 +14,7 @@ See INSTALL for compilation hints.
|
|||
|
||||
PyAudio : Python Bindings for PortAudio.
|
||||
|
||||
Copyright (c) 2006-2014 Hubert Pham
|
||||
Copyright (c) 2006 Hubert Pham
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
@ -36,4 +36,3 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
======================================================================
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
|
|||
data = wf.readframes(CHUNK)
|
||||
|
||||
# play stream (3)
|
||||
while data != '':
|
||||
while len(data) > 0:
|
||||
stream.write(data)
|
||||
data = wf.readframes(CHUNK)
|
||||
|
|
@ -64,7 +64,7 @@ stream = p.open(
|
|||
data = wf.readframes(chunk)
|
||||
|
||||
# play stream
|
||||
while data != '':
|
||||
while len(data) > 0:
|
||||
stream.write(data)
|
||||
data = wf.readframes(chunk)
|
||||
|
|
@ -12,6 +12,7 @@ import sys
|
|||
WIDTH = 2
|
||||
CHANNELS = 2
|
||||
RATE = 44100
|
||||
DURATION = 5
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
CHANNELS = 1
|
||||
|
@ -30,7 +31,9 @@ stream = p.open(format=p.get_format_from_width(WIDTH),
|
|||
|
||||
stream.start_stream()
|
||||
|
||||
while stream.is_active():
|
||||
start = time.time()
|
||||
|
||||
while stream.is_active() and (time.time() - start) < DURATION:
|
||||
time.sleep(0.1)
|
||||
|
||||
stream.stop_stream()
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
PyAudio v0.2.8: Python Bindings for PortAudio.
|
||||
PyAudio v0.2.11: Python Bindings for PortAudio.
|
||||
|
||||
Copyright (c) 2006-2014 Hubert Pham
|
||||
Copyright (c) 2006 Hubert Pham
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
@ -26,25 +26,25 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from distutils.core import setup, Extension
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
try:
|
||||
from setuptools import setup, Extension
|
||||
except ImportError:
|
||||
from distutils.core import setup, Extension
|
||||
|
||||
__version__ = "0.2.8"
|
||||
__version__ = "0.2.12"
|
||||
|
||||
# Note: distutils will try to locate and link dynamically
|
||||
# against portaudio.
|
||||
# distutils will try to locate and link dynamically against portaudio.
|
||||
#
|
||||
# You probably don't want to statically link in the PortAudio
|
||||
# library unless you're building on Microsoft Windows.
|
||||
# If you would rather statically link in the portaudio library (e.g.,
|
||||
# typically on Microsoft Windows), run:
|
||||
#
|
||||
# In any case, if you would rather statically link in libportaudio,
|
||||
# run:
|
||||
# % python setup.py build --static-link
|
||||
#
|
||||
# % python setup.py build --static-link
|
||||
#
|
||||
# Be sure to specify the location of the libportaudio.a in
|
||||
# the `extra_link_args' variable below.
|
||||
# Specify the environment variable PORTAUDIO_PATH with the build tree
|
||||
# of PortAudio.
|
||||
|
||||
STATIC_LINKING = False
|
||||
|
||||
|
@ -56,7 +56,6 @@ portaudio_path = os.environ.get("PORTAUDIO_PATH", "./portaudio-v19")
|
|||
mac_sysroot_path = os.environ.get("SYSROOT_PATH", None)
|
||||
|
||||
pyaudio_module_sources = ['src/_portaudiomodule.c']
|
||||
|
||||
include_dirs = []
|
||||
external_libraries = []
|
||||
extra_compile_args = []
|
||||
|
@ -64,24 +63,24 @@ extra_link_args = []
|
|||
scripts = []
|
||||
defines = []
|
||||
|
||||
if STATIC_LINKING:
|
||||
extra_link_args = [
|
||||
os.path.join(portaudio_path, 'lib/.libs/libportaudio.a')
|
||||
]
|
||||
include_dirs = [os.path.join(portaudio_path, 'include/')]
|
||||
else:
|
||||
# dynamic linking
|
||||
external_libraries = ['portaudio']
|
||||
extra_link_args = []
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
defines += [('MACOSX', '1')]
|
||||
|
||||
if mac_sysroot_path:
|
||||
extra_compile_args += ["-isysroot", mac_sysroot_path]
|
||||
extra_link_args += ["-isysroot", mac_sysroot_path]
|
||||
elif sys.platform == 'win32':
|
||||
bits = platform.architecture()[0]
|
||||
if '64' in bits:
|
||||
defines.append(('MS_WIN64', '1'))
|
||||
|
||||
if STATIC_LINKING:
|
||||
if not STATIC_LINKING:
|
||||
external_libraries = ['portaudio']
|
||||
extra_link_args = []
|
||||
else:
|
||||
include_dirs = [os.path.join(portaudio_path, 'include/')]
|
||||
extra_link_args = [
|
||||
os.path.join(portaudio_path, 'lib/.libs/libportaudio.a')
|
||||
]
|
||||
|
||||
# platform specific configuration
|
||||
if sys.platform == 'darwin':
|
||||
|
@ -89,45 +88,36 @@ if STATIC_LINKING:
|
|||
'-framework', 'AudioToolbox',
|
||||
'-framework', 'AudioUnit',
|
||||
'-framework', 'Carbon']
|
||||
|
||||
elif sys.platform == 'cygwin':
|
||||
external_libraries += ['winmm']
|
||||
extra_link_args += ['-lwinmm']
|
||||
|
||||
elif sys.platform == 'win32':
|
||||
# i.e., Win32 Python with mingw32
|
||||
# run: python setup.py build -cmingw32
|
||||
external_libraries += ['winmm']
|
||||
extra_link_args += ['-lwinmm']
|
||||
|
||||
elif sys.platform == 'linux2':
|
||||
extra_link_args += ['-lrt', '-lm', '-lpthread']
|
||||
|
||||
# Since you're insisting on linking statically against
|
||||
# PortAudio on GNU/Linux, be sure to link in whatever sound
|
||||
# backend you used in portaudio (e.g., ALSA, JACK, etc...)
|
||||
|
||||
# I'll start you off with ALSA, since that's the most common
|
||||
# today. If you need JACK support, add it here.
|
||||
|
||||
# GNU/Linux has several audio systems (backends) available; be
|
||||
# sure to specify the desired ones here. Start with ALSA and
|
||||
# JACK, since that's common today.
|
||||
extra_link_args += ['-lasound', '-ljack']
|
||||
|
||||
|
||||
pyaudio = Extension('_portaudio',
|
||||
sources=pyaudio_module_sources,
|
||||
include_dirs=include_dirs,
|
||||
define_macros=defines,
|
||||
libraries=external_libraries,
|
||||
extra_compile_args=extra_compile_args,
|
||||
extra_link_args=extra_link_args)
|
||||
|
||||
setup(name = 'PyAudio',
|
||||
version = __version__,
|
||||
author = "Hubert Pham",
|
||||
url = "http://people.csail.mit.edu/hubert/pyaudio/",
|
||||
description = 'PortAudio Python Bindings',
|
||||
long_description = __doc__.lstrip(),
|
||||
scripts = scripts,
|
||||
py_modules = ['pyaudio'],
|
||||
package_dir = {'': 'src'},
|
||||
ext_modules = [pyaudio])
|
||||
setup(name='PyAudio',
|
||||
version=__version__,
|
||||
author="Hubert Pham",
|
||||
url="http://people.csail.mit.edu/hubert/pyaudio/",
|
||||
description='PortAudio Python Bindings',
|
||||
long_description=__doc__.lstrip(),
|
||||
scripts=scripts,
|
||||
py_modules=['pyaudio'],
|
||||
package_dir={'': 'src'},
|
||||
ext_modules=[
|
||||
Extension('_portaudio',
|
||||
sources=pyaudio_module_sources,
|
||||
include_dirs=include_dirs,
|
||||
define_macros=defines,
|
||||
libraries=external_libraries,
|
||||
extra_compile_args=extra_compile_args,
|
||||
extra_link_args=extra_link_args)
|
||||
])
|
||||
|
|
|
@ -42,16 +42,16 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = 'PyAudio'
|
||||
copyright = '2014, Hubert Pham'
|
||||
copyright = '2006, Hubert Pham'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.2.8'
|
||||
version = '0.2.11'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.2.8'
|
||||
release = '0.2.11'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Example: Blocking Mode Audio I/O
|
||||
--------------------------------
|
||||
|
||||
.. literalinclude:: ../test/play_wave.py
|
||||
.. literalinclude:: ../examples/play_wave.py
|
||||
|
||||
To use PyAudio, first instantiate PyAudio using
|
||||
:py:func:`pyaudio.PyAudio` (1), which sets up the portaudio system.
|
||||
|
@ -30,7 +30,7 @@ Finally, terminate the portaudio session using
|
|||
Example: Callback Mode Audio I/O
|
||||
--------------------------------
|
||||
|
||||
.. literalinclude:: ../test/play_wave_callback.py
|
||||
.. literalinclude:: ../examples/play_wave_callback.py
|
||||
|
||||
In callback mode, PyAudio will call a specified callback function (2)
|
||||
whenever it needs new audio data (to play) and/or when there is new
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* PyAudio : API Header File
|
||||
*
|
||||
* Copyright (c) 2006-2012 Hubert Pham
|
||||
* Copyright (c) 2006 Hubert Pham
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# PyAudio : Python Bindings for PortAudio.
|
||||
|
||||
# Copyright (c) 2006-2012 Hubert Pham
|
||||
# Copyright (c) 2006 Hubert Pham
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
@ -106,7 +106,7 @@ Overview
|
|||
"""
|
||||
|
||||
__author__ = "Hubert Pham"
|
||||
__version__ = "0.2.8"
|
||||
__version__ = "0.2.11"
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
import sys
|
||||
|
@ -115,9 +115,8 @@ import sys
|
|||
try:
|
||||
import _portaudio as pa
|
||||
except ImportError:
|
||||
print("Please build and install the PortAudio Python " +
|
||||
"bindings first.")
|
||||
sys.exit(-1)
|
||||
print("Could not import the PyAudio C module '_portaudio'.")
|
||||
raise
|
||||
|
||||
############################################################
|
||||
# GLOBALS
|
||||
|
@ -472,7 +471,7 @@ class Stream:
|
|||
|
||||
def get_output_latency(self):
|
||||
"""
|
||||
Return the input latency.
|
||||
Return the output latency.
|
||||
|
||||
:rtype: float
|
||||
"""
|
||||
|
@ -562,7 +561,7 @@ class Stream:
|
|||
Defaults to None, in which this value will be
|
||||
automatically computed.
|
||||
:param exception_on_underflow:
|
||||
Specifies whether an exception should be thrown
|
||||
Specifies whether an IOError exception should be thrown
|
||||
(or silently ignored) on buffer underflow. Defaults
|
||||
to False for improved performance, especially on
|
||||
slower platforms.
|
||||
|
@ -587,12 +586,16 @@ class Stream:
|
|||
exception_on_underflow)
|
||||
|
||||
|
||||
def read(self, num_frames):
|
||||
def read(self, num_frames, exception_on_overflow=True):
|
||||
"""
|
||||
Read samples from the stream. Do not call when using
|
||||
*non-blocking* mode.
|
||||
|
||||
:param num_frames: The number of frames to read.
|
||||
:param exception_on_overflow:
|
||||
Specifies whether an IOError exception should be thrown
|
||||
(or silently ignored) on input buffer overflow. Defaults
|
||||
to True.
|
||||
:raises IOError: if stream is not an input stream
|
||||
or if the read operation was unsuccessful.
|
||||
:rtype: string
|
||||
|
@ -602,7 +605,7 @@ class Stream:
|
|||
raise IOError("Not input stream",
|
||||
paCanNotReadFromAnOutputOnlyStream)
|
||||
|
||||
return pa.read_stream(self._stream, num_frames)
|
||||
return pa.read_stream(self._stream, num_frames, exception_on_overflow)
|
||||
|
||||
def get_read_available(self):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import pyaudio
|
||||
|
||||
class PyAudioErrorTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.p = pyaudio.PyAudio()
|
||||
|
||||
def tearDown(self):
|
||||
self.p.terminate()
|
||||
|
||||
def test_invalid_sample_size(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.p.get_sample_size(10)
|
||||
|
||||
def test_invalid_width(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.p.get_format_from_width(8)
|
||||
|
||||
def test_invalid_device(self):
|
||||
with self.assertRaises(IOError):
|
||||
self.p.get_host_api_info_by_type(-1)
|
||||
|
||||
def test_invalid_hostapi(self):
|
||||
with self.assertRaises(IOError):
|
||||
self.p.get_host_api_info_by_index(-1)
|
||||
|
||||
def test_invalid_host_api_devinfo(self):
|
||||
with self.assertRaises(IOError):
|
||||
self.p.get_device_info_by_host_api_device_index(0, -1)
|
||||
|
||||
with self.assertRaises(IOError):
|
||||
self.p.get_device_info_by_host_api_device_index(-1, 0)
|
||||
|
||||
def test_invalid_device_devinfo(self):
|
||||
with self.assertRaises(IOError):
|
||||
self.p.get_device_info_by_index(-1)
|
||||
|
||||
def test_error_without_stream_start(self):
|
||||
with self.assertRaises(IOError):
|
||||
stream = self.p.open(channels=1,
|
||||
rate=44100,
|
||||
format=pyaudio.paInt16,
|
||||
input=True,
|
||||
start=False) # not starting stream
|
||||
stream.read(2)
|
||||
|
||||
def test_error_writing_to_readonly_stream(self):
|
||||
with self.assertRaises(IOError):
|
||||
stream = self.p.open(channels=1,
|
||||
rate=44100,
|
||||
format=pyaudio.paInt16,
|
||||
input=True)
|
||||
stream.write('foo')
|
||||
|
||||
def test_error_negative_frames(self):
|
||||
with self.assertRaises(ValueError):
|
||||
stream = self.p.open(channels=1,
|
||||
rate=44100,
|
||||
format=pyaudio.paInt16,
|
||||
input=True)
|
||||
stream.read(-1)
|
||||
|
||||
def test_invalid_attr_on_closed_stream(self):
|
||||
stream = self.p.open(channels=1,
|
||||
rate=44100,
|
||||
format=pyaudio.paInt16,
|
||||
input=True)
|
||||
stream.close()
|
||||
with self.assertRaises(IOError):
|
||||
stream.get_input_latency()
|
||||
with self.assertRaises(IOError):
|
||||
stream.read(1)
|
||||
|
||||
def test_invalid_format_supported(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.p.is_format_supported(8000, -1, 1, pyaudio.paInt16)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.p.is_format_supported(8000, 0, -1, pyaudio.paInt16)
|
||||
|
||||
def test_write_underflow_exception(self):
|
||||
stream = self.p.open(channels=1,
|
||||
rate=44100,
|
||||
format=pyaudio.paInt16,
|
||||
output=True)
|
||||
time.sleep(0.5)
|
||||
stream.write('\x00\x00\x00\x00', exception_on_underflow=False)
|
||||
|
||||
# It's difficult to invoke an underflow on ALSA, so skip.
|
||||
if sys.platform in ('linux', 'linux2'):
|
||||
return
|
||||
|
||||
with self.assertRaises(IOError) as err:
|
||||
time.sleep(0.5)
|
||||
stream.write('\x00\x00\x00\x00', exception_on_underflow=True)
|
||||
|
||||
self.assertEqual(err.exception.errno, pyaudio.paOutputUnderflowed)
|
||||
self.assertEqual(err.exception.strerror, 'Output underflowed')
|
||||
|
||||
def test_read_overflow_exception(self):
|
||||
stream = self.p.open(channels=1,
|
||||
rate=44100,
|
||||
format=pyaudio.paInt16,
|
||||
input=True)
|
||||
time.sleep(0.5)
|
||||
stream.read(2, exception_on_overflow=False)
|
||||
|
||||
# It's difficult to invoke an underflow on ALSA, so skip.
|
||||
if sys.platform in ('linux', 'linux2'):
|
||||
return
|
||||
|
||||
with self.assertRaises(IOError) as err:
|
||||
time.sleep(0.5)
|
||||
stream.read(2, exception_on_overflow=True)
|
||||
|
||||
self.assertEqual(err.exception.errno, pyaudio.paInputOverflowed)
|
||||
self.assertEqual(err.exception.strerror, 'Input overflowed')
|
|
@ -0,0 +1,642 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Automated unit tests for testing audio playback and capture.
|
||||
|
||||
These tests require an OS loopback sound device that forwards audio
|
||||
output, generated by PyAudio for playback, and forwards it to an input
|
||||
device, which PyAudio can record and verify against a test signal.
|
||||
|
||||
On Mac OS X, Soundflower can create such a device.
|
||||
|
||||
On GNU/Linux, the snd-aloop kernel module provides a loopback ALSA
|
||||
device. Use examples/system_info.py to identify the name of the loopback
|
||||
device.
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import time
|
||||
import unittest
|
||||
import wave
|
||||
import sys
|
||||
|
||||
import numpy
|
||||
|
||||
import pyaudio
|
||||
|
||||
DUMP_CAPTURE=False
|
||||
|
||||
class PyAudioTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.p = pyaudio.PyAudio()
|
||||
(self.loopback_input_idx,
|
||||
self.loopback_output_idx) = self.get_audio_loopback()
|
||||
assert (self.loopback_input_idx is None
|
||||
or self.loopback_input_idx >= 0), "No loopback device found"
|
||||
assert (self.loopback_output_idx is None
|
||||
or self.loopback_output_idx >= 0), "No loopback device found"
|
||||
|
||||
def tearDown(self):
|
||||
self.p.terminate()
|
||||
|
||||
def get_audio_loopback(self):
|
||||
if sys.platform == 'darwin':
|
||||
return self._find_audio_loopback(
|
||||
'Soundflower (2ch)', 'Soundflower (2ch)')
|
||||
if sys.platform in ('linux', 'linux2'):
|
||||
return self._find_audio_loopback(
|
||||
'Loopback: PCM (hw:1,0)', 'Loopback: PCM (hw:1,1)')
|
||||
if sys.platform == 'win32':
|
||||
# Assumes running in a VM, in which the hypervisor can
|
||||
# set up a loopback device to back the "default" audio devices.
|
||||
# Here, None indicates default device.
|
||||
return None, None
|
||||
|
||||
return -1, -1
|
||||
|
||||
def _find_audio_loopback(self, indev, outdev):
|
||||
"""Utility to find audio loopback device."""
|
||||
input_idx, output_idx = -1, -1
|
||||
for device_idx in range(self.p.get_device_count()):
|
||||
devinfo = self.p.get_device_info_by_index(device_idx)
|
||||
if (outdev == devinfo.get('name') and
|
||||
devinfo.get('maxOutputChannels', 0) > 0):
|
||||
output_idx = device_idx
|
||||
|
||||
if (indev == devinfo.get('name') and
|
||||
devinfo.get('maxInputChannels', 0) > 0):
|
||||
input_idx = device_idx
|
||||
|
||||
if output_idx > -1 and input_idx > -1:
|
||||
break
|
||||
|
||||
return input_idx, output_idx
|
||||
|
||||
def test_system_info(self):
|
||||
"""Basic system info tests"""
|
||||
self.assertTrue(self.p.get_host_api_count() > 0)
|
||||
self.assertTrue(self.p.get_device_count() > 0)
|
||||
api_info = self.p.get_host_api_info_by_index(0)
|
||||
self.assertTrue(len(api_info.items()) > 0)
|
||||
|
||||
def test_input_output_blocking(self):
|
||||
"""Test blocking-based record and playback."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
# Blocking-mode might add some initial choppiness on some
|
||||
# platforms/loopback devices, so set a longer duration.
|
||||
duration = 3 # seconds
|
||||
frames_per_chunk = 1024
|
||||
|
||||
freqs = [130.81, 329.63, 440.0, 466.16, 587.33, 739.99]
|
||||
test_signal = self.create_reference_signal(freqs, rate, width, duration)
|
||||
audio_chunks = self.signal_to_chunks(
|
||||
test_signal, frames_per_chunk, channels)
|
||||
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx)
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx)
|
||||
|
||||
captured = []
|
||||
for chunk in audio_chunks:
|
||||
out_stream.write(chunk)
|
||||
captured.append(in_stream.read(frames_per_chunk))
|
||||
# Capture a few more frames, since there is some lag.
|
||||
for i in range(8):
|
||||
captured.append(in_stream.read(frames_per_chunk))
|
||||
|
||||
in_stream.stop_stream()
|
||||
out_stream.stop_stream()
|
||||
|
||||
if DUMP_CAPTURE:
|
||||
self.write_wav('test_blocking.wav', b''.join(captured),
|
||||
width, channels, rate)
|
||||
|
||||
captured_signal = self.pcm16_to_numpy(b''.join(captured))
|
||||
captured_left_channel = captured_signal[::2]
|
||||
captured_right_channel = captured_signal[1::2]
|
||||
|
||||
self.assert_pcm16_spectrum_nearly_equal(
|
||||
rate,
|
||||
captured_left_channel,
|
||||
test_signal,
|
||||
len(freqs))
|
||||
self.assert_pcm16_spectrum_nearly_equal(
|
||||
rate,
|
||||
captured_right_channel,
|
||||
test_signal,
|
||||
len(freqs))
|
||||
|
||||
def test_input_output_callback(self):
|
||||
"""Test callback-based record and playback."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
duration = 1 # second
|
||||
frames_per_chunk = 1024
|
||||
|
||||
freqs = [130.81, 329.63, 440.0, 466.16, 587.33, 739.99]
|
||||
test_signal = self.create_reference_signal(freqs, rate, width, duration)
|
||||
audio_chunks = self.signal_to_chunks(
|
||||
test_signal, frames_per_chunk, channels)
|
||||
|
||||
state = {'count': 0}
|
||||
def out_callback(_, frame_count, time_info, status):
|
||||
if state['count'] >= len(audio_chunks):
|
||||
return ('', pyaudio.paComplete)
|
||||
rval = (audio_chunks[state['count']], pyaudio.paContinue)
|
||||
state['count'] += 1
|
||||
return rval
|
||||
|
||||
captured = []
|
||||
def in_callback(in_data, frame_count, time_info, status):
|
||||
captured.append(in_data)
|
||||
return (None, pyaudio.paContinue)
|
||||
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx,
|
||||
stream_callback=in_callback)
|
||||
|
||||
in_stream.start_stream()
|
||||
out_stream.start_stream()
|
||||
time.sleep(duration + 1)
|
||||
in_stream.stop_stream()
|
||||
out_stream.stop_stream()
|
||||
|
||||
if DUMP_CAPTURE:
|
||||
self.write_wav('test_callback.wav', b''.join(captured),
|
||||
width, channels, rate)
|
||||
|
||||
captured_signal = self.pcm16_to_numpy(b''.join(captured))
|
||||
captured_left_channel = captured_signal[::2]
|
||||
captured_right_channel = captured_signal[1::2]
|
||||
|
||||
self.assert_pcm16_spectrum_nearly_equal(
|
||||
rate,
|
||||
captured_left_channel,
|
||||
test_signal,
|
||||
len(freqs))
|
||||
self.assert_pcm16_spectrum_nearly_equal(
|
||||
rate,
|
||||
captured_right_channel,
|
||||
test_signal,
|
||||
len(freqs))
|
||||
|
||||
def test_device_lock_gil_order(self):
|
||||
"""Ensure no deadlock between Pa_{Open,Start,Stop}Stream and GIL."""
|
||||
# This test targets OSX/macOS CoreAudio, which seems to use
|
||||
# audio device locks. On ALSA and Win32 MME, this problem
|
||||
# doesn't seem to appear despite not releasing the GIL when
|
||||
# calling into PortAudio.
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def out_callback(_, frame_count, time_info, status):
|
||||
return ('', pyaudio.paComplete)
|
||||
|
||||
def in_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx,
|
||||
stream_callback=in_callback)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
in_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Opening a stream and starting it MUST
|
||||
# release the GIL before attempting to acquire the device
|
||||
# lock. Otherwise, the following code will wait for the device
|
||||
# lock (while holding the GIL), while the in_callback thread
|
||||
# will be waiting for the GIL once time.sleep completes (while
|
||||
# holding the device lock), leading to deadlock.
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
out_stream.start_stream()
|
||||
|
||||
time.sleep(0.1)
|
||||
in_stream.stop_stream()
|
||||
out_stream.stop_stream()
|
||||
|
||||
def test_stream_state_gil(self):
|
||||
"""Ensure no deadlock between Pa_IsStream{Active,Stopped} and GIL."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def out_callback(_, frame_count, time_info, status):
|
||||
return ('', pyaudio.paComplete)
|
||||
|
||||
def in_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx,
|
||||
stream_callback=in_callback)
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
in_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Checking the state of the stream MUST
|
||||
# not require the device lock, but if it does, it must release the GIL
|
||||
# before attempting to acquire the device
|
||||
# lock. Otherwise, the following code will wait for the device
|
||||
# lock (while holding the GIL), while the in_callback thread
|
||||
# will be waiting for the GIL once time.sleep completes (while
|
||||
# holding the device lock), leading to deadlock.
|
||||
self.assertTrue(in_stream.is_active())
|
||||
self.assertFalse(in_stream.is_stopped())
|
||||
|
||||
self.assertTrue(out_stream.is_stopped())
|
||||
self.assertFalse(out_stream.is_active())
|
||||
out_stream.start_stream()
|
||||
self.assertFalse(out_stream.is_stopped())
|
||||
self.assertTrue(out_stream.is_active())
|
||||
|
||||
time.sleep(0.1)
|
||||
in_stream.stop_stream()
|
||||
out_stream.stop_stream()
|
||||
|
||||
def test_get_stream_time_gil(self):
|
||||
"""Ensure no deadlock between PA_GetStreamTime and GIL."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def out_callback(_, frame_count, time_info, status):
|
||||
return ('', pyaudio.paComplete)
|
||||
|
||||
def in_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx,
|
||||
stream_callback=in_callback)
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
in_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Getting the stream time MUST not
|
||||
# require the device lock, but if it does, it must release the
|
||||
# GIL before attempting to acquire the device lock. Otherwise,
|
||||
# the following code will wait for the device lock (while
|
||||
# holding the GIL), while the in_callback thread will be
|
||||
# waiting for the GIL once time.sleep completes (while holding
|
||||
# the device lock), leading to deadlock.
|
||||
self.assertGreater(in_stream.get_time(), -1)
|
||||
self.assertGreater(out_stream.get_time(), 1)
|
||||
|
||||
time.sleep(0.1)
|
||||
in_stream.stop_stream()
|
||||
out_stream.stop_stream()
|
||||
|
||||
def test_get_stream_cpuload_gil(self):
|
||||
"""Ensure no deadlock between Pa_GetStreamCpuLoad and GIL."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def out_callback(_, frame_count, time_info, status):
|
||||
return ('', pyaudio.paComplete)
|
||||
|
||||
def in_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx,
|
||||
stream_callback=in_callback)
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
in_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Getting the stream cpuload MUST not
|
||||
# require the device lock, but if it does, it must release the
|
||||
# GIL before attempting to acquire the device lock. Otherwise,
|
||||
# the following code will wait for the device lock (while
|
||||
# holding the GIL), while the in_callback thread will be
|
||||
# waiting for the GIL once time.sleep completes (while holding
|
||||
# the device lock), leading to deadlock.
|
||||
self.assertGreater(in_stream.get_cpu_load(), -1)
|
||||
self.assertGreater(out_stream.get_cpu_load(), -1)
|
||||
|
||||
time.sleep(0.1)
|
||||
in_stream.stop_stream()
|
||||
out_stream.stop_stream()
|
||||
|
||||
def test_get_stream_write_available_gil(self):
|
||||
"""Ensure no deadlock between Pa_GetStreamWriteAvailable and GIL."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def in_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx,
|
||||
stream_callback=in_callback)
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
in_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Getting the stream write available MUST not
|
||||
# require the device lock, but if it does, it must release the
|
||||
# GIL before attempting to acquire the device lock. Otherwise,
|
||||
# the following code will wait for the device lock (while
|
||||
# holding the GIL), while the in_callback thread will be
|
||||
# waiting for the GIL once time.sleep completes (while holding
|
||||
# the device lock), leading to deadlock.
|
||||
self.assertGreater(out_stream.get_write_available(), -1)
|
||||
|
||||
time.sleep(0.1)
|
||||
in_stream.stop_stream()
|
||||
|
||||
def test_get_stream_read_available_gil(self):
|
||||
"""Ensure no deadlock between Pa_GetStreamReadAvailable and GIL."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def out_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
in_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
input=True,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
input_device_index=self.loopback_input_idx)
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
out_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Getting the stream read available MUST not
|
||||
# require the device lock, but if it does, it must release the
|
||||
# GIL before attempting to acquire the device lock. Otherwise,
|
||||
# the following code will wait for the device lock (while
|
||||
# holding the GIL), while the in_callback thread will be
|
||||
# waiting for the GIL once time.sleep completes (while holding
|
||||
# the device lock), leading to deadlock.
|
||||
self.assertGreater(in_stream.get_read_available(), -1)
|
||||
|
||||
time.sleep(0.1)
|
||||
in_stream.stop_stream()
|
||||
|
||||
def test_terminate_gil(self):
|
||||
"""Ensure no deadlock between Pa_Terminate and GIL."""
|
||||
rate = 44100 # frames per second
|
||||
width = 2 # bytes per sample
|
||||
channels = 2
|
||||
frames_per_chunk = 1024
|
||||
|
||||
def out_callback(in_data, frame_count, time_info, status):
|
||||
# Release the GIL for a bit
|
||||
time.sleep(2)
|
||||
return (None, pyaudio.paComplete)
|
||||
|
||||
out_stream = self.p.open(
|
||||
format=self.p.get_format_from_width(width),
|
||||
channels=channels,
|
||||
rate=rate,
|
||||
output=True,
|
||||
start=False,
|
||||
frames_per_buffer=frames_per_chunk,
|
||||
output_device_index=self.loopback_output_idx,
|
||||
stream_callback=out_callback)
|
||||
# In a separate (C) thread, portaudio/driver will grab the device lock,
|
||||
# then the GIL to call in_callback.
|
||||
out_stream.start_stream()
|
||||
# Wait a bit to let that callback thread start.
|
||||
time.sleep(1)
|
||||
# in_callback will eventually drop the GIL when executing
|
||||
# time.sleep (while retaining the device lock), allowing the
|
||||
# following code to run. Terminating PyAudio MUST not
|
||||
# require the device lock, but if it does, it must release the
|
||||
# GIL before attempting to acquire the device lock. Otherwise,
|
||||
# the following code will wait for the device lock (while
|
||||
# holding the GIL), while the in_callback thread will be
|
||||
# waiting for the GIL once time.sleep completes (while holding
|
||||
# the device lock), leading to deadlock.
|
||||
self.p.terminate()
|
||||
|
||||
@staticmethod
|
||||
def create_reference_signal(freqs, sampling_rate, width, duration):
|
||||
"""Return reference signal with several sinuoids with frequencies
|
||||
specified by freqs."""
|
||||
total_frames = int(sampling_rate * duration)
|
||||
max_amp = float(2**(width * 8 - 1) - 1)
|
||||
avg_amp = max_amp / len(freqs)
|
||||
return [
|
||||
int(sum(avg_amp * math.sin(2*math.pi*freq*(k/float(sampling_rate)))
|
||||
for freq in freqs))
|
||||
for k in range(total_frames)]
|
||||
|
||||
@staticmethod
|
||||
def signal_to_chunks(frame_data, frames_per_chunk, channels):
|
||||
"""Given an array of values comprising the signal, return an iterable
|
||||
of binary chunks, with each chunk containing frames_per_chunk
|
||||
frames. Each frame represents a single value from the signal,
|
||||
duplicated for each channel specified by channels.
|
||||
"""
|
||||
frames = [struct.pack('h', x) * channels for x in frame_data]
|
||||
# Chop up frames into chunks
|
||||
return [b''.join(chunk_frames) for chunk_frames in
|
||||
tuple(frames[i:i+frames_per_chunk]
|
||||
for i in range(0, len(frames), frames_per_chunk))]
|
||||
|
||||
@staticmethod
|
||||
def pcm16_to_numpy(bytestring):
|
||||
"""From PCM 16-bit bytes, return an equivalent numpy array of values."""
|
||||
return struct.unpack('%dh' % (len(bytestring) / 2), bytestring)
|
||||
|
||||
@staticmethod
|
||||
def write_wav(filename, data, width, channels, rate):
|
||||
"""Write PCM data to wave file."""
|
||||
wf = wave.open(filename, 'wb')
|
||||
wf.setnchannels(channels)
|
||||
wf.setsampwidth(width)
|
||||
wf.setframerate(rate)
|
||||
wf.writeframes(data)
|
||||
wf.close()
|
||||
|
||||
def assert_pcm16_spectrum_nearly_equal(self, sampling_rate, cap, ref,
|
||||
num_freq_peaks_expected):
|
||||
"""Compares the discrete fourier transform of a captured signal
|
||||
against the reference signal and ensures that the frequency peaks
|
||||
match."""
|
||||
# When passing a reference signal through the loopback device,
|
||||
# the captured signal may include additional noise, as well as
|
||||
# time lag, so testing that the captured signal is "similar
|
||||
# enough" to the reference using bit-wise equality won't work
|
||||
# well. Instead, the approach here a) assumes the reference
|
||||
# signal is a sum of sinusoids and b) computes the discrete
|
||||
# fourier transform of the reference and captured signals, and
|
||||
# ensures that the frequencies of the top
|
||||
# num_freq_peaks_expected frequency peaks are close.
|
||||
cap_fft = numpy.absolute(numpy.fft.rfft(cap))
|
||||
ref_fft = numpy.absolute(numpy.fft.rfft(ref))
|
||||
# Find the indices of the peaks:
|
||||
cap_peak_indices = sorted(numpy.argpartition(
|
||||
cap_fft, -num_freq_peaks_expected)[-num_freq_peaks_expected:])
|
||||
ref_peak_indices = sorted(numpy.argpartition(
|
||||
ref_fft, -num_freq_peaks_expected)[-num_freq_peaks_expected:])
|
||||
# Ensure that the corresponding frequencies of the peaks are close:
|
||||
for cap_freq_index, ref_freq_index in zip(cap_peak_indices,
|
||||
ref_peak_indices):
|
||||
cap_freq = cap_freq_index / float(len(cap)) * (sampling_rate / 2)
|
||||
ref_freq = ref_freq_index / float(len(ref)) * (sampling_rate / 2)
|
||||
diff = abs(cap_freq - ref_freq)
|
||||
self.assertLess(diff, 1.0)
|
||||
|
||||
# As an additional test, verify that the spectrum (not just
|
||||
# the peaks) of the reference and captured signal are similar
|
||||
# by computing the cross-correlation of the spectra. Assuming they
|
||||
# are nearly identical, the cross-correlation should contain a large
|
||||
# peak when the spectra overlap and mostly 0s elsewhere. Verify that
|
||||
# using a histogram of the cross-correlation:
|
||||
freq_corr_hist, _ = numpy.histogram(
|
||||
numpy.correlate(cap_fft, ref_fft, mode='full'),
|
||||
bins=10)
|
||||
self.assertLess(sum(freq_corr_hist[2:])/sum(freq_corr_hist), 1e-2)
|
Loading…
Reference in New Issue