/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Routines for control of 8-bit SoundBlaster cards and clones
 *  Code needs to be debugged... I haven't the access to old SB soundcards.
 *
 *
 *   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.
 *
 * --
 *
 * Thu Apr 29 20:36:17 BST 1999 George David Morrison <gdm@gedamo.demon.co.uk>
 *   DSP can't respond to commands whilst in "high speed" mode. Caused 
 *   glitching during playback. Fixed.
 */

#include "driver.h"
#include "sb.h"

static inline void snd_sb8_ack_8bit(sbdsp_t * codec)
{
	inb(SBP(codec, DATA_AVAIL));
}

static void snd_sb8_compute_rate(snd_pcm1_t * pcm1, int direction)
{
	sbdsp_t *codec;
	snd_pcm1_channel_t *pchn;
	int rate;

	pchn = direction == SND_PCM1_PLAYBACK ?
					&pcm1->playback : &pcm1->record;
	codec = (sbdsp_t *) pcm1->private_data;
	pchn->real_rate = rate = pchn->rate;
	switch (codec->hardware) {
	case SB_HW_10:
	case SB_HW_20:
		if (direction == SND_PCM1_RECORD && rate > 13000)
			rate = 13000;
		if (rate < 4000)
			pchn->real_rate = 4000;
		if (rate > 23000)
			rate = 23000;
		codec->speed8 = (65535 - (256000000 + rate / 2) / rate) >> 8;
		if (direction == SND_PCM1_PLAYBACK) {
			codec->fmt8 = SB_DSP_LO_OUTPUT_AUTO;	/* not used with SB 1.0 */
		} else {
			codec->fmt8 = SB_DSP_LO_INPUT_AUTO;	/* not used with SB 1.0 */
		}
		pchn->real_rate = 256000000 / (65536 - (codec->speed8 << 8));
		break;
	case SB_HW_201:
		if (direction == SND_PCM1_RECORD && rate > 15000)
			rate = 15000;
		if (rate < 4000)
			rate = 4000;
		if (rate > 44100)
			rate = 44100;
		codec->speed8 = (65536 - (256000000 + rate / 2) / rate) >> 8;
		if (direction == SND_PCM1_PLAYBACK) {
			codec->fmt8 = rate > 23000 ? SB_DSP_HI_OUTPUT_AUTO : SB_DSP_LO_OUTPUT_AUTO;
		} else {
			codec->fmt8 = rate > 13000 ? SB_DSP_HI_INPUT_AUTO : SB_DSP_LO_INPUT_AUTO;
		}
		pchn->real_rate = 256000000 / (65536 - (codec->speed8 << 8));
		break;
	case SB_HW_PRO:
		if (rate < 4000)
			rate = 4000;
		if (direction == SND_PCM1_RECORD) {
			if (rate > 15000)
				rate = 15000;
		}
		if (pchn->voices > 1) {
			rate = rate > (22050 + 11025) / 2 ? 22050 : 11025;
			codec->speed8 = (65536 - (256000000 + rate) / (rate << 1)) >> 8;
			if (direction == SND_PCM1_PLAYBACK) {
				codec->fmt8 = SB_DSP_HI_OUTPUT_AUTO;
			} else {
				codec->fmt8 = SB_DSP_HI_INPUT_AUTO;
			}
		} else {
			if (rate > 44100)
				rate = 44100;
			codec->speed8 = (65536 - (256000000 + rate / 2) / rate) >> 8;
			if (direction == SND_PCM1_PLAYBACK) {
				codec->fmt8 = rate > 23000 ? SB_DSP_HI_OUTPUT_AUTO : SB_DSP_LO_OUTPUT_AUTO;
			} else {
				codec->fmt8 = rate > 23000 ? SB_DSP_HI_INPUT_AUTO : SB_DSP_LO_INPUT_AUTO;
			}
		}
		pchn->real_rate = (256000000 / (65536 - (codec->speed8 << 8))) / pchn->voices;
#if 0
		snd_printd("snd_sb8_compute_rate: rate = %u, pchn->real_rate = %u\n", rate, pchn->real_rate);
#endif
		break;
	default:
		snd_printd("unknown hardware for sb8 compute rate!!!\n");
	}
}

static int snd_sb8_playback_ioctl(snd_pcm1_t * pcm1,
				  unsigned int cmd, unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		snd_sb8_compute_rate(pcm1, SND_PCM1_PLAYBACK);
		return 0;
	}
	return -ENXIO;
}

static int snd_sb8_record_ioctl(snd_pcm1_t * pcm1,
				unsigned int cmd, unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		snd_sb8_compute_rate(pcm1, SND_PCM1_RECORD);
		return 0;
	}
	return -ENXIO;
}

static void snd_sb8_playback_prepare(snd_pcm1_t * pcm1,
				     unsigned char *buffer,
				     unsigned int size,
				     unsigned int offset,
				     unsigned int count)
{
	unsigned long flags, flags1;
	sbdsp_t *codec;
	snd_pcm1_channel_t *pchn;

	pchn = &pcm1->playback;
	codec = (sbdsp_t *) pcm1->private_data;
	switch (codec->hardware) {
	case SB_HW_10:
		snd_spin_lock(codec, reg, &flags);
		snd_sb8_ack_8bit(codec);
		snd_sb8dsp_command(codec, SB_DSP_SAMPLE_RATE);
		snd_sb8dsp_command(codec, codec->speed8);
		snd_sb8_ack_8bit(codec);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_ON);
		snd_spin_unlock(codec, reg, &flags);
		snd_dma_program(codec->dma8, &buffer[offset], count, DMA_MODE_WRITE);
		codec->count8 = count - 1;
		break;
	case SB_HW_20:
	case SB_HW_201:
	case SB_HW_PRO:
		snd_spin_lock(codec, reg, &flags);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (snd_sb8dsp_reset(codec) < 0) {
			snd_spin_unlock(codec, reg, &flags);
			snd_printk("sb8 - reset failed!!!\n");
			return;
		}
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (codec->hardware == SB_HW_PRO) {
			snd_spin_lock(&codec->mixer, mixer, &flags1);
			if (pchn->voices > 1) {
				snd_sb8mixer_write(&codec->mixer, 0x0e, snd_sb8mixer_read(&codec->mixer, 0x0e) | 0x02);
			} else {
				snd_sb8mixer_write(&codec->mixer, 0x0e, snd_sb8mixer_read(&codec->mixer, 0x0e) & ~0x02);
			}
			snd_spin_unlock(&codec->mixer, mixer, &flags1);
		}
		snd_sb8_ack_8bit(codec);
		snd_sb8dsp_command(codec, SB_DSP_SAMPLE_RATE);
		snd_sb8dsp_command(codec, codec->speed8);
		snd_sb8_ack_8bit(codec);
		snd_spin_unlock(codec, reg, &flags);
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_ON);
		codec->count8 = count - 1;
		if (codec->fmt8 == SB_DSP_LO_OUTPUT_AUTO) {
			snd_spin_lock(codec, reg, &flags);
			snd_sb8dsp_command(codec, SB_DSP_BLOCK_SIZE);
			snd_sb8dsp_command(codec, codec->count8 & 0xff);
			snd_sb8dsp_command(codec, codec->count8 >> 8);
			snd_sb8dsp_command(codec, codec->fmt8);
			snd_sb8dsp_command(codec, SB_DSP_DMA8_OFF);
			snd_spin_unlock(codec, reg, &flags);
		}
		break;
	default:
		snd_printd("unknown hardware for sb8 playback prepare!!!\n");
	}
}

static void snd_sb8_playback_trigger(snd_pcm1_t * pcm1, int up)
{
	unsigned long flags;
	sbdsp_t *codec;

	codec = (sbdsp_t *) pcm1->private_data;
	snd_spin_lock(codec, reg, &flags);
	switch (codec->hardware) {
	case SB_HW_10:
		if (up) {
			codec->mode8 = SB_MODE8_PLAYBACK;
			if (snd_sb8dsp_command(codec, SB_DSP_OUTPUT)) {
				snd_sb8dsp_command(codec, codec->count8 & 0xff);
				snd_sb8dsp_command(codec, codec->count8 >> 8);
			} else {
				snd_printk("SB 1.0 - unable to start output\n");
			}
		} else {
			snd_sb8dsp_reset(codec);
			codec->mode8 = SB_MODE8_HALT;
		}
		break;
	case SB_HW_20:
	case SB_HW_201:
	case SB_HW_PRO:
		if (codec->fmt8 == SB_DSP_HI_OUTPUT_AUTO) {
			if (up) {
				snd_sb8dsp_command(codec, SB_DSP_BLOCK_SIZE);
				snd_sb8dsp_command(codec, codec->count8 & 0xff);
				snd_sb8dsp_command(codec, codec->count8 >> 8);
				snd_sb8dsp_command(codec, codec->fmt8);
			} else {
				snd_sb8dsp_reset(codec);
			}
		} else {
			snd_sb8dsp_command(codec, up ? SB_DSP_DMA8_ON : SB_DSP_DMA8_OFF);
		}
		codec->mode8 = up ? SB_MODE8_PLAYBACK : SB_MODE8_HALT;
		break;
	}
	snd_spin_unlock(codec, reg, &flags);
}

static void snd_sb8_record_prepare(snd_pcm1_t * pcm1,
				   unsigned char *buffer,
				   unsigned int size,
				   unsigned int offset,
				   unsigned int count)
{
	unsigned long flags, flags1;
	sbdsp_t *codec;
	snd_pcm1_channel_t *pchn;

	pchn = &pcm1->record;
	codec = (sbdsp_t *) pcm1->private_data;
	switch (codec->hardware) {
	case SB_HW_10:
		snd_spin_lock(codec, reg, &flags);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		snd_sb8_ack_8bit(codec);
		snd_sb8dsp_command(codec, SB_DSP_SAMPLE_RATE);
		snd_sb8dsp_command(codec, codec->speed8);
		snd_sb8_ack_8bit(codec);
		snd_spin_unlock(codec, reg, &flags);
		snd_dma_program(codec->dma8, &buffer[offset], count, DMA_MODE_READ);
		codec->count8 = count - 1;
		break;
	case SB_HW_20:
	case SB_HW_201:
	case SB_HW_PRO:
		snd_spin_lock(codec, reg, &flags);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (snd_sb8dsp_reset(codec) < 0) {
			snd_spin_unlock(codec, reg, &flags);
			snd_printk("sb8 - reset failed!!!\n");
			return;
		}
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (codec->hardware == SB_HW_PRO) {
			snd_spin_lock(&codec->mixer, mixer, &flags1);
			if (pchn->voices > 1) {
				snd_sb8mixer_write(&codec->mixer, 0x0e, snd_sb8mixer_read(&codec->mixer, 0x0e) | 0x02);
			} else {
				snd_sb8mixer_write(&codec->mixer, 0x0e, snd_sb8mixer_read(&codec->mixer, 0x0e) & ~0x02);
			}
			snd_spin_unlock(&codec->mixer, mixer, &flags1);
		}
		if (codec->hardware == SB_HW_PRO) {
			snd_sb8dsp_command(codec, pcm1->record.voices > 1 ? SB_DSP_STEREO_8BIT : SB_DSP_MONO_8BIT);
		}
		snd_sb8_ack_8bit(codec);
		snd_sb8dsp_command(codec, SB_DSP_SAMPLE_RATE);
		snd_sb8dsp_command(codec, codec->speed8);
		snd_sb8_ack_8bit(codec);
		snd_spin_unlock(codec, reg, &flags);
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT);
		codec->count8 = count - 1;
		if (codec->fmt8 == SB_DSP_LO_INPUT_AUTO) {
			snd_spin_lock(codec, reg, &flags);
			snd_sb8dsp_command(codec, SB_DSP_BLOCK_SIZE);
			snd_sb8dsp_command(codec, codec->count8 & 0xff);
			snd_sb8dsp_command(codec, codec->count8 >> 8);
			snd_sb8dsp_command(codec, codec->fmt8);
			snd_sb8dsp_command(codec, SB_DSP_DMA8_OFF);
			snd_spin_unlock(codec, reg, &flags);
		}
		break;
	default:
		snd_printd("unknown hardware for sb8 record prepare!!!\n");
	}
}

static void snd_sb8_record_trigger(snd_pcm1_t * pcm1, int up)
{
	unsigned long flags;
	sbdsp_t *codec;

	codec = (sbdsp_t *) pcm1->private_data;
	snd_spin_lock(codec, reg, &flags);
	switch (codec->hardware) {
	case SB_HW_10:
		if (up) {
			codec->mode8 = SB_MODE8_RECORD;
			if (snd_sb8dsp_command(codec, SB_DSP_INPUT)) {
				snd_sb8dsp_command(codec, codec->count8 & 0xff);
				snd_sb8dsp_command(codec, codec->count8 >> 8);
			} else {
				snd_printk("SB 1.0 - unable to start input\n");
			}
		} else {
			snd_sb8dsp_reset(codec);
			codec->mode8 = SB_MODE8_HALT;
		}
		break;
	case SB_HW_20:
	case SB_HW_201:
	case SB_HW_PRO:
		if (codec->fmt8 == SB_DSP_HI_INPUT_AUTO) {
			if (up) {
				snd_sb8dsp_command(codec, SB_DSP_BLOCK_SIZE);
				snd_sb8dsp_command(codec, codec->count8 & 0xff);
				snd_sb8dsp_command(codec, codec->count8 >> 8);
				snd_sb8dsp_command(codec, codec->fmt8);
			} else {
				snd_sb8dsp_reset(codec);
			}
		} else {
			snd_sb8dsp_command(codec, up ? SB_DSP_DMA8_ON : SB_DSP_DMA8_OFF);
		}
		codec->mode8 = up ? SB_MODE8_RECORD : SB_MODE8_HALT;
		break;
	}
	snd_spin_unlock(codec, reg, &flags);
}

void snd_sb8dsp_interrupt(snd_pcm_t * pcm)
{
	snd_pcm1_t *pcm1;
	sbdsp_t *codec;

	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;
	switch (codec->mode8) {
	case SB_MODE8_PLAYBACK:	/* ok.. playback is active */
		pcm1->playback.ack(pcm1);
		break;
	case SB_MODE8_RECORD:
		pcm1->record.ack(pcm1);
		break;
	}
	snd_sb8_ack_8bit(codec);
}

/*

 */

static int snd_sb8_playback_open(snd_pcm1_t * pcm1)
{
	unsigned long flags;
	sbdsp_t *codec;
	int err;

	codec = (sbdsp_t *) pcm1->private_data;
	snd_spin_lock(codec, open8, &flags);
	if (codec->open8) {
		snd_spin_unlock(codec, open8, &flags);
		return -EBUSY;
	}
	codec->open8 |= SB_OPEN_PCM;
	snd_spin_unlock(codec, open8, &flags);
	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, codec->dma8ptr, "Sound Blaster DSP")) < 0) {
		snd_spin_lock(codec, open8, &flags);
		codec->open8 &= ~SB_OPEN_PCM;
		snd_spin_unlock(codec, open8, &flags);
		return err;
	}
	return 0;
}

static int snd_sb8_record_open(snd_pcm1_t * pcm1)
{
	unsigned long flags;
	sbdsp_t *codec;
	int err;

	codec = (sbdsp_t *) pcm1->private_data;
	snd_spin_lock(codec, open8, &flags);
	if (codec->open8) {
		snd_spin_unlock(codec, open8, &flags);
		return -EBUSY;
	}
	codec->open8 |= SB_OPEN_PCM;
	snd_spin_unlock(codec, open8, &flags);
	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_RECORD, codec->dma8ptr, "Sound Blaster DSP")) < 0) {
		snd_spin_lock(codec, open8, &flags);
		codec->open8 &= ~SB_OPEN_PCM;
		snd_spin_unlock(codec, open8, &flags);
		return err;
	}
	return 0;
}

static void snd_sb8_playback_close(snd_pcm1_t * pcm1)
{
	unsigned long flags;
	sbdsp_t *codec;
	snd_card_t *card;

	card = pcm1->card;
	codec = (sbdsp_t *) pcm1->private_data;
	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, codec->dma8ptr);
	snd_spin_lock(codec, open8, &flags);
	codec->open8 &= ~SB_OPEN_PCM;
	snd_spin_unlock(codec, open8, &flags);
}

static void snd_sb8_record_close(snd_pcm1_t * pcm1)
{
	unsigned long flags;
	sbdsp_t *codec;
	snd_card_t *card;

	card = pcm1->card;
	codec = (sbdsp_t *) pcm1->private_data;
	snd_pcm1_dma_free(pcm1, SND_PCM1_RECORD, codec->dma8ptr);
	snd_spin_lock(codec, open8, &flags);
	codec->open8 &= ~SB_OPEN_PCM;
	snd_spin_unlock(codec, open8, &flags);
}

static unsigned int snd_sb8_playback_pointer(snd_pcm1_t * pcm1, unsigned int used_size)
{
	sbdsp_t *codec;

	codec = (sbdsp_t *) pcm1->private_data;
	if (codec->mode8 != SB_MODE8_PLAYBACK)
		return 0;
	return used_size - snd_dma_residue(codec->dma8);
}

static unsigned int snd_sb8_record_pointer(snd_pcm1_t * pcm1, unsigned int used_size)
{
	sbdsp_t *codec;

	codec = (sbdsp_t *) pcm1->private_data;
	if (codec->mode8 != SB_MODE8_RECORD)
		return 0;
	return used_size - snd_dma_residue(codec->dma8);
}

/*

 */

struct snd_stru_pcm1_hardware snd_sb8_playback =
{
	NULL,			/* private data */
	NULL,			/* private_free */
	SND_PCM1_HW_AUTODMA,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8,	/* formats */
	SND_PCM_FMT_U8,		/* hardware formats */
	0,			/* align value */
	6,			/* minimal fragment */
	4000,			/* min. rate */
	23000,			/* max. rate */
	1,			/* max. voices */
	snd_sb8_playback_open,
	snd_sb8_playback_close,
	snd_sb8_playback_ioctl,
	snd_sb8_playback_prepare,
	snd_sb8_playback_trigger,
	snd_sb8_playback_pointer,
	snd_pcm1_playback_dma_ulaw,
	snd_pcm1_dma_move,
	snd_pcm1_playback_dma_neutral
};

struct snd_stru_pcm1_hardware snd_sb8_record =
{
	NULL,			/* private data */
	NULL,			/* private free */
	SND_PCM1_HW_AUTODMA,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8,	/* formats */
	SND_PCM_FMT_U8,		/* hardware formats */
	0,			/* align value */
	6,			/* minimal fragment */
	4000,			/* min. rate */
	13000,			/* max. rate */
	1,			/* max. voices */
	snd_sb8_record_open,
	snd_sb8_record_close,
	snd_sb8_record_ioctl,
	snd_sb8_record_prepare,
	snd_sb8_record_trigger,
	snd_sb8_record_pointer,
	snd_pcm1_record_dma_ulaw,
	snd_pcm1_dma_move,
	NULL
};
