/*	MikMod sound library
	(c) 1998, 1999, 2000 Miodrag Vallat and others - see file AUTHORS for
	complete list.

	This library is free software; you can redistribute it and/or modify
	it under the terms of the GNU Library 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 Library General Public License for more details.

	You should have received a copy of the GNU Library General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
	02111-1307, USA.
*/

/*==============================================================================

  $Id$

  Driver for output on Linux and FreeBSD Open Sound System (OSS) (/dev/dsp)

==============================================================================*/

/*

	Written by Chris Conn <cconn@tohs.abacom.com>

	Extended by Miodrag Vallat:
	- compatible with all OSS/Voxware versions on Linux/i386, at least
	- support for uLaw output (for sparc systems)

*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "mikmod_internals.h"

#ifdef DRV_OSS

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <errno.h>
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#include <string.h>
#include <stdlib.h>
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#ifdef HAVE_SYS_SOUNDCARD_H
#include <sys/soundcard.h> /* Linux and newer BSD versions - OSS standart */
#elif defined(HAVE_MACHINE_SOUNDCARD_H)
#include <machine/soundcard.h> /*  Some old BSD versions */
#elif defined(HAVE_SOUNDCARD_H)
#include <soundcard.h> /* Some old BSD versions and also newer OpenBSD versions */
#endif

/* Compatibility with old versions of OSS
   (Voxware <= 2.03, Linux kernel < 1.1.31) */
#ifndef AFMT_S16_LE
#define AFMT_S16_LE 16
#endif
#ifndef AFMT_S16_BE
#define AFMT_S16_BE 0x20
#endif
#ifndef AFMT_U8
#define AFMT_U8 8
#endif
#ifndef SNDCTL_DSP_SETFMT
#define SNDCTL_DSP_SETFMT SNDCTL_DSP_SAMPLESIZE
#endif

/* Compatibility with not-so-old versions of OSS
   (OSS < 3.7, Linux kernel < 2.1.16) */
#ifndef AFMT_S16_NE
#if defined(WORDS_BIGENDIAN)
#define AFMT_S16_NE AFMT_S16_BE
#else
#define AFMT_S16_NE AFMT_S16_LE
#endif
#endif

/* Compatibility with OSS 4.x: AFMT_FLOAT is documented
   as "not recommended" on the OSS website.  This post
   on the OSS mailing lists says: "In general AFMT_FLOAT
   is not supported by OSS except in some special cases."
   (http://sf.net/p/opensound/mailman/message/28840674/) */
#ifndef AFMT_FLOAT
#define AFMT_FLOAT 0x00004000
#endif

static	int sndfd=-1;
static	SBYTE *audiobuffer=NULL;
static	int buffersize;
static	int play_precision;

#define DEFAULT_CARD 0
static	int card=DEFAULT_CARD;

#ifdef SNDCTL_DSP_SETFRAGMENT

#define DEFAULT_FRAGSIZE 14
#define DEFAULT_NUMFRAGS 16

static	int fragsize=DEFAULT_FRAGSIZE;
static	int numfrags=DEFAULT_NUMFRAGS;

#endif

static void OSS_CommandLine(const CHAR *cmdline)
{
	CHAR *ptr;

#ifdef SNDCTL_DSP_SETFRAGMENT
	if((ptr=MD_GetAtom("buffer",cmdline,0)) != NULL) {
		fragsize=atoi(ptr);
		if((fragsize<7)||(fragsize>17)) fragsize=DEFAULT_FRAGSIZE;
		MikMod_free(ptr);
	}
	if((ptr=MD_GetAtom("count",cmdline,0)) != NULL) {
		numfrags=atoi(ptr);
		if((numfrags<2)||(numfrags>255)) numfrags=DEFAULT_NUMFRAGS;
		MikMod_free(ptr);
	}
#endif
	if((ptr=MD_GetAtom("card",cmdline,0)) != NULL) {
		card = atoi(ptr);
		if((card<0)||(card>99)) card=DEFAULT_CARD;
		MikMod_free(ptr);
	}
}

static char *OSS_GetDeviceName(void)
{
	static char sounddevice[20];

	/* First test for devfs enabled Linux sound devices */
	if (card)
		sprintf(sounddevice,"/dev/sound/dsp%d",card);
	else
		strcpy(sounddevice,"/dev/sound/dsp");
	if(!access(sounddevice,F_OK))
		return sounddevice;

	sprintf(sounddevice,"/dev/dsp%d",card);
	if (!card) {
		/* prefer /dev/dsp0 over /dev/dsp, as /dev/dsp might be a symbolic link
		   to something else than /dev/dsp0. Revert to /dev/dsp if /dev/dsp0
		   does not exist. */
		if(access("/dev/dsp0",F_OK))
			strcpy(sounddevice,"/dev/dsp");
	}

	return sounddevice;
}

static BOOL OSS_IsThere(void)
{
	/* under Linux, and perhaps other Unixes, access()ing the device is not
	   enough since it won't fail if the machine doesn't have sound support
	   in the kernel or sound hardware                                      */
	int fd;

	if((fd=open(OSS_GetDeviceName(),O_WRONLY|O_NONBLOCK))>=0) {
		close(fd);
		return 1;
	}
	return (errno==EACCES?1:0);
}

static int OSS_Init_internal(void)
{
	int play_stereo,play_rate;
	int orig_precision,orig_stereo;
	int formats;
#if SOUND_VERSION >= 301
	audio_buf_info buffinf;
#endif

#ifdef SNDCTL_DSP_GETFMTS
	/* Ask device for supported formats */
	if(ioctl(sndfd,SNDCTL_DSP_GETFMTS,&formats)<0) {
		_mm_errno=MMERR_OPENING_AUDIO;
		return 1;
	}
#else
	formats=AFMT_S16_NE|AFMT_U8;
#endif

	orig_precision=play_precision=(md_mode&DMODE_FLOAT)? AFMT_FLOAT :
					(md_mode&DMODE_16BITS)? AFMT_S16_NE : AFMT_U8;

    /* Device does not support the format we would prefer... */
    if(!(formats & play_precision)) {
        if(play_precision==AFMT_FLOAT) {
            _mm_errno=MMERR_NO_FLOAT32;
            return 1;
        }
        /* We could try 8 bit sound if available */
        if(play_precision==AFMT_S16_NE &&(formats&AFMT_U8)) {
            _mm_errno=MMERR_8BIT_ONLY;
            return 1;
        }
#ifdef AFMT_MU_LAW
        /* We could try uLaw if available */
        if(formats&AFMT_MU_LAW) {
            if((md_mode&DMODE_STEREO)||(md_mode&DMODE_16BITS)||
                md_mixfreq!=8000) {
                _mm_errno=MMERR_ULAW;
                return 1;
            } else
                orig_precision=play_precision=AFMT_MU_LAW;
        } else
#endif
            /* Otherwise, just abort */
        {
            _mm_errno=MMERR_OSS_SETSAMPLESIZE;
            return 1;
        }
    }

	if((ioctl(sndfd,SNDCTL_DSP_SETFMT,&play_precision)<0)||
	   (orig_precision!=play_precision)) {
		_mm_errno=MMERR_OSS_SETSAMPLESIZE;
		return 1;
	}
#ifdef SNDCTL_DSP_CHANNELS
	orig_stereo=play_stereo=(md_mode&DMODE_STEREO)?2:1;
	if((ioctl(sndfd,SNDCTL_DSP_CHANNELS,&play_stereo)<0)||
	   (orig_stereo!=play_stereo)) {
		_mm_errno=MMERR_OSS_SETSTEREO;
		return 1;
	}
#else
	orig_stereo=play_stereo=(md_mode&DMODE_STEREO)?1:0;
	if((ioctl(sndfd,SNDCTL_DSP_STEREO,&play_stereo)<0)||
	   (orig_stereo!=play_stereo)) {
		_mm_errno=MMERR_OSS_SETSTEREO;
		return 1;
	}
#endif

	play_rate=md_mixfreq;
	if((ioctl(sndfd,SNDCTL_DSP_SPEED,&play_rate)<0)) {
		_mm_errno=MMERR_OSS_SETSPEED;
		return 1;
	}
	if(play_rate>65535) { /* md_mixfreq is an uint16_t */
		_mm_errno=MMERR_OSS_SETSPEED;
		return 1;
	}
	md_mixfreq=play_rate;

#if SOUND_VERSION >= 301
	/* This call fails on Linux/PPC */
	if((ioctl(sndfd,SNDCTL_DSP_GETOSPACE,&buffinf)<0))
		ioctl(sndfd,SNDCTL_DSP_GETBLKSIZE,&buffinf.fragsize);
	if(!(audiobuffer=(SBYTE*)MikMod_malloc(buffinf.fragsize)))
		return 1;

	buffersize = buffinf.fragsize;
#else
	ioctl(sndfd,SNDCTL_DSP_GETBLKSIZE,&buffersize);
	if(!(audiobuffer=(SBYTE*)MikMod_malloc(buffersize)))
		return 1;
#endif

	return VC_Init();
}

static int OSS_Init(void)
{
#ifdef SNDCTL_DSP_SETFRAGMENT
	int fragmentsize;
#endif

	if((sndfd=open(OSS_GetDeviceName(),O_WRONLY))<0) {
		_mm_errno=MMERR_OPENING_AUDIO;
		return 1;
	}

#ifdef SNDCTL_DSP_SETFRAGMENT
	if((fragsize==DEFAULT_FRAGSIZE)&&(getenv("MM_FRAGSIZE"))) {
		fragsize=atoi(getenv("MM_FRAGSIZE"));
		if((fragsize<7)||(fragsize>17)) fragsize=DEFAULT_FRAGSIZE;
	}
	if((numfrags==DEFAULT_NUMFRAGS)&&(getenv("MM_NUMFRAGS"))) {
		numfrags=atoi(getenv("MM_NUMFRAGS"));
		if((numfrags<2)||(numfrags>255)) numfrags=DEFAULT_NUMFRAGS;
	}

	fragmentsize=(numfrags<<16)|fragsize;

	if(ioctl(sndfd,SNDCTL_DSP_SETFRAGMENT,&fragmentsize)<0) {
		_mm_errno=MMERR_OSS_SETFRAGMENT;
		return 1;
	}
#endif

	return OSS_Init_internal();
}

static void OSS_Exit_internal(void)
{
	VC_Exit();
	MikMod_free(audiobuffer);
	audiobuffer = NULL;
}

static void OSS_Exit(void)
{
	OSS_Exit_internal();

	if (sndfd>=0) {
		close(sndfd);
		sndfd=-1;
	}
}

static void OSS_PlayStop(void)
{
	VC_PlayStop();

	ioctl(sndfd,SNDCTL_DSP_POST,0);
}

static void OSS_Update(void)
{
	int done;

#if SOUND_VERSION >= 301
	audio_buf_info buffinf;

	buffinf.fragments = 2;
	for(;;) {
		/* This call fails on Linux/PPC */
		if ((ioctl(sndfd,SNDCTL_DSP_GETOSPACE,&buffinf)<0)) {
			buffinf.fragments--;
			buffinf.fragsize = buffinf.bytes = buffersize;
		}
		if(!buffinf.fragments)
			break;
		done=VC_WriteBytes(audiobuffer,buffinf.fragsize>buffinf.bytes?
						   buffinf.bytes:buffinf.fragsize);
#ifdef AFMT_MU_LAW
		if (play_precision==AFMT_MU_LAW)
			unsignedtoulaw((char *)audiobuffer,done);
#endif
		write(sndfd,audiobuffer,done);
	}
#else
	done=VC_WriteBytes(audiobuffer,buffersize);
#ifdef AFMT_MU_LAW
	if (play_precision==AFMT_MU_LAW)
		unsignedtoulaw(audiobuffer,done);
#endif
	write(sndfd,audiobuffer,done);
#endif
}

static int OSS_Reset(void)
{
	OSS_Exit_internal();
	ioctl(sndfd,SNDCTL_DSP_RESET,0);
	return OSS_Init_internal();
}

MIKMODAPI MDRIVER drv_oss={
	NULL,
	"Open Sound System",
	"Open Sound System driver v1.7",
	0,255,
	"oss",
#ifdef SNDCTL_DSP_SETFRAGMENT
	"buffer:r:7,17,14:Audio buffer log2 size\n"
	"count:r:2,255,16:Audio buffer count\n"
#endif
	"card:r:0,99,0:Sound card id\n",
	OSS_CommandLine,
	OSS_IsThere,
	VC_SampleLoad,
	VC_SampleUnload,
	VC_SampleSpace,
	VC_SampleLength,
	OSS_Init,
	OSS_Exit,
	OSS_Reset,
	VC_SetNumVoices,
	VC_PlayStart,
	OSS_PlayStop,
	OSS_Update,
	NULL,
	VC_VoiceSetVolume,
	VC_VoiceGetVolume,
	VC_VoiceSetFrequency,
	VC_VoiceGetFrequency,
	VC_VoiceSetPanning,
	VC_VoiceGetPanning,
	VC_VoicePlay,
	VC_VoiceStop,
	VC_VoiceStopped,
	VC_VoiceGetPosition,
	VC_VoiceRealVolume
};

#else

MISSING(drv_oss);

#endif

/* ex:set ts=4: */
