From 551e59a148519be36fd39d3f907d82f0167fb4b4 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Sun, 12 Apr 2026 17:12:07 -0700 Subject: [PATCH] playback improvements to visualizer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Reactivate audio session 2. Process stale widget commands 3. Sync currentTime/duration from live AVPlayer ← new 4. Reinstall periodic time observer 5. Resume crossfade manager 6. Restart nowPlayingSyncTimer 7. Restart vis timers (offline vis or level simulation) The vis timer’s very first frame now reads the correct position instead of the stale one from backgrounding. --- Shared/Audio/AudioPlayer.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Shared/Audio/AudioPlayer.swift b/Shared/Audio/AudioPlayer.swift index c055beb..018e49c 100644 --- a/Shared/Audio/AudioPlayer.swift +++ b/Shared/Audio/AudioPlayer.swift @@ -259,6 +259,19 @@ class AudioPlayer: NSObject, ObservableObject { // by widget intents sit in App Group UserDefaults until we check here. WidgetBridge.shared.processAnyPendingCommand() + // Sync currentTime/duration from the live AVPlayer state. + // While backgrounded, the time observer was removed (suspendVisTimers) + // but AVPlayer kept advancing. Without this sync, the vis timer's first + // frames read the stale position from when the app backgrounded, causing + // the waveform to render from the wrong song position then snap-correct + // once the reinstalled time observer fires (~0.1s later). + if let p = player, p.currentTime().isNumeric { + currentTime = p.currentTime().seconds + } + if let dur = playerItem?.duration.seconds, dur.isFinite, dur > 0 { + duration = dur + } + // Always reinstall time observers — they were removed on background regardless // of play state. Without this, currentTime is frozen after background+pause+play. reinstallTimeObserver() @@ -445,6 +458,7 @@ class AudioPlayer: NSObject, ObservableObject { // Prepare next track in standby prepareNextForCrossfade() + pushWidgetState() return } } @@ -1138,6 +1152,7 @@ class AudioPlayer: NSObject, ObservableObject { self.updateNowPlayingInfo() self.fetchAndSetArtwork(coverArtId: self.currentSong?.coverArt) + self.pushWidgetState() // Prepare next-next (picks up any queue changes that happened mid-fade) self.prepareNextForCrossfade() @@ -1784,7 +1799,9 @@ class AudioPlayer: NSObject, ObservableObject { // produce the same "custom_al-123" key → blur is reused, not redone. artKey = "custom_\(id)" } else if let url = ServerManager.shared.client.coverArtURL(id: id, size: 600) { - coverImage = ImageCache.shared.memoryOnlyImage(for: url) + // cachedImage checks memory first, then disk — acceptable here because + // pushWidgetState only fires on song change / pause / resume, not per-frame. + coverImage = ImageCache.shared.cachedImage(for: url) } }