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
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
# 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:
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:
# 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:
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]}",
# 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."""