
/*
    opti93x.c - lowlevel code for OPTi 82c93x chips
    Copyright (C) 1999-2000 by Massimo Piccioni <dafastidio@libero.it>

    Inspired from CS4231 code by Jaroslav Kysela.

    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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#define __SND_OSS_COMPAT__
#define SND_MAIN_OBJECT_FILE
#include "../../include/driver.h"
#include "../../include/opti93x.h"

#define snd_opti93x_printk(args...)	snd_printk(__FILE__": " ##args)

static struct {
	unsigned int hertz;
	unsigned int rate;
	unsigned char bits;
} snd_opti93x_freq[] = {
	{ 5,	5512,	0x01 },
	{ 6,	6615,	0x0f },
	{ 8,	8000,	0x00 },
	{ 9,	9600,	0x0e },
	{ 11,	11025,	0x03 },
	{ 16,	16000,	0x02 },
	{ 18,	18900,	0x05 },
	{ 22,	22050,	0x07 },
	{ 27,	27428,	0x04 },
	{ 32,	32000,	0x06 },
	{ 33,	33075,	0x0d },
	{ 37,	37800,	0x09 },
	{ 44,	44100,	0x0b },
	{ 48,	48000,	0x0c }
};

static unsigned char snd_opti93x_default_image[32] =
{
	0x00,		/* 00/00 - l_mixout_outctrl */
	0x00,		/* 01/01 - r_mixout_outctrl */
	0x88,		/* 02/02 - l_cd_inctrl */
	0x88,		/* 03/03 - r_cd_inctrl */
	0x88,		/* 04/04 - l_a1/fm_inctrl */
	0x88,		/* 05/05 - r_a1/fm_inctrl */
	0x80,		/* 06/06 - l_dac_inctrl */
	0x80,		/* 07/07 - r_dac_inctrl */
	0x00,		/* 08/08 - ply_dataform_reg */
	0x00,		/* 09/09 - if_conf */
	0x00,		/* 0a/10 - pin_ctrl */
	0x00,		/* 0b/11 - err_init_reg */
	0x0a,		/* 0c/12 - id_reg */
	0x00,		/* 0d/13 - reserved */
	0x00,		/* 0e/14 - ply_upcount_reg */
	0x00,		/* 0f/15 - ply_lowcount_reg */
	0x88,		/* 10/16 - reserved/l_a1_inctrl */
	0x88,		/* 11/17 - reserved/r_a1_inctrl */
	0x88,		/* 12/18 - l_line_inctrl */
	0x88,		/* 13/19 - r_line_inctrl */
	0x88,		/* 14/20 - l_mic_inctrl */
	0x88,		/* 15/21 - r_mic_inctrl */
	0x80,		/* 16/22 - l_out_outctrl */
	0x80,		/* 17/23 - r_out_outctrl */
	0x00,		/* 18/24 - reserved */
	0x00,		/* 19/25 - reserved */
	0x00,		/* 1a/26 - reserved */
	0x00,		/* 1b/27 - reserved */
	0x00,		/* 1c/28 - cap_dataform_reg */
	0x00,		/* 1d/29 - reserved */
	0x00,		/* 1e/30 - cap_upcount_reg */
	0x00		/* 1f/31 - cap_lowcount_reg */
};


static int snd_opti93x_busy_wait(opti93x_t *codec)
{
	int timeout;

	for (timeout = 250; timeout-- > 0; udelay(10))
		if (!(inb(OPTi93X_PORT(codec, INDEX)) & OPTi93X_INIT))
			return 0;

	snd_opti93x_printk("chip still busy.\n");
	return -EBUSY;
}

static unsigned char snd_opti93x_in(opti93x_t *codec, unsigned char reg)
{
	snd_opti93x_busy_wait(codec);
	outb(codec->mce_bit | (reg & 0x1f), OPTi93X_PORT(codec, INDEX));
	return inb(OPTi93X_PORT(codec, DATA));
}

static void snd_opti93x_out(opti93x_t *codec, unsigned char reg,
			    unsigned char value)
{
	snd_opti93x_busy_wait(codec);
	outb(codec->mce_bit | (reg & 0x1f), OPTi93X_PORT(codec, INDEX));
	outb(value, OPTi93X_PORT(codec, DATA));
}

static void snd_opti93x_out_image(opti93x_t *codec, unsigned char reg,
				  unsigned char value)
{
	snd_opti93x_out(codec, reg, codec->image[reg] = value);
}

static void snd_opti93x_out_mask(opti93x_t *codec, unsigned char reg,
				 unsigned char mask, unsigned char value)
{
	snd_opti93x_out_image(codec, reg,
		(codec->image[reg] & ~mask) | (value & mask));
}


static void snd_opti93x_mce_up(opti93x_t *codec)
{
	snd_opti93x_busy_wait(codec);

	codec->mce_bit = OPTi93X_MCE;
	if (!(inb(OPTi93X_PORT(codec, INDEX)) & OPTi93X_MCE))
		outb(codec->mce_bit, OPTi93X_PORT(codec, INDEX));
}

static void snd_opti93x_mce_down(opti93x_t *codec)
{
	snd_opti93x_busy_wait(codec);

	codec->mce_bit = 0;
	if (inb(OPTi93X_PORT(codec, INDEX)) & OPTi93X_MCE)
		outb(codec->mce_bit, OPTi93X_PORT(codec, INDEX));
}

#define snd_opti93x_mute_reg(codec, reg, mute)	\
	snd_opti93x_out(codec, reg, mute ? 0x80 : codec->image[reg]);

static void snd_opti93x_mute(opti93x_t *codec, int mute)
{
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	mute = mute ? 1 : 0;
	if (codec->mute == mute) {
		spin_unlock_irqrestore(&codec->lock, flags);
		return;
	}
	codec->mute = mute;

	snd_opti93x_mute_reg(codec, OPTi93X_CD_LEFT_INPUT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_CD_RIGHT_INPUT, mute);
	switch (codec->hardware) {
	case OPTi9XX_HW_82C930:
		snd_opti93x_mute_reg(codec, OPTi930_AUX_LEFT_INPUT, mute);
		snd_opti93x_mute_reg(codec, OPTi930_AUX_RIGHT_INPUT, mute);
		break;
	case OPTi9XX_HW_82C931:
	case OPTi9XX_HW_82C933:
		snd_opti93x_mute_reg(codec, OPTi931_FM_LEFT_INPUT, mute);
		snd_opti93x_mute_reg(codec, OPTi931_FM_RIGHT_INPUT, mute);
		snd_opti93x_mute_reg(codec, OPTi931_AUX_LEFT_INPUT, mute);
		snd_opti93x_mute_reg(codec, OPTi931_AUX_RIGHT_INPUT, mute);
	}
	snd_opti93x_mute_reg(codec, OPTi93X_DAC_LEFT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_DAC_RIGHT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_LINE_LEFT_INPUT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_LINE_RIGHT_INPUT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_MIC_LEFT_INPUT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_MIC_RIGHT_INPUT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_OUT_LEFT, mute);
	snd_opti93x_mute_reg(codec, OPTi93X_OUT_RIGHT, mute);

	spin_unlock_irqrestore(&codec->lock, flags);
}


