From: Marcel van der Veldt Date: Thu, 18 Apr 2024 08:50:29 +0000 (+0200) Subject: A few small fixes and optimizations to playback (#1232) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=1aa95017865e84f20764ff315977ea6bab135583;p=music-assistant-server.git A few small fixes and optimizations to playback (#1232) --- diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index 02d3da99..c31ad3c1 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -552,6 +552,7 @@ class MusicController(CoreController): true_peak=result["true_peak"], lra=result["lra"], threshold=result["threshold"], + target_offset=result["target_offset"], ) return None diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index e28a2c31..cdfdaf34 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -295,15 +295,10 @@ class StreamsController(CoreController): input_format=pcm_format, output_format=output_format, filter_params=get_player_filter_params(self.mass, queue_player.player_id), - extra_input_args=[ - # use readrate to limit buffering ahead too much - "-readrate", - "1.2", - ], ): try: await resp.write(chunk) - except (BrokenPipeError, ConnectionResetError): + except (BrokenPipeError, ConnectionResetError, ConnectionError): break if queue.stream_finished is not None: queue.stream_finished = True @@ -372,15 +367,10 @@ class StreamsController(CoreController): output_format=output_format, filter_params=get_player_filter_params(self.mass, queue_player.player_id), chunk_size=icy_meta_interval if enable_icy else None, - extra_input_args=[ - # use readrate to limit buffering ahead too much - "-readrate", - "1.2", - ], ): try: await resp.write(chunk) - except (BrokenPipeError, ConnectionResetError): + except (BrokenPipeError, ConnectionResetError, ConnectionError): # race condition break @@ -711,17 +701,32 @@ class StreamsController(CoreController): # collect all arguments for ffmpeg filter_params = [] extra_input_args = [] + # add loudnorm filter: volume normalization + # more info: https://k.ylo.ph/2016/04/04/loudnorm.html if streamdetails.target_loudness is not None: - # add loudnorm filters - filter_rule = f"loudnorm=I={streamdetails.target_loudness}:TP=-1.5:LRA=11" if streamdetails.loudness: + # we have a measurement so we can do linear mode + target_loudness = streamdetails.target_loudness + # we must ensure that target loudness does not exceed the measured value + # otherwise ffmpeg falls back to dynamic again + # https://github.com/slhck/ffmpeg-normalize/issues/251 + target_loudness = min( + streamdetails.target_loudness, + streamdetails.loudness.integrated + streamdetails.loudness.lra - 1, + ) + filter_rule = f"loudnorm=I={target_loudness}:TP=-2.0:LRA=7.0:linear=true" filter_rule += f":measured_I={streamdetails.loudness.integrated}" filter_rule += f":measured_LRA={streamdetails.loudness.lra}" filter_rule += f":measured_tp={streamdetails.loudness.true_peak}" filter_rule += f":measured_thresh={streamdetails.loudness.threshold}" if streamdetails.loudness.target_offset is not None: filter_rule += f":offset={streamdetails.loudness.target_offset}" - filter_rule += ":linear=true" + else: + # if we have no measurement, we use dynamic mode + # which also collects the measurement on the fly during playback + filter_rule = ( + f"loudnorm=I={streamdetails.target_loudness}:TP=-2.0:LRA=7.0:offset=0.0" + ) filter_rule += ":print_format=json" filter_params.append(filter_rule) if streamdetails.fade_in: @@ -751,13 +756,7 @@ class StreamsController(CoreController): input_format=streamdetails.audio_format, output_format=pcm_format, filter_params=filter_params, - extra_input_args=[ - *extra_input_args, - # we criple ffmpeg a bit on purpose with the filter_threads - # option so it doesn't consume all cpu when calculating loudnorm - "-filter_threads", - "2", - ], + extra_input_args=extra_input_args, collect_log_history=True, logger=logger, ) as ffmpeg_proc: diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py index 10735711..c8e0cabf 100644 --- a/music_assistant/server/helpers/audio.py +++ b/music_assistant/server/helpers/audio.py @@ -1039,18 +1039,22 @@ def get_ffmpeg_args( # 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 + or input_format.bit_depth > output_format.bit_depth ): # prefer resampling with libsoxr due to its high quality if libsoxr_support: - resample_filter = "aresample=resampler=soxr:precision=28" + resample_filter = "aresample=resampler=soxr:precision=30" else: resample_filter = "aresample=resampler=swr" - if output_format.bit_depth < input_format.bit_depth: - # apply dithering when going down to 16 bits - resample_filter += ":osf=s16:dither_method=triangular_hp" + + # sample rate conversion if input_format.sample_rate != output_format.sample_rate: resample_filter += f":osr={output_format.sample_rate}" + + # bit depth conversion: apply dithering when going down to 16 bits + if output_format.bit_depth < input_format.bit_depth: + resample_filter += ":osf=s16:dither_method=triangular_hp" + filter_params.append(resample_filter) if filter_params and "-filter_complex" not in extra_args: diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py index 6b6f4910..e289c5da 100644 --- a/music_assistant/server/providers/filesystem_local/base.py +++ b/music_assistant/server/providers/filesystem_local/base.py @@ -614,8 +614,11 @@ class FileSystemProviderBase(MusicProvider): item_id, self.instance_id ) if library_item is None: - msg = f"Item not found: {item_id}" - raise MediaNotFoundError(msg) + # this could be a file that has just been added, try parsing it + file_item = await self.resolve(item_id) + if not (library_item := await self._parse_track(file_item)): + msg = f"Item not found: {item_id}" + raise MediaNotFoundError(msg) prov_mapping = next(x for x in library_item.provider_mappings if x.item_id == item_id) file_item = await self.resolve(item_id) diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index e15fc699..55defd87 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -466,7 +466,6 @@ class SlimprotoProvider(PlayerProvider): queue = self.mass.player_queues.get(media.queue_id or player_id) slimplayer.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode] slimplayer.extra_data["playlist shuffle"] = int(queue.shuffle_enabled) - # slimplayer.extra_data["can_seek"] = 1 if queue_item else 0 await slimplayer.play_url( url=url, mime_type=f"audio/{url.split('.')[-1].split('?')[0]}", @@ -480,6 +479,24 @@ class SlimprotoProvider(PlayerProvider): # to coordinate a start of multiple synced players autostart=auto_play, ) + # if queue is set to single track repeat, + # immediately set this track as the next + # this prevents race conditions with super short audio clips (on single repeat) + # https://github.com/music-assistant/hass-music-assistant/issues/2059 + if queue.repeat_mode == RepeatMode.ONE: + self.mass.call_later( + 0.2, + slimplayer.play_url( + url=url, + mime_type=f"audio/{url.split('.')[-1].split('?')[0]}", + metadata=metadata, + enqueue=True, + send_flush=False, + transition=SlimTransition.CROSSFADE if crossfade else SlimTransition.NONE, + transition_duration=transition_duration, + autostart=True, + ), + ) async def cmd_pause(self, player_id: str) -> None: """Send PAUSE command to given player.""" diff --git a/music_assistant/server/providers/ugp/__init__.py b/music_assistant/server/providers/ugp/__init__.py index 2dc45bb0..f9ecb214 100644 --- a/music_assistant/server/providers/ugp/__init__.py +++ b/music_assistant/server/providers/ugp/__init__.py @@ -354,7 +354,7 @@ class UniversalGroupProvider(PlayerProvider): raise web.HTTPNotFound(reason=f"Unknown UGP player: {ugp_player_id}") if not (stream := self.streams.get(ugp_player_id, None)) or stream.done: - raise web.HTTPNotFound(f"There is no active UGP stream for {ugp_player_id}!") + raise web.HTTPNotFound(body=f"There is no active UGP stream for {ugp_player_id}!") resp = web.StreamResponse( status=200,