/*
 *  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(snd_pcm_subchn_t * subchn)
{
	snd_pcm_runtime_t *runtime = subchn->runtime;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, runtime->private_data, );
	int rate, capture;

	rate = runtime->format.rate;
	capture = subchn->channel == SND_PCM_CHANNEL_CAPTURE;
	switch (codec->hardware) {
	case SB_HW_10:
	case SB_HW_20:
		if (capture && rate > 13000)
			rate = 13000;
		if (rate < 4000)
			rate = 4000;
		if (rate > 23000)
			rate = 23000;
		codec->speed8 = (65535 - (256000000 + rate / 2) / rate) >> 8;
		if (!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 */
		}
		runtime->format.rate = 256000000 / (65536 - (codec->speed8 << 8));
		break;
	case SB_HW_201:
		if (capture && rate > 15000)
			rate = 15000;
		if (rate < 4000)
			rate = 4000;
		if (rate > 44100)
			rate = 44100;
		codec->speed8 = (65536 - (256000000 + rate / 2) / rate) >> 8;
		if (!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;
		}
		runtime->format.rate = 256000000 / (65536 - (codec->speed8 << 8));
		break;
	case SB_HW_PRO:
		if (rate < 4000)
			rate = 4000;
		if (capture) {
			if (rate > 15000)
				rate = 15000;
		}
		if (runtime->format.voices > 1) {
			rate = rate > (22050 + 11025) / 2 ? 22050 : 11025;
			codec->speed8 = (65536 - (256000000 + rate) / (rate << 1)) >> 8;
			if (!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 (!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;
			}
		}
		runtime->format.rate = (256000000 / (65536 - (codec->speed8 << 8))) / runtime->format.voices;
		break;
	default:
		snd_printd("unknown hardware for sb8 compute rate!!!\n");
	}
#if 0
	snd_printd(__FUNCTION__ ": rate = %u, pchn->real_rate = %u\n", rate, runtime->format.rate);
#endif
}

static int snd_sb8_playback_ioctl(void *private_data,
				  snd_pcm_subchn_t * subchn,
				  unsigned int cmd,
				  unsigned long *arg)
{
	int result;
	
	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	if (cmd == SND_PCM_IOCTL1_PARAMS)
		snd_sb8_compute_rate(subchn);
	return 0;
}

static int snd_sb8_capture_ioctl(void *private_data,
				snd_pcm_subchn_t * subchn,
				unsigned int cmd,
				unsigned long *arg)
{
	return snd_sb8_playback_ioctl(private_data, subchn, cmd, arg);
}

static int snd_sb8_playback_prepare(void *private_data,
				    snd_pcm_subchn_t * subchn)
{
	unsigned long flags, flags1;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->p_dma_size = snd_pcm_lib_transfer_size(subchn);
	codec->p_frag_size = snd_pcm_lib_transfer_fragment(subchn);
	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, runtime->dma_area->buf, codec->p_dma_size, DMA_MODE_WRITE);
		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 -EINVAL;
		}
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (codec->hardware == SB_HW_PRO) {
			spin_lock_irqsave(&codec->mixer.lock, flags1);
			if (runtime->format.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, runtime->dma_area->buf, codec->p_dma_size, DMA_MODE_WRITE | DMA_AUTOINIT);
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_ON);
		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->p_frag_size - 1) & 0xff);
			snd_sb8dsp_command(codec, (codec->p_frag_size - 1) >> 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");
	}
	return 0;
}

static int snd_sb8_playback_trigger(void *private_data,
				    snd_pcm_subchn_t * subchn,
				    int cmd)
{
	unsigned long flags;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);

	spin_lock_irqsave(&codec->reg_lock, flags);
	switch (codec->hardware) {
	case SB_HW_10:
		if (cmd == SND_PCM_TRIGGER_GO) {
			codec->mode8 = SB_MODE8_PLAYBACK;
			if (snd_sb8dsp_command(codec, SB_DSP_OUTPUT)) {
				snd_sb8dsp_command(codec, (codec->p_frag_size-1) & 0xff);
				snd_sb8dsp_command(codec, (codec->p_frag_size-1) >> 8);
			} else {
				snd_printk("SB 1.0 - unable to start output\n");
			}
		} else if (cmd == SND_PCM_TRIGGER_STOP) {
			snd_sb8dsp_reset(codec);
			codec->mode8 = SB_MODE8_HALT;
		} else {
			spin_unlock_irqrestore(&codec->reg_lock, flags);
			return -EINVAL;
		}
		break;
	case SB_HW_20:
	case SB_HW_201:
	case SB_HW_PRO:
		if (codec->fmt8 == SB_DSP_HI_OUTPUT_AUTO) {
			if (cmd == SND_PCM_TRIGGER_GO) {
				snd_sb8dsp_command(codec, SB_DSP_BLOCK_SIZE);
				snd_sb8dsp_command(codec, (codec->p_frag_size-1) & 0xff);
				snd_sb8dsp_command(codec, (codec->p_frag_size-1) >> 8);
				snd_sb8dsp_command(codec, codec->fmt8);
			} else if (cmd == SND_PCM_TRIGGER_STOP) {
				snd_sb8dsp_reset(codec);
			} else {
				spin_unlock_irqrestore(&codec->reg_lock, flags);
				return -EINVAL;
			}
		} else {
			if (cmd == SND_PCM_TRIGGER_GO) {
				snd_sb8dsp_command(codec, SB_DSP_DMA8_ON);
			} else if (cmd == SND_PCM_TRIGGER_STOP) {
				snd_sb8dsp_command(codec, SB_DSP_DMA8_OFF);
			} else {
				spin_unlock_irqrestore(&codec->reg_lock, flags);
				return -EINVAL;
			}
		}
		codec->mode8 = cmd == SND_PCM_TRIGGER_GO ? SB_MODE8_PLAYBACK : SB_MODE8_HALT;
		break;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static int snd_sb8_capture_prepare(void *private_data,
				   snd_pcm_subchn_t * subchn)
{
	unsigned long flags, flags1;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->c_dma_size = snd_pcm_lib_transfer_size(subchn);
	codec->c_frag_size = snd_pcm_lib_transfer_fragment(subchn);
	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, runtime->dma_area->buf, codec->c_dma_size, DMA_MODE_READ);
		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 -EINVAL;
		}
		snd_sb8dsp_command(codec, SB_DSP_SPEAKER_OFF);
		if (codec->hardware == SB_HW_PRO) {
			spin_lock_irqsave(&codec->mixer.lock, flags1);
			if (runtime->format.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, runtime->format.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, runtime->dma_area->buf, codec->c_dma_size, DMA_MODE_READ | DMA_AUTOINIT);
		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->c_frag_size-1) & 0xff);
			snd_sb8dsp_command(codec, (codec->c_frag_size-1) >> 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");
	}
	return 0;
}

