/*
** sound/SVR4.2/soundcard.c
**
** Soundcard driver for SVR4.2 UNIX.
**
** Copyright by Ian Hartas 1994
** 
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met: 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer. 2.
** Redistributions in binary form must reproduce the above copyright notice,
** this list of conditions and the following disclaimer in the documentation
** and/or other materials provided with the distribution.
** 
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY
** EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
** DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
** ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
** LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
** OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
** SUCH DAMAGE.
**
*/

/*
** History.
** Created by Ian Hartas, based on the examples of the other OS drivers
**
** Last amended by Ian Hartas, Sep/1994 to Oct/1994
** to tidy up and to make this a loadable driver.
** Incorporating ideas from Steve Kimball, Hannu Salvolainen and Gary Jones.
** Converted the sco/soundcard.c sprintf function to stdarg and used it here
** as the SVR4.2 kernel sprintf is very limited.
**
*/

#include "sound_config.h"

#ifdef CONFIGURE_SOUNDCARD

#include "dev_table.h"

#define FIX_RETURN(ret) return (ret < 0 ? -ret : 0) 

static int      timer_running = 0;

int	soundcard_configured = 0;

struct fileinfo files[SND_NDEVS];

int         sndopen(dev_t *, int , int , cred_t *);
int         sndclose(dev_t , int , int , cred_t *);
int         sndioctl(dev_t , int , void *, int , cred_t *, int *);
int         sndread(dev_t , uio_t *, cred_t *);
int         sndwrite(dev_t , uio_t *, cred_t *);
static void sound_mem_init (void);

int snddevflag = D_NEW | D_DMA;

#ifndef SVR40
/*****************************************************************************/

#define DRVNAME	"snd - Soundcard Driver"
static int snd_load(), snd_unload(); 
void sndinit(); 
static void snd_freemem();

MOD_DRV_WRAPPER(snd, snd_load, snd_unload, NULL, DRVNAME);

static int
snd_load()
{
	sndinit();
	mod_drvattach(&snd_attach_info);

	return (0);
}

static int
snd_unload()
{
	mod_drvdetach(&snd_attach_info);
	snd_freemem();
	return (0);
}

#endif /* SVR40 */

/*****************************************************************************/
/*
** The following memory allocation functions are required because the 
** driver unload function must release all memory allocated throughout 
** the life of the driver. Hence any kernel malloc calls must be fed
** through snd_alloc and released through snd_free . Any remaining
** memory on the last close can then be released on an unload request
** from the modadmin command.
*/
/*****************************************************************************/
struct mem_chain
{
	int	size;
	struct mem_chain *next;
	void *ptr;
};

struct mem_chain mem_chain;

/*****************************************************************************/
/*
** Allocate memory in chunks of requeste size.
** Flag specifies KM_SLEEP or KM_NOSLEEP
*/
void *
snd_alloc(int size, int flag)
{
	struct mem_chain *mem_ptr;

	if ((mem_ptr = (struct mem_chain *)
			kmem_zalloc(sizeof(struct mem_chain), flag)) == NULL)
		return (NULL);

	if ((mem_ptr->ptr = kmem_zalloc(size, flag)) == NULL)
	{
		kmem_free(mem_ptr, sizeof(struct mem_chain));
		return (NULL);
	}

	mem_ptr->size = size;
	mem_ptr->next = mem_chain.next;
	mem_chain.next = mem_ptr;

	return (mem_ptr->ptr);

}

/*****************************************************************************/
/*
** Release memory allocated by snd_alloc(). The pointer used must match
** one of the addresses allocated by snd_alloc()
*/
void
snd_free(void *ptr)
{
	struct mem_chain *mem_ptr, *last_ptr;

	last_ptr = &mem_chain;

	while (mem_ptr = last_ptr->next)
	{
		if (mem_ptr->ptr == ptr)
		{
			last_ptr->next = mem_ptr->next;
			kmem_free(mem_ptr->ptr, mem_ptr->size);
			kmem_free(mem_ptr, sizeof(struct mem_chain));
			break;
		}
		last_ptr = mem_ptr;
	}
}

/*****************************************************************************/
/*
** Release any remaining memory not already released by snd_free()
** Used by the unload function.
*/
static void
snd_freemem()
{
	struct mem_chain *mem_ptr, *next_ptr;

	mem_ptr = mem_chain.next;

	do
	{	
		next_ptr = mem_ptr->next;
		kmem_free(mem_ptr->ptr, mem_ptr->size);
		kmem_free(mem_ptr, sizeof(struct mem_chain));
	} while (mem_ptr = next_ptr);
}

/*****************************************************************************/
/*
** DMA support functions.
*/
struct dma_cb *dma_cbptr[D37A_MAX_CHAN];

int
snd_dma_alloc(int chan)
{
	if (dma_cbptr[chan] == NULL)
	{
		dma_cbptr[chan] = dma_get_cb(DMA_SLEEP);
		dma_cbptr[chan]->targ_step = DMA_STEP_INC;
		dma_cbptr[chan]->targ_path = (chan & 4) ? DMA_PATH_16 : DMA_PATH_8;	
		dma_cbptr[chan]->trans_type = DMA_TRANS_SNGL;
		dma_cbptr[chan]->targ_type = DMA_TYPE_IO;
		dma_cbptr[chan]->bufprocess = DMA_BUF_SNGL;	/* will be modified later */
		/* dma_cbptr[chan]->cycles = dma_get_best_mode(dma_cbptr[chan]); */
	}
	return (0);
}

/*****************************************************************************/
void
snd_dma_release(int chan)
{
	struct dma_buf *dma_bufptr;
	struct dma_buf *dma_bufnextptr;

	/*
	** If there are any dma_bufs then release them too
	*/
	dma_bufptr = dma_cbptr[chan]->targbufs;
	while (dma_bufptr)
	{
		dma_bufnextptr = dma_bufptr->next_buf;
		dma_free_buf(dma_bufptr);
		dma_bufptr = dma_bufnextptr;
	}

	dma_free_cb(dma_cbptr[chan]);
	dma_cbptr[chan] = NULL;
	return ;
}

/*****************************************************************************/
void
snd_dma_init()
{
	static int dma_init_done = FALSE;

	/*
	** Shouldn't really have to do this...
	*/
	if (!dma_init_done)
	{
		dma_init_done = TRUE;
		dma_init();
	}
}
/*****************************************************************************/
/*
** Function to initiate a DMA transfer. This code is capable of building
** a DMA chain, ie more than one page at a time. However, this assumes
** that the "physaddr" parameter is really a virtual address.
*/
void
snd_dma_prog(int chan, ulong physaddr, int count, 
			 int dma_mode, int dma_auto_flag)
{
	int count_left;
	int this_page;
	struct dma_buf *targbufs_ptr;
	struct dma_buf *targbufs_nextptr;
	register struct dma_cb *local_cbptr = dma_cbptr[chan];
	caddr_t vaddr;
	paddr_t paddr;
	proc_t *procp;

	local_cbptr->command = (dma_mode == DMA_MODE_READ) 
				? DMA_CMD_READ : DMA_CMD_WRITE;

	if (local_cbptr->targbufs == NULL)
		local_cbptr->targbufs = dma_get_buf(DMA_SLEEP);
	targbufs_ptr = local_cbptr->targbufs;

	count_left = count;
	drv_getparm(UPROCP, (ulong_t *)&procp);
	/*
	** If we are truely passed a physical address, then use the next two
	** lines. Otherwise, use the two following them .
	*/
/* 	vaddr = (caddr_t)phystokv((paddr_t)physaddr);
	paddr = physaddr; */

	/*
	** The system lies, the address is a virtual address. This is necessary
	** so that we can chain down the address if necessary to create a DMA
	** chain of physical addresses. Also the code in dmabuf.c is wrong
	** to assume that you can increment physical addresses to address the
	** next virtual page !
	*/
	vaddr = (caddr_t)physaddr;		/* physaddr is really a virtual address */
	paddr = vtop(vaddr, procp);		/* paddr is the physical ! */

	while (count_left)
	{
		this_page = min(count_left, pgbnd(vaddr));
		targbufs_ptr->address = paddr; 
		targbufs_ptr->count = this_page; 
		count_left -= this_page;

		if (count_left)
		{
			if (targbufs_ptr->next_buf == NULL)
			{
				/* allocate another targbuf and link it in */
				targbufs_ptr->next_buf = dma_get_buf(DMA_SLEEP);
				targbufs_ptr->physical = vtop((caddr_t)targbufs_ptr->next_buf, 
						procp);
			}
			targbufs_ptr = targbufs_ptr->next_buf;
			vaddr += this_page;
			paddr = vtop(vaddr, procp);
		}
		else
		{
			/*
			** If the last time this chain was used, it was longer, 
			** there may be extra bufs at the end. Release them now and
			** null terminate the current chain.
			*/
			if (targbufs_ptr->next_buf != NULL)
			{
				targbufs_nextptr = targbufs_ptr->next_buf;
				targbufs_ptr->next_buf = NULL;
				targbufs_ptr->physical = 0L;
				targbufs_ptr = targbufs_nextptr;
				while (targbufs_ptr)
				{
					targbufs_nextptr = targbufs_ptr->next_buf;
					dma_free_buf(targbufs_ptr);
					targbufs_ptr = targbufs_nextptr;
				}
			}
		}
	}	

	if (local_cbptr->targbufs->next_buf != NULL)	/* multiple buffers ? */
		local_cbptr->bufprocess = DMA_BUF_CHAIN;	/* yes, it's a chain */
	else
		local_cbptr->bufprocess = DMA_BUF_SNGL;		/* no, single one */

	if (dma_auto_flag)			/* DMA auto mode requested */
		local_cbptr->bufprocess |= DMA_BUF_AUTO;

	dma_prog(local_cbptr, chan, DMA_SLEEP);
	dma_enable(chan);
}

/*****************************************************************************/


#ifdef NEED_SPRINTF
/* This code was borrowed from the SCO soundcard drive which says that... */
/*** This code was borrowed from the 386BSD code ***/

/*
** Put a number (base <= 16) in a buffer in reverse order; return an
** optional length and a pointer to the NULL terminated (preceded?)
** buffer.
*/
static char *
ksprintn(ul, base, lenp)
register u_long ul;
register int base, *lenp;
{					/* A long in base 8, plus NULL. */
	static char buf[sizeof(long) * NBBY / 3 + 2];
	register char *p;

	p = buf;
	do 
	{
		*++p = "0123456789abcdef"[ul % base];
	} while (ul /= base);
	if (lenp)
		*lenp = p - buf;
	return (p);
}

