Merge tag 'tags/mute-led-rework' into for-next
[sfrench/cifs-2.6.git] / sound / core / control_led.c
diff --git a/sound/core/control_led.c b/sound/core/control_led.c
new file mode 100644 (file)
index 0000000..d4fb8b8
--- /dev/null
@@ -0,0 +1,770 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  LED state routines for driver control interface
+ *  Copyright (c) 2021 by Jaroslav Kysela <perex@perex.cz>
+ */
+
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/leds.h>
+#include <sound/core.h>
+#include <sound/control.h>
+
+MODULE_AUTHOR("Jaroslav Kysela <perex@perex.cz>");
+MODULE_DESCRIPTION("ALSA control interface to LED trigger code.");
+MODULE_LICENSE("GPL");
+
+#define MAX_LED (((SNDRV_CTL_ELEM_ACCESS_MIC_LED - SNDRV_CTL_ELEM_ACCESS_SPK_LED) \
+                       >> SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) + 1)
+
+enum snd_ctl_led_mode {
+        MODE_FOLLOW_MUTE = 0,
+        MODE_FOLLOW_ROUTE,
+        MODE_OFF,
+        MODE_ON,
+};
+
+struct snd_ctl_led_card {
+       struct device dev;
+       int number;
+       struct snd_ctl_led *led;
+};
+
+struct snd_ctl_led {
+       struct device dev;
+       struct list_head controls;
+       const char *name;
+       unsigned int group;
+       enum led_audio trigger_type;
+       enum snd_ctl_led_mode mode;
+       struct snd_ctl_led_card *cards[SNDRV_CARDS];
+};
+
+struct snd_ctl_led_ctl {
+       struct list_head list;
+       struct snd_card *card;
+       unsigned int access;
+       struct snd_kcontrol *kctl;
+       unsigned int index_offset;
+};
+
+static DEFINE_MUTEX(snd_ctl_led_mutex);
+static bool snd_ctl_led_card_valid[SNDRV_CARDS];
+static struct snd_ctl_led snd_ctl_leds[MAX_LED] = {
+       {
+               .name = "speaker",
+               .group = (SNDRV_CTL_ELEM_ACCESS_SPK_LED >> SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) - 1,
+               .trigger_type = LED_AUDIO_MUTE,
+               .mode = MODE_FOLLOW_MUTE,
+       },
+       {
+               .name = "mic",
+               .group = (SNDRV_CTL_ELEM_ACCESS_MIC_LED >> SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) - 1,
+               .trigger_type = LED_AUDIO_MICMUTE,
+               .mode = MODE_FOLLOW_MUTE,
+       },
+};
+
+static void snd_ctl_led_sysfs_add(struct snd_card *card);
+static void snd_ctl_led_sysfs_remove(struct snd_card *card);
+
+#define UPDATE_ROUTE(route, cb) \
+       do { \
+               int route2 = (cb); \
+               if (route2 >= 0) \
+                       route = route < 0 ? route2 : (route | route2); \
+       } while (0)
+
+static inline unsigned int access_to_group(unsigned int access)
+{
+       return ((access & SNDRV_CTL_ELEM_ACCESS_LED_MASK) >>
+                               SNDRV_CTL_ELEM_ACCESS_LED_SHIFT) - 1;
+}
+
+static inline unsigned int group_to_access(unsigned int group)
+{
+       return (group + 1) << SNDRV_CTL_ELEM_ACCESS_LED_SHIFT;
+}
+
+static struct snd_ctl_led *snd_ctl_led_get_by_access(unsigned int access)
+{
+       unsigned int group = access_to_group(access);
+       if (group >= MAX_LED)
+               return NULL;
+       return &snd_ctl_leds[group];
+}
+
+static int snd_ctl_led_get(struct snd_ctl_led_ctl *lctl)
+{
+       struct snd_kcontrol *kctl = lctl->kctl;
+       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 += lctl->index_offset;
+       info.id.numid += lctl->index_offset;
+       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 void snd_ctl_led_set_state(struct snd_card *card, unsigned int access,
+                                 struct snd_kcontrol *kctl, unsigned int ioff)
+{
+       struct snd_ctl_led *led;
+       struct snd_ctl_led_ctl *lctl;
+       int route;
+       bool found;
+
+       led = snd_ctl_led_get_by_access(access);
+       if (!led)
+               return;
+       route = -1;
+       found = false;
+       mutex_lock(&snd_ctl_led_mutex);
+       /* the card may not be registered (active) at this point */
+       if (card && !snd_ctl_led_card_valid[card->number]) {
+               mutex_unlock(&snd_ctl_led_mutex);
+               return;
+       }
+       list_for_each_entry(lctl, &led->controls, list) {
+               if (lctl->kctl == kctl && lctl->index_offset == ioff)
+                       found = true;
+               UPDATE_ROUTE(route, snd_ctl_led_get(lctl));
+       }
+       if (!found && kctl && card) {
+               lctl = kzalloc(sizeof(*lctl), GFP_KERNEL);
+               if (lctl) {
+                       lctl->card = card;
+                       lctl->access = access;
+                       lctl->kctl = kctl;
+                       lctl->index_offset = ioff;
+                       list_add(&lctl->list, &led->controls);
+                       UPDATE_ROUTE(route, snd_ctl_led_get(lctl));
+               }
+       }
+       mutex_unlock(&snd_ctl_led_mutex);
+       switch (led->mode) {
+       case MODE_OFF:          route = 1; break;
+       case MODE_ON:           route = 0; break;
+       case MODE_FOLLOW_ROUTE: if (route >= 0) route ^= 1; break;
+       case MODE_FOLLOW_MUTE:  /* noop */ break;
+       }
+       if (route >= 0)
+               ledtrig_audio_set(led->trigger_type, route ? LED_OFF : LED_ON);
+}
+
+static struct snd_ctl_led_ctl *snd_ctl_led_find(struct snd_kcontrol *kctl, unsigned int ioff)
+{
+       struct list_head *controls;
+       struct snd_ctl_led_ctl *lctl;
+       unsigned int group;
+
+       for (group = 0; group < MAX_LED; group++) {
+               controls = &snd_ctl_leds[group].controls;
+               list_for_each_entry(lctl, controls, list)
+                       if (lctl->kctl == kctl && lctl->index_offset == ioff)
+                               return lctl;
+       }
+       return NULL;
+}
+
+static unsigned int snd_ctl_led_remove(struct snd_kcontrol *kctl, unsigned int ioff,
+                                      unsigned int access)
+{
+       struct snd_ctl_led_ctl *lctl;
+       unsigned int ret = 0;
+
+       mutex_lock(&snd_ctl_led_mutex);
+       lctl = snd_ctl_led_find(kctl, ioff);
+       if (lctl && (access == 0 || access != lctl->access)) {
+               ret = lctl->access;
+               list_del(&lctl->list);
+               kfree(lctl);
+       }
+       mutex_unlock(&snd_ctl_led_mutex);
+       return ret;
+}
+
+static void snd_ctl_led_notify(struct snd_card *card, unsigned int mask,
+                              struct snd_kcontrol *kctl, unsigned int ioff)
+{
+       struct snd_kcontrol_volatile *vd;
+       unsigned int access, access2;
+
+       if (mask == SNDRV_CTL_EVENT_MASK_REMOVE) {
+               access = snd_ctl_led_remove(kctl, ioff, 0);
+               if (access)
+                       snd_ctl_led_set_state(card, access, NULL, 0);
+       } else if (mask & SNDRV_CTL_EVENT_MASK_INFO) {
+               vd = &kctl->vd[ioff];
+               access = vd->access & SNDRV_CTL_ELEM_ACCESS_LED_MASK;
+               access2 = snd_ctl_led_remove(kctl, ioff, access);
+               if (access2)
+                       snd_ctl_led_set_state(card, access2, NULL, 0);
+               if (access)
+                       snd_ctl_led_set_state(card, access, kctl, ioff);
+       } else if ((mask & (SNDRV_CTL_EVENT_MASK_ADD |
+                           SNDRV_CTL_EVENT_MASK_VALUE)) != 0) {
+               vd = &kctl->vd[ioff];
+               access = vd->access & SNDRV_CTL_ELEM_ACCESS_LED_MASK;
+               if (access)
+                       snd_ctl_led_set_state(card, access, kctl, ioff);
+       }
+}
+
+static int snd_ctl_led_set_id(int card_number, struct snd_ctl_elem_id *id,
+                             unsigned int group, bool set)
+{
+       struct snd_card *card;
+       struct snd_kcontrol *kctl;
+       struct snd_kcontrol_volatile *vd;
+       unsigned int ioff, access, new_access;
+       int err = 0;
+
+       card = snd_card_ref(card_number);
+       if (card) {
+               down_write(&card->controls_rwsem);
+               kctl = snd_ctl_find_id(card, id);
+               if (kctl) {
+                       ioff = snd_ctl_get_ioff(kctl, id);
+                       vd = &kctl->vd[ioff];
+                       access = vd->access & SNDRV_CTL_ELEM_ACCESS_LED_MASK;
+                       if (access != 0 && access != group_to_access(group)) {
+                               err = -EXDEV;
+                               goto unlock;
+                       }
+                       new_access = vd->access & ~SNDRV_CTL_ELEM_ACCESS_LED_MASK;
+                       if (set)
+                               new_access |= group_to_access(group);
+                       if (new_access != vd->access) {
+                               vd->access = new_access;
+                               snd_ctl_led_notify(card, SNDRV_CTL_EVENT_MASK_INFO, kctl, ioff);
+                       }
+               } else {
+                       err = -ENOENT;
+               }
+unlock:
+               up_write(&card->controls_rwsem);
+               snd_card_unref(card);
+       } else {
+               err = -ENXIO;
+       }
+       return err;
+}
+
+static void snd_ctl_led_refresh(void)
+{
+       unsigned int group;
+
+       for (group = 0; group < MAX_LED; group++)
+               snd_ctl_led_set_state(NULL, group_to_access(group), NULL, 0);
+}
+
+static void snd_ctl_led_ctl_destroy(struct snd_ctl_led_ctl *lctl)
+{
+       list_del(&lctl->list);
+       kfree(lctl);
+}
+
+static void snd_ctl_led_clean(struct snd_card *card)
+{
+       unsigned int group;
+       struct snd_ctl_led *led;
+       struct snd_ctl_led_ctl *lctl;
+
+       for (group = 0; group < MAX_LED; group++) {
+               led = &snd_ctl_leds[group];
+repeat:
+               list_for_each_entry(lctl, &led->controls, list)
+                       if (!card || lctl->card == card) {
+                               snd_ctl_led_ctl_destroy(lctl);
+                               goto repeat;
+                       }
+       }
+}
+
+static int snd_ctl_led_reset(int card_number, unsigned int group)
+{
+       struct snd_card *card;
+       struct snd_ctl_led *led;
+       struct snd_ctl_led_ctl *lctl;
+       struct snd_kcontrol_volatile *vd;
+       bool change = false;
+
+       card = snd_card_ref(card_number);
+       if (!card)
+               return -ENXIO;
+
+       mutex_lock(&snd_ctl_led_mutex);
+       if (!snd_ctl_led_card_valid[card_number]) {
+               mutex_unlock(&snd_ctl_led_mutex);
+               snd_card_unref(card);
+               return -ENXIO;
+       }
+       led = &snd_ctl_leds[group];
+repeat:
+       list_for_each_entry(lctl, &led->controls, list)
+               if (lctl->card == card) {
+                       vd = &lctl->kctl->vd[lctl->index_offset];
+                       vd->access &= ~group_to_access(group);
+                       snd_ctl_led_ctl_destroy(lctl);
+                       change = true;
+                       goto repeat;
+               }
+       mutex_unlock(&snd_ctl_led_mutex);
+       if (change)
+               snd_ctl_led_set_state(NULL, group_to_access(group), NULL, 0);
+       snd_card_unref(card);
+       return 0;
+}
+
+static void snd_ctl_led_register(struct snd_card *card)
+{
+       struct snd_kcontrol *kctl;
+       unsigned int ioff;
+
+       if (snd_BUG_ON(card->number < 0 ||
+                      card->number >= ARRAY_SIZE(snd_ctl_led_card_valid)))
+               return;
+       mutex_lock(&snd_ctl_led_mutex);
+       snd_ctl_led_card_valid[card->number] = true;
+       mutex_unlock(&snd_ctl_led_mutex);
+       /* the register callback is already called with held card->controls_rwsem */
+       list_for_each_entry(kctl, &card->controls, list)
+               for (ioff = 0; ioff < kctl->count; ioff++)
+                       snd_ctl_led_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, kctl, ioff);
+       snd_ctl_led_refresh();
+       snd_ctl_led_sysfs_add(card);
+}
+
+static void snd_ctl_led_disconnect(struct snd_card *card)
+{
+       snd_ctl_led_sysfs_remove(card);
+       mutex_lock(&snd_ctl_led_mutex);
+       snd_ctl_led_card_valid[card->number] = false;
+       snd_ctl_led_clean(card);
+       mutex_unlock(&snd_ctl_led_mutex);
+       snd_ctl_led_refresh();
+}
+
+/*
+ * sysfs
+ */
+
+static ssize_t show_mode(struct device *dev,
+                        struct device_attribute *attr, char *buf)
+{
+       struct snd_ctl_led *led = container_of(dev, struct snd_ctl_led, dev);
+       const char *str;
+
+       switch (led->mode) {
+       case MODE_FOLLOW_MUTE:  str = "follow-mute"; break;
+       case MODE_FOLLOW_ROUTE: str = "follow-route"; break;
+       case MODE_ON:           str = "on"; break;
+       case MODE_OFF:          str = "off"; break;
+       }
+       return sprintf(buf, "%s\n", str);
+}
+
+static ssize_t store_mode(struct device *dev, struct device_attribute *attr,
+                         const char *buf, size_t count)
+{
+       struct snd_ctl_led *led = container_of(dev, struct snd_ctl_led, dev);
+       char _buf[16];
+       size_t l = min(count, sizeof(_buf) - 1) + 1;
+       enum snd_ctl_led_mode mode;
+
+       memcpy(_buf, buf, l);
+       _buf[l] = '\0';
+       if (strstr(_buf, "mute"))
+               mode = MODE_FOLLOW_MUTE;
+       else if (strstr(_buf, "route"))
+               mode = MODE_FOLLOW_ROUTE;
+       else if (strncmp(_buf, "off", 3) == 0 || strncmp(_buf, "0", 1) == 0)
+               mode = MODE_OFF;
+       else if (strncmp(_buf, "on", 2) == 0 || strncmp(_buf, "1", 1) == 0)
+               mode = MODE_ON;
+       else
+               return count;
+
+       mutex_lock(&snd_ctl_led_mutex);
+       led->mode = mode;
+       mutex_unlock(&snd_ctl_led_mutex);
+
+       snd_ctl_led_set_state(NULL, group_to_access(led->group), NULL, 0);
+       return count;
+}
+
+static ssize_t show_brightness(struct device *dev,
+                              struct device_attribute *attr, char *buf)
+{
+       struct snd_ctl_led *led = container_of(dev, struct snd_ctl_led, dev);
+
+       return sprintf(buf, "%u\n", ledtrig_audio_get(led->trigger_type));
+}
+
+static DEVICE_ATTR(mode, 0644, show_mode, store_mode);
+static DEVICE_ATTR(brightness, 0444, show_brightness, NULL);
+
+static struct attribute *snd_ctl_led_dev_attrs[] = {
+       &dev_attr_mode.attr,
+       &dev_attr_brightness.attr,
+       NULL,
+};
+
+static const struct attribute_group snd_ctl_led_dev_attr_group = {
+       .attrs = snd_ctl_led_dev_attrs,
+};
+
+static const struct attribute_group *snd_ctl_led_dev_attr_groups[] = {
+       &snd_ctl_led_dev_attr_group,
+       NULL,
+};
+
+static char *find_eos(char *s)
+{
+       while (*s && *s != ',')
+               s++;
+       if (*s)
+               s++;
+       return s;
+}
+
+static char *parse_uint(char *s, unsigned int *val)
+{
+       unsigned long long res;
+       if (kstrtoull(s, 10, &res))
+               res = 0;
+       *val = res;
+       return find_eos(s);
+}
+
+static char *parse_string(char *s, char *val, size_t val_size)
+{
+       if (*s == '"' || *s == '\'') {
+               char c = *s;
+               s++;
+               while (*s && *s != c) {
+                       if (val_size > 1) {
+                               *val++ = *s;
+                               val_size--;
+                       }
+                       s++;
+               }
+       } else {
+               while (*s && *s != ',') {
+                       if (val_size > 1) {
+                               *val++ = *s;
+                               val_size--;
+                       }
+                       s++;
+               }
+       }
+       *val = '\0';
+       if (*s)
+               s++;
+       return s;
+}
+
+static char *parse_iface(char *s, unsigned int *val)
+{
+       if (!strncasecmp(s, "card", 4))
+               *val = SNDRV_CTL_ELEM_IFACE_CARD;
+       else if (!strncasecmp(s, "mixer", 5))
+               *val = SNDRV_CTL_ELEM_IFACE_MIXER;
+       return find_eos(s);
+}
+
+/*
+ * These types of input strings are accepted:
+ *
+ *   unsigned integer - numid (equivaled to numid=UINT)
+ *   string - basic mixer name (equivalent to iface=MIXER,name=STR)
+ *   numid=UINT
+ *   [iface=MIXER,][device=UINT,][subdevice=UINT,]name=STR[,index=UINT]
+ */
+static ssize_t set_led_id(struct snd_ctl_led_card *led_card, const char *buf, size_t count,
+                         bool attach)
+{
+       char buf2[256], *s;
+       size_t len = max(sizeof(s) - 1, count);
+       struct snd_ctl_elem_id id;
+       int err;
+
+       strncpy(buf2, buf, len);
+       buf2[len] = '\0';
+       memset(&id, 0, sizeof(id));
+       id.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+       s = buf2;
+       while (*s) {
+               if (!strncasecmp(s, "numid=", 6)) {
+                       s = parse_uint(s + 6, &id.numid);
+               } else if (!strncasecmp(s, "iface=", 6)) {
+                       s = parse_iface(s + 6, &id.iface);
+               } else if (!strncasecmp(s, "device=", 7)) {
+                       s = parse_uint(s + 7, &id.device);
+               } else if (!strncasecmp(s, "subdevice=", 10)) {
+                       s = parse_uint(s + 10, &id.subdevice);
+               } else if (!strncasecmp(s, "name=", 5)) {
+                       s = parse_string(s + 5, id.name, sizeof(id.name));
+               } else if (!strncasecmp(s, "index=", 6)) {
+                       s = parse_uint(s + 6, &id.index);
+               } else if (s == buf2) {
+                       while (*s) {
+                               if (*s < '0' || *s > '9')
+                                       break;
+                               s++;
+                       }
+                       if (*s == '\0')
+                               parse_uint(buf2, &id.numid);
+                       else {
+                               for (; *s >= ' '; s++);
+                               *s = '\0';
+                               strlcpy(id.name, buf2, sizeof(id.name));
+                       }
+                       break;
+               }
+               if (*s == ',')
+                       s++;
+       }
+
+       err = snd_ctl_led_set_id(led_card->number, &id, led_card->led->group, attach);
+       if (err < 0)
+               return err;
+
+       return count;
+}
+
+static ssize_t parse_attach(struct device *dev, struct device_attribute *attr,
+                           const char *buf, size_t count)
+{
+       struct snd_ctl_led_card *led_card = container_of(dev, struct snd_ctl_led_card, dev);
+       return set_led_id(led_card, buf, count, true);
+}
+
+static ssize_t parse_detach(struct device *dev, struct device_attribute *attr,
+                           const char *buf, size_t count)
+{
+       struct snd_ctl_led_card *led_card = container_of(dev, struct snd_ctl_led_card, dev);
+       return set_led_id(led_card, buf, count, false);
+}
+
+static ssize_t ctl_reset(struct device *dev, struct device_attribute *attr,
+                        const char *buf, size_t count)
+{
+       struct snd_ctl_led_card *led_card = container_of(dev, struct snd_ctl_led_card, dev);
+       int err;
+
+       if (count > 0 && buf[0] == '1') {
+               err = snd_ctl_led_reset(led_card->number, led_card->led->group);
+               if (err < 0)
+                       return err;
+       }
+       return count;
+}
+
+static ssize_t ctl_list(struct device *dev,
+                       struct device_attribute *attr, char *buf)
+{
+       struct snd_ctl_led_card *led_card = container_of(dev, struct snd_ctl_led_card, dev);
+       struct snd_card *card;
+       struct snd_ctl_led_ctl *lctl;
+       char *buf2 = buf;
+       size_t l;
+
+       card = snd_card_ref(led_card->number);
+       if (!card)
+               return -ENXIO;
+       down_read(&card->controls_rwsem);
+       mutex_lock(&snd_ctl_led_mutex);
+       if (snd_ctl_led_card_valid[led_card->number]) {
+               list_for_each_entry(lctl, &led_card->led->controls, list)
+                       if (lctl->card == card) {
+                               if (buf2 - buf > PAGE_SIZE - 16)
+                                       break;
+                               if (buf2 != buf)
+                                       *buf2++ = ' ';
+                               l = scnprintf(buf2, 15, "%u",
+                                               lctl->kctl->id.numid +
+                                                       lctl->index_offset);
+                               buf2[l] = '\0';
+                               buf2 += l + 1;
+                       }
+       }
+       mutex_unlock(&snd_ctl_led_mutex);
+       up_read(&card->controls_rwsem);
+       snd_card_unref(card);
+       return buf2 - buf;
+}
+
+static DEVICE_ATTR(attach, 0200, NULL, parse_attach);
+static DEVICE_ATTR(detach, 0200, NULL, parse_detach);
+static DEVICE_ATTR(reset, 0200, NULL, ctl_reset);
+static DEVICE_ATTR(list, 0444, ctl_list, NULL);
+
+static struct attribute *snd_ctl_led_card_attrs[] = {
+       &dev_attr_attach.attr,
+       &dev_attr_detach.attr,
+       &dev_attr_reset.attr,
+       &dev_attr_list.attr,
+       NULL,
+};
+
+static const struct attribute_group snd_ctl_led_card_attr_group = {
+       .attrs = snd_ctl_led_card_attrs,
+};
+
+static const struct attribute_group *snd_ctl_led_card_attr_groups[] = {
+       &snd_ctl_led_card_attr_group,
+       NULL,
+};
+
+static struct device snd_ctl_led_dev;
+
+static void snd_ctl_led_sysfs_add(struct snd_card *card)
+{
+       unsigned int group;
+       struct snd_ctl_led_card *led_card;
+       struct snd_ctl_led *led;
+       char link_name[32];
+
+       for (group = 0; group < MAX_LED; group++) {
+               led = &snd_ctl_leds[group];
+               led_card = kzalloc(sizeof(*led_card), GFP_KERNEL);
+               if (!led_card)
+                       goto cerr2;
+               led_card->number = card->number;
+               led_card->led = led;
+               device_initialize(&led_card->dev);
+               if (dev_set_name(&led_card->dev, "card%d", card->number) < 0)
+                       goto cerr;
+               led_card->dev.parent = &led->dev;
+               led_card->dev.groups = snd_ctl_led_card_attr_groups;
+               if (device_add(&led_card->dev))
+                       goto cerr;
+               led->cards[card->number] = led_card;
+               snprintf(link_name, sizeof(link_name), "led-%s", led->name);
+               WARN(sysfs_create_link(&card->ctl_dev.kobj, &led_card->dev.kobj, link_name),
+                       "can't create symlink to controlC%i device\n", card->number);
+               WARN(sysfs_create_link(&led_card->dev.kobj, &card->card_dev.kobj, "card"),
+                       "can't create symlink to card%i\n", card->number);
+
+               continue;
+cerr:
+               put_device(&led_card->dev);
+cerr2:
+               printk(KERN_ERR "snd_ctl_led: unable to add card%d", card->number);
+               kfree(led_card);
+       }
+}
+
+static void snd_ctl_led_sysfs_remove(struct snd_card *card)
+{
+       unsigned int group;
+       struct snd_ctl_led_card *led_card;
+       struct snd_ctl_led *led;
+       char link_name[32];
+
+       for (group = 0; group < MAX_LED; group++) {
+               led = &snd_ctl_leds[group];
+               led_card = led->cards[card->number];
+               if (!led_card)
+                       continue;
+               snprintf(link_name, sizeof(link_name), "led-%s", led->name);
+               sysfs_remove_link(&card->ctl_dev.kobj, link_name);
+               sysfs_remove_link(&led_card->dev.kobj, "card");
+               device_del(&led_card->dev);
+               kfree(led_card);
+               led->cards[card->number] = NULL;
+       }
+}
+
+/*
+ * Control layer registration
+ */
+static struct snd_ctl_layer_ops snd_ctl_led_lops = {
+       .module_name = SND_CTL_LAYER_MODULE_LED,
+       .lregister = snd_ctl_led_register,
+       .ldisconnect = snd_ctl_led_disconnect,
+       .lnotify = snd_ctl_led_notify,
+};
+
+static int __init snd_ctl_led_init(void)
+{
+       struct snd_ctl_led *led;
+       unsigned int group;
+
+       device_initialize(&snd_ctl_led_dev);
+       snd_ctl_led_dev.class = sound_class;
+       dev_set_name(&snd_ctl_led_dev, "ctl-led");
+       if (device_add(&snd_ctl_led_dev)) {
+               put_device(&snd_ctl_led_dev);
+               return -ENOMEM;
+       }
+       for (group = 0; group < MAX_LED; group++) {
+               led = &snd_ctl_leds[group];
+               INIT_LIST_HEAD(&led->controls);
+               device_initialize(&led->dev);
+               led->dev.parent = &snd_ctl_led_dev;
+               led->dev.groups = snd_ctl_led_dev_attr_groups;
+               dev_set_name(&led->dev, led->name);
+               if (device_add(&led->dev)) {
+                       put_device(&led->dev);
+                       for (; group > 0; group--) {
+                               led = &snd_ctl_leds[group];
+                               device_del(&led->dev);
+                       }
+                       device_del(&snd_ctl_led_dev);
+                       return -ENOMEM;
+               }
+       }
+       snd_ctl_register_layer(&snd_ctl_led_lops);
+       return 0;
+}
+
+static void __exit snd_ctl_led_exit(void)
+{
+       struct snd_ctl_led *led;
+       struct snd_card *card;
+       unsigned int group, card_number;
+
+       snd_ctl_disconnect_layer(&snd_ctl_led_lops);
+       for (card_number = 0; card_number < SNDRV_CARDS; card_number++) {
+               if (!snd_ctl_led_card_valid[card_number])
+                       continue;
+               card = snd_card_ref(card_number);
+               if (card) {
+                       snd_ctl_led_sysfs_remove(card);
+                       snd_card_unref(card);
+               }
+       }
+       for (group = 0; group < MAX_LED; group++) {
+               led = &snd_ctl_leds[group];
+               device_del(&led->dev);
+       }
+       device_del(&snd_ctl_led_dev);
+       snd_ctl_led_clean(NULL);
+}
+
+module_init(snd_ctl_led_init)
+module_exit(snd_ctl_led_exit)