/*
 *   ALSA driver for VIA VT82C686A (South Bridge)
 *
 *	Copyright (c) 2000 Jaroslav Kysela <perex@suse.cz>
 *
 *   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_MAIN_OBJECT_FILE

#include "../include/driver.h"
#include "../include/pcm.h"
#include "../include/mixer.h"
#include "../include/info.h"
#include "../include/ac97_codec.h"
#include "../include/mpu401.h"
#include "../include/initval.h"

MODULE_DESCRIPTION("\
Driver: VIA VT82C686A\n\
PCI: 0x1106=0x3058\n\
");

int snd_index[SND_CARDS] = SND_DEFAULT_IDX;	/* Index 0-MAX */
char *snd_id[SND_CARDS] = SND_DEFAULT_STR;	/* ID for this card */
int snd_mpu_port[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = -1};
int snd_joystick[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 0};
int snd_pbk_frame_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
int snd_cap_frame_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
int snd_fm_frame_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
MODULE_PARM(snd_index, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_index, "Index value for VIA 82C686A bridge.");
MODULE_PARM(snd_id, "1-" __MODULE_STRING(SND_CARDS) "s");
MODULE_PARM_DESC(snd_id, "ID string for VIA 82C686A bridge.");
MODULE_AUTHOR("Jaroslav Kysela <perex@suse.cz>");
MODULE_PARM(snd_mpu_port, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_mpu_port, "MPU-401 port.");
MODULE_PARM(snd_pbk_frame_size, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_pbk_frame_size, "Playback frame size in kB.");
MODULE_PARM(snd_cap_frame_size, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_cap_frame_size, "Capture frame size in kB.");
MODULE_PARM(snd_fm_frame_size, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_fm_frame_size, "FM playback frame size in kB.");
MODULE_PARM(snd_joystick, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_joystick, "Joystick support. [BOOL]");

/*
 *  Direct registers
 */

#ifndef PCI_DEVICE_ID_VIA_82C686_5
#define PCI_DEVICE_ID_VIA_82C686_5	0x3058
#endif

#define VIAREG(via, x) ((via)->port + VIA_REG_##x)

/* offsets */
#define VIA_REG_OFFSET_STATUS		0x00	/* byte - channel status */
#define   VIA_REG_STAT_ACTIVE		0x80	/* RO */
#define   VIA_REG_STAT_PAUSED		0x40	/* RO */
#define   VIA_REG_STAT_TRIGGER_QUEUED	0x08	/* RO */
#define   VIA_REG_STAT_STOPPED		0x04	/* RWC */
#define   VIA_REG_STAT_EOL		0x02	/* RWC */
#define   VIA_REG_STAT_FLAG		0x01	/* RWC */
#define VIA_REG_OFFSET_CONTROL		0x01	/* byte - channel control */
#define   VIA_REG_CTRL_START		0x80	/* WO */
#define   VIA_REG_CTRL_TERMINATE	0x40	/* WO */
#define   VIA_REG_CTRL_PAUSE		0x08	/* RW */
#define   VIA_REG_CTRL_RESET		0x01	/* RW - probably reset? undocumented */
#define VIA_REG_OFFSET_TYPE		0x02	/* byte - channel type */
#define   VIA_REG_TYPE_AUTOSTART	0x80	/* RW - autostart at EOL */
#define   VIA_REG_TYPE_16BIT		0x20	/* RW */
#define   VIA_REG_TYPE_STEREO		0x10	/* RW */
#define   VIA_REG_TYPE_INT_LLINE	0x00
#define   VIA_REG_TYPE_INT_LSAMPLE	0x04
#define   VIA_REG_TYPE_INT_LESSONE	0x08
#define   VIA_REG_TYPE_INT_MASK		0x0c
#define   VIA_REG_TYPE_INT_EOL		0x02
#define   VIA_REG_TYPE_INT_FLAG		0x01
#define VIA_REG_OFFSET_TABLE_PTR	0x04	/* dword - channel table pointer */
#define VIA_REG_OFFSET_CURR_PTR		0x04	/* dword - channel current pointer */
#define VIA_REG_OFFSET_CURR_COUNT	0x0c	/* dword - channel current count */
/* playback block */
#define VIA_REG_PLAYBACK_STATUS		0x00	/* byte - channel status */
#define VIA_REG_PLAYBACK_CONTROL	0x01	/* byte - channel control */
#define VIA_REG_PLAYBACK_TYPE		0x02	/* byte - channel type */
#define VIA_REG_PLAYBACK_TABLE_PTR	0x04	/* dword - channel table pointer */
#define VIA_REG_PLAYBACK_CURR_PTR	0x04	/* dword - channel current pointer */
#define VIA_REG_PLAYBACK_CURR_COUNT	0x0c	/* dword - channel current count */
/* capture block */
#define VIA_REG_CAPTURE_STATUS		0x10	/* byte - channel status */
#define VIA_REG_CAPTURE_CONTROL		0x11	/* byte - channel control */
#define VIA_REG_CAPTURE_TYPE		0x12	/* byte - channel type */
#define VIA_REG_CAPTURE_TABLE_PTR	0x14	/* dword - channel table pointer */
#define VIA_REG_CAPTURE_CURR_PTR	0x14	/* dword - channel current pointer */
#define VIA_REG_CAPTURE_CURR_COUNT	0x1c	/* dword - channel current count */
/* FM block */
#define VIA_REG_FM_STATUS		0x20	/* byte - channel status */
#define VIA_REG_FM_CONTROL		0x21	/* byte - channel control */
#define VIA_REG_FM_TYPE			0x22	/* byte - channel type */
#define VIA_REG_FM_TABLE_PTR		0x24	/* dword - channel table pointer */
#define VIA_REG_FM_CURR_PTR		0x24	/* dword - channel current pointer */
#define VIA_REG_FM_CURR_COUNT		0x2c	/* dword - channel current count */
/* AC'97 */
#define VIA_REG_AC97			0x80	/* dword */
#define   VIA_REG_AC97_CODEC_ID_MASK	(3<<30)
#define   VIA_REG_AC97_CODEC_ID_SHIFT	30
#define   VIA_REG_AC97_SECONDARY_VALID	(1<<27)
#define   VIA_REG_AC97_PRIMARY_VALID	(1<<25)
#define   VIA_REG_AC97_BUSY		(1<<24)
#define   VIA_REG_AC97_READ		(1<<23)
#define   VIA_REG_AC97_CMD_SHIFT	16
#define   VIA_REG_AC97_CMD_MASK		0x7e
#define   VIA_REG_AC97_DATA_SHIFT	0
#define   VIA_REG_AC97_DATA_MASK	0xffff
#define VIA_REG_SGD_SHADOW		0x84	/* dword */

/*
 *  
 */

typedef struct {
	unsigned int reg_offset;
	unsigned int *table;
	unsigned int rates;
        snd_pcm_subchn_t *subchn;
        unsigned long physbuf;
        unsigned int size;
        unsigned int fragsize;
	unsigned int position;
} viadev_t;

typedef struct snd_stru_via686a via686a_t;

struct snd_stru_via686a {
	snd_dma_t * dma_pbk;	/* playback */
	snd_dma_t * dma_cap;	/* capture */
	snd_dma_t * dma_fm;	/* FM playback */
	snd_irq_t * irqptr;

	unsigned long port;
	unsigned char revision;

	struct pci_dev *pci;
	snd_card_t *card;

	snd_pcm_t *pcm;
	snd_pcm_t *pcm_fm;
	viadev_t playback;
	viadev_t capture;
	viadev_t playback_fm;

	ac97_t *ac97;
	unsigned short ac97_ext_id;
	snd_kmixer_t *mixer;

	spinlock_t reg_lock;
	snd_info_entry_t *proc_entry;

	void *tables;
};

typedef struct snd_via686a_card {
	struct pci_dev *pci;
	snd_irq_t *irqptr;
	snd_dma_t *dma_pbk;	/* playback */
	snd_dma_t *dma_cap;	/* capture */
	snd_dma_t *dma_fm;	/* MIC capture */
	snd_card_t *card;
	via686a_t *codec;
	snd_pcm_t *pcm;
	snd_pcm_t *pcm_fm;
	snd_kmixer_t *mixer;
	snd_rawmidi_t *midi_uart;
} via686a_card_t;

static via686a_card_t *snd_via686a_cards[SND_CARDS] = SND_DEFAULT_PTR;

/*
 *  Basic I/O
 */

static inline unsigned int snd_via686a_codec_xread(via686a_t *codec)
{
	return (unsigned int)inw(VIAREG(codec, AC97)) |
	       ((unsigned int)inw(VIAREG(codec, AC97) + 2) << 16);
}
 
static inline void snd_via686a_codec_xwrite(via686a_t *codec, unsigned int val)
{
	outw((unsigned short)val, VIAREG(codec, AC97));
	outw((unsigned short)(val >> 16), VIAREG(codec, AC97) + 2);
}
 
static int snd_via686a_codec_ready(via686a_t *codec, int secondary, int sched)
{
	signed long end_time;
	unsigned int val;

	end_time = jiffies + 3 * (HZ >> 2);
	do {
		if (!((val = snd_via686a_codec_xread(codec)) & VIA_REG_AC97_BUSY))
			return val & 0xffff;
		if (sched) {
			set_current_state(TASK_UNINTERRUPTIBLE);
			schedule_timeout(1);
		}
	} while (end_time - (signed long)jiffies >= 0);
	snd_printk("snd_via686a_codec_ready: codec %i is not ready [0x%x]\n", secondary, snd_via686a_codec_xread(codec));
	return -EIO;
}
 
static int snd_via686a_codec_valid(via686a_t *codec, int secondary)
{
	signed long end_time;
	int val;
	int stat = !secondary ? VIA_REG_AC97_PRIMARY_VALID :
				VIA_REG_AC97_SECONDARY_VALID;
	
	end_time = jiffies + 3 * (HZ >> 2);
	do {
		if ((val = snd_via686a_codec_xread(codec)) & stat)
			return 0;
	} while (end_time - (signed long)jiffies >= 0);
	snd_printk("snd_via686a_codec_valid: codec %i is not valid [0x%x]\n", secondary, snd_via686a_codec_xread(codec));
	return -EIO;
}
 
static void snd_via686a_codec_write(void *private_data,
				    unsigned short reg,
				    unsigned short val)
{
	via686a_t *codec = (via686a_t *)private_data;
	unsigned int xval;
	
	snd_via686a_codec_ready(codec, 0, 0);
	xval = 0 << VIA_REG_AC97_CODEC_ID_SHIFT;
	xval |= VIA_REG_AC97_PRIMARY_VALID;
	xval |= reg << VIA_REG_AC97_CMD_SHIFT;
	xval |= val << VIA_REG_AC97_DATA_SHIFT;
	snd_via686a_codec_xwrite(codec, xval);
}

static unsigned short snd_via686a_codec_read(void *private_data,
					      unsigned short reg)
{
	via686a_t *codec = (via686a_t *)private_data;
	unsigned int xval;

	if (snd_via686a_codec_ready(codec, 0, 0) < 0)
		return ~0;
	xval = 0 << VIA_REG_AC97_CODEC_ID_SHIFT;
	xval |= VIA_REG_AC97_PRIMARY_VALID;
	xval |= VIA_REG_AC97_READ;
	xval |= reg << VIA_REG_AC97_CMD_SHIFT;
	snd_via686a_codec_xwrite(codec, xval);
	if (snd_via686a_codec_ready(codec, 0, 0) < 0)
		return ~0;
	if (snd_via686a_codec_valid(codec, 0) < 0)
		return ~0;
	return snd_via686a_codec_xread(codec) & 0xffff;
}

#if 0
static void snd_via686a_channel_print(via686a_t *codec, viadev_t *viadev)
{
	unsigned long port = codec->port + viadev->reg_offset;

	printk("[0x%lx] status = 0x%x, control = 0x%x, type = 0x%x, ptr = 0x%x, count = 0x%x\n",
			port,
			inb(port + VIA_REG_OFFSET_STATUS),
			inb(port + VIA_REG_OFFSET_CONTROL),
			inb(port + VIA_REG_OFFSET_TYPE),
			inl(port + VIA_REG_OFFSET_CURR_PTR),
			inl(port + VIA_REG_OFFSET_CURR_COUNT));
}
#endif

static void snd_via686a_channel_reset(via686a_t *codec, viadev_t *viadev)
{
	unsigned long port = codec->port + viadev->reg_offset;

	outb(VIA_REG_CTRL_PAUSE | VIA_REG_CTRL_TERMINATE | VIA_REG_CTRL_RESET, port + VIA_REG_OFFSET_CONTROL);
	udelay(50);
	outb(0x00, port + VIA_REG_OFFSET_CONTROL);
	outb(0xff, port + VIA_REG_OFFSET_STATUS);
	outb(0x00, port + VIA_REG_OFFSET_TYPE);
	outl(0, port + VIA_REG_OFFSET_CURR_PTR);
}

static int snd_via686a_trigger(via686a_t *codec, viadev_t *viadev, int cmd)
{
	unsigned char val = 0;
	unsigned long port = codec->port + viadev->reg_offset;
	
	switch (cmd) {
	case SND_PCM_TRIGGER_GO:
		val = VIA_REG_CTRL_START;
		break;
	case SND_PCM_TRIGGER_STOP:
		val = VIA_REG_CTRL_TERMINATE | VIA_REG_CTRL_RESET;
		break;
	case SND_PCM_TRIGGER_PAUSE_PUSH:
		val = VIA_REG_CTRL_PAUSE;
		break;
	case SND_PCM_TRIGGER_PAUSE_RELEASE:
		val = 0;
		break;
	default:
		return -EINVAL;
	}
	outb(val, port + VIA_REG_OFFSET_CONTROL);
	if (cmd == SND_PCM_TRIGGER_STOP)
		snd_via686a_channel_reset(codec, viadev);
	return 0;
}

static void snd_via686a_setup_fragments(via686a_t *codec, viadev_t *viadev,
					snd_pcm_runtime_t *runtime) 
{
	int idx, frags;
	unsigned int *table = viadev->table;
	unsigned long port = codec->port + viadev->reg_offset;

	snd_via686a_channel_reset(codec, viadev);
	outl(virt_to_bus(viadev->table), port + VIA_REG_OFFSET_TABLE_PTR);
	outb(VIA_REG_TYPE_AUTOSTART |
	     (runtime->format.format == SND_PCM_SFMT_S16_LE ? VIA_REG_TYPE_16BIT : 0) |
	     (runtime->format.voices > 1 ? VIA_REG_TYPE_STEREO : 0) |
	     VIA_REG_TYPE_INT_EOL |
	     VIA_REG_TYPE_INT_FLAG, port + VIA_REG_OFFSET_TYPE);
	if (viadev->size == viadev->fragsize) {
		table[0] = viadev->physbuf;
		table[1] = 0xc0000000 | /* EOL + flag */
			   viadev->fragsize;
	} else {
		frags = viadev->size / viadev->fragsize;
		for (idx = 0; idx < frags - 1; idx++) {
			table[(idx << 1) + 0] = (viadev->physbuf + (idx * viadev->fragsize));
			table[(idx << 1) + 1] = 0x40000000 |	/* flag */
						viadev->fragsize;
		}
		table[((frags-1) << 1) + 0] = (viadev->physbuf + ((frags-1) * viadev->fragsize));
		table[((frags-1) << 1) + 1] = 0x80000000 |	/* EOL */
					      viadev->fragsize;
	}
}

/*
 *  Interrupt handler
 */

static inline void snd_via686a_update(via686a_t *codec, viadev_t *viadev)
{
	spin_lock(&codec->reg_lock);
	viadev->position += viadev->fragsize;
	viadev->position %= viadev->size;
	spin_unlock(&codec->reg_lock);
	snd_pcm_transfer_done(viadev->subchn);
	outb(VIA_REG_STAT_FLAG | VIA_REG_STAT_EOL, VIAREG(codec, OFFSET_STATUS) + viadev->reg_offset);
}

static void snd_via686a_card_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	via686a_card_t *scard = (via686a_card_t *) dev_id;
	via686a_t *codec;
	unsigned int status;

	if (scard == NULL || (codec = scard->codec) == NULL)
		return;
	status = inl(VIAREG(codec, SGD_SHADOW));
	if ((status & 0x00000077) == 0)
		return;
	if (inb(VIAREG(codec, PLAYBACK_STATUS)) & (VIA_REG_STAT_EOL|VIA_REG_STAT_FLAG))
		snd_via686a_update(codec, &codec->playback);
	if (inb(VIAREG(codec, FM_STATUS)) & (VIA_REG_STAT_EOL|VIA_REG_STAT_FLAG))
		snd_via686a_update(codec, &codec->playback_fm);
	if (inb(VIAREG(codec, CAPTURE_STATUS)) & (VIA_REG_STAT_EOL|VIA_REG_STAT_FLAG))
		snd_via686a_update(codec, &codec->capture);
}

/*
 *  PCM part
 */

static int snd_via686a_playback_ioctl(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      unsigned int cmd,
				      unsigned long *arg)
{
	int result;
	// via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);

	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	if (cmd == SND_PCM_IOCTL1_PARAMS) {
		snd_pcm_runtime_t *runtime = subchn->runtime;

		if (runtime->frags > 32)
			runtime->frags = 32;
	}
	return 0;
}

static int snd_via686a_capture_ioctl(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      unsigned int cmd,
				      unsigned long *arg)
{
	int result;
	// via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);

	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	if (cmd == SND_PCM_IOCTL1_PARAMS) {
		snd_pcm_runtime_t *runtime = subchn->runtime;

		if (runtime->frags > 32)
			runtime->frags = 32;
	}
	return 0;
}

static int snd_via686a_playback_trigger(void *private_data,
					snd_pcm_subchn_t * pcm,
					int cmd)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);

	return snd_via686a_trigger(codec, &codec->playback, cmd);
}

static int snd_via686a_capture_trigger(void *private_data,
				       snd_pcm_subchn_t * subchn,
				       int cmd)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);

	return snd_via686a_trigger(codec, &codec->capture, cmd);
}

static int snd_via686a_playback_prepare(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->playback.physbuf = virt_to_bus(runtime->dma_area->buf);
	codec->playback.size = snd_pcm_lib_transfer_size(subchn);
	codec->playback.fragsize = snd_pcm_lib_transfer_fragment(subchn);
	codec->playback.position = 0;
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	snd_ac97_write_lock(codec->ac97, AC97_PCM_FRONT_DAC_RATE, runtime->format.rate);
	/* enable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0000);
	spin_lock_irqsave(&codec->reg_lock, flags);
	snd_via686a_setup_fragments(codec, &codec->playback, runtime);
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static int snd_via686a_capture_prepare(void *private_data,
				       snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->capture.physbuf = virt_to_bus(runtime->dma_area->buf);
	codec->capture.size = snd_pcm_lib_transfer_size(subchn);
	codec->capture.fragsize = snd_pcm_lib_transfer_fragment(subchn);
	codec->capture.position = 0;
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0100);
	snd_ac97_write_lock(codec->ac97, AC97_PCM_LR_ADC_RATE, runtime->format.rate);
	/* enable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0000);
	spin_lock_irqsave(&codec->reg_lock, flags);
	snd_via686a_setup_fragments(codec, &codec->capture, runtime);
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static unsigned int snd_via686a_playback_pointer(void *private_data,
						  snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	unsigned int result;

	spin_lock_irqsave(&codec->reg_lock, flags);
	if (inb(VIAREG(codec, PLAYBACK_STATUS)) & VIA_REG_STAT_ACTIVE) {
		result = codec->playback.position +
			 (codec->playback.fragsize -
			  inl(VIAREG(codec, PLAYBACK_CURR_COUNT)));
	} else {
		result = 0;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

static unsigned int snd_via686a_capture_pointer(void *private_data,
						snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	unsigned int result;

	spin_lock_irqsave(&codec->reg_lock, flags);
	if (inb(VIAREG(codec, CAPTURE_STATUS)) & VIA_REG_STAT_ACTIVE) {
		result = codec->capture.position +
			 (codec->capture.fragsize -
			  inl(VIAREG(codec, CAPTURE_CURR_COUNT)));
	} else {
		result = 0;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

static snd_pcm_hardware_t snd_via686a_playback =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID |
	SND_PCM_CHNINFO_PAUSE,	/* flags */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* hardware formats */
	0,			/* overwritten */
	8000,			/* min. rate */
	48000,			/* max. rate */
	1,			/* min. voices */
	2,			/* max. voices */
	32,			/* min. fragment size */
	128 * 1024,		/* max. fragment size */
	31,			/* fragment align */
	0,			/* FIFO size (unknown) */
	16,			/* transfer block size */
	snd_via686a_playback_ioctl,
	snd_via686a_playback_prepare,
	snd_via686a_playback_trigger,
	snd_via686a_playback_pointer
};

static snd_pcm_hardware_t snd_via686a_capture =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID,	/* flags */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* hardware formats */
	0,			/* overwritten */
	8000,			/* min. rate */
	48000,			/* max. rate */
	1,			/* min. voices */
	2,			/* max. voices */
	32,			/* min. fragment size */
	128 * 1024,		/* max. fragment size */
	31,			/* fragment align */
	0,			/* FIFO size (unknown) */
	16,			/* transfer block size */
	snd_via686a_capture_ioctl,
	snd_via686a_capture_prepare,
	snd_via686a_capture_trigger,
	snd_via686a_capture_pointer
};

static int snd_via686a_playback_open(void *private_data,
			             snd_pcm_subchn_t * subchn)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	snd_pcm_hardware_t *hw;
	int err;

	hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(snd_pcm_hardware_t), GFP_KERNEL);
	if (hw == NULL)
		return -ENOMEM;
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma_pbk,
				     "VIA 82C686A - DAC")) < 0) {
		snd_kfree(hw);
		return err;
	}
	codec->playback.subchn = subchn;
	memcpy(hw, &snd_via686a_playback, sizeof(*hw));
	hw->rates = codec->playback.rates;
	if (!(hw->rates & SND_PCM_RATE_8000))
		hw->min_rate = 48000;
	subchn->runtime->hw = hw;
	subchn->runtime->hw_free = _snd_kfree;
	snd_pcm_set_mixer(subchn, codec->mixer->device, codec->ac97->me_playback);
	return 0;
}

static int snd_via686a_capture_open(void *private_data,
				    snd_pcm_subchn_t * subchn)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	snd_pcm_hardware_t *hw;
	int err;

	hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(snd_pcm_hardware_t), GFP_KERNEL);
	if (hw == NULL)
		return -ENOMEM;
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma_cap,
				     "VIA 82C686A - ADC")) < 0) {
		snd_kfree(hw);
		return err;
	}
	codec->capture.subchn = subchn;
	memcpy(hw, &snd_via686a_capture, sizeof(*hw));
	hw->rates = codec->capture.rates;
	if (!(hw->rates & SND_PCM_RATE_8000))
		hw->min_rate = 48000;
	subchn->runtime->hw = hw;
	subchn->runtime->hw_free = _snd_kfree;
	snd_pcm_set_mixer(subchn, codec->mixer->device, codec->ac97->me_capture);
	return 0;
}

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

	codec->playback.subchn = NULL;
	snd_pcm_dma_free(subchn);
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	return 0;
}

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

	codec->capture.subchn = NULL;
	snd_pcm_dma_free(subchn);
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0100);
	return 0;
}

