Update Volume listener for ddc controls

This commit is contained in:
Hariharan Mudaliar 2026-03-15 12:45:10 +05:30
commit 68cb3fd2de
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
6 changed files with 74 additions and 18 deletions

View file

@ -4091,6 +4091,10 @@
},
"shouldTranslate" : false
},
"Atoll always listens to selected-provider brightness events, and listens to provider volume events only when external volume listener is enabled." : {
"comment" : "A footer text that explains the behavior of the third-party DDC integration settings.",
"isCommentAutoGenerated" : true
},
"Atoll can show all your upcoming events in one place. Access to your calendar is needed to display your schedule." : {
"localizations" : {
"pt-BR" : {
@ -4111,8 +4115,8 @@
}
}
},
"Atoll listens to the selected provider and renders its brightness/volume HUD updates using your currently active HUD style above." : {
"comment" : "A footer text that explains the functionality of the \"Enable third-party DDC app integration\" toggle.",
"Atoll keeps native volume key interception. External provider volume payloads are ignored while this is off." : {
"comment" : "A description of the behavior of Atoll's volume key interception when the external volume control listener is disabled.",
"isCommentAutoGenerated" : true
},
"Atoll needs Full Disk Access to read the system notification database. If Atoll does not appear in the list, use 'Reveal App in Finder' and drag it into the settings window." : {
@ -4126,6 +4130,10 @@
}
}
},
"Atoll's built-in volume key interception is disabled while external volume listening is on. Volume HUD/OSD will follow %@ payloads." : {
"comment" : "A description below the toggle that explains the effect of enabling or disabling the feature.",
"isCommentAutoGenerated" : true
},
"Attached Files" : {
"localizations" : {
"fr" : {
@ -10140,6 +10148,10 @@
}
}
},
"Enable external volume control listener" : {
"comment" : "A toggle that enables external volume control listener.",
"isCommentAutoGenerated" : true
},
"Enable global keyboard shortcuts above to customize your shortcuts." : {
"localizations" : {
"pt-BR" : {

View file

@ -741,6 +741,7 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .hudAndOSD, title: "Icon & Progress Color", keywords: ["color", "icon", "white", "black", "gray", "osd"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Icon & Progress Color")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC app integration", keywords: ["ddc", "third party", "external", "display", "betterdisplay", "lunar"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC app integration")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Third-party DDC provider", keywords: ["provider", "betterdisplay", "lunar", "integration", "refresh detection"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Third-party DDC provider")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Enable external volume control listener", keywords: ["external volume", "ddc volume", "betterdisplay volume", "lunar volume", "disable native volume"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Enable external volume control listener")),
// Media
SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"], highlightID: SettingsTab.media.highlightID(for: "Music Source")),
@ -1931,6 +1932,7 @@ private struct HUDAndOSDSettingsView: View {
private struct ExternalDisplayIntegrationsSection: View {
@Default(.enableThirdPartyDDCIntegration) var enableThirdPartyDDCIntegration
@Default(.thirdPartyDDCProvider) var thirdPartyDDCProvider
@Default(.enableExternalVolumeControlListener) var enableExternalVolumeControlListener
@ObservedObject private var betterDisplayManager = BetterDisplayManager.shared
@ObservedObject private var lunarManager = LunarManager.shared
@ -1970,21 +1972,21 @@ private struct ExternalDisplayIntegrationsSection: View {
switch thirdPartyDDCProvider {
case .betterDisplay:
if !betterDisplayManager.isDetected {
return "Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD."
return "Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness (and optional volume) through Atoll's HUD."
}
if !betterDisplayManager.isRunning {
return "BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration."
}
return "BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in Settings › Application › Integration."
return "BetterDisplay OSD events will be routed through Atoll's active HUD style. Brightness is always routed; volume is routed when external volume control listener is enabled below. Make sure BetterDisplay's OSD integration is enabled in Settings › Application › Integration."
case .lunar:
if !lunarManager.isDetected {
return "Install [Lunar](https://lunar.fyi) to control external display brightness, contrast, and volume through Atoll's HUD via DDC."
return "Install [Lunar](https://lunar.fyi) to control external display brightness, contrast, and optional volume through Atoll's HUD via DDC."
}
if !lunarManager.isRunning {
return "Lunar is installed but not currently running. Launch Lunar to enable integration."
}
if lunarManager.isConnected {
return "Connected to Lunar's DDC socket. Brightness, contrast, and volume adjustments will be shown through Atoll's HUD."
return "Connected to Lunar's DDC socket. Brightness and contrast adjustments are shown through Atoll's HUD; volume follows when external volume control listener is enabled below."
}
return "Lunar is running but the socket connection is not yet established. It will connect automatically."
}
@ -2013,6 +2015,17 @@ private struct ExternalDisplayIntegrationsSection: View {
}
.settingsHighlight(id: highlightID("Third-party DDC provider"))
Toggle("Enable external volume control listener", isOn: $enableExternalVolumeControlListener)
.settingsHighlight(id: highlightID("Enable external volume control listener"))
Text(
enableExternalVolumeControlListener
? "Atoll's built-in volume key interception is disabled while external volume listening is on. Volume HUD/OSD will follow \(thirdPartyDDCProvider.displayName) payloads."
: "Atoll keeps native volume key interception. External provider volume payloads are ignored while this is off."
)
.font(.caption)
.foregroundStyle(.secondary)
HStack {
Text("Status")
Spacer()
@ -2039,7 +2052,7 @@ private struct ExternalDisplayIntegrationsSection: View {
}
} footer: {
if enableThirdPartyDDCIntegration {
Text("Atoll listens to the selected provider and renders its brightness/volume HUD updates using your currently active HUD style above.")
Text("Atoll always listens to selected-provider brightness events, and listens to provider volume events only when external volume listener is enabled.")
.foregroundStyle(.secondary)
.font(.caption)
}

View file

@ -119,6 +119,10 @@ final class BetterDisplayManager: ObservableObject {
Defaults[.enableThirdPartyDDCIntegration] && Defaults[.thirdPartyDDCProvider] == .betterDisplay
}
private var isExternalVolumeListenerEnabled: Bool {
isBetterDisplayIntegrationEnabled && Defaults[.enableExternalVolumeControlListener]
}
private init() {
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
@ -239,6 +243,7 @@ final class BetterDisplayManager: ObservableObject {
let isExternalDisplay = isExternal(displayID: osd.displayID, resolvedScreen: targetScreen)
let inferredMute = osd.controlTarget == "mute" || osd.systemIconID == 4
let hasVolumeData = category == .volume && osd.value != nil
let externalVolumeListenerEnabled = isExternalVolumeListenerEnabled
let targetText = osd.controlTarget ?? "nil"
let displayIDText = osd.displayID.map { String($0) } ?? "nil"
let resolvedScreenText = targetScreen?.localizedName ?? "nil"
@ -247,7 +252,7 @@ final class BetterDisplayManager: ObservableObject {
let normalizedText = String(format: "%.3f", normalizedValue)
NSLog(
"📺 BetterDisplay routed payload: category=\(categoryName(category)) target=\(targetText) displayID=\(displayIDText) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternalDisplay) rawValue=\(valueText) maxValue=\(maxValueText) normalized=\(normalizedText) hasVolumeData=\(hasVolumeData) inferredMute=\(inferredMute)"
"📺 BetterDisplay routed payload: category=\(categoryName(category)) target=\(targetText) displayID=\(displayIDText) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternalDisplay) rawValue=\(valueText) maxValue=\(maxValueText) normalized=\(normalizedText) hasVolumeData=\(hasVolumeData) inferredMute=\(inferredMute) externalVolumeListener=\(externalVolumeListenerEnabled)"
)
switch category {
@ -256,6 +261,10 @@ final class BetterDisplayManager: ObservableObject {
dispatchBrightnessHUD(value: normalizedValue, customSymbol: icon, onScreen: targetScreen)
case .volume:
guard externalVolumeListenerEnabled else {
NSLog("📺 BetterDisplay volume payload ignored because external volume listener is disabled")
return
}
let isMuted = osd.controlTarget == "mute" || osd.systemIconID == 4
dispatchVolumeHUD(value: normalizedValue, isMuted: isMuted, onScreen: targetScreen)

View file

@ -91,6 +91,10 @@ final class LunarManager: ObservableObject {
Defaults[.enableThirdPartyDDCIntegration] && Defaults[.thirdPartyDDCProvider] == .lunar
}
private var isExternalVolumeListenerEnabled: Bool {
isLunarIntegrationEnabled && Defaults[.enableExternalVolumeControlListener]
}
private init() {
isDetected = Self.checkInstallation()
isRunning = Self.checkRunning()
@ -293,6 +297,7 @@ final class LunarManager: ObservableObject {
let targetScreen = resolveScreen(for: data.display)
let isExternal = isExternalDisplay(data.display, resolvedScreen: targetScreen)
let hasVolumeData = data.volume != nil || data.mute != nil
let externalVolumeListenerEnabled = isExternalVolumeListenerEnabled
let resolvedScreenText = targetScreen?.localizedName ?? "nil"
let brightnessText = data.brightness.map { String($0) } ?? "nil"
let contrastText = data.contrast.map { String($0) } ?? "nil"
@ -300,7 +305,7 @@ final class LunarManager: ObservableObject {
let muteText = data.mute.map { String($0) } ?? "nil"
NSLog(
"🌙 Lunar routed payload: display=\(data.display) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternal) brightness=\(brightnessText) contrast=\(contrastText) volume=\(volumeText) mute=\(muteText) hasVolumeData=\(hasVolumeData)"
"🌙 Lunar routed payload: display=\(data.display) resolvedScreen=\(resolvedScreenText) isExternal=\(isExternal) brightness=\(brightnessText) contrast=\(contrastText) volume=\(volumeText) mute=\(muteText) hasVolumeData=\(hasVolumeData) externalVolumeListener=\(externalVolumeListenerEnabled)"
)
// Nits-only events are informational (no user-facing HUD needed).
@ -321,13 +326,17 @@ final class LunarManager: ObservableObject {
}
// Volume
if let volume = data.volume {
let value = CGFloat(volume)
let isMuted = data.mute ?? false
dispatchVolumeHUD(value: value, isMuted: isMuted, onScreen: targetScreen)
} else if let mute = data.mute {
// Mute toggle without a volume value
dispatchVolumeHUD(value: mute ? 0 : 1, isMuted: mute, onScreen: targetScreen)
if externalVolumeListenerEnabled {
if let volume = data.volume {
let value = CGFloat(volume)
let isMuted = data.mute ?? false
dispatchVolumeHUD(value: value, isMuted: isMuted, onScreen: targetScreen)
} else if let mute = data.mute {
// Mute toggle without a volume value
dispatchVolumeHUD(value: mute ? 0 : 1, isMuted: mute, onScreen: targetScreen)
}
} else if hasVolumeData {
NSLog("🌙 Lunar volume payload ignored because external volume listener is disabled")
}
}

View file

@ -195,6 +195,13 @@ class SystemHUDManager {
await self.startSystemObserver()
}
}.store(in: &cancellables)
Defaults.publisher(.enableExternalVolumeControlListener, options: []).sink { [weak self] _ in
guard let self = self, self.isSetupComplete else { return }
Task { @MainActor in
await self.startSystemObserver()
}
}.store(in: &cancellables)
}
private var cancellables = Set<AnyCancellable>()
@ -227,7 +234,7 @@ class SystemHUDManager {
}
}
/// Resolves the effective control flags, applying BetterDisplay overrides.
/// Resolves the effective control flags, applying third-party DDC overrides.
private func resolvedControlFlags() -> (volume: Bool, brightness: Bool, backlight: Bool) {
var volumeEnabled: Bool
var brightnessEnabled: Bool
@ -252,6 +259,11 @@ class SystemHUDManager {
if Defaults[.enableThirdPartyDDCIntegration] {
brightnessEnabled = false
keyboardBacklightEnabled = false
// Optional: route volume exclusively from the selected external provider.
if Defaults[.enableExternalVolumeControlListener] {
volumeEnabled = false
}
}
return (volumeEnabled, brightnessEnabled, keyboardBacklightEnabled)
@ -280,7 +292,7 @@ class SystemHUDManager {
// Force disable system HUD to ensure no duplicates
SystemOSDManager.disableSystemHUD()
print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), ThirdPartyDDC: \(Defaults[.enableThirdPartyDDCIntegration]), Provider: \(Defaults[.thirdPartyDDCProvider].displayName))")
print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), ThirdPartyDDC: \(Defaults[.enableThirdPartyDDCIntegration]), Provider: \(Defaults[.thirdPartyDDCProvider].displayName), ExternalVolumeListener: \(Defaults[.enableExternalVolumeControlListener]))")
isSystemOperationInProgress = false
}

View file

@ -1048,6 +1048,7 @@ extension Defaults.Keys {
// MARK: Third-Party DDC Integration
static let enableThirdPartyDDCIntegration = Key<Bool>("enableThirdPartyDDCIntegration", default: false)
static let thirdPartyDDCProvider = Key<ThirdPartyDDCProvider>("thirdPartyDDCProvider", default: .betterDisplay)
static let enableExternalVolumeControlListener = Key<Bool>("enableExternalVolumeControlListener", default: false)
static let didMigrateThirdPartyDDCIntegration = Key<Bool>("didMigrateThirdPartyDDCIntegration", default: false)
// Legacy keys retained for migration/backward compatibility