From 0033cc6262a61cf6c4eec509f72515d0dddcc39b Mon Sep 17 00:00:00 2001 From: Links2004 <974709+Links2004@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:32:26 +0000 Subject: [PATCH 1/2] Add --force-audio-format to advertise only the preferred format Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sendspin/cli.py | 22 ++++++++++++++++++++++ sendspin/daemon/daemon.py | 5 ++++- sendspin/tui/app.py | 5 ++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/sendspin/cli.py b/sendspin/cli.py index e5fd56b..79c1c22 100644 --- a/sendspin/cli.py +++ b/sendspin/cli.py @@ -210,6 +210,15 @@ def _add_player_runtime_options(target: ArgumentTarget, *, suppress_defaults: bo "(e.g., flac:48000:24:2). Verified against the audio device on startup." ), ) + target.add_argument( + "--force-audio-format", + action="store_true", + default=argparse.SUPPRESS if suppress_defaults else False, + help=( + "Advertise only the --audio-format as supported format " + "instead of just preferring it." + ), + ) target.add_argument( "--disable-mpris", action="store_true", @@ -455,6 +464,15 @@ def _build_parser() -> argparse.ArgumentParser: "(e.g., flac:48000:24:2). Verified against the audio device on startup." ), ) + daemon_parser.add_argument( + "--force-audio-format", + action="store_true", + default=False, + help=( + "Advertise only the --audio-format as supported format " + "instead of just preferring it." + ), + ) daemon_parser.add_argument( "--settings-dir", type=str, @@ -758,6 +776,7 @@ async def _run_daemon_mode( listen_port=args.listen_port, use_mpris=args.use_mpris, preferred_format=_resolve_preferred_format(args.audio_format, audio_device), + force_preferred_format=args.force_audio_format, volume_controller=volume_controller, hook_start=args.hook_start, hook_stop=args.hook_stop, @@ -880,6 +899,8 @@ async def _run_client_mode(args: argparse.Namespace) -> int: args.use_mpris = not args.disable_mpris and settings.use_mpris if args.audio_format is None: args.audio_format = settings.audio_format + if args.force_audio_format and args.audio_format is None: + raise CLIError("--force-audio-format requires --audio-format", 1) if args.hardware_volume is None: if settings.use_hardware_volume is not None: args.hardware_volume = settings.use_hardware_volume @@ -979,6 +1000,7 @@ async def _run_client_mode(args: argparse.Namespace) -> int: output_delay_ms=args.output_delay_ms, use_mpris=args.use_mpris, preferred_format=_resolve_preferred_format(args.audio_format, audio_device), + force_preferred_format=args.force_audio_format, volume_controller=volume_controller, hook_start=args.hook_start, hook_stop=args.hook_stop, diff --git a/sendspin/daemon/daemon.py b/sendspin/daemon/daemon.py index 2a8c891..1d519cb 100644 --- a/sendspin/daemon/daemon.py +++ b/sendspin/daemon/daemon.py @@ -45,6 +45,7 @@ class DaemonArgs: listen_port: int = 8928 use_mpris: bool = True preferred_format: SupportedAudioFormat | None = None + force_preferred_format: bool = False volume_controller: VolumeController | None = None hook_start: str | None = None hook_stop: str | None = None @@ -85,7 +86,9 @@ def _create_client(self) -> SendspinClient: client_roles.extend([Roles.METADATA, Roles.CONTROLLER]) supported_formats = detect_supported_audio_formats(self._args.audio_device) - if self._args.preferred_format is not None: + if self._args.preferred_format is not None and self._args.force_preferred_format: + supported_formats = [self._args.preferred_format] + elif self._args.preferred_format is not None: supported_formats = [f for f in supported_formats if f != self._args.preferred_format] supported_formats.insert(0, self._args.preferred_format) diff --git a/sendspin/tui/app.py b/sendspin/tui/app.py index ab8a303..2f4fc0d 100644 --- a/sendspin/tui/app.py +++ b/sendspin/tui/app.py @@ -234,6 +234,7 @@ class AppArgs: output_delay_ms: float | None = None use_mpris: bool = True preferred_format: SupportedAudioFormat | None = None + force_preferred_format: bool = False volume_controller: VolumeController | None = None hook_start: str | None = None hook_stop: str | None = None @@ -493,7 +494,9 @@ def request_shutdown() -> None: # Detect supported audio formats for the output device supported_formats = detect_supported_audio_formats(args.audio_device) - if args.preferred_format is not None: + if args.preferred_format is not None and args.force_preferred_format: + supported_formats = [args.preferred_format] + elif args.preferred_format is not None: supported_formats = [f for f in supported_formats if f != args.preferred_format] supported_formats.insert(0, args.preferred_format) self._supported_formats = supported_formats From 85979a66c9166137ec2fdbf1336520fcdb980d02 Mon Sep 17 00:00:00 2001 From: Links2004 <974709+Links2004@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:28:54 +0000 Subject: [PATCH 2/2] Support force_audio_format setting and document --force-audio-format Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 14 ++++++++++++++ sendspin/cli.py | 1 + sendspin/settings.py | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 2b9d004..d75ba63 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ Settings are stored in `~/.config/sendspin/`: "name": "Living Room", "audio_device": "2", "audio_format": "flac:48000:24:2", + "force_audio_format": false, "log_level": "INFO", "listen_port": 8927, "use_mpris": true, @@ -151,6 +152,7 @@ Settings are stored in `~/.config/sendspin/`: | `name` | string | All | Friendly name for client or server (`--name`) | | `audio_device` | string | TUI/daemon | Audio device index, name prefix, or ALSA device name (`--audio-device`) | | `audio_format` | string | TUI/daemon | Preferred audio format (`--audio-format`, e.g., `flac:48000:24:2`) | +| `force_audio_format` | boolean | TUI/daemon | Advertise only `audio_format` as supported (`--force-audio-format`, default: false). Requires `audio_format` | | `log_level` | string | All | Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL | | `listen_port` | integer | daemon/serve | Listen port (`--port`, default: 8927) | | `use_mpris` | boolean | TUI/daemon | Enable MPRIS integration (default: true) | @@ -289,6 +291,18 @@ The format string uses the pattern `codec:sample_rate:bit_depth:channels`: The specified format is validated against the audio device on startup. If the device doesn't support it, the player will exit with an error. +#### Forcing the audio format + +`--force-audio-format` advertises only the format from `--audio-format` (or the saved `audio_format` setting) instead of just preferring it, so the server cannot pick anything else. It requires an audio format from the CLI or settings. It can also be saved as `"force_audio_format": true`. + +```bash +# TUI +sendspin --audio-format pcm:48000:24:2 --force-audio-format + +# Daemon +sendspin daemon --audio-format pcm:48000:24:2 --force-audio-format +``` + ### System Volume Control On Linux with PulseAudio/PipeWire, Sendspin can control your system output volume directly. Volume adjustments (keyboard shortcuts, server commands) change the system volume. The current system volume is read on startup — the `player_volume` and `player_muted` settings are only used when hardware volume is disabled. diff --git a/sendspin/cli.py b/sendspin/cli.py index 79c1c22..d3bf681 100644 --- a/sendspin/cli.py +++ b/sendspin/cli.py @@ -899,6 +899,7 @@ async def _run_client_mode(args: argparse.Namespace) -> int: args.use_mpris = not args.disable_mpris and settings.use_mpris if args.audio_format is None: args.audio_format = settings.audio_format + args.force_audio_format = args.force_audio_format or settings.force_audio_format if args.force_audio_format and args.audio_format is None: raise CLIError("--force-audio-format requires --audio-format", 1) if args.hardware_volume is None: diff --git a/sendspin/settings.py b/sendspin/settings.py index 78f4c64..60faaf2 100644 --- a/sendspin/settings.py +++ b/sendspin/settings.py @@ -128,6 +128,7 @@ class ClientSettings(BaseSettings): audio_device: str | None = None use_mpris: bool = True audio_format: str | None = None + force_audio_format: bool = False use_hardware_volume: bool | None = None unpaired_access: bool = True hook_set_volume: str | None = None @@ -158,6 +159,7 @@ def update( listen_port: int | None = None, use_mpris: bool | None = None, audio_format: str | None = None, + force_audio_format: bool | None = None, use_hardware_volume: bool | None = None, hook_set_volume: str | None = None, hook_start: str | None = None, @@ -190,6 +192,7 @@ def update( "listen_port": listen_port, "use_mpris": use_mpris, "audio_format": audio_format, + "force_audio_format": force_audio_format, "use_hardware_volume": use_hardware_volume, "hook_set_volume": hook_set_volume, "hook_start": hook_start, @@ -236,6 +239,7 @@ def _load(self) -> bool: self.audio_device = data.get("audio_device") self.use_mpris = data.get("use_mpris", True) self.audio_format = data.get("audio_format") + self.force_audio_format = data.get("force_audio_format", False) self.use_hardware_volume = data.get("use_hardware_volume") self.unpaired_access = data.get("unpaired_access", True) self.hook_set_volume = data.get("hook_set_volume")