/** EMULib Emulation Library *********************************/
/**                                                         **/
/**                          Sound.c                        **/
/**                                                         **/
/** This file file implements core part of the sound API    **/
/** and functions needed to log soundtrack into a MIDI      **/
/** file. See Sound.h for declarations.                     **/
/**                                                         **/
/** Copyright (C) Marat Fayzullin 1996-2003                 **/
/**     You are not allowed to distribute this software     **/
/**     commercially. Please, notify me, if you make any    **/
/**     changes to this file.                               **/
/*************************************************************/
#include "Sound.h"

#include <stdio.h>
#include <string.h>

#ifdef UNIX
#include <unistd.h>
#endif

typedef unsigned char byte;
typedef unsigned short word;

struct SndDriverStruct SndDriver =
{
  (void (*)(int,int))0,
  (void (*)(int,int))0,
  (void (*)(int,int))0,
  (void (*)(int,int,int))0,
  (void (*)(int,const signed char *,int,int))0
};

static const struct { byte Note;word Wheel; } Freqs[4096] =
{
#include "MIDIFreq.h"
};

static const int Programs[5] =
{
  80,  /* SND_MELODIC/SND_RECTANGLE */
  80,  /* SND_TRIANGLE */
  122, /* SND_NOISE */
  122, /* SND_PERIODIC */
  80   /* SND_WAVE */
};

static struct
{
  int Type;
  int Note;
  int Pitch;
  int Level;
} CH[MIDI_CHANNELS];

static const char *LogName  = 0;
static int  Logging   = MIDI_OFF;
static int  TickCount = 0;
static int  LastMsg   = -1;
static FILE *MIDIOut  = 0;

static void MIDISound(int Channel,int Freq,int Volume);
static void MIDISetSound(int Channel,int Type);
static void MIDIDrum(int Type,int Force);
static void MIDIMessage(byte D0,byte D1,byte D2);
static void NoteOn(byte Channel,byte Note,byte Level);
static void NoteOff(byte Channel);
static void WriteDelta(void);
static void WriteTempo(int Freq);

/** SHIFT() **************************************************/
/** Make MIDI channel#10 last, as it is normally used for   **/
/** percussion instruments only and doesn't sound nice.     **/
/*************************************************************/
#define SHIFT(Ch) (Ch==15? 9:Ch>8? Ch+1:Ch)

/** Sound() **************************************************/
/** Generate sound of given frequency (Hz) and volume       **/
/** (0..255) via given channel. Setting Freq=0 or Volume=0  **/
/** turns sound off.                                        **/
/*************************************************************/
void Sound(int Channel,int Freq,int Volume)
{
  if(Channel<0) return;
  Freq   = Freq<0? 0:Freq;
  Volume = Volume<0? 0:Volume>255? 255:Volume;

  /* Call sound driver if present */
  if(SndDriver.Sound) (*SndDriver.Sound)(Channel,Freq,Volume);

  /* Log sound to MIDI file */
  MIDISound(Channel,Freq,Volume);
}

/** Drum() ***************************************************/
/** Hit a drum of given type with given force (0..255).     **/
/** MIDI drums can be used by ORing their numbers with      **/
/** SND_MIDI.                                               **/
/*************************************************************/
void Drum(int Type,int Force)
{
  Force = Force<0? 0:Force>255? 255:Force;

  if(SndDriver.Drum) (*SndDriver.Drum)(Type,Force);

  /* Log drum to MIDI file */
  MIDIDrum(Type,Force);
}

/** SetSound() ***********************************************/
/** Set sound type at a given channel. MIDI instruments can **/
/** be set directly by ORing their numbers with SND_MIDI.   **/
/*************************************************************/
void SetSound(int Channel,int Type)
{
  if(Channel<0) return;

  if(SndDriver.SetSound) (*SndDriver.SetSound)(Channel,Type);

  /* Log instrument change to MIDI file */
  MIDISetSound(Channel,Type);
}

