Skip to content

Instantly share code, notes, and snippets.

@kbravh
Last active June 6, 2024 12:44
Show Gist options
  • Save kbravh/1117a974f89cc53664e55823a55ac320 to your computer and use it in GitHub Desktop.
Save kbravh/1117a974f89cc53664e55823a55ac320 to your computer and use it in GitHub Desktop.
Switch audio output devices on Linux

Audio Output Switcher

This script will cycle to the next available audio output device. It can be tied to a hotkey to easily be triggered. This is handy, for example, for swapping between speakers and headphones.

This script will work on systems running PulseAudio or Pipewire services.

Install

  1. Download the audio-device-switch.sh script and place it in /usr/local/bin.
  2. Make the script executable: sudo chmod 755 /usr/local/bin/audio-device-switch.sh.
  3. Open the Keyboard Shortcuts settings page, add a new shortcut, tell it to execute audio-device-switch.sh, and set up your shortcut!
  4. Install the notify-send library if you want to see a popup notification when the audio device switches: sudo apt install libnotify-bin.

Customizations

Feel free to modify this script and make it your own. Some ideas for customization:

Different icon in the notification

Line 83 of the script calls notify-send with the -i flag which defines which icon is displayed. Stock icons are found in:

  • /usr/share/icons/gnome/32x32
  • /usr/share/notify-osd/icons/

Acknowledgements

This is a more modern, robust rewrite of tsvetan's solution on the Ubuntu forums.

#!/bin/bash
# Check which sound server is running
if pgrep pulseaudio >/dev/null; then
sound_server="pulseaudio"
elif pgrep pipewire >/dev/null; then
sound_server="pipewire"
else
echo "Neither PulseAudio nor PipeWire is running."
exit 1
fi
# Grab a count of how many audio sinks we have
if [[ "$sound_server" == "pulseaudio" ]]; then
sink_count=$(pacmd list-sinks | grep -c "index:[[:space:]][[:digit:]]")
# Create an array of the actual sink IDs
sinks=()
mapfile -t sinks < <(pacmd list-sinks | grep 'index:[[:space:]][[:digit:]]' | sed -n -e 's/.*index:[[:space:]]\([[:digit:]]\)/\1/p')
# Get the ID of the active sink
active_sink=$(pacmd list-sinks | sed -n -e 's/[[:space:]]*\*[[:space:]]index:[[:space:]]\([[:digit:]]\)/\1/p')
elif [[ "$sound_server" == "pipewire" ]]; then
sink_count=$(pactl list sinks | grep -c "Sink #[[:digit:]]")
# Create an array of the actual sink IDs
sinks=()
mapfile -t sinks < <(pactl list sinks | grep 'Sink #[[:digit:]]' | sed -n -e 's/.*Sink #\([[:digit:]]\)/\1/p')
# Get the ID of the active sink
active_sink_name=$(pactl info | grep 'Default Sink:' | sed -n -e 's/.*Default Sink:[[:space:]]\+\(.*\)/\1/p')
active_sink=$(pactl list sinks | grep -B 2 "$active_sink_name" | sed -n -e 's/Sink #\([[:digit:]]\)/\1/p' | head -n 1)
fi
# Get the ID of the last sink in the array
final_sink=${sinks[$((sink_count - 1))]}
# Find the index of the active sink
for index in "${!sinks[@]}"; do
if [[ "${sinks[$index]}" == "$active_sink" ]]; then
active_sink_index=$index
fi
done
# Default to the first sink in the list
next_sink=${sinks[0]}
next_sink_index=0
# If we're not at the end of the list, move up the list
if [[ $active_sink -ne $final_sink ]]; then
next_sink_index=$((active_sink_index + 1))
next_sink=${sinks[$next_sink_index]}
fi
#change the default sink
if [[ "$sound_server" == "pulseaudio" ]]; then
pacmd "set-default-sink ${next_sink}"
elif [[ "$sound_server" == "pipewire" ]]; then
# Get the name of the next sink
next_sink_name=$(pactl list sinks | grep -C 2 "Sink #$next_sink" | sed -n -e 's/.*Name:[[:space:]]\+\(.*\)/\1/p' | head -n 1)
pactl set-default-sink "$next_sink_name"
fi
#move all inputs to the new sink
if [[ "$sound_server" == "pulseaudio" ]]; then
for app in $(pacmd list-sink-inputs | sed -n -e 's/index:[[:space:]]\([[:digit:]]\)/\1/p'); do
pacmd "move-sink-input $app $next_sink"
done
elif [[ "$sound_server" == "pipewire" ]]; then
for app in $(pactl list sink-inputs | sed -n -e 's/.*Sink Input #\([[:digit:]]\)/\1/p'); do
pactl "move-sink-input $app $next_sink"
done
fi
# Create a list of the sink descriptions
sink_descriptions=()
if [[ "$sound_server" == "pulseaudio" ]]; then
mapfile -t sink_descriptions < <(pacmd list-sinks | sed -n -e 's/.*alsa.name[[:space:]]=[[:space:]]"\(.*\)"/\1/p')
elif [[ "$sound_server" == "pipewire" ]]; then
mapfile -t sink_descriptions < <(pactl list sinks | sed -n -e 's/.*Description:[[:space:]]\+\(.*\)/\1/p')
fi
# Find the index that matches our new active sink
for sink_index in "${!sink_descriptions[@]}"; do
if [[ "$sink_index" == "$next_sink_index" ]]; then
notify-send -i audio-volume-high "Sound output switched to ${sink_descriptions[$sink_index]}"
exit
fi
done
@pgillet
Copy link

pgillet commented Jun 16, 2022

That's great, but I had to invert line 26 with line 27 to make it work for me: you must increment next_sink_index before accessing the next-sink element in the sinks array, right!?

Also I had leading whitespaces in the string returned for active_sink variable (line 9), so I had to change it with:
active_sink=$(pacmd list-sinks | sed -n -e 's/[[:space:]]*\*[[:space:]]index:[[:space:]]\([[:digit:]]\)/\1/p')

See https://gist.github.com/pgillet/534041a0f2ae1ae94aff2e74bf6096f1

@Superkikim
Copy link

Thank you. With your corrections, it works great. It's a shame that the author didn't fix it.

@pgillet
Copy link

pgillet commented Feb 21, 2023

Give me a ⭐ then 😜

@kbravh
Copy link
Author

kbravh commented Feb 21, 2023

@pgillet Thanks for your corrections! I've updated the above script (and given you a star 😉). I've also added support for PipeWire instead of just PulseAudio.

@Superkikim
Copy link

Give me a star then stuck_out_tongue_winking_eye

Déjà fait sur ton fork ^^

@fcaponetto
Copy link

fcaponetto commented Feb 16, 2024

For those using PulseAudio and looking to exclude a particular sink from their output, incorporating an awk filter into your command line can be very effective.

For instance, if you wish to omit sink index 5 from your results, your awk filter might look like this:

filter='
  /index: [0-9]+/ {
    if ($2 == "5") in_block = 1; else in_block = 0; print_index = ($2 != "5")
  }
  !in_block && print_index
  '
  
  sink_count=$(pacmd list-sinks | awk "$filter" | grep -c "index:[[:space:]][[:digit:]]")
  
  mapfile -t sinks < <(pacmd list-sinks | awk "$filter" | grep 'index:[[:space:]][[:digit:]]' | sed -n -e 's/.*index:[[:space:]]\([[:digit:]]\)/\1/p')

@VamshiKrishna-jillela
Copy link

I would be great, someone update or generate new script to switch between the audio input (microphones).

Thanks in advance

@tapmoo
Copy link

tapmoo commented Mar 7, 2024

And for those who want to skip a sink name (since my indexes change every boot), below is my hacky workaround to avoid using the microphone as output. Add this at the very bottom:

if [[ ${sink_descriptions[$sink_index]} =~ "USB Audio" ]]; then 
  exec "/usr/local/bin/audio-device-switch.sh"      # run again 
fi 
exit 

And change the exit at the bottom of the original code to break (inside the for loop & if).

@micoro
Copy link

micoro commented Jun 5, 2024

really helpful, I have been using this for a while now. There is something similar to change the input (microphone)?

@pgillet
Copy link

pgillet commented Jun 6, 2024

AFAIK, PipeWire and/or PulseAudio can list all audio input and output devices...
See also the comment from @kbravh ^^: https://gist.github.com/kbravh/1117a974f89cc53664e55823a55ac320?permalink_comment_id=4478290#gistcomment-4478290

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