mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
BugFix: Jitter brightness and volume mute/unmute
This commit is contained in:
parent
16def3befc
commit
78ee069e18
2 changed files with 238 additions and 31 deletions
|
|
@ -401,6 +401,8 @@
|
|||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = DynamicIsland/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Atoll;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "Dynamic Island uses AppleScripts to control Spotify and Apple Music";
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
||||
|
|
@ -412,7 +414,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "1.0.1-beta";
|
||||
MARKETING_VERSION = "1.0.2-beta";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -449,6 +451,8 @@
|
|||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = DynamicIsland/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Atoll;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "Dynamic Island uses AppleScripts to control Spotify and Apple Music";
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
||||
|
|
@ -460,7 +464,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "1.0.1-beta";
|
||||
MARKETING_VERSION = "1.0.2-beta";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -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,19 +68,54 @@ final class SystemVolumeController {
|
|||
}
|
||||
|
||||
func setVolume(_ value: Float) {
|
||||
var volume = max(0, min(1, value))
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
|
@ -159,6 +198,9 @@ final class SystemVolumeController {
|
|||
}
|
||||
|
||||
private func getVolume() -> Float {
|
||||
let elements = volumeElements()
|
||||
|
||||
if elements.isEmpty {
|
||||
var volume = Float32(0)
|
||||
let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
|
||||
if status != noErr {
|
||||
|
|
@ -167,16 +209,76 @@ final class SystemVolumeController {
|
|||
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 {
|
||||
let elements = muteElements()
|
||||
|
||||
if elements.isEmpty {
|
||||
var mute: UInt32 = 0
|
||||
let status = getData(selector: kAudioDevicePropertyMute, data: &mute)
|
||||
if status != noErr {
|
||||
// Some devices do not support mute; treat as not muted
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
return fallback != 0
|
||||
}
|
||||
|
||||
private func refreshPropertyElements() {
|
||||
volumeElement = resolveElement(selector: kAudioDevicePropertyVolumeScalar, deviceID: currentDeviceID)
|
||||
muteElement = resolveElement(selector: kAudioDevicePropertyMute, deviceID: currentDeviceID)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue