
/* MidiOut.c */

/* Musical Notation Editor for X, Chris Cannam 1994    */
/* Some Midi output fns.  Thanks to Andy for midi code */

#include "General.h"
#include "Tags.h"
#include "Classes.h"
#include "Notes.h"
#include "Stave.h"
#include "StaveEdit.h"
#include "StavePrivate.h"
#include "Format.h"
#include "ItemList.h"

#include <Yawn.h>
#include <MidiFile.h>
#include <MidiTrack.h>

static int midiBarStart = 0;	/* JPff */
extern int ampBarEmphasis;	/* JPff */


static void MidiWriteTrackZero(MajorStave sp, void *fp, String name)
{
  MajorStaveRec *mstave = (MajorStaveRec *)sp;
  MIDIFileHandle file = (MIDIFileHandle) fp;
  ItemList       items;
  EventList      events;
  MIDIEvent      endEvent;
  long           delta = 0;
  EventList      iTempo;

  Begin("MidiWriteTrackZero");

  events = Midi_EventCreateList
    (Midi_EventCreateTextEvt(MIDI_TRACK_NAME, 0, name), False);

  Nconc(events, Midi_EventCreateList
	(Midi_EventCreateTextEvt
	 (MIDI_TEXT_MARKER, 0,
	  "Created by the Rosegarden editor"), False));

  Nconc(events, iTempo =
	Midi_EventCreateList(Midi_EventCreateTempoEvt(0, 120), False));

  items = (ItemList)First(mstave->music[0]);

  while (items) {

    if (items->item->object_class == GroupClass) {

      Group *group = (Group *)items->item;

      if (group->group.type == GroupTupled) { /* have to miss anything in it */
	items = group->group.end;
	delta += (10L * MTimeToNumber(group->methods->get_length(group)))
	  / TagToNumber(Hemidemisemiquaver, False);
      } else {
	items = iNext(items);
      }

      continue;
    }

    switch (items->item->object_class) {

    case KeyClass:
      Nconc(events, Midi_EventCreateList
	    (Midi_EventCreateKeySigEvt
	     (delta, (byte)
	      (((Key *)items->item)->key.visual->sharps ?
	       ((Key *)items->item)->key.visual->number :
	       - ((Key *)items->item)->key.visual->number), (byte)0), False));
      break;

    case MetronomeClass:
      Nconc(events, Midi_EventCreateList
	    (Midi_EventCreateTempoEvt
	     (delta, (long)
	      (((Metronome *)items->item)->metronome.setting *
	       MTimeToNumber(((Metronome *)items->item)->metronome.beat_length)
	       / TagToNumber(Crotchet, False))), False));

      if (iTempo && delta == 0) { Remove(iTempo); iTempo = NULL; }
      break;

    default: break;
    }

    delta += (10L * MTimeToNumber(items->item->methods->get_length
				  (items->item)))
      / TagToNumber(Hemidemisemiquaver, False);
  
    items = iNext(items);
  }

  endEvent = (MIDIEvent)XtMalloc(sizeof(MIDIEventStruct));
  endEvent->DeltaTime = delta;
  endEvent->EventCode = MIDI_FILE_META_EVENT;
  endEvent->EventData.MetaEvent.MetaEventCode = MIDI_END_OF_TRACK;
  endEvent->EventData.MetaEvent.NBytes = 0;

  Nconc(events, Midi_EventCreateList(endEvent, False));
  Midi_FileWriteTrack(file, events);
  Midi_TrackDelete(events);

  End;
}


static void MidiWriteTrack(String name, ItemList list,
			   StaveEltList barList, int staveNo, void *fp)
{
  MIDIFileHandle file = (MIDIFileHandle)fp;
  ClefTag        clef = TrebleClef;
  long           delta = 0;
  EventList      events;
  EventList      events2p;
  MIDIEvent      endEvent;
  Group         *group;
  ItemList       i;
  Bar           *bar;
  Bar           *nextBar;

  Begin("MidiWriteTrack");

  events = Midi_EventCreateList
    (Midi_EventCreateTextEvt(MIDI_TRACK_NAME, 0, name), False);

  bar = barList->bars[staveNo];
  nextBar = Next(barList) ? (((StaveEltList)Next(barList))->bars[staveNo]) : 0;

  /* First bar time signature */

  Nconc(events, Midi_EventCreateList
	(Midi_EventCreateTimeSigEvt
	 (0, bar->bar.time.numerator, bar->bar.time.denominator), False));

  /* Find the original clef */

  for (i = list; i; i = iNext(i))
    if (i->item->object_class == ClefClass) {
      clef = ((Clef *)i->item)->clef.clef;
      break;
    }

  /* Get the events */

  for (i = list; i; i = iNext(i)) {

    /* start of a new bar? */

    if (nextBar && nextBar->group.start == i) {

      if (!BarTimesEqual(bar, nextBar)) {
	Nconc(events, Midi_EventCreateList
	      (Midi_EventCreateTimeSigEvt(delta, nextBar->bar.time.numerator,
					  nextBar->bar.time.denominator),
	       False));
      }

      barList = (StaveEltList)Next(barList);
      if ((bar = nextBar)) {
	nextBar = Next(barList) ?
	  (((StaveEltList)Next(barList))->bars[staveNo]) : 0;
      }

      midiBarStart = ampBarEmphasis;
    }

    if (i->item->object_class == ClefClass) clef= ((Clef *)i->item)->clef.clef;

    if (i->item->object_class == GroupClass) {

      long savedDelta = delta;

      group = ((Group *)i->item);
      if (group->group.type != GroupTupled) continue;

      for (ItemList_ITERATE_GROUP(i, group)) {
	/* use staveNo for channel number, for now */
	i->item->methods->write_midi(i->item, (List)events, &delta,
				     staveNo, clef, group);
      }

      i = group->group.end;
      delta = savedDelta + ((10 * MTimeToNumber(group->methods->get_length
					       ((MusicObject)group)))
			    / TagToNumber(Hemidemisemiquaver, False));
      continue;
    }

    i->item->methods->write_midi(i->item, (List)events, &delta,
				 staveNo, clef, NULL);
    events = (EventList)Last(events);
  }

  endEvent = (MIDIEvent)XtMalloc(sizeof(MIDIEventStruct));
  endEvent->DeltaTime = delta;
  endEvent->EventCode = MIDI_FILE_META_EVENT;
  endEvent->EventData.MetaEvent.MetaEventCode = MIDI_END_OF_TRACK;
  endEvent->EventData.MetaEvent.NBytes = 0;
  Nconc(events, Midi_EventCreateList(endEvent, False));
  events = (EventList)First(events);

  /* And write them out */

  events2p = Midi_TrackConvertToTwoPointRepresentation(events);
  Midi_FileWriteTrack(file, events2p);
  Midi_TrackDelete(events2p);
  Midi_TrackDelete(events);
}


Result MidiWriteStave(MajorStave sp, String fname, String title)
{
  MajorStaveRec  *mstave = (MajorStaveRec *)sp;
  MIDIHeaderChunk header;    
  MIDIFileHandle  file;
  String          msg;
  int             i;
  
  Begin("MidiWriteStave");

  header.Format = MIDI_SIMULTANEOUS_TRACK_FILE;
  header.NumTracks = mstave->staves + 1;
  header.Timing.Division = 160;

  StaveBusyStartCount(mstave->staves*2 + 1);
  StaveBusyMakeCount(0);

  if ((file = Midi_FileOpen(fname, &header, MIDI_WRITE)) == NULL) {
    
    msg = (String)XtMalloc(300);

    if (!title) sprintf(msg, "Sorry, I can't open `%s' for writing.", fname);
    else        sprintf(msg, "Sorry, I can't open a temporary file.");

    XBell(display, 70);
    (void)YQuery(topLevel, msg, 1, 0, 0, "Cancel", NULL);

    XtFree((void *)msg);
    Return(Failed);
  }

  if (title) {

    MidiWriteTrackZero(sp, (void *)file, title);

  } else {

    for (msg = fname + strlen(fname) - 1; msg >= fname; --msg)
      if (*msg == '/') break;

    if (msg <= fname) MidiWriteTrackZero(sp, (void *)file, fname);
    else              MidiWriteTrackZero(sp, (void *)file, ++msg);
  }

  for (i = 0; i < mstave->staves; ++i) {

    StaveBusyMakeCount(i*2 + 1);
    UnformatItemList(&mstave->music[i], NULL);

    StaveBusyMakeCount(i*2 + 2);
    MidiWriteTrack(mstave->names[i], mstave->music[i],
		   (StaveEltList)First(mstave->bar_list), i, (void *)file);
    StaveResetFormatting(sp, i);
  }

  Midi_FileClose(file);

  staveMoved = True;
  StaveRefresh(stave, -1);
  StaveBusyFinishCount();

  Return(Succeeded);
}



