Support ALSA plugin devices (dmix) for dual mono setups - #193
Merged
Conversation
Allow --audio-device to accept raw ALSA device names (e.g., 'dmixer', 'olohuone') in addition to numeric indices and name prefixes. When a string doesn't match any PortAudio-enumerated device, it's passed directly to sounddevice, letting PortAudio's ALSA backend open named plugin devices like dmix. This enables multiple daemon instances to share a single sound card via dmix for multi-room audio setups. Fixes #58 https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends Sendspin’s audio-device selection to support raw ALSA plugin device names (e.g., dmix/custom pcm.* names) in addition to PortAudio-enumerated indices and name prefixes, enabling dual-mono/multi-daemon setups on a single sound card via dmix.
Changes:
- Add
alsa_device_name+device_idtoAudioDevice, and widen format-probing/validation APIs to acceptint | str | None. - Update CLI audio-device resolution to fall back to opening a raw ALSA device name when enumeration doesn’t match, and improve Linux help/output hints.
- Switch TUI/daemon format detection to use
device_idrather than assuming a numeric index; document usage in README.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
sendspin/audio.py |
Adds alsa_device_name and device_id; updates sounddevice-related function signatures and stream creation to accept string device IDs. |
sendspin/cli.py |
Adds ALSA-name fallback resolution for --audio-device, updates help text, and adds Linux-specific guidance to --list-audio-devices. |
sendspin/tui/app.py |
Uses device_id for supported-format detection. |
sendspin/daemon/daemon.py |
Uses device_id for supported-format detection. |
README.md |
Documents selecting raw ALSA device names and provides a dual-mono example. |
Comments suppressed due to low confidence (1)
sendspin/audio.py:473
- The log message ends with
device=%sbut passes the entireAudioDeviceobject, which will produce a verbose dataclass repr (and for ALSA-named devices may includeindex=None). Consider loggingdevice.device_idinstead for a stable, user-focused identifier.
logger.info(
"Audio stream configured: codec=%s, sample_rate=%d, channels=%d, bit_depth=%d, blocksize=%d, latency=high, device=%s",
audio_format.codec.value,
pcm_format.sample_rate,
pcm_format.channels,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Pass the AudioDevice object directly to detect_supported_audio_formats, validate_audio_format, and _check_format instead of extracting device_id at each call site. The functions now handle the device_id extraction internally. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
balloob
marked this pull request as draft
March 24, 2026 16:04
All callers already pass a resolved AudioDevice, so drop the None default from _check_format, detect_supported_audio_formats, and validate_audio_format. Device resolution happens in cli.py before these are called. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
On Linux, parse `aplay -L` output to show ALSA plugin devices (dmix, plug, etc.) alongside the PortAudio-enumerated devices. This lets users see the device names they can pass to --audio-device for setups like dual mono via dmix. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Move list_audio_devices, resolve_audio_device, resolve_audio_format, and their helpers into a dedicated sendspin/audio_devices.py module. cli.py now imports and delegates to this module, keeping it focused on argument parsing and command routing. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
- Move list_audio_devices() back to cli.py since it's about printing - Keep only querying/resolution functions in audio_devices.py - Move OSError catch for PortAudio back to cli.py call site - Move all imports to top of audio_devices.py - Use inline imports in cli.py for audio_devices functions - Move PORTAUDIO_NOT_FOUND_MESSAGE back to cli.py https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
- Remove None acceptance from resolve_audio_format; callers handle None - Move logging of preferred format to cli.py (_resolve_preferred_format) - Restore OSError catch for PortAudio import inside list_audio_devices https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
The thin wrapper in audio_devices.py served no purpose after the logging was moved out. Inline the parse + validate logic directly into _resolve_preferred_format in cli.py and remove the function from audio_devices.py. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Remove inaccurate "format validation" and "enumerates PortAudio devices" bullets, clarify ALSA listing purpose and safe defaults fallback. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Replace Finnish device names (olohuone, keittio) with English equivalents (living_room, kitchen) for clarity. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Ensure every AudioDevice has either an index or alsa_device_name, and tighten device_id return type to int | str. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
No need for a custom exception class — ValueError conveys the same meaning for invalid device/format arguments. Also fix pre-existing mypy errors in discovery.py by avoiding reassignment of imported type names. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
audio.py is the playback engine — AudioDevice and query_devices are about device enumeration/resolution, so they belong in audio_devices.py. audio.py re-imports AudioDevice from audio_devices to keep its internal usage working. All other files now import from audio_devices directly. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Move SOUNDDEVICE_DTYPE_MAP, _check_format, detect_supported_audio_formats, parse_audio_format, and validate_audio_format to audio_devices.py. audio.py now only contains the playback engine (AudioPlayer). https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
balloob
marked this pull request as ready for review
March 25, 2026 01:49
Log device.device_id instead of the full AudioDevice object in the stream configured message for cleaner output. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
Only attempt _try_alsa_device() on Linux where ALSA is available. On other platforms, just report the device wasn't found without mentioning ALSA. https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--audio-deviceto accept raw ALSA device names (e.g.,dmixer,olohuone) in addition to numeric indices and name prefixesRoot cause
PortAudio only enumerates hardware ALSA devices, not plugin devices (dmix, plug, etc.). When one daemon opens a hardware device via PortAudio, it acquires exclusive access, preventing the second daemon from even enumerating devices —
sounddevice.query_devices()returns an empty list. ALSA dmix is designed for device sharing, but PortAudio bypasses it by opening the raw hardware directly.Changes
sendspin/audio.py: ExtendedAudioDevicewith optionalalsa_device_namefield anddevice_idproperty; updated type signatures to acceptint | str | Nonefor device parameterssendspin/cli.py: Added_try_alsa_device()fallback in device resolution; updated help text and--list-audio-devicesoutput with ALSA hint on Linuxsendspin/tui/app.pyandsendspin/daemon/daemon.py: Usedevice_idinstead ofindexfor format detectionREADME.md: Documented ALSA device name usage with dual mono exampleUsage
Test plan
--list-audio-devicesstill works and shows ALSA hint on Linux--audio-device 0)--audio-device "MacBook")Fixes #58
https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS