From 9bcd387db219f2ed269c4329c69447b93af461fa Mon Sep 17 00:00:00 2001 From: Blaze Malan Date: Fri, 5 Jun 2026 15:12:24 -0700 Subject: [PATCH 1/2] Warn when Dolby Atmos hijacks the lossless stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Spatial Audio / Dolby Atmos is enabled, Music plays the lossy AAC asset (decoded via ACMP4AACBaseDecoder @ 48 kHz) and never touches the Apple Lossless decoder, so there is no lossless sample rate to switch to and the device silently stays put. This adds a positive detector for that state and a red warning row in the menu so the user knows why switching appears stuck. - CMPlayerParser.detectAtmos(): classifies the current CoreAudio log window as Lossless (alac decoder), Atmos/Spatial (aac @ 48 kHz, no alac), or unknown (no decode info -> keep prior state). - OutputDevices.isAtmosActive: published flag updated in the existing detection loop. - MenuView: red "Dolby Atmos is on — not playing in Lossless" row shown while active. Co-Authored-By: Claude Opus 4.8 (1M context) --- Quality/CMPlayerStuff.swift | 31 +++++++++++++++++++++++++++++++ Quality/MenuView.swift | 8 +++++++- Quality/OutputDevices.swift | 7 +++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index d40d5f5..946b03d 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -113,6 +113,37 @@ 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? { + var sawLossless = false + var sawLossyAAC = false + + for entry in entries { + let message = entry.message + if message.contains("ACAppleLosslessDecoder.cpp") { + sawLossless = true + } + if message.contains("ACMP4AACBaseDecoder.cpp"), + message.contains("Output format:"), + message.contains("48000 Hz") { + sawLossyAAC = true + } + } + + if sawLossless { return false } // a lossless stream is decoding -> not Atmos + if sawLossyAAC { return true } // lossy AAC @ 48 kHz, no lossless -> Atmos/Spatial + return nil // no decode info in window -> unknown + } + 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)") } From 879ea75f0efc7349fac3188d0a40515325f8d186 Mon Sep 17 00:00:00 2001 From: Blaze Malan Date: Fri, 5 Jun 2026 17:40:50 -0700 Subject: [PATCH 2/2] Decide Atmos detection on the most recent decode line Iterate the (newest-first) log entries and return on the first lossless or AAC decoder line, instead of letting any occurrence in the 5s window decide. Fixes switching into Atmos, where a stale alac line from the previous track could otherwise keep the result false. Addresses PR review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) --- Quality/CMPlayerStuff.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index 946b03d..69120b2 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -124,24 +124,25 @@ class CMPlayerParser { /// 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? { - var sawLossless = false - var sawLossyAAC = false - + // `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") { - sawLossless = true + return false // most recent decode is lossless -> not Atmos } if message.contains("ACMP4AACBaseDecoder.cpp"), message.contains("Output format:"), message.contains("48000 Hz") { - sawLossyAAC = true + return true // most recent decode is the lossy AAC (Atmos/Spatial) asset } } - if sawLossless { return false } // a lossless stream is decoding -> not Atmos - if sawLossyAAC { return true } // lossy AAC @ 48 kHz, no lossless -> Atmos/Spatial - return nil // no decode info in window -> unknown + return nil // no decode info in this window -> keep previous value } static func parseCoreMediaConsoleLogs(_ entries: [SimpleConsole]) -> [CMPlayerStats] {