Skip to content

Add Pulse Audio Out player provider#3683

Closed
iVolt1 wants to merge 164 commits intomusic-assistant:devfrom
iVolt1:dev
Closed

Add Pulse Audio Out player provider#3683
iVolt1 wants to merge 164 commits intomusic-assistant:devfrom
iVolt1:dev

Conversation

@iVolt1
Copy link
Copy Markdown
Contributor

@iVolt1 iVolt1 commented Apr 13, 2026

Add Pulse Audio Out player provider

Summary

Adds a new pulse_audio player provider that exposes PulseAudio sinks as Music Assistant sendspin players. Each sink is registered as an external Sendspin bridge client, enabling synchronized multi-room playback alongside existing Sendspin players. Audio is written directly to PulseAudio via a minimal libpulse-simple ctypes wrapper — no additional Python dependencies are required beyond numpy for software volume scaling.

Motivation

Users running Music Assistant on Home Assistant OS with multi-channel sound cards have no way to use those cards as MA players. The existing local audio provider uses PortAudio/sounddevice which cannot enumerate PulseAudio virtual sinks such as module-remap-sink stereo pairs. This provider targets PulseAudio directly via pactl and libpulse-simple, correctly discovering and playing to all sinks including remap sinks, combined sinks, S/PDIF, and HDMI outputs.

Changes

New provider — music_assistant/providers/pulse_audio/

File Description
init.py Provider entry point, config entries, and setup
provider.py LocalPulseAudioProvider — thin shell delegating to bridge manager
player.py LocalPulseAudioPlayer — MA player model per sink with software volume and mute
sendspin_bridge.py SendspinPulseAudioBridge and LocalPulseAudioBridgeManager — sink enumeration via pactl, Sendspin bridge registration, audio writer loop
pa_simple.py Minimal ctypes wrapper around libpulse-simple for direct PCM playback to a named PA sink
helpers.py find_pactl() and pactl_env() utilities
constants.py Shared constants — UUID namespace, config keys, volume control mode
manifest.json Provider manifest declaring depends_on: sendspin, type player, stage alpha

Dependencies

  • libpulse-simple.so.0 — must be present on the host (standard PulseAudio installation)
  • pactl — required at startup for sink enumeration (pulseaudio-utils on Debian/Ubuntu, pulseaudio on Alpine)
  • numpy — used for PCM volume scaling (already a MA dependency)
  • Sendspin provider (depends_on: sendspin)

Acknowledgements

This provider is based on the architecture of the existing local_audio player provider, adapting its Sendspin bridge pattern and per-device player model to target PulseAudio sinks directly rather than PortAudio devices.

Expanding Outputs with Stereo Pair Remap Sinks

Multi-channel sound cards (5.1, 7.1 surround) expose a single multi-channel PulseAudio sink by default. To use each channel pair as an independent MA player, PulseAudio module-remap-sink can split a multi-channel sink into individual stereo sinks — one per channel pair (front, rear, side, center/LFE). The Pulse Audio Out provider discovers and registers all remap sinks automatically alongside physical sinks, so no additional configuration is needed in MA once the remap sinks exist.

For Home Assistant OS users, the companion addon Pulse Audio Stereo Pairs automates this setup. It runs as a lightweight HA addon that creates the remap sinks on startup and reacts to audio device hot-plug and unplug events, removing the need to configure remap sinks manually via pactl. Once both the addon and this provider are running, each channel pair of every multi-channel card appears as a separate player in Music Assistant.

## Add Pulse Audio Out player provider

Summary

Adds a new pulse_audio player provider that exposes PulseAudio sinks as Music Assistant players. Each sink is registered as an external Sendspin bridge client, enabling synchronized multi-room playback alongside existing Sendspin players. Audio is written directly to PulseAudio via a minimal libpulse-simple ctypes wrapper — no additional Python dependencies are required beyond numpy for software volume scaling.

Motivation

Users running Music Assistant on Home Assistant OS with multi-channel sound cards have no way to use those cards as MA players. The existing local audio provider uses PortAudio/sounddevice which cannot enumerate PulseAudio virtual sinks such as module-remap-sink stereo pairs. This provider targets PulseAudio directly via pactl and libpulse-simple, correctly discovering and playing to all sinks including remap sinks, combined sinks, S/PDIF, and HDMI outputs.

Changes

New provider — music_assistant/providers/pulse_audio/

File Description
__init__.py Provider entry point, config entries, and setup
provider.py LocalPulseAudioProvider — thin shell delegating to bridge manager
player.py LocalPulseAudioPlayer — MA player model per sink with software volume and mute
sendspin_bridge.py SendspinPulseAudioBridge and LocalPulseAudioBridgeManager — sink enumeration via pactl, Sendspin bridge registration, audio writer loop
pa_simple.py Minimal ctypes wrapper around libpulse-simple for direct PCM playback to a named PA sink
helpers.py find_pactl() and pactl_env() utilities
constants.py Shared constants — UUID namespace, config keys, volume control mode
manifest.json Provider manifest declaring depends_on: sendspin, type player, stage alpha