static unsigned int snd_opti93x_get_count(unsigned char format,
					  unsigned int size)
{
	switch (format & 0xe0) {
	case OPTi93X_LINEAR_16_LIT:
	case OPTi93X_LINEAR_16_BIG:
		size >>= 1;
		break;
	case OPTi93X_ADPCM_16:
		return size >> 2;
	}
	return (format & OPTi93X_STEREO) ? (size >> 1) : size;
}

static unsigned char snd_opti93x_get_freq(unsigned int freq)
{
	int i;

	freq /= 1000;
	for (i = 0; i < 13; i++)
		if (freq <= snd_opti93x_freq[i].hertz)
			break;

	return snd_opti93x_freq[i].bits;
}

static unsigned int snd_opti93x_xrate(opti93x_t *codec, unsigned int rate)
{
	int i;

	rate /= 1000;
	for (i = 0; i < 13; i++)
		if (rate <= snd_opti93x_freq[i].hertz)
			break;

	return snd_opti93x_freq[i].rate;
}


static int snd_opti93x_playback_ioctl(void *private_data,
				      snd_pcm_subchn_t *subchn,
				      unsigned int cmd, unsigned long *arg)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	int error;

	if ((error = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg)) < 0)
		return error;

	if (cmd == SND_PCM_IOCTL1_PARAMS)
		subchn->runtime->format.rate = codec->set_rate(codec,
			subchn->runtime->format.rate);
	return 0;
}

static int snd_opti93x_capture_ioctl(void *private_data,
				     snd_pcm_subchn_t *subchn,
				     unsigned int cmd, unsigned long *arg)
{
	return snd_opti93x_playback_ioctl(private_data, subchn, cmd, arg);
}


static unsigned char snd_opti93x_get_format(opti93x_t *codec,
					    unsigned int format, int voices)
{
	unsigned char retval = OPTi93X_LINEAR_8;

	switch (format) {
	case SND_PCM_SFMT_MU_LAW:
		retval = OPTi93X_ULAW_8;
		break;
	case SND_PCM_SFMT_A_LAW:
		retval = OPTi93X_ALAW_8;
		break;
	case SND_PCM_SFMT_S16_LE:
		retval = OPTi93X_LINEAR_16_LIT;
		break;
	case SND_PCM_SFMT_S16_BE:
		retval = OPTi93X_LINEAR_16_BIG;
		break;
	case SND_PCM_SFMT_IMA_ADPCM:
		retval = OPTi93X_ADPCM_16;
	}
	return (voices > 1) ? (retval | OPTi93X_STEREO) : retval;
}


static void snd_opti93x_playback_format(opti93x_t *codec, unsigned char fmt)
{
	unsigned long flags;
	unsigned char mask;

	spin_lock_irqsave(&codec->lock, flags);
	snd_opti93x_mute(codec, 1);

	snd_opti93x_mce_up(codec);
	mask = (codec->mode & OPTi93X_MODE_CAPTURE) ? 0xf0 : 0xff;
	snd_opti93x_out_mask(codec, OPTi93X_PLAY_FORMAT, mask, fmt);
	snd_opti93x_mce_down(codec);

	snd_opti93x_mute(codec, 0);
	spin_unlock_irqrestore(&codec->lock, flags);
}

static void snd_opti93x_capture_format(opti93x_t *codec, unsigned char fmt)
{
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);
	snd_opti93x_mute(codec, 1);

	snd_opti93x_mce_up(codec);
	if (!(codec->mode & OPTi93X_MODE_PLAY))
		snd_opti93x_out_mask(codec, OPTi93X_PLAY_FORMAT, 0x0f, fmt);
	else
		fmt = codec->image[OPTi93X_PLAY_FORMAT] & 0xf0;
	snd_opti93x_out_image(codec, OPTi93X_CAPT_FORMAT, fmt);
	snd_opti93x_mce_down(codec);

	snd_opti93x_mute(codec, 0);
	spin_unlock_irqrestore(&codec->lock, flags);
}


static void snd_opti93x_open(opti93x_t *codec, unsigned int mode)
{
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	if (!(codec->mode & OPTi93X_MODE_OPEN)) {
		outb(0x00, OPTi93X_PORT(codec, STATUS));
		snd_opti93x_out_mask(codec, OPTi93X_PIN_CTRL,
			OPTi93X_IRQ_ENABLE, OPTi93X_IRQ_ENABLE);
		codec->mode = mode;
	}
	else
		codec->mode |= mode;

	spin_unlock_irqrestore(&codec->lock, flags);
}

static void snd_opti93x_close(opti93x_t *codec, unsigned int mode)
{
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	codec->mode &= ~mode;
	if (codec->mode & OPTi93X_MODE_OPEN) {
		spin_unlock_irqrestore(&codec->lock, flags);
		return;
	}

	snd_opti93x_mute(codec, 1);

	outb(0, OPTi93X_PORT(codec, STATUS));
	snd_opti93x_out_mask(codec, OPTi93X_PIN_CTRL, OPTi93X_IRQ_ENABLE,
		~OPTi93X_IRQ_ENABLE);

	snd_opti93x_mce_up(codec);
	snd_opti93x_out_image(codec, OPTi93X_IFACE_CONF, 0x00);
	snd_opti93x_mce_down(codec);
	codec->mode = 0;

	snd_opti93x_mute(codec, 0);
	spin_unlock_irqrestore(&codec->lock, flags);
}

static int snd_opti93x_trigger(opti93x_t *codec, unsigned char what,
			       int channel, int cmd)
{
	int error = 0;
	unsigned long flags;
	snd_pcm_subchn_t *psubchn = NULL;
	snd_pcm_subchn_t *csubchn = NULL;

	if (cmd == SND_PCM_TRIGGER_SYNC_GO) {
		cmd = SND_PCM_TRIGGER_GO;
		psubchn = codec->playback_subchn;
		csubchn = codec->capture_subchn;

		if (psubchn == NULL || csubchn == NULL ||
				memcmp(&psubchn->runtime->sync_group,
					&csubchn->runtime->sync_group,
					sizeof(snd_pcm_sync_t))) {
		    	if (what & OPTi93X_PLAYBACK_ENABLE)
				csubchn = NULL;
			else
				psubchn = NULL;
		    	goto __trigger;
		}

		if (*psubchn->runtime->status != SND_PCM_STATUS_PREPARED)
		    	return -EINVAL;
		if (*csubchn->runtime->status != SND_PCM_STATUS_PREPARED)
		    	return -EINVAL;

		if (!snd_pcm_playback_data(psubchn) ||
		    !snd_pcm_capture_empty(csubchn))
			return -EINVAL;

		if ((error = snd_pcm_channel_go_pre(psubchn,
				SND_PCM_TRIGGER_GO)))
			goto __cleanup;
		if ((error = snd_pcm_channel_go_pre(csubchn,
				SND_PCM_TRIGGER_GO)))
			goto __cleanup;

		what = OPTi93X_PLAYBACK_ENABLE | OPTi93X_CAPTURE_ENABLE;
	}

__trigger:
	spin_lock_irqsave(&codec->lock, flags);

	switch (cmd) {
	case SND_PCM_TRIGGER_GO:
	case SND_PCM_TRIGGER_STOP:
		snd_opti93x_out_mask(codec, OPTi93X_IFACE_CONF, what,
			what & (cmd == SND_PCM_TRIGGER_GO ? 0xff: 0x00));
		break;
	default:
		error = -EINVAL;
	}

	spin_unlock_irqrestore(&codec->lock, flags);

__cleanup:
	if (psubchn)
		snd_pcm_channel_go_post(psubchn, SND_PCM_TRIGGER_GO, error);
	if (csubchn)
		snd_pcm_channel_go_post(csubchn, SND_PCM_TRIGGER_GO, error);

	return error;
}

static int snd_opti93x_playback_trigger(void *private_data,
					snd_pcm_subchn_t *subchn, int cmd)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	return snd_opti93x_trigger(codec,
		OPTi93X_PLAYBACK_ENABLE, SND_PCM_CHANNEL_PLAYBACK, cmd);
}

static int snd_opti93x_capture_trigger(void *private_data,
				       snd_pcm_subchn_t * subchn, int cmd)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	return snd_opti93x_trigger(codec,
		OPTi93X_CAPTURE_ENABLE, SND_PCM_CHANNEL_CAPTURE, cmd);
}

static int snd_opti93x_playback_prepare(void *private_data,
					snd_pcm_subchn_t * subchn)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	unsigned long flags;
	unsigned char format;
	unsigned int count = snd_pcm_lib_transfer_fragment(subchn);
	unsigned int size = snd_pcm_lib_transfer_size(subchn);

	spin_lock_irqsave(&codec->lock, flags);

	codec->p_dma_size = size;
	snd_opti93x_out_mask(codec, OPTi93X_IFACE_CONF,
		OPTi93X_PLAYBACK_ENABLE | OPTi93X_PLAYBACK_PIO,
		~(OPTi93X_PLAYBACK_ENABLE | OPTi93X_PLAYBACK_PIO));

	snd_dma_program(codec->dma1, runtime->dma_area->buf, size,
		DMA_MODE_WRITE | DMA_AUTOINIT);

	format = snd_opti93x_get_freq(runtime->format.rate);
	format |= snd_opti93x_get_format(codec, runtime->format.format,
		runtime->format.voices);
	codec->set_playback_format(codec, format);
	format = codec->image[OPTi93X_PLAY_FORMAT];

	count = snd_opti93x_get_count(format, count) - 1;
	snd_opti93x_out_image(codec, OPTi93X_PLAY_LWR_CNT, count);
	snd_opti93x_out_image(codec, OPTi93X_PLAY_UPR_CNT, count >> 8);

	spin_unlock_irqrestore(&codec->lock, flags);
	return 0;
}

static int snd_opti93x_capture_prepare(void *private_data,
				       snd_pcm_subchn_t *subchn)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	unsigned long flags;
	unsigned char format;
	unsigned int count = snd_pcm_lib_transfer_fragment(subchn);
	unsigned int size = snd_pcm_lib_transfer_size(subchn);

	spin_lock_irqsave(&codec->lock, flags);

	codec->c_dma_size = size;
	snd_opti93x_out_mask(codec, OPTi93X_IFACE_CONF,
		OPTi93X_CAPTURE_ENABLE | OPTi93X_CAPTURE_PIO,
		(unsigned char)~(OPTi93X_CAPTURE_ENABLE | OPTi93X_CAPTURE_PIO));

	snd_dma_program(codec->dma2, runtime->dma_area->buf, size,
		DMA_MODE_READ | DMA_AUTOINIT);

	format = snd_opti93x_get_freq(runtime->format.rate);
	format |= snd_opti93x_get_format(codec, runtime->format.format,
		runtime->format.voices);
	codec->set_capture_format(codec, format);
	format = codec->image[OPTi93X_CAPT_FORMAT];

	count = snd_opti93x_get_count(format, count) - 1;
	snd_opti93x_out_image(codec, OPTi93X_CAPT_LWR_CNT, count);
	snd_opti93x_out_image(codec, OPTi93X_CAPT_UPR_CNT, count >> 8);

	spin_unlock_irqrestore(&codec->lock, flags);
	return 0;
}

static unsigned int snd_opti93x_playback_pointer(void *private_data,
						 snd_pcm_subchn_t *subchn)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);

	if (!(codec->image[OPTi93X_IFACE_CONF] & OPTi93X_PLAYBACK_ENABLE))
		return 0;

	return codec->p_dma_size - snd_dma_residue(codec->dma1);
}

static unsigned int snd_opti93x_capture_pointer(void *private_data,
						snd_pcm_subchn_t *subchn)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	
	if (!(codec->image[OPTi93X_IFACE_CONF] & OPTi93X_CAPTURE_ENABLE))
		return 0;

	return codec->c_dma_size - snd_dma_residue(codec->dma2);
}


static void snd_opti93x_overrange(opti93x_t *codec)
{
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	if (snd_opti93x_in(codec, OPTi93X_ERR_INIT) & (0x08 | 0x02))
		codec->capture_subchn->runtime->overrange++;

	spin_unlock_irqrestore(&codec->lock, flags);
}

void snd_opti93x_interrupt(snd_pcm_t *pcm, unsigned char status)
{
	opti93x_t *codec;

	codec = snd_magic_cast(opti93x_t, pcm->private_data, );
	if (codec == NULL)
		return;

	status = snd_opti9xx_read(codec->chip, OPTi9XX_MC_REG(11));
	if ((status & OPTi93X_IRQ_PLAYBACK) && codec->playback_subchn)
		snd_pcm_transfer_done(codec->playback_subchn);
	if ((status & OPTi93X_IRQ_CAPTURE) && codec->capture_subchn) {
		snd_opti93x_overrange(codec);
		snd_pcm_transfer_done(codec->capture_subchn);
	}

	spin_lock(&codec->lock);
	outb(0x00, OPTi93X_PORT(codec, STATUS));
	spin_unlock(&codec->lock);
}


static snd_pcm_hardware_t snd_opti93x_playback = {
	/* flags */
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_MMAP_VALID,
	/* formats */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_A_LAW | SND_PCM_FMT_IMA_ADPCM |
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE | SND_PCM_FMT_S16_BE,
	SND_PCM_RATE_KNOT | SND_PCM_RATE_8000_48000,
	5512,			/* min rate */
	48000,			/* max rate */
	1,			/* min voices */
	2,			/* max voices */
	64,			/* min fragment */
	(128*1024),		/* max fragment */
	3,			/* frag. align */
	0,			/* FIFO size */
	4,			/* transfer block size */
	snd_opti93x_playback_ioctl,
	snd_opti93x_playback_prepare,
	snd_opti93x_playback_trigger,
	snd_opti93x_playback_pointer,
};

static snd_pcm_hardware_t snd_opti93x_capture = {
	/* flags */
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_MMAP_VALID,
	/* formats */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_A_LAW | SND_PCM_FMT_IMA_ADPCM |
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE | SND_PCM_FMT_S16_BE,
	SND_PCM_RATE_8000_48000,
	5512,			/* min rate */
	48000,			/* max rate */
	1,			/* min voices */
	2,			/* max voices */
	64,			/* min fragment */
	(128*1024),		/* max fragment */
	3,			/* frag. align */
	0,			/* FIFO size */
	4,			/* transfer block size */
	snd_opti93x_capture_ioctl,
	snd_opti93x_capture_prepare,
	snd_opti93x_capture_trigger,
	snd_opti93x_capture_pointer,
};

static int snd_opti93x_playback_open(void *private_data,
				     snd_pcm_subchn_t *subchn)
{
	int error;
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	if (!(runtime->hw = (snd_pcm_hardware_t *) snd_kmalloc(
			sizeof(*runtime->hw), GFP_KERNEL)))
		return -ENOMEM;
	memcpy(runtime->hw, &snd_opti93x_playback, sizeof(*runtime->hw));
	runtime->hw_free = _snd_kfree;

	snd_pcm_set_sync(subchn);
	snd_pcm_set_mixer(subchn, codec->mixer->device, codec->me_playback);

	if (!(error = snd_pcm_dma_alloc(subchn, codec->dma1ptr,
			"OPTi 82C93X (playback)"))) {
		snd_opti93x_open(codec, OPTi93X_MODE_PLAY);
		codec->playback_subchn = subchn;
	}
	return error;
}

static int snd_opti93x_capture_open(void *private_data,
				    snd_pcm_subchn_t *subchn)
{
	int error;
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	if (!(runtime->hw = (snd_pcm_hardware_t *) snd_kmalloc(
			sizeof(*runtime->hw), GFP_KERNEL)))
		return -ENOMEM;
	memcpy(runtime->hw, &snd_opti93x_capture, sizeof(*runtime->hw));
	runtime->hw_free = _snd_kfree;

	snd_pcm_set_sync(subchn);
	snd_pcm_set_mixer(subchn, codec->mixer->device, codec->me_capture);

	if (!(error = snd_pcm_dma_alloc(subchn, codec->dma2ptr,
			"OPTi 82C93X (capture)"))) {
		snd_opti93x_open(codec, OPTi93X_MODE_CAPTURE);
		codec->capture_subchn = subchn;
	}
	return error;
}

static int snd_opti93x_playback_close(void *private_data,
				      snd_pcm_subchn_t *subchn)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);

	codec->playback_subchn = NULL;
	snd_opti93x_close(codec, OPTi93X_MODE_PLAY);
	snd_pcm_dma_free(subchn);
	return 0;
}

static int snd_opti93x_capture_close(void *private_data,
				     snd_pcm_subchn_t *subchn)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, -ENXIO);

	codec->capture_subchn = NULL;
	snd_opti93x_close(codec, OPTi93X_MODE_CAPTURE);
	snd_pcm_dma_free(subchn);
	return 0;
}


static void snd_opti93x_init(opti93x_t *codec)
{
	unsigned long flags;
	int i;

	spin_lock_irqsave(&codec->lock, flags);
	snd_opti93x_mce_up(codec);

	for (i = 0; i < 32; i++)
		snd_opti93x_out_image(codec, i, snd_opti93x_default_image[i]);

	snd_opti93x_mce_down(codec);
	spin_unlock_irqrestore(&codec->lock, flags);
}

static int snd_opti93x_probe(snd_pcm_t *pcm)
{
	opti93x_t *codec;
	unsigned long flags;
	unsigned char val;

	codec = snd_magic_cast(opti93x_t, pcm->private_data, -ENXIO);

	spin_lock_irqsave(&codec->lock, flags);
	val = snd_opti93x_in(codec, OPTi93X_ID) & 0x0f;
	spin_unlock_irqrestore(&codec->lock, flags);

	return (val == 0x0a) ? 0 : -ENODEV;
}

static void snd_opti93x_free(void *private_data)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, private_data, );
	snd_magic_kfree(codec);
}

int snd_opti93x_new_pcm(snd_card_t *card, int device, opti9xx_t *chip,
			snd_dma_t *dma1ptr, snd_dma_t *dma2ptr,
			snd_pcm_t **rpcm)
{
	int error;
	snd_pcm_t *pcm;
	opti93x_t *codec;
	char *str;

	*rpcm = NULL;
	if ((error = snd_pcm_new(card, "OPTi 82C93X", device, 1, 1, &pcm)))
		return error;

	codec = snd_magic_kcalloc(opti93x_t, 0, GFP_KERNEL);
	if (codec == NULL) {
		snd_device_free(card, pcm);
		return -ENOMEM;
	}

	codec->pcm = pcm;
	codec->card = pcm->card;
	codec->port = chip->wss_base + 4;
	codec->irq = chip->irq;
	codec->dma1 = chip->dma1;
	codec->dma2 = chip->dma2;
	codec->dma1ptr = dma1ptr;
	codec->dma2ptr = dma2ptr;

	spin_lock_init(&codec->lock);
	codec->hardware = chip->hardware;
	codec->chip = chip;

	codec->set_rate = snd_opti93x_xrate;
	codec->set_playback_format = snd_opti93x_playback_format;
	codec->set_capture_format = snd_opti93x_capture_format;

	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].open = snd_opti93x_playback_open;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].close = snd_opti93x_playback_close;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_opti93x_capture_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_opti93x_capture_close;

	pcm->private_data = codec;
	pcm->private_free = snd_opti93x_free;
	pcm->info_flags = SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_CAPTURE |
		SND_PCM_INFO_DUPLEX | SND_PCM_INFO_DUPLEX_RATE;

	if ((error = snd_opti93x_probe(pcm))) {
		snd_device_free(card, pcm);
		return error;
	}

	switch (codec->hardware) {
	case OPTi9XX_HW_82C930:
		str = "82C930";
		break;
	case OPTi9XX_HW_82C931:
		str = "82C931";
		break;
	case OPTi9XX_HW_82C933:
		str = "82C933";
		break;
	default:
		snd_device_free(card, pcm);
		return -ENODEV;
	}
	strcpy(pcm->name, str);

	snd_opti93x_init(codec);

	*rpcm = pcm;
	return 0;
}


static int snd_opti93x_mixer_get_recsrc(opti93x_t *codec,
					snd_kmixer_element_t *element)
{
	if (element == codec->me_mux_line)
		return OPTi93X_MIXOUT_LINE;
	if (element == codec->me_mux_cd)
		return OPTi93X_MIXOUT_CD;
	if (element == codec->me_mux_mic)
		return OPTi93X_MIXOUT_MIC;
	if (element == codec->me_mux_mix)
		return OPTi93X_MIXOUT_MIXER;
	return -EINVAL;
}

static snd_kmixer_element_t *snd_opti93x_mixer_get_recsrc_element(
						opti93x_t *codec, int value)
{
	value &= OPTi93X_MIXOUT_MIXER;
	switch (value) {
	case OPTi93X_MIXOUT_LINE:
		return codec->me_mux_line;
	case OPTi93X_MIXOUT_CD:
		return codec->me_mux_cd;
	case OPTi93X_MIXOUT_MIC:
		return codec->me_mux_mic;
	case OPTi93X_MIXOUT_MIXER:
		return codec->me_mux_mix;
	}
	return NULL;
}

int snd_opti93x_mixer_mux(snd_kmixer_element_t *element, int w_flag,
			  snd_kmixer_element_t **elements)
{
	int change = 0, left, right, nleft, nright;
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	left = codec->image[OPTi93X_MIXOUT_LEFT] & OPTi93X_MIXOUT_MIXER;
	right = codec->image[OPTi93X_MIXOUT_RIGHT] & OPTi93X_MIXOUT_MIXER;

	if (w_flag) {
		nleft = snd_opti93x_mixer_get_recsrc(codec, elements[0]);
		nright = snd_opti93x_mixer_get_recsrc(codec, elements[1]);

		if (nleft < 0 || nright < 0)
			change = -EINVAL;
		else {
			snd_opti93x_out_mask(codec, OPTi93X_MIXOUT_LEFT,
 				OPTi93X_MIXOUT_MIXER, (unsigned char) nleft);
			snd_opti93x_out_mask(codec, OPTi93X_MIXOUT_RIGHT,
 				OPTi93X_MIXOUT_MIXER, (unsigned char) nright);

			change = (nleft != left) || (nright != right);
		}
	}
	else {
		elements[0] = snd_opti93x_mixer_get_recsrc_element(codec, left);
		elements[1] = snd_opti93x_mixer_get_recsrc_element(codec,
			right);
	}

	spin_unlock_irqrestore(&codec->lock, flags);
	return change;
}

int snd_opti93x_mixer_stereo_volume(snd_kmixer_element_t *element,
				    int w_flag, int *voices,
				    int max, int invert, int shift,
				    unsigned char left_reg,
				    unsigned char right_reg)
{
	int change = 0, left, right;
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	left = (codec->image[left_reg] >> shift) & max;
	right = (codec->image[right_reg] >> shift) & max;

	if (!w_flag) {
		voices[0] = invert ? (max - left) : left;
		voices[1] = invert ? (max - right) : right;
	}
	else {
		change = invert ?
			(max - left != voices[0]) || (max - right != voices[1])
			: (left != voices[0]) || (right != voices[1]);

		left = (invert ? (max - voices[0]) : voices[0]) << shift;
		right = (invert ? (max - voices[1]) : voices[1]) << shift;

		snd_opti93x_out_mask(codec, left_reg, max << shift, left);
		snd_opti93x_out_mask(codec, right_reg, max << shift, right);
	}

	spin_unlock_irqrestore(&codec->lock, flags);
	return change;	
}

int snd_opti93x_mixer_stereo_switch(snd_kmixer_element_t *element,
				    int w_flag,
				    unsigned int *bitmap,
				    int bit, int invert,
				    unsigned char left_reg,
				    unsigned char right_reg)
{
	int change = 0, left, right;
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	unsigned long flags;

	spin_lock_irqsave(&codec->lock, flags);

	left = (codec->image[left_reg] >> bit) & 1;
	right = (codec->image[right_reg] >> bit) & 1;

	if (!w_flag) {
		snd_mixer_set_bit(bitmap, 0, invert ? left ^ 1 : left);
		snd_mixer_set_bit(bitmap, 1, invert ? right ^ 1 : right);
	}
	else {
		if (invert)
			change = (left ^ 1) != snd_mixer_get_bit(bitmap, 0) ||
			         (right ^ 1) != snd_mixer_get_bit(bitmap, 1);
		else
			change = left != snd_mixer_get_bit(bitmap, 0) ||
			         right != snd_mixer_get_bit(bitmap, 1);

		left = snd_mixer_get_bit(bitmap, 0);
		right = snd_mixer_get_bit(bitmap, 1);
		if (invert) {
			left ^= 1;
			right ^= 1;
		}

		snd_opti93x_out_mask(codec, left_reg, 1 << bit, left << bit);
		snd_opti93x_out_mask(codec, right_reg, 1 << bit, right << bit);
	}

	spin_unlock_irqrestore(&codec->lock, flags);
	return change;	
}

int snd_opti93x_mixer_mic_switch(snd_kmixer_element_t *element, int w_flag,
				 unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		OPTi93X_MIC_LEFT_INPUT,
		OPTi93X_MIC_RIGHT_INPUT);
}

int snd_opti93x_mixer_mic_volume(snd_kmixer_element_t *element, int w_flag,
				 int *voices)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		15, 1, codec->hardware == OPTi9XX_HW_82C930 ? 0 : 1,
		OPTi93X_MIC_LEFT_INPUT,
		OPTi93X_MIC_RIGHT_INPUT);
}

int snd_opti93x_mixer_aux_switch(snd_kmixer_element_t *element, int w_flag,
				 unsigned int *bitmap)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		codec->hardware == OPTi9XX_HW_82C930 ?
			OPTi930_AUX_LEFT_INPUT : OPTi931_AUX_LEFT_INPUT,
		codec->hardware == OPTi9XX_HW_82C930 ?
			OPTi930_AUX_RIGHT_INPUT : OPTi931_AUX_RIGHT_INPUT);
}

int snd_opti93x_mixer_aux_volume(snd_kmixer_element_t *element, int w_flag,
				 int *voices)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		15, 1, 1,
		codec->hardware == OPTi9XX_HW_82C930 ?
			OPTi930_AUX_LEFT_INPUT : OPTi931_AUX_LEFT_INPUT,
		codec->hardware == OPTi9XX_HW_82C930 ?
			OPTi930_AUX_RIGHT_INPUT : OPTi931_AUX_RIGHT_INPUT);
}

int snd_opti93x_mixer_cd_switch(snd_kmixer_element_t *element, int w_flag,
				unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		OPTi93X_CD_LEFT_INPUT,
		OPTi93X_CD_RIGHT_INPUT);
}

int snd_opti93x_mixer_cd_volume(snd_kmixer_element_t *element, int w_flag,
				int *voices)
{
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		15, 1, 1,
		OPTi93X_CD_LEFT_INPUT,
		OPTi93X_CD_RIGHT_INPUT);
}

int snd_opti931_mixer_fm_switch(snd_kmixer_element_t *element, int w_flag,
				unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		OPTi931_FM_LEFT_INPUT,
		OPTi931_FM_RIGHT_INPUT);
}

int snd_opti931_mixer_fm_volume(snd_kmixer_element_t *element, int w_flag,
				int *voices)
{
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		15, 1, 1,
		OPTi931_FM_LEFT_INPUT,
		OPTi931_FM_RIGHT_INPUT);
}

int snd_opti93x_mixer_line_switch(snd_kmixer_element_t *element, int w_flag,
				  unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		OPTi93X_LINE_LEFT_INPUT,
		OPTi93X_LINE_RIGHT_INPUT);
}

int snd_opti93x_mixer_line_volume(snd_kmixer_element_t *element, int w_flag,
				  int *voices)
{
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		15, 1, 1,
		OPTi93X_LINE_LEFT_INPUT,
		OPTi93X_LINE_RIGHT_INPUT);
}

int snd_opti93x_mixer_igain_volume(snd_kmixer_element_t *element, int w_flag,
				   int *voices)
{
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		15, 0, 0,
		OPTi93X_MIXOUT_LEFT,
		OPTi93X_MIXOUT_RIGHT);
}

int snd_opti93x_mixer_mgain_switch(snd_kmixer_element_t *element, int w_flag,
				   unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		5, 0,
		OPTi93X_MIXOUT_LEFT,
		OPTi93X_MIXOUT_RIGHT);
}

int snd_opti93x_mixer_dac_switch(snd_kmixer_element_t *element, int w_flag,
				 unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		OPTi93X_DAC_LEFT,
		OPTi93X_DAC_RIGHT);
}

int snd_opti93x_mixer_dac_volume(snd_kmixer_element_t *element, int w_flag,
				 int *voices)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		31, 1, codec->hardware == OPTi9XX_HW_82C930 ? 1 : 0,
		OPTi93X_DAC_LEFT,
		OPTi93X_DAC_RIGHT);
}

int snd_opti93x_mixer_out_switch(snd_kmixer_element_t *element, int w_flag,
				 unsigned int *bitmap)
{
	return snd_opti93x_mixer_stereo_switch(element,
		w_flag, bitmap,
		7, 1,
		OPTi93X_OUT_LEFT,
		OPTi93X_OUT_RIGHT);
}

int snd_opti93x_mixer_out_volume(snd_kmixer_element_t *element, int w_flag,
				 int *voices)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, element->private_data,
		-ENXIO);
	
	return snd_opti93x_mixer_stereo_volume(element,
		w_flag, voices,
		31, 1, codec->hardware == OPTi9XX_HW_82C930 ? 0 : 1,
		OPTi93X_OUT_LEFT,
		OPTi93X_OUT_RIGHT);
}

int snd_opti93x_mixer_group_ctrl(snd_kmixer_group_t *group,
				 snd_kmixer_file_t *file,
				 int w_flag,
				 snd_mixer_group_t *ugroup,
				 snd_mixer_volume1_control_t *volume1,
				 snd_kmixer_element_t *volume1_element,
				 int max,
				 snd_mixer_sw1_control_t *sw1,
				 snd_kmixer_element_t *sw1_element,
				 snd_kmixer_element_t *mux_in)
{
	unsigned int bitmap;
	int change = 0;
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);
	snd_kmixer_element_t *elements[2];
	int voices[2];

	if (!w_flag) {
		ugroup->caps = 0;
		ugroup->channels = SND_MIXER_CHN_MASK_STEREO;

		if (volume1) {
			ugroup->caps |= SND_MIXER_GRPCAP_VOLUME;
			volume1(volume1_element, 0, voices);
			ugroup->volume.names.front_left = voices[0];
			ugroup->volume.names.front_right = voices[1];
			ugroup->min = 0;
			ugroup->max = max;
		}

		if (sw1) {
			ugroup->caps |= SND_MIXER_GRPCAP_MUTE;
			sw1(sw1_element, 0, &bitmap);
			ugroup->mute = 0;
			if (!snd_mixer_get_bit(&bitmap, 0))
				ugroup->mute |= SND_MIXER_CHN_MASK_FRONT_LEFT;
			if (!snd_mixer_get_bit(&bitmap, 1))
				ugroup->mute |= SND_MIXER_CHN_MASK_FRONT_RIGHT;
		}

		if (mux_in) {
			ugroup->caps |= SND_MIXER_GRPCAP_CAPTURE |
				SND_MIXER_GRPCAP_EXCL_CAPTURE;
			ugroup->capture_group = 1;
			snd_opti93x_mixer_mux(codec->me_mux, 0, elements);
			ugroup->capture = 0;
			if (elements[0] == mux_in)
				ugroup->capture |=
					SND_MIXER_CHN_MASK_FRONT_LEFT;
			if (elements[1] == mux_in)
				ugroup->capture |=
					SND_MIXER_CHN_MASK_FRONT_RIGHT;
		}
	}
	else {
		if (volume1) {
			voices[0] = ugroup->volume.names.front_left & max;
			voices[1] = ugroup->volume.names.front_right & max;
			if (volume1(volume1_element, 1, voices) > 0) {
				snd_mixer_element_value_change(file,
					volume1_element, 0);
				change = 1;
			}
		}

		if (sw1) {
			bitmap = 0;
			if (!(ugroup->mute & SND_MIXER_CHN_MASK_FRONT_LEFT))
				snd_mixer_set_bit(&bitmap, 0, 1);
			if (!(ugroup->mute & SND_MIXER_CHN_MASK_FRONT_RIGHT))
				snd_mixer_set_bit(&bitmap, 1, 1);
			if (sw1(sw1_element, 1, &bitmap) > 0) {
				snd_mixer_element_value_change(file,
					sw1_element, 0);
				change = 1;
			}
		}

		if (mux_in) {
			snd_opti93x_mixer_mux(codec->me_mux, 0, elements);
			if (ugroup->capture & SND_MIXER_CHN_MASK_FRONT_LEFT)
				elements[0] = mux_in;
			if (ugroup->capture & SND_MIXER_CHN_MASK_FRONT_RIGHT)
				elements[1] = mux_in;
			if (snd_opti93x_mixer_mux(codec->me_mux, 1,
					elements) > 0) {
				snd_mixer_element_value_change_all_file(file,
					codec->me_mux, 0);
				change = 1;
			}
		}
	}
	return change;
}

static int snd_opti93x_mixer_group_mic(snd_kmixer_group_t *group,
				       snd_kmixer_file_t *file,
				       int w_flag,
				       snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_mic_volume,
		codec->me_vol_mic,
		15,
		snd_opti93x_mixer_mic_switch,
		codec->me_sw_mic,
		codec->me_mux_mic);
}

static int snd_opti93x_mixer_group_line(snd_kmixer_group_t *group,
					snd_kmixer_file_t *file,
					int w_flag,
					snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_line_volume,
		codec->me_vol_line,
		15,
		snd_opti93x_mixer_line_switch,
		codec->me_sw_line,
		codec->me_mux_line);
}

static int snd_opti93x_mixer_group_aux(snd_kmixer_group_t *group,
				       snd_kmixer_file_t *file,
				       int w_flag,
				       snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_aux_volume,
		codec->me_vol_aux,
		15,
		snd_opti93x_mixer_aux_switch,
		codec->me_sw_aux,
		NULL);
}

static int snd_opti93x_mixer_group_cd(snd_kmixer_group_t *group,
				      snd_kmixer_file_t *file,
				      int w_flag,
				      snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_cd_volume,
		codec->me_vol_cd,
		15,
		snd_opti93x_mixer_cd_switch,
		codec->me_sw_cd,
		codec->me_mux_cd);
}

static int snd_opti931_mixer_group_fm(snd_kmixer_group_t *group,
				      snd_kmixer_file_t *file,
				      int w_flag,
				      snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti931_mixer_fm_volume,
		codec->me_vol_fm,
		15,
		snd_opti931_mixer_fm_switch,
		codec->me_sw_fm,
		NULL);
}

static int snd_opti93x_mixer_group_igain(snd_kmixer_group_t *group,
					 snd_kmixer_file_t *file,
					 int w_flag,
					 snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_igain_volume,
		codec->me_vol_igain,
		15,
		NULL,
		NULL,
		NULL);
}

static int snd_opti93x_mixer_group_dac(snd_kmixer_group_t *group,
				       snd_kmixer_file_t *file,
				       int w_flag,
				       snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_dac_volume,
		codec->me_vol_dac,
		31,
		snd_opti93x_mixer_dac_switch,
		codec->me_sw_dac,
		codec->me_mux_mix);
}

static int snd_opti93x_mixer_group_out(snd_kmixer_group_t *group,
				       snd_kmixer_file_t *file,
				       int w_flag,
				       snd_mixer_group_t *ugroup)
{
	opti93x_t *codec = snd_magic_cast(opti93x_t, group->private_data,
		-ENXIO);

	return snd_opti93x_mixer_group_ctrl(group, file, w_flag, ugroup,
		snd_opti93x_mixer_out_volume,
		codec->me_vol_out,
		31,
		snd_opti93x_mixer_out_switch,
		codec->me_sw_out,
		NULL);
}

int snd_opti93x_new_mixer(snd_pcm_t *pcm, int device, snd_kmixer_t **rmixer)
{
	int error;
	opti93x_t *codec;
	snd_kmixer_t *mixer;
	snd_kmixer_group_t *group;
	static struct snd_mixer_element_volume1_range dac_range[2] = {
		{0, 31, -4650, 0},
		{0, 31, -4650, 0}
	};
	static struct snd_mixer_element_volume1_range mixout_range[2] = {
		{0, 15, 0, 2250},
		{0, 15, 0, 2250}
	};
	static struct snd_mixer_element_volume1_range mlca_range[2] = {
		{0, 15, -3300, 1200},
		{0, 15, -3300, 1200}
	};
	static struct snd_mixer_element_volume1_range out_range[2] = {
		{0, 31, -9300, 0},
		{0, 31, -9300, 0}
	};

	snd_debug_check(rmixer == NULL, -EINVAL);
	*rmixer = NULL;

	snd_debug_check(pcm == NULL || pcm->card == NULL, -EINVAL);
	codec = snd_magic_cast(opti93x_t, pcm->private_data, -ENXIO);
	if ((error = snd_mixer_new(pcm->card, pcm->id, device, &mixer)))
		return error;
	strcpy(mixer->name, pcm->name);

	if (!(codec->me_mux = snd_mixer_lib_mux1(mixer,
			SND_MIXER_ELEMENT_INPUT_MUX, 0, 0, 2,
			snd_opti93x_mixer_mux, codec)))
		goto __on_error;
	if (!(codec->me_accu = snd_mixer_lib_accu1(mixer,
			SND_MIXER_ELEMENT_OUTPUT_ACCU, 0, 0)))
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_accu, codec->me_mux))
		goto __on_error;
	codec->me_mux_mix = codec->me_accu;

	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_OUT_MASTER,
			0, SND_MIXER_OSS_VOLUME, snd_opti93x_mixer_group_out,
			codec)) == NULL)
		goto __on_error;
	if ((codec->me_out_master = snd_mixer_lib_io_stereo(mixer,
			SND_MIXER_OUT_MASTER, 0, SND_MIXER_ETYPE_OUTPUT,
			0)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_accu,
			codec->me_out_master) < 0)
		goto __on_error;
	if ((codec->me_vol_out = snd_mixer_lib_volume1(mixer, "Master Volume",
			0, 2, out_range, snd_opti93x_mixer_out_volume,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_out) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_out_master,
			codec->me_vol_out) < 0)
		goto __on_error;
	if ((codec->me_sw_out = snd_mixer_lib_sw1(mixer, "Master Switch",
			0, 2, snd_opti93x_mixer_out_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_out) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_out,
			codec->me_sw_out) < 0)
		goto __on_error;

	if ((codec->me_dig_accu = snd_mixer_lib_accu1(mixer,
			SND_MIXER_ELEMENT_DIGITAL_ACCU, 0, 0)) == NULL)
		goto __on_error;

	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_MIC,
			0, SND_MIXER_OSS_MIC, snd_opti93x_mixer_group_mic,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_mux) < 0)
		goto __on_error;
	if ((codec->me_in_mic = snd_mixer_lib_io_stereo(mixer,
			SND_MIXER_IN_MIC, 0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __on_error;
	if ((codec->me_sw_mgain = snd_mixer_lib_sw1(mixer, "MIC Gain Switch",
			0, 2, snd_opti93x_mixer_mgain_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_mgain) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_in_mic,
			codec->me_sw_mgain) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_mgain,
			codec->me_mux) < 0)
		goto __on_error;
	codec->me_mux_mic = codec->me_in_mic;
	if ((codec->me_vol_mic = snd_mixer_lib_volume1(mixer, "MIC Volume", 0,
			2, mlca_range, snd_opti93x_mixer_mic_volume,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_mic) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_in_mic,
			codec->me_vol_mic) < 0)
		goto __on_error;
	if ((codec->me_sw_mic = snd_mixer_lib_sw1(mixer, "MIC Switch",
			0, 2, snd_opti93x_mixer_mic_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_mic) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_mic,
			codec->me_sw_mic) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_mic,
			codec->me_accu) < 0)
		goto __on_error;

	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_LINE,
			0, SND_MIXER_OSS_LINE, snd_opti93x_mixer_group_line,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_mux) < 0)
		goto __on_error;
	if ((codec->me_in_line = snd_mixer_lib_io_stereo(mixer,
			SND_MIXER_IN_LINE, 0, SND_MIXER_ETYPE_INPUT,
			0)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer,
			codec->me_in_line, codec->me_mux) < 0)
		goto __on_error;
	codec->me_mux_line = codec->me_in_line;
	if ((codec->me_vol_line = snd_mixer_lib_volume1(mixer,
			"Line Input Volume", 0, 2, mlca_range,
			snd_opti93x_mixer_line_volume, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_line) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_in_line,
			codec->me_vol_line) < 0)
		goto __on_error;
	if ((codec->me_sw_line = snd_mixer_lib_sw1(mixer, "Line Input Switch",
			0, 2, snd_opti93x_mixer_line_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_line) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_line,
			codec->me_sw_line) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_line,
			codec->me_accu) < 0)
		goto __on_error;

	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_CD, 0,
			SND_MIXER_OSS_CD, snd_opti93x_mixer_group_cd,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_mux) < 0)
		goto __on_error;
	if ((codec->me_in_cd = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_CD,
			0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer,
			codec->me_in_cd, codec->me_mux) < 0)
		goto __on_error;
	codec->me_mux_cd = codec->me_in_cd;
	if ((codec->me_vol_cd = snd_mixer_lib_volume1(mixer, "CD Volume", 0,
			2, mlca_range, snd_opti93x_mixer_cd_volume,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_cd) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_in_cd,
			codec->me_vol_cd) < 0)
		goto __on_error;
	if ((codec->me_sw_cd = snd_mixer_lib_sw1(mixer, "CD Switch",
			0, 2, snd_opti93x_mixer_cd_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_cd) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_cd,
			codec->me_sw_cd) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_cd,
			codec->me_accu) < 0)
		goto __on_error;

	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_AUX, 0,
			SND_MIXER_OSS_LINE1, snd_opti93x_mixer_group_aux,
			codec)) == NULL)
		goto __on_error;
	if ((codec->me_in_aux = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_AUX,
			0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __on_error;
	if ((codec->me_vol_aux = snd_mixer_lib_volume1(mixer,
			"Aux Input Volume", 0, 2, mlca_range,
			snd_opti93x_mixer_aux_volume, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_aux) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_in_aux,
			codec->me_vol_aux) < 0)
		goto __on_error;
	if ((codec->me_sw_aux = snd_mixer_lib_sw1(mixer, "Aux Input Switch",
			0, 2, snd_opti93x_mixer_aux_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_aux) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_aux,
			codec->me_sw_aux) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_aux,
			codec->me_accu) < 0)
		goto __on_error;

	if (codec->hardware == OPTi9XX_HW_82C930)
		goto __skip_fm;
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_FM, 0,
			SND_MIXER_OSS_SYNTH, snd_opti931_mixer_group_fm,
			codec)) == NULL)
		goto __on_error;
	if ((codec->me_in_fm = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_FM,
			0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __on_error;
	if ((codec->me_vol_fm = snd_mixer_lib_volume1(mixer, "FM Input Volume",
			0, 2, mlca_range, snd_opti931_mixer_fm_volume,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_fm) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_in_fm,
			codec->me_vol_fm) < 0)
		goto __on_error;
	if ((codec->me_sw_fm = snd_mixer_lib_sw1(mixer, "FM Input Switch", 0,
			2, snd_opti931_mixer_fm_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_fm) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_fm,
			codec->me_sw_fm) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_fm,
			codec->me_accu) < 0)
		goto __on_error;

__skip_fm:
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_GRP_IGAIN,
			0, SND_MIXER_OSS_IGAIN, snd_opti93x_mixer_group_igain,
			codec)) == NULL)
		goto __on_error;
	if ((codec->me_vol_igain = snd_mixer_lib_volume1(mixer,
			"Input Gain Volume", 0, 2, mixout_range,
			 snd_opti93x_mixer_igain_volume, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_igain) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_mux,
			codec->me_vol_igain) < 0)
		goto __on_error;

	if ((codec->me_adc = snd_mixer_lib_converter(mixer,
			SND_MIXER_ELEMENT_ADC, 0, SND_MIXER_ETYPE_ADC,
			16)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_igain,
			codec->me_adc) < 0)
		goto __on_error;

	if ((codec->me_capture = snd_mixer_lib_pcm1(mixer,
			SND_MIXER_ELEMENT_CAPTURE, 0, SND_MIXER_ETYPE_CAPTURE1,
			1, &pcm->device)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_adc,
			codec->me_capture) < 0)
		goto __on_error;

	if ((codec->me_playback = snd_mixer_lib_pcm1(mixer,
			SND_MIXER_ELEMENT_PLAYBACK, 0,
			SND_MIXER_ETYPE_PLAYBACK1, 1, &pcm->device)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_playback,
			codec->me_dig_accu) < 0)
		goto __on_error;

	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_PCM, 0,
			SND_MIXER_OSS_PCM, snd_opti93x_mixer_group_dac,
			codec)) == NULL)
		goto __on_error;
	if ((codec->me_dac = snd_mixer_lib_converter(mixer,
			SND_MIXER_ELEMENT_DAC, 0, SND_MIXER_ETYPE_DAC,
			16)) == NULL)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_dig_accu,
			codec->me_dac) < 0)
		goto __on_error;
	if ((codec->me_vol_dac = snd_mixer_lib_volume1(mixer, "PCM Volume",
			0, 2, dac_range, snd_opti93x_mixer_dac_volume,
			codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_vol_dac) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_dac,
			codec->me_vol_dac) < 0)
		goto __on_error;
	if ((codec->me_sw_dac = snd_mixer_lib_sw1(mixer, "PCM Switch", 0,
			2, snd_opti93x_mixer_dac_switch, codec)) == NULL)
		goto __on_error;
	if (snd_mixer_group_element_add(mixer, group, codec->me_sw_dac) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_vol_dac,
			codec->me_sw_dac) < 0)
		goto __on_error;
	if (snd_mixer_element_route_add(mixer, codec->me_sw_dac,
			codec->me_accu) < 0)
		goto __on_error;

	mixer->private_data = codec;
	*rmixer = codec->mixer = mixer;
	return 0;

      __on_error:
      	snd_device_free(pcm->card, mixer);
      	return -ENOMEM;
}

EXPORT_SYMBOL(snd_opti93x_interrupt);
EXPORT_SYMBOL(snd_opti93x_new_pcm);
EXPORT_SYMBOL(snd_opti93x_new_mixer);

static int __init alsa_opti93x_init(void)
{
	return 0;
}

static void __exit alsa_opti93x_exit(void)
{
}

module_init(alsa_opti93x_init)
module_exit(alsa_opti93x_exit)
