/
/
/
1"""Constants for snapcast provider."""
2
3import pathlib
4
5from music_assistant_models.enums import ContentType
6from music_assistant_models.media_items.audio_format import AudioFormat
7
8CONF_SERVER_HOST = "snapcast_server_host"
9CONF_SERVER_CONTROL_PORT = "snapcast_server_control_port"
10CONF_USE_EXTERNAL_SERVER = "snapcast_use_external_server"
11CONF_SERVER_BUFFER_SIZE = "snapcast_server_built_in_buffer_size"
12CONF_SERVER_CHUNK_MS = "snapcast_server_built_in_chunk_ms"
13CONF_SERVER_INITIAL_VOLUME = "snapcast_server_built_in_initial_volume"
14CONF_SERVER_TRANSPORT_CODEC = "snapcast_server_built_in_codec"
15CONF_SERVER_SEND_AUDIO_TO_MUTED = "snapcast_server_built_in_send_muted"
16CONF_STREAM_IDLE_THRESHOLD = "snapcast_stream_idle_threshold"
17CONF_STREAM_SAMPLE_RATE = "snapcast_stream_sample_rate"
18CONF_STREAM_BIT_DEPTH = "snapcast_stream_bit_depth"
19
20CONF_CATEGORY_GENERIC = "generic"
21CONF_CATEGORY_BUILT_IN = "Built-in Snapserver Settings"
22
23CONF_HELP_LINK = (
24 "https://raw.githubusercontent.com/badaix/snapcast/refs/heads/master/server/etc/snapserver.conf"
25)
26
27DEFAULT_SNAPSERVER_IP = "127.0.0.1"
28DEFAULT_SNAPSERVER_PORT = 1705
29DEFAULT_SNAPSTREAM_IDLE_THRESHOLD = 60000
30DEFAULT_SNAPSERVER_PLUGIN_DIR = "/usr/share/snapserver/plug-ins"
31DEFAULT_SNAPSERVER_CONFIG_FILE = "/etc/snapserver.conf"
32SHIPPED_SNAPSERVER_CONFIG_FILE = (
33 pathlib.Path(__file__).parent / "snapserver" / "snapserver.conf"
34).resolve()
35
36# snapserver has no TCP keepalive (https://github.com/snapcast/snapcast/issues/995) and
37# never times out abruptly powered-off clients, so we poll lastSeen freshness ourselves.
38SNAPCLIENT_LIVENESS_POLL_INTERVAL = 5 # poll_interval (seconds) for snapcast players
39SNAPCLIENT_STALE_THRESHOLD = 15 # mark unavailable if lastSeen frozen this long (seconds)
40
41# Socket path template for control script communication
42# The {queue_id} placeholder will be replaced with the actual queue ID
43CONTROL_SOCKET_PATH_TEMPLATE = "/tmp/ma-snapcast-{queue_id}.sock" # noqa: S108
44
45MASS_STREAM_PREFIX = "Music Assistant - "
46MASS_ANNOUNCEMENT_POSTFIX = " (announcement)"
47SNAPWEB_DIR = pathlib.Path(__file__).parent.resolve().joinpath("snapweb")
48CONTROL_SCRIPT = pathlib.Path(__file__).parent.resolve().joinpath("control.py")
49
50# Supported PCM formats for the Music Assistant -> Snapserver TCP source.
51# 24-bit requires Snapserver packed_s24le ingest support (snapcast/snapcast#1532).
52SNAPCAST_SAMPLE_RATES = (48000, 96000, 192000)
53SNAPCAST_BIT_DEPTHS = (16, 24)
54
55DEFAULT_SNAPCAST_FORMAT = AudioFormat(
56 content_type=ContentType.PCM_S16LE,
57 sample_rate=48000,
58 bit_depth=16,
59 channels=2,
60)
61
62DEFAULT_SNAPCAST_PCM_FORMAT = AudioFormat(
63 # the format that is used as intermediate pcm stream,
64 # we prefer F32 here to account for volume normalization
65 content_type=ContentType.PCM_F32LE,
66 sample_rate=48000,
67 bit_depth=16,
68 channels=2,
69)
70
71
72def snapcast_stream_format(sample_rate: int, bit_depth: int) -> AudioFormat:
73 """Return the PCM AudioFormat used for the Snapcast TCP source stream."""
74 if sample_rate not in SNAPCAST_SAMPLE_RATES:
75 sample_rate = DEFAULT_SNAPCAST_FORMAT.sample_rate
76 if bit_depth not in SNAPCAST_BIT_DEPTHS:
77 bit_depth = DEFAULT_SNAPCAST_FORMAT.bit_depth
78 content_type = ContentType.PCM_S24LE if bit_depth == 24 else ContentType.PCM_S16LE
79 return AudioFormat(
80 content_type=content_type,
81 sample_rate=sample_rate,
82 bit_depth=bit_depth,
83 channels=2,
84 )
85
86
87def snapcast_sampleformat_query(audio_format: AudioFormat) -> str:
88 """
89 Build the sampleformat query fragment for a Snapcast TCP source URI.
90
91 For 24-bit streams, also enables packed_s24le so ffmpeg's packed s24le
92 output can be ingested by Snapserver (see snapcast/snapcast#1532).
93 """
94 sampleformat = f"{audio_format.sample_rate}:{audio_format.bit_depth}:{audio_format.channels}"
95 if audio_format.bit_depth == 24:
96 return f"sampleformat={sampleformat}&packed_s24le=true"
97 return f"sampleformat={sampleformat}"
98