/* Record.c
   Copyright (c) 1990,1991,1992 by Thomas E. Janzen
   All Rights Reserved

   THIS SOFTWARE IS FURNISHED FREE OF CHARGE FOR STUDY AND USE AND MAY
   BE COPIED ONLY FOR PERSONAL USE OR COMPLETELY AS OFFERED WITH NO
   CHANGES FOR FREE DISTRIBUTION.  NO TITLE TO AND OWNERSHIP OF THE
   SOFTWARE IS HEREBY TRANSFERRED.  THOMAS E. JANZEN ASSUMES NO 
   RESPONSBILITY FOR THE USE OR RELIABILITY OF THIS SOFTWARE.

   Thomas E. Janzen
   58A School St. Apt. 2-L
   Hudson, MA 01749-2518
   (508)562-1295
*/
/*
**  FACILITY:
**
**	AlgoRhythms music improviser on Commodore (TM) Amiga (TM)
**	compiled with SAS/C V5.10b
**
**  ABSTRACT:
**
**	   Record.c contains functions for MIDI event recording and saving
**	
**  AUTHORS: Thomas E. Janzen
**
**  CREATION DATE:	9-DEC-1991
**
**  MODIFICATION HISTORY:
**    DATE	NAME	DESCRIPTION
**  4 Jan 92 TEJ  last changes for 2.0
*/

#include <stdlib.h>
#include <stdio.h>
#include <intuition/intuition.h>
#include <proto/timer.h>
#include <ctype.h>
#include <string.h>
#include "AlgoRhythms.h"
#include "Menus.h"
#include "record.h"

#define NOTES_PER_BLOCK 1024
#define MULTITRAK 1
#define MIDIHDRLEN   14
#define MIDITRKHDRLEN 8
#define TICKS_PER_QUARTER 240
#define NOTEON       0X90
#define META_EVENT   0XFF
#define TEXT_EVENT   0X01
#define TRACK_NAME   0X03
#define EOT_EVENT    0X2F
#define TEMPO_EVENT  0X51
#define TIME_SIG_EVENT 0X58
#define PACK_BYTES(A, B, C, D)  (D | (C << 8) | (B << 16) | (A << 24))

struct Note_Record_Struct
{
   struct timeval Event_Time;
   unsigned char  Channel,
                  Pitch,
                  Velocity;
};
typedef struct Note_Record_Struct NOTE_RECORD_TYPE;

typedef struct Page_List_Struct PAGE_LIST_TYPE;
struct Page_List_Struct
{
   PAGE_LIST_TYPE   *Succ,
                    *Pred;
   NOTE_RECORD_TYPE *Page;
};

struct MIDI_Note_struct
{
   unsigned char  Status_Channel,
                  Note_Number,
                  Velocity;
};
typedef struct MIDI_Note_struct MIDI_NOTE_TYPE;

struct MIDI_Header
{
   unsigned int   Chunk_Type,
                  Chunk_Len;
   unsigned short int   Format,
                        Num_Tracks,
                        Division; /* of time, usu. 24 MIDI ticks */
};
typedef struct MIDI_Header MIDI_HEADER_TYPE;

struct MIDI_Track
{
   unsigned int Chunk_Type,
                Chunk_Len;
};
typedef struct MIDI_Track MIDI_TRACK_TYPE;

static unsigned char *MIDI_Score = NULL;
static unsigned int Notes_Qty = 0;

static PAGE_LIST_TYPE Page_List_Head = {NULL, NULL, NULL};
static PAGE_LIST_TYPE *Current_List_Ptr = NULL;
unsigned int Recording = FALSE;

static void Add_Page (void);

static int Variable_Length_Values (int Number, unsigned char *Array);
static void Insert_Meta_Event (unsigned int *MIDI_Index,
                                       unsigned char *MIDI_Score,
                                       unsigned char Event_Type,
                                       unsigned int Event_Len);
static void Store_a_Note (  unsigned int *MIDI_Index,
                           unsigned char *MIDI_Score,
                           unsigned char Current_Channel,
                           unsigned char Current_Dynamic,
                           unsigned char Pitch,
                           unsigned char *Running_Status);

unsigned int Record_Init (void)
{
   /*
   ** When Record is selected:
   ** If page list header is null, 
   ** allocate a page list entry.
   ** set up a list of pages
   ** set global recording on
   ** write "RECORDING" in red jam2 to the right on the window
   */
   /* to turn off recording, just have menu_parse reset Recording_G */
   if (Page_List_Head.Page == NULL)
   {
      Page_List_Head.Page 
         = malloc (sizeof (NOTE_RECORD_TYPE) * NOTES_PER_BLOCK);
      Page_List_Head.Succ = NULL;
      Page_List_Head.Pred = NULL;
      Current_List_Ptr = &Page_List_Head;
      Notes_Qty = 0;
   }
   return TRUE;
}

int Record_Note_Event (NOTEEVENT *Note_Event)
{
   static NOTE_RECORD_TYPE Note_Record = {{0, 0}, 0, 0, 0};
   static int Note_Event_Index = 0;

   if (Current_List_Ptr->Page == NULL)
   {
      Current_List_Ptr->Page 
         = malloc (sizeof (NOTE_RECORD_TYPE) * NOTES_PER_BLOCK);
      if (Current_List_Ptr->Page == NULL)
      {
         Recording = FALSE;
         Clear_Record ();
         /* post an error message */
         return FALSE;
      }
   }   
   if (Note_Event_Index >= NOTES_PER_BLOCK)
   {
      Add_Page ();
      Note_Event_Index = 0;
   }
   Note_Record.Pitch    = Note_Event->CurPitch;
   Note_Record.Channel  = Note_Event->Channel;
   Note_Record.Velocity = Note_Event->Dynamic;
   if (Note_Event->Playing) /* it's a note on */
   {
      Note_Record.Event_Time = Note_Event->StartTime;
   }
   else
   {
      Note_Record.Event_Time = Note_Event->StopTime;
   }
   memcpy (&(Current_List_Ptr->Page[Note_Event_Index]),
           &Note_Record, sizeof (NOTE_RECORD_TYPE));
   Note_Event_Index++;
   Notes_Qty++;
   return TRUE;
}

static void Add_Page (void)
{
   Current_List_Ptr->Succ = malloc (sizeof (PAGE_LIST_TYPE));
   if (Current_List_Ptr->Succ == NULL)
   {
      Recording = FALSE;
      Clear_Record ();
      return;
   }
   Current_List_Ptr->Succ->Pred = Current_List_Ptr;
   Current_List_Ptr = Current_List_Ptr->Succ;
   Current_List_Ptr->Page 
      = malloc (sizeof (NOTE_RECORD_TYPE) * NOTES_PER_BLOCK);
   if (Current_List_Ptr->Page == NULL)
   {
      Recording = FALSE;
      Clear_Record ();
      /* post an error message */
      return;
   }
   Current_List_Ptr->Succ = NULL;

   return;
}

void Erase_Recording (void)
{
   auto PAGE_LIST_TYPE *Temp_Entry;

   Temp_Entry = &Page_List_Head;
   
   while (Temp_Entry->Succ != NULL)
   {
      Temp_Entry = Temp_Entry->Succ;
   }
   /* 
   ** we are now pointing at the last list entry
   ** so unwind the data 
   */
   while (Temp_Entry->Pred != NULL)
   {
      free (Temp_Entry->Page);
      Temp_Entry = Temp_Entry->Pred;
      free (Temp_Entry->Succ);
      Temp_Entry->Succ = NULL;
   }
   Notes_Qty = 0;
   /*
   ** We should now point to the head of the list 
   */
   return;
}

