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
113 changes: 97 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')
_check(_lib.Pa_HostApiTypeIdToHostApiIndex(_lib.paALSA),
'ALSA host API is unavailable')
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,36 @@ 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'):
info = query_devices(device)
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 device == DEVICE_FROM_EXTRA_SETTINGS:
import math
import numbers
if extra_settings is None:
raise ValueError('DEVICE_FROM_EXTRA_SETTINGS requires extra_settings')
if (isinstance(channels, bool) or
not isinstance(channels, numbers.Integral) or channels < 1):
raise ValueError('A positive channel count is required')
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')
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')
try:
# If NumPy is available, get canonical dtype name
dtype = _sys.modules['numpy'].dtype(dtype).name
Expand All @@ -2764,10 +2849,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