/***************************************************************************
*	NAME:  IWSOUND.C $Revision: 1.4 $
**	COPYRIGHT:
**	"Copyright (c) 1994,1995 by e-Tek Labs"
**
**       "This software is furnished under a license and may be used,
**       copied, or disclosed only in accordance with the terms of such
**       license and with the inclusion of the above copyright notice.
**       This software or any other copies thereof may not be provided or
**       otherwise made available to any other person. No title to and
**       ownership of the software is hereby transfered."
****************************************************************************
* $Log: iwsound.c $
* Revision 1.4  1995/10/26 14:59:33  mleibow
* Added mutes for software mixer controls.
* Revision 1.3  1995/10/13 17:27:56  mleibow
* Changed master volume to have 10dB drop off instead of 6dB.
* Revision 1.2  1995/06/22 08:30:48  sdsmith
* Added master volume query functions
* Revision 1.1  1995/02/23 11:07:19  unknown
* Initial revision
***************************************************************************/

#include <dos.h>
#include "iw.h"
#include "iwl.h"
#include "globals.h"

#define UCHAR unsigned char
#define USHORT unsigned short
#define ULONG unsigned long

#define STAT_MASK 3
#define STAT_UNUSED 0
#define STAT_PAUSED 1
#define STAT_STARTED 2

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

LOCAL DEFINITION:
sound_voice_status - structure to track activity on voices 

DESCRIPTION:
sound_voice_status tracks the activity on voices being used to play
digital music (e.g. MOD files).
*/
static struct sound_voice_status {
	UCHAR status;
	UCHAR voice_control;
	USHORT bend;
	ULONG frequency;
	short pan;
	short voice;
	void (RFAR *callback)(int);
} sound_voice_status[32];

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

LOCAL DEFINITION:
vrperiod - time to change one volume level

DESCRIPTION:
vrperiod inidicates the time it takes to make a volume change of
one step.  The units of time are 10^-7 seconds.
*/
static long vrperiod = 116100;
short iwl_sound_master_volume;
short iwl_sound_mute_atten;

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

FUNCTION DEFINITION:
sound_init - initialize digital music system

NOTE: part of IW/OK auto-init system

RETURNS: int - IW_OK
*/
int iwl_sound_init(void)
{
    USHORT v;
    struct sound_voice_status *vs;

    for (vs=sound_voice_status, v=0; v < iwl_voices; v++, vs++) {
	vs->voice = v;
	vs->status = STAT_UNUSED;
	vs->voice_control = IWL_STOPPED|IWL_STOP;
    }
    iwl_sound_master_volume = IWL_TO_FMM(iw_atten_tab[100]);
    if (iwl_sound_master_volume > IW_MAX_VOLUME)
    	iwl_sound_master_volume = IW_MAX_VOLUME;
    iwl_sound_mute_atten = 0;
    return(IW_OK);
}

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

FUNCTION DEFINITION:
set_pan - set the voice pan control register

RETURNS: void
*/
static void set_pan(
  struct sound_voice_status *vs,
  short pan,   /* pan value 0 = full left; 127 = full right */
  short immediately)
{
    short vlo, vro;

    ENTER;
    if (pan < 0) pan = 0;
    if (pan > 127) pan = 127;
    vs->pan = pan;
    vlo = iw_atten_tab[(IW_ATTENTABSIZE-1)-pan];
    vro = iw_atten_tab[pan];
    if (pan != (IW_ATTENTABSIZE-1) && pan != 0) {
	vlo >>= 1;
	vro >>= 1;
    }
    vlo += iwl_sound_master_volume + iwl_sound_mute_atten;
    vro += iwl_sound_master_volume + iwl_sound_mute_atten;
    if (vlo > IW_MAX_VOLUME) vlo = IW_MAX_VOLUME;
    if (vro > IW_MAX_VOLUME) vro = IW_MAX_VOLUME;
    /* set page register */
    IWL_SET_PAGE(vs->voice);
    /* set pan position */
    vlo <<= 4;
    if (immediately) IWL_OUT_W(SET_LEFT_OFFSET, vlo);
    IWL_OUT_W(SET_LEFT_FINAL_OFFSET, vlo);
    vro <<= 4;
    if (immediately) IWL_OUT_W(SET_RIGHT_OFFSET, vro);
    IWL_OUT_W(SET_RIGHT_FINAL_OFFSET, vro);
    LEAVE;
}

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

FUNCTION DEFINITION:
set_vol - set the volume of a digital music voice

DESCRIPTION:
set_vol computes the volume change rate and increment to change the
volume of a voice from its current setting to a new setting within the
peroid specified.  If the period specified is zero, the voice is 
changed at the fastest possible rate.

The ramp rate is compsed of 1 byte with 2 parts:

bits 7,6 - Range
bits 5-0 - increment

Where range is defined as follows:

0 = Fu  (Fu = 1/(1.6uS * max(#active voices,14)
1 = Fu/8
2 = Fu/64
3 = Fu/512

The range indicates how often the volume increment is applied to the
voice volume.  The increment is 6 bits thus ranging from 0 - 63.

To determine the volume ramp rate, the range and increment that will best
result in the period specified.

RETURNS: void
*/
static void set_vol(USHORT voice, short vol, long rperiod)
{
    long period;
    int i;
    short old_volume;
    ULONG increment;
    USHORT volume_control;

    if (vol < 0) vol = 0;
    if (vol > 127) vol = 127;
    ENTER;
#ifdef DEBUG
    dprintf("rperiod = %ld\n", rperiod);
#endif
    vol = (4095 - iw_atten_tab[vol]) >> 4;
    /* set page register */
    IWL_SET_PAGE(voice);
    OS_OUTPORTB(iwl_register_select, SET_VOLUME_CONTROL);
    OS_OUTPORTB(iwl_data_high, IWL_STOP|IWL_STOPPED);
    /* set voice volume */
    OS_OUTPORTB(iwl_register_select, GET_VOLUME);
    old_volume = (USHORT)OS_INPORTW(iwl_data_low) >> 8;
    OS_OUTPORTB(iwl_register_select, SET_VOLUME_RATE);
    if (rperiod == 0) {
	fastest:
	OS_OUTPORTB(iwl_data_high, 63); /* may cause zipper noise */
    } else {
	period = vrperiod * (long)(vol - old_volume) << 4;
#ifdef DEBUG
	dprintf("initial period = %ld, vol %d - %d\n", period, vol, old_volume);
#endif
	if (period < 0) period = -period;
	if (period == 0) goto fastest;
	for (i=3; i >= 0; i--) {
#ifdef DEBUG
	    dprintf("rate %d, period = %ld\n", i, period);
#endif
	    if (period < rperiod) {
		increment = 1;
		break;
	    } else {
		increment = (period/rperiod);
#ifdef DEBUG
		dprintf("checking increment %ld\n", increment);
#endif
		if (increment <= 63) break;
	    }
	    period >>= 3;
	}
	if (i < 0) goto fastest;
#ifdef DEBUG
	dprintf("chose %d:%ld\n", i, increment);
#endif
	OS_OUTPORTB(iwl_data_high, (UCHAR)(i<<6)|(UCHAR)increment);
    }
    if (old_volume > vol) {
	volume_control = IWL_DIR_DEC;
	OS_OUTPORTB(iwl_register_select, SET_VOLUME_START);
    } else if (old_volume < vol) {
	volume_control = 0;
	OS_OUTPORTB(iwl_register_select, SET_VOLUME_END);
    }
    if (old_volume != vol) {
	OS_OUTPORTB(iwl_data_high, vol);
	OS_OUTPORTB(iwl_register_select, SET_VOLUME_CONTROL);
	OS_OUTPORTB(iwl_data_high, volume_control);
    }
    LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_volume - change the volume of a digital music voice

DESCRIPTION:
iw_sound_volume changes the volume of a digital music voice to the volume
indicated in the amount of time specified.

The volume can be in the range of 0 to 127.  The period is in the
units of 10ths of microseconds.

RETURNS: void
*/
void iw_sound_volume(
  USHORT voice,          /* voice to change volume */
  int volume,            /* new volume setting */
  ULONG period) /* period over which to change volume */
{
    struct sound_voice_status *vs = &sound_voice_status[voice];

    ENTER;
    if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	set_vol(voice, volume, period);
    }
    LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_pan - set the pan for a digital music voice

DESCRIPTION:
iw_sound_pan sets the pan for a digital music voice.  The pan is a value
between 0 and 127 with 0 being full left and 127 being full right.

RETURNS: void
*/
void iw_sound_pan(
  USHORT voice, /* voice to set panning */
  short pan)   /* pan value to set */
{
    struct sound_voice_status *vs = &sound_voice_status[voice];

    ENTER;
    if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	set_pan(vs, pan, 0);
    }
    LEAVE;
}

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

FUNCTION DEFINITION:
change_freq - set a voice's frequency control register

DESCRIPTION:
change_freq calculates the proper value to set for a voice's frequency
control register based on the playback frequency and pitch bend set
for the voice.

RETURNS: void
*/
static void change_freq(
  int voice) /* voice to set frequency control register */
{
	USHORT fc;
   	struct sound_voice_status *vs = &sound_voice_status[voice];

	fc = (USHORT)(((vs->frequency<<10L)+22050UL) / 44100UL);
	if (vs->bend != 1024) {
	    fc = (USHORT)((ULONG)fc * vs->bend >> 10);
	}
	/* set playback rate */
	IWL_SET_PAGE(voice);
	OS_OUTPORTB(iwl_register_select, SET_FREQUENCY);
	OS_OUTPORTW(iwl_data_low, fc);
}

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

FUNCTION DEFINITION:
iw_sound_bend - change the pitch of a digital music voice

DESCRIPTION:
iw_sound_bend applies a pitch bend to a voice playing digital music.  The
pitch bend offsets the playback frequency by a small amount to produce 
various effects.

RETURNS: void
*/
void iw_sound_bend(
  USHORT voice,  /* voice to set pitch bend */
  USHORT bend)   /* pitch bend to set */
{
   	struct sound_voice_status *vs = &sound_voice_status[voice];

	/* calc fc register */
	ENTER;
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    vs->bend = bend;
	    change_freq(voice);
	}
	LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_frequency - set the playback frequency of a digital music voice

DESCRIPTION:
iw_sound_frequency changes the playback rate of the voice indicated by
the frequency specified.  The voice is first checked to be sure it is
being used for digital music playback.

RETURNS: void
*/
void iw_sound_frequency(
  USHORT voice, /* voice to set frequency */
  ULONG freq)   /* new playback frequency */
{
   	struct sound_voice_status *vs = &sound_voice_status[voice];

	ENTER;
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    vs->frequency = freq;
	    change_freq(voice);
	}
	LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_stop - stop play of digital music data

DESCRIPTION:
iw_sound_stop stops and frees the voice playing digital music data.

RETURNS: void
*/
void RFAR iw_sound_stop(
  int voice) /* digital music voice to stop */
{
   	struct sound_voice_status *vs = &sound_voice_status[voice];
	unsigned char vc;

	ENTER;
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    set_vol(voice, 0, 0); /* ramp volume down */
	    do {
		OS_OUTPORTB(iwl_register_select, GET_VOLUME_CONTROL);
		vc = OS_INPORTB(iwl_data_high);
	    } while ((vc & (IWL_STOP|IWL_STOPPED)) == 0) ;
	    vs->voice_control |= (IWL_STOPPED|IWL_STOP);
	    OS_OUTPORTB(iwl_register_select, SET_CONTROL);
	    OS_OUTPORTB(iwl_data_high, vs->voice_control);
	    vs->status = STAT_UNUSED;
	    iw_free_voice(voice);
	    if (vs->callback) vs->callback(voice);
	}
	LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_mode - setup voice to play digital music data properly

DESCRIPTION:
iw_sound_mode sets the voice control registers to properly play the
digial music data indicated.  The type of data will be a bitwise 
OR of the following flags:

IW_SND_LOOP       - play the data continuously
IW_SND_LOOP_NONE  - play the data once
IW_SND_LOOP_BIDIR - play the data continuously in both directions
IW_SND_8BIT       - data is 8-bit data
IW_SND_BACKWARD   - play the data backwards

RETURNS: void
*/
void iw_sound_mode(
  int voice,                    /* voice used for digital music data */
  struct iw_sound RFAR *sound,  /* digital music data */
  UCHAR type)                   /* type of digital music data */
{
   	struct sound_voice_status *vs = &sound_voice_status[voice];
	ULONG addrs, addre;

	ENTER;
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    /* set page register */
	    IWL_SET_PAGE(voice);
	    switch (type & IW_SND_LOOP_MASK) {
		case IW_SND_LOOP_NONE:
		    vs->voice_control &= ~(IWL_LPE|IWL_DIR_DEC|IWL_BLE);
		    break;
		case IW_SND_LOOP:
		    vs->voice_control &= ~(IWL_DIR_DEC|IWL_BLE);
		    vs->voice_control |= IWL_LPE;
		    break;
		case IW_SND_LOOP_BIDIR:
		    vs->voice_control &= ~(IWL_DIR_DEC);
		    vs->voice_control |= (IWL_BLE|IWL_LPE);
		    break;
	    }
	    if (type & IW_SND_BACKWARD) {
		vs->voice_control |= IWL_DIR_DEC;
	    }
	    addrs = sound->mem_pos;
	    if (!(type & IW_SND_8BIT)) {
		addrs = iwl_convert_to_16bit(addrs);
	    }
	    addrs <<= 4;
	    addre = addrs + sound->end_loop;
	    addrs = addrs + sound->start_loop;
	    iwl_set_addr_f_regs(voice, addrs, SET_START_LOW, SET_START_HIGH);
	    iwl_set_addr_f_regs(voice, addre, SET_END_LOW, SET_END_HIGH);
	    vs->voice_control &= ~(IWL_STOPPED|IWL_STOP);
	    OS_OUTPORTB(iwl_register_select, SET_CONTROL);
	    OS_OUTPORTB(iwl_data_high, vs->voice_control);
	}
	LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_playing - get status of a digital music voice

DESCRIPTION:
iw_sound_playing get the status of the voice specified.  If the voice
is being used for digital music, the current status of the voice is
returned.

RETURNS: int - 0 = voice is not a digital music voice
               otherwise bitwise OR of following flags:
	         IW_SOUND_ACTIVE  - voice is allocated for digital music
		 IW_SOUND_PLAYING - voice is playing digital music
*/
int iw_sound_playing(
  int voice) /* voice to check for digital music playing */
{
   	struct sound_voice_status *vs = &sound_voice_status[voice];
	int rval;

	ENTER;
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    rval = IW_SOUND_ACTIVE;
	    IWL_SET_PAGE(voice);
	    OS_OUTPORTB(iwl_register_select, GET_CONTROL);
	    if (!(OS_INPORTB(iwl_data_high) & (IWL_STOPPED | IWL_STOP))) {
		rval |= IW_SOUND_PLAYING;
	    }
	} else {
	    rval = 0;
	}
	LEAVE;
	return(rval);
}

void iw_sound_master_volume(short master_volume)
{
    struct sound_voice_status *vs;
    short i;

    if (master_volume < 0) master_volume = 0;
    if (master_volume > 127) master_volume = 127;
    ENTER;
    iwl_sound_master_volume = IWL_TO_FMM(iw_atten_tab[master_volume]);
    if (iwl_sound_master_volume > IW_MAX_VOLUME)
    	iwl_sound_master_volume = IW_MAX_VOLUME;
    for (vs=sound_voice_status, i=0; i < 32; vs++, i++) {
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    set_pan(vs, vs->pan, 0);
	}
    }
    LEAVE;
}

short iw_sound_get_master_volume(void)
{
    int i;
    short rc;

	for (i=0; i<IW_ATTENTABSIZE; i++) {
		if (IWL_FROM_FMM(iwl_sound_master_volume) > iw_atten_tab[i]) {
			break;
		}
	}
	if (i)
		rc = i - 1;
	else
		rc = 0;
	if (rc < 0)
		rc = 0;
	if (rc > 127)
		rc = 127;
    return(rc);
}

void iw_sound_mute(short mute)
{
    struct sound_voice_status *vs;
    short i;

    ENTER;
    if (mute) {
	iwl_sound_mute_atten = IW_MAX_VOLUME;
    } else {
	iwl_sound_mute_atten = 0;
    }
    for (vs=sound_voice_status, i=0; i < 32; vs++, i++) {
	if ((vs->status & STAT_MASK) != STAT_UNUSED) {
	    set_pan(vs, vs->pan, 0);
	}
    }
    LEAVE;
}

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

FUNCTION DEFINITION:
iw_sound_start - initiate digital music playback

DESCRIPTION:
iw_sound_start allocates and initializes a synthesizer voice to be used
for playback of digital music data (e.g. MOD files).

RETURNS: int - number of voice to be used for playback
*/
int iw_sound_start(
	USHORT priority,
	struct iw_sound RFAR *sound,
	short volume,
	ULONG period,
	short pan,
	ULONG freq,
	USHORT bend,
	void (RFAR *callback)(int voice))
{
   	struct sound_voice_status *vs;
	void (RFAR *callback_addr)(int);
	ULONG addr;
	int voice;
	UCHAR mode;

	ENTER;
#if defined (__BORLANDC__) && NEARCODE == 1
	asm mov word ptr callback_addr, offset iw_sound_stop
	asm mov word ptr callback_addr+2, cs
#else
	callback_addr = iw_sound_stop;
#endif
	voice = iw_allocate_voice(priority, callback_addr);
	if (voice == IW_NO_MORE_VOICES) {
	    LEAVE;
	    return(voice);
	}
	IWL_SET_PAGE(voice);
	mode = IWL_SMSI_OFFEN;
	if (sound->type & IW_SND_ULAW) mode |= IWL_SMSI_ULAW;
	IWL_OUT_B(SET_VOICE_MODE, mode); /* activate voice */
	vs = &sound_voice_status[voice];
	vs->status = STAT_PAUSED;
	vs->callback = callback;
	vs->bend = bend;
	vs->frequency = freq;
	change_freq(voice);
	addr = sound->mem_pos;
	if (sound->type & IW_SND_8BIT) {
	    vs->voice_control = 0;
	} else {
	    addr = iwl_convert_to_16bit(addr);
	    vs->voice_control = IWL_WT16;
	}
	if (sound->type & IW_SND_BACKWARD) {
	    addr += sound->end_loop>>4;
	}
	iwl_set_addr_regs(voice, addr, SET_ACC_LOW, SET_ACC_HIGH);
	set_pan(vs, pan, 1);
	iw_sound_mode(voice, sound, sound->type);
	iw_sound_volume(voice, volume, period);
	vs->status = STAT_STARTED;
	LEAVE;
	return(voice);
}