Modified — music_assistant/providers/sendspin/bridge_role.py

  • setup_audio_requirements() gains optional sample_rate, bit_depth, and channels parameters (defaulting to existing bridge constants — no breaking change for AirPlay and other existing callers)
  • Added preferred_format property exposing the configured AudioRequirements so playback.py can read per-bridge format

Modified — music_assistant/providers/sendspin/playback.py

  • _get_member_output_format() — the BridgePlayerRole branch now reads role.preferred_format instead of returning hardcoded bridge constants, enabling per-sink native format routing

How It Works

On startup the provider enumerates all PulseAudio sinks via pactl --format=json list sinks. For each sink it registers a Sendspin bridge client advertising the sink's native sample rate and bit depth in ClientHelloPlayerSupport. This causes MA to transcode each track to the correct format per sink — a 96kHz/24-bit sink receives 96kHz/24-bit PCM, a 48kHz/16-bit sink receives 48kHz/16-bit PCM, and so on.

Audio chunks arrive via BridgePlayerRole.on_audio_chunk, pass through software volume scaling, and are written to the PA sink via pa_simple_write. The 24-bit path handles MA's left-justified 32-bit container delivery, repacking to packed s24le before writing.

The provider also reacts to volume and mute commands from the MA UI via BridgePlayerRole.set_player_volume and set_player_mute.

Key Implementation Notes

  • PA sample format constants are verified via pa_sample_format_to_stringPA_SAMPLE_S32LE = 7, PA_SAMPLE_S24LE = 9 (not the values commonly found in older documentation)
  • 24-bit audio is delivered by MA in left-justified 32-bit containers; the bridge unpacks, scales, and repacks to packed s24le for PA
  • Multi-channel sinks (5.1, 7.1) are supported — the bridge opens a stereo stream and PulseAudio handles channel remapping automatically
  • Stable player IDs use UUIDv5 derived from the PA sink name so players persist across restarts
  • Hardware volume ceiling is applied via pactl set-sink-volume at startup to prevent clipping

Testing

Tested on Home Assistant OS 17.1 (amd64) with the following sink types:

Sink Format Result
Creative X-Fi 7.1 stereo pairs s32le 2ch 96000Hz ✅ Clean playback, volume working
HD Audio Generic 5.1 stereo pairs s32le 2ch 96000Hz ✅ Clean playback, volume working
AudioQuest DragonFly Black s24le 2ch 96000Hz ✅ Clean playback, volume working
USB Sound Device 7.1 stereo pairs s16le 2ch 48000Hz ✅ Clean playback, volume working
USB True HD Audio S/PDIF s32le 2ch 96000Hz ✅ Clean playback, volume working

Dependencies

  • libpulse-simple.so.0 — must be present on the host (standard PulseAudio installation)
  • pactl — required at startup for sink enumeration (pulseaudio-utils on Debian/Ubuntu, pulseaudio on Alpine)
  • numpy — used for PCM volume scaling (already a MA dependency)
  • Sendspin provider (depends_on: sendspin)

Acknowledgements

This provider is based on the architecture of the existing local_audio player provider, adapting its Sendspin bridge pattern and per-device player model to target PulseAudio sinks directly rather than PortAudio devices.

Expanding Outputs with Stereo Pair Remap Sinks

Multi-channel sound cards (5.1, 7.1 surround) expose a single multi-channel PulseAudio sink by default. To use each channel pair as an independent MA player, PulseAudio module-remap-sink can split a multi-channel sink into individual stereo sinks — one per channel pair (front, rear, side, center/LFE). The Pulse Audio Out provider discovers and registers all remap sinks automatically alongside physical sinks, so no additional configuration is needed in MA once the remap sinks exist.

