Skip to content

Add AlsaSettings for opening an ALSA PCM by address - #644

Open
whatisyourname0 wants to merge 4 commits into
spatialaudio:masterfrom
whatisyourname0:feat/alsa-direct-device
Open

whatisyourname0 wants to merge 4 commits into
spatialaudio:masterfrom
whatisyourname0:feat/alsa-direct-device

Conversation

@whatisyourname0

@whatisyourname0 whatisyourname0 commented Oct 17, 2026 •

Copy link
Copy Markdown

Applications with an ALSA PCM address can open it directly through AlsaSettings(device_string=...), using PortAudio's PaAlsaStreamInfo.deviceString. Direct device selection is explicit: pass device=sd.DEVICE_FROM_EXTRA_SETTINGS, corresponding to PortAudio's paUseHostApiSpecificDeviceSpecification.

This implements the ALSA device-string item discussed in #4 through the existing platform-specific settings interface. Related PR #85 proposes a broader host-API interface. This change does not resolve all of #4.

import sounddevice as sd

# ALSA's null PCM requires no physical audio device.
with sd.RawInputStream(
    device=sd.DEVICE_FROM_EXTRA_SETTINGS,
    samplerate=48000,
    channels=2,
    dtype='int16',
    blocksize=1024,
    latency=4096 / 48000,
    extra_settings=sd.AlsaSettings(device_string='null'),
) as stream:
    data, overflowed = stream.read(1024)
    print(len(data), overflowed)

Replace 'null' with a hardware PCM address or named ALSA configuration as needed.

API behavior

  • AlsaSettings alone does not override default.device. Select DEVICE_FROM_EXTRA_SETTINGS explicitly, either in the call or in default.device. PortAudio rejects ALSA settings combined with a regular device index; the error is propagated.
  • Shared stream-parameter and convenience-function code no longer checks for the AlsaSettings type. Device information is queried only when required for channel-count/sample-rate defaults or symbolic latency. Query errors propagate.
  • Direct streams require explicit or configured channel count, sample rate and finite nonnegative numeric latency. 'low' and 'high' require enumerated device information and raise an error for the sentinel.
  • Format checks ignore latency, as PortAudio does, and do not query device information solely for it.
  • Input, output, direct duplex and mixed direct/indexed duplex are supported. stream.device reports -2 for a directly specified direction. Settings and their C buffers remain alive through stream close.
  • Convenience output with explicit mapping=[1] needs device information for automatic stereo detection, so it fails for direct devices. Use OutputStream with an explicit channel count instead.

Validation and separate tests

The feature PR includes implementation, API documentation and the documentation-only PortAudio stub update needed to expose the constant. It adds no test infrastructure, dependencies, CI steps or lockfile changes.

The updated suite and standalone instructions are on test/alsa-direct-device-regressions, pinned at 1149e87. That branch differs from feature commit ffcda44 only by the five files under tests/.

Current local validation:

  • 127 passed on Linux ARM64, CPython 3.14.7, PortAudio 19.6.0, using an installed wheel built from feature commit ffcda44 and the separate test suite. Tests use isolated ALSA null/file PCMs and require no physical devices.
  • The differential suite runs 74 identical public API calls in separate baseline (626fcb0) and candidate processes, using the same ALSA configuration. 72 outcomes match exactly, including index/name/default selection, raw input/output/duplex, format checks and float32 input callbacks. Two intentional differences are asserted explicitly: an invalid numeric device now fails during opening instead of device-info lookup, and format checks ignore invalid default latency. No other differences were observed in this Linux matrix.
  • To include those 74 differential cases, build the baseline and set SOUNDDEVICE_BASELINE_PATH as documented in the test README. Without that environment variable, those cases are explicitly skipped and the 53 direct-ALSA cases run.
  • Coverage includes byte-exact input, reopening after failure, explicit/default sentinel selection, unchanged index/name selection, missing defaults and symbolic-latency errors, generic query-failure propagation and recovery, settings without an ALSA class identity, mixed duplex and settings lifetime.
  • Import/device/version smoke checks, Pyright source analysis, warning-as-error Sphinx HTML build, focused Ruff checks on the tests, git diff --check, and uv lock --check passed.

To run the separate tests against this PR on Linux with PortAudio installed:

# In the fork checkout, on feat/alsa-direct-device:
git fetch origin test/alsa-direct-device-regressions
validation_dir=$(mktemp -d /tmp/sounddevice-alsa.XXXXXX)
git archive 1149e870c64d7379988271c4308736d3f0c9a538 tests | tar -x -C "$validation_dir"
python -m pip install . pytest numpy
python -m pytest -q -o cache_dir="$validation_dir/.pytest_cache" "$validation_dir/tests"

The fixture sets its isolated ALSA configuration before PortAudio initialization and cleans up its temporary PCM configuration/data. The extracted test files remain in $validation_dir until manually removed. Testing infrastructure can be discussed separately in the context of #14 and #131.

Limits

The revised sentinel API has not been rerun across the earlier multi-version Python matrix or on physical hardware. Native Linux USB capture, hotplug and native macOS/Windows runtime behavior remain unverified. Local external link checking was not repeated; hosted checks are tracked on this PR.

An ALSA PCM address is not persistent hardware identity. Serial lookup, hotplug resolution and application recovery remain outside this patch. PortAudio initialization may still enumerate devices. No performance improvement or negotiated hardware buffer size is claimed.

Earlier validation of the pre-sentinel candidate (not rerun for this revision)

At original candidate 07f9935, CPython 3.7–3.14 passed the original 33 synthetic tests per version; PyPy 3.9/3.11 passed 32 with one optional NumPy skip. Python 3.7/3.8 used a wheel built with Python 3.14, so those results establish runtime compatibility only. The original suite and instructions remain available at that immutable commit.

