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

#include <linux/pcsp.h>

#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/ptrace.h>
#include <linux/timex.h>
#include <linux/ioport.h>
#include <linux/mm.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

/* the maximal samplerange for PC-Speaker: 18357 Hz */
#define MIN_CONST	65

#define MAX_SRATE	(CLOCK_TICK_RATE / MIN_CONST)
#define SRATE		(CLOCK_TICK_RATE / pcsp.realrate)

/*
 * the default blocksize for playing thru
 * PC-Speaker, STO1 and STNC use now 32k
 */

#ifndef ABLK_SIZE
#define ABLK_SIZE	16184
#endif

/* the default samplerate for /dev/audio */
#ifndef DEFAULT_RATE
#define DEFAULT_RATE	8000
#endif

#ifndef PCSP_MIXER
#define DEFAULT_LEFT	100
#define DEFAULT_RIGHT	100
#endif

/*
 * This function will be used to generate a time delay on pentium
 * processors; this delay is only needed for the STO1 because it has
 * only one latch, so we must wait some time after we write the value
 * for the left channel before we can write the right one
 *
 * as long as I haven't such, it's empty :-)
 */

#define PCSP_PENTIUM_DELAY	

/*
   the "critical" frequency: if the machine is too slow for this, PCSP
   is disabled
*/
#ifndef PCSP_CRITICAL_FREQ
#define PCSP_CRITICAL_FREQ	12500
#endif

/* PCSP internal maximum volume, it's hardcoded */
#define PCSP_MAX_VOLUME		256

/* need this macros */
#define MIN(a,b)	( ((a) < (b)) ? (a) : (b) )
#define MAX(a,b)	( ((a) > (b)) ? (a) : (b) )

/* the parallel-ports */
static pcsp_ports[]       = { 0x3bc, 0x378, 0x278 };
static pcsp_port_enable[] = { 0,     0,     0     };

#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 {
	int		last_clocks;
	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	left;		/* left volume */
	unsigned	right;		/* right volume */
	unsigned	srate;		/* sample rate */
	unsigned	si;		/* precalculated step const */
	unsigned	timerC;		/* hardware timer ticks for srate */
	unsigned	timerCF;	/* for fixed samplerate */
	unsigned	act_dev;	/* which device is playing */
	unsigned	port;		/* on which lp-port */
	unsigned	portS;		/* for Stereo */
	unsigned	actual;		/* actual buffer */
	unsigned	realrate;	/* the real sample rate */
	unsigned	maxrate;	/* maximum real sample rate */
	unsigned	audio_fmt;	/* 16 or 8 bit */
	unsigned	fmt_msk;	/* supported data formats */
	unsigned	ablk_size;	/* length of one audio-buffer */
	unsigned char	e;
	char		timer_on;
	char		mode;		/* Mono / Stereo */
	char		is_ulaw;	/* need ULAW->LINEAR */
#ifdef PCSP_16BIT_SUPPORT
	char		stereo_emu;	/* set if Stereo is emulated */
	char		enable_emu;	/* set if the emulation is enabled */
#endif
} pcsp;

/* Volume-tables */

static unsigned char vl_tab[256];
static unsigned char left_vol[256], right_vol[256];
#ifdef PCSP_16BIT_SUPPORT
static unsigned char left_volS[256], right_volS[256];
#endif

#ifdef PCSP_MIXER
extern int pcsp_mixer_set(int whichDev, unsigned int level);
extern int pcsp_mixer_ioctl(struct inode * inode, struct file * file,
                            unsigned int cmd, unsigned long arg);
#endif

/* need this to be global */
char pcsp_active = 0;
char pcsp_speaker = 0;
char pcsp_enabled = -1;
volatile int pcsp_timer0_latch, pcsp_clockticks;

static struct wait_queue *pcsp_sleep = NULL;

extern int pcsp_set_irq(int (*func)(void));
extern int pcsp_release_irq(void);
extern void do_timer(int irq, struct pt_regs * regs);
static void pcsp_stop_timer(void);

/* test if a parallel port is free */

inline static int pcsp_free_port(int port)
{
	return (pcsp_port_enable[port] || !check_region(LP_B(port), 3));
}

static void pcsp_reserve_port(int port)
{
	pcsp_port_enable[port] = 1;
	request_region(LP_B(port), 3, "pcsp");
}

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