/*
** Scaled down version of msprintf(3).
*/
snd_sprintf(char *buf, char *cfmt, ...)
{
	const char *fmt = cfmt;
	register char *p, *bp;
	register int ch, base;
	u_long ul;
	int lflag;
	int count = 10;
	VA_LIST ap;

	VA_START(ap, fmt);
	for (bp = buf; ; ) 
	{
		while ((ch = *(u_char *)fmt++) != '%')
			if ((*bp++ = ch) == '\0')
				return ((bp - buf) - 1);

		lflag = 0;
reswitch:	
		switch (ch = *(u_char *)fmt++) 
		{
			case 'l':
				lflag = 1;
				goto reswitch;

			case 'c':
				*bp++ = VA_ARG(ap, int);
				break;

			case 's':
				p = VA_ARG(ap, char *);
				while (*bp++ = *p++)
					;
				--bp;
				break;

			case 'd':
				ul = lflag ? VA_ARG(ap, long) : VA_ARG(ap, int);
				if ((long)ul < 0) 
				{
					*bp++ = '-';
					ul = -(long)ul;
				}
				base = 10;
				goto number;
				break;

			case 'o':
				ul = lflag ? VA_ARG(ap, u_long) : VA_ARG(ap, u_int);
				base = 8;
				goto number;
				break;

			case 'u':
				ul = lflag ? VA_ARG(ap, u_long) : VA_ARG(ap, u_int);
				base = 10;
				goto number;
				break;

			case 'x':
				ul = lflag ? VA_ARG(ap, u_long) : VA_ARG(ap, u_int);
				base = 16;
number:			
				for (p = (char*)ksprintn(ul, base, NULL); ch = *p--;)
					*bp++ = ch;
				break;

			default:
				*bp++ = '%';
				if (lflag)
					*bp++ = 'l';
				/* FALLTHROUGH */

			case '%':
				*bp++ = ch;
		}
	}
	VA_END(ap);
}
#endif

/*****************************************************************************/
ulong_t
snd_get_lbolt()
{
	ulong_t local_lbolt;

	drv_getparm(LBOLT, &local_lbolt);

	return (local_lbolt);
}

/*****************************************************************************/
int
snd_proc_abort(int q, int f)
{
	proc_t *procp;

/*
** This code breaks DDI conformance. Anybody know of a clean way
** of doing this ?
** For MP systems this would have to be lwpp->l_xflag
*/
	drv_getparm(UPROCP, (ulong_t *)&procp);

#if defined(XABORTCLOSE)
	return ((procp->p_xflag & XABORTCLOSE) || (q & WK_SIGNAL));
#else
	return (q & WK_SIGNAL);
#endif
}

/*****************************************************************************/
snd_ioctl_in(int *arg)
{
	int word;

	if (copyin((caddr_t)arg, (caddr_t)&word, sizeof(word)))
		cmn_err(CE_WARN, "snd_ioctl_in fault\n");
	return (word);
}

/*****************************************************************************/
void
snd_ioctl_out(int *arg, int ret)
{
	if (copyout((caddr_t)&ret, (caddr_t)arg, sizeof(ret)))
		cmn_err(CE_WARN, "snd_ioctl_out fault\n");
}

/*****************************************************************************/
int
sndread(dev_t dev, uio_t *uiop, cred_t *credp)
{
	int ret;
	dev_t minor_dev = geteminor(dev);

	ret = sound_read_sw(minor_dev, &files[minor_dev], uiop, uiop->uio_resid);
	FIX_RETURN(ret);
}

/*****************************************************************************/
int
sndwrite(dev_t dev, uio_t *uiop, cred_t *credp)
{
	int ret;
	dev_t minor_dev = geteminor(dev);

	ret = sound_write_sw(minor_dev, &files[minor_dev], uiop, uiop->uio_resid);
	FIX_RETURN(ret);
}

/*****************************************************************************/
int
sndopen(dev_t *devp, int flags, int otyp, cred_t *credp)
{
	int             retval;
	struct fileinfo tmp_file;

	dev_t minor_dev = geteminor (*devp);

	if (!soundcard_configured && minor_dev)
	{
		printk("SoundCard Error: The soundcard system has not been configured\n");
		return (ENODEV);
	}

	tmp_file.mode = 0;

	if (flags & FREAD && flags & FWRITE)
		tmp_file.mode = OPEN_READWRITE;
	else if (flags & FREAD)
		tmp_file.mode = OPEN_READ;
	else if (flags & FWRITE)
		tmp_file.mode = OPEN_WRITE;

	if ((retval = sound_open_sw(minor_dev, &tmp_file)) < 0)
		FIX_RETURN(retval);

	memcpy((char *)&files[minor_dev], (char *)&tmp_file, sizeof(tmp_file));

	return (0);
}

/*****************************************************************************/
int
sndclose(dev_t dev, int flags, int otyp, cred_t *credp)
{

	dev_t minor_dev = geteminor(dev);

	sound_release_sw(minor_dev, &files[minor_dev]);

	return (0);
}

/*****************************************************************************/
int
sndioctl(dev_t dev, int cmd, void *arg, int mode, cred_t *credp, int *rvalp)
{
	int ret;
	dev_t minor_dev = geteminor(dev);

	ret = sound_ioctl_sw(minor_dev, &files[minor_dev], cmd, (unsigned int) arg);

	if (ret < 0)
		return (-ret);
	else
	{
		*rvalp = ret;
		return (0);
	}
}

/*****************************************************************************/
void
sndinit()
{
	int	i;
	int	mem_start = 0xefffffff;

	/* 
	** This must match the declaration in the space.c file
	*/
	struct cfg_tab 
	{
		int unit, addr, irq, dma;
	};

	extern struct cfg_tab snd_cfg_tab[];

	/*
	** First read the config info from the Space.c
	*/

	i = 0;
	while (i < 20 && snd_cfg_tab[i].unit != -1)
	{
		snd_cfg_tab[i].dma == -1 ? 0 : snd_cfg_tab[i].dma;
		sound_chconf(snd_cfg_tab[i].unit, snd_cfg_tab[i].addr, 
			snd_cfg_tab[i].irq, 
			(snd_cfg_tab[i].dma == -1) ? 0 : snd_cfg_tab[i].dma);
		i++;
	}

	soundcard_configured = 1;

	/* 
	** Initialize call tables and detect cards 
	*/
	mem_start = sndtable_init (mem_start);	

	if ((i = sndtable_get_cardcount()) == 0)
		return ;		/* No cards detected */

#ifndef EXCLUDE_AUDIO
	if (num_audiodevs)		/* Audio devices present */
	{
		mem_start = DMAbuf_init (mem_start);
		mem_start = audio_init (mem_start);
 		sound_mem_init(); 
	}
#endif

#ifndef EXCLUDE_MIDI
	if (num_midis)
	{
		mem_start = MIDIbuf_init (mem_start);
	}
#endif 

#ifndef EXCLUDE_SEQUENCER
	if (num_midis + num_synths)
	{	
		timer_running = 0;
		mem_start = sequencer_init (mem_start);
	}
#endif 

	return ;
}

/*****************************************************************************/
#ifndef EXCLUDE_SEQUENCER
void
request_sound_timer(int count)
{
	static int      current = 0;
	int             tmp = count;

	if (count < 0)
	{
		timer_running = timeout(sequencer_timer, 0, -count);
	}
	else
	{

		if (count < current)
			current = 0;		/* Timer restarted */

		count = count - current;

		current = tmp;

		if (!count)
			count = 1;
		timer_running = timeout(sequencer_timer, 0, count);
	}
}
#endif 
/*****************************************************************************/
int
snd_sound_timeout(caddr_t  arg)
{
	unsigned long flags;

	DISABLE_INTR(flags);
	if (*arg & WK_SLEEP) 
		wakeup(arg);
	*arg = WK_TIMEOUT;
	RESTORE_INTR(flags);
}

/*****************************************************************************/
void
sound_stop_timer (void)
{
	if (timer_running)
		untimeout(timer_running);
	timer_running = 0;
}


/*****************************************************************************/
#ifndef EXCLUDE_AUDIO

