/*
 *  Copyright (c) 1999 by Uros Bizjak <uros@kss-loka.si>
 *                        Takashi Iwai <iwai@ww.uni-erlangen.de>
 *
 *  SB16ASP/AWE32 CSP control
 *
 *   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/info.h"
#include "../../include/sb.h"
#include "../../include/sb16_csp.h"

/*
 * CSP information (stored on hwdep->private_data)
 */
typedef struct snd_sb_csp {
	sbdsp_t *codec;		/* SB16 DSP */
	int used;		/* usage flag - exclusive */
	char codec_name[16];	/* name of codec */
	unsigned short func_nr;	/* function number */
	unsigned int acc_format;	/* accepted PCM formats */
	int acc_channels;	/* accepted channels */
	int mode;		/* MODE */
	unsigned int run_format;	/* active PCM format */
	int run_channels;	/* current CSP channels */
	int version;		/* CSP version (0x10 - 0x1f) */
	int running;		/* running state */
	int qpos_left;		/* Q-Sound left position */
	int qpos_right;		/* Q-Sound right position */
	struct semaphore access_mutex;	/* locking */
	snd_info_entry_t *proc;	/* proc interface */
} snd_sb_csp_t;

/*
 * RIFF data format
 */
typedef struct riff_header {
	char name[4];
	__u32 len;
} riff_header_t;

typedef struct desc_header {
	riff_header_t info;
	__u16 func_nr;
	__u16 VOC_type;
	__u16 flags_play_rec;
	__u16 flags_16bit_8bit;
	__u16 flags_stereo_mono;
	__u16 flags_unknown;
} desc_header_t;

typedef struct file_header {
	riff_header_t info;
	char desc[4];
} file_header_t;

/*
 * prototypes
 */
static void snd_sb_csp_free(void *private_data);
static int snd_sb_csp_open(snd_hwdep_t * hw, struct file *file);
static int snd_sb_csp_ioctl(snd_hwdep_t * hw, struct file *file, unsigned int cmd, unsigned long arg);
static int snd_sb_csp_riff_load(snd_sb_csp_t * p, snd_sb_csp_microcode_t * code);
static int snd_sb_csp_release(snd_hwdep_t * hw, struct file *file);

static int command_seq(sbdsp_t * codec, const unsigned char *seq, int size);
static int set_codec_parameter(sbdsp_t * codec, unsigned char par, unsigned char val);
static int set_register(sbdsp_t * codec, unsigned char reg, unsigned char val);
static int read_register(sbdsp_t * codec, unsigned char reg);
static int set_mode_register(sbdsp_t * codec, unsigned char mode);
static int get_version(sbdsp_t * codec);
static int check_version(snd_sb_csp_t * p);

static int snd_sb_csp_detect(sbdsp_t * codec, int *version);
static int snd_sb_csp_load(snd_sb_csp_t * p, const unsigned char *buf, int size, int initblock);
static int snd_sb_csp_start(snd_sb_csp_t * p, snd_sb_csp_start_t * info);
static int snd_sb_csp_stop(snd_sb_csp_t * p);
static int snd_sb_csp_pause(snd_sb_csp_t * p);
static int snd_sb_csp_restart(snd_sb_csp_t * p);

static int snd_sb_csp_qsound_start(snd_sb_csp_t * p);
static int snd_sb_csp_qsound_pos(snd_sb_csp_t * p, int left, int right);
static int snd_sb_csp_qsound_stop(snd_sb_csp_t * p);

static int init_proc_entry(snd_sb_csp_t * p);
static void delete_proc_entry(snd_sb_csp_t * p);
static void info_read(snd_info_buffer_t * buffer, void *private_data);

/*
 * Detect CSP chip and create a new instance
 */
snd_hwdep_t *snd_sb_csp_new_device(sbdsp_t * codec)
{
	snd_sb_csp_t *p;
	int version;
	snd_hwdep_t *hw;

	if (snd_sb_csp_detect(codec, &version))
		return NULL;

	if ((hw = snd_hwdep_new_device(codec->card, "SB16-CSP")) == NULL)
		return NULL;

	if ((p = snd_kcalloc(sizeof(*p), GFP_KERNEL)) == NULL) {
		snd_hwdep_free(hw);
		return NULL;
	}
	p->codec = codec;
	p->version = version;
	init_MUTEX(&p->access_mutex);

	sprintf(hw->name, "SB16 CSP : CSP v%d.%d", (version >> 4), (version & 0x0F));
	hw->type = SND_HWDEP_TYPE_SB16CSP;
	hw->private_data = p;
	hw->private_free = snd_sb_csp_free;

	/* operators - only write/ioctl */
	hw->ops.open = snd_sb_csp_open;
	hw->ops.ioctl = snd_sb_csp_ioctl;
	hw->ops.release = snd_sb_csp_release;

	/* create a proc entry */
	init_proc_entry(p);

	return hw;
}

/*
 * free_private for hwdep instance
 */
static void snd_sb_csp_free(void *private_data)
{
	snd_sb_csp_t *p = private_data;
	if (p) {
		if (p->running & SND_SB_CSP_ST_RUNNING)
			snd_sb_csp_stop(p);
		delete_proc_entry(p);
		snd_kfree(p);
	}
}

/*
 * open the device exclusively
 */
static int snd_sb_csp_open(snd_hwdep_t * hw, struct file *file)
{
	snd_sb_csp_t *p = hw->private_data;

	down(&p->access_mutex);
	if (p->used) {
		up(&p->access_mutex);
		return -EBUSY;
	}
	p->used++;
	up(&p->access_mutex);

	return 0;
}

/*
 * ioctl for hwdep device:
 */
static int snd_sb_csp_ioctl(snd_hwdep_t * hw, struct file *file, unsigned int cmd, unsigned
			    long arg)
{
	snd_sb_csp_t *p = hw->private_data;
	snd_sb_csp_info_t info;
	snd_sb_csp_qsound_t qpos;
	int err;

	if (p == NULL)
		return -EINVAL;

	if (check_version(p))
		return -ENODEV;

	down(&p->access_mutex);

	switch (cmd) {
		/* get information */
	case SND_SB_CSP_IOCTL_INFO:
		memcpy(info.codec_name, p->codec_name, sizeof(p->codec_name));
		info.func_nr = p->func_nr;
		info.acc_format = p->acc_format;
		info.acc_channels = p->acc_channels;
		info.csp_mode = p->mode;
		info.run_format = p->run_format;
		info.run_channels = p->run_channels;
		info.version = p->version;
		info.state = p->running;
		err = copy_to_user((void *) arg, &info, sizeof(info));
		break;

		/* load CSP microcode */
	case SND_SB_CSP_IOCTL_LOAD_CODE:
		if (p->running & SND_SB_CSP_ST_RUNNING)
			err = -EBUSY;
		else
			err = snd_sb_csp_riff_load(p, (snd_sb_csp_microcode_t *) arg);
		break;

		/* change CSP running state */
	case SND_SB_CSP_IOCTL_START:
		err = snd_sb_csp_start(p, (snd_sb_csp_start_t *) arg);
		break;
	case SND_SB_CSP_IOCTL_STOP:
		err = snd_sb_csp_stop(p);
		break;
	case SND_SB_CSP_IOCTL_PAUSE:
		err = snd_sb_csp_pause(p);
		break;
	case SND_SB_CSP_IOCTL_RESTART:
		err = snd_sb_csp_restart(p);
		break;

		/* QSound stuffs */
	case SND_SB_CSP_IOCTL_QSOUND_START:
		err = snd_sb_csp_qsound_start(p);
		break;
	case SND_SB_CSP_IOCTL_QSOUND_STOP:
		err = snd_sb_csp_qsound_stop(p);
		break;
	case SND_SB_CSP_IOCTL_QSOUND_POS:
		if (copy_from_user(&qpos, (void *) arg, sizeof(qpos)))
			err = -EFAULT;
		else
			err = snd_sb_csp_qsound_pos(p, qpos.left, qpos.right);
		break;
	case SND_SB_CSP_IOCTL_QSOUND_GET_POS:
		qpos.left = p->qpos_left;
		qpos.right = p->qpos_right;
		err = copy_to_user((void *) arg, &qpos, sizeof(qpos));
		break;

	default:
		err = -EINVAL;
		break;
	}

	up(&p->access_mutex);
	return err;
}

/*
 * load microcode via ioctl:
 * code is user-space pointer
 */
static int snd_sb_csp_riff_load(snd_sb_csp_t * p, snd_sb_csp_microcode_t * mcode)
{
	snd_sb_csp_mc_header_t info;

	unsigned char *data;
	int data_len, pos, err;
	unsigned short func_nr;
	file_header_t file_h;
	riff_header_t item_h, code_h;
	desc_header_t funcdesc_h;
	unsigned char item_desc_1;

	if (copy_from_user(&info, mcode, sizeof(info)))
		return -EFAULT;

	p->func_nr = info.func_req;
	func_nr = -1;

	data = mcode->data;
	if (copy_from_user(&file_h, data, sizeof(file_h)))
		return -EFAULT;
	if ((memcmp(file_h.info.name, "RIFF", 4) != 0) ||
	    (memcmp(file_h.desc, "CSP ", 4) != 0) ||
	    (file_h.info.len >= SND_SB_CSP_MAX_MICROCODE_FILE_SIZE - sizeof(riff_header_t))) {
		snd_printd("sb_csp_riff_load: invalid RIFF header!\n");
		return -EINVAL;
	}
	data_len = file_h.info.len;
	for (pos = sizeof(file_header_t); pos < data_len; pos += item_h.len) {
		if (copy_from_user(&item_h, (unsigned char *) (data + pos), sizeof(item_h)))
			return -EFAULT;
		pos += sizeof(item_h);
		if (memcmp(item_h.name, "LIST", 4) != 0)
			continue;

		if (copy_from_user(&item_desc_1, (unsigned char *) (data + pos), sizeof(char)))
			 return -EFAULT;
		switch (item_desc_1) {
		case 0x66:	/* func */
			if (copy_from_user(&funcdesc_h, (unsigned char *) (data + pos + 4), sizeof(funcdesc_h)))
				return -EFAULT;
			func_nr = funcdesc_h.func_nr;
			p->acc_format = 0x00;
			switch (funcdesc_h.VOC_type) {
			case 0x0001:	/* PCM */
				if (funcdesc_h.flags_16bit_8bit & SND_SB_CSP_SAMPLE_8BIT)
					p->acc_format |= SND_PCM_FMT_U8;	/* 8 bit unsigned */
				if (funcdesc_h.flags_16bit_8bit & SND_SB_CSP_SAMPLE_16BIT)
					p->acc_format |= SND_PCM_FMT_S16_LE;	/* 16 bit signed little endian */
				break;
			case 0x0006:	/* A Law */
				p->acc_format |= SND_PCM_FMT_A_LAW;
				break;
			case 0x0007:	/* Mu Law */
				p->acc_format |= SND_PCM_FMT_MU_LAW;
				break;
			case 0x0011:	/* IMA ADPCM */
				p->acc_format |= SND_PCM_FMT_IMA_ADPCM;
				break;
			default:	/* other formats are unsupported */
				snd_printd("sb_csp_riff_load: invalid or unsupported PCM type: 0x%x\n",
					   funcdesc_h.VOC_type);
				return -EINVAL;
			}
			p->mode = funcdesc_h.flags_play_rec;
			/* QSound mode only for raw PCM formats */
			if ((funcdesc_h.VOC_type == 0x0001) && (funcdesc_h.flags_play_rec & SND_SB_CSP_MODE_DSP_WRITE))
				p->mode |= SND_SB_CSP_MODE_QSOUND;
			p->acc_channels = funcdesc_h.flags_stereo_mono;
			p->run_format = -1;	/* will be set by run command */
			p->run_channels = -1;
			break;
		case 0x63:	/* code */
			if (func_nr != p->func_nr)
				break;	/* code doesn't match, try next */
			pos += 4;	/* add "code" */

			/* load microcode blocks */
			for (;;) {
				if (pos >= data_len)
					return -EINVAL;
				if (copy_from_user(&code_h, (unsigned char *) (data + pos), sizeof(code_h)))
					return -EFAULT;

				/* init microcode blocks */
				if (memcmp(code_h.name, "init", 4) != 0)
					break;
				pos += sizeof(code_h);
				err = snd_sb_csp_load(p, (unsigned char *) (data + pos), code_h.len, 1);
				if (err)
					return err;
				pos += code_h.len;
			}
			/* main microcode block */
			if (copy_from_user(&code_h, (unsigned char *) (data + pos), sizeof(code_h)))
				return -EFAULT;

			if (memcmp(code_h.name, "main", 4) != 0) {
				snd_printd("sb_csp_riff_load: missing 'main' code!\n");
				return -EINVAL;
			}
			pos += sizeof(code_h);
			err = snd_sb_csp_load(p, (unsigned char *) (data + pos), code_h.len, 0);
			if (err)
				return err;

			/* copy codec_name */
			memset(p->codec_name, 0, sizeof(p->codec_name));
			strncpy(p->codec_name, info.codec_name, sizeof(p->codec_name) - 1);
			p->codec_name[sizeof(p->codec_name) - 1] = 0;

			/* finished loading successfully */
			p->running |= SND_SB_CSP_ST_LOADED;
			return 0;
		}
	}
	return -EINVAL;
}

/*
 * release the device
 */
static int snd_sb_csp_release(snd_hwdep_t * hw, struct file *file)
{
	snd_sb_csp_t *p = hw->private_data;

	down(&p->access_mutex);
	p->used--;
	up(&p->access_mutex);

	return 0;
}

/*
 * send command sequence to DSP
 */
static int command_seq(sbdsp_t * codec, const unsigned char *seq, int size)
{
	int i;
	for (i = 0; i < size; i++) {
		if (!snd_sb16dsp_command(codec, seq[i]))
			return -EIO;
	}
	return 0;
}

/*
 * set CSP codec parameter
 */
static int set_codec_parameter(sbdsp_t * codec, unsigned char par, unsigned char val)
{
	unsigned char dsp_cmd[3];

	dsp_cmd[0] = 0x05;	/* CSP set codec parameter */
	dsp_cmd[1] = val;	/* Parameter value */
	dsp_cmd[2] = par;	/* Parameter */
	command_seq(codec, dsp_cmd, 3);
	snd_sb16dsp_command(codec, 0x03);	/* DSP read? */
	if (snd_sb16dsp_get_byte(codec) != par)
		return -EIO;
	return 0;
}

/*
 * set CSP register
 */
static int set_register(sbdsp_t * codec, unsigned char reg, unsigned char val)
{
	unsigned char dsp_cmd[3];

	dsp_cmd[0] = 0x0E;	/* CSP set register */
	dsp_cmd[1] = reg;	/* CSP Register */
	dsp_cmd[2] = val;	/* value */
	return command_seq(codec, dsp_cmd, 3);
}

/*
 * read CSP register
 * return < 0 -> error
 */
static int read_register(sbdsp_t * codec, unsigned char reg)
{
	unsigned char dsp_cmd[2];

	dsp_cmd[0] = 0x0F;	/* CSP read register */
	dsp_cmd[1] = reg;	/* CSP Register */
	command_seq(codec, dsp_cmd, 2);
	return snd_sb16dsp_get_byte(codec);	/* Read DSP value */
}

/*
 * set CSP mode register
 */
static int set_mode_register(sbdsp_t * codec, unsigned char mode)
{
	unsigned char dsp_cmd[2];

	dsp_cmd[0] = 0x04;	/* CSP set mode register */
	dsp_cmd[1] = mode;	/* mode */
	return command_seq(codec, dsp_cmd, 2);
}

/*
 * Detect CSP
 * return 0 if CSP exists.
 */
static int snd_sb_csp_detect(sbdsp_t * codec, int *version)
{
	unsigned char csp_test1, csp_test2;
	unsigned long flags;
	int result = -ENODEV;

	spin_lock_irqsave(&codec->reg_lock, flags);

	set_codec_parameter(codec, 0x00, 0x00);
	set_mode_register(codec, 0xfc);		/* 0xFC = ?? */

	csp_test1 = read_register(codec, 0x83);
	set_register(codec, 0x83, ~csp_test1);
	csp_test2 = read_register(codec, 0x83);
	if (csp_test2 != (csp_test1 ^ 0xFF))
		goto __fail;

	set_register(codec, 0x83, csp_test1);
	csp_test2 = read_register(codec, 0x83);
	if (csp_test2 != csp_test1)
		goto __fail;

	set_mode_register(codec, 0x00);		/* 00 = reset CSP? */

	*version = get_version(codec);
	snd_sb16dsp_reset(codec);	/* reset DSP after getversion! */
	if (*version >= 0x10 && *version <= 0x1f)
		result = 0;	/* valid version id */

      __fail:
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

/*
 * get CSP version number
 */
static int get_version(sbdsp_t * codec)
{
	unsigned char dsp_cmd[2];

	dsp_cmd[0] = 0x08;	/* SB_DSP_!something! */
	dsp_cmd[1] = 0x03;	/* get chip version id? */
	command_seq(codec, dsp_cmd, 2);

	return (snd_sb16dsp_get_byte(codec));
}

/*
 * check if the CSP version is valid
 */
static int check_version(snd_sb_csp_t * p)
{
	if (p->version < 0x10 || p->version > 0x1f) {
		snd_printd("sb_csp: invalid version 0x%d\n", p->version);
		return 1;
	}
	return 0;
}

/*
 * download microcode to CSP.  The buffer is on user space.
 * if initblock=1, load "init" block of microcode. Microcode 
 * should have one "main" block.
 */
static int snd_sb_csp_load(snd_sb_csp_t * p, const unsigned char *buf, int size, int initblock)
{
	int i, err;
	int result = -EIO;
	unsigned long flags;

	spin_lock_irqsave(&p->codec->reg_lock, flags);

	snd_sb16dsp_command(p->codec, 0x01);	/* CSP download command */

	if (snd_sb16dsp_get_byte(p->codec) != 0) {
		snd_printd("sb_csp_load: Download command failed!\n");
		goto __fail;
	}
	/* Send CSP lo_bit (lenght -1) */
	snd_sb16dsp_command(p->codec, (unsigned char) (size - 1));
	/* Send hi_bit */
	snd_sb16dsp_command(p->codec, (unsigned char) (size >> 8));
	/* send microcode sequence */
	for (i = 0; i < size; i++, buf++) {
		unsigned c;
		if (copy_from_user(&c, buf, 1)) {
			result = -EFAULT;
			goto __fail;
		}
		if (!snd_sb16dsp_command(p->codec, c))
			goto __fail;
	}

	snd_delay(1);		/* wait a moment */

	if (snd_sb16dsp_get_byte(p->codec) != 0)
		goto __fail;

	if (initblock) {
		/* confirm 'init' block */
		snd_sb16dsp_command(p->codec, 0x03);
		if (snd_sb16dsp_get_byte(p->codec) != 0x55) {
			snd_printd("sb_csp_load: Not an 'init' code block!\n");
			goto __fail;
		}
	} else {
		/* Read mixer register SB_DSP4_CSP [0x81] after loading 'main' code.
		 * If top 3 bits [xxx00000] are cleared, then start loaded code.
		 * Some kind of autorun - or just a bugfix?
		 */
		if ((snd_sb16mixer_read(&p->codec->mixer, SB_DSP4_CSP) & 0xE0) == 0x00) {
			err = (set_codec_parameter(p->codec, 0xAA, 0x00) ||
			       set_codec_parameter(p->codec, 0xFF, 0x00));
			snd_sb16dsp_reset(p->codec);	/* really! */
			if (err)
				goto __fail;
			set_mode_register(p->codec, 0xC0);	/* C0 = STOP */
			set_mode_register(p->codec, 0x70);	/* 70 = RUN */

			p->running |= SND_SB_CSP_ST_RUNNING;
		}
	}

	result = 0;

      __fail:
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);
	return result;
}

/*
 * start CSP
 */
static int snd_sb_csp_start(snd_sb_csp_t * p, snd_sb_csp_start_t * info)
{
	snd_sb_csp_start_t start_info;

	unsigned char s_type;	/* sample type */
	int result = -EIO;
	unsigned long flags;

	if (!(p->running & SND_SB_CSP_ST_LOADED)) {
		snd_printd("sb_csp_start: codec not loaded\n");
		return -ENXIO;
	}
	if (p->running & SND_SB_CSP_ST_RUNNING) {
		snd_printd("sb_csp_start: CSP already running\n");
		return -EBUSY;
	}
	if (copy_from_user(&start_info, info, sizeof(start_info)))
		return -EFAULT;

	if (((start_info.pcm_format & p->acc_format) == 0) ||
	    ((start_info.channels & p->acc_channels) == 0))
		return -EINVAL;

	/* TODO: slowly fade down PCM volume to avoid crackling */

	spin_lock_irqsave(&p->codec->reg_lock, flags);

	set_mode_register(p->codec, 0xC0);	/* C0 = STOP */
	set_mode_register(p->codec, 0x70);	/* 70 = RUN */

	s_type = 0x00;
	if (start_info.channels & SND_SB_CSP_MONO)
		s_type = 0x11;	/* 000n 000n    (n = 1 if mono) */
	if (start_info.pcm_format & SND_PCM_FMT_U8)
		s_type |= 0x22;	/* 00dX 00dX    (d = 1 if 8 bit samples) */

	if (set_codec_parameter(p->codec, 0x81, s_type)) {
		snd_printd("cb_csp_start: Set sample type command failed!\n");
		goto __fail;
	}
	if (set_codec_parameter(p->codec, 0x80, 0x00)) {
		snd_printd("sb_csp_start: Codec start command failed!\n");
		goto __fail;
	}
	p->run_format = start_info.pcm_format;
	p->run_channels = start_info.channels;

	p->running |= SND_SB_CSP_ST_RUNNING;

	result = 0;

      __fail:
	/* TODO: slowly fade up PCM volume to saved value */
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);
	return result;
}

/*
 * stop CSP
 */
static int snd_sb_csp_stop(snd_sb_csp_t * p)
{
	int result;
	unsigned long flags;

	if (!(p->running & SND_SB_CSP_ST_RUNNING))
		return 0;

	if (p->running & SND_SB_CSP_ST_QSOUND) {
		result = snd_sb_csp_qsound_stop(p);
		if (result)
			return result;
	}
	spin_lock_irqsave(&p->codec->reg_lock, flags);
	/* TODO: fade down volume to avoid crackling */
	result = set_mode_register(p->codec, 0xC0);	/* C0 = STOP */
	/* TODO: fade up volume to saved value */
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);

	if (result == 0)
		p->running = SND_SB_CSP_ST_LOADED;	/* clear all other flags */
	return result;
}

/*
 * pause CSP codec and hold DMA transfer
 */
static int snd_sb_csp_pause(snd_sb_csp_t * p)
{
	int result;
	unsigned long flags;

	if (!(p->running & SND_SB_CSP_ST_RUNNING))
		return -EBUSY;

	spin_lock_irqsave(&p->codec->reg_lock, flags);
	result = set_codec_parameter(p->codec, 0x80, 0xFF);
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);
	if (result == 0)
		p->running |= SND_SB_CSP_ST_PAUSED;

	return result;
}

/*
 * restart CSP codec and resume DMA transfer
 */
static int snd_sb_csp_restart(snd_sb_csp_t * p)
{
	int result;
	unsigned long flags;

	if (!(p->running & SND_SB_CSP_ST_PAUSED))
		return -EBUSY;

	spin_lock_irqsave(&p->codec->reg_lock, flags);
	result = set_codec_parameter(p->codec, 0x80, 0x00);
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);
	if (result == 0)
		p->running &= ~SND_SB_CSP_ST_PAUSED;

	return result;
}

/*
 * initialize QSound codec
 */
static int snd_sb_csp_qsound_start(snd_sb_csp_t * p)
{
	unsigned long flags;

	if (!(p->mode & SND_SB_CSP_MODE_QSOUND))
		return -ENXIO;
	if (!(p->running & SND_SB_CSP_ST_RUNNING) ||
	    (p->running & SND_SB_CSP_ST_QSOUND))
		return -EBUSY;

	/* center position 0x10
	 * set Left channel and
	 * set Right channel
	 */
	spin_lock_irqsave(&p->codec->reg_lock, flags);
	set_codec_parameter(p->codec, 0xE0, 0x01);
	set_codec_parameter(p->codec, 0x00, 0xFF);
	set_codec_parameter(p->codec, 0x01, 0xFF);
	set_codec_parameter(p->codec, 0x00, 0x10);
	set_codec_parameter(p->codec, 0x02, 0x00);
	set_codec_parameter(p->codec, 0x03, 0x00);
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);

	p->qpos_left = p->qpos_right = 0x10;

	p->running |= SND_SB_CSP_ST_QSOUND;

	return 0;
}

/*
 * set QSound position for left and right channel sample
 * with arguments ranging from 0x00 to 0x20.
 */
static int snd_sb_csp_qsound_pos(snd_sb_csp_t * p, int left, int right)
{
	unsigned long flags;

	if (!(p->mode & SND_SB_CSP_MODE_QSOUND))
		return -ENXIO;
	if (!(p->running & SND_SB_CSP_ST_RUNNING) ||
	    !(p->running & SND_SB_CSP_ST_QSOUND))
		return -EBUSY;

	spin_lock_irqsave(&p->codec->reg_lock, flags);
	/* left channel */
	set_codec_parameter(p->codec, 0xE0, 0x01);
	set_codec_parameter(p->codec, 0x00, left);
	set_codec_parameter(p->codec, 0x02, 0x00);
	/* right channel */
	set_codec_parameter(p->codec, 0x0E, 0x01);
	set_codec_parameter(p->codec, 0x00, right);
	set_codec_parameter(p->codec, 0x03, 0x00);
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);

	p->qpos_left = left;
	p->qpos_right = right;

	return 0;
}

/*
 * leave QSound
 */
static int snd_sb_csp_qsound_stop(snd_sb_csp_t * p)
{
	unsigned long flags;

	if (!(p->mode & SND_SB_CSP_MODE_QSOUND))
		return -ENXIO;
	if (!(p->running & SND_SB_CSP_ST_RUNNING) ||
	    !(p->running & SND_SB_CSP_ST_QSOUND))
		return -EBUSY;

	spin_lock_irqsave(&p->codec->reg_lock, flags);
	set_codec_parameter(p->codec, 0xE0, 0x01);
	set_codec_parameter(p->codec, 0x00, 0x00);
	set_codec_parameter(p->codec, 0x01, 0x00);
	set_codec_parameter(p->codec, 0xE0, 0x01);
	set_codec_parameter(p->codec, 0x00, 0x00);
	set_codec_parameter(p->codec, 0x01, 0x00);
	spin_unlock_irqrestore(&p->codec->reg_lock, flags);

	p->running &= ~SND_SB_CSP_ST_QSOUND;

	return 0;
}

/*
 * proc interface
 */
static int init_proc_entry(snd_sb_csp_t * p)
{
	snd_info_entry_t *entry;
	entry = p->proc = snd_info_create_entry(p->codec->card, "sb16-csp");
	if (entry) {
		entry->type = SND_INFO_ENTRY_TEXT;
		entry->t.text.read_size = 256;
		entry->t.text.read = info_read;
		entry->private_data = p;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			p->proc = NULL;
		}
	}
	return 0;
}

static void delete_proc_entry(snd_sb_csp_t * p)
{
	if (p->proc) {
		snd_info_unregister(p->proc);
		p->proc = NULL;
	}
}

static void info_read(snd_info_buffer_t * buffer, void *private_data)
{
	snd_sb_csp_t *p = private_data;
	snd_iprintf(buffer, "Creative Signal Processor [v%d.%d]\n", (p->version >> 4), (p->version & 0x0F));
	snd_iprintf(buffer, "State: %cx%c%c%c\n", ((p->running & SND_SB_CSP_ST_QSOUND) ? 'Q' : '-'),
		    ((p->running & SND_SB_CSP_ST_PAUSED) ? 'P' : '-'),
		    ((p->running & SND_SB_CSP_ST_RUNNING) ? 'R' : '-'),
		    ((p->running & SND_SB_CSP_ST_LOADED) ? 'L' : '-'));
	if (p->running & SND_SB_CSP_ST_LOADED) {
		snd_iprintf(buffer, "Codec loaded: %s [func #%d]\n", p->codec_name, p->func_nr);
		snd_iprintf(buffer, "PCM Formats supported: 0x%x [%s/%s] [%c/%c/%c]\n", p->acc_format,
		    ((p->acc_channels & SND_SB_CSP_MONO) ? "mono" : "-"),
		((p->acc_channels & SND_SB_CSP_STEREO) ? "stereo" : "-"),
			((p->mode & SND_SB_CSP_MODE_QSOUND) ? 'Q' : '-'),
		      ((p->mode & SND_SB_CSP_MODE_DSP_READ) ? 'R' : '-'),
		    ((p->mode & SND_SB_CSP_MODE_DSP_WRITE) ? 'W' : '-'));
	}
	if (p->running & SND_SB_CSP_ST_RUNNING) {
		snd_iprintf(buffer, "Running PCM format: 0x%x [%s]\n", (p->run_format),
			    ((p->run_channels) & SND_SB_CSP_MONO) ? "mono" : "stereo");
	}
	if (p->running & SND_SB_CSP_ST_QSOUND) {
		snd_iprintf(buffer, "Qsound: left = 0x%x, right = 0x%x\n",
			    p->qpos_left, p->qpos_right);
	}
}

/*
 */

EXPORT_SYMBOL(snd_sb_csp_new_device);

/*
 *  INIT part
 */

int init_module(void)
{
	return 0;
}

void cleanup_module(void)
{
}
