Switch audio sink/output with keyboard shortcut

I want to switch between sound sinks/outputs on Ubuntu using a shortcut. I want the solution to work with any number of devices and switching all sink-inputs in the process.


Solution 1:

I want to provide my solution for anyone to use and improve as it got me pretty frustrated that none of the solutions (i.e. Audio output device, fast switch?) worked under all conditions.

Thus i created a python script with a little overkill (and bad and ugly regexs) to do the job:

#!/usr/bin/env python3
#this little script switches the sound sink on ubuntu
# https://askubuntu.com/questions/156895/how-to-switch-sound-output-with-key-shortcut/1203350#1203350 and https://askubuntu.com/questions/1011806/how-do-i-switch-the-audio-outputs-of-an-audio-device-from-cli?noredirect=1&lq=1 were helpful
import argparse
import logging
import subprocess
import re

#a simple representation of all relevant info of an audio sink for this script
class Sink:
    def __init__(self, index, name, state):
        self.index = index
        self.name = name
        self.state = state
        if state in ["RUNNING", "IDLE"]:
            self.selected = True
        else:
            self.selected = False

    def __str__(self):
        return 'sink\nindex: {self.index}\nname: {self.name}\nstate: {self.state}\nselected: {self.selected}\n'.format(self=self)

#a simple representation of all relevant info of an audio sink-input for this script
class Sink_Input:
    def __init__(self, index, application_name, sink, state):
        self.index = index
        self.application_name = application_name
        self.sink = sink
        self.state = state

    def __str__(self):
        return 'sink-input\nindex: {self.index}\napplication_name: {self.application_name}\nsink: {self.sink}\nstate: {self.state}\n'.format(self=self)

        
def get_sinks():
    pacmd_output = str(subprocess.check_output(["pacmd", "list-sinks"]))
    sinks_raw = pacmd_output.split("index: ")
    sinks = []
    for sink_raw in sinks_raw[1:]:
        index = int(re.findall("^\d+", sink_raw)[0])
        name = re.findall("device.description = \"[^\"]*\"", sink_raw)[0][22:-1]
        state = re.findall("state: [A-Z]*", sink_raw)[0][7:]
        sink = Sink(index, name, state)
        sinks.append(sink)
    return sinks

def get_sink_inputs():
    sink_inputs = []
    pacmd_output = str(subprocess.check_output(["pacmd", "list-sink-inputs"]))
    inputs_raw = pacmd_output.split("index: ")
    for input_raw in inputs_raw[1:]:
        index = int(re.findall("^\d+", input_raw)[0])
        sink = int(re.findall("sink: \d*", input_raw)[0][5:])
        application_name = re.findall("application.name = \"[^\"]*\"", input_raw)[0][20:-1]
        state = re.findall("state: [A-Z]*", input_raw)[0][7:]
        sink_input = Sink_Input(index, application_name, sink, state)
        sink_inputs.append(sink_input)
    return sink_inputs

def switch_to_next_sink(sinks, notify):
    current_sink = None
    next_sink = sinks[0]
    for i in range(len(sinks)):
        if sinks[i].selected:
            current_sink = sinks[i]
            if i == len(sinks) -1:
                next_sink = sinks[0]
            else:
                next_sink = sinks[i+1]
    #switch default sink to next sink
    subprocess.call(["pacmd", "set-default-sink", str(next_sink.index)])
    #move all apps to next sink
    for sink_input in get_sink_inputs():
        subprocess.call(["pacmd", "move-sink-input", str(sink_input.index), str(next_sink.index)])
    if notify:
        subprocess.call(["notify-send", "Changed audio sink", "new audio sink is " + next_sink.name])

def main():
    parser = argparse.ArgumentParser(description='''Switches to the 'next' audio sink on Ubuntu and can provide additional info on sound sinks.
    If no arguments are passed only the next audio sink is selected.
    For ease of use add /usr/bin/python3 /home/sebi/misc/switch_sound_sink.py as keyboard shortcut i.e. Super+Shift+S (Super+O somehow not working) ''')
    parser.add_argument('-s', '--state',
                        help='boiled down output of pacmd list-sinks and list-sink-inputs', action='store_true')
    parser.add_argument('-n', '--notify', help='send notification to the desktop', action='store_true')
    
    logLevelsRange = [logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARN, logging.ERROR, logging.CRITICAL]
    parser.add_argument('-l', '--logLevel', help='the log level', type=int, choices=logLevelsRange,
                        default=logging.INFO)

    args = parser.parse_args()

    if args.state:
        sinks = get_sinks()
        for sink in sinks:
            print(sink)
        sink_inputs = get_sink_inputs()
        for sink_input in sink_inputs:
            print(sink_input)
    else:
        sinks = get_sinks()
        switch_to_next_sink(sinks, args.notify)
        
main()

I added optional desktops notifications but don't use them as they can't be shown only temporarily. (How can I send a custom desktop notification?)