/** SetChannels() ********************************************/
/** Set master volume (0..255) and switch channels on/off.  **/
/** Each channel N has corresponding bit 2^N in Switch. Set **/
/** or reset this bit to turn the channel on or off.        **/ 
/*************************************************************/
void SetChannels(int Volume,int Switch)
{
  Volume = Volume<0? 0:Volume>255? 255:Volume;

  if(SndDriver.SetChannels) (*SndDriver.SetChannels)(Volume,Switch);
}

/** SetWave() ************************************************/
/** Set waveform for a given channel. The channel will be   **/
/** marked with sound type SND_WAVE. Set Rate=0 if you want **/
/** waveform to be an instrument or set it to the waveform  **/
/** own playback rate.                                      **/
/*************************************************************/
void SetWave(int Channel,const signed char *Data,int Length,int Rate)
{
  if((Channel<0)||(Length<=0)) return;

  if(SndDriver.SetWave) (*SndDriver.SetWave)(Channel,Data,Length,Rate);

  /* Log instrument change to MIDI file */
  MIDISetSound(Channel,Rate? -1:SND_MELODIC);
}

/** InitMIDI() ***********************************************/
/** Initialize soundtrack logging into MIDI file FileName.  **/
/** Repeated calls to InitMIDI() will close current MIDI    **/
/** file and continue logging into a new one.               **/ 
/*************************************************************/
void InitMIDI(const char *FileName)
{
  int WasLogging,J;

  /* Must pass a name! */
  if(!FileName) return;

  /* Memorize logging status */
  WasLogging=Logging;

  /* If MIDI logging in progress, close current file */
  if(MIDIOut) TrashMIDI();

  /* Clear instrument types */
  for(J=0;J<MIDI_CHANNELS;J++) CH[J].Type=-1;

  /* Set log file name and ticks/second parameter, no logging yet */
  LogName   = FileName;
  Logging   = MIDI_OFF;
  LastMsg   = -1;
  TickCount = 0;
  MIDIOut   = 0;

  /* If was logging, restart */
  if(WasLogging) MIDILogging(MIDI_ON);
}

/** TrashMIDI() **********************************************/
/** Finish logging soundtrack and close the MIDI file.      **/
/*************************************************************/
void TrashMIDI(void)
{
  long Length;
  int J;

  /* If not logging, drop out */
  if(!MIDIOut) return;
  /* Turn sound off */
  for(J=0;J<MIDI_CHANNELS;J++) NoteOff(J);
  /* End of track */
  MIDIMessage(0xFF,0x2F,0x00);
  /* Put track length in file */
  fseek(MIDIOut,0,SEEK_END);
  Length=ftell(MIDIOut)-22;
  fseek(MIDIOut,18,SEEK_SET);
  fputc((Length>>24)&0xFF,MIDIOut);
  fputc((Length>>16)&0xFF,MIDIOut);
  fputc((Length>>8)&0xFF,MIDIOut);
  fputc(Length&0xFF,MIDIOut);
  /* Done logging */
  fclose(MIDIOut);
  Logging   = MIDI_OFF;
  LastMsg   = -1;
  TickCount = 0;
  MIDIOut   = 0;
}

/** MIDILogging() ********************************************/
/** Turn soundtrack logging on/off and return its current   **/
/** status. Possible values of Switch are MIDI_OFF (turn    **/
/** logging off), MIDI_ON (turn logging on), MIDI_TOGGLE    **/
/** (toggle logging), and MIDI_QUERY (just return current   **/
/** state of logging).                                      **/
/*************************************************************/
int MIDILogging(int Switch)
{
  static const char MThd[] = "MThd\0\0\0\006\0\0\0\1";
                           /* ID  DataLen   Fmt Trks */
  static const char MTrk[] = "MTrk\0\0\0\0";
                           /* ID  TrkLen   */
  int J,I;

  /* Toggle logging if requested */
  if(Switch==MIDI_TOGGLE) Switch=!Logging;

  if((Switch==MIDI_ON)||(Switch==MIDI_OFF))
    if(Switch^Logging)
    {
      /* When turning logging off, silence all channels */
      if(!Switch&&MIDIOut)
        for(J=0;J<MIDI_CHANNELS;J++) NoteOff(J);

      /* When turning logging on, open MIDI file */
      if(Switch&&!MIDIOut&&LogName)
      {
        /* No messages have been sent yet */
        LastMsg=-1;

        /* Clear all storage */
        for(J=0;J<MIDI_CHANNELS;J++)
          CH[J].Note=CH[J].Pitch=CH[J].Level=-1;

        /* Open new file and write out the header */
        MIDIOut=fopen(LogName,"wb");
        if(!MIDIOut) return(MIDI_OFF);
        if(fwrite(MThd,1,12,MIDIOut)!=12)
        { fclose(MIDIOut);MIDIOut=0;return(MIDI_OFF); }
        fputc((MIDI_DIVISIONS>>8)&0xFF,MIDIOut);
        fputc(MIDI_DIVISIONS&0xFF,MIDIOut);
        if(fwrite(MTrk,1,8,MIDIOut)!=8)
        { fclose(MIDIOut);MIDIOut=0;return(MIDI_OFF); }

        /* Write out the tempo */
        WriteTempo(MIDI_DIVISIONS);
      }

      /* Turn logging off on failure to open MIDIOut */
      if(!MIDIOut) Switch=MIDI_OFF;

      /* Assign new switch value */
      Logging=Switch;

      /* If switching logging on... */
      if(Switch)
      {
        /* Start logging without a pause */
        TickCount=0;

        /* Write instrument changes */
        for(J=0;J<MIDI_CHANNELS;J++)
          if((CH[J].Type>=0)&&(CH[J].Type&0x10000))
          {
            I=CH[J].Type&~0x10000;
            CH[J].Type=-1;
            MIDISetSound(J,I);
          }
      }
    }

  /* Return current logging status */
  return(Logging);
}

/** MIDITicks() **********************************************/
/** Log N 1ms MIDI ticks.                                   **/
/*************************************************************/
void MIDITicks(int N)
{
  if(Logging&&MIDIOut&&(N>0)) TickCount+=N;
}

/** MIDISound() **********************************************/
/** Set sound frequency (Hz) and volume (0..255) for a      **/
/** given channel.                                          **/
/*************************************************************/
void MIDISound(int Channel,int Freq,int Volume)
{
  int MIDIVolume,MIDINote,MIDIWheel;

  /* If logging off, file closed, or invalid channel, drop out */
  if(!Logging||!MIDIOut||(Channel>=MIDI_CHANNELS-1)||(Channel<0)) return;
  /* Frequency must be in range */
  if((Freq<MIDI_MINFREQ)||(Freq>MIDI_MAXFREQ)) Freq=0;
  /* Volume must be in range */
  if(Volume<0) Volume=0; else if(Volume>255) Volume=255;
  /* Instrument number must be valid */
  if(CH[Channel].Type<0) Freq=0;

  if(!Volume||!Freq) NoteOff(Channel);
  else
  {
    /* SND_TRIANGLE is twice quieter than SND_MELODIC */
    if(CH[Channel].Type==SND_TRIANGLE) Volume=(Volume+1)/2;
    /* Compute MIDI note parameters */
    MIDIVolume = Volume=(Volume+1)/2;;
    MIDINote   = Freqs[Freq/3].Note;
    MIDIWheel  = Freqs[Freq/3].Wheel;

    /* Play new note */
    NoteOn(Channel,MIDINote,MIDIVolume);

    /* Change pitch */
    if(CH[Channel].Pitch!=MIDIWheel)
    {
      MIDIMessage(0xE0+SHIFT(Channel),MIDIWheel&0x7F,(MIDIWheel>>7)&0x7F);
      CH[Channel].Pitch=MIDIWheel;
    }
  }
}

/** MIDISetSound() *******************************************/
/** Set sound type for a given channel.                     **/
/*************************************************************/
void MIDISetSound(int Channel,int Type)
{
  /* Channel must be valid */
  if((Channel>=MIDI_CHANNELS-1)||(Channel<0)) return;

  /* If instrument changed... */
  if(CH[Channel].Type!=Type)
  {
    /* If logging off or file closed, drop out */
    if(!Logging||!MIDIOut) CH[Channel].Type=Type|0x10000;
    else
    {
      CH[Channel].Type=Type;
      if(Type<0) NoteOff(Channel);
      else
      {
        Type=Type&SND_MIDI? (Type&0x7F):Programs[Type%5];
        MIDIMessage(0xC0+SHIFT(Channel),Type,255);
      }
    }
  }
}

/** MIDIDrum() ***********************************************/
/** Hit a drum of a given type with given force.            **/
/*************************************************************/
void MIDIDrum(int Type,int Force)
{
  /* If logging off or invalid channel, drop out */
  if(!Logging||!MIDIOut) return;
  /* The only non-MIDI drum is a click ("Low Wood Block") */
  Type=Type&DRM_MIDI? (Type&0x7F):77;
  MIDIMessage(0x99,Type,(Force&0xFF)/2);
}

/** MIDIMessage() ********************************************/
/** Write out a MIDI message.                               **/
/*************************************************************/
void MIDIMessage(byte D0,byte D1,byte D2)
{
  /* Write number of ticks that passed */
  WriteDelta();

  /* Write out the command */
  if(D0!=LastMsg) { LastMsg=D0;fputc(D0,MIDIOut); }

  /* Write out the arguments */
  if(D1<128)
  {
    fputc(D1,MIDIOut);
    if(D2<128) fputc(D2,MIDIOut);
  }
}

/** NoteOn() *************************************************/
/** Turn on a note on a given channel.                      **/
/*************************************************************/
void NoteOn(byte Channel,byte Note,byte Level)
{
  Note&=0x7F;
  Level&=0x7F;

  if((CH[Channel].Note!=Note)||(CH[Channel].Level!=Level))
  {
    if(CH[Channel].Note==Note)
      MIDIMessage(0xA0+SHIFT(Channel),Note,Level);
    else
    {
      if(CH[Channel].Note>=0) NoteOff(Channel);
      MIDIMessage(0x90+SHIFT(Channel),Note,Level);
    }

    CH[Channel].Note=Note;
    CH[Channel].Level=Level;
  }
}

/** NoteOff() ************************************************/
/** Turn off a note on a given channel.                     **/
/*************************************************************/
void NoteOff(byte Channel)
{
  if(CH[Channel].Note>=0)
  {
    MIDIMessage(0x80+SHIFT(Channel),CH[Channel].Note,127);
    CH[Channel].Note=-1;
  }
}

/** WriteDelta() *********************************************/
/** Write number of ticks since the last MIDI command and   **/
/** reset the counter.                                      **/
/*************************************************************/
void WriteDelta(void)
{
  if(TickCount<128) fputc(TickCount,MIDIOut);
  else
  {
    if(TickCount<128*128)
    {
      fputc((TickCount>>7)|0x80,MIDIOut);
      fputc(TickCount&0x7F,MIDIOut);
    }
    else
    {
      fputc(((TickCount>>14)&0x7F)|0x80,MIDIOut);
      fputc(((TickCount>>7)&0x7F)|0x80,MIDIOut);
      fputc(TickCount&0x7F,MIDIOut);
    }
  }

  TickCount=0;
}

/** WriteTempo() *********************************************/
/** Write out soundtrack tempo (Hz).                        **/
/*************************************************************/
void WriteTempo(int Freq)
{
  int J;

  J=500000*MIDI_DIVISIONS*2/Freq;
  WriteDelta();
  fputc(0xFF,MIDIOut);
  fputc(0x51,MIDIOut);
  fputc(0x03,MIDIOut);
  fputc((J>>16)&0xFF,MIDIOut);
  fputc((J>>8)&0xFF,MIDIOut);
  fputc(J&0xFF,MIDIOut);
}
