Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
085b00d
Create \\192.168.1.117\addons\music_assistant_dev\server\music_assist…
iVolt1 Apr 2, 2026
0c1fdca
Update \\192.168.1.117\addons\music_assistant_dev\server\music_assist…
iVolt1 Apr 2, 2026
bcd6800
Delete music_assistant/providers/\\192.168.1.117\addons\music_assista…
iVolt1 Apr 2, 2026
7524da3
Create spotify_connect_go
iVolt1 Apr 2, 2026
d363cc1
Delete music_assistant/providers/spotify_connect_go
iVolt1 Apr 2, 2026
4bade3e
Create __init__.py
iVolt1 Apr 2, 2026
e6f5c79
Create manifest.json
iVolt1 Apr 2, 2026
71c7cb3
Update __init__.py
iVolt1 Apr 2, 2026
5fa8908
Update manifest.json
iVolt1 Apr 2, 2026
b6d2533
Update __init__.py
iVolt1 Apr 3, 2026
af981ae
Update __init__.py
iVolt1 Apr 3, 2026
5045837
Update __init__.py
iVolt1 Apr 3, 2026
329cd7d
Update __init__.py
iVolt1 Apr 3, 2026
9d8d042
Update __init__.py
iVolt1 Apr 3, 2026
ca80b18
fix: replace players.get with get_player
iVolt1 Apr 3, 2026
7e926c9
fix: binary path and AsyncProcess.close
iVolt1 Apr 3, 2026
c2e00c7
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
09d65a4
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
262d3de
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
ab20d59
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
69172f9
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
e771416
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
58ac97f
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
68014a9
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
e416020
fix: binary path and AsyncProcess.close 2
iVolt1 Apr 3, 2026
75a7867
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 3, 2026
f49ebd5
fix: changes to _on_***_callback
iVolt1 Apr 3, 2026
675415a
fix: changes to _on_***_callback
iVolt1 Apr 3, 2026
0d66e97
fix: changes to _on_***_callback
iVolt1 Apr 3, 2026
090f4c1
feat: add active player tracking and source takeover handling
iVolt1 Apr 4, 2026
d31a348
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 4, 2026
73ab26d
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 5, 2026
baa4d3d
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 5, 2026
74d2156
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 5, 2026
e30a1b0
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 6, 2026
1739d1d
Create manifest.json
iVolt1 Apr 6, 2026
2ddfad6
Update manifest.json
iVolt1 Apr 6, 2026
a63b61e
Create __init__.py
iVolt1 Apr 6, 2026
49e1a1f
Update __init__.py
iVolt1 Apr 6, 2026
27c78b5
Create provider.py
iVolt1 Apr 6, 2026
40b1564
Update provider.py
iVolt1 Apr 6, 2026
2855ddd
Create constants.py
iVolt1 Apr 6, 2026
5a6e0d5
Update constants.py
iVolt1 Apr 6, 2026
49dd19f
Create pa_simple.py
iVolt1 Apr 6, 2026
8428bc3
Update pa_simple.py
iVolt1 Apr 6, 2026
a8a4358
Create player.py
iVolt1 Apr 6, 2026
1be6e0d
Update player.py
iVolt1 Apr 6, 2026
0baaa02
Create sendspin_bridge.py
iVolt1 Apr 6, 2026
f963c33
Update sendspin_bridge.py
iVolt1 Apr 6, 2026
5d2a3b0
Update sendspin_bridge.py
iVolt1 Apr 6, 2026
9271887
Update pa_simple.py
iVolt1 Apr 6, 2026
4587ce5
Update pa_simple.py
iVolt1 Apr 6, 2026
571eeae
Update pa_simple.py
iVolt1 Apr 6, 2026
9639648
Update Dockerfile.base
iVolt1 Apr 6, 2026
f76d635
Update pa_simple.py
iVolt1 Apr 6, 2026
e84af8a
Update sendspin_bridge.py
iVolt1 Apr 6, 2026
22f1af6
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 6, 2026
bc8c69e
Update Dockerfile
iVolt1 Apr 6, 2026
a0aa015
Update Dockerfile.base
iVolt1 Apr 6, 2026
055fc4d
Update sendspin_bridge.py
iVolt1 Apr 6, 2026
9a04b2b
Update __init__.py
iVolt1 Apr 6, 2026
a0861d4
Update constants.py
iVolt1 Apr 6, 2026
d9cb052
Update Dockerfile
iVolt1 Apr 6, 2026
bfbfb4d
Update Dockerfile
iVolt1 Apr 6, 2026
487a930
Update Dockerfile
iVolt1 Apr 6, 2026
37e36c3
Update Dockerfile
iVolt1 Apr 6, 2026
1129cb0
Update Dockerfile
iVolt1 Apr 6, 2026
9d255a0
Update Dockerfile.base
iVolt1 Apr 6, 2026
bf3ef7f
Update Dockerfile
iVolt1 Apr 6, 2026
2329ade
Update Dockerfile.base
iVolt1 Apr 7, 2026
0df45a6
Create bin
iVolt1 Apr 7, 2026
c8f818e
Delete music_assistant/providers/pulse_audio/bin
iVolt1 Apr 7, 2026
70a821c
Create bin
iVolt1 Apr 7, 2026
358b9f0
Delete music_assistant/providers/pulse_audio/bin
iVolt1 Apr 7, 2026
cbce355
Create pactl
iVolt1 Apr 7, 2026
9a0bfb2
Delete music_assistant/providers/pulse_audio/bin/pactl
iVolt1 Apr 7, 2026
ed51d22
Create test
iVolt1 Apr 7, 2026
94a18a9
Add binaries
iVolt1 Apr 7, 2026
9872693
Delete music_assistant/providers/pulse_audio/bin/test
iVolt1 Apr 7, 2026
ee25c2a
Delete music_assistant/providers/pulse_audio/bin/pactl
iVolt1 Apr 7, 2026
0e97e35
Add files via upload
iVolt1 Apr 7, 2026
7d4fd19
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 7, 2026
6514d5d
Create helpers.py
iVolt1 Apr 7, 2026
d02cbf6
Update sendspin_bridge.py
iVolt1 Apr 7, 2026
04b14c6
Update player.py
iVolt1 Apr 7, 2026
21adb2f
Create test
iVolt1 Apr 7, 2026
e0457af
Add files via upload
iVolt1 Apr 7, 2026
b5f0bd6
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 7, 2026
e078eed
Delete music_assistant/providers/pulse_audio/bin/lib/test
iVolt1 Apr 7, 2026
ed5d796
Delete music_assistant/providers/pulse_audio/bin/libpulsecommon-16.1.so
iVolt1 Apr 7, 2026
edc9c2f
Update sendspin_bridge.py
iVolt1 Apr 7, 2026
54f0fd1
Update helpers.py
iVolt1 Apr 7, 2026
34c02e5
Update player.py
iVolt1 Apr 7, 2026
7473907
Update sendspin_bridge.py
iVolt1 Apr 8, 2026
d78035b
Update constants.py
iVolt1 Apr 8, 2026
5bafc47
Update __init__.py
iVolt1 Apr 8, 2026
f5cc0a3
Update player.py
iVolt1 Apr 8, 2026
f2f3fd9
Update sendspin_bridge.py
iVolt1 Apr 8, 2026
1ccf617
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 8, 2026
9af7a86
Create create_stereo_pairs.sh
iVolt1 Apr 8, 2026
ee1ddf3
Update create_stereo_pairs.sh
iVolt1 Apr 8, 2026
f8bbc4e
Update create_stereo_pairs.sh
iVolt1 Apr 8, 2026
ee57595
Create create_stereo_pairs.sh
iVolt1 Apr 8, 2026
ba9d56d
Delete scripts/scripts directory
iVolt1 Apr 8, 2026
f20590b
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
2500d7c
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
7fdde61
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
e81c2c6
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
770b324
Update playback.py
iVolt1 Apr 9, 2026
f5aff2e
Update playback.py
iVolt1 Apr 9, 2026
728d7f6
Update playback.py
iVolt1 Apr 9, 2026
d84e49a
Update playback.py
iVolt1 Apr 9, 2026
1323590
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
e72c002
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
31e8880
Update pa_simple.py
iVolt1 Apr 9, 2026
263f4a2
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
3daa5d3
Update bridge_role.py
iVolt1 Apr 9, 2026
484d0cc
Update playback.py
iVolt1 Apr 9, 2026
39c2767
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
009b78f
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
5e606de
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
287419b
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
6d3904a
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
718d252
Update bridge_role.py
iVolt1 Apr 9, 2026
b73740c
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
9c88e25
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
7e7ae3a
Update bridge_role.py
iVolt1 Apr 9, 2026
f90edd7
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 9, 2026
2946ae6
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
808cc06
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
ea541f1
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
6560b89
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
2f6546b
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
ce24c18
Update sendspin_bridge.py
iVolt1 Apr 9, 2026
bde19cc
Update player.py
iVolt1 Apr 9, 2026
d54efaa
Update player.py
iVolt1 Apr 9, 2026
a4962c8
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 9, 2026
b038e01
Update sendspin_bridge.py
iVolt1 Apr 10, 2026
1d84612
Update player.py
iVolt1 Apr 10, 2026
bfddbd9
Update sendspin_bridge.py
iVolt1 Apr 10, 2026
c31aa0d
Update pa_simple.py
iVolt1 Apr 10, 2026
7d49664
Update sendspin_bridge.py
iVolt1 Apr 10, 2026
b4d0457
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 10, 2026
a0c9cac
sample_rate/bit_depth/channels by player preferred_format property
iVolt1 Apr 10, 2026
6644066
rRead role.preferred_format instead of returning hardcoded constants
iVolt1 Apr 10, 2026
1a0d0a5
Read role.preferred_format instead of returning hardcoded constants
iVolt1 Apr 10, 2026
8b1ae49
Read role.preferred_format instead of returning constants
iVolt1 Apr 10, 2026
d926774
Change PA_SAMPLE_S24LE=9 to PA_SAMPLE_S24_32LE=11 for 24-bit:
iVolt1 Apr 10, 2026
0756763
Removed the packed 3-byte reshape logic for 24-bit
iVolt1 Apr 10, 2026
4dbba67
Revert back to PA_SAMPLE_S24LE=9 for 24-bit:
iVolt1 Apr 10, 2026
07ce486
Update sendspin_bridge.py
iVolt1 Apr 10, 2026
d450246
Fix clip range is the issues
iVolt1 Apr 10, 2026
578f15e
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 10, 2026
fbf0b4d
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 11, 2026
0ab4b06
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 11, 2026
cea08ae
One-line change — channels < 2 → channels < 1
iVolt1 Apr 11, 2026
622efc4
Revert cea08ae
iVolt1 Apr 11, 2026
48aa686
Create README.md
iVolt1 Apr 11, 2026
91bbdc4
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 11, 2026
3971889
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 12, 2026
bbfc3b5
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 13, 2026
b623833
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 13, 2026
610d60f
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 14, 2026
4326fb0
Merge branch 'music-assistant:dev' into dev
iVolt1 Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# copy the already built /app dir
COPY --from=builder /app /app

