/*
  !MidiPlay   A MIDI synthesiser and file player.

  player.c - MIDI file player

  created  16/07/14
*/

#include "main.h"
#include "kbd.h"
#include "player.h"
#include "midisyn.h"
#include "kbd_dsplay.h"

#define BUFF_LEN          256      // text buffer lengths
#define MAX_MIDI_FILE_LEN 0x100000 // 1MB
#define NUM_TRACKS        64       // maximum number of tracks for type 1 files

mp_global_t mpg;

// configuration, not file specific
typedef struct mp_confg_s
{
  int tempo;     // user tempo scaler in percent, 10% to 1000%
  int transpose; // user pitch transposition in semitones signed, -12 to +12
  int loop;      // true if player is looping
} mp_config_t;

// file information
typedef struct mp_info_s
{
  int nn;        // numerator of the time signature as it would be notated
  int dd;        // denominator of the time signature as it would be notated
  int cc;        // MIDI clocks in a metronome click
  int bb;        // number of notated 32nd-notes in a quarter-note
  int sf;        // -7..+7, number of sharps or flats in the key, positive for major keys, negative for minor keys
  int mi;        // mi = 0: major key, mi = 1: minor key
} mp_info_t;

// track data
typedef struct mp_track_s
{
  int start;     // track start position in file
  int leni;      // initial length of track
  int pos;       // index to current position within file
  int len;       // number of bytes to read in track
  int state;     // player state control
  int delta;     // current event delta time
  int status;    // current event status
  int event;     // current event type
} mp_track_t;

// recording control, when saving the created audio
typedef struct mp_record_s
{
  FILE *f;
  int n;
  int active;
  wave_pcm_t h;
  char name[BUFF_LEN]; // full pathname of current recording file
} mp_record_t;


// file player
typedef struct midi_player_s
{
  mp_config_t config;        // configuration

  unsigned char *file;       // pointer to file
  int mflen;                 // file length in bytes
  int flags;                 // see below
  int format;                // format type, 0,1,2
  int tracks;                // number of tracks
  unsigned int division;     // delta time ticks per quarter note
  unsigned int tempo;        // microseconds per quarter note
  unsigned int scaled_tempo; // tempo scaled by user control config.tempo
  unsigned int bps;          // beats per second, fixed point fractional, see QBPS below
  unsigned int clock_timer;  // song timer    )
  unsigned int position;     // song position ) units of 1/CLK_RATE seconds
  unsigned int duration;     // song length   )
  unsigned int delta_timer;  // )
  unsigned int numerator;    // ) delta time clock prescaler
  unsigned int denominator;  // )
  int last_msg;              // time in seconds of the last midi msg, used to sense long gaps in the song
  int lyric_trk;             // lyric track number
  int long_gap;              // gap in seconds with no messages after which the player assumes the song has ended
  int finished;              // counts tracks that are finished
  int lead_out;              // counter to provide a few seconds after the file end before stopping

  mp_record_t rec;           // recording control

  mp_info_t mi;              // file information

  mp_track_t tk[NUM_TRACKS]; // track data

  char name[BUFF_LEN];       // currently loaded filename, as displayed

} midi_player_t;

#define QBPS          12 // number of fractional bits in mp.bps
#define CLK_RATE      1  // position and duration resolution in counts per second (1 or 10)

//note: mp.tempo and mp.scaled_tempo are beat periods in us, not rates.
//      mp.config.tempo is a percentage of the derived tempo rate.
//      i.e. increases in mp.config.tempo reduce mp.scaled_tempo.

static midi_player_t mp;


// midi_player_t.flags, bits
enum
{
  FILE_PLAY,   // (entry option) TRUE when player is playing or paused
  FILE_INFO,   // (entry option) TRUE when player is scanning the file for information
  QUIET,       // (entry option) TRUE to suppress all messages
  PAUSED,      // (control)      TRUE when player is paused
  SEEKING      // (internal use) TRUE when seeking to a position
};


// read_chunk_type(), return codes
enum
{
  MTxx,        // unknown chunk
  MThd,        // header chunk
  MTrk         // track chunk
};


// read_midi_track(), states and return codes
#define RESCHEDULED 0 // return code
#define FINISHED    1 // return code
#define DELTA       0 // track state
#define EVENT       1 // track state

#define DEFAULT_BPM  120 // default tempo in beats per minute
#define LONG_GAP      10 // default gap in seconds after which the player assumes the song has ended
#define EXTRA_SECS     1 // Extra seconds to play after the file has finished to allow notes to conclude.


void display_error(int err, char *info); // command.c
extern const char sw_version[]; // version.c
static int player_tempo(int data);


/*
 * midiPlayer_new
 * --------------
 */
void midiPlayer_new(void)
{
  int err = NO_ERROR_;

  memset(&mpg, 0, sizeof(mpg));
  memset(&mp, 0, sizeof(mp));

  midiPlayer_cmds("VERSION");
  int data = (int)sw_version;
  midiPlayer_display(ITEM_VERSION, &data);

  mp.config.tempo = 100; // percent
  if((err = midiSynth_init(44100)) < NO_ERROR_) // set sample rate
    display_error(err, "midiSynth_init");

  // load Choices if possible
  FILE *f;
  if((f = fopen(APP_DIR".Choices", "r")) != 0)
  {
    char c;
    while(fread(&c, 1, 1, f))
      midiPlayer_cmd_in(c);
    fclose(f);
  }
  else // set defaults
    midiPlayer_cmds("VOLUME,512\n" // master volume (-6dB).
                    "BALANCE,0\n"  // centre balance
                    "TONE,I\n"     // enable tone controls
                    "CHORUS,I\n"   // enable chorus
                    "REVERB,D\n"   // disabe reverb
                    "FLANGER,D\n"  // disable the flanger
                    "GLIDE,100\n"  // set portamento rate for the current midi spec
                    "ECHO,D\n");   // disable echo
}


/*
 * midiPlayer_term
 * ---------------
 */
void midiPlayer_term(void)
{
  if(mpg.log)
    fclose(mpg.log);
  if(mp.file)
    free(mp.file);

  // save Choices
  if((mpg.choices = fopen(APP_DIR".Choices", "w")) == NULL)
    myprintf("Cannot open Choices file\n");
  else
  {
    fprintf(mpg.choices, "# "APP_NAME" settings\n");
    midiPlayer_cmds(
                    "AUDIO!\n"
                    "BALANCE!\n"
                    "CHORUS!\n"
                    "ECHO!\n"
                    "FLANGER!\n"
                    "GLIDE!\n"
                    "PITCH!\n"
                    "PLAYER!\n"
                    "REVERB!\n"
                    "TEMPO!\n"
                    "TONE!\n"
                    "VOLUME!\n"
                    "BUFFER!\n"
                    "SLEEP!\n"
                    "RATE!\n"
                    "INTERFACE!\n"
                    );
    fclose(mpg.choices);
  }

  midiSynth_term();
  memset(&mp, 0, sizeof(mp));
}


/*
 * midi_msg
 * --------
 * Sends a midi event to the interpreter.
 */
static void midi_msg(int status, unsigned char *buff, int n)
{
  if((status & 0xf) != (PERCUSSION_CHAN-1))
    if(((status & 0xf0) == NOTE_ON) || ((status & 0xf0) == NOTE_OFF))
      buff[0] += mp.config.transpose;

  if(status)
    if(mpg.debug & (1<<DEBUG_MIDI))
      print_midi_msg(PLAYER, status, buff, n);

  if(n < 2)
  {
    buff[1] = 0;
    if(n < 1)
      buff[0] = 0;
  }

  midiSynth_midi_in(status);
  while(n--)
    midiSynth_midi_in(*buff++);
}


/*
 * read_var_len
 * ------------
 * updates the global "len"
 * returns the value
 */
static unsigned int read_var_len(int trk)
{
  unsigned int c, n = 0, value = 0;

  do
  {
    c = mp.file[mp.tk[trk].pos++];
    value = (value << 7) | (c & 0x7f);
    n++;
  } while(c & 0x80);

  mp.tk[trk].len -= n;
  return value;
}


/*
 * read_int
 * --------
 * n = number of bytes to read
 * updates the global "len"
 * returns the value
 */
static unsigned int read_int(int trk, int n)
{
  unsigned int value = 0;

  mp.tk[trk].len -= n;
  while(n--)
    value = (value << 8) | mp.file[mp.tk[trk].pos++];

  return value;
}


/*
 * read_chunk_type
 * ---------------
 * returns a tag depending on the 4 char string found
 */
static int read_chunk_type(int trk)
{
  unsigned int i;
  char buff[4];

  for(i = 0; i < 4; i++)
    buff[i] = mp.file[mp.tk[trk].pos++];

  if(strncmp(buff, "MThd", 4) == 0)
    return MThd;

  if(strncmp(buff, "MTrk", 4) == 0)
    return MTrk;

  return MTxx;
}


/*
 * read_text
 * ---------
 */
static void read_text(int trk, char *type, int n, int meta)
{
  if(!(mp.flags & (1<<QUIET)))
  {
    if(mpg.debug & (1<<DEBUG_MIDI))
      myprintf("track %d, %s: ", trk, type);

    int len = n;
    unsigned char *b =  mp.file + mp.tk[trk].pos;

    // some files have lyrics coded as text types so need to check for both. Unfortunately some
    // files have both! so for those files the lyrics get printed twice.
    if(((meta == 1)||(meta == 5)) && (mp.flags & (1<<FILE_PLAY))) // text or lyrics
    {
      char c = *b;
      if((mp.lyric_trk == 0) && ((c == '\\')||(c == '/')))
        mp.lyric_trk = trk;

      if((mp.lyric_trk == 0) || (mp.lyric_trk == trk))
      {
        if(c == '@') // title, credits, etc.
        {
          switch(b[1])
          {
//            case 'K': // Type of file / copyright information. eg. MIDI KARAOKE FILE
//            case 'L': // language of the song. eg. ENGL
//            case 'V': // Version number. eg. 0100
            case 'T': // Title of song, artist, sequencer
//            case 'I': // Other information
              b += 2; // skip the '@' and string qualifier
              while(len-- > 2)
                myprintf("%c", *b++);
              myprintf("\n");
              break;
          }
        }

        else // lyrics
          while(len-- > 0)
          {
            c = *b++;
            if(c & 0x80)
              c = '.';
            else if((c == '\r') || (c == '/')) // new line
              c = '\n';
            else if(c == '\\') // new paragraph or verse, extra new line
            {
              c = '\n';
              myprintf("%c", c);
            }
            myprintf("%c", c);
          }
      }
    }

    // not text or lyrics
    else if((meta != 1) && (meta != 5) && (mpg.debug & (1<<DEBUG_MIDI)))
//    if((meta != 1) && (meta != 5) && ((mpg.debug & (1<<DEBUG_MIDI)) || (mp.flags & (1<<FILE_INFO))))
    {
      myprintf("track %d, %s: ", trk, type);
      while(n--)
      {
        unsigned char c = *b++;
        if(c & 0x80)
          c = '.';
        myprintf("%c", c);
        if(c == '\r')
          myprintf("\n");
      }
      myprintf("\r\n");
      return;
    }
/*
    else if(meta == 3) // track name
    {
      myprintf("track %d: ", trk);
      while(n--)
      {
        unsigned char c = read_int(trk, 1);
        if(c & 0x80)
          c = '.';
        myprintf("%c", c);
        if(c == '\r')
          myprintf("\n");
      }
      myprintf("\r\n");
      return;
    }
*/
  }
}


/*
 * read_data
 * ---------
 */
static void read_data(int trk, char *type, int n, int meta)
{
  unsigned char *b = mp.file + mp.tk[trk].pos;

  if(mpg.debug & (1<<DEBUG_MIDI))
    myprintf("Meta: track %d, %s: ", trk, type);

  int i;
  for(i=0; i<n; i++)
    if(mpg.debug & (1<<DEBUG_MIDI))
      myprintf("%02X ", b[i]);
  if(mpg.debug & (1<<DEBUG_MIDI))
    myprintf("\n");

  switch(meta)
  {
    case 0x51: // tempo
      mp.tempo = (b[0] << 16) | (b[1] << 8) | b[2]; // microseconds per MIDI quarter-note (beat, 24 MIDI clocks)
      mp.scaled_tempo = (mp.tempo * 100) / mp.config.tempo;
      mp.bps = ((1000000 << QBPS) + (mp.scaled_tempo >> 1)) / mp.scaled_tempo; // rounded
      mp.denominator = mp.division * mp.bps;
      break;

    case 0x58: // time signature, unused, only for info
      mp.mi.nn = b[0]; // numerator  of the time signature as  it  would  be notated
      mp.mi.dd = 1 << b[1]; // denominator of the time signature as  it  would  be notated
      mp.mi.cc = b[2]; // MIDI clocks in a  metronome  click
      mp.mi.bb = b[3]; // the  number  of  notated  32nd-notes  in  a  MIDI quarter-note (24 MIDI clocks)
      break;

    case 0x59: // key signature, unused, only for info
      mp.mi.sf = b[0] | 0x80000000; // set bit 31 to indicate that it has been loaded
      mp.mi.mi = b[1];
      break;
  }
}


/*
 * read_midi_track
 * ---------------
 * Interprets the track data continuously until a delta time greater than zero is encountered.
 * It then starts a timer which when expired calls this task to continue the process. This
 * continues until the end of track is encountered.
 * Returns RESCHEDULED or FINISHED
 */
static int read_midi_track(int trk)
{
  int n = 0, meta;
  unsigned char buff[4], *b = buff;
  mp_track_t *t = &mp.tk[trk];

  for(;;)
    switch(t->state)
    {
      case DELTA:
        if((t->len <= 0) || (t->pos > mp.mflen))
          return FINISHED;

        t->state = EVENT;
        t->delta = read_var_len(trk);
        if(t->delta > 0)
          return RESCHEDULED;
        // if delta is zero, follow through to EVENT

      case EVENT:
        t->event = read_int(trk, 1);
        switch(t->event)
        {
          case 0xf0: // sysex
            n = read_var_len(trk);
            midi_msg(t->event, mp.file + t->pos, n);
            t->len -= n;
            t->pos += n;
            break;

          case 0xf7: // escape
            if(mpg.debug & (1<<DEBUG_MIDI))
              myprintf("%02X ", t->event);
            n = read_var_len(trk);
            midi_msg(mp.file[t->pos], mp.file + t->pos + 1, n - 1);
            t->len -= n;
            t->pos += n;
            break;

          case 0xff: // meta
            meta = read_int(trk, 1);
            n = read_var_len(trk);
            switch(meta)
            {
              case 0x01: read_text(trk, "text",               n, meta); break;
              case 0x02: read_text(trk, "copyright",          n, meta); break;
              case 0x03: read_text(trk, "name",               n, meta); break;
              case 0x04: read_text(trk, "instrument",         n, meta); break;
              case 0x05: read_text(trk, "lyric",              n, meta); break;
              case 0x06: read_text(trk, "marker",             n, meta); break;
              case 0x07: read_text(trk, "cue point",          n, meta); break;
              case 0x08: read_text(trk, "program name",       n, meta); break;
              case 0x09: read_text(trk, "device name",        n, meta); break;

              case 0x00: read_data(trk, "sequence number",    n, meta); break;
              case 0x20: read_data(trk, "channel prefix",     n, meta); break;
              case 0x21: read_data(trk, "port prefix",        n, meta); break;
              case 0x51: read_data(trk, "tempo",              n, meta); break;
              case 0x54: read_data(trk, "SMPTE offset",       n, meta); break;
              case 0x58: read_data(trk, "time signature",     n, meta); break;
              case 0x59: read_data(trk, "key signature",      n, meta); break;
              case 0x7f: read_data(trk, "sequencer specific", n, meta); break;

              case 0x2f: read_data(trk, "track end",          n, meta);
                return FINISHED;
            }
            t->len -= n;
            t->pos += n;
            break;

          default: // midi channel message
            if(t->event < 0xf0) // skip invalid events f1-f6 f8-fe
            {
              if(t->event >= 0x80) // midi status
              {
                t->status = t->event;
                switch(t->status & 0xf0)
                {
                  case NOTE_OFF:
                  case NOTE_ON:
                  case KEY_PRESSURE:
                  case CONTROL:
                  case PITCH_WHEEL:
                    *b++ = read_int(trk, 1);
                  case PROGRAM:
                  case CHAN_PRESSURE:
                    *b++ = read_int(trk, 1);
                }
              }
              else // < 0x80, running status
              {
                *b++ = t->event;
                switch(t->status & 0xf0)
                {
                  case NOTE_OFF:
                  case NOTE_ON:
                  case KEY_PRESSURE:
                  case CONTROL:
                  case PITCH_WHEEL:
                    *b++ = read_int(trk, 1);
                }
              }
              if(!(mp.flags & (1<<FILE_INFO)) ||
                 ((mp.flags & (1<<SEEKING)) && ((t->status & 0xf0) != NOTE_OFF) && ((t->status & 0xf0) != NOTE_ON)))
                midi_msg(t->status, buff, b - buff);
              b = buff;
              mp.last_msg = mp.position; // to check for long gaps
            }
            break;
        }
        t->state = DELTA;
        break;
    }

  return NO_ERROR_; // this should never happen
}


/*
 * replay_midi_file
 * ----------------
 * Restart playback, used when looping and also when seeking backwards.
 */
static void replay_midi_file(void)
{
  int t;

  midiSynth_reset();
  mp.finished = 0;
  mp.last_msg = 0;
  mp.position = 0;
  mp.clock_timer = 0;
  mp.delta_timer = 0;
  for(t=1; t <= mp.tracks; t++)
  {
    mp.tk[t].len = mp.tk[t].leni;  // reset length of track
    mp.tk[t].pos = mp.tk[t].start; // reset current position to start
    mp.tk[t].state = DELTA;        // reset current state
    mp.finished += read_midi_track(t);  // start playback, read_midi_track will reschedule itself for each track as required
  }
}


/*
 * midiPlayer_status
 * -----------------
 * Returns a status word as follows
 * bit 0: set if File Loaded
 * bit 1: set if Playing
 * bit 2: set if Recording
 * bit 3: set if Looping
 * bit 4: set if Paused
 * bit 5: set if current instrument is a percussion kit
 * bits 31..8 sample_rate
 */
int midiPlayer_status(void)
{
  int status =
      (mp.mflen > 0)
    | (((mp.flags >> FILE_PLAY) & 1) << 1)
    | (mp.rec.active << 2)
    | (mp.config.loop << 3)
    | (((mp.flags >> PAUSED) & 1) << 4)
    | ((syn.kbd_patch.list[syn.kbd_patch.cur].hi == PERCUSSION_BANK) << 5)
    | (syn.sample_rate << 8);

  return status;
}


/*
 * midi_file_player
 * ----------------
 * handles the rescheduling, expects to be called at syn.sample_rate
 */
void midi_file_player(void)
{
  int t;
  int data[2];

  if(!(mp.flags & (1<<FILE_PLAY)) || (mp.flags & (1<<PAUSED)))
    return;

  if(mp.lead_out)
  {
    if(!--mp.lead_out)
    {
      mp.flags = 0;
      midiSynth_reset();
      int data = 0;
      midiPlayer_display(ITEM_PAUSE, &data);
      midiPlayer_display(ITEM_START, &data);
      data = 1;
      midiPlayer_display(ITEM_STOP, &data);
    }
    return;
  }

  mp.delta_timer += mp.denominator;
  while(mp.delta_timer >= mp.numerator)
  {
    mp.delta_timer -= mp.numerator;

    // following code executes at the delta tick rate

    // read tracks
    for(t=1; t <= mp.tracks; t++)
      if(mp.tk[t].delta)
        if(!--mp.tk[t].delta)
          mp.finished += read_midi_track(t);

    // update elapsed time
    if((mp.clock_timer += (CLK_RATE<<QBPS)) >= mp.denominator)
    {
      mp.clock_timer -= mp.denominator;
      mp.position++;
      data[0] = mp.position / CLK_RATE;
      data[1] = mp.duration / CLK_RATE;
      midiPlayer_display(ITEM_POSN, data);
    }

    // check if all tracks done, or a long gap
    if((mp.finished >= mp.tracks) || ((mp.position - mp.last_msg) >= mp.long_gap))
    {
      // all finished, restart if looping
      if(mp.config.loop)
      {
        replay_midi_file();
        data[0] = 0;
        data[1] = mp.duration;
        midiPlayer_display(ITEM_POSN, data);
      }
      else // if not looping, stop player, or start timer to stop player later
      {
        data[0] = -1;
        data[1] = 0;
        midiPlayer_display(ITEM_POSN, data);
        if((mp.lead_out = syn.sample_rate * EXTRA_SECS) == 0)
          mp.flags = 0;
      }
      mp.rec.active = FALSE;
    }
  }
}


/*
 * midiPlayer_fastseek
 * ---------------
 * pos = seconds or MAX_INT.
 * Reposition tracks to a given position.
 * mp.position is updated on exit with either pos or song duration if the end is reached first.
 */
static void midiPlayer_fastseek(int pos)
{
  int t;

  // we can seek forwards from the current posiition but to seek backwards we need to restart
  if((pos < mp.position) || (mp.position == 0)) // seeking backwards
    replay_midi_file();
  else // seeking forwards
    reset_all_gates(REMOTE_CHAN); // ensure no notes are left on when seeking forwards

  if (pos)
  {
    // go through the file at top speed to find the seek position
    for(;;)
    {
      for(t=1; t <= mp.tracks; t++)
        if(mp.tk[t].delta)
          if(!--mp.tk[t].delta)
            mp.finished += read_midi_track(t);

      // run the clock
      if((mp.clock_timer += (CLK_RATE<<QBPS)) >= mp.denominator)
      {
        mp.clock_timer -= mp.denominator;
        mp.position++;
      }

      // exit from this loop when ...
      if((mp.position >= pos)                            // the position is found
        || (mp.finished >= mp.tracks)                    // or reached the end of the song
        || ((mp.position - mp.last_msg) >= mp.long_gap)) // or there was a long gap (end assumed)
      {
        mp.delta_timer = 0; // clear the delta timer
        break;
      }
    }
  }
}


/*
 * player_start
 * ------------
 * starts the midi file player. action is either 1<<FILE_INFO or 1<<FILE_PLAY plus optionally 1<<QUIET.
 * FILE_INFO performs a scan of the file for all information required to play the file.
 * FILE_PLAY starts playing the file from the start and requires FILE_INFO to have been called first.
 * returns any errors.
 */
static int player_start(int action)
{
  int t;

  if(mp.mflen <= 0)
    return -NO_FILE_LOADED;

  mp.flags = action;        // set entry options

  if(mp.flags & (1<<FILE_INFO)) // initial file scan when file is loaded into memory
  {
    // track[0] is used for the initial track search
    // track[1] is used for type0 files and is usually the tempo track in type1 files although the code doesn't assume this.
    mp.tk[0].pos = 0;
    mp.tk[0].len = mp.mflen;
    mp.lead_out = 0; // stop the lead out timer

    if(read_chunk_type(0) != MThd)
      return -NO_HEADER;

    if(read_int(0, 4) < 6)
      return -INVALID_HEADER_LENGTH;

    mp.format = read_int(0, 2);
    if((mp.format != 0) && (mp.format != 1))
      return -UNSUPPORTED_FORMAT;

    mp.tracks = read_int(0, 2);

    //if(((mp.format == 0) && (mp.tracks != 1)) || (mp.tracks > (NUM_TRACKS-2)))
    //  return -INVALID_NUMBER_OF_TRACKS;

    // if the following cause problems, go back to the above code, the safety exit approach
    if(mp.format == 0)
      mp.tracks = 1; // try to play type 0 files with dodgy track numbers
    else if(mp.tracks > (NUM_TRACKS-2))
      mp.tracks = NUM_TRACKS-2; // try to play type 1 files with crazy numbers of empty tracks

    // set initial values for the timing variables
    mp.clock_timer = 0;
    mp.position = 0;
    mp.delta_timer = 0;
    mp.division = read_int(0, 2); // delta time tick per quarter note (beat)
    mp.tempo = 60000000 / DEFAULT_BPM;
    mp.scaled_tempo = (mp.tempo * 100) / mp.config.tempo;
    mp.bps = ((1000000 << QBPS) + (mp.scaled_tempo >> 1)) / mp.scaled_tempo; // beats per second, rounded
    mp.numerator = syn.sample_rate << QBPS;
    mp.denominator = mp.division * mp.bps;
    mp.long_gap = (LONG_GAP * CLK_RATE * 100) / mp.config.tempo; // adjust gap detection for changes in tempo
    if(mp.long_gap < 2)
      mp.long_gap = 2;

    mp.last_msg = 0;
    mp.lyric_trk = 0;
    memset(&mp.mi, 0, sizeof(mp.mi));
    memset(&mp.tk, 0, sizeof(mp.tk));

    for(t=1;;)
    {
      // find track start addresses
      int type = read_chunk_type(0);
      int len = read_int(0, 4);
      if(type == MTrk)
      {
        mp.tk[t].leni = len;           // initial length of track
        mp.tk[t].len = len;            // length of track
        mp.tk[t].start = mp.tk[0].pos; // start address of track
        mp.tk[t].pos = mp.tk[0].pos;   // reset current position to start
        mp.tk[t].state = DELTA;        // reset current state
        if(++t > mp.tracks)
          break;
      }
      mp.tk[0].pos += len;
      if(mp.tk[0].pos > mp.mflen)
        return -INCOMPLETE;
    }

    // duration can be obtained by seeking to end of file of infinite duration
    midiPlayer_fastseek(0x7FFFFFFF);
    mp.duration = mp.position; // save duration

    // display the duration
    int secs = mp.position / CLK_RATE;
    int mins = secs / 60;
    secs -= mins * 60;
    char s[16];
    sprintf(s, "%d:%02d", mins, secs);
    int data = (int)s;
    midiPlayer_display(ITEM_LEN, &data);

    mp.flags = 0; // disable player
  }

  else if(mp.flags & (1<<FILE_PLAY)) // start playing the file from the start
  {
    myprintf("\n"); // newline for any heading text
    midiPlayer_fastseek(0);

    // display the position
    int data[2];
    data[0] = 0;
    data[1] = mp.duration / CLK_RATE;
    midiPlayer_display(ITEM_POSN, data);

    midiPlayer_display(ITEM_PAUSE, data);
    midiPlayer_display(ITEM_STOP, data);
    data[0] = 1;
    midiPlayer_display(ITEM_START, data);
  }

  return NO_ERROR_;
}


/*
 * player_seek
 * -----------
 * pos = the clicked position within the song position bar image,
 * max = the maximum that pos can be. i.e. the right end of the bar.
 * The function restarts the song at top speed until the required
 * position is reached when it changes to real time playback.
 * During seeking, midi note on and off messages are not sent to the
 * synth but all other control messages are.
 * Returns any error.
 */
static int player_seek(int pos, int max)
{
  if(mp.mflen <= 0)
    return NO_ERROR_; // no file loaded
  if(!(mp.flags & (1<<FILE_PLAY)))
    return NO_ERROR_; // not playing
  if(mp.flags & (1<<PAUSED))
    return NO_ERROR_; // paused
  if(mp.rec.active)
    return NO_ERROR_; // recording

  pos = (mp.duration * pos) / max; // position in seconds from the song start

  if(mpg.debug & (1<<DEBUG_MIDI))
    myprintf("\nseek %d:%02d\n", pos / 60, pos % 60);

  unsigned int old_flags = mp.flags; // save flags

  mp.flags |= (1<<QUIET)|(1<<FILE_INFO)|(1<<SEEKING);
  midiPlayer_fastseek(pos);

  mp.flags &= old_flags;  // restore flags

  // display the position
  int data[2];
  data[0] = mp.position / CLK_RATE;
  data[1] = mp.duration / CLK_RATE;
  midiPlayer_display(ITEM_POSN, data);

  // midi_file_player will take over from here
  return NO_ERROR_;
}


/*
 * midiPlayer_info
 * ---------------
 * Display information about the currently loaded file
 */
void midiPlayer_info(void)
{
  // key signature strings
  static const char *key_sig[2][15] =
  {
    { // major keys
      "Cb Major","Gb Major","Db Major","Ab Major","Eb Major","Bb Major","F Major", // flats 7 to 1
      "C Major",
      "G Major","D Major","A Major","E Major","B Major","F# Major","C# Major"      // sharps 1 to 7
    },
    { // minor keys
      "Ab Minor","Eb Minor","Bb Minor","F Minor","C Minor","G Minor","D Minor",    // flats 7 to 1
      "A Minor",
      "E Minor","B Minor","F# Minor","C# Minor","G# Minor","D# Minor","A# Minor"   // sharps 1 to 7
    }
  };

  myprintf("\nPlayer\n");
  if(mp.mflen == 0)
    myprintf("  no file loaded\n");
  else
  {
    myprintf("  file: %s\n", mp.name);
    myprintf("    type: %d\n", mp.format);
    if(mp.format != 0)
      myprintf("    tracks: %d\n", mp.tracks);
    myprintf("    current tempo: %d bpm\n", 60000000 / mp.tempo);

    if((mp.mi.nn != 0) && (mp.mi.dd != 0))
      myprintf("    time signature: %d/%d\n", mp.mi.nn, mp.mi.dd);

    if(mp.mi.sf & 0x80000000)
    {
      int sf = (mp.mi.sf << 25) >> 25; // 7 bit sign extend

      if((sf >= -7) && (sf <= 7) && (mp.mi.mi >= 0) && (mp.mi.mi <= 1))
        myprintf("    key signature: %s\n", key_sig[mp.mi.mi][sf + 7]);
    }

    int secs = mp.duration / CLK_RATE;
    int mins = secs / 60;
    secs -= mins * 60;
    myprintf("    duration: %d mins %d secs\n", mins, secs);

  }
/*
  printf("  pitch transpose: %d semitones\n", mp.config.transpose);
  printf("  tempo scale factor: %d%%\n", mp.config.tempo);

  printf("\nSynth\n");
  printf("  sound set: %s\n", syn.idat.name);
  printf("  audio output: %s\n",
          (syn.switches & (1<<MONO_AUDIO)) ? "Mono" :
          (syn.switches & (1<<L_R_SWAP)) ? "Stereo L/R swapped" : "Stereo");

  printf("  glide time scale factor: %d\n", syn.scale_factor);
  printf("  polyphony: %d\n", syn.num_gens);
  printf("  sample rate: system %d, synth %d\n\n", mod.audio.sys_rate >> 10, syn.sample_rate);
*/
  // testing
  if(mpg.debug & (1<<DEBUG_MIDI))
  {
    myprintf("Timing Details\n");
    if(!mp.mflen)
      myprintf("  No file loaded\n");
    else
    {
      myprintf("  delta rate: %d Hz\n", mp.denominator >> QBPS);
      myprintf("  division: %d\n", mp.division);
      myprintf("  tempo: %d\n", mp.tempo);
      myprintf("  scaled tempo: %d\n", mp.scaled_tempo);
      myprintf("  bps: &%X.%03X (bpm %d)\n", mp.bps >> QBPS, mp.bps & ((1<<QBPS)-1), ((mp.bps * 60)+(1<<(QBPS-1))) >> QBPS);
      myprintf("  numerator:   %10u\n", mp.numerator);
      myprintf("  denominator: %10u\n", mp.denominator);
    }
  }
}


