Feat: Add DSP pipeline details to stream information (#1875)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Thu, 16 Jan 2025 17:18:24 +0000 (18:18 +0100)
committerGitHub <noreply@github.com>
Thu, 16 Jan 2025 17:18:24 +0000 (18:18 +0100)
* 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
music_assistant/helpers/audio.py

index 5b84227ca0a72cd662cf7ae52b7915a4e37bf101..8aa2198f86382f94dcda0c975cd2d7e16159fea8 100644 (file)
@@ -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 = (
index 3b01bd62e98195973dda92f3d34c7f1d3196c016..197436afbab9c8f32b02bd15b347d09d8bd36454 100644 (file)
@@ -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