# the /app contents have correct permissions but for some reason /app itself does not.
# so apply again, but ONLY to the dir (otherwise we increase the size)
RUN chmod 777 /app

# Set some labels
Expand Down
19 changes: 19 additions & 0 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ RUN PYAV_VERSION=$(python -c "import json; reqs=json.load(open('/tmp/sendspin_ma

##################################################################################################

# Small builder to install pulseaudio-utils and extract binaries/libs
FROM debian:bookworm AS pa-builder

RUN apt-get update && apt-get install -y --no-install-recommends \
pulseaudio-utils \
&& rm -rf /var/lib/apt/lists/*

# Ensure the files we need exist (pactl, pacmd and pulse libs)
# They will be copied into the final base image.

FROM python:3.13-slim-bookworm

# Enable non-free and contrib repositories for codec libraries
Expand Down Expand Up @@ -197,6 +207,7 @@ RUN set -x \
# AirPlay receiver dependencies (shairport-sync)
libconfig9 \
libpopt0 \
pulseaudio-utils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand Down Expand Up @@ -224,6 +235,14 @@ COPY --from=ffmpeg-builder /usr/local/lib/libpostproc.so* /usr/local/lib/
# Copy pre-built PyAV wheel for use by downstream images
COPY --from=pyav-builder /wheels/ /usr/local/share/pyav-wheels/

# Copy pulseaudio binaries and libs from pa-builder
COPY --from=pa-builder /usr/bin/pactl /usr/bin/pactl
COPY --from=pa-builder /usr/bin/pacmd /usr/bin/pacmd
# PulseAudio libraries location on Debian x86_64; copy the whole pulse dir to be safe
COPY --from=pa-builder /usr/lib/x86_64-linux-gnu/pulse /usr/lib/x86_64-linux-gnu/pulse
# Some systems place libs directly under /usr/lib; include these patterns if needed:
COPY --from=pa-builder /usr/lib/x86_64-linux-gnu/libpulse* /usr/lib/x86_64-linux-gnu/ || true

# Update shared library cache and verify FFmpeg
RUN ldconfig && ffmpeg -version && ffprobe -version

Expand Down
101 changes: 101 additions & 0 deletions music_assistant/providers/pulse_audio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Pulse Audio Out Provider

## Overview

The Pulse Audio Out provider exposes PulseAudio sinks (USB DACs, built-in audio, HDMI, remap sinks, etc.) as players in Music Assistant. It leverages the Sendspin provider for synchronization and timing, registering each sink as an external Sendspin bridge client.

### Key Features

- **Automatic Sink Discovery**: Enumerates all PulseAudio output sinks via `pactl`, including virtual sinks such as remap and combined sinks
- **Native Format Negotiation**: Each sink advertises its native sample rate and bit depth (16, 24, or 32-bit) so Music Assistant transcodes to the correct format per sink — no unnecessary resampling
- **Sendspin Integration**: Each sink is registered as a Sendspin bridge client, enabling synchronized multi-room playback
- **Software Volume Control**: Per-sink volume and mute via PCM sample scaling
- **Stable Player IDs**: Uses UUIDv5 derived from the PulseAudio sink name so players persist across restarts
- **Hardware Volume Ceiling**: Configurable per-provider PA sink volume ceiling applied at startup

## Architecture

### Component Overview

```
┌──────────────────────────────────────────────────────────────┐
│ LocalPulseAudioProvider │
│ - Thin provider shell, delegates to bridge manager │
└──────────────────────────────────────────────────────────────┘
┌────────────────▼────────────────┐
│ LocalPulseAudioBridgeManager │
│ - Enumerates PA sinks via pactl│
│ - Creates/stops bridges │
└────────────────┬────────────────┘
┌───────────────────┼───────────────────┐
│ │
┌─────────▼──────────┐ ┌─────────────▼──────────┐
│ SendspinPulseAudio │ │ SendspinPulseAudio │
│ Bridge (Sink A) │ │ Bridge (Sink B) │
│ │ │ │
│ Sendspin Client ──► │ │ Sendspin Client ──► │
│ BridgePlayerRole │ │ BridgePlayerRole │
│ pa_simple output │ │ pa_simple output │
└─────────────────────┘ └────────────────────────┘
```

### Audio Flow

```
Sendspin PushStream
BridgePlayerRole.on_audio_chunk
▼ (software volume/mute applied, format conversion for 24-bit)
asyncio.Queue
PASimpleStream (libpulse-simple via ctypes)
PulseAudio Sink
Physical Audio Device
```

### Bit Depth Handling

| Sink Format | MA Delivery | PA Stream Format | Conversion |
|-------------|-----------------|---------------------|-------------------------------------|
| `s16le` | 16-bit PCM | `PA_SAMPLE_S16LE` | None |
| `s24le` | 32-bit container (left-justified) | `PA_SAMPLE_S24LE` | Unpack int32, repack to 3-byte LE |
| `s32le` | 32-bit PCM | `PA_SAMPLE_S32LE` | None |

### File Structure

| File | Description |
|------|-------------|
| `__init__.py` | Provider entry point, setup, and config |
| `provider.py` | `LocalPulseAudioProvider` class |
| `sendspin_bridge.py` | Bridge manager and per-sink bridge implementation |
| `player.py` | `LocalPulseAudioPlayer` — MA player model for each sink |
| `pa_simple.py` | Minimal ctypes wrapper around `libpulse-simple` for direct PCM output |
| `helpers.py` | `find_pactl()` and `pactl_env()` utilities |
| `constants.py` | Shared constants (UUID namespace, config keys) |
| `manifest.json` | Provider metadata and dependencies |

## Dependencies

- **Sendspin provider** (`depends_on: sendspin`): Required for audio synchronization and player management
- **libpulse / libpulse-simple**: PulseAudio client libraries (must be present on the host); accessed via ctypes — no Python PulseAudio bindings required
- **pactl**: Used at startup for sink enumeration (`pulseaudio-utils` package on Debian/Ubuntu, `pulseaudio` on Alpine)
- **numpy**: Used for PCM volume scaling

## Notes

- The bundled `pactl` binary (if present) is `amd64` only. On other architectures the system `pactl` must be available in `PATH` or `PULSE_SERVER` must be set.
- Multi-channel sinks (5.1, 7.1) are supported — the bridge opens a stereo stream and PulseAudio handles channel remapping automatically.
- Virtual sinks created by `module-remap-sink` (stereo pairs split from multi-channel cards) are fully supported and are the recommended way to expose individual speaker pairs as independent MA players.

## Related Documentation

- [Sendspin Provider](../sendspin/README.md)
60 changes: 60 additions & 0 deletions music_assistant/providers/pulse_audio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Local PulseAudio Out player provider for Music Assistant."""
from __future__ import annotations

from typing import TYPE_CHECKING

from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption
from music_assistant_models.enums import ConfigEntryType, ProviderFeature

from music_assistant.mass import MusicAssistant

from .constants import (
CONF_HARDWARE_VOLUME_CEILING,
CONF_VOLUME_CONTROL,
DEFAULT_HARDWARE_VOLUME_CEILING,
VOLUME_CONTROL_DISABLED,
VOLUME_CONTROL_HARDWARE,
VOLUME_CONTROL_SOFTWARE,
)
from .provider import LocalPulseAudioProvider

if TYPE_CHECKING:
from music_assistant_models.config_entries import ConfigValueType, ProviderConfig
from music_assistant_models.provider import ProviderManifest
from music_assistant.models import ProviderInstanceType

SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
}


async def get_config_entries(
mass: MusicAssistant,
instance_id: str | None = None,
action: str | None = None,
values: dict[str, ConfigValueType] | None = None,
) -> tuple[ConfigEntry, ...]:
"""Return Config entries to setup this provider."""
# ruff: noqa: ARG001
return (
ConfigEntry(
key=CONF_HARDWARE_VOLUME_CEILING,
type=ConfigEntryType.INTEGER,
label="Hardware volume ceiling",
description=(
"Sets the PulseAudio sink volume to this level on every startup. "
"This attenuates the maximum output level of the hardware. "
"Day-to-day volume control uses software scaling within this ceiling. "
"Range: 0-100. Default: 50."
),
default_value=DEFAULT_HARDWARE_VOLUME_CEILING,
required=False,
),
)


async def setup(
mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
) -> ProviderInstanceType:
"""Initialize provider(instance) with given configuration."""
return LocalPulseAudioProvider(mass, manifest, config, SUPPORTED_FEATURES)
Binary file not shown.
Binary file added music_assistant/providers/pulse_audio/bin/pactl
Binary file not shown.
14 changes: 14 additions & 0 deletions music_assistant/providers/pulse_audio/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Constants for Local PulseAudio Out provider."""
from __future__ import annotations

import uuid

DEVICE_UUID_NAMESPACE = uuid.UUID("b2c3d4e5-f6a7-8901-bcde-f12345678901")

CONF_VOLUME_CONTROL = "volume_control"
VOLUME_CONTROL_SOFTWARE = "software"
VOLUME_CONTROL_HARDWARE = "hardware"
VOLUME_CONTROL_DISABLED = "disabled"

CONF_HARDWARE_VOLUME_CEILING = "hardware_volume_ceiling"
DEFAULT_HARDWARE_VOLUME_CEILING = 50
43 changes: 43 additions & 0 deletions music_assistant/providers/pulse_audio/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Shared helpers for Local PulseAudio Out provider."""
from __future__ import annotations

import os
import shutil

from .pa_simple import PULSE_SERVER


def find_pactl() -> str:
"""Find the pactl binary, preferring the bundled version."""
bundled = os.path.join(os.path.dirname(__file__), "bin", "pactl")
if os.path.isfile(bundled):
if not os.access(bundled, os.X_OK):
# Fix permissions if the wheel lost the execute bit
try:
os.chmod(bundled, 0o777)
except OSError:
pass
if os.access(bundled, os.X_OK):
return bundled
if path := shutil.which("pactl"):
return path
for candidate in ("/usr/bin/pactl", "/usr/local/bin/pactl", "/bin/pactl"):
if os.path.isfile(candidate):
return candidate
raise FileNotFoundError(
"pactl not found — bundled binary missing and pulseaudio-utils not installed"
)

def pactl_env() -> dict[str, str]:
"""Build environment dict for pactl subprocess calls.

Sets LD_LIBRARY_PATH to include the bundled lib directory so that
libpulsecommon is found, and sets PULSE_SERVER to the detected socket.
"""
lib_dir = os.path.join(os.path.dirname(__file__), "lib")
existing_ld = os.environ.get("LD_LIBRARY_PATH", "")
ld_path = f"{lib_dir}:{existing_ld}" if existing_ld else lib_dir
env = {**os.environ, "LD_LIBRARY_PATH": ld_path}
if PULSE_SERVER:
env["PULSE_SERVER"] = PULSE_SERVER
return env
12 changes: 12 additions & 0 deletions music_assistant/providers/pulse_audio/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "player",
"domain": "pulse_audio",
"name": "Pulse Audio Out",
"description": "Play audio through locally attached pulse audio outputs.",
"codeowners": ["@music-assistant"],
"depends_on": "sendspin",
"documentation": "https://music-assistant.io/player-support/pulse-audio/",
"builtin": true,
"allow_disable": true,
"stage": "alpha"
}
Loading
Loading