BugFix: Jitter brightness and volume mute/unmute

This commit is contained in:
Hariharan Mudaliar 2025-10-26 11:54:15 +05:30
commit 78ee069e18
2 changed files with 238 additions and 31 deletions

View file

@ -46,6 +46,10 @@ final class SystemVolumeController {
}
func adjust(by delta: Float) {
guard delta != 0 else { return }
if isMuted {
setMuted(false)
}
var newValue = currentVolume + delta
newValue = max(0, min(1, newValue))
setVolume(newValue)
@ -64,18 +68,53 @@ final class SystemVolumeController {
}
func setVolume(_ value: Float) {
var volume = max(0, min(1, value))
let status = setData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
if status != noErr {
NSLog("⚠️ Failed to set volume: \(status)")
let clamped = max(0, min(1, value))
if isMuted && clamped > 0 {
setMuted(false)
}
let elements = volumeElements()
if elements.isEmpty {
var volume = clamped
let status = setData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
if status != noErr {
NSLog("⚠️ Failed to set volume: \(status)")
}
} else {
for element in elements {
var volume = clamped
let status = setData(selector: kAudioDevicePropertyVolumeScalar, element: element, data: &volume)
if status != noErr {
NSLog("⚠️ Failed to set volume for element \(element): \(status)")
} else {
cache(element: element, for: kAudioDevicePropertyVolumeScalar)
}
}
}
notifyCurrentState()
}
func setMuted(_ muted: Bool) {
var muteFlag: UInt32 = muted ? 1 : 0
let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag)
if status != noErr {
NSLog("⚠️ Failed to set mute state: \(status)")
let elements = muteElements()
if elements.isEmpty {
let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag)
if status != noErr {
NSLog("⚠️ Failed to set mute state: \(status)")
}
return
}
for element in elements {
var value = muteFlag
let status = setData(selector: kAudioDevicePropertyMute, element: element, data: &value)
if status != noErr {
NSLog("⚠️ Failed to set mute state for element \(element): \(status)")
} else {
cache(element: element, for: kAudioDevicePropertyMute)
}
}
}
@ -159,22 +198,85 @@ final class SystemVolumeController {
}
private func getVolume() -> Float {
var volume = Float32(0)
let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
if status != noErr {
NSLog("⚠️ Unable to fetch volume: \(status)")
let elements = volumeElements()
if elements.isEmpty {
var volume = Float32(0)
let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
if status != noErr {
NSLog("⚠️ Unable to fetch volume: \(status)")
}
return volume
}
return volume
var masterVolume: Float?
var accumulator: Float = 0
var count: Float = 0
for element in elements {
var value = Float32(0)
let status = getData(selector: kAudioDevicePropertyVolumeScalar, element: element, data: &value)
if status == noErr {
if element == kAudioObjectPropertyElementMaster {
masterVolume = value
}
accumulator += value
count += 1
}
}
if let masterVolume {
return masterVolume
}
if count > 0 {
return accumulator / count
}
var fallback = Float32(0)
let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &fallback)
if status != noErr {
NSLog("⚠️ Unable to fetch fallback volume: \(status)")
}
return fallback
}
private func getMuteState() -> Bool {
var mute: UInt32 = 0
let status = getData(selector: kAudioDevicePropertyMute, data: &mute)
let elements = muteElements()
if elements.isEmpty {
var mute: UInt32 = 0
let status = getData(selector: kAudioDevicePropertyMute, data: &mute)
if status != noErr {
return false
}
return mute != 0
}
var retrieved = false
var allMuted = true
for element in elements {
var value: UInt32 = 0
let status = getData(selector: kAudioDevicePropertyMute, element: element, data: &value)
if status == noErr {
retrieved = true
if value == 0 {
allMuted = false
}
}
}
if retrieved {
return allMuted
}
var fallback: UInt32 = 0
let status = getData(selector: kAudioDevicePropertyMute, data: &fallback)
if status != noErr {
// Some devices do not support mute; treat as not muted
return false
}
return mute != 0
return fallback != 0
}
private func refreshPropertyElements() {
@ -264,6 +366,38 @@ final class SystemVolumeController {
}
return lastStatus
}
private func getData<T>(selector: AudioObjectPropertySelector, element: AudioObjectPropertyElement, data: inout T) -> OSStatus {
var address = makeAddress(selector: selector, element: element)
guard propertyExists(deviceID: currentDeviceID, address: &address) else {
return kAudioHardwareUnknownPropertyError
}
var size = UInt32(MemoryLayout<T>.size)
return AudioObjectGetPropertyData(currentDeviceID, &address, 0, nil, &size, &data)
}
private func setData<T>(selector: AudioObjectPropertySelector, element: AudioObjectPropertyElement, data: inout T) -> OSStatus {
var address = makeAddress(selector: selector, element: element)
guard propertyExists(deviceID: currentDeviceID, address: &address) else {
return kAudioHardwareUnknownPropertyError
}
let size = UInt32(MemoryLayout<T>.size)
return AudioObjectSetPropertyData(currentDeviceID, &address, 0, nil, size, &data)
}
private func volumeElements() -> [AudioObjectPropertyElement] {
candidateElements.filter { element in
var address = makeAddress(selector: kAudioDevicePropertyVolumeScalar, element: element)
return propertyExists(deviceID: currentDeviceID, address: &address)
}
}
private func muteElements() -> [AudioObjectPropertyElement] {
candidateElements.filter { element in
var address = makeAddress(selector: kAudioDevicePropertyMute, element: element)
return propertyExists(deviceID: currentDeviceID, address: &address)
}
}
}
final class SystemBrightnessController {
@ -275,9 +409,17 @@ final class SystemBrightnessController {
private var observers: [NSObjectProtocol] = []
private var notificationsInstalled = false
private var displayID: CGDirectDisplayID = CGMainDisplayID()
private var brightnessAnimationTimer: Timer?
private var brightnessAnimationStart: Float = 0
private var brightnessAnimationTarget: Float = 0
private var brightnessAnimationStartDate: Date?
private let brightnessAnimationDuration: TimeInterval = 0.18
private let brightnessAnimationSteps = 10
private var lastEmittedBrightness: Float = 0.5
private init() {
registerExternalNotifications()
lastEmittedBrightness = currentBrightness
}
func start() {
@ -286,6 +428,8 @@ final class SystemBrightnessController {
func stop() {
onBrightnessChange = nil
brightnessAnimationTimer?.invalidate()
brightnessAnimationTimer = nil
}
func adjust(by delta: Float) {
@ -294,17 +438,8 @@ final class SystemBrightnessController {
func setBrightness(_ value: Float) {
let clamped = max(0, min(1, value))
if setBrightnessViaDisplayServices(clamped) {
notifyCurrentBrightness()
return
}
guard let service = displayService() else { return }
let status = IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, clamped)
IOObjectRelease(service)
if status == kIOReturnSuccess {
notifyCurrentBrightness()
} else {
NSLog("⚠️ Failed to set brightness via IODisplay: \(status)")
DispatchQueue.main.async {
self.beginBrightnessAnimation(to: clamped)
}
}
@ -324,10 +459,77 @@ final class SystemBrightnessController {
private func notifyCurrentBrightness() {
let brightness = currentBrightness
DispatchQueue.main.async {
self.onBrightnessChange?(brightness)
self.notificationCenter.post(name: .systemBrightnessDidChange, object: nil, userInfo: ["value": brightness])
emitBrightnessChange(value: brightness)
}
private func beginBrightnessAnimation(to target: Float) {
brightnessAnimationTimer?.invalidate()
let start = lastEmittedBrightness
if abs(start - target) <= 0.0005 {
applyBrightness(target)
emitBrightnessChange(value: target)
return
}
brightnessAnimationStart = start
brightnessAnimationTarget = target
brightnessAnimationStartDate = Date()
let interval = brightnessAnimationDuration / Double(brightnessAnimationSteps)
let timer = Timer(timeInterval: interval, repeats: true) { [weak self] timer in
guard let self else { return }
guard let startDate = self.brightnessAnimationStartDate else {
timer.invalidate()
self.brightnessAnimationTimer = nil
return
}
let elapsed = Date().timeIntervalSince(startDate)
let progress = min(elapsed / self.brightnessAnimationDuration, 1)
let eased = self.ease(progress)
let value = self.brightnessAnimationStart + (self.brightnessAnimationTarget - self.brightnessAnimationStart) * Float(eased)
self.applyBrightness(value)
self.emitBrightnessChange(value: value)
if progress >= 1 {
timer.invalidate()
self.brightnessAnimationTimer = nil
}
}
brightnessAnimationTimer = timer
RunLoop.main.add(timer, forMode: .common)
timer.fire()
}
private func applyBrightness(_ value: Float) {
let clamped = max(0, min(1, value))
if setBrightnessViaDisplayServices(clamped) {
return
}
guard let service = displayService() else { return }
let status = IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, clamped)
IOObjectRelease(service)
if status != kIOReturnSuccess {
NSLog("⚠️ Failed to set brightness via IODisplay: \(status)")
}
}
private func emitBrightnessChange(value: Float) {
let clamped = max(0, min(1, value))
lastEmittedBrightness = clamped
let dispatchBlock = {
self.onBrightnessChange?(clamped)
self.notificationCenter.post(name: .systemBrightnessDidChange, object: nil, userInfo: ["value": clamped])
}
if Thread.isMainThread {
dispatchBlock()
} else {
DispatchQueue.main.async(execute: dispatchBlock)
}
}
private func ease(_ t: Double) -> Double {
let clamped = min(max(t, 0), 1)
return 1 - pow(1 - clamped, 3)
}
private func displayService() -> io_service_t? {
@ -386,6 +588,7 @@ final class SystemBrightnessController {
}
deinit {
brightnessAnimationTimer?.invalidate()
observers.forEach { DistributedNotificationCenter.default().removeObserver($0) }
}
}