/*
 *  Copyright (c) by Jaromir Koutek <miri@punknet.cz>,
 *                   Jaroslav Kysela <perex@jcu.cz>,
 *                   Thomas Sailer <sailer@ife.ee.ethz.ch>
 *  Driver for very cheap (and noisy) ESS Solo-1.
 *  Mine has not even a pre-amplifier... Everything sound like from telephone.
 *  Documentation is at ftp://ftp.esstech.com.tw/PCIAudio/Solo1/.
 *  Based on s3_86c617.c source.
 *
 *  BUGS:
 *    many
 *
 *  TODO:
 *    MPU401, OPL3, ADC
 *
 *   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 "esssolo1.h"
#include "sndpci.h"
#include "dma.h"

/* #define SOLO_MDEBUG */
/* #define SOLO_IDEBUG */
/* #define SOLO_DEBUGIO */
/* #define SOLO_PDEBUG */
/* #define SOLO_DDEBUG */

#ifdef SOLO_DEBUGIO
#define SL_OUTB dbg_outb
#define SL_OUTW dbg_outw
#define SL_OUTL dbg_outl
#define SL_INB dbg_inb
#define SL_INW dbg_inw
static void dbg_outb(unsigned char val, int port)
{
	snd_printk("outb: 0x%x, 0x%x\n", val, port);
	outb(val, port);
}

static void dbg_outw(unsigned int val, int port)
{
	snd_printk("outw: 0x%x, 0x%x\n", val, port);
	outw(val, port);
}

static void dbg_outl(unsigned int val, int port)
{
	snd_printk("outl: 0x%x, 0x%x\n", val, port);
	outl(val, port);
}

static unsigned char dbg_inb(int port)
{
	unsigned char val = inb(port);
	snd_printk("inb: 0x%x, 0x%x\n", val, port);
	return val;
}

static unsigned char dbg_inw(int port)
{
	unsigned int val = inw(port);
	snd_printk("inw: 0x%x, 0x%x\n", val, port);
	return val;
}
#else
#define SL_OUTB outb
#define SL_OUTW outw
#define SL_OUTL outl
#define SL_INB inb
#define SL_INW inw
#endif

#if 0
static void snd_solo_outbmask(int mask, int val, int reg)
{
	SL_OUTB((val & mask) | (inb(reg) & (~mask)), reg);
}
#endif

static void snd_solo_mixer_out(esssolo_t * solo, int reg, int mask, int val)
{
	int a;
#ifdef SOLO_MDEBUG
	snd_printk("snd_solo_mixer_out: r: 0x%x, m: 0x%x, v: 0x%x\n", reg, mask, val);
#endif
	outb(reg, SLSB_REG(solo, MIXERADDR));
	a = (mask != 0xff) ? (inb(SLSB_REG(solo, MIXERDATA)) & (~mask)) : 0;
	outb((val & mask) | a, SLSB_REG(solo, MIXERDATA));
}

static int snd_solo_mixer_in(esssolo_t * solo, int reg)
{
	SL_OUTB(reg, SLSB_REG(solo, MIXERADDR));
	return SL_INB(SLSB_REG(solo, MIXERDATA));
}

static void snd_solo_write_cmd(esssolo_t * solo, int cmd)
{
	int i, v;
	snd_printd("snd_solo_write_cmd: 0x%x\n", cmd);
#define WRITE_LOOP_TIMEOUT 1000
	for (i = 0; i < WRITE_LOOP_TIMEOUT; i++) {
		if (!(v = inb(SLSB_REG(solo, READSTATUS)) & 0x80)) {
			outb(cmd, SLSB_REG(solo, WRITEDATA));
			return;
		}
		snd_delay(1);
	}
	snd_printk("snd_solo_write_cmd timeout (0x02%x/0x02%x)\n", cmd, v);
}
static int snd_solo_get_byte(esssolo_t * solo)
{
	int i, v;
#define GET_LOOP_TIMEOUT 100
	for (i = GET_LOOP_TIMEOUT; i; i--)
		if ((v = inb(SLSB_REG(solo, READSTATUS))) & 0x40)
			return inb(SLSB_REG(solo, READDATA));
	snd_printk("snd_solo_get_byte timeout: status 0x02%x\n", v);
	return -ENODEV;
}

static void snd_solo_write_reg(esssolo_t * solo, int reg, int val)
{
	snd_solo_write_cmd(solo, reg);
	snd_solo_write_cmd(solo, val);
}

static int snd_solo_read_cmd(esssolo_t * solo, int cmd)
{
	int v;

	snd_solo_write_reg(solo, SL_CMD_READREG, cmd);
	v = snd_solo_get_byte(solo);
	snd_printd("snd_solo_read_cmd: 0x%x 0x%x\n", cmd, v);
	return v;
}

static void snd_solo_mask_irq(esssolo_t * solo, unsigned char mask, unsigned char val)
{
	unsigned char i = solo->irqmask &= ~mask;
	i |= val;
	SL_OUTB(i, SLIO_REG(solo, IRQCONTROL));
	solo->irqmask = i;
#ifdef SOLO_IDEBUG
	snd_printk("irq mask: %#x\n",i);
#endif
}

#define SOLO_CLK1 768000
#define SOLO_CLK0 793800
#define SOLO_FILTERCLK 7160000

static unsigned int snd_solo_set_dac_rate(esssolo_t * solo, unsigned int rate, int pcmnum, int set)
{
	int x0, x1, r0, r1, which;
	if (rate > 48000)
		rate = 48000;
	if (rate < 6000)
		rate = 6000;
	x0 = SOLO_CLK0 / rate;
	x1 = SOLO_CLK1 / rate;
	r0 = SOLO_CLK0 / x0;
	r1 = SOLO_CLK1 / x1;
	which = abs(r0 - rate) < abs(r1 - rate) ? 0 : 128;
	if (which) {
		x0 = x1;
		r0 = r1;
	}
	x0 = which | (128 - x0);
	if (set) {
		int f = (((10 * SOLO_FILTERCLK) / (8 * 82 * r0)) & 0xff);
#ifdef SOLO_PDEBUG
		int f0 = SOLO_FILTERCLK / f / 82;
		snd_printk("snd_solo_set_dac_rate: rate: %d (0x%x), filter: %d (0x%x)\n", r0, x0, f0, 256 - f);
#endif
		if (pcmnum) {
			snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2SAMPLE, 0xff, x0);
			snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2FILTER, 0xff, 256 - f);
		} else {
			snd_solo_write_cmd(solo, SL_CMD_EXTSAMPLERATE);
			snd_solo_write_cmd(solo, x0);
			snd_solo_write_cmd(solo, SL_CMD_FILTERDIV);
			snd_solo_write_cmd(solo, 256 - f);
		}
	}
	return r0;
}

static void snd_solo_trigger(esssolo_t * solo, int up, int pcmnum)
{
	snd_mutex_lock(solo, reg);
#ifdef SOLO_PDEBUG
	snd_printk("snd_solo_trigger: %i, %i\n", pcmnum, up);
#endif
	if (!pcmnum) {
		unsigned char h;
		h = snd_solo_read_cmd(solo, SL_CMD_DMACONTROL) | (up ? 1: 0);
		snd_solo_write_reg(solo, SL_CMD_DMACONTROL, h);
		snd_delay(100);
		snd_solo_write_cmd(solo, up ? SL_CMD_ENABLEAUDIO1 : SL_CMD_STOPAUDIO1);
		snd_solo_write_cmd(solo, up ? SL_CMD_CONTDMA : SL_CMD_PAUSEDMA);
#if 0
		snd_solo_write_cmd(solo, SL_CMD_AUDIO1STATUS);
#define XREAD_LOOP_TIMEOUT 1000
		{
		int i;
		for (i = 0; i < XREAD_LOOP_TIMEOUT; i++) {
			if (inb(SLSB_REG(solo, STATUS)) & 0x80)
				break;
			snd_delay(1);
		}
		if (i == XREAD_LOOP_TIMEOUT) {
			snd_printk("snd_solo_read_cmd timeout\n");
			goto __end;
		}
		h = inb(SLSB_REG(solo, READDATA));

		snd_printk("dma enable: 0x%x\n", h);
		}
#endif
	} else {
		snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2CONTROL1, 3, up ? 3 : 0);
		SL_OUTB(up ? 0xa : 8, SLIO_REG(solo, AUDIO2MODE));
	}
#if 0
__end:
#endif
	snd_mutex_unlock(solo, reg);
}

/*
 *  PCM part
 */

static int snd_solo_playback_ioctl(snd_pcm1_t * pcm1, unsigned int cmd, unsigned long *arg)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;

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

static int snd_solo_playback2_ioctl(snd_pcm1_t * pcm1, unsigned int cmd, unsigned long *arg)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;

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

static void snd_solo_playback_trigger(snd_pcm1_t * pcm1, int up)
{
	snd_solo_trigger((esssolo_t *) pcm1->private_data, up, 0);
}

static void snd_solo_playback2_trigger(snd_pcm1_t * pcm1, int up)
{
	snd_solo_trigger((esssolo_t *) pcm1->private_data, up, 1);
}

static void snd_solo_playback_prepare(snd_pcm1_t * pcm1,
				      unsigned char *buffer,
				      unsigned int size,
				      unsigned int offset,
				      unsigned int count)
{
	unsigned long h;
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;
	int u, is8, mono;

	mono = (pcm1->playback.voices > 1) ? 0 : 1;
	is8 = (pcm1->playback.mode & SND_PCM1_MODE_16) ? 0 : 1;
	u = pcm1->playback.mode & SND_PCM1_MODE_U;

	snd_mutex_lock(solo, reg);

#ifdef SOLO_PDEBUG
	snd_printk("snd_solo_playback_prepare: buf: 0x%x, size: 0x%x, ofs: 0x%x, cnt: 0x%x\n",
		(unsigned int) virt_to_bus(buffer), size, offset, count);
#endif

	count = 0x10000 - count;

	snd_solo_mask_irq(solo, 0x10, 0);

	/* 3. program direction and type */
	snd_solo_write_reg(solo, SL_CMD_DMACONTROL, 4);
	h = (snd_solo_read_cmd(solo, SL_CMD_ANALOGCONTROL) & 0xfc) | (mono ? 2 : 1);
	snd_solo_write_reg(solo, SL_CMD_ANALOGCONTROL, h);
	snd_solo_write_reg(solo, SL_CMD_DMATYPE, 2);

	/* 4. set clock and counters */
	snd_solo_set_dac_rate(solo, pcm1->playback.real_rate, 0, 1);
	snd_solo_write_reg(solo, SL_CMD_DMACNTRELOADL, count & 0xff);
	snd_solo_write_reg(solo, SL_CMD_DMACNTRELOADH, count >> 8);

	/* 5. initialize and configure DACs */
	snd_solo_write_reg(solo, SL_CMD_SETFORMAT, u ? 0x80 : 0);
	snd_solo_write_reg(solo, SL_CMD_SETFORMAT2, u ? 0x51 : 0x71);
	snd_solo_write_reg(solo, SL_CMD_SETFORMAT2,
	      0x90 | (u ? 0 : 0x20) | (is8 ? 0 : 4) | (mono ? 0x40 : 8));

	/* 6. enable/select DMA channel and IRQ channel */
	snd_solo_write_reg(solo, SL_CMD_IRQCONTROL, 0x50);
	snd_solo_write_reg(solo, SL_CMD_DRQCONTROL, 0x50);

	/* 7. configure system interrupt controller and DMA controller */
	SL_OUTB(0xff, SLDM_REG(solo, DMACLEAR));
	SL_OUTB(1, SLDM_REG(solo, DMAMASK));
	SL_OUTB(0x18, SLDM_REG(solo, DMAMODE));
	SL_OUTL(virt_to_bus(buffer), SLDM_REG(solo, DMABASE));
	SL_OUTW(size, SLDM_REG(solo, DMACOUNT));
	SL_OUTB(0xc4, SLDM_REG(solo, DMACOMMAND));
	SL_OUTB(0, SLDM_REG(solo, DMAMASK));

	/* 8. start DMA */
/*      h = snd_solo_read_cmd(solo, SL_CMD_DMACONTROL) | 1;
 *       printk("startdma: 0x%x\n", h);
 *      snd_solo_write_cmd(solo, SL_CMD_DMACONTROL);
 *      snd_solo_write_cmd(solo, h); */

	/* 9. delay 100ms, enable DAC input */
	/* snd_delay(10); *//* !!! FIXME !!! */
//    snd_solo_write_cmd(solo, SL_CMD_ENABLEAUDIO1);

	snd_solo_mask_irq(solo, 0xf0, 0xf0);

	snd_mutex_unlock(solo, reg);

}

static void snd_solo_playback2_prepare(snd_pcm1_t * pcm1,
				       unsigned char *buffer,
				       unsigned int size,
				       unsigned int offset,
				       unsigned int count)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;
	int u, is8, mono, save;

	mono = (pcm1->playback.voices > 1) ? 0 : 1;
	is8 = (pcm1->playback.mode & SND_PCM1_MODE_16) ? 0 : 1;
	u = pcm1->playback.mode & SND_PCM1_MODE_U;

	snd_mutex_lock(solo, reg);

#ifdef SOLO_PDEBUG
	snd_printk("snd_solo_playback2_prepare: buf: 0x%x, size: 0x%x, ofs: 0x%x, cnt: 0x%x\n",
		(unsigned int) virt_to_bus(buffer), size, offset, count);
#endif

	count >>= 1;
	count = 0x10000 - count;
	/* 1. reset */

	save = snd_solo_mixer_in(solo, SLSB_IREG_AUDIO2);

	snd_solo_mask_irq(solo, 0x20, 0);

	/* 2. program auto-init dma */
	snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2CONTROL1, 0xff, 0x10);

	/* 3. set clock and counters */
	snd_solo_set_dac_rate(solo, pcm1->playback.real_rate, 1, 1);
	snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2TCOUNTL, 0xff, count & 0xff);
	snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2TCOUNTH, 0xff, count >> 8);

	/* 4. initialize and configure Audio 2 DAC */
	snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2CONTROL2, 0xff, 0x40 | (u ? 0 : 4) | (mono ? 0 : 2) | (is8 ? 0 : 1));

	snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2MODE, 0xff, 0x22);

	/* 5. enable IRQ channel */
	snd_solo_mask_irq(solo, 0x20, 0x20);

	/* 6. program DMA */
	SL_OUTL(virt_to_bus(buffer), SLIO_REG(solo, AUDIO2DMAADDR));
	SL_OUTW(size, SLIO_REG(solo, AUDIO2DMACOUNT));
	SL_OUTB(0x8, SLIO_REG(solo, AUDIO2MODE));

	/* 7. start DMA */
/*  snd_solo_mixer_out( solo, SLSB_IREG_AUDIO2CONTROL1, 3, 3 ); */

	/* 8. delay, enable Audio 2 DAC playback */
	/*  snd_delay( 10 ); *//* !!! FIXME !!! */
	snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2, 0xff, save);

	snd_mutex_unlock(solo, reg);

}

static int snd_solo_playback_open(snd_pcm1_t * pcm1)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, solo->dma1ptr, "ESS Solo-1 (playback)")) < 0)
		return err;
	return 0;
}

static int snd_solo_playback2_open(snd_pcm1_t * pcm1)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, solo->dma2ptr, "ESS Solo-1 (playback 2)")) < 0)
		return err;
	return 0;
}

static void snd_solo_playback_close(snd_pcm1_t * pcm1)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, solo->dma1ptr);
}

static void snd_solo_playback2_close(snd_pcm1_t * pcm1)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, solo->dma2ptr);
}

static unsigned int snd_solo_playback2_pointer(snd_pcm1_t * pcm1, unsigned int used_size)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;
	int v;

	v = used_size - SL_INW(SLIO_REG(solo, AUDIO2DMACOUNT));
#ifdef SOLO_PDEBUG
	snd_printk("snd_solo_playback2_pointer: us: 0x%x, v: 0x%x\n", used_size, v);
#endif
	return v;
}

static unsigned int snd_solo_playback_pointer(snd_pcm1_t * pcm1, unsigned int used_size)
{
	esssolo_t *solo = (esssolo_t *) pcm1->private_data;
	int v;
	v = used_size - SL_INW(SLDM_REG(solo, DMACOUNT));
#ifdef SOLO_PDEBUG
	snd_printk("snd_solo_playback_pointer: us: 0x%x, v: 0x%x\n", used_size, v);
#endif
	return v;
}

static struct snd_stru_pcm1_hardware snd_solo_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 */
	6000,			/* min. rate */
	48000,			/* max. rate */
	2,			/* max. voices */
	snd_solo_playback_open,
	snd_solo_playback_close,
	snd_solo_playback_ioctl,
	snd_solo_playback_prepare,
	snd_solo_playback_trigger,
	snd_solo_playback_pointer,
	snd_pcm1_playback_dma_ulaw,
	snd_pcm1_dma_move,
	snd_pcm1_playback_dma_neutral
};

static struct snd_stru_pcm1_hardware snd_solo_playback2 =
{
	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 */
	6000,			/* min. rate */
	48000,			/* max. rate */
	2,			/* max. voices */
	snd_solo_playback2_open,
	snd_solo_playback2_close,
	snd_solo_playback2_ioctl,
	snd_solo_playback2_prepare,
	snd_solo_playback2_trigger,
	snd_solo_playback2_pointer,
	snd_pcm1_playback_dma_ulaw,
	snd_pcm1_dma_move,
	snd_pcm1_playback_dma_neutral
};

static void snd_solo_pcm_free(void *private_data)
{
	esssolo_t *solo = (esssolo_t *) private_data;
	solo->pcm = NULL;
}

static void snd_solo_pcm2_free(void *private_data)
{
	esssolo_t *solo = (esssolo_t *) private_data;
	solo->pcm2 = NULL;
}

snd_pcm_t *snd_solo_pcm(esssolo_t * solo)
{
	snd_pcm_t *pcm;
	snd_pcm1_t *pcm1;

	pcm = snd_pcm1_new_device(solo->card, "es1938");
	if (!pcm)
		return NULL;
	pcm1 = (snd_pcm1_t *) pcm->private_data;
	memcpy(&pcm1->playback.hw, &snd_solo_playback2, sizeof(snd_solo_playback));
	pcm1->private_data = solo;
	pcm1->private_free = snd_solo_pcm2_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP | SND_PCM_INFO_TRIGGER |
	    SND_PCM_INFO_PLAYBACK;
	strcpy(pcm->name, "ESS Solo-1");
	return solo->pcm2 = pcm;
}

snd_pcm_t *snd_solo_pcm2(esssolo_t * solo)
{
	snd_pcm_t *pcm;
	snd_pcm1_t *pcm1;

	pcm = snd_pcm1_new_device(solo->card, "es1938/2");
	if (!pcm)
		return NULL;
	pcm1 = (snd_pcm1_t *) pcm->private_data;
	memcpy(&pcm1->playback.hw, &snd_solo_playback, sizeof(snd_solo_playback));
	pcm1->private_data = solo;
	pcm1->private_free = snd_solo_pcm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP | SND_PCM_INFO_TRIGGER |
	    SND_PCM_INFO_PLAYBACK;
	strcpy(pcm->name, "ESS Solo-1 (2)");
	return solo->pcm = pcm;
}

/*
 *  Mixer part
 */

#define SOLO_PRIVATE( ireg, oreg, recsrc ) (((ireg) << 16) | ((oreg) << 8) | (recsrc))
#define SOLO_PRIVATE_RECSRC( x ) ( (x) & 0xff )
#define SOLO_PRIVATE_OREG( x ) ( ( (x) >> 8 ) & 0xff )
#define SOLO_PRIVATE_IREG( x ) ( ( (x) >> 16 ) & 0xff )

