Fix smart fades sometimes skipping a fadeout abruptly.
authorMarvin Schenkel <marvinschenkel@gmail.com>
Wed, 25 Feb 2026 13:36:44 +0000 (14:36 +0100)
committerMarvin Schenkel <marvinschenkel@gmail.com>
Wed, 25 Feb 2026 13:36:44 +0000 (14:36 +0100)
music_assistant/controllers/music.py
music_assistant/controllers/streams/smart_fades/fades.py
music_assistant/controllers/streams/smart_fades/mixer.py

index 82b813bcd12f6b63430c1c2a08357859c68d4e6f..7316011f2327da2d848946bdc7935960e488e082 100644 (file)
@@ -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()
 
index ebc2ba6c6c35e9ef290a1c2735333c30b751f3e1..3811f24b05bf9c92ef0b3729fd0e0aca25525c7d 100644 (file)
@@ -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:
index 1b25170a3f5da36222a92e989f70e17ee711f0b7..ece93077d2bdf613da4099851be970db4fdb14dc 100644 (file)
@@ -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,