/*
 * cmd_play
 * --------
 * Controls playing midi files that are already loaded
 */
int cmd_play(char *msg, int len)
{
  int i = -1;

  if(len == 0) // display status
    myprintf("Player %s\n", (mp.flags & (1<<FILE_PLAY)) ? "active" : "stopped");

  else if(msg[0] == '?') // display help
    myprintf(" File playback control.\n"
           "  ,I  initialise and start\n"
           "  ,D  disable and stop\n"
           "  ,F  file information\n"
           "  ,P  pause/continue playing\n"
           "  ,L,<0|1>  disable/enable looping (repeating)\n"
           "  ,B,<0|1>  disable/enable variation banks\n"
           "  ,K,<0|1>  disable/enable variation drum kits\n"
           "  ,O,<bank> set the bank override, 0 to disable\n"
           "  ,V,<kit>  set the drum kit override, 0 to disable\n"
           "  ,S,<posn>  seek in tenth percent of song length\n");

  else if(msg[0] == '!') // report for controller
    myprintf("PLAYER,L,%d\n"
             "PLAYER,B,%d\n"
             "PLAYER,K,%d\n"
             "PLAYER,O,%d\n"
             "PLAYER,V,%d\n",
             mp.config.loop, (syn.switches >> ALL_BANKS) & 1, (syn.switches >> ALL_KITS) & 1,
             syn.bank_override, syn.kit_override);

  else
  {
    int data[2];
    sscanf(msg+2, ",%d", &i);
    switch(msg[1])
    {
      case 'F': // info
      case 'f':
        midiPlayer_info();
        break;

      case 'L': // enable/disable loop
      case 'l':
        if((i < 0) || (i > 1))
          return -OUT_OF_RANGE;
        mp.config.loop = i;
        midiPlayer_display(ITEM_LOOP, &i);
        break;

      case 'B': // enable/disable variation banks
      case 'b':
        if((i < 0) || (i > 1))
          return -OUT_OF_RANGE;
        syn.switches = (syn.switches & ~(1<<ALL_BANKS)) | (i << ALL_BANKS);
        midiPlayer_display(ITEM_BANKS, &i);
        break;

      case 'K': // enable/disable variation drum kits
      case 'k':
        if((i < 0) || (i > 1))
          return -OUT_OF_RANGE;
        syn.switches = (syn.switches & ~(1<<ALL_KITS)) | (i << ALL_KITS);
        midiPlayer_display(ITEM_KITS, &i);
        break;

      case 'O': // bank override
      case 'o':
        if((i < 0) || (i > 127))
          return -OUT_OF_RANGE;
        syn.bank_override = i;
        midiPlayer_display(ITEM_BANK_OVERRIDE, &i);
        break;

      case 'V': // drum kit override
      case 'v':
        if((i < 0) || (i > 127))
          return -OUT_OF_RANGE;
        syn.kit_override = i;
        midiPlayer_display(ITEM_KIT_OVERRIDE, &i);
        break;

      case 'I': // initialise (start)
      case 'i':
        return player_start(1<<FILE_PLAY);

      case 'D': // disable (stop)
      case 'd':
        mp.flags = 0;
        midiSynth_reset();
        data[0] = -1;
        data[1] = 0;
        midiPlayer_display(ITEM_POSN, data);
        data[0] = 0;
        midiPlayer_display(ITEM_PAUSE, data);
        midiPlayer_display(ITEM_START, data);
        data[0] = 1;
        midiPlayer_display(ITEM_STOP, data);
        break;

      case 'P': // pause/continue
      case 'p':
        if(mp.flags & (1<<FILE_PLAY))
        {
          if(mp.flags & (1<<PAUSED))
          {
            mp.flags &= ~(1<<PAUSED);
            data[0] = 0;
            midiPlayer_display(ITEM_PAUSE, data);
            data[0] = 1;
            midiPlayer_display(ITEM_START, data);
          }
          else
          {
            mp.flags |= (1<<PAUSED);
            reset_all_gates(REMOTE_CHAN);
            data[0] = 1;
            midiPlayer_display(ITEM_PAUSE, data);
            data[0] = 0;
            midiPlayer_display(ITEM_START, data);
          }
        }
        break;

      case 'S': // seek in percent
      case 's':
        if((i < 0) || (i > 1000))
          return -OUT_OF_RANGE;
        player_seek(i, 1000);
        break;

      default: return -PARAMETER_ERROR;
    }
  }
  return NO_ERROR_;
}


/*
 * harden_spaces
 * -------------
 * replace spaces (&20) with (&A0)
 */
void harden_spaces(char *str)
{
  while(*str)
  {
    if(*str == 0x20)
      *str = 0xa0;
    str++;
  }
}


/*
 * play_file
 * ---------
 */