Earlier physical input check

A separate local run used a MacBook Pro built-in microphone through Apple Virtualization.framework and an experimental virtio-snd guest kernel. The Linux runtime was a chroot containing Python 3.14.7, PortAudio 19.6.0 and ALSA 1.2.14. The installed sounddevice source matched this candidate byte-for-byte.

arecord completed two 10-second captures. A C program using PaAlsaStreamInfo.deviceString completed 60 seconds and a 5-second reopen. With query_devices() forbidden, AlsaSettings('hw:0,0') completed 60 seconds (2,880,000 mono frames), two 5-second reopens and recovery after a nonexistent PCM failed. All captures had nonzero signal, with no reported read errors or overflows. The host input route was monitored as the built-in microphone. Audio samples were processed in memory; only aggregate measurements were saved.

This establishes physical input through that particular VZ guest, including experimental capture compatibility changes. Native Linux USB devices, hotplug and stock virtio-snd remain unverified. The capture trial did not run the recorder or Docker Compose. Guest shutdown did not complete within the harness timeout and required a second launcher signal to force-stop the test VM; orderly VM shutdown was not established.

@mgeier

mgeier commented Oct 18, 2026

Copy link
Copy Markdown
Member

Thanks for this PR!

It seems to add two (or more?) different things, though: a new ALSA setting and a whole new testing framework.

I think both are valuable, but they should be discussed separately.

Regarding tests, there are already some discussions in #14 and #131, and there has been a very recent PR that also comes with its own test framework: #645.

@whatisyourname0

Copy link
Copy Markdown
Author

Thanks for the review! I agree that adding AlsaSettings and introducing test infrastructure should be discussed separately.

I'll update this PR to focus on the ALSA PCM-address support and its documentation, removing the new test suite, test dependencies, and test-related CI changes from the diff. I'll preserve the regression tests on a separate branch and keep the validation results and reproduction instructions available for reviewing the feature.

Thanks also for pointing me to #14, #131, and #645. I'll follow up on testing separately, building on those discussions before proposing a shared approach.

@whatisyourname0

Copy link
Copy Markdown
Author

Done in 8e0aeba: this PR now contains only the ALSA PCM-address support and its documentation. The tests and reproduction instructions are preserved on test/alsa-direct-device-regressions, with links and validation details in the updated PR description.

@mgeier

mgeier commented Oct 22, 2026

Copy link
Copy Markdown
Member

Thanks for the update!

I like the AlsaSettings class, but I don't like its special handling in code that should ideally be platform-independent.

Would overriding default.device when AlsaSettings is supplied fit the existing API, or should direct selection require an explicit sentinel?

I think an explicit sentinel would be better, as it would make the implementation more straightforward and it would be closer to PortAudio's C API.

Maybe something like

DEVICE_FROM_EXTRA_SETTINGS = _lib.paUseHostApiSpecificDeviceSpecification

Maybe you can come up with a better name?

Your example would then look something like this:

with sd.RawInputStream(
    device=sd.DEVICE_FROM_EXTRA_SETTINGS,
    samplerate=48000,
    channels=2,
    dtype='int16',
    blocksize=1024,
    latency=4096 / 48000,
    extra_settings=sd.AlsaSettings(device_string='null'),
) as stream:
    data, overflowed = stream.read(1024)
    print(len(data), overflowed)

Is requiring numeric latency acceptable for this path?

It is kinda annoying, but I guess we don't really have a choice, right?

However, I would also prefer the handling of this to be less host-API-specific.

Whenever 'low' or 'high' is given and query_devices(device) is not successful, we could raise an error. There is no need to make any of this specific to ALSA or any other host API.

@whatisyourname0

Copy link
Copy Markdown
Author

Thanks for the review, and that makes sense. Your proposed name communicates the intent clearly than mine.

I'll also rework the shared parameter handling around whether device information is available, removing the AlsaSettings type checks from that path. If 'low' or 'high' latency requires device information that cannot be queried, it should produce an error. I'll apply the same principle to channel-count, and sample-rate defaults.

I'll update the examples and the separately maintained regression tests to cover the changed behavior.

@whatisyourname0

whatisyourname0 commented Oct 23, 2026 •

Copy link
Copy Markdown
Author

Updated and pushed in ffcda44: device selection now uses DEVICE_FROM_EXTRA_SETTINGS, and shared parameter handling no longer checks for AlsaSettings. The examples and PR description are updated too.

@mgeier mgeier left a comment

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.

Here's a first round of review ... I must postpone the rest because I couldn't manage to run this yet, I always get a sounddevice.PortAudioError: Error querying device -2. Is there anything special I need to do for this to work?

Comment thread src/sounddevice.py
* Module-wide default settings: `default`

* Platform-specific settings:
`AsioSettings`, `CoreAudioSettings`, `WasapiSettings`

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.

You could also add the new class here.

Comment on lines +18 to +21
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.

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?

Comment thread src/sounddevice.py
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"?

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

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.

Comment thread src/sounddevice.py
Comment on lines +1674 to +1676
*extra_settings*, e.g. with `AlsaSettings`. In this case,
channels, sample rate and numeric latency must be supplied
explicitly or through defaults.

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`.

Comment thread src/sounddevice.py
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.

Comment thread src/sounddevice.py
Comment on lines +2337 to +2341
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.

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.

Comment thread src/sounddevice.py
Comment on lines +2350 to +2357
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.

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?

Comment thread src/sounddevice.py
@@ -690,7 +693,7 @@
"""
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,

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants