Hi, On Thu, Aug 06, 2020 at 01:31:03AM +0800, Tom Yan wrote: > Hi all, > > I just wonder if it's a "no one cares" or a "no one was aware of it" > issue (or maybe both?). > > When you change (integer) values (e.g. volume) of a mixer control, it > usually (if not always) involves calling two functions/methods of a > snd_kcontrol_new, which are get and put, in order to do relative > volume adjustments. (Apparently it is often done relatively even if we > have absolute values, for reasons.) > > While these two "actions" can be and probably are mostly "atomic" > (with the help of mutex) in the kernel drivers *respectively*, they > are not and cannot be atomic as a whole. > > This won't really be an issue when the actions (either for one or > multiple channels) are done "synchronously" in *one* program run (e.g. > amixer -c STX set Master 1+). However, if such a program run is issued > multiple times "asynchronously" (e.g. binding it to some > XF86Audio{Raise,Lower}Volume scroll wheel), volume adjustment becomes > a total mess / failure. > > If it isn't obvious enough. it could happen like the following: > get1(100 100) > set1(101 100) > get2(101 100) > set2(102 100) > ... > > Or worse: > get1(100 100) > get2(100 100) > set1(101 100) > set2(100 101) > ... > > Not only that it may/will not finish the first set of adjustments for > all channels before the second, get() from the second set could happen > before set() from the first, reverting the effect of the earlier > one(s). > > Certainly one can use something like `flock` with amixer to make sure > the atomicity of each issue/run, but not only that it looks silly and > primitive, we don't always manipulate the mixer control with an > "executable". For example, this weird issue in pulseaudio is probably > related: https://bugs.freedesktop.org/show_bug.cgi?id=92717 > > So I wonder, is there a particular reason that mixer control doesn't > possess some form of lock, which allows any form of userspace > manipulation to lock it until what should be / is considered atomic is > finished? ALSA control core allows applications to lock/unlock a control element so that any write opreation to the control element fails for processes except for owner process. When a process requests `SNDRV_CTL_IOCTL_ELEM_LOCK`[1] against a control element. After operating the request, the control element is under 'owned by the process' state. In this state, any request of `SNDRV_CTL_IOCTL_ELEM_WRITE` from the other processes fails with `-EPERM`[2]. The write operation from the owner process is successful only. When the owner process is going to finish, the state is released[3]. ALSA userspace library, a.k.a alsa-lib, has a pair of `snd_ctl_elem_lock()` and `snd_ctl_elem_unlock()` as its exported API[4]. If application developers would like to bring failure to requests of `SNDRV_CTL_IOCTL_ELEM_WRITE` from the other processes in the period that the process requests `SNDRV_CTL_IOCTL_ELEM_READ` and `SNDRV_CTL_IOCTL_ELEM_WRITE` as a transaction, the lock/unlock mechanism is available. However, as long as I know, it's not used popularly. This is a simple demonstration about the above mechanism. PyGObject and alsa-gobject[5] is required to install: ``` #!/usr/bin/env python3 import gi gi.require_version('ALSACtl', '0.0') from gi.repository import ALSACtl import subprocess def run_amixer(should_err): cmd = ('amixer', '-c', str(card_id), 'cset', 'iface={},name="{}",index={},device={},subdevice={},numid={}'.format( eid.get_iface().value_nick, eid.get_name(), eid.get_index(), eid.get_device_id(), eid.get_subdevice_id(), eid.get_numid()), '0,0', ) result = subprocess.run(cmd, capture_output=True) if result.stderr: err = result.stderr.decode('UTF-8').rstrip() print(' ', 'expected' if should_err else 'unexpected') print(' ', err) if result.stdout: output = result.stdout.decode('UTF-8').rstrip().split('\n') print(' ', 'expected' if not should_err else 'unexpected') print(' ', output[-2]) card_id = 0 card = ALSACtl.Card.new() card.open(card_id, 0) for eid in card.get_elem_id_list(): prev_info = card.get_elem_info(eid) if (prev_info.get_property('type') != ALSACtl.ElemType.INTEGER or 'write' not in prev_info.get_property('access').value_nicks or 'lock' in prev_info.get_property('access').value_nicks): continue card.lock_elem(eid, True) print(' my program locks: "{}"'.format(eid.get_name())) run_amixer_subprocess(True) card.lock_elem(eid, False) print(' my program unlocks: "{}"'.format(eid.get_name())) run_amixer_subprocess(False) ``` You can see the result of amixer execution is different in the cases of locked and unlocked, like: ``` $ /tmp/lock-demo ... my program locks: "Headphone Playback Volume" expected amixer: Control hw:1 element write error: Operation not permitted my program unlocks: "Headphone Playback Volume" expected : values=0,0 ... ``` [1] https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/tree/include/uapi/sound/asound.h#n1083 [2] https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/tree/sound/core/control.c#n1108 [3] https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/tree/sound/core/control.c#n122 [4] https://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html#ga1fba1f7e08ab11505a617af5d54f4580 [5] https://github.com/alsa-project/alsa-gobject Regards Takashi Sakamoto