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(
player_id=player_id,
url=url,
use_pre_announce=use_pre_announce,
+ volume_level=volume_level,
)
# PlayerGroup related endpoints/commands
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,
CONF_LOG_LEVEL,
CONF_OUTPUT_CHANNELS,
CONF_SYNC_ADJUST,
+ CONF_TTS_PRE_ANNOUNCE,
CONF_VOLUME_NORMALIZATION,
CONF_VOLUME_NORMALIZATION_TARGET,
SECURE_STRING_SUBSTITUTE,
"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.",
+)
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"
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,
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)
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):
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
# 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;
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...",
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:
output_format=output_format,
extra_args=extra_args,
filter_params=filter_params,
+ loglevel="info",
):
yield chunk
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
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,
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
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
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)
# 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
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,
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