Skip to contentSkip to conversation

snd-ice1712: Add Philips PSC724 Ultimate Edge

RFC PATCH Superseded Ondrej Zary 2 participants 10 messages Sep 18, 2012 – Sep 19, 2012 Sound

v2 of 3 Superseded Latest is v3 →

lore
Share & tools
lore
#0 lore raw

v2 cover snd-ice1712: Add Philips PSC724 Ultimate Edge

Hello,
this is second attempt to add Philips PSC724 Ultimate Edge sound card support to snd-ice1712 driver. Unlike other snd-ice1712 subdrivers, the codec code is splitted into separate modules and thus reusable by other ice1712 subdrivers.

Working: all analog outputs (front, rear, center+lfe, headphone) and inputs (front mic, rear mic, line, cd, aux), mixer, headphone jack detection, suspend/resume

Currently untested: SPDIF

Buggy: switching AGC mode in mixer causes alsamixer to crash - looks like it does not like notifications about control activating/deactivating

#1 lore raw

PATCH v2 1/5 snd-ice1712: add chip_exit callback

Add chip_exit callback to allow card subdrivers to do cleanup work on module removal.

Needed by Philips PSC724 subdriver to cancel delayed work.

---
 sound/pci/ice1712/ice1712.c |    8 +++++++-
 sound/pci/ice1712/ice1712.h |    3 +++
 sound/pci/ice1712/ice1724.c |    8 +++++++-
 3 files changed, 17 insertions(+), 2 deletions(-)
