Fast audio switcher
Create a hotkey to quickly switch between audio output sources.
Next ChapterAPIs used today | Description |
hs.alert | Draws an alert message on the screen. |
hs.audiodevice | Allows you to interact with your computer's audio devices. |
hs.chooser | Creates an Alfred-style chooser popup with filter-as-you-type. |
hs.hotkey | Everything 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.
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.

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.

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:

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.

touch ~/.hammerspoon/audio-switcher.lua
And require it in your main config:

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.

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.

-- 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:

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?

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