mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-15 14:15:55 -06:00
- Fixed not working after sleep mode for some on Apple Silicon Not working after sleep mode #530 - Fixed some LG and Samsung displays having problems with Mute (improved 'Enable Mute DDC command') - LG Monitor: have to unmute manually after muting #170 - Fixed app not working with multiple identical monitors on Intel - App does not work with multiple identical monitors #49 - Added 'Safe Mode' option - pressing the Shift key during startup resets preferences and disables DDC read. - Upon first start if DDC is unreadable, default brightness/volume/contrast values are now set to a sensible 75% instead of 0% - DDC write commands are issued twice on Intel (as it already was on Arm64) to improve stability on some setups. - Make sure DDC communications don't happen in parallel when both slider menu and keyboard is used (this might have caused problems with some docks with multiple display outputs). - Fixed volume control feedback audio (clicking sound) during key repeat (it should play on keyup only as this is the macOS standard). - Fixed duplication of volume control feedback audio if there are multiple external displays and 'Change... for all screens' is enabled. - Internal DDC library for Intel (based on the work of reitermarkus)
421 lines
15 KiB
Swift
421 lines
15 KiB
Swift
import AVFoundation
|
|
import Cocoa
|
|
import IOKit
|
|
import os.log
|
|
|
|
class ExternalDisplay: Display {
|
|
var brightnessSliderHandler: SliderHandler?
|
|
var volumeSliderHandler: SliderHandler?
|
|
var contrastSliderHandler: SliderHandler?
|
|
var ddc: IntelDDC?
|
|
var arm64ddc: Bool = false
|
|
var arm64avService: IOAVService?
|
|
|
|
let DDC_HARD_MAX_LIMIT: Int = 100
|
|
|
|
let ddcQueue = DispatchQueue(label: "DDC queue")
|
|
|
|
private let prefs = UserDefaults.standard
|
|
|
|
var enableMuteUnmute: Bool {
|
|
get {
|
|
return self.prefs.bool(forKey: "enableMuteUnmute-\(self.identifier)")
|
|
}
|
|
set {
|
|
self.prefs.set(newValue, forKey: "enableMuteUnmute-\(self.identifier)")
|
|
os_log("Set `enableMuteUnmute` for %{private}@ to: %{public}@", type: .info, String(self.identifier), String(newValue))
|
|
}
|
|
}
|
|
|
|
var hideOsd: Bool {
|
|
get {
|
|
return self.prefs.bool(forKey: "hideOsd-\(self.identifier)")
|
|
}
|
|
set {
|
|
self.prefs.set(newValue, forKey: "hideOsd-\(self.identifier)")
|
|
os_log("Set `hideOsd` to: %{public}@", type: .info, String(newValue))
|
|
}
|
|
}
|
|
|
|
var needsLongerDelay: Bool {
|
|
get {
|
|
return self.prefs.object(forKey: "longerDelay-\(self.identifier)") as? Bool ?? false
|
|
}
|
|
set {
|
|
self.prefs.set(newValue, forKey: "longerDelay-\(self.identifier)")
|
|
os_log("Set `needsLongerDisplay` to: %{public}@", type: .info, String(newValue))
|
|
}
|
|
}
|
|
|
|
override init(_ identifier: CGDirectDisplayID, name: String, vendorNumber: UInt32?, modelNumber: UInt32?, isVirtual: Bool = false) {
|
|
super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, isVirtual: isVirtual)
|
|
|
|
if !isVirtual, !Arm64DDC.isArm64 {
|
|
self.ddc = IntelDDC(for: identifier)
|
|
}
|
|
}
|
|
|
|
func isMuted() -> Bool {
|
|
return self.getValue(for: .audioMuteScreenBlank) == 1
|
|
}
|
|
|
|
func toggleMute(fromVolumeSlider: Bool = false) {
|
|
var muteValue: Int
|
|
var volumeOSDValue: Int
|
|
|
|
if !self.isMuted() {
|
|
muteValue = 1
|
|
volumeOSDValue = 0
|
|
} else {
|
|
muteValue = 2
|
|
volumeOSDValue = self.getValue(for: .audioSpeakerVolume)
|
|
|
|
// The volume that will be set immediately after setting unmute while the old set volume was 0 is unpredictable
|
|
// Hence, just set it to a single filled chiclet
|
|
if volumeOSDValue == 0 {
|
|
volumeOSDValue = self.stepSize(for: .audioSpeakerVolume, isSmallIncrement: false)
|
|
self.saveValue(volumeOSDValue, for: .audioSpeakerVolume)
|
|
}
|
|
}
|
|
|
|
let volumeDDCValue = UInt16(volumeOSDValue)
|
|
|
|
if self.enableMuteUnmute {
|
|
guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
|
|
return
|
|
}
|
|
}
|
|
|
|
self.saveValue(muteValue, for: .audioMuteScreenBlank)
|
|
|
|
if !self.enableMuteUnmute || volumeOSDValue > 0 {
|
|
_ = self.writeDDCValues(command: .audioSpeakerVolume, value: volumeDDCValue)
|
|
}
|
|
|
|
if !fromVolumeSlider {
|
|
if !self.hideOsd {
|
|
self.showOsd(command: volumeOSDValue > 0 ? .audioSpeakerVolume : .audioMuteScreenBlank, value: volumeOSDValue, roundChiclet: true)
|
|
}
|
|
|
|
if volumeOSDValue > 0 {
|
|
self.playVolumeChangedSound()
|
|
}
|
|
|
|
if let slider = self.volumeSliderHandler?.slider {
|
|
slider.intValue = Int32(volumeDDCValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stepVolume(isUp: Bool, isSmallIncrement: Bool, isPressed: Bool) {
|
|
let currentValue = self.getValue(for: .audioSpeakerVolume)
|
|
guard isPressed else {
|
|
self.playVolumeChangedSound()
|
|
return
|
|
}
|
|
var muteValue: Int?
|
|
let maxValue = self.getMaxValue(for: .audioSpeakerVolume)
|
|
let volumeOSDValue = self.calcNewValue(currentValue: currentValue, maxValue: maxValue, isUp: isUp, isSmallIncrement: isSmallIncrement)
|
|
let volumeDDCValue = UInt16(volumeOSDValue)
|
|
if self.isMuted(), volumeOSDValue > 0 {
|
|
muteValue = 2
|
|
} else if !self.isMuted(), volumeOSDValue == 0 {
|
|
muteValue = 1
|
|
}
|
|
let isAlreadySet = volumeOSDValue == self.getValue(for: .audioSpeakerVolume)
|
|
if !isAlreadySet {
|
|
if let muteValue = muteValue, self.enableMuteUnmute {
|
|
guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
|
|
return
|
|
}
|
|
self.saveValue(muteValue, for: .audioMuteScreenBlank)
|
|
}
|
|
|
|
if !self.enableMuteUnmute || volumeOSDValue != 0 {
|
|
_ = self.writeDDCValues(command: .audioSpeakerVolume, value: volumeDDCValue)
|
|
}
|
|
}
|
|
if !self.hideOsd {
|
|
self.showOsd(command: .audioSpeakerVolume, value: volumeOSDValue, roundChiclet: !isSmallIncrement)
|
|
}
|
|
if !isAlreadySet {
|
|
self.saveValue(volumeOSDValue, for: .audioSpeakerVolume)
|
|
if let slider = self.volumeSliderHandler?.slider {
|
|
slider.intValue = Int32(volumeDDCValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func isSwOnly() -> Bool {
|
|
return (!self.arm64ddc && self.ddc == nil && !self.isVirtual)
|
|
}
|
|
|
|
func isSw() -> Bool {
|
|
if self.prefs.bool(forKey: "forceSw-\(self.identifier)") || self.isSwOnly() {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
let swAfterOsdAnimationSemaphore = DispatchSemaphore(value: 1)
|
|
var lastAnimationStartedTime: CFTimeInterval = CACurrentMediaTime()
|
|
func doSwAfterOsdAnimation() {
|
|
self.lastAnimationStartedTime = CACurrentMediaTime()
|
|
DispatchQueue.global(qos: .userInteractive).async {
|
|
self.swAfterOsdAnimationSemaphore.wait()
|
|
guard CACurrentMediaTime() < self.lastAnimationStartedTime + 0.05 else {
|
|
self.swAfterOsdAnimationSemaphore.signal()
|
|
return
|
|
}
|
|
for value: Int in stride(from: 1, to: 6, by: 1) {
|
|
guard self.getValue(for: .brightness) == 0 else {
|
|
self.swAfterOsdAnimationSemaphore.signal()
|
|
return
|
|
}
|
|
self.showOsd(command: .brightness, value: value, roundChiclet: false)
|
|
Thread.sleep(forTimeInterval: Double(value * 2) / 300)
|
|
}
|
|
for value: Int in stride(from: 5, to: 0, by: -1) {
|
|
guard self.getValue(for: .brightness) == 0 else {
|
|
self.swAfterOsdAnimationSemaphore.signal()
|
|
return
|
|
}
|
|
self.showOsd(command: .brightness, value: value, roundChiclet: false)
|
|
Thread.sleep(forTimeInterval: Double(value * 2) / 300)
|
|
}
|
|
self.showOsd(command: .brightness, value: 0, roundChiclet: true)
|
|
self.swAfterOsdAnimationSemaphore.signal()
|
|
}
|
|
}
|
|
|
|
func stepBrightnessPart(osdValue: Int, isSmallIncrement: Bool) -> Bool {
|
|
if self.isSw(), self.prefs.bool(forKey: Utils.PrefKeys.fallbackSw.rawValue) {
|
|
if self.setSwBrightness(value: UInt8(osdValue), smooth: true) {
|
|
self.showOsd(command: .brightness, value: osdValue, roundChiclet: !isSmallIncrement)
|
|
self.saveValue(osdValue, for: .brightness)
|
|
if let slider = brightnessSliderHandler?.slider {
|
|
slider.intValue = Int32(osdValue)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func stepBrightnessswAfterBirghtnessMode(osdValue: Int, isUp: Bool, isSmallIncrement: Bool) -> Bool {
|
|
let isAlreadySet = osdValue == self.getValue(for: .brightness)
|
|
var swAfterBirghtnessMode: Bool = isSwBrightnessNotDefault()
|
|
if isAlreadySet, !isUp, !swAfterBirghtnessMode, self.prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) {
|
|
swAfterBirghtnessMode = true
|
|
}
|
|
|
|
if swAfterBirghtnessMode {
|
|
let currentSwBrightness = UInt8(self.getSwBrightnessPrefValue())
|
|
var swBirghtnessValue = self.calcNewValue(currentValue: Int(currentSwBrightness), maxValue: Int(getSwMaxBrightness()), isUp: isUp, isSmallIncrement: isSmallIncrement)
|
|
if swBirghtnessValue >= Int(getSwMaxBrightness()) {
|
|
swBirghtnessValue = Int(getSwMaxBrightness())
|
|
swAfterBirghtnessMode = false
|
|
}
|
|
if self.setSwBrightness(value: UInt8(swBirghtnessValue)) {
|
|
if let slider = brightnessSliderHandler?.slider {
|
|
slider.intValue = Int32(Float(slider.maxValue / 2) * (Float(swBirghtnessValue) / Float(getSwMaxBrightness())))
|
|
}
|
|
self.doSwAfterOsdAnimation()
|
|
}
|
|
}
|
|
return swAfterBirghtnessMode
|
|
}
|
|
|
|
override func stepBrightness(isUp: Bool, isSmallIncrement: Bool) {
|
|
let currentValue = self.getValue(for: .brightness)
|
|
let maxValue = self.isSw() ? Int(self.getSwMaxBrightness()) : self.getMaxValue(for: .brightness)
|
|
let osdValue = self.calcNewValue(currentValue: currentValue, maxValue: maxValue, isUp: isUp, isSmallIncrement: isSmallIncrement)
|
|
|
|
if self.stepBrightnessPart(osdValue: osdValue, isSmallIncrement: isSmallIncrement) {
|
|
return
|
|
}
|
|
|
|
if self.stepBrightnessswAfterBirghtnessMode(osdValue: osdValue, isUp: isUp, isSmallIncrement: isSmallIncrement) {
|
|
return
|
|
}
|
|
|
|
let ddcValue = UInt16(osdValue)
|
|
guard self.writeDDCValues(command: .brightness, value: ddcValue) == true else {
|
|
return
|
|
}
|
|
if let slider = brightnessSliderHandler?.slider {
|
|
if !self.isSw(), self.prefs.bool(forKey: Utils.PrefKeys.lowerSwAfterBrightness.rawValue) {
|
|
slider.intValue = Int32(slider.maxValue / 2) + Int32(ddcValue)
|
|
} else {
|
|
slider.intValue = Int32(ddcValue)
|
|
}
|
|
}
|
|
self.showOsd(command: .brightness, value: osdValue, roundChiclet: !isSmallIncrement)
|
|
self.saveValue(osdValue, for: .brightness)
|
|
}
|
|
|
|
public func writeDDCValues(command: Command, value: UInt16, errorRecoveryWaitTime _: UInt32? = nil) -> Bool? {
|
|
guard app.sleepID == 0, app.reconfigureID == 0, !self.forceSw else {
|
|
return false
|
|
}
|
|
var success: Bool = false
|
|
self.ddcQueue.sync {
|
|
if Arm64DDC.isArm64 {
|
|
if self.arm64ddc {
|
|
success = Arm64DDC.write(service: self.arm64avService, command: command.rawValue, value: value)
|
|
}
|
|
} else {
|
|
success = self.ddc?.write(command: command.rawValue, value: value, errorRecoveryWaitTime: 2000) ?? false
|
|
}
|
|
}
|
|
return success
|
|
}
|
|
|
|
func readDDCValues(for command: Command, tries: UInt, minReplyDelay delay: UInt64?) -> (current: UInt16, max: UInt16)? {
|
|
var values: (UInt16, UInt16)?
|
|
guard app.sleepID == 0, app.reconfigureID == 0, !self.forceSw else {
|
|
return values
|
|
}
|
|
if Arm64DDC.isArm64 {
|
|
guard self.arm64ddc else {
|
|
return nil
|
|
}
|
|
self.ddcQueue.sync {
|
|
if let unwrappedDelay = delay {
|
|
values = Arm64DDC.read(service: self.arm64avService, command: command.rawValue, tries: UInt8(min(tries, 255)), minReplyDelay: UInt32(unwrappedDelay / 1000))
|
|
} else {
|
|
values = Arm64DDC.read(service: self.arm64avService, command: command.rawValue, tries: UInt8(min(tries, 255)))
|
|
}
|
|
}
|
|
} else {
|
|
self.ddcQueue.sync {
|
|
values = self.ddc?.read(command: command.rawValue, tries: tries, minReplyDelay: delay)
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func calcNewValue(currentValue: Int, maxValue: Int, isUp: Bool, isSmallIncrement: Bool) -> Int {
|
|
let nextValue: Int
|
|
|
|
if isSmallIncrement {
|
|
nextValue = currentValue + (isUp ? 1 : -1)
|
|
} else {
|
|
let osdChicletFromValue = OSDUtils.chiclet(fromValue: Float(currentValue), maxValue: Float(maxValue))
|
|
|
|
let distance = OSDUtils.getDistance(fromNearestChiclet: osdChicletFromValue)
|
|
// get the next rounded chiclet
|
|
var nextFilledChiclet = isUp ? ceil(osdChicletFromValue) : floor(osdChicletFromValue)
|
|
|
|
// Depending on the direction, if the chiclet is above or below a certain threshold, we go to the next whole chiclet
|
|
let distanceThreshold = Float(0.25) // 25% of the distance between the edges of an osd box
|
|
if distance == 0 {
|
|
nextFilledChiclet += isUp ? 1 : -1
|
|
} else if !isUp, distance < distanceThreshold {
|
|
nextFilledChiclet -= 1
|
|
} else if isUp, distance > (1 - distanceThreshold) {
|
|
nextFilledChiclet += 1
|
|
}
|
|
|
|
nextValue = Int(round(OSDUtils.value(fromChiclet: nextFilledChiclet, maxValue: Float(maxValue))))
|
|
|
|
os_log("next: .value %{public}@/%{public}@, .osd %{public}@/%{public}@", type: .debug, String(nextValue), String(maxValue), String(nextFilledChiclet), String(OSDUtils.chicletCount))
|
|
}
|
|
return max(0, min(maxValue, nextValue))
|
|
}
|
|
|
|
func getValue(for command: Command) -> Int {
|
|
return self.prefs.integer(forKey: "\(command.rawValue)-\(self.identifier)")
|
|
}
|
|
|
|
func getValueExists(for command: Command) -> Bool {
|
|
return self.prefs.object(forKey: "\(command.rawValue)-\(self.identifier)") != nil
|
|
}
|
|
|
|
func saveValue(_ value: Int, for command: Command) {
|
|
self.prefs.set(value, forKey: "\(command.rawValue)-\(self.identifier)")
|
|
}
|
|
|
|
func saveMaxValue(_ maxValue: Int, for command: Command) {
|
|
self.prefs.set(maxValue, forKey: "max-\(command.rawValue)-\(self.identifier)")
|
|
}
|
|
|
|
func getMaxValue(for command: Command) -> Int {
|
|
let max = self.prefs.integer(forKey: "max-\(command.rawValue)-\(self.identifier)")
|
|
return min(self.DDC_HARD_MAX_LIMIT, max == 0 ? self.DDC_HARD_MAX_LIMIT : max)
|
|
}
|
|
|
|
func getRestoreValue(for command: Command) -> Int {
|
|
return self.prefs.integer(forKey: "restore-\(command.rawValue)-\(self.identifier)")
|
|
}
|
|
|
|
func setRestoreValue(_ value: Int?, for command: Command) {
|
|
self.prefs.set(value, forKey: "restore-\(command.rawValue)-\(self.identifier)")
|
|
}
|
|
|
|
func setPollingMode(_ value: Int) {
|
|
self.prefs.set(String(value), forKey: "pollingMode-\(self.identifier)")
|
|
}
|
|
|
|
/*
|
|
Polling Modes:
|
|
0 -> .none -> 0 tries
|
|
1 -> .minimal -> 5 tries
|
|
2 -> .normal -> 10 tries
|
|
3 -> .heavy -> 100 tries
|
|
4 -> .custom -> $pollingCount tries
|
|
*/
|
|
func getPollingMode() -> Int {
|
|
// Reading as string so we don't get "0" as the default value
|
|
return Int(self.prefs.string(forKey: "pollingMode-\(self.identifier)") ?? "2") ?? 2
|
|
}
|
|
|
|
func getPollingCount() -> Int {
|
|
let selectedMode = self.getPollingMode()
|
|
switch selectedMode {
|
|
case 0:
|
|
return Utils.PollingMode.none.value
|
|
case 1:
|
|
return Utils.PollingMode.minimal.value
|
|
case 2:
|
|
return Utils.PollingMode.normal.value
|
|
case 3:
|
|
return Utils.PollingMode.heavy.value
|
|
case 4:
|
|
let val = self.prefs.integer(forKey: "pollingCount-\(self.identifier)")
|
|
return Utils.PollingMode.custom(value: val).value
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func setPollingCount(_ value: Int) {
|
|
self.prefs.set(value, forKey: "pollingCount-\(self.identifier)")
|
|
}
|
|
|
|
private func stepSize(for command: Command, isSmallIncrement: Bool) -> Int {
|
|
return isSmallIncrement ? 1 : Int(floor(Float(self.getMaxValue(for: command)) / OSDUtils.chicletCount))
|
|
}
|
|
|
|
override func showOsd(command: Command, value: Int, maxValue _: Int = 100, roundChiclet: Bool = false, lock: Bool = false) {
|
|
super.showOsd(command: command, value: value, maxValue: self.getMaxValue(for: command), roundChiclet: roundChiclet, lock: lock)
|
|
}
|
|
|
|
private var audioPlayer: AVAudioPlayer?
|
|
|
|
private func playVolumeChangedSound() {
|
|
// Check if user has enabled "Play feedback when volume is changed" in Sound Preferences
|
|
guard let preferences = Utils.getSystemPreferences(), let hasSoundEnabled = preferences["com.apple.sound.beep.feedback"] as? Int, hasSoundEnabled == 1
|
|
else {
|
|
return
|
|
}
|
|
do {
|
|
self.audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff"))
|
|
self.audioPlayer?.volume = 1
|
|
self.audioPlayer?.play()
|
|
} catch {
|
|
os_log("%{public}@", type: .error, error.localizedDescription)
|
|
}
|
|
}
|
|
}
|