Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
whitelist_rules:
only_rules:
- attributes
- block_based_kvo
- class_delegate_protocol
Expand Down
4 changes: 4 additions & 0 deletions MultiSoundChanger.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -58,6 +59,7 @@
F3925974262F2B8000B7AD62 /* ApplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationController.swift; sourceTree = "<group>"; };
F3925978262F2F9F00B7AD62 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
F392597B2631ACE700B7AD62 /* Runner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Runner.swift; sourceTree = "<group>"; };
F7E845062CF9F96D0001647F /* BetterDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterDisplay.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -224,6 +226,7 @@
children = (
F3925978262F2F9F00B7AD62 /* Logger.swift */,
F392597B2631ACE700B7AD62 /* Runner.swift */,
F7E845062CF9F96D0001647F /* BetterDisplay.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
8 changes: 8 additions & 0 deletions MultiSoundChanger/Other/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
55 changes: 51 additions & 4 deletions MultiSoundChanger/Sources/Frameworks/Audio.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<UInt32>.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<UInt32>.size)
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
43 changes: 43 additions & 0 deletions MultiSoundChanger/Sources/Utils/BetterDisplay.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}