Skip to content

Instantly share code, notes, and snippets.

@tstephansen
Created January 17, 2023 17:42
Show Gist options
  • Save tstephansen/5cdbaac3b75af2d031713203e8a50ee0 to your computer and use it in GitHub Desktop.
Save tstephansen/5cdbaac3b75af2d031713203e8a50ee0 to your computer and use it in GitHub Desktop.
Controlling System Volume on macOS in Maui Blazor
#!/bin/bash
swiftc MacSystemVolumeController.swift -emit-library
## Uncomment the line below to move the file to the /usr/local/lib folder.
# sudo cp libMacSystemVolumeController.dylib /usr/local/lib/
using System.Runtime.InteropServices;
namespace ControlExample;
public class Example
{
public float GetMainVolume() => getMainVolume();
public void SetMainVolume(float volume) => setMainVolume(volume);
public bool IsMainVolumeMuted() => isMainVolumeMuted();
public void MuteMainVolume() => muteMainVolume();
public void UnmuteMainVolume() => unmuteMainVolume();
[DllImport("libMacSystemVolumeController.dylib")]
private static extern float getMainVolume();
[DllImport("libMacSystemVolumeController.dylib")]
private static extern void setMainVolume(float volume);
[DllImport("libMacSystemVolumeController.dylib")]
private static extern bool isMainVolumeMuted();
[DllImport("libMacSystemVolumeController.dylib")]
private static extern void muteMainVolume();
[DllImport("libMacSystemVolumeController.dylib")]
private static extern void unmuteMainVolume();
}
// The code for this was taken from the following link and slightly modified https://github.com/mabi99/NSSound_SystemVolumeExtension/blob/master/NSSound_SystemVolumeExtension.swift
import CoreAudioKit
private func obtainDefaultOutputDevice() -> AudioDeviceID
{
var theAnswer : AudioDeviceID = kAudioObjectUnknown
var theSize = UInt32(MemoryLayout.size(ofValue: theAnswer)) // needs to be converted to UInt32?
var theAddress : AudioObjectPropertyAddress
theAddress = AudioObjectPropertyAddress.init(mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain)
//first be sure that a default device exists
if (!AudioObjectHasProperty(AudioObjectID(kAudioObjectSystemObject), &theAddress) ) {
print("Unable to get default audio device")
return theAnswer
}
//get the property 'default output device'
let theError : OSStatus = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &theAddress, UInt32(0), nil, &theSize, &theAnswer)
if (theError != noErr) {
print("Unable to get output audio device")
return theAnswer
}
return theAnswer
}
private func getSystemVolume() -> Float
{
var defaultDevID: AudioDeviceID = kAudioObjectUnknown
var theSize = UInt32(MemoryLayout.size(ofValue: defaultDevID))
var theError: OSStatus
var theVolume: Float32 = 0
var theAddress: AudioObjectPropertyAddress
defaultDevID = obtainDefaultOutputDevice()
if (defaultDevID == kAudioObjectUnknown) {
print("Audio device not found!")
return 0.0
} //device not found: return 0
theAddress = AudioObjectPropertyAddress.init(mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain)
//be sure that the default device has the volume property
if (!AudioObjectHasProperty(defaultDevID, &theAddress) ) {
print("No volume control for device 0x%0x",defaultDevID)
return 0.0
}
//now read the property and correct it, if outside [0...1]
theError = AudioObjectGetPropertyData(defaultDevID, &theAddress, 0, nil, &theSize, &theVolume)
if ( theError != noErr ) {
print("Unable to read volume for device 0x%0x", defaultDevID)
return 0.0
}
theVolume = theVolume > 1.0 ? 1.0 : (theVolume < 0.0 ? 0.0 : theVolume)
return theVolume
}
private func setSystemVolume(theVolume: Float, muteOff: Bool = true)
{
var newValue: Float = theVolume
var theAddress: AudioObjectPropertyAddress
var defaultDevID: AudioDeviceID
var theError: OSStatus = noErr
var muted: UInt32
var canSetVol: DarwinBoolean = true
var muteValue: Bool
var hasMute:Bool = true
var canMute: DarwinBoolean = true
defaultDevID = obtainDefaultOutputDevice()
if (defaultDevID == kAudioObjectUnknown) {
//device not found: return without trying to set
print("Audio Device unknown")
return
}
//check if the new value is in the correct range - normalize it if not
newValue = theVolume > 1.0 ? 1.0 : (theVolume < 0.0 ? 0.0 : theVolume)
if (newValue != theVolume) {
print("Tentative volume (%5.2f) was out of range; reset to %5.2f", theVolume, newValue)
}
theAddress = AudioObjectPropertyAddress.init(mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain)
//set the selector to mute or not by checking if under threshold (5% here)
//and check if a mute command is available
muteValue = (newValue < 0.05)
if (muteValue) {
theAddress.mSelector = kAudioDevicePropertyMute
hasMute = AudioObjectHasProperty(defaultDevID, &theAddress)
if (hasMute) {
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canMute)
if (theError != noErr || !(canMute.boolValue))
{
canMute = false
print("Should mute device 0x%0x but did not succeed",defaultDevID)
}
}
else {canMute = false}
} else {
theAddress.mSelector = kAudioHardwareServiceDeviceProperty_VirtualMainVolume
//theAddress.mSelector = kAudioHardwareServiceDeviceProperty_VirtualMasterVolume
}
// **** now manage the volume following the what we found ****
//be sure the device has a volume command
if (!AudioObjectHasProperty(defaultDevID, &theAddress)) {
print("The device 0x%0x does not have a volume to set", defaultDevID)
return
}
//be sure the device can set the volume
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canSetVol)
if ( theError != noErr || !canSetVol.boolValue ) {
print("The volume of device 0x%0x cannot be set", defaultDevID)
return
}
//if under the threshold then mute it, only if possible - done/exit
if (muteValue && hasMute && canMute.boolValue) {
muted = 1
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, nil, UInt32(MemoryLayout.size(ofValue: muted)), &muted)
if (theError != noErr) {
print("The device 0x%0x was not muted",defaultDevID)
return
}
} else { //else set it
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, nil, UInt32(MemoryLayout.size(ofValue: newValue)), &newValue)
if (theError != noErr) {
print("The device 0x%0x was unable to set volume", defaultDevID)
}
//if device is able to handle muting, maybe it was muted, so unlock it
if (muteOff && hasMute && canMute.boolValue) {
theAddress.mSelector = kAudioDevicePropertyMute
muted = 0
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, nil, UInt32(MemoryLayout.size(ofValue: muted)), &muted)
}
}
if (theError != noErr) {
print("Unable to set volume for device 0x%0x", defaultDevID)
}
}
private func systemVolumeSetMuted(_ m:Bool) {
var defaultDevID: AudioDeviceID = kAudioObjectUnknown
var theAddress: AudioObjectPropertyAddress
var hasMute: Bool
var canMute: DarwinBoolean = true
var theError: OSStatus = noErr
var muted: UInt32 = 0
defaultDevID = obtainDefaultOutputDevice()
if (defaultDevID == kAudioObjectUnknown) {
//device not found
print("Audio device unknown")
return
}
theAddress = AudioObjectPropertyAddress.init(mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain)
muted = m ? 1 : 0
hasMute = AudioObjectHasProperty(defaultDevID, &theAddress)
if (hasMute)
{
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canMute)
if (theError == noErr && canMute.boolValue)
{
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, nil, UInt32(MemoryLayout.size(ofValue: muted)), &muted)
if (theError != noErr) {
print("Cannot change mute status of device 0x%0x", defaultDevID)
}
}
}
}
private func fadeSystemVolumeToMutePrivate(seconds:Float) {
// prevent muting times longer than 10 seconds
var secs = (seconds > 0) ? seconds : (seconds*(-1.0))
secs = (secs > 10.0) ? 10.0 : secs
let currentVolume = getSystemVolume()
let delta = currentVolume / (seconds*2)
var secondsLeft = secs
var newVolume = currentVolume
while(secondsLeft > 0) {
newVolume = newVolume - delta;
setSystemVolume(theVolume:newVolume)
Thread.sleep(forTimeInterval: 0.5)
secondsLeft -= 0.5
}
setSystemVolume(theVolume: currentVolume, muteOff: false)
}
private func getSystemVolumeIsMuted() -> Bool
{
var defaultDevID: AudioDeviceID = kAudioObjectUnknown
var theAddress: AudioObjectPropertyAddress
var hasMute: Bool
var canMute: DarwinBoolean = true
var theError: OSStatus = noErr
var muted: UInt32 = 0
var mutedSize = UInt32(MemoryLayout.size(ofValue: muted))
defaultDevID = obtainDefaultOutputDevice()
if (defaultDevID == kAudioObjectUnknown) {
//device not found
print("Audio device unknown")
return false // works, but not the best return code for this
}
theAddress = AudioObjectPropertyAddress.init(mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain)
hasMute = AudioObjectHasProperty(defaultDevID, &theAddress)
if (hasMute) {
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canMute)
if (theError == noErr && canMute.boolValue) {
theError = AudioObjectGetPropertyData(defaultDevID, &theAddress, 0, nil, &mutedSize, &muted)
if (muted != 0) {
return true
}
}
}
return false
}
@_cdecl("getMainVolume")
public func getMainVolume() -> Float {
return getSystemVolume()
}
@_cdecl("setMainVolume")
public func setMainVolume(newVolume:Float) {
setSystemVolume(theVolume:newVolume)
}
@_cdecl("isMainVolumeMuted")
public func isMainVolumeMuted() -> Bool{
return getSystemVolumeIsMuted()
}
@_cdecl("muteMainVolume")
public func muteMainVolume() {
systemVolumeSetMuted(true)
}
@_cdecl("unmuteMainVolume")
public func unmuteMainVolume() {
systemVolumeSetMuted(false)
}
@tstephansen
Copy link
Author

Just wanted to add that I'm primarily a dotnet developer so I have no clue if there's a better way to do this or if what I did was even necessary but it works so that's all I really care about. If you can improve it or have some pointers feel free to comment and let me know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment