From: Marvin Schenkel Date: Wed, 25 Feb 2026 13:36:44 +0000 (+0100) Subject: Fix smart fades sometimes skipping a fadeout abruptly. X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=21f08e50b860a0cd28f38472dc364b77d69ed234;p=music-assistant-server.git Fix smart fades sometimes skipping a fadeout abruptly. --- diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index 82b813bc..7316011f 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -101,7 +101,7 @@ CONF_RESET_DB = "reset_db" DEFAULT_SYNC_INTERVAL = 12 * 60 # default sync interval in minutes CONF_SYNC_INTERVAL = "sync_interval" CONF_DELETED_PROVIDERS = "deleted_providers" -DB_SCHEMA_VERSION: Final[int] = 27 +DB_SCHEMA_VERSION: Final[int] = 29 CACHE_CATEGORY_LAST_SYNC: Final[int] = 9 CACHE_CATEGORY_SEARCH_RESULTS: Final[int] = 10 @@ -2475,6 +2475,12 @@ class MusicController(CoreController): await db.execute(full_query) await db.commit() + if prev_version <= 29: + # Smart fades analyses were previously computed on silence-stripped audio, + # so beat timestamps are misaligned with the unstripped buffers now passed + # to the crossfade mixer. Truncate the table so all analyses are re-computed. + await self._database.execute(f"DELETE FROM {DB_TABLE_SMART_FADES_ANALYSIS}") + # save changes await self._database.commit() diff --git a/music_assistant/controllers/streams/smart_fades/fades.py b/music_assistant/controllers/streams/smart_fades/fades.py index ebc2ba6c..3811f24b 100644 --- a/music_assistant/controllers/streams/smart_fades/fades.py +++ b/music_assistant/controllers/streams/smart_fades/fades.py @@ -135,30 +135,7 @@ class SmartFade(ABC): try: # Execute the enhanced smart fade with full buffer - returncode, raw_crossfade_output, stderr = await communicate(args, fade_in_part) - - expected_min_output = ( - len(fade_out_part) + len(fade_in_part) - int(pcm_format.pcm_sample_size * 10) - ) # rough minimum: both inputs minus ~10s overlap - self.logger.debug( - "FFmpeg smartfade result: returncode=%d%s, " - "output=%.2fs (%d bytes), fadeout_input=%.2fs, fadein_input=%.2fs%s, " - "stderr=%s", - returncode, - " *** NONZERO - crossfade likely FAILED or produced partial output!" - if returncode != 0 - else "", - len(raw_crossfade_output) / pcm_format.pcm_sample_size - if raw_crossfade_output - else 0, - len(raw_crossfade_output) if raw_crossfade_output else 0, - len(fade_out_part) / pcm_format.pcm_sample_size, - len(fade_in_part) / pcm_format.pcm_sample_size, - f" *** OUTPUT SUSPICIOUSLY SMALL (expected >={expected_min_output} bytes)" - if raw_crossfade_output and len(raw_crossfade_output) < expected_min_output - else "", - stderr.decode().strip() if stderr else "(empty)", - ) + _, raw_crossfade_output, stderr = await communicate(args, fade_in_part) if raw_crossfade_output: return raw_crossfade_output @@ -243,25 +220,6 @@ class SmartCrossFade(SmartFade): bpm=self.fade_out_analysis.bpm, ) - # Check if we would have enough audio after beat alignment for the crossfade - if fadein_start_pos: - required = fadein_start_pos + crossfade_duration - self.logger.debug( - "Trim validation: fadein_start=%.2fs + xfade=%.2fs" - " = %.2fs needed. Checked against constant=%ds" - " (pass=%s). NOTE: if actual fade_in buffer is" - " shorter than %ds after silence stripping," - " FFmpeg acrossfade WILL fail (only %.2fs would" - " remain, need %.2fs)", - fadein_start_pos, - crossfade_duration, - required, - SMART_CROSSFADE_DURATION, - required <= SMART_CROSSFADE_DURATION, - SMART_CROSSFADE_DURATION, - SMART_CROSSFADE_DURATION - required, - crossfade_duration, - ) if fadein_start_pos and fadein_start_pos + crossfade_duration <= SMART_CROSSFADE_DURATION: self.filters.append(TrimFilter(logger=self.logger, fadein_start_pos=fadein_start_pos)) else: diff --git a/music_assistant/controllers/streams/smart_fades/mixer.py b/music_assistant/controllers/streams/smart_fades/mixer.py index 1b25170a..ece93077 100644 --- a/music_assistant/controllers/streams/smart_fades/mixer.py +++ b/music_assistant/controllers/streams/smart_fades/mixer.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import TYPE_CHECKING from music_assistant.controllers.streams.smart_fades.fades import ( - SMART_CROSSFADE_DURATION, SmartCrossFade, SmartFade, StandardCrossFade, @@ -52,42 +51,25 @@ class SmartFadesMixer: # but just to be sure... return fade_out_part + fade_in_part - # strip silence from end of audio of fade_out_part - fade_out_part = await strip_silence( - self.streams.mass, - fade_out_part, - pcm_format=pcm_format, - reverse=True, - ) - # Ensure frame alignment after silence stripping - fade_out_part = align_audio_to_frame_boundary(fade_out_part, pcm_format) - - # strip silence from begin of audio of fade_in_part - fade_in_part = await strip_silence( - self.streams.mass, - fade_in_part, - pcm_format=pcm_format, - reverse=False, - ) - # Ensure frame alignment after silence stripping - fade_in_part = align_audio_to_frame_boundary(fade_in_part, pcm_format) - fadeout_duration = len(fade_out_part) / pcm_format.pcm_sample_size - fadein_duration = len(fade_in_part) / pcm_format.pcm_sample_size - fadeout_stripped = SMART_CROSSFADE_DURATION - fadeout_duration - fadein_stripped = SMART_CROSSFADE_DURATION - fadein_duration - self.logger.debug( - "Buffer durations after silence stripping: " - "fade_out=%.2fs (%.2fs stripped), fade_in=%.2fs (%.2fs stripped)%s", - fadeout_duration, - fadeout_stripped, - fadein_duration, - fadein_stripped, - " *** WARNING: fade_in significantly shorter than" - f" SMART_CROSSFADE_DURATION ({SMART_CROSSFADE_DURATION}s)!" - if fadein_stripped > 2.0 - else "", - ) if mode == SmartFadesMode.STANDARD_CROSSFADE: + # strip silence from end of audio of fade_out_part + fade_out_part = await strip_silence( + self.streams.mass, + fade_out_part, + pcm_format=pcm_format, + reverse=True, + ) + # Ensure frame alignment after silence stripping + fade_out_part = align_audio_to_frame_boundary(fade_out_part, pcm_format) + # strip silence from begin of audio of fade_in_part + fade_in_part = await strip_silence( + self.streams.mass, + fade_in_part, + pcm_format=pcm_format, + reverse=False, + ) + # Ensure frame alignment after silence stripping + fade_in_part = align_audio_to_frame_boundary(fade_in_part, pcm_format) smart_fade: SmartFade = StandardCrossFade( logger=self.logger, crossfade_duration=standard_crossfade_duration,