Skip to content

Support ALSA plugin devices (dmix) for dual mono setups - #193

Merged
balloob merged 19 commits into
mainfrom
claude/investigate-issue-58-ROGTX
Mar 25, 2026
Merged

Support ALSA plugin devices (dmix) for dual mono setups#193
balloob merged 19 commits into
mainfrom
claude/investigate-issue-58-ROGTX

Conversation

@balloob

@balloob balloob commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Refactor audio device handling to new audio_devices.py
  • 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 validated and 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 dual mono setups

Root 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: Extended AudioDevice with optional alsa_device_name field and device_id property; updated type signatures to accept int | str | None for device parameters
  • sendspin/cli.py: Added _try_alsa_device() fallback in device resolution; updated help text and --list-audio-devices output with ALSA hint on Linux
  • sendspin/tui/app.py and sendspin/daemon/daemon.py: Use device_id instead of index for format detection
  • README.md: Documented ALSA device name usage with dual mono example

Usage

# Room 1: left channel via dmix
sendspin daemon --name "Living Room" --audio-device olohuone

# Room 2: right channel via dmix  
sendspin daemon --name "Kitchen" --audio-device keittio

Test plan

  • Verify --list-audio-devices still works and shows ALSA hint on Linux
  • Verify numeric device index selection still works (--audio-device 0)
  • Verify name prefix selection still works (--audio-device "MacBook")
  • Verify raw ALSA device name works on a Linux system with dmix configured
  • Verify two daemons can run simultaneously with different dmix devices
  • Verify invalid ALSA device name gives a clear error message

Fixes #58

https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS

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
Copilot AI review requested due to automatic review settings March 24, 2026 14:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_id to AudioDevice, and widen format-probing/validation APIs to accept int | 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_id rather 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=%s but passes the entire AudioDevice object, which will produce a verbose dataclass repr (and for ALSA-named devices may include index=None). Consider logging device.device_id instead 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.

Comment thread sendspin/cli.py Outdated
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
balloob marked this pull request as draft March 24, 2026 16:04
claude and others added 15 commits March 24, 2026 16:06
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
balloob marked this pull request as ready for review March 25, 2026 01:49
balloob added 2 commits March 24, 2026 21:50
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
@balloob
balloob merged commit 7c23650 into main Mar 25, 2026
1 check passed
@balloob
balloob deleted the claude/investigate-issue-58-ROGTX branch March 25, 2026 01:51
@balloob balloob added the new-feature Request or implement a new feature label Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-feature Request or implement a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dual mono setup doesn't work because two clients can't open dmix pcm devices

3 participants