/*
 *  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 "driver.h"
#include "info.h"
#include "control.h"
#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;

	snd_spin_lock(sonic, reg, &flags);
	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	outb(value, SV_REG(sonic, DATA));
	snd_delay(1);
	snd_spin_unlock(sonic, reg, &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;

	snd_spin_lock(sonic, reg, &flags);
	outb(reg, SV_REG(sonic, INDEX));
	snd_delay(1);
	value = inb(SV_REG(sonic, DATA));
	snd_delay(1);
	snd_spin_unlock(sonic, reg, &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;

	snd_spin_lock(sonic, reg, &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);
	snd_spin_unlock(sonic, reg, &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) {
		snd_spin_lock(sonic, reg, &flags);
		snd_sonicvibes_out1(sonic, reg, m);
		snd_sonicvibes_out1(sonic, reg + 1, r | n);
		snd_spin_unlock(sonic, reg, &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) {
		snd_spin_lock(sonic, reg, &flags);
		snd_sonicvibes_out1(sonic, SV_IREG_PCM_RATE_HIGH, div >> 8);
		snd_sonicvibes_out1(sonic, SV_IREG_PCM_RATE_LOW, div);
		snd_spin_unlock(sonic, reg, &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) {
		snd_spin_lock(sonic, reg, &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);
		}
		snd_spin_unlock(sonic, reg, &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;

	snd_spin_lock(sonic, reg, &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);
		}
	}
	snd_spin_unlock(sonic, reg, &flags);
}

/*
 *  PCM part
 */

static int snd_sonicvibes_playback_ioctl(snd_pcm1_t * pcm1,
					 unsigned int cmd, unsigned long *arg)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;

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

static int snd_sonicvibes_record_ioctl(snd_pcm1_t * pcm1,
				       unsigned int cmd, unsigned long *arg)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;

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

static void snd_sonicvibes_playback_trigger(snd_pcm1_t * pcm1, int up)
{
	snd_sonicvibes_trigger((sonicvibes_t *) pcm1->private_data, 1, up);
}

static void snd_sonicvibes_record_trigger(snd_pcm1_t * pcm1, int up)
{
	snd_sonicvibes_trigger((sonicvibes_t *) pcm1->private_data, 2, up);
}

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

	count--;
	if (pcm1->playback.voices > 1)
		fmt |= 1;
	if (pcm1->playback.mode & SND_PCM1_MODE_16)
		fmt |= 2;
	snd_sonicvibes_setfmt(sonic, ~3, fmt);
	snd_sonicvibes_set_dac_rate(sonic, pcm1->playback.real_rate, 1);
	snd_spin_lock(sonic, reg, &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);
	snd_spin_unlock(sonic, reg, &flags);
}

static void snd_sonicvibes_record_prepare(snd_pcm1_t * pcm1,
					  unsigned char *buffer,
					  unsigned int size,
					  unsigned int offset,
					  unsigned int count)
{
	unsigned long flags;
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;
	unsigned char fmt = 0;

	count >>= 1;
	count--;
	if (pcm1->record.voices > 1)
		fmt |= 0x10;
	if (pcm1->record.mode & SND_PCM1_MODE_16)
		fmt |= 0x20;
	snd_sonicvibes_setfmt(sonic, ~0x30, fmt);
	snd_sonicvibes_set_adc_rate(sonic, pcm1->record.real_rate, 1);
	snd_spin_lock(sonic, reg, &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);
	snd_spin_unlock(sonic, reg, &flags);
}

static int snd_sonicvibes_playback_open(snd_pcm1_t * pcm1)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK,
				      sonic->dma1ptr,
				      "S3 SonicVibes (playback)")) < 0)
		return err;
	sonic->mode |= SV_MODE_PLAY;
	return 0;
}

static int snd_sonicvibes_record_open(snd_pcm1_t * pcm1)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_RECORD,
				      sonic->dma2ptr,
				      "S3 SonicVibes (record)")) < 0)
		return err;
	sonic->mode |= SV_MODE_RECORD;
	return 0;
}

static void snd_sonicvibes_playback_close(snd_pcm1_t * pcm1)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, sonic->dma1ptr);
	sonic->mode &= ~SV_MODE_PLAY;
}

static void snd_sonicvibes_record_close(snd_pcm1_t * pcm1)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_RECORD, sonic->dma2ptr);
	sonic->mode &= ~SV_MODE_RECORD;
}

