In a previous blog, I used a polling to loop to detect and configure an audio device that might become available only after boot. The better approach is to trigger the configuration from the kernel directly using a device manager. Alpine Linux uses Busybox' mdev as device manager. This blog explains how to use it.
Hotplugging
When the Linux kernel detects a hardware change it will call whatever is configured at /proc/sys/kernel/hotplug. In the default Alpine Linux setup that would be /sbin/mdev, which is a softlink to /bin/busybox.
Busybox mdev
Mdev is not a drop-in replacement for the more commonly used udev. Mdev is more suitable for smaller and simpler comfigurations, e.g. without desktop, and without D-BUS; perfect for our single purpose snapcast setup.
A file /etc/mdev.conf is used to configure mdev. The basic Alpine Linux mdev installation and configuration is described here. For me mdev worked out of the box; all I needed to was configure a script to be called when the audio device became available.
/etc/mdev.conf
The simplified syntax for /etc/mdev.conf is:
device uid:gid permissions @command
where:
- device is a regular expression to filter out the device for which the kernel detected a change
- uid:gid sets ownership of the device, e.g. root:audio
- permission: in octal, e.g. 664 for read/write by owner and group, and read for others
- command: preceded with an '@' for the command to be executed on device creation/deletion
For our case we update /etc/mdev.conf as follows:
#SUBSYSTEM=sound;.* root:audio 0660
SUBSYSTEM=sound;.* root:audio 0660 @/etc/mdev/sound.sh
This will call /etc/mdev/sound.sh each time a change in a sound device has been detected by the Linux kernel.
/etc/mdev/sound.sh
The script called from mdev willl have a few environment variables set:
- MDEV: the device that has the change
- ACTION: what the change was; add for a newy created device; remove for removal.
The script:
ALSACONFFILE=/etc/asound.conf
LOGGER=/usr/bin/logger
AMIXER=/usr/bin/amixer
APLAY=/usr/bin/aplay
RC_SERVICE=/sbin/rc-service
PCMDEV=snd/pcm
source /etc/conf.d/snapcast-client
test_audiodev() {
echo $(cat /sys/class/sound/card*/id | grep ${AUDIOCARD})
}
case "$ACTION" in
add)
${LOGGER} "Sound device ${MDEV} added"
DEV=$(test_audiodev)
${LOGGER} ": DEV: ${DEV}"
if [ _${MDEV:0:7} == _${PCMDEV} ] && [ _${DEV} == _${AUDIOCARD} ] ; then
${LOGGER} Audio device ${AUDIOCARD} available, making it the default
echo 'defaults.pcm.!card' ${AUDIOCARD} > ${ALSACONFFILE}
echo 'defaults.pcm.!device' ${AUDIODEV} >> ${ALSACONFFILE}
echo 'defaults.ctl.!card' ${AUDIOCARD} >> ${ALSACONFFILE}
${LOGGER} Setting the volume to ${ANNOUNCEMENTVOLUME} for annoucement
${AMIXER} set ${AUDIOCHANNEL} ${ANNOUNCEMENTVOLUME}
${APLAY} -q ${ANNOUNCEMENT}
${AMIXER} set ${AUDIOCHANNEL} ${AUDIOVOLUME}
${LOGGER} Setting the volume to ${AUDIOVOLUME}
${AMIXER} set ${AUDIOCHANNEL} ${AUDIOVOLUME}
${LOGGER} starting snapclient
${RC_SERVICE} snapcast-client stop
${RC_SERVICE} snapcast-client zap
${RC_SERVICE} snapcast-client start
fi
;;
remove)
${LOGGER} "Sound device $MDEV removed"
;;
esac
The functionaliy is as follows:
- When called it tests if the device is added or removed. So far, I have only seen that the script is called when a device is added though
- It then tests if the desired device is indeed added, and then proceeds to configure it.
Note that it sources /etc/conf.d/snapcast-client to obtain some variables:
# snapclient options
AUDIOCARD=Device
AUDIODEV=0
AUDIOCHANNEL=PCM
AUDIOVOLUME=90%
ANNOUNCEMENT=/root/bin/snapcast-is-configured-ready2play.wav
ANNOUNCEMENTVOLUME=20%
snapclient_opts="-s ${AUDIOCARD}"
Adjust as needed for different configurations.
Because this Raspberry Pi does not have a display, it is hard to tell if the configuration actually worked. Therefore I let it give an audible indication, by having it play a wav file using aplay. Because the volume of announcement was too high, a separate volume control variable ANNOUNCEMENTVOLUME was introduced.
Saving work
The normal procedure to save work in a diskless Alpine linx is system is by using the lbu_commit command. That will load the configuration after initial boot. Howver, mdev is already loaded by the initial ramdisk, aka initramfs. The proper way of saving the work would therefore be to create a new initramfs with the changed configuration files included. That seems however a separate project, and therefore I took a shortcut by still using lbu_commit, but also configuring the snapcast-client to start at boot:
# rc-update add snapcast-client
The logic is that:
- if the speakers are already available, then it will work because the snapcast client is started at boot.
- If the speakers become available at a later time then this is detected by mdev.
Do not forget to also add /root to lbu, because that's where the announcement file lives:
# lbu add /root