/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *                   Creative Labs, Inc.
 *  Routines for control of EMU10K1 chips / PCM routines
 *
 *  BUGS:
 *    --
 *
 *  TODO:
 *    --
 *
 *   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/emu10k1.h"

static void snd_emu10k1_pcm_interrupt(emu10k1_t *emu, emu10k1_voice_t *voice)
{
	emu10k1_pcm_t *epcm;

	if ((epcm = voice->epcm) == NULL)
		return;
	if (epcm->subchn == NULL)
		return;
#if 0
	printk("IRQ: position = 0x%x, fragment = 0x%x, size = 0x%x\n",
			epcm->subchn->runtime->hw->pointer(emu, epcm->subchn),
			snd_pcm_lib_transfer_fragment(epcm->subchn),
			snd_pcm_lib_transfer_size(epcm->subchn));
#endif
	snd_pcm_transfer_done(epcm->subchn);
}

static void snd_emu10k1_pcm_ac97adc_interrupt(emu10k1_t *emu, unsigned int status)
{
#if 0
	if (status & IPR_ADCBUFHALFFULL) {
		if (emu->pcm_capture_subchn->runtime->mode == SND_PCM_MODE_STREAM)
			return;
	}
#endif
	snd_pcm_transfer_done(emu->pcm_capture_subchn);
}

static void snd_emu10k1_pcm_ac97mic_interrupt(emu10k1_t *emu, unsigned int status)
{
#if 0
	if (status & IPR_MICBUFHALFFULL) {
		if (emu->pcm_capture_mic_subchn->runtime->mode == SND_PCM_MODE_STREAM)
			return;
	}
#endif
	snd_pcm_transfer_done(emu->pcm_capture_mic_subchn);
}

static void snd_emu10k1_pcm_efx_interrupt(emu10k1_t *emu, unsigned int status)
{
#if 0
	if (status & IPR_EFXBUFHALFFULL) {
		if (emu->pcm_capture_efx_subchn->runtime->mode == SND_PCM_MODE_STREAM)
			return;
	}
#endif
	snd_pcm_transfer_done(emu->pcm_capture_efx_subchn);
}

static int snd_emu10k1_pcm_voice_alloc(emu10k1_pcm_t * epcm, int voices, int stream)
{
	int err;

	if (epcm->voices[1] != NULL && voices < 2) {
		snd_emu10k1_voice_free(epcm->emu, epcm->voices[1]);
		epcm->voices[1] = NULL;
	}
	if (voices == 1 && epcm->voices[0] != NULL)
		return 0;		/* already allocated */
	if (voices == 2 && epcm->voices[0] != NULL && epcm->voices[1] != NULL)
		return 0;
	if (voices > 1) {
		if (epcm->voices[0] != NULL && epcm->voices[1] == NULL) {
			snd_emu10k1_voice_free(epcm->emu, epcm->voices[0]);
			epcm->voices[0] = NULL;
		}
	}
	err = snd_emu10k1_voice_alloc(epcm->emu, EMU10K1_PCM, voices > 1, &epcm->voices[0]);
	if (err < 0)
		return err;
	epcm->voices[0]->epcm = epcm;
	if (stream)
		epcm->voices[0]->interrupt = snd_emu10k1_pcm_interrupt;
	if (voices > 1) {
		epcm->voices[1] = &epcm->emu->voices[epcm->voices[0]->number + 1];
		epcm->voices[1]->epcm = epcm;
	}
	if (!stream && epcm->extra == NULL) {
		err = snd_emu10k1_voice_alloc(epcm->emu, EMU10K1_PCM, 0, &epcm->extra);
		if (err < 0) {
			// printk("pcm_voice_alloc: failed extra: voices=%d, stream=%d\n", voices, stream);
			snd_emu10k1_voice_free(epcm->emu, epcm->voices[0]);
			epcm->voices[0] = NULL;
			if (epcm->voices[1])
				snd_emu10k1_voice_free(epcm->emu, epcm->voices[1]);
			epcm->voices[1] = NULL;
			return err;
		}
		epcm->extra->epcm = epcm;
		epcm->extra->interrupt = snd_emu10k1_pcm_interrupt;
	}
	return 0;
}

static int capture_sizes[4] = {384, 448, 512, 640};

static unsigned int snd_emu10k1_capture_transfer_size(unsigned int size)
{
	int idx;
	unsigned int val;

	for (idx = 30; idx >= 0; idx--) {
		val = capture_sizes[idx & 3] * (1 << (idx >> 2));
		if (val <= size)
			return val;
	}
	return capture_sizes[0];
}

