Automate Your Mac’s Audio Input with a Simple Script

How a shell script and the HammerSpoon app can save you from microphone embarrassment

Published on
Oct 12, 2024

Read time
5 min read

Introduction

The rise of remote work has brought with it a new set of video call-related problems. The fact that I am a software engineer—someone expected to know how these things work—only makes the embarrassment worse when issues arise.

Recently, I have found myself contending with a particularly annoying problem. Often, MacOS seems determined to choose the wrong audio input at the worst possible moment.

To be fair to my laptop, it’s not entirely its fault. My audio setup is a a bit of mess, for two main reasons:

  1. I used to produce music as a hobby and so my home office is filled with audio equipment.
  2. I like to work from different locations: my home office, my company’s office, and at one of my favourite coffee shops.

This means that I have a lot of audio devices connecting to (or disconnecting from) my laptop, and the default audio input often changes to something I don’t want. Too often, I join a video call and no one can hear me. Of course, I quickly switch to the correct input, but the damage is done.

I had a look around for some software that could help me with this, but I couldn’t find anything. Time to code a solution!

What I Want

To solve this, I want to specify my preferred audio inputs so that, whatever is connected or disconnected, my laptop will always set my preference to the default.

For me, this is:

  1. Jabra Evolve2 40 SE—if connected, I want to use this.
  2. Yeti Stereo Microphone—the next best option, though it’s only available in my home office.
  3. MacBook Pro Microphone—my laptop’s microphone, used as a fallback.

All my other audio devices are intended for output only and should be ignored. The main reason we’re in this mess is that MacOS will happily switch to any audio device that is connected, even if it’s output only!

The Solution

My solution involves:

  • a shell script using the SwitchAudioSource library, and
  • the HammerSpoon app—a powerful automation tool for MacOS that allows you to write Lua scripts to automate tasks.

Whenever an audio device is connected or disconnected, we will use HammerSpoon to run our shell script, which will set the audio input to our preferred device.

Part 1: The Shell Script

First, let’s install the SwitchAudioSource library via Homebrew:

brew install switchaudio-osx

Next, we’ll want to grab the location of the SwitchAudioSource binary. We can do this by running:

which SwitchAudioSource

For me, this returned /opt/homebrew/bin/SwitchAudioSource.

After that, we’ll write our script. I’ve named mine switch_audio_input.sh and put it in my ~/scripts directory, making sure to reference the correct path to SwitchAudioSource. Here’s the script:

# ~/scripts/switch_audio_input.sh
PREFERRED_DEVICES=("Jabra Evolve2 40 SE" "Yeti Stereo Microphone")

AVAILABLE_INPUTS=$(/opt/homebrew/bin/SwitchAudioSource -a -t input)

if [ -n "$1" ]; then
    echo "Adding $1 to available inputs"
    AVAILABLE_INPUTS="$AVAILABLE_INPUTS"$'\n'"$1"
fi

echo "Available inputs: $AVAILABLE_INPUTS"

for DEVICE in "${PREFERRED_DEVICES[@]}"; do
    if echo "$AVAILABLE_INPUTS" | grep -q "$DEVICE"; then
        /opt/homebrew/bin/SwitchAudioSource -t input -s "$DEVICE"
        osascript -e "display notification \"Input switched to $DEVICE\" with title \"Audio Input\""
        exit 0
    fi
done

/opt/homebrew/bin/SwitchAudioSource -t input -s "MacBook Pro Microphone"
osascript -e "display notification \"Input switched to default: MacBook Pro Microphone\" with title \"Audio Input\""

In this script, we set our preferred devices in the PREFERRED_DEVICES array. Order matters here, as we will try to set the audio input to the first device in the array, then the second, and so on.

You should update this array with your own preferred devices. To find the names of your devices, make sure they are connected, then run the following command in the terminal:

SwitchAudioSource -a -t input

Our script accepts an optional argument, which is the name of any input device that has just been connected. I found this helpful in getting around any delays in SwitchAudioSource detecting the newly connected device. At the end of the script, if no preferred devices are available, we set the default input to the MacBook Pro Microphone. Also, whenver our script runs, it will display a notification with the name of the device it has switched to, handled by the osascript command.

To run the script, we’ll need to ensure it’s executable:

chmod +x ~/scripts/switch_audio_input.sh # update with your chosen path

We can then test it by running it with no arguments:

bash ~/scripts/switch_audio_input.sh

If everything is working, you should see a notification telling you which audio input has been set!

But we don’t want to have to run this script manually every time we connect a new audio device. Let’s automate that!

Part 2: HammerSpoon

Hammerspoon is an automation tool for MacOS that allows you to write Lua scripts to automate tasks: here, we want to recognise when an audio device is connected or disconnected. There are other options for this kind of automation (such as using a combination of Automator, AppleScript, and launchd) but, from my research, HammerSpoon was the simplest.

We’ll start by installing HammerSpoon from its official website.

Once installed and unzipped, go into the application’s preferences and enable accessibility permissions. I also recommend enabling the “Launch Hammerspoon at login” option. For debugging purposes, we can open the HammerSpoon console by clicking on the icon in the menu bar and selecting “Open Console”.

I’m going to write my HammerSpoon script in the default ~/.hammerspoon/init.lua file, but you can create a separate file if your needs are more complex of you’re already using HammerSpoon for something else.

-- ~/.hammerspoon/init.lua
local log = hs.logger.new("audio_watcher", "info")

local function audioDeviceWatcherCallback(event)
  -- trigger the script when an input device changes
  if event == "dev#" then
    -- add a delay to allow the system to fully switch devices
    hs.timer.doAfter(2, function()
      local inputDevice = hs.audiodevice.defaultInputDevice():name()

      local logFile = "~/scripts/audio_input_log.txt"

      hs.execute("~/scripts/switch_audio_input.sh "
        .. inputDevice
        .. " > "
        .. logFile
        .. " 2>&1 &"
      )

      log.i("Script executed and notification sent")
    end)
  end
end

hs.audiodevice.watcher.setCallback(audioDeviceWatcherCallback)
hs.audiodevice.watcher.start()

This script listens for changes in the connected audio devices and runs our shell script when a change is dedicated. We add a short delay of two seconds to allow the system time to fully switch devices before running our script. I’m also logging the output of the script to a file, audio_input_log.txt, in my ~/scripts directory, which is useful for debugging. If you run into any issues, make sure to check the HammerSpoon console and the audio_input_log.txt for any error messages.

Make sure to reload the HammerSpoon configuration by clicking on the icon in the menu bar and selecting “Reload Config”. Now, when you connect or disconnect an audio device, you should see a notification telling you which audio input has been set!

And that’s everything you need. I hope this helps you streamline your audio input setup—and here’s hoping that I’ll never have to deal with microphone embarrassment again!

© 2024 Bret Cameron