static int play_file(char *name, int options)
{
  int err = NO_ERROR_;
  int len;
  FILE *f;
  char *p;

  mp.mflen = 0;
  if (mp.file != NULL)
  {
    free(mp.file);
    mp.file = NULL;
  }
  mp.rec.active = FALSE;

  harden_spaces(name);

  if((f = fopen(name, "rb")) == NULL)
    err = -PROBLEM_OPENING_FILE;
  else
  {
    fseek(f, 0, SEEK_END);
    if((len = ftell(f)) > MAX_MIDI_FILE_LEN)
      err = -FILE_TOO_LARGE;
    else if ((mp.file = malloc(len)) == NULL)
      err = -CANNOT_ALLOCATE_MEMORY;
    else
    {
      fseek(f, 0, SEEK_SET);
      if(fread(mp.file, 1, len, f) != len)
        err = -PROBLEM_READING_FILE;
      else
      {
        #ifdef WIN32_
        if((p = strrchr(name, '.')) != NULL)
          *p = 0;
        p = strrchr(name, '\\');
        #endif
        #ifdef LINUX
        if((p = strrchr(name, '.')) != NULL)
          *p = 0;
        p = strrchr(name, '/');
        #endif
        #ifdef __riscos__
        if((p = strrchr(name, '/')) != NULL)
          *p = 0;
        p = strrchr(name, '.');
        #endif
        strcpy(mp.name, (p) ? p+1 : name);
        mp.mflen = len;
        if((err = player_start((1<<FILE_INFO) | options)) >= NO_ERROR_) // output info, find duration
        {
          midiPlayer_info();
          err = player_start((1<<FILE_PLAY) | options); // play file
        }

        if(err >= NO_ERROR_)
        {
          int data = (int)mp.name;
          midiPlayer_display(ITEM_NAME, &data);
        }
      }
    }
    fclose(f);
  }
  return err;
}


/*
 * cmd_load
 * --------
 * Loads and plays a MIDI type 0 or 1 file.
 */
int cmd_load(char *msg, int len)
{
  if(len == 0) // display status
    ; // no status

  else if(msg[0] == '?') // display help
    myprintf(" Plays a MIDI type 0 or 1 file\n"
           "  ,<file>\n");

  else if(msg[0] == '!') // report for controller
    ; // no report

  else
    return play_file(msg+1, 0);

  return NO_ERROR_;
}


/*
 * restart_player
 * --------------
 * if the player is playing when the sample rate is changed,
 * playing must be restarted to recalculate timing parameters
 */
void restart_player(void)
{
  if(mp.flags & (1<<FILE_PLAY))
    player_tempo(mp.config.tempo);
}


// Conversion from MIDI to Wav files
// ---------------------------------

#define MAX_FILES 100
typedef struct queue_s
{
  char name[MAX_FILES][BUFF_LEN];
  int len;
  int pos;
  int active;
} queue_t;
static queue_t queue;


/*
 * cmd_save
 * --------
 * Saves the created audio of a loaded MIDI file.
 * A wav file will be created in the given directory.
 */
int cmd_save(char *msg, int len)
{
  if(len == 0) // display status
    myprintf("Recording %s\n", (mp.rec.active) ? "active" : "inactive");

  else if(msg[0] == '?') // display help
    myprintf(" Records the created audio of a loaded MIDI file to a wav file\n"
             "  ,<pathname>  start saving to file with full pathname\n");

  else if(msg[0] == '!') // report for controller
    myprintf("SAVE,%d\n", mp.rec.active);

  else
  {
    mp.config.loop = 0;
    midiPlayer_display(ITEM_LOOP, &mp.config.loop);

    int err;
    if((err = player_start((1<<FILE_PLAY) | (1<<QUIET))) < NO_ERROR_) // start playback
      return err;

    harden_spaces(msg+1);

    if((mp.rec.f = fopen(msg+1, "wb")) == NULL)
      return -PROBLEM_OPENING_FILE;
    else
    {
      strcpy(mp.rec.name, msg+1); // so we can set filetype later
      mp.rec.active = TRUE;
      mp.rec.n = 0;
      fseek(mp.rec.f, sizeof(wave_pcm_t), SEEK_SET);
      // the recording will be picked up by midiPlayer_recording() below.
    }
  }

  return NO_ERROR_;
}

// Batch conversion of a number of files

/*
 * start_conversion
 * ----------------
 */
static int start_conversion(char *filename)
{
  mp.config.loop = 0;
  midiPlayer_display(ITEM_LOOP, &mp.config.loop);

  int err;
  if((err = play_file(filename, 1<<QUIET)) < NO_ERROR_) // start playback
    return err;

  char s[BUFF_LEN];
  #ifdef WIN32_
  sprintf(s, "%s\\data\\%s.wav", path, mp.name);
  #endif
  #ifdef LINUX
  sprintf(s, "%s/data/%s.wav", path, mp.name);
  #endif
  #ifdef __riscos__
  sprintf(s, APP_DIR".wav.%s", mp.name);
  harden_spaces(s);
  #endif
  if((mp.rec.f = fopen(s, "wb")) == NULL)
    myprintf("cannot open file %s\n", s);
  else
  {
    strcpy(mp.rec.name, s); // so we can set filetype later
    mp.rec.active = TRUE;
    mp.rec.n = 0;
    fseek(mp.rec.f, sizeof(wave_pcm_t), SEEK_SET);
    // the recording will be picked up by do_recording() below.
  }

  return NO_ERROR_;
}


/*
 * midiPlayer_recording
 * --------------------
 * Called from the main message loop. Does nothing if a recording file is not open.
 */
