squashed a cpu hogging bug
This commit is contained in:
parent
0f8d47fb2a
commit
80b6835dc7
1 changed files with 69 additions and 13 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue