squashed a cpu hogging bug

This commit is contained in:
Dallas Groot 2026-04-10 17:39:46 -07:00
parent 0f8d47fb2a
commit 80b6835dc7

View file

@ -111,12 +111,67 @@ class AudioPlayer: NSObject, ObservableObject {
AlbumCoverStore.shared.$updateTrigger
.sink { [weak self] _ in
guard let self, let id = self.currentSong?.coverArt else { return }
self.cachedArtworkCoverArtId = nil // Force refetch
self.cachedArtworkCoverArtId = nil
self.fetchAndSetArtwork(coverArtId: id)
}
.store(in: &cancellables)
// Stop all visualizer timers when backgrounded they serve no purpose without
// a visible Canvas and burn CPU (causing XPC_EXIT_REASON_FAULT at ~169% CPU).
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in self?.suspendVisTimers() }
.store(in: &cancellables)
// Restart the correct timer when returning to foreground
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in self?.resumeVisTimers() }
.store(in: &cancellables)
#endif
}
#if os(iOS)
private func suspendVisTimers() {
stopOfflineVisTimer()
stopLevelTimer()
// Remove the periodic time observer it fires at 10Hz updating @Published currentTime,
// which triggers SwiftUI re-evaluation of the entire view tree in background.
// This is the primary driver of the 93% background CPU that kills the process.
removeTimeObserver()
alog("Background: timers + time observer 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 isUsingOfflineVis {
startOfflineVisSync()
} else {
startLevelSimulation()
}
alog("Foreground: timers + time observer resumed (offlineVis=\(isUsingOfflineVis))")
}
private func removeTimeObserver() {
if let observer = timeObserver {
player?.removeTimeObserver(observer)
timeObserver = nil
}
}
private func reinstallTimeObserver() {
guard player != nil, timeObserver == nil else { return }
let interval = CMTime(seconds: 0.1, preferredTimescale: 600)
timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
guard let self = self else { return }
self.currentTime = time.seconds
if let dur = self.playerItem?.duration.seconds, !dur.isNaN {
self.duration = dur
}
}
}
#endif
// MARK: - Audio Session
@ -1374,33 +1429,34 @@ class AudioPlayer: NSObject, ObservableObject {
private func startLevelSimulation() {
stopLevelTimer()
// Force a fresh target generation on the first tick after resume
// by invalidating the cached phase. Otherwise currentTime hasn't
// advanced yet and the wave smooths to a stale constant value.
lastTargetPhase = -1
levelTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] _ in
guard let self = self, self.isPlaying else {
if self?.internalLevels.contains(where: { $0 > 0.01 }) == true {
self?.internalLevels = Array(repeating: 0, count: 30)
for i in 0..<(self?.internalLevels.count ?? 0) {
self?.internalLevels[i] = 0
}
self?.setLevels(self?.internalLevels ?? [])
}
return
}
let time = self.currentTime
let time = self.currentTime
let phase = Int(time * 12)
if phase != self.lastTargetPhase {
self.lastTargetPhase = phase
self.targetLevels = (0..<30).map { _ in
Float.random(in: 0.1...0.85)
// In-place random target no allocation
for i in 0..<self.targetLevels.count {
self.targetLevels[i] = Float.random(in: 0.1...0.85)
}
}
// In-place smooth no zip/map allocation
let smoothing: Float = 0.15
self.internalLevels = zip(self.internalLevels, self.targetLevels).map { current, target in
current + (target - current) * smoothing
for i in 0..<self.internalLevels.count {
self.internalLevels[i] += (self.targetLevels[i] - self.internalLevels[i]) * smoothing
}
self.publishCounter += 1
if self.publishCounter % 2 == 0 {
self.setLevels(self.internalLevels)