/* 
 *   Creation Date: <2000/05/28 02:37:51 samuel>
 *   Time-stamp: <2004/01/11 21:41:37 samuel>
 *   
 *	<osi_sound.c>
 *	
 *	Sound Driver
 *   
 *   Copyright (C) 2000-2004 Samuel Rydh (samuel@ibrium.se)
 *   
 *   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
 *   
 */

#define ALSA_PCM_NEW_HW_PARAMS_API
/* #define ALSA_PCM_NEW_SW_PARAMS_API */

#include "mol_config.h"
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <sys/poll.h>
#include <sys/resource.h>
#include <sys/param.h>
#ifdef CONFIG_ALSA
#include <alsa/asoundlib.h>
#include <alsa/mixer.h>
#include <alsa/error.h>
#endif
#include "driver_mgr.h"
#include "debugger.h"
#include "os_interface.h"
#include "verbose.h"
#include "memory.h"
#include "thread.h"
#include "res_manager.h"
#include "osi_driver.h"
#include "pic.h"
#include "async.h"
#include "booter.h"
#include "pci.h"
#include "timer.h"
#include "mac_registers.h"
#include "sound_sh.h"


typedef struct {
	void	(*cleanup)( void );

	int	(*query_mode)( int format, int rate, int *frasize );
	int	(*open)( int format, int rate, int *fragsize );
	void	(*close)( void );

	void	(*write)( char *fragptr, int size );
	void	(*flush)( void );
	void	(*volume)( void );
} sound_ops_t;

static struct {
#ifdef CONFIG_OSS
	struct { 
		int 		fd; 
	} oss;
#endif
	struct {
		sound_ops_t 	*old_ops;
		int		usecs_in_pipe;		/* usecs in pipe (from mark) */
		uint		tb_mark;
	} nosound;

#ifdef CONFIG_ALSA
	struct {
		snd_pcm_t	*pcm;
		snd_mixer_t	*mixer;
		snd_mixer_elem_t *pcm_elem;
	} alsa;
#endif

	pthread_mutex_t lock;
	int		irq;
	int		started;

	/* misc */
	int		save_priority;	/* old nice level of main thread */
	int		rate_limit;	/* user imposed rate restriction */
	volatile int	thread_running;	/* 2=restarting, 1=running, 0=dead, -1=exiting */

	/* volume */
	int		mute;
	int		vol_updated;
	int		lvol, rvol;	/* PCM volume (0-255) */

	/* current mode */
	int		format;
	uint		rate;		/* in Hz */
	uint		bpf;		/* bytes per frame */

	/* sound parameters */
	int		fragsize;	/* client fragsize */

	/* output driver */
	sound_ops_t	*ops;

	/* dubble buffering */
	int		dbufsize;	/* dubbel buffer size */
	char		*dbuf_src;	/* next input buffer */
	int		dbuf_srcsize;	/* size of input buffer */
	volatile int	dbuf_go;	/* generate irqs */

	/* ring buffer */
	char		*ringbuf;	/* ringbuf (if non-NULL) */
	int		ringfrags;	/* #frags in ringbuf */
	timestamp_t	timestamp;
	volatile uint	ringind;	/* last frag passed to hardware */

	/* misc */
	char		*startboingbuf;	/* too be freed */

	/* debugging */
	/* int		debug_tbl; */
} ss;

#define oss		ss.oss
#define alsa		ss.alsa
#define nosound		ss.nosound

#define LOCK		pthread_mutex_lock( &ss.lock )
#define UNLOCK		pthread_mutex_unlock( &ss.lock )


/************************************************************************/
/*	OSS support							*/
/************************************************************************/

#ifdef CONFIG_OSS
static int
oss_convert_format( int format, int *fmt, int *channels )
{
	static struct {
		int mol_fmt, linux_fmt;
	} fmt_table[] = {
		{ kSoundFormat_U8,	AFMT_U8,	},
		{ kSoundFormat_S16_BE,	AFMT_S16_BE,	},
		{ kSoundFormat_S16_LE,	AFMT_S16_LE,	},
	};
	int f, i;	

	for( f=-1, i=0; i<sizeof(fmt_table)/sizeof(fmt_table[0]); i++ )
		if( format & fmt_table[i].mol_fmt )
			f = fmt_table[i].linux_fmt;
	if( f ==  -1 )
		return -1;

	*fmt = f;
	*channels = (format & kSoundFormat_Stereo)? 2:1;
	return 0;
}

static void
oss_close( void )
{
	close( oss.fd );
}

static const char *
oss_devname( void ) 
{
	char *s;
	if( !(s=get_filename_res("oss.device")) )
		s = "/dev/dsp";
	return s;
}

static int
oss_open( int format, int rate, int *fragsize )
{
	int nbuf, size, err, fmt, channels, rate_, fmt_, channels_;
	uint frag;
	
	/* printm("OSS Open: %d [%d]\n", rate, format ); */
	
	nbuf = get_numeric_res("oss.nbufs");
	size = get_numeric_res("oss.fragsize");

	if( nbuf < 2 || nbuf > 16 )
		nbuf = 4;
	if( size <= 0 || size > 8192 )
		size = is_classic_boot() ? 4096 : 256;
	if( size > 0 )
		frag = (nbuf << 16) | ilog2(size);

	/* must set fragsize before returning 1 (temporary unavailable) */
	*fragsize = size;

	if( oss_convert_format(format, &fmt, &channels) )
		return -1;
	if( (oss.fd=open(oss_devname(), O_WRONLY | O_NONBLOCK)) < 0 )
		return 1;

	err=0;
	fmt_ = fmt; channels_ = channels; rate_ = rate;
	if( ioctl(oss.fd, SNDCTL_DSP_SETFMT, &fmt) < 0 || fmt_ != fmt )
		err--;
	if( ioctl(oss.fd, SNDCTL_DSP_CHANNELS, &channels) < 0 || channels_ != channels )
		err--;
	if( ioctl(oss.fd, SNDCTL_DSP_SPEED, &rate) < 0 || rate_ != rate )
		err--;
	ioctl( oss.fd, SNDCTL_DSP_SETFRAGMENT, &frag );
	ioctl( oss.fd, SNDCTL_DSP_GETBLKSIZE, &size );
#if 0
	if( (1 << (frag & 0xffff)) != size )
		printm("oss: fragsize %d not accepted (using %d)\n", 1<<(frag & 0x1f), size );
#endif
	if( size <= 32 )
		size = 4096;

	*fragsize = size;
	if( err )
		close( oss.fd );
	return err;
}

/* 0: ok, 1: might be supported, <0: unsupported/error */
static int
oss_query_mode( int format, int rate, int *fragsize )
{
	int ret;

	if( (ret=oss_open(format, rate, fragsize)) )
		return ret;
	oss_close();
	return 0;
}

static void
oss_flush( void )
{
#if 0
	/* we are closing the device, this should not be necessary */
	if( ioctl(oss.fd, SNDCTL_DSP_SYNC) < 0 )
		perrorm("SNDCTL_DSP_SYNC\n");
#endif
}

/* size should _always_ equal ss.fragsize */
static void
oss_write( char *buf, int size )
{
	struct pollfd ufd;
	int s;

	while( size ) {
		ufd.fd = oss.fd;
		ufd.events = POLLOUT;

		poll( &ufd, 1, -1 );
		if( (s=write(oss.fd, buf, size)) < 0 )
			break;
		size -= s;
		buf += s;
	}
}

static void
oss_volume( void )
{
	int fd, vol, lvol, rvol;
	
	/* we can't use oss.fd here; it might be invalid */

	lvol = ss.lvol * 100 / 0xff;
	rvol = ss.rvol * 100 / 0xff;
	vol = rvol*0x100 | lvol;

	if( (fd=open(oss_devname(), O_RDONLY | O_NONBLOCK)) < 0 ) {
		printm("failed to set volume\n");
		return;
	}
	ioctl( fd, SOUND_MIXER_WRITE_VOLUME, &vol );
	close( fd );
}


static sound_ops_t oss_driver_ops = {
	.cleanup	= NULL,
	.query_mode	= oss_query_mode,
	.open		= oss_open,
	.close 		= oss_close,
	.write		= oss_write,
	.flush		= oss_flush,
	.volume		= oss_volume
};

