Fix playback speed handling on queue item and not on queue
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 15:06:49 +0000 (16:06 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 15:06:49 +0000 (16:06 +0100)
music_assistant/controllers/player_queues.py
music_assistant/controllers/streams/streams_controller.py
music_assistant/helpers/audio.py

index f33da0aeaa8884e9254663a76ab3f119e32e503e..d9703164120b5076ae1df6295ee3fbec4093164e 100644 (file)
@@ -390,8 +390,14 @@ class PlayerQueuesController(CoreController):
                 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.
@@ -399,10 +405,19 @@ class PlayerQueuesController(CoreController):
         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)
index 5d3611b1428ef18ed900834cf81536580e6e1391..09d3cdd526d45b7991b5e3d7d411bb80c0e33780 100644 (file)
@@ -518,6 +518,9 @@ class StreamsController(CoreController):
                 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
@@ -934,6 +937,9 @@ class StreamsController(CoreController):
                 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,
@@ -1053,6 +1059,9 @@ class StreamsController(CoreController):
                 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
@@ -1356,6 +1365,7 @@ class StreamsController(CoreController):
         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."""
@@ -1403,6 +1413,10 @@ class StreamsController(CoreController):
             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
@@ -1611,7 +1625,10 @@ class StreamsController(CoreController):
         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:
@@ -1768,7 +1785,11 @@ class StreamsController(CoreController):
             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
index e44f1d02cdf12a2662d34e56625a47edf64b0330..2dd60995171d964ae99ccb18a809affce9cd503c 100644 (file)
@@ -1465,12 +1465,6 @@ def get_player_filter_params(
     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")