From: Maxim Raznatovski Date: Tue, 4 Mar 2025 21:03:29 +0000 (+0100) Subject: Feat: Move the output limiter option from DSP to Player Settings (#1981) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=0e586d715665d4f51bceec3bfb9b1828b9304901;p=music-assistant-server.git Feat: Move the output limiter option from DSP to Player Settings (#1981) * feat: add output limiter option to the player settings * feat: use the `OUTPUT_LIMITER` option to determine if the limiter should be enabled * feat: add migration logic * fix: use default value from config key * fix: move migration logic to _migrate * refactor: delete migrated `output_limiter` key --- diff --git a/music_assistant/constants.py b/music_assistant/constants.py index ebc24a60..e05ffea1 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -47,6 +47,7 @@ CONF_USERNAME: Final[str] = "username" CONF_PASSWORD: Final[str] = "password" CONF_VOLUME_NORMALIZATION: Final[str] = "volume_normalization" CONF_VOLUME_NORMALIZATION_TARGET: Final[str] = "volume_normalization_target" +CONF_OUTPUT_LIMITER: Final[str] = "output_limiter" CONF_DEPRECATED_EQ_BASS: Final[str] = "eq_bass" CONF_DEPRECATED_EQ_MID: Final[str] = "eq_mid" CONF_DEPRECATED_EQ_TREBLE: Final[str] = "eq_treble" @@ -221,6 +222,15 @@ CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( category="advanced", ) +CONF_ENTRY_OUTPUT_LIMITER = ConfigEntry( + key=CONF_OUTPUT_LIMITER, + type=ConfigEntryType.BOOLEAN, + label="Enable limiting to prevent clipping", + default_value=True, + description="Activates a limiter that prevents audio distortion by making loud peaks quieter.", + category="audio", +) + # These EQ Options are deprecated and will be removed in the future # To allow for automatic migration to the new DSP system, they are still included in the config CONF_ENTRY_DEPRECATED_EQ_BASS = ConfigEntry( @@ -575,6 +585,7 @@ BASE_PLAYER_CONFIG_ENTRIES = ( CONF_ENTRY_PLAYER_ICON, CONF_ENTRY_FLOW_MODE, CONF_ENTRY_VOLUME_NORMALIZATION, + CONF_ENTRY_OUTPUT_LIMITER, CONF_ENTRY_AUTO_PLAY, CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, CONF_ENTRY_HIDE_PLAYER, diff --git a/music_assistant/controllers/config.py b/music_assistant/controllers/config.py index 63e37d00..6cbee71e 100644 --- a/music_assistant/controllers/config.py +++ b/music_assistant/controllers/config.py @@ -38,6 +38,7 @@ from music_assistant.constants import ( CONF_DEPRECATED_EQ_MID, CONF_DEPRECATED_EQ_TREBLE, CONF_ONBOARD_DONE, + CONF_OUTPUT_LIMITER, CONF_PLAYER_DSP, CONF_PLAYERS, CONF_PROVIDERS, @@ -822,6 +823,25 @@ class ConfigController: for x in sample_rates ] changed = True + # migrate DSPConfig.output_limiter + for player_id, dsp_config in list(self._data.get(CONF_PLAYER_DSP, {}).items()): + output_limiter = dsp_config.get("output_limiter") + enabled = dsp_config.get("enabled") + if output_limiter is None or enabled is None or output_limiter: + continue + + if enabled: + # The DSP is enabled, and the user disabled the output limiter in a prior version + # Migrate the output limiter option to the player config + if (players := self._data.get(f"{CONF_PLAYERS}")) and ( + player := players.get(player_id) + ): + player["values"][CONF_OUTPUT_LIMITER] = False + # Delete the old option, so this migration logic will never be called + # anymore for this player. + del dsp_config["output_limiter"] + changed = True + # set 'onboard_done' flag if we have any (non default) provider configs if not self._data.get(CONF_ONBOARD_DONE): default_providers = {x.domain for x in self.mass.get_provider_manifests() if x.builtin} diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index 8b7176f7..d078f650 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -35,6 +35,7 @@ from music_assistant_models.helpers import get_global_cache_value, set_global_ca from music_assistant_models.streamdetails import AudioFormat from music_assistant.constants import ( + CONF_ENTRY_OUTPUT_LIMITER, CONF_OUTPUT_CHANNELS, CONF_VOLUME_NORMALIZATION, CONF_VOLUME_NORMALIZATION_RADIO, @@ -322,12 +323,13 @@ def get_player_dsp_details( # remove disabled filters dsp_config.filters = [x for x in dsp_config.filters if x.enabled] + output_limiter = is_output_limiter_enabled(mass, player) 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, + output_limiter=output_limiter, output_format=player.output_format, ) @@ -1145,6 +1147,27 @@ def is_grouping_preventing_dsp(player: Player) -> bool: return is_multiple_devices and not multi_device_dsp_supported +def is_output_limiter_enabled(mass: MusicAssistant, player: Player) -> bool: + """Check if the player has the output limiter enabled. + + Unlike DSP, the limiter is still configurable when synchronized without MULTI_DEVICE_DSP. + So in grouped scenarios without MULTI_DEVICE_DSP, the permanent sync group or the leader gets + decides if the limiter should be turned on or not. + """ + deciding_player_id = player.player_id + if player.active_group: + # Syncgroup, get from the group player + deciding_player_id = player.active_group + elif player.synced_to: + # Not in sync group, but synced, get from the leader + deciding_player_id = player.synced_to + return mass.config.get_raw_player_config_value( + deciding_player_id, + CONF_ENTRY_OUTPUT_LIMITER.key, + CONF_ENTRY_OUTPUT_LIMITER.default_value, + ) + + def get_player_filter_params( mass: MusicAssistant, player_id: str, @@ -1155,6 +1178,7 @@ def get_player_filter_params( filter_params = [] dsp = mass.config.get_player_dsp_config(player_id) + limiter_enabled = True if player := mass.players.get(player_id): if is_grouping_preventing_dsp(player): @@ -1180,6 +1204,8 @@ def get_player_filter_params( # later be able to show this to the user in the UI. player.output_format = output_format + limiter_enabled = is_output_limiter_enabled(mass, player) + if dsp.enabled: # Apply input gain if dsp.input_gain != 0: @@ -1207,8 +1233,8 @@ def get_player_filter_params( elif conf_channels == "right": filter_params.append("pan=mono|c0=FR") - # Add safety limiter at the end, if not explicitly disabled - if not dsp.enabled or dsp.output_limiter: + # Add safety limiter at the end + if limiter_enabled: filter_params.append("alimiter=limit=-2dB:level=false:asc=true") LOGGER.debug("Generated ffmpeg params for player %s: %s", player_id, filter_params)