📖   Chapter 5

Fast audio switcher

Create a hotkey to quickly switch between audio output sources.

Next Chapter
APIs used today Description
hs.alertDraws an alert message on the screen.
hs.audiodeviceAllows you to interact with your computer's audio devices.
hs.chooserCreates an Alfred-style chooser popup with filter-as-you-type.
hs.hotkeyEverything you need to bind functions to hotkeys.

What you’re building #

Today we’re going to build a keyboard-driven UI to quickly switch between your computer’s audio output devices. Once we’re done, you’ll be able to hit ⌘⇧Space to show a popup that lets you select an audio device by either typing the name in, or quickly hitting ⌘1-9.

The built hs.chooser API gives us a nice UI for free.

Introduction to hs.chooser #

Before we make our audio switcher, I want to quickly show you the hs.chooser library. A chooser takes a list of choices, and a function to be called when a selection is made.

Each choice has a text and subText field, which the chooser uses to render its UI.

copy
choice = {
  text = "My choice",
  subText = "Some text to display below it",
}

You can add any additional key/values you want to each choice object–this data will then be available to you in your selection callback handler.

copy
choice = {
  text = "My choice",
  subText = "Some text to display below it",
  extraField = {
    foo = "bar",
    baz = true,
  },
}

The example below creates a chooser to select between fruits, and binds a hotkey under cmd + shift + space to reveal it. When you select a fruit from the list of choices, the handleFruitSelection() function is called, and we show an alert saying which fruit was selected and whether or not it’s juicy.

Paste this snippet into your Hammerspoon console and press cmd + shift + space to test it out for yourself:

copy
handleFruitSelection = function(choice)
  local isJuicy = choice.juicy and 'juicy' or 'not juicy'

  hs.alert.show("Selected fruit: " .. choice.text .. ". It is " .. isJuicy)
end

fruitChooser = hs.chooser.new(handleFruitSelection)
fruitChooser:width(20) -- set the width of the UI

hs.hotkey.bind({ 'cmd', 'shift' }, 'space', function()
  fruitChooser:choices({
    {
      text = "Banana",  
      subText = "Nice and ripe",
      juicy = false,
    },
    {
      text = "Strawberry",
      subText = "They'll stain your shirt",
      juicy = true,
    }
  })

  fruitChooser:show()
end)

Create a new config file #

First, make a config file to hold all your audio switcher code.

copy
touch ~/.hammerspoon/audio-switcher.lua

And require it in your main config:

copy
require("audio-switcher")

Make a list of your audio devices #

The first thing you need to do is make a list of the audio devices available on your computer. This getDeviceChoices() function loops through all the audio devices, and creates a single choice with text, subText, and uuid that we can pass to our chooser later.

copy
local function getDeviceChoices()
  local devices = hs.audiodevice.allOutputDevices()
  local choices = {}

  for i, device in ipairs(devices) do
    -- We can show a different icon if the device is currently muted or not.
    icon = "🔊"

    if device:outputMuted() then
      icon = "🔇"
    end

    local subText = icon

    -- Let's show the current volume of each device as the chooser sub text.
    if device:outputVolume() then
      subText = subText .. " Volume " .. math.floor(device:outputVolume()) .. "%"
    end

    choices[i] = {
      text = device:name(),
      subText = subText,
      
      -- Save the uuid for later, so we can switch to this device when selected.
      uuid = device:uid()
    }
  end

  return choices
end

When I call this function on my computer, it returns a table like this:

{ 
  {
    subText = "🔊 Volume 75%",
    text = "David’s AirPods Pro",
    uuid = "28-f0-33-72-b9-38:output"
  },
  {
    subText = "🔊",
    text = "DisplayPort",
    uuid = "AppleGFXHDAEngineOutputDP:0:{AC10-A131-3058344C}"
  },
  {
    subText = "🔊 Volume 74%",
    text = "Built-in Output",
    uuid = "AppleHDAEngineOutput:1F,3,0,1,2:0"
  },
  {
    subText = "🔊 Volume 55%",
    text = "Built-in Line Output",
    uuid = "AppleHDAEngineOutput:1F,3,0,1,3:1"
  }, 
  {
    subText = "🔊 Volume 55%",
    text = "Built-in Line Output",
    uuid = "AppleHDAEngineOutput:1F,3,0,1,4:2"
  }, 
  {
    subText = "🔊",
    text = "Built-in Digital Output",
    uuid = "AppleHDAEngineOutput:1F,3,0,1,5:3"
  }, 
  {
    subText = "🔊 Volume 45%",
    text = "Samson C01U Pro Mic",
    uuid = "AppleUSBAudioEngine:Samson Technologies:Samson C01U Pro Mic:14311200:2,1"
  }, 
  {
    subText = "🔊",
    text = "Universal Audio Thunderbolt",
    uuid = "com_uaudio_driver_UAD2AudioEngine:0"
  }
}

Create the interactive chooser #

Just like in the introduction above, we’ll create a chooser and give it a callback function to run whenever a choice is selected.

When an audio device is selected, you’ll set it as the system default, make sure it gets unmuted, and show a nice message indicating the device has been switched to.

copy
-- This function is called when an option is selected from the chooser.
local function handleAudioChoice(choice)
  -- If nothing was selected, we can just return early.
  if not choice then
    return
  end

  -- The `choice` is one of the table entries returned from our
  -- `getDeviceChoices()` function.

  -- Find the audio hardware device by UUID.
  local device = hs.audiodevice.findDeviceByUID(choice.uuid)

  -- Make this device the system default.
  device:setDefaultOutputDevice()

  -- Unmute the device, in case it's muted.
  device:setOutputMuted(false)

  hs.alert("Switched to " .. choice.text)
end

local audioChooser = hs.chooser.new(handleAudioChoice)

-- Set the width of the chooser when rendered:
audioChooser:width(20)

Bind the chooser to a hotkey #

Last, we need to wire this up to a hotkey so that you can show the chooser whenever you want to switch audio devices. The below snippet binds ⌘⇧Space to show the audio chooser:

copy
hs.hotkey.bind({ 'cmd', 'shift' }, 'space', function()
  -- Load all our hardware devices
  local choices = getDeviceChoices()

  -- Set the selectable choices here.
  audioChooser:choices(choices)

  -- Show the chooser.
  audioChooser:show()
end)

Monitor input switcher

Get the entire script #

Want to just paste in this whole project to your audio-switcher.lua file?

copy
TK fill this in at the very end once we're sure all the code is solid
Monitor input switcher