Pete Hinchley: Use PowerShell to Mute the Volume when Headphones are not Connected to a Windows Computer

I was recently asked to devise a solution to prevent staff from listening to audio on a Windows computer without using headphones (i.e. to disable sound via speakers, but to allow sound via headphones). An odd request, I admit, and not quite as simple to implement as I first expected.

In many computers, the internal speakers/audio-out jack, and the headphone jack, are represented as distinct devices that can be independently enabled and disabled. In these systems, preventing sound via speakers, but allowing sound via headphones, is as straight forward as disabling the associated device. However, in some computers, the audio interfaces are all managed via a single device, and it isn't possible to disable one interface, without also disabling the others.

To cater for this scenario, I decided on the following approach:

  1. I would mute the sound when headphones were not attached to the computer, and unmute the sound when headphones were attached.
  2. I would override any attempt to unmute the sound when headphones were not attached.

Instead of muting the sound, I could have chosen to completely disable the Windows Audio service (audiosrv) - this would have been an easier solution - but stopping and starting the service seemed like an overly heavy-handed approach.

The first step in building the solution was to find a way of detecting when headphones were plugged into to the computer. Unfortunately, on the system I was using, which managed all audio through a single device, this proved difficult. In the end, instead of trying to proactively hook a system event that signalled a change in the status of the headphone jack, I decided to look for a registry value that would toggle state whenever headphones were connected. This was relatively easy - I started Process Monitor, filtered on the RegSetValue operation, and then plugged/unplugged a set of headphones. This revealed that a specific registry value was changed to 1 whenever headphones were connected, and 0 when they were disconnected.

Note: I have intentionally omitted the actual registry value from this article, as it is specific to the model of computer (and audio device driver) that I was using, and therefore unlikely to be relevant to others. You should use Process Monitor to determine the path that is used on your system.

Now that I had a way of programmatically determining if headphones were connected, I needed a way of triggering an action whenever the value of the corresponding registry entry was changed. For this I decided to use a registry monitor.

Before delving into the actual code I used in the final solution, it might be helpful to provide a quick generic example of how to monitor changes to a registry value using the register-wmievent PowerShell cmdlet.

The following code will monitor the Bar value under the HKLM:\Software\Foo key, and output a message (the name of the key) to the console each time the value is modified. Note: The script assumes the registry entry already exists.

As you can see, the auto-generated $event variable within the action scriptblock is used to retrieve information about the registry value that was changed.

# always use double slashes for the registry key.
$key = "software\\foo"

# the registry value (under the key) to monitor.
$val = "bar"

# the name assigned to the event filter.
$sourceidentifier = "regmon"

# the wmi query defining the event filter.
$query = "select * from registryvaluechangeevent where hive = 'HKEY_LOCAL_MACHINE' and keypath = '$key' and valuename = '$val'"

# the scriptblock to run when the event is triggered.
$action = {
  $e = $event.sourceargs.newevent
  write-host $("{0} was passed into the event action" -f $event.messagedata.foo)
  write-host $("HKLM:{0}\{1} was changed" -f $e.keypath, $e.valuename)
}

# data to pass into the action.
$messagedata = @{ foo = "Foobar" }

# start monitoring.
register-wmievent -sourceidentifier $sourceidentifier -query $query -action $action -messagedata $messagedata

To stop the event monitor, run the following command:

unregister-event "regmon"

The next step involves programmatically muting the sound of the computer. There is a great example on StackOverflow demonstrating how this can be achieved with PowerShell via the Core Audio API. Unfortunately, invoking COM from managed code is never pretty, so after looking for a cleaner solution, I decided to leverage the NAudio Library.

Working with NAudio in PowerShell is straight forward. Just download the compiled library, reference the extracted NAudio.dll, and start exploring the API.

As an example, the following script uses the library to prevent users from changing the system volume level. The approach is similar to that used in the previous example; however, in this case we are using the register-objectevent cmdlet to hook the OnVolumeNotification event, which is then used to reset the volume to the value retrieved (via the MasterVolumeLevelScalar property) at the time the handler was registered.

add-type -path 'c:/naudio/naudio.dll'

$devices = new-object naudio.coreaudioapi.mmdeviceenumerator
$defaultdevice = $devices.getdefaultaudioendpoint([naudio.coreaudioapi.dataflow]::render, [naudio.coreaudioapi.role]::multimedia)
$volumecontrol = $defaultdevice.audioendpointvolume

register-objectevent -inputobject $volumecontrol -sourceidentifier 'volume' -eventname onvolumenotification -action { $volumecontrol.mastervolumelevelscalar = $event.messagedata } -messagedata $volumecontrol.mastervolumelevelscalar

And this time, to unregister the monitor:

unregister-event "volume"

We can now combine the concepts explored in these two examples - using a registry monitor to detect the presence of headphones, and another monitor to detect changes in system volume - to develop the final solution (as shown below).

When the script is started it will query the registry to determine if headphones are connected (I am using a bogus registry path of HKLM\Software\Audio\Headphones; you should use the path that is appropriate for your system), and if they are not, it will mute the volume, and register an event handler to detect changes to it. It will also configure a registry monitor to detect when the headphone status changes. When headphones are connected, the handler will remove the event monitor for detecting changes to the volume, and then unmute the system; and conversely, when headphones are removed, the volume will be muted, and the volume control event monitor will be re-instated.

add-type -path 'c:/naudio/naudio.dll'

$key = "software\\audio";
$val = "headphones"

$devices = new-object naudio.coreaudioapi.mmdeviceenumerator
$defaultdevice = $devices.getdefaultaudioendpoint([naudio.coreaudioapi.dataflow]::render, [naudio.coreaudioapi.role]::multimedia)
$volumecontrol = $defaultdevice.audioendpointvolume

function mute($volumecontrol) {
  $volumecontrol.mute = $true
  register-objectevent -inputobject $volumecontrol -sourceidentifier mute -eventname onvolumenotification -action { $volumecontrol.mute = $true }
}

function unmute($volumecontrol) {
  unregister-event -sourceidentifier mute
  $volumecontrol.mute = $false
}

$action = {
  $volumecontrol = $event.messagedata.volumecontrol

  $mute = $event.messagedata.mute
  $unmute = $event.messagedata.unmute

  $key = $event.sourceeventargs.newevent.keypath
  $val = $event.sourceeventargs.newevent.valuename

  $status = get-itemproperty -path hklm:$key -name $val | select -expandproperty $val

  if ($noheadphones = $status -eq 0) { & $mute $volumecontrol } else { & $unmute $volumecontrol }
}

$wmi = @{
  sourceidentifier = "regmon";
  query = "select * from registryvaluechangeevent where hive = 'hkey_local_machine' and keypath = '$key' and valuename = '$val'";
  action = $action;
  messagedata = @{ volumecontrol = $volumecontrol; mute = ${function:mute}; unmute = ${function:unmute} }
}

$status = get-itemproperty -path hklm:$($key -replace '\\', '\') -name $val | select -expandproperty $val
if ($noheadphones = $status -eq 0) { mute $volumecontrol }

$result = register-wmievent @wmi

It is worth pointing out that both event monitors used in the script (one for the registry, and the other for the volume) retrospectively respond to changes of state. i.e. the event handlers fire after the event has occurred; they do not actually block the event. In other words, if a user attempts to unmute the volume when headphones are not connected, there will be a brief period (in the order of milliseconds) before the event handler is effective in reversing the action.

The final step in implementing the solution is to create a scheduled task, running under the context of the local system account, that will launch the PowerShell script on system startup. This will ensure the requirement for using headphones to listen to audio will apply to all users of the computer, and furthermore, it will prevent non-privileged users from circumventing the restriction by killing the executing process (only local administrators will be able to terminate the script).

Note: When executing the above script via a non-interactive PowerShell session, it is essential you use the -noexit switch, as the event monitors will be torn down when the parent process exits. For example:

powershell.exe -noexit -file c:\scripts\mute.ps1

I don't expect too many people will need to implement a solution such as this, but hopefully the examples that demonstrate the use of event monitors and the NAudio library via PowerShell are helpful to others.