static int snd_sb8_capture_trigger(void *private_data,
				   snd_pcm_subchn_t * subchn,
				   int cmd)
{
	unsigned long flags;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);

	spin_lock_irqsave(&codec->reg_lock, flags);
	switch (codec->hardware) {
	case SB_HW_10:
		if (cmd == SND_PCM_TRIGGER_GO) {
			codec->mode8 = SB_MODE8_CAPTURE;
			if (snd_sb8dsp_command(codec, SB_DSP_INPUT)) {
				snd_sb8dsp_command(codec, (codec->c_frag_size-1) & 0xff);
				snd_sb8dsp_command(codec, (codec->c_frag_size-1) >> 8);
			} else {
				snd_printk("SB 1.0 - unable to start input\n");
			}
		} else if (cmd == SND_PCM_TRIGGER_STOP) {
			snd_sb8dsp_reset(codec);
			codec->mode8 = SB_MODE8_HALT;
		} else {
			spin_unlock_irqrestore(&codec->reg_lock, flags);
			return -EINVAL;
		}
		break;
	case SB_HW_20:
	case SB_HW_201:
	case SB_HW_PRO:
		if (codec->fmt8 == SB_DSP_HI_INPUT_AUTO) {
			if (cmd == SND_PCM_TRIGGER_GO) {
				snd_sb8dsp_command(codec, SB_DSP_BLOCK_SIZE);
				snd_sb8dsp_command(codec, (codec->c_frag_size-1) & 0xff);
				snd_sb8dsp_command(codec, (codec->c_frag_size-1) >> 8);
				snd_sb8dsp_command(codec, codec->fmt8);
			} else if (cmd == SND_PCM_TRIGGER_STOP) {
				snd_sb8dsp_reset(codec);
			} else {
				spin_unlock_irqrestore(&codec->reg_lock, flags);
				return -EINVAL;
			}
		} else {
			if (cmd == SND_PCM_TRIGGER_GO) {
				snd_sb8dsp_command(codec, SB_DSP_DMA8_ON);
			} else if (cmd == SND_PCM_TRIGGER_STOP) {
				snd_sb8dsp_command(codec, SB_DSP_DMA8_OFF);
			} else {
				spin_unlock_irqrestore(&codec->reg_lock, flags);
				return -EINVAL;
			}
		}
		codec->mode8 = cmd == SND_PCM_TRIGGER_GO ? SB_MODE8_CAPTURE : SB_MODE8_HALT;
		break;
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

void snd_sb8dsp_interrupt(snd_pcm_t * pcm)
{
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, pcm->private_data, );
	snd_pcm_subchn_t *subchn;
	snd_pcm_runtime_t *runtime;

	snd_sb8_ack_8bit(codec);
	switch (codec->mode8) {
	case SB_MODE8_PLAYBACK:	/* ok.. playback is active */
		subchn = codec->playback_subchn;
		runtime = subchn->runtime;
		snd_pcm_transfer_done(subchn);
		if ((codec->hardware == SB_HW_10 ||
		     codec->hardware == SB_HW_20) &&
		    *subchn->runtime->status == SND_PCM_STATUS_RUNNING) {
			snd_dma_program(codec->dma8, &runtime->dma_area->buf[runtime->buf_position % codec->p_dma_size], codec->p_dma_size, DMA_MODE_WRITE);
		    	snd_sb8_playback_trigger(runtime->private_data, subchn, SND_PCM_TRIGGER_GO);
		}
		break;
	case SB_MODE8_CAPTURE:
		subchn = codec->capture_subchn;
		runtime = subchn->runtime;
		snd_pcm_transfer_done(subchn);
		if ((codec->hardware == SB_HW_10 ||
		     codec->hardware == SB_HW_20) &&
		    *subchn->runtime->status == SND_PCM_STATUS_RUNNING) {
			snd_dma_program(codec->dma8, &runtime->dma_area->buf[runtime->buf_position % codec->c_dma_size], codec->c_dma_size, DMA_MODE_READ);
		    	snd_sb8_capture_trigger(runtime->private_data, subchn, SND_PCM_TRIGGER_GO);
		}
		break;
	}
}

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

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

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

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

/*

 */

static snd_pcm_hardware_t snd_sb8_playback =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_MMAP_VALID,	/* flags */
	SND_PCM_FMT_U8,		/* formats */
	SND_PCM_RATE_CONTINUOUS | SND_PCM_RATE_8000 |
	SND_PCM_RATE_11025 | SND_PCM_RATE_22050,
	4000,			/* min. rate */
	23000,			/* max. rate */
	1,			/* min. voices */
	1,			/* max. voices */
	64,			/* min. fragment size */
	32768,			/* max. fragment size */
	3,			/* fragment align */
	0,			/* FIFO size (unknown) */
	4,			/* transfer block size */
	snd_sb8_playback_ioctl,
	snd_sb8_playback_prepare,
	snd_sb8_playback_trigger,
	snd_sb8_playback_pointer
};

static snd_pcm_hardware_t snd_sb8_capture =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_MMAP_VALID,	/* flags */
	SND_PCM_FMT_U8,		/* formats */
	SND_PCM_RATE_CONTINUOUS | SND_PCM_RATE_8000 |
	SND_PCM_RATE_11025,
	4000,			/* min. rate */
	13000,			/* max. rate */
	1,			/* min. voices */
	1,			/* max. voices */
	64,			/* min. fragment size */
	32768,			/* max. fragment size */
	3,			/* fragment align */
	0,			/* FIFO size (unknown) */
	4,			/* transfer block size */
	snd_sb8_capture_ioctl,
	snd_sb8_capture_prepare,
	snd_sb8_capture_trigger,
	snd_sb8_capture_pointer
};

/*
 *
 */
 
int snd_sb8_playback_open(void *private_data, snd_pcm_subchn_t *subchn)
{
	unsigned long flags;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	int err;

	spin_lock_irqsave(&codec->open8_lock, flags);
	if (codec->open8) {
		spin_unlock_irqrestore(&codec->open8_lock, flags);
		return -EAGAIN;
	}
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
		codec->playback_subchn = subchn;
	} else {
		codec->capture_subchn = subchn;
	}
	codec->open8 |= SB_OPEN_PCM;
	spin_unlock_irqrestore(&codec->open8_lock, flags);
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma8ptr, "Sound Blaster DSP")) < 0) {
	      __error:
		spin_lock_irqsave(&codec->open8_lock, flags);
		codec->playback_subchn = NULL;
		codec->capture_subchn = NULL;
		codec->open8 &= ~SB_OPEN_PCM;
		spin_unlock_irqrestore(&codec->open8_lock, flags);
		return err;
	}
	runtime->hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(*runtime->hw), GFP_KERNEL);
	if (runtime->hw == NULL) {
		err = -ENOMEM;
		snd_pcm_dma_free(subchn);
		goto __error;
	}
	runtime->hw_free = _snd_kfree;
	snd_pcm_set_mixer(subchn, codec->kmixer->device, codec->mixer.me_playback);
	runtime->private_data = codec;
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
		memcpy(runtime->hw, &snd_sb8_playback, sizeof(*runtime->hw));
	} else {
		memcpy(runtime->hw, &snd_sb8_capture, sizeof(*runtime->hw));
	}
	switch (codec->hardware) {
	case SB_HW_PRO:
		runtime->hw->max_voices = 2;
		/* follow thru */
	case SB_HW_201:
		runtime->hw->max_rate = 44100;
		break;
	}
	return 0;	
}

int snd_sb8_playback_close(void *private_data, snd_pcm_subchn_t *subchn)
{
	unsigned long flags;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);

	codec->playback_subchn = NULL;
	codec->capture_subchn = NULL;
	snd_pcm_dma_free(subchn);
	spin_lock_irqsave(&codec->open8_lock, flags);
	codec->open8 &= ~SB_OPEN_PCM;
	spin_unlock_irqrestore(&codec->open8_lock, flags);
	return 0;
}

int snd_sb8_capture_open(void *private_data, snd_pcm_subchn_t *subchn)
{
	int err;
	sbdsp_t *codec = snd_magic_cast(sbdsp_t, private_data, -ENXIO);
	
	err = snd_sb8_playback_open(private_data, subchn);
	if (err < 0)
		return err;
	if (codec->hardware == SB_HW_201 ||
	    codec->hardware == SB_HW_PRO)
		subchn->runtime->hw->max_rate = 15000;
	snd_pcm_set_mixer(subchn, codec->kmixer->device, codec->mixer.me_capture);
	return 0;	
}

int snd_sb8_capture_close(void *private_data, snd_pcm_subchn_t *subchn)
{
	return snd_sb8_playback_close(private_data, subchn);
}