/* test if a new Stereo-Circuit is on lp(port) */
inline static int stereo_nc_detect(unsigned port)
{
	if (! pcsp_free_port(port))
		return 0;
	outb(0, LP_B(port));
	if (LP_S(port) & 0x40) {
		outb(0xFF, LP_B(port));
		return (LP_S(port) & 0x40) ? 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_reserve_port(i);
			pcsp.port    = LP_B(i);
			pcsp.act_dev = SNDCARD_STO1;
			return i;
		}
	return (-ENODEV);
}

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

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

#ifdef PCSP_TEST_SPEED

/*
   this is a stupid beep which occurs if PCSP is disabled;
   it's not needed because we have the message, but who reads it...
   and this is the PC-Speaker driver :-)
*/
void pcsp_beep(int count, int cycles)
{
	/* enable counter 2 */
	outb_p(inb_p(0x61)|3, 0x61);
	/* set command for counter 2, 2 byte write */
	outb_p(0xB6, 0x43);
	/* select desired HZ */
	outb_p(count & 0xff, 0x42);
	outb((count >> 8) & 0xff, 0x42);

	while (cycles--);
		 
	/* disable counter 2 */
	outb(inb_p(0x61)&0xFC, 0x61);
}

volatile int pcsp_test_running;

/*
   the timer-int for testing cpu-speed, mostly the same as
   for PC-Speaker
 */
static int pcsp_test_intspeed(void)
{
	if (pcsp.index < pcsp.in[pcsp.actual]) {
		outb(pcsp.e, 0x61);
		__asm__("xorb $0,%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)	/* NEVER */
			nop();
		if (pcsp.in[pcsp.actual] == 0xFFFF)
			pcsp.actual ^= 1;
	}

	if ( (pcsp_clockticks -= pcsp.timerCF) < 0)
		pcsp_clockticks += LATCH;
	++pcsp_test_running;
	return 1;
}

/*
   this routine measures the time needed for one timer-int if
   we play thru PC-Speaker. This is kind of ugly but does the
   trick.
 */
static int pcsp_measurement(long kmem_start, int addon)
{
	int count;
	unsigned long flags;

	pcsp_clockticks   = 0;
	pcsp.timerCF	  = LATCH;
	pcsp.buf[0]       = (unsigned char *)kmem_start;
	pcsp.buffer       = pcsp.buf[0];
	pcsp.index	  = 0;
	pcsp.xfer	  = 0;
	pcsp.si		  = 1 << 16;
	pcsp.in[0]        = 5 + addon;
	pcsp.in[1] 	  = 0;
	pcsp.actual       = 0;
	pcsp.e 		  = inb(0x61) & 0xFC;

	pcsp_test_running = 0;

	if (pcsp_set_irq(pcsp_test_intspeed) < 0)
		panic("PCSP could not modify timer IRQ!");

	/*
	   Currently (0.99.15d) Linux call chr_dev_init with ints
	   disabled; so we need a sti() to enable them.
	   However, because this can be changed in the future we use
	   save_flags() and restore_flags()
	*/
	save_flags(flags);
	sti();

	/*
	  Perhaps we need some sort of timeout here, but if IRQ0
	  isn't working the system hangs later ...
	*/
	while (pcsp_test_running < 5);
	restore_flags(flags);

	if (pcsp_release_irq() < 0)
		panic("PCSP could not reset timer IRQ!");

	outb_p(0x00, 0x43);		/* latch the count ASAP */
	count = inb_p(0x40);		/* read the latched count */
	count |= inb(0x40) << 8;
	return (LATCH - count);
}

static int pcsp_test_speed(long kmem_start)
{
	int worst, worst1, best, best1;

	worst  = pcsp_measurement(kmem_start, 0);
	worst1 = pcsp_measurement(kmem_start, 0);
	best   = pcsp_measurement(kmem_start, 5);
	best1  = pcsp_measurement(kmem_start, 5);

	worst = MIN(worst, worst1);
	best  = MIN(best, best1);

#ifdef PCSP_DEBUG
	printk("  PCSP-Timerint needs %d Ticks in worst case\n", worst);
	printk("  PCSP-Timerint needs %d Ticks in best case\n", best);
#endif
	/* We allow a CPU-usage of 90 % for the best-case ! */
	pcsp.realrate = best * 10 / 9;
	pcsp.maxrate  = CLOCK_TICK_RATE / pcsp.realrate;
	printk("  PCSP-Measurement: Maximal samplerate %d Hz", pcsp.maxrate);

	if (pcsp.maxrate > PCSP_CRITICAL_FREQ) {
		if (MIN_CONST > pcsp.realrate) {
			pcsp.realrate = MIN_CONST;
			printk(", %d Hz", MAX_SRATE);
		}
		printk(" used\n");
		return 1;
	}

	/* very ugly beep, but you hopefully never hear it */
	pcsp_beep(12000,800000);
	pcsp_beep(10000,800000);
	
	printk("\n  This is too SLOW! PCSP-driver DISABLED\n");
	return 0;
}
#endif

/* the timer-int for playing thru PC-Speaker */
int pcsp_do_timer(void)
{
	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.in[pcsp.actual] == 0)
			pcsp_stop_timer();
	}

	if ( (pcsp_clockticks -= pcsp.timerCF) < 0) {
		pcsp_clockticks += LATCH;
		return 0;
	}
	return 1;
}

/* timer-int for playing thru STO1 */
int pcsp_do_sto1_timer(void)
{
	static int ret;

	if (pcsp.buffer < pcsp.end) {
		if (pcsp.mode) {
			register short w = ((short)pcsp.port);
			__asm__("xlatb\n\t"
				"outb %0,%1\n\t"
				"addl $2,%1\n\t"
				"movb $1,%0\n\t"
				"outb %0,%1\n\t"
				"movb $0,%0\n\t"
				"outb %0,%1"
				:
				: "a" ((char) *pcsp.buffer++), "d" ((short)w), "b" ((long)right_vol)
				: "ax", "dx", "bx"
				);

			/* 
			 * I move the following code because 
			 * I need some time delay for the left DAC
			 * on my 486DX2
			 * this will hopefully enough or we need some
			 * really time wasting jumps here
			 *
			 * This time delay will really be a problem for
			 * pentiums :-(
			 *
			 */

			if ( (pcsp_clockticks -= pcsp.timerC) < 0) {
				pcsp_clockticks += LATCH;
				ret = 0;
			}
			else
				ret = 1;

			PCSP_PENTIUM_DELAY;

 			__asm__( "xlatb\n\t"
				"outb %0,%1\n\t"
				"addl $2,%1\n\t"
				"movb $2,%0\n\t"
				"outb %0,%1\n\t"
				"movb $0,%0\n\t"
				"outb %0,%1"
				:
				: "a" ((char) *pcsp.buffer++), "d" ((short)w), "b" ((long)left_vol)
				: "ax", "dx", "bx"
				);
		}
		else {	/* Mono */
			outb(left_vol[*pcsp.buffer++], (short)pcsp.port);
			if ( (pcsp_clockticks -= pcsp.timerC) < 0) {
				pcsp_clockticks += LATCH;
				ret = 0;
			}
			else
				ret = 1;
		}
	}
	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.in[pcsp.actual] == 0)
			pcsp_stop_timer();
	}
	return (ret);
}

/* timer-int for playing thru DACs */
int pcsp_do_dac_timer(void)
{
	if (pcsp.buffer < pcsp.end) {
		if (pcsp.act_dev == SNDCARD_DACS)  {
			if (pcsp.mode) {
				outb(left_vol[*pcsp.buffer++], pcsp.port);
				outb(right_vol[*pcsp.buffer++], pcsp.portS);
			}
			else {
				outb(left_vol[*pcsp.buffer], pcsp.port);
				outb(left_vol[*pcsp.buffer++], pcsp.portS);
			}
		}
		else	/* Simple DAC */
			outb(left_vol[*pcsp.buffer++], pcsp.port);
	}
	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.in[pcsp.actual] == 0)
			pcsp_stop_timer();
	}

	if ( (pcsp_clockticks -= pcsp.timerC) < 0) {
		pcsp_clockticks += LATCH;
		return 0;
	}
	return 1;
}

/* calculate all needed time-consts, return the 'adjusted' samplerate */
static unsigned pcsp_calc_srate(unsigned rate)
{
	pcsp.timerC = (CLOCK_TICK_RATE + rate / 2) / rate;
	pcsp.srate  = (CLOCK_TICK_RATE + pcsp.timerC / 2) / pcsp.timerC;
	/* and now for the PC-Speaker */

	pcsp.timerCF = pcsp.realrate;
	pcsp.si     = (pcsp.srate << 16) / SRATE;
	return pcsp.srate;
}

static void pcsp_start_timer(void)
{
	int result;

	/* use the first buffer */
	pcsp.actual  = 0;
	pcsp.xfer    = pcsp.index = 0;
        pcsp.buffer  = pcsp.buf[pcsp.actual];
	pcsp.end     = pcsp.buffer + pcsp.in[pcsp.actual];

	if (pcsp.act_dev == SNDCARD_PCSP) {
		pcsp_speaker = 1;
	        pcsp.e = inb(0x61) | 0x03;
		outb_p(0x92, 0x43);	/* binary, mode 1, LSB only, ch 2 */
		outb_p(0x34,0x43);	/* binary, mode 2, LSB/MSB, ch 0 */
		outb_p(pcsp.timerCF & 0xFF, 0x40);
		outb(pcsp.timerCF >> 8 , 0x40);
		if (pcsp_set_irq(pcsp_do_timer) < 0)
			panic("PCSP: could not modify timer IRQ!");
		pcsp_timer0_latch = pcsp.timerCF;
	}
	else {	/* it's a DAC */
		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);  
		if (pcsp.act_dev == SNDCARD_STO1) 
       	        	result = pcsp_set_irq(pcsp_do_sto1_timer);
		else
       	        	result = pcsp_set_irq(pcsp_do_dac_timer);
		if (result < 0)
			panic("PCSP: could not modify timer IRQ!");
		pcsp_timer0_latch = pcsp.timerC;
	}
		pcsp_clockticks = pcsp.last_clocks;
		pcsp.timer_on = 1;
}

/* reset the timer to 100 Hz and reset old timer-int */
static void pcsp_stop_timer(void)
{
	if (pcsp.timer_on) {
		/* 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 */

		/* clear clock tick counter */
		pcsp.last_clocks = pcsp_clockticks;
		pcsp_timer0_latch = pcsp_clockticks = LATCH;	

		if (pcsp_release_irq() < 0)
			panic("PCSP: could not reset timer IRQ!");

		pcsp.timer_on = 0;
	}

	/* 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;
}

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

	if (pcsp.is_ulaw)
		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[ulaw[j + 128]];
		}
	else
		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];
		}
}

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

	if (pcsp.is_ulaw)
		for (i = 0; i < 256; ++i) {
			j = ((i - 128) * volume) >> 8;
			*tab++ = ulaw[j + 128];
		}
	else
		for (i = 0; i < 256; ++i) {
			j = ((i - 128) * volume) >> 8;
			*tab++ = j + 128;
		}
}
#ifdef PCSP_MIXER

inline void pcsp_set_voltables(void)
{
#ifdef PCSP_16BIT_SUPPORT
	if (pcsp.stereo_emu) {
		pcsp_calc_voltab(pcsp.left, left_volS);
		pcsp_calc_voltab(pcsp.right, right_volS);
		pcsp_calc_vol(PCSP_MAX_VOLUME);
		pcsp_calc_voltab(PCSP_MAX_VOLUME, left_vol);
		pcsp_calc_voltab(PCSP_MAX_VOLUME, right_vol);
	}
	else 
#endif
	{
		pcsp_calc_voltab(pcsp.left, left_vol);
		pcsp_calc_voltab(pcsp.right, right_vol);
		pcsp_calc_vol(pcsp.volume);
	}
}

/* this is called if /dev/pcmixer change Mastervolume */
inline void pcsp_set_volume(unsigned short v)
{
	pcsp.left   = (((unsigned)(v & 0x7F) << 8) + 50) / 100;
	pcsp.right  = (((unsigned)(v & 0x7F00)) + 50) / 100;
        pcsp.volume = (pcsp.right + pcsp.left) >> 1;
	pcsp_set_voltables();
}

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

/*
   set the speed for /dev/pcsp, it's now from 4000 - 44100 Hz,
   but DAC's are bounded by the maximal samplerate
 */
inline unsigned long pcsp_set_speed(unsigned long speed)
{
	if (speed < 4000)
		speed = 4000;
	if (speed > 44100)
		speed = 44100;
	if (pcsp.act_dev != SNDCARD_PCSP)
		if (speed > pcsp.maxrate)
			speed = pcsp.maxrate;
	return speed;
}

/*
   set the audio type
*/
int pcsp_set_format(int fmt)
{
	static char ulaw;

	if (fmt != AFMT_QUERY) {
		ulaw = 0;
		if (! (pcsp.fmt_msk & fmt))	/* Not supported */
			if (fmt == AFMT_MU_LAW)
				ulaw = 1;
			else
				fmt = AFMT_U8;	/* this is supported */
		pcsp.audio_fmt = fmt;
		/*
		   we must recalculate the volume-tables is we change
		   ulaw-state
		*/
		if (pcsp.is_ulaw != ulaw) {
			pcsp.is_ulaw = ulaw;
			pcsp_set_voltables();
		}
	}
	return pcsp.audio_fmt;
}

static void pcsp_sync(void)
{
	while (!(current->signal & ~current->blocked) &&
               (pcsp.in[0] || pcsp.in[1]) ) {
		/* 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;

	/* we set 8000 Hz for /dev/audio (ulaw flag set before open) */
	if (pcsp.is_ulaw)
		pcsp_calc_srate(DEFAULT_RATE);

	pcsp_active   = 1;
	return 0;
}

static int pcsp_ioctl_out(unsigned long *ptr, long value)
{
	static int error;

	if (value < 0)
		return value;
	error = verify_area(VERIFY_WRITE, ptr, sizeof(long));
	if (error)
		return error;
	put_fs_long(value, ptr);
	return 0;
}


/* 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;

#ifdef PCSP_MIXER
	if ((cmd >> 8) & 0xff == 'M')	/* it's a Mixer IOCTL */
		return pcsp_mixer_ioctl(inode, file, cmd, arg); 
#endif
	switch (cmd) {
		case SNDCTL_DSP_SPEED:	/* simulate SB 1.0 */
			error = verify_area(VERIFY_READ, ptr, 4);
			if (error)
				return (error);
			arg = pcsp_set_speed(get_fs_long(ptr));
			arg = pcsp_calc_srate(arg);
			return pcsp_ioctl_out(ptr, arg);
		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 ) 
#ifdef PCSP_16BIT_SUPPORT
				pcsp.stereo_emu = 0;
			else {
				pcsp.stereo_emu = (arg) ? 1 : 0;
				/* test if the emulation is disabled */
				if (! pcsp.enable_emu && pcsp.stereo_emu) {
					pcsp.stereo_emu = 0;
					pcsp.mode = 0;
					return -EINVAL;
				}
			}
			pcsp.mode = (arg) ? 1 : 0;
			pcsp_set_voltables();
#else
				pcsp.mode = (arg) ? 1 : 0;
			else {
				pcsp.mode = 0;
				if (arg)
					return -EINVAL;
			}
#endif
			return (0);
		case SNDCTL_DSP_GETBLKSIZE:
			return pcsp_ioctl_out(ptr, pcsp.ablk_size);
		case SNDCTL_DSP_SYNC:	/* syncinc, so speed changes work correct */
			pcsp_sync();
			pcsp_stop_timer();
			return (0);
		case SNDCTL_DSP_RESET:	/* stops output immediately */
			pcsp_stop_timer();
			pcsp_calc_srate(DEFAULT_RATE);
			return (0);
		case SNDCTL_DSP_GETFMTS:
			return pcsp_ioctl_out(ptr, pcsp.fmt_msk);
		case SNDCTL_DSP_SETFMT:
			error = verify_area(VERIFY_READ, ptr, 4);
			if (error)
				return (error);
			arg = get_fs_long(ptr);
			return pcsp_ioctl_out(ptr, pcsp_set_format(arg));
	
		/*
		   the following ioctls currently do nothing, but
		   exist for compatibility; however, because pcsp's
		   implementation is somewhat strange, they are not needed,
		   because pcsp start output after any count of data
		   written to one buffer
		   this may change in the future
		*/
		case SNDCTL_DSP_SUBDIVIDE:
		case SNDCTL_DSP_SETFRAGMENT:
		case SNDCTL_DSP_POST:
			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;
				case SNDCARD_STNC: 
					if (stereo_nc_init() < 0)
						return (-ENODEV);
					break;
				default:
					return (-ENODEV);
			}
			/* Perhaps we need to adjust the samplerate */
			pcsp.srate = pcsp_set_speed(pcsp.srate);
			pcsp_calc_srate(pcsp.srate);
			return (0);
		case PCSP_GET_DEV:
                        return pcsp_ioctl_out(ptr, pcsp.act_dev);
		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 if (pcsp.act_dev == SNDCARD_STNC) {
					if (stereo_nc_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 pcsp_ioctl_out(ptr, ret);
                case PCSP_GET_VOL:
                        return pcsp_ioctl_out(ptr, pcsp.volume);
                case PCSP_SET_VOL:
                        error = verify_area(VERIFY_READ, ptr, 4);
                        if (error)
                                return (error);
                        pcsp.volume = get_fs_long(ptr);
#ifdef PCSP_MIXER
			arg = MIN(256, pcsp.volume);
			arg = (arg * 100) / 256;
			arg = (arg << 8 ) | arg;
			pcsp_mixer_set(SOUND_MIXER_VOLUME, arg);
#else
			pcsp_calc_vol(pcsp.volume);
#endif
			return (0); 
                case PCSP_GET_SRATE:
                        return pcsp_ioctl_out(ptr, SRATE);
                case PCSP_SET_SRATE:
                        error = verify_area(VERIFY_READ, ptr, 4);
                        if (error)
                                return (error);
			arg = get_fs_long(ptr);
			if (arg < 10000 || arg > MAX_SRATE || arg > pcsp.maxrate)
				return (-EINVAL);
			pcsp.realrate = (CLOCK_TICK_RATE + arg / 2) / arg;
			return (0); 
                case PCSP_GET_MEASURE:
			return pcsp_ioctl_out(ptr, pcsp.maxrate);
		case PCSP_SET_EMU_MODE:
#ifdef PCSP_16BIT_SUPPORT
                        error = verify_area(VERIFY_READ, ptr, 4);
                        if (error)
                                return (error);
                        arg = get_fs_long(ptr);
			if (arg == PCSP_EMULATION_ON) {
				pcsp.enable_emu = 1;
				pcsp.fmt_msk |= AFMT_S16_LE;
			}
			else if (arg == PCSP_EMULATION_OFF) {
				pcsp.enable_emu = 0;
				pcsp.fmt_msk &= ~AFMT_S16_LE;
			}
			return pcsp_ioctl_out(ptr, pcsp.enable_emu);
#endif
		default	:
			return (-EINVAL);
	}
}

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;
#ifdef PCSP_16BIT_SUPPORT
	int factor = 1;

#define CNT(x)		((x) * factor)
#else
#define CNT(x)		(x)
#endif

#ifdef PCSP_16BIT_SUPPORT
	/* the audio format shouldn't be changed during output */
	if (pcsp.audio_fmt == AFMT_S16_LE) {
		count  >>= 1;
		factor <<= 1;
	}
	if (pcsp.stereo_emu) {
		count  >>= 1;
		factor <<= 1;
	}
#endif
	do {
		bytes_written = 0;
		copy_size = (count <= pcsp.ablk_size) ? count : pcsp.ablk_size;
		i = pcsp.in[0] ? 1 : 0;
		if (copy_size && !pcsp.in[i]) {

#ifdef PCSP_16BIT_SUPPORT
			if (pcsp.audio_fmt == AFMT_S16_LE)
				if (pcsp.stereo_emu)
					__asm__(
					"cld\n"
					"1:\t"
					"incl %%esi\n\t"
					"fs ; movb (%%esi),%%al\n\t"
					"xorb $0x80,%%al\n\t"
					"xlat\n\t"
					"incl %%esi\n\t"
					"xchgl %%ebx, %%edx\n\t"
					"movb %%al,%%ah\n\t"
					"incl %%esi\n\t"
					"fs ; movb (%%esi),%%al\n\t"
					"xorb $0x80,%%al\n\t"
					"xlat\n\t"
					"incl %%esi\n\t"
					"xchgl %%ebx, %%edx\n\t"
					"addb %%ah,%%al\n\t"
					"rcrb $1,%%al\n\t"
					"stosb\n\t"
					"loop 1b"
					: /* no outputs */
					:"b" ((long) left_volS),
					 "d" ((long) right_volS),
					 "c" (copy_size),
					 "D" ((long) pcsp.buf[i]),
					 "S" ((long) buffer)
					:"ax","bx","cx","dx",
					 "si","di","memory");
				else
					__asm__(
					"cld\n"
					"1:\t"
					"incl %%esi\n\t"
					"fs ; lodsb\n\t"
					"xorb $0x80,%%al\n\t"
					"stosb\n\t"
					"loop 1b"
					: /* no outputs */
					:"c" (copy_size),
					 "D" ((long) pcsp.buf[i]),
					 "S" ((long) buffer)
					:"ax","cx","di","si","memory");
			else
			if (pcsp.stereo_emu)
					__asm__(
					"cld\n"
					"1:\t"
					"fs ; lodsw\n\t"
					"xlat\n\t"
					"xchgl %%edx,%%ebx\n\t"
					"xchgb %%ah,%%al\n\t"
					"xlat\n\t"
					"xchgl %%edx,%%ebx\n\t"
					"addb %%ah,%%al\n\t"
					"rcrb $1,%%al\n\t"
					"stosb\n\t"
					"loop 1b"
					: /* no outputs */
					:"b" ((long) left_volS),
					 "d" ((long) right_volS),
					 "c" (copy_size),
					 "D" ((long) pcsp.buf[i]),
					 "S" ((long) buffer)
					:"ax","bx","cx","dx",
					 "si","di","memory");
			else
#endif
				memcpy_fromfs(pcsp.buf[i], buffer, 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 CNT(total_bytes_written + bytes_written);
				else
					return -EINTR;
			}
		}
		total_bytes_written += bytes_written;
		count -= bytes_written;
		
	} while (count > 0);
	return CNT(total_bytes_written);
#undef CNT
}

void pcsp_setup(char *s, int *p)
{
	if (!strcmp(s, "off")) {
		pcsp_enabled = 0;
		return;
	}
	if (p[0] > 0)
		pcsp.maxrate = p[1];
	pcsp_enabled = 1;
}

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;

	if (pcsp_enabled < 0) {
#ifdef PCSP_TEST_SPEED
		pcsp_enabled = pcsp_test_speed(kmem_start);
#else
		pcsp.maxrate  = 44100;	/* only a BIG freq */
		pcsp.realrate = (CLOCK_TICK_RATE + SELECTED_SRATE / 2) /
				SELECTED_SRATE;
		pcsp_enabled  = 1;
#endif
	}
	else {
		pcsp.realrate = MIN(pcsp.maxrate, MAX_SRATE);
		pcsp.realrate = (CLOCK_TICK_RATE + pcsp.realrate / 2) /
				pcsp.realrate;
	}
	if (! pcsp_enabled)
		return kmem_start;

	pcsp.xfer	  = 0;
	pcsp_clockticks   = pcsp_timer0_latch = pcsp.last_clocks = LATCH;
	pcsp_active       = 0;
	pcsp_speaker      = 0;
	pcsp.timer_on     = 0;
	pcsp.mode         = 0;
	pcsp.is_ulaw      = 0;
	pcsp.audio_fmt	  = AFMT_U8;
#ifdef PCSP_16BIT_SUPPORT
	pcsp.fmt_msk	  = AFMT_U8 | AFMT_S16_LE;
#else
	pcsp.fmt_msk	  = AFMT_U8;
#endif
	pcsp.buffer       = pcsp.buf[0];
	pcsp.in[0]        = pcsp.in[1] = 0;
	pcsp.actual       = 0;
	pcsp.act_dev      = SNDCARD_PCSP;
	pcsp.port         = pcsp.portS = 0;
	pcsp.volume	  = (DEFAULT_LEFT + DEFAULT_RIGHT) * 128 / 100;
	pcsp.ablk_size    = ABLK_SIZE;
	pcsp_calc_srate(DEFAULT_RATE);
	pcsp_calc_vol(pcsp.volume);
	pcsp_calc_voltab(DEFAULT_LEFT,  left_vol);
	pcsp_calc_voltab(DEFAULT_RIGHT, right_vol);
#ifdef PCSP_16BIT_SUPPORT
	memcpy(left_volS,  left_vol,  sizeof(left_volS));
	memcpy(right_volS, right_vol, sizeof(right_volS));
	pcsp.stereo_emu   = 0;
	pcsp.enable_emu   = 1;
#endif
	printk("  PC-Speaker");
	i = stereo1_init();
	if (i >= 0) {
			printk(", Stereo-on-One at lpt%d", i);
			pcsp.ablk_size = 2 * ABLK_SIZE;
	}
	i = stereo_nc_init();
	if (i >= 0) {
			printk(", New Stereo Circuit at lpt%d", i);
			pcsp.ablk_size = 2 * ABLK_SIZE;
	}
	pcsp.buf[0] = (unsigned char *)kmem_start;
	pcsp.buf[1] = (unsigned char *)(kmem_start + pcsp.ablk_size);
	return (kmem_start + 2 * pcsp.ablk_size);
}
