Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Quality/CMPlayerStuff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +126 to +146

static func parseCoreMediaConsoleLogs(_ entries: [SimpleConsole]) -> [CMPlayerStats] {
let kTimeDifferenceAcceptance = 5.0 // seconds
var lastDate: Date?
Expand Down
8 changes: 7 additions & 1 deletion Quality/MenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Comment on lines +17 to +21

ContentView()

Divider()

Button {
Expand Down
7 changes: 7 additions & 0 deletions Quality/OutputDevices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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 }
}
Comment on lines +149 to +151

// allStats.sort(by: {$0.priority > $1.priority})
print("[getAllStats] \(allStats)")
}
Expand Down