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

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

static void snd_sb8_compute_rate(sbdsp_t *codec, snd_pcm_subchn_t * subchn)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	int rate;

	subchn1->real_rate = rate = subchn1->rate;
	switch (codec->hardware) {
	case SB_HW_10:
	case SB_HW_20:
		if (subchn->capture && rate > 13000)
			rate = 13000;
		if (rate < 4000)
			rate = 4000;
		if (rate > 23000)
			rate = 23000;
		codec->speed8 = (65535 - (256000000 + rate / 2) / rate) >> 8;
		if (!subchn->capture) {
			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 */
		}
		subchn1->real_rate = 256000000 / (65536 - (codec->speed8 << 8));
		break;
	case SB_HW_201:
		if (subchn->capture && rate > 15000)
			rate = 15000;
		if (rate < 4000)
			rate = 4000;
		if (rate > 44100)
			rate = 44100;
		codec->speed8 = (65536 - (256000000 + rate / 2) / rate) >> 8;
		if (!subchn->capture) {
			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;
		}
		subchn1->real_rate = 256000000 / (65536 - (codec->speed8 << 8));
		break;
	case SB_HW_PRO:
		if (rate < 4000)
			rate = 4000;
		if (subchn->capture) {
			if (rate > 15000)
				rate = 15000;
		}
		if (subchn1->voices > 1) {
			rate = rate > (22050 + 11025) / 2 ? 22050 : 11025;
			codec->speed8 = (65536 - (256000000 + rate) / (rate << 1)) >> 8;
			if (!subchn->capture) {
				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 (!subchn->capture) {
				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;
			}
		}
		subchn1->real_rate = (256000000 / (65536 - (codec->speed8 << 8))) / subchn1->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(void *private_data,
				  snd_pcm_subchn_t * subchn,
				  unsigned int cmd,
				  unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		snd_sb8_compute_rate((sbdsp_t *)private_data, subchn);
		return 0;
	}
	return -ENXIO;
}

static int snd_sb8_capture_ioctl(void *private_data,
				snd_pcm_subchn_t * subchn,
				unsigned int cmd,
				unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		snd_sb8_compute_rate((sbdsp_t *)private_data, subchn);
		return 0;
	}
	return -ENXIO;
}

static void snd_sb8_playback_prepare(void *private_data,
				     snd_pcm_subchn_t * subchn,
				     unsigned char *buffer,
				     unsigned int size,
				     unsigned int offset,
				     unsigned int count)
{
	unsigned long flags, flags1;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (codec->hardware) {
	case SB_HW_10:
		spin_lock_irqsave(&codec->reg_lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, 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:
		spin_lock_irqsave(&codec->reg_lock, flags);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (snd_sb8dsp_reset(codec) < 0) {
			spin_unlock_irqrestore(&codec->reg_lock, flags);
			snd_printk("sb8 - reset failed!!!\n");
			return;
		}
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (codec->hardware == SB_HW_PRO) {
			spin_lock_irqsave(&codec->mixer.lock, flags1);
			if (subchn1->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);
			}
			spin_unlock_irqrestore(&codec->mixer.lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_WRITE | DMA_AUTOINIT);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_ON);
		codec->count8 = count - 1;
		if (codec->fmt8 == SB_DSP_LO_OUTPUT_AUTO) {
			spin_lock_irqsave(&codec->reg_lock, 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);
			spin_unlock_irqrestore(&codec->reg_lock, flags);
		}
		break;
	default:
		snd_printd("unknown hardware for sb8 playback prepare!!!\n");
	}
}

