/*
 * linux/kernel/chr_drv/sound/pcsndriv.c
 *
 * /dev/pcsp implementation
 *
 * Copyright (C) 1993  Michael Beck 
 */

#include <linux/pcsp.h>

#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>

#include <asm/io.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <asm/irq.h>

#include "tables.h"
#include "local.h"

/* the timer stuff */
#define TIMER_IRQ 0
#define LATCH ((1193180 + HZ/2)/HZ)

#define NEW_CONST	65
#define SAMPLE_RATE	(1193180 / NEW_CONST) 

#define TIME_TO_CALL	((LATCH + NEW_CONST/2) / NEW_CONST)

#ifndef ABLK_SIZE
#define ABLK_SIZE	8192
#endif

#ifndef DEFAULT_RATE
#define DEFAULT_RATE	8000
#endif

static pcsp_ports[] = { 0x3bc, 0x378, 0x278 };

#define LP_NO 		3
#define LP_B(port)	pcsp_ports[port]	/* IO address */
#define LP_S(port)	inb_p(LP_B(port) + 1)	/* status */

/* general pcsp data */

static struct pcsp_status {
	unsigned	jiffies;
	unsigned char	*buf[2];	/* double buffering */
	unsigned char   *buffer;
	unsigned char	*end;
	unsigned	in[2];		/* buffers fill */
	unsigned	xfer;
	unsigned	index;
	unsigned	volume;		/* volume for pc-speaker */
	unsigned	srate;		/* sample rate */
	unsigned	si;		/* precalculated sample const */
	unsigned	timerC;		/* hardware timer ticks for srate */
	unsigned	act_dev;	/* which device is playing */
	unsigned	port;		/* on which lp-port */
	unsigned	portS;		/* for Stereo */
	unsigned	time_to_call;	/* if we have real samplerate */
	char		actual;		/* actual buffer */
	unsigned char	e;
	char		timer_on;
	char		mode;		/* Mono / Stereo */
} pcsp;

/* Volume-tables */

static unsigned char vl_tab[256];
#ifdef PCSP_MIXER
static unsigned char left_vol[256], right_vol[256];
#endif

/* need this to be global */
char pcsp_active, pcsp_speaker, pcsp_to_ulaw;

static struct wait_queue *pcsp_sleep = NULL;

extern void do_timer(struct pt_regs * regs);


/* translate n bytes in the buff, using table */
inline void translate_bytes(const void *table, void *buff, unsigned long n)
{
  __asm__("cld\n"
          "1:\tlodsb\n\t"
          "xlatb\n\t"
          "stosb\n\t"
          "loop 1b\n\t"
          :
          :"b" ((long)table), "c" (n), "D" ((long)buff), "S" ((long)buff)
          :"bx","cx","di","si","ax");
}

/* test if a Stereo-on-One is on lp(port) */
inline static int stereo1_detect(unsigned port)
{
	outb(0, LP_B(port));
	if (LP_S(port) & 0x80) {
		outb(0xFF, LP_B(port));
		return (LP_S(port) & 0x80) ? 0 : 1;
	}
	return 0;
}

/* search for Stereo-on-One, return it's port if found */
static int stereo1_init(void)
{
	register int i;

	for (i = 0; i < LP_NO; ++i) 
		if (stereo1_detect(i)) {
			pcsp.port    = LP_B(i);
			pcsp.act_dev = SNDCARD_STO1;
			return i;
		}
	return (-ENODEV);
}

/* the timer-int for playing thru PC-Speaker */
void pcsp_do_timer(struct pt_regs * regs)
{
	if (pcsp.index < pcsp.in[pcsp.actual]) {
		outb(pcsp.e, 0x61);
		__asm__("xorb $1,%0\n\t"
			"outb %0,$97"
			: : "a" ((char)pcsp.e) );
		__asm__("xlatb\n\t"
			"outb %0,$66"
			: : "a" ((char)pcsp.buffer[pcsp.index]),
			    "b" ((long)vl_tab) );
		pcsp.xfer += pcsp.si;
		pcsp.index = pcsp.xfer >> 16;
	}
	if (pcsp.index >= pcsp.in[pcsp.actual]) {
		pcsp.xfer = pcsp.index = 0;
		pcsp.in[pcsp.actual] = 0;
		pcsp.actual ^= 1;
		pcsp.buffer = pcsp.buf[pcsp.actual];
                if (pcsp_sleep)
			wake_up_interruptible(&pcsp_sleep);
	}

	if (! --pcsp.jiffies) {
		pcsp.jiffies = TIME_TO_CALL;
		do_timer(regs);
	}
}

/* timer-int for playing thru STO1 */
void pcsp_do_sto1_timer(struct pt_regs * regs)
{
	if (pcsp.buffer < pcsp.end) {
		if (pcsp.mode) {
			register short w = ((short)pcsp.port);
#ifdef PCSP_MIXER
			__asm__("xlatb\n\t"
				"outb %0,%1\n\t"
#else
			__asm__("outb %0,%1\n\t"
#endif
				"addl $2,%1\n\t"
				"movb $1,%0\n\t"
				"outb %0,%1\n\t"
				"movb $0,%0\n\t"
				"outb %0,%1"
				:
#ifdef PCSP_MIXER
				: "a" ((char) *pcsp.buffer++), "d" ((short)w), "b" ((long)right_vol)
				: "ax", "dx", "bx"
#else
				: "a" ((char) *pcsp.buffer++), "d" ((short)w)
				: "ax", "dx"
#endif
				);
#ifdef PCSP_MIXER
 			__asm__("xlatb\n\t"
				"outb %0,%1\n\t"
#else
			__asm__("outb %0,%1\n\t"
#endif
				"addl $2,%1\n\t"
				"movb $2,%0\n\t"
				"outb %0,%1\n\t"
				"movb $0,%0\n\t"
				"outb %0,%1"
				:
#ifdef PCSP_MIXER
				: "a" ((char) *pcsp.buffer++), "d" ((short)w), "b" ((long)left_vol)
				: "ax", "dx", "bx"
#else
				: "a" ((char) *pcsp.buffer++), "d" ((short)w)
				: "ax", "dx"
#endif
				);
		}
		else	/* Mono */
#ifdef PCSP_MIXER
			outb(left_vol[*pcsp.buffer++], (short)pcsp.port);
#else
			outb(*pcsp.buffer++, (short)pcsp.port);
#endif
			
	}
	if (pcsp.buffer >= pcsp.end) {
		pcsp.in[pcsp.actual] = 0;
		pcsp.actual ^= 1;
 		pcsp.buffer  = pcsp.buf[pcsp.actual];
		pcsp.end     = pcsp.buffer + pcsp.in[pcsp.actual];
                if (pcsp_sleep)
			wake_up_interruptible(&pcsp_sleep);
	}

	if (! --pcsp.jiffies) {
		pcsp.jiffies = pcsp.time_to_call;
		do_timer(regs);
	}
}

/* timer-int for playing thru DACs */
void pcsp_do_dac_timer(struct pt_regs * regs)
{
	if (pcsp.buffer < pcsp.end) {
		if (pcsp.act_dev == SNDCARD_DACS)  {
			if (pcsp.mode) {
#ifdef PCSP_MIXER
				outb(left_vol[*pcsp.buffer++], pcsp.port);
				outb(right_vol[*pcsp.buffer++], pcsp.portS);
#else
				outb(*pcsp.buffer++, pcsp.port);
				outb(*pcsp.buffer++, pcsp.portS);
#endif
			}
			else {
#ifdef PCSP_MIXER
				outb(left_vol[*pcsp.buffer], pcsp.port);
				outb(left_vol[*pcsp.buffer++], pcsp.portS);
#else
				outb(*pcsp.buffer, pcsp.port);
				outb(*pcsp.buffer++, pcsp.portS);
#endif
			}
		}
		else	/* Simple DAC */
#ifdef PCSP_MIXER
			outb(left_vol[*pcsp.buffer++], pcsp.port);
#else
			outb(*pcsp.buffer++, pcsp.port);
#endif
	}
	if (pcsp.buffer >= pcsp.end) {
		pcsp.in[pcsp.actual] = 0;
		pcsp.actual ^= 1;
 		pcsp.buffer  = pcsp.buf[pcsp.actual];
		pcsp.end     = pcsp.buffer + pcsp.in[pcsp.actual];
                if (pcsp_sleep)
			wake_up_interruptible(&pcsp_sleep);
	}

	if (! --pcsp.jiffies) {
		pcsp.jiffies = pcsp.time_to_call;
		do_timer(regs);
	}
}

static void pcsp_start_timer(void)
{
	/* use the first buffer */
	pcsp.actual  = 0;
	pcsp.xfer    = pcsp.index = 0;
        pcsp.buffer  = pcsp.buf[pcsp.actual];
	
	if (pcsp.act_dev == SNDCARD_PCSP) {
		pcsp_speaker = 1;
		pcsp.jiffies = TIME_TO_CALL;

	        pcsp.e = inb(0x61) | 0x03;
		outb_p(0x92, 0x43);		/* binary, mode 1, LSB only, ch 2 */

		/* get the timer */
		outb_p(0x34,0x43);              /* binary, mode 2, LSB/MSB, ch 0 */
		outb_p(NEW_CONST & 0xFF, 0x40);	/* LSB */
		outb(NEW_CONST >> 8 , 0x40);	/* MSB */
		free_irq(TIMER_IRQ);
		request_irq(TIMER_IRQ,(void (*)(int)) pcsp_do_timer);
	}
	else {
		pcsp.end     = pcsp.buffer + pcsp.in[pcsp.actual];
		pcsp.jiffies = pcsp.time_to_call;
		if (pcsp.act_dev == SNDCARD_STO1)
			outb(3,pcsp.port + 2);

		/* get the timer */
		outb_p(0x34,0x43);              /* binary, mode 2, LSB/MSB, ch 0 */
		outb_p(pcsp.timerC & 0xFF, 0x40); 
                outb(pcsp.timerC >> 8 , 0x40);  
                free_irq(TIMER_IRQ);
		if (pcsp.act_dev == SNDCARD_STO1)
                	request_irq(TIMER_IRQ,(void (*)(int)) pcsp_do_sto1_timer);
		else	/* DACs */
                	request_irq(TIMER_IRQ,(void (*)(int)) pcsp_do_dac_timer);
 	}
		pcsp.timer_on = 1;
}

static void pcsp_stop_timer(void)
{
	/* restore the timer */
        outb_p(0x34,0x43);              /* binary, mode 2, LSB/MSB, ch 0 */
        outb_p(LATCH & 0xff , 0x40);    /* LSB */
        outb(LATCH >> 8 , 0x40);        /* MSB */
	free_irq(TIMER_IRQ);
	request_irq(TIMER_IRQ,(void (*)(int)) do_timer);

	/* reset the buffer */
	pcsp.in[0]    = pcsp.in[1] = 0;
	pcsp.xfer     = pcsp.index = 0; 
	pcsp.actual   = 0;
	pcsp.buffer   = pcsp.end = pcsp.buf[pcsp.actual];

	pcsp_speaker  = 0;
	pcsp.timer_on = 0;
}

/* calculate a translation-table for PC-Speaker */
static void pcsp_calc_vol(int volume)
{
	int i, j;

	for (i = 0; i < 256; ++i) {
		j = ((i - 128) * volume) >> 8;
		if (j < -128)
			j = -128;
		if (j > 127)
			j = 127;
		vl_tab[i] = sp_tab[j + 128];
	}
}

#ifdef PCSP_MIXER

/* calculate linear translation table for DACs */
static void pcsp_calc_voltab(int volume, unsigned char *tab)
{
	int i, j;

	for (i = 0; i < 256; ++i) {
		j = ((i - 128) * volume) >> 8;
		*tab++ = j + 128;
	}
}

/* this is called if /dev/pxmixer change Mastervolume */
void pcsp_set_volume(unsigned short v)
{
	unsigned left  = ((unsigned)v & 0x7F) * 256 / 100;
	unsigned right = ((unsigned)((v >> 8) & 0x7F)) * 256 / 100;
	pcsp_calc_voltab(left, left_vol);
	pcsp_calc_voltab(right, right_vol);
        pcsp.volume = (right + left) >> 1;
	pcsp_calc_vol(pcsp.volume);
}

unsigned pcsp_get_mode(void)
{
	return pcsp.mode;
}
#endif

static void pcsp_sync(void)
{
	while (!(current->signal & ~current->blocked) &&
               (pcsp.in[1] || pcsp.in[2]) ) {
		/* Wait until a complete block are ready */
                interruptible_sleep_on(&pcsp_sleep);
        }
}

static void pcsp_release(struct inode * inode, struct file * file)
{
	pcsp_sync();
	pcsp_stop_timer(); 
	outb_p(0xb6,0x43);		/* binary, mode 2, LSB/MSB, ch 2 */

	pcsp_active   = 0;
}

static int pcsp_open(struct inode * inode, struct file * file)
{
	if (pcsp_active)
		return -EBUSY;

	pcsp.buffer   = pcsp.end   = pcsp.buf[0];
	pcsp.in[0]    = pcsp.in[1] = 0;
	pcsp.timer_on = 0;
	pcsp_active   = 1;
	return 0;
}

#ifdef SOUND_VERSION
/* the new version 2 IOCTL's */
static int pcsp_ioctl(struct inode * inode, struct file * file,
			unsigned int cmd, unsigned long arg)
{
	unsigned long ret;
	unsigned long *ptr = (unsigned long *)arg;
	int i, error;

	switch (cmd) {
		case SNDCTL_DSP_SPEED:	/* simulate SB 1.0 */
			error = verify_area(VERIFY_READ, ptr, 4);
			if (error)
				return (error);
			arg = get_fs_long(ptr);
			if (arg < 4000 || arg > 22222)
				return (-EINVAL);
			else 		/* reset default speed */
				pcsp.srate = arg;
			pcsp.si = (pcsp.srate << 16) / SAMPLE_RATE;
			pcsp.timerC = (1193180 + pcsp.srate / 2) / pcsp.srate;
			pcsp.time_to_call = (LATCH + pcsp.timerC / 2) / pcsp.timerC;
			return 0;
		case SNDCTL_DSP_STEREO:	/* it's now possible */
			error = verify_area(VERIFY_READ, ptr, 4);
			if (error)
				return (error);
			arg = get_fs_long(ptr);
			if (pcsp.act_dev == SNDCARD_STO1 || pcsp.act_dev == SNDCARD_DACS) 
				pcsp.mode = (arg) ? 1 : 0;
			else {
				pcsp.mode = 0;
				if (arg)
					return (-EINVAL);
			}
			return (0);
		case SNDCTL_DSP_GETBLKSIZE:
			error = verify_area(VERIFY_WRITE, ptr, 4);
			if (error)
				return (error);
			put_fs_long(ABLK_SIZE, ptr);
			return (0);
		case SNDCTL_DSP_SYNC:	/* syncinc, so speed changes work correct */
			pcsp_sync();
			return (0);
		case SNDCTL_DSP_RESET:	/* stops immediatly output */
			pcsp_stop_timer();
			pcsp.srate  = DEFAULT_RATE;
			pcsp.si     = (pcsp.srate << 16) / SAMPLE_RATE;
                        pcsp.timerC = (1193180 + pcsp.srate / 2) / pcsp.srate;
                        pcsp.time_to_call = (LATCH + pcsp.timerC / 2) / pcsp.timerC;
			return (0);
		case SNDCTL_DSP_SAMPLESIZE:
			error = verify_area(VERIFY_READ, ptr, 4);
			if (error)
				return (error);
			arg = get_fs_long(ptr);
			if (arg != 8)
				return (-EINVAL);
			return (0);
		case PCSP_SET_DEV:
                        error = verify_area(VERIFY_READ, ptr, 4);
                        if (error)
                                return (error);
                        arg = get_fs_long(ptr);
			switch(arg) {
				case SNDCARD_STO1: 
					if (stereo1_init() < 0)
						return (-ENODEV);
					break;
				case SNDCARD_PCSP: 
				case SNDCARD_DACM: 
				case SNDCARD_DACS:
					pcsp.act_dev = arg; break;
				default:
					return (-ENODEV);
			}
			return (0);
		case PCSP_GET_DEV:
                        error = verify_area(VERIFY_WRITE, ptr, 4);
                        if (error)
                                return (error);
                        put_fs_long((unsigned long)pcsp.act_dev, ptr);
                        return (0);
		case PCSP_SET_PORTS:
                        error = verify_area(VERIFY_READ, ptr, 4);
                        if (error)
                                return (error);
                        arg = get_fs_long(ptr);
			if ((arg & 0xFF) < LP_NO && (arg >> 8) < LP_NO) {
				if (pcsp.act_dev == SNDCARD_STO1) {
					if (stereo1_detect(arg & 0xFF)) {
						pcsp.port = LP_B(arg & 0xFF);
						return (0);
					}
				}
				else {
					pcsp.port  = LP_B(arg & 0xFF);
					pcsp.portS = LP_B(arg >> 8);
					return (0);
				}
			}
			return (-EINVAL);
		case PCSP_GET_PORTS: 
                        error = verify_area(VERIFY_WRITE, ptr, 4);
                        if (error)
                                return (error);
			ret = 0;
			for (i = 0; i < LP_NO; ++i)
				if (LP_B(i) == pcsp.port)
					ret = i;
			for (i = 0; i < LP_NO; ++i)
				if (LP_B(i) == pcsp.portS)
					ret |= i << 8;
                        put_fs_long(ret, ptr);
			return (0);
                case PCSP_GET_VOL:
                        error = verify_area(VERIFY_WRITE, ptr, 4);
                        if (error)
                                return (error);
                        put_fs_long((unsigned long)pcsp.volume, ptr);
                        return (0);
                case PCSP_SET_VOL:
                        error = verify_area(VERIFY_READ, ptr, 4);
                        if (error)
                                return (error);
                        pcsp.volume = get_fs_long(ptr);
			pcsp_calc_vol(pcsp.volume);
			return (0); 
		default	:
			return -EINVAL;
	}
}

#else
/* the old version 1 IOCTL's */
static int pcsp_ioctl(struct inode * inode, struct file * file,
			unsigned int cmd, unsigned long arg)
{
	unsigned long ret;
	int i;

	switch (cmd) {
		case SNDCTL_DSP_SPEED:	/* simulate SB 1.0 */
			if (arg >= 4000 && arg <= 22222)
				pcsp.srate = arg;
			else 		/* reset default speed */
				pcsp.srate = DEFAULT_RATE;
			pcsp.si = (pcsp.srate << 16) / SAMPLE_RATE;
			pcsp.timerC = (1193180 + pcsp.srate / 2) / pcsp.srate;
			pcsp.time_to_call = (LATCH + pcsp.timerC / 2) / pcsp.timerC;
			return (pcsp.srate);
		case SNDCTL_DSP_STEREO:	/* it's now possible */
			if (pcsp.act_dev == SNDCARD_STO1 || pcsp.act_dev == SNDCARD_DACS) 
				pcsp.mode = (arg) ? 1 : 0;
			else 
				pcsp.mode = 0;
			return pcsp.mode;
		case SNDCTL_DSP_GETBLKSIZE:
			return (ABLK_SIZE);
		case SNDCTL_DSP_SYNC:	/* syncinc, so speed changes work correct */
			pcsp_sync();
			return (0);
		case SNDCTL_DSP_RESET:	/* stops immediatly output */
			pcsp_stop_timer();
			pcsp.srate  = DEFAULT_RATE;
			pcsp.si     = (pcsp.srate << 16) / SAMPLE_RATE;
                        pcsp.timerC = (1193180 + pcsp.srate / 2) / pcsp.srate;
                        pcsp.time_to_call = (LATCH + pcsp.timerC / 2) / pcsp.timerC;
			return (0);
		case SNDCTL_DSP_SAMPLESIZE:
			return (8);
		case PCSP_SET_DEV:
			switch(arg) {
				case SNDCARD_STO1: 
					if (stereo1_init() < 0)
						return -ENODEV;
					break;
				case SNDCARD_PCSP: 
				case SNDCARD_DACM: 
				case SNDCARD_DACS:
					pcsp.act_dev = arg; break;
				default:
					return -ENODEV;
			}
			return (0);
		case PCSP_GET_DEV:
			return (pcsp.act_dev);
		case PCSP_SET_PORTS:
			if ((arg & 0xFF) < LP_NO && (arg >> 8) < LP_NO) {
				if (pcsp.act_dev == SNDCARD_STO1) {
					if (stereo1_detect(arg & 0xFF)) {
						pcsp.port = LP_B(arg & 0xFF);
						return (0);
					}
				}
				else {
					pcsp.port  = LP_B(arg & 0xFF);
					pcsp.portS = LP_B(arg >> 8);
					return (0);
				}
			}
			return -EINVAL;
		case PCSP_GET_PORTS: 
			ret = 0;
			for (i = 0; i < LP_NO; ++i)
				if (LP_B(i) == pcsp.port)
					ret = i;
			for (i = 0; i < LP_NO; ++i)
				if (LP_B(i) == pcsp.portS)
					ret |= i << 8;
			return (ret);
		case PCSP_GET_VOL:
			return (pcsp.volume);
		case PCSP_SET_VOL:
			pcsp.volume = arg;
                        pcsp_calc_vol(pcsp.volume);
			return (0);
		default	:
			return -EINVAL;
	}
}
#endif

static int pcsp_read(struct inode * inode, struct file * file, char * buffer, int count)
{
	return -EINVAL;
}

static int pcsp_write(struct inode * inode, struct file * file, 
                      char * buffer, int count)
{
	unsigned long copy_size;
	unsigned long total_bytes_written = 0;
	unsigned bytes_written;
	int i;

	do {
		bytes_written = 0;
		copy_size = (count <= ABLK_SIZE) ? count : ABLK_SIZE;
		i = pcsp.in[0] ? 1 : 0;
		if (copy_size && !pcsp.in[i]) {
			memcpy_fromfs(pcsp.buf[i], buffer, copy_size);
			if (pcsp_to_ulaw)
				translate_bytes(ulaw, pcsp.buf[i], copy_size);
			pcsp.in[i] = copy_size;
			if (! pcsp.timer_on)
				pcsp_start_timer();
			bytes_written += copy_size;
			buffer += copy_size;
		}
 
		if (pcsp.in[0] && pcsp.in[1]) {
			interruptible_sleep_on(&pcsp_sleep);
			if (current->signal & ~current->blocked) {
				if (total_bytes_written + bytes_written)
					return total_bytes_written + bytes_written;
				else
					return -EINTR;
			}
		}
		total_bytes_written += bytes_written;
		count -= bytes_written;
		
	} while (count > 0);
	return total_bytes_written;
}

struct file_operations pcspeaker_pcsp_fops = {
	NULL,		/* pcsp_seek */
	pcsp_read,
	pcsp_write,
	NULL, 		/* pcsp_readdir */
	NULL,	 	/* pcsp_select */
	pcsp_ioctl,	/* pcsp_ioctl */
	NULL,		/* pcsp_mmap */
	pcsp_open,
	pcsp_release,
};

long pcspeaker_init(long kmem_start)
{ 
	int i;

	pcsp_active       = 0;
	pcsp_speaker      = 0;
	pcsp.timer_on     = 0;
	pcsp.mode         = 0;
	pcsp.buf[0]       = (unsigned char *)kmem_start;
	pcsp.buf[1]       = (unsigned char *)(kmem_start + ABLK_SIZE);
	pcsp.buffer       = pcsp.buf[0];
	pcsp.in[0]        = pcsp.in[1] = 0;
	pcsp.actual       = 0;
	pcsp.srate        = DEFAULT_RATE;
	pcsp.si 	  = (pcsp.srate << 16) / SAMPLE_RATE;
	pcsp.timerC       = (1193180 + pcsp.srate / 2) / pcsp.srate;
	pcsp.time_to_call = (LATCH + pcsp.timerC / 2) / pcsp.timerC; 
	pcsp.act_dev      = SNDCARD_PCSP;
	pcsp.port         = pcsp.portS = 0;

	pcsp.volume	  = (DEFAULT_LEFT + DEFAULT_RIGHT) * 128 / 100;

	pcsp_calc_vol(pcsp.volume);
#ifdef PCSP_MIXER
	pcsp_calc_voltab(DEFAULT_LEFT,  left_vol);
	pcsp_calc_voltab(DEFAULT_RIGHT, right_vol);
#endif

	printk("  PC-Speaker installed\n");
	i = stereo1_init();
	if (i >= 0) 
			printk("  Stereo-on-One at lpt%d detected\n", i);

	return (kmem_start + 2*ABLK_SIZE);
}
