self._enqueue_next_item(queue_id, next_item)
@api_command("player_queues/set_playback_speed")
- async def set_playback_speed(self, queue_id: str, speed: float) -> None:
- """Set the playback speed for the given queue.
+ async def set_playback_speed(
+ self, queue_id: str, speed: float, queue_item_id: str | None = None
+ ) -> None:
+ """
+ Set the playback speed for the given queue item.
+
+ If queue_item_id is not provided,
+ the speed will be set for the current item in the queue.
:param queue_id: queue_id of the queue to configure.
:param speed: playback speed multiplier (0.5 to 2.0). 1.0 = normal speed.
if not (0.5 <= speed <= 2.0):
raise InvalidDataError(f"Playback speed must be between 0.5 and 2.0, got {speed}")
queue = self._queues[queue_id]
- current_speed = float(queue.extra_attributes.get("playback_speed") or 1.0)
+ if not queue.current_item:
+ raise QueueEmpty("Cannot set playback speed: queue is empty")
+ queue_item_id = queue_item_id or queue.current_item.queue_item_id
+ queue_item = self.get_item(queue_id, queue_item_id)
+ if not queue_item:
+ raise InvalidDataError(f"Queue item {queue_item_id} not found in queue")
+ if not queue_item.duration or queue_item.media_type == MediaType.RADIO:
+ raise InvalidCommand("Cannot set playback speed for items with unknown duration")
+ current_speed = float(queue_item.extra_attributes.get("playback_speed") or 1.0)
if abs(current_speed - speed) < 0.001:
return # no change
- queue.extra_attributes["playback_speed"] = speed
+ # use extra_attributes of the queue item to store the playback speed
+ queue_item.extra_attributes["playback_speed"] = speed
self.signal_update(queue_id)
if queue.state == PlaybackState.PLAYING:
await self.resume(queue_id)
queue_item=queue_item,
pcm_format=pcm_format,
seek_position=queue_item.streamdetails.seek_position,
+ playback_speed=cast(
+ "float", queue_item.extra_attributes.get("playback_speed", 1.0)
+ ),
)
# stream the audio
# this final ffmpeg process in the chain will convert the raw, lossless PCM audio into
self.get_queue_item_stream(
queue_item=queue_item,
pcm_format=pcm_format,
+ playback_speed=cast(
+ "float", queue_item.extra_attributes.get("playback_speed", 1.0)
+ ),
),
buffer_size=10,
min_buffer_before_yield=2,
queue_track,
pcm_format=pcm_format,
seek_position=queue_track.streamdetails.seek_position,
+ playback_speed=cast(
+ "float", queue_track.extra_attributes.get("playback_speed", 1.0)
+ ),
raise_on_error=False,
):
total_chunks_received += 1
queue_item: QueueItem,
pcm_format: AudioFormat,
seek_position: int = 0,
+ playback_speed: float = 1.0,
raise_on_error: bool = True,
) -> AsyncGenerator[bytes, None]:
"""Get the (PCM) audio stream for a single queue item."""
filter_params.append(f"volume={gain_correct}dB")
streamdetails.volume_normalization_gain_correct = gain_correct
+ # handle playback speed
+ if playback_speed != 1.0:
+ filter_params.append(f"atempo={playback_speed}")
+
allow_buffer = bool(
self.mass.config.get_raw_core_config_value(
self.domain, CONF_ALLOW_BUFFER, CONF_ALLOW_BUFFER_DEFAULT
total_chunks_received = 0
req_buffer_size = crossfade_buffer_size
async for chunk in self.get_queue_item_stream(
- queue_item, pcm_format, seek_position=discard_seconds
+ queue_item,
+ pcm_format,
+ seek_position=discard_seconds,
+ playback_speed=cast("float", queue_item.extra_attributes.get("playback_speed", 1.0)),
):
total_chunks_received += 1
if discard_leftover:
buffer = b""
try:
async for chunk in self.get_queue_item_stream(
- next_queue_item, next_queue_item_pcm_format
+ next_queue_item,
+ next_queue_item_pcm_format,
+ playback_speed=cast(
+ "float", queue_item.extra_attributes.get("playback_speed", 1.0)
+ ),
):
# append to buffer until we reach crossfade size
# we only need the first X seconds of the NEXT track so we can
elif conf_channels == "right":
filter_params.append("pan=mono|c0=FR")
- # Apply playback speed via atempo filter if a non-default speed is set on the queue.
- if _speed_queue := mass.player_queues.get_active_queue(player_id):
- speed = float(_speed_queue.extra_attributes.get("playback_speed") or 1.0)
- if speed != 1.0:
- filter_params.append(f"atempo={speed:.4f}")
-
# Add safety limiter at the end
if limiter_enabled:
filter_params.append("alimiter=limit=-2dB:level=false:asc=true")