[ALSA] add CMI8788 driver

Add the snd-oxygen driver for the C-Media CMI8788 (Oxygen) chip, used on
the Asound A-8788, AuzenTech X-Meridian, Bgears b-Enspirer,
Club3D Theatron DTS, HT-Omega Claro, Razer Barracuda AC-1,
Sondigo Inferno, and TempoTec HIFIER sound cards.

Signed-off-by: Clemens Ladisch <clemens@ladisch.de>
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
diff --git a/sound/pci/Kconfig b/sound/pci/Kconfig
index 45f0f6c..d3be87d 100644
--- a/sound/pci/Kconfig
+++ b/sound/pci/Kconfig
@@ -183,6 +183,31 @@
 	  To compile this driver as a module, choose M here: the module
 	  will be called snd-cmipci.
 
+config SND_OXYGEN_LIB
+        tristate
+	depends on SND
+	select SND_PCM
+	select SND_MPU401_UART
+
+config SND_OXYGEN
+	tristate "C-Media 8788 (Oxygen)"
+	depends on SND
+	select SND_OXYGEN_LIB
+	help
+	  Say Y here to include support for sound cards based on the
+	  C-Media CMI8788 (Oxygen HD Audio) chip:
+	   * Asound A-8788
+	   * AuzenTech X-Meridian
+	   * Bgears b-Enspirer
+	   * Club3D Theatron DTS
+	   * HT-Omega Claro
+	   * Razer Barracuda AC-1
+	   * Sondigo Inferno
+	   * TempoTec HIFIER
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called snd-oxygen.
+
 config SND_CS4281
 	tristate "Cirrus Logic (Sound Fusion) CS4281"
 	depends on SND
diff --git a/sound/pci/Makefile b/sound/pci/Makefile
index 56738da..2d42fd2 100644
--- a/sound/pci/Makefile
+++ b/sound/pci/Makefile
@@ -68,6 +68,7 @@
 	korg1212/ \
 	mixart/ \
 	nm256/ \
+	oxygen/ \
 	pcxhr/ \
 	riptide/ \
 	rme9652/ \
