Some enhancements to the Announcement feature (#1184)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 26 Mar 2024 19:04:53 +0000 (20:04 +0100)
committerGitHub <noreply@github.com>
Tue, 26 Mar 2024 19:04:53 +0000 (20:04 +0100)
music_assistant/client/players.py
music_assistant/common/models/config_entries.py
music_assistant/constants.py
music_assistant/server/controllers/players.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/audio.py
music_assistant/server/models/player_provider.py
music_assistant/server/providers/snapcast/__init__.py
music_assistant/server/providers/sonos/__init__.py

index 2e9203244b6a1842cf30f685a51e8b8c469f55e3..f4d4e7094e31366d5d2ab196154a28f83e16c2c1 100644 (file)
@@ -125,7 +125,8 @@ class Players:
         self,
         player_id: str,
         url: str,
-        use_pre_announce: bool = False,
+        use_pre_announce: bool | None = None,
+        volume_level: int | None = None,
     ) -> None:
         """Handle playback of an announcement (url) on given player."""
         await self.client.send_command(
@@ -133,6 +134,7 @@ class Players:
             player_id=player_id,
             url=url,
             use_pre_announce=use_pre_announce,
+            volume_level=volume_level,
         )
 
     #  PlayerGroup related endpoints/commands
index 48e3faec030ebf793f747ad4394b2fa5e93a9eda..0234fbbf051c9f61c9c1977cd3b2bf7c60698053 100644 (file)
@@ -12,6 +12,10 @@ from mashumaro import DataClassDictMixin
 
 from music_assistant.common.models.enums import ProviderType
 from music_assistant.constants import (
+    CONF_ANNOUNCE_VOLUME,
+    CONF_ANNOUNCE_VOLUME_MAX,
+    CONF_ANNOUNCE_VOLUME_MIN,
+    CONF_ANNOUNCE_VOLUME_STRATEGY,
     CONF_AUTO_PLAY,
     CONF_CROSSFADE,
     CONF_CROSSFADE_DURATION,
@@ -24,6 +28,7 @@ from music_assistant.constants import (
     CONF_LOG_LEVEL,
     CONF_OUTPUT_CHANNELS,
     CONF_SYNC_ADJUST,
+    CONF_TTS_PRE_ANNOUNCE,
     CONF_VOLUME_NORMALIZATION,
     CONF_VOLUME_NORMALIZATION_TARGET,
     SECURE_STRING_SUBSTITUTE,
@@ -434,3 +439,54 @@ CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
     "you can shift the audio a bit.",
     advanced=True,
 )
+
+
+CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry(
+    key=CONF_TTS_PRE_ANNOUNCE,
+    type=ConfigEntryType.BOOLEAN,
+    default_value=True,
+    label="Pre-announce TTS announcements",
+    description="When a TTS (text-to-speech) message is sent to the Announce feature, "
+    "prepend the announcement with a short pre-announcement sound (bell whistle).",
+)
+
+
+CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry(
+    key=CONF_ANNOUNCE_VOLUME_STRATEGY,
+    type=ConfigEntryType.STRING,
+    options=[
+        ConfigValueOption("Absolute volume", "absolute"),
+        ConfigValueOption("Relative volume increase", "relative"),
+        ConfigValueOption("Percentual volume increase", "percentual"),
+        ConfigValueOption("Do not adjust volume", "none"),
+    ],
+    default_value="percentual",
+    label="Volume strategy for Announcements",
+    description="When an announcement is being broadcast to this player, "
+    "how should the volume be adjusted temporary (use in combination with the volume value below).",
+)
+
+CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry(
+    key=CONF_ANNOUNCE_VOLUME,
+    type=ConfigEntryType.INTEGER,
+    default_value=85,
+    label="Volume for Announcements",
+    description="The percentual, relative or absolute volume level, "
+    "used with the strategy for announcements.",
+)
+
+CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry(
+    key=CONF_ANNOUNCE_VOLUME_MIN,
+    type=ConfigEntryType.INTEGER,
+    default_value=15,
+    label="Minimum Volume level for Announcements",
+    description="The volume (adjustment) of announcements should no go below this level.",
+)
+
+CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry(
+    key=CONF_ANNOUNCE_VOLUME_MAX,
+    type=ConfigEntryType.INTEGER,
+    default_value=75,
+    label="Maximum Volume level for Announcements",
+    description="The volume (adjustment) of announcements should no go above this level.",
+)
index 4ec33f5ea988363122a75262b72c76cabbc45fe6..383952b59c765d230adbd3c755eb9037e406c0f7 100644 (file)
@@ -55,6 +55,12 @@ CONF_GROUP_MEMBERS: Final[str] = "group_members"
 CONF_HIDE_PLAYER: Final[str] = "hide_player"
 CONF_ENFORCE_MP3: Final[str] = "enforce_mp3"
 CONF_SYNC_ADJUST: Final[str] = "sync_adjust"
+CONF_TTS_PRE_ANNOUNCE: Final[str] = "tts_pre_announce"
+CONF_ANNOUNCE_VOLUME_STRATEGY: Final[str] = "announce_volume_strategy"
+CONF_ANNOUNCE_VOLUME: Final[str] = "announce_volume"
+CONF_ANNOUNCE_VOLUME_MIN: Final[str] = "announce_volume_min"
+CONF_ANNOUNCE_VOLUME_MAX: Final[str] = "announce_volume_max"
+
 
 # config default values
 DEFAULT_HOST: Final[str] = "0.0.0.0"
index b57087baf815bab7115756f9ee060abd82b2646d..c44f2cbcd9bf5f2f342bc8ca6454f2bf84ace9a2 100644 (file)
@@ -10,6 +10,13 @@ from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast
 import shortuuid
 
 from music_assistant.common.helpers.util import get_changed_values
+from music_assistant.common.models.config_entries import (
+    CONF_ENTRY_ANNOUNCE_VOLUME,
+    CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
+    CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+    CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
+    CONF_ENTRY_TTS_PRE_ANNOUNCE,
+)
 from music_assistant.common.models.enums import (
     ContentType,
     EventType,
@@ -600,7 +607,8 @@ class PlayerController(CoreController):
         self,
         player_id: str,
         url: str,
-        use_pre_announce: bool = False,
+        use_pre_announce: bool | None = None,
+        volume_level: int | None = None,
     ) -> None:
         """Handle playback of an announcement (url) on given player."""
         player = self.get(player_id, True)
@@ -615,6 +623,49 @@ class PlayerController(CoreController):
                 use_pre_announce,
                 url,
             )