static int snd_solo_volume_level(snd_kmixer_t * mixer,
				 snd_kmixer_channel_t * channel,
				 unsigned int type, int left, int right)
{
	esssolo_t *solo;
	unsigned char rg;
  
	switch(type) {
		case SND_MIX_VOL_OUTPUT:
			rg = SOLO_PRIVATE_OREG((int) (channel->hw.private_value));
			solo = (esssolo_t *) mixer->private_data;
			snd_mutex_lock(solo, reg);
			if (rg == SLSB_IREG_MASTER) {
				/* hw mute */
				snd_solo_mixer_out(solo, SLSB_IREG_MASTER_LEFT, 0x3f, left);
				snd_solo_mixer_out(solo, SLSB_IREG_MASTER_RIGHT, 0x3f, right);
			} else {
				snd_solo_mixer_out(solo, rg, 0xf0, left << 4);
				snd_solo_mixer_out(solo, rg, 0x0f, right & 0x0f);
			}
			snd_mutex_unlock(solo, reg);
#ifdef SOLO_MDEBUG
		    snd_printk("set_volume: '%s': left = 0x%x, right = 0x%x\n", channel->hw.name, snd_solo_mixer_in(solo, rg), /* channel -> hw.stereo ? */ snd_solo_mixer_in(solo, rg) /* : 0 */ );
#endif
			return 0;
			break;
		case SND_MIX_VOL_INPUT:
			rg = SOLO_PRIVATE_IREG((int) (channel->hw.private_value));
			solo = (esssolo_t *) mixer->private_data;
			snd_mutex_lock(solo, reg);
			snd_solo_mixer_out(solo, rg, 0xf0, left << 4);
			snd_solo_mixer_out(solo, rg, 0x0f, right & 0x0f);
			snd_mutex_unlock(solo, reg);
#ifdef SOLO_MDEBUG
		    snd_printk("set_volume: '%s': left = 0x%x, right = 0x%x\n", channel->hw.name, snd_solo_mixer_in(solo, rg), /* channel -> hw.stereo ? */ snd_solo_mixer_in(solo, rg) /* : 0 */ );
#endif
		    return 0;
  }
  return -EINVAL;
}

static int snd_solo_control(snd_kmixer_t * mixer,
			    snd_kmixer_channel_t * channel,
			    unsigned int ctrl, long val)
{
	esssolo_t *solo;
	int rg;

	switch (ctrl) {
	case SND_MIX_CTRL_MUTE_OUT:
		rg = SOLO_PRIVATE_OREG((int) (channel->hw.private_value));
		solo = (esssolo_t *) mixer->private_data;
		if (rg == SLSB_IREG_MASTER) {
			/* hw mute */
			snd_mutex_lock(solo, reg);
			snd_solo_mixer_out(solo, SLSB_IREG_MASTER_LEFT, 0x40, (val & SND_MIX_MUTE_LEFT) ? 0x40 : 0);
			snd_solo_mixer_out(solo, SLSB_IREG_MASTER_RIGHT, 0x40, (val & SND_MIX_MUTE_RIGHT) ? 0x40 : 0);
			snd_mutex_unlock(solo, reg);
#ifdef SOLO_MDEBUG
			snd_printk("set_mute: '%s': %li, left = 0x%x, right = 0x%x\n", channel->hw.name, val, snd_solo_mixer_in(solo, SLSB_IREG_MASTER_LEFT), /* channel -> hw.stereo ? */ snd_solo_mixer_in(solo, SLSB_IREG_MASTER_RIGHT) /* : 0  */ );
#endif
		} else {
			return -EINVAL;
		}
		return 0;
	case SND_MIX_CTRL_MUTE_IN:
		rg = SOLO_PRIVATE_RECSRC((int) (channel->hw.private_value));
		solo = (esssolo_t *) mixer->private_data;
		snd_mutex_lock(solo, reg);
		snd_solo_mixer_out(solo, SLSB_IREG_RECSRC, 7, val ? rg : SL_RECSRC_NONE);
		snd_mutex_unlock(solo, reg);
#ifdef SOLO_MDEBUG
		snd_printk("recsrc: '%s': 0x%x, 0x%lx, read = 0x%x\n", channel->hw.name, rg, val, snd_solo_mixer_in(solo, SLSB_IREG_RECSRC));
#endif
		return 0;
	}
	snd_printd("control einval: '%s': 0x%x, 0x%lx\n", channel->hw.name, ctrl, val);
	return -EINVAL;
}

static int snd_solo_3d_mute(snd_kmixer_t * mixer,
			    snd_kmixer_channel_t * channel,
			    unsigned int ctrl, long val)
{
	esssolo_t *solo;
	int v;

	if (ctrl == (SND_MIX_CTRL_MUTE_OUT)) {
		solo = (esssolo_t *) mixer->private_data;
		snd_mutex_lock(solo, reg);
		v = val ? 0 : 0x0c;
		snd_solo_mixer_out(solo, SLSB_IREG_SPATCONTROL, 0xff, v);
		snd_mutex_unlock(solo, reg);
		return 0;
	}
#ifdef SOLO_MDEBUG
	snd_printk("3d control einval: '%s': 0x%x, 0x%lx\n", channel->hw.name, ctrl, val);
#endif
	return -EINVAL;
}

static int snd_solo_3d_volume(snd_kmixer_t * mixer,
			      snd_kmixer_channel_t * channel,
			      unsigned int type, int left, int right)
{
	esssolo_t *solo;

	solo = (esssolo_t *) mixer->private_data;
	snd_mutex_lock(solo, reg);
	snd_solo_mixer_out(solo, SLSB_IREG_SPATLEVEL, 0xff, left);
	snd_mutex_unlock(solo, reg);
	return 0;
}

static struct snd_stru_mixer_channel_hw snd_solo_mixs[] =
{
	{
		SND_MIXER_PRI_MASTER,	/* priority */
		SND_MIXER_PRI_PARENT,	/* parent priority */
		SND_MIXER_ID_MASTER,	/* device name */
		SND_MIXER_OSS_VOLUME,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT,
		/* Output capabilities */
        SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_HWMUTE,
		/* Input capabilities */
		0, 
		0, 63,		/* omin, omax value */
		-4725, 0,	/* omin, omax dB */
		0, 0,		/* imin, imax value */
		0, 0,		/* imin, imax dB */
		snd_solo_control,	/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(0 ,SLSB_IREG_MASTER, SL_RECSRC_NONE)
	},
	{
		SND_MIXER_PRI_CD,	/* priority */
		SND_MIXER_PRI_PARENT,	/* parent priority */
		SND_MIXER_ID_CD,	/* device name */
		SND_MIXER_OSS_CD,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT | SND_MIXER_CINFO_CAP_INPUT,
		/* Output capabilities */
	    SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME,
		/* Input capabilities */
		SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME | 
		SND_MIXER_CINFO_DCAP_EXCLUSIVE | SND_MIXER_CINFO_DCAP_HWMUTE | 
		SND_MIXER_CINFO_DCAP_JOINMUTE,
		0, 15,		/* omin, omax value */
		-2850, 300,	/* omin, omax dB */
		0, 15,		/* imin, imax value */
		-2850, 300, /* imin, imax dB */
		snd_solo_control,	/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(SLSB_IREG_AUXACD_RVOL, SLSB_IREG_AUXACD, SL_RECSRC_AUXACD)
	},
	{
		SND_MIXER_PRI_LINE,	/* priority */
		SND_MIXER_PRI_PARENT,	/* parent priority */
		SND_MIXER_ID_LINE,	/* device name */
		SND_MIXER_OSS_LINE,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT | SND_MIXER_CINFO_CAP_INPUT |
		SND_MIXER_CINFO_CAP_EXTINPUT,
		/* Output capabilities */
   	    SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME,
		/* Input capabilities */
		SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_EXCLUSIVE | SND_MIXER_CINFO_DCAP_HWMUTE |
        SND_MIXER_CINFO_DCAP_JOINMUTE,
		0, 15,		/* omin, omax value */
		-2850, 300,	/* omin, omax dB */
		0, 15,		/* imin, imax value */
  	    -2850, 300,	/* imin, imax dB */
		snd_solo_control,	/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(SLSB_IREG_LINE_RVOL, SLSB_IREG_LINE, SL_RECSRC_LINE)
	},
	{
		SND_MIXER_PRI_MIC,	/* priority */
		SND_MIXER_PRI_PARENT,	/* parent priority */
		SND_MIXER_ID_MIC,	/* device name */
		SND_MIXER_OSS_MIC,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT | SND_MIXER_CINFO_CAP_INPUT |
		SND_MIXER_CINFO_CAP_EXTINPUT,
		/* Output capabilities */
		SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME,
		/* Input capabilities */
		SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_EXCLUSIVE | SND_MIXER_CINFO_DCAP_HWMUTE |
   		SND_MIXER_CINFO_DCAP_JOINMUTE,
		0, 15,	  	  /* omin, omax value */
		-2100, 1050,  /* omin, omax dB */
		0, 15,		  /* imin, imax value */
        -2100, 1050,  /* imin, imax dB */
		snd_solo_control,	/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(SLSB_IREG_MIC_RVOL, SLSB_IREG_MICMIX, SL_RECSRC_MIC)
	},
	{
		SND_MIXER_PRI_SYNTHESIZER,	/* priority */
		SND_MIXER_PRI_MASTER,	/* parent priority */
		SND_MIXER_ID_SYNTHESIZER,	/* device name */
		SND_MIXER_OSS_SYNTH,	/* OSS device # */
		/* General capabilities */
        SND_MIXER_CINFO_CAP_OUTPUT,
		/* Output capabilities */
		SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_STEREO,
		/* Input capabilities */
		0,
		0, 15,		/* omin, omax value */
		-2850, 300,	/* omin, omax dB */
		0, 0,		/* imin, imax value */
		0, 0,		/* imin, imax dB */
		NULL,		/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(0, SLSB_IREG_FM, SL_RECSRC_NONE)
	},
	{
		SND_MIXER_PRI_AUXB,	/* priority */
		SND_MIXER_PRI_PARENT,	/* parent priority */
		SND_MIXER_ID_AUXB,	/* device name */
		SND_MIXER_OSS_LINE2,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT | SND_MIXER_CINFO_CAP_INPUT |
		SND_MIXER_CINFO_CAP_EXTINPUT,
		/* Output capabilities */
	        SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME,
		/* Input capabilities */
		SND_MIXER_CINFO_DCAP_STEREO | SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_EXCLUSIVE | SND_MIXER_CINFO_DCAP_HWMUTE |
		SND_MIXER_CINFO_DCAP_JOINMUTE, 	    
		0, 15,		/* omin, omax value */
		-2850, 300,	/* omin, omax dB */
		0, 15,		/* imin, imax value */
		-2850, 300,	        /* imin, imax dB */
		snd_solo_control,	/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(SLSB_IREG_AUXB_RVOL, SLSB_IREG_AUXB, SL_RECSRC_AUXB)
	},
	{
		SND_MIXER_PRI_PCM1,	/* priority */
		SND_MIXER_PRI_MASTER,	/* parent priority */
		SND_MIXER_ID_PCM1,	/* device name */
		SND_MIXER_OSS_ALTPCM,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT,
		/* Output capabilities */
		SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_STEREO,
		/* Input capabilities */
		0,
		0, 15,		/* omin, omax value */
		-3150, 0,	/* omin, omax dB */
		0, 0,		/* imin, imax value */
		0, 0,		/* imin, imax dB */
		NULL,		/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(0, SLSB_IREG_AUDIO1, SL_RECSRC_NONE)
	},
	{
		SND_MIXER_PRI_3D_SPACE,		/* priority */
		SND_MIXER_PRI_MASTER,	/* parent priority */
		SND_MIXER_ID_3D_SPACE,	/* device name */
		SND_MIXER_OSS_DIGITAL1,		/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT,
		/* Output capabilities */
		SND_MIXER_CINFO_DCAP_VOLUME |
		SND_MIXER_CINFO_DCAP_HWMUTE,
		/* Input capabilities */
		0,
		0, 63,		/* omin, omax value */
		0, 0,		/* omin, omax dB */
		0, 0,		/* imin, imax value */
		0, 0,		/* imin, imax dB */
		snd_solo_3d_mute,	/* set control */
		snd_solo_3d_volume,	/* set volume level */
		0
	},
	{
		SND_MIXER_PRI_PCM,	/* priority */
		SND_MIXER_PRI_MASTER,	/* parent priority */
		SND_MIXER_ID_PCM,	/* device name */
		SND_MIXER_OSS_PCM,	/* OSS device # */
		/* General capabilities */
		SND_MIXER_CINFO_CAP_OUTPUT,
		/* Output capabilities */
		SND_MIXER_CINFO_DCAP_VOLUME | SND_MIXER_CINFO_DCAP_STEREO,
		/* Input capabilities */
		0,
		0, 15,		/* omin, omax value */
		-3150, 0,	/* omin, omax dB */
		0, 0,		/* imin, imax value */
		0, 0,		/* imin, imax dB */
		NULL,		/* set control */
		snd_solo_volume_level,	/* set volume level */
		SOLO_PRIVATE(0, SLSB_IREG_AUDIO2, SL_RECSRC_NONE)
	}
};

#define SOLO_MIXS 9

static void snd_solo_mixer_free(void *data)
{
	((esssolo_t *) data)->mixer = NULL;
}

snd_kmixer_t *snd_solo_mixer(esssolo_t * solo)
{
	int idx;
	snd_kmixer_t *mixer;
	snd_kmixer_channel_t *channel;

	if (!solo)
		return NULL;
	mixer = snd_mixer_new(solo->card, "ess es1938");
	if (!mixer)
		return NULL;
	strcpy(mixer->name, "ESS Solo-1");
	for (idx = 0; idx < SOLO_MIXS; idx++) {
		channel = snd_mixer_new_channel(mixer, &snd_solo_mixs[idx]);
		if (!channel) {
			snd_mixer_free(mixer);
			return NULL;
		}
	}
	mixer->hw.caps = 0;
	mixer->private_data = solo;
	mixer->private_free = snd_solo_mixer_free;
	return solo->mixer = mixer;
}

/*

 */

esssolo_t *snd_solo_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)
{
	esssolo_t *solo;
	unsigned long c;
	int i;

	solo = (esssolo_t *) snd_calloc(sizeof(esssolo_t));
	if (!solo)
		return NULL;
	snd_mutex_prepare(solo, reg);
	solo->card = card;
	solo->pci = pci;
	solo->irqptr = irqptr;
	solo->dma1ptr = dma1ptr;
	solo->dma2ptr = dma2ptr;
	solo->io_port = PCI_IO_ADDR(pci->base_address[0]);
	solo->sb_port = PCI_IO_ADDR(pci->base_address[1]);
	solo->vc_port = PCI_IO_ADDR(pci->base_address[2]);
	solo->mpu_port = PCI_IO_ADDR(pci->base_address[3]);
	solo->game_port = PCI_IO_ADDR(pci->base_address[4]);
#ifdef SOLO_DDEBUG
	snd_printk("snd_solo_create: irq: 0x%x, io: 0x%x, sb: 0x%x, vc: 0x%x, mpu: 0x%x, game: 0x%x\n",
		   solo->irqptr->irq, solo->io_port, solo->sb_port, solo->vc_port, solo->mpu_port, solo->game_port);
#endif
	/* reset chip */
	outb(3, SLSB_REG(solo, RESET));
//	snd_solo_outbmask(3, 3, SLSB_REG(solo, RESET));
	inb(SLSB_REG(solo, RESET));
	outb(0, SLSB_REG(solo, RESET));
//	snd_solo_outbmask(3, 0, SLSB_REG(solo, RESET));
	for (i = 0; i < 1000; i++) {
		snd_delay(1);
		if (inb(SLSB_REG(solo, STATUS)) & 0x80) {
			if (inb(SLSB_REG(solo, READDATA)) == 0xaa)
				break;
		}
	}
	if (i == 100)
		snd_printk("ESS Solo-1 reset failed\n");

	snd_pci_write_config_word(pci, PCI_COMMAND, 0x5);
	snd_pci_write_config_byte(pci, PCI_LATENCY_TIMER, 0x40);

	/* set DDMA base */
	snd_pci_write_config_word(pci, SL_PCI_DDMACONTROL, solo->vc_port | 1);
	solo->ddma_port = solo->vc_port;

	/* set DMA/IRQ policy */
	snd_pci_read_config_dword(pci, SL_PCI_CONFIG, (unsigned long *)&c);
	c &= (~(0x700 | 0x6000));
	snd_pci_write_config_dword(pci, SL_PCI_CONFIG, c);

	snd_pci_read_config_dword(pci, SL_PCI_CONFIG, (unsigned long *)&c);
	snd_printd("CONFIG AFTER- %#lx\n",(long)c);

	/* disable Audio 1 and Audio 2 IRQ */
	snd_solo_mask_irq(solo, 0xf0, 0);

	/* reset DMA */
	outb(0xff, SLDM_REG(solo, DMACLEAR));
	snd_solo_write_cmd(solo, SL_CMD_ENABLEEXT);

	return solo;
}

