Ubuntu – Pulseaudio with external sound adapter: work around broken sound

audio-volumepulseaudioUbuntuxfce

I'm using an external USB sound adapter on a laptop that has an internal sound card. There are some issues:

  • The applications by default continue to use internal sound card. I can work around this manually by opening pavucontrol: going to "output devices", finding the adapter output and clicking on "set as fallback", and sometimes finding an already playing application in "Playback" tab and changing its sink.

  • The keyboard volume buttons still operate on the internal sound card, not the external adapter.

  • The adapter has physical volume-up,down and toggle-mute buttons. When pressed, the system performs the expected actions, but on the internal sound card, not the external adapter. Also a microphone mute button, untested.
  • Setting volume of the external adapter using pavucontrol works but if set below 35% there's no sound at all. Since the volume is very loud, 35% is just okay for quiet sound sources, unacceptable for loud sources. I can work around this manually by setting per-application volume limiting (clicking in pavucontrol window), but it's tedious and has to be redone whenever an application starts a new stream.

How to have:

  • (1) applications play to that adapter by default
  • (2) the keyboard volume buttons work on a chosen sound card (here, the adapter)
  • (3) the physical adapter buttons work on a chosen sound card (here, the adapter)
  • (4) volume setting for the adapter that can go below 35% without going straight to silence

System is Ubuntu 14.10 running XFCE 4.10. Adapter is "ID 0d8c:000c C-Media Electronics, Inc. Audio Adapter"

Best Answer

I have an experimental workaround.

First get adapter sink name

First one has to figure out the sink name for the adapter. Open a shell. We'll assume bash and prevent any localization issue by switching to the default locale:

export LC_ALL=C 

To get a list of sinks:

pacmd list-sinks | grep name:

You can read the output and copy-paste manually the name into a command line: ADAPTER_SINK_NAME=name_in_your_setup

More automatically, the line below finds the name of the first non-pci adapter, which will work for me and should work in many cases.

ADAPTER_SINK_NAME=$( pacmd list-sinks | sed -n 's/^.*name: <\([^>]*\.usb[^>]*\)>$/\1/p' | head -n 1 )
echo $ADAPTER_SINK_NAME

Alternative using pactl (but do run the export line above or it won't find anything in non-default locales):

ADAPTER_SINK_NAME=$( pactl list sinks | sed -n 's/^.*Name: \(.*\.usb.*\)$/\1/p' | head -n 1 ) 
echo $ADAPTER_SINK_NAME

Get partial fix (1) (2) (3)

Now this will provide points (1) (2) (3) of the question:

pactl set-default-sink $ADAPTER_SINK_NAME

Interestingly, I first used pacmd set-default-sink ... (pacmd instead of pactl) which provided (2) and (3) but not always (1).

Get full fix (1) to (4)

It's based on Can I use PulseAudio to playback music on two sound cards simultaneously? though the principle is rather to play on "one sound card simultaneously". ;-)

pacmd load-module module-combine sink_name=adapter-soft-volume slaves=$ADAPTER_SINK_NAME
pactl set-default-sink adapter-soft-volume

Now everything works as requested in the question.

Clean-up.

If you run the commands above several times, there will be several combine sinks and the first one will be used, not the last one. If for any reason you want to start over, first run this to clean up the combined sink:

pacmd unload-module module-combine

Don't use the cleanup now if you want to continue

Bonus: move currently playing streams to the adapter

Command above work for new streams, not those already playing. This will adjust what's needed:

for SINK_ID in $(pactl list sink-inputs | sed -n 's/^Sink Input #\([0-9]*\)$/\1/p')
do
    echo Sink $SINK_ID
    pactl move-sink-input $SINK_ID $ADAPTER_SINK_NAME
    pactl move-sink-input $SINK_ID adapter-soft-volume
done

It will try to move all sink inputs, including the combined one which would cause a circular path. Fortunately, pulseaudio will do the right thing and print Failure: Invalid argument once to indicate that.

Automate the thing

Here's the script providing benefits (1) to (4). You might want to add the move-sink-input from above. It might be interesting to have it run whenever the adapter is plugged it.

#!/bin/bash

export LC_ALL=C

ADAPTER_SINK_NAME=$( pacmd list-sinks | sed -n 's/^.*name: <\([^>]*\.usb[^>]*\)>$/\1/p' | head -n 1 )

if [[ -z "${ADAPTER_SINK_NAME:-}" ]]
then
    ADAPTER_SINK_NAME=$( pactl list sinks | sed -n 's/^.*Name: \(.*\.usb.*\)$/\1/p' | head -n 1 ) 
fi

echo will plug on $ADAPTER_SINK_NAME

# partial fix, can be run anyway to provide partial benefit if later steps fail
pactl set-default-sink $ADAPTER_SINK_NAME

pacmd unload-module module-combine # not useful first, used to clean things up if run several times

pacmd load-module module-combine sink_name=adapter-soft-volume slaves=$ADAPTER_SINK_NAME
pactl set-default-sink adapter-soft-volume
Related Question