mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +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;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = DynamicIsland/Info.plist;
|
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_NSAppleEventsUsageDescription = "Dynamic Island uses AppleScripts to control Spotify and Apple Music";
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch";
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
||||||
|
|
@ -412,7 +414,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "1.0.1-beta";
|
MARKETING_VERSION = "1.0.2-beta";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
|
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
@ -449,6 +451,8 @@
|
||||||
);
|
);
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = DynamicIsland/Info.plist;
|
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_NSAppleEventsUsageDescription = "Dynamic Island uses AppleScripts to control Spotify and Apple Music";
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Show calender events inside the notch";
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
||||||
|
|
@ -460,7 +464,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "1.0.1-beta";
|
MARKETING_VERSION = "1.0.2-beta";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
|
PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ final class SystemVolumeController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func adjust(by delta: Float) {
|
func adjust(by delta: Float) {
|
||||||
|
guard delta != 0 else { return }
|
||||||
|
if isMuted {
|
||||||
|
setMuted(false)
|
||||||
|
}
|
||||||
var newValue = currentVolume + delta
|
var newValue = currentVolume + delta
|
||||||
newValue = max(0, min(1, newValue))
|
newValue = max(0, min(1, newValue))
|
||||||
setVolume(newValue)
|
setVolume(newValue)
|
||||||
|
|
@ -64,19 +68,54 @@ final class SystemVolumeController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setVolume(_ value: Float) {
|
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)
|
let status = setData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
|
||||||
if status != noErr {
|
if status != noErr {
|
||||||
NSLog("⚠️ Failed to set volume: \(status)")
|
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) {
|
func setMuted(_ muted: Bool) {
|
||||||
var muteFlag: UInt32 = muted ? 1 : 0
|
var muteFlag: UInt32 = muted ? 1 : 0
|
||||||
|
let elements = muteElements()
|
||||||
|
|
||||||
|
if elements.isEmpty {
|
||||||
let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag)
|
let status = setData(selector: kAudioDevicePropertyMute, data: &muteFlag)
|
||||||
if status != noErr {
|
if status != noErr {
|
||||||
NSLog("⚠️ Failed to set mute state: \(status)")
|
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
|
// MARK: - Private
|
||||||
|
|
@ -159,6 +198,9 @@ final class SystemVolumeController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getVolume() -> Float {
|
private func getVolume() -> Float {
|
||||||
|
let elements = volumeElements()
|
||||||
|
|
||||||
|
if elements.isEmpty {
|
||||||
var volume = Float32(0)
|
var volume = Float32(0)
|
||||||
let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
|
let status = getData(selector: kAudioDevicePropertyVolumeScalar, data: &volume)
|
||||||
if status != noErr {
|
if status != noErr {
|
||||||
|
|
@ -167,16 +209,76 @@ final class SystemVolumeController {
|
||||||
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 {
|
private func getMuteState() -> Bool {
|
||||||
|
let elements = muteElements()
|
||||||
|
|
||||||
|
if elements.isEmpty {
|
||||||
var mute: UInt32 = 0
|
var mute: UInt32 = 0
|
||||||
let status = getData(selector: kAudioDevicePropertyMute, data: &mute)
|
let status = getData(selector: kAudioDevicePropertyMute, data: &mute)
|
||||||
if status != noErr {
|
if status != noErr {
|
||||||
// Some devices do not support mute; treat as not muted
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return mute != 0
|
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() {
|
private func refreshPropertyElements() {
|
||||||
volumeElement = resolveElement(selector: kAudioDevicePropertyVolumeScalar, deviceID: currentDeviceID)
|
volumeElement = resolveElement(selector: kAudioDevicePropertyVolumeScalar, deviceID: currentDeviceID)
|
||||||
muteElement = resolveElement(selector: kAudioDevicePropertyMute, deviceID: currentDeviceID)
|
muteElement = resolveElement(selector: kAudioDevicePropertyMute, deviceID: currentDeviceID)
|
||||||
|
|
@ -264,6 +366,38 @@ final class SystemVolumeController {
|
||||||
}
|
}
|
||||||
return lastStatus
|
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 {
|
final class SystemBrightnessController {
|
||||||
|
|
@ -275,9 +409,17 @@ final class SystemBrightnessController {
|
||||||
private var observers: [NSObjectProtocol] = []
|
private var observers: [NSObjectProtocol] = []
|
||||||
private var notificationsInstalled = false
|
private var notificationsInstalled = false
|
||||||
private var displayID: CGDirectDisplayID = CGMainDisplayID()
|
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() {
|
private init() {
|
||||||
registerExternalNotifications()
|
registerExternalNotifications()
|
||||||
|
lastEmittedBrightness = currentBrightness
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|
@ -286,6 +428,8 @@ final class SystemBrightnessController {
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
onBrightnessChange = nil
|
onBrightnessChange = nil
|
||||||
|
brightnessAnimationTimer?.invalidate()
|
||||||
|
brightnessAnimationTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func adjust(by delta: Float) {
|
func adjust(by delta: Float) {
|
||||||
|
|
@ -294,17 +438,8 @@ final class SystemBrightnessController {
|
||||||
|
|
||||||
func setBrightness(_ value: Float) {
|
func setBrightness(_ value: Float) {
|
||||||
let clamped = max(0, min(1, value))
|
let clamped = max(0, min(1, value))
|
||||||
if setBrightnessViaDisplayServices(clamped) {
|
DispatchQueue.main.async {
|
||||||
notifyCurrentBrightness()
|
self.beginBrightnessAnimation(to: clamped)
|
||||||
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)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,10 +459,77 @@ final class SystemBrightnessController {
|
||||||
|
|
||||||
private func notifyCurrentBrightness() {
|
private func notifyCurrentBrightness() {
|
||||||
let brightness = currentBrightness
|
let brightness = currentBrightness
|
||||||
DispatchQueue.main.async {
|
emitBrightnessChange(value: brightness)
|
||||||
self.onBrightnessChange?(brightness)
|
|
||||||
self.notificationCenter.post(name: .systemBrightnessDidChange, object: nil, userInfo: ["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? {
|
private func displayService() -> io_service_t? {
|
||||||
|
|
@ -386,6 +588,7 @@ final class SystemBrightnessController {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
brightnessAnimationTimer?.invalidate()
|
||||||
observers.forEach { DistributedNotificationCenter.default().removeObserver($0) }
|
observers.forEach { DistributedNotificationCenter.default().removeObserver($0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue