mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Fix: Polling speed for volume and brightness
This commit is contained in:
parent
194d6b3d0e
commit
494f2d57cc
1 changed files with 226 additions and 23 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Foundation
|
import Foundation
|
||||||
import Defaults
|
import Defaults
|
||||||
|
import CoreAudio
|
||||||
|
|
||||||
class SystemChangesObserver {
|
class SystemChangesObserver {
|
||||||
private var oldVolume: Float = 0
|
private var oldVolume: Float = 0
|
||||||
|
|
@ -17,8 +18,22 @@ class SystemChangesObserver {
|
||||||
|
|
||||||
private weak var coordinator: DynamicIslandViewCoordinator?
|
private weak var coordinator: DynamicIslandViewCoordinator?
|
||||||
|
|
||||||
// Backup timer for edge cases only
|
// Adaptive polling timer
|
||||||
private var backupTimer: Timer?
|
private var pollingTimer: Timer?
|
||||||
|
private var currentPollingInterval: TimeInterval = 2.0
|
||||||
|
|
||||||
|
// Polling intervals
|
||||||
|
private let rapidPollingInterval: TimeInterval = 0.05 // Burst mode for 5 seconds after key press
|
||||||
|
private let fastPollingInterval: TimeInterval = 0.3 // Fast mode when headphones connected
|
||||||
|
private let normalPollingInterval: TimeInterval = 2.0 // Normal mode when idle
|
||||||
|
|
||||||
|
// Activity tracking
|
||||||
|
private var lastBrightnessChangeTime: Date = Date.distantPast
|
||||||
|
private var lastVolumeChangeTime: Date = Date.distantPast
|
||||||
|
private var lastVolumeCheckTime: Date = Date.distantPast
|
||||||
|
|
||||||
|
// Headphone detection
|
||||||
|
private var hasHeadphonesConnected: Bool = false
|
||||||
|
|
||||||
init(coordinator: DynamicIslandViewCoordinator) {
|
init(coordinator: DynamicIslandViewCoordinator) {
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
|
|
@ -32,38 +47,170 @@ class SystemChangesObserver {
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("Failed to retrieve initial display brightness: \(error)")
|
NSLog("Failed to retrieve initial display brightness: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect headphones on init
|
||||||
|
updateHeadphoneStatus()
|
||||||
|
setupAudioRouteChangeListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startObserving() {
|
func startObserving() {
|
||||||
guard Defaults[.enableSystemHUD] else { return }
|
guard Defaults[.enableSystemHUD] else { return }
|
||||||
|
|
||||||
startMinimalBackupTimer()
|
// Start with appropriate polling rate
|
||||||
|
startPollingTimer(interval: determinePollingInterval())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startMinimalBackupTimer() {
|
private func determinePollingInterval() -> TimeInterval {
|
||||||
// More frequent backup check (every 300ms) for better responsiveness
|
let now = Date()
|
||||||
// This is a reasonable compromise between the original 200ms and the previous 5s
|
|
||||||
backupTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
// Burst mode: Rapid polling within 5 seconds of any key press
|
||||||
guard Defaults[.enableSystemHUD] else { return }
|
if now.timeIntervalSince(lastVolumeChangeTime) < 5.0 ||
|
||||||
self?.performBackupCheck()
|
now.timeIntervalSince(lastBrightnessChangeTime) < 5.0 {
|
||||||
|
return rapidPollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast mode: When headphones are connected
|
||||||
|
if hasHeadphonesConnected {
|
||||||
|
return fastPollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal mode: Idle state
|
||||||
|
return normalPollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startPollingTimer(interval: TimeInterval) {
|
||||||
|
// Don't restart if same interval
|
||||||
|
if pollingTimer != nil && currentPollingInterval == interval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPollingInterval = interval
|
||||||
|
pollingTimer?.invalidate()
|
||||||
|
|
||||||
|
pollingTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
||||||
|
guard let self = self, Defaults[.enableSystemHUD] else { return }
|
||||||
|
|
||||||
|
// Check if we need to adjust polling rate
|
||||||
|
let newInterval = self.determinePollingInterval()
|
||||||
|
if newInterval != self.currentPollingInterval {
|
||||||
|
self.startPollingTimer(interval: newInterval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll volume
|
||||||
|
self.checkVolumeChanges()
|
||||||
|
|
||||||
|
// Poll brightness only within 5 seconds of key press
|
||||||
|
if Date().timeIntervalSince(self.lastBrightnessChangeTime) < 5.0 {
|
||||||
|
self.checkBrightnessChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performBackupCheck() {
|
let mode = interval == rapidPollingInterval ? "BURST (50ms)" :
|
||||||
// Only check if we haven't had recent activity
|
interval == fastPollingInterval ? "FAST (300ms)" : "NORMAL (2s)"
|
||||||
checkVolumeChanges()
|
print("✅ Polling started: \(mode) mode")
|
||||||
checkBrightnessChanges()
|
}
|
||||||
|
|
||||||
|
// MARK: - Headphone Detection
|
||||||
|
|
||||||
|
private func setupAudioRouteChangeListener() {
|
||||||
|
// Listen for default audio device changes
|
||||||
|
var propertyAddress = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||||
|
mScope: kAudioObjectPropertyScopeGlobal,
|
||||||
|
mElement: kAudioObjectPropertyElementMain
|
||||||
|
)
|
||||||
|
|
||||||
|
AudioObjectAddPropertyListener(
|
||||||
|
AudioObjectID(kAudioObjectSystemObject),
|
||||||
|
&propertyAddress,
|
||||||
|
audioDeviceChangeListener,
|
||||||
|
Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc fileprivate func audioRouteChanged() {
|
||||||
|
updateHeadphoneStatus()
|
||||||
|
|
||||||
|
// Adjust polling rate immediately
|
||||||
|
startPollingTimer(interval: determinePollingInterval())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHeadphoneStatus() {
|
||||||
|
// Check for headphones/external audio devices
|
||||||
|
var propertyAddress = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||||
|
mScope: kAudioObjectPropertyScopeGlobal,
|
||||||
|
mElement: kAudioObjectPropertyElementMain
|
||||||
|
)
|
||||||
|
|
||||||
|
var deviceID: AudioDeviceID = 0
|
||||||
|
var propertySize = UInt32(MemoryLayout<AudioDeviceID>.size)
|
||||||
|
|
||||||
|
let status = AudioObjectGetPropertyData(
|
||||||
|
AudioObjectID(kAudioObjectSystemObject),
|
||||||
|
&propertyAddress,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
&propertySize,
|
||||||
|
&deviceID
|
||||||
|
)
|
||||||
|
|
||||||
|
guard status == noErr else {
|
||||||
|
hasHeadphonesConnected = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if device is not built-in speakers
|
||||||
|
var transportType: UInt32 = 0
|
||||||
|
var transportSize = UInt32(MemoryLayout<UInt32>.size)
|
||||||
|
var transportAddress = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioDevicePropertyTransportType,
|
||||||
|
mScope: kAudioObjectPropertyScopeGlobal,
|
||||||
|
mElement: kAudioObjectPropertyElementMain
|
||||||
|
)
|
||||||
|
|
||||||
|
let transportStatus = AudioObjectGetPropertyData(
|
||||||
|
deviceID,
|
||||||
|
&transportAddress,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
&transportSize,
|
||||||
|
&transportType
|
||||||
|
)
|
||||||
|
|
||||||
|
// Consider headphones if: Bluetooth, USB, or any non-built-in device
|
||||||
|
let wasConnected = hasHeadphonesConnected
|
||||||
|
hasHeadphonesConnected = transportStatus == noErr &&
|
||||||
|
transportType != kAudioDeviceTransportTypeBuiltIn
|
||||||
|
|
||||||
|
if wasConnected != hasHeadphonesConnected {
|
||||||
|
print(hasHeadphonesConnected ? "🎧 Headphones connected - switching to FAST polling" :
|
||||||
|
"🔊 Headphones disconnected - switching to NORMAL polling")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public Media Key Event Handlers (called from MediaKeyApplication)
|
// MARK: - Public Media Key Event Handlers (called from MediaKeyApplication)
|
||||||
|
|
||||||
func handleVolumeKeyEvent() {
|
func handleVolumeKeyEvent() {
|
||||||
|
// Mark as user-initiated and trigger burst mode
|
||||||
|
lastVolumeChangeTime = Date()
|
||||||
|
|
||||||
|
// Switch to burst mode immediately
|
||||||
|
startPollingTimer(interval: rapidPollingInterval)
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
self.handleVolumeKeyPress()
|
self.handleVolumeKeyPress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBrightnessKeyEvent() {
|
func handleBrightnessKeyEvent() {
|
||||||
|
// Mark as user-initiated and trigger burst mode
|
||||||
|
lastBrightnessChangeTime = Date()
|
||||||
|
|
||||||
|
// Switch to burst mode immediately
|
||||||
|
startPollingTimer(interval: rapidPollingInterval)
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
self.handleBrightnessKeyPress()
|
self.handleBrightnessKeyPress()
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +221,9 @@ class SystemChangesObserver {
|
||||||
private func handleVolumeKeyPress() {
|
private func handleVolumeKeyPress() {
|
||||||
guard Defaults[.enableVolumeHUD] else { return }
|
guard Defaults[.enableVolumeHUD] else { return }
|
||||||
|
|
||||||
|
// Mark as user-initiated (don't debounce key presses)
|
||||||
|
lastVolumeChangeTime = Date()
|
||||||
|
|
||||||
let newVolume = SystemVolumeManager.getOutputVolume()
|
let newVolume = SystemVolumeManager.getOutputVolume()
|
||||||
let newMuted = SystemVolumeManager.isMuted()
|
let newMuted = SystemVolumeManager.isMuted()
|
||||||
|
|
||||||
|
|
@ -99,7 +249,7 @@ class SystemChangesObserver {
|
||||||
let newBrightness = try SystemDisplayManager.getDisplayBrightness()
|
let newBrightness = try SystemDisplayManager.getDisplayBrightness()
|
||||||
|
|
||||||
// Always show HUD for key press feedback, even if value doesn't change
|
// Always show HUD for key press feedback, even if value doesn't change
|
||||||
print("☀️ Brightness key pressed - showing HUD: brightness=\(newBrightness)")
|
print("☀️ Brightness key pressed - showing HUD: brightness=\(newBrightness) [BURST MODE]")
|
||||||
sendBrightnessNotification(value: newBrightness)
|
sendBrightnessNotification(value: newBrightness)
|
||||||
|
|
||||||
// Update stored value
|
// Update stored value
|
||||||
|
|
@ -109,9 +259,20 @@ class SystemChangesObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Polling Check Methods
|
||||||
|
|
||||||
private func checkVolumeChanges() {
|
private func checkVolumeChanges() {
|
||||||
guard Defaults[.enableVolumeHUD] else { return }
|
guard Defaults[.enableVolumeHUD] else { return }
|
||||||
|
|
||||||
|
// Throttle AppleScript checks based on current mode
|
||||||
|
let now = Date()
|
||||||
|
let minInterval: TimeInterval = currentPollingInterval == rapidPollingInterval ? 0.05 : 0.2
|
||||||
|
|
||||||
|
if now.timeIntervalSince(lastVolumeCheckTime) < minInterval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastVolumeCheckTime = now
|
||||||
|
|
||||||
let newVolume = SystemVolumeManager.getOutputVolume()
|
let newVolume = SystemVolumeManager.getOutputVolume()
|
||||||
let newMuted = SystemVolumeManager.isMuted()
|
let newMuted = SystemVolumeManager.isMuted()
|
||||||
|
|
||||||
|
|
@ -119,13 +280,21 @@ class SystemChangesObserver {
|
||||||
let muteChanged = newMuted != oldMuted
|
let muteChanged = newMuted != oldMuted
|
||||||
|
|
||||||
if volumeChanged || muteChanged {
|
if volumeChanged || muteChanged {
|
||||||
print("🔊 Volume change detected: \(oldVolume) → \(newVolume), muted: \(oldMuted) → \(newMuted)")
|
// Don't show HUD if change was just triggered by key press (within 0.1s in burst mode, 0.3s otherwise)
|
||||||
|
let debounceWindow = currentPollingInterval == rapidPollingInterval ? 0.1 : 0.3
|
||||||
|
let timeSinceKeyPress = now.timeIntervalSince(lastVolumeChangeTime)
|
||||||
|
|
||||||
|
if timeSinceKeyPress > debounceWindow {
|
||||||
|
let mode = currentPollingInterval == rapidPollingInterval ? "[BURST]" : "[EXTERNAL]"
|
||||||
|
print("🔊 Volume change detected \(mode): \(oldVolume) → \(newVolume), muted: \(oldMuted) → \(newMuted)")
|
||||||
|
|
||||||
if newMuted {
|
if newMuted {
|
||||||
sendVolumeNotification(value: 0.0)
|
sendVolumeNotification(value: 0.0)
|
||||||
} else {
|
} else {
|
||||||
sendVolumeNotification(value: newVolume)
|
sendVolumeNotification(value: newVolume)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
oldVolume = newVolume
|
oldVolume = newVolume
|
||||||
oldMuted = newMuted
|
oldMuted = newMuted
|
||||||
}
|
}
|
||||||
|
|
@ -178,16 +347,50 @@ class SystemChangesObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
// Remove audio device change listener
|
||||||
|
var propertyAddress = AudioObjectPropertyAddress(
|
||||||
|
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||||
|
mScope: kAudioObjectPropertyScopeGlobal,
|
||||||
|
mElement: kAudioObjectPropertyElementMain
|
||||||
|
)
|
||||||
|
|
||||||
|
AudioObjectRemovePropertyListener(
|
||||||
|
AudioObjectID(kAudioObjectSystemObject),
|
||||||
|
&propertyAddress,
|
||||||
|
audioDeviceChangeListener,
|
||||||
|
Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
)
|
||||||
|
|
||||||
stopObserving()
|
stopObserving()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopObserving() {
|
func stopObserving() {
|
||||||
// Clean up backup timer
|
// Clean up polling timer
|
||||||
backupTimer?.invalidate()
|
pollingTimer?.invalidate()
|
||||||
backupTimer = nil
|
pollingTimer = nil
|
||||||
|
|
||||||
print("🎹 Media key detection stopped")
|
print("🎹 Media key detection stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - CoreAudio Callback for Device Changes
|
||||||
|
|
||||||
|
private func audioDeviceChangeListener(
|
||||||
|
_ inObjectID: AudioObjectID,
|
||||||
|
_ inNumberAddresses: UInt32,
|
||||||
|
_ inAddresses: UnsafePointer<AudioObjectPropertyAddress>,
|
||||||
|
_ inClientData: UnsafeMutableRawPointer?
|
||||||
|
) -> OSStatus {
|
||||||
|
guard let inClientData = inClientData else { return noErr }
|
||||||
|
|
||||||
|
let observer = Unmanaged<SystemChangesObserver>.fromOpaque(inClientData).takeUnretainedValue()
|
||||||
|
|
||||||
|
// Handle on main thread
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
observer.audioRouteChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
return noErr
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue