/* Emacs style mode select   -*- C++ -*- */
/*-----------------------------------------------------------------------------*/

/* $Id:$*/

/* Copyright (C) 1993-1996 by id Software, Inc.*/

/* This source is available for distribution and/or modification*/
/* only under the terms of the DOOM Source Code License as*/
/* published by id Software. All rights reserved.*/

/* The source is distributed in the hope that it will be useful,*/
/* but WITHOUT ANY WARRANTY; without even the implied warranty of*/
/* FITNESS FOR A PARTICULAR PURPOSE. See the DOOM Source Code License*/
/* for more details.*/

/* $Log:$*/

/* DESCRIPTION:*/
/*	System interface for sound.*/

/*-----------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

#include <math.h>

#include "ROsupport.h"

#include "z_zone.h"

#include "i_system.h"
#include "i_sound.h"
#include "m_argv.h"
#include "m_misc.h"
#include "w_wad.h"

#include "doomdef.h"

/* The number of independant channels, hardware or software
 */
#define NUM_CHANNELS		8
/* The amount of data between each possible change of sound params
 */
#define BUFFERSIZE		512 /* bytes */
/* The samplerate of the raw data.
 */
#define SAMPLERATE		11025 /* Hz  */

/* This depends on your dolby decoder. 20ms is often used.
 */
#define DOLBYDELAY		20e-3 /* seconds */


/* Channel data. One block for each channel,
 * 8 words each.
 */
typedef struct {
  byte* data;	/* +0  Pointer to the sample 	*/
    int length;	/* +4  Length of sample		*/
    int Lamp;	/* +8  Left amplitude (10.22)	*/
    int Ramp;	/* +12 Right amplitude (10.22)	*/
    int pitch;	/* +16 Pitch (16.16)		*/
    int phacc;	/* +20 Phase accumulator (16.16)*/
    int start;	/*     Tic that sound started	*/
    int id;	/*     Sample ID		*/
} chan_info;

/* For storing sound configuration
 */
typedef struct {
  int channels;
  int buffer;
  int period;
  void *channel_handler;
  void *scheduler;
} sound_config;

int		sound_ready = 0;
sound_config	sound_old = {-1,0,0,NULL,NULL};
boolean		sound_16bit = false;
boolean		force8bit = false;
int		sound_prevrate;
int		sound_prevpan[NUM_CHANNELS];
boolean		nosound = false;
boolean		nomusic = false;
chan_info	chan[NUM_CHANNELS];

/* Lookup tables
 */
int	lengths[NUMSFX];	/* Lengths of the effects		*/
byte	LinToLogTable[256];	/* unsigned lin -> VIDC ulaw		*/
int	pitchtable[256];	/* Note to pitch lookup, 4 octaves.	*/
int	pantable[256];		/* Pan to amp table (24.8)		*/

/* Some assembler functions
 */
extern int		SetStereoPosition(int channel,int position);
extern void		ConfigureSound(sound_config* config);
extern int		SoundLog(int linear);
extern boolean          Sound16_Available(void);
extern int              Sound16_SetRate(int frequency);
extern _kernel_oserror* Sound16_InstallHandler(void);
extern _kernel_oserror* Sound16_RemoveHandler(void);
extern int		SoundControl(int ch,int vol,int pan);
extern char*		InstallVoice(void);
extern char*		RemoveVoice(void);
extern boolean		Mus_CheckAndInit(void);
extern void		Mus_ClearQueue(void);
extern void		Mus_StopSound(void);
extern int		Mus_StartClock(int rate,int time);
extern int		Mus_TxCommand(int cmd,int time);

/* This function loads the sound data from the WAD lump,
 */
void* getsfx(char* sfxname,int* len)
{
    char                name[20];
    int                 sfxlump;

    /* Make lump name
     */
    sprintf(name, "ds%s", sfxname);

    if ( W_CheckNumForName(name) == -1 )
      sfxlump = W_GetNumForName("dspistol"); /* Replacement if not found */
    else
      sfxlump = W_GetNumForName(name);

    *len = W_LumpLength( sfxlump )-8;

    /* Return start of sample.
     */
    return (byte*)W_CacheLumpNum(sfxlump,PU_STATIC)+8;
}


/* This allocates a channel for a new sound
 */
int getchannel(int sfxid)
{
  int		i;
  int		oldest = gametic;
  int		oldestnum = 0;

  /* Chainsaw troubles.
   * Play these sound effects only one at a time.
   */
  if (sfxid == sfx_sawup  || sfxid == sfx_sawidl || sfxid == sfx_sawful
   || sfxid == sfx_sawhit || sfxid == sfx_stnmov)
    for (i=0 ; i<NUM_CHANNELS ; i++)
      if ( (chan[i].data) && (chan[i].id == sfxid) )
      {
	chan[i].data = 0;
	break;
      }
  /* Loop all channels to find oldest SFX.
   */
  for (i=0; (i<NUM_CHANNELS) && (chan[i].data!=0); i++)
    if (chan[i].start < oldest)
    {
      oldestnum = i;
      oldest = chan[i].start;
    }
  /* Tales from the crypt.
   * If we found a channel, fine.
   * If not, we simply overwrite the first one, 0.
   * Probably only happens at startup.
   */
  if (i == NUM_CHANNELS)
    return oldestnum;
  else
    return i;
}


/* Calculate and set L and R amplitude (only for 16bit)
 *   according to volume and pan.
 * Volume is 0..15
 * Pan is -255..255:
 *  -255 Right
 *  -128 Center Back
 *     0 Left
 *   128 Center Front
 *   255 Right
 */
void SetLRamp(int ch,int vol,int pan)
{
  if (pan>=0)
  {
    chan[ch].Ramp=(vol*pantable[pan])<<10;
    chan[ch].Lamp=(vol*pantable[255-pan])<<10;	/* (10.22) */
  }
  else
  {
    chan[ch].Ramp=(vol*-pantable[-pan])<<10;
    chan[ch].Lamp=(vol*pantable[255+pan])<<10;	/* (10.22) */
  }
}

/* Retrieve the raw data lump index for a given SFX name.
 */

int I_GetSfxLumpNum(sfxinfo_t* sfx)
{
    char namebuf[9];
    sprintf(namebuf, "ds%s", sfx->name);
    return W_GetNumForName(namebuf);
}

/* ---------------------------------------- SOUND API -------------------------------------------
*/

void I_SetSfxVolume(int volume) { }

void
I_InitSound()
{
  int	i;
  int*	pitchtablemid = pitchtable + 128;
  #ifndef SOUND_PRECACHE
    char	name[20];
    int		sfxlump;
  #endif

  /* All channels dormant
   */
  for (i=0; i<NUM_CHANNELS; i++)
    chan[i].data=0;
    
  /* Minimal initialisation done, can exit now. */
  if (nosound)
    return;

  /* Initialise external data (all sounds) at start, keep static.
   */
  #ifdef SOUND_PRECACHE
    printf("I_InitSound: Pre-caching all sound data\n");
  #endif
  for (i=1 ; i<NUMSFX ; i++)
  {
    /* Alias? Example is the chaingun sound linked to pistol.
     */
    if (!S_sfx[i].link)
    {
      #ifdef SOUND_PRECACHE
        /* Load data from WAD file.
         */
        S_sfx[i].data = getsfx( S_sfx[i].name, &lengths[i] );
      #else
        /* Make lump name
         */
        sprintf(name,"ds%s",S_sfx[i].name);
        if ( W_CheckNumForName(name) == -1 )
          sfxlump = W_GetNumForName("dspistol"); /* Replacement if not found */
        else
          sfxlump = W_GetNumForName(name);

        lengths[i]=W_LumpLength(sfxlump)-8;
        S_sfx[i].lumpnum=sfxlump;
        S_sfx[i].data=0;
      #endif
    }
    else
    {
      /* Previously loaded already?
       */
      S_sfx[i].data = S_sfx[i].link->data;
      lengths[i] = lengths[(S_sfx[i].link - S_sfx)/sizeof(sfxinfo_t)];
    }
  }


  /* Make pitch table
   */
  for (i=-128 ; i<128 ; i++)
    pitchtablemid[i] = (int)(pow(2.0,(i/64.0))*65536.0);
  /* Make audible pan table. Center gives -3dB for each channel.
   */
  for (i=0 ; i<256 ; i++)
    pantable[i] = (int)(16.1525080187*pow((double)(i+1),0.498289214233));
  printf("I_InitSound: Sound module ready\n");
}


/* This will initialise anything that needs unshared
 * system resources
 */
void I_ClaimSound(void)
{
  int	i;
  char*	err;

  /* Disable escape side effects (like stopping sound)
   */
  OSByte(230,1,0);

  /* First determine if we've got a 16bit output.
   */
  sound_16bit = (Sound16_Available() && !force8bit);
  
  /* OK, different from now on
   */
  if (sound_16bit)
  {
    /* fprintf(logfile,"I_ClaimSound: Using 16 bit output\n"); */
    sound_prevrate = Sound16_SetRate(SAMPLERATE*1024);
    Sound16_InstallHandler(); 
    sound_ready=1;
  }
  else
  {  
    /* fprintf(logfile,"I_ClaimSound: Using 8 bit output\n"); */
    /* Build a LinToLog table based on configured volume. The user can configure
     * the overall volume, and there's a good reason for this, so we use it!
     */
    for (i=0; i<256; i++)
      LinToLogTable[i] = SoundLog((i-128)<<24);
    sound_old.channels = NUM_CHANNELS;
    sound_old.buffer = BUFFERSIZE;
    sound_old.period = 1000000/SAMPLERATE;
    sound_old.channel_handler = NULL;
    sound_old.scheduler = NULL;
    /* Configure sound, and store old setup
     */
    ConfigureSound(&sound_old);
    for (i=0;i<NUM_CHANNELS;i++)
      sound_prevpan[i]=SetStereoPosition(i,-128);
    if ((err=InstallVoice()))
    {
      fprintf(logfile,"I_ClaimSound: %s\n",err);
      return;
    }
    else
      sound_ready=1;
  }
}

/* This will restore system resources to their state
 * before I_ClaimSound was called
 */
void I_ReleaseSound(void)
{
  /* Wait until things have quieted down
   */
  int i,done = 0;

  if (nosound)
    return;

  while (!done && sound_ready)
  {
    for( i=0 ; i<NUM_CHANNELS && !chan[i].data ; i++);
    if (i==NUM_CHANNELS) done=1;
  }
  sound_ready = 0;
  if (sound_16bit)
  {
    Sound16_RemoveHandler();
    Sound16_SetRate(sound_prevrate);
  }  
  else
  {
    RemoveVoice();
    if (sound_old.channels >= 0)
    {
      for (i=0;i<NUM_CHANNELS;i++)
        SetStereoPosition(i,sound_prevpan[i]);
      ConfigureSound(&sound_old);
    }
  }
  OSByte(230,0,0); /* reset escape effects */
}


/* This function starts a given sound.
 * input  id		Just the id
 *        vol		0..15 (log)
 *        pan		-255..255 (see SetLRamp for meaning)
 *        pitch		0..255 (in 1/64th octaves, 128 is nominal freq)
 * output channel       for updating the sound later
 */
int I_StartSound(int id,int vol,int pan,int pitch)
{
  int ch;

  ch=getchannel(id);
  /* Just in case of an IRQ before we finish setting up
   */
  chan[ch].data = 0;
  chan[ch].length = lengths[id];			/* Set length of raw data	*/
  chan[ch].start = gametic;				/* Start tic			*/
  chan[ch].id = id;					/* Sound ID			*/
  chan[ch].pitch = pitchtable[pitch];			/* Set pitch (16.16)		*/
  if (sound_16bit)
  {
    SetLRamp(ch,vol,pan);
    if (pan<0)
      chan[ch].phacc = (int)(DOLBYDELAY*SAMPLERATE*65536);
      							/* Advance rear sounds by 20ms.	*/
    else
      chan[ch].phacc = 0;				/* No delay on front		*/
    chan[ch].data = (byte*) S_sfx[id].data;		/* Set pointer to raw data	*/
  }
  else
  {
    chan[ch].phacc = 0;					/* No delay			*/
    chan[ch].data = (byte*) S_sfx[id].data;		/* Set pointer to raw data	*/
    SoundControl(ch,vol,pan);
  }
  /* fprintf(logfile,"I_StartSound: %s, ch %i, pan %i\n",S_sfx[id].name,ch,pan); */
  return ch;
}

void I_UpdateSoundParams(int ch,int vol,int pan,int pitch)
{
  chan[ch].pitch=pitchtable[pitch];
  if (sound_16bit)
    SetLRamp(ch,vol,pan);
  else
    SoundControl(ch,vol,pan);
}


int I_SoundIsPlaying(int ch)
{
  return (chan[ch].data!=0);
}


void I_StopSound (int ch)
{
  chan[ch].data=0;
}


void I_ShutdownSound(void)
{
  /* Nothing to do, really */
}



/* ------------------------------------------- MUSIC API -------------------------------------------
 */


/* Info about music playing */
boolean	music_ready=false;	/* true if MIDI initialised ok			*/
byte*	music_data=NULL;	/* Pointer to score if registered, else 0	*/
byte*	music_pos=NULL;		/* Current position if playing, else 0	 	*/
int	music_loop;		/* Loop flag if playing				*/
int 	music_time;		/* Current music time if playing		*/
int 	music_vel[16];		/* Previous velocity on midichannel		*/
int	music_pause=0;		/* Time that music was paused, or 0 if it isn't	*/
int 	music_mainvol;		/* Main volume (-127..0)			*/

char midimap[16]={0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,9};

/* This is called once at startup */
void I_InitMusic(void)
{
  music_ready=music_pause=0;
  music_data=music_pos=NULL;
}

/* This will initialise anything that needs unshared
 * system resources
 */
void I_ClaimMusic(void)
{
  if (nomusic)
    return;
  if (music_pause==0)
  {
    music_ready=Mus_CheckAndInit();
    if (!music_ready)
      fprintf(logfile,"I_ClaimMusic: Could not initialise MIDI\n");
  }
  else
    I_ResumeSong(1);
}

/* This will restore system resources to their state
 * before I_ClaimMusic was called
 */
void I_ReleaseMusic(void)
{
  if (!music_ready)
    return;
  I_PauseSong(1);
}


/* Song has been loaded, and placed at 'data'.
   If music is ready, get ready to play.
   Return a dummy handle */
int I_RegisterSong(byte* data)
{
  unsigned short offset;
  
  if (!music_ready) return 0;
  I_UnRegisterSong(1);
  if (((int*)data)[0]!=0x1a53554d) return 1;
  offset=*(data+6)+((*(data+7))<<8);
  music_data=data+offset;
  return 1;
}

/* If a song is registered, start playing it */
void I_PlaySong(int handle, int loop)
{
  int i;

  if ((!music_ready)||(music_data==NULL)) return;
  I_StopSong(handle);
  music_loop=loop;
  music_pos=music_data;
  music_time=music_pause=0;
  for (i=0; i<16; i++) music_vel[i]=64;
  Mus_StartClock(100,0);
  I_FillMusBuffer(handle);
}

/* Keep scheduler buffer full */
void I_FillMusBuffer(int handle)
{
  int free,cmd,delay,ch,par1,par2;

  if (music_pos==NULL) return;
  free=Mus_TxCommand(0xFE,music_time);
  while ((free>0) && (music_pos!=NULL))
  {
    cmd=*music_pos++;
    ch=midimap[cmd &15];
    switch ((cmd>>4) &7)
    {
      case 0: /* NoteOff */
        free=Mus_TxCommand(0x400080|ch|(*music_pos++<<8),music_time);
        break;
      case 1: /* NoteOn */
        par1=*music_pos++;
        if (par1 &128)
          par2=(music_vel[ch]=*music_pos++)+music_mainvol;
        else
          par2=music_vel[ch]+music_mainvol;
        par1&=127;
        if (par2>0) free=Mus_TxCommand(0x90|ch|(par1<<8)|(par2<<16),music_time);
        break;
      case 2: /* Bend */
        par1=*music_pos++;
        free=Mus_TxCommand(0xE0|ch|((par1 &127)<<8)|((par1 &128)<<1),music_time);
        break;
      case 3: /* System */
        switch (*music_pos++)
        {
          case 10:
          case 11: free=Mus_TxCommand(0x7BB0|ch,music_time); break;
          case 12: free=Mus_TxCommand(0x7EB0|ch,music_time); break;
          case 13: free=Mus_TxCommand(0x7FB0|ch,music_time); break;
          case 14: free=Mus_TxCommand(0x79B0|ch,music_time); break;
        }
        break;
      case 4: /* ControlChange */
        par1=*music_pos++;
        par2=*music_pos++;
        switch (par1)
        {
          case 0: free=Mus_TxCommand(0x00C0|ch|(par2<<8),music_time); break;
          case 2: free=Mus_TxCommand(0x01B0|ch|(par2<<16),music_time); break;
          case 3: free=Mus_TxCommand(0x07B0|ch|(par2<<16),music_time); break;
          case 4: free=Mus_TxCommand(0x0AB0|ch|(par2<<16),music_time); break;
          case 5: free=Mus_TxCommand(0x0BB0|ch|(par2<<16),music_time); break;
          case 6: free=Mus_TxCommand(0x5BB0|ch|(par2<<16),music_time); break;
          case 7: free=Mus_TxCommand(0x5DB0|ch|(par2<<16),music_time); break;
          case 8: free=Mus_TxCommand(0x40B0|ch|(par2<<16),music_time); break;
          case 9: free=Mus_TxCommand(0x43B0|ch|(par2<<16),music_time); break;
        }
        break;
      case 6: /* End */
        if (music_loop)
          music_pos=music_data;
        else
          music_pos=NULL;
        break;
    }
    if (cmd>127)
    {
      delay=0;
      while (1)
      {
        cmd=*music_pos++;
        delay=(cmd & 127)|(delay<<7);
        if (cmd<128) break;
      }
      music_time+=delay;
    }  
  }  
}

/* Is the song playing, even if paused? */
int I_QrySongPlaying(int handle)
{
  return (music_pos!=NULL);
}

void I_SetMusicVolume(int volume) /* 0..15 */
{
  if (!music_ready) return;
  music_mainvol=(volume-15)*127/15;
}

/* Pause song, be silent */
void I_PauseSong (int handle)
{
  if ((!music_ready)||(music_pause!=0))
    return;				/* Already paused */
  music_pause=Mus_StartClock(0,0);	/* Stop clock and note time */
  Mus_StopSound();
}

/* Resume song, obviously */
void I_ResumeSong (int handle)
{
  if ((!music_ready)||
     (music_pause==0)) return;		/* Wasn't paused */
  Mus_StartClock(100,music_pause);	/* Restart clock */
  music_pause=0;
}

/* Stop song and sounds */
void I_StopSong(int handle)
{
  if (!music_ready) return;
  Mus_ClearQueue();			/* Might be stopped already if not looping */
  Mus_StopSound();
  music_pos=NULL;
  music_pause=0;
}

/* Stop and forget song */
void I_UnRegisterSong(int handle)
{
  if (music_data==NULL) return;		/* Not registered */
  I_StopSong(handle);
  music_data=NULL;
}

/* This should kill playing music, and tidy up */
void I_ShutdownMusic(void)
{
  if (!music_ready) return;
  I_UnRegisterSong(1);
  music_ready=false;
}