+            # work out preferences for announcements
+            if use_pre_announce is None and "tts" in url:
+                use_pre_announce = self.mass.config.get_raw_player_config_value(
+                    player_id,
+                    CONF_ENTRY_TTS_PRE_ANNOUNCE.key,
+                    CONF_ENTRY_TTS_PRE_ANNOUNCE.default_value,
+                )
+            volume_strategy = self.mass.config.get_raw_player_config_value(
+                player_id,
+                CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.key,
+                CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.default_value,
+            )
+            volume_strategy = self.mass.config.get_raw_player_config_value(
+                player_id,
+                CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.key,
+                CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.default_value,
+            )
+            volume_strategy_volume = self.mass.config.get_raw_player_config_value(
+                player_id,
+                CONF_ENTRY_ANNOUNCE_VOLUME.key,
+                CONF_ENTRY_ANNOUNCE_VOLUME.default_value,
+            )
+            if volume_level is None and volume_strategy == "absolute":
+                volume_level = volume_strategy_volume
+            elif volume_level is None and volume_strategy == "relative":
+                volume_level = player.volume_level + volume_strategy_volume
+            elif volume_level is None and volume_strategy == "percentual":
+                percentual = (player.volume_level / 100) * volume_strategy_volume
+                volume_level = player.volume_level + percentual
+            if volume_level is not None:
+                announce_volume_min = self.mass.config.get_raw_player_config_value(
+                    player_id,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value,
+                )
+                announce_volume_max = self.mass.config.get_raw_player_config_value(
+                    player_id,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value,
+                )
+                volume_level = max(announce_volume_min, volume_level)
+                volume_level = min(announce_volume_max, volume_level)
+
             # check for native announce support
             if PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features:
                 if prov := self.mass.get_provider(player.provider):