void MidiWriteNothing(MusicObject obj, List events, long *delta, int channel,
		      ClefTag clef, MusicObject group)
{
  Begin("MidiWriteNothing");
  End;
}


/* So we can emphasise first note (JPff change, not finished) */
void MidiWriteRest(MusicObject obj, List events, long *delta, int channel,
		   ClefTag clef, MusicObject group)
{
  Begin("MidiWriteRest");
  midiBarStart = 0;
  *delta += (10L * MTimeToNumber(((Rest *)obj)->methods->get_length(obj)))
    / TagToNumber(Hemidemisemiquaver, False);
  End;
}


void MidiWriteText(MusicObject obj, List events, long *delta, int channel,
		   ClefTag clef, MusicObject group)
{
  Text *text = (Text *)obj;

  Begin("MidiWriteText");

  Nconc(events, Midi_EventCreateList
	(Midi_EventCreateTextEvt(MIDI_TEXT_EVENT, *delta, text->text.text),
	 False));

  End;
}


int VoiceToMidiPitch(NoteVoice *voice, ClefTag clef)
{
  int rtn;
  int octave = 5;
  Pitch pitch = voice->pitch;

  Begin("VoiceToMidiPitch");

  while (pitch < 0) { octave -= 1; pitch += 7; }
  while (pitch > 7) { octave += 1; pitch -= 7; }

  if (pitch > 4) ++octave;

  switch(pitch) {

  case 0: rtn =  4; break;	/* bottom line, treble clef: E */
  case 1: rtn =  5; break;	/* F */
  case 2: rtn =  7; break;	/* G */
  case 3: rtn =  9; break;	/* A, in next octave */
  case 4: rtn = 11; break;	/* B, likewise*/
  case 5: rtn =  0; break;	/* C, moved up an octave (see above) */
  case 6: rtn =  2; break;	/* D, likewise */
  case 7: rtn =  4; break;	/* E, likewise */
  }

  if (voice->modifiers & ModSharp) ++ rtn;
  if (voice->modifiers & ModFlat)  -- rtn;

  switch(clef) {

  case  TrebleClef: break;
  case   TenorClef: octave -= 1; break;
  case    AltoClef: octave -= 1; break;
  case    BassClef: octave -= 2; break;
  case InvalidClef: break;
  }

  rtn += 12 * octave;

  Return(rtn);
}


static Boolean ThisChordTiedForward(Chord *c)
{
  MarkList mlist;
  Begin("ThisChordTiedForward");

  for (mlist = (MarkList)First(c->item.marks); mlist;
       mlist = (MarkList)Next(mlist)) {

    if (mlist->mark->type == Tie && mlist->mark->start) Return(True);
  }

  Return(False);
}


void MidiWriteChord(MusicObject obj, List events, long *delta, int channel,
		    ClefTag clef, MusicObject g)
{
  int      i;
  Chord   *chord = (Chord *)obj;
  long     length;
  double   ratio = 1.0;
  Group   *group = (Group *)g;

  static long rememberLength = 0L;

  Begin("MidiWriteChord");

  if (group && group->group.type == GroupTupled) {

    group->group.type = GroupNoDecoration;
    length = MTimeToNumber(group->methods->get_length(group));
    group->group.type = GroupTupled;

    ratio = ((double)MTimeToNumber(group->methods->get_length(group))) /
      (double)length;

    length = (long)
      ((10.0 * ratio * (double)MTimeToNumber(chord->methods->get_length
					     ((MusicObject)chord)))
       / (double)TagToNumber(Hemidemisemiquaver, False));

  } else {

    length = (10 * MTimeToNumber(chord->methods->get_length
				 ((MusicObject)chord)))
      / TagToNumber(Hemidemisemiquaver, False);

    if (ThisChordTiedForward(chord)) {
      rememberLength = length;
      End;
    } else {
      length += rememberLength;
      rememberLength = 0L;
    }
  }

  /* arg 4 of Midi_EventCreateNote changed from    */
  /* plain (byte)64 by JPff for bar-start emphasis */

  for (i = 0; i < chord->chord.voice_count; ++i) {
    byte pitch = (byte)VoiceToMidiPitch(&chord->chord.voices[i], clef);
    byte emph =
      (byte)((chord->chord.modifiers & ModAccent ? 100:64)+midiBarStart);

    Nconc(events, Midi_EventCreateList
	  (Midi_EventCreateNote(*delta, (byte)channel, pitch, emph, length),
	   False));
  }

  *delta += length;
  
  midiBarStart = 0;		/* Emphasis on first note of bar */
  End;
}