static sound_ops_t *
oss_probe( int dummy_exact )
{
	int fd = open( oss_devname(), O_WRONLY | O_NONBLOCK );
	if( fd >= 0 )
		close( fd );
	if( fd < 0 ) {
		if( errno != EBUSY )
			return NULL;
		else
			printm("OSS sound device unavailable (will be used anyway)\n");
	}
	printm("OSS sound driver\n");
	return &oss_driver_ops;
}
#else
static inline sound_ops_t *
oss_probe( int exact )
{
	return NULL;
}
#endif /* CONFIG_OSS */

/************************************************************************/
/*	ALSA support							*/
/************************************************************************/

#ifdef CONFIG_ALSA

static int
alsa_fragsize( void ) 
{
	int s, size = is_classic_boot()? 2048 : 512;

	s = get_numeric_res("alsa.fragsize");
	if( s > 128 && s <= 32768 )
		size = s;
	return size;
}

static int
alsa_query_mode( int format, int rate, int *fragsize )
{
	if( rate != 44100 && rate != 22050 && rate != 48000 )
		return -1;
	if( !(format & (kSoundFormat_S16_BE | kSoundFormat_U8)) )
		return -1;
	*fragsize = alsa_fragsize();
	return 0;
}

static void
alsa_prefill( void )
{
	snd_pcm_status_t *status;
	char buf[512];
	int frames, cnt;

	snd_pcm_status_malloc( &status );
	snd_pcm_status( alsa.pcm, status );
	frames = snd_pcm_status_get_avail(status);
	snd_pcm_status_free( status );

	memset( buf, 0, sizeof(buf) );
	cnt = sizeof(buf)/ss.bpf;
	for( ; frames > 0; frames -= cnt  ) {
		if( cnt > frames )
			cnt = frames;
		snd_pcm_writei( alsa.pcm, buf, sizeof(buf)/ss.bpf );
	}
}

static const char *
alsa_devname( int for_mixer )
{
	const char *name = NULL;
	if( for_mixer )
		name = get_str_res("alsa.mixerdevice");
	if( !name )
		name = get_str_res("alsa.device");
	if( !name )
		name = "default";
	return name;
}

static int
alsa_open( int format, int rate, int *fragsize )
{
	const char *name = alsa_devname(0);
	snd_pcm_hw_params_t *params;
	int alsaformat;
	
	/* printm("ALSA open (%s) %d \n", name, rate ); */
	if( snd_pcm_open(&alsa.pcm, name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK ) < 0 )
		return -1;
	snd_pcm_close( alsa.pcm );
	if( snd_pcm_open(&alsa.pcm, name, SND_PCM_STREAM_PLAYBACK, 0) < 0 )
		return -1;

	if( snd_pcm_hw_params_malloc(&params) < 0 ) {
		snd_pcm_close( alsa.pcm );
		return -1;
	}

	alsaformat = SND_PCM_FORMAT_S16_BE;
	if( format & kSoundFormat_U8 )
		alsaformat = SND_PCM_FORMAT_U8;
	
	snd_pcm_hw_params_any( alsa.pcm, params );
	snd_pcm_hw_params_set_access( alsa.pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED );
	snd_pcm_hw_params_set_format( alsa.pcm, params, alsaformat );
	snd_pcm_hw_params_set_rate_near( alsa.pcm, params, &rate, 0 );
	snd_pcm_hw_params_set_channels( alsa.pcm, params, (format & kSoundFormat_Stereo)? 2:1 );
	snd_pcm_hw_params( alsa.pcm, params );

	snd_pcm_hw_params_free( params );
	snd_pcm_prepare( alsa.pcm );

	if( ss.ringbuf )
		alsa_prefill();

	*fragsize = alsa_fragsize();
	return 0;
}

static void
alsa_close( void )
{
	snd_pcm_drain( alsa.pcm );
	snd_pcm_close( alsa.pcm );
	alsa.pcm = NULL;
}

static void
alsa_write( char *buf, int size ) 
{
	struct pollfd pfd;

	snd_pcm_poll_descriptors( alsa.pcm, &pfd, 1 );
	poll( &pfd, 1, -1 );

	snd_pcm_writei( alsa.pcm, buf, size/ss.bpf );
}

static void
alsa_flush( void ) 
{
}

static void
alsa_volume( void )
{
	if( alsa.pcm_elem ) {
		snd_mixer_selem_set_playback_volume(alsa.pcm_elem,
						    SND_MIXER_SCHN_FRONT_LEFT, ss.lvol);
		snd_mixer_selem_set_playback_volume(alsa.pcm_elem,
						    SND_MIXER_SCHN_FRONT_RIGHT, ss.rvol);
	}
}