sound/pci/ice1712/ice1712.c +7 -1
index 5be2e12..f42b5b1 100644
@@ -2686,6 +2686,7 @@ static int __devinit snd_ice1712_probe(struct pci_dev *pci,
 	for (tbl = card_tables; *tbl; tbl++) {
 		for (c = *tbl; c->subvendor; c++) {
 			if (c->subvendor == ice->eeprom.subvendor) {
+				ice->card_info = c;
 				strcpy(card->shortname, c->name);
 				if (c->driver) /* specific driver? */
 					strcpy(card->driver, c->driver);
@@ -2799,7 +2800,12 @@ static int __devinit snd_ice1712_probe(struct pci_dev *pci,
 
 static void __devexit snd_ice1712_remove(struct pci_dev *pci)
 {
-	snd_card_free(pci_get_drvdata(pci));
+	struct snd_card *card = pci_get_drvdata(pci);
+	struct snd_ice1712 *ice = card->private_data;
+
+	if (ice->card_info && ice->card_info->chip_exit)
+		ice->card_info->chip_exit(ice);
+	snd_card_free(card);
 	pci_set_drvdata(pci, NULL);
 }
 
sound/pci/ice1712/ice1712.h +3 -0
index 0da778a..79f8aeb 100644
@@ -288,6 +288,7 @@ struct snd_ice1712_spdif {
 	} ops;
 };
 
+struct snd_ice1712_card_info;
 
 struct snd_ice1712 {
 	unsigned long conp_dma_size;
@@ -324,6 +325,7 @@ struct snd_ice1712 {
 	struct snd_info_entry *proc_entry;
 
 	struct snd_ice1712_eeprom eeprom;
+	struct snd_ice1712_card_info *card_info;
 
 	unsigned int pro_volumes[20];
 	unsigned int omni:1;		/* Delta Omni I/O */
@@ -517,6 +519,7 @@ struct snd_ice1712_card_info {
 	char *model;
 	char *driver;
 	int (*chip_init)(struct snd_ice1712 *);
+	void (*chip_exit)(struct snd_ice1712 *);
 	int (*build_controls)(struct snd_ice1712 *);
 	unsigned int no_mpu401:1;
 	unsigned int mpu401_1_info_flags;
sound/pci/ice1712/ice1724.c +7 -1
index bed9f34..a529d30 100644
@@ -2348,6 +2348,7 @@ static int __devinit snd_vt1724_read_eeprom(struct snd_ice1712 *ice,
 				ice->eeprom.subvendor = c->subvendor;
 			} else if (c->subvendor != ice->eeprom.subvendor)
 				continue;
+			ice->card_info = c;
 			if (!c->eeprom_size || !c->eeprom_data)
 				goto found;
 			/* if the EEPROM is given by the driver, use it */
@@ -2788,7 +2789,12 @@ __found:
 
 static void __devexit snd_vt1724_remove(struct pci_dev *pci)
 {
-	snd_card_free(pci_get_drvdata(pci));
+	struct snd_card *card = pci_get_drvdata(pci);
+	struct snd_ice1712 *ice = card->private_data;
+
+	if (ice->card_info && ice->card_info->chip_exit)
+		ice->card_info->chip_exit(ice);
+	snd_card_free(card);
 	pci_set_drvdata(pci, NULL);
 }
 
Signature · 1 line
Ondrej Zary
#2 lore raw

PATCH v2 2/5 snd-ice1712: Add Wolfson Microelectronics WM8766 codec support

In reply to Ondrej Zary

Needed by Philips PSC724 subdriver. The code does not contain any card-specific bits so other ice17xx cards using this codec could be converted to use this generic code.

---
 sound/pci/ice1712/wm8766.c |  383 ++++++++++++++++++++++++++++++++++++++++++++
 sound/pci/ice1712/wm8766.h |  161 +++++++++++++++++++
 2 files changed, 544 insertions(+), 0 deletions(-)
 create mode 100644 sound/pci/ice1712/wm8766.c
 create mode 100644 sound/pci/ice1712/wm8766.h
sound/pci/ice1712/wm8766.c new file +383 -0
new file mode 100644
index 0000000..a782533
@@ -0,0 +1,383 @@
+/*
+ *   ALSA driver for WM8766 codecs
+ *
+ *	Copyright (c) 2012 Ondrej Zary <[email protected]>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include "wm8766.h"
+
+MODULE_AUTHOR("Ondrej Zary <[email protected]>");
+MODULE_DESCRIPTION("Routines for control of WM8766 codecs");
+MODULE_LICENSE("GPL");
+
+/* low-level access */
+
+static void snd_wm8766_write(struct snd_wm8766 *wm, u16 addr, u16 data)
+{
+	if (addr < WM8766_REG_RESET)
+		wm->regs[addr] = data;
+	wm->ops.write(wm, addr, data);
+}
+
+/* mixer controls */
+
+static const DECLARE_TLV_DB_SCALE(wm8766_tlv, -12750, 50, 1);
+
+static struct snd_wm8766_ctl default_ctl[WM8766_CTL_COUNT] = {
+	[WM8766_CTL_CH1_VOL] = {
+		.name = "Channel 1 Playback Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8766_tlv,
+		.reg1 = WM8766_REG_DACL1,
+		.reg2 = WM8766_REG_DACR1,
+		.mask1 = WM8766_VOL_MASK,
+		.mask2 = WM8766_VOL_MASK,
+		.max = 0xff,
+		.flags = WM8766_FLAG_STEREO | WM8766_FLAG_VOL_UPDATE,
+	},
+	[WM8766_CTL_CH2_VOL] = {
+		.name = "Channel 2 Playback Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8766_tlv,
+		.reg1 = WM8766_REG_DACL2,
+		.reg2 = WM8766_REG_DACR2,
+		.mask1 = WM8766_VOL_MASK,
+		.mask2 = WM8766_VOL_MASK,
+		.max = 0xff,
+		.flags = WM8766_FLAG_STEREO | WM8766_FLAG_VOL_UPDATE,
+	},
+	[WM8766_CTL_CH3_VOL] = {
+		.name = "Channel 3 Playback Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8766_tlv,
+		.reg1 = WM8766_REG_DACL3,
+		.reg2 = WM8766_REG_DACR3,
+		.mask1 = WM8766_VOL_MASK,
+		.mask2 = WM8766_VOL_MASK,
+		.max = 0xff,
+		.flags = WM8766_FLAG_STEREO | WM8766_FLAG_VOL_UPDATE,
+	},
+	[WM8766_CTL_CH1_SW] = {
+		.name = "Channel 1 Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_MUTE1,
+		.flags = WM8766_FLAG_INVERT,
+	},
+	[WM8766_CTL_CH2_SW] = {
+		.name = "Channel 2 Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_MUTE2,
+		.flags = WM8766_FLAG_INVERT,
+	},
+	[WM8766_CTL_CH3_SW] = {
+		.name = "Channel 3 Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_MUTE3,
+		.flags = WM8766_FLAG_INVERT,
+	},
+	[WM8766_CTL_PHASE1_SW] = {
+		.name = "Channel 1 Phase Invert Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_IFCTRL,
+		.mask1 = WM8766_PHASE_INVERT1,
+	},
+	[WM8766_CTL_PHASE2_SW] = {
+		.name = "Channel 2 Phase Invert Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_IFCTRL,
+		.mask1 = WM8766_PHASE_INVERT2,
+	},
+	[WM8766_CTL_PHASE3_SW] = {
+		.name = "Channel 3 Phase Invert Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_IFCTRL,
+		.mask1 = WM8766_PHASE_INVERT3,
+	},
+	[WM8766_CTL_DEEMPH1_SW] = {
+		.name = "Channel 1 Deemphasis Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_DEEMP1,
+	},
+	[WM8766_CTL_DEEMPH2_SW] = {
+		.name = "Channel 2 Deemphasis Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_DEEMP2,
+	},
+	[WM8766_CTL_DEEMPH3_SW] = {
+		.name = "Channel 3 Deemphasis Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_DEEMP3,
+	},
+	[WM8766_CTL_IZD_SW] = {
+		.name = "Infinite Zero Detect Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL1,
+		.mask1 = WM8766_DAC_IZD,
+	},
+	[WM8766_CTL_ZC_SW] = {
+		.name = "Zero Cross Detect Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8766_REG_DACCTRL2,
+		.mask1 = WM8766_DAC2_ZCD,
+		.flags = WM8766_FLAG_INVERT,
+	},
+};
+
+/* exported functions */
+
+void snd_wm8766_init(struct snd_wm8766 *wm)
+{
+	int i;
+	static const u16 default_values[] = {
+		0x000, 0x100,
+		0x120, 0x000,
+		0x000, 0x100, 0x000, 0x100, 0x000,
+		0x000, 0x080,
+	};
+
+	memcpy(wm->ctl, default_ctl, sizeof(wm->ctl));
+
+	snd_wm8766_write(wm, WM8766_REG_RESET, 0x00); /* reset */
+	udelay(10);
+	/* load defaults */
+	for (i = 0; i < ARRAY_SIZE(default_values); i++)
+		snd_wm8766_write(wm, i, default_values[i]);
+}
+EXPORT_SYMBOL(snd_wm8766_init);
+
+void snd_wm8766_resume(struct snd_wm8766 *wm)
+{
+	int i;
+
+	for (i = 0; i < WM8766_REG_COUNT; i++)
+		snd_wm8766_write(wm, i, wm->regs[i]);
+}
+EXPORT_SYMBOL(snd_wm8766_resume);
+
+void snd_wm8766_set_if(struct snd_wm8766 *wm, u16 dac)
+{
+	u16 val = wm->regs[WM8766_REG_IFCTRL] & ~WM8766_IF_MASK;
+
+	dac &= WM8766_IF_MASK;
+	snd_wm8766_write(wm, WM8766_REG_IFCTRL, val | dac);
+}
+EXPORT_SYMBOL(snd_wm8766_set_if);
+
+void snd_wm8766_set_master_mode(struct snd_wm8766 *wm, u16 mode)
+{
+	u16 val = wm->regs[WM8766_REG_DACCTRL3] & ~WM8766_DAC3_MSTR_MASK;
+
+	mode &= WM8766_DAC3_MSTR_MASK;
+	snd_wm8766_write(wm, WM8766_REG_DACCTRL3, val | mode);
+}
+EXPORT_SYMBOL(snd_wm8766_set_master_mode);
+
+void snd_wm8766_set_power(struct snd_wm8766 *wm, u16 power)
+{
+	u16 val = wm->regs[WM8766_REG_DACCTRL3] & ~WM8766_DAC3_POWER_MASK;
+
+	power &= WM8766_DAC3_POWER_MASK;
+	snd_wm8766_write(wm, WM8766_REG_DACCTRL3, val | power);
+}
+EXPORT_SYMBOL(snd_wm8766_set_power);
+
+void snd_wm8766_volume_restore(struct snd_wm8766 *wm)
+{
+	u16 val = wm->regs[WM8766_REG_DACR1];
+	/* restore volume after MCLK stopped */
+	snd_wm8766_write(wm, WM8766_REG_DACR1, val | WM8766_VOL_UPDATE);
+}
+EXPORT_SYMBOL(snd_wm8766_volume_restore);
+
+/* mixer callbacks */
+
+static int snd_wm8766_volume_info(struct snd_kcontrol *kcontrol,
+				   struct snd_ctl_elem_info *uinfo)
+{
+	struct snd_wm8766 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = (wm->ctl[n].flags & WM8766_FLAG_STEREO) ? 2 : 1;
+	uinfo->value.integer.min = wm->ctl[n].min;
+	uinfo->value.integer.max = wm->ctl[n].max;
+
+	return 0;
+}
+
+static int snd_wm8766_enum_info(struct snd_kcontrol *kcontrol,
+				      struct snd_ctl_elem_info *uinfo)
+{
+	struct snd_wm8766 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+
+	return snd_ctl_enum_info(uinfo, 1, wm->ctl[n].max,
+						wm->ctl[n].enum_names);
+}
+
+static int snd_wm8766_ctl_get(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_wm8766 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+	u16 val1, val2;
+
+	if (wm->ctl[n].get)
+		wm->ctl[n].get(wm, &val1, &val2);
+	else {
+		val1 = wm->regs[wm->ctl[n].reg1] & wm->ctl[n].mask1;
+		val1 >>= __ffs(wm->ctl[n].mask1);
+		if (wm->ctl[n].flags & WM8766_FLAG_STEREO) {
+			val2 = wm->regs[wm->ctl[n].reg2] & wm->ctl[n].mask2;
+			val2 >>= __ffs(wm->ctl[n].mask2);
+			if (wm->ctl[n].flags & WM8766_FLAG_VOL_UPDATE)
+				val2 &= ~WM8766_VOL_UPDATE;
+		}
+	}
+	if (wm->ctl[n].flags & WM8766_FLAG_INVERT) {
+		val1 = wm->ctl[n].max - (val1 - wm->ctl[n].min);
+		val2 = wm->ctl[n].max - (val2 - wm->ctl[n].min);
+	}
+	ucontrol->value.integer.value[0] = val1;
+	if (wm->ctl[n].flags & WM8766_FLAG_STEREO)
+		ucontrol->value.integer.value[1] = val2;
+
+	return 0;
+}
+
+static int snd_wm8766_ctl_put(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_wm8766 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+	u16 val, regval1, regval2;
+
+	/* this also works for enum because value is an union */
+	regval1 = ucontrol->value.integer.value[0];
+	regval2 = ucontrol->value.integer.value[1];
+	if (wm->ctl[n].flags & WM8766_FLAG_INVERT) {
+		regval1 = wm->ctl[n].max - (regval1 - wm->ctl[n].min);
+		regval2 = wm->ctl[n].max - (regval2 - wm->ctl[n].min);
+	}
+	if (wm->ctl[n].set)
+		wm->ctl[n].set(wm, regval1, regval2);
+	else {
+		val = wm->regs[wm->ctl[n].reg1] & ~wm->ctl[n].mask1;
+		val |= regval1 << __ffs(wm->ctl[n].mask1);
+		/* both stereo controls in one register */
+		if (wm->ctl[n].flags & WM8766_FLAG_STEREO &&
+				wm->ctl[n].reg1 == wm->ctl[n].reg2) {
+			val &= ~wm->ctl[n].mask2;
+			val |= regval2 << __ffs(wm->ctl[n].mask2);
+		}
+		snd_wm8766_write(wm, wm->ctl[n].reg1, val);
+		/* stereo controls in different registers */
+		if (wm->ctl[n].flags & WM8766_FLAG_STEREO &&
+				wm->ctl[n].reg1 != wm->ctl[n].reg2) {
+			val = wm->regs[wm->ctl[n].reg2] & ~wm->ctl[n].mask2;
+			val |= regval2 << __ffs(wm->ctl[n].mask2);
+			if (wm->ctl[n].flags & WM8766_FLAG_VOL_UPDATE)
+				val |= WM8766_VOL_UPDATE;
+			snd_wm8766_write(wm, wm->ctl[n].reg2, val);
+		}
+	}
+
+	return 0;
+}
+
+static int snd_wm8766_add_control(struct snd_wm8766 *wm, int num)
+{
+	struct snd_kcontrol_new cont;
+	struct snd_kcontrol *ctl;
+
+	memset(&cont, 0, sizeof(cont));
+	cont.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+	cont.private_value = num;
+	cont.name = wm->ctl[num].name;
+	cont.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+	if (wm->ctl[num].flags & WM8766_FLAG_LIM ||
+	    wm->ctl[num].flags & WM8766_FLAG_ALC)
+		cont.access |= SNDRV_CTL_ELEM_ACCESS_INACTIVE;
+	cont.tlv.p = NULL;
+	cont.get = snd_wm8766_ctl_get;
+	cont.put = snd_wm8766_ctl_put;
+
+	switch (wm->ctl[num].type) {
+	case SNDRV_CTL_ELEM_TYPE_INTEGER:
+		cont.info = snd_wm8766_volume_info;
+		cont.access |= SNDRV_CTL_ELEM_ACCESS_TLV_READ;
+		cont.tlv.p = wm->ctl[num].tlv;
+		break;
+	case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+		wm->ctl[num].max = 1;
+		if (wm->ctl[num].flags & WM8766_FLAG_STEREO)
+			cont.info = snd_ctl_boolean_stereo_info;
+		else
+			cont.info = snd_ctl_boolean_mono_info;
+		break;
+	case SNDRV_CTL_ELEM_TYPE_ENUMERATED:
+		cont.info = snd_wm8766_enum_info;
+		break;
+	default:
+		return -EINVAL;
+	}
+	ctl = snd_ctl_new1(&cont, wm);
+	if (!ctl)
+		return -ENOMEM;
+	wm->ctl[num].kctl = ctl;
+
+	return snd_ctl_add(wm->card, ctl);
+}
+
+int snd_wm8766_build_controls(struct snd_wm8766 *wm)
+{
+	int err, i;
+
+	for (i = 0; i < WM8766_CTL_COUNT; i++)
+		if (wm->ctl[i].name) {
+			err = snd_wm8766_add_control(wm, i);
+			if (err < 0)
+				return err;
+		}
+
+	return 0;
+}
+EXPORT_SYMBOL(snd_wm8766_build_controls);
+
+static int __init alsa_wm8766_module_init(void)
+{
+	return 0;
+}
+
+static void __exit alsa_wm8766_module_exit(void)
+{
+}
+
+module_init(alsa_wm8766_module_init)
+module_exit(alsa_wm8766_module_exit)
sound/pci/ice1712/wm8766.h new file +161 -0

161 lines not on this page · raw message

Signature · 1 line
Ondrej Zary
#3 lore raw

PATCH v2 3/5 snd-ice1712: Add Wolfson Microelectronics WM8776 codec support

In reply to Ondrej Zary

Needed by Philips PSC724 subdriver. The code does not contain any card-specific bits so other ice17xx cards using this codec could be converted to use this generic code.

---
 sound/pci/ice1712/wm8776.c |  655 ++++++++++++++++++++++++++++++++++++++++++++
 sound/pci/ice1712/wm8776.h |  224 +++++++++++++++
 2 files changed, 879 insertions(+), 0 deletions(-)
 create mode 100644 sound/pci/ice1712/wm8776.c
 create mode 100644 sound/pci/ice1712/wm8776.h
sound/pci/ice1712/wm8776.c new file +655 -0
new file mode 100644
index 0000000..36a3060
@@ -0,0 +1,655 @@
+/*
+ *   ALSA driver for WM8776 codecs
+ *
+ *	Copyright (c) 2012 Ondrej Zary <[email protected]>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include "wm8776.h"
+
+MODULE_AUTHOR("Ondrej Zary <[email protected]>");
+MODULE_DESCRIPTION("Routines for control of WM8776 codecs");
+MODULE_LICENSE("GPL");
+
+/* low-level access */
+
+static void snd_wm8776_write(struct snd_wm8776 *wm, u16 addr, u16 data)
+{
+	u8 bus_addr = addr << 1 | data >> 8;	/* addr + 9th data bit */
+	u8 bus_data = data & 0xff;		/* remaining 8 data bits */
+
+	if (addr < WM8776_REG_RESET)
+		wm->regs[addr] = data;
+	wm->ops.write(wm, bus_addr, bus_data);
+}
+
+/* register-level functions */
+
+static void snd_wm8776_activate_ctl(struct snd_wm8776 *wm, char *ctl_name,
+				    bool active)
+{
+	struct snd_card *card = wm->card;
+	struct snd_kcontrol *kctl;
+	struct snd_kcontrol_volatile *vd;
+	struct snd_ctl_elem_id elem_id;
+	unsigned int index_offset;
+
+	memset(&elem_id, 0, sizeof(elem_id));
+	strncpy(elem_id.name, ctl_name, sizeof(elem_id.name));
+	elem_id.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+	kctl = snd_ctl_find_id(card, &elem_id);
+	if (!kctl)
+		return;
+	index_offset = snd_ctl_get_ioff(kctl, &kctl->id);
+	vd = &kctl->vd[index_offset];
+	if (active)
+		vd->access &= ~SNDRV_CTL_ELEM_ACCESS_INACTIVE;
+	else
+		vd->access |= SNDRV_CTL_ELEM_ACCESS_INACTIVE;
+	snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_INFO, &kctl->id);
+}
+
+static void snd_wm8776_update_agc_ctl(struct snd_wm8776 *wm)
+{
+	int i, flags_on = 0, flags_off = 0;
+
+	switch (wm->agc_mode) {
+	case WM8776_AGC_OFF:
+		flags_off = WM8776_FLAG_LIM | WM8776_FLAG_ALC;
+		break;
+	case WM8776_AGC_LIM:
+		flags_off = WM8776_FLAG_ALC;
+		flags_on = WM8776_FLAG_LIM;
+		break;
+	case WM8776_AGC_ALC_R:
+	case WM8776_AGC_ALC_L:
+	case WM8776_AGC_ALC_STEREO:
+		flags_off = WM8776_FLAG_LIM;
+		flags_on = WM8776_FLAG_ALC;
+		break;
+	}
+
+	for (i = 0; i < WM8776_CTL_COUNT; i++)
+		if (wm->ctl[i].flags & flags_off)
+			snd_wm8776_activate_ctl(wm, wm->ctl[i].name, false);
+		else if (wm->ctl[i].flags & flags_on)
+			snd_wm8776_activate_ctl(wm, wm->ctl[i].name, true);
+}
+
+static void snd_wm8776_set_agc(struct snd_wm8776 *wm, u16 agc, u16 nothing)
+{
+	u16 alc1 = wm->regs[WM8776_REG_ALCCTRL1] & ~WM8776_ALC1_LCT_MASK;
+	u16 alc2 = wm->regs[WM8776_REG_ALCCTRL2] & ~WM8776_ALC2_LCEN;
+
+	switch (agc) {
+	case 0:	/* Off */
+		wm->agc_mode = WM8776_AGC_OFF;
+		break;
+	case 1: /* Limiter */
+		alc2 |= WM8776_ALC2_LCEN;
+		wm->agc_mode = WM8776_AGC_LIM;
+		break;
+	case 2: /* ALC Right */
+		alc1 |= WM8776_ALC1_LCSEL_ALCR;
+		alc2 |= WM8776_ALC2_LCEN;
+		wm->agc_mode = WM8776_AGC_ALC_R;
+		break;
+	case 3: /* ALC Left */
+		alc1 |= WM8776_ALC1_LCSEL_ALCL;
+		alc2 |= WM8776_ALC2_LCEN;
+		wm->agc_mode = WM8776_AGC_ALC_L;
+		break;
+	case 4: /* ALC Stereo */
+		alc1 |= WM8776_ALC1_LCSEL_ALCSTEREO;
+		alc2 |= WM8776_ALC2_LCEN;
+		wm->agc_mode = WM8776_AGC_ALC_STEREO;
+		break;
+	}
+	snd_wm8776_write(wm, WM8776_REG_ALCCTRL1, alc1);
+	snd_wm8776_write(wm, WM8776_REG_ALCCTRL2, alc2);
+	snd_wm8776_update_agc_ctl(wm);
+}
+
+static void snd_wm8776_get_agc(struct snd_wm8776 *wm, u16 *mode, u16 *nothing)
+{
+	*mode = wm->agc_mode;
+}
+
+/* mixer controls */
+
+static const DECLARE_TLV_DB_SCALE(wm8776_hp_tlv, -7400, 100, 1);
+static const DECLARE_TLV_DB_SCALE(wm8776_dac_tlv, -12750, 50, 1);
+static const DECLARE_TLV_DB_SCALE(wm8776_adc_tlv, -10350, 50, 1);
+static const DECLARE_TLV_DB_SCALE(wm8776_lct_tlv, -1600, 100, 0);
+static const DECLARE_TLV_DB_SCALE(wm8776_maxgain_tlv, 0, 400, 0);
+static const DECLARE_TLV_DB_SCALE(wm8776_ngth_tlv, -7800, 600, 0);
+static const DECLARE_TLV_DB_SCALE(wm8776_maxatten_lim_tlv, -1200, 100, 0);
+static const DECLARE_TLV_DB_SCALE(wm8776_maxatten_alc_tlv, -2100, 400, 0);
+
+static struct snd_wm8776_ctl default_ctl[WM8776_CTL_COUNT] = {
+	[WM8776_CTL_DAC_VOL] = {
+		.name = "Master Playback Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_dac_tlv,
+		.reg1 = WM8776_REG_DACLVOL,
+		.reg2 = WM8776_REG_DACRVOL,
+		.mask1 = WM8776_DACVOL_MASK,
+		.mask2 = WM8776_DACVOL_MASK,
+		.max = 0xff,
+		.flags = WM8776_FLAG_STEREO | WM8776_FLAG_VOL_UPDATE,
+	},
+	[WM8776_CTL_DAC_SW] = {
+		.name = "Master Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_DACCTRL1,
+		.reg2 = WM8776_REG_DACCTRL1,
+		.mask1 = WM8776_DAC_PL_LL,
+		.mask2 = WM8776_DAC_PL_RR,
+		.flags = WM8776_FLAG_STEREO,
+	},
+	[WM8776_CTL_DAC_ZC_SW] = {
+		.name = "Master Zero Cross Detect Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_DACCTRL1,
+		.mask1 = WM8776_DAC_DZCEN,
+	},
+	[WM8776_CTL_HP_VOL] = {
+		.name = "Headphone Playback Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_hp_tlv,
+		.reg1 = WM8776_REG_HPLVOL,
+		.reg2 = WM8776_REG_HPRVOL,
+		.mask1 = WM8776_HPVOL_MASK,
+		.mask2 = WM8776_HPVOL_MASK,
+		.min = 0x2f,
+		.max = 0x7f,
+		.flags = WM8776_FLAG_STEREO | WM8776_FLAG_VOL_UPDATE,
+	},
+	[WM8776_CTL_HP_SW] = {
+		.name = "Headphone Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_PWRDOWN,
+		.mask1 = WM8776_PWR_HPPD,
+		.flags = WM8776_FLAG_INVERT,
+	},
+	[WM8776_CTL_HP_ZC_SW] = {
+		.name = "Headphone Zero Cross Detect Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_HPLVOL,
+		.reg2 = WM8776_REG_HPRVOL,
+		.mask1 = WM8776_VOL_HPZCEN,
+		.mask2 = WM8776_VOL_HPZCEN,
+		.flags = WM8776_FLAG_STEREO,
+	},
+	[WM8776_CTL_AUX_SW] = {
+		.name = "AUX Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_OUTMUX,
+		.mask1 = WM8776_OUTMUX_AUX,
+	},
+	[WM8776_CTL_BYPASS_SW] = {
+		.name = "Bypass Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_OUTMUX,
+		.mask1 = WM8776_OUTMUX_BYPASS,
+	},
+	[WM8776_CTL_DAC_IZD_SW] = {
+		.name = "Infinite Zero Detect Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_DACCTRL1,
+		.mask1 = WM8776_DAC_IZD,
+	},
+	[WM8776_CTL_PHASE_SW] = {
+		.name = "Phase Invert Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_PHASESWAP,
+		.reg2 = WM8776_REG_PHASESWAP,
+		.mask1 = WM8776_PHASE_INVERTL,
+		.mask2 = WM8776_PHASE_INVERTR,
+		.flags = WM8776_FLAG_STEREO,
+	},
+	[WM8776_CTL_DEEMPH_SW] = {
+		.name = "Deemphasis Playback Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_DACCTRL2,
+		.mask1 = WM8776_DAC2_DEEMPH,
+	},
+	[WM8776_CTL_ADC_VOL] = {
+		.name = "Input Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_adc_tlv,
+		.reg1 = WM8776_REG_ADCLVOL,
+		.reg2 = WM8776_REG_ADCRVOL,
+		.mask1 = WM8776_ADC_GAIN_MASK,
+		.mask2 = WM8776_ADC_GAIN_MASK,
+		.max = 0xff,
+		.flags = WM8776_FLAG_STEREO | WM8776_FLAG_VOL_UPDATE,
+	},
+	[WM8776_CTL_ADC_SW] = {
+		.name = "Input Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_ADCMUX,
+		.reg2 = WM8776_REG_ADCMUX,
+		.mask1 = WM8776_ADC_MUTEL,
+		.mask2 = WM8776_ADC_MUTER,
+		.flags = WM8776_FLAG_STEREO | WM8776_FLAG_INVERT,
+	},
+	[WM8776_CTL_INPUT1_SW] = {
+		.name = "AIN1 Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_ADCMUX,
+		.mask1 = WM8776_ADC_MUX_AIN1,
+	},
+	[WM8776_CTL_INPUT2_SW] = {
+		.name = "AIN2 Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_ADCMUX,
+		.mask1 = WM8776_ADC_MUX_AIN2,
+	},
+	[WM8776_CTL_INPUT3_SW] = {
+		.name = "AIN3 Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_ADCMUX,
+		.mask1 = WM8776_ADC_MUX_AIN3,
+	},
+	[WM8776_CTL_INPUT4_SW] = {
+		.name = "AIN4 Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_ADCMUX,
+		.mask1 = WM8776_ADC_MUX_AIN4,
+	},
+	[WM8776_CTL_INPUT5_SW] = {
+		.name = "AIN5 Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_ADCMUX,
+		.mask1 = WM8776_ADC_MUX_AIN5,
+	},
+	[WM8776_CTL_AGC_SEL] = {
+		.name = "AGC Select Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = { "Off", "Limiter", "ALC Right", "ALC Left",
+				"ALC Stereo" },
+		.max = 5,	/* .enum_names item count */
+		.set = snd_wm8776_set_agc,
+		.get = snd_wm8776_get_agc,
+	},
+	[WM8776_CTL_LIM_THR] = {
+		.name = "Limiter Threshold Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_lct_tlv,
+		.reg1 = WM8776_REG_ALCCTRL1,
+		.mask1 = WM8776_ALC1_LCT_MASK,
+		.max = 15,
+		.flags = WM8776_FLAG_LIM,
+	},
+	[WM8776_CTL_LIM_ATK] = {
+		.name = "Limiter Attack Time Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = { "0.25 ms", "0.5 ms", "1 ms", "2 ms", "4 ms",
+			"8 ms", "16 ms", "32 ms", "64 ms", "128 ms", "256 ms" },
+		.max = 11,	/* .enum_names item count */
+		.reg1 = WM8776_REG_ALCCTRL3,
+		.mask1 = WM8776_ALC3_ATK_MASK,
+		.flags = WM8776_FLAG_LIM,
+	},
+	[WM8776_CTL_LIM_DCY] = {
+		.name = "Limiter Decay Time Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = {	"1.2 ms", "2.4 ms", "4.8 ms", "9.6 ms",
+			"19.2 ms", "38.4 ms", "76.8 ms", "154 ms", "307 ms",
+			"614 ms", "1.23 s" },
+		.max = 11,	/* .enum_names item count */
+		.reg1 = WM8776_REG_ALCCTRL3,
+		.mask1 = WM8776_ALC3_DCY_MASK,
+		.flags = WM8776_FLAG_LIM,
+	},
+	[WM8776_CTL_LIM_TRANWIN] = {
+		.name = "Limiter Transient Window Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = {	"0 us", "62.5 us", "125 us", "250 us", "500 us",
+			"1 ms", "2 ms", "4 ms" },
+		.max = 8,	/* .enum_names item count */
+		.reg1 = WM8776_REG_LIMITER,
+		.mask1 = WM8776_LIM_TRANWIN_MASK,
+		.flags = WM8776_FLAG_LIM,
+	},
+	[WM8776_CTL_LIM_MAXATTN] = {
+		.name = "Limiter Maximum Attenuation Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_maxatten_lim_tlv,
+		.reg1 = WM8776_REG_LIMITER,
+		.mask1 = WM8776_LIM_MAXATTEN_MASK,
+		.min = 3,
+		.max = 12,
+		.flags = WM8776_FLAG_LIM | WM8776_FLAG_INVERT,
+	},
+	[WM8776_CTL_ALC_TGT] = {
+		.name = "ALC Target Level Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_lct_tlv,
+		.reg1 = WM8776_REG_ALCCTRL1,
+		.mask1 = WM8776_ALC1_LCT_MASK,
+		.max = 15,
+		.flags = WM8776_FLAG_ALC,
+	},
+	[WM8776_CTL_ALC_ATK] = {
+		.name = "ALC Attack Time Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = { "8.40 ms", "16.8 ms", "33.6 ms", "67.2 ms",
+			"134 ms", "269 ms", "538 ms", "1.08 s",	"2.15 s",
+			"4.3 s", "8.6 s" },
+		.max = 11,	/* .enum_names item count */
+		.reg1 = WM8776_REG_ALCCTRL3,
+		.mask1 = WM8776_ALC3_ATK_MASK,
+		.flags = WM8776_FLAG_ALC,
+	},
+	[WM8776_CTL_ALC_DCY] = {
+		.name = "ALC Decay Time Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = {	"33.5 ms", "67.0 ms", "134 ms", "268 ms",
+			"536 ms", "1.07 s", "2.14 s", "4.29 s",	"8.58 s",
+			"17.2 s", "34.3 s" },
+		.max = 11,	/* .enum_names item count */
+		.reg1 = WM8776_REG_ALCCTRL3,
+		.mask1 = WM8776_ALC3_DCY_MASK,
+		.flags = WM8776_FLAG_ALC,
+	},
+	[WM8776_CTL_ALC_MAXGAIN] = {
+		.name = "ALC Maximum Gain Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_maxgain_tlv,
+		.reg1 = WM8776_REG_ALCCTRL1,
+		.mask1 = WM8776_ALC1_MAXGAIN_MASK,
+		.min = 1,
+		.max = 7,
+		.flags = WM8776_FLAG_ALC,
+	},
+	[WM8776_CTL_ALC_MAXATTN] = {
+		.name = "ALC Maximum Attenuation Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_maxatten_alc_tlv,
+		.reg1 = WM8776_REG_LIMITER,
+		.mask1 = WM8776_LIM_MAXATTEN_MASK,
+		.min = 10,
+		.max = 15,
+		.flags = WM8776_FLAG_ALC | WM8776_FLAG_INVERT,
+	},
+	[WM8776_CTL_ALC_HLD] = {
+		.name = "ALC Hold Time Capture Enum",
+		.type = SNDRV_CTL_ELEM_TYPE_ENUMERATED,
+		.enum_names = {	"0 ms", "2.67 ms", "5.33 ms", "10.6 ms",
+			"21.3 ms", "42.7 ms", "85.3 ms", "171 ms", "341 ms",
+			"683 ms", "1.37 s", "2.73 s", "5.46 s", "10.9 s",
+			"21.8 s", "43.7 s" },
+		.max = 16,	/* .enum_names item count */
+		.reg1 = WM8776_REG_ALCCTRL2,
+		.mask1 = WM8776_ALC2_HOLD_MASK,
+		.flags = WM8776_FLAG_ALC,
+	},
+	[WM8776_CTL_NGT_SW] = {
+		.name = "Noise Gate Capture Switch",
+		.type = SNDRV_CTL_ELEM_TYPE_BOOLEAN,
+		.reg1 = WM8776_REG_NOISEGATE,
+		.mask1 = WM8776_NGAT_ENABLE,
+		.flags = WM8776_FLAG_ALC,
+	},
+	[WM8776_CTL_NGT_THR] = {
+		.name = "Noise Gate Threshold Capture Volume",
+		.type = SNDRV_CTL_ELEM_TYPE_INTEGER,
+		.tlv = wm8776_ngth_tlv,
+		.reg1 = WM8776_REG_NOISEGATE,
+		.mask1 = WM8776_NGAT_THR_MASK,
+		.max = 7,
+		.flags = WM8776_FLAG_ALC,
+	},
+};
+
+/* exported functions */
+
+void snd_wm8776_init(struct snd_wm8776 *wm)
+{
+	int i;
+	static const u16 default_values[] = {
+		0x000, 0x100, 0x000,
+		0x000, 0x100, 0x000,
+		0x000, 0x090, 0x000, 0x000,
+		0x022, 0x022, 0x022,
+		0x008, 0x0cf, 0x0cf, 0x07b, 0x000,
+		0x032, 0x000, 0x0a6, 0x001, 0x001
+	};
+
+	memcpy(wm->ctl, default_ctl, sizeof(wm->ctl));
+
+	snd_wm8776_write(wm, WM8776_REG_RESET, 0x00); /* reset */
+	udelay(10);
+	/* load defaults */
+	for (i = 0; i < ARRAY_SIZE(default_values); i++)
+		snd_wm8776_write(wm, i, default_values[i]);
+}
+EXPORT_SYMBOL(snd_wm8776_init);
+
+void snd_wm8776_resume(struct snd_wm8776 *wm)
+{
+	int i;
+
+	for (i = 0; i < WM8776_REG_COUNT; i++)
+		snd_wm8776_write(wm, i, wm->regs[i]);
+}
+EXPORT_SYMBOL(snd_wm8776_resume);
+
+void snd_wm8776_set_dac_if(struct snd_wm8776 *wm, u16 dac)
+{
+	snd_wm8776_write(wm, WM8776_REG_DACIFCTRL, dac);
+}
+EXPORT_SYMBOL(snd_wm8776_set_dac_if);
+
+void snd_wm8776_set_adc_if(struct snd_wm8776 *wm, u16 adc)
+{
+	snd_wm8776_write(wm, WM8776_REG_ADCIFCTRL, adc);
+}
+EXPORT_SYMBOL(snd_wm8776_set_adc_if);
+
+void snd_wm8776_set_master_mode(struct snd_wm8776 *wm, u16 mode)
+{
+	snd_wm8776_write(wm, WM8776_REG_MSTRCTRL, mode);
+}
+EXPORT_SYMBOL(snd_wm8776_set_master_mode);
+
+void snd_wm8776_set_power(struct snd_wm8776 *wm, u16 power)
+{
+	snd_wm8776_write(wm, WM8776_REG_PWRDOWN, power);
+}
+EXPORT_SYMBOL(snd_wm8776_set_power);
+
+void snd_wm8776_volume_restore(struct snd_wm8776 *wm)
+{
+	u16 val = wm->regs[WM8776_REG_DACRVOL];
+	/* restore volume after MCLK stopped */
+	snd_wm8776_write(wm, WM8776_REG_DACRVOL, val | WM8776_VOL_UPDATE);
+}
+EXPORT_SYMBOL(snd_wm8776_volume_restore);
+
+/* mixer callbacks */
+
+static int snd_wm8776_volume_info(struct snd_kcontrol *kcontrol,
+				   struct snd_ctl_elem_info *uinfo)
+{
+	struct snd_wm8776 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = (wm->ctl[n].flags & WM8776_FLAG_STEREO) ? 2 : 1;
+	uinfo->value.integer.min = wm->ctl[n].min;
+	uinfo->value.integer.max = wm->ctl[n].max;
+
+	return 0;
+}
+
+static int snd_wm8776_enum_info(struct snd_kcontrol *kcontrol,
+				      struct snd_ctl_elem_info *uinfo)
+{
+	struct snd_wm8776 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+
+	return snd_ctl_enum_info(uinfo, 1, wm->ctl[n].max,
+						wm->ctl[n].enum_names);
+}
+
+static int snd_wm8776_ctl_get(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_wm8776 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+	u16 val1, val2;
+
+	if (wm->ctl[n].get)
+		wm->ctl[n].get(wm, &val1, &val2);
+	else {
+		val1 = wm->regs[wm->ctl[n].reg1] & wm->ctl[n].mask1;
+		val1 >>= __ffs(wm->ctl[n].mask1);
+		if (wm->ctl[n].flags & WM8776_FLAG_STEREO) {
+			val2 = wm->regs[wm->ctl[n].reg2] & wm->ctl[n].mask2;
+			val2 >>= __ffs(wm->ctl[n].mask2);
+			if (wm->ctl[n].flags & WM8776_FLAG_VOL_UPDATE)
+				val2 &= ~WM8776_VOL_UPDATE;
+		}
+	}
+	if (wm->ctl[n].flags & WM8776_FLAG_INVERT) {
+		val1 = wm->ctl[n].max - (val1 - wm->ctl[n].min);
+		val2 = wm->ctl[n].max - (val2 - wm->ctl[n].min);
+	}
+	ucontrol->value.integer.value[0] = val1;
+	if (wm->ctl[n].flags & WM8776_FLAG_STEREO)
+		ucontrol->value.integer.value[1] = val2;
+
+	return 0;
+}
+
+static int snd_wm8776_ctl_put(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_wm8776 *wm = snd_kcontrol_chip(kcontrol);
+	int n = kcontrol->private_value;
+	u16 val, regval1, regval2;
+
+	/* this also works for enum because value is an union */
+	regval1 = ucontrol->value.integer.value[0];
+	regval2 = ucontrol->value.integer.value[1];
+	if (wm->ctl[n].flags & WM8776_FLAG_INVERT) {
+		regval1 = wm->ctl[n].max - (regval1 - wm->ctl[n].min);
+		regval2 = wm->ctl[n].max - (regval2 - wm->ctl[n].min);
+	}
+	if (wm->ctl[n].set)
+		wm->ctl[n].set(wm, regval1, regval2);
+	else {
+		val = wm->regs[wm->ctl[n].reg1] & ~wm->ctl[n].mask1;
+		val |= regval1 << __ffs(wm->ctl[n].mask1);
+		/* both stereo controls in one register */
+		if (wm->ctl[n].flags & WM8776_FLAG_STEREO &&
+				wm->ctl[n].reg1 == wm->ctl[n].reg2) {
+			val &= ~wm->ctl[n].mask2;
+			val |= regval2 << __ffs(wm->ctl[n].mask2);
+		}
+		snd_wm8776_write(wm, wm->ctl[n].reg1, val);
+		/* stereo controls in different registers */
+		if (wm->ctl[n].flags & WM8776_FLAG_STEREO &&
+				wm->ctl[n].reg1 != wm->ctl[n].reg2) {
+			val = wm->regs[wm->ctl[n].reg2] & ~wm->ctl[n].mask2;
+			val |= regval2 << __ffs(wm->ctl[n].mask2);
+			if (wm->ctl[n].flags & WM8776_FLAG_VOL_UPDATE)
+				val |= WM8776_VOL_UPDATE;
+			snd_wm8776_write(wm, wm->ctl[n].reg2, val);
+		}
+	}
+
+	return 0;
+}
+
+static int snd_wm8776_add_control(struct snd_wm8776 *wm, int num)
+{
+	struct snd_kcontrol_new cont;
+	struct snd_kcontrol *ctl;
+
+	memset(&cont, 0, sizeof(cont));
+	cont.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+	cont.private_value = num;
+	cont.name = wm->ctl[num].name;
+	cont.access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
+	if (wm->ctl[num].flags & WM8776_FLAG_LIM ||
+	    wm->ctl[num].flags & WM8776_FLAG_ALC)
+		cont.access |= SNDRV_CTL_ELEM_ACCESS_INACTIVE;
+	cont.tlv.p = NULL;
+	cont.get = snd_wm8776_ctl_get;
+	cont.put = snd_wm8776_ctl_put;
+
+	switch (wm->ctl[num].type) {
+	case SNDRV_CTL_ELEM_TYPE_INTEGER:
+		cont.info = snd_wm8776_volume_info;
+		cont.access |= SNDRV_CTL_ELEM_ACCESS_TLV_READ;
+		cont.tlv.p = wm->ctl[num].tlv;
+		break;
+	case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+		wm->ctl[num].max = 1;
+		if (wm->ctl[num].flags & WM8776_FLAG_STEREO)
+			cont.info = snd_ctl_boolean_stereo_info;
+		else
+			cont.info = snd_ctl_boolean_mono_info;
+		break;
+	case SNDRV_CTL_ELEM_TYPE_ENUMERATED:
+		cont.info = snd_wm8776_enum_info;
+		break;
+	default:
+		return -EINVAL;
+	}
+	ctl = snd_ctl_new1(&cont, wm);
+	if (!ctl)
+		return -ENOMEM;
+
+	return snd_ctl_add(wm->card, ctl);
+}
+
+int snd_wm8776_build_controls(struct snd_wm8776 *wm)
+{
+	int err, i;
+
+	for (i = 0; i < WM8776_CTL_COUNT; i++)
+		if (wm->ctl[i].name) {
+			err = snd_wm8776_add_control(wm, i);
+			if (err < 0)
+				return err;
+		}
+
+	return 0;
+}
+EXPORT_SYMBOL(snd_wm8776_build_controls);
+
+static int __init alsa_wm8776_module_init(void)
+{
+	return 0;
+}
+
+static void __exit alsa_wm8776_module_exit(void)
+{
+}
+
+module_init(alsa_wm8776_module_init)
+module_exit(alsa_wm8776_module_exit)
sound/pci/ice1712/wm8776.h new file +224 -0

224 lines not on this page · raw message

Signature · 1 line
Ondrej Zary
#4 lore raw

PATCH v2 4/5 snd-ice1712: Add Philips PSC724 Ultimate Edge

In reply to Ondrej Zary

Add psc724 subdriver to snd-ice1712 that provides full support for Philips PSC724 Ultimate Edge sound cards. Outputs and inputs are provided by WM8766 and WM8776 codec drivers.

---
 sound/pci/Kconfig           |    2 +-
 sound/pci/ice1712/Makefile  |    6 +-
 sound/pci/ice1712/ice1724.c |    4 +-
 sound/pci/ice1712/psc724.c  |  464 +++++++++++++++++++++++++++++++++++++++++++
 sound/pci/ice1712/psc724.h  |   13 ++
 5 files changed, 485 insertions(+), 4 deletions(-)
 create mode 100644 sound/pci/ice1712/psc724.c
 create mode 100644 sound/pci/ice1712/psc724.h
sound/pci/Kconfig +1 -1
index ff3af6e..5df1635 100644
@@ -630,7 +630,7 @@ config SND_ICE1724
 	  AudioTrak Prodigy 192, 7.1 (HIFI/LT/XT), HD2; Hercules
 	  Fortissimo IV; ESI Juli@; Pontis MS300; EGO-SYS WaveTerminal
 	  192M; Albatron K8X800 Pro II; Chaintech ZNF3-150/250, 9CJS,
