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"
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(
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,
CONF_DEPRECATED_EQ_MID,
CONF_DEPRECATED_EQ_TREBLE,
CONF_ONBOARD_DONE,
+ CONF_OUTPUT_LIMITER,
CONF_PLAYER_DSP,
CONF_PLAYERS,
CONF_PROVIDERS,
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}
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,
# 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,
)
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,
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):
# 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:
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)