diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index d40d5f5..69120b2 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -113,6 +113,38 @@ class CMPlayerParser { return stats } + /// Detects whether Music is playing a lossy Dolby Atmos / Spatial stream + /// instead of a Lossless one, using the same CoreAudio logs we already fetch. + /// + /// - Lossless playback decodes via `ACAppleLosslessDecoder` (subType `alac`). + /// - Atmos / Spatial playback bypasses lossless entirely and decodes the lossy + /// AAC asset (`ACMP4AACBaseDecoder`, `Output format: ... 48000 Hz`). + /// + /// Returns `true` (Atmos), `false` (Lossless), or `nil` when the recent log + /// window contains no decode info — callers should keep the previous value in + /// that case rather than flip-flopping during steady playback. + static func detectAtmos(_ entries: [SimpleConsole]) -> Bool? { + // `entries` are newest-first (Console.getRecentEntries reverses the + // chronological order), so the first decoder line we encounter is the + // most recent. Decide on that and stop, rather than letting any + // occurrence in the window decide — otherwise a stale decoder line from + // the previous track (e.g. an `alac` line still in the window right + // after switching into Atmos) could outvote the current one. + for entry in entries { + let message = entry.message + if message.contains("ACAppleLosslessDecoder.cpp") { + return false // most recent decode is lossless -> not Atmos + } + if message.contains("ACMP4AACBaseDecoder.cpp"), + message.contains("Output format:"), + message.contains("48000 Hz") { + return true // most recent decode is the lossy AAC (Atmos/Spatial) asset + } + } + + return nil // no decode info in this window -> keep previous value + } + static func parseCoreMediaConsoleLogs(_ entries: [SimpleConsole]) -> [CMPlayerStats] { let kTimeDifferenceAcceptance = 5.0 // seconds var lastDate: Date? diff --git a/Quality/MenuView.swift b/Quality/MenuView.swift index 0e0f252..dc100f4 100644 --- a/Quality/MenuView.swift +++ b/Quality/MenuView.swift @@ -14,8 +14,14 @@ struct MenuView: View { var body: some View { VStack { + if outputDevices.isAtmosActive { + Text("⚠️ Dolby Atmos is on — not playing in Lossless") + .foregroundColor(.red) + Divider() + } + ContentView() - + Divider() Button { diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 7e367ac..c5ed763 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -17,6 +17,7 @@ class OutputDevices: ObservableObject { @Published var outputDevices = [AudioDevice]() @Published var currentSampleRate: Float64? @Published var currentBitDepth: Int? + @Published var isAtmosActive = false // Music is playing lossy Atmos/Spatial, not Lossless @Published var enableBitDepthDetection = Defaults.shared.userPreferBitDepthDetection private var enableBitDepthDetectionCancellable: AnyCancellable? @@ -143,6 +144,12 @@ class OutputDevices: ObservableObject { // allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) // } + // Flag lossy Dolby Atmos / Spatial playback (no lossless stream to switch to). + // nil means no decode info in this window, so keep the previous value. + if let atmos = CMPlayerParser.detectAtmos(coreAudioLogs) { + DispatchQueue.main.async { self.isAtmosActive = atmos } + } + // allStats.sort(by: {$0.priority > $1.priority}) print("[getAllStats] \(allStats)") }