-	  AV-710; Shuttle SN25P.
+	  AV-710; Shuttle SN25P; Philips PSC724 Ultimate Edge.
 
 	  To compile this driver as a module, choose M here: the module
 	  will be called snd-ice1724.
sound/pci/ice1712/Makefile +4 -2
index f7ce33f..6502636 100644
@@ -4,9 +4,11 @@
 #
 
 snd-ice17xx-ak4xxx-objs := ak4xxx.o
+snd-wm8766-objs := wm8766.o
+snd-wm8776-objs := wm8776.o
 snd-ice1712-objs := ice1712.o delta.o hoontech.o ews.o
-snd-ice1724-objs := ice1724.o amp.o revo.o aureon.o vt1720_mobo.o pontis.o prodigy192.o prodigy_hifi.o juli.o phase.o wtm.o se.o maya44.o quartet.o
+snd-ice1724-objs := ice1724.o amp.o revo.o aureon.o vt1720_mobo.o pontis.o prodigy192.o prodigy_hifi.o juli.o phase.o wtm.o se.o maya44.o quartet.o psc724.o
 
 # Toplevel Module Dependency
 obj-$(CONFIG_SND_ICE1712) += snd-ice1712.o snd-ice17xx-ak4xxx.o
-obj-$(CONFIG_SND_ICE1724) += snd-ice1724.o snd-ice17xx-ak4xxx.o
+obj-$(CONFIG_SND_ICE1724) += snd-ice1724.o snd-ice17xx-ak4xxx.o snd-wm8766.o snd-wm8776.o
sound/pci/ice1712/ice1724.c +3 -1
index a529d30..0eb7ec6 100644
@@ -54,6 +54,7 @@
 #include "wtm.h"
 #include "se.h"
 #include "quartet.h"