@@ -624,10 +675,10 @@ class PlayerController(CoreController):
                     announcement_url = self.mass.streams.get_announcement_url(
                         player.player_id, url, use_pre_announce=use_pre_announce
                     )
-                    await prov.play_announcement(player_id, announcement_url)
+                    await prov.play_announcement(player_id, announcement_url, volume_level)
                     return
             # use fallback/default implementation
-            await self._play_announcement(player, url, use_pre_announce)
+            await self._play_announcement(player, url, use_pre_announce, volume_level)
         finally:
             player.announcement_in_progress = False
 
@@ -1081,7 +1132,13 @@ class PlayerController(CoreController):
             # if a child player turned ON while the group player is on, we need to resync/resume
             self.mass.create_task(self._sync_syncgroup(group_player.player_id))
 
-    async def _play_announcement(self, player: Player, url: str, use_pre_announce: bool) -> None:
+    async def _play_announcement(
+        self,
+        player: Player,
+        url: str,
+        use_pre_announce: bool,
+        announcement_volume: int | None = None,
+    ) -> None:
         """Handle (default/fallback) implementation of the play announcement feature.
 
         This default implementation will;
@@ -1139,16 +1196,17 @@ class PlayerController(CoreController):
             await self.cmd_stop(player.player_id)
             # wait for the player to stop
             with suppress(TimeoutError):
-                await self.wait_for_state(player, PlayerState.IDLE, 5)
-        # increase volume a bit
-        temp_volume = max(int(min(75, prev_volume) * 1.5), 15)
+                await self.wait_for_state(player, PlayerState.IDLE, 10)
+        # adjust volume if needed
+        temp_volume = announcement_volume or player.volume_level
         if temp_volume > prev_volume:
             self.logger.debug(
                 "Announcement to player %s - setting temporary volume (%s)...",
                 player.display_name,
-                temp_volume,
+                announcement_volume,
             )
-            await self.cmd_volume_set(player.player_id, temp_volume)
+            await self.cmd_volume_set(player.player_id, announcement_volume)
+            await asyncio.sleep(0.5)
             # play the announcement
             self.logger.debug(
                 "Announcement to player %s - playing the announcement on the player...",
@@ -1157,20 +1215,23 @@ class PlayerController(CoreController):
         await self.play_media(player_id=player.player_id, queue_item=queue_item)
         # wait for the player to play
         with suppress(TimeoutError):
-            await self.wait_for_state(player, PlayerState.PLAYING, 5)
+            await self.wait_for_state(player, PlayerState.PLAYING, 10)
         self.logger.debug(
             "Announcement to player %s - waiting on the player to stop playing...",
             player.display_name,
         )
         # wait for the player to stop playing
         with suppress(TimeoutError):
-            await self.wait_for_state(player, PlayerState.IDLE, 30)
+            await self.wait_for_state(
+                player, PlayerState.IDLE, (queue_item.streamdetails.duration or 30) + 3
+            )
         self.logger.debug(
             "Announcement to player %s - restore previous state...", player.display_name
         )
         # restore volume
         if temp_volume != prev_volume:
             await self.cmd_volume_set(player.player_id, prev_volume)
+            await asyncio.sleep(0.5)
         player.current_item_id = prev_item_id
         # either power off the player or resume playing
         if not prev_power:
index 79e19a8f5b0fa04b50dd3433533192d115f35ec0..b6b74fddf0f72d4318d6fa3a63071ff6fd93002c 100644 (file)
@@ -949,6 +949,7 @@ class StreamsController(CoreController):
             output_format=output_format,
             extra_args=extra_args,
             filter_params=filter_params,
+            loglevel="info",
         ):
             yield chunk
 
index 584a326762f64ab4ed58bcb67f43719a73817512..f76a54e2c01f14a0b9bb35a6f576ed4180e512e4 100644 (file)
@@ -878,7 +878,7 @@ def get_ffmpeg_args(
     if input_format.sample_rate != output_format.sample_rate and libsoxr_support:
         filter_params.append("aresample=resampler=soxr")
 
-    if filter_params:
+    if filter_params and "-filter_complex" not in extra_args:
         extra_args += ["-af", ",".join(filter_params)]
 
     return generic_args + input_args + extra_args + output_args
index 22aad036eb94675e06b97b2bcb5aca88aa4be993..b869c042ff8ff5c8b080e56f3649bed520600f0f 100644 (file)
@@ -6,8 +6,13 @@ from abc import abstractmethod
 from typing import TYPE_CHECKING
 
 from music_assistant.common.models.config_entries import (
+    CONF_ENTRY_ANNOUNCE_VOLUME,
+    CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
+    CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+    CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
     CONF_ENTRY_AUTO_PLAY,
     CONF_ENTRY_HIDE_PLAYER,
+    CONF_ENTRY_TTS_PRE_ANNOUNCE,
     CONF_ENTRY_VOLUME_NORMALIZATION,
     CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
     ConfigEntry,
@@ -40,6 +45,11 @@ class PlayerProvider(Provider):
             CONF_ENTRY_AUTO_PLAY,
             CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
             CONF_ENTRY_HIDE_PLAYER,
+            CONF_ENTRY_TTS_PRE_ANNOUNCE,
+            CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
+            CONF_ENTRY_ANNOUNCE_VOLUME,
+            CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+            CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
         )
         if player_id.startswith(SYNCGROUP_PREFIX):
             # add default entries for syncgroups
@@ -133,7 +143,9 @@ class PlayerProvider(Provider):
         This will NOT be called if the player is using flow mode to playback the queue.
         """
 
-    async def play_announcement(self, player_id: str, announcement_url: str) -> None:
+    async def play_announcement(
+        self, player_id: str, announcement_url: str, volume_level: int | None = None
+    ) -> None:
         """Handle (provider native) playback of an announcement on given player."""
         # will only be called for players with PLAY_ANNOUNCEMENT feature set.
         raise NotImplementedError
index 0065bbd0e56e10b05cec42a2dd9d9a276c885f4e..ac4daae12446bfb3954a6404f557bab36e69c9e1 100644 (file)
@@ -348,11 +348,12 @@ class SnapCastProvider(PlayerProvider):
             self.mass.players.update(player_id)
 
             def stream_callback(_stream) -> None:
-                player.state = PlayerState(stream.status)
+                player.state = PlayerState(_stream.status)
                 if player.state == PlayerState.PLAYING:
                     player.current_item_id = f"{queue_item.queue_id}.{queue_item.queue_item_id}"
                     player.elapsed_time = 0
                     player.elapsed_time_last_updated = time.time()
+                self.mass.players.update(player_id)
                 self._set_childs_state(player_id, player.state)
 
             stream.set_callback(stream_callback)
@@ -379,6 +380,7 @@ class SnapCastProvider(PlayerProvider):
                     # we need to wait a bit for the stream status to become idle
                     # to ensure that all snapclients have consumed the audio
                     await self.mass.players.wait_for_state(player, PlayerState.IDLE)
+                    await asyncio.sleep(5)
             finally:
                 self.logger.debug("Finished streaming to %s", stream_path)
                 # there is no way to unsub the callback to we do this nasty
index 0150da87dd8ca80034c820f09bdd8bf090a3266b..6917734164da017738d927775685fbcc3a02fb62 100644 (file)
@@ -396,11 +396,11 @@ class SonosPlayerProvider(PlayerProvider):
 
         await self._enqueue_item(sonos_player, url=url, queue_item=queue_item)
 
-    async def play_announcement(self, player_id: str, announcement_url: str) -> None:
+    async def play_announcement(
+        self, player_id: str, announcement_url: str, volume_level: int | None = None
+    ) -> None:
         """Handle (provider native) playback of an announcement on given player."""
         sonos_player = self.sonosplayers[player_id]
-        mass_player = self.mass.players.get(player_id)
-        temp_volume = max(int(min(75, mass_player.volume_level) * 1.5), 15)
         self.logger.debug(
             "Playing announcement %s using websocket audioclip on %s",
             announcement_url,
@@ -409,7 +409,7 @@ class SonosPlayerProvider(PlayerProvider):
         try:
             response, _ = await sonos_player.websocket.play_clip(
                 announcement_url,
-                volume=temp_volume,
+                volume=volume_level,
             )
         except SonosWebsocketError as exc:
             raise PlayerCommandFailed(f"Error when calling Sonos websocket: {exc}") from exc