static unsigned int snd_emu10k1_capture_transfer_reg(unsigned int size)
{
	int idx;
	unsigned int val;

	for (idx = 30; idx >= 0; idx--) {
		val = capture_sizes[idx & 3] * (1 << (idx >> 2));
		if (val <= size)
			return idx + 1;
	}
	return 1;
}

static unsigned int snd_emu10k1_capture_rate(unsigned int rate)
{
	if (rate < (8000 + 11025) / 2)
		return 8000;
	if (rate < (11025 + 16000) / 2)
		return 11025;
	if (rate < (16000 + 22050) / 2)
		return 16000;
	if (rate < (22050 + 24000) / 2)
		return 22050;
	if (rate < (24000 + 32000) / 2)
		return 24000;
	if (rate < (32000 + 44100) / 2)
		return 32000;
	if (rate < (44100 + 48000) / 2)
		return 44100;
	return 48000;
}

static unsigned int snd_emu10k1_capture_rate_reg(unsigned int rate)
{
	switch (rate) {
	case 8000:	return ADCCR_SAMPLERATE_8;
	case 11025:	return ADCCR_SAMPLERATE_11;
	case 16000:	return ADCCR_SAMPLERATE_16;
	case 22050:	return ADCCR_SAMPLERATE_22;
	case 24000:	return ADCCR_SAMPLERATE_24;
	case 32000:	return ADCCR_SAMPLERATE_32;
	case 44100:	return ADCCR_SAMPLERATE_44;
	case 48000:	return ADCCR_SAMPLERATE_48;
	default:
			return ADCCR_SAMPLERATE_8;
	}
}

static int snd_emu10k1_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;
	return 0;
}

static int snd_emu10k1_capture_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_pcm_runtime_t *runtime = subchn->runtime;
	
		runtime->format.rate = snd_emu10k1_capture_rate(runtime->format.rate);
		if (runtime->mode == SND_PCM_MODE_STREAM)
			return 0;
		runtime->buf.block.frag_size = snd_emu10k1_capture_transfer_size(runtime->buf.block.frag_size * 2) / 2;
		runtime->frags = 2;
	}
	return 0;
}

static void snd_emu10k1_pcm_init_voice(emu10k1_t *emu, int master, 
				       int extra, int voice,
				       int stereo, int rate, int w_16,
				       unsigned int start_addr,
				       unsigned int end_addr)
{
	unsigned int pitch, val = 0, start = 0, sample = 0, silent_page;
	int left, right;

	if (!extra) {
		if (stereo) {
			start_addr >>= 1;
			end_addr >>= 1;
		}
	}
	if (w_16) {
		start_addr >>= 1;
		end_addr >>= 1;
	}
	/* volume parameters */
	if (extra) {
		left = right = 0;
	} else {
		left = right = 255;
		if (stereo) {
			if (master)
				right = 0;
			else
				left = 0;
		}
	}
	pitch = snd_emu10k1_rate_to_pitch(rate) >> 8;
	// printk("init voice: master=%i, voice=%i, stereo=%i, w_16=%i\n", master, voice, stereo, w_16);
	// printk("init voice: start_addr=0x%x, end_addr=0x%x, pitch = 0x%x, target = 0x%x\n", start_addr, end_addr, pitch, (int)(IP_TO_CP(pitch) >> 16));
	snd_emu10k1_ptr_write(emu, DCYSUSV, voice, ENV_OFF);
	snd_emu10k1_ptr_write(emu, VTFT, voice, 0xffff);
	snd_emu10k1_ptr_write(emu, CVCF, voice, 0xffff);
	// Stop CA
	// assumption that PT is already 0 so no harm overwritting
	snd_emu10k1_ptr_write(emu, PTRX, voice, (0xff << 8) | right);
	if (master) {
		if (stereo) {
			val = 64;
			snd_emu10k1_ptr_write(emu, CPF, voice, 0x8000);
			snd_emu10k1_ptr_write(emu, CPF, (voice + 1), 0x8000);
		} else {
			val = 32;
			snd_emu10k1_ptr_write(emu, CPF, voice, 0);
		}
		if (w_16) {
			sample = 0;
		} else {
			val *= 2;
			sample = 0x80808080;
		}
		val -= 4;
		if (stereo) {
			snd_emu10k1_ptr_write(emu, CCR, voice, 0x3c << 16);
			snd_emu10k1_ptr_write(emu, CCR, voice + 1, val << 16);
			snd_emu10k1_ptr_write(emu, CDE, voice + 1, sample);
			snd_emu10k1_ptr_write(emu, CDF, voice + 1, sample);
			start = start_addr + val / 2;
			val <<= 25;
			val |= 0x3c << 16;
		} else {
			snd_emu10k1_ptr_write(emu, CCR, voice, 0x1c << 16);
			snd_emu10k1_ptr_write(emu, CDE, voice, sample);
			snd_emu10k1_ptr_write(emu, CDF, voice, sample);
			start = start_addr + val;
			val <<= 25;
			val |= 0x1c << 16;
		}
		start |= CCCA_INTERPROM_0;
	}
	snd_emu10k1_ptr_write(emu, DSL, voice, end_addr);
	snd_emu10k1_ptr_write(emu, PSST, voice, start_addr | (left << 24));
	snd_emu10k1_ptr_write(emu, CCCA, voice, start | (w_16 ? 0 : CCCA_8BITSELECT));
	// Clear filter delay memory
	snd_emu10k1_ptr_write(emu, Z1, voice, 0);
	snd_emu10k1_ptr_write(emu, Z2, voice, 0);
	// invalidate maps
	silent_page = ((unsigned int)virt_to_bus(emu->silent_page) << 1) | MAP_PTI_MASK;
	snd_emu10k1_ptr_write(emu, MAPA, voice, silent_page);
	snd_emu10k1_ptr_write(emu, MAPB, voice, silent_page);
	// fill cache
	if (master)
		snd_emu10k1_ptr_write(emu, CCR, voice, val);

	snd_emu10k1_ptr_write(emu, ATKHLDV, voice, ATKHLDV_HOLDTIME_MASK | ATKHLDV_ATTACKTIME_MASK);
	snd_emu10k1_ptr_write(emu, LFOVAL1, voice, 0x8000);
	snd_emu10k1_ptr_write(emu, ATKHLDM, voice, 0);
	snd_emu10k1_ptr_write(emu, DCYSUSM, voice, DCYSUSM_DECAYTIME_MASK);
	snd_emu10k1_ptr_write(emu, LFOVAL2, voice, 0x8000);
	snd_emu10k1_ptr_write(emu, IP, voice, pitch);
	snd_emu10k1_ptr_write(emu, PEFE, voice, 0x7f);
	snd_emu10k1_ptr_write(emu, FMMOD, voice, 0);
	snd_emu10k1_ptr_write(emu, TREMFRQ, voice, 0);
	snd_emu10k1_ptr_write(emu, FM2FRQ2, voice, 0);
	snd_emu10k1_ptr_write(emu, ENVVAL, voice, 0xbfff);
	snd_emu10k1_ptr_write(emu, ENVVOL, voice, 0xbfff);
	snd_emu10k1_ptr_write(emu, IFATN, voice, IFATN_FILTERCUTOFF_MASK);
}

static int snd_emu10k1_playback_prepare(void *private_data,
				        snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);
	unsigned int start_addr, end_addr;
	int stream, err;

	stream = runtime->mode == SND_PCM_MODE_STREAM;
	if ((err = snd_emu10k1_pcm_voice_alloc(epcm, runtime->format.voices, stream)) < 0)
		return err;
	start_addr = epcm->start_addr;
	if (!stream) {
		end_addr = snd_pcm_lib_transfer_fragment(subchn);
		if (runtime->format.voices == 2)
			end_addr >>= 1;
		end_addr += start_addr;
		snd_emu10k1_pcm_init_voice(emu, 1, 1, epcm->extra->number,
					   0,
					   runtime->format.rate,
					   snd_pcm_format_width(runtime->format.format) == 16,
					   start_addr, end_addr);
	}
	end_addr = epcm->start_addr + snd_pcm_lib_transfer_size(subchn);
	snd_emu10k1_pcm_init_voice(emu, 1, 0, epcm->voices[0]->number,
				   runtime->format.voices == 2,
				   runtime->format.rate,
				   snd_pcm_format_width(runtime->format.format) == 16,
				   start_addr, end_addr);
	if (epcm->voices[1])
		snd_emu10k1_pcm_init_voice(emu, 0, 0, epcm->voices[1]->number,
					   1,
					   runtime->format.rate,
					   snd_pcm_format_width(runtime->format.format) == 16,
					   start_addr, end_addr);
	return 0;
}

static int snd_emu10k1_capture_prepare(void *private_data,
				       snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);

	snd_emu10k1_ptr_write(emu, epcm->capture_bs_reg, 0, 0);
	switch (epcm->type) {
	case CAPTURE_AC97ADC:
		snd_emu10k1_ptr_write(emu, ADCCR, 0, 0);
		break;
	case CAPTURE_EFX:
		snd_emu10k1_ptr_write(emu, FXWC, 0, 0);
		break;
	default:
		break;
	}	
	snd_emu10k1_ptr_write(emu, epcm->capture_ba_reg, 0, (unsigned int)virt_to_bus(runtime->dma_area->buf));
	epcm->capture_bs_val = snd_emu10k1_capture_transfer_reg(epcm->capture_bufsize = snd_pcm_lib_transfer_size(subchn));
	if (epcm->type == CAPTURE_AC97ADC) {
		epcm->capture_cr_val = ADCCR_LCHANENABLE;
		if (runtime->format.voices > 1)
			epcm->capture_cr_val |= ADCCR_RCHANENABLE;
		epcm->capture_cr_val |= snd_emu10k1_capture_rate_reg(runtime->format.rate);
	}
	return 0;
}

static void snd_emu10k1_playback_trigger_voice(emu10k1_t *emu, emu10k1_voice_t *voice, unsigned int pitch, int master, int extra, int stream)
{
	unsigned int val, pitch_target;

	if (voice == NULL)
		return;
	pitch_target = IP_TO_CP(pitch) >> 16;
	snd_emu10k1_ptr_write(emu, CPF_CURRENTPITCH, voice->number, pitch_target);
	if (master)
		snd_emu10k1_ptr_write(emu, CPF_CURRENTPITCH, voice->number, pitch_target);
	snd_emu10k1_ptr_write(emu, VTFT, voice->number, 0xffff);
	snd_emu10k1_ptr_write(emu, CVCF, voice->number, 0xffff);
	snd_emu10k1_voice_clear_loop_stop(emu, voice->number);		
	val = ENV_ON;
	if (!extra) {
		val |= 0x7f7f;
		if (stream && master)
			snd_emu10k1_voice_intr_enable(emu, voice->number);
	} else {
		snd_emu10k1_voice_intr_enable(emu, voice->number);
	}
	snd_emu10k1_ptr_write(emu, DCYSUSV, voice->number, val);
}

static void snd_emu10k1_playback_stop_voice(emu10k1_t *emu, emu10k1_voice_t *voice)
{
	if (voice == NULL)
		return;
	snd_emu10k1_ptr_write(emu, IFATN, voice->number, 0xffff);
	snd_emu10k1_ptr_write(emu, IP, voice->number, 0);
	snd_emu10k1_ptr_write(emu, VTFT, voice->number, 0xffff);
	snd_emu10k1_ptr_write(emu, CPF_CURRENTPITCH, voice->number, 0);
	snd_emu10k1_voice_intr_disable(emu, voice->number);
}

static int snd_emu10k1_playback_trigger(void *private_data,
				        snd_pcm_subchn_t * subchn,
				        int cmd)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);
	unsigned long flags;

	if (cmd == SND_PCM_TRIGGER_SYNC_GO)
		return -EINVAL;
	// printk("trigger - emu10k1 = 0x%x, cmd = %i, pointer = %i, int = 0x%x\n", (int)emu10k1, cmd, runtime->hw->pointer(private_data, subchn), inl(TRID_REG(emu10k1, T4D_MISCINT)));
	spin_lock_irqsave(&emu->reg_lock, flags);
	if (cmd == SND_PCM_TRIGGER_GO) {
		unsigned int pitch = snd_emu10k1_rate_to_pitch(epcm->subchn->runtime->format.rate) >> 8;
		int stream = epcm->subchn->runtime->mode == SND_PCM_MODE_STREAM;
		snd_emu10k1_playback_trigger_voice(emu, epcm->voices[0], pitch, 1, 0, stream);
		snd_emu10k1_playback_trigger_voice(emu, epcm->voices[1], pitch, 0, 0, stream);
		snd_emu10k1_playback_trigger_voice(emu, epcm->extra, pitch, 1, 1, stream);
		epcm->running = 1;
	} else if (cmd == SND_PCM_TRIGGER_STOP) {
		epcm->running = 0;
		snd_emu10k1_playback_stop_voice(emu, epcm->voices[0]);
		snd_emu10k1_playback_stop_voice(emu, epcm->voices[1]);
		snd_emu10k1_playback_stop_voice(emu, epcm->extra);
	}
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	return 0;
}

static int snd_emu10k1_capture_trigger(void *private_data,
				       snd_pcm_subchn_t * subchn,
				       int cmd)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);
	unsigned long flags;

	if (cmd == SND_PCM_TRIGGER_SYNC_GO)
		return -EINVAL;
	// printk("trigger - emu10k1 = 0x%x, cmd = %i, pointer = %i, int = 0x%x\n", (int)emu10k1, cmd, runtime->hw->pointer(private_data, subchn), inl(TRID_REG(emu10k1, T4D_MISCINT)));
	spin_lock_irqsave(&emu->reg_lock, flags);
	if (cmd == SND_PCM_TRIGGER_GO) {
		snd_emu10k1_intr_enable(emu, epcm->capture_inte);
		// printk("adccr = 0x%x, adcbs = 0x%x\n", epcm->adccr, epcm->adcbs);
		switch (epcm->type) {
		case CAPTURE_AC97ADC:
			snd_emu10k1_ptr_write(emu, ADCCR, 0, epcm->capture_cr_val);
			break;
		case CAPTURE_EFX:
			snd_emu10k1_ptr_write(emu, FXWC, 0, epcm->capture_cr_val);
			break;
		default:	
			break;
		}
		snd_emu10k1_ptr_write(emu, epcm->capture_bs_reg, 0, epcm->capture_bs_val);
		epcm->running = 1;
	} else if (cmd == SND_PCM_TRIGGER_STOP) {
		epcm->running = 0;
		snd_emu10k1_intr_disable(emu, epcm->capture_inte);
		snd_emu10k1_ptr_write(emu, epcm->capture_bs_reg, 0, 0);
		switch (epcm->type) {
		case CAPTURE_AC97ADC:
			snd_emu10k1_ptr_write(emu, ADCCR, 0, 0);
			break;
		case CAPTURE_EFX:
			snd_emu10k1_ptr_write(emu, FXWC, 0, 0);
			break;
		default:
			break;
		}
	}
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	return 0;
}

static unsigned int snd_emu10k1_playback_pointer(void *private_data,
						 snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);
	unsigned long flags;
	unsigned int result = 0;

	spin_lock_irqsave(&emu->reg_lock, flags);
	if (epcm->running) {
		result = snd_emu10k1_ptr_read(emu, CCCA, epcm->voices[0]->number) & 0x00ffffff;
		if (runtime->format.voices > 1)
			result <<= 1;
		if (snd_pcm_format_width(runtime->format.format) == 16)
			result <<= 1;
		result -= epcm->start_addr;
	}
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	return result;
}

static unsigned int snd_emu10k1_capture_pointer(void *private_data,
						snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);
	unsigned long flags;
	unsigned int result = 0;

	spin_lock_irqsave(&emu->reg_lock, flags);
	if (epcm->running) {
		result = snd_emu10k1_ptr_read(emu, epcm->capture_idx_reg, 0) & 0x0000ffff;

	}
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	return result;
}

/*
 *  Playback support device description
 */

static snd_pcm_hardware_t snd_emu10k1_playback =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID,		/* flags */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* formats */
	SND_PCM_RATE_CONTINUOUS | SND_PCM_RATE_8000_48000, /* supported RATES */
	4000,			/* min. rate */
	48000,			/* max. rate */
	1,			/* min. voices */
	2,			/* max. voices */
	64,			/* min. fragment size */
	(128*1024),		/* max. fragment size */
	3,			/* fragment align */
	0,			/* FIFO size (unknown) */
	32,			/* transfer block size */
	snd_emu10k1_playback_ioctl,
	snd_emu10k1_playback_prepare,
	snd_emu10k1_playback_trigger,
	snd_emu10k1_playback_pointer
};

/*
 *  Capture support device description
 */

static snd_pcm_hardware_t snd_emu10k1_capture =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID,		/* flags */
	SND_PCM_FMT_S16_LE,	/* formats */
	SND_PCM_RATE_8000_48000, /* supported RATES */
	8000,			/* min. rate */
	48000,			/* max. rate */
	1,			/* min. voices */
	2,			/* max. voices */
	384,			/* min. fragment size */
	(64*1024),		/* max. fragment size */
	63,			/* fragment align */
	0,			/* FIFO size (unknown) */
	32,			/* transfer block size */
	snd_emu10k1_capture_ioctl,
	snd_emu10k1_capture_prepare,
	snd_emu10k1_capture_trigger,
	snd_emu10k1_capture_pointer
};

/*
 *
 */

static void snd_emu10k1_pcm_free_subchn(void *private_data)
{
	emu10k1_pcm_t *epcm = snd_magic_cast(emu10k1_pcm_t, private_data, );
	emu10k1_t *emu;

	if (epcm) {
		emu = epcm->emu;
		if (epcm->extra)
			snd_emu10k1_voice_free(epcm->emu, epcm->extra);
		if (epcm->voices[1])
			snd_emu10k1_voice_free(epcm->emu, epcm->voices[1]);
		if (epcm->voices[0])
			snd_emu10k1_voice_free(epcm->emu, epcm->voices[0]);
		snd_magic_kfree(epcm);
	}
}

static int snd_emu10k1_playback_open(void *private_data,
				     snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	emu10k1_pcm_t *epcm;
	snd_pcm_runtime_t *runtime = subchn->runtime;
	char *buf;
	int err;
	snd_emux_memblk_t *memblk;

	if ((err = snd_pcm_dma_alloc(subchn,
				     emu->dma1ptr,
				     "EMU10K1 (playback)")) < 0)
		return err;
	buf = runtime->dma_area->buf;
	memblk = snd_emu10k1_alloc_pages(emu, buf, runtime->dma_area->size);
	if (memblk == NULL) {
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	epcm = snd_magic_kcalloc(emu10k1_pcm_t, 0, GFP_KERNEL);
	if (epcm == NULL) {
		snd_emu10k1_free_pages(emu, memblk);
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	epcm->emu = emu;
	epcm->type = PLAYBACK_EMUVOICE;
	epcm->subchn = subchn;
	epcm->memblk = memblk;
	epcm->start_addr = memblk->offset;
	// emu10k1->pcm_mixer[subchn->number].number = emu->number;
	// snd_emu10k1_pcm_mixer_build(emu, subchn->number);
	/* FIXME: */
	snd_pcm_set_mixer(subchn, emu->mixer->device, emu->ac97->me_playback);
	runtime->private_data = epcm;
	runtime->private_free = snd_emu10k1_pcm_free_subchn;
	runtime->hw = &snd_emu10k1_playback;
	return 0;
}

static int snd_emu10k1_playback_close(void *private_data,
				      snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;
	emu10k1_pcm_t *epcm;
	// snd_emu10k1_pcm_mixer_free(emu, subchn->number);
	if (runtime && runtime->private_data) {
		epcm = snd_magic_cast(emu10k1_pcm_t, runtime->private_data, -ENXIO);
		if (epcm->memblk)
			snd_emu10k1_free_pages(emu, epcm->memblk);
	}
	snd_pcm_dma_free(subchn);
	return 0;
}

static int snd_emu10k1_capture_open(void *private_data,
				    snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	emu10k1_pcm_t *epcm;
	snd_pcm_runtime_t *runtime = subchn->runtime;
	int err;

	if ((err = snd_pcm_dma_alloc(subchn,
				     emu->dma2ptr,
				     "EMU10K1 (capture)")) < 0)
		return err;
	epcm = snd_magic_kcalloc(emu10k1_pcm_t, 0, GFP_KERNEL);
	if (epcm == NULL) {
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	epcm->emu = emu;
	epcm->type = CAPTURE_AC97ADC;
	epcm->subchn = subchn;
	epcm->capture_inte = INTE_ADCBUFENABLE;
	epcm->capture_ba_reg = ADCBA;
	epcm->capture_bs_reg = ADCBS;
	epcm->capture_idx_reg = ADCIDX;
	runtime->private_data = epcm;
	runtime->private_free = snd_emu10k1_pcm_free_subchn;
	runtime->hw = &snd_emu10k1_capture;
	/* FIXME: */
	snd_pcm_set_mixer(subchn, emu->mixer->device, emu->ac97->me_capture);
	emu->capture_interrupt = snd_emu10k1_pcm_ac97adc_interrupt;
	emu->pcm_capture_subchn = subchn;
	return 0;
}

static int snd_emu10k1_capture_close(void *private_data,
				     snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);

	emu->capture_interrupt = NULL;
	emu->pcm_capture_subchn = NULL;
	snd_pcm_dma_free(subchn);
	return 0;
}

static int snd_emu10k1_capture_mic_open(void *private_data,
					snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	emu10k1_pcm_t *epcm;
	snd_pcm_runtime_t *runtime = subchn->runtime;
	int err;

	if ((err = snd_pcm_dma_alloc(subchn,
				     emu->dma2ptr,
				     "EMU10K1 MIC (capture)")) < 0)
		return err;
	epcm = snd_magic_kcalloc(emu10k1_pcm_t, 0, GFP_KERNEL);
	if (epcm == NULL) {
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	epcm->emu = emu;
	epcm->type = CAPTURE_AC97MIC;
	epcm->subchn = subchn;
	epcm->capture_inte = INTE_MICBUFENABLE;
	epcm->capture_ba_reg = MICBA;
	epcm->capture_bs_reg = MICBS;
	epcm->capture_idx_reg = MICIDX;
	subchn->runtime->private_data = epcm;
	subchn->runtime->private_free = snd_emu10k1_pcm_free_subchn;
	runtime->hw = snd_kcalloc(sizeof(snd_emu10k1_capture), GFP_KERNEL);
	if (runtime->hw == NULL) {
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	runtime->hw_free = _snd_kfree;
	memcpy(runtime->hw, &snd_emu10k1_capture, sizeof(snd_emu10k1_capture));
	runtime->hw->rates = SND_PCM_RATE_8000;
	runtime->hw->min_rate = runtime->hw->max_rate = 8000;
	runtime->hw->max_voices = 1;
	/* FIXME: */
	snd_pcm_set_mixer(subchn, emu->mixer->device, emu->ac97->me_capture);
	emu->capture_mic_interrupt = snd_emu10k1_pcm_ac97mic_interrupt;
	emu->pcm_capture_mic_subchn = subchn;
	return 0;
}

static int snd_emu10k1_capture_mic_close(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);

	emu->capture_interrupt = NULL;
	emu->pcm_capture_mic_subchn = NULL;
	snd_pcm_dma_free(subchn);
	return 0;
}

static int snd_emu10k1_capture_efx_open(void *private_data,
					snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);
	emu10k1_pcm_t *epcm;
	snd_pcm_runtime_t *runtime = subchn->runtime;
	unsigned long flags;
	int err, idx;

	if ((err = snd_pcm_dma_alloc(subchn,
				     emu->dma2ptr,
				     "EMU10K1 EFX (capture)")) < 0)
		return err;
	epcm = snd_magic_kcalloc(emu10k1_pcm_t, 0, GFP_KERNEL);
	if (epcm == NULL) {
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	epcm->emu = emu;
	epcm->type = CAPTURE_EFX;
	epcm->subchn = subchn;
	epcm->capture_inte = INTE_EFXBUFENABLE;
	epcm->capture_ba_reg = FXBA;
	epcm->capture_bs_reg = FXBS;
	epcm->capture_idx_reg = FXIDX;
	subchn->runtime->private_data = epcm;
	subchn->runtime->private_free = snd_emu10k1_pcm_free_subchn;
	runtime->hw = snd_kcalloc(sizeof(snd_emu10k1_capture), GFP_KERNEL);
	if (runtime->hw == NULL) {
		snd_pcm_dma_free(subchn);
		return -ENOMEM;
	}
	runtime->hw_free = _snd_kfree;
	memcpy(runtime->hw, &snd_emu10k1_capture, sizeof(snd_emu10k1_capture));
	runtime->hw->rates = SND_PCM_RATE_48000;
	runtime->hw->min_rate = runtime->hw->max_rate = 48000;
	/* FIXME: */
	snd_pcm_set_mixer(subchn, emu->mixer->device, emu->ac97->me_capture);
	spin_lock_irqsave(&emu->reg_lock, flags);
	runtime->hw->min_voices = runtime->hw->max_voices = 0;
	for (idx = 0; idx < 32; idx++) {
		if (emu->efx_voices_mask & (1 << idx)) {
			runtime->hw->min_voices++;
			runtime->hw->max_voices++;
		}
	}
	epcm->capture_cr_val = emu->efx_voices_mask;
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	emu->capture_efx_interrupt = snd_emu10k1_pcm_efx_interrupt;
	emu->pcm_capture_efx_subchn = subchn;
	return 0;
}

static int snd_emu10k1_capture_efx_close(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, -ENXIO);

	emu->capture_interrupt = NULL;
	emu->pcm_capture_efx_subchn = NULL;
	snd_pcm_dma_free(subchn);
	return 0;
}

static void snd_emu10k1_pcm_free(void *private_data)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, );
	emu->pcm = NULL;
}

int snd_emu10k1_pcm(emu10k1_t * emu, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;

	if ((err = snd_pcm_new(emu->card, "emu10k1", device, 32, 1, &pcm)) < 0)
		return err;

	pcm->private_data = emu;
	pcm->private_free = snd_emu10k1_pcm_free;

	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].private_data = emu;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].open = snd_emu10k1_playback_open;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].close = snd_emu10k1_playback_close;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = emu;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_emu10k1_capture_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_emu10k1_capture_close;

	pcm->info_flags = SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_CAPTURE |
			  SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "EMU10K1");
	*rpcm = emu->pcm = pcm;
	return 0;
}

static void snd_emu10k1_pcm_mic_free(void *private_data)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, );
	emu->pcm_mic = NULL;
}

int snd_emu10k1_pcm_mic(emu10k1_t * emu, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;

	if ((err = snd_pcm_new(emu->card, "emu10k1 mic", device, 0, 1, &pcm)) < 0)
		return err;

	pcm->private_data = emu;
	pcm->private_free = snd_emu10k1_pcm_mic_free;

	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = emu;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_emu10k1_capture_mic_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_emu10k1_capture_mic_close;

	pcm->info_flags = SND_PCM_INFO_CAPTURE;
	strcpy(pcm->name, "EMU10K1 MIC");
	*rpcm = emu->pcm_mic = pcm;
	return 0;
}

static int snd_emu10k1_pcm_efx_get_voices_mask(snd_pcm_t * pcm,
					       snd_kswitch_t * kswitch,
					       snd_switch_t * uswitch)
{
	unsigned long flags;
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, kswitch->private_data, -ENXIO);

	uswitch->type = SND_SW_TYPE_DWORD;
	uswitch->subtype = SND_SW_SUBTYPE_HEXA;
	uswitch->low = 0x00000001;
	uswitch->high = 0xffffffff;
	spin_lock_irqsave(&emu->reg_lock, flags);
	uswitch->value.data32[0] = emu->efx_voices_mask;
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	return 0;
}

static int snd_emu10k1_pcm_efx_set_voices_mask(snd_pcm_t * pcm,
					       snd_kswitch_t * kswitch,
					       snd_switch_t * uswitch)
{
	unsigned long flags;
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, kswitch->private_data, -ENXIO);
	int bits = 0, idx;

	if (uswitch->type != SND_SW_TYPE_DWORD)
		return -EINVAL;
	for (idx = 0; idx < 32; idx++) {
		if (uswitch->value.data32[0] & (1 << idx))
			bits++;
	}
	if (bits != 1 && bits != 2 && bits != 4 && bits != 8 && bits != 16 && bits != 32)
		return -EINVAL;
	spin_lock_irqsave(&emu->reg_lock, flags);
	emu->efx_voices_mask = uswitch->value.data32[0];
	spin_unlock_irqrestore(&emu->reg_lock, flags);
	return 0;
}

static snd_kswitch_t snd_emu10k1_pcm_efx_voices_mask_switch = {
	"EFX voices mask",
	(snd_get_switch_t *)snd_emu10k1_pcm_efx_get_voices_mask,
	(snd_set_switch_t *)snd_emu10k1_pcm_efx_set_voices_mask,
	0,
	NULL,
	NULL
};

static void snd_emu10k1_pcm_efx_free(void *private_data)
{
	emu10k1_t *emu = snd_magic_cast(emu10k1_t, private_data, );
	emu->pcm_efx = NULL;
}

int snd_emu10k1_pcm_efx(emu10k1_t * emu, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;

	if ((err = snd_pcm_new(emu->card, "emu10k1 efx", device, 0, 1, &pcm)) < 0)
		return err;

	pcm->private_data = emu;
	pcm->private_free = snd_emu10k1_pcm_efx_free;

	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = emu;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_emu10k1_capture_efx_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_emu10k1_capture_efx_close;

	pcm->info_flags = SND_PCM_INFO_CAPTURE;
	strcpy(pcm->name, "EMU10K1 EFX");
	*rpcm = emu->pcm_efx = pcm;

	emu->efx_voices_mask = FXWC_DEFAULTROUTE_C | FXWC_DEFAULTROUTE_A;
	snd_pcm_switch_new(&pcm->chn[SND_PCM_CHANNEL_CAPTURE], &snd_emu10k1_pcm_efx_voices_mask_switch, emu);
	return 0;
}
