Skip to content

Commit

Permalink
A few small fixes and optimizations to playback (#1232)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt authored and MarvinSchenkel committed Apr 21, 2024
1 parent b019e3c commit b6af0a0
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 31 deletions.
1 change: 1 addition & 0 deletions music_assistant/server/controllers/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ async def get_track_loudness(
true_peak=result["true_peak"],
lra=result["lra"],
threshold=result["threshold"],
target_offset=result["target_offset"],
)
return None

Expand Down
43 changes: 21 additions & 22 deletions music_assistant/server/controllers/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,10 @@ async def serve_queue_item_stream(self, request: web.Request) -> web.Response:
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
Expand Down Expand Up @@ -372,15 +367,10 @@ async def serve_queue_flow_stream(self, request: web.Request) -> web.Response:
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

Expand Down Expand Up @@ -711,17 +701,32 @@ async def get_media_stream(
# 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:
Expand Down Expand Up @@ -751,13 +756,7 @@ async def get_media_stream(
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:
Expand Down
14 changes: 9 additions & 5 deletions music_assistant/server/helpers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 5 additions & 2 deletions music_assistant/server/providers/filesystem_local/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,8 +614,11 @@ async def get_stream_details(self, item_id: str) -> StreamDetails:
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)
Expand Down
19 changes: 18 additions & 1 deletion music_assistant/server/providers/slimproto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,6 @@ async def _handle_play_url(
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]}",
Expand All @@ -480,6 +479,24 @@ async def _handle_play_url(
# 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."""
Expand Down
2 changes: 1 addition & 1 deletion music_assistant/server/providers/ugp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ async def _serve_ugp_stream(self, request: web.Request) -> web.Response:
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,
Expand Down

0 comments on commit b6af0a0

Please sign in to comment.