Better handling of crossfade buffer frame aligment
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 2 Nov 2025 22:26:31 +0000 (23:26 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 2 Nov 2025 22:26:31 +0000 (23:26 +0100)
music_assistant/controllers/streams.py
music_assistant/helpers/smart_fades.py

index c750a6346ffb36a8428ebeb16ff0746216ed1553..515146c64780753e1b1f18d6f71ee9f128aedda2 100644 (file)
@@ -997,7 +997,13 @@ class StreamsController(CoreController):
                 if queue_track.streamdetails.duration
                 else crossfade_buffer_duration,
             )
+            # Ensure crossfade buffer size is aligned to frame boundaries
+            # Frame size = bytes_per_sample * channels
+            bytes_per_sample = pcm_format.bit_depth // 8
+            frame_size = bytes_per_sample * pcm_format.channels
             crossfade_buffer_size = int(pcm_format.pcm_sample_size * crossfade_buffer_duration)
+            # Round down to nearest frame boundary
+            crossfade_buffer_size = (crossfade_buffer_size // frame_size) * frame_size
 
             bytes_written = 0
             buffer = b""
@@ -1405,7 +1411,13 @@ class StreamsController(CoreController):
             if streamdetails.duration
             else crossfade_buffer_duration,
         )
+        # Ensure crossfade buffer size is aligned to frame boundaries
+        # Frame size = bytes_per_sample * channels
+        bytes_per_sample = pcm_format.bit_depth // 8
+        frame_size = bytes_per_sample * pcm_format.channels
         crossfade_buffer_size = int(pcm_format.pcm_sample_size * crossfade_buffer_duration)
+        # Round down to nearest frame boundary
+        crossfade_buffer_size = (crossfade_buffer_size // frame_size) * frame_size
         fade_out_data: bytes | None = None
 
         if crossfade_data:
index 32b76dfa1a68ca7ab6aa06ed364431b018025886..075aa3189c725f9d068b8bea7e96a2e74513eb21 100644 (file)
@@ -74,6 +74,19 @@ class SmartFadesAnalyzer:
             # Convert PCM bytes to numpy array and then to mono for analysis
             audio_array = np.frombuffer(audio_data, dtype=np.float32)
             if pcm_format.channels > 1:
+                # Ensure array size is divisible by channel count
+                samples_per_channel = len(audio_array) // pcm_format.channels
+                valid_samples = samples_per_channel * pcm_format.channels
+                if valid_samples != len(audio_array):
+                    self.logger.warning(
+                        "Audio buffer size (%d) not divisible by channels (%d), "
+                        "truncating %d samples",
+                        len(audio_array),
+                        pcm_format.channels,
+                        len(audio_array) - valid_samples,
+                    )
+                    audio_array = audio_array[:valid_samples]
+
                 # Reshape to separate channels and take average for mono conversion
                 audio_array = audio_array.reshape(-1, pcm_format.channels)
                 mono_audio = np.asarray(np.mean(audio_array, axis=1, dtype=np.float32))
@@ -81,6 +94,14 @@ class SmartFadesAnalyzer:
                 # Single channel - ensure consistent array type
                 mono_audio = np.asarray(audio_array, dtype=np.float32)
 
+            # Validate that the audio is finite (no NaN or Inf values)
+            if not np.all(np.isfinite(mono_audio)):
+                self.logger.error(
+                    "Audio buffer contains non-finite values (NaN/Inf) for %s, cannot analyze",
+                    stream_details_name,
+                )
+                return None
+
             analysis = await self._analyze_track_beats(mono_audio, fragment, pcm_format.sample_rate)
 
             total_time = time.perf_counter() - start_time