/*
 *  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 "../../include/driver.h"
#include "../../include/sb.h"
#include "../../include/info.h"
#include "../../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 int 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;
		if (copy_from_user(sbuffer, user, size))
			return -EFAULT;
		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;
	}
	return 0;
}

static int snd_sb16_capture_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;
			}
		}
		if (copy_to_user(user, sbuffer, size))
			return -EFAULT;
		count -= size;
		user += size;
	}
	return 0;
}

static int 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;
		if (copy_from_user(sbuffer, user, size))
			return -EFAULT;
		pbuffer = sbuffer + 1;	/* little endian data */
		size1 >>= 1;
		while (size1-- > 0) {
			*buffer++ = *pbuffer;
			pbuffer += 2;
		}
		count -= size;
		user += size;
	}
	return 0;
}

static int snd_sb16_capture_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++;
		}
		if (copy_to_user(user, sbuffer, size))
			return -EFAULT;
		count -= size;
		user += size;
	}
	return 0;
}

static int snd_sb16_playback_ioctl(void *private_data,
				   snd_pcm_subchn_t * subchn,
				   unsigned int cmd,
				   unsigned long *arg)
{
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pcm->playback.private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		subchn1->real_rate = subchn1->rate;
		if (subchn1->real_rate < pchn1->hw.min_rate)
			subchn1->real_rate = pchn1->hw.min_rate;
		if (subchn1->real_rate > pchn1->hw.max_rate)
			subchn1->real_rate = pchn1->hw.max_rate;
		return 0;
	}
	return -ENXIO;
}

static int snd_sb16_capture_ioctl(void *private_data,
				 snd_pcm_subchn_t * subchn,
				 unsigned int cmd,
				 unsigned long *arg)
{
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pcm->capture.private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		subchn1->real_rate = subchn1->rate;
		if (subchn1->real_rate < pchn1->hw.min_rate)
			subchn1->real_rate = pchn1->hw.min_rate;
		if (subchn1->real_rate > pchn1->hw.max_rate)
			subchn1->real_rate = pchn1->hw.max_rate;
		return 0;
	}
	return -ENXIO;
}

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

	spin_lock_irqsave(&codec->reg_lock, flags);
	if (codec->mode16 & (!capture ? SB_MODE16_PLAYBACK16 : SB_MODE16_CAPTURE16))
		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);
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

static void snd_sb16_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;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	unsigned char format;

	format = SB_DSP4_MODE_UNS_MONO;
	if (subchn1->mode & SND_PCM1_MODE_U) {
		format = subchn1->voices > 1 ? SB_DSP4_MODE_UNS_STEREO : SB_DSP4_MODE_UNS_MONO;
	} else {
		format = subchn1->voices > 1 ? SB_DSP4_MODE_SIGN_STEREO : SB_DSP4_MODE_SIGN_MONO;
	}
	snd_sb16_setup_rate(codec, subchn1->real_rate, 0);
#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 (!(subchn1->mode & SND_PCM1_MODE_16)) {
			size <<= 1;
			count <<= 1;
		}
		snd_dma_program(codec->dma16, buffer, size, DMA_MODE_WRITE | DMA_AUTOINIT);
		count >>= 1;
		count--;
		spin_lock_irqsave(&codec->reg_lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
	} else {
		if (subchn1->mode & SND_PCM1_MODE_16) {
			size >>= 1;
			count >>= 1;
		}
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_WRITE | DMA_AUTOINIT);
		count--;
		spin_lock_irqsave(&codec->reg_lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
	}
}

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

#if 0
	snd_printk("playback trigger - %i\n", up);
#endif
	spin_lock_irqsave(&codec->reg_lock, 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("capture trigger!!!! (from playback)");
#endif
			snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_CAPTURE16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON);
		}
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

static void snd_sb16_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;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	unsigned char format;

	format = SB_DSP4_MODE_UNS_MONO;
	if (subchn1->mode & SND_PCM1_MODE_U) {
		format = subchn1->voices > 1 ? SB_DSP4_MODE_UNS_STEREO : SB_DSP4_MODE_UNS_MONO;
	} else {
		format = subchn1->voices > 1 ? SB_DSP4_MODE_SIGN_STEREO : SB_DSP4_MODE_SIGN_MONO;
	}
	snd_sb16_setup_rate(codec, subchn1->real_rate, SND_PCM1_CAPTURE);
#if 0
	snd_printk("capture - buffer = 0x%lx, size = %i (%i), count = %i\n", (long) buffer, size, pchn->size, count);
#endif
	if (codec->mode16 & SB_MODE16_CAPTURE16) {	/* use 16-bit DMA */
		if (!(subchn1->mode & SND_PCM1_MODE_16)) {
			size <<= 1;
			count <<= 1;
		}
		snd_dma_program(codec->dma16, buffer, size, DMA_MODE_READ | DMA_AUTOINIT);
		count >>= 1;
		count--;
		spin_lock_irqsave(&codec->reg_lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
	} else {
		if (subchn1->mode & SND_PCM1_MODE_16) {
			size >>= 1;
			count >>= 1;
		}
		snd_dma_program(codec->dma8, buffer, size, DMA_MODE_READ | DMA_AUTOINIT);
		count--;
		spin_lock_irqsave(&codec->reg_lock, 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);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
	}
}

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

#if 0
	snd_printk("sb16: capture trigger - %i\n", up);
#endif
	spin_lock_irqsave(&codec->reg_lock, flags);
	if (up) {
		codec->mode16 |= SB_MODE16_RATE_LOCK_R;
		snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_CAPTURE16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON);
	} else {
		codec->mode16 &= ~SB_MODE16_RATE_LOCK_R;
		snd_sb16dsp_command(codec, codec->mode16 & SB_MODE16_CAPTURE16 ? 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);
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

void snd_sb16dsp_interrupt(snd_pcm_t * pcm, unsigned short status)
{
	int ok;
	sbdsp_t *codec = (sbdsp_t *) pcm->private_data;
	unsigned long flags;

#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) {
			codec->playback_subchn1->ack(codec->playback_subchn);
			ok++;
		}
		if ((codec->mode16 & (SB_MODE16_CAPTURE16 | SB_MODE16_CAPTURE)) == SB_MODE16_CAPTURE) {
			codec->capture_subchn1->ack(codec->capture_subchn);
			ok++;
		}
		spin_lock_irqsave(&codec->reg_lock, flags);
		if (!ok)
			snd_sb16dsp_command(codec, SB_DSP_DMA8_OFF);
		snd_sb16_ack_8bit(codec);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
	}
	if (status & 0x02) {
		ok = 0;
		if ((codec->mode16 & (SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK)) == (SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK)) {
			codec->playback_subchn1->ack(codec->playback_subchn);
			ok++;
		}
		if ((codec->mode16 & (SB_MODE16_CAPTURE16 | SB_MODE16_CAPTURE)) == (SB_MODE16_CAPTURE16 | SB_MODE16_CAPTURE)) {
			codec->capture_subchn1->ack(codec->capture_subchn);
			ok++;
		}
		spin_lock_irqsave(&codec->reg_lock, flags);
		if (!ok)
			snd_sb16dsp_command(codec, SB_DSP_DMA16_OFF);
		snd_sb16_ack_16bit(codec);
		spin_unlock_irqrestore(&codec->reg_lock, flags);
	}
}

/*

 */

static int snd_sb16_playback_open(void *private_data,
				  snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;
	int err = -EBUSY;

	if (codec->dma16ptr &&
	    (codec->force_mode16 & (SB_MODE16_PLAYBACK | SB_MODE16_AUTO))) {
		if ((err = snd_pcm1_dma_alloc(subchn, codec->dma16ptr, "Sound Blaster 16 DSP")) >= 0) {
			spin_lock_irqsave(&codec->open16_lock, flags);
			codec->mode16 |= SB_MODE16_PLAYBACK16 | SB_MODE16_PLAYBACK;
			pchn1->hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pchn1->hw.flags |= SND_PCM1_HW_16BITONLY;
			pchn1->hw.hw_formats |= SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE;
			pchn1->hw.hw_formats &= ~(SND_PCM_FMT_S8 | SND_PCM_FMT_U8);
			codec->playback_subchn = subchn;
			codec->playback_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
			spin_unlock_irqrestore(&codec->open16_lock, 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(subchn, codec->dma8ptr, "Sound Blaster 16 DSP")) >= 0) {
			spin_lock_irqsave(&codec->open16_lock, flags);
			codec->mode16 &= ~SB_MODE16_PLAYBACK16;
			codec->mode16 |= SB_MODE16_PLAYBACK;
			pchn1->hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pchn1->hw.flags |= SND_PCM1_HW_8BITONLY;
			pchn1->hw.hw_formats |= SND_PCM_FMT_U8 | SND_PCM_FMT_S8;
			pchn1->hw.hw_formats &= ~(SND_PCM_FMT_U16_LE | SND_PCM_FMT_S16_LE);
			codec->playback_subchn = subchn;
			codec->playback_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
			spin_unlock_irqrestore(&codec->open16_lock, flags);
			return 0;
		}
	}
	return err;
}

static int snd_sb16_capture_open(void *private_data,
				 snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;
	int err = -EBUSY;

	if (codec->dma16ptr &&
	    (codec->force_mode16 & (SB_MODE16_CAPTURE | SB_MODE16_AUTO))) {
		if ((err = snd_pcm1_dma_alloc(subchn, codec->dma16ptr, "Sound Blaster 16 DSP")) >= 0) {
			spin_lock_irqsave(&codec->open16_lock, flags);
			codec->mode16 |= SB_MODE16_CAPTURE16 | SB_MODE16_CAPTURE;
			pchn1->hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pchn1->hw.flags |= SND_PCM1_HW_16BITONLY;
			pchn1->hw.hw_formats |= SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE;
			pchn1->hw.hw_formats &= ~(SND_PCM_FMT_S8 | SND_PCM_FMT_U8);
			codec->capture_subchn = subchn;
			codec->capture_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
			spin_unlock_irqrestore(&codec->open16_lock, flags);
			return 0;
		}
	}
	if (codec->dma8ptr &&
	    ((codec->force_mode16 & (SB_MODE16_CAPTURE | SB_MODE16_AUTO)) == SB_MODE16_AUTO ||
	    (codec->force_mode16 & (SB_MODE16_CAPTURE | SB_MODE16_AUTO)) == 0)) {
		if ((err = snd_pcm1_dma_alloc(subchn, codec->dma8ptr, "Sound Blaster 16 DSP")) >= 0) {
			spin_lock_irqsave(&codec->open16_lock, flags);
			codec->mode16 &= ~SB_MODE16_CAPTURE16;
			codec->mode16 |= SB_MODE16_CAPTURE;
			pchn1->hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
			pchn1->hw.flags |= SND_PCM1_HW_8BITONLY;
			pchn1->hw.hw_formats |= SND_PCM_FMT_U8 | SND_PCM_FMT_S8;
			pchn1->hw.hw_formats &= ~(SND_PCM_FMT_U16_LE | SND_PCM_FMT_S16_LE);
			codec->capture_subchn = subchn;
			codec->capture_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
			spin_unlock_irqrestore(&codec->open16_lock, flags);
			return 0;
		}
	}
	return err;
}

static void snd_sb16_playback_close(void *private_data,
				    snd_pcm_subchn_t * subchn)
{
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

	codec->playback_subchn = NULL;
	codec->playback_subchn1 = NULL;
	snd_pcm1_dma_free(subchn);
	codec->mode16 &= ~(SB_MODE16_PLAYBACK | SB_MODE16_PLAYBACK16);
	pchn1->hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
	pchn1->hw.hw_formats |= SND_PCM_FMT_U8 | SND_PCM_FMT_S8 |
				SND_PCM_FMT_U16_LE | SND_PCM_FMT_S16_LE;
}

static void snd_sb16_capture_close(void *private_data,
				   snd_pcm_subchn_t * subchn)
{
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

	codec->capture_subchn = NULL;
	codec->capture_subchn1 = NULL;
	snd_pcm1_dma_free(subchn);
	codec->mode16 &= ~(SB_MODE16_CAPTURE | SB_MODE16_CAPTURE16);
	pchn1->hw.flags &= ~(SND_PCM1_HW_8BITONLY | SND_PCM1_HW_16BITONLY);
	pchn1->hw.hw_formats |= SND_PCM_FMT_U8 | SND_PCM_FMT_S8 |
				SND_PCM_FMT_U16_LE | SND_PCM_FMT_S16_LE;
}

static unsigned int snd_sb16_playback_pointer(void *private_data,
					      snd_pcm_subchn_t * subchn,
					      unsigned int used_size)
{
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	unsigned int result;

	if (codec->mode16 & SB_MODE16_PLAYBACK16) {
		result = used_size - snd_dma_residue(codec->dma16);
		if (!(subchn1->mode & SND_PCM1_MODE_16))
			result >>= 1;
	} else {
		result = used_size - snd_dma_residue(codec->dma8);
		if (subchn1->mode & SND_PCM1_MODE_16)
			result <<= 1;
	}
	return result;
}

static unsigned int snd_sb16_capture_pointer(void *private_data,
					     snd_pcm_subchn_t * subchn,
					     unsigned int used_size)
{
	sbdsp_t *codec = (sbdsp_t *) private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	unsigned int result;

	if (codec->mode16 & SB_MODE16_CAPTURE16) {
		result = used_size - snd_dma_residue(codec->dma16);
		if (!(subchn1->mode & SND_PCM1_MODE_16))
			result >>= 1;
	} else {
		result = used_size - snd_dma_residue(codec->dma8);
		if (subchn1->mode & SND_PCM1_MODE_16)
			result <<= 1;
	}
	return result;
}

static int snd_sb16_playback_dma(void *private_data,
				 snd_pcm_subchn_t * subchn,
			      	 unsigned char *buffer, unsigned int offset,
				 unsigned char *user, unsigned int count)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

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

static int snd_sb16_capture_dma(void *private_data,
				snd_pcm_subchn_t * subchn,
			        unsigned char *buffer, unsigned int offset,
				unsigned char *user, unsigned int count)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

	if (pchn1->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (subchn1->mode & SND_PCM1_MODE_16) {
			if (snd_sb16_capture_8to16(buffer, offset, user, count))
				return -EFAULT;
			return 0;
		}
	}
	if (pchn1->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(subchn1->mode & SND_PCM1_MODE_16)) {
			if (snd_sb16_capture_16to8(buffer, offset, user, count,
				(subchn1->mode & SND_PCM1_MODE_ULAW) ? 1 : 0))
				return -EFAULT;
			return 0;
		}
	}
	if (subchn1->mode & SND_PCM1_MODE_ULAW) {
		if (snd_translate_to_user(snd_dsp_ulaw_loud, user, &buffer[offset], count))
			return -EFAULT;
	} else {
		if (copy_to_user(user, &buffer[offset], count))
			return -EFAULT;
	}
	return 0;
}

static int snd_sb16_playback_dma_move(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      unsigned char *dbuffer,
				      unsigned int dest_offset,
			 	      unsigned char *sbuffer,
			 	      unsigned int src_offset,
				      unsigned int count)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

	if (pchn1->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (subchn1->mode & SND_PCM1_MODE_16) {
			memcpy(&dbuffer[dest_offset >> 1], &sbuffer[src_offset >> 1], count >> 1);
			return 0;
		}
	}
	if (pchn1->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(subchn1->mode & SND_PCM1_MODE_16)) {
			memcpy(&dbuffer[dest_offset << 1], &sbuffer[src_offset << 1], count << 1);
			return 0;
		}
	}
	return snd_pcm1_dma_move(private_data, subchn, dbuffer, dest_offset,
				 sbuffer, src_offset, count);
}

static int snd_sb16_capture_dma_move(void *private_data,
				     snd_pcm_subchn_t * subchn,
				     unsigned char *dbuffer,
				     unsigned int dest_offset,
			 	     unsigned char *sbuffer,
			 	     unsigned int src_offset,
				     unsigned int count)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

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

static int snd_sb16_playback_dma_neutral(void *private_data,
					 snd_pcm_subchn_t * subchn,
			      		 unsigned char *buffer,
			      		 unsigned int offset,
					 unsigned int count,
					 unsigned char neutral_byte)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	snd_pcm1_channel_t *pchn1 = (snd_pcm1_channel_t *)subchn->pchn->private_data;

	if (pchn1->hw.flags & SND_PCM1_HW_8BITONLY) {
		if (subchn1->mode & SND_PCM1_MODE_16) {
			memset(&buffer[offset >> 1], neutral_byte, count >> 1);
			return 0;
		}
	}
	if (pchn1->hw.flags & SND_PCM1_HW_16BITONLY) {
		if (!(subchn1->mode & SND_PCM1_MODE_16)) {
			memset(&buffer[offset << 1], neutral_byte, count << 1);
			return 0;
		}
	}
	return snd_pcm1_playback_dma_neutral(private_data, subchn, buffer, offset,
					     count, neutral_byte);
}

/*

 */

struct snd_stru_pcm1_hardware snd_sb16_playback =
{
	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_capture =
{
	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_capture_open,
	snd_sb16_capture_close,
	snd_sb16_capture_ioctl,
	snd_sb16_capture_prepare,
	snd_sb16_capture_trigger,
	snd_sb16_capture_pointer,
	snd_sb16_capture_dma,
	snd_sb16_capture_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_CAPTURE)
		tmp = "16-bit";
	tmp1 = "inactive";
	if (codec->mode16 & SB_MODE16_CAPTURE) {
		if (codec->mode16 & SB_MODE16_CAPTURE16)
			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_CAPTURE;
	}
}

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_info_entry_t *entry;

	codec = (sbdsp_t *) pcm->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;

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