Small improvements to the announce/alert feature for TTS (#366)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 15 Jun 2022 12:17:51 +0000 (14:17 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Jun 2022 12:17:51 +0000 (14:17 +0200)
* small improvements to alert stream

* remove redundant code

music_assistant/controllers/streams.py
music_assistant/models/player_queue.py

index 9e0e1544ddf86980e41330b45facc01f9d2d640e..72c21bbc625152f865673c7ab20ce604607d4223 100644 (file)
@@ -169,6 +169,7 @@ class StreamsController:
         seek_position: int,
         fade_in: bool,
         output_format: ContentType,
+        is_alert: bool,
     ) -> QueueStream:
         """Start running a queue stream."""
         # cleanup stale previous queue tasks
@@ -184,7 +185,12 @@ class StreamsController:
         streamdetails = await get_stream_details(self.mass, first_item, queue.queue_id)
 
         # work out pcm details
-        if queue.settings.crossfade_mode == CrossFadeMode.ALWAYS:
+        if is_alert:
+            pcm_sample_rate = 41000
+            pcm_bit_depth = 16
+            pcm_channels = 2
+            pcm_resample = True
+        elif queue.settings.crossfade_mode == CrossFadeMode.ALWAYS:
             pcm_sample_rate = min(96000, queue.max_sample_rate)
             pcm_bit_depth = 24
             pcm_channels = 2
@@ -212,6 +218,7 @@ class StreamsController:
             pcm_bit_depth=pcm_bit_depth,
             pcm_channels=pcm_channels,
             pcm_resample=pcm_resample,
+            is_alert=is_alert,
             autostart=True,
         )
         self.mass.create_task(self.cleanup_stale)
@@ -244,6 +251,7 @@ class QueueStream:
         pcm_channels: int = 2,
         pcm_floating_point: bool = False,
         pcm_resample: bool = False,
+        is_alert: bool = False,
         autostart: bool = False,
     ):
         """Init QueueStreamJob instance."""
@@ -259,6 +267,7 @@ class QueueStream:
         self.pcm_channels = pcm_channels
         self.pcm_floating_point = pcm_floating_point
         self.pcm_resample = pcm_resample
+        self.is_alert = is_alert
         self.url = queue.mass.streams.get_stream_url(stream_id, output_format)
 
         self.mass = queue.mass
@@ -356,11 +365,11 @@ class QueueStream:
                     self.seconds_streamed += len(audio_chunk) / sample_size
                     del audio_chunk
                     # allow clients to only buffer max ~30 seconds ahead
-                    seconds_allowed = int(time() - self.streaming_started) + 30
+                    seconds_allowed = int(time() - self.streaming_started)
                     diff = self.seconds_streamed - seconds_allowed
-                    if diff > 1:
+                    if diff > 30:
                         self.logger.debug(
-                            "Player is buffering %s seconds ahead, slowing it down",
+                            "Player is buffering %s seconds ahead, slowing it down a bit",
                             diff,
                         )
                         await asyncio.sleep(10)
@@ -467,9 +476,6 @@ class QueueStream:
                 )
                 continue
 
-            if queue_track.name == "alert":
-                self.pcm_resample = True
-
             # check the PCM samplerate/bitrate
             if not self.pcm_resample and streamdetails.bit_depth > self.pcm_bit_depth:
                 self.signal_next = True
@@ -525,7 +531,7 @@ class QueueStream:
             queue_track.streamdetails.seconds_skipped = seek_position
             fade_in_part = b""
             cur_chunk = 0
-            prev_chunk = None
+            prev_chunk = b""
             bytes_written = 0
             # handle incoming audio chunks
             async for is_last_chunk, chunk in get_media_stream(
@@ -543,11 +549,6 @@ class QueueStream:
                 if len(chunk) == 0 and bytes_written == 0 and is_last_chunk:
                     # stream error: got empy first chunk ?!
                     self.logger.warning("Stream error on %s", queue_track.uri)
-                elif cur_chunk == 1 and is_last_chunk:
-                    # audio only has one single chunk (alert?)
-                    bytes_written += len(chunk)
-                    yield chunk
-                    del chunk
                 elif cur_chunk == 1 and last_fadeout_data:
                     prev_chunk = chunk
                     del chunk
@@ -597,7 +598,7 @@ class QueueStream:
                     bytes_written += len(remaining_bytes)
                     del remaining_bytes
                     del chunk
-                    prev_chunk = None  # needed to prevent this chunk being sent again
+                    prev_chunk = b""  # needed to prevent this chunk being sent again
                 # HANDLE LAST PART OF TRACK
                 elif prev_chunk and is_last_chunk:
                     # last chunk received so create the last_part
@@ -631,6 +632,10 @@ class QueueStream:
                         del last_part
                         del remaining_bytes
                         del chunk
+                elif is_last_chunk:
+                    # there is only one chunk (e.g. alert sound)
+                    yield chunk
+                    del chunk
                 # MIDDLE PARTS OF TRACK
                 else:
                     # middle part of the track
index 65c9575c3c4c6e06bd14a0112a047b448ab864a9..a06304a7e62a27fac099582bb8ecf14e14ffa6aa 100644 (file)
@@ -9,7 +9,13 @@ from asyncio import Task, TimerHandle
 from dataclasses import dataclass
 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
 
-from music_assistant.models.enums import EventType, MediaType, QueueOption, RepeatMode
+from music_assistant.models.enums import (
+    ContentType,
+    EventType,
+    MediaType,
+    QueueOption,
+    RepeatMode,
+)
 from music_assistant.models.errors import MediaNotFoundError, MusicAssistantError
 from music_assistant.models.event import MassEvent
 
@@ -40,6 +46,8 @@ class QueueSnapShot:
     items: List[QueueItem]
     index: Optional[int]
     position: int
+    repeat: RepeatMode
+    shuffle: bool
 
 
 class PlayerQueue:
@@ -271,9 +279,7 @@ class PlayerQueue:
         announce: Prepend the (TTS) alert with a small announce sound.
         gain_correct: Adjust the gain of the alert sound (in dB).
         """
-        if self._snapshot:
-            self.logger.debug("Ignore play_alert: already in progress")
-            # return
+        assert not self._snapshot, "Alert already in progress"
 
         # create snapshot
         await self.snapshot_create()
@@ -300,11 +306,13 @@ class PlayerQueue:
             else:
                 raise MediaNotFoundError(f"Invalid uri: {uri}") from err
 
-        # load queue with alert sound(s)
-        await self.load(queue_items)
+        # start queue with alert sound(s)
+        self._items = queue_items
+        self._settings.repeat_mode = RepeatMode.OFF
+        self._settings.shuffle_enabled = False
+        await self.queue_stream_start(0, 0, False, is_alert=True)
 
-        # wait for the alert to finish playing
-        await self.stream.done.wait()
+        # wait for the player to finish playing
         alert_done = asyncio.Event()
 
         def handle_event(evt: MassEvent):
@@ -315,7 +323,10 @@ class PlayerQueue:
             handle_event, EventType.QUEUE_UPDATED, self.queue_id
         )
         try:
+            await asyncio.wait_for(self.stream.done.wait(), 30)
             await asyncio.wait_for(alert_done.wait(), 30)
+        except asyncio.TimeoutError:
+            self.logger.warning("Timeout while playing alert")
         finally:
             unsub()
             # restore queue
@@ -397,12 +408,15 @@ class PlayerQueue:
 
     async def snapshot_create(self) -> None:
         """Create snapshot of current Queue state."""
+        self.logger.debug("Creating snapshot...")
         self._snapshot = QueueSnapShot(
             powered=self.player.powered,
             state=self.player.state,
             items=self._items,
             index=self._current_index,
             position=self._current_item_elapsed_time,
+            repeat=self._settings.repeat_mode,
+            shuffle=self._settings.shuffle_enabled,
         )
 
     async def snapshot_restore(self) -> None:
@@ -411,6 +425,8 @@ class PlayerQueue:
         # clear queue first
         await self.clear()
         # restore queue
+        self._settings.repeat_mode = self._snapshot.repeat
+        self._settings.shuffle_enabled = self._snapshot.shuffle
         await self.update(self._snapshot.items)
         self._current_index = self._snapshot.index
         self._current_item_elapsed_time = self._snapshot.position
@@ -421,6 +437,7 @@ class PlayerQueue:
         if not self._snapshot.powered:
             await self.player.power(False)
         # reset snapshot once restored
+        self.logger.debug("Restored snapshot...")
         self._snapshot = None
 
     async def play_index(
@@ -625,10 +642,19 @@ class PlayerQueue:
             )
 
     async def queue_stream_start(
-        self, start_index: int, seek_position: int, fade_in: bool, passive: bool = False
+        self,
+        start_index: int,
+        seek_position: int,
+        fade_in: bool,
+        is_alert: bool = False,
+        passive: bool = False,
     ) -> QueueStream:
         """Start the queue stream runner."""
-        output_format = self._settings.stream_type
+        if is_alert and ContentType.MP3 in self.player.supported_content_types:
+            # force MP3 for alert messages
+            output_format = ContentType.MP3
+        else:
+            output_format = self._settings.stream_type
         if self.player.use_multi_stream:
             # if multi stream is enabled, all child players should receive the same audio stream
             expected_clients = len(get_child_players(self.player, True))
@@ -646,6 +672,7 @@ class PlayerQueue:
             seek_position=seek_position,
             fade_in=fade_in,
             output_format=output_format,
+            is_alert=is_alert,
         )
         self._stream_id = stream.stream_id
         # execute the play command on the player(s)