/* song.c
 *
 *  Routines to convert one or more SMUS TRAK chunks
 *  to a Music Studio score
 */

#include "smus.h"
#include "music.h"


/*
 *  Given 6 bits of IFF duration, get the Music Studio duration
 *    for storage in the song.  To get the real duration in 96ths of
 *    a whole note, use the result as an index into DurTab[]
 */
int
MSDuration(IFFDur)
UBYTE IFFDur;
{
   int DurTabIndex;     /* the result: an index into DurTab[] */

   IFFDur = IFFDur & noteDurMask;
   if (IFFDur >= 32)
      return(1);        /* return minimum note if quint or sept note??? */
   DurTabIndex = MSDurTab[IFFDur];
   if (DurTabIndex == 0xFF)
      return(1);        /* return minimum note if 64th,128th note */
   if (DurTabIndex > 22)
      return(22);       /* clip index to maximum of 22 */
   return(DurTabIndex);
} /* MSDuration */

char
SetFreq(MNote)
   MCmd *MNote;
{
   int  result;

   result = (MNote->freq % 12);
   if ((result == 0)
    || (result == 2)
    || (result == 4)
    || (result == 5)
    || (result == 7)
    || (result == 9)
    || (result == 11))
      {
         if (keytab[result] == result)
            return(NOTACCIDENT);              /* note is in scale */
         else return(NATURAL);
      }
   else                                      /* must be natural, # or b */
   {
      if ((song[INITIAL_KEY] <= 8))           /* C to F# have sharps*/
      {
         MNote->freq--;
         if (keytab[result-1] == result)
           return(NOTACCIDENT);
         else
           return(SHARP);
      }
      else                                   /* flat key */
      {
         MNote->freq++;
         if (keytab[result+1] == result)
            return(NOTACCIDENT);
         else
            return(FLAT);
      }
   } /* else not white key */
} /* end SetFreq */

/*
 * Clip IFF frequency to within MS limits
 *  for Octave Range = 0
 */
ClipFreq(MNote)
   MCmd  *MNote;
{
   while (MNote->freq < 0x24)     /* if too small */
      MNote->freq += 12;             /* go up one octave */
   while (MNote->freq > 0x60)     /* if too large */
      MNote->freq -= 12;             /* go down one octave */
}

BOOL Last_Tied[MAXCOLORS+1];

InitTies()
{
   int i;

   for (i = 0; i <= MAXCOLORS; i++)
      Last_Tied[i] = FALSE;
} /* InitTies */


/*
 *  Extract information from IFF Note
 *   and store MS equivalent in song[]
 */
NoteToMS(INote,TrackNum)
   SNote *INote;
   int TrackNum;     /* IFF track # playing this IFF Note */
{
   int IDur;         /* Holds IFF Duration */
   char TieBits,AccBits;   /* Hold Tie and Accent bit patterns */
   MCmd MNote;       /* local Music Studio note structure */
   char Color;       /* "color" = instrument to play this note */

      /* Play track 1 on instrument 1, track 2 on instrument 2, etc.. */
   Color = 0x0F & WhichColor(TrackNum & 0xFF);
   if (Color > MAXCOLORS)
   {
      printf("Burp!: %d\n",TrackNum);
      Color = 0;
   }

   if (INote->tone == SID_Rest)         /* If rest... */
   {
      Color = Color | 0x10;            /*  set Rest bit */
      MNote.freq = MS_REST_HEIGHT;
      AccBits = 0;
      TieBits = 0;
      (VOID) SetFreq(&MNote);
   }
   else     /* It's a note! */
   {
       MNote.freq = INote->tone;       /* MNote MSB automagically zeroed */
         /* set up acc bits */
      AccBits = SetFreq(&MNote);

      ClipFreq(&MNote);

      TieBits = 0;               /* default = not tied */
      if (Last_Tied[Color])             /* If last note was tied */
         TieBits |= 0x01;        /*   then we have TieIn */
      if (Is_Tied_Out(INote))    /* Check if tied out */
      {
         TieBits |= 0x02;        /* Combine with possible TieIn */
         Last_Tied[Color] = TRUE;       /* This note IS tied... */
      } else Last_Tied[Color] = FALSE;  /* This note NOT tied... */
   }
   IDur = MSDuration(INote->data);   /* Extract IFF note duration */

      /* Assemble MS note duration byte */
   MNote.durbyt = (IDur & 0x1F) | (AccBits & 0xC0);

      /* Assemble MS color byte */
   MNote.colbyt = (Color & 0x1F) | ((TieBits<<5) & 0x60);
      /* command bit is zero for all notes */

   if (Debug)
      printf("   N: %3x %3x %3x\n   ",
          MNote.colbyt,MNote.durbyt,MNote.freq);

   *cursng++ = (char) MNote.colbyt;      /* add this filled in note */
   *cursng++ = (char) MNote.durbyt;      /*  to MS Song array and */
   *cursng++ = (char) MNote.freq;        /*  also advance cursor */

} /* Note_to_MS */

static BOOL GotKeySig = FALSE;
static BOOL GotTimeSig = FALSE;


/*
 * Convert an IFF Event to Music Studio command(s)
 */
EventToMS(IEvent,TrackNum)
SEvent *IEvent;
int TrackNum;
{
   int i;

   if (IEvent->sID <= SID_Rest)              /* If Event is rest or note */
      NoteToMS( (SNote *) IEvent,TrackNum);
   else
      switch (IEvent->sID)
      {
         case SID_TimeSig:
            if (!GotTimeSig)
            {
                  /* MS time signature range 1..7 */
               for (i = 1; i < 7; i++)
                  if (TimeSigTab[i] == IEvent->data)
                  {
                     song[INITIAL_TIME] = i;
                     GotTimeSig = TRUE;
                     if (Debug)
                        printf("### Time: %d\n",i);
                  }
            }
            break;
         case SID_KeySig:
            if (!GotKeySig)
            {
               song[INITIAL_KEY] = IEvent->data + 1;
               SetKeyTab(IEvent->data + 1);
               GotKeySig = TRUE;
               if (Debug)
                  printf("### Key: %d\n",IEvent->data+1);
            }
            break;
         case SID_Instrument:
            break;
         case SID_Dynamic:
         case SID_MIDI_Chnl:
         case SID_MIDI_Preset:
         default:
            break;
      } /* switch */
} /* EventToMS */

/*
 * Get current event on given track;
 *  return a pointer to it, or NULL if no more events
 */
   SEvent *
GetEvent(ThisTrack)
   int   ThisTrack;
{
   SEvent *FirstEv;
   SEvent *ThisEv;

      /* check for invalid track */
   if (ThisTrack > NumTracks)
      return(NULL);

      /* check if track already completed */
   if (Tracks[ThisTrack].Finished) {
      return(NULL);
   }

      /* check for Event number too high */
   if (Tracks[ThisTrack].EventPos >=
      (Tracks[ThisTrack].TrackSize / sizeof(SEvent)))
         return(NULL);

      /* Check for null Track */
   if ((FirstEv = Tracks[ThisTrack].TrackData) == NULL)
      return(NULL);

      /* OK; now we're there */
   ThisEv = &(FirstEv[ (Tracks[ThisTrack].EventPos) ]);

   return(ThisEv);
}

/*
 * Skip ahead over events that aren't notes or rests
 *  sending them to EventToMS for interpretation
 *  Return a pointer to the first note or rest event
 */
   SEvent *
SkipEvents(TrkNum)
   int TrkNum;
{
   SEvent *MyEvent;

   while (((MyEvent = GetEvent(TrkNum))->sID > SID_Rest) &&
          (Tracks[TrkNum].EventPos <
              (Tracks[TrkNum].TrackSize/sizeof(SEvent))))
   {
      EventToMS(MyEvent,TrkNum);
      Tracks[TrkNum].EventPos++;
   }
   return(MyEvent);
} /* SkipEvents */

/*
 * Return a pointer to the next note or rest on track "TrkNum"
 */
   SEvent *
NextEvent(TrkNum)
   int   TrkNum;
{
   SEvent *MyEvent;

   if ((Tracks[TrkNum].EventPos < (Tracks[TrkNum].TrackSize/sizeof(SEvent)))
      && !(Tracks[TrkNum].Finished))
   {
      if (Tracks[TrkNum].EventPos > 0) /* if not first event */
         Tracks[TrkNum].EventPos++;
   }
   else
      return(NULL);

   MyEvent = SkipEvents(TrkNum);

   return(MyEvent);
}

#define  BIG   32768

/*
 * Return the smallest remaining time on all busy tracks,
 *  or if nothing now playing return BIG
 */
   int
MinTimeToGo(Trak)
   int   *Trak;
{
   int   MinSoFar = BIG;
   int   TrackNo;
   int   ThisTime;
   int   MinTrak;

   for (TrackNo = 0; TrackNo < NumTracks; TrackNo++)
      if (!Tracks[TrackNo].Finished)
      {
         ThisTime = Tracks[TrackNo].TimeToGo;
         if ((ThisTime > 0) && (ThisTime < MinSoFar))
         {
            MinSoFar = ThisTime;
            MinTrak = TrackNo;
         }
      }
   if (MinSoFar < BIG)
      *Trak = MinTrak;
   return(MinSoFar);
}

#define REST_TRACK   0       /* all rests have this 'color' */

/*
 * Start this frame with a rest of given duration
 */
Rest(RestTime)
   int   RestTime;
{
   int   RestInd;
   SEvent RestEvent;

   while (RestTime > 0)
   {
      RestInd = MS_WHOLE_NOTE;   /* max inserted rest is whole note */
      while ((DurTab[RestInd] > RestTime) && (RestInd > 1))
         RestInd--;
      RestTime -= DurTab[RestInd];
      RestEvent.sID = SID_Rest;
      RestEvent.data = IFFDurTab[RestInd] & 0xFF;
      EventToMS(&RestEvent,REST_TRACK);
   } /* while */
} /* Rest */

#define  NO_TRACK 42    /* can't have track# this big */

/*
 *  Return the duration and track number of the minimum duration note
 *    among all the tracks changing this frame
 *  If no notes to be played, return BIG
 */
   int
MinDurNote(MinTrackNo)
   int   *MinTrackNo;
{
   int   MinDur = BIG;  /* No durations larger than this, certianly! */
   int   MinTrak = NO_TRACK;   /* default is no min-track */
   int   MinRDur = BIG;
   int   MinRTrak = NO_TRACK;
   int   ThisDur;       /* Duration of note being checked */
   int   i;
   SEvent *ThisEvent;   /* Pointer to current IFF Event */

   /* Find the minimum duration note to be played */
   for (i=0; i < NumTracks; i++)
      if ((Tracks[i].TimeToGo == 0) &&
          (!Tracks[i].Finished))     /* if track changing now */
      {
         if ((ThisEvent = NextEvent(i)) != NULL)    /* if events left */
         {
            ThisDur = DurTab[MSDuration(ThisEvent->data)];
            if (ThisEvent->sID < SID_Rest)
            {
               if (ThisDur < MinDur)
               {
                  MinDur = ThisDur;
                  MinTrak = i;
               } /* if new min */
            } /* if note */
            else
               if (ThisEvent->sID == SID_Rest)
               {
                  if (ThisDur < MinRDur)
                  {
                     MinRDur = ThisDur;
                     MinRTrak = i;
                  }
               }

         } /* if events remaining on this track */
         else
         {  
            Tracks[i].Finished = TRUE;    /* No events left -- done! */
            if (Debug)
               printf("Done with track %d\n",i);
         } /* else */
      } /* if no time remaining in current event */

      /* If no note duration but a rest duration return it instead */
/*   if ((MinDur == BIG) && (MinRDur < BIG)) */
   if (MinRDur < MinDur)
   {
      MinDur = MinRDur;
      *MinTrackNo = MinRTrak;
   }
   else
      *MinTrackNo = MinTrak;

   return(MinDur);
} /* MinDurTrack */

/*
 * 'Move Ahead' to next frame by subtracting length of this
 *   frame from remaining time of all active notes & rests.
 *  Return TRUE iff all tracks are finished
 *
 */
   BOOL
AdvanceTime(ThisFrameDur)
   int   ThisFrameDur;
{
   int   i;
   BOOL  Finish = TRUE;  /* finish unless we find a track not finished yet */

      /* Subtract duration of this frame from all note times-remaining */
   Finish = TRUE;
   for (i = 0; i < NumTracks; i++)
      if (!Tracks[i].Finished)
      {
         Tracks[i].TimeToGo -= ThisFrameDur;    /* Cut down time remaining */
         if (Tracks[i].TimeToGo < 0)   /* but make sure time left */
            Tracks[i].TimeToGo = 0;    /* is always positive */
         Finish = FALSE;
      }
   return(Finish);
}

/*
 *Convert notes,rests until an unchorded note found
 */
TrackNotes(ThisTrack,RestsAllowed)
   int  ThisTrack;
   BOOL RestsAllowed;
{
   SEvent *ThisEvent;
   int Chord = TRUE;

  if (!Tracks[ThisTrack].Finished)
   while (Chord) {  /* repeat until found an unchorded note */
      if ((ThisEvent = GetEvent(ThisTrack)) != NULL)
      {
            /* Set time to go on this event from duration */
         if (ThisEvent->sID <= SID_Rest)
         {
            Tracks[ThisTrack].TimeToGo =
                         DurTab[MSDuration(ThisEvent->data)];

            if ((ThisEvent->sID < SID_Rest) ||
                (RestsAllowed && (ThisEvent->sID == SID_Rest)))
               EventToMS(ThisEvent,ThisTrack);
         }
         else
            EventToMS(ThisEvent,ThisTrack);

      } /* if still notes on this track */

      Chord = FALSE;
      if (ThisEvent != NULL)                       /* if event */
         if (ThisEvent->sID < SID_Rest)            /* if note */
            if ((ThisEvent->data & 0x80) != 0)     /* if chorded */
               if ((ThisEvent = NextEvent(ThisTrack)) != NULL)
               {
                  Chord = TRUE;
                  if (Debug)
                     printf("c");
               }

   } /* while not done with this track */
} /* TrackNotes */

/*
 * Convert all tracks, now stored in RAM, to a single stream
 *  of Music Studio commands
 */
TracksToMS()
{
   BOOL  FoundEnd = FALSE; /* true when all tracks finished */
   int   TrackNo;
   int   FrameNum = 0;
   int   MinNoteDur;       /* Duration of shortest note on all tracks */
   int   MinNoteTrack;     /* Track number with the shortest note */
   int   MinRemainDur;     /* Minimum time remaining on all tracks */
   int   FrameDur;         /* Duration of this frame */
   int   MinRTrack;

      /* assign remaining colors to tracks that have no instrument match */
   AssignColors(NumTracks);

   InitTies();

   while (!FoundEnd)
   {
      *cursng++ = FRAMEMARK;

      if (Debug)
         printf("Fr %d:\n   ",FrameNum++);

         /*  Find track number with shortest note to be played this frame */
      MinNoteDur = MinDurNote(&MinNoteTrack);
      MinRemainDur = MinTimeToGo(&MinRTrack);

         /* If an old note will end before even the shortest new one */
      if (MinRemainDur < MinNoteDur)
      {
         if (Debug)
            printf("   Min Remain: %d (%d)\n  ",MinRemainDur,MinRTrack);
         Rest(MinRemainDur);        /* shortest event in frame is rest */
         FrameDur = MinRemainDur;
         MinNoteTrack = NO_TRACK;
      }
      else
      if (MinNoteDur < BIG)
      {
         if (Debug)
            printf("   Min Note: %d (%d)\n   ",MinNoteDur,MinNoteTrack);
            /* shortest event in frame is note (or rest) */
         TrackNotes(MinNoteTrack,TRUE);
         FrameDur = MinNoteDur;
      }
      else
         FrameDur = 0;

         /* Track all other notes ready to be played */
      for (TrackNo = 0; TrackNo < NumTracks; TrackNo++)
         if ((Tracks[TrackNo].TimeToGo == 0) && (TrackNo != MinNoteTrack))
            TrackNotes(TrackNo,FALSE);

      FoundEnd = AdvanceTime(FrameDur);

   } /* while */
   FreeTracks();        /* free memory allocated for track chunks */
} /* TracksToMS() */