static void snd_sb8_playback_trigger(void *private_data,
				     snd_pcm_subchn_t * subchn,
				     int up)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;

	spin_lock_irqsave(&codec->reg_lock, 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;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

static void snd_sb8_capture_prepare(void *private_data,
				   snd_pcm_subchn_t * subchn,
				   unsigned char *buffer,
				   unsigned int size,
				   unsigned int offset,
				   unsigned int count)
{
	unsigned long flags, flags1;
	sbdsp_t *codec = (sbdsp_t *)private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (codec->hardware) {
	case SB_HW_10:
		spin_lock_irqsave(&codec->reg_lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, 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:
		spin_lock_irqsave(&codec->reg_lock, flags);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (snd_sb8dsp_reset(codec) < 0) {
			spin_unlock_irqrestore(&codec->reg_lock, flags);
			snd_printk("sb8 - reset failed!!!\n");
			return;
		}
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (codec->hardware == SB_HW_PRO) {
			spin_lock_irqsave(&codec->mixer.lock, flags1);
			if (subchn1->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);
			}
			spin_unlock_irqrestore(&codec->mixer.lock, &flags1);
		}
		if (codec->hardware == SB_HW_PRO) {
			snd_sb8dsp_command(codec, subchn1->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);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_READ | DMA_AUTOINIT);
		codec->count8 = count - 1;
		if (codec->fmt8 == SB_DSP_LO_INPUT_AUTO) {
			spin_lock_irqsave(&codec->reg_lock, 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);
			spin_unlock_irqrestore(&codec->reg_lock, flags);
		}
		break;
	default:
		snd_printd("unknown hardware for sb8 capture prepare!!!\n");
	}
}

static void snd_sb8_capture_trigger(void *private_data,
				    snd_pcm_subchn_t * subchn,
				    int up)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;

	spin_lock_irqsave(&codec->reg_lock, flags);
	switch (codec->hardware) {
	case SB_HW_10:
		if (up) {
			codec->mode8 = SB_MODE8_CAPTURE;
			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_CAPTURE : SB_MODE8_HALT;
		break;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

void snd_sb8dsp_interrupt(snd_pcm_t * pcm)
{
	sbdsp_t *codec = (sbdsp_t *) pcm->private_data;

	switch (codec->mode8) {
	case SB_MODE8_PLAYBACK:	/* ok.. playback is active */
		codec->playback_subchn1->ack(codec->playback_subchn);
		break;
	case SB_MODE8_CAPTURE:
		codec->capture_subchn1->ack(codec->capture_subchn);
		break;
	}
	snd_sb8_ack_8bit(codec);
}

/*

 */

static int snd_sb8_playback_open(void *private_data,
				 snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	int err;

	spin_lock_irqsave(&codec->open8_lock, flags);
	if (codec->open8) {
		spin_unlock_irqrestore(&codec->open8_lock, flags);
		return -EBUSY;
	}
	codec->playback_subchn = subchn;
	codec->playback_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	codec->open8 |= SB_OPEN_PCM;
	spin_unlock_irqrestore(&codec->open8_lock, flags);
	if ((err = snd_pcm1_dma_alloc(subchn, codec->dma8ptr, "Sound Blaster DSP")) < 0) {
		spin_lock_irqsave(&codec->open8_lock, flags);
		codec->playback_subchn = NULL;
		codec->playback_subchn1 = NULL;
		codec->open8 &= ~SB_OPEN_PCM;
		spin_unlock_irqrestore(&codec->open8_lock, flags);
		return err;
	}
	return 0;
}

static int snd_sb8_capture_open(void *private_data,
				snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	int err;

	spin_lock_irqsave(&codec->open8_lock, flags);
	if (codec->open8) {
		spin_unlock_irqrestore(&codec->open8_lock, flags);
		return -EBUSY;
	}
	codec->capture_subchn = subchn;
	codec->capture_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	codec->open8 |= SB_OPEN_PCM;
	spin_unlock_irqrestore(&codec->open8_lock, flags);
	if ((err = snd_pcm1_dma_alloc(subchn, codec->dma8ptr, "Sound Blaster DSP")) < 0) {
		spin_lock_irqsave(&codec->open8_lock, flags);
		codec->capture_subchn = NULL;
		codec->capture_subchn1 = NULL;
		codec->open8 &= ~SB_OPEN_PCM;
		spin_unlock_irqrestore(&codec->open8_lock, flags);
		return err;
	}
	return 0;
}

static void snd_sb8_playback_close(void *private_data,
				   snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;

	codec->capture_subchn = NULL;
	codec->capture_subchn1 = NULL;
	snd_pcm1_dma_free(subchn);
	spin_lock_irqsave(&codec->open8_lock, flags);
	codec->open8 &= ~SB_OPEN_PCM;
	spin_unlock_irqrestore(&codec->open8_lock, flags);
}

static void snd_sb8_capture_close(void *private_data,
				  snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;

	codec->capture_subchn = NULL;
	codec->capture_subchn1 = NULL;
	snd_pcm1_dma_free(subchn);
	spin_lock_irqsave(&codec->open8_lock, flags);
	codec->open8 &= ~SB_OPEN_PCM;
	spin_unlock_irqrestore(&codec->open8_lock, flags);
}

static unsigned int snd_sb8_playback_pointer(void *private_data,
					     snd_pcm_subchn_t * subchn,
					     unsigned int used_size)
{
	sbdsp_t *codec = (sbdsp_t *) private_data;

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

static unsigned int snd_sb8_capture_pointer(void *private_data,
					    snd_pcm_subchn_t * subchn,
					    unsigned int used_size)
{
	sbdsp_t *codec = (sbdsp_t *) private_data;

	if (codec->mode8 != SB_MODE8_CAPTURE)
		return 0;
	return used_size - snd_dma_residue(codec->dma8);
}

/*

 */

struct snd_stru_pcm1_hardware snd_sb8_playback =
{
	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_capture =
{
	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_capture_open,
	snd_sb8_capture_close,
	snd_sb8_capture_ioctl,
	snd_sb8_capture_prepare,
	snd_sb8_capture_trigger,
	snd_sb8_capture_pointer,
	snd_pcm1_capture_dma_ulaw,
	snd_pcm1_dma_move,
	NULL
};