static void snd_via686a_pcm_free(void *private_data)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, );
	codec->pcm = NULL;
}

int snd_via686a_pcm(via686a_t * codec, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;
	err = snd_pcm_new(codec->card, "VIA 82C686A", device, 1, 1, &pcm);
	if (err < 0)
		return err;

	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].open = snd_via686a_playback_open;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].close = snd_via686a_playback_close;

	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_via686a_capture_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_via686a_capture_close;

	pcm->private_data = codec;
	pcm->private_free = snd_via686a_pcm_free;
	pcm->info_flags = SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_CAPTURE |
			  SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "VIA 82C686A");
	*rpcm = codec->pcm = pcm;
	return 0;
}

#if 0

/*
 *  PCM code - MIC
 */

static int snd_via686a_playback_fm_ioctl(void *private_data,
					  snd_pcm_subchn_t * subchn,
					  unsigned int cmd,
					  unsigned long *arg)
{
	int result;
	// via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);

	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	if (cmd == SND_PCM_IOCTL1_PARAMS) {
		snd_pcm_runtime_t *runtime = subchn->runtime;

		if (runtime->frags > 32)
			runtime->frags = 32;
	}
	return 0;
}

static int snd_via686a_playback_fm_trigger(void *private_data,
					    snd_pcm_subchn_t * subchn,
					    int cmd)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);

	return snd_via686a_trigger(codec, &codec->playback_fm, cmd);
}

static int snd_via686a_playback_fm_prepare(void *private_data,
					    snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->playback_fm.physbuf = virt_to_bus(runtime->dma_area->buf);
	codec->playback_fm.size = snd_pcm_lib_transfer_size(subchn);
	codec->playback_fm.fragsize = snd_pcm_lib_transfer_fragment(subchn);
	codec->playback_fm.position = 0;
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	snd_ac97_write_lock(codec->ac97, AC97_PCM_FRONT_DAC_RATE, runtime->format.rate);
	/* enable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0000);
	spin_lock_irqsave(&codec->reg_lock, flags);
	snd_via686a_setup_fragments(codec, &codec->playback_fm, runtime);
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static unsigned int snd_via686a_playback_fm_pointer(void *private_data,
						     snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	unsigned int result;

	spin_lock_irqsave(&codec->reg_lock, flags);
	if (inb(VIAREG(codec, FM_STATUS)) & VIA_REG_STAT_ACTIVE) {
		result = codec->playback_fm.position +
			 (codec->playback_fm.fragsize -
			  inl(VIAREG(codec, FM_CURR_COUNT)));
	} else {
		result = 0;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

static snd_pcm_hardware_t snd_via686a_playback_fm =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID |
	SND_PCM_CHNINFO_PAUSE,	/* flags */
	SND_PCM_FMT_S16_LE,	/* hardware formats */
	SND_PCM_RATE_KNOT,	/* rates */
	24000,			/* min. rate */
	24000,			/* max. rate */
	2,			/* min. voices */
	2,			/* max. voices */
	32,			/* min. fragment size */
	128 * 1024,		/* max. fragment size */
	31,			/* fragment align */
	0,			/* FIFO size (unknown) */
	16,			/* transfer block size */
	snd_via686a_playback_fm_ioctl,
	snd_via686a_playback_fm_prepare,
	snd_via686a_playback_fm_trigger,
	snd_via686a_playback_fm_pointer
};

static int snd_via686a_playback_fm_open(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, -ENXIO);
	snd_pcm_hardware_t *hw;
	int err;

	hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(snd_pcm_hardware_t), GFP_KERNEL);
	if (hw == NULL)
		return -ENOMEM;
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma_fm,
				     "VIA 82C686A - FM DAC")) < 0) {
		snd_kfree(hw);
		return err;
	}
	codec->playback_fm.subchn = subchn;
	memcpy(hw, &snd_via686a_playback_fm, sizeof(*hw));
#if 0
	hw->rates = codec->playback_fm.rates;
	if (!(hw->rates & SND_PCM_RATE_8000))
		hw->min_rate = 48000;
#endif
	subchn->runtime->hw = hw;
	subchn->runtime->hw_free = _snd_kfree;
	return 0;
}

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

	codec->playback_fm.subchn = NULL;
	snd_pcm_dma_free(subchn);
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	return 0;
}

static void snd_via686a_pcm_fm_free(void *private_data)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, );
	codec->pcm_fm = NULL;
}

int snd_via686a_pcm_fm(via686a_t * codec, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;
	err = snd_pcm_new(codec->card, "VIA 82C686A - FM DAC", device, 1, 0, &pcm);
	if (err < 0)
		return err;

	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].open = snd_via686a_playback_fm_open;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].close = snd_via686a_playback_fm_close;

	pcm->private_data = codec;
	pcm->private_free = snd_via686a_pcm_fm_free;
	pcm->info_flags = SND_PCM_INFO_PLAYBACK;
	strcpy(pcm->name, "VIA 82C686A - FM DAC");

	*rpcm = codec->pcm_fm = pcm;	
	return 0;
}

#endif

/*
 *  Mixer part
 */

static void snd_via686a_codec_init(void *private_data, ac97_t *ac97)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, );

	if (codec->ac97_ext_id & 0x0001)	/* VRA */
		snd_ac97_write_lock(ac97, AC97_EXTENDED_STATUS, 0x0009);	
	/* disable DAC & ADC power */
	snd_ac97_write_bitmask_lock(ac97, AC97_POWERDOWN, ~0x0300, 0x0300);
	/* disable center DAC/surround DAC/LFE DAC/MIC ADC */
	snd_ac97_write_bitmask_lock(ac97, AC97_EXTENDED_STATUS, (short)~0xe800, 0xe800);
}

static void snd_via686a_mixer_free_ac97(void *private_data)
{
	via686a_t *codec = snd_magic_cast(via686a_t, private_data, );
	codec->mixer = NULL;
}

int snd_via686a_mixer(via686a_t * codec, int device, int pcm_count, int *pcm_devs, snd_kmixer_t ** rmixer)
{
	snd_kmixer_t *mixer;
	ac97_t ac97;
	int err;

	*rmixer = NULL;
	memset(&ac97, 0, sizeof(ac97));
	ac97.write = snd_via686a_codec_write;
	ac97.read = snd_via686a_codec_read;
	ac97.init = snd_via686a_codec_init;
	ac97.private_data = codec;
	ac97.private_free = snd_via686a_mixer_free_ac97;
	if ((err = snd_ac97_mixer(codec->card, device, &ac97, pcm_count, pcm_devs, &mixer)) < 0) {
		return err;
	}
	codec->ac97 = snd_magic_cast(ac97_t, mixer->private_data, -ENXIO);
	*rmixer = codec->mixer = mixer;
	return 0;
}

/*
 *
 */

static int snd_via686a_test_rate(via686a_t *codec, int reg, int rate)
{
	unsigned short val;

	snd_via686a_codec_write(codec, reg, rate);
	val = snd_via686a_codec_read(codec, reg);
	return val == rate;
}

static void snd_via686a_determine_rates(via686a_t *codec, int reg, unsigned int *r_result)
{
	unsigned int result = 0;

	/* test a non-standard frequency */
	if (snd_via686a_test_rate(codec, reg, 11000))
		result |= SND_PCM_RATE_CONTINUOUS;
	/* let's try to obtain standard frequencies */
	if (snd_via686a_test_rate(codec, reg, 8000))
		result |= SND_PCM_RATE_8000;
	if (snd_via686a_test_rate(codec, reg, 11025))
		result |= SND_PCM_RATE_11025;
	if (snd_via686a_test_rate(codec, reg, 16000))
		result |= SND_PCM_RATE_16000;
	if (snd_via686a_test_rate(codec, reg, 22050))
		result |= SND_PCM_RATE_22050;
	if (snd_via686a_test_rate(codec, reg, 32000))
		result |= SND_PCM_RATE_32000;
	if (snd_via686a_test_rate(codec, reg, 44100))
		result |= SND_PCM_RATE_44100;
	if (snd_via686a_test_rate(codec, reg, 48000))
		result |= SND_PCM_RATE_48000;
	*r_result = result;
}

static int snd_via686a_chip_init(via686a_t *codec)
{
	unsigned short ext_id;

	/* disable all legacy ports */
	pci_write_config_byte(codec->pci, 0x42, 0);

	/* deassert ACLink reset */
	pci_write_config_byte(codec->pci, 0x41, 0x40);
	udelay(100);
	/* deassert ACLink reset, force SYNC (warm AC'97 reset) */
	pci_write_config_byte(codec->pci, 0x41, 0x60);
	udelay(2);
	/* ACLink on, deassert ACLink reset, VSR, SGD data out */
	/* note - FM data out has trouble with non VRA codecs !! */
	pci_write_config_byte(codec->pci, 0x41, 0xcc);

	snd_via686a_codec_ready(codec, 0, 1);
	snd_via686a_codec_write(codec, AC97_POWERDOWN, 0x0000);
	snd_via686a_codec_ready(codec, 0, 1);

#if 0
	{
	unsigned char cmdb;

	pci_read_config_byte(codec->pci, 0x40, &cmdb);
	printk("PCI[0x40] = 0x%x\n", cmdb);
	pci_read_config_byte(codec->pci, 0x42, &cmdb);
	printk("PCI[0x42] = 0x%x\n", cmdb);
	pci_read_config_byte(codec->pci, 0x43, &cmdb);
	printk("PCI[0x43] = 0x%x\n", cmdb);
	pci_read_config_byte(codec->pci, 0x44, &cmdb);
	printk("PCI[0x44] = 0x%x\n", cmdb);
	pci_read_config_byte(codec->pci, 0x48, &cmdb);
	printk("PCI[0x48] = 0x%x\n", cmdb);
	}
#endif

	codec->ac97_ext_id = 
	ext_id = snd_via686a_codec_read(codec, AC97_EXTENDED_ID);
	if (ext_id == 0xffff) {
		snd_printk("snd_via686a_chip_init: invalid AC'97 codec\n");
		return -EIO;
	}
	if (ext_id & 0x0001) {
		snd_via686a_codec_write(codec, AC97_EXTENDED_STATUS, 0x0009);
		snd_via686a_determine_rates(codec, AC97_PCM_FRONT_DAC_RATE, &codec->playback.rates);
		snd_via686a_determine_rates(codec, AC97_PCM_LR_ADC_RATE, &codec->capture.rates);
	} else {
		codec->playback.rates = SND_PCM_RATE_48000;
		codec->capture.rates = SND_PCM_RATE_48000;
	}
	if (ext_id & 0x0008) {
		snd_via686a_determine_rates(codec, AC97_PCM_MIC_ADC_RATE, &codec->playback_fm.rates);
	} else {
		codec->playback_fm.rates = SND_PCM_RATE_48000;
	}
	/* disable interrupts */
	snd_via686a_channel_reset(codec, &codec->playback);
	snd_via686a_channel_reset(codec, &codec->capture);
	snd_via686a_channel_reset(codec, &codec->playback_fm);
	return 0;
}

static int snd_via686a_free(via686a_t * codec)
{
	/* disable interrupts */
	snd_via686a_channel_reset(codec, &codec->playback);
	snd_via686a_channel_reset(codec, &codec->capture);
	snd_via686a_channel_reset(codec, &codec->playback_fm);
	/* --- */
	synchronize_irq();
	if (codec->tables)
		snd_kfree(codec->tables);
	snd_magic_kfree(codec);
	return 0;
}

static int __init snd_via686a_create(snd_card_t * card,
				     struct pci_dev *pci,
				     snd_dma_t * dma_pbk,
				     snd_dma_t * dma_cap,
				     snd_dma_t * dma_fm,
				     snd_irq_t * irqptr,
				     via686a_t ** r_via)
{
	via686a_t *codec;
	unsigned short cmdw;
	int err;
        static snd_device_ops_t ops = {
		(snd_dev_free_t *)snd_via686a_free,
		NULL,
		NULL
        };

	if ((codec = snd_magic_kcalloc(via686a_t, 0, GFP_KERNEL)) == NULL)
		return -ENOMEM;

	spin_lock_init(&codec->reg_lock);
	codec->card = card;
	codec->pci = pci;
	codec->dma_pbk = dma_pbk;
	codec->dma_cap = dma_cap;
	codec->dma_fm = dma_fm;
	codec->irqptr = irqptr;
	codec->port = pci_resource_start(pci, 0);
	pci_read_config_word(pci, PCI_COMMAND, &cmdw);
	if ((cmdw & PCI_COMMAND_IO) != PCI_COMMAND_IO) {
		cmdw |= PCI_COMMAND_IO;
		pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
#if 0
	{
	unsigned char cmdb;
	pci_set_master(pci);
	pci_read_config_byte(pci, PCI_LATENCY_TIMER, &cmdb);
	if (cmdb < 64)
		cmdb = 64;
	pci_write_config_byte(pci, PCI_LATENCY_TIMER, cmdb);
	}
#endif
	pci_read_config_byte(pci, PCI_REVISION_ID, &codec->revision);
	synchronize_irq();

	/* initialize offsets */
	codec->playback.reg_offset = VIA_REG_PLAYBACK_STATUS;
	codec->capture.reg_offset = VIA_REG_CAPTURE_STATUS;
	codec->playback_fm.reg_offset = VIA_REG_FM_STATUS;

	/* allocate buffer descriptor lists */
	/* the start of each lists must be aligned to 8 bytes */
	codec->tables = (unsigned int *)snd_kcalloc(8 + sizeof(unsigned int) * 32 * 2, GFP_KERNEL);
	if (codec->tables == NULL) {
		snd_via686a_free(codec);
		return -ENOMEM;
	}
	codec->playback.table = (unsigned int *)(((unsigned long)codec->tables + 7) & ~7);
	codec->capture.table = codec->playback.table + 32 * 2;
	codec->playback_fm.table = codec->capture.table + 32 * 2;

	if ((err = snd_via686a_chip_init(codec)) < 0) {
		snd_via686a_free(codec);
		return err;
	}

	if ((err = snd_device_new(card, SND_DEV_LOWLEVEL, codec, 0, &ops, NULL)) < 0) {
		snd_via686a_free(codec);
		return err;
	}

	*r_via = codec;
	return 0;
}

static void snd_via686a_use_inc(snd_card_t * card)
{
	MOD_INC_USE_COUNT;
}

static void snd_via686a_use_dec(snd_card_t * card)
{
	MOD_DEC_USE_COUNT;
}

static int __init snd_via686a_detect(snd_card_t * card, via686a_card_t *scard,
				      unsigned short vendor, unsigned short device)
{
	if ((scard->pci = pci_find_device(vendor, device, scard->pci)) == NULL)
		return -ENODEV;
	if (snd_register_ioport(card, pci_resource_start(scard->pci, 0), 256, "VIA 82C686A - AC'97", NULL) < 0)
		goto __nodev;
	return 0;
      __nodev:
	snd_unregister_ioports(card);
	return -ENODEV;
}

static int __init snd_via686a_resources(snd_card_t * card,
					via686a_card_t *scard,
					int dev)
{
	int err;

	if ((err = snd_register_interrupt(card,
			"VIA 82C686A", scard->pci->irq,
			SND_IRQ_TYPE_PCI, snd_via686a_card_interrupt,
			scard, NULL, &scard->irqptr)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"VIA 82C686A - playback", 0,
			SND_DMA_TYPE_PCI, snd_pbk_frame_size[dev],
			NULL, &scard->dma_pbk)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"VIA 82C686A - capture", 1,
			SND_DMA_TYPE_PCI, snd_cap_frame_size[dev],
			NULL, &scard->dma_cap)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"VIA 82C686A - FM playback", 2,
			SND_DMA_TYPE_PCI, snd_fm_frame_size[dev],
			NULL, &scard->dma_fm)) < 0)
		return err;
	return 0;
}

static int __init snd_via686a_probe(int dev, via686a_card_t *scard)
{
	snd_card_t *card;
	snd_pcm_t *pcm = NULL, *pcm_fm = NULL;
	snd_kmixer_t *mixer = NULL;
	snd_rawmidi_t *midi_uart = NULL;
	via686a_t *codec;
	int pcm_dev = 0, mixer_dev = 0;
	unsigned char legacy;
	unsigned char legacy_cfg;
	int rev_h = 0;

	card = snd_card_new(snd_index[dev], snd_id[dev],
			    snd_via686a_use_inc, snd_via686a_use_dec);
	if (card == NULL)
		return -ENOMEM;
	card->type = SND_CARD_TYPE_VIA82C686A;
	do {
		if (!snd_via686a_detect(card, scard, PCI_VENDOR_ID_VIA, PCI_DEVICE_ID_VIA_82C686_5))
			break;
	} while (scard->pci);
	if (scard->pci != NULL)
		goto __found;
	snd_card_free(card);
	return -ENODEV;

      __found:
	if (pci_enable_device(scard->pci)) {
		snd_card_free(card);
		return -ENODEV;
	}
	if (snd_via686a_resources(card, scard, dev) < 0) {
		snd_card_free(card);
		return -ENODEV;
	}

	pci_read_config_byte(scard->pci, 0x42, &legacy);
	pci_read_config_byte(scard->pci, 0x43, &legacy_cfg);

	if (snd_via686a_create(card,
			       scard->pci,
			       scard->dma_pbk,
			       scard->dma_cap,
			       scard->dma_fm,
			       scard->irqptr,
			       &scard->codec) < 0)
		goto __nodev;
	codec = scard->codec;

	if (snd_via686a_mixer(codec, mixer_dev++, 1, &pcm_dev, &mixer) < 0)
		goto __nodev;
	if (snd_via686a_pcm(codec, pcm_dev++, &pcm) < 0)
		goto __nodev;
#if 0
	if (snd_via686a_pcm_fm(codec, pcm_dev++, &pcm_fm) < 0)
		goto __nodev;
#endif

	pci_write_config_byte(scard->pci, 0x42, legacy);
	pci_write_config_byte(scard->pci, 0x43, legacy_cfg);
	if (codec->revision >= 0x20) {
		if (snd_register_ioport(card, pci_resource_start(scard->pci, 2), 4, "VIA 82C686A - MPU401 config", NULL) < 0) {
			rev_h = 0;
			legacy &= ~0x40;
		}
		rev_h = 1;
	}
	if (snd_joystick[dev] > 0) {
		if (rev_h && (legacy & 0x40) && (legacy & 8) == 0)
			outw(0x200, pci_resource_start(scard->pci, 2) + 2);
		legacy |= 8;
	}
	if (rev_h && (legacy & 0x40) && snd_mpu_port[dev] >= 0x200) { /* rev H+ */
		legacy |= 0x02;	/* enable MPU */
		outw(snd_mpu_port[dev], pci_resource_start(scard->pci, 2) + 0);
	} else {
		if (rev_h & (legacy & 0x40) && (legacy & 0x02)) {
			snd_mpu_port[dev] = inw(pci_resource_start(scard->pci, 2) + 0);
		} else {
			switch ((legacy >> 2) & 3) {
			case 0:		snd_mpu_port[dev] = 0x300; break;
			case 1:		snd_mpu_port[dev] = 0x310; break;
			case 2:		snd_mpu_port[dev] = 0x320; break;
			case 3:		snd_mpu_port[dev] = 0x330; break;
			}
		}
	}
	switch (snd_mpu_port[dev]) {
	case 0x300:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 0 << 2; legacy |= 0x02; break;
	case 0x310:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 1 << 2; legacy |= 0x02; break;
	case 0x320:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 2 << 2; legacy |= 0x02; break;
	case 0x330:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 3 << 2; legacy |= 0x02; break;
	}
	pci_write_config_byte(scard->pci, 0x42, legacy);
	pci_write_config_byte(scard->pci, 0x43, legacy_cfg);
	if (legacy & 0x02) {
		if (snd_register_ioport(card, snd_mpu_port[dev], 2, "VIA 82C686A - MPU401", NULL) < 0) {
			snd_printk("VIA686A: unable to register MPU-401 port at 0x%x, skipping\n", snd_mpu_port[dev]);
			legacy &= ~0x02;
			pci_write_config_byte(scard->pci, 0x42, legacy);
			goto __skip_mpu;
		}
		if (snd_mpu401_uart_new(card, 0, MPU401_HW_VIA686A,
					snd_mpu_port[dev],
					-1,
					&midi_uart) < 0) {
			snd_printk("VIA686A: unable to initialize MPU-401 at 0x%x, skipping\n", snd_mpu_port[dev]);
			legacy &= ~0x02;
			pci_write_config_byte(scard->pci, 0x42, legacy);
		}
	      __skip_mpu:
	}
	
	strcpy(card->abbreviation, "VIA686A");
	strcpy(card->shortname, "VIA 82C686A");
	
	sprintf(card->longname, "%s at 0x%lx, irq %li",
		card->shortname, codec->port, scard->irqptr->irq);

	if (!snd_card_register(card)) {
		scard->card = card;
		scard->mixer = mixer;
		scard->pcm = pcm;
		scard->pcm_fm = pcm_fm;
		scard->midi_uart = midi_uart;
		return 0;
	}
	goto __nodev;

      __nodev:
	pci_write_config_byte(scard->pci, 0x42, legacy);
	pci_write_config_byte(scard->pci, 0x43, legacy_cfg);
	snd_card_free(card);
	return -ENXIO;
}

#ifdef MODULE

static int __exit snd_via686a_card_free(int dev)
{
	via686a_card_t *scard;

	scard = snd_via686a_cards[dev];
	snd_via686a_cards[dev] = NULL;
	if (scard) {
		snd_card_unregister(scard->card);
		snd_kfree(scard);
	}
	return 0;
}

#endif

int __init alsa_card_via686a_init(void)
{
	int dev, cards;
	via686a_card_t *scard;

	for (dev = cards = 0; dev < SND_CARDS; dev++) {
		scard = (via686a_card_t *)
				snd_kcalloc(sizeof(via686a_card_t), GFP_KERNEL);
		if (scard == NULL)
			continue;
		if (snd_via686a_probe(dev, scard) < 0) {
			snd_kfree(scard);
			break;
		}
		snd_via686a_cards[dev] = scard;
		cards++;
	}
	if (!cards) {
#ifdef MODULE
		snd_printk("VIA 82C686A soundcard #%i not found or device busy\n", dev + 1);
#endif
		return -ENODEV;
	}
	return 0;
}

void __exit alsa_card_via686a_exit(void)
{
	int dev;

	for (dev = 0; dev < SND_CARDS; dev++)
		snd_via686a_card_free(dev);
}

module_init(alsa_card_via686a_init)
module_exit(alsa_card_via686a_exit)
