While the sound architecture in Windows desktops is far from ideal, it’s still one of the most comfortable setups, at least from the user experience perspective. Most of the time it works and the default setup is suitable for vast majority of situations.
Meanwhile, ALSA in Linux has really made good progress. Most of the time, it does not require any configuration to deliver sound to desktop applications right after installing your favourite distribution. It almost levels with Windows from desktop experience perspective.
The difference comes when you want to do something even slightly unusual. On Windows it is very hard, users are down to finding some third party apps which might or might not do the job.
If you however run Linux and on its altar make an offering of your time and a prayer over the ALSA configuration, everything is possible. What’s more, Google’s search AI provides working examples (of course, bugged, but still somewhat useful), therefore within 1h I was able to get what I needed: sound simultaneously sent to multiple devices, adding a software volume where it is missing and mixing sound outputs.
In this article I will explain a configuration of 2 or more devices. Below are the 3 devices detected by ALSA on my desktop.
- 1st sound device – an intel chip on the motherboard. This one could be connected to a set of speakers, for example. I’m not using it right now.
- 2nd sound device – an audio chip onboard of my nVidia card, with a HDMI cable connected to my monitor, which has a built in speaker. Good whenever I want my ears to rest and get the sound from this speaker.
- 3rd device – Fiio USB-bluetooth adapter. It presents itself as an USB audio device, and has a transceiver for bluetooth to communicate with my headset
In a Windows-like setup, you’d like to simply enable 2 or 3 devices and switch between them within a few mouse clicks. As it turns out in this setup, the Linux version requires one click less 😉 Also, I’ve decided to cut out all the overhead, so there’s no pulseaudio, JACK or any other extras, just plain ALSA with minimal configuration. Let’s begin.
First, kernel drivers are needed so that ALSA can recognize the sound devices. This step can be skipped unless you manage your own kernel config. In my case, the typical drivers are needed:
- sound card support
- snd-hda-intel for intel HDA and HDMI devices
- CONFIG_SND_USB for any sound cards connected through USB, including some usb-bluetooth hubs like the Fiio.
The alsa-utils package provides a simple client to play audio files called aplay. It also allows listing sound devices, which is helpful to prepare the ALSA configuration. It will show devices here if the kernel drivers are compiled and loaded (if compiled as modules). For me it looks like this:
fst@ryba ~ % aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: Generic Analog [Generic Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0: PCH [HDA Intel PCH], device 1: Generic Digital [Generic Digital]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 3: HDMI 0 [ROG PG27AQ] <-- my monitor
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 7: HDMI 1 [HDMI 1]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 8: HDMI 2 [HDMI 2]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: NVidia [HDA NVidia], device 9: HDMI 3 [HDMI 3]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: PRO [FiiO BTA30 PRO], device 0: USB Audio [USB Audio] <-- usb/bluetooth
Subdevices: 1/1
Subdevice #0: subdevice #0
What’s worth noting from the above:
- card 0 remains unused. You can use it by declaring slave devices under hw0,0 and hw0,1 for analog and digital respectively.
- card 1 is the nVidia oncard sound chip connected to the speaker built into my monitor
- card 2 is the USB device
And here’s the /etc/asound.conf config file. You can also declare your config locally. As per the documentation, changing the config does not require restarting the ALSA service. Only restart the sound client such as mplayer to observe the changes.
The key to understanding this config is “stacking”. ALSA by default will pick a device and it will work unless you tell it to work in a specific way. This way is building stacks, like:
- define a hardware device using type hw
- connect a sound mixer to this hw device using dmix (needed when you want to play back more than 1 thing at a time)
- optionally apply a software volume mixer using softvol
- combine output of dmix/softvol to multiple sound cards using multi
- point the default device to point 4
# A PCM to mix sound output and send it to the HDMI hardware device
# only required settings are used,
# period_size specifies the amount of bytes consumed per interrupt. It
# requires a buffer_size larger than it to avoid sound stutter
# ipc_key is used to set an unique identifier of the device
pcm.dmixer_hdmi {
type dmix
ipc_key 2048
slave {
pcm "hw:1,3"
period_size 1024
buffer_size 4096
}
bindings {
0 0
1 1
}
}
# Per analogiam, mix and send output to the USB sound card
pcm.dmixer_fiio {
type dmix
ipc_key 4096
slave {
pcm "hw:2,0"
buffer_size 4096
period_size 1024
}
bindings {
0 0
1 1
}
}
# HDMI comes with hardware control in the monitor.
# This software volume control is plugged on top of
# dmix and allows applying a volume control to it.
pcm.hdmi_sv {
type softvol
slave.pcm dmixer_hdmi
control.name hdmi_volume
control.card 1
}
# finally a device to send the same sound as output
# to the selected sound cards
pcm.all {
type plug
slave.pcm {
type multi
slaves {
a { channels 2 pcm "dmixer_fiio" }
b { channels 2 pcm "hdmi_sv" }
}
bindings {
0 { slave a channel 0 }
1 { slave a channel 1 }
2 { slave b channel 0 }
3 { slave b channel 1 }
}
}
ttable [
[ 1 0 1 0 ]
[ 0 1 0 1 ]
]
}
# Instruct ALSA to use the "all" PCM whenever an app on the desktop
# needs to provide sound, as a default device. The stack is
# default->all->plug->multi->(softvol)->dmix->hw
pcm.!default {
type plug
slave.pcm "all"
}
# some apps don't use the PCM interface, chosing CTL instead
# such as alsamixer. This allows opening the usb device by default
# the '!' char replaces default configuration with creating the
# one specified in this config
ctl.!default {
type hw
card 2
}
That’s all. Switching between the devices can be achieved simply using a mixer app, or alsamixer in the terminal. For XFCE, I use xfce-mixer, but there are mixers also for Gnome or KDE.
Fun fact at the end. Before publishing this guide, which I hope will help you get familiar with ALSA, I did of course try Gemini AI to see if it would produce a reasonable configuration set. The outcome is very interesting. While the config itself was not bad in general, it had one bug that made it useless. Namely, the ipc_key is supposed to be an unique identifier, but the config provided had the same value for each device, which obviously will never work and will be hard to find out why. Maybe if this requirement (why is it even a configurable item, dear ALSA devs?) was more clearly defined in the docs on which Google’s model was trained wouldn’t result in such buggy answer?