Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/static-analyzers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Run static analysis tools
on: [push, pull_request]
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- name: Double-check Python version
run: |
python --version
- name: Clone Git repository
uses: actions/checkout@v4
- name: Install sounddevice module
run: |
python -m pip install .
- name: Install Pyright and NumPy
run: |
python -m pip install pyright numpy
- name: Check sounddevice module with Pyright
run: |
python -m pyright src/sounddevice.py
- name: Install dependencies for examples
run: |
python -m pip install ffmpeg-python matplotlib soundfile
- name: Check examples with Pyright
run: |
python -m pyright examples/*.py
2 changes: 1 addition & 1 deletion examples/play_long_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def callback(outdata, frames, time, status):
callback=callback, finished_callback=event.set)
with stream:
timeout = args.blocksize * args.buffersize / f.samplerate
while len(data):
while len(data): # type: ignore
data = f.read(args.blocksize)
q.put(data, timeout=timeout)
event.wait() # Wait until playback is finished
Expand Down
2 changes: 1 addition & 1 deletion examples/play_long_file_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def callback(outdata, frames, time, status):
callback=callback, finished_callback=event.set)
with stream:
timeout = args.blocksize * args.buffersize / f.samplerate
while data:
while data: # type: ignore
data = f.buffer_read(args.blocksize, dtype='float32')
q.put(data, timeout=timeout)
event.wait() # Wait until playback is finished
Expand Down
6 changes: 2 additions & 4 deletions examples/rec_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ def validate(self):

class RecGui(tk.Tk):

stream = None

def __init__(self):
super().__init__()

Expand Down Expand Up @@ -122,7 +120,7 @@ def __init__(self):
self.update_gui()

def create_stream(self, device=None):
if self.stream is not None:
with contextlib.suppress(AttributeError):
self.stream.close()
self.stream = sd.InputStream(
device=device, channels=1, callback=self.audio_callback)
Expand Down Expand Up @@ -205,7 +203,7 @@ def on_settings(self, *args):
def init_buttons(self):
self.rec_button['text'] = 'record'
self.rec_button['command'] = self.on_rec
if self.stream:
if hasattr(self, 'stream'):
self.rec_button['state'] = 'normal'
self.settings_button['state'] = 'normal'

Expand Down
2 changes: 1 addition & 1 deletion examples/spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def int_or_str(text):
print(sd.query_devices())
parser.exit(0)
parser = argparse.ArgumentParser(
description=__doc__ + '\n\nSupported keys:' + usage_line,
description=__doc__ + '\n\nSupported keys:' + usage_line, # type: ignore
formatter_class=argparse.RawDescriptionHelpFormatter,
parents=[parser])
parser.add_argument(
Expand Down
70 changes: 39 additions & 31 deletions src/sounddevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
break
else:
raise OSError('PortAudio library not found')
_lib = _ffi.dlopen(_libname)
_lib: ... = _ffi.dlopen(_libname)
except OSError:
if _platform.system() == 'Darwin':
_libname = 'libportaudio.dylib'
Expand All @@ -87,9 +87,9 @@
import _sounddevice_data
_libname = _os.path.join(
next(iter(_sounddevice_data.__path__)), 'portaudio-binaries', _libname)
_lib = _ffi.dlopen(_libname)
_lib: ... = _ffi.dlopen(_libname)

_sampleformats = {
_sampleformats: ... = {
'float32': _lib.paFloat32,
'int32': _lib.paInt32,
'int24': _lib.paInt24,
Expand Down Expand Up @@ -457,7 +457,7 @@ def get_stream():
raise RuntimeError('play()/rec()/playrec() was not called yet')


def query_devices(device=None, kind=None):
def query_devices(device=None, kind=None) -> ...:
"""Return information about available devices.

Information and capabilities of PortAudio devices.
Expand Down Expand Up @@ -575,7 +575,7 @@ def query_devices(device=None, kind=None):
if not info:
raise PortAudioError(f'Error querying device {device}')
assert info.structVersion == 2
name_bytes = _ffi.string(info.name)
name_bytes = _ffi_string(info.name)
try:
# We don't know beforehand if DirectSound and MME device names use
# 'utf-8' or 'mbcs' encoding. Let's try 'utf-8' first, because it more
Expand Down Expand Up @@ -610,7 +610,7 @@ def query_devices(device=None, kind=None):
return device_dict


def query_hostapis(index=None):
def query_hostapis(index=None) -> ...:
"""Return information about available host APIs.

Parameters
Expand Down Expand Up @@ -655,7 +655,7 @@ def query_hostapis(index=None):
raise PortAudioError(f'Error querying host API {index}')
assert info.structVersion == 1
return {
'name': _ffi.string(info.name).decode(),
'name': _ffi_string(info.name).decode(),
'devices': [_lib.Pa_HostApiDeviceIndexToDeviceIndex(index, i)
for i in range(info.deviceCount)],
'default_input_device': info.defaultInputDevice,
Expand Down Expand Up @@ -725,7 +725,7 @@ def get_portaudio_version():
(1899, 'PortAudio V19-devel (built Feb 15 2014 23:28:00)')

"""
return _lib.Pa_GetVersion(), _ffi.string(_lib.Pa_GetVersionText()).decode()
return _lib.Pa_GetVersion(), _ffi_string(_lib.Pa_GetVersionText()).decode()


class _StreamBase:
Expand Down Expand Up @@ -1208,7 +1208,7 @@ def _raw_read(self, frames):
"""
channels, _ = _split(self._channels)
samplesize, _ = _split(self._samplesize)
data = _ffi.new('signed char[]', channels * samplesize * frames)
data = _ffi.new('signed char[]', channels * samplesize * frames) # type: ignore
err = _lib.Pa_ReadStream(self._ptr, data, frames)
if err == _lib.paInputOverflowed:
overflowed = True
Expand Down Expand Up @@ -1306,7 +1306,7 @@ def _raw_write(self, data):
pass # input is not a buffer
_, samplesize = _split(self._samplesize)
_, channels = _split(self._channels)
samples, remainder = divmod(len(data), samplesize)
samples, remainder = divmod(len(data), samplesize) # type: ignore
if remainder:
raise ValueError('len(data) not divisible by samplesize')
frames, remainder = divmod(samples, channels)
Expand Down Expand Up @@ -2056,7 +2056,7 @@ class _InputOutputPair:
_indexmapping = {'input': 0, 'output': 1}

def __init__(self, parent, default_attr):
self._pair = [None, None]
self._pair: ... = [None, None]
self._parent = parent
self._default_attr = default_attr

Expand Down Expand Up @@ -2117,7 +2117,7 @@ class default:
_pairs = 'device', 'channels', 'dtype', 'latency', 'extra_settings'
# The class attributes listed in _pairs are only provided here for static
# analysis tools and for the docs. They're overwritten in __init__().
device = None, None
device: ... = (None, None)
"""Index or query string of default input/output device.

If not overwritten, this is queried from PortAudio.
Expand All @@ -2127,15 +2127,17 @@ class default:
`default`, `query_devices()`, the *device* argument of `Stream`

"""
channels = _default_channels = None, None
_default_channels = None, None
channels: ... = _default_channels
"""Default number of input/output channels.

See Also
--------
`default`, `query_devices()`, the *channels* argument of `Stream`

"""
dtype = _default_dtype = 'float32', 'float32'
_default_dtype = 'float32', 'float32'
dtype: ... = _default_dtype
"""Default data type used for input/output samples.

The types ``'float32'``, ``'int32'``, ``'int16'``, ``'int8'`` and
Expand All @@ -2151,25 +2153,27 @@ class default:
`default`, `numpy:numpy.dtype`, the *dtype* argument of `Stream`

"""
latency = _default_latency = 'high', 'high'
_default_latency = 'high', 'high'
latency: ... = _default_latency
"""See the *latency* argument of `Stream`."""
extra_settings = _default_extra_settings = None, None
_default_extra_settings = None, None
extra_settings: ... = _default_extra_settings
"""Host-API-specific input/output settings.

See Also
--------
AsioSettings, CoreAudioSettings, WasapiSettings

"""
samplerate = None
samplerate: ... = None
"""Sampling frequency in Hertz (= frames per second).

See Also
--------
`default`, `query_devices()`

"""
blocksize = _lib.paFramesPerBufferUnspecified
blocksize: ... = _lib.paFramesPerBufferUnspecified
"""See the *blocksize* argument of `Stream`."""
clip_off = False
"""Disable clipping.
Expand Down Expand Up @@ -2229,10 +2233,12 @@ def _default_device(self):
_lib.Pa_GetDefaultOutputDevice())

@property
def hostapi(self):
def hostapi(self): # type: ignore
"""Index of the default host API (read-only)."""
return _check(_lib.Pa_GetDefaultHostApi())

hostapi: int

def reset(self):
"""Reset all attributes to their "factory default"."""
vars(self).clear()
Expand All @@ -2241,7 +2247,8 @@ def reset(self):

if not hasattr(_ffi, 'I_AM_FAKE'):
# This object shadows the 'default' class, except when building the docs.
default = default()
_default_instance: ... = default()
default = _default_instance


class PortAudioError(Exception):
Expand Down Expand Up @@ -2511,13 +2518,10 @@ def __init__(self, exclusive=False, auto_convert=False, explicit_sample_format=F
class _CallbackContext:
"""Helper class for reuse in play()/rec()/playrec() callbacks."""

blocksize = None
data = None
out = None
frame = 0
frames: int
input_channels = output_channels = None
input_dtype = output_dtype = None
input_mapping = output_mapping = None
silent_channels = None

def __init__(self, loop=False):
Expand Down Expand Up @@ -2561,7 +2565,7 @@ def check_data(self, data, mapping, device):
if len(mapping) + len(silent_channels) != channels:
raise ValueError('each channel may only appear once in mapping')

self.data = data
self.data: np.typing.NDArray = data
self.output_channels = channels
self.output_dtype = dtype
self.output_mapping = mapping
Expand Down Expand Up @@ -2645,8 +2649,8 @@ def callback_exit(self):
def finished_callback(self):
self.event.set()
# Drop temporary audio buffers to free memory
self.data = None
self.out = None
del self.data
del self.out
# Drop CFFI objects to avoid reference cycles
self.stream._callback = None
self.stream._finished_callback = None
Expand Down Expand Up @@ -2679,6 +2683,10 @@ def wait(self, ignore_errors=True):
return self.status if self.status else None


def _ffi_string(cdata) -> bytes:
return _ffi.string(cdata) # type: ignore


def _remove_self(d):
"""Return a copy of d without the 'self' entry."""
d = d.copy()
Expand Down Expand Up @@ -2715,7 +2723,7 @@ def _check_dtype(dtype):


def _get_stream_parameters(kind, device, channels, dtype, latency,
extra_settings, samplerate):
extra_settings: ..., samplerate):
"""Get parameters for one direction (input or output) of a stream."""
assert kind in ('input', 'output')
if device is None:
Expand Down Expand Up @@ -2753,7 +2761,7 @@ def _get_stream_parameters(kind, device, channels, dtype, latency,
latency = info['default_' + latency + '_' + kind + '_latency']
if samplerate is None:
samplerate = info['default_samplerate']
parameters = _ffi.new('PaStreamParameters*', (
parameters: ... = _ffi.new('PaStreamParameters*', (
device, channels, sampleformat, latency,
extra_settings._streaminfo if extra_settings else _ffi.NULL))
return parameters, dtype, samplesize, samplerate
Expand Down Expand Up @@ -2809,7 +2817,7 @@ def _check(err, msg=''):
if err >= 0:
return err

errormsg = _ffi.string(_lib.Pa_GetErrorText(err)).decode()
errormsg = _ffi_string(_lib.Pa_GetErrorText(err)).decode()
if msg:
errormsg = f'{msg}: {errormsg}'

Expand All @@ -2820,7 +2828,7 @@ def _check(err, msg=''):
# in scenarios where multiple APIs are being used simultaneously.
info = _lib.Pa_GetLastHostErrorInfo()
host_api = _lib.Pa_HostApiTypeIdToHostApiIndex(info.hostApiType)
hosterror_text = _ffi.string(info.errorText).decode()
hosterror_text = _ffi_string(info.errorText).decode()
hosterror_info = host_api, info.errorCode, hosterror_text
raise PortAudioError(errormsg, err, hosterror_info)

Expand Down