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
)
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(
)
# 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
"""
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,
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):