void snd_solo_free(esssolo_t * solo)
{
	snd_mutex_destroy(solo, reg);
	snd_free(solo, sizeof(esssolo_t));
}

void snd_solo_midi(esssolo_t * solo, mpu401_t * mpu)
{
	mpu->private_data = solo;
	mpu->open_input = NULL;	/* snd_solo_midi_input_open; */
	mpu->close_input = NULL;	/* snd_solo_midi_input_close; */
	mpu->open_output = NULL;	/* snd_solo_midi_output_open; */
}

void snd_solo_interrupt(esssolo_t * solo)
{
	snd_pcm1_t *pcm1;
	unsigned char status, s1;

#ifdef SOLO_IDEBUG
	snd_printk("snd_solo_interrupt start\n");
#endif
	snd_mutex_lock(solo, reg);
	outb(0, SLIO_REG(solo, IRQCONTROL));
	status = inb(SLIO_REG(solo, IRQCONTROL));
	s1 = inb(SLSB_REG(solo, STATUS));
	if (status & 0x20) {
		if (solo->pcm2) {
			snd_solo_mixer_out(solo, SLSB_IREG_AUDIO2CONTROL2, 0x80, 0);
			pcm1 = (snd_pcm1_t *) solo->pcm2->private_data;
			if (pcm1 && pcm1->playback.ack)
		// MH 000518 
		// unlock the mutex, because snd_solo_trigger will be called and
		// wants to lock the mutex - maybe it's better to change that
		// function, than change it here
		{snd_mutex_unlock(solo, reg);
				pcm1->playback.ack(pcm1);
		// MH 000518
		snd_mutex_lock(solo, reg);}
#ifdef SOLO_IDEBUG
			{
				int v, v2;
				v = inw(SLIO_REG(solo, AUDIO2DMACOUNT));
				v2 = snd_solo_mixer_in(solo, SLSB_IREG_AUDIO2TCOUNTL);
				v2 += snd_solo_mixer_in(solo, SLSB_IREG_AUDIO2TCOUNTH) << 8;
				v2 <<= 1;
				v2 = 0x10000 - v2;
				snd_printk("snd_solo_interrupt playback ack, dmaaddr: 0x%x, dmacnt: 0x%x, cnt: 0x%x, pcm1: 0x%x\n",
					   inl(SLIO_REG(solo, AUDIO2DMAADDR)), v, v2, pcm1->playback.processed_bytes);
			}
#endif
		}
	}
    if (status & 0x10) {
        if (solo->pcm) {
            pcm1 = (snd_pcm1_t *) solo->pcm->private_data;
            if (pcm1 && pcm1->playback.ack)
                pcm1->playback.ack(pcm1);
#ifdef SOLO_IDEBUG
           snd_printk("snd_solo_interrupt playback2 ack\n");
#endif
		}
	}
#ifdef SOLO_IDEBUG
	snd_printk("snd_solo_interrupt: status 0x%x, s1 0x%x, mask 0x%x\n", status, s1, solo->irqmask);
#endif
	outb(solo->irqmask, SLIO_REG(solo, IRQCONTROL));
	snd_mutex_unlock(solo, reg);
#ifdef SOLO_IDEBUG
	snd_printk("snd_solo_interrupt end\n");
#endif
}