int Write_MIDI (char *MIDI_File_Name)
{
   auto unsigned char filemode[2] = "w",
                      Running_Status = 0,
                      Track_Name[32] = "AlgoRhythms by Tom Janzen";
   auto unsigned int MIDI_Index = 0,
                     Local_Notes_Qty = 0,
                     Intra_Block_Index = 0,
                     Time_int,
                     time_len;
   auto struct timeval   Temp_Time = {0, 0},
                           Last_Time = {0, 0};
   auto FILE *MIDI_File = NULL;
   auto NOTE_RECORD_TYPE Note_Record;
   auto MIDI_NOTE_TYPE Note_Event;
   auto PAGE_LIST_TYPE *Local_List_Ptr = NULL;
   auto MIDI_HEADER_TYPE MIDI_File_Hdr = 
                           {PACK_BYTES ('M','T','h','d'), 6, 
                              MULTITRAK, 1, TICKS_PER_QUARTER};
   auto MIDI_TRACK_TYPE Track = {PACK_BYTES ('M', 'T', 'r', 'k'), 0};

   Local_List_Ptr = &Page_List_Head; /* initialize list pointer */
   MIDI_Index = 0;
   /* 
   ** can't be larger than * 8 + header; 
   ** note = 4 bytes delay max, 3 for event max
   */
   MIDI_Score = malloc ((Notes_Qty * 10) + 18 + 4); 
   if (MIDI_Score == NULL)
   {
      return FALSE;
   }
   MIDI_Score[MIDI_Index] = 0;
   MIDI_Index++;
   Insert_Meta_Event (&MIDI_Index, MIDI_Score, TRACK_NAME, 
                      strlen (Track_Name));
   strcpy (&MIDI_Score[MIDI_Index], Track_Name);
   MIDI_Index += strlen (Track_Name);

   MIDI_Score[MIDI_Index] = 0;
   MIDI_Index++;
   
   Insert_Meta_Event (&MIDI_Index, MIDI_Score, TEMPO_EVENT, 3);
   MIDI_Score[MIDI_Index] = 0x0F;
   MIDI_Index++;
   MIDI_Score[MIDI_Index] = 0x42;
   MIDI_Index++;
   MIDI_Score[MIDI_Index] = 0x40; /* these 3 numbers give 
                                  ** 1 million ticks/quarter note */
   MIDI_Index++;
   MIDI_Score[MIDI_Index] = 0; /* required zero delay */

   MIDI_Index++;
   Insert_Meta_Event (&MIDI_Index, MIDI_Score, TIME_SIG_EVENT, 4);
   MIDI_Score[MIDI_Index] = 4;
   MIDI_Index++;
   MIDI_Score[MIDI_Index] = 2;
   MIDI_Index++;
   MIDI_Score[MIDI_Index] = 24;
   MIDI_Index++;
   MIDI_Score[MIDI_Index] = 8;
   MIDI_Index++;
   for (Local_Notes_Qty = 0; Local_Notes_Qty < Notes_Qty;
         Local_Notes_Qty++)
   {
      if (Intra_Block_Index >= NOTES_PER_BLOCK)
      {
         Local_List_Ptr = Local_List_Ptr->Succ; /* what if it's null?*/
         if ((Local_List_Ptr == NULL) || (Local_List_Ptr->Page == NULL))
         {
            free (MIDI_Score);
            return FALSE; /* post error */
         }
         Intra_Block_Index = 0;
      }
      memcpy (&Note_Record, &(Local_List_Ptr->Page[Intra_Block_Index]),
            sizeof (NOTE_RECORD_TYPE));
      Intra_Block_Index++;
      Note_Event.Note_Number = Note_Record.Pitch;
      Note_Event.Status_Channel = NOTEON | Note_Record.Channel;
      Note_Event.Velocity = Note_Record.Velocity;
      /*
      ** convert absolute time struct to MIDI delay bytes
      */
      Temp_Time = Note_Record.Event_Time;
      if (Local_Notes_Qty == 0)
      {
         Last_Time = Temp_Time;
      }
      if (CmpTime (&Temp_Time, &Last_Time) == 1) /* -1, first > second */
      /*
      ** The new time can be earlier than the last time if the user
      ** stopped the music and hit "Play" i.e., from the beginning
      */
      {
         Last_Time = Temp_Time;
         Time_int = 0;
      }
      else
      {
         SubTime (&Temp_Time, &Last_Time); /* f(a, b), a = a - b */
         Last_Time = Note_Record.Event_Time;
         Time_int 
            = (Temp_Time.tv_micro / 1000) + (Temp_Time.tv_secs * 1000);
         Time_int = (Time_int * 240) / 1000; /* give 240/second */
      }
      time_len 
         = Variable_Length_Values (Time_int, &MIDI_Score[MIDI_Index]);
      MIDI_Index += time_len;
      Store_a_Note (&MIDI_Index, MIDI_Score, Note_Event.Status_Channel, 
         Note_Event.Velocity, Note_Event.Note_Number, &Running_Status);
   }
   MIDI_Score[MIDI_Index] = 0;
   MIDI_Index++;
   Insert_Meta_Event (&MIDI_Index, MIDI_Score, EOT_EVENT, 0);
   MIDI_File = fopen (MIDI_File_Name, filemode);
   if (MIDI_File == NULL) 
   {
      free (MIDI_Score);
      /* report an error */
      return FALSE;
   }
   fwrite ((char *)&MIDI_File_Hdr,sizeof (MIDI_HEADER_TYPE), 1, MIDI_File);
   Track.Chunk_Len = MIDI_Index;
   fwrite ((char *)&Track, sizeof (MIDI_TRACK_TYPE), 1, MIDI_File);
   fwrite ((char *)MIDI_Score, MIDI_Index, 1, MIDI_File);
   fflush (MIDI_File);
   fclose (MIDI_File);
   free (MIDI_Score);

   return TRUE;
}

