More fixes for crossfading across sample rates
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 20 Dec 2025 10:46:38 +0000 (11:46 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 20 Dec 2025 10:46:38 +0000 (11:46 +0100)
music_assistant/constants.py
music_assistant/controllers/players/sync_groups.py
music_assistant/controllers/streams/streams_controller.py
music_assistant/providers/sonos/const.py
music_assistant/providers/sonos/player.py
music_assistant/providers/sonos_s1/constants.py
music_assistant/providers/squeezelite/player.py

index d14f7a2e2549d21350ac2a47ebee240c0f855629..906d4154ffb744ac7c98df9e85d8d5f090b07b54 100644 (file)
@@ -638,14 +638,14 @@ CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED = ConfigEntry.from_dict(
     }
 )
 
-CONF_ENTRY_SUPPORT_CROSSFADE_DIFFERENT_SAMPLE_RATES = ConfigEntry(
-    key="crossfade_different_sample_rates",
+CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES = ConfigEntry(
+    key="gapless_different_sample_rates",
     type=ConfigEntryType.BOOLEAN,
-    label="Allow crossfade between tracks with different sample rates",
-    description="Enable this option to allow crossfading between tracks that have different "
+    label="Allow gapless playback (and crossfades) between tracks of different sample rates",
+    description="Enable this option to allow gapless playback between tracks that have different "
     "sample rates (e.g. 44.1kHz to 48kHz). \n\n "
     "Only enable this option if your player actually support this, otherwise you may "
-    "experience audio glitches during crossfades.",
+    "experience audio glitches during transitioning between tracks.",
     default_value=False,
     category="advanced",
 )
index 1bfd04be42a30c710f5eb407d1d56eae30fab754..53b324bf547dbacdfe44c6d4e54769715e1c2c2b 100644 (file)
@@ -59,7 +59,6 @@ SUPPORT_DYNAMIC_LEADER = {
 OPTIONAL_FEATURES = {
     PlayerFeature.ENQUEUE,
     PlayerFeature.GAPLESS_PLAYBACK,
-    PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
     PlayerFeature.NEXT_PREVIOUS,
     PlayerFeature.PAUSE,
     PlayerFeature.PLAY_ANNOUNCEMENT,
index 0df15462debd4de7f426a53793f8b7aeb4d057e7..6ca7926a3991790a4607defdc2656654f95e91c9 100644 (file)
@@ -44,6 +44,7 @@ from music_assistant.constants import (
     CONF_CROSSFADE_DURATION,
     CONF_ENTRY_ENABLE_ICY_METADATA,
     CONF_ENTRY_LOG_LEVEL,
+    CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES,
     CONF_HTTP_PROFILE,
     CONF_OUTPUT_CHANNELS,
     CONF_OUTPUT_CODEC,
@@ -1892,9 +1893,9 @@ class StreamsController(CoreController):
             ),
         )
         supported_sample_rates = tuple(int(x[0]) for x in supported_rates_conf)
-        # use highest supported rate below content rate
+        # use highest supported rate within content rate
         output_sample_rate = max(
-            (r for r in supported_sample_rates if r < streamdetails.audio_format.sample_rate),
+            (r for r in supported_sample_rates if r <= streamdetails.audio_format.sample_rate),
             default=48000,
         )
         # work out pcm format based on streamdetails
@@ -1909,7 +1910,11 @@ class StreamsController(CoreController):
             pcm_format.channels = 2  # force stereo for crossfading
             # allows upsample to same sample rate for crossfade
             # if the device does not support gapless playback with different sample rates
-            if PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE not in player.supported_features:
+            if not self.mass.config.get_raw_player_config_value(
+                player.player_id,
+                CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES.key,
+                CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES.default_value,
+            ):
                 new_sample_rate = max(
                     pcm_format.sample_rate,
                     48000,  # sane/safe default
@@ -1917,7 +1922,8 @@ class StreamsController(CoreController):
                 if new_sample_rate != pcm_format.sample_rate:
                     self.logger.debug(
                         "Player does not support crossfade with different sample rates, "
-                        "content will be (HQ) upsampled to at least 48kHz for crossfade."
+                        "content will be (HQ) upsampled to %s for crossfade.",
+                        new_sample_rate,
                     )
                     pcm_format.sample_rate = new_sample_rate
 
index 375f34bfd5f0351eb4e156b79931b9329bd840ae..9b4c8a9fafa204c1efe042e372fe0fa6f7dd96af 100644 (file)
@@ -22,7 +22,6 @@ PLAYER_FEATURES_BASE = {
     PlayerFeature.SEEK,
     PlayerFeature.SELECT_SOURCE,
     PlayerFeature.GAPLESS_PLAYBACK,
-    PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
 }
 
 SOURCE_LINE_IN = "line_in"
index 3ec0bd3107596991321557654e80e5d5eca5ce1e..c3b7d55208a661beee90545ac60180de6458cd80 100644 (file)
@@ -63,7 +63,6 @@ SUPPORTED_FEATURES = {
     PlayerFeature.SELECT_SOURCE,
     PlayerFeature.SET_MEMBERS,
     PlayerFeature.GAPLESS_PLAYBACK,
-    PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
 }
 
 
index 711f5fced4fbe81cc8697562a5215c3574f25eab..9ca1c1905cb45ae4c005f62eba27e5f51767da11 100644 (file)
@@ -22,7 +22,6 @@ PLAYER_FEATURES = (
     PlayerFeature.VOLUME_SET,
     PlayerFeature.ENQUEUE,
     PlayerFeature.GAPLESS_PLAYBACK,
-    PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
 )
 
 # Source Mapping
index 9613fe83eb9cbfa12508a3da4fc98fc318667a2a..17882e50b24bc3a0134f55ec93f7b3293a956311 100644 (file)
@@ -23,10 +23,7 @@ from music_assistant_models.enums import (
     PlayerType,
     RepeatMode,
 )
-from music_assistant_models.errors import (
-    InvalidCommand,
-    MusicAssistantError,
-)
+from music_assistant_models.errors import InvalidCommand, MusicAssistantError
 from music_assistant_models.media_items import AudioFormat
 
 from music_assistant.constants import (
@@ -35,7 +32,7 @@ from music_assistant.constants import (
     CONF_ENTRY_DEPRECATED_EQ_TREBLE,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
     CONF_ENTRY_OUTPUT_CODEC,
-    CONF_ENTRY_SUPPORT_CROSSFADE_DIFFERENT_SAMPLE_RATES,
+    CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES,
     CONF_ENTRY_SYNC_ADJUST,
     INTERNAL_PCM_FORMAT,
     VERBOSE_LOG_LEVEL,
@@ -91,7 +88,6 @@ class SqueezelitePlayer(Player):
             PlayerFeature.PAUSE,
             PlayerFeature.ENQUEUE,
             PlayerFeature.GAPLESS_PLAYBACK,
-            PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
         }
         self._attr_can_group_with = {provider.instance_id}
         self.multi_client_stream: MultiClientStream | None = None
@@ -170,7 +166,7 @@ class SqueezelitePlayer(Player):
             create_sample_rates_config_entry(
                 max_sample_rate=max_sample_rate, max_bit_depth=24, safe_max_bit_depth=24
             ),
-            CONF_ENTRY_SUPPORT_CROSSFADE_DIFFERENT_SAMPLE_RATES,
+            CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES,
         ]
 
     async def power(self, powered: bool) -> None: