/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>,
 *                   Thomas Sailer <sailer@ife.ee.ethz.ch>
 *  Routines for control of S3 SonicVibes (86c617) chip
 *
 *  BUGS:
 *    It looks like 86c617 rev 3 doesn't supports DDMA buffers above 16MB?
 *    Driver sometimes hangs... Nobody knows why at this moment...
 *
 *  TODO:
 *    ---
 *
 *   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/info.h"
#include "../../include/control.h"
#include "../../include/sonicvibes.h"

/*
 *  common I/O routines
 */

static inline void snd_sonicvibes_setdmaa(sonicvibes_t * sonic,
					  unsigned int addr,
					  unsigned int count)
{
	count--;
	outl(addr, sonic->dmaa_port + SV_DMA_ADDR0);
	outl(count, sonic->dmaa_port + SV_DMA_COUNT0);
	outb(0x18, sonic->dmaa_port + SV_DMA_MODE);
#if 0
	printk("program dmaa: addr = 0x%x, paddr = 0x%x\n", addr, inl(sonic->dmaa_port + SV_DMA_ADDR0));
#endif
}

static inline void snd_sonicvibes_setdmac(sonicvibes_t * sonic,
					  unsigned int addr,
					  unsigned int count)
{
	/* note: dmac is working in word mode!!! */
	count >>= 1;
	count--;
	outl(addr, sonic->dmac_port + SV_DMA_ADDR0);
	outl(count, sonic->dmac_port + SV_DMA_COUNT0);
	outb(0x14, sonic->dmac_port + SV_DMA_MODE);
#if 0
	printk("program dmac: addr = 0x%x, paddr = 0x%x\n", addr, inl(sonic->dmac_port + SV_DMA_ADDR0));
#endif
}

static inline unsigned int snd_sonicvibes_getdmaa(sonicvibes_t * sonic)
{
	return (inl(sonic->dmaa_port + SV_DMA_COUNT0) & 0xffffff) + 1;
}

static inline unsigned int snd_sonicvibes_getdmac(sonicvibes_t * sonic)
{
	/* note: dmac is working in word mode!!! */
	return ((inl(sonic->dmac_port + SV_DMA_COUNT0) & 0xffffff) + 1) << 1;
}

static void snd_sonicvibes_out1(sonicvibes_t * sonic,
				unsigned char reg,
				unsigned char value)
{
	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	outb(value, SV_REG(sonic, DATA));
	snd_delay(1);
}

static void snd_sonicvibes_out(sonicvibes_t * sonic,
			       unsigned char reg,
			       unsigned char value)
{
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	outb(value, SV_REG(sonic, DATA));
	snd_delay(1);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
}

static unsigned char snd_sonicvibes_in1(sonicvibes_t * sonic, unsigned char reg)
{
	unsigned char value;

	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	value = inb(SV_REG(sonic, DATA));
	snd_delay(1);
	return value;
}

static unsigned char snd_sonicvibes_in(sonicvibes_t * sonic, unsigned char reg)
{
	unsigned long flags;
	unsigned char value;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	value = inb(SV_REG(sonic, DATA));
	snd_delay(1);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return value;
}

static void snd_sonicvibes_outm1(sonicvibes_t * sonic, unsigned char reg,
                                 unsigned char mask, unsigned char value)
{
	unsigned char prev;

	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	prev = inb(SV_REG(sonic, DATA));
	snd_delay(1);
	outb((prev & mask) | value, SV_REG(sonic, DATA));
	snd_delay(1);
#if 0
	printk("outm: 0x%x, prev = 0x%x, mask = 0x%x, value = 0x%x, final = 0x%x\n", reg, prev, mask, value, (prev & mask) | value);
#endif
}

#ifdef CONFIG_SND_DEBUG
void snd_sonicvibes_debug(sonicvibes_t * sonic)
{
	printk("SV REGS:          INDEX = 0x%02x  ", inb(SV_REG(sonic, INDEX)));
	printk("                 STATUS = 0x%02x\n", inb(SV_REG(sonic, STATUS)));
	printk("  0x00: left input      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x00));
	printk("  0x20: synth rate low  = 0x%02x\n", snd_sonicvibes_in(sonic, 0x20));
	printk("  0x01: right input     = 0x%02x  ", snd_sonicvibes_in(sonic, 0x01));
	printk("  0x21: synth rate high = 0x%02x\n", snd_sonicvibes_in(sonic, 0x21));
	printk("  0x02: left AUX1       = 0x%02x  ", snd_sonicvibes_in(sonic, 0x02));
	printk("  0x22: ADC clock       = 0x%02x\n", snd_sonicvibes_in(sonic, 0x22));
	printk("  0x03: right AUX1      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x03));
	printk("  0x23: ADC alt rate    = 0x%02x\n", snd_sonicvibes_in(sonic, 0x23));
	printk("  0x04: left CD         = 0x%02x  ", snd_sonicvibes_in(sonic, 0x04));
	printk("  0x24: ADC pll M       = 0x%02x\n", snd_sonicvibes_in(sonic, 0x24));
	printk("  0x05: right CD        = 0x%02x  ", snd_sonicvibes_in(sonic, 0x05));
	printk("  0x25: ADC pll N       = 0x%02x\n", snd_sonicvibes_in(sonic, 0x25));
	printk("  0x06: left line       = 0x%02x  ", snd_sonicvibes_in(sonic, 0x06));
	printk("  0x26: Synth pll M     = 0x%02x\n", snd_sonicvibes_in(sonic, 0x26));
	printk("  0x07: right line      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x07));
	printk("  0x27: Synth pll N     = 0x%02x\n", snd_sonicvibes_in(sonic, 0x27));
	printk("  0x08: MIC             = 0x%02x  ", snd_sonicvibes_in(sonic, 0x08));
	printk("  0x28: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x28));
	printk("  0x09: Game port       = 0x%02x  ", snd_sonicvibes_in(sonic, 0x09));
	printk("  0x29: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x29));
	printk("  0x0a: left synth      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x0a));
	printk("  0x2a: MPU401          = 0x%02x\n", snd_sonicvibes_in(sonic, 0x2a));
	printk("  0x0b: right synth     = 0x%02x  ", snd_sonicvibes_in(sonic, 0x0b));
	printk("  0x2b: drive ctrl      = 0x%02x\n", snd_sonicvibes_in(sonic, 0x2b));
	printk("  0x0c: left AUX2       = 0x%02x  ", snd_sonicvibes_in(sonic, 0x0c));
	printk("  0x2c: SRS space       = 0x%02x\n", snd_sonicvibes_in(sonic, 0x2c));
	printk("  0x0d: right AUX2      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x0d));
	printk("  0x2d: SRS center      = 0x%02x\n", snd_sonicvibes_in(sonic, 0x2d));
	printk("  0x0e: left analog     = 0x%02x  ", snd_sonicvibes_in(sonic, 0x0e));
	printk("  0x2e: wave source     = 0x%02x\n", snd_sonicvibes_in(sonic, 0x2e));
	printk("  0x0f: right analog    = 0x%02x  ", snd_sonicvibes_in(sonic, 0x0f));
	printk("  0x2f: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x2f));
	printk("  0x10: left PCM        = 0x%02x  ", snd_sonicvibes_in(sonic, 0x10));
	printk("  0x30: analog power    = 0x%02x\n", snd_sonicvibes_in(sonic, 0x30));
	printk("  0x11: right PCM       = 0x%02x  ", snd_sonicvibes_in(sonic, 0x11));
	printk("  0x31: analog power    = 0x%02x\n", snd_sonicvibes_in(sonic, 0x31));
	printk("  0x12: DMA data format = 0x%02x  ", snd_sonicvibes_in(sonic, 0x12));
	printk("  0x32: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x32));
	printk("  0x13: P/C enable      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x13));
	printk("  0x33: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x33));
	printk("  0x14: U/D button      = 0x%02x  ", snd_sonicvibes_in(sonic, 0x14));
	printk("  0x34: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x34));
	printk("  0x15: revision        = 0x%02x  ", snd_sonicvibes_in(sonic, 0x15));
	printk("  0x35: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x35));
	printk("  0x16: ADC output ctrl = 0x%02x  ", snd_sonicvibes_in(sonic, 0x16));
	printk("  0x36: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x36));
	printk("  0x17: ---             = 0x%02x  ", snd_sonicvibes_in(sonic, 0x17));
	printk("  0x37: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x37));
	printk("  0x18: DMA A upper cnt = 0x%02x  ", snd_sonicvibes_in(sonic, 0x18));
	printk("  0x38: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x38));
	printk("  0x19: DMA A lower cnt = 0x%02x  ", snd_sonicvibes_in(sonic, 0x19));
	printk("  0x39: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x39));
	printk("  0x1a: ---             = 0x%02x  ", snd_sonicvibes_in(sonic, 0x1a));
	printk("  0x3a: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x3a));
	printk("  0x1b: ---             = 0x%02x  ", snd_sonicvibes_in(sonic, 0x1b));
	printk("  0x3b: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x3b));
	printk("  0x1c: DMA C upper cnt = 0x%02x  ", snd_sonicvibes_in(sonic, 0x1c));
	printk("  0x3c: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x3c));
	printk("  0x1d: DMA C upper cnt = 0x%02x  ", snd_sonicvibes_in(sonic, 0x1d));
	printk("  0x3d: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x3d));
	printk("  0x1e: PCM rate low    = 0x%02x  ", snd_sonicvibes_in(sonic, 0x1e));
	printk("  0x3e: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x3e));
	printk("  0x1f: PCM rate high   = 0x%02x  ", snd_sonicvibes_in(sonic, 0x1f));
	printk("  0x3f: ---             = 0x%02x\n", snd_sonicvibes_in(sonic, 0x3f));
}

#endif

static void snd_sonicvibes_setfmt(sonicvibes_t * sonic,
                                  unsigned char mask,
                                  unsigned char value)
{
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	outb(SV_MCE | SV_IREG_DMA_DATA_FMT, SV_REG(sonic, INDEX));
	if (mask) {
		sonic->format = inb(SV_REG(sonic, DATA));
		snd_delay(1);
	}
	sonic->format = (sonic->format & mask) | value;
	outb(sonic->format, SV_REG(sonic, DATA));
	snd_delay(1);
	outb(0, SV_REG(sonic, INDEX));
	snd_delay(1);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
}

static unsigned int snd_sonicvibes_setpll(sonicvibes_t * sonic,
                                          unsigned char reg,
                                          unsigned int rate, int set)
{
	unsigned long flags;
	unsigned char r, m = 0, n = 0;
	unsigned int xm, xn, xr, xd, metric = ~0U;

	if (rate < 625000 / SV_ADCMULT)
		rate = 625000 / SV_ADCMULT;
	if (rate > 150000000 / SV_ADCMULT)
		rate = 150000000 / SV_ADCMULT;
	/* slight violation of specs, needed for continuous sampling rates */
	for (r = 0; rate < 75000000 / SV_ADCMULT; r += 0x20, rate <<= 1);
	for (xn = 3; xn < 33; xn++)	/* 35 */
		for (xm = 3; xm < 257; xm++) {
			xr = ((SV_REFFREQUENCY / SV_ADCMULT) * xm) / xn;
			xd = abs((signed) (xr - rate));
			if (xd < metric) {
				metric = xd;
				m = xm - 2;
				n = xn - 2;
			}
		}
#if 0
	printk("metric = %i, xm = %i, xn = %i\n", metric, xm, xn);
	printk("pll set - reg = 0x%x, m = 0x%x, r = 0x%x, n = 0x%x\n", reg, m, r, n);
#endif
	if (set) {
		spin_lock_irqsave(&sonic->reg_lock, flags);
		snd_sonicvibes_out1(sonic, reg, m);
		snd_sonicvibes_out1(sonic, reg + 1, r | n);
		spin_unlock_irqrestore(&sonic->reg_lock, flags);
	}
	return (SV_REFFREQUENCY / SV_ADCMULT * (m + 2) / (n + 2)) >> ((r >> 5) & 7);
}

static unsigned int snd_sonicvibes_set_dac_rate(sonicvibes_t * sonic,
						unsigned int rate, int set)
{
	unsigned div;
	unsigned long flags;

	if (rate > 48000)
		rate = 48000;
	if (rate < 4000)
		rate = 4000;
	div = (rate * 65536 + SV_FULLRATE / 2) / SV_FULLRATE;
	if (div > 65535)
		div = 65535;
	if (set) {
		spin_lock_irqsave(&sonic->reg_lock, flags);
		snd_sonicvibes_out1(sonic, SV_IREG_PCM_RATE_HIGH, div >> 8);
		snd_sonicvibes_out1(sonic, SV_IREG_PCM_RATE_LOW, div);
		spin_unlock_irqrestore(&sonic->reg_lock, flags);
	}
	return (div * SV_FULLRATE + 32768) / 65536;
}

static unsigned int snd_sonicvibes_set_adc_rate(sonicvibes_t * sonic,
						unsigned int rate, int set)
{
	unsigned long flags;
	unsigned rate1, rate2, div;

	if (rate > 48000)
		rate = 48000;
	if (rate < 4000)
		rate = 4000;
	rate1 = snd_sonicvibes_setpll(sonic, SV_IREG_ADC_PLL, rate, set);
	div = (48000 + rate / 2) / rate;
	if (div > 8)
		div = 8;
	rate2 = (48000 + div / 2) / div;
	if (set) {
		spin_lock_irqsave(&sonic->reg_lock, flags);
		snd_sonicvibes_out1(sonic, SV_IREG_ADC_ALT_RATE, (div - 1) << 4);
		if (abs((signed) (rate - rate2)) <= abs((signed) (rate - rate1))) {
			snd_sonicvibes_out1(sonic, SV_IREG_ADC_CLOCK, 0x10);
			rate1 = rate2;
		} else {
			snd_sonicvibes_out1(sonic, SV_IREG_ADC_CLOCK, 0x00);
		}
		spin_unlock_irqrestore(&sonic->reg_lock, flags);
	} else {
		if (abs((signed) (rate - rate2)) <= abs((signed) (rate - rate1)))
			rate1 = rate2;
	}
	return rate1;
}

static void snd_sonicvibes_trigger(sonicvibes_t * sonic, int what, int up)
{
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	if (up) {
		if (!(sonic->enable & what)) {
			sonic->enable |= what;
			snd_sonicvibes_out1(sonic, SV_IREG_PC_ENABLE, sonic->enable);
		}
	} else {
		if (sonic->enable & what) {
			sonic->enable &= ~what;
			snd_sonicvibes_out1(sonic, SV_IREG_PC_ENABLE, sonic->enable);
		}
	}
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
}

/*
 *  PCM part
 */

static int snd_sonicvibes_playback_ioctl(void *private_data,
					 snd_pcm_subchn_t * subchn,
					 unsigned int cmd,
					 unsigned long *arg)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		subchn1->real_rate = snd_sonicvibes_set_dac_rate(sonic, subchn1->rate, 0);
		return 0;
	}
	return -ENXIO;
}

static int snd_sonicvibes_capture_ioctl(void *private_data,
				        snd_pcm_subchn_t * subchn,
				        unsigned int cmd,
				        unsigned long *arg)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		subchn1->real_rate = snd_sonicvibes_set_adc_rate(sonic, subchn1->rate, 0);
		return 0;
	}
	return -ENXIO;
}

static void snd_sonicvibes_playback_trigger(void *private_data,
					    snd_pcm_subchn_t * subchn,
					    int up)
{
	snd_sonicvibes_trigger((sonicvibes_t *) private_data, 1, up);
}

static void snd_sonicvibes_capture_trigger(void *private_data,
					   snd_pcm_subchn_t * subchn,
					   int up)
{
	snd_sonicvibes_trigger((sonicvibes_t *) private_data, 2, up);
}

static void snd_sonicvibes_playback_prepare(void *private_data,
					    snd_pcm_subchn_t * subchn,
					    unsigned char *buffer,
					    unsigned int size,
					    unsigned int offset,
					    unsigned int count)
{
	unsigned long flags;
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	unsigned char fmt = 0;

	count--;
	if (subchn1->voices > 1)
		fmt |= 1;
	if (subchn1->mode & SND_PCM1_MODE_16)
		fmt |= 2;
	snd_sonicvibes_setfmt(sonic, ~3, fmt);
	snd_sonicvibes_set_dac_rate(sonic, subchn1->real_rate, 1);
	spin_lock_irqsave(&sonic->reg_lock, flags);
	snd_sonicvibes_setdmaa(sonic, virt_to_bus(buffer), size);
	snd_sonicvibes_out1(sonic, SV_IREG_DMA_A_UPPER, count >> 8);
	snd_sonicvibes_out1(sonic, SV_IREG_DMA_A_LOWER, count);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
}

static void snd_sonicvibes_capture_prepare(void *private_data,
					   snd_pcm_subchn_t * subchn,
					   unsigned char *buffer,
					   unsigned int size,
					   unsigned int offset,
					   unsigned int count)
{
	unsigned long flags;
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	unsigned char fmt = 0;

	count >>= 1;
	count--;
	if (subchn1->voices > 1)
		fmt |= 0x10;
	if (subchn1->mode & SND_PCM1_MODE_16)
		fmt |= 0x20;
	snd_sonicvibes_setfmt(sonic, ~0x30, fmt);
	snd_sonicvibes_set_adc_rate(sonic, subchn1->real_rate, 1);
	spin_lock_irqsave(&sonic->reg_lock, flags);
	snd_sonicvibes_setdmac(sonic, virt_to_bus(buffer), size);
	snd_sonicvibes_out1(sonic, SV_IREG_DMA_C_UPPER, count >> 8);
	snd_sonicvibes_out1(sonic, SV_IREG_DMA_C_LOWER, count);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
}

static int snd_sonicvibes_playback_open(void *private_data,
					snd_pcm_subchn_t * subchn)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(subchn,
				      sonic->dma1ptr,
				      "S3 SonicVibes (playback)")) < 0)
		return err;
	sonic->mode |= SV_MODE_PLAY;
	sonic->playback_subchn = subchn;
	sonic->playback_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	return 0;
}

static int snd_sonicvibes_capture_open(void *private_data,
				       snd_pcm_subchn_t * subchn)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(subchn,
				      sonic->dma2ptr,
				      "S3 SonicVibes (capture)")) < 0)
		return err;
	sonic->mode |= SV_MODE_CAPTURE;
	sonic->capture_subchn = subchn;
	sonic->capture_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	return 0;
}

static void snd_sonicvibes_playback_close(void *private_data,
					  snd_pcm_subchn_t * subchn)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;

	sonic->playback_subchn = NULL;
	sonic->playback_subchn1 = NULL;
	snd_pcm1_dma_free(subchn);
	sonic->mode &= ~SV_MODE_PLAY;
}

static void snd_sonicvibes_capture_close(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;

	sonic->capture_subchn = NULL;
	sonic->capture_subchn1 = NULL;
	snd_pcm1_dma_free(subchn);
	sonic->mode &= ~SV_MODE_CAPTURE;
}

static unsigned int snd_sonicvibes_playback_pointer(void *private_data,
						    snd_pcm_subchn_t * subchn,
                                                    unsigned int used_size)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;

	if (!(sonic->enable & 1))
		return 0;
	return used_size - snd_sonicvibes_getdmaa(sonic);
}

static unsigned int snd_sonicvibes_capture_pointer(void *private_data,
						   snd_pcm_subchn_t * subchn,
						   unsigned int used_size)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;

	if (!(sonic->enable & 2))
		return 0;
	return used_size - snd_sonicvibes_getdmac(sonic);
}

static struct snd_stru_pcm1_hardware snd_sonicvibes_playback =
{
	SND_PCM1_HW_AUTODMA,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE, /* formats */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* formats */
	0,			/* align value */
	6,			/* minimal fragment */
	4000,			/* min. rate */
	48000,			/* max. rate */
	2,			/* max. voices */
	snd_sonicvibes_playback_open,
	snd_sonicvibes_playback_close,
	snd_sonicvibes_playback_ioctl,
	snd_sonicvibes_playback_prepare,
	snd_sonicvibes_playback_trigger,
	snd_sonicvibes_playback_pointer,
	snd_pcm1_playback_dma_ulaw_loud,
	snd_pcm1_dma_move,
	snd_pcm1_playback_dma_neutral
};

static struct snd_stru_pcm1_hardware snd_sonicvibes_capture =
{
	SND_PCM1_HW_AUTODMA,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE, /* formats */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* hardware formats */
	0,			/* align value */
	6,			/* minimal fragment */
	4000,			/* min. rate */
	48000,			/* max. rate */
	2,			/* max. voices */
	snd_sonicvibes_capture_open,
	snd_sonicvibes_capture_close,
	snd_sonicvibes_capture_ioctl,
	snd_sonicvibes_capture_prepare,
	snd_sonicvibes_capture_trigger,
	snd_sonicvibes_capture_pointer,
	snd_pcm1_capture_dma_ulaw_loud,
	snd_pcm1_dma_move,
	NULL
};

static void snd_sonicvibes_pcm_free(void *private_data)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	sonic->pcm = NULL;
}

snd_pcm_t *snd_sonicvibes_pcm(sonicvibes_t * sonic)
{
	snd_pcm_t *pcm;
	snd_pcm1_channel_t *pchn1;

	pcm = snd_pcm1_new_device(sonic->card, "s3_86c617", 1, 1);
	if (!pcm)
		return NULL;
	pchn1 = (snd_pcm1_channel_t *)pcm->playback.private_data;
	memcpy(&pchn1->hw, &snd_sonicvibes_playback, sizeof(snd_sonicvibes_playback));
	pchn1->private_data = sonic;
	pchn1 = (snd_pcm1_channel_t *)pcm->capture.private_data;	
	memcpy(&pchn1->hw, &snd_sonicvibes_capture, sizeof(snd_sonicvibes_capture));
	pchn1->private_data = sonic;
	pcm->private_data = sonic;
	pcm->private_free = snd_sonicvibes_pcm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
	    		  SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_CAPTURE |
	    		  SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "S3 SonicVibes");
	return sonic->pcm = pcm;
}

/*
 *  Mixer part
 */

static int snd_sonicvibes_mixer_get_recsrc(sonicvibes_t *sonic, snd_kmixer_element_t *element)
{
	if (element == sonic->me_mux_cd)
		return SV_RECSRC_CD;
	if (element == sonic->me_mux_dac)
		return SV_RECSRC_DAC;
	if (element == sonic->me_mux_aux2)
		return SV_RECSRC_AUX2;
	if (element == sonic->me_mux_line)
		return SV_RECSRC_LINE;
	if (element == sonic->me_mux_aux1)
		return SV_RECSRC_AUX1;
	if (element == sonic->me_mux_mic)
		return SV_RECSRC_MIC;
	if (element == sonic->me_mux_out)
		return SV_RECSRC_OUT;
	return -EINVAL;
}

static snd_kmixer_element_t *snd_sonicvibes_mixer_get_recsrc_element(sonicvibes_t *sonic, int value)
{
	value &= SV_RECSRC_OUT;
	switch (value) {
	case SV_RECSRC_CD:
		return sonic->me_mux_cd;
	case SV_RECSRC_DAC:
		return sonic->me_mux_dac;
	case SV_RECSRC_AUX2:
		return sonic->me_mux_aux2;
	case SV_RECSRC_LINE:
		return sonic->me_mux_line;
	case SV_RECSRC_AUX1:
		return sonic->me_mux_aux1;
	case SV_RECSRC_MIC:
		return sonic->me_mux_mic;
	case SV_RECSRC_OUT:
		return sonic->me_mux_out;
	}
	return NULL;
}

int snd_sonicvibes_mixer_mux(void *private_data, int w_flag, snd_kmixer_element_t **elements)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;
	unsigned long flags;
	int change = 0;
	int left, right, nleft, nright;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	left = snd_sonicvibes_in1(sonic, SV_IREG_LEFT_ADC) & SV_RECSRC_OUT;
	right = snd_sonicvibes_in1(sonic, SV_IREG_RIGHT_ADC) & SV_RECSRC_OUT;
	if (w_flag) {
		nleft = snd_sonicvibes_mixer_get_recsrc(sonic, elements[0]);
		nright = snd_sonicvibes_mixer_get_recsrc(sonic, elements[1]);
		if (nleft >= 0 && nright >= 0) {
			change = nleft != left || nright != right;
			snd_sonicvibes_outm1(sonic, SV_IREG_LEFT_ADC, (unsigned char)~SV_RECSRC_OUT, (unsigned char)nleft);
			snd_sonicvibes_outm1(sonic, SV_IREG_RIGHT_ADC, (unsigned char)~SV_RECSRC_OUT, (unsigned char)nright);
		} else {
			change = -EINVAL;
		}
	} else {
		elements[0] = snd_sonicvibes_mixer_get_recsrc_element(sonic, left);
		elements[1] = snd_sonicvibes_mixer_get_recsrc_element(sonic, right);
	}
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return change;
}

static int snd_sonicvibes_mixer_stereo_volume(sonicvibes_t *sonic,
					      int w_flag, int *voices,
					      int max, int invert, int shift,
					      unsigned char left_reg,
					      unsigned char right_reg)
{
	int change = 0, left, right;
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	left = (snd_sonicvibes_in1(sonic, left_reg) >> shift) & max;
	right = (snd_sonicvibes_in1(sonic, right_reg) >> shift) & max;
	if (!w_flag) {
		if (invert) {
			voices[0] = max - left;
			voices[1] = max - right;
		} else {
			voices[0] = left;
			voices[1] = right;
		}
	} else {
		if (invert) {
			change = max - left != voices[0] || max - right != voices[1];
		} else {
			change = left != voices[0] || right != voices[1];
		}
		left = voices[0]; right = voices[1];
		if (invert) {
			left = max - left;
			right = max - right;
		}
		snd_sonicvibes_outm1(sonic, left_reg, ~(max << shift), left << shift);
		snd_sonicvibes_outm1(sonic, right_reg, ~(max << shift), right << shift);
	}
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return change;	
}

static int snd_sonicvibes_mixer_mono_volume(sonicvibes_t *sonic,
					    int w_flag, int *voices,
					    int max, int invert, int shift,
					    unsigned char reg)
{
	int change = 0, val;
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	val = (snd_sonicvibes_in1(sonic, reg) >> shift) & max;
	if (!w_flag) {
		voices[0] = invert ? max - val : val;
	} else {
		change = (invert ? max - val : val) != voices[0];
		val = voices[0];
		if (invert)
			val = max - val;
		snd_sonicvibes_outm1(sonic, reg, ~(max << shift), val << shift);
	}
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return change;	
}

static int snd_sonicvibes_mixer_igain_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  15, 0, 0,
						  SV_IREG_LEFT_ADC,
						  SV_IREG_RIGHT_ADC);
}

static int snd_sonicvibes_mixer_aux1_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  31, 1, 0,
						  SV_IREG_LEFT_AUX1,
						  SV_IREG_RIGHT_AUX1);
}

static int snd_sonicvibes_mixer_cd_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  31, 1, 0,
						  SV_IREG_LEFT_CD,
						  SV_IREG_RIGHT_CD);
}

static int snd_sonicvibes_mixer_line_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  31, 1, 0,
						  SV_IREG_LEFT_LINE,
						  SV_IREG_RIGHT_LINE);
}

static int snd_sonicvibes_mixer_mic_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_mono_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  15, 1, 0,
						  SV_IREG_MIC);
}

static int snd_sonicvibes_mixer_mic_boost(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_mono_volume((sonicvibes_t *)private_data,
						w_flag, voices,
						1, 0, 4,
						SV_IREG_LEFT_ADC);
}

static int snd_sonicvibes_mixer_synth_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  31, 1, 0,
						  SV_IREG_LEFT_SYNTH,
						  SV_IREG_RIGHT_SYNTH);
}

static int snd_sonicvibes_mixer_aux2_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  31, 1, 0,
						  SV_IREG_LEFT_AUX2,
						  SV_IREG_RIGHT_AUX2);
}

static int snd_sonicvibes_mixer_master_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  31, 1, 0,
						  SV_IREG_LEFT_ANALOG,
						  SV_IREG_RIGHT_ANALOG);
}

static int snd_sonicvibes_mixer_pcm_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_stereo_volume((sonicvibes_t *)private_data,
						  w_flag, voices,
						  63, 1, 0,
						  SV_IREG_LEFT_PCM,
						  SV_IREG_RIGHT_PCM);
}

static int snd_sonicvibes_mixer_loop_volume(void *private_data, int w_flag, int *voices)
{
	return snd_sonicvibes_mixer_mono_volume((sonicvibes_t *)private_data,
						w_flag, voices,
						63, 1, 2,
						SV_IREG_ADC_OUTPUT_CTRL);
}

static int snd_sonicvibes_mixer_stereo_switch(sonicvibes_t *sonic, int w_flag,
					      unsigned int *bitmap,
					      int bit, int invert,
					      unsigned char left_reg,
					      unsigned char right_reg)
{
	int change = 0, left, right;
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	left = (snd_sonicvibes_in1(sonic, left_reg) >> bit) & 1;
	right = (snd_sonicvibes_in1(sonic, left_reg) >> bit) & 1;
	if (!w_flag) {
		if (invert) {
			snd_mixer_set_bit(bitmap, 0, left ^ 1);
			snd_mixer_set_bit(bitmap, 1, right ^ 1);
		} else {
			snd_mixer_set_bit(bitmap, 0, left);
			snd_mixer_set_bit(bitmap, 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_sonicvibes_outm1(sonic, left_reg, ~(1 << bit), left << bit);
		snd_sonicvibes_outm1(sonic, right_reg, ~(1 << bit), right << bit);
	}
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return change;	
}

static int snd_sonicvibes_mixer_mono_switch(sonicvibes_t *sonic, int w_flag,
					    int *value,
					    int bit, int invert,
					    unsigned char reg)
{
	int change = 0, val;
	unsigned long flags;

	spin_lock_irqsave(&sonic->reg_lock, flags);
	val = (snd_sonicvibes_in1(sonic, reg) >> bit) & 1;
	if (!w_flag) {
		*value = invert ? val ^ 1 : val;
	} else {
		change = (invert ? val ^ 1 : val) != (*value != 0);
		val = *value != 0;
		if (invert)
			val ^= 1;
		snd_sonicvibes_outm1(sonic, reg, ~(1 << bit), val << bit);
	}
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return change;	
}

static int snd_sonicvibes_mixer_aux1_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_AUX1,
						  SV_IREG_RIGHT_AUX1);
}

static int snd_sonicvibes_mixer_cd_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_CD,
						  SV_IREG_RIGHT_CD);
}

static int snd_sonicvibes_mixer_line_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_LINE,
						  SV_IREG_RIGHT_LINE);
}

static int snd_sonicvibes_mixer_mic_switch(void *private_data, int w_flag, int *value)
{
	return snd_sonicvibes_mixer_mono_switch((sonicvibes_t *)private_data,
						w_flag, value,
						7, 1,
						SV_IREG_MIC);
}

static int snd_sonicvibes_mixer_synth_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_SYNTH,
						  SV_IREG_RIGHT_SYNTH);
}

static int snd_sonicvibes_mixer_aux2_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_AUX2,
						  SV_IREG_RIGHT_AUX2);
}

static int snd_sonicvibes_mixer_master_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_ANALOG,
						  SV_IREG_RIGHT_ANALOG);
}

static int snd_sonicvibes_mixer_pcm_switch(void *private_data, int w_flag, unsigned int *bitmap)
{
	return snd_sonicvibes_mixer_stereo_switch((sonicvibes_t *)private_data,
						  w_flag, bitmap,
						  7, 1,
						  SV_IREG_LEFT_PCM,
						  SV_IREG_RIGHT_PCM);
}

static int snd_sonicvibes_mixer_loop_switch(void *private_data, int w_flag, int *value)
{
	return snd_sonicvibes_mixer_mono_switch((sonicvibes_t *)private_data,
						w_flag, value,
						0, 0,
						SV_IREG_ADC_OUTPUT_CTRL);
}

static int snd_sonicvibes_mixer_group_ctrl1(sonicvibes_t *sonic,
					    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)
{
	int voices[2];
	snd_kmixer_element_t *elements[2];
	unsigned int bitmap;
	int change = 0;

	if (!w_flag) {
		ugroup->caps = 0;
		ugroup->channels = SND_MIXER_CHN_MASK_STEREO;
		if (volume1) {
			ugroup->caps |= SND_MIXER_GRPCAP_VOLUME;
			volume1(sonic, 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(sonic, 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_sonicvibes_mixer_mux(sonic, 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(sonic, 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(sonic, 1, &bitmap) > 0) {
				snd_mixer_element_value_change(file, sw1_element, 0);
				change = 1;
			}
		}
		if (mux_in) {
			snd_sonicvibes_mixer_mux(sonic, 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_sonicvibes_mixer_mux(sonic, 1, elements) > 0) {
				snd_mixer_element_value_change(file, sonic->me_mux, 0);
				change = 1;
			}
		}
	}
	return change;
}

static int snd_sonicvibes_mixer_group_ctrl2(sonicvibes_t *sonic,
					    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_sw2_control_t *sw2,
					    snd_kmixer_element_t *sw2_element,
					    snd_kmixer_element_t *mux_in)
{
	int voices[2];
	snd_kmixer_element_t *elements[2];
	int value;
	int change = 0;

	if (!w_flag) {
		ugroup->caps = 0;
		ugroup->channels = mux_in ? SND_MIXER_CHN_MASK_STEREO : SND_MIXER_CHN_MASK_MONO;
		if (volume1) {
			ugroup->caps |= SND_MIXER_GRPCAP_VOLUME;
			if (mux_in)
				ugroup->caps |= SND_MIXER_GRPCAP_JOINTLY_VOLUME;
			volume1(sonic, 0, voices);
			ugroup->volume.names.front_left = voices[0];
			if (mux_in)
				ugroup->volume.names.front_right = voices[0];
			ugroup->min = 0;
			ugroup->max = max;
		}
		if (sw2) {
			ugroup->caps |= SND_MIXER_GRPCAP_MUTE;
			if (mux_in)
				ugroup->caps |= SND_MIXER_GRPCAP_JOINTLY_MUTE;
			sw2(sonic, 0, &value);
			ugroup->mute = 0;
			if (!value) {
				if (mux_in)
					ugroup->mute |= SND_MIXER_CHN_MASK_STEREO;
				else
					ugroup->mute |= SND_MIXER_CHN_MASK_FRONT_LEFT;
			}
		}
		if (mux_in) {
			ugroup->caps |= SND_MIXER_GRPCAP_CAPTURE | SND_MIXER_GRPCAP_EXCL_CAPTURE;
			ugroup->capture_group = 1;
			snd_sonicvibes_mixer_mux(sonic, 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;
			if (volume1(sonic, 1, voices) > 0) {
				snd_mixer_element_value_change(file, volume1_element, 0);
				change = 1;
			}
		}
		if (sw2) {
			value = 0;
			if (!(ugroup->mute & SND_MIXER_CHN_MASK_FRONT_LEFT))
				value = 1;
			if (sw2(sonic, 1, &value) > 0) {
				snd_mixer_element_value_change(file, sw2_element, 0);
				change = 1;
			}
		}
		if (mux_in) {
			snd_sonicvibes_mixer_mux(sonic, 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_sonicvibes_mixer_mux(sonic, 1, elements) > 0) {
				snd_mixer_element_value_change(file, sonic->me_mux, 0);
				change = 1;
			}
		}
	}
	return change;
}

static int snd_sonicvibes_mixer_group_igain(void *private_data,
				            snd_kmixer_file_t * file,
				            int w_flag,
				            snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_igain_volume,
					        sonic->me_vol_igain,
					        15,
					        NULL,
					        NULL,
					    	NULL);
}

static int snd_sonicvibes_mixer_group_aux1(void *private_data,
				           snd_kmixer_file_t * file,
				           int w_flag,
				           snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_aux1_volume,
					        sonic->me_vol_aux1,
					        31,
					        snd_sonicvibes_mixer_aux1_switch,
					        sonic->me_sw_aux1,
					    	sonic->me_mux_aux1);
}

static int snd_sonicvibes_mixer_group_cd(void *private_data,
				         snd_kmixer_file_t * file,
				         int w_flag,
				         snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_cd_volume,
					        sonic->me_vol_cd,
					        31,
					        snd_sonicvibes_mixer_cd_switch,
					        sonic->me_sw_cd,
					    	sonic->me_mux_cd);
}

static int snd_sonicvibes_mixer_group_line(void *private_data,
				           snd_kmixer_file_t * file,
				           int w_flag,
				           snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_line_volume,
					        sonic->me_vol_line,
					        31,
					        snd_sonicvibes_mixer_line_switch,
					        sonic->me_sw_line,
					    	sonic->me_mux_line);
}

static int snd_sonicvibes_mixer_group_mic(void *private_data,
				          snd_kmixer_file_t * file,
				          int w_flag,
				          snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl2(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_mic_volume,
					        sonic->me_vol_mic,
					        15,
					        snd_sonicvibes_mixer_mic_switch,
					        sonic->me_sw_mic,
					    	sonic->me_mux_mic);
}

static int snd_sonicvibes_mixer_group_synth(void *private_data,
				            snd_kmixer_file_t * file,
				            int w_flag,
				            snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_synth_volume,
					        sonic->me_vol_synth,
					        31,
					        snd_sonicvibes_mixer_synth_switch,
					        sonic->me_sw_synth,
					    	NULL);
}

static int snd_sonicvibes_mixer_group_aux2(void *private_data,
				           snd_kmixer_file_t * file,
				           int w_flag,
				           snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_aux2_volume,
					        sonic->me_vol_aux2,
					        31,
					        snd_sonicvibes_mixer_aux2_switch,
					        sonic->me_sw_aux2,
					    	sonic->me_mux_aux2);
}

static int snd_sonicvibes_mixer_group_master(void *private_data,
				             snd_kmixer_file_t * file,
				             int w_flag,
				             snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_master_volume,
					        sonic->me_vol_master,
					        31,
					        snd_sonicvibes_mixer_master_switch,
					        sonic->me_sw_master,
					    	sonic->me_mux_out);
}

static int snd_sonicvibes_mixer_group_pcm(void *private_data,
				          snd_kmixer_file_t * file,
				          int w_flag,
				          snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl1(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_pcm_volume,
					        sonic->me_vol_pcm,
					        63,
					        snd_sonicvibes_mixer_pcm_switch,
					        sonic->me_sw_pcm,
					    	sonic->me_mux_dac);
}

static int snd_sonicvibes_mixer_group_loop(void *private_data,
				           snd_kmixer_file_t * file,
				           int w_flag,
				           snd_mixer_group_t * ugroup)
{
	sonicvibes_t *sonic = (sonicvibes_t *)private_data;

	return snd_sonicvibes_mixer_group_ctrl2(sonic, file, w_flag, ugroup,
					        snd_sonicvibes_mixer_loop_volume,
					        sonic->me_vol_loop,
					        63,
					        snd_sonicvibes_mixer_loop_switch,
					        sonic->me_sw_loop,
					    	NULL);
}

static void snd_sonicvibes_mixer_free(void *data)
{
	((sonicvibes_t *) data)->mixer = NULL;
}

snd_kmixer_t *snd_sonicvibes_mixer(sonicvibes_t * sonic)
{
	snd_kmixer_t *mixer;
	snd_kmixer_group_t *group;
	snd_kmixer_element_t *element;
	int pcm_dev = 0;
	static struct snd_mixer_element_volume1_range master_range[2] = {
		{0, 31, -4800, 0},
		{0, 31, -4800, 0},
	};
	static struct snd_mixer_element_volume1_range mic_boost_range[1] = {
		{0, 1, 0, 2000},
	};
	static struct snd_mixer_element_volume1_range mic_range[1] = {
		{0, 15, -1200, 1200},
	};
	static struct snd_mixer_element_volume1_range std_range[2] = {
		{0, 31, -3600, 1200},
		{0, 31, -3600, 1200}
	};
	static struct snd_mixer_element_volume1_range igain_range[2] = {
		{0, 15, 0, 2250},
		{0, 15, 0, 2250}
	};
	static struct snd_mixer_element_volume1_range pcm_range[2] = {
		{0, 63, -9600, 0},
		{0, 63, -9600, 0}
	};
	static struct snd_mixer_element_volume1_range loop_range[1] = {
		{0, 63, -9600, 0}
	};

	if (!sonic)
		return NULL;
	mixer = snd_mixer_new(sonic->card, "s3_86c617");
	if (!mixer)
		return NULL;
	strcpy(mixer->name, "S3 SonicVibes");

	/* build input MUX and accumulators */
	if ((sonic->me_mux = snd_mixer_lib_mux1(mixer, SND_MIXER_ELEMENT_INPUT_MUX, 0, 0, 2, snd_sonicvibes_mixer_mux, sonic)) == NULL)
		goto __error;
	if ((sonic->me_accu = snd_mixer_lib_accu1(mixer, SND_MIXER_ELEMENT_OUTPUT_ACCU, 0, 0)) == NULL)
		goto __error;
	if ((sonic->me_dig_accu = snd_mixer_lib_accu1(mixer, SND_MIXER_ELEMENT_DIGITAL_ACCU, 0, 0)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_accu, sonic->me_mux) < 0)
		goto __error;
	sonic->me_mux_out = sonic->me_accu;
	/* build master output */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_OUT_MASTER, 0, SND_MIXER_OSS_VOLUME, snd_sonicvibes_mixer_group_master, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((sonic->me_vol_master = snd_mixer_lib_volume1(mixer, "Master Volume", 0, 2, master_range, snd_sonicvibes_mixer_master_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_master) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_accu, sonic->me_vol_master) < 0)
		goto __error;
	if ((sonic->me_sw_master = snd_mixer_lib_sw1(mixer, "Master Switch", 0, 2, snd_sonicvibes_mixer_master_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_master) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_master, sonic->me_sw_master) < 0)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_OUT_MASTER, 0, SND_MIXER_ETYPE_OUTPUT, 0)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_master, element) < 0)
		goto __error;
	/* build MIC input */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_MIC, 0, SND_MIXER_OSS_MIC, snd_sonicvibes_mixer_group_mic, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_MIC, 0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if ((sonic->me_vol_mic_boost = snd_mixer_lib_volume1(mixer, "MIC Boost", 0, 1, mic_boost_range, snd_sonicvibes_mixer_mic_boost, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_mic_boost) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_vol_mic_boost) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_mic_boost, sonic->me_mux) < 0)
		goto __error;
	if ((sonic->me_vol_mic = snd_mixer_lib_volume1(mixer, "MIC Volume", 0, 2, mic_range, snd_sonicvibes_mixer_mic_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_mic) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_mic_boost, sonic->me_vol_mic) < 0)
		goto __error;
	if ((sonic->me_sw_mic = snd_mixer_lib_sw2(mixer, "MIC Switch", 0, snd_sonicvibes_mixer_mic_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_mic) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_mic, sonic->me_sw_mic) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_mic, sonic->me_accu) < 0)
		goto __error;
	sonic->me_mux_mic = sonic->me_vol_mic_boost;
	/* build Line-In */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_LINE, 0, SND_MIXER_OSS_LINE, snd_sonicvibes_mixer_group_line, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_LINE, 0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_mux) < 0)
		goto __error;
	sonic->me_mux_line = element;
	if ((sonic->me_vol_line = snd_mixer_lib_volume1(mixer, "Line Volume", 0, 2, std_range, snd_sonicvibes_mixer_line_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_line) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_vol_line) < 0)
		goto __error;
	if ((sonic->me_sw_line = snd_mixer_lib_sw1(mixer, "Line Switch", 0, 2, snd_sonicvibes_mixer_line_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_line) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_line, sonic->me_sw_line) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_line, sonic->me_accu) < 0)
		goto __error;
	/* build CD */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_CD, 0, SND_MIXER_OSS_CD, snd_sonicvibes_mixer_group_cd, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_CD, 0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_mux) < 0)
		goto __error;
	sonic->me_mux_cd = element;
	if ((sonic->me_vol_cd = snd_mixer_lib_volume1(mixer, "CD Volume", 0, 2, std_range, snd_sonicvibes_mixer_cd_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_cd) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_vol_cd) < 0)
		goto __error;
	if ((sonic->me_sw_cd = snd_mixer_lib_sw1(mixer, "CD Switch", 0, 2, snd_sonicvibes_mixer_cd_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_cd) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_cd, sonic->me_sw_cd) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_cd, sonic->me_accu) < 0)
		goto __error;
	/* build SYNTH */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_SYNTHESIZER, 0, SND_MIXER_OSS_SYNTH, snd_sonicvibes_mixer_group_synth, sonic)) == NULL)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_SYNTHESIZER, 0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if ((sonic->me_vol_synth = snd_mixer_lib_volume1(mixer, "Synth Volume", 0, 2, std_range, snd_sonicvibes_mixer_synth_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_synth) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_vol_synth) < 0)
		goto __error;
	if ((sonic->me_sw_synth = snd_mixer_lib_sw1(mixer, "Synth Switch", 0, 2, snd_sonicvibes_mixer_cd_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_synth) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_synth, sonic->me_sw_synth) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_synth, sonic->me_accu) < 0)
		goto __error;
	/* build AUX1 */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_AUX, 0, SND_MIXER_OSS_LINE1, snd_sonicvibes_mixer_group_aux1, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_AUX, 0, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_mux) < 0)
		goto __error;
	sonic->me_mux_aux1 = element;
	if ((sonic->me_vol_aux1 = snd_mixer_lib_volume1(mixer, "Aux Volume", 0, 2, std_range, snd_sonicvibes_mixer_aux1_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_aux1) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_vol_aux1) < 0)
		goto __error;
	if ((sonic->me_sw_aux1 = snd_mixer_lib_sw1(mixer, "Aux Switch", 0, 2, snd_sonicvibes_mixer_aux1_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_aux1) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_aux1, sonic->me_sw_aux1) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_aux1, sonic->me_accu) < 0)
		goto __error;
	/* build AUX2 */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_AUX, 1, SND_MIXER_OSS_LINE2, snd_sonicvibes_mixer_group_aux2, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((element = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_AUX, 1, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_mux) < 0)
		goto __error;
	sonic->me_mux_aux2 = element;
	if ((sonic->me_vol_aux2 = snd_mixer_lib_volume1(mixer, "Aux Volume", 1, 2, std_range, snd_sonicvibes_mixer_aux2_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_aux2) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element, sonic->me_vol_aux2) < 0)
		goto __error;
	if ((sonic->me_sw_aux2 = snd_mixer_lib_sw1(mixer, "Aux Switch", 1, 2, snd_sonicvibes_mixer_aux2_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_aux2) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_aux2, sonic->me_sw_aux2) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_aux2, sonic->me_accu) < 0)
		goto __error;
	/* build input GAIN */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_GRP_IGAIN, 0, SND_MIXER_OSS_IGAIN, snd_sonicvibes_mixer_group_igain, sonic)) == NULL)
		goto __error;
	if ((sonic->me_vol_igain = snd_mixer_lib_volume1(mixer, "Input Gain Volume", 0, 2, igain_range, snd_sonicvibes_mixer_igain_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_igain) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_mux, sonic->me_vol_igain) < 0)
		goto __error;
	/* build ADC */
	if ((sonic->me_adc = snd_mixer_lib_converter(mixer, SND_MIXER_ELEMENT_ADC, 0, SND_MIXER_ETYPE_ADC, 16)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_igain, sonic->me_adc) < 0)
		goto __error;
	/* build capture endpoint */
	if ((sonic->me_capture = snd_mixer_lib_pcm(mixer, SND_MIXER_ELEMENT_CAPTURE, 0, SND_MIXER_ETYPE_CAPTURE, 1, &pcm_dev)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_adc, sonic->me_capture) < 0)
		goto __error;
	/* build loopback */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_GRP_DIGITAL_LOOPBACK, 0, SND_MIXER_OSS_DIGITAL1, snd_sonicvibes_mixer_group_loop, sonic)) == NULL)
		goto __error;
	if ((sonic->me_vol_loop = snd_mixer_lib_volume1(mixer, "Loopback Volume", 0, 1, loop_range, snd_sonicvibes_mixer_loop_volume, sonic)) == NULL)
		goto __error;	
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_loop) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_adc, sonic->me_vol_loop) < 0)
		goto __error;
	if ((sonic->me_sw_loop = snd_mixer_lib_sw2(mixer, "Loopback Switch", 0, snd_sonicvibes_mixer_loop_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_loop) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_loop, sonic->me_sw_loop) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_loop, sonic->me_dig_accu) < 0)
		goto __error;
	/* build playback endpoint */
	if ((sonic->me_playback = snd_mixer_lib_pcm(mixer, SND_MIXER_ELEMENT_PLAYBACK, 0, SND_MIXER_ETYPE_PLAYBACK, 1, &pcm_dev)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_playback, sonic->me_dig_accu) < 0)
		goto __error;
	/* build DAC */
	if ((group = snd_mixer_lib_group_ctrl(mixer, SND_MIXER_IN_PCM, 0, SND_MIXER_OSS_PCM, snd_sonicvibes_mixer_group_pcm, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_mux) < 0)
		goto __error;
	if ((sonic->me_dac = snd_mixer_lib_converter(mixer, SND_MIXER_ELEMENT_DAC, 0, SND_MIXER_ETYPE_DAC, 16)) == NULL)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_dig_accu, sonic->me_dac) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_dac, sonic->me_mux) < 0)
		goto __error;
	sonic->me_mux_dac = sonic->me_dac;
	if ((sonic->me_vol_pcm = snd_mixer_lib_volume1(mixer, "PCM Volume", 0, 2, pcm_range, snd_sonicvibes_mixer_pcm_volume, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_vol_pcm) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_dac, sonic->me_vol_pcm) < 0)
		goto __error;
	if ((sonic->me_sw_pcm = snd_mixer_lib_sw1(mixer, "PCM Switch", 0, 2, snd_sonicvibes_mixer_pcm_switch, sonic)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, sonic->me_sw_pcm) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_vol_pcm, sonic->me_sw_pcm) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, sonic->me_sw_pcm, sonic->me_accu) < 0)
		goto __error;

	mixer->private_data = sonic;
	mixer->private_free = snd_sonicvibes_mixer_free;
	sonic->mixer = mixer;
	return mixer;

      __error:
      	snd_mixer_free(mixer);
      	return NULL;
}

/*

 */

static void snd_sonicvibes_proc_read(snd_info_buffer_t * buffer,
				     void *private_data)
{
	sonicvibes_t *sonic = (sonicvibes_t *) private_data;
	unsigned char tmp;

	tmp = sonic->srs_space & 0x0f;
	snd_iprintf(buffer, "SRS 3D           : %s\n",
		    sonic->srs_space & 0x80 ? "off" : "on");
	snd_iprintf(buffer, "SRS Space        : %s\n",
		    tmp == 0x00 ? "100%" :
		    tmp == 0x01 ? "75%" :
		    tmp == 0x02 ? "50%" :
		    tmp == 0x03 ? "25%" : "0%");
	tmp = sonic->srs_center & 0x0f;
	snd_iprintf(buffer, "SRS Center       : %s\n",
		    tmp == 0x00 ? "100%" :
		    tmp == 0x01 ? "75%" :
		    tmp == 0x02 ? "50%" :
		    tmp == 0x03 ? "25%" : "0%");
	tmp = sonic->wave_source & 0x03;
	snd_iprintf(buffer, "WaveTable Source : %s\n",
		    tmp == 0x00 ? "on-board ROM" :
		    tmp == 0x01 ? "PCI bus" : "on-board ROM + PCI bus");
	tmp = sonic->mpu_switch;
	snd_iprintf(buffer, "Onboard synth    : %s\n", tmp & 0x01 ? "on" : "off");
	snd_iprintf(buffer, "Ext. Rx to synth : %s\n", tmp & 0x02 ? "on" : "off");
	snd_iprintf(buffer, "MIDI to ext. Tx  : %s\n", tmp & 0x04 ? "on" : "off");
}

static void snd_sonicvibes_proc_init(sonicvibes_t * sonic)
{
	snd_info_entry_t *entry;

	if ((entry = snd_info_create_entry(sonic->card, "sonicvibes")) != NULL) {
		entry->private_data = sonic;
		entry->mode = S_IFREG | S_IRUGO | S_IWUSR;
		entry->t.text.read_size = 256;
		entry->t.text.read = snd_sonicvibes_proc_read;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	sonic->proc_entry = entry;
}

static void snd_sonicvibes_proc_done(sonicvibes_t * sonic)
{
	if (sonic->proc_entry) {
		snd_info_unregister(sonic->proc_entry);
		sonic->proc_entry = NULL;
	}
}

/*

 */

static int snd_sonicvibes_get_game_switch(snd_card_t * card,
					  snd_kswitch_t * kswitch,
					  snd_switch_t * uswitch)
{
	sonicvibes_t *sonic = (sonicvibes_t *) kswitch->private_data;

	uswitch->type = SND_SW_TYPE_BYTE;
	uswitch->low = 0;
	uswitch->high = 15;
	uswitch->value.data8[0] =
		    (snd_sonicvibes_in(sonic, SV_IREG_GAME_PORT) >> 1) & 0x0f;
	return 0;
}

static int snd_sonicvibes_set_game_switch(snd_card_t * card,
					  snd_kswitch_t * kswitch,
					  snd_switch_t * uswitch)
{
	unsigned long flags;
	sonicvibes_t *sonic = (sonicvibes_t *) kswitch->private_data;
	int change = 0;
	unsigned char reg;

	if (uswitch->type != SND_SW_TYPE_BYTE ||
	    uswitch->value.data8[0] > 15)
		return -EINVAL;
	spin_lock_irqsave(&sonic->reg_lock, flags);
	reg = snd_sonicvibes_in1(sonic, SV_IREG_GAME_PORT)>>1;
	change = reg != uswitch->value.data8[0];
	snd_sonicvibes_out1(sonic, SV_IREG_GAME_PORT, uswitch->value.data8[0]<<1);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	return change;
}

static snd_kswitch_t snd_sonicvibes_game_switch =
{
	SND_CTL_SW_JOYSTICK_SPEED,
	(snd_get_switch_t *)snd_sonicvibes_get_game_switch,
	(snd_set_switch_t *)snd_sonicvibes_set_game_switch,
	0,
	NULL,
	NULL
};

sonicvibes_t *snd_sonicvibes_create(snd_card_t * card,
				    struct pci_dev *pci,
				    snd_dma_t * dma1ptr,
				    snd_dma_t * dma2ptr,
				    snd_irq_t * irqptr,
				    int reverb,
				    int mge)
{
	sonicvibes_t *sonic;
	unsigned short cmdw;

	sonic = (sonicvibes_t *) snd_kcalloc(sizeof(sonicvibes_t), GFP_KERNEL);
	if (!sonic)
		return NULL;
	sonic->reg_lock = SPIN_LOCK_UNLOCKED;
	sonic->card = card;
	sonic->pci = pci;
	sonic->dma1ptr = dma1ptr;
	sonic->dma2ptr = dma2ptr;
	sonic->irqptr = irqptr;
#ifdef NEW_PCI
	sonic->sb_port = pci->resource[0].start;
	sonic->enh_port = pci->resource[1].start;
	sonic->synth_port = pci->resource[2].start;
	sonic->midi_port = pci->resource[3].start;
	sonic->game_port = pci->resource[4].start;
#else
	sonic->sb_port = pci->base_address[0] & ~PCI_BASE_ADDRESS_SPACE;
	sonic->enh_port = pci->base_address[1] & ~PCI_BASE_ADDRESS_SPACE;
	sonic->synth_port = pci->base_address[2] & ~PCI_BASE_ADDRESS_SPACE;
	sonic->midi_port = pci->base_address[3] & ~PCI_BASE_ADDRESS_SPACE;
	sonic->game_port = pci->base_address[4] & ~PCI_BASE_ADDRESS_SPACE;
#endif
	pci_read_config_word(pci, PCI_COMMAND, &cmdw);
	if (!(cmdw & PCI_COMMAND_IO)) {
		cmdw |= PCI_COMMAND_IO;
		pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
	pci_read_config_dword(pci, 0x40, &sonic->dmaa_port);
	pci_read_config_dword(pci, 0x48, &sonic->dmac_port);
	sonic->dmaa_port &= ~0x0f;
	sonic->dmac_port &= ~0x0f;
	pci_write_config_dword(pci, 0x40, sonic->dmaa_port | 9);	/* enable + enhanced */
	pci_write_config_dword(pci, 0x48, sonic->dmac_port | 9);	/* enable */
	/* ok.. initialize S3 SonicVibes chip */
	outb(SV_RESET, SV_REG(sonic, CONTROL));		/* reset chip */
	snd_delay(10);
	outb(0, SV_REG(sonic, CONTROL));	/* release reset */
	snd_delay(10);
	outb(SV_ENHANCED | SV_INTA | (reverb ? SV_REVERB : 0), SV_REG(sonic, CONTROL));
	inb(SV_REG(sonic, STATUS));	/* clear IRQs */
#if 1
	snd_sonicvibes_out(sonic, SV_IREG_DRIVE_CTRL, 0);	/* drive current 16mA */
#else
	snd_sonicvibes_out(sonic, SV_IREG_DRIVE_CTRL, 0x40);	/* drive current 8mA */
#endif
	snd_sonicvibes_out(sonic, SV_IREG_PC_ENABLE, sonic->enable = 0);	/* disable playback & capture */
	outb(sonic->irqmask = ~(SV_DMAA_MASK | SV_DMAC_MASK | SV_UD_MASK), SV_REG(sonic, IRQMASK));
	inb(SV_REG(sonic, STATUS));	/* clear IRQs */
	snd_sonicvibes_out(sonic, SV_IREG_ADC_CLOCK, 0);	/* use PLL as clock source */
	snd_sonicvibes_out(sonic, SV_IREG_ANALOG_POWER, 0);	/* power up analog parts */
	snd_sonicvibes_out(sonic, SV_IREG_DIGITAL_POWER, 0);	/* power up digital parts */
	snd_sonicvibes_setpll(sonic, SV_IREG_ADC_PLL, 8000, 1);
	snd_sonicvibes_out(sonic, SV_IREG_SRS_SPACE, sonic->srs_space = 0x80);	/* SRS space off */
	snd_sonicvibes_out(sonic, SV_IREG_SRS_CENTER, sonic->srs_center = 0x00);	/* SRS center off */
	snd_sonicvibes_out(sonic, SV_IREG_MPU401, sonic->mpu_switch = 0x05);	/* MPU-401 switch */
	snd_sonicvibes_out(sonic, SV_IREG_WAVE_SOURCE, sonic->wave_source = 0x00);	/* onboard ROM */
	snd_sonicvibes_out(sonic, SV_IREG_PCM_RATE_LOW, (8000 * 65536 / SV_FULLRATE) & 0xff);
	snd_sonicvibes_out(sonic, SV_IREG_PCM_RATE_HIGH, ((8000 * 65536 / SV_FULLRATE) >> 8) & 0xff);
	snd_sonicvibes_out(sonic, SV_IREG_ADC_OUTPUT_CTRL, 0);
	snd_sonicvibes_out(sonic, SV_IREG_LEFT_ADC, mge ? 0xd0 : 0xc0);
	snd_sonicvibes_out(sonic, SV_IREG_RIGHT_ADC, 0xc0);
#if 0
	snd_sonicvibes_debug(sonic);
#endif
	sonic->revision = snd_sonicvibes_in(sonic, SV_IREG_REVISION);
	snd_control_switch_new(card, &snd_sonicvibes_game_switch, sonic);
	snd_sonicvibes_proc_init(sonic);
	return sonic;
}

void snd_sonicvibes_free(sonicvibes_t * sonic)
{
	snd_sonicvibes_proc_done(sonic);
	pci_write_config_dword(sonic->pci, 0x40, sonic->dmaa_port);
	pci_write_config_dword(sonic->pci, 0x48, sonic->dmac_port);
	snd_kfree(sonic);
}

/*
 *  MIDI section
 */

static int snd_sonicvibes_get_midi_switch(snd_rawmidi_t * rmidi,
					  snd_kswitch_t * kswitch,
					  snd_switch_t * uswitch)
{
	sonicvibes_t *sonic = (sonicvibes_t *) kswitch->private_data;

	uswitch->type = SND_SW_TYPE_BYTE;
	uswitch->low = 0;
	uswitch->high = 2;
	uswitch->value.data8[0] = sonic->wave_source & 0x03;
	return 0;
}

static int snd_sonicvibes_set_midi_switch(snd_rawmidi_t * rmidi,
					  snd_kswitch_t * kswitch,
					  snd_switch_t * uswitch)
{
	unsigned long flags;
	sonicvibes_t *sonic = (sonicvibes_t *) kswitch->private_data;
	int change = 0;

	if (uswitch->type != SND_SW_TYPE_BYTE ||
	    uswitch->value.data8[0] > 2)
		return -EINVAL;
	spin_lock_irqsave(&sonic->reg_lock, flags);
	change = (sonic->wave_source & 0x03) !=
		 (uswitch->value.data8[0] & 0x03);
	sonic->wave_source = (sonic->wave_source & 0xfc) |
			     (uswitch->value.data8[0] & 0x03);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	snd_sonicvibes_out(sonic, SV_IREG_WAVE_SOURCE, sonic->wave_source);
	return change;
}

static int snd_sonicvibes_get_midi_switch1(snd_rawmidi_t * rmidi,
					   snd_kswitch_t * kswitch,
					   snd_switch_t * uswitch)
{
	sonicvibes_t *sonic = (sonicvibes_t *) kswitch->private_data;

	uswitch->type = SND_SW_TYPE_BOOLEAN;
	uswitch->value.enable =
			sonic->mpu_switch & kswitch->private_value ? 1 : 0;
	return 0;
}

static int snd_sonicvibes_set_midi_switch1(snd_rawmidi_t * rmidi,
					   snd_kswitch_t * kswitch,
					   snd_switch_t * uswitch)
{
	unsigned long flags;
	sonicvibes_t *sonic = (sonicvibes_t *) kswitch->private_data;
	int change = 0;

	if (uswitch->type != SND_SW_TYPE_BOOLEAN)
		return -EINVAL;
	spin_lock_irqsave(&sonic->reg_lock, flags);
	change = (sonic->mpu_switch & kswitch->private_value) !=
		 (uswitch->value.enable ? kswitch->private_value : 0);
	sonic->mpu_switch = (sonic->mpu_switch & ~kswitch->private_value) |
	                    (uswitch->value.enable ? kswitch->private_value : 0);
	spin_unlock_irqrestore(&sonic->reg_lock, flags);
	snd_sonicvibes_out(sonic, SV_IREG_MPU401, sonic->mpu_switch);
	return change;
}

static snd_kswitch_t snd_sonicvibes_switch_wavesource =
{
	"SonicVibes Wave Source",
	(snd_get_switch_t *)snd_sonicvibes_get_midi_switch,
	(snd_set_switch_t *)snd_sonicvibes_set_midi_switch,
	0,
	NULL,
	NULL
};

static snd_kswitch_t snd_sonicvibes_switch_synth =
{
	"SonicVibes Onboard Synth",
	(snd_get_switch_t *)snd_sonicvibes_get_midi_switch1,
	(snd_set_switch_t *)snd_sonicvibes_set_midi_switch1,
	0x01,
	NULL,
	NULL
};

static snd_kswitch_t snd_sonicvibes_switch_rxtosynth =
{
	"SonicVibes External Rx to Synth",
	(snd_get_switch_t *)snd_sonicvibes_get_midi_switch1,
	(snd_set_switch_t *)snd_sonicvibes_set_midi_switch1,
	0x02,
	NULL,
	NULL
};

static snd_kswitch_t snd_sonicvibes_switch_txtoext =
{
	"SonicVibes External Tx",
	(snd_get_switch_t *)snd_sonicvibes_get_midi_switch1,
	(snd_set_switch_t *)snd_sonicvibes_set_midi_switch1,
	0x04,
	NULL,
	NULL
};

static void snd_sonicvibes_midi_input_open(mpu401_t * mpu)
{
	sonicvibes_t *sonic = (sonicvibes_t *) mpu->private_data;
	outb(sonic->irqmask &= ~SV_MIDI_MASK, SV_REG(sonic, IRQMASK));
}

static void snd_sonicvibes_midi_input_close(mpu401_t * mpu)
{
	sonicvibes_t *sonic = (sonicvibes_t *) mpu->private_data;
	outb(sonic->irqmask |= SV_MIDI_MASK, SV_REG(sonic, IRQMASK));
}

static void snd_sonicvibes_midi_output_open(mpu401_t * mpu)
{
	sonicvibes_t *sonic = (sonicvibes_t *) mpu->private_data;
	snd_rawmidi_direction_t *dir;

	dir = &sonic->rmidi->output;
	if (!sonic->switch_wavesource) {
		sonic->switch_wavesource =
			snd_rawmidi_switch_new(dir, &snd_sonicvibes_switch_wavesource, sonic);
	}
	if (!sonic->switch_synth) {
		sonic->switch_synth =
			snd_rawmidi_switch_new(dir, &snd_sonicvibes_switch_synth, sonic);
	}
	if (!sonic->switch_rxtosynth) {
		sonic->switch_rxtosynth =
			snd_rawmidi_switch_new(dir, &snd_sonicvibes_switch_rxtosynth, sonic);
	}
	if (!sonic->switch_txtoext) {
		sonic->switch_txtoext =
			snd_rawmidi_switch_new(dir, &snd_sonicvibes_switch_txtoext, sonic);
	}
}

void snd_sonicvibes_midi(sonicvibes_t * sonic, mpu401_t * mpu)
{
	mpu->private_data = sonic;
	mpu->open_input = snd_sonicvibes_midi_input_open;
	mpu->close_input = snd_sonicvibes_midi_input_close;
	mpu->open_output = snd_sonicvibes_midi_output_open;
}

void snd_sonicvibes_interrupt(sonicvibes_t * sonic)
{
	unsigned char status, udreg;
	int vol;

	status = inb(SV_REG(sonic, STATUS));
	if (!(status & (SV_DMAA_IRQ | SV_DMAC_IRQ | SV_MIDI_IRQ)))
		return;
	if (status == 0xff) {	/* failure */
		outb(sonic->irqmask = ~0, SV_REG(sonic, IRQMASK));
		snd_printk("SonicVibes: IRQ failure - interrupts disabled!!\n");
		return;
	}
	if (sonic->pcm) {
		if (status & SV_DMAA_IRQ) {
			sonic->playback_subchn1->ack(sonic->playback_subchn);
		}
		if (status & SV_DMAC_IRQ) {
			sonic->capture_subchn1->ack(sonic->capture_subchn);
		}
	}
	if (sonic->rmidi) {
		if (status & SV_MIDI_IRQ)
			snd_mpu401_uart_interrupt(sonic->rmidi);
	}
	if (sonic->mixer) {
		if (status & SV_UD_IRQ) {
			udreg = snd_sonicvibes_in(sonic, SV_IREG_UD_BUTTON);
			vol = udreg & 0x3f;
			if (!(udreg & 0x40))
				vol = -vol;
#if 0 /* TODO!!! */
			snd_mixer_hardware_volume(sonic->mixer,
						  SND_MIXER_PRI_MASTER,
						  SND_MIX_HW_CHANGE |
						  SND_MIX_HW_ADD_VOLUME |
						  SND_MIX_HW_XOR_MUTE |
						  SND_MIX_HW_JOIN_MUTE,
						  SND_MIX_VOL_OUTPUT,
						  vol, vol,
						  ((udreg & 0x80) ? SND_MIX_MUTE : 0));
#endif
		}
	}
}

EXPORT_SYMBOL(snd_sonicvibes_create);
EXPORT_SYMBOL(snd_sonicvibes_free);
EXPORT_SYMBOL(snd_sonicvibes_interrupt);
EXPORT_SYMBOL(snd_sonicvibes_pcm);
EXPORT_SYMBOL(snd_sonicvibes_mixer);
EXPORT_SYMBOL(snd_sonicvibes_midi);

/*
 *  INIT part
 */

int init_module(void)
{
	return 0;
}

void cleanup_module(void)
{
}