static int Variable_Length_Values (int Number, unsigned char *Array)
{
   auto unsigned char   Byte4,
                        MSByte, 
                        MIDbyte, 
                        LSByte;
               
   LSByte = (Number & 0x0000007F);
   MIDbyte = ((Number & 0x00003F80) >> 7) | 0x80;
   MSByte = ((Number & 0x001FC000) >> 14) | 0x80;
   Byte4 = ((Number &  0x0FE00000) >> 21) | 0x80;
   if (Byte4 != 0x80)
   {
      Array[0] = Byte4;
      Array[1] = MSByte;
      Array[2] = MIDbyte;
      Array[3] = LSByte;
      return 4;
   }
   if (MSByte != 0x80)
   {
      Array[0] = MSByte;
      Array[1] = MIDbyte;
      Array[2] = LSByte;
      return 3;
   }

   if (MIDbyte != 0x80)
   {
      Array[0] = MIDbyte;
      Array[1] = LSByte;
      return 2;
   }
   Array[0] = LSByte;
  
   return TRUE;
}

static void Insert_Meta_Event (unsigned int *MIDI_Index,
                               unsigned char *MIDI_Score,
                               unsigned char Event_Type,
                               unsigned int Event_Len)
{
   MIDI_Score[*MIDI_Index] = META_EVENT;
   (*MIDI_Index)++;
   MIDI_Score[*MIDI_Index] = Event_Type;
   (*MIDI_Index)++;
   MIDI_Score[*MIDI_Index] = Event_Len;
   (*MIDI_Index)++;

   return;
}

static void Store_a_Note (  unsigned int *MIDI_Index,
                           unsigned char *MIDI_Score,
                           unsigned char Current_Channel,
                           unsigned char Current_Dynamic,
                           unsigned char Pitch,
                           unsigned char *Running_Status)
{
   static MIDI_NOTE_TYPE MIDI_Note;

   MIDI_Note.Status_Channel = Current_Channel;
   MIDI_Note.Note_Number = Pitch;
   MIDI_Note.Velocity = Current_Dynamic;
   if (MIDI_Note.Status_Channel == *Running_Status)
   {
      memcpy ( &MIDI_Score[*MIDI_Index], &(MIDI_Note.Note_Number), 2);
      (*MIDI_Index) += 2;
   }
   else  /* running status changed */
   {
      memcpy (&MIDI_Score[*MIDI_Index], &MIDI_Note, sizeof MIDI_Note);
      (*MIDI_Index) += sizeof (MIDI_NOTE_TYPE);
      *Running_Status = MIDI_Note.Status_Channel;
   }
   return;
}
