Skip to content

Commit 7a9264d

Browse files
committed
improve volume/mute handling and fix OSD/slider
1 parent eb6deb1 commit 7a9264d

File tree

4 files changed

+193
-91
lines changed

4 files changed

+193
-91
lines changed

MonitorControl/AppDelegate.swift

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1818

1919
var monitorItems: [NSMenuItem] = []
2020

21-
let step = 100 / 16
22-
2321
var displayManager: DisplayManager?
2422
var mediaKeyTap: MediaKeyTap?
2523
var prefsController: NSWindowController?
@@ -214,20 +212,14 @@ extension AppDelegate: MediaKeyTapDelegate {
214212
for display in allDisplays {
215213
if (prefs.object(forKey: "\(display.identifier)-state") as? Bool) ?? true {
216214
switch mediaKey {
217-
case .brightnessUp:
218-
let value = display.calcNewValue(for: .brightness, withRel: +(isSmallIncrement ? self.step / 4 : self.step))
219-
display.setBrightness(to: value, isSmallIncrement: isSmallIncrement)
220-
case .brightnessDown:
221-
let value = currentDisplay.calcNewValue(for: .brightness, withRel: -(isSmallIncrement ? self.step / 4 : self.step))
222-
display.setBrightness(to: value, isSmallIncrement: isSmallIncrement)
215+
case .brightnessUp, .brightnessDown:
216+
let osdValue = display.calcNewValue(for: .brightness, isUp: mediaKey == .brightnessUp, isSmallIncrement: isSmallIncrement)
217+
display.setBrightness(to: osdValue)
223218
case .mute:
224-
display.mute()
225-
case .volumeUp:
226-
let value = display.calcNewValue(for: .audioSpeakerVolume, withRel: +(isSmallIncrement ? self.step / 4 : self.step))
227-
display.setVolume(to: value, isSmallIncrement: isSmallIncrement)
228-
case .volumeDown:
229-
let value = display.calcNewValue(for: .audioSpeakerVolume, withRel: -(isSmallIncrement ? self.step / 4 : self.step))
230-
display.setVolume(to: value, isSmallIncrement: isSmallIncrement)
219+
display.toggleMute()
220+
case .volumeUp, .volumeDown:
221+
let osdValue = display.calcNewValue(for: .audioSpeakerVolume, isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement)
222+
display.setVolume(to: osdValue)
231223
default:
232224
return
233225
}

MonitorControl/Display.swift

Lines changed: 142 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ class Display {
88
let name: String
99
let isBuiltin: Bool
1010
var isEnabled: Bool
11-
var isMuted: Bool = false
1211
var brightnessSliderHandler: SliderHandler?
1312
var volumeSliderHandler: SliderHandler?
1413
var contrastSliderHandler: SliderHandler?
@@ -37,13 +36,14 @@ class Display {
3736
private let prefs = UserDefaults.standard
3837
private var audioPlayer: AVAudioPlayer?
3938

39+
private let osdChicletBoxes: Float = 16
40+
4041
init(_ identifier: CGDirectDisplayID, name: String, isBuiltin: Bool, isEnabled: Bool = true) {
4142
self.identifier = identifier
4243
self.name = name
4344
self.isEnabled = isBuiltin ? false : isEnabled
4445
self.ddc = DDC(for: identifier)
4546
self.isBuiltin = isBuiltin
46-
self.isMuted = self.getValue(for: .audioMuteScreenBlank) == 1
4747
}
4848

4949
// On some displays, the display's OSD overlaps the macOS OSD,
@@ -58,77 +58,115 @@ class Display {
5858
}
5959
}
6060

61-
func mute(forceVolume: Int? = nil) {
62-
var value = 0
63-
64-
if self.isMuted, forceVolume == nil || forceVolume! > 0 {
65-
value = forceVolume ?? self.getValue(for: .audioSpeakerVolume)
66-
self.saveValue(value, for: .audioSpeakerVolume)
61+
func isMuted() -> Bool {
62+
return self.getValue(for: .audioMuteScreenBlank) == 1
63+
}
6764

68-
self.isMuted = false
69-
} else if !self.isMuted, forceVolume == nil || forceVolume == 0 {
70-
self.isMuted = true
65+
func toggleMute(fromVolumeSlider: Bool = false) {
66+
var muteValue: Int
67+
var volumeOSDValue: Int
68+
69+
if !self.isMuted() {
70+
muteValue = 1
71+
volumeOSDValue = 0
72+
} else {
73+
muteValue = 2
74+
volumeOSDValue = self.getValue(for: .audioSpeakerVolume)
75+
76+
// The volume that will be set immediately after setting unmute while the old set volume was 0 is unpredictable
77+
// Hence, just set it to a single filled chiclet
78+
if volumeOSDValue == 0 {
79+
volumeOSDValue = self.stepSize(for: .audioSpeakerVolume, isSmallIncrement: false)
80+
self.saveValue(volumeOSDValue, for: .audioSpeakerVolume)
81+
}
7182
}
7283

7384
DispatchQueue.global(qos: .userInitiated).async {
74-
let muteValue = self.isMuted ? 1 : 2
75-
guard self.ddc?.write(command: .audioMuteScreenBlank, value: UInt16(muteValue), errorRecoveryWaitTime: self.hideOsd ? 0 : nil) == true else {
76-
self.setVolume(to: value)
85+
let volumeDDCValue = UInt16(volumeOSDValue)
86+
87+
guard self.ddc?.write(command: .audioSpeakerVolume, value: volumeDDCValue) == true else {
7788
return
7889
}
7990

80-
if forceVolume == nil || forceVolume == 0 {
81-
self.hideDisplayOsd()
82-
self.showOsd(command: .audioSpeakerVolume, value: value)
83-
self.playVolumeChangedSound()
91+
if self.supportsMuteCommand() {
92+
guard self.ddc?.write(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
93+
return
94+
}
8495
}
8596

8697
self.saveValue(muteValue, for: .audioMuteScreenBlank)
87-
}
8898

89-
if let slider = volumeSliderHandler?.slider {
90-
DispatchQueue.main.async {
91-
slider.intValue = Int32(value)
99+
if !fromVolumeSlider {
100+
self.hideDisplayOsd()
101+
self.showOsd(command: .audioSpeakerVolume, value: volumeOSDValue)
102+
103+
if volumeOSDValue > 0 {
104+
self.playVolumeChangedSound()
105+
}
106+
107+
if let slider = self.volumeSliderHandler?.slider {
108+
DispatchQueue.main.async {
109+
slider.intValue = Int32(volumeDDCValue)
110+
}
111+
}
92112
}
93113
}
94114
}
95115

96-
func setVolume(to value: Int, isSmallIncrement: Bool = false) {
97-
if value > 0, self.isMuted {
98-
self.mute(forceVolume: value)
99-
} else if value == 0 {
100-
self.mute(forceVolume: 0)
101-
return
116+
func setVolume(to volumeOSDValue: Int) {
117+
var muteValue: Int?
118+
let volumeDDCValue = UInt16(volumeOSDValue)
119+
120+
if self.isMuted(), volumeOSDValue > 0 {
121+
muteValue = 2
122+
} else if !self.isMuted(), volumeOSDValue == 0 {
123+
muteValue = 1
102124
}
103125

104126
DispatchQueue.global(qos: .userInitiated).async {
105-
guard self.ddc?.write(command: .audioSpeakerVolume, value: UInt16(value), errorRecoveryWaitTime: self.hideOsd ? 0 : nil) == true else {
127+
guard self.ddc?.write(command: .audioSpeakerVolume, value: volumeDDCValue) == true else {
106128
return
107129
}
108130

131+
if muteValue != nil {
132+
// If the mute command is supported, set its value accordingly
133+
if self.supportsMuteCommand() {
134+
guard self.ddc?.write(command: .audioMuteScreenBlank, value: UInt16(muteValue!)) == true else {
135+
return
136+
}
137+
}
138+
139+
self.saveValue(muteValue!, for: .audioMuteScreenBlank)
140+
}
141+
142+
self.saveValue(volumeOSDValue, for: .audioSpeakerVolume)
143+
109144
self.hideDisplayOsd()
110-
self.showOsd(command: .audioSpeakerVolume, value: value, isSmallIncrement: isSmallIncrement)
111-
self.playVolumeChangedSound()
112-
}
145+
self.showOsd(command: .audioSpeakerVolume, value: volumeOSDValue)
113146

114-
if let slider = volumeSliderHandler?.slider {
115-
DispatchQueue.main.async {
116-
slider.intValue = Int32(value)
147+
if volumeOSDValue > 0 {
148+
self.playVolumeChangedSound()
117149
}
118-
}
119150

120-
self.saveValue(value, for: .audioSpeakerVolume)
151+
if let slider = self.volumeSliderHandler?.slider {
152+
DispatchQueue.main.async {
153+
slider.intValue = Int32(volumeDDCValue)
154+
}
155+
}
156+
}
121157
}
122158

123-
func setBrightness(to value: Int, isSmallIncrement: Bool = false) {
159+
func setBrightness(to osdValue: Int) {
160+
let ddcValue = UInt16(osdValue)
161+
124162
if self.prefs.bool(forKey: Utils.PrefKeys.lowerContrast.rawValue) {
125-
if value == 0 {
163+
if ddcValue == 0 {
126164
DispatchQueue.global(qos: .userInitiated).async {
127-
_ = self.ddc?.write(command: .contrast, value: UInt16(value))
165+
_ = self.ddc?.write(command: .contrast, value: ddcValue)
128166
}
129167

130168
if let slider = contrastSliderHandler?.slider {
131-
slider.intValue = Int32(value)
169+
slider.intValue = Int32(ddcValue)
132170
}
133171
} else if self.getValue(for: DDC.Command.brightness) == 0 {
134172
let contrastValue = self.getValue(for: DDC.Command.contrast)
@@ -140,23 +178,67 @@ class Display {
140178
}
141179

142180
DispatchQueue.global(qos: .userInitiated).async {
143-
guard self.ddc?.write(command: .brightness, value: UInt16(value)) == true else {
181+
guard self.ddc?.write(command: .brightness, value: ddcValue) == true else {
144182
return
145183
}
146184

147-
self.showOsd(command: .brightness, value: value, isSmallIncrement: isSmallIncrement)
185+
self.showOsd(command: .brightness, value: osdValue)
148186
}
149187

150188
if let slider = brightnessSliderHandler?.slider {
151-
slider.intValue = Int32(value)
189+
slider.intValue = Int32(ddcValue)
152190
}
153191

154-
self.saveValue(value, for: .brightness)
192+
self.saveValue(osdValue, for: .brightness)
155193
}
156194

157-
func calcNewValue(for command: DDC.Command, withRel rel: Int) -> Int {
195+
func readDDCValues(for command: DDC.Command, tries: UInt, minReplyDelay delay: UInt64?) -> (current: UInt16, max: UInt16)? {
196+
var values: (UInt16, UInt16)?
197+
198+
if self.ddc?.supported(minReplyDelay: delay) == true {
199+
os_log("Display supports DDC.", type: .debug)
200+
} else {
201+
os_log("Display does not support DDC.", type: .debug)
202+
}
203+
204+
if self.ddc?.enableAppReport() == true {
205+
os_log("Display supports enabling DDC application report.", type: .debug)
206+
} else {
207+
os_log("Display does not support enabling DDC application report.", type: .debug)
208+
}
209+
210+
values = self.ddc?.read(command: command, tries: tries, minReplyDelay: delay)
211+
212+
if values != nil {
213+
return values!
214+
}
215+
216+
return nil
217+
}
218+
219+
func calcNewValue(for command: DDC.Command, isUp: Bool, isSmallIncrement: Bool) -> Int {
158220
let currentValue = self.getValue(for: command)
159-
return max(0, min(self.getMaxValue(for: command), currentValue + rel))
221+
let nextValue: Int
222+
223+
if isSmallIncrement {
224+
nextValue = currentValue + (isUp ? 1 : -1)
225+
} else {
226+
let filledChicletBoxes = self.osdChicletBoxes * (Float(currentValue) / Float(self.getMaxValue(for: command)))
227+
228+
var nextFilledChicletBoxes: Float
229+
var fillecChicletBoxesRel: Float = isUp ? 1 : -1
230+
231+
// This is a workaround to ensure that if the user has set the value using a small step (that is, the current chiclet box isn't completely filled,
232+
// the next regular up or down step will only fill or empty that chiclet, and not the next one as well - it only really works because the max value is 100
233+
if (isUp && ceil(filledChicletBoxes) - filledChicletBoxes > 0.15) || (!isUp && filledChicletBoxes - floor(filledChicletBoxes) > 0.15) {
234+
fillecChicletBoxesRel = 0
235+
}
236+
237+
nextFilledChicletBoxes = isUp ? ceil(filledChicletBoxes + fillecChicletBoxesRel) : floor(filledChicletBoxes + fillecChicletBoxesRel)
238+
nextValue = Int(Float(self.getMaxValue(for: command)) * (nextFilledChicletBoxes / self.osdChicletBoxes))
239+
}
240+
241+
return max(0, min(self.getMaxValue(for: command), Int(nextValue)))
160242
}
161243

162244
func getValue(for command: DDC.Command) -> Int {
@@ -225,32 +307,37 @@ class Display {
225307
self.prefs.set(value, forKey: "pollingCount-\(self.identifier)")
226308
}
227309

228-
private func showOsd(command: DDC.Command, value: Int, isSmallIncrement: Bool = false) {
310+
private func stepSize(for command: DDC.Command, isSmallIncrement: Bool) -> Int {
311+
return isSmallIncrement ? 1 : Int(floor(Float(self.getMaxValue(for: command)) / self.osdChicletBoxes))
312+
}
313+
314+
private func showOsd(command: DDC.Command, value: Int) {
229315
guard let manager = OSDManager.sharedManager() as? OSDManager else {
230316
return
231317
}
232318

233-
let maxValue = self.getMaxValue(for: command)
234-
235319
var osdImage: Int64 = 1 // Brightness Image
236320
if command == .audioSpeakerVolume {
237321
osdImage = 3 // Speaker image
238-
if self.isMuted {
322+
if self.isMuted() {
239323
osdImage = 4 // Mute speaker
240324
}
241325
}
242326

243-
let step = isSmallIncrement ? maxValue / maxValue : maxValue / 16
244-
245327
manager.showImage(osdImage,
246328
onDisplayID: self.identifier,
247329
priority: 0x1F4,
248330
msecUntilFade: 1000,
249-
filledChiclets: UInt32(value / step),
250-
totalChiclets: UInt32(maxValue / step),
331+
filledChiclets: UInt32(value),
332+
totalChiclets: UInt32(self.getMaxValue(for: command)),
251333
locked: false)
252334
}
253335

336+
private func supportsMuteCommand() -> Bool {
337+
// Monitors which don't support the mute command - e.g. Dell U3419W - will have a maximum value of 100 for the DDC mute command
338+
return self.getMaxValue(for: .audioMuteScreenBlank) == 2
339+
}
340+
254341
private func playVolumeChangedSound() {
255342
let soundPath = "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff"
256343
let soundUrl = URL(fileURLWithPath: soundPath)

0 commit comments

Comments
 (0)