From 97d55f9a254e9b3c9002112d6a1634ba22045d18 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 16 Jan 2025 22:39:36 +0100 Subject: [PATCH] Chore: Simplify internal pcm format just always use 32 bits floating points --- music_assistant/constants.py | 27 +++++++++- music_assistant/controllers/streams.py | 50 +++---------------- music_assistant/helpers/ffmpeg.py | 9 ++-- music_assistant/providers/airplay/const.py | 6 ++- .../providers/player_group/__init__.py | 7 +-- .../providers/slimproto/__init__.py | 7 +-- .../providers/snapcast/__init__.py | 3 +- 7 files changed, 53 insertions(+), 56 deletions(-) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 875748c1d..de32b3ffc 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -4,7 +4,8 @@ from typing import Final from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption -from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.enums import ConfigEntryType, ContentType +from music_assistant_models.media_items import AudioFormat API_SCHEMA_VERSION: Final[int] = 26 MIN_SCHEMA_VERSION: Final[int] = 24 @@ -523,3 +524,27 @@ def create_sample_rates_config_entry( CONF_ENTRY_SAMPLE_RATES, CONF_ENTRY_HTTP_PROFILE_FORCED_2, ) + + +DEFAULT_STREAM_HEADERS = { + "Server": "Music Assistant", + "transferMode.dlna.org": "Streaming", + "contentFeatures.dlna.org": "DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000", # noqa: E501 + "Cache-Control": "no-cache", + "Pragma": "no-cache", +} +ICY_HEADERS = { + "icy-name": "Music Assistant", + "icy-description": "Music Assistant - Your personal music assistant", + "icy-version": "1", + "icy-logo": MASS_LOGO_ONLINE, +} + +DEFAULT_PCM_FORMAT = AudioFormat( + # always prefer float32 as internal pcm format to create headroom + # for filters such as dsp and volume normalization + content_type=ContentType.PCM_F32LE, + sample_rate=48000, + bit_depth=32, + channels=2, +) diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index ed22d5051..97c4a737e 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -39,12 +39,13 @@ CONF_OUTPUT_CHANNELS, CONF_PUBLISH_IP, CONF_SAMPLE_RATES, - CONF_VOLUME_NORMALIZATION, CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO, CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS, CONF_VOLUME_NORMALIZATION_RADIO, CONF_VOLUME_NORMALIZATION_TRACKS, - MASS_LOGO_ONLINE, + DEFAULT_PCM_FORMAT, + DEFAULT_STREAM_HEADERS, + ICY_HEADERS, SILENCE_FILE, VERBOSE_LOG_LEVEL, ) @@ -74,23 +75,6 @@ from music_assistant_models.streamdetails import StreamDetails -DEFAULT_STREAM_HEADERS = { - "Server": "Music Assistant", - "transferMode.dlna.org": "Streaming", - "contentFeatures.dlna.org": "DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0d500000000000000000000000000000", # noqa: E501 - "Cache-Control": "no-cache", - "Pragma": "no-cache", -} -ICY_HEADERS = { - "icy-name": "Music Assistant", - "icy-description": "Music Assistant - Your personal music assistant", - "icy-version": "1", - "icy-logo": MASS_LOGO_ONLINE, -} -FLOW_DEFAULT_SAMPLE_RATE = 48000 -FLOW_DEFAULT_BIT_DEPTH = 24 - - isfile = wrap(os.path.isfile) @@ -346,17 +330,10 @@ async def serve_queue_item_stream(self, request: web.Request) -> web.Response: ) # pick pcm format based on the streamdetails and player capabilities - if self.mass.config.get_raw_player_config_value(queue_id, CONF_VOLUME_NORMALIZATION, True): - # prefer f32 when volume normalization is enabled - bit_depth = 32 - floating_point = True - else: - bit_depth = queue_item.streamdetails.audio_format.bit_depth - floating_point = False pcm_format = AudioFormat( - content_type=ContentType.from_bit_depth(bit_depth, floating_point), + content_type=DEFAULT_PCM_FORMAT.content_type, sample_rate=queue_item.streamdetails.audio_format.sample_rate, - bit_depth=bit_depth, + bit_depth=DEFAULT_PCM_FORMAT.bit_depth, channels=2, ) chunk_num = 0 @@ -1081,24 +1058,13 @@ async def _select_flow_format( player.player_id, CONF_SAMPLE_RATES ) supported_sample_rates: tuple[int] = tuple(x[0] for x in supported_rates_conf) - supported_bit_depths: tuple[int] = tuple(x[1] for x in supported_rates_conf) - player_max_bit_depth = max(supported_bit_depths) - for sample_rate in (192000, 96000, 48000, 44100): + for sample_rate in (192000, 96000, DEFAULT_PCM_FORMAT.sample_rate): if sample_rate in supported_sample_rates: output_sample_rate = sample_rate break - if self.mass.config.get_raw_player_config_value( - player.player_id, CONF_VOLUME_NORMALIZATION, True - ): - # prefer f32 when volume normalization is enabled - output_bit_depth = 32 - floating_point = True - else: - output_bit_depth = min(24, player_max_bit_depth) - floating_point = False return AudioFormat( - content_type=ContentType.from_bit_depth(output_bit_depth, floating_point), + content_type=DEFAULT_PCM_FORMAT.content_type, sample_rate=output_sample_rate, - bit_depth=output_bit_depth, + bit_depth=DEFAULT_PCM_FORMAT.bit_depth, channels=2, ) diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py index 82ac1b511..d187271ef 100644 --- a/music_assistant/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -289,10 +289,9 @@ def get_ffmpeg_args( *filter_params, ] - # determine if we need to do resampling - if ( - input_format.sample_rate != output_format.sample_rate - or input_format.bit_depth > output_format.bit_depth + # determine if we need to do resampling (or dithering) + if input_format.sample_rate != output_format.sample_rate or ( + input_format.bit_depth > 16 and output_format.bit_depth == 16 ): libsoxr_support = get_global_cache_value(CACHE_ATTR_LIBSOXR_PRESENT) # prefer resampling with libsoxr due to its high quality @@ -309,6 +308,8 @@ def get_ffmpeg_args( resample_filter += f":osr={output_format.sample_rate}" # bit depth conversion: apply dithering when going down to 16 bits + # this is only needed when we need to back to 16 bits + # when going from 32bits FP to 24 bits no dithering is needed if output_format.bit_depth == 16 and input_format.bit_depth > 16: resample_filter += ":osf=s16:dither_method=triangular_hp" diff --git a/music_assistant/providers/airplay/const.py b/music_assistant/providers/airplay/const.py index 802456c0b..526ff5e57 100644 --- a/music_assistant/providers/airplay/const.py +++ b/music_assistant/providers/airplay/const.py @@ -5,6 +5,8 @@ from music_assistant_models.enums import ContentType from music_assistant_models.media_items import AudioFormat +from music_assistant.constants import DEFAULT_PCM_FORMAT + DOMAIN = "airplay" CONF_ENCRYPTION = "encryption" @@ -22,9 +24,9 @@ FALLBACK_VOLUME = 20 AIRPLAY_FLOW_PCM_FORMAT = AudioFormat( - content_type=ContentType.PCM_F32LE, + content_type=DEFAULT_PCM_FORMAT.content_type, sample_rate=44100, - bit_depth=32, + bit_depth=DEFAULT_PCM_FORMAT.bit_depth, ) AIRPLAY_PCM_FORMAT = AudioFormat( content_type=ContentType.from_bit_depth(16), sample_rate=44100, bit_depth=16 diff --git a/music_assistant/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py index ba1a23ab0..60e76c587 100644 --- a/music_assistant/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -53,6 +53,7 @@ CONF_GROUP_MEMBERS, CONF_HTTP_PROFILE, CONF_SAMPLE_RATES, + DEFAULT_PCM_FORMAT, create_sample_rates_config_entry, ) from music_assistant.controllers.streams import DEFAULT_STREAM_HEADERS @@ -75,9 +76,9 @@ UGP_FORMAT = AudioFormat( - content_type=ContentType.PCM_F32LE, - sample_rate=48000, - bit_depth=32, + content_type=DEFAULT_PCM_FORMAT.content_type, + sample_rate=DEFAULT_PCM_FORMAT.sample_rate, + bit_depth=DEFAULT_PCM_FORMAT.bit_depth, ) # ruff: noqa: ARG002 diff --git a/music_assistant/providers/slimproto/__init__.py b/music_assistant/providers/slimproto/__init__.py index 601103c65..9dbbe51af 100644 --- a/music_assistant/providers/slimproto/__init__.py +++ b/music_assistant/providers/slimproto/__init__.py @@ -54,6 +54,7 @@ CONF_ENTRY_SYNC_ADJUST, CONF_PORT, CONF_SYNC_ADJUST, + DEFAULT_PCM_FORMAT, VERBOSE_LOG_LEVEL, create_sample_rates_config_entry, ) @@ -365,9 +366,9 @@ async def play_media( # this is a syncgroup, we need to handle this with a multi client stream master_audio_format = AudioFormat( - content_type=ContentType.PCM_F32LE, - sample_rate=48000, - bit_depth=32, + content_type=DEFAULT_PCM_FORMAT.content_type, + sample_rate=DEFAULT_PCM_FORMAT.sample_rate, + bit_depth=DEFAULT_PCM_FORMAT.bit_depth, ) if media.media_type == MediaType.ANNOUNCEMENT: # special case: stream announcement diff --git a/music_assistant/providers/snapcast/__init__.py b/music_assistant/providers/snapcast/__init__.py index 2fa7e41b4..916f8f4f7 100644 --- a/music_assistant/providers/snapcast/__init__.py +++ b/music_assistant/providers/snapcast/__init__.py @@ -35,6 +35,7 @@ CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_FLOW_MODE_ENFORCED, + DEFAULT_PCM_FORMAT, create_sample_rates_config_entry, ) from music_assistant.helpers.audio import FFMpeg, get_ffmpeg_stream, get_player_filter_params @@ -520,7 +521,7 @@ async def play_media(self, player_id: str, media: PlayerMedia) -> None: start_queue_item=self.mass.player_queues.get_item( media.queue_id, media.queue_item_id ), - pcm_format=input_format, + pcm_format=DEFAULT_PCM_FORMAT, ) else: # assume url or some other direct path