I’ve had a longstanding issue somewhere in my system’s config where when I
adjust my system volume using the hardware wheel, the left and right channels
get out of sync. It’s certainly something I can work around, but it’s
frustrating to have to either have to turn the volume all the way down, which
resynchronizes the channels, or to open alsamixer or similar and manually
adjust the right and left channels to match.
Here’s me spamming the volume wheel, and watching the issue in alsamixer:
It’s an inconvenient reality that sometimes fixing the root cause1 of the issue just isn’t nearly as easy as patching together a service that addresses the symptom! (This was also a good excuse to gradually introduce myself to ALSA’s libraries; I’m not sure I would have figured out the root cause of the issue if I hadn’t been playing with this project.) This program periodically checks the volume of the default output’s left channel, and adjusts the right channel’s volume to match:
// sudo pacman -Sy alsa-lib
#include <alsa/asoundlib.h>
#include <time.h>
// Based on the code at
// https://stackoverflow.com/questions/6787318/set-alsa-master-volume-from-c-code
int main()
{
	// Set up the mixer for card "default"
	snd_mixer_t *handle;
	snd_mixer_open(&handle, 0);
	snd_mixer_attach(handle, "default");
	snd_mixer_selem_register(handle, NULL, NULL);
	snd_mixer_load(handle);
	// Obtain the mixer element "Master" from the mixer
	snd_mixer_selem_id_t *sid;
	snd_mixer_selem_id_alloca(&sid);
	snd_mixer_selem_id_set_index(sid, 0);
	snd_mixer_selem_id_set_name(sid, "Master");
	snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid);
	for (;;) {
		// If any events (say, volume changes) happened since the last
		// iteration, update our handle to reflect that. Without this, no volume
		// changes will be picked up:
		// https://www.raspberrypi.org/forums/viewtopic.php?t=175511
		snd_mixer_handle_events(handle);
		// Find the left channel's volume, and sync it with the right channel
		long left_ch_vol;
		snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &left_ch_vol);
		snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, left_ch_vol);
		// Don't monopolize the CPU; it's okay if the levels desync for 100ms
		nanosleep(&(struct timespec){0, 100 * 1000 * 1000}, NULL); // 100ms
	}
	// Clean up
	snd_mixer_close(handle);
	handle = NULL;
	snd_config_update_free_global();
}
For the time being, I’m running this as a systemd user service. (Since this
exists to deal with an issue in my sway config, it could also be run with an
exec line in my sway config.)
chandler@xenon ~ % systemctl status --user volume-balancer.service
● volume-balancer.service - Keep L/R channel volumes in sync
     Loaded: loaded (/home/chandler/.config/systemd/user/volume-balancer.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2021-10-02 20:00:56 CDT; 42s ago
   Main PID: 218353 (volume-balancer)
      Tasks: 2 (limit: 19096)
     Memory: 1.1M
        CPU: 217ms
     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/volume-balancer.service
             └─218353 /home/chandler/bin/volume-balancer
Oct 02 20:00:56 xenon systemd[2288]: Started Keep L/R channel volumes in sync.
chandler@xenon ~ % cat .config/systemd/user/volume-balancer.service
[Unit]
Description=Keep L/R channel volumes in sync
[Service]
ExecStart=/home/chandler/bin/volume-balancer
Type=simple
Restart=always
[Install]
WantedBy=default.target
chandler@xenon ~ %
Update 2024-04-18: Hajo Noerenberg informs me that this can be made substantially more efficient by waiting for a mixer event rather than polling inputs:
- // Don't monopolize the CPU; it's okay if the levels desync for 100ms
- nanosleep(&(struct timespec){0, 100 * 1000 * 1000}, NULL); // 100ms
+ // Wait for a mixer to become ready (i.e. at least one event pending)
+ snd_mixer_wait(handle, 1000);
Hajo also provides a much cleaner solution for a similar problem:
In the meantime I’ve released a slightly more complex daemon to watch for audio activity and volume changes:
https://github.com/hn/linkplay-a31/blob/main/openwrt-linkplay-a31/linkplay-emu/src/linkplay-emu.c
Might be helpful for some people perhaps.
Thanks, Hajo!
- 
It seems that amixerruns into a race condition when run multiple times concurrently. This happens as a result of myswayconfig;amixeris called on each “press” of theXF86AudioRaiseVolumekey, which may happen begin the previously calledamixerinstance terminates. ↩︎