static void
sound_mem_init (void)
{
	int             i, dev;
	unsigned long   dma_pagesize;
	static unsigned long dsp_init_mask = 0;

	for (dev = 0; dev < num_audiodevs; dev++)	/* Enumerate devices */
		if (!(dsp_init_mask & (1 << dev)))	/* Not already done */
			if (audio_devs[dev]->buffcount > 0 && audio_devs[dev]->dmachan > 0)
			{
				dsp_init_mask |= (1 << dev);

				if (!(audio_devs[dev]->flags & DMA_AUTOMODE))
					if (audio_devs[dev]->buffcount == 1)
					{
						audio_devs[dev]->buffcount = 2;
						audio_devs[dev]->buffsize /= 2;
					}

				/* Larger not possible yet */
				if (audio_devs[dev]->buffsize > 65536)	
					audio_devs[dev]->buffsize = 65536;

				if (audio_devs[dev]->dmachan > 3 
						&& audio_devs[dev]->buffsize > 65536)
					dma_pagesize = 131072;	/* 128k */
				else
					dma_pagesize = 65536;

				/* More sanity checks */

				if (audio_devs[dev]->buffsize > dma_pagesize)
					audio_devs[dev]->buffsize = dma_pagesize;
	
				/* Portably truncate to n*4k */
				audio_devs[dev]->buffsize &= ~0x0fff;	

				if (audio_devs[dev]->buffsize < 4096)
					audio_devs[dev]->buffsize = 4096;

				/* Allocate the buffers now. Not later at open time */

				for (audio_devs[dev]->dmap->raw_count = 0; 
				    audio_devs[dev]->dmap->raw_count < audio_devs[dev]->buffcount;
				    audio_devs[dev]->dmap->raw_count++)
				{
					/*
			  		** The DMA buffer allocation algorithm hogs memory. 
					** We allocate a memory area which is two times the 
					** required size. This guarantees that it contains 
					** at least one valid DMA buffer.
				   	** This really needs some kind of finetuning.
				   	*/
					char *tmpbuf;
					unsigned long   addr, rounded, start, end;

					tmpbuf = (char *)snd_alloc(2 * audio_devs[dev]->buffsize, KM_NOSLEEP);

					if (tmpbuf == NULL)
					{
						printk ("snd: Unable to allocate %d bytes of buffer\n",
						    2 * audio_devs[dev]->buffsize);
						return;
					}

					addr = kvtophys (tmpbuf);
					/*
					** Align the start address if required
					*/
					start = (addr & ~(dma_pagesize - 1));
					end = ((addr + audio_devs[dev]->buffsize) & ~(dma_pagesize - 1));

					if (start != end)
						rounded = end;
					else
						rounded = addr;	/* Fits to the same DMA page */

					audio_devs[dev]->dmap->raw_buf[audio_devs[dev]->dmap->raw_count] =
					    &tmpbuf[rounded - addr];	/* Compute offset */
					/*
					** Keep a copy of the virtual address in the phys addr field
					** This is dirty programing. The dmabuf.c code ought to 
					** be rewritten to only use virtual addresses, leaving the
					** virtual to physical translation to the DMA handling
					** functions.
					*/
					audio_devs[dev]->dmap->raw_buf_phys[audio_devs[dev]->dmap->raw_count] =
					    (unsigned long) audio_devs[dev]->dmap->raw_buf[audio_devs[dev]->dmap->raw_count];
				}
			}			/* for dev */
}
#endif

/*****************************************************************************/

irq_entry snd_irq_tab[16] = 
{
	NULL
};

int
snd_set_irq_handler (int vect, INT_HANDLER_PROTO(), char *name)
{
	if (snd_irq_tab[vect])
		printk("Sound Warning: IRQ#%d was already in use.\n", vect);

	snd_irq_tab[vect] = hndlr;
	return 1;
}

/*****************************************************************************/
void
snd_release_irq(int vect)
{
	snd_irq_tab[vect] = (irq_entry)NULL;
}

/*****************************************************************************/
void
sndintr(int ivect)
{
	if (ivect < 0 || ivect > 15)
		return ;
	
	if (!snd_irq_tab[ivect])
		return ;
	
	snd_irq_tab[ivect](ivect);	/* Call the actual handler */
	return ;
}
/*****************************************************************************/
#endif /* CONFIGURE_SOUNDCARD */
