Skip to content

Commit af320bf

Browse files
Refactor Smart fades (#2582)
1 parent 30cb776 commit af320bf

File tree

6 files changed

+670
-702
lines changed

6 files changed

+670
-702
lines changed

music_assistant/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,12 @@
310310
label="Enable Smart Fades",
311311
options=[
312312
ConfigValueOption("Disabled", "disabled"),
313-
ConfigValueOption("Smart Fades", "smart_fades"),
313+
ConfigValueOption("Smart Crossfade", "smart_crossfade"),
314314
ConfigValueOption("Standard Crossfade", "standard_crossfade"),
315315
],
316316
default_value="disabled",
317317
description="Select the crossfade mode to use when transitioning between tracks.\n\n"
318-
"- 'Smart Fades': Uses beat matching and DJ-like EQ filters to create smooth transitions"
318+
"- 'Smart Crossfade': Uses beat matching and EQ filters to create smooth transitions"
319319
" between tracks.\n"
320320
"- 'Standard Crossfade': Regular crossfade that crossfades the last/first x-seconds of a "
321321
"track.",

music_assistant/controllers/config.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,12 +1048,23 @@ async def _migrate(self) -> None: # noqa: PLR0915
10481048

10491049
# Migrate the crossfade setting into Smart Fade Mode = 'crossfade'
10501050
for player_config in self._data.get(CONF_PLAYERS, {}).values():
1051-
if (crossfade := player_config.pop(CONF_DEPRECATED_CROSSFADE, None)) is None:
1051+
if not (values := player_config.get("values")):
1052+
continue
1053+
if (crossfade := values.pop(CONF_DEPRECATED_CROSSFADE, None)) is None:
10521054
continue
10531055
# Check if player has old crossfade enabled but no smart fades mode set
1054-
if crossfade is True and CONF_SMART_FADES_MODE not in player_config:
1056+
if crossfade is True and CONF_SMART_FADES_MODE not in values:
10551057
# Set smart fades mode to standard_crossfade
1056-
player_config[CONF_SMART_FADES_MODE] = "standard_crossfade"
1058+
values[CONF_SMART_FADES_MODE] = "standard_crossfade"
1059+
changed = True
1060+
1061+
# Migrate smart_fades mode value to smart_crossfade
1062+
for player_config in self._data.get(CONF_PLAYERS, {}).values():
1063+
if not (values := player_config.get("values")):
1064+
continue
1065+
if values.get(CONF_SMART_FADES_MODE) == "smart_fades":
1066+
# Update old 'smart_fades' value to new 'smart_crossfade' value
1067+
values[CONF_SMART_FADES_MODE] = "smart_crossfade"
10571068
changed = True
10581069

10591070
# migrate player configs: always use lookup key for provider

music_assistant/controllers/streams.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ async def get_queue_flow_stream(
998998
# calculate crossfade buffer size
999999
crossfade_buffer_duration = (
10001000
SMART_CROSSFADE_DURATION
1001-
if smart_fades_mode == SmartFadesMode.SMART_FADES
1001+
if smart_fades_mode == SmartFadesMode.SMART_CROSSFADE
10021002
else standard_crossfade_duration
10031003
)
10041004
crossfade_buffer_duration = min(
@@ -1396,7 +1396,7 @@ async def get_queue_item_stream_with_smartfade(
13961396
self,
13971397
queue_item: QueueItem,
13981398
pcm_format: AudioFormat,
1399-
smart_fades_mode: SmartFadesMode = SmartFadesMode.SMART_FADES,
1399+
smart_fades_mode: SmartFadesMode = SmartFadesMode.SMART_CROSSFADE,
14001400
standard_crossfade_duration: int = 10,
14011401
) -> AsyncGenerator[bytes, None]:
14021402
"""Get the audio stream for a single queue item with (smart) crossfade to the next item."""
@@ -1434,7 +1434,7 @@ async def get_queue_item_stream_with_smartfade(
14341434
# calculate crossfade buffer size
14351435
crossfade_buffer_duration = (
14361436
SMART_CROSSFADE_DURATION
1437-
if smart_fades_mode == SmartFadesMode.SMART_FADES
1437+
if smart_fades_mode == SmartFadesMode.SMART_CROSSFADE
14381438
else standard_crossfade_duration
14391439
)
14401440
crossfade_buffer_duration = min(

music_assistant/helpers/audio.py

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -98,105 +98,6 @@ def align_audio_to_frame_boundary(audio_data: bytes, pcm_format: AudioFormat) ->
9898
return audio_data
9999

100100

101-
async def crossfade_pcm_parts(
102-
fade_in_part: bytes,
103-
fade_out_part: bytes,
104-
pcm_format: AudioFormat,
105-
fade_out_pcm_format: AudioFormat | None = None,
106-
) -> bytes:
107-
"""Crossfade two chunks of pcm/raw audio using ffmpeg."""
108-
if fade_out_pcm_format is None:
109-
fade_out_pcm_format = pcm_format
110-
111-
# calculate the fade_length from the smallest chunk
112-
fade_length = min(
113-
len(fade_in_part) / pcm_format.pcm_sample_size,
114-
len(fade_out_part) / fade_out_pcm_format.pcm_sample_size,
115-
)
116-
# write the fade_out_part to a temporary file
117-
fadeout_filename = f"/tmp/{shortuuid.random(20)}.pcm" # noqa: S108
118-
async with aiofiles.open(fadeout_filename, "wb") as outfile:
119-
await outfile.write(fade_out_part)
120-
121-
args = [
122-
# generic args
123-
"ffmpeg",
124-
"-hide_banner",
125-
"-loglevel",
126-
"quiet",
127-
# fadeout part (as file)
128-
"-acodec",
129-
fade_out_pcm_format.content_type.name.lower(),
130-
"-ac",
131-
str(fade_out_pcm_format.channels),
132-
"-ar",
133-
str(fade_out_pcm_format.sample_rate),
134-
"-channel_layout",
135-
"mono" if fade_out_pcm_format.channels == 1 else "stereo",
136-
"-f",
137-
fade_out_pcm_format.content_type.value,
138-
"-i",
139-
fadeout_filename,
140-
# fade_in part (stdin)
141-
"-acodec",
142-
pcm_format.content_type.name.lower(),
143-
"-ac",
144-
str(pcm_format.channels),
145-
"-channel_layout",
146-
"mono" if pcm_format.channels == 1 else "stereo",
147-
"-ar",
148-
str(pcm_format.sample_rate),
149-
"-f",
150-
pcm_format.content_type.value,
151-
"-i",
152-
"-",
153-
# filter args
154-
"-filter_complex",
155-
f"[0][1]acrossfade=d={fade_length}",
156-
# output args
157-
"-acodec",
158-
pcm_format.content_type.name.lower(),
159-
"-ac",
160-
str(pcm_format.channels),
161-
"-channel_layout",
162-
"mono" if pcm_format.channels == 1 else "stereo",
163-
"-ar",
164-
str(pcm_format.sample_rate),
165-
"-f",
166-
pcm_format.content_type.value,
167-
"-",
168-
]
169-
_, crossfaded_audio, _ = await communicate(args, fade_in_part)
170-
await remove_file(fadeout_filename)
171-
if crossfaded_audio:
172-
LOGGER.log(
173-
VERBOSE_LOG_LEVEL,
174-
"crossfaded 2 pcm chunks. fade_in_part: %s - "
175-
"fade_out_part: %s - fade_length: %s seconds",
176-
len(fade_in_part),
177-
len(fade_out_part),
178-
fade_length,
179-
)
180-
return crossfaded_audio
181-
# no crossfade_data, return original data instead
182-
LOGGER.debug(
183-
"crossfade of pcm chunks failed: not enough data? - fade_in_part: %s - fade_out_part: %s",
184-
len(fade_in_part),
185-
len(fade_out_part),
186-
)
187-
if fade_out_pcm_format.sample_rate != pcm_format.sample_rate:
188-
# Edge case: the sample rates are different,
189-
# we need to resample the fade_out part to the same sample rate as the fade_in part
190-
async with FFMpeg(
191-
audio_input="-",
192-
input_format=fade_out_pcm_format,
193-
output_format=pcm_format,
194-
) as ffmpeg:
195-
res = await ffmpeg.communicate(fade_out_part)
196-
return res[0] + fade_in_part
197-
return fade_out_part + fade_in_part
198-
199-
200101
async def strip_silence(
201102
mass: MusicAssistant, # noqa: ARG001
202103
audio_data: bytes,

0 commit comments

Comments
 (0)