fixes hopefully

This commit is contained in:
Dallas Groot 2026-04-10 18:05:10 -07:00
parent 3cfcf026d7
commit 16fd347b44
2 changed files with 34 additions and 13 deletions

View file

@ -26,11 +26,12 @@ class AudioPlayer: NSObject, ObservableObject {
@Published var volume: Float = 0.8
@Published var repeatMode: RepeatMode = .off
@Published var shuffleEnabled = false
// audioLevels is NOT @Published avoids 60fps SwiftUI state thrashing.
// Instead, levelTick increments every time levels change. The visualizer
// observes levelTick: when it changes, body re-evaluates, Canvas re-executes,
// and pulls the latest levels via currentLevels(). Guaranteed dependency.
@Published private(set) var levelTick: Int = 0
// audioLevels is NOT @Published avoids SwiftUI state thrashing.
// The Canvas reads currentLevels() directly on every TimelineView tick.
// levelTick is kept as an internal counter but intentionally NOT @Published
// making it @Published caused 30fps objectWillChange on AudioPlayer, forcing
// every observing view (NowPlayingView, MainTabView, etc.) to re-evaluate 30x/sec.
private(set) var levelTick: Int = 0
nonisolated(unsafe) private var _audioLevels: [Float] = Array(repeating: 0, count: 30)
func currentLevels() -> [Float] { _audioLevels }
@ -134,14 +135,11 @@ class AudioPlayer: NSObject, ObservableObject {
private func suspendVisTimers() {
stopOfflineVisTimer()
stopLevelTimer()
// 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
AudioPreFetcher.shared.cancelAll()
removeTimeObserver()
alog("Background: timers + time observer + analysis task suspended")
alog("Background: timers + time observer + analysis + prefetch suspended")
}
private func resumeVisTimers() {
@ -1234,8 +1232,19 @@ class AudioPlayer: NSObject, ObservableObject {
offlineVisFPS = VisualizerSettings.shared.effectiveFPS
let points = VisualizerSettings.shared.nowPlaying.numberOfPoints
let cutoff = VisualizerSettings.shared.frequencyCutoff
analysisTask?.cancel() // cancel any in-flight analysis from the previous song
analysisTask?.cancel()
// Don't start analysis if already in background it will be triggered
// when the app returns to foreground instead (via resumeVisTimers path
// or next song load in foreground).
#if os(iOS)
guard UIApplication.shared.applicationState != .background else {
alog("Offline vis: skipping analysis in background for \(songId)")
return
}
#endif
analysisTask = Task {
let storage = VisualizerStorageManager.shared
let hasCache = await storage.hasCache(for: songId)

View file

@ -16,7 +16,13 @@ class AudioPreFetcher {
/// Pre-caches the next `maxPrefetch` tracks that aren't already downloaded.
func prefetchUpcoming(queue: [Song], currentIndex: Int) {
guard !queue.isEmpty else { return }
#if os(iOS)
// Don't start prefetch tasks in background URLSession.shared data tasks
// are not background-session tasks and consume CPU/network budget
guard UIApplication.shared.applicationState != .background else { return }
#endif
let start = currentIndex + 1
let end = min(start + maxPrefetch, queue.count)
guard start < end else { return }
@ -95,6 +101,12 @@ class AudioPreFetcher {
}
}
/// Cancel all in-flight prefetch tasks called when app enters background
func cancelAll() {
prefetchTasks.values.forEach { $0.cancel() }
prefetchTasks.removeAll()
}
func clearCache() {
let dir = Self.prefetchCacheDir()
try? FileManager.default.removeItem(at: dir)