static unsigned int snd_sonicvibes_playback_pointer(snd_pcm1_t * pcm1,
                                                    unsigned int used_size)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;

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

static unsigned int snd_sonicvibes_record_pointer(snd_pcm1_t * pcm1,
						  unsigned int used_size)
{
	sonicvibes_t *sonic = (sonicvibes_t *) pcm1->private_data;

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

static struct snd_stru_pcm1_hardware snd_sonicvibes_playback =
{
	NULL,			/* private data */
	NULL,			/* private_free */
	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_record =
{
	NULL,			/* private data */
	NULL,			/* private_free */
	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_record_open,
	snd_sonicvibes_record_close,
	snd_sonicvibes_record_ioctl,
	snd_sonicvibes_record_prepare,
	snd_sonicvibes_record_trigger,
	snd_sonicvibes_record_pointer,
	snd_pcm1_record_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_t *pcm1;

	pcm = snd_pcm1_new_device(sonic->card, "s3_86c617");
	if (!pcm)
		return NULL;
	pcm1 = (snd_pcm1_t *) pcm->private_data;
	memcpy(&pcm1->playback.hw,
	       &snd_sonicvibes_playback,
	       sizeof(snd_sonicvibes_playback));
	memcpy(&pcm1->record.hw,
	       &snd_sonicvibes_record,
	       sizeof(snd_sonicvibes_record));
	pcm1->private_data = sonic;
	pcm1->private_free = snd_sonicvibes_pcm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
	    SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD | SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "S3 SonicVibes");
	return sonic->pcm = pcm;
}

/*
 *  Mixer part
 */

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;

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

	snd_printk("TODO: MIXER!!!\n"); 

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

/*

 */

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;
	snd_spin_lock(sonic, reg, &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);
	snd_spin_unlock(sonic, reg, &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 snd_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_calloc(sizeof(sonicvibes_t));
	if (!sonic)
		return NULL;
	snd_spin_prepare(sonic, reg);
	sonic->card = card;
	sonic->pci = pci;
	sonic->dma1ptr = dma1ptr;
	sonic->dma2ptr = dma2ptr;
	sonic->irqptr = irqptr;
	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;
	snd_pci_read_config_word(pci, PCI_COMMAND, &cmdw);
	if (!(cmdw & PCI_COMMAND_IO)) {
		cmdw |= PCI_COMMAND_IO;
		snd_pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
	snd_pci_read_config_dword(pci, 0x40, &sonic->dmaa_port);
	snd_pci_read_config_dword(pci, 0x48, &sonic->dmac_port);
	sonic->dmaa_port &= ~0x0f;
	sonic->dmac_port &= ~0x0f;
	snd_pci_write_config_dword(pci, 0x40, sonic->dmaa_port | 9);	/* enable + enhanced */
	snd_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);
	snd_pci_write_config_dword(sonic->pci, 0x40, sonic->dmaa_port);
	snd_pci_write_config_dword(sonic->pci, 0x48, sonic->dmac_port);
	snd_free(sonic, sizeof(sonicvibes_t));
}

/*
 *  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;
	snd_spin_lock(sonic, reg, &flags);
	change = (sonic->wave_source & 0x03) !=
		 (uswitch->value.data8[0] & 0x03);
	sonic->wave_source = (sonic->wave_source & 0xfc) |
			     (uswitch->value.data8[0] & 0x03);
	snd_spin_unlock(sonic, reg, &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;
	snd_spin_lock(sonic, reg, &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);
	snd_spin_unlock(sonic, reg, &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)
{
	snd_pcm1_t *pcm1;
	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) {
			pcm1 = (snd_pcm1_t *) sonic->pcm->private_data;
			if (pcm1 && pcm1->playback.ack)
				pcm1->playback.ack(pcm1);
		}
		if (status & SV_DMAC_IRQ) {
			pcm1 = (snd_pcm1_t *) sonic->pcm->private_data;
			if (pcm1 && pcm1->record.ack)
				pcm1->record.ack(pcm1);
		}
	}
	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
		}
	}
}

/*
 *  INIT part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_s3_86c617_export;
#endif

int init_module(void)
{
#ifndef LINUX_2_1
	if (register_symtab(&snd_symbol_table_s3_86c617_export) < 0)
		return -ENOMEM;
#endif
	return 0;
}

void cleanup_module(void)
{
}
