diff --git a/Shared/Audio/AudioPlayer.swift b/Shared/Audio/AudioPlayer.swift index 88da2ee..b632ce1 100644 --- a/Shared/Audio/AudioPlayer.swift +++ b/Shared/Audio/AudioPlayer.swift @@ -139,20 +139,26 @@ class AudioPlayer: NSObject, ObservableObject { analysisTask = nil AudioPreFetcher.shared.cancelAll() removeTimeObserver() - alog("Background: timers + time observer + analysis + prefetch suspended") + // SmartCrossfadeManager has its own 10Hz time observer that writes + // @Published currentTime/duration — same SwiftUI churn as AudioPlayer's + if isUsingCrossfade { + SmartCrossfadeManager.shared.suspendForBackground() + } + alog("Background: all timers + observers suspended") } private func resumeVisTimers() { guard isPlaying else { return } - // Reinstall the time observer before restarting vis timers so currentTime - // is accurate when the offline vis timer first reads position. reinstallTimeObserver() + if isUsingCrossfade { + SmartCrossfadeManager.shared.resumeFromBackground() + } if isUsingOfflineVis { startOfflineVisSync() - } else { + } else if !isUsingCrossfade { startLevelSimulation() } - alog("Foreground: timers + time observer resumed (offlineVis=\(isUsingOfflineVis))") + alog("Foreground: timers + observers resumed (crossfade=\(isUsingCrossfade) offlineVis=\(isUsingOfflineVis))") } private func removeTimeObserver() { diff --git a/iOS/Data/AudioPreFetcher.swift b/iOS/Data/AudioPreFetcher.swift index 98ffdbb..e5fec0e 100644 --- a/iOS/Data/AudioPreFetcher.swift +++ b/iOS/Data/AudioPreFetcher.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit /// Pre-fetches the next few tracks in the queue so playback is instant. /// Uses OfflineManager's download system but marks pre-fetched files as temporary diff --git a/iOS/Views/Companion/SmartCrossfadeManager.swift b/iOS/Views/Companion/SmartCrossfadeManager.swift index de1fc4c..356ff6d 100644 --- a/iOS/Views/Companion/SmartCrossfadeManager.swift +++ b/iOS/Views/Companion/SmartCrossfadeManager.swift @@ -363,6 +363,20 @@ class SmartCrossfadeManager: ObservableObject { timeObserver = nil } } + + /// Called when app enters background — removes the 10Hz time observer that + /// drives @Published currentTime/duration updates and burns main-thread CPU. + func suspendForBackground() { + removeTimeObserver() + displayLink?.invalidate() + displayLink = nil + } + + /// Called when app returns to foreground — reinstalls the time observer. + func resumeFromBackground() { + guard timeObserver == nil else { return } + installTimeObserver() + } private func removeAllObservers() { removeBoundaryObserver()