From 842cb6353b6485d2ebae8aa953837ddcc9a4deb8 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Tue, 14 Apr 2026 00:44:38 -0700 Subject: [PATCH] Widget v2 glassmorphism, lyrics, backup, crossfade fixes Widget v2: - Glassmorphism glass panel, 40-bar waveform Canvas with SeekToIntent - CIAreaAverage color extraction + secondary color for adaptive theming - Small: inset glass. Medium/Large: flush edge-to-edge (contentMarginsDisabled) - Large: 3-item queue with real cover art thumbnails, crossfade countdown - Waveform sampled from offline vis buffer, seeded PRNG fallback Live Lyrics: - LyricsService: direct LRCLIB from iOS, no companion dependency - Embedded lyrics read via AVAsset.load(.metadata) from downloads - Karaoke word-by-word gradient fill, auto-scroll, fade edges - Tap-to-sync timing editor with +/-0.1s offset adjust - Companion API fallback only for server-side embedded tags Backup System: - .nvdbackup export/import via ZIPFoundation - UTI registered for AirDrop, passwords stripped on export - PendingOperationsQueue with retry + disk persistence Crossfade fixes: - Seek bar: reports from incoming player immediately, not at 50% - songHandoff at midpoint: art/colors/text/lyrics transition mid-fade - Scrobble fires before metadata swap (correct outgoing song) Visualizer fixes: - stopAll() zeros _audioLevels + clears offlineVisBuffer - All 5 simulation start sites gated behind realAudioAnalysis check - Bars stay flat between skips, only rise when real vis data loads Smart DJ bulk prefetch, appendingPathComponent slash fix --- Shared/Audio/AudioPlayer.swift | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Shared/Audio/AudioPlayer.swift b/Shared/Audio/AudioPlayer.swift index 16b825b..e32faae 100644 --- a/Shared/Audio/AudioPlayer.swift +++ b/Shared/Audio/AudioPlayer.swift @@ -293,9 +293,10 @@ class AudioPlayer: NSObject, ObservableObject { if isUsingOfflineVis { startOfflineVisSync() - } else if !isUsingCrossfade { + } else if !isUsingCrossfade && !VisualizerSettings.shared.realAudioAnalysis { startLevelSimulation() } + // When realAudioAnalysis is on but vis data hasn't loaded yet: stay at 0. alog("Foreground: session reactivated, observers + vis timers resumed (crossfade=\(isUsingCrossfade) offlineVis=\(isUsingOfflineVis))") } @@ -475,7 +476,9 @@ class AudioPlayer: NSObject, ObservableObject { updateNowPlayingInfo() fetchAndSetArtwork(coverArtId: song.coverArt) - startLevelSimulation() + if !VisualizerSettings.shared.realAudioAnalysis { + startLevelSimulation() + } // Load offline visualizer for crossfade path too if VisualizerSettings.shared.realAudioAnalysis { @@ -867,9 +870,12 @@ class AudioPlayer: NSObject, ObservableObject { #if os(iOS) if isRadioStream { startRadioSimulation() - } else { + } else if !VisualizerSettings.shared.realAudioAnalysis { + // Simulation mode only — real analysis waits for offline vis data startLevelSimulation() } + // When realAudioAnalysis is on: levels stay at 0 until + // loadOfflineVisualizer → startOfflineVisSync fills them. #else startLevelSimulation() #endif @@ -1090,7 +1096,7 @@ class AudioPlayer: NSObject, ObservableObject { guard let self = self else { return } self.setLevels(self.rawFFTLevels) } - } else { + } else if !VisualizerSettings.shared.realAudioAnalysis { DebugLogger.shared.log("resume: simulation path — levelTimer nil=\(levelTimer == nil)", category: "VisDebug") // Simulation path: seed internalLevels from a fresh random target // so the wave has height immediately rather than starting from floor. @@ -1100,6 +1106,7 @@ class AudioPlayer: NSObject, ObservableObject { setLevels(internalLevels) if levelTimer == nil { startLevelSimulation() } } + // else: realAudioAnalysis on, vis data loading — stay at 0 pushWidgetState() #endif updateNowPlayingInfo() @@ -1626,7 +1633,7 @@ class AudioPlayer: NSObject, ObservableObject { } } catch { alog("Offline vis: analysis failed: \(error.localizedDescription)") - await MainActor.run { self.startLevelSimulation() } + // Don't fall back to simulation — levels stay at 0 } } // end if !fetched }