/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Routines for control of 16-bit SoundBlaster cards and clones
 *  Note: This is very ugly hardware which uses one 8-bit DMA channel and
 *        second 16-bit DMA channel. Unfortunately 8-bit DMA channel can't
 *        transfer 16-bit samples and 16-bit DMA channels can't transfer
 *        8-bit samples. This make full duplex more complicated than
 *        can be... People, don't buy these shity soundcards for full 16-bit
 *        duplex!!!
 *  Note: 16-bit wide is assigned to first direction which made request.
 *        With full duplex - playback is preferred with abstract layer.
 *
 *
 *   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.
 *
 */

#include "driver.h"
#include "sb.h"
#include "info.h"
#include "ulaw.h"

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

static inline void snd_sb16_ack_16bit(sbdsp_t * codec)
{
	inb(SBP(codec, DATA_AVAIL_16));
}

static void snd_sb16_playback_8to16(unsigned char *buffer,
				    unsigned int offset,
				    unsigned char *user,
				    unsigned int count,
				    int ulaw)
{
	unsigned char *pbuffer;
	unsigned int size1;
	unsigned char sbuffer[512];
	unsigned int size;

#if 0
	snd_printk("p8to16: buffer = 0x%lx, offset = %i, count = %i, ulaw = %i\n", (long) buffer, offset, count, ulaw);
#endif
	buffer += offset << 1;
	while (count > 0) {
		size = size1 = sizeof(sbuffer) < count ?
						sizeof(sbuffer) : count;
		copy_from_user(sbuffer, user, size);
		pbuffer = sbuffer;
		if (ulaw) {
			while (size1-- > 0) {
				*buffer++ = 0x80;
				*buffer++ = snd_ulaw_dsp_loud[*pbuffer++];
			}
		} else {
			while (size1-- > 0) {
				*buffer++ = 0x80;
				*buffer++ = *pbuffer++;
			}
		}
		count -= size;
		user += size;
	}
}

static void snd_sb16_record_16to8(unsigned char *buffer,
				  unsigned int offset,
				  unsigned char *user,
				  unsigned int count,
				  int ulaw)
{
	unsigned char *pbuffer;
	unsigned int size1;
	unsigned char sbuffer[512];
	unsigned int size;

#if 0
	snd_printk("r16to8: buffer = 0x%lx, offset = %i, count = %i, ulaw = %i\n", (long) buffer, offset, count, ulaw);
#endif
	buffer += (offset << 1) + 1;
	while (count > 0) {
		size = size1 = sizeof(sbuffer) < count ?
						sizeof(sbuffer) : count;
		pbuffer = sbuffer;	/* little endian */
		if (ulaw) {
			while (size1-- > 0) {
				*pbuffer++ = snd_dsp_ulaw_loud[*buffer];
				buffer += 2;
			}
		} else {
			while (size1-- > 0) {
				*pbuffer++ = *buffer;
				buffer += 2;
			}
		}
		copy_to_user(user, sbuffer, size);
		count -= size;
		user += size;
	}
}

static void snd_sb16_playback_16to8(unsigned char *buffer,
				    unsigned int offset,
				    unsigned char *user,
				    unsigned int count)
{
	unsigned char *pbuffer;
	unsigned int size1;
	unsigned char sbuffer[512];
	unsigned int size;

#if 0
	snd_printk("p8to16: buffer = 0x%lx, offset = %i, count = %i\n", (long) buffer, offset, count);
#endif
	buffer += offset >> 1;
	while (count > 0) {
		size = size1 = sizeof(sbuffer) < count ?
						sizeof(sbuffer) : count;
		copy_from_user(sbuffer, user, size);
		pbuffer = sbuffer + 1;	/* little endian data */
		size1 >>= 1;
		while (size1-- > 0) {
			*buffer++ = *pbuffer;
			pbuffer += 2;
		}
		count -= size;
		user += size;
	}
}

