[PATCH] [RFC] ALSA: control - add generic LED API to sound core routines

Hans de Goede hdegoede at redhat.com
Sun Feb 7 21:52:56 CET 2021


Hi Jaroslav,

On 2/7/21 9:11 PM, Jaroslav Kysela wrote:
> [DO NOT MERGE!]
> [This is just an early proposal for comments]
> [The code is not tested / debugged]
> 
> The recent laptops have usually two LEDs assigned to reflect
> the speaker and microphone mute state. This implementation
> adds a tiny layer on top of the control API which calculates
> the state for those LEDs using the driver callbacks.
> 
> Two new access flags are introduced to describe the controls
> which affects the audio path settings (an easy path for drivers).
> 
> The LED resource can be shared with multiple sound cards with
> this code. The user space controls may be added to the state
> chain, too.
> 
> This code should replace the LED code in the HDA driver and
> add a possibility to easy extend the other drivers (ASoC
> codecs etc.).
> 
> Note: The snd_ctl_notify_one() changes should be moved to
> a separate patch. I will do this, when this proposal is
> agreed.
> 
> Signed-off-by: Jaroslav Kysela <perex at perex.cz>
> Cc: Perry Yuan <Perry.Yuan at dell.com>
> Cc: Hans de Goede <hdegoede at redhat.com>

Nice I think that having this handled in a generic manner
would be a great improvement.

I have 2 HP x2 models which are Bay Trail resp. Cherry Trail
based, which means they use Intel's LPE (Low Power Engine)
for audio which uses ASoC.

These come with detachable USB keyboard-docks which have
a speaker mute LED. I have already added LED-class support to
the HID driver for these, but I never got around to hooking
up a trigger from the ASoC code.

So if I understand things correctly, then if I add this
patch to a kernel and in the ASoC codec driver add
SNDRV_CTL_ELEM_ACCESS_SPK_LED to one or more of the controls,
and add .default_trigger = "audio-mute" to the led_class_dev
for the LED, then if I toggle the control on / off in
alsamixer this should then control the LED ?

If I got that right I will certainly give this a try.

Any advice for which control would be the best to use?
Looking at the code, it assumes that if a control is enabled
that then there will be a route, which makes sense if there
are e.g. headphones and speaker and lineout controls. But what
about chained controls, e.g. separate volume + mute controls
or multiple volume controls chained. With ASoC this is not
unheard of. I guess the answer is to pick the control which
we will also want to use for hw volume-control if/when UCM
profiles grow hw volume-control support ?

Regards,

Hans




p.s. This would only / at least add proper support for these LEDs
at the kernel level. ATM these devices which use UCM profiles don't
actually use any kind of hw volume-control. So we would also need to
fix that (in userspace) to get these LEDs to really work when an
user hits the mute key, or lowers the volume to NIL.




