From fcba22d31987ed2fd6453c2719b1b8ad02a89fcb Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Tue, 20 Jan 2026 19:01:06 +0100 Subject: [PATCH] Add 24 and 32-bit audio support for Sendspin (#2977) * feat: add 32-bit audio and mono output support for sendspin * refactor: remove mono output conversion for per-player streams The mono rendering with aiosendspin upmixing added complexity without clear benefit. Reverting to direct output format usage. --- music_assistant/providers/sendspin/player.py | 7 +++---- .../providers/sendspin/timed_client_stream.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/music_assistant/providers/sendspin/player.py b/music_assistant/providers/sendspin/player.py index 69687b19..6e4625f7 100644 --- a/music_assistant/providers/sendspin/player.py +++ b/music_assistant/providers/sendspin/player.py @@ -148,7 +148,6 @@ class MusicAssistantMediaStream(MediaStream): return None # Get per-player DSP filter parameters - # Convert from internal format to output format filter_params = get_player_filter_params( mass, player_id, self.internal_format, self.output_format ) @@ -396,10 +395,11 @@ class SendspinPlayer(Player): async def _run_playback(self, media: PlayerMedia) -> None: """Run the actual playback in a background task.""" try: + # Use 32-bit for the main channel: aiosendspin converts per player as needed pcm_format = AudioFormat( - content_type=ContentType.PCM_S16LE, + content_type=ContentType.PCM_S32LE, sample_rate=48000, - bit_depth=16, + bit_depth=32, channels=2, ) flow_pcm_format = AudioFormat( @@ -426,7 +426,6 @@ class SendspinPlayer(Player): ) # Setup the main channel subscription - # aiosendspin only really supports 16-bit for now TODO: upgrade later to 32-bit main_channel_gen, main_position = await self.timed_client_stream.get_stream( output_format=pcm_format, filter_params=None, # TODO: this should probably still include the safety limiter diff --git a/music_assistant/providers/sendspin/timed_client_stream.py b/music_assistant/providers/sendspin/timed_client_stream.py index 738c0d82..bb0b9620 100644 --- a/music_assistant/providers/sendspin/timed_client_stream.py +++ b/music_assistant/providers/sendspin/timed_client_stream.py @@ -218,7 +218,12 @@ class TimedClientStream: """ audio_gen, position = await self.subscribe_raw() + # Calculate frame size for alignment + # Frame size = channels * bytes_per_sample + bytes_per_frame = output_format.channels * (output_format.bit_depth // 8) + async def _stream_with_ffmpeg() -> AsyncGenerator[bytes, None]: + buffer = b"" try: async for chunk in get_ffmpeg_stream( audio_input=audio_gen, @@ -226,7 +231,17 @@ class TimedClientStream: output_format=output_format, filter_params=filter_params, ): - yield chunk + buffer += chunk + # Yield only complete frames + aligned_size = (len(buffer) // bytes_per_frame) * bytes_per_frame + if aligned_size > 0: + yield buffer[:aligned_size] + buffer = buffer[aligned_size:] + # Yield any remaining complete frames at end of stream + if buffer: + aligned_size = (len(buffer) // bytes_per_frame) * bytes_per_frame + if aligned_size > 0: + yield buffer[:aligned_size] finally: # Ensure audio_gen cleanup runs immediately with suppress(Exception): -- 2.34.1