From 892a1f1ef87a9a790e7b70abe1936bda70f338a9 Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Thu, 16 Jan 2025 18:18:24 +0100 Subject: [PATCH] Feat: Add DSP pipeline details to stream information (#1875) * refactor: add `is_grouping_preventing_dsp()` * feat: attach DSPDetails to StreamDetails * refactor: create `get_dsp_details()` * feat: Attach DSPDetails of all grouped childs to StreamDetails * fix: playergroups don't have a leader, so don't generate DSPDetails * refactor: consolidate `dsp` and `dsp_grouped_childs` streamdetails * refactor: add `get_stream_dsp_details` * refactor: rename `get_dsp_details` to `get_player_dsp_details` * feat: update `streamdetails.dsp` on group/ungroup --- music_assistant/controllers/player_queues.py | 13 ++++- music_assistant/helpers/audio.py | 61 +++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 5b84227c..8aa2198f 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -57,7 +57,7 @@ from music_assistant.constants import ( MASS_LOGO_ONLINE, ) from music_assistant.helpers.api import api_command -from music_assistant.helpers.audio import get_stream_details +from music_assistant.helpers.audio import get_stream_details, get_stream_dsp_details from music_assistant.helpers.throttle_retry import BYPASS_THROTTLER from music_assistant.helpers.util import get_changed_keys from music_assistant.models.core_controller import CoreController @@ -108,6 +108,7 @@ class CompareState(TypedDict): elapsed_time: int stream_title: str | None content_type: str | None + group_childs_count: int class PlayerQueuesController(CoreController): @@ -962,6 +963,7 @@ class PlayerQueuesController(CoreController): next_item_id=None, elapsed_time=0, stream_title=None, + group_childs_count=0, ), ) @@ -978,6 +980,7 @@ class PlayerQueuesController(CoreController): content_type=queue.current_item.streamdetails.audio_format.output_format_str if queue.current_item and queue.current_item.streamdetails else None, + group_childs_count=len(player.group_childs), ) changed_keys = get_changed_keys(prev_state, new_state) # return early if nothing changed @@ -999,6 +1002,14 @@ class PlayerQueuesController(CoreController): else: self._prev_states.pop(queue_id, None) + if "group_childs_count" in changed_keys: + # refresh DSP details since a player has been added/removed from the group + dsp = get_stream_dsp_details(self.mass, queue_id) + if queue.current_item and queue.current_item.streamdetails: + queue.current_item.streamdetails.dsp = dsp + if queue.next_item and queue.next_item.streamdetails: + queue.next_item.streamdetails.dsp = dsp + # detect change in current index to report that a item has been played prev_item_id = prev_state["current_item_id"] player_stopped = ( diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index 3b01bd62..197436af 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -14,6 +14,7 @@ from typing import TYPE_CHECKING import aiofiles from aiohttp import ClientTimeout +from music_assistant_models.dsp import DSPConfig, DSPDetails, DSPState from music_assistant_models.enums import ( ContentType, MediaType, @@ -51,6 +52,7 @@ from .util import TimedAsyncGenerator, create_tempfile, detect_charset if TYPE_CHECKING: from music_assistant_models.config_entries import CoreConfig, PlayerConfig + from music_assistant_models.player import Player from music_assistant_models.player_queue import QueueItem from music_assistant_models.streamdetails import StreamDetails @@ -183,6 +185,49 @@ async def strip_silence( return stripped_data +def get_player_dsp_details(mass: MusicAssistant, player: Player) -> DSPDetails: + """Return DSP details of single a player.""" + dsp_config = mass.config.get_player_dsp_config(player.player_id) + dsp_state = DSPState.ENABLED if dsp_config.enabled else DSPState.DISABLED + if dsp_state == DSPState.ENABLED and is_grouping_preventing_dsp(player): + dsp_state = DSPState.DISABLED_BY_UNSUPPORTED_GROUP + dsp_config = DSPConfig(enabled=False) + + # remove disabled filters + dsp_config.filters = [x for x in dsp_config.filters if x.enabled] + + return DSPDetails( + state=dsp_state, + input_gain=dsp_config.input_gain, + filters=dsp_config.filters, + output_gain=dsp_config.output_gain, + output_limiter=dsp_config.output_limiter, + ) + + +def get_stream_dsp_details( + mass: MusicAssistant, + queue_id: str, +) -> dict[str, DSPDetails]: + """Return DSP details of all players playing this queue, keyed by player_id.""" + player = mass.players.get(queue_id) + dsp = {} + + # We skip the PlayerGroups as they don't provide an audio output + # by themselves, but only sync other players. + if not player.provider.startswith("player_group"): + details = get_player_dsp_details(mass, player) + details.is_leader = True + dsp[player.player_id] = details + + if player and player.group_childs: + # grouped playback, get DSP details for each player in the group + for child_id in player.group_childs: + if child_player := mass.players.get(child_id): + dsp[child_id] = get_player_dsp_details(mass, child_player) + return dsp + + async def get_stream_details( mass: MusicAssistant, queue_item: QueueItem, @@ -270,6 +315,9 @@ async def get_stream_details( core_config, player_settings, streamdetails ) + # attach the DSP details of all group members + streamdetails.dsp = get_stream_dsp_details(mass, streamdetails.queue_id) + process_time = int((time.time() - time_start) * 1000) LOGGER.debug( "retrieved streamdetails for %s in %s milliseconds", @@ -868,6 +916,16 @@ def get_chunksize( return int((320000 / 8) * seconds) +def is_grouping_preventing_dsp(player: Player) -> bool: + """Check if grouping is preventing DSP from being applied. + + If this returns True, no DSP should be applied to the player. + """ + is_grouped = bool(player.synced_to) or bool(player.group_childs) + multi_device_dsp_supported = PlayerFeature.MULTI_DEVICE_DSP in player.supported_features + return is_grouped and not multi_device_dsp_supported + + def get_player_filter_params( mass: MusicAssistant, player_id: str, @@ -879,8 +937,7 @@ def get_player_filter_params( dsp = mass.config.get_player_dsp_config(player_id) if player := mass.players.get(player_id): - is_grouped = bool(player.synced_to) or bool(player.group_childs) - if is_grouped and PlayerFeature.MULTI_DEVICE_DSP not in player.supported_features: + if is_grouping_preventing_dsp(player): # We can not correctly apply DSP to a grouped player without multi-device DSP support, # so we disable it. dsp.enabled = False -- 2.34.1