+#include "psc724.h"
 
 MODULE_AUTHOR("Jaroslav Kysela <[email protected]>");
 MODULE_DESCRIPTION("VIA ICEnsemble ICE1724/1720 (Envy24HT/PT)");
@@ -2257,6 +2258,7 @@ static struct snd_ice1712_card_info *card_tables[] __devinitdata = {
 	snd_vt1724_se_cards,
 	snd_vt1724_qtet_cards,
 	snd_vt1724_ooaoo_cards,
+	snd_vt1724_psc724_cards,
 	NULL,
 };
 
@@ -2372,7 +2374,7 @@ static int __devinit snd_vt1724_read_eeprom(struct snd_ice1712 *ice,
 		return -EIO;
 	}
 	ice->eeprom.version = snd_vt1724_read_i2c(ice, dev, 0x05);
-	if (ice->eeprom.version != 2)
+	if (ice->eeprom.version != 1 && ice->eeprom.version != 2)
 		printk(KERN_WARNING "ice1724: Invalid EEPROM version %i\n",
 		       ice->eeprom.version);
 	size = ice->eeprom.size - 6;
sound/pci/ice1712/psc724.c new file +464 -0

464 lines not on this page · raw message

sound/pci/ice1712/psc724.h new file +13 -0
new file mode 100644
index 0000000..858e5fd
@@ -0,0 +1,13 @@
+#ifndef __SOUND_PSC724_H
+#define __SOUND_PSC724_H
+
+/* ID */
+#define PSC724_DEVICE_DESC	\
+		"{Philips,PSC724 Ultimate Edge},"
+
+#define VT1724_SUBDEVICE_PSC724		0xab170619
+
+/* entry struct */
+extern struct snd_ice1712_card_info snd_vt1724_psc724_cards[];
+
+#endif /* __SOUND_PSC724_H */
Signature · 1 line
Ondrej Zary
#5 lore raw

PATCH v2 5/5 snd-ice1712: Fix resume on ice1724

In reply to Ondrej Zary

set_pro_rate() is called from hw_params() but not from prepare(), breaking running PCM on suspend/resume. Call it from prepare() if PCM was suspended to fix the problem.

---
 sound/pci/ice1712/ice1724.c |   14 ++++++++++++++
 1 files changed, 14 insertions(+), 0 deletions(-)
sound/pci/ice1712/ice1724.c +14 -0
index 0eb7ec6..ade3354 100644
@@ -783,6 +783,13 @@ static int snd_vt1724_playback_pro_prepare(struct snd_pcm_substream *substream)
 	struct snd_ice1712 *ice = snd_pcm_substream_chip(substream);
 	unsigned char val;
 	unsigned int size;
+	int err;
+
+	if (substream->runtime->status->state == SNDRV_PCM_STATE_SUSPENDED) {
+		err = snd_vt1724_set_pro_rate(ice, substream->runtime->rate, 0);
+		if (err < 0)
+			return err;
+	}
 
 	spin_lock_irq(&ice->reg_lock);
 	val = (8 - substream->runtime->channels) >> 1;
@@ -853,6 +860,13 @@ static int snd_vt1724_pcm_prepare(struct snd_pcm_substream *substream)
 {
 	struct snd_ice1712 *ice = snd_pcm_substream_chip(substream);
 	const struct vt1724_pcm_reg *reg = substream->runtime->private_data;
+	int err;
+
+	if (substream->runtime->status->state == SNDRV_PCM_STATE_SUSPENDED) {
+		err = snd_vt1724_set_pro_rate(ice, substream->runtime->rate, 0);
+		if (err < 0)
+			return err;
+	}
 
 	spin_lock_irq(&ice->reg_lock);
 	outl(substream->runtime->dma_addr, ice->profi_port + reg->addr);
Signature · 1 line
Ondrej Zary
#6 lore raw
In reply to Ondrej Zary

At Tue, 18 Sep 2012 20:58:31 +0200,

Quoted 8 lines · Ondrej Zary

Hello,
this is second attempt to add Philips PSC724 Ultimate Edge sound card support to snd-ice1712 driver. Unlike other snd-ice1712 subdrivers, the codec code is splitted into separate modules and thus reusable by other ice1712 subdrivers.

Well, the problem is that most of ice17xx boards use only a part of
channels on codecs. Thus you don't want to expose controls for all
channels, too. Otherwise user will get confusing dead controls.

If these new modules are supposed to be used by other ice1712/ice1724 sub drivers, they should provide some good binding mechanism, too. (And that's one of the good reasons why ASoC has been nominated.)

thanks,

Takashi

Quoted 13 lines

Working: all analog outputs (front, rear, center+lfe, headphone) and inputs (front mic, rear mic, line, cd, aux), mixer, headphone jack detection, suspend/resume

Currently untested: SPDIF

Buggy: switching AGC mode in mixer causes alsamixer to crash - looks like it does not like notifications about control activating/deactivating
_______________________________________________
Alsa-devel mailing list
[email protected]
http://mailman.alsa-project.org/mailman/listinfo/alsa-devel

#7 lore raw
Quoted 9 lines · On Wednesday 19 September 2012 17:28:21 Takashi Iwai wrote:

At Tue, 18 Sep 2012 20:58:31 +0200,

Quoted 5 lines · Ondrej Zary

Hello,
this is second attempt to add Philips PSC724 Ultimate Edge sound card support to snd-ice1712 driver. Unlike other snd-ice1712 subdrivers, the codec code is splitted into separate modules and thus reusable by other ice1712 subdrivers.

Quoted 3 lines

Well, the problem is that most of ice17xx boards use only a part of
channels on codecs. Thus you don't want to expose controls for all
channels, too. Otherwise user will get confusing dead controls.

The controls can be easily hidden by setting its name to NULL. It's even used in the psc724 subdriver.

Quoted 3 lines

If these new modules are supposed to be used by other ice1712/ice1724 sub drivers, they should provide some good binding mechanism, too. (And that's one of the good reasons why ASoC has been nominated.)

They're easy to use, you just need to provide wm87x6_write() function. I think that other subdrivers could be easily converted to use this (with hardware to test).

Quoted 16 lines

thanks,

Takashi

Quoted 12 lines

Working: all analog outputs (front, rear, center+lfe, headphone) and inputs (front mic, rear mic, line, cd, aux), mixer, headphone jack detection, suspend/resume

Currently untested: SPDIF

Buggy: switching AGC mode in mixer causes alsamixer to crash - looks like it does not like notifications about control activating/deactivating
_______________________________________________
Alsa-devel mailing list
[email protected]
http://mailman.alsa-project.org/mailman/listinfo/alsa-devel

#8 lore raw

At Wed, 19 Sep 2012 19:03:14 +0200,

Quoted 18 lines · Ondrej Zary
Quoted 9 lines · On Wednesday 19 September 2012 17:28:21 Takashi Iwai wrote:

At Tue, 18 Sep 2012 20:58:31 +0200,

Quoted 5 lines · Ondrej Zary

Hello,
this is second attempt to add Philips PSC724 Ultimate Edge sound card support to snd-ice1712 driver. Unlike other snd-ice1712 subdrivers, the codec code is splitted into separate modules and thus reusable by other ice1712 subdrivers.

Quoted 3 lines

Well, the problem is that most of ice17xx boards use only a part of
channels on codecs. Thus you don't want to expose controls for all
channels, too. Otherwise user will get confusing dead controls.

The controls can be easily hidden by setting its name to NULL. It's even used in the psc724 subdriver.

Quoted 7 lines
Quoted 3 lines

If these new modules are supposed to be used by other ice1712/ice1724 sub drivers, they should provide some good binding mechanism, too. (And that's one of the good reasons why ASoC has been nominated.)

They're easy to use, you just need to provide wm87x6_write() function. I think that other subdrivers could be easily converted to use this (with hardware to test).

OK, but I'd like to have modules only when the code sharing happens. Also, it'd be better to have different module names, indicating that they are ice17xx-specific.

thanks,

Takashi

Quoted 18 lines
Quoted 17 lines

thanks,

Takashi

Quoted 12 lines

Working: all analog outputs (front, rear, center+lfe, headphone) and inputs (front mic, rear mic, line, cd, aux), mixer, headphone jack detection, suspend/resume

Currently untested: SPDIF

Buggy: switching AGC mode in mixer causes alsamixer to crash - looks like it does not like notifications about control activating/deactivating
_______________________________________________
Alsa-devel mailing list
[email protected]
http://mailman.alsa-project.org/mailman/listinfo/alsa-devel

#9 lore raw
Quoted 24 lines · On Wednesday 19 September 2012 19:58:15 Takashi Iwai wrote:

At Wed, 19 Sep 2012 19:03:14 +0200,

Quoted 21 lines · Ondrej Zary
Quoted 9 lines · On Wednesday 19 September 2012 17:28:21 Takashi Iwai wrote:

At Tue, 18 Sep 2012 20:58:31 +0200,

Quoted 5 lines · Ondrej Zary

Hello,
this is second attempt to add Philips PSC724 Ultimate Edge sound card support to snd-ice1712 driver. Unlike other snd-ice1712 subdrivers, the codec code is splitted into separate modules and thus reusable by other ice1712 subdrivers.

Quoted 3 lines

Well, the problem is that most of ice17xx boards use only a part of
channels on codecs. Thus you don't want to expose controls for all
channels, too. Otherwise user will get confusing dead controls.

The controls can be easily hidden by setting its name to NULL. It's even used in the psc724 subdriver.

Quoted 3 lines

If these new modules are supposed to be used by other ice1712/ice1724 sub drivers, they should provide some good binding mechanism, too. (And that's one of the good reasons why ASoC has been nominated.)

Quoted 7 lines
Quoted 3 lines

They're easy to use, you just need to provide wm87x6_write() function. I think that other subdrivers could be easily converted to use this (with hardware to test).

OK, but I'd like to have modules only when the code sharing happens. Also, it'd be better to have different module names, indicating that they are ice17xx-specific.

OK, I'll change the code to not produce modules.

Quoted 9 lines
Quoted 9 lines
Quoted 9 lines
Quoted 9 lines

Working: all analog outputs (front, rear, center+lfe, headphone) and inputs (front mic, rear mic, line, cd, aux), mixer, headphone jack detection, suspend/resume

Currently untested: SPDIF

Buggy: switching AGC mode in mixer causes alsamixer to crash - looks like it does not like notifications about control activating/deactivating

What about this? Is it a bug in my code or somewhere else?

Signature · 1 line
Ondrej Zary