diff --git a/.swiftlint.yml b/.swiftlint.yml index 71468ec..248c4b1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,4 +1,4 @@ -whitelist_rules: +only_rules: - attributes - block_based_kvo - class_delegate_protocol diff --git a/MultiSoundChanger.xcodeproj/project.pbxproj b/MultiSoundChanger.xcodeproj/project.pbxproj index 13666c4..4e2f3a2 100644 --- a/MultiSoundChanger.xcodeproj/project.pbxproj +++ b/MultiSoundChanger.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ F3925975262F2B8000B7AD62 /* ApplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3925974262F2B8000B7AD62 /* ApplicationController.swift */; }; F3925979262F2F9F00B7AD62 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3925978262F2F9F00B7AD62 /* Logger.swift */; }; F392597C2631ACE700B7AD62 /* Runner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F392597B2631ACE700B7AD62 /* Runner.swift */; }; + F7E845072CF9F96D0001647F /* BetterDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E845062CF9F96D0001647F /* BetterDisplay.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -58,6 +59,7 @@ F3925974262F2B8000B7AD62 /* ApplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationController.swift; sourceTree = ""; }; F3925978262F2F9F00B7AD62 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; F392597B2631ACE700B7AD62 /* Runner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Runner.swift; sourceTree = ""; }; + F7E845062CF9F96D0001647F /* BetterDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterDisplay.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -224,6 +226,7 @@ children = ( F3925978262F2F9F00B7AD62 /* Logger.swift */, F392597B2631ACE700B7AD62 /* Runner.swift */, + F7E845062CF9F96D0001647F /* BetterDisplay.swift */, ); path = Utils; sourceTree = ""; @@ -381,6 +384,7 @@ F373D8B52561D1A600642274 /* MediaManager.swift in Sources */, F373D8B42561D1A600642274 /* AudioManager.swift in Sources */, F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */, + F7E845072CF9F96D0001647F /* BetterDisplay.swift in Sources */, 4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */, F373D8BF2561D22000642274 /* VolumeViewController.swift in Sources */, F3925979262F2F9F00B7AD62 /* Logger.swift in Sources */, diff --git a/MultiSoundChanger/Other/Constants.swift b/MultiSoundChanger/Other/Constants.swift index a6ea1b1..4b9beb8 100644 --- a/MultiSoundChanger/Other/Constants.swift +++ b/MultiSoundChanger/Other/Constants.swift @@ -51,5 +51,13 @@ enum Constants { static func selectedDeviceVolume(volume: String) -> String { return "Selected device volume: \(volume)" } + + static func deviceDoesNotSupportVolume(deviceName: String) -> String { + return "The device \(deviceName) does not support volume control" + } + + static func deviceDoesNotSupportMute(deviceName: String) -> String { + return "The device \(deviceName) does not support mute control" + } } } diff --git a/MultiSoundChanger/Sources/Frameworks/Audio.swift b/MultiSoundChanger/Sources/Frameworks/Audio.swift index 1c245f6..7317b99 100644 --- a/MultiSoundChanger/Sources/Frameworks/Audio.swift +++ b/MultiSoundChanger/Sources/Frameworks/Audio.swift @@ -74,6 +74,29 @@ final class AudioImpl: Audio { return deviceType == kAudioDeviceTransportTypeAggregate } + func isAudioDevicePossibleDisplay(deviceID: AudioDeviceID) -> Bool { + var propertyAddress = AudioObjectPropertyAddress( + mSelector: kAudioDevicePropertyTransportType, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster + ) + + var transportType: UInt32 = 0 + var size = UInt32(MemoryLayout.size) + let status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &size, &transportType) + + if status == noErr { + switch transportType { + case kAudioDeviceTransportTypeHDMI, kAudioDeviceTransportTypeDisplayPort: + return true + default: + return false + } + } else { + return false + } + } + func isDeviceMuted(deviceID: AudioDeviceID) -> Bool { var mutedValue: UInt32 = 0 var propertySize = UInt32(MemoryLayout.size) @@ -118,13 +141,26 @@ final class AudioImpl: Audio { var size = UInt32(0) AudioObjectGetPropertyDataSize(deviceID, &masterLevelPropertyAddress, 0, nil, &size) - AudioObjectSetPropertyData(deviceID, &masterLevelPropertyAddress, 0, nil, size, &masterLevel) + let statusMaster = AudioObjectSetPropertyData(deviceID, &masterLevelPropertyAddress, 0, nil, size, &masterLevel) AudioObjectGetPropertyDataSize(deviceID, &leftLevelPropertyAddress, 0, nil, &size) - AudioObjectSetPropertyData(deviceID, &leftLevelPropertyAddress, 0, nil, size, &leftLevel) + let statusLeft = AudioObjectSetPropertyData(deviceID, &leftLevelPropertyAddress, 0, nil, size, &leftLevel) AudioObjectGetPropertyDataSize(deviceID, &rightLevelPropertyAddress, 0, nil, &size) - AudioObjectSetPropertyData(deviceID, &rightLevelPropertyAddress, 0, nil, size, &rigthLevel) + let statusRight = AudioObjectSetPropertyData(deviceID, &rightLevelPropertyAddress, 0, nil, size, &rigthLevel) + + if statusMaster != noErr && statusLeft != noErr && statusRight != noErr { + let isVolumeSupported = (AudioObjectHasProperty(deviceID, &masterLevelPropertyAddress) || + AudioObjectHasProperty(deviceID, &leftLevelPropertyAddress) || + AudioObjectHasProperty(deviceID, &rightLevelPropertyAddress)) + if !isVolumeSupported { + if isAudioDevicePossibleDisplay(deviceID: deviceID) { + BetterDisplay.setVolume(masterLevel, deviceName: getDeviceName(deviceID: deviceID)) + } else { + Logger.debug(Constants.InnerMessages.deviceDoesNotSupportVolume(deviceName: getDeviceName(deviceID: deviceID))) + } + } + } } func setDeviceMute(deviceID: AudioDeviceID, isMute: Bool) { @@ -136,7 +172,18 @@ final class AudioImpl: Audio { mScope: AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput), mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) - AudioObjectSetPropertyData(deviceID, &propertyAddress, 0, nil, propertySize, &mutedValue) + let status = AudioObjectSetPropertyData(deviceID, &propertyAddress, 0, nil, propertySize, &mutedValue) + + if status != noErr { + let isMuteSupported = (AudioObjectHasProperty(deviceID, &propertyAddress)) + if !isMuteSupported { + if isAudioDevicePossibleDisplay(deviceID: deviceID) { + BetterDisplay.mute(isMute, deviceName: getDeviceName(deviceID: deviceID)) + } else { + Logger.debug(Constants.InnerMessages.deviceDoesNotSupportMute(deviceName: getDeviceName(deviceID: deviceID))) + } + } + } } func setOutputDevice(newDeviceID: AudioDeviceID) { diff --git a/MultiSoundChanger/Sources/Utils/BetterDisplay.swift b/MultiSoundChanger/Sources/Utils/BetterDisplay.swift new file mode 100644 index 0000000..6dcd1d4 --- /dev/null +++ b/MultiSoundChanger/Sources/Utils/BetterDisplay.swift @@ -0,0 +1,43 @@ +// +// BetterDisplay.swift +// MultiSoundChanger +// +// Created by aone on 29.11.24. +// Copyright © 2021 Dmitry Medyuho. All rights reserved. +// + +import AppKit + +enum BetterDisplay { + private static let executablePath = "/Applications/BetterDisplay.app/Contents/MacOS/BetterDisplay" + private static let requestNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.request") + + private struct IntegrationNotificationRequestData: Codable { + var uuid: String? + var commands: [String] = [] + var parameters: [String: String?] = [:] + } + + private static let isInstalled: Bool = FileManager.default.fileExists(atPath: executablePath) + + private static func set(_ parameter: String, value: String, deviceName: String) { + if !isInstalled { + return + } + let data = IntegrationNotificationRequestData(uuid: UUID().uuidString, commands: ["n", "set"], parameters: [deviceName: "", parameter: value]) + do { + let encodedData = try JSONEncoder().encode(data) + if let encodedDataString = String(data: encodedData, encoding: .utf8) { + DistributedNotificationCenter.default().postNotificationName(requestNotificationName, object: encodedDataString, userInfo: nil, deliverImmediately: true) + } + } catch {} + } + + static func setVolume(_ volume: Float, deviceName: String) { + set("volume", value: String(volume), deviceName: deviceName) + } + + static func mute(_ mute: Bool, deviceName: String) { + set("mute", value: mute ? "on" : "off", deviceName: deviceName) + } +}