> ---
>  include/sound/control.h     |  12 +++
>  include/uapi/sound/asound.h |   2 +
>  sound/core/Kconfig          |   6 ++
>  sound/core/Makefile         |   1 +
>  sound/core/control.c        |  83 ++++++++++++++-------
>  sound/core/control_led.c    | 144 ++++++++++++++++++++++++++++++++++++
>  sound/pci/hda/Kconfig       |   3 +-
>  7 files changed, 225 insertions(+), 26 deletions(-)
>  create mode 100644 sound/core/control_led.c
> 
> diff --git a/include/sound/control.h b/include/sound/control.h
> index 77d9fa10812d..ed967b18ffc9 100644
> --- a/include/sound/control.h
> +++ b/include/sound/control.h
> @@ -261,4 +261,16 @@ snd_kctl_jack_new(const char *name, struct snd_card *card);
>  void snd_kctl_jack_report(struct snd_card *card,
>  			  struct snd_kcontrol *kctl, bool status);
>  
> +#if IS_ENABLED(CONFIG_SND_LED)
> +void snd_ctl_led_notify(struct snd_card *card, unsigned int mask,
> +			struct snd_kcontrol *kctl, unsigned int ioff);
> +void snd_ctl_led_register(struct snd_card *card);
> +void snd_ctl_led_disconnect(struct snd_card *card);
> +#else
> +static inline void snd_ctl_led_notify(struct snd_card *card, unsigned int mask,
> +				      struct snd_kcontrol *kctl, unsigned int ioff) {}
> +static inline void snd_ctl_led_register(struct snd_card *card) {}
> +static inline void snd_ctl_led_disconnect(struct snd_card *card) {}
> +#endif
> +
>  #endif	/* __SOUND_CONTROL_H */
> diff --git a/include/uapi/sound/asound.h b/include/uapi/sound/asound.h
> index 535a7229e1d9..3faacdf5eaea 100644
> --- a/include/uapi/sound/asound.h
> +++ b/include/uapi/sound/asound.h
> @@ -982,6 +982,8 @@ typedef int __bitwise snd_ctl_elem_iface_t;
>  #define SNDRV_CTL_ELEM_ACCESS_INACTIVE		(1<<8)	/* control does actually nothing, but may be updated */
>  #define SNDRV_CTL_ELEM_ACCESS_LOCK		(1<<9)	/* write lock */
>  #define SNDRV_CTL_ELEM_ACCESS_OWNER		(1<<10)	/* write lock owner */
> +#define SNDRV_CTL_ELEM_ACCESS_SPK_LED		(1<<11)	/* speaker (output) LED flag */
> +#define SNDRV_CTL_ELEM_ACCESS_MIC_LED		(1<<12)	/* microphone (input) LED flag */
>  #define SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK	(1<<28)	/* kernel use a TLV callback */
>  #define SNDRV_CTL_ELEM_ACCESS_USER		(1<<29) /* user space element */
>  /* bits 30 and 31 are obsoleted (for indirect access) */
> diff --git a/sound/core/Kconfig b/sound/core/Kconfig
> index a4050f87f230..1bde494fa1fe 100644
> --- a/sound/core/Kconfig
> +++ b/sound/core/Kconfig
> @@ -203,4 +203,10 @@ config SND_DMA_SGBUF
>  	def_bool y
>  	depends on X86
>  
> +config SND_LED
> +	bool
> +	select NEW_LEDS if SND_LED
> +	select LEDS_TRIGGERS if SND_LED
> +	select LEDS_TRIGGER_AUDIO if SND_LED
> +
>  source "sound/core/seq/Kconfig"
> diff --git a/sound/core/Makefile b/sound/core/Makefile
> index ee4a4a6b99ba..81b33877a03d 100644
> --- a/sound/core/Makefile
> +++ b/sound/core/Makefile
> @@ -13,6 +13,7 @@ snd-$(CONFIG_ISA_DMA_API) += isadma.o
>  snd-$(CONFIG_SND_OSSEMUL) += sound_oss.o
>  snd-$(CONFIG_SND_VMASTER) += vmaster.o
>  snd-$(CONFIG_SND_JACK)	  += ctljack.o jack.o
> +snd-$(CONFIG_SND_LED)     += control_led.o
>  
>  snd-pcm-y := pcm.o pcm_native.o pcm_lib.o pcm_misc.o \
>  		pcm_memory.o memalloc.o
> diff --git a/sound/core/control.c b/sound/core/control.c
> index 5165741a8400..3171e7f2798e 100644
> --- a/sound/core/control.c
> +++ b/sound/core/control.c
> @@ -181,6 +181,28 @@ void snd_ctl_notify(struct snd_card *card, unsigned int mask,
>  }
>  EXPORT_SYMBOL(snd_ctl_notify);
>  
> +/**
> + * snd_ctl_notify_one - Send notification to user-space for a control change
> + * @card: the card to send notification
> + * @mask: the event mask, SNDRV_CTL_EVENT_*
> + * @kctl: the pointer with the control instance
> + * @ioff: the additional offset to the control index
> + *
> + * This function calls snd_ctl_notify() and does additional jobs
> + * like LED state changes.
> + */
> +void snd_ctl_notify_one(struct snd_card *card, unsigned int mask,
> +			struct snd_kcontrol *kctl, unsigned int ioff)
> +{
> +	struct snd_ctl_elem_id id = kctl->id;
> +
> +	id.index += ioff;
> +	id.numid += ioff;
> +	snd_ctl_led_notify(card, mask, kctl, ioff);
> +	snd_ctl_notify(card, mask, &id);
> +}
> +EXPORT_SYMBOL(snd_ctl_notify_one);
> +
>  /**
>   * snd_ctl_new - create a new control instance with some elements
>   * @kctl: the pointer to store new control instance
> @@ -250,6 +272,8 @@ struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,
>  		   SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE |
>  		   SNDRV_CTL_ELEM_ACCESS_TLV_COMMAND |
>  		   SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK |
> +		   SNDRV_CTL_ELEM_ACCESS_SPK_LED |
> +		   SNDRV_CTL_ELEM_ACCESS_MIC_LED |
>  		   SNDRV_CTL_ELEM_ACCESS_SKIP_CHECK);
>  
>  	err = snd_ctl_new(&kctl, count, access, NULL);
> @@ -342,7 +366,6 @@ static int __snd_ctl_add_replace(struct snd_card *card,
>  {
>  	struct snd_ctl_elem_id id;
>  	unsigned int idx;
> -	unsigned int count;
>  	struct snd_kcontrol *old;
>  	int err;
>  
> @@ -376,10 +399,8 @@ static int __snd_ctl_add_replace(struct snd_card *card,
>  	kcontrol->id.numid = card->last_numid + 1;
>  	card->last_numid += kcontrol->count;
>  
> -	id = kcontrol->id;
> -	count = kcontrol->count;
> -	for (idx = 0; idx < count; idx++, id.index++, id.numid++)
> -		snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_ADD, &id);
> +	for (idx = 0; idx < kcontrol->count; idx++)
> +		snd_ctl_notify_one(card, SNDRV_CTL_EVENT_MASK_ADD, kcontrol, idx);
>  
>  	return 0;
>  }
> @@ -462,16 +483,15 @@ EXPORT_SYMBOL(snd_ctl_replace);
>   */
>  int snd_ctl_remove(struct snd_card *card, struct snd_kcontrol *kcontrol)
>  {
> -	struct snd_ctl_elem_id id;
>  	unsigned int idx;
>  
>  	if (snd_BUG_ON(!card || !kcontrol))
>  		return -EINVAL;
>  	list_del(&kcontrol->list);
>  	card->controls_count -= kcontrol->count;
> -	id = kcontrol->id;
> -	for (idx = 0; idx < kcontrol->count; idx++, id.index++, id.numid++)
> -		snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_REMOVE, &id);
> +
> +	for (idx = 0; idx < kcontrol->count; idx++)
> +		snd_ctl_notify_one(card, SNDRV_CTL_EVENT_MASK_REMOVE, kcontrol, idx);
>  	snd_ctl_free_one(kcontrol);
>  	return 0;
>  }
> @@ -584,11 +604,13 @@ int snd_ctl_activate_id(struct snd_card *card, struct snd_ctl_elem_id *id,
>  		vd->access |= SNDRV_CTL_ELEM_ACCESS_INACTIVE;
>  	}
>  	snd_ctl_build_ioff(id, kctl, index_offset);
> -	ret = 1;
> +	downgrade_write(&card->controls_rwsem);
> +	snd_ctl_notify_one(card, SNDRV_CTL_EVENT_MASK_INFO, kctl, index_offset);
> +	up_read(&card->controls_rwsem);
> +	return 1;
> +
>   unlock:
>  	up_write(&card->controls_rwsem);
> -	if (ret > 0)
> -		snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_INFO, id);
>  	return ret;
>  }
>  EXPORT_SYMBOL_GPL(snd_ctl_activate_id);
> @@ -1110,25 +1132,34 @@ static int snd_ctl_elem_write(struct snd_card *card, struct snd_ctl_file *file,
>  	unsigned int index_offset;
>  	int result;
>  
> +	down_write(&card->controls_rwsem);
>  	kctl = snd_ctl_find_id(card, &control->id);
> -	if (kctl == NULL)
> +	if (kctl == NULL) {
> +		up_write(&card->controls_rwsem);
>  		return -ENOENT;
> +	}
>  
>  	index_offset = snd_ctl_get_ioff(kctl, &control->id);
>  	vd = &kctl->vd[index_offset];
>  	if (!(vd->access & SNDRV_CTL_ELEM_ACCESS_WRITE) || kctl->put == NULL ||
>  	    (file && vd->owner && vd->owner != file)) {
> +		up_write(&card->controls_rwsem);
>  		return -EPERM;
>  	}
>  
>  	snd_ctl_build_ioff(&control->id, kctl, index_offset);
>  	result = kctl->put(kctl, control);
> -	if (result < 0)
> +	if (result < 0) {
> +		up_write(&card->controls_rwsem);
>  		return result;
> +	}
>  
>  	if (result > 0) {
> -		struct snd_ctl_elem_id id = control->id;
> -		snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &id);
> +		downgrade_write(&card->controls_rwsem);
> +		snd_ctl_notify_one(card, SNDRV_CTL_EVENT_MASK_VALUE, kctl, index_offset);
> +		up_read(&card->controls_rwsem);
> +	} else {
> +		up_write(&card->controls_rwsem);
>  	}
>  
>  	return 0;
> @@ -1150,9 +1181,7 @@ static int snd_ctl_elem_write_user(struct snd_ctl_file *file,
>  	if (result < 0)
>  		goto error;
>  
> -	down_write(&card->controls_rwsem);
>  	result = snd_ctl_elem_write(card, file, control);
> -	up_write(&card->controls_rwsem);
>  	if (result < 0)
>  		goto error;
>  
> @@ -1301,7 +1330,6 @@ static int replace_user_tlv(struct snd_kcontrol *kctl, unsigned int __user *buf,
>  {
>  	struct user_element *ue = kctl->private_data;
>  	unsigned int *container;
> -	struct snd_ctl_elem_id id;
>  	unsigned int mask = 0;
>  	int i;
>  	int change;
> @@ -1333,10 +1361,8 @@ static int replace_user_tlv(struct snd_kcontrol *kctl, unsigned int __user *buf,
>  	ue->tlv_data_size = size;
>  
>  	mask |= SNDRV_CTL_EVENT_MASK_TLV;
> -	for (i = 0; i < kctl->count; ++i) {
> -		snd_ctl_build_ioff(&id, kctl, i);
> -		snd_ctl_notify(ue->card, mask, &id);
> -	}
> +	for (i = 0; i < kctl->count; ++i)
> +		snd_ctl_notify_one(ue->card, mask, kctl, i);
>  
>  	return change;
>  }
> @@ -1998,9 +2024,14 @@ static const struct file_operations snd_ctl_f_ops =
>  static int snd_ctl_dev_register(struct snd_device *device)
>  {
>  	struct snd_card *card = device->device_data;
> +	int err;
>  
> -	return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,
> -				   &snd_ctl_f_ops, card, &card->ctl_dev);
> +	err = snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,
> +				  &snd_ctl_f_ops, card, &card->ctl_dev);
> +	if (err < 0)
> +		return err;
> +	snd_ctl_led_register(card);
> +	return 0;
>  }
>  
>  /*
> @@ -2019,6 +2050,8 @@ static int snd_ctl_dev_disconnect(struct snd_device *device)
>  	}
>  	read_unlock_irqrestore(&card->ctl_files_rwlock, flags);
>  
> +	snd_ctl_led_disconnect(card);
> +
>  	return snd_unregister_device(&card->ctl_dev);
>  }
>  
> diff --git a/sound/core/control_led.c b/sound/core/control_led.c
> new file mode 100644
> index 000000000000..e70855ea54d1
> --- /dev/null
> +++ b/sound/core/control_led.c
> @@ -0,0 +1,144 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + *  LED state routines for driver control interface
> + *  Copyright (c) 2021 by Jaroslav Kysela <perex at perex.cz>
> + */
> +
> +#include <linux/leds.h>
> +#include <sound/core.h>
> +#include <sound/control.h>
> +
> +static DEFINE_MUTEX(snd_control_led_mutex);
> +static unsigned int snd_control_led_last_card;
> +static struct snd_card *snd_control_led_cards[SNDRV_CARDS];
> +
> +#define UPDATE_ROUTE(route, cb) \
> +	do { \
> +		int route2 = (cb); \
> +		if (route2 >= 0) \
> +			route = route < 0 ? route2 : (route | route2); \
> +	} while (0)
> +
> +static int snd_ctl_led_get(struct snd_card *card,
> +			   struct snd_kcontrol *kctl, unsigned int ioff)
> +{
> +	struct snd_ctl_elem_info info;
> +	struct snd_ctl_elem_value value;
> +	unsigned int i;
> +	int result;
> +
> +	memset(&info, 0, sizeof(info));
> +	info.id = kctl->id;
> +	info.id.index += ioff;
> +	info.id.numid += ioff;
> +	result = kctl->info(kctl, &info);
> +	if (result < 0)
> +		return -1;
> +	memset(&value, 0, sizeof(value));
> +	value.id = info.id;
> +	result = kctl->get(kctl, &value);
> +	if (result < 0)
> +		return -1;
> +	if (info.type == SNDRV_CTL_ELEM_TYPE_BOOLEAN ||
> +	    info.type == SNDRV_CTL_ELEM_TYPE_INTEGER) {
> +		for (i = 0; i < info.count; i++)
> +			if (value.value.integer.value[i] != info.value.integer.min)
> +				return 1;
> +	} else if (info.type == SNDRV_CTL_ELEM_TYPE_INTEGER64) {
> +		for (i = 0; i < info.count; i++)
> +			if (value.value.integer64.value[i] != info.value.integer64.min)
> +				return 1;
> +	}
> +	return 0;
> +}
> +
> +static int snd_ctl_led_set_state_for_card(int card_number, unsigned int group)
> +{
> +	struct snd_card *card = snd_control_led_cards[card_number];
> +	struct snd_kcontrol *kctl;
> +	struct snd_kcontrol_volatile *vd;
> +	unsigned int ioff;
> +	int route = -1;
> +
> +	down_read(&card->controls_rwsem);
> +	list_for_each_entry(kctl, &card->controls, list) {
> +		for (ioff = 0; ioff < kctl->count; ioff++) {
> +			vd = &kctl->vd[ioff];
> +			if (vd->access & group)
> +				UPDATE_ROUTE(route, snd_ctl_led_get(card, kctl, ioff));
> +		}
> +	}
> +	up_read(&card->controls_rwsem);
> +	return route;
> +}
> +
> +static void snd_ctl_led_set_state(struct snd_card *card, unsigned int group)
> +{
> +	int card_number;
> +	enum led_audio led_trigger_type;
> +	int route;
> +
> +	if (group == SNDRV_CTL_ELEM_ACCESS_SPK_LED) {
> +		led_trigger_type = LED_AUDIO_MUTE;
> +	} else if (group == SNDRV_CTL_ELEM_ACCESS_MIC_LED) {
> +		led_trigger_type = LED_AUDIO_MICMUTE;
> +	} else {
> +		return;
> +	}
> +	route = -1;
> +	mutex_lock(&snd_control_led_mutex);
> +	/* the card may not be registered (active) at this point */
> +	if (snd_control_led_cards[card->number] == NULL) {
> +		mutex_unlock(&snd_control_led_mutex);
> +		return;
> +	}
> +	for (card_number = 0; card_number <= snd_control_led_last_card; card_number++)
> +		UPDATE_ROUTE(route, snd_ctl_led_set_state_for_card(card_number, group));
> +	mutex_unlock(&snd_control_led_mutex);
> +	if (route >= 0)
> +		ledtrig_audio_set(led_trigger_type, route ? LED_OFF : LED_ON);
> +
> +}
> +
> +void snd_ctl_led_notify(struct snd_card *card, unsigned int mask,
> +			struct snd_kcontrol *kctl, unsigned int ioff)
> +{
> +	if (mask == SNDRV_CTL_EVENT_MASK_REMOVE ||
> +	    (mask & (SNDRV_CTL_EVENT_MASK_INFO |
> +		     SNDRV_CTL_EVENT_MASK_ADD |
> +		     SNDRV_CTL_EVENT_MASK_VALUE)) != 0) {
> +		struct snd_kcontrol_volatile *vd = &kctl->vd[ioff];
> +		if (vd->access & SNDRV_CTL_ELEM_ACCESS_SPK_LED)
> +			snd_ctl_led_set_state(card, SNDRV_CTL_ELEM_ACCESS_SPK_LED);
> +		if (vd->access & SNDRV_CTL_ELEM_ACCESS_MIC_LED)
> +			snd_ctl_led_set_state(card, SNDRV_CTL_ELEM_ACCESS_MIC_LED);
> +	}
> +}
> +
> +void snd_ctl_led_register(struct snd_card *card)
> +{
> +	if (snd_BUG_ON(card->number < 0 ||
> +		       card->number >= ARRAY_SIZE(snd_control_led_cards)))
> +		return;
> +	mutex_lock(&snd_control_led_mutex);
> +	snd_control_led_cards[card->number] = card;
> +	if (card->number > snd_control_led_last_card)
> +		snd_control_led_last_card = card->number;
> +	mutex_unlock(&snd_control_led_mutex);
> +	snd_ctl_led_set_state(card, SNDRV_CTL_ELEM_ACCESS_SPK_LED);
> +	snd_ctl_led_set_state(card, SNDRV_CTL_ELEM_ACCESS_MIC_LED);
> +}
> +
> +void snd_ctl_led_disconnect(struct snd_card *card)
> +{
> +	mutex_lock(&snd_control_led_mutex);
> +	if (snd_control_led_last_card == card->number) {
> +		while (snd_control_led_last_card > 0)
> +			if (snd_control_led_cards[--snd_control_led_last_card])
> +				break;
> +	}
> +	snd_control_led_cards[card->number] = NULL;
> +	mutex_unlock(&snd_control_led_mutex);
> +	snd_ctl_led_set_state(card, SNDRV_CTL_ELEM_ACCESS_SPK_LED);
> +	snd_ctl_led_set_state(card, SNDRV_CTL_ELEM_ACCESS_MIC_LED);
> +}
> diff --git a/sound/pci/hda/Kconfig b/sound/pci/hda/Kconfig
> index 90759391cbac..ccf788ede8ba 100644
> --- a/sound/pci/hda/Kconfig
> +++ b/sound/pci/hda/Kconfig
> @@ -9,7 +9,8 @@ config SND_HDA
>  	select SND_HDA_CORE
>  
>  config SND_HDA_GENERIC_LEDS
> -       bool
> +	bool
> +	select SND_LED	# just for test, ignore, please!
>  
>  config SND_HDA_INTEL
>  	tristate "HD Audio PCI"
> 



More information about the Alsa-devel mailing list