This commit is contained in:
Dallas Groot 2026-04-10 17:50:26 -07:00
parent 80b6835dc7
commit 3cfcf026d7
2 changed files with 17 additions and 5 deletions

View file

@ -89,6 +89,7 @@ class AudioPlayer: NSObject, ObservableObject {
private var offlineVisBuffer = VisFrameBuffer.empty
private var offlineVisTimer: Timer?
private var offlineVisFPS: Double = 30.0
private var analysisTask: Task<Void, Never>?
#endif
// MARK: - Now Playing Artwork
@ -133,11 +134,14 @@ class AudioPlayer: NSObject, ObservableObject {
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.
// Cancel any in-progress offline vis analysis it's a CPU-intensive FFT
// loop that will otherwise run to completion in background, burning ~60% CPU
analysisTask?.cancel()
analysisTask = nil
// Remove the periodic time observer it fires at 10Hz updating @Published
// currentTime, driving SwiftUI re-evaluation of the entire view tree in background
removeTimeObserver()
alog("Background: timers + time observer suspended")
alog("Background: timers + time observer + analysis task suspended")
}
private func resumeVisTimers() {
@ -1231,7 +1235,8 @@ class AudioPlayer: NSObject, ObservableObject {
let points = VisualizerSettings.shared.nowPlaying.numberOfPoints
let cutoff = VisualizerSettings.shared.frequencyCutoff
Task {
analysisTask?.cancel() // cancel any in-flight analysis from the previous song
analysisTask = Task {
let storage = VisualizerStorageManager.shared
let hasCache = await storage.hasCache(for: songId)
@ -1504,6 +1509,8 @@ class AudioPlayer: NSObject, ObservableObject {
nowPlayingSyncTimer = nil
#if os(iOS)
stopOfflineVisTimer()
analysisTask?.cancel()
analysisTask = nil
if isUsingCrossfade {
SmartCrossfadeManager.shared.stop()
isUsingCrossfade = false

View file

@ -104,6 +104,9 @@ actor OfflineAudioAnalyzer {
var nextFrameSample = 0 // the audio sample index at which to take the next vis frame
while file.framePosition < totalFrames {
// Cooperatively cancel if the app backgrounded mid-analysis
try Task.checkCancellation()
let toRead = min(AVAudioFrameCount(readChunkSamples),
AVAudioFrameCount(totalFrames - file.framePosition))
readBuffer.frameLength = 0
@ -138,6 +141,8 @@ actor OfflineAudioAnalyzer {
let chunkEnd = chunkStart + chunkLen
while nextFrameSample < chunkEnd {
// Check cancellation every frame the inner loop is the hot path
try Task.checkCancellation()
// We need fftSize samples ending at nextFrameSample + fftSize/2
// (centre the FFT window on the frame position for better transient response)
let windowStart = nextFrameSample - fftSize / 2