diff --git a/sound/pci/oxygen/Makefile b/sound/pci/oxygen/Makefile
new file mode 100644
index 0000000..99455dd
--- /dev/null
+++ b/sound/pci/oxygen/Makefile
@@ -0,0 +1,5 @@
+snd-oxygen-lib-objs := oxygen_io.o oxygen_lib.o oxygen_mixer.o oxygen_pcm.o
+snd-oxygen-objs := oxygen.o
+
+obj-$(CONFIG_SND_OXYGEN_LIB) += snd-oxygen-lib.o
+obj-$(CONFIG_SND_OXYGEN) += snd-oxygen.o
diff --git a/sound/pci/oxygen/oxygen.c b/sound/pci/oxygen/oxygen.c
new file mode 100644
index 0000000..bfef5ab
--- /dev/null
+++ b/sound/pci/oxygen/oxygen.c
@@ -0,0 +1,314 @@
+/*
+ * C-Media CMI8788 driver for C-Media's reference design and for the X-Meridian
+ *
+ * Copyright (c) Clemens Ladisch <clemens@ladisch.de>
+ *
+ *
+ *  This driver is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License, version 2.
+ *
+ *  This driver 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 driver; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+
+/*
+ * SPI 0 -> 1st AK4396 (front)
+ * SPI 1 -> 2nd AK4396 (side)
+ * SPI 2 -> 3rd AK4396 (center/LFE)
+ * SPI 3 -> WM8785
+ * SPI 4 -> 4th AK4396 (rear)
+ *
+ * GPIO 0 -> DFS0 of AK5385
+ * GPIO 1 -> DFS1 of AK5385
+ */
+
+#include <sound/driver.h>
+#include <linux/pci.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/tlv.h>
+#include "oxygen.h"
+
+MODULE_AUTHOR("Clemens Ladisch <clemens@ladisch.de>");
+MODULE_DESCRIPTION("C-Media CMI8788 driver");
+MODULE_LICENSE("GPL");
+MODULE_SUPPORTED_DEVICE("{{C-Media,CMI8788}}");
+
+static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
+static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
+static int enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;
+
+module_param_array(index, int, NULL, 0444);
+MODULE_PARM_DESC(index, "card index");
+module_param_array(id, charp, NULL, 0444);
+MODULE_PARM_DESC(id, "ID string");
+module_param_array(enable, bool, NULL, 0444);
+MODULE_PARM_DESC(enable, "enable card");
+
+static struct pci_device_id oxygen_ids[] __devinitdata = {
+	{ OXYGEN_PCI_SUBID(0x10b0, 0x0216) },
+	{ OXYGEN_PCI_SUBID(0x10b0, 0x0218) },
+	{ OXYGEN_PCI_SUBID(0x10b0, 0x0219) },
+	{ OXYGEN_PCI_SUBID(0x13f6, 0x0001) },
+	{ OXYGEN_PCI_SUBID(0x13f6, 0x0010) },
+	{ OXYGEN_PCI_SUBID(0x13f6, 0x8788) },
+	{ OXYGEN_PCI_SUBID(0x147a, 0xa017) },
+	{ OXYGEN_PCI_SUBID(0x14c3, 0x1710) },
+	{ OXYGEN_PCI_SUBID(0x14c3, 0x1711) },
+	{ OXYGEN_PCI_SUBID(0x1a58, 0x0910) },
+	{ OXYGEN_PCI_SUBID(0x415a, 0x5431), .driver_data = 1 },
+	{ OXYGEN_PCI_SUBID(0x7284, 0x9761) },
+	{ }
+};
+MODULE_DEVICE_TABLE(pci, oxygen_ids);
+
+#define AK4396_WRITE	0x2000
+
+/* register 0 */
+#define AK4396_RSTN		0x01
+#define AK4396_DIF_24_MSB	0x04
+/* register 1 */
+#define AK4396_SMUTE		0x01
+#define AK4396_DEM_OFF		0x02
+#define AK4396_DFS_MASK		0x18
+#define AK4396_DFS_NORMAL	0x00
+#define AK4396_DFS_DOUBLE	0x08
+#define AK4396_DFS_QUAD		0x10
+
+/* register 0 */
+#define WM8785_OSR_SINGLE	0x000
+#define WM8785_OSR_DOUBLE	0x008
+#define WM8785_OSR_QUAD		0x010
+#define WM8785_FORMAT_LJUST	0x020
+#define WM8785_FORMAT_I2S	0x040
+/* register 1 */
+#define WM8785_WL_16		0x000
+#define WM8785_WL_20		0x001
+#define WM8785_WL_24		0x002
+#define WM8785_WL_32		0x003
+
+static void ak4396_write(struct oxygen *chip, unsigned int codec,
+			 u8 reg, u8 value)
+{
+	/* maps ALSA channel pair number to SPI output */
+	static const u8 codec_spi_map[4] = {
+		0, 4, 2, 1
+	};
+	oxygen_write_spi(chip, OXYGEN_SPI_TRIGGER_WRITE |
+			 OXYGEN_SPI_DATA_LENGTH_2 |
+			 (codec_spi_map[codec] << OXYGEN_SPI_CODEC_SHIFT) |
+			 OXYGEN_SPI_MAGIC,
+			 AK4396_WRITE | (reg << 8) | value);
+}
+
+static void wm8785_write(struct oxygen *chip, u8 reg, unsigned int value)
+{
+	oxygen_write_spi(chip, OXYGEN_SPI_TRIGGER_WRITE |
+			 OXYGEN_SPI_DATA_LENGTH_2 |
+			 (3 << OXYGEN_SPI_CODEC_SHIFT),
+			 (reg << 9) | value);
+}
+
+static void ak4396_init(struct oxygen *chip)
+{
+	unsigned int i;
+
+	chip->ak4396_reg1 = AK4396_DEM_OFF | AK4396_DFS_NORMAL;
+	for (i = 0; i < 4; ++i) {
+		ak4396_write(chip, i, 0, AK4396_DIF_24_MSB | AK4396_RSTN);
+		ak4396_write(chip, i, 1, chip->ak4396_reg1);
+		ak4396_write(chip, i, 2, 0);
+		ak4396_write(chip, i, 3, 0xff);
+		ak4396_write(chip, i, 4, 0xff);
+	}
+	snd_component_add(chip->card, "AK4396");
+}
+
+static void ak5385_init(struct oxygen *chip)
+{
+	oxygen_set_bits16(chip, OXYGEN_GPIO_CONTROL, 0x0003);
+	oxygen_clear_bits16(chip, OXYGEN_GPIO_DATA, 0x0003);
+	snd_component_add(chip->card, "AK5385");
+}
+
+static void wm8785_init(struct oxygen *chip)
+{
+	wm8785_write(chip, 7, 0);
+	wm8785_write(chip, 0, WM8785_FORMAT_LJUST | WM8785_OSR_SINGLE);
+	wm8785_write(chip, 1, WM8785_WL_24);
+	snd_component_add(chip->card, "WM8785");
+}
+
+static void generic_init(struct oxygen *chip)
+{
+	ak4396_init(chip);
+	wm8785_init(chip);
+}
+
+static void meridian_init(struct oxygen *chip)
+{
+	ak4396_init(chip);
+	ak5385_init(chip);
+}
+
+static void generic_cleanup(struct oxygen *chip)
+{
+}
+
+static void set_ak4396_params(struct oxygen *chip,
+			      struct snd_pcm_hw_params *params)
+{
+	unsigned int i;
+	u8 value;
+
+	value = chip->ak4396_reg1 & ~AK4396_DFS_MASK;
+	if (params_rate(params) <= 54000)
+		value |= AK4396_DFS_NORMAL;
+	else if (params_rate(params) < 120000)
+		value |= AK4396_DFS_DOUBLE;
+	else
+		value |= AK4396_DFS_QUAD;
+	chip->ak4396_reg1 = value;
+	for (i = 0; i < 4; ++i) {
+		ak4396_write(chip, i, 0, AK4396_DIF_24_MSB);
+		ak4396_write(chip, i, 1, value);
+		ak4396_write(chip, i, 0, AK4396_DIF_24_MSB | AK4396_RSTN);
+	}
+}
+
+static void update_ak4396_volume(struct oxygen *chip)
+{
+	unsigned int i;
+
+	for (i = 0; i < 4; ++i) {
+		ak4396_write(chip, i, 3, chip->dac_volume[i * 2]);
+		ak4396_write(chip, i, 4, chip->dac_volume[i * 2 + 1]);
+	}
+}
+
+static void update_ak4396_mute(struct oxygen *chip)
+{
+	unsigned int i;
+	u8 value;
+
+	value = chip->ak4396_reg1 & ~AK4396_SMUTE;
+	if (chip->dac_mute)
+		value |= AK4396_SMUTE;
+	for (i = 0; i < 4; ++i)
+		ak4396_write(chip, i, 1, value);
+}
+
+static void set_wm8785_params(struct oxygen *chip,
+			      struct snd_pcm_hw_params *params)
+{
+	unsigned int value;
+
+	wm8785_write(chip, 7, 0);
+
+	value = WM8785_FORMAT_LJUST;
+	if (params_rate(params) == 96000)
+		value |= WM8785_OSR_DOUBLE;
+	else if (params_rate(params) == 192000)
+		value |= WM8785_OSR_QUAD;
+	else
+		value |= WM8785_OSR_SINGLE;
+	wm8785_write(chip, 0, value);
+
+	if (snd_pcm_format_width(params_format(params)) <= 16)
+		value = WM8785_WL_16;
+	else
+		value = WM8785_WL_24;
+	wm8785_write(chip, 1, value);
+}
+
+static void set_ak5385_params(struct oxygen *chip,
+			      struct snd_pcm_hw_params *params)
+{
+	unsigned int value;
+
+	if (params_rate(params) <= 54000)
+		value = 0;
+	else if (params_rate(params) <= 108000)
+		value = 1;
+	else
+		value = 2;
+	oxygen_write16_masked(chip, OXYGEN_GPIO_DATA, value, 0x0003);
+}
+
+static const DECLARE_TLV_DB_LINEAR(ak4396_db_scale, TLV_DB_GAIN_MUTE, 0);
+
+static const struct oxygen_model model_generic = {
+	.shortname = "C-Media CMI8788",
+	.longname = "C-Media Oxygen HD Audio",
+	.chip = "CMI8788",
+	.owner = THIS_MODULE,
+	.init = generic_init,
+	.cleanup = generic_cleanup,
+	.set_dac_params = set_ak4396_params,
+	.set_adc_params = set_wm8785_params,
+	.update_dac_volume = update_ak4396_volume,
+	.update_dac_mute = update_ak4396_mute,
+	.dac_tlv = ak4396_db_scale,
+};
+static const struct oxygen_model model_meridian = {
+	.shortname = "C-Media CMI8788",
+	.longname = "C-Media Oxygen HD Audio",
+	.chip = "CMI8788",
+	.owner = THIS_MODULE,
+	.init = meridian_init,
+	.cleanup = generic_cleanup,
+	.set_dac_params = set_ak4396_params,
+	.set_adc_params = set_ak5385_params,
+	.update_dac_volume = update_ak4396_volume,
+	.update_dac_mute = update_ak4396_mute,
+	.dac_tlv = ak4396_db_scale,
+	.record_from_dma_b = 1,
+};
+
+static int __devinit generic_oxygen_probe(struct pci_dev *pci,
+					  const struct pci_device_id *pci_id)
+{
+	static int dev;
+	const struct oxygen_model *model;
+	int err;
+
+	if (dev >= SNDRV_CARDS)
+		return -ENODEV;
+	if (!enable[dev]) {
+		++dev;
+		return -ENOENT;
+	}
+	model = pci_id->driver_data ? &model_meridian : &model_generic;
+	err = oxygen_pci_probe(pci, index[dev], id[dev], model);
+	if (err >= 0)
+		++dev;
+	return err;
+}
+
+static struct pci_driver oxygen_driver = {
+	.name = "CMI8788",
+	.id_table = oxygen_ids,
+	.probe = generic_oxygen_probe,
+	.remove = __devexit_p(oxygen_pci_remove),
+};
+
+static int __init alsa_card_oxygen_init(void)
+{
+	return pci_register_driver(&oxygen_driver);
+}
+
+static void __exit alsa_card_oxygen_exit(void)
+{
+	pci_unregister_driver(&oxygen_driver);
+}
+
+module_init(alsa_card_oxygen_init)
+module_exit(alsa_card_oxygen_exit)
diff --git a/sound/pci/oxygen/oxygen.h b/sound/pci/oxygen/oxygen.h
new file mode 100644
index 0000000..248f7ed
--- /dev/null
+++ b/sound/pci/oxygen/oxygen.h
@@ -0,0 +1,167 @@
+#ifndef OXYGEN_H_INCLUDED
+#define OXYGEN_H_INCLUDED
+
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+#include "oxygen_regs.h"
+
+/* 1 << PCM_x == OXYGEN_CHANNEL_x */
+#define PCM_A		0
+#define PCM_B		1
+#define PCM_C		2
+#define PCM_SPDIF	3
+#define PCM_MULTICH	4
+#define PCM_AC97	5
+#define PCM_COUNT	6
+
+#define OXYGEN_PCI_SUBID(sv, sd) \
+	.vendor = PCI_VENDOR_ID_CMEDIA, \
+	.device = 0x8788, \
+	.subvendor = sv, \
+	.subdevice = sd
+
+struct pci_dev;
+struct snd_card;
+struct snd_pcm_substream;
+struct snd_pcm_hw_params;
+struct snd_rawmidi;
+struct oxygen_model;
+
+struct oxygen {
+	unsigned long addr;
+	spinlock_t reg_lock;
+	struct mutex mutex;
+	struct snd_card *card;
+	struct pci_dev *pci;
+	struct snd_rawmidi *midi;
+	int irq;
+	const struct oxygen_model *model;
+	unsigned int interrupt_mask;
+	u8 dac_volume[8];
+	u8 dac_mute;
+	u8 pcm_active;
+	u8 pcm_running;
+	u8 dac_routing;
+	u8 spdif_playback_enable;
+	u8 ak4396_reg1;
+	u8 revision;
+	u8 has_2nd_ac97_codec;
+	u32 spdif_bits;
+	u32 spdif_pcm_bits;
+	struct snd_pcm_substream *streams[PCM_COUNT];
+	struct snd_kcontrol *spdif_pcm_ctl;
+	struct snd_kcontrol *spdif_input_bits_ctl;
+	struct work_struct spdif_input_bits_work;
+};
+
+struct oxygen_model {
+	const char *shortname;
+	const char *longname;
+	const char *chip;
+	struct module *owner;
+	void (*init)(struct oxygen *chip);
+	int (*mixer_init)(struct oxygen *chip);
+	void (*cleanup)(struct oxygen *chip);
+	void (*set_dac_params)(struct oxygen *chip,
+			       struct snd_pcm_hw_params *params);
+	void (*set_adc_params)(struct oxygen *chip,
+			       struct snd_pcm_hw_params *params);
+	void (*update_dac_volume)(struct oxygen *chip);
+	void (*update_dac_mute)(struct oxygen *chip);
+	const unsigned int *dac_tlv;
+	u8 record_from_dma_b;
+	u8 cd_in_from_video_in;
+	u8 dac_minimum_volume;
+};
+
+/* oxygen_lib.c */
+
+int oxygen_pci_probe(struct pci_dev *pci, int index, char *id,
+		     const struct oxygen_model *model);
+void oxygen_pci_remove(struct pci_dev *pci);
+
+/* oxygen_mixer.c */
+
+int oxygen_mixer_init(struct oxygen *chip);
+void oxygen_update_dac_routing(struct oxygen *chip);
+void oxygen_update_spdif_source(struct oxygen *chip);
+
+/* oxygen_pcm.c */
+
+int oxygen_pcm_init(struct oxygen *chip);
+
+/* oxygen_io.c */
+
+u8 oxygen_read8(struct oxygen *chip, unsigned int reg);
+u16 oxygen_read16(struct oxygen *chip, unsigned int reg);
+u32 oxygen_read32(struct oxygen *chip, unsigned int reg);
+void oxygen_write8(struct oxygen *chip, unsigned int reg, u8 value);
+void oxygen_write16(struct oxygen *chip, unsigned int reg, u16 value);
+void oxygen_write32(struct oxygen *chip, unsigned int reg, u32 value);
+void oxygen_write8_masked(struct oxygen *chip, unsigned int reg,
+			  u8 value, u8 mask);
+void oxygen_write16_masked(struct oxygen *chip, unsigned int reg,
+			   u16 value, u16 mask);
+void oxygen_write32_masked(struct oxygen *chip, unsigned int reg,
+			   u32 value, u32 mask);
+
+u16 oxygen_read_ac97(struct oxygen *chip, unsigned int codec,
+		     unsigned int index);
+void oxygen_write_ac97(struct oxygen *chip, unsigned int codec,
+		       unsigned int index, u16 data);
+void oxygen_write_ac97_masked(struct oxygen *chip, unsigned int codec,
+			      unsigned int index, u16 data, u16 mask);
+
+void oxygen_write_spi(struct oxygen *chip, u8 control, unsigned int data);
+
+static inline void oxygen_set_bits8(struct oxygen *chip,
+				    unsigned int reg, u8 value)
+{
+	oxygen_write8_masked(chip, reg, value, value);
+}
+
+static inline void oxygen_set_bits16(struct oxygen *chip,
+				     unsigned int reg, u16 value)
+{
+	oxygen_write16_masked(chip, reg, value, value);
+}
+
+static inline void oxygen_set_bits32(struct oxygen *chip,
+				     unsigned int reg, u32 value)
+{
+	oxygen_write32_masked(chip, reg, value, value);
+}
+
+static inline void oxygen_clear_bits8(struct oxygen *chip,
+				      unsigned int reg, u8 value)
+{
+	oxygen_write8_masked(chip, reg, 0, value);
+}
+
+static inline void oxygen_clear_bits16(struct oxygen *chip,
+				       unsigned int reg, u16 value)
+{
+	oxygen_write16_masked(chip, reg, 0, value);
+}
+
+static inline void oxygen_clear_bits32(struct oxygen *chip,
+				       unsigned int reg, u32 value)
+{
+	oxygen_write32_masked(chip, reg, 0, value);
+}
+
+static inline void oxygen_ac97_set_bits(struct oxygen *chip, unsigned int codec,
+					unsigned int index, u16 value)
+{
+	oxygen_write_ac97_masked(chip, codec, index, value, value);
+}
+
+static inline void oxygen_ac97_clear_bits(struct oxygen *chip,
+					  unsigned int codec,
+					  unsigned int index, u16 value)
+{
+	oxygen_write_ac97_masked(chip, codec, index, 0, value);
+}
+
+#endif
diff --git a/sound/pci/oxygen/oxygen_io.c b/sound/pci/oxygen/oxygen_io.c
new file mode 100644
index 0000000..5f4feea
--- /dev/null
+++ b/sound/pci/oxygen/oxygen_io.c
@@ -0,0 +1,194 @@
+/*
+ * C-Media CMI8788 driver - helper functions
+ *
+ * Copyright (c) Clemens Ladisch <clemens@ladisch.de>
+ *
+ *
+ *  This driver is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License, version 2.
+ *
+ *  This driver 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 driver; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+
+#include <sound/driver.h>
+#include <linux/delay.h>
+#include <linux/sched.h>
+#include <sound/core.h>
+#include <asm/io.h>
+#include "oxygen.h"
+
+u8 oxygen_read8(struct oxygen *chip, unsigned int reg)
+{
+	return inb(chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_read8);
+
+u16 oxygen_read16(struct oxygen *chip, unsigned int reg)
+{
+	return inw(chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_read16);
+
+u32 oxygen_read32(struct oxygen *chip, unsigned int reg)
+{
+	return inl(chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_read32);
+
+void oxygen_write8(struct oxygen *chip, unsigned int reg, u8 value)
+{
+	outb(value, chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_write8);
+
+void oxygen_write16(struct oxygen *chip, unsigned int reg, u16 value)
+{
+	outw(value, chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_write16);
+
+void oxygen_write32(struct oxygen *chip, unsigned int reg, u32 value)
+{
+	outl(value, chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_write32);
+
+void oxygen_write8_masked(struct oxygen *chip, unsigned int reg,
+			  u8 value, u8 mask)
+{
+	u8 tmp = inb(chip->addr + reg);
+	outb((tmp & ~mask) | (value & mask), chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_write8_masked);
+
+void oxygen_write16_masked(struct oxygen *chip, unsigned int reg,
+			   u16 value, u16 mask)
+{
+	u16 tmp = inw(chip->addr + reg);
+	outw((tmp & ~mask) | (value & mask), chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_write16_masked);
+
+void oxygen_write32_masked(struct oxygen *chip, unsigned int reg,
+			   u32 value, u32 mask)
+{
+	u32 tmp = inl(chip->addr + reg);
+	outl((tmp & ~mask) | (value & mask), chip->addr + reg);
+}
+EXPORT_SYMBOL(oxygen_write32_masked);
+
+static int oxygen_ac97_wait(struct oxygen *chip, unsigned int mask)
+{
+	unsigned long timeout = jiffies + msecs_to_jiffies(1);
+	do {
+		udelay(5);
+		cond_resched();
+		if (oxygen_read8(chip, OXYGEN_AC97_INTERRUPT_STATUS) & mask)
+			return 0;
+	} while (time_after_eq(timeout, jiffies));
+	return -EIO;
+}
+
+/*
+ * About 10% of AC'97 register reads or writes fail to complete, but even those
+ * where the controller indicates completion aren't guaranteed to have actually
+ * happened.
+ *
+ * It's hard to assign blame to either the controller or the codec because both
+ * were made by C-Media ...
+ */
+
+void oxygen_write_ac97(struct oxygen *chip, unsigned int codec,
+		       unsigned int index, u16 data)
+{
+	unsigned int count, succeeded;
+	u32 reg;
+
+	reg = data;
+	reg |= index << OXYGEN_AC97_REG_ADDR_SHIFT;
+	reg |= OXYGEN_AC97_REG_DIR_WRITE;
+	reg |= codec << OXYGEN_AC97_REG_CODEC_SHIFT;
+	succeeded = 0;
+	for (count = 5; count > 0; --count) {
+		udelay(5);
+		oxygen_write32(chip, OXYGEN_AC97_REGS, reg);
+		/* require two "completed" writes, just to be sure */
+		if (oxygen_ac97_wait(chip, OXYGEN_AC97_WRITE_COMPLETE) >= 0 &&
+		    ++succeeded >= 2)
+			return;
+	}
+	snd_printk(KERN_ERR "AC'97 write timeout\n");
+}
+EXPORT_SYMBOL(oxygen_write_ac97);
+
+u16 oxygen_read_ac97(struct oxygen *chip, unsigned int codec,
+		     unsigned int index)
+{
+	unsigned int count;
+	unsigned int last_read = UINT_MAX;
+	u32 reg;
+
+	reg = index << OXYGEN_AC97_REG_ADDR_SHIFT;
+	reg |= OXYGEN_AC97_REG_DIR_READ;
+	reg |= codec << OXYGEN_AC97_REG_CODEC_SHIFT;
+	for (count = 5; count > 0; --count) {
+		udelay(5);
+		oxygen_write32(chip, OXYGEN_AC97_REGS, reg);
+		udelay(10);
+		if (oxygen_ac97_wait(chip, OXYGEN_AC97_READ_COMPLETE) >= 0) {
+			u16 value = oxygen_read16(chip, OXYGEN_AC97_REGS);
+			/* we require two consecutive reads of the same value */
+			if (value == last_read)
+				return value;
+			last_read = value;
+			/*
+			 * Invert the register value bits to make sure that two
+			 * consecutive unsuccessful reads do not return the same
+			 * value.
+			 */
+			reg ^= 0xffff;
+		}
+	}
+	snd_printk(KERN_ERR "AC'97 read timeout on codec %u\n", codec);
+	return 0;
+}
+EXPORT_SYMBOL(oxygen_read_ac97);
+
+void oxygen_write_ac97_masked(struct oxygen *chip, unsigned int codec,
+			      unsigned int index, u16 data, u16 mask)
+{
+	u16 value = oxygen_read_ac97(chip, codec, index);
+	value &= ~mask;
+	value |= data & mask;
+	oxygen_write_ac97(chip, codec, index, value);
+}
+EXPORT_SYMBOL(oxygen_write_ac97_masked);
+
+void oxygen_write_spi(struct oxygen *chip, u8 control, unsigned int data)
+{
+	unsigned int count;
+
+	/* should not need more than 3.84 us (24 * 160 ns) */
+	count = 10;
+	while ((oxygen_read8(chip, OXYGEN_SPI_CONTROL) & OXYGEN_SPI_BUSY)
+	       && count > 0) {
+		udelay(1);
+		--count;
+	}
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_write8(chip, OXYGEN_SPI_DATA1, data);
+	oxygen_write8(chip, OXYGEN_SPI_DATA2, data >> 8);
+	if (control & OXYGEN_SPI_DATA_LENGTH_3)
+		oxygen_write8(chip, OXYGEN_SPI_DATA3, data >> 16);
+	oxygen_write8(chip, OXYGEN_SPI_CONTROL, control);
+	spin_unlock_irq(&chip->reg_lock);
+}
+EXPORT_SYMBOL(oxygen_write_spi);
diff --git a/sound/pci/oxygen/oxygen_lib.c b/sound/pci/oxygen/oxygen_lib.c
new file mode 100644
index 0000000..6180cc8
--- /dev/null
+++ b/sound/pci/oxygen/oxygen_lib.c
@@ -0,0 +1,361 @@
+/*
+ * C-Media CMI8788 driver - main driver module
+ *
+ * Copyright (c) Clemens Ladisch <clemens@ladisch.de>
+ *
+ *
+ *  This driver is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License, version 2.
+ *
+ *  This driver 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 driver; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+
+#include <sound/driver.h>
+#include <linux/delay.h>
+#include <linux/interrupt.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <sound/ac97_codec.h>
+#include <sound/asoundef.h>
+#include <sound/core.h>
+#include <sound/info.h>
+#include <sound/mpu401.h>
+#include <sound/pcm.h>
+#include "oxygen.h"
+
+MODULE_AUTHOR("Clemens Ladisch <clemens@ladisch.de>");
+MODULE_DESCRIPTION("C-Media CMI8788 helper library");
+MODULE_LICENSE("GPL");
+
+
+static irqreturn_t oxygen_interrupt(int dummy, void *dev_id)
+{
+	struct oxygen *chip = dev_id;
+	unsigned int status, clear, elapsed_streams, i;
+
+	status = oxygen_read16(chip, OXYGEN_INTERRUPT_STATUS);
+	if (!status)
+		return IRQ_NONE;
+
+	spin_lock(&chip->reg_lock);
+
+	clear = status & (OXYGEN_CHANNEL_A |
+			  OXYGEN_CHANNEL_B |
+			  OXYGEN_CHANNEL_C |
+			  OXYGEN_CHANNEL_SPDIF |
+			  OXYGEN_CHANNEL_MULTICH |
+			  OXYGEN_CHANNEL_AC97 |
+			  OXYGEN_INT_SPDIF_IN_CHANGE |
+			  OXYGEN_INT_GPIO);
+	if (clear) {
+		if (clear & OXYGEN_INT_SPDIF_IN_CHANGE)
+			chip->interrupt_mask &= ~OXYGEN_INT_SPDIF_IN_CHANGE;
+		oxygen_write16(chip, OXYGEN_INTERRUPT_MASK,
+			       chip->interrupt_mask & ~clear);
+		oxygen_write16(chip, OXYGEN_INTERRUPT_MASK,
+			       chip->interrupt_mask);
+	}
+
+	elapsed_streams = status & chip->pcm_running;
+
+	spin_unlock(&chip->reg_lock);
+
+	for (i = 0; i < PCM_COUNT; ++i)
+		if ((elapsed_streams & (1 << i)) && chip->streams[i])
+			snd_pcm_period_elapsed(chip->streams[i]);
+
+	if (status & OXYGEN_INT_SPDIF_IN_CHANGE) {
+		spin_lock(&chip->reg_lock);
+		i = oxygen_read32(chip, OXYGEN_SPDIF_CONTROL);
+		if (i & OXYGEN_SPDIF_IN_CHANGE) {
+			oxygen_write32(chip, OXYGEN_SPDIF_CONTROL, i);
+			schedule_work(&chip->spdif_input_bits_work);
+		}
+		spin_unlock(&chip->reg_lock);
+	}
+
+	if (status & OXYGEN_INT_GPIO)
+		;
+
+	if ((status & OXYGEN_INT_MIDI) && chip->midi)
+		snd_mpu401_uart_interrupt(0, chip->midi->private_data);
+
+	return IRQ_HANDLED;
+}
+
+static void oxygen_spdif_input_bits_changed(struct work_struct *work)
+{
+	struct oxygen *chip = container_of(work, struct oxygen,
+					   spdif_input_bits_work);
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_clear_bits32(chip, OXYGEN_SPDIF_CONTROL, OXYGEN_SPDIF_IN_INVERT);
+	spin_unlock_irq(&chip->reg_lock);
+	msleep(1);
+	if (!(oxygen_read32(chip, OXYGEN_SPDIF_CONTROL)
+	      & OXYGEN_SPDIF_IN_VALID)) {
+		spin_lock_irq(&chip->reg_lock);
+		oxygen_set_bits32(chip, OXYGEN_SPDIF_CONTROL,
+				  OXYGEN_SPDIF_IN_INVERT);
+		spin_unlock_irq(&chip->reg_lock);
+		msleep(1);
+		if (!(oxygen_read32(chip, OXYGEN_SPDIF_CONTROL)
+		      & OXYGEN_SPDIF_IN_VALID)) {
+			spin_lock_irq(&chip->reg_lock);
+			oxygen_clear_bits32(chip, OXYGEN_SPDIF_CONTROL,
+					    OXYGEN_SPDIF_IN_INVERT);
+			spin_unlock_irq(&chip->reg_lock);
+		}
+	}
+
+	if (chip->spdif_input_bits_ctl) {
+		spin_lock_irq(&chip->reg_lock);
+		chip->interrupt_mask |= OXYGEN_INT_SPDIF_IN_CHANGE;
+		oxygen_write16(chip, OXYGEN_INTERRUPT_MASK,
+			       chip->interrupt_mask);
+		spin_unlock_irq(&chip->reg_lock);
+
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
+			       &chip->spdif_input_bits_ctl->id);
+	}
+}
+
+#ifdef CONFIG_PROC_FS
+static void oxygen_proc_read(struct snd_info_entry *entry,
+			     struct snd_info_buffer *buffer)
+{
+	struct oxygen *chip = entry->private_data;
+	int i, j;
+
+	snd_iprintf(buffer, "CMI8788\n\n");
+	for (i = 0; i < 0x100; i += 0x10) {
+		snd_iprintf(buffer, "%02x:", i);
+		for (j = 0; j < 0x10; ++j)
+			snd_iprintf(buffer, " %02x", oxygen_read8(chip, i + j));
+		snd_iprintf(buffer, "\n");
+	}
+	if (mutex_lock_interruptible(&chip->mutex) < 0)
+		return;
+	snd_iprintf(buffer, "\nAC97\n");
+	for (i = 0; i < 0x80; i += 0x10) {
+		snd_iprintf(buffer, "%02x:", i);
+		for (j = 0; j < 0x10; j += 2)
+			snd_iprintf(buffer, " %04x",
+				    oxygen_read_ac97(chip, 0, i + j));
+		snd_iprintf(buffer, "\n");
+	}
+	mutex_unlock(&chip->mutex);
+}
+
+static void __devinit oxygen_proc_init(struct oxygen *chip)
+{
+	struct snd_info_entry *entry;
+
+	if (!snd_card_proc_new(chip->card, "cmi8788", &entry))
+		snd_info_set_text_ops(entry, chip, oxygen_proc_read);
+}
+#else
+#define oxygen_proc_init(chip)
+#endif
+
+static void __devinit oxygen_init(struct oxygen *chip)
+{
+	unsigned int i;
+
+	chip->dac_routing = 1;
+	for (i = 0; i < 8; ++i)
+		chip->dac_volume[i] = 0xff;
+	chip->spdif_playback_enable = 1;
+	chip->spdif_bits = OXYGEN_SPDIF_C | OXYGEN_SPDIF_ORIGINAL |
+		(IEC958_AES1_CON_PCM_CODER << OXYGEN_SPDIF_CATEGORY_SHIFT);
+	chip->spdif_pcm_bits = chip->spdif_bits;
+
+	if (oxygen_read8(chip, OXYGEN_REVISION) & OXYGEN_REVISION_2)
+		chip->revision = 2;
+	else
+		chip->revision = 1;
+
+	if (chip->revision == 1)
+		oxygen_set_bits8(chip, OXYGEN_MISC, OXYGEN_MISC_MAGIC);
+
+	oxygen_set_bits8(chip, OXYGEN_FUNCTION,
+			 OXYGEN_FUNCTION_RESET_CODEC |
+			 OXYGEN_FUNCTION_ENABLE_SPI_4_5);
+	oxygen_write16(chip, OXYGEN_I2S_MULTICH_FORMAT, 0x010a);
+	oxygen_write16(chip, OXYGEN_I2S_A_FORMAT, 0x010a);
+	oxygen_write16(chip, OXYGEN_I2S_B_FORMAT, 0x010a);
+	oxygen_write16(chip, OXYGEN_I2S_C_FORMAT, 0x010a);
+	oxygen_set_bits32(chip, OXYGEN_SPDIF_CONTROL, OXYGEN_SPDIF_MAGIC2);
+	oxygen_write32(chip, OXYGEN_SPDIF_OUTPUT_BITS, chip->spdif_bits);
+	oxygen_write16(chip, OXYGEN_PLAY_ROUTING, 0x6c00);
+	oxygen_write8(chip, OXYGEN_REC_ROUTING, 0x10);
+	oxygen_write8(chip, OXYGEN_ADC_MONITOR, 0x00);
+	oxygen_write8(chip, OXYGEN_A_MONITOR_ROUTING, 0xe4);
+
+	oxygen_write16(chip, OXYGEN_INTERRUPT_MASK, 0);
+	oxygen_write16(chip, OXYGEN_DMA_STATUS, 0);
+
+	oxygen_write8(chip, OXYGEN_AC97_INTERRUPT_MASK, 0x00);
+	oxygen_clear_bits16(chip, OXYGEN_AC97_OUT_CONFIG,
+			    OXYGEN_AC97_OUT_MAGIC3);
+	oxygen_set_bits16(chip, OXYGEN_AC97_IN_CONFIG,
+			  OXYGEN_AC97_IN_MAGIC3);
+	oxygen_write_ac97(chip, 0, AC97_RESET, 0);
+	msleep(1);
+	oxygen_ac97_set_bits(chip, 0, 0x70, 0x0300);
+	oxygen_ac97_set_bits(chip, 0, 0x64, 0x8043);
+	oxygen_ac97_set_bits(chip, 0, 0x62, 0x180f);
+	oxygen_write_ac97(chip, 0, AC97_MASTER, 0x0000);
+	oxygen_write_ac97(chip, 0, AC97_PC_BEEP, 0x8000);
+	oxygen_write_ac97(chip, 0, AC97_MIC, 0x8808);
+	oxygen_write_ac97(chip, 0, AC97_LINE, 0x0808);
+	oxygen_write_ac97(chip, 0, AC97_CD, 0x8808);
+	oxygen_write_ac97(chip, 0, AC97_VIDEO, 0x8808);
+	oxygen_write_ac97(chip, 0, AC97_AUX, 0x8808);
+	oxygen_write_ac97(chip, 0, AC97_REC_GAIN, 0x8000);
+	oxygen_write_ac97(chip, 0, AC97_CENTER_LFE_MASTER, 0x8080);
+	oxygen_write_ac97(chip, 0, AC97_SURROUND_MASTER, 0x8080);
+	oxygen_ac97_clear_bits(chip, 0, 0x72, 0x0001);
+	/* power down unused ADCs and DACs */
+	oxygen_ac97_set_bits(chip, 0, AC97_POWERDOWN,
+			     AC97_PD_PR0 | AC97_PD_PR1);
+	oxygen_ac97_set_bits(chip, 0, AC97_EXTENDED_STATUS,
+			     AC97_EA_PRI | AC97_EA_PRJ | AC97_EA_PRK);
+}
+
+static void oxygen_card_free(struct snd_card *card)
+{
+	struct oxygen *chip = card->private_data;
+
+	spin_lock_irq(&chip->reg_lock);
+	chip->interrupt_mask = 0;
+	chip->pcm_running = 0;
+	oxygen_write16(chip, OXYGEN_DMA_STATUS, 0);
+	oxygen_write16(chip, OXYGEN_INTERRUPT_MASK, 0);
+	spin_unlock_irq(&chip->reg_lock);
+	if (chip->irq >= 0) {
+		free_irq(chip->irq, chip);
+		synchronize_irq(chip->irq);
+	}
+	flush_scheduled_work();
+	chip->model->cleanup(chip);
+	mutex_destroy(&chip->mutex);
+	pci_release_regions(chip->pci);
+	pci_disable_device(chip->pci);
+}
+
+int __devinit oxygen_pci_probe(struct pci_dev *pci, int index, char *id,
+			       const struct oxygen_model *model)
+{
+	struct snd_card *card;
+	struct oxygen *chip;
+	int err;
+
+	card = snd_card_new(index, id, model->owner, sizeof *chip);
+	if (!card)
+		return -ENOMEM;
+
+	chip = card->private_data;
+	chip->card = card;
+	chip->pci = pci;
+	chip->irq = -1;
+	chip->model = model;
+	spin_lock_init(&chip->reg_lock);
+	mutex_init(&chip->mutex);
+	INIT_WORK(&chip->spdif_input_bits_work,
+		  oxygen_spdif_input_bits_changed);
+
+	err = pci_enable_device(pci);
+	if (err < 0)
+		goto err_card;
+
+	err = pci_request_regions(pci, model->chip);
+	if (err < 0) {
+		snd_printk(KERN_ERR "cannot reserve PCI resources\n");
+		goto err_pci_enable;
+	}
+
+	if (!(pci_resource_flags(pci, 0) & IORESOURCE_IO) ||
+	    pci_resource_len(pci, 0) < 0x100) {
+		snd_printk(KERN_ERR "invalid PCI I/O range\n");
+		err = -ENXIO;
+		goto err_pci_regions;
+	}
+	chip->addr = pci_resource_start(pci, 0);
+
+	pci_set_master(pci);
+	snd_card_set_dev(card, &pci->dev);
+	card->private_free = oxygen_card_free;
+
+	oxygen_init(chip);
+	model->init(chip);
+
+	err = request_irq(pci->irq, oxygen_interrupt, IRQF_SHARED,
+			  model->chip, chip);
+	if (err < 0) {
+		snd_printk(KERN_ERR "cannot grab interrupt %d\n", pci->irq);
+		goto err_card;
+	}
+	chip->irq = pci->irq;
+
+	strcpy(card->driver, model->chip);
+	strcpy(card->shortname, model->shortname);
+	sprintf(card->longname, "%s (rev %u) at %#lx, irq %i",
+		model->longname, chip->revision, chip->addr, chip->irq);
+	strcpy(card->mixername, model->chip);
+	snd_component_add(card, model->chip);
+
+	err = oxygen_pcm_init(chip);
+	if (err < 0)
+		goto err_card;
+
+	err = oxygen_mixer_init(chip);
+	if (err < 0)
+		goto err_card;
+
+	if (oxygen_read8(chip, OXYGEN_MISC) & OXYGEN_MISC_MIDI) {
+		err = snd_mpu401_uart_new(card, 0, MPU401_HW_CMIPCI,
+					  chip->addr + OXYGEN_MPU401,
+					  MPU401_INFO_INTEGRATED, 0, 0,
+					  &chip->midi);
+		if (err < 0)
+			goto err_card;
+	}
+
+	oxygen_proc_init(chip);
+
+	spin_lock_irq(&chip->reg_lock);
+	chip->interrupt_mask |= OXYGEN_INT_SPDIF_IN_CHANGE;
+	oxygen_write16(chip, OXYGEN_INTERRUPT_MASK, chip->interrupt_mask);
+	spin_unlock_irq(&chip->reg_lock);
+
+	err = snd_card_register(card);
+	if (err < 0)
+		goto err_card;
+
+	pci_set_drvdata(pci, card);
+	return 0;
+
+err_pci_regions:
+	pci_release_regions(pci);
+err_pci_enable:
+	pci_disable_device(pci);
+err_card:
+	snd_card_free(card);
+	return err;
+}
+EXPORT_SYMBOL(oxygen_pci_probe);
+
+void __devexit oxygen_pci_remove(struct pci_dev *pci)
+{
+	snd_card_free(pci_get_drvdata(pci));
+	pci_set_drvdata(pci, NULL);
+}
+EXPORT_SYMBOL(oxygen_pci_remove);
diff --git a/sound/pci/oxygen/oxygen_mixer.c b/sound/pci/oxygen/oxygen_mixer.c
new file mode 100644
index 0000000..e252aba
--- /dev/null
+++ b/sound/pci/oxygen/oxygen_mixer.c
@@ -0,0 +1,623 @@
+/*
+ * C-Media CMI8788 driver - mixer code
+ *
+ * Copyright (c) Clemens Ladisch <clemens@ladisch.de>
+ *
+ *
+ *  This driver is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License, version 2.
+ *
+ *  This driver 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 driver; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+
+#include <sound/driver.h>
+#include <linux/mutex.h>
+#include <sound/ac97_codec.h>
+#include <sound/asoundef.h>
+#include <sound/control.h>
+#include <sound/tlv.h>
+#include "oxygen.h"
+
+static int dac_volume_info(struct snd_kcontrol *ctl,
+			   struct snd_ctl_elem_info *info)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	info->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	info->count = 8;
+	info->value.integer.min = chip->model->dac_minimum_volume;
+	info->value.integer.max = 0xff;
+	return 0;
+}
+
+static int dac_volume_get(struct snd_kcontrol *ctl,
+			  struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	unsigned int i;
+
+	mutex_lock(&chip->mutex);
+	for (i = 0; i < 8; ++i)
+		value->value.integer.value[i] = chip->dac_volume[i];
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int dac_volume_put(struct snd_kcontrol *ctl,
+			  struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	unsigned int i;
+	int changed;
+
+	changed = 0;
+	mutex_lock(&chip->mutex);
+	for (i = 0; i < 8; ++i)
+		if (value->value.integer.value[i] != chip->dac_volume[i]) {
+			chip->dac_volume[i] = value->value.integer.value[i];
+			changed = 1;
+		}
+	if (changed)
+		chip->model->update_dac_volume(chip);
+	mutex_unlock(&chip->mutex);
+	return changed;
+}
+
+static int dac_mute_get(struct snd_kcontrol *ctl,
+			struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	mutex_lock(&chip->mutex);
+	value->value.integer.value[0] = !chip->dac_mute;
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int dac_mute_put(struct snd_kcontrol *ctl,
+			  struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	int changed;
+
+	mutex_lock(&chip->mutex);
+	changed = !value->value.integer.value[0] != chip->dac_mute;
+	if (changed) {
+		chip->dac_mute = !value->value.integer.value[0];
+		chip->model->update_dac_mute(chip);
+	}
+	mutex_unlock(&chip->mutex);
+	return changed;
+}
+
+static int upmix_info(struct snd_kcontrol *ctl, struct snd_ctl_elem_info *info)
+{
+	static const char *const names[3] = {
+		"Front", "Front+Rear", "Front+Rear+Side"
+	};
+	info->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
+	info->count = 1;
+	info->value.enumerated.items = 3;
+	if (info->value.enumerated.item > 2)
+		info->value.enumerated.item = 2;
+	strcpy(info->value.enumerated.name, names[info->value.enumerated.item]);
+	return 0;
+}
+
+static int upmix_get(struct snd_kcontrol *ctl, struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	mutex_lock(&chip->mutex);
+	value->value.enumerated.item[0] = chip->dac_routing;
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+void oxygen_update_dac_routing(struct oxygen *chip)
+{
+	/*
+	 * hardware channel order: front, side, center/lfe, rear
+	 * ALSA channel order:     front, rear, center/lfe, side
+	 */
+	static const unsigned int reg_values[3] = {
+		0x6c00, 0x2c00, 0x2000
+	};
+	unsigned int reg_value;
+
+	if ((oxygen_read8(chip, OXYGEN_PLAY_CHANNELS) &
+	     OXYGEN_PLAY_CHANNELS_MASK) == OXYGEN_PLAY_CHANNELS_2)
+		reg_value = reg_values[chip->dac_routing];
+	else
+		reg_value = 0x6c00;
+	oxygen_write16_masked(chip, OXYGEN_PLAY_ROUTING, reg_value, 0xff00);
+}
+
+static int upmix_put(struct snd_kcontrol *ctl, struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	int changed;
+
+	mutex_lock(&chip->mutex);
+	changed = value->value.enumerated.item[0] != chip->dac_routing;
+	if (changed) {
+		chip->dac_routing = min(value->value.enumerated.item[0], 2u);
+		spin_lock_irq(&chip->reg_lock);
+		oxygen_update_dac_routing(chip);
+		spin_unlock_irq(&chip->reg_lock);
+	}
+	mutex_unlock(&chip->mutex);
+	return changed;
+}
+
+static int spdif_switch_get(struct snd_kcontrol *ctl,
+			    struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	mutex_lock(&chip->mutex);
+	value->value.integer.value[0] = chip->spdif_playback_enable;
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static unsigned int oxygen_spdif_rate(unsigned int oxygen_rate)
+{
+	switch (oxygen_rate) {
+	case OXYGEN_RATE_32000:
+		return IEC958_AES3_CON_FS_32000 << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	case OXYGEN_RATE_44100:
+		return IEC958_AES3_CON_FS_44100 << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	default: /* OXYGEN_RATE_48000 */
+		return IEC958_AES3_CON_FS_48000 << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	case OXYGEN_RATE_64000:
+		return 0xb << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	case OXYGEN_RATE_88200:
+		return 0x8 << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	case OXYGEN_RATE_96000:
+		return 0xa << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	case OXYGEN_RATE_176400:
+		return 0xc << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	case OXYGEN_RATE_192000:
+		return 0xe << OXYGEN_SPDIF_CS_RATE_SHIFT;
+	}
+}
+
+void oxygen_update_spdif_source(struct oxygen *chip)
+{
+	u32 old_control, new_control;
+	u16 old_routing, new_routing;
+	unsigned int oxygen_rate;
+
+	old_control = oxygen_read32(chip, OXYGEN_SPDIF_CONTROL);
+	old_routing = oxygen_read16(chip, OXYGEN_PLAY_ROUTING);
+	if (chip->pcm_active & (1 << PCM_SPDIF)) {
+		new_control = old_control | OXYGEN_SPDIF_OUT_ENABLE;
+		new_routing = (old_routing & ~0x00e0) | 0x0000;
+		oxygen_rate = (old_control >> OXYGEN_SPDIF_OUT_RATE_SHIFT)
+			& OXYGEN_I2S_RATE_MASK;
+		/* S/PDIF rate was already set by the caller */
+	} else if ((chip->pcm_active & (1 << PCM_MULTICH)) &&
+		   chip->spdif_playback_enable) {
+		new_routing = (old_routing & ~0x00e0) | 0x0020;
+		oxygen_rate = oxygen_read16(chip, OXYGEN_I2S_MULTICH_FORMAT)
+			& OXYGEN_I2S_RATE_MASK;
+		new_control = (old_control & ~OXYGEN_SPDIF_OUT_RATE_MASK) |
+			(oxygen_rate << OXYGEN_SPDIF_OUT_RATE_SHIFT) |
+			OXYGEN_SPDIF_OUT_ENABLE;
+	} else {
+		new_control = old_control & ~OXYGEN_SPDIF_OUT_ENABLE;
+		new_routing = old_routing;
+		oxygen_rate = OXYGEN_RATE_44100;
+	}
+	if (old_routing != new_routing) {
+		oxygen_write32(chip, OXYGEN_SPDIF_CONTROL,
+			       new_control & ~OXYGEN_SPDIF_OUT_ENABLE);
+		oxygen_write16(chip, OXYGEN_PLAY_ROUTING, new_routing);
+	}
+	if (new_control & OXYGEN_SPDIF_OUT_ENABLE)
+		oxygen_write32(chip, OXYGEN_SPDIF_OUTPUT_BITS,
+			       oxygen_spdif_rate(oxygen_rate) |
+			       ((chip->pcm_active & (1 << PCM_SPDIF)) ?
+				chip->spdif_pcm_bits : chip->spdif_bits));
+	oxygen_write32(chip, OXYGEN_SPDIF_CONTROL, new_control);
+}
+
+static int spdif_switch_put(struct snd_kcontrol *ctl,
+			    struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	int changed;
+
+	mutex_lock(&chip->mutex);
+	changed = value->value.integer.value[0] != chip->spdif_playback_enable;
+	if (changed) {
+		chip->spdif_playback_enable = !!value->value.integer.value[0];
+		spin_lock_irq(&chip->reg_lock);
+		oxygen_update_spdif_source(chip);
+		spin_unlock_irq(&chip->reg_lock);
+	}
+	mutex_unlock(&chip->mutex);
+	return changed;
+}
+
+static int spdif_info(struct snd_kcontrol *ctl, struct snd_ctl_elem_info *info)
+{
+	info->type = SNDRV_CTL_ELEM_TYPE_IEC958;
+	info->count = 1;
+	return 0;
+}
+
+static void oxygen_to_iec958(u32 bits, struct snd_ctl_elem_value *value)
+{
+	value->value.iec958.status[0] =
+		bits & (OXYGEN_SPDIF_NONAUDIO | OXYGEN_SPDIF_C |
+			OXYGEN_SPDIF_PREEMPHASIS);
+	value->value.iec958.status[1] = /* category and original */
+		bits >> OXYGEN_SPDIF_CATEGORY_SHIFT;
+}
+
+static u32 iec958_to_oxygen(struct snd_ctl_elem_value *value)
+{
+	u32 bits;
+
+	bits = value->value.iec958.status[0] &
+		(OXYGEN_SPDIF_NONAUDIO | OXYGEN_SPDIF_C |
+		 OXYGEN_SPDIF_PREEMPHASIS);
+	bits |= value->value.iec958.status[1] << OXYGEN_SPDIF_CATEGORY_SHIFT;
+	if (bits & OXYGEN_SPDIF_NONAUDIO)
+		bits |= OXYGEN_SPDIF_V;
+	return bits;
+}
+
+static inline void write_spdif_bits(struct oxygen *chip, u32 bits)
+{
+	oxygen_write32_masked(chip, OXYGEN_SPDIF_OUTPUT_BITS, bits,
+			      OXYGEN_SPDIF_NONAUDIO |
+			      OXYGEN_SPDIF_C |
+			      OXYGEN_SPDIF_PREEMPHASIS |
+			      OXYGEN_SPDIF_CATEGORY_MASK |
+			      OXYGEN_SPDIF_ORIGINAL |
+			      OXYGEN_SPDIF_V);
+}
+
+static int spdif_default_get(struct snd_kcontrol *ctl,
+			     struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	mutex_lock(&chip->mutex);
+	oxygen_to_iec958(chip->spdif_bits, value);
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int spdif_default_put(struct snd_kcontrol *ctl,
+			     struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	u32 new_bits;
+	int changed;
+
+	new_bits = iec958_to_oxygen(value);
+	mutex_lock(&chip->mutex);
+	changed = new_bits != chip->spdif_bits;
+	if (changed) {
+		chip->spdif_bits = new_bits;
+		if (!(chip->pcm_active & (1 << PCM_SPDIF)))
+			write_spdif_bits(chip, new_bits);
+	}
+	mutex_unlock(&chip->mutex);
+	return changed;
+}
+
+static int spdif_mask_get(struct snd_kcontrol *ctl,
+			  struct snd_ctl_elem_value *value)
+{
+	value->value.iec958.status[0] = IEC958_AES0_NONAUDIO |
+		IEC958_AES0_CON_NOT_COPYRIGHT | IEC958_AES0_CON_EMPHASIS;
+	value->value.iec958.status[1] =
+		IEC958_AES1_CON_CATEGORY | IEC958_AES1_CON_ORIGINAL;
+	return 0;
+}
+
+static int spdif_pcm_get(struct snd_kcontrol *ctl,
+			 struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	mutex_lock(&chip->mutex);
+	oxygen_to_iec958(chip->spdif_pcm_bits, value);
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int spdif_pcm_put(struct snd_kcontrol *ctl,
+			 struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	u32 new_bits;
+	int changed;
+
+	new_bits = iec958_to_oxygen(value);
+	mutex_lock(&chip->mutex);
+	changed = new_bits != chip->spdif_pcm_bits;
+	if (changed) {
+		chip->spdif_pcm_bits = new_bits;
+		if (chip->pcm_active & (1 << PCM_SPDIF))
+			write_spdif_bits(chip, new_bits);
+	}
+	mutex_unlock(&chip->mutex);
+	return changed;
+}
+
+static int spdif_input_mask_get(struct snd_kcontrol *ctl,
+				struct snd_ctl_elem_value *value)
+{
+	value->value.iec958.status[0] = 0xff;
+	value->value.iec958.status[1] = 0xff;
+	value->value.iec958.status[2] = 0xff;
+	value->value.iec958.status[3] = 0xff;
+	return 0;
+}
+
+static int spdif_input_default_get(struct snd_kcontrol *ctl,
+				   struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	u32 bits;
+
+	bits = oxygen_read32(chip, OXYGEN_SPDIF_INPUT_BITS);
+	value->value.iec958.status[0] = bits;
+	value->value.iec958.status[1] = bits >> 8;
+	value->value.iec958.status[2] = bits >> 16;
+	value->value.iec958.status[3] = bits >> 24;
+	return 0;
+}
+
+static int ac97_switch_get(struct snd_kcontrol *ctl,
+			   struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	unsigned int index = ctl->private_value & 0xff;
+	unsigned int bitnr = (ctl->private_value >> 8) & 0xff;
+	int invert = ctl->private_value & (1 << 16);
+	u16 reg;
+
+	mutex_lock(&chip->mutex);
+	reg = oxygen_read_ac97(chip, 0, index);
+	mutex_unlock(&chip->mutex);
+	if (!(reg & (1 << bitnr)) ^ !invert)
+		value->value.integer.value[0] = 1;
+	else
+		value->value.integer.value[0] = 0;
+	return 0;
+}
+
+static int ac97_switch_put(struct snd_kcontrol *ctl,
+			   struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	unsigned int index = ctl->private_value & 0xff;
+	unsigned int bitnr = (ctl->private_value >> 8) & 0xff;
+	int invert = ctl->private_value & (1 << 16);
+	u16 oldreg, newreg;
+	int change;
+
+	mutex_lock(&chip->mutex);
+	oldreg = oxygen_read_ac97(chip, 0, index);
+	newreg = oldreg;
+	if (!value->value.integer.value[0] ^ !invert)
+		newreg |= 1 << bitnr;
+	else
+		newreg &= ~(1 << bitnr);
+	change = newreg != oldreg;
+	if (change) {
+		oxygen_write_ac97(chip, 0, index, newreg);
+		if (index == AC97_LINE)
+			oxygen_write_ac97_masked(chip, 0, 0x72,
+						 !!(newreg & 0x8000), 0x0001);
+	}
+	mutex_unlock(&chip->mutex);
+	return change;
+}
+
+static int ac97_volume_info(struct snd_kcontrol *ctl,
+			    struct snd_ctl_elem_info *info)
+{
+	info->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	info->count = 2;
+	info->value.integer.min = 0;
+	info->value.integer.max = 0x1f;
+	return 0;
+}
+
+static int ac97_volume_get(struct snd_kcontrol *ctl,
+			   struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	unsigned int index = ctl->private_value;
+	u16 reg;
+
+	mutex_lock(&chip->mutex);
+	reg = oxygen_read_ac97(chip, 0, index);
+	mutex_unlock(&chip->mutex);
+	value->value.integer.value[0] = 31 - (reg & 0x1f);
+	value->value.integer.value[1] = 31 - ((reg >> 8) & 0x1f);
+	return 0;
+}
+
+static int ac97_volume_put(struct snd_kcontrol *ctl,
+			   struct snd_ctl_elem_value *value)
+{
+	struct oxygen *chip = ctl->private_data;
+	unsigned int index = ctl->private_value;
+	u16 oldreg, newreg;
+	int change;
+
+	mutex_lock(&chip->mutex);
+	oldreg = oxygen_read_ac97(chip, 0, index);
+	newreg = oldreg;
+	newreg = (newreg & ~0x1f) |
+		(31 - (value->value.integer.value[0] & 0x1f));
+	newreg = (newreg & ~0x1f00) |
+		((31 - (value->value.integer.value[0] & 0x1f)) << 8);
+	change = newreg != oldreg;
+	if (change)
+		oxygen_write_ac97(chip, 0, index, newreg);
+	mutex_unlock(&chip->mutex);
+	return change;
+}
+
+#define AC97_SWITCH(xname, index, bitnr, invert) { \
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
+		.name = xname, \
+		.info = snd_ctl_boolean_mono_info, \
+		.get = ac97_switch_get, \
+		.put = ac97_switch_put, \
+		.private_value = ((invert) << 16) | ((bitnr) << 8) | (index), \
+	}
+#define AC97_VOLUME(xname, index) { \
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
+		.name = xname, \
+		.info = ac97_volume_info, \
+		.get = ac97_volume_get, \
+		.put = ac97_volume_put, \
+		.tlv = { .p = ac97_db_scale, }, \
+		.private_value = (index), \
+	}
+
+static DECLARE_TLV_DB_SCALE(ac97_db_scale, -3450, 150, 0);
+
+static const struct snd_kcontrol_new controls[] = {
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "PCM Playback Volume",
+		.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+			  SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+		.info = dac_volume_info,
+		.get = dac_volume_get,
+		.put = dac_volume_put,
+		.tlv = {
+			.p = NULL, /* set later */
+		},
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "PCM Playback Switch",
+		.info = snd_ctl_boolean_mono_info,
+		.get = dac_mute_get,
+		.put = dac_mute_put,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = "Stereo Upmixing",
+		.info = upmix_info,
+		.get = upmix_get,
+		.put = upmix_put,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, SWITCH),
+		.info = snd_ctl_boolean_mono_info,
+		.get = spdif_switch_get,
+		.put = spdif_switch_put,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.device = 1,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, DEFAULT),
+		.info = spdif_info,
+		.get = spdif_default_get,
+		.put = spdif_default_put,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.device = 1,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, CON_MASK),
+		.access = SNDRV_CTL_ELEM_ACCESS_READ,
+		.info = spdif_info,
+		.get = spdif_mask_get,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.device = 1,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, PCM_STREAM),
+		.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+			  SNDRV_CTL_ELEM_ACCESS_INACTIVE,
+		.info = spdif_info,
+		.get = spdif_pcm_get,
+		.put = spdif_pcm_put,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.device = 1,
+		.name = SNDRV_CTL_NAME_IEC958("", CAPTURE, MASK),
+		.access = SNDRV_CTL_ELEM_ACCESS_READ,
+		.info = spdif_info,
+		.get = spdif_input_mask_get,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.device = 1,
+		.name = SNDRV_CTL_NAME_IEC958("", CAPTURE, DEFAULT),
+		.access = SNDRV_CTL_ELEM_ACCESS_READ,
+		.info = spdif_info,
+		.get = spdif_input_default_get,
+	},
+	AC97_VOLUME("Mic Capture Volume", AC97_MIC),
+	AC97_SWITCH("Mic Capture Switch", AC97_MIC, 15, 1),
+	AC97_SWITCH("Mic Boost (+20dB)", AC97_MIC, 6, 0),
+	AC97_SWITCH("Line Capture Switch", AC97_LINE, 15, 1),
+	AC97_VOLUME("CD Capture Volume", AC97_CD),
+	AC97_SWITCH("CD Capture Switch", AC97_CD, 15, 1),
+	AC97_VOLUME("Aux Capture Volume", AC97_AUX),
+	AC97_SWITCH("Aux Capture Switch", AC97_AUX, 15, 1),
+};
+
+static void oxygen_any_ctl_free(struct snd_kcontrol *ctl)
+{
+	struct oxygen *chip = ctl->private_data;
+
+	/* I'm too lazy to write a function for each control :-) */
+	chip->spdif_pcm_ctl = NULL;
+	chip->spdif_input_bits_ctl = NULL;
+}
+
+int oxygen_mixer_init(struct oxygen *chip)
+{
+	unsigned int i;
+	struct snd_kcontrol *ctl;
+	int err;
+
+	for (i = 0; i < ARRAY_SIZE(controls); ++i) {
+		ctl = snd_ctl_new1(&controls[i], chip);
+		if (!ctl)
+			return -ENOMEM;
+		if (!strcmp(ctl->id.name, "PCM Playback Volume"))
+			ctl->tlv.p = chip->model->dac_tlv;
+		else if (chip->model->cd_in_from_video_in &&
+			 !strncmp(ctl->id.name, "CD Capture ", 11))
+			ctl->private_value ^= AC97_CD ^ AC97_VIDEO;
+		err = snd_ctl_add(chip->card, ctl);
+		if (err < 0)
+			return err;
+		if (!strcmp(ctl->id.name,
+			    SNDRV_CTL_NAME_IEC958("", PLAYBACK, PCM_STREAM))) {
+			chip->spdif_pcm_ctl = ctl;
+			ctl->private_free = oxygen_any_ctl_free;
+		} else if (!strcmp(ctl->id.name,
+				 SNDRV_CTL_NAME_IEC958("", CAPTURE, DEFAULT))) {
+			chip->spdif_input_bits_ctl = ctl;
+			ctl->private_free = oxygen_any_ctl_free;
+		}
+	}
+	return chip->model->mixer_init ? chip->model->mixer_init(chip) : 0;
+}
diff --git a/sound/pci/oxygen/oxygen_pcm.c b/sound/pci/oxygen/oxygen_pcm.c
new file mode 100644
index 0000000..941399b
--- /dev/null
+++ b/sound/pci/oxygen/oxygen_pcm.c
@@ -0,0 +1,726 @@
+/*
+ * C-Media CMI8788 driver - PCM code
+ *
+ * Copyright (c) Clemens Ladisch <clemens@ladisch.de>
+ *
+ *
+ *  This driver is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License, version 2.
+ *
+ *  This driver 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 driver; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+
+#include <sound/driver.h>
+#include <linux/pci.h>
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include "oxygen.h"
+
+static struct snd_pcm_hardware oxygen_hardware[PCM_COUNT] = {
+	[PCM_A] = {
+		.info = SNDRV_PCM_INFO_MMAP |
+			SNDRV_PCM_INFO_MMAP_VALID |
+			SNDRV_PCM_INFO_INTERLEAVED |
+			SNDRV_PCM_INFO_PAUSE |
+			SNDRV_PCM_INFO_SYNC_START,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE |
+			   SNDRV_PCM_FMTBIT_S32_LE,
+		.rates = SNDRV_PCM_RATE_44100 |
+			 SNDRV_PCM_RATE_48000 |
+			 SNDRV_PCM_RATE_96000 |
+			 SNDRV_PCM_RATE_192000,
+		.rate_min = 44100,
+		.rate_max = 192000,
+		.channels_min = 2,
+		.channels_max = 2,
+		.buffer_bytes_max = 256 * 1024,
+		.period_bytes_min = 128,
+		.period_bytes_max = 128 * 1024,
+		.periods_min = 2,
+		.periods_max = 2048,
+	},
+	[PCM_B] = {
+		.info = SNDRV_PCM_INFO_MMAP |
+			SNDRV_PCM_INFO_MMAP_VALID |
+			SNDRV_PCM_INFO_INTERLEAVED |
+			SNDRV_PCM_INFO_PAUSE |
+			SNDRV_PCM_INFO_SYNC_START,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE |
+			   SNDRV_PCM_FMTBIT_S32_LE,
+		.rates = SNDRV_PCM_RATE_32000 |
+			 SNDRV_PCM_RATE_44100 |
+			 SNDRV_PCM_RATE_48000 |
+			 SNDRV_PCM_RATE_64000 |
+			 SNDRV_PCM_RATE_88200 |
+			 SNDRV_PCM_RATE_96000 |
+			 SNDRV_PCM_RATE_176400 |
+			 SNDRV_PCM_RATE_192000,
+		.rate_min = 32000,
+		.rate_max = 192000,
+		.channels_min = 2,
+		.channels_max = 2,
+		.buffer_bytes_max = 256 * 1024,
+		.period_bytes_min = 128,
+		.period_bytes_max = 128 * 1024,
+		.periods_min = 2,
+		.periods_max = 2048,
+	},
+	[PCM_C] = {
+		.info = SNDRV_PCM_INFO_MMAP |
+			SNDRV_PCM_INFO_MMAP_VALID |
+			SNDRV_PCM_INFO_INTERLEAVED |
+			SNDRV_PCM_INFO_PAUSE |
+			SNDRV_PCM_INFO_SYNC_START,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE |
+			   SNDRV_PCM_FMTBIT_S32_LE,
+		.rates = SNDRV_PCM_RATE_44100 |
+			 SNDRV_PCM_RATE_48000 |
+			 SNDRV_PCM_RATE_88200 |
+			 SNDRV_PCM_RATE_96000,
+		.rate_min = 44100,
+		.rate_max = 96000,
+		.channels_min = 2,
+		.channels_max = 2,
+		.buffer_bytes_max = 256 * 1024,
+		.period_bytes_min = 128,
+		.period_bytes_max = 128 * 1024,
+		.periods_min = 2,
+		.periods_max = 2048,
+	},
+	[PCM_SPDIF] = {
+		.info = SNDRV_PCM_INFO_MMAP |
+			SNDRV_PCM_INFO_MMAP_VALID |
+			SNDRV_PCM_INFO_INTERLEAVED |
+			SNDRV_PCM_INFO_PAUSE |
+			SNDRV_PCM_INFO_SYNC_START,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE |
+			   SNDRV_PCM_FMTBIT_S32_LE,
+		.rates = SNDRV_PCM_RATE_32000 |
+			 SNDRV_PCM_RATE_44100 |
+			 SNDRV_PCM_RATE_48000 |
+			 SNDRV_PCM_RATE_64000 |
+			 SNDRV_PCM_RATE_88200 |
+			 SNDRV_PCM_RATE_96000 |
+			 SNDRV_PCM_RATE_176400 |
+			 SNDRV_PCM_RATE_192000,
+		.rate_min = 32000,
+		.rate_max = 192000,
+		.channels_min = 2,
+		.channels_max = 2,
+		.buffer_bytes_max = 256 * 1024,
+		.period_bytes_min = 128,
+		.period_bytes_max = 128 * 1024,
+		.periods_min = 2,
+		.periods_max = 2048,
+	},
+	[PCM_MULTICH] = {
+		.info = SNDRV_PCM_INFO_MMAP |
+			SNDRV_PCM_INFO_MMAP_VALID |
+			SNDRV_PCM_INFO_INTERLEAVED |
+			SNDRV_PCM_INFO_PAUSE |
+			SNDRV_PCM_INFO_SYNC_START,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE |
+			   SNDRV_PCM_FMTBIT_S32_LE,
+		.rates = SNDRV_PCM_RATE_32000 |
+			 SNDRV_PCM_RATE_44100 |
+			 SNDRV_PCM_RATE_48000 |
+			 SNDRV_PCM_RATE_64000 |
+			 SNDRV_PCM_RATE_88200 |
+			 SNDRV_PCM_RATE_96000 |
+			 SNDRV_PCM_RATE_176400 |
+			 SNDRV_PCM_RATE_192000,
+		.rate_min = 32000,
+		.rate_max = 192000,
+		.channels_min = 2,
+		.channels_max = 8,
+		.buffer_bytes_max = 2048 * 1024,
+		.period_bytes_min = 128,
+		.period_bytes_max = 256 * 1024,
+		.periods_min = 2,
+		.periods_max = 16384,
+	},
+	[PCM_AC97] = {
+		.info = SNDRV_PCM_INFO_MMAP |
+			SNDRV_PCM_INFO_MMAP_VALID |
+			SNDRV_PCM_INFO_INTERLEAVED |
+			SNDRV_PCM_INFO_PAUSE |
+			SNDRV_PCM_INFO_SYNC_START,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		.rates = SNDRV_PCM_RATE_48000,
+		.rate_min = 48000,
+		.rate_max = 48000,
+		.channels_min = 2,
+		.channels_max = 2,
+		.buffer_bytes_max = 256 * 1024,
+		.period_bytes_min = 128,
+		.period_bytes_max = 128 * 1024,
+		.periods_min = 2,
+		.periods_max = 2048,
+	},
+};
+
+static int oxygen_open(struct snd_pcm_substream *substream,
+		       unsigned int channel)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	int err;
+
+	runtime->private_data = (void *)channel;
+	runtime->hw = oxygen_hardware[channel];
+	err = snd_pcm_hw_constraint_step(runtime, 0,
+					 SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 32);
+	if (err < 0)
+		return err;
+	err = snd_pcm_hw_constraint_step(runtime, 0,
+					 SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 32);
+	if (err < 0)
+		return err;
+	if (runtime->hw.formats & SNDRV_PCM_FMTBIT_S32_LE) {
+		err = snd_pcm_hw_constraint_msbits(runtime, 0, 32, 24);
+		if (err < 0)
+			return err;
+	}
+	if (runtime->hw.channels_max > 2) {
+		err = snd_pcm_hw_constraint_step(runtime, 0,
+						 SNDRV_PCM_HW_PARAM_CHANNELS,
+						 2);
+		if (err < 0)
+			return err;
+	}
+	snd_pcm_set_sync(substream);
+	chip->streams[channel] = substream;
+
+	mutex_lock(&chip->mutex);
+	chip->pcm_active |= 1 << channel;
+	if (channel == PCM_SPDIF) {
+		chip->spdif_pcm_bits = chip->spdif_bits;
+		chip->spdif_pcm_ctl->vd[0].access &=
+			~SNDRV_CTL_ELEM_ACCESS_INACTIVE;
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE |
+			       SNDRV_CTL_EVENT_MASK_INFO,
+			       &chip->spdif_pcm_ctl->id);
+	}
+	mutex_unlock(&chip->mutex);
+
+	return 0;
+}
+
+static int oxygen_rec_a_open(struct snd_pcm_substream *substream)
+{
+	return oxygen_open(substream, PCM_A);
+}
+
+static int oxygen_rec_b_open(struct snd_pcm_substream *substream)
+{
+	return oxygen_open(substream, PCM_B);
+}
+
+static int oxygen_rec_c_open(struct snd_pcm_substream *substream)
+{
+	return oxygen_open(substream, PCM_C);
+}
+
+static int oxygen_spdif_open(struct snd_pcm_substream *substream)
+{
+	return oxygen_open(substream, PCM_SPDIF);
+}
+
+static int oxygen_multich_open(struct snd_pcm_substream *substream)
+{
+	return oxygen_open(substream, PCM_MULTICH);
+}
+
+static int oxygen_ac97_open(struct snd_pcm_substream *substream)
+{
+	return oxygen_open(substream, PCM_AC97);
+}
+
+static int oxygen_close(struct snd_pcm_substream *substream)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	unsigned int channel = (unsigned int)substream->runtime->private_data;
+
+	mutex_lock(&chip->mutex);
+	chip->pcm_active &= ~(1 << channel);
+	if (channel == PCM_SPDIF) {
+		chip->spdif_pcm_ctl->vd[0].access |=
+			SNDRV_CTL_ELEM_ACCESS_INACTIVE;
+		snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE |
+			       SNDRV_CTL_EVENT_MASK_INFO,
+			       &chip->spdif_pcm_ctl->id);
+	}
+	if (channel == PCM_SPDIF || channel == PCM_MULTICH)
+		oxygen_update_spdif_source(chip);
+	mutex_unlock(&chip->mutex);
+
+	chip->streams[channel] = NULL;
+	return 0;
+}
+
+static unsigned int oxygen_format(struct snd_pcm_hw_params *hw_params)
+{
+	if (params_format(hw_params) == SNDRV_PCM_FORMAT_S32_LE)
+		return OXYGEN_FORMAT_24;
+	else
+		return OXYGEN_FORMAT_16;
+}
+
+static unsigned int oxygen_rate(struct snd_pcm_hw_params *hw_params)
+{
+	switch (params_rate(hw_params)) {
+	case 32000:
+		return OXYGEN_RATE_32000;
+	case 44100:
+		return OXYGEN_RATE_44100;
+	default: /* 48000 */
+		return OXYGEN_RATE_48000;
+	case 64000:
+		return OXYGEN_RATE_64000;
+	case 88200:
+		return OXYGEN_RATE_88200;
+	case 96000:
+		return OXYGEN_RATE_96000;
+	case 176400:
+		return OXYGEN_RATE_176400;
+	case 192000:
+		return OXYGEN_RATE_192000;
+	}
+}
+
+static unsigned int oxygen_i2s_magic2(struct snd_pcm_hw_params *hw_params)
+{
+	return params_rate(hw_params) <= 96000 ? 0x10 : 0x00;
+}
+
+static unsigned int oxygen_i2s_format(struct snd_pcm_hw_params *hw_params)
+{
+	if (params_format(hw_params) == SNDRV_PCM_FORMAT_S32_LE)
+		return OXYGEN_I2S_FORMAT_24;
+	else
+		return OXYGEN_I2S_FORMAT_16;
+}
+
+static unsigned int oxygen_play_channels(struct snd_pcm_hw_params *hw_params)
+{
+	switch (params_channels(hw_params)) {
+	default: /* 2 */
+		return OXYGEN_PLAY_CHANNELS_2;
+	case 4:
+		return OXYGEN_PLAY_CHANNELS_4;
+	case 6:
+		return OXYGEN_PLAY_CHANNELS_6;
+	case 8:
+		return OXYGEN_PLAY_CHANNELS_8;
+	}
+}
+
+static const unsigned int channel_base_registers[PCM_COUNT] = {
+	[PCM_A] = OXYGEN_DMA_A_ADDRESS,
+	[PCM_B] = OXYGEN_DMA_B_ADDRESS,
+	[PCM_C] = OXYGEN_DMA_C_ADDRESS,
+	[PCM_SPDIF] = OXYGEN_DMA_SPDIF_ADDRESS,
+	[PCM_MULTICH] = OXYGEN_DMA_MULTICH_ADDRESS,
+	[PCM_AC97] = OXYGEN_DMA_AC97_ADDRESS,
+};
+
+static int oxygen_hw_params(struct snd_pcm_substream *substream,
+			    struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	unsigned int channel = (unsigned int)substream->runtime->private_data;
+	int err;
+
+	err = snd_pcm_lib_malloc_pages(substream,
+				       params_buffer_bytes(hw_params));
+	if (err < 0)
+		return err;
+
+	oxygen_write32(chip, channel_base_registers[channel],
+		       (u32)substream->runtime->dma_addr);
+	if (channel == PCM_MULTICH) {
+		oxygen_write32(chip, OXYGEN_DMA_MULTICH_COUNT,
+			       params_buffer_bytes(hw_params) / 4 - 1);
+		oxygen_write32(chip, OXYGEN_DMA_MULTICH_TCOUNT,
+			       params_period_bytes(hw_params) / 4 - 1);
+	} else {
+		oxygen_write16(chip, channel_base_registers[channel] + 4,
+			       params_buffer_bytes(hw_params) / 4 - 1);
+		oxygen_write16(chip, channel_base_registers[channel] + 6,
+			       params_period_bytes(hw_params) / 4 - 1);
+	}
+	return 0;
+}
+
+static int oxygen_rec_a_hw_params(struct snd_pcm_substream *substream,
+				  struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	int err;
+
+	err = oxygen_hw_params(substream, hw_params);
+	if (err < 0)
+		return err;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_write8_masked(chip, OXYGEN_REC_FORMAT,
+			     oxygen_format(hw_params) << OXYGEN_REC_FORMAT_A_SHIFT,
+			     OXYGEN_REC_FORMAT_A_MASK);
+	oxygen_write8_masked(chip, OXYGEN_I2S_A_FORMAT,
+			     oxygen_rate(hw_params) |
+			     oxygen_i2s_magic2(hw_params) |
+			     oxygen_i2s_format(hw_params),
+			     OXYGEN_I2S_RATE_MASK |
+			     OXYGEN_I2S_MAGIC2_MASK |
+			     OXYGEN_I2S_FORMAT_MASK);
+	oxygen_clear_bits8(chip, OXYGEN_REC_ROUTING, 0x08);
+	spin_unlock_irq(&chip->reg_lock);
+
+	mutex_lock(&chip->mutex);
+	chip->model->set_adc_params(chip, hw_params);
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int oxygen_rec_b_hw_params(struct snd_pcm_substream *substream,
+				  struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	int err;
+
+	err = oxygen_hw_params(substream, hw_params);
+	if (err < 0)
+		return err;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_write8_masked(chip, OXYGEN_REC_FORMAT,
+			     oxygen_format(hw_params) << OXYGEN_REC_FORMAT_B_SHIFT,
+			     OXYGEN_REC_FORMAT_B_MASK);
+	oxygen_write8_masked(chip, OXYGEN_I2S_B_FORMAT,
+			     oxygen_rate(hw_params) |
+			     oxygen_i2s_magic2(hw_params) |
+			     oxygen_i2s_format(hw_params),
+			     OXYGEN_I2S_RATE_MASK |
+			     OXYGEN_I2S_MAGIC2_MASK |
+			     OXYGEN_I2S_FORMAT_MASK);
+	oxygen_clear_bits8(chip, OXYGEN_REC_ROUTING, 0x10);
+	spin_unlock_irq(&chip->reg_lock);
+
+	mutex_lock(&chip->mutex);
+	chip->model->set_adc_params(chip, hw_params);
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int oxygen_rec_c_hw_params(struct snd_pcm_substream *substream,
+				  struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	int err;
+
+	err = oxygen_hw_params(substream, hw_params);
+	if (err < 0)
+		return err;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_write8_masked(chip, OXYGEN_REC_FORMAT,
+			     oxygen_format(hw_params) << OXYGEN_REC_FORMAT_C_SHIFT,
+			     OXYGEN_REC_FORMAT_C_MASK);
+	oxygen_clear_bits8(chip, OXYGEN_REC_ROUTING, 0x20);
+	spin_unlock_irq(&chip->reg_lock);
+	return 0;
+}
+
+static int oxygen_spdif_hw_params(struct snd_pcm_substream *substream,
+				  struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	int err;
+
+	err = oxygen_hw_params(substream, hw_params);
+	if (err < 0)
+		return err;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_clear_bits32(chip, OXYGEN_SPDIF_CONTROL,
+			    OXYGEN_SPDIF_OUT_ENABLE);
+	oxygen_write8_masked(chip, OXYGEN_PLAY_FORMAT,
+			     oxygen_format(hw_params) << OXYGEN_SPDIF_FORMAT_SHIFT,
+			     OXYGEN_SPDIF_FORMAT_MASK);
+	oxygen_write32_masked(chip, OXYGEN_SPDIF_CONTROL,
+			      oxygen_rate(hw_params) << OXYGEN_SPDIF_OUT_RATE_SHIFT,
+			      OXYGEN_SPDIF_OUT_RATE_MASK);
+	oxygen_update_spdif_source(chip);
+	spin_unlock_irq(&chip->reg_lock);
+	return 0;
+}
+
+static int oxygen_multich_hw_params(struct snd_pcm_substream *substream,
+				    struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	int err;
+
+	err = oxygen_hw_params(substream, hw_params);
+	if (err < 0)
+		return err;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_write8_masked(chip, OXYGEN_PLAY_CHANNELS,
+			     oxygen_play_channels(hw_params),
+			     OXYGEN_PLAY_CHANNELS_MASK);
+	oxygen_write8_masked(chip, OXYGEN_PLAY_FORMAT,
+			     oxygen_format(hw_params) << OXYGEN_MULTICH_FORMAT_SHIFT,
+			     OXYGEN_MULTICH_FORMAT_MASK);
+	oxygen_write16_masked(chip, OXYGEN_I2S_MULTICH_FORMAT,
+			      oxygen_rate(hw_params) | oxygen_i2s_format(hw_params),
+			      OXYGEN_I2S_RATE_MASK | OXYGEN_I2S_FORMAT_MASK);
+	oxygen_clear_bits16(chip, OXYGEN_PLAY_ROUTING, 0x001f);
+	oxygen_update_dac_routing(chip);
+	oxygen_update_spdif_source(chip);
+	spin_unlock_irq(&chip->reg_lock);
+
+	mutex_lock(&chip->mutex);
+	chip->model->set_dac_params(chip, hw_params);
+	mutex_unlock(&chip->mutex);
+	return 0;
+}
+
+static int oxygen_ac97_hw_params(struct snd_pcm_substream *substream,
+				 struct snd_pcm_hw_params *hw_params)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	int err;
+
+	err = oxygen_hw_params(substream, hw_params);
+	if (err < 0)
+		return err;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_write8_masked(chip, OXYGEN_PLAY_FORMAT,
+			     oxygen_format(hw_params) << OXYGEN_AC97_FORMAT_SHIFT,
+			     OXYGEN_AC97_FORMAT_MASK);
+	spin_unlock_irq(&chip->reg_lock);
+	return 0;
+}
+
+static int oxygen_hw_free(struct snd_pcm_substream *substream)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	unsigned int channel = (unsigned int)substream->runtime->private_data;
+
+	spin_lock_irq(&chip->reg_lock);
+	chip->interrupt_mask &= ~(1 << channel);
+	oxygen_write16(chip, OXYGEN_INTERRUPT_MASK, chip->interrupt_mask);
+	spin_unlock_irq(&chip->reg_lock);
+
+	return snd_pcm_lib_free_pages(substream);
+}
+
+static int oxygen_spdif_hw_free(struct snd_pcm_substream *substream)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_clear_bits32(chip, OXYGEN_SPDIF_CONTROL,
+			    OXYGEN_SPDIF_OUT_ENABLE);
+	spin_unlock_irq(&chip->reg_lock);
+	return oxygen_hw_free(substream);
+}
+
+static int oxygen_prepare(struct snd_pcm_substream *substream)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	unsigned int channel = (unsigned int)substream->runtime->private_data;
+	unsigned int channel_mask = 1 << channel;
+
+	spin_lock_irq(&chip->reg_lock);
+	oxygen_set_bits8(chip, OXYGEN_DMA_FLUSH, channel_mask);
+	oxygen_clear_bits8(chip, OXYGEN_DMA_FLUSH, channel_mask);
+
+	chip->interrupt_mask |= channel_mask;
+	oxygen_write16(chip, OXYGEN_INTERRUPT_MASK, chip->interrupt_mask);
+	spin_unlock_irq(&chip->reg_lock);
+	return 0;
+}
+
+static int oxygen_trigger(struct snd_pcm_substream *substream, int cmd)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	struct snd_pcm_substream *s;
+	unsigned int mask = 0;
+	int running;
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_STOP:
+	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+		running = 0;
+		break;
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+		running = 1;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	snd_pcm_group_for_each_entry(s, substream) {
+		if (snd_pcm_substream_chip(s) == chip) {
+			mask |= 1 << (unsigned int)s->runtime->private_data;
+			snd_pcm_trigger_done(s, substream);
+		}
+	}
+
+	spin_lock(&chip->reg_lock);
+	if (running)
+		chip->pcm_running |= mask;
+	else
+		chip->pcm_running &= ~mask;
+	oxygen_write8(chip, OXYGEN_DMA_STATUS, chip->pcm_running);
+	spin_unlock(&chip->reg_lock);
+	return 0;
+}
+
+static snd_pcm_uframes_t oxygen_pointer(struct snd_pcm_substream *substream)
+{
+	struct oxygen *chip = snd_pcm_substream_chip(substream);
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	unsigned int channel = (unsigned int)runtime->private_data;
+	u32 curr_addr;
+
+	/* no spinlock, this read should be atomic */
+	curr_addr = oxygen_read32(chip, channel_base_registers[channel]);
+	return bytes_to_frames(runtime, curr_addr - (u32)runtime->dma_addr);
+}
+
+static struct snd_pcm_ops oxygen_rec_a_ops = {
+	.open      = oxygen_rec_a_open,
+	.close     = oxygen_close,
+	.ioctl     = snd_pcm_lib_ioctl,
+	.hw_params = oxygen_rec_a_hw_params,
+	.hw_free   = oxygen_hw_free,
+	.prepare   = oxygen_prepare,
+	.trigger   = oxygen_trigger,
+	.pointer   = oxygen_pointer,
+};
+
+static struct snd_pcm_ops oxygen_rec_b_ops = {
+	.open      = oxygen_rec_b_open,
+	.close     = oxygen_close,
+	.ioctl     = snd_pcm_lib_ioctl,
+	.hw_params = oxygen_rec_b_hw_params,
+	.hw_free   = oxygen_hw_free,
+	.prepare   = oxygen_prepare,
+	.trigger   = oxygen_trigger,
+	.pointer   = oxygen_pointer,
+};
+
+static struct snd_pcm_ops oxygen_rec_c_ops = {
+	.open      = oxygen_rec_c_open,
+	.close     = oxygen_close,
+	.ioctl     = snd_pcm_lib_ioctl,
+	.hw_params = oxygen_rec_c_hw_params,
+	.hw_free   = oxygen_hw_free,
+	.prepare   = oxygen_prepare,
+	.trigger   = oxygen_trigger,
+	.pointer   = oxygen_pointer,
+};
+
+static struct snd_pcm_ops oxygen_spdif_ops = {
+	.open      = oxygen_spdif_open,
+	.close     = oxygen_close,
+	.ioctl     = snd_pcm_lib_ioctl,
+	.hw_params = oxygen_spdif_hw_params,
+	.hw_free   = oxygen_spdif_hw_free,
+	.prepare   = oxygen_prepare,
+	.trigger   = oxygen_trigger,
+	.pointer   = oxygen_pointer,
+};
+
+static struct snd_pcm_ops oxygen_multich_ops = {
+	.open      = oxygen_multich_open,
+	.close     = oxygen_close,
+	.ioctl     = snd_pcm_lib_ioctl,
+	.hw_params = oxygen_multich_hw_params,
+	.hw_free   = oxygen_hw_free,
+	.prepare   = oxygen_prepare,
+	.trigger   = oxygen_trigger,
+	.pointer   = oxygen_pointer,
+};
+
+static struct snd_pcm_ops oxygen_ac97_ops = {
+	.open      = oxygen_ac97_open,
+	.close     = oxygen_close,
+	.ioctl     = snd_pcm_lib_ioctl,
+	.hw_params = oxygen_ac97_hw_params,
+	.hw_free   = oxygen_hw_free,
+	.prepare   = oxygen_prepare,
+	.trigger   = oxygen_trigger,
+	.pointer   = oxygen_pointer,
+};
+
+static void oxygen_pcm_free(struct snd_pcm *pcm)
+{
+	snd_pcm_lib_preallocate_free_for_all(pcm);
+}
+
+int __devinit oxygen_pcm_init(struct oxygen *chip)
+{
+	struct snd_pcm *pcm;
+	int err;
+
+	err = snd_pcm_new(chip->card, "Analog", 0, 1, 1, &pcm);
+	if (err < 0)
+		return err;
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &oxygen_multich_ops);
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
+			chip->model->record_from_dma_b ?
+			&oxygen_rec_b_ops : &oxygen_rec_a_ops);
+	pcm->private_data = chip;
+	pcm->private_free = oxygen_pcm_free;
+	strcpy(pcm->name, "Analog");
+	snd_pcm_lib_preallocate_pages(pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream,
+				      SNDRV_DMA_TYPE_DEV,
+				      snd_dma_pci_data(chip->pci),
+				      512 * 1024, 2048 * 1024);
+	snd_pcm_lib_preallocate_pages(pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream,
+				      SNDRV_DMA_TYPE_DEV,
+				      snd_dma_pci_data(chip->pci),
+				      128 * 1024, 256 * 1024);
+
+	err = snd_pcm_new(chip->card, "Digital", 1, 1, 1, &pcm);
+	if (err < 0)
+		return err;
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &oxygen_spdif_ops);
+	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &oxygen_rec_c_ops);
+	pcm->private_data = chip;
+	pcm->private_free = oxygen_pcm_free;
+	strcpy(pcm->name, "Digital");
+	snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
+					      snd_dma_pci_data(chip->pci),
+					      128 * 1024, 256 * 1024);
+
+	if (chip->has_2nd_ac97_codec) {
+		err = snd_pcm_new(chip->card, "AC97", 2, 1, 0, &pcm);
+		if (err < 0)
+			return err;
+		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
+				&oxygen_ac97_ops);
+		pcm->private_data = chip;
+		pcm->private_free = oxygen_pcm_free;
+		strcpy(pcm->name, "Front Panel");
+		snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
+						      snd_dma_pci_data(chip->pci),
+						      128 * 1024, 256 * 1024);
+	}
+	return 0;
+}
diff --git a/sound/pci/oxygen/oxygen_regs.h b/sound/pci/oxygen/oxygen_regs.h
new file mode 100644
index 0000000..7a4726d
--- /dev/null
+++ b/sound/pci/oxygen/oxygen_regs.h
@@ -0,0 +1,246 @@
+#ifndef OXYGEN_REGS_H_INCLUDED
+#define OXYGEN_REGS_H_INCLUDED
+
+/* recording channel A */
+#define OXYGEN_DMA_A_ADDRESS		0x00	/* 32-bit base address */
+#define OXYGEN_DMA_A_COUNT		0x04	/* buffer counter (dwords) */
+#define OXYGEN_DMA_A_TCOUNT		0x06	/* interrupt counter (dwords) */
+
+/* recording channel B */
+#define OXYGEN_DMA_B_ADDRESS		0x08
+#define OXYGEN_DMA_B_COUNT		0x0c
+#define OXYGEN_DMA_B_TCOUNT		0x0e
+
+/* recording channel C */
+#define OXYGEN_DMA_C_ADDRESS		0x10
+#define OXYGEN_DMA_C_COUNT		0x14
+#define OXYGEN_DMA_C_TCOUNT		0x16
+
+/* SPDIF playback channel */
+#define OXYGEN_DMA_SPDIF_ADDRESS	0x18
+#define OXYGEN_DMA_SPDIF_COUNT		0x1c
+#define OXYGEN_DMA_SPDIF_TCOUNT		0x1e
+
+/* multichannel playback channel */
+#define OXYGEN_DMA_MULTICH_ADDRESS	0x20
+#define OXYGEN_DMA_MULTICH_COUNT	0x24	/* 32 bits */
+#define OXYGEN_DMA_MULTICH_TCOUNT	0x28	/* 32 bits */
+
+/* AC'97 (front panel) playback channel */
+#define OXYGEN_DMA_AC97_ADDRESS		0x30
+#define OXYGEN_DMA_AC97_COUNT		0x34
+#define OXYGEN_DMA_AC97_TCOUNT		0x36
+
+/* all registers 0x00..0x36 return current position on read */
+
+#define OXYGEN_DMA_STATUS		0x40	/* 1 = running, 0 = stop */
+#define  OXYGEN_CHANNEL_A		0x01
+#define  OXYGEN_CHANNEL_B		0x02
+#define  OXYGEN_CHANNEL_C		0x04
+#define  OXYGEN_CHANNEL_SPDIF		0x08
+#define  OXYGEN_CHANNEL_MULTICH		0x10
+#define  OXYGEN_CHANNEL_AC97		0x20
+
+#define OXYGEN_DMA_RESET		0x42
+/* OXYGEN_CHANNEL_* */
+
+#define OXYGEN_PLAY_CHANNELS		0x43
+#define  OXYGEN_PLAY_CHANNELS_MASK	0x03
+#define  OXYGEN_PLAY_CHANNELS_2		0x00
+#define  OXYGEN_PLAY_CHANNELS_4		0x01
+#define  OXYGEN_PLAY_CHANNELS_6		0x02
+#define  OXYGEN_PLAY_CHANNELS_8		0x03
+
+#define OXYGEN_INTERRUPT_MASK		0x44
+/* OXYGEN_CHANNEL_* */
+#define  OXYGEN_INT_SPDIF_IN_CHANGE	0x0100
+#define  OXYGEN_INT_GPIO		0x0800
+
+#define OXYGEN_INTERRUPT_STATUS		0x46
+/* OXYGEN_CHANNEL_* amd OXYGEN_INT_* */
+#define  OXYGEN_INT_MIDI		0x1000
+
+#define OXYGEN_MISC			0x48
+#define  OXYGEN_MISC_MAGIC		0x20
+#define  OXYGEN_MISC_MIDI		0x40
+
+#define OXYGEN_REC_FORMAT		0x4a
+#define  OXYGEN_REC_FORMAT_A_MASK	0x03
+#define  OXYGEN_REC_FORMAT_A_SHIFT	0
+#define  OXYGEN_REC_FORMAT_B_MASK	0x0c
+#define  OXYGEN_REC_FORMAT_B_SHIFT	2
+#define  OXYGEN_REC_FORMAT_C_MASK	0x30
+#define  OXYGEN_REC_FORMAT_C_SHIFT	4
+#define  OXYGEN_FORMAT_16		0x00
+#define  OXYGEN_FORMAT_24		0x01
+#define  OXYGEN_FORMAT_32		0x02
+
+#define OXYGEN_PLAY_FORMAT		0x4b
+#define  OXYGEN_SPDIF_FORMAT_MASK	0x03
+#define  OXYGEN_SPDIF_FORMAT_SHIFT	0
+#define  OXYGEN_MULTICH_FORMAT_MASK	0x0c
+#define  OXYGEN_MULTICH_FORMAT_SHIFT	2
+#define  OXYGEN_AC97_FORMAT_MASK	0x30
+#define  OXYGEN_AC97_FORMAT_SHIFT	4
+/* OXYGEN_FORMAT_* */
+
+#define OXYGEN_REC_CHANNELS		0x4c
+#define  OXYGEN_REC_A_CHANNELS_MASK	0x07
+#define  OXYGEN_REC_CHANNELS_2		0x00
+#define  OXYGEN_REC_CHANNELS_4		0x01
+#define  OXYGEN_REC_CHANNELS_6		0x03	/* or 0x02 */
+#define  OXYGEN_REC_CHANNELS_8		0x04
+
+#define OXYGEN_FUNCTION			0x50
+#define  OXYGEN_FUNCTION_RESET_CODEC	0x02
+#define  OXYGEN_FUNCTION_ENABLE_SPI_4_5	0x80
+
+#define OXYGEN_I2S_MULTICH_FORMAT	0x60
+#define  OXYGEN_I2S_RATE_MASK		0x0007
+#define  OXYGEN_RATE_32000		0x0000
+#define  OXYGEN_RATE_44100		0x0001
+#define  OXYGEN_RATE_48000		0x0002
+#define  OXYGEN_RATE_64000		0x0003
+#define  OXYGEN_RATE_88200		0x0004
+#define  OXYGEN_RATE_96000		0x0005
+#define  OXYGEN_RATE_176400		0x0006
+#define  OXYGEN_RATE_192000		0x0007
+#define  OXYGEN_I2S_MAGIC1_MASK		0x0008
+#define  OXYGEN_I2S_MAGIC2_MASK		0x0030
+#define  OXYGEN_I2S_FORMAT_MASK		0x00c0
+#define  OXYGEN_I2S_FORMAT_16		0x0000
+#define  OXYGEN_I2S_FORMAT_20		0x0040
+#define  OXYGEN_I2S_FORMAT_24		0x0080
+#define  OXYGEN_I2S_FORMAT_32		0x00c0
+
+#define OXYGEN_I2S_A_FORMAT		0x62
+#define OXYGEN_I2S_B_FORMAT		0x64
+#define OXYGEN_I2S_C_FORMAT		0x66
+/* OXYGEN_I2S_RATE_* and OXYGEN_I2S_FORMAT_* */
+
+#define OXYGEN_SPDIF_CONTROL		0x70
+#define  OXYGEN_SPDIF_OUT_ENABLE	0x00000002
+#define  OXYGEN_SPDIF_LOOPBACK		0x00000004
+#define  OXYGEN_SPDIF_MAGIC2		0x00000020
+#define  OXYGEN_SPDIF_MAGIC3		0x00000040
+#define  OXYGEN_SPDIF_IN_VALID		0x00001000
+#define  OXYGEN_SPDIF_IN_CHANGE		0x00008000	/* r/wc */
+#define  OXYGEN_SPDIF_IN_INVERT		0x00010000	/* ? */
+#define  OXYGEN_SPDIF_OUT_RATE_MASK	0x07000000
+#define  OXYGEN_SPDIF_OUT_RATE_SHIFT	24
+/* OXYGEN_RATE_* << OXYGEN_SPDIF_OUT_RATE_SHIFT */
+
+#define OXYGEN_SPDIF_OUTPUT_BITS	0x74
+#define  OXYGEN_SPDIF_NONAUDIO		0x00000002
+#define  OXYGEN_SPDIF_C			0x00000004
+#define  OXYGEN_SPDIF_PREEMPHASIS	0x00000008
+#define  OXYGEN_SPDIF_CATEGORY_MASK	0x000007f0
+#define  OXYGEN_SPDIF_CATEGORY_SHIFT	4
+#define  OXYGEN_SPDIF_ORIGINAL		0x00000800
+#define  OXYGEN_SPDIF_CS_RATE_MASK	0x0000f000
+#define  OXYGEN_SPDIF_CS_RATE_SHIFT	12
+#define  OXYGEN_SPDIF_V			0x00010000	/* 0 = valid */
+
+#define OXYGEN_SPDIF_INPUT_BITS		0x78
+/* 32 bits, IEC958_AES_* */
+
+#define OXYGEN_2WIRE_CONTROL		0x90
+#define  OXYGEN_2WIRE_DIR_MASK		0x01
+#define  OXYGEN_2WIRE_DIR_WRITE		0x00	/* ? */
+#define  OXYGEN_2WIRE_DIR_READ		0x01	/* ? */
+#define  OXYGEN_2WIRE_ADDRESS_MASK	0xfe	/* slave device address */
+#define  OXYGEN_2WIRE_ADDRESS_SHIFT	1
+
+#define OXYGEN_2WIRE_MAP		0x91	/* address, 8 bits */
+#define OXYGEN_2WIRE_DATA		0x92	/* data, 16 bits */
+
+#define OXYGEN_2WIRE_BUS_STATUS		0x94
+#define  OXYGEN_2WIRE_BUSY		0x01
+
+#define OXYGEN_SPI_CONTROL		0x98
+#define  OXYGEN_SPI_BUSY		0x01	/* read */
+#define  OXYGEN_SPI_TRIGGER_WRITE	0x01	/* write */
+#define  OXYGEN_SPI_DATA_LENGTH_MASK	0x02
+#define  OXYGEN_SPI_DATA_LENGTH_2	0x00
+#define  OXYGEN_SPI_DATA_LENGTH_3	0x02
+#define  OXYGEN_SPI_CODEC_MASK		0x70	/* 0..5 */
+#define  OXYGEN_SPI_CODEC_SHIFT		4
+#define  OXYGEN_SPI_MAGIC		0x80
+
+#define OXYGEN_SPI_DATA1		0x99
+#define OXYGEN_SPI_DATA2		0x9a
+#define OXYGEN_SPI_DATA3		0x9b
+
+#define OXYGEN_MPU401			0xa0
+
+#define OXYGEN_GPI_DATA			0xa4
+
+#define OXYGEN_GPI_INTERRUPT_MASK	0xa5
+
+#define OXYGEN_GPIO_DATA		0xa6
+
+#define OXYGEN_GPIO_CONTROL		0xa8
+/* 0: input, 1: output */
+
+#define OXYGEN_GPIO_INTERRUPT_MASK	0xaa
+
+#define OXYGEN_DEVICE_SENSE		0xac	/* ? */
+
+#define OXYGEN_PLAY_ROUTING		0xc0
+#define  OXYGEN_PLAY_DAC0_SOURCE_MASK	0x0300
+#define  OXYGEN_PLAY_DAC1_SOURCE_MASK	0x0700
+#define  OXYGEN_PLAY_DAC2_SOURCE_MASK	0x3000
+#define  OXYGEN_PLAY_DAC3_SOURCE_MASK	0x7000
+
+#define OXYGEN_REC_ROUTING		0xc2
+
+#define OXYGEN_ADC_MONITOR		0xc3
+#define  OXYGEN_ADC_MONITOR_MULTICH	0x01
+#define  OXYGEN_ADC_MONITOR_AC97	0x04
+#define  OXYGEN_ADC_MONITOR_SPDIF	0x10
+
+#define OXYGEN_A_MONITOR_ROUTING	0xc4
+
+#define OXYGEN_AC97_CONTROL		0xd0
+#define  OXYGEN_AC97_RESET1		0x0001
+#define  OXYGEN_AC97_RESET1_BUSY	0x0002
+#define  OXYGEN_AC97_RESET2		0x0008
+#define  OXYGEN_AC97_CODEC_0		0x0010
+#define  OXYGEN_AC97_CODEC_1		0x0020
+
+#define OXYGEN_AC97_INTERRUPT_MASK	0xd2
+
+#define OXYGEN_AC97_INTERRUPT_STATUS	0xd3
+#define  OXYGEN_AC97_READ_COMPLETE	0x01
+#define  OXYGEN_AC97_WRITE_COMPLETE	0x02
+
+#define OXYGEN_AC97_OUT_CONFIG		0xd4
+#define  OXYGEN_AC97_OUT_MAGIC1		0x00000011
+#define  OXYGEN_AC97_OUT_MAGIC2		0x00000033
+#define  OXYGEN_AC97_OUT_MAGIC3		0x0000ff00
+
+#define OXYGEN_AC97_IN_CONFIG		0xd8
+#define  OXYGEN_AC97_IN_MAGIC1		0x00000011
+#define  OXYGEN_AC97_IN_MAGIC2		0x00000033
+#define  OXYGEN_AC97_IN_MAGIC3		0x00000300
+
+#define OXYGEN_AC97_REGS		0xdc
+#define  OXYGEN_AC97_REG_DATA_MASK	0x0000ffff
+#define  OXYGEN_AC97_REG_ADDR_MASK	0x007f0000
+#define  OXYGEN_AC97_REG_ADDR_SHIFT	16
+#define  OXYGEN_AC97_REG_DIR_MASK	0x00800000
+#define  OXYGEN_AC97_REG_DIR_WRITE	0x00000000
+#define  OXYGEN_AC97_REG_DIR_READ	0x00800000
+#define  OXYGEN_AC97_REG_CODEC_MASK	0x01000000
+#define  OXYGEN_AC97_REG_CODEC_SHIFT	24
+
+#define OXYGEN_DMA_FLUSH		0xe1
+/* OXYGEN_CHANNEL_* */
+
+#define OXYGEN_CODEC_VERSION		0xe4
+
+#define OXYGEN_REVISION			0xe6
+#define  OXYGEN_REVISION_2		0x08	/* bit flag */
+#define  OXYGEN_REVISION_8787		0x14	/* all 8 bits */
+
+#endif