static void
alsa_cleanup( void )
{
	if( alsa.mixer ) {
		snd_mixer_close( alsa.mixer );
		alsa.mixer = NULL;
	}
}

static sound_ops_t alsa_driver_ops = {
	.cleanup	= alsa_cleanup,
	.query_mode	= alsa_query_mode,
	.open		= alsa_open,
	.close 		= alsa_close,
	.write		= alsa_write,
	.flush		= alsa_flush,
	.volume		= alsa_volume,
};

static sound_ops_t *
alsa_probe( int exact )
{
	snd_pcm_t *pcm;
	const char *name = alsa_devname(0);
	
	if( !snd_mixer_open(&alsa.mixer, 0) ) {
		snd_mixer_selem_id_t *selem_id;
		const char *s, *mixername = get_str_res("alsa.mixer");
		const char *devname = alsa_devname(1);
		int mixerindex=0;
		
		if( (s=get_str_res_ind("alsa.mixer", 0, 1)) )
			mixerindex = atoi(s);
		if( mixername )
			printm("Using ALSA mixer %s %d (mixerdevice '%s')\n",
			       mixername, mixerindex, devname );
		if( !mixername )
			mixername = "PCM";

		snd_mixer_selem_id_alloca( &selem_id );
		snd_mixer_selem_id_set_index( selem_id, mixerindex );
		snd_mixer_selem_id_set_name( selem_id, mixername );

		int err = snd_mixer_attach( alsa.mixer, devname )
			|| snd_mixer_selem_register( alsa.mixer, NULL, NULL )
			|| snd_mixer_load( alsa.mixer );
		if( !err )
			alsa.pcm_elem=snd_mixer_find_selem( alsa.mixer, selem_id );
		if( alsa.pcm_elem )
			snd_mixer_selem_set_playback_volume_range( alsa.pcm_elem, 0, 0xff );
	}
	if( !snd_pcm_open(&pcm, name, SND_PCM_STREAM_PLAYBACK, 1 * SND_PCM_NONBLOCK) )
		snd_pcm_close( pcm );
	else if( !exact )
		return NULL;
	else
		printm("ALSA device unavailable (will be used anyway)\n");

	printm("ALSA sound driver (device '%s')\n", name);
	if( !alsa.pcm_elem )
		printm("ALSA: failed to setup mixer\n");
	return &alsa_driver_ops;
}
#else
static inline sound_ops_t *
alsa_probe( int exact )
{
	return NULL;
}
#endif /* CONFIG_ALSA */


/************************************************************************/
/*	no-sound driver support						*/
/************************************************************************/

static int	nosound_open( int format, int rate, int *fragsize );
static void	nosound_close( void );

static int
nosound_query_mode( int format, int rate, int *fragsize )
{
	if( (rate != 44100 && rate != 22050) || 
	    !(format & (kSoundFormat_S16_BE | kSoundFormat_U8)) )
		return -1;
	*fragsize = ss.fragsize ? ss.fragsize : 4096;
	return 0;
}

static void
nosound_write( char *dummybuf, int size )
{
	int usecs_res;
	uint mark;
	
	if( !size )
		return;

	nosound.usecs_in_pipe += 1000000/ss.rate * size/ss.bpf;
	for( ;; ) {
		mark = get_tbl();
		usecs_res = nosound.usecs_in_pipe - mticks_to_usecs(mark - nosound.tb_mark);
		if( usecs_res <= 0 )
			break;
		usleep(1);
	}
	nosound.usecs_in_pipe = usecs_res;
	nosound.tb_mark = mark;
}

static sound_ops_t nosound_driver_ops = {
	.cleanup	= NULL,
	.query_mode	= nosound_query_mode,
	.open		= nosound_open,
	.close 		= nosound_close,
	.write		= nosound_write,
	.volume		= NULL,
};

static int
nosound_open( int format, int rate, int *fragsize )
{	
	if( nosound_query_mode(format, rate, fragsize) )
		return -1;

	nosound.old_ops = ss.ops;
	ss.ops = &nosound_driver_ops;

	nosound.tb_mark = get_tbl();
	nosound.usecs_in_pipe = 0;
	return 0;
}

static void
nosound_close( void ) 
{
	ss.ops = nosound.old_ops;
}

/************************************************************************/
/*	Sound Engine							*/
/************************************************************************/

static void
ring_engine( void )
{
	char *p;
	
	while( ss.thread_running > 0 ) {
		p = ss.ringbuf + ss.ringind * ss.fragsize;

		if( ss.vol_updated && ss.ops->volume ) {
			ss.vol_updated = 0;
			(*ss.ops->volume)();
		}
		UNLOCK;
		(*ss.ops->write)( p, ss.fragsize );
		LOCK;

		/* handle quick start-stop-start cycles */
		if( ss.thread_running > 1 ) {
			ss.thread_running = 1;
			continue;
		} else if( ss.thread_running <= 0 )
			break;

		if( ++ss.ringind == ss.ringfrags ) {
			ss.ringind = 0;
			get_timestamp( &ss.timestamp );
			irq_line_hi( ss.irq );
		} else {
			abort_doze();
		}
	}
}


static void
dbuf_engine( void )
{
	char *dbuf = malloc( ss.dbufsize );
	int dbuf_cnt = 0;

	for( ;; ) {
		/* dubbel buffering */
		if( dbuf_cnt ) {
			if( ss.vol_updated && ss.ops->volume ) {
				ss.vol_updated = 0;
				(*ss.ops->volume)();
			}
			UNLOCK;
			(*ss.ops->write)( dbuf, dbuf_cnt );
			dbuf_cnt = 0;
			LOCK;
		}

		/* switch dubbelbuffer */
		if( !ss.dbuf_srcsize ) {
			if( ss.thread_running < 0 )
				break;
			/* buffer underrun, drop frame */
			/* printm("sndframe dropped\n"); */
			memset( dbuf, 0, ss.dbufsize );
			dbuf_cnt = ss.dbufsize;
		} else {
			int size = MIN(ss.dbuf_srcsize, ss.dbufsize);
			memcpy( dbuf, ss.dbuf_src, size );
			dbuf_cnt = size;
			ss.dbuf_src += size;
			ss.dbuf_srcsize -= size;
		}
	
		if( ss.dbuf_go ) {
			ss.dbuf_go = 0;
			irq_line_hi( ss.irq );
		}
	}
	free( dbuf );
}


static void
audio_thread( void *dummy )
{
	setpriority( PRIO_PROCESS, getpid(), -20 );

	LOCK;
	if( ss.ringbuf )
		ring_engine();
	else
		dbuf_engine();

	if( ss.ops->flush )
		(*ss.ops->flush)();

	(*ss.ops->close)();
	ss.thread_running = 0;

	if( ss.startboingbuf ) {
		free( ss.startboingbuf );
		ss.startboingbuf = NULL;
	}
	UNLOCK;
	setpriority( PRIO_PROCESS, getpid(), 0 );
}


/************************************************************************/
/*	generic routines						*/
/************************************************************************/

static int
wait_sound_stopped( void )
{
	if( ss.started )
		return -1;

	/* wait untill thread has exited */
	while( ss.thread_running < 0 )
		sched_yield();
	return 0;
}

static int
query_mode( int format, int rate )
{
	int dummy_fragsize;
	
	if( ss.started )
		return 1;
	if( ss.rate_limit && rate > ss.rate_limit )
		return -1;
	return (*ss.ops->query_mode)( format, rate, &dummy_fragsize );
}

static int
set_mode( int format, int rate )
{
	int ret, size, fragsize, bufsize;
	static int lastformat=-2, lastrate;

	/* optimize away the evil wait_sound_stopped() */
	if( lastformat == format && lastrate == rate )
		return 0;
	
	if( wait_sound_stopped() )
		return 1;
	if( (ret=(*ss.ops->query_mode)(format, rate, &fragsize)) < 0 )
		return ret;

	lastformat = format;
	lastrate = rate;
	
	ss.rate = rate;
	ss.bpf = (format & kSoundFormat_Len1Mask) ? 1 : 2;
	ss.bpf *= (format & kSoundFormat_Stereo) ? 2 : 1;
	ss.fragsize = fragsize;
	ss.format = format;

	/* set dubbelbuffer size (must be a fragsize multiple) */
	if( (size=get_numeric_res("sound.bufsize")) == -1 )
		size = fragsize * 2;
	for( bufsize=fragsize*2; bufsize < size ; bufsize += fragsize )
		;
	ss.dbufsize = bufsize;

	/* printm("SoundMode: %x, %d Hz, %d/%d\n", format, rate, fragsize, bufsize ); */
	return 0;
}

static void
stop_sound( void )
{
	if( !ss.started )
		return;
	ss.started = 0;

	LOCK;
	setpriority( PRIO_PROCESS, getpid(), ss.save_priority );

	ss.thread_running = -1;
	irq_line_low( ss.irq );
	UNLOCK;

	/* sound is stopped asynchronously */
}

static int
start_sound( void ) 
{
	pid_t pid = getpid();

	if( ss.started || !ss.format )
		return -1;
	ss.started = 1;

	/* printm("- start -\n"); ss.debug_tbl = get_tbl(); */

	LOCK;
	ss.ringind = 0;
	ss.dbuf_go = 0;

	ss.save_priority = getpriority( PRIO_PROCESS, pid );
	setpriority( PRIO_PROCESS, pid, -12 );

	if( ss.thread_running ) {
		/* jump start already running thread */
		ss.thread_running = 2;
	} else {
		if( (*ss.ops->open)(ss.format, ss.rate, &ss.fragsize) ) {
			printm("audiodevice unavailable\n");
			nosound_open( ss.format, ss.rate, &ss.fragsize );
		}
		ss.thread_running = 1;
		create_thread( (void*)audio_thread, NULL, "audio-thread" );
	}

	UNLOCK;
	return 0;
}

static void
resume_sound( void )
{
	if( ss.started )
		ss.dbuf_go = 1;
}

static void
pause_sound( void )
{
	if( ss.started )
		ss.dbuf_go = 0;
}


/************************************************************************/
/*	OSI interface							*/
/************************************************************************/

static int
osip_sound_irq_ack( int sel, int *params )
{
	mregs->gpr[4] = ss.timestamp.hi;
	mregs->gpr[5] = ss.timestamp.lo;
	return irq_line_low( ss.irq );
}

static int
osip_sound_cntl( int sel, int *params )
{
	switch( params[0] ) {
	/* ---- Version Checking (new API) ---- */
	case kSoundCheckAPI:
		if( params[1] != OSI_SOUND_API_VERSION ) {
			printm("Incompatible MOLAudio version %d!\n", params[1] );
			return -1;
		}
		break;
	case kSoundOSXDriverVersion:
		set_driver_ok_flag( kOSXAudioDriverOK, params[1] == OSX_SOUND_DRIVER_VERSION );
		break;

	/* ---- New API (OSX) ---- */
	case kSoundSetupRing: /* bugfize, buf */
		ss.ringfrags = params[1] / ss.fragsize;
		ss.ringbuf = params[2] ? transl_mphys(params[2]) : NULL;
		break;
	case kSoundGetRingSample: 
		return ss.ringind * ss.fragsize/ss.bpf;

	case kSoundGetSampleOffset:
		/* no-touch zone, in frames */
		return 0;
	case kSoundGetSampleLatency:
		/* internal buffering, in frames */
		return 2048;

	/* ---- Both new and old API ---- */
	case kSoundQueryFormat: /* format, rate */
		return query_mode( params[1], params[2] );
	case kSoundSetFormat: /* format, rate */
		return set_mode( params[1], params[2] );
	case kSoundStart:
		return start_sound();
	case kSoundStop:
		stop_sound();
		break;
	case kSoundFlush:
		/* no-op */
		break;

	/* ---- Old API (classic) ---- */
	case kSoundClassicDriverVersion:
		if( params[1] < CLASSIC_SOUND_DRIVER_VERSION ) {
			printm("Install the latest MOLAudio extension!\n");
			return -1;
		}
		break;
	case kSoundResume:
		resume_sound();
		break;
	case kSoundPause:
		pause_sound();
		break;
	case kSoundGetBufsize: /* return dubbelbuf size */
		/* printm("kSoundGetFragsize: %d (hw %d)\n", ss.dbufsize, ss.fragsize ); */
		return ss.dbufsize;

	case kSoundRouteIRQ:
		oldworld_route_irq( params[1], &ss.irq, "sound" );
		break;
	default:
		return -1;
	}
	return 0;
}

/* phys_buffer, len, restart */
static int
osip_sound_write( int sel, int *params )
{
	char *buf = transl_mphys( params[0] );
	int len = params[1];
	
	if( !params[0] || !buf )
		return -1;

	if( ss.dbuf_srcsize )
		printm("warning: ss.dbuf_srcsize != 0\n");

	ss.dbuf_src = buf;
	ss.dbuf_srcsize = len;
	ss.dbuf_go = 1;

	/* printm("WRITE %ld (%d bytes)\n", get_tbl() - ss.debug_tbl, len ); ss.debug_tbl = get_tbl(); */
	
	return 0;
}

/* hwVolume, speakerVolume, hwMute */
static int
osip_volume( int sel, int *params )
{
	uint lvol = (params[0] & 0xffff0000) >> 16;
	uint rvol = params[0] & 0xffff;

	/* speaker volume is ignored for now */
	if( lvol > 255 )
		lvol = 255;
	if( rvol > 255 )
		rvol = 255;

	LOCK;
	ss.lvol = lvol;
	ss.rvol = rvol;
	ss.mute = params[2] ? 1:0;
	ss.vol_updated = 1;
	UNLOCK;
	return 0;
}


static void
startboing( void )
{
	char *name = get_filename_res("startboing.file");
	int len, fd;
	char *buf;
	
	if( !name )
		return;
	if( (fd=open(name, O_RDONLY)) < 0 ) {
		printm("Could not open '%s'", name);
		return;
	}
	len = lseek( fd, 0, SEEK_END );
	lseek( fd, 0, SEEK_SET );

	if( (buf=malloc(len)) ) {
		read( fd, buf, len );
		ss.dbuf_src = buf;
		ss.dbuf_srcsize = len;
		ss.dbuf_go = 1;
		ss.startboingbuf = buf;
	}
	close( fd );

	set_mode( kSoundFormat_S16_BE | kSoundFormat_Stereo, 22050 );
	start_sound();
	stop_sound();
}


/************************************************************************/
/*	init / cleanup							*/
/************************************************************************/

static osi_iface_t osi_iface[] = {
	{ OSI_SOUND_CNTL,	osip_sound_cntl		},
	{ OSI_SOUND_IRQ_ACK,	osip_sound_irq_ack	},
	{ OSI_SOUND_WRITE,	osip_sound_write	},
	{ OSI_SOUND_SET_VOLUME,	osip_volume 		},
};

static int 
osi_sound_init( void )
{
	static pci_dev_info_t pci_config = { 0x6666, 0x6668, 0x02, 0x0, 0x0000 };
	char *drv = is_oldworld_boot() ? "sound" : NULL;
	int i, limit;
	
	memset( &ss, 0, sizeof(ss) );

	if( !register_osi_driver(drv, "mol-audio", is_classic_boot()? &pci_config : NULL, &ss.irq) ) {
		printm("Failed to register sound card\n");
		return 0;
	}
	pthread_mutex_init( &ss.lock, NULL );

	/* handle user rate limits */
	if( (limit=get_numeric_res("sound.maxrate")) > 0 )
		ss.rate_limit = limit;

	/* select output driver */
	for( i=0 ;; i++ ) {
		char *s = get_str_res_ind("sound.driver", 0, i );
		if( i && !s )
			break;
		if( s && !strcasecmp("any", s) )
			s = NULL;
		if( (!s || !strcasecmp("alsa", s)) && (ss.ops=alsa_probe(s?1:0)) )
			break;
		if( (!s || !strcasecmp("oss", s) || !strcasecmp("dsp", s)) && (ss.ops=oss_probe(s?1:0)) )
			break;
		if( !s || !strcasecmp("nosound", s) ) {
			ss.ops = &nosound_driver_ops;
			break;
		}
	}
	if( !ss.ops )
		ss.ops = &nosound_driver_ops;
	else {
		if( get_bool_res("startboing.enabled") == 1 )
			startboing();
	}

	/* the osi procs must be registered even if sound is unavailable */
	register_osi_iface( osi_iface, sizeof(osi_iface) );
	return 1;
}

static void
osi_sound_cleanup( void )
{
	stop_sound();
	wait_sound_stopped();
	if( ss.ops->cleanup )
		(*ss.ops->cleanup)();
}

driver_interface_t osi_sound_driver = {
    "osi_sound", osi_sound_init, osi_sound_cleanup
};
