Skip to content

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

Open
whatisyourname0 wants to merge 1 commit into
spatialaudio:masterfrom
whatisyourname0:feat/alsa-direct-device
Open

whatisyourname0 wants to merge 1 commit into
spatialaudio:masterfrom
whatisyourname0:feat/alsa-direct-device

Conversation

@whatisyourname0

Copy link
Copy Markdown

Applications that already have an ALSA PCM address currently have to match it to a PortAudio device index or display name before opening a stream. This adds AlsaSettings(device_string=...) through the existing extra_settings interface, using PortAudio's PaAlsaStreamInfo.deviceString and paUseHostApiSpecificDeviceSpecification.

This implements the ALSA device-string item discussed in #4. Related PR #85 proposes a broader host-API interface; this change uses the existing platform-specific settings pattern and exposes only the PCM address. It does not resolve all of #4.

import sounddevice as sd

# ALSA's null PCM allows this example to run without an audio device.
with sd.RawInputStream(
    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)
# Synthetic test result: 4096 False

For a hardware PCM or a named ALSA configuration, replace 'null' with the desired address. The settings object retains the C string, and the stream retains its effective settings. Existing device-index and name selection remain available.

API behavior

  • With AlsaSettings, device=None selects the direct address and overrides default.device. An explicit conflicting device index or name raises ValueError; -2 is accepted.
  • Channels and sample rate must be provided explicitly or through defaults. Stream latency must be a finite, nonnegative number; 'low' and 'high' depend on enumerated device information and are unavailable on this path.
  • Format checks do not require latency, since PortAudio ignores suggested latency for these checks.
  • Input, output, mixed and direct duplex streams are supported. stream.device reports -2 for a directly specified direction.
  • Convenience output with explicit mapping=[1] raises an explanatory error because its automatic stereo detection requires device information. OutputStream supports explicit channel counts.

Validation

Tested locally on Linux ARM64 with Python 3.14.7, PortAudio 19.6.0 and ALSA 1.2.14.

A small C program first exercised PortAudio's existing API: two null-PCM capture/reopen cycles, a deterministic file PCM capture with all 16,384 bytes matching, and failure for a nonexistent PCM.

The PR includes 33 synthetic ALSA regression cases, a temporary configuration/data fixture and a README with standalone commands. The fixture is configured before PortAudio initialization, includes no system/user ALSA configuration, and removes its files at session cleanup. No application code or absolute workspace paths are required. The existing Python CI workflow now runs these tests.

Local Linux ARM64 results:

Interpreter Result Installation
CPython 3.7.17 33 passed Wheel built with Python 3.14
CPython 3.8 33 passed Same wheel
CPython 3.9–3.14 33 passed per version Source install
PyPy 3.9 / 3.11 32 passed, 1 skipped per version Source install, no NumPy

The skipped case uses the NumPy convenience API; raw input/output/duplex and settings validation run on both PyPy versions. CI installs NumPy for CPython and records the optional skip on PyPy. Python 3.7/3.8 source builds are limited by the existing project's modern license metadata/build-tool compatibility; the wheel tests establish runtime compatibility only. Python 3.7 used ALSA 1.2.4, and the other local environments used ALSA 1.2.14, with PortAudio 19.6.0 throughout.

The tests cover direct capture/output/duplex, byte-for-byte input, reopening after failure, settings lifetime (including caller-mutated lists), explicit empty settings pairs, invalid arguments, default overrides, mixed direct/indexed streams, and existing index/name selection. Earlier separate application tests additionally checked callback delivery and reopening with device-name queries forbidden; those application tests are excluded from this PR.

Pyright and the Sphinx HTML build passed during library review. The final test addition passed focused static checks, git diff --check, and uv lock --check. GitHub-hosted CI has not run, and external link checking has not been performed.

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.

Limits and review questions

The automated regression suite uses synthetic ALSA null/file tests. Physical input has additionally been tested through the VZ route above. Native Linux USB devices, hotplug, and native macOS/Windows execution of sounddevice have not been tested. The test latency request does not establish a negotiated 4,096-frame hardware buffer. No performance improvement is claimed.

An ALSA PCM address does not provide persistent hardware identity. Serial lookup, hotplug resolution and application recovery remain outside this patch. PortAudio initialization may still enumerate devices; this change bypasses sounddevice's name/index resolution when constructing direct stream parameters.

Would overriding default.device when AlsaSettings is supplied fit the existing API, or should direct selection require an explicit sentinel? Is requiring numeric latency acceptable for this path? The synthetic suite is proposed under tests/ and added to the existing Python CI workflow.

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.

1 participant