Skip to content
Open
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
10 changes: 10 additions & 0 deletions doc/api/platform-specific-settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ Platform-specific Settings
.. autosummary::
:nosignatures:

AlsaSettings
AsioSettings
CoreAudioSettings
WasapiSettings

.. autodata:: DEVICE_FROM_EXTRA_SETTINGS

Pass this as ``device`` to select a device through ``extra_settings``.
Device information is unavailable for this value, so channels and sample
rate must be set explicitly or through defaults, and stream latency must
be numeric. Format checks do not require latency.
Comment on lines +18 to +21

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you adding this text here instead of sounddevice.py?

Also, this text seems a bit long ... what about linking to AlsaSettings (which is AFAIK the only host API that supports it) and having all the details documented there, avoiding some repetition?


.. autoclass:: AlsaSettings

.. autoclass:: AsioSettings

.. autoclass:: CoreAudioSettings
Expand Down
1 change: 1 addition & 0 deletions doc/fake__sounddevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class FakeLibrary:

paFloat32 = paInt32 = paInt24 = paInt16 = paInt8 = paUInt8 = NotImplemented
paFramesPerBufferUnspecified = 0
paUseHostApiSpecificDeviceSpecification = -2

def Pa_Initialize(self):
return 0
Expand Down
9 changes: 9 additions & 0 deletions sounddevice_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@

typedef unsigned long PaWinWaveFormatChannelMask;

/* pa_linux_alsa.h */
typedef struct PaAlsaStreamInfo {
unsigned long size;
PaHostApiTypeId hostApiType;
unsigned long version;
const char *deviceString;
} PaAlsaStreamInfo;
void PaAlsa_InitializeStreamInfo(PaAlsaStreamInfo *info);

/* pa_asio.h */

#define paAsioUseChannelSelectors 0x01
Expand Down
126 changes: 110 additions & 16 deletions src/sounddevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
next(iter(_sounddevice_data.__path__)), 'portaudio-binaries', _libname)
_lib: ... = _ffi.dlopen(_libname)

DEVICE_FROM_EXTRA_SETTINGS = _lib.paUseHostApiSpecificDeviceSpecification
"""Select a device through host-API-specific ``extra_settings``."""

_sampleformats: ... = {
'float32': _lib.paFloat32,
'int32': _lib.paInt32,
Expand Down Expand Up @@ -690,7 +693,7 @@ def check_input_settings(device=None, channels=None, dtype=None,
"""
parameters, dtype, samplesize, samplerate = _get_stream_parameters(
'input', device=device, channels=channels, dtype=dtype, latency=None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole for_check argument seems unnecessarily complicated, why not just pass 0 as latency?

Suggested change
'input', device=device, channels=channels, dtype=dtype, latency=None,
'input', device=device, channels=channels, dtype=dtype, latency=0,

extra_settings=extra_settings, samplerate=samplerate)
extra_settings=extra_settings, samplerate=samplerate, for_check=True)
_check(_lib.Pa_IsFormatSupported(parameters, _ffi.NULL, samplerate))


Expand All @@ -704,7 +707,7 @@ def check_output_settings(device=None, channels=None, dtype=None,
"""
parameters, dtype, samplesize, samplerate = _get_stream_parameters(
'output', device=device, channels=channels, dtype=dtype, latency=None,
extra_settings=extra_settings, samplerate=samplerate)
extra_settings=extra_settings, samplerate=samplerate, for_check=True)
_check(_lib.Pa_IsFormatSupported(_ffi.NULL, parameters, samplerate))


Expand Down Expand Up @@ -808,6 +811,12 @@ def __init__(self, kind, samplerate=None, blocksize=None, device=None,
if prime_output_buffers_using_stream_callback:
stream_flags |= _lib.paPrimeOutputBuffersUsingStreamCallback

# Retain effective settings, including their C buffers, through close.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is hard to understand. I guess it's about what in other places is called "keep alive"?

if extra_settings is None:
extra_settings = tuple(default.extra_settings)
self._extra_settings = (tuple(_split(extra_settings)),
tuple(default.extra_settings))
Comment on lines +815 to +818

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how that combination of tuple and _split() and handling of default is supposed to work?

Note that _split() is also used later, and _get_stream_parameters() is supposed to handle default values.


if kind == 'duplex':
idevice, odevice = _split(device)
ichannels, ochannels = _split(channels)
Expand Down Expand Up @@ -1661,6 +1670,10 @@ def __init__(self, samplerate=None, blocksize=None,
all space-separated parts in the right order. Each device
string contains the name of the corresponding host API in
the end. The string comparison is case-insensitive.
Use `DEVICE_FROM_EXTRA_SETTINGS` to select a device through
*extra_settings*, e.g. with `AlsaSettings`. In this case,
channels, sample rate and numeric latency must be supplied
explicitly or through defaults.
Comment on lines +1674 to +1676

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't think this needs to be repeated here:

Suggested change
*extra_settings*, e.g. with `AlsaSettings`. In this case,
channels, sample rate and numeric latency must be supplied
explicitly or through defaults.
*extra_settings*, e.g. with `AlsaSettings`.

channels : int or pair of int, optional
The number of channels of sound to be delivered to the
stream callback or accessed by `read()` or `write()`. It
Expand Down Expand Up @@ -2167,7 +2180,7 @@ class default:

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

"""
samplerate: ... = None
Expand Down Expand Up @@ -2316,6 +2329,56 @@ class CallbackAbort(Exception):
"""


class AlsaSettings:

def __init__(self, device_string):
"""ALSA-specific settings for opening a PCM by address.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other classes use something like this:

Suggested change
"""ALSA-specific settings for opening a PCM by address.
"""ALSA-specific input/output settings.

You can also use the next sentence from the other classes, to keep them consistent.


Use as ``extra_settings`` with
``device=sd.DEVICE_FROM_EXTRA_SETTINGS``. Supplying these settings
alone does not override `default.device`. Channels and sample rate
must be supplied explicitly or through defaults. Streams also
require a numeric latency in seconds.
Comment on lines +2337 to +2341

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be moved to the documentation of device_string, because theoretically other things might be added in the future, then this paragraph might not make sense anymore.


Parameters
----------
device_string : str
ALSA PCM address, such as ``'hw:2,0'`` or a named PCM from the
ALSA configuration. It must be nonempty and contain no NUL.
An ALSA address is not a persistent hardware identifier.

Notes
-----
The stream's device attribute reports PortAudio's special value
``-2`` when `DEVICE_FROM_EXTRA_SETTINGS` is selected.
PortAudio rejects these settings with a regular ALSA device index.
For direct output with ``mapping=[1]`` in `play()` or `playrec()`,
use `OutputStream` with explicit channels instead; automatic
stereo detection requires enumerated device information.
Comment on lines +2350 to +2357

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these notes also belong into the device_string section?


Examples
--------
>>> import sounddevice as sd
>>> with sd.RawInputStream(
... device=sd.DEVICE_FROM_EXTRA_SETTINGS,
... samplerate=48000, channels=2, dtype='int16',
... blocksize=1024, latency=4096 / 48000,
... extra_settings=sd.AlsaSettings('null')) as stream:
... data, overflowed = stream.read(1024)

"""
if not isinstance(device_string, str):
raise TypeError('device_string must be a string')
if not device_string or '\0' in device_string:
raise ValueError('device_string must be nonempty and contain no NUL')
Comment on lines +2370 to +2373

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those errors are not really necessary:

Suggested change
if not isinstance(device_string, str):
raise TypeError('device_string must be a string')
if not device_string or '\0' in device_string:
raise ValueError('device_string must be nonempty and contain no NUL')

_check(_lib.Pa_HostApiTypeIdToHostApiIndex(_lib.paALSA),
'ALSA host API is unavailable')
Comment on lines +2374 to +2375

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this check for?

self._device_string = _ffi.new('char[]', device_string.encode('utf-8'))
self._streaminfo: ... = _ffi.new('PaAlsaStreamInfo*')
_lib.PaAlsa_InitializeStreamInfo(self._streaminfo)
self._streaminfo.deviceString = self._device_string


class AsioSettings:

def __init__(self, channel_selectors):
Expand Down Expand Up @@ -2730,11 +2793,12 @@ def _check_dtype(dtype):


def _get_stream_parameters(kind, device, channels, dtype, latency,
extra_settings: ..., samplerate):
extra_settings: ..., samplerate, *, for_check=False):
"""Get parameters for one direction (input or output) of a stream."""
assert kind in ('input', 'output')
if device is None:
device = default.device
if extra_settings is None:
extra_settings = default.extra_settings
extra_settings = _select_input_or_output(extra_settings, kind)
device = _get_device_id(device, kind, raise_on_error=True)
if channels is None:
channels = default.channels
Expand All @@ -2745,15 +2809,49 @@ def _get_stream_parameters(kind, device, channels, dtype, latency,
if latency is None:
latency = default.latency
latency = _select_input_or_output(latency, kind)
if extra_settings is None:
extra_settings = default.extra_settings
extra_settings = _select_input_or_output(extra_settings, kind)
if samplerate is None:
samplerate = default.samplerate

info = query_devices(device)
if channels is None:
channels = info['max_' + kind + '_channels']
if for_check:
latency = 0.0 # Pa_IsFormatSupported ignores suggested latency.
if channels is None or samplerate is None or latency in ('low', 'high'):
try:
info = query_devices(device)
except PortAudioError as error:
required = []
if channels is None:
required.append('channels')
if samplerate is None:
required.append('samplerate')
if latency in ('low', 'high'):
required.append('numeric latency')
raise ValueError(
'Cannot determine {} stream parameters from device information; '
'specify {} explicitly or through defaults'.format(
kind, ', '.join(required))) from error
if channels is None:
channels = info['max_' + kind + '_channels']
if samplerate is None:
samplerate = info['default_samplerate']
if latency in ('low', 'high'):
latency = info['default_' + latency + '_' + kind + '_latency']
Comment on lines +2817 to +2837

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the if-conditions appear 3 times ... I think 2 times is enough. Maybe something like this (error messages can be improved):

Suggested change
if channels is None or samplerate is None or latency in ('low', 'high'):
try:
info = query_devices(device)
except PortAudioError as error:
required = []
if channels is None:
required.append('channels')
if samplerate is None:
required.append('samplerate')
if latency in ('low', 'high'):
required.append('numeric latency')
raise ValueError(
'Cannot determine {} stream parameters from device information; '
'specify {} explicitly or through defaults'.format(
kind, ', '.join(required))) from error
if channels is None:
channels = info['max_' + kind + '_channels']
if samplerate is None:
samplerate = info['default_samplerate']
if latency in ('low', 'high'):
latency = info['default_' + latency + '_' + kind + '_latency']
if channels is None or samplerate is None or latency in ('low', 'high'):
try:
info = query_devices(device)
error = None
except PortAudioError as e:
info = {}
error = e
if channels is None:
if error is None:
channels = info['max_' + kind + '_channels']
else:
raise TypeError('channels=None is not supported') from error
if samplerate is None:
if error is None:
samplerate = info['default_samplerate']
else:
raise TypeError('samplerate=None is not supported') from error
if latency in ('low', 'high'):
if error is None:
latency = info['default_' + latency + '_' + kind + '_latency']
else:
raise TypeError("latency='low'/'high' is not supported") from error

What do you think about that?


if device == DEVICE_FROM_EXTRA_SETTINGS:
import math
import numbers
if extra_settings is None:
raise ValueError('DEVICE_FROM_EXTRA_SETTINGS requires extra_settings')
Comment on lines +2842 to +2843

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this error is necessary:

Suggested change
if extra_settings is None:
raise ValueError('DEVICE_FROM_EXTRA_SETTINGS requires extra_settings')

Without it, the error is

sounddevice.PortAudioError: Error opening RawInputStream: Invalid device [PaErrorCode -9996]

I think that's good enough for this niche use case.

if (isinstance(channels, bool) or
not isinstance(channels, numbers.Integral) or channels < 1):
raise ValueError('A positive channel count is required')
Comment on lines +2844 to +2846

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you check this?

We are nowhere else checking for positivity or non-bool-ness. I think the behavior is reasonable enough without this check.

if (isinstance(samplerate, bool) or
not isinstance(samplerate, numbers.Real) or
not math.isfinite(samplerate) or samplerate <= 0):
raise ValueError('A positive finite sample rate is required')
Comment on lines +2847 to +2850

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, why this check?

if (isinstance(latency, bool) or
not isinstance(latency, numbers.Real) or
not math.isfinite(latency) or latency < 0):
raise ValueError('Finite numeric latency >= 0 is required')
Comment on lines +2851 to +2854

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here.

This is not about 'high'/'low', right? This was already checked before ...

try:
# If NumPy is available, get canonical dtype name
dtype = _sys.modules['numpy'].dtype(dtype).name
Expand All @@ -2764,10 +2862,6 @@ def _get_stream_parameters(kind, device, channels, dtype, latency,
except KeyError as e:
raise ValueError('Invalid ' + kind + ' sample format') from e
samplesize = _check(_lib.Pa_GetSampleSize(sampleformat))
if latency in ('low', 'high'):
latency = info['default_' + latency + '_' + kind + '_latency']
if samplerate is None:
samplerate = info['default_samplerate']
parameters: ... = _ffi.new('PaStreamParameters*', (
device, channels, sampleformat, latency,
extra_settings._streaminfo if extra_settings else _ffi.NULL))
Expand Down
Loading