static void snd_sb16_record_8to16(unsigned char *buffer,
				  unsigned int offset,
				  unsigned char *user,
				  unsigned int count)
{
	unsigned char *pbuffer;
	unsigned int size1;
	unsigned char sbuffer[512];
	unsigned int size;

#if 0
	snd_printk("r8to16: buffer = 0x%lx, offset = %i, count = %i\n", (long) buffer, offset, count);
#endif
	buffer += offset >> 1;
	while (count > 0) {
		size = size1 = sizeof(sbuffer) < count ? sizeof(sbuffer) : count;
		pbuffer = sbuffer;	/* little endian */
		size1 >>= 1;
		while (size1-- > 0) {
			*pbuffer++ = 0x80;
			*pbuffer++ = *buffer++;
		}
		copy_to_user(user, sbuffer, size);
		count -= size;
		user += size;
	}
}

static int snd_sb16_playback_ioctl(snd_pcm1_t * pcm1,
				   unsigned int cmd, unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		pcm1->playback.real_rate = pcm1->playback.rate;
		if (pcm1->playback.real_rate < 4000)
			pcm1->playback.real_rate = 4000;
		if (pcm1->playback.real_rate > 44100)
			pcm1->playback.real_rate = 44100;
		return 0;
	}
	return -ENXIO;
}

static int snd_sb16_record_ioctl(snd_pcm1_t * pcm1,
				 unsigned int cmd, unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		pcm1->record.real_rate = pcm1->record.rate;
		if (pcm1->record.real_rate < 4000)
			pcm1->record.real_rate = 4000;
		if (pcm1->record.real_rate > 44100)
			pcm1->record.real_rate = 44100;
		return 0;
	}
	return -ENXIO;
}

static void snd_sb16_setup_rate(sbdsp_t * codec,
				unsigned short rate, int direction)
{
	unsigned long flags;

	snd_spin_lock(codec, reg, &flags);
	if (codec->mode16 & (direction == SND_PCM1_PLAYBACK ? SB_MODE16_PLAYBACK16 : SB_MODE16_RECORD16))
		snd_sb16_ack_16bit(codec);
	else
		snd_sb16_ack_8bit(codec);
	if (!(codec->mode16 & SB_MODE16_RATE_LOCK)) {
		snd_sb16dsp_command(codec, SB_DSP_SAMPLE_RATE_IN);
		snd_sb16dsp_command(codec, rate >> 8);
		snd_sb16dsp_command(codec, rate & 0xff);
		snd_sb16dsp_command(codec, SB_DSP_SAMPLE_RATE_OUT);
		snd_sb16dsp_command(codec, rate >> 8);
		snd_sb16dsp_command(codec, rate & 0xff);
	}
	snd_spin_unlock(codec, reg, &flags);
}

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

	pchn = &pcm1->playback;
	codec = (sbdsp_t *) pcm1->private_data;
	format = SB_DSP4_MODE_UNS_MONO;
	if (pchn->mode & SND_PCM1_MODE_U) {
		format = pchn->voices > 1 ? SB_DSP4_MODE_UNS_STEREO : SB_DSP4_MODE_UNS_MONO;
	} else {
		format = pchn->voices > 1 ? SB_DSP4_MODE_SIGN_STEREO : SB_DSP4_MODE_SIGN_MONO;
	}
	snd_sb16_setup_rate(codec, pchn->real_rate, SND_PCM1_PLAYBACK);
#if 0
	snd_printk("playback - buffer = 0x%lx, size = %i (%i), count = %i\n", (long) buffer, size, pchn->size, count);
#endif
	if (codec->mode16 & SB_MODE16_PLAYBACK16) {	/* use 16-bit DMA */
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			size <<= 1;
			count <<= 1;
		}
		snd_dma_program(codec->dma16, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT);
		count >>= 1;
		count--;
		snd_spin_lock(codec, reg, &flags);
		snd_sb16dsp_command(codec, SB_DSP_SPEAKER_ON);
		snd_sb16dsp_command(codec, SB_DSP4_OUT16_AI);
		snd_sb16dsp_command(codec, format);
		snd_sb16dsp_command(codec, count & 0xff);
		snd_sb16dsp_command(codec, count >> 8);
		snd_sb16dsp_command(codec, SB_DSP_DMA16_OFF);
		snd_spin_unlock(codec, reg, &flags);
	} else {
		if (pchn->mode & SND_PCM1_MODE_16) {
			size >>= 1;
			count >>= 1;
		}
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT);
		count--;
		snd_spin_lock(codec, reg, &flags);
		snd_sb16dsp_command(codec, SB_DSP_SPEAKER_ON);
		snd_sb16dsp_command(codec, SB_DSP4_OUT8_AI);
		snd_sb16dsp_command(codec, format);
		snd_sb16dsp_command(codec, count & 0xff);
		snd_sb16dsp_command(codec, count >> 8);
		snd_sb16dsp_command(codec, SB_DSP_DMA8_OFF);
		snd_spin_unlock(codec, reg, &flags);
	}
}

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

	codec = (sbdsp_t *) pcm1->private_data;
#if 0
	snd_printk("playback trigger - %i\n", up);
#endif
	snd_spin_lock(codec, reg, &flags);
	if (up) {
		snd_sb16dsp_command(codec, SB_DSP_SPEAKER_ON);
		snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_PLAYBACK16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON);
		codec->mode16 |= SB_MODE16_RATE_LOCK_P;
	} else {
		codec->mode16 &= ~SB_MODE16_RATE_LOCK_P;
		snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_PLAYBACK16 ? SB_DSP_DMA16_OFF : SB_DSP_DMA8_OFF);
		snd_sb16dsp_command(codec, SB_DSP_SPEAKER_OFF);
		/* next two lines are needed for some types of DSP4 (SB AWE 32 - 4.13) */
		if (codec->mode16 & SB_MODE16_RATE_LOCK_R) {
#if 0
			snd_printk("record trigger!!!! (from playback)");
#endif
			snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_RECORD16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON);
		}
	}
	snd_spin_unlock(codec, reg, &flags);
}

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

	pchn = &pcm1->record;
	codec = (sbdsp_t *) pcm1->private_data;
	format = SB_DSP4_MODE_UNS_MONO;
	if (pchn->mode & SND_PCM1_MODE_U) {
		format = pchn->voices > 1 ? SB_DSP4_MODE_UNS_STEREO : SB_DSP4_MODE_UNS_MONO;
	} else {
		format = pchn->voices > 1 ? SB_DSP4_MODE_SIGN_STEREO : SB_DSP4_MODE_SIGN_MONO;
	}
	snd_sb16_setup_rate(codec, pchn->real_rate, SND_PCM1_RECORD);
#if 0
	snd_printk("record - buffer = 0x%lx, size = %i (%i), count = %i\n", (long) buffer, size, pchn->size, count);
#endif
	if (codec->mode16 & SB_MODE16_RECORD16) {	/* use 16-bit DMA */
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			size <<= 1;
			count <<= 1;
		}
		snd_dma_program(codec->dma16, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT);
		count >>= 1;
		count--;
		snd_spin_lock(codec, reg, &flags);
		snd_sb16dsp_command(codec, SB_DSP4_IN16_AI);
		snd_sb16dsp_command(codec, format);
		snd_sb16dsp_command(codec, count & 0xff);
		snd_sb16dsp_command(codec, count >> 8);
		snd_sb16dsp_command(codec, SB_DSP_DMA16_OFF);
		snd_spin_unlock(codec, reg, &flags);
	} else {
		if (pchn->mode & SND_PCM1_MODE_16) {
			size >>= 1;
			count >>= 1;
		}
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT);
		count--;
		snd_spin_lock(codec, reg, &flags);
		snd_sb16dsp_command(codec, SB_DSP4_IN8_AI);
		snd_sb16dsp_command(codec, format);
		snd_sb16dsp_command(codec, count & 0xff);
		snd_sb16dsp_command(codec, count >> 8);
		snd_sb16dsp_command(codec, SB_DSP_DMA8_OFF);
		snd_spin_unlock(codec, reg, &flags);
	}
}

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

#if 0
	snd_printk("sb16: record trigger - %i\n", up);
#endif
	codec = (sbdsp_t *) pcm1->private_data;
	snd_spin_lock(codec, reg, &flags);
	if (up) {
		codec->mode16 |= SB_MODE16_RATE_LOCK_R;
		snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_RECORD16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON);
	} else {
		codec->mode16 &= ~SB_MODE16_RATE_LOCK_R;
		snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_RECORD16 ? SB_DSP_DMA16_OFF : SB_DSP_DMA8_OFF);
		/* next two lines are needed for some types of DSP4 (SB AWE 32 - 4.13) */
		if (codec->mode16 & SB_MODE16_RATE_LOCK_P)
			snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_PLAYBACK16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON);
	}
	snd_spin_unlock(codec, reg, &flags);
}

void snd_sb16dsp_interrupt(snd_pcm_t * pcm, unsigned short status)
{
	int ok;
	sbdsp_t *codec;
	snd_pcm1_t *pcm1;
	unsigned long flags;

	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;
#if 0
	PRINTK("sb16: interrupt - status = 0x%x\n", status);
#endif
	if (status & 0x01) {
		ok = 0;
		if ((codec->mode16 & (SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK)) == SB_MODE16_PLAYBACK) {
			pcm1->playback.ack(pcm1);
			ok++;
		}
		if ((codec->mode16 & (SB_MODE16_RECORD16 | SB_MODE16_RECORD)) == SB_MODE16_RECORD) {
			pcm1->record.ack(pcm1);
			ok++;
		}
		snd_spin_lock(codec, reg, &flags);
		if (!ok)
			snd_sb16dsp_command(codec, SB_DSP_DMA8_OFF);
		snd_sb16_ack_8bit(codec);
		snd_spin_unlock(codec, reg, &flags);
	}
	if (status & 0x02) {
		ok = 0;
		if ((codec->mode16 & (SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK)) == (SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK)) {
			pcm1->playback.ack(pcm1);
			ok++;
		}
		if ((codec->mode16 & (SB_MODE16_RECORD16 | SB_MODE16_RECORD)) == (SB_MODE16_RECORD16 | SB_MODE16_RECORD)) {
			pcm1->record.ack(pcm1);
			ok++;
		}
		snd_spin_lock(codec, reg, &flags);
		if (!ok)
			snd_sb16dsp_command(codec, SB_DSP_DMA16_OFF);
		snd_sb16_ack_16bit(codec);
		snd_spin_unlock(codec, reg, &flags);
	}
}

/*

 */

static int snd_sb16_playback_open(snd_pcm1_t * pcm1)
{
	unsigned long flags;
	sbdsp_t *codec;
	int err = -EBUSY;

	codec = (sbdsp_t *) pcm1->private_data;
	if (codec->dma16ptr &&
	    (codec->force_mode16 & (SB_MODE16_PLAYBACK | SB_MODE16_AUTO))) {
		if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, codec->dma16ptr, "Sound Blaster 16 DSP")) >= 0) {
			snd_spin_lock(codec, open16, &flags);
			codec->mode16 |= SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK;
			pcm1->playback.hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pcm1->playback.hw.flags |= SND_PCM1_HW_16BITONLY;
			pcm1->record.hw.hw_formats = SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE;
			snd_spin_unlock(codec, open16, &flags);
			return 0;
		}
	}
	if (codec->dma8ptr &&
	    ((codec->force_mode16 & (SB_MODE16_PLAYBACK | SB_MODE16_AUTO)) == SB_MODE16_AUTO ||
	     (codec->force_mode16 & (SB_MODE16_PLAYBACK | SB_MODE16_AUTO)) == 0)
	    ) {
		if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, codec->dma8ptr, "Sound Blaster 16 DSP")) >= 0) {
			snd_spin_lock(codec, open16, &flags);
			codec->mode16 &= ~SB_MODE16_PLAYBACK16;
			codec->mode16 |= SB_MODE16_PLAYBACK;
			pcm1->playback.hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pcm1->playback.hw.flags |= SND_PCM1_HW_8BITONLY;
			pcm1->record.hw.hw_formats = SND_PCM_FMT_U8 | SND_PCM_FMT_S8;
			snd_spin_unlock(codec, open16, &flags);
			return 0;
		}
	}
	return err;
}

static int snd_sb16_record_open(snd_pcm1_t * pcm1)
{
	unsigned long flags;
	sbdsp_t *codec;
	int err = -EBUSY;

	codec = (sbdsp_t *) pcm1->private_data;
	if (codec->dma16ptr &&
	    (codec->force_mode16 & (SB_MODE16_RECORD | SB_MODE16_AUTO))) {
		if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_RECORD, codec->dma16ptr, "Sound Blaster 16 DSP")) >= 0) {
			snd_spin_lock(codec, open16, &flags);
			codec->mode16 |= SB_MODE16_RECORD16 | SB_MODE16_RECORD;
			pcm1->record.hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pcm1->record.hw.flags |= SND_PCM1_HW_16BITONLY;
			pcm1->record.hw.hw_formats = SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE;
			snd_spin_unlock(codec, open16, &flags);
			return 0;
		}
	}
	if (codec->dma8ptr &&
	    ((codec->force_mode16 & (SB_MODE16_RECORD | SB_MODE16_AUTO)) == SB_MODE16_AUTO ||
	    (codec->force_mode16 & (SB_MODE16_RECORD | SB_MODE16_AUTO)) == 0)) {
		if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_RECORD, codec->dma8ptr, "Sound Blaster 16 DSP")) >= 0) {
			snd_spin_lock(codec, open16, &flags);
			codec->mode16 &= ~SB_MODE16_RECORD16;
			codec->mode16 |= SB_MODE16_RECORD;
			pcm1->record.hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pcm1->record.hw.flags |= SND_PCM1_HW_8BITONLY;
			pcm1->record.hw.hw_formats = SND_PCM_FMT_U8 | SND_PCM_FMT_S8;
			snd_spin_unlock(codec, open16, &flags);
			return 0;
		}
	}
	return err;
}

static void snd_sb16_playback_close(snd_pcm1_t * pcm1)
{
	sbdsp_t *codec;

	codec = (sbdsp_t *) pcm1->private_data;
	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK,
			  codec->mode16 & SB_MODE16_PLAYBACK16 ?
			  codec->dma16ptr : codec->dma8ptr);
	codec->mode16 &= ~(SB_MODE16_PLAYBACK | SB_MODE16_PLAYBACK16);
	pcm1->playback.hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
}

static void snd_sb16_record_close(snd_pcm1_t * pcm1)
{
	sbdsp_t *codec;

	codec = (sbdsp_t *) pcm1->private_data;
	snd_pcm1_dma_free(pcm1, SND_PCM1_RECORD,
			  codec->mode16 & SB_MODE16_RECORD16 ?
			  codec->dma16ptr : codec->dma8ptr);
	codec->mode16 &= ~(SB_MODE16_RECORD | SB_MODE16_RECORD16);
	pcm1->record.hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
}

static unsigned int snd_sb16_playback_pointer(snd_pcm1_t * pcm1,
					      unsigned int used_size)
{
	sbdsp_t *codec;
	snd_pcm1_channel_t *pchn;
	unsigned int result;

	codec = (sbdsp_t *) pcm1->private_data;
	pchn = &pcm1->playback;
	if (codec->mode16 & SB_MODE16_PLAYBACK16) {
		result = used_size - snd_dma_residue(codec->dma16);
		if (!(pchn->mode & SND_PCM1_MODE_16))
			result >>= 1;
	} else {
		result = used_size - snd_dma_residue(codec->dma8);
		if (pchn->mode & SND_PCM1_MODE_16)
			result <<= 1;
	}
	return result;
}

static unsigned int snd_sb16_record_pointer(snd_pcm1_t * pcm1,
					    unsigned int used_size)
{
	sbdsp_t *codec;
	snd_pcm1_channel_t *pchn;
	unsigned int result;

	codec = (sbdsp_t *) pcm1->private_data;
	pchn = &pcm1->record;
	if (codec->mode16 & SB_MODE16_RECORD16) {
		result = used_size - snd_dma_residue(codec->dma16);
		if (!(pchn->mode & SND_PCM1_MODE_16))
			result >>= 1;
	} else {
		result = used_size - snd_dma_residue(codec->dma8);
		if (pchn->mode & SND_PCM1_MODE_16)
			result <<= 1;
	}
	return result;
}

static void snd_sb16_playback_dma(snd_pcm1_t * pcm1,
			      	  unsigned char *buffer, unsigned int offset,
				  unsigned char *user, unsigned int count)
{
	snd_pcm1_channel_t *pchn;

	pchn = &pcm1->playback;
	if (pchn->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (pchn->mode & SND_PCM1_MODE_16) {
			snd_sb16_playback_16to8(buffer, offset, user, count);
			return;
		}
	}
	if (pchn->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			snd_sb16_playback_8to16(buffer, offset, user, count, (pchn->mode & SND_PCM1_MODE_ULAW) ? 1 : 0);
			return;
		}
	}
	if (pcm1->playback.mode & SND_PCM1_MODE_ULAW)
		snd_translate_from_user(snd_ulaw_dsp_loud, &buffer[offset], user, count);
	else
		copy_from_user(&buffer[offset], user, count);
}

static void snd_sb16_record_dma(snd_pcm1_t * pcm1,
			        unsigned char *buffer, unsigned int offset,
				unsigned char *user, unsigned int count)
{
	snd_pcm1_channel_t *pchn;

	pchn = &pcm1->record;
	if (pchn->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (pchn->mode & SND_PCM1_MODE_16) {
			snd_sb16_record_8to16(buffer, offset, user, count);
			return;
		}
	}
	if (pchn->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			snd_sb16_record_16to8(buffer, offset, user, count, (pchn->mode & SND_PCM1_MODE_ULAW) ? 1 : 0);
			return;
		}
	}
	if (pcm1->record.mode & SND_PCM1_MODE_ULAW)
		snd_translate_to_user(snd_dsp_ulaw_loud, user, &buffer[offset], count);
	else
		copy_to_user(user, &buffer[offset], count);
}

static void snd_sb16_playback_dma_move(snd_pcm1_t * pcm1,
				       unsigned char *dbuffer,
				       unsigned int dest_offset,
			 	       unsigned char *sbuffer,
			 	       unsigned int src_offset,
				       unsigned int count)
{
	snd_pcm1_channel_t *pchn;

	pchn = &pcm1->playback;
	if (pchn->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (pchn->mode & SND_PCM1_MODE_16) {
			memcpy(&dbuffer[dest_offset >> 1], &sbuffer[src_offset >> 1], count >> 1);
			return;
		}
	}
	if (pchn->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			memcpy(&dbuffer[dest_offset << 1], &sbuffer[src_offset << 1], count << 1);
			return;
		}
	}
	snd_pcm1_dma_move(pcm1, dbuffer, dest_offset, sbuffer, src_offset, count);
}

static void snd_sb16_record_dma_move(snd_pcm1_t * pcm1,
				     unsigned char *dbuffer,
				     unsigned int dest_offset,
			 	     unsigned char *sbuffer,
			 	     unsigned int src_offset,
				     unsigned int count)
{
	snd_pcm1_channel_t *pchn;

#if 0
	snd_printk("record dma move, buffer = 0x%lx, dest_offset = %i, src_offset = %i, count = %i\n", (long) buffer, dest_offset, src_offset, count);
#endif
	pchn = &pcm1->record;
	if (pchn->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (pchn->mode & SND_PCM1_MODE_16) {
			memcpy(&dbuffer[dest_offset >> 1], &sbuffer[src_offset >> 1], count >> 1);
			return;
		}
	}
	if (pchn->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			memcpy(&dbuffer[dest_offset << 1], &sbuffer[src_offset << 1], count << 1);
			return;
		}
	}
	snd_pcm1_dma_move(pcm1, dbuffer, dest_offset, sbuffer, src_offset, count);
}

static void snd_sb16_playback_dma_neutral(snd_pcm1_t * pcm1,
			      		  unsigned char *buffer,
			      		  unsigned int offset,
					  unsigned int count,
					  unsigned char neutral_byte)
{
	snd_pcm1_channel_t *pchn;

	pchn = &pcm1->playback;
	if (pchn->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (pchn->mode & SND_PCM1_MODE_16) {
			memset(&buffer[offset >> 1], neutral_byte, count >> 1);
			return;
		}
	}
	if (pchn->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(pchn->mode & SND_PCM1_MODE_16)) {
			memset(&buffer[offset << 1], neutral_byte, count << 1);
			return;
		}
	}
	snd_pcm1_playback_dma_neutral(pcm1, buffer, offset, count, neutral_byte);
}

/*

 */

struct snd_stru_pcm1_hardware snd_sb16_playback =
{
	NULL,			/* private data */
	NULL,			/* private_free */
	SND_PCM1_HW_AUTODMA,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8 | SND_PCM_FMT_S8 |
	    SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE, /* formats */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S8 | SND_PCM_FMT_S16_LE |
	    SND_PCM_FMT_U16_LE,	/* hardware formats */
	0,			/* align value */
	6,			/* minimal fragment */
	4000,			/* min. rate */
	44100,			/* max. rate */
	2,			/* max. voices */
	snd_sb16_playback_open,
	snd_sb16_playback_close,
	snd_sb16_playback_ioctl,
	snd_sb16_playback_prepare,
	snd_sb16_playback_trigger,
	snd_sb16_playback_pointer,
	snd_sb16_playback_dma,
	snd_sb16_playback_dma_move,
	snd_sb16_playback_dma_neutral
};

struct snd_stru_pcm1_hardware snd_sb16_record =
{
	NULL,			/* private data */
	NULL,			/* private free */
	SND_PCM1_HW_AUTODMA,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8 | SND_PCM_FMT_S8 |
	    SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE,	/* formats */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S8 | SND_PCM_FMT_S16_LE |
	    SND_PCM_FMT_U16_LE,	/* hardware formats */
	0,			/* align value */
	6,			/* minimal fragment */
	4000,			/* min. rate */
	44100,			/* max. rate */
	2,			/* max. voices */
	snd_sb16_record_open,
	snd_sb16_record_close,
	snd_sb16_record_ioctl,
	snd_sb16_record_prepare,
	snd_sb16_record_trigger,
	snd_sb16_record_pointer,
	snd_sb16_record_dma,
	snd_sb16_record_dma_move,
	NULL
};

/*
 *  /proc interface
 */

static void snd_sb16_proc_read(snd_info_buffer_t * buffer, void *private_data)
{
	sbdsp_t *codec;
	char *tmp, *tmp1;

	codec = (sbdsp_t *) private_data;
	snd_iprintf(buffer, "SB16 DMA usage\n\n");
	if (codec->dma8ptr == NULL || codec->dma16ptr == NULL) {
		snd_iprintf(buffer, "Shared DMA    : %s\n", codec->dma8ptr == NULL ? "16-bit" : "8-bit");
		return;
	}
	tmp = "8-bit";
	if (codec->force_mode16 & SB_MODE16_AUTO)
		tmp = "auto";
	else if (codec->force_mode16 & SB_MODE16_PLAYBACK)
		tmp = "16-bit";
	tmp1 = "inactive";
	if (codec->mode16 & SB_MODE16_PLAYBACK) {
		if (codec->mode16 & SB_MODE16_PLAYBACK16)
			tmp1 = "16-bit";
		else
			tmp1 = "8-bit";
	}
	snd_iprintf(buffer, "Playback DMA  : %s (%s)\n", tmp, tmp1);
	tmp = "8-bit";
	if (codec->force_mode16 & SB_MODE16_AUTO)
		tmp = "auto";
	else if (codec->force_mode16 & SB_MODE16_RECORD)
		tmp = "16-bit";
	tmp1 = "inactive";
	if (codec->mode16 & SB_MODE16_RECORD) {
		if (codec->mode16 & SB_MODE16_RECORD16)
			tmp1 = "16-bit";
		else
			tmp1 = "8-bit";
	}
	snd_iprintf(buffer, "Record DMA    : %s (%s)\n", tmp, tmp1);
}

static void snd_sb16_proc_setup_mode(sbdsp_t * codec, int what)
{
	if (codec->dma8ptr == NULL || codec->dma16ptr == NULL)
		return;
	if (what == 0) {
		codec->force_mode16 = SB_MODE16_AUTO;
	} else if (what == 1) {
		codec->force_mode16 = SB_MODE16_PLAYBACK;
	} else if (what == 2) {
		codec->force_mode16 = SB_MODE16_RECORD;
	}
}

static void snd_sb16_proc_write(snd_info_buffer_t * buffer, void *private_data)
{
	sbdsp_t *codec;
	char line[80], str[80];
	char *ptr;

	codec = (sbdsp_t *) private_data;
	if (snd_info_get_line(buffer, line, sizeof(line))) {
		buffer->error = -EINVAL;
		return;
	}
	if (!strncmp(line, "Playback ", 9)) {
		ptr = snd_info_get_str(str, line + 9, sizeof(str));
		if (!strncmp(str, "16", 2))
			snd_sb16_proc_setup_mode(codec, 1);
		else if (!strncmp(str, "8", 1))
			snd_sb16_proc_setup_mode(codec, 2);
		else
			snd_sb16_proc_setup_mode(codec, 0);
	} else if (!strncmp(line, "Record ", 7)) {
		ptr = snd_info_get_str(str, line + 7, sizeof(str));
		if (!strncmp(str, "16", 2))
			snd_sb16_proc_setup_mode(codec, 2);
		else if (!strncmp(str, "8", 1))
			snd_sb16_proc_setup_mode(codec, 1);
		else
			snd_sb16_proc_setup_mode(codec, 0);
	} else {
		buffer->error = -EINVAL;
	}
}

void snd_sb16dsp_proc_init(snd_pcm_t * pcm)
{
	sbdsp_t *codec;
	snd_pcm1_t *pcm1;
	snd_info_entry_t *entry;

	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;
	if ((entry = snd_info_create_entry(pcm->card, "sb16")) != NULL) {
		entry->private_data = codec;
		entry->mode = S_IFREG | S_IRUGO | S_IWUSR;
		entry->t.text.read_size = 256;
		entry->t.text.read = snd_sb16_proc_read;
		entry->t.text.write_size = 128;
		entry->t.text.write = snd_sb16_proc_write;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	codec->proc_entry = entry;
}

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

	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;
	if (codec->proc_entry) {
		snd_info_unregister(codec->proc_entry);
		codec->proc_entry = NULL;
	}
}