void midiPlayer_recording(void)
{
  if(!mp.rec.f)
    return;

  int n;
  // record a maximum of 1 seconds worth of samples
  for(n=0; (n < syn.sample_rate) && mp.rec.active; n++)
    fwrite(midiPlayer_sample(), sizeof(unsigned int), 1, mp.rec.f);
  mp.rec.n += n;

  if(!mp.rec.active) // file finished
  {
    // keep recording for a few more seconds
    int end = mp.rec.n + (syn.sample_rate * EXTRA_SECS);
    for(; mp.rec.n < end; mp.rec.n++)
      fwrite(midiPlayer_sample(), sizeof(unsigned int), 1, mp.rec.f);

    // write header
    mp.rec.h = (wave_pcm_t){{"RIFF",36,"WAVE"},{"fmt ",16,1,2,0,4,4,16},{"data",0}};
    mp.rec.h.fmt.samp_rate = syn.sample_rate;
    mp.rec.h.fmt.byte_rate *= syn.sample_rate;
    mp.rec.h.data.len = 4 * mp.rec.n;
    mp.rec.h.wave.len += mp.rec.h.data.len;
    fseek(mp.rec.f, 0, SEEK_SET);
    fwrite(&mp.rec.h, sizeof(wave_pcm_t), 1, mp.rec.f);
/*
//---------- testing
    fseek(mp.rec.f, 0, SEEK_END);
    int act_len = ftell(mp.rec.f);
    myprintf("header written, file length = %X, (actual = %X)\n", mp.rec.h.data.len + sizeof(wave_pcm_t), act_len);
//---------- test end
*/
    fclose(mp.rec.f); // <<<--- This fails sometimes on the RPCEmu (locks emulation)
    mp.rec.f = NULL;

//    myprintf("file closed\n");

    #ifdef __riscos__
    set_filetype(mp.rec.name, WAVE);
    #endif

    myprintf("file %d: %s\n", queue.pos+1, mp.rec.name);
    // check if more files to convert
    if(queue.active)
    {
      if(++queue.pos < queue.len)
        start_conversion(queue.name[queue.pos]);
      else
      {
        myprintf("%d files converted\n", queue.len);
        queue.active = FALSE;
        queue.len = queue.pos = 0;
      }
    }
  }
}


/*
 * cmd_convert
 * -----------
 * Queues midi files for conversion to wav files
 */
int cmd_convert(char *msg, int len)
{
  if(len == 0) // display status
    myprintf("%d files queued\n", queue.len - queue.pos);

  else if(msg[0] == '?') // display help
    myprintf(" Queues midi files for conversion to wav files.\n"
           "  ,<file>  full path name of MIDI file for conversion.\n");

  else if(msg[0] == '!') // report for controller
    myprintf("CONVERT,%d\n", queue.len - queue.pos);

  else if(len > 1)
  {
    if(queue.len >= MAX_FILES)
      return -TOO_MANY_FILES;

    strcpy(queue.name[queue.len++], msg+1);
    if(!queue.active)
    {
      queue.active = TRUE;
      queue.pos = 0;
      int err;
      if((err = start_conversion(queue.name[queue.pos])) < NO_ERROR_)
        return err;
    }
  }

  else
    return -PARAMETER_ERROR;

  return NO_ERROR_;
}


// Tempo and Pitch controls
// ------------------------

/*
 * player_tempo
 * ------------
 * data = new  tempo, 10% to 1000%
 * Returns any error
 */
static int player_tempo(int data)
{
  if((data < 10) || (data > 1000))
    return -OUT_OF_RANGE;

  int current = mp.config.tempo;
  mp.config.tempo = data;

  if(mp.mflen > 0) // if file is loaded
  { // recalculate affected variables
    mp.scaled_tempo = (mp.tempo * 100) / mp.config.tempo;
    mp.bps = ((1000000 << QBPS) + (mp.scaled_tempo >> 1)) / mp.scaled_tempo; // rounded
    mp.denominator = mp.division * mp.bps;
    mp.long_gap = (LONG_GAP * CLK_RATE * 100) / mp.config.tempo;
    if(mp.long_gap < 2)
      mp.long_gap = 2;

    mp.position = (mp.position * current) / data;
    mp.last_msg = mp.position;
    mp.duration = (mp.duration * current) / data;

    // display the duration
    int secs = mp.duration / CLK_RATE;
    int mins = secs / 60;
    secs %= 60;
    char s[16];
    sprintf(s, "%d:%02d", mins, secs);
    int data[2];
    data[0] = (int)s;
    midiPlayer_display(ITEM_LEN, data);
    // display the position
    data[0] = mp.position / CLK_RATE;
    data[1] = mp.duration / CLK_RATE;
    midiPlayer_display(ITEM_POSN, data);
  }
  return NO_ERROR_;
}


/*
 * player_pitch
 * ------------
 * data = new pitch transposition, -12 to +12, an octave up or down
 * Returns any error
 */
static int player_pitch(int data)
{
  if((data < -12) || (data > 12))
    return -OUT_OF_RANGE;

  mp.config.transpose = data;

  return NO_ERROR_;
}


/*
 * cmd_tempo
 * ---------
 * Controls the overall tempo
 */
int cmd_tempo(char *msg, int len)
{
  int err, data;

  if(len == 0) // display status
    myprintf("tempo = %d%%\n", mp.config.tempo);

  else if(msg[0] == '?') // display help
    myprintf(" Controls the Player overall tempo.\n"
           "  ,<tempo>  10..1000, %% of normal, 100 = normal\n");

  else if(msg[0] == '!') // report for controller
    myprintf("TEMPO,%d\n", mp.config.tempo);

  else if(sscanf(msg, ",%d", &data) != 1)
    return -PARAMETER_ERROR;

  else if((err = player_tempo(data)) < NO_ERROR_)
    return err;

  else
  {
    data = mp.config.tempo;
    midiPlayer_display(ITEM_TEMPO, &data);
  }

  return NO_ERROR_;
}


/*
 * cmd_pitch
 * ---------
 * Controls pitch transposition
 */
int cmd_pitch(char *msg, int len)
{
  int err, data;

  if(len == 0) // display status
    myprintf("transposition = %d\n", mp.config.transpose);

  else if(msg[0] == '?') // display help
    myprintf(" Controls pitch transposition.\n"
           "  ,<pitch>  -12..12, in semitones, 0 = normal\n");

  else if(msg[0] == '!') // report for controller
    myprintf("PITCH,%d\n", mp.config.transpose);

  else if(sscanf(msg, ",%d", &data) != 1)
    return -PARAMETER_ERROR;

  else if((err = player_pitch(data)) < NO_ERROR_)
    return err;

  else
    midiPlayer_display(ITEM_PITCH, &mp.config.transpose);

  return NO_ERROR_;
}