For Home Assistant OS users, the companion addon [Pulse Audio Stereo Pairs](https://github.com/iVolt1/hassio-apps) automates this setup. It runs as a lightweight HA addon that creates the remap sinks on startup and reacts to audio device hot-plug and unplug events, removing the need to configure remap sinks manually via pactl. Once both the addon and this provider are running, each channel pair of every multi-channel card appears as a separate player in Music Assistant.

iVolt1 added 30 commits April 2, 2026 18:13
…nt_dev\server\music_assistant\providers\spotify_connect_go
iVolt1 added 13 commits April 10, 2026 15:56
The logic is now: MA sends 32-bit containers → unpack as int32 → scale → clip to 24-bit range → view as uint8 → take low 3 bytes per sample → write as packed s24le to PA. The PA stream opens as PA_SAMPLE_S24LE=9 which matches what the DragonFly remap sink expects.
Three fixes in this pass:

Volume passthrough — 24-bit now always repacks even at volume=100, since PA needs packed s24le regardless
Byte extraction — [:, 1:] takes bytes 1, 2, 3 (the upper 3 bytes of the left-justified int32), not [:, :3]
Mute silence — corrected to 3/4 the input length for s24le
One-line change — channels < 2 → channels < 1. The bridge already opens all streams as 2ch (BRIDGE_CHANNELS = 2), so PA receives stereo and remaps to however many channels the sink has. The 5.1 and 7.1 sinks will now appear as players.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 13, 2026

🔒 Dependency Security Report

✅ No dependency changes detected in this PR.

@marcelveldt
Copy link
Copy Markdown
Member

Can't we merge this with the normal local out player provider ?
Just some option to select which mode you want tuse

@iVolt1
Copy link
Copy Markdown
Contributor Author

iVolt1 commented Apr 14, 2026

Your merging this merge this with the normal local out player provider would be ideal. Thanks.

@apophisnow
Copy link
Copy Markdown
Contributor

apophisnow commented Apr 14, 2026

The claim that PortAudio does not support PulseAudio seems incorrect. It looks to me like there is a hostapi for PulseAudio https://github.com/PortAudio/portaudio/tree/master/src/hostapi/pulseaudio

I imagine there is probably a cleaner way to implement pulseaudio in the current local audio provider.

@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented Apr 14, 2026

Your merging this merge this with the normal local out player provider would be ideal. Thanks.

We would be looking for you to make this a PR against the existing local audio out player

@iVolt1
Copy link
Copy Markdown
Contributor Author

iVolt1 commented Apr 14, 2026

The claim that PortAudio does not support PulseAudio seems incorrect. It looks to me like there is a hostapi for PulseAudio https://github.com/PortAudio/portaudio/tree/master/src/hostapi/pulseaudio

I imagine there is probably a cleaner way to implement pulseaudio in the current local audio provider.

Yes, the claim that PortAudio does not support PulseAudio is technically incorrect so I can remove that. Are you ok with me continuing to use pa_simple instead of PortAudio?

@iVolt1
Copy link
Copy Markdown
Contributor Author

iVolt1 commented Apr 14, 2026

Your merging this merge this with the normal local out player provider would be ideal. Thanks.

We would be looking for you to make this a PR against the existing local audio out player

Is it ok for me to just move the code from the pulse_audio provider to the local_audio provider and make a new PR?

@apophisnow
Copy link
Copy Markdown
Contributor

Your merging this merge this with the normal local out player provider would be ideal. Thanks.

We would be looking for you to make this a PR against the existing local audio out player

Is it ok for me to just move the code from the pulse_audio provider to the local_audio provider and make a new PR?

I would like to see this as an enhancement of the Local Audio provider, but this PR needs more work than just moving files around. It looks like it touches files that it doesn't have any reason to, and doesn't cleanly integrate in some that it does (take the Dockerfile.base for instance). I'd recommend implementing it into the local audio provider, but also carefully going over everything it does and how it is implemented. It looks like it was genereated with AI (which is fine), and needs a bit of human touch and engineering to get it over the finish line.

@iVolt1
Copy link
Copy Markdown
Contributor Author

iVolt1 commented Apr 14, 2026

Thanks for your comments. I am working on a clean version. Yes, Claude has been substantially involved. I will continue with pa_simple instead of portaudio if that is ok.

One piece of unfinished business is making pactl more available by having MA install it via the Dockerfile rather than having pactl installed in bin directory of the provider. I don't have access to the MA Dockerfile when developing on the Music Assistant DEV SERVER app as far as I can tell so is this something the MA team would like to do or should I continue with locating pactl in the provider bin directory?

Thanks.

@apophisnow
Copy link
Copy Markdown
Contributor

Thanks for your comments. I am working on a clean version. Yes, Claude has been substantially involved. I will continue with pa_simple instead of portaudio if that is ok.

One piece of unfinished business is making pactl more available by having MA install it via the Dockerfile rather than having pactl installed in bin directory of the provider. I don't have access to the MA Dockerfile when developing on the Music Assistant DEV SERVER app as far as I can tell so is this something the MA team would like to do or should I continue with locating pactl in the provider bin directory?

Thanks.

I think my preference would be to include the dependency in the Dockerfile.base, but it looks like your PR was adding a whole separate FROM and RUN step rather than just including the dependency with all the others already being installed.

Committing binary files directly seems sketchy as opposed to installing a library from a known repo.

As far as portaudio goes, although they have some pulseaudio stuff committed in the main branch, it looks like it hasn't made it to an actual release yet, so we probably can't use it yet unless we wanted to build that from source.

@iVolt1 iVolt1 marked this pull request as draft April 17, 2026 21:44
@iVolt1 iVolt1 deleted the branch music-assistant:dev April 18, 2026 19:28
@iVolt1 iVolt1 closed this Apr 18, 2026
@iVolt1 iVolt1 deleted the dev branch April 18, 2026 19:28
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.

4 participants