/*
  MidiPlay - MIDI file player driver module

  midi_player.c - MIDI file player

  created  16/07/14
  8/9/22  Modified to provide tenth second resolution for song duration and current position.
*/

#define DEFINE_ERROR_STRINGS
#include "main.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
#define MPLAY_BUFF_LEN    80       // text buffer in original mplay format
#define LYRICS_BUFF_LEN   2048     // length of circular lyrics buffer

// 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
  int flags;     // configuration options
} 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;

// lyrics text
typedef struct mp_lyrics_s
{
  int size;      // length of buffer in bytes
  int read;      // read buffer index
  int write;     // write buffer index
  char buff[LYRICS_BUFF_LEN];
} mp_lyrics_t;

// file player
typedef struct midi_player_s
{
  mp_config_t config;        // player parameters, not file specific

  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_RES 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;             // lyrics 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_info_t mi;              // file information

  mp_track_t tk[NUM_TRACKS]; // track data

  mp_lyrics_t text;          // lyrics text
  char mplay_text[MPLAY_BUFF_LEN]; // text in MPLAY format

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

} midi_player_t;

#define QBPS          12 // number of fractional bits in mp.bps
#define CLK_RES       10 // 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
};

// mp.config.flags bits
enum
{
  IGNORE_SYSEX  // discard system exclusive messages
};

// 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.

// seek_ctrls.c
void sc_reset(void);
void sc_store(int status, unsigned char *buff, int n);
void sc_send(void);


/*
 * midiPlayer_new
 * --------------
 */
void midiPlayer_new(void)
{
  mp.config.flags = (1<<IGNORE_SYSEX);
  mp.config.tempo = 100; // percent
  mp.text.size = sizeof(mp.text.buff);
}


/*
 * midiPlayer_term
 * ---------------
 */
void midiPlayer_term(void)
{
  if(mp.file)
    free(mp.file);
}

/*
 * midi_msg
 * --------
 * Sends a midi event to the interpreter.
 */
static void midi_msg(int status, unsigned char *buff, int n)
{
  if(mp.config.flags & (1<<IGNORE_SYSEX))
    if(n > 2)
      return; // ignore system exclusive messages

  if(mp.flags & (1<<SEEKING))
  {
    sc_store(status, buff, n);
    return;
  }

  if((status & 0xf) != (PERCUSSION_CHAN-1))
    if(((status & 0xf0) == NOTE_ON) || ((status & 0xf0) == NOTE_OFF))
      buff[0] += mp.config.transpose;

  if(n < 3)
    mod.midi->tx_word(status, buff, n);
  else // sysex
  {
    mod.midi->tx_byte(status);
    while(n--)
      mod.midi->tx_byte(*buff++);
  }
}


/*
 * read_var_len
 * ------------
 * updates track len and pos
 * 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 track len and pos
 * returns the value
 * note. multibyte integers are big-endian
 */
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;
}


/*
 * text_out
 * --------
 * Adds a text character to the output buffer.
 * If the buffer is full the oldest character is discarded.
 */
static void text_out(char c)
{
  int next;
  int write = mp.text.write;
  int read = mp.text.read;
  int end = mp.text.size - 1;

  next = (write < end) ? write + 1 : 0;
  mp.text.buff[write] = c;
  mp.text.write = next;

  if(next == read) // if the buffer is full ...
    mp.text.read = (read < end) ? read + 1 : 0;  // loose the oldest character

  if(mod.debug & (1<<DBG_LYRICS))
    if(mod.log)fputc(c, mod.log);
}


/*
 * read_text
 * ---------
 * Reads a text event from the midi file. Sends text to the output buffer and
 & fills the MIDIPlay format text buffer.
 */
static void read_text(int trk, char *type, int n, int meta)
{
  if((mp.flags & ((1<<QUIET)|(1<<FILE_PLAY))) == ((0<<QUIET)|(1<<FILE_PLAY)))
  {
    // Some files have lyrics coded as text types so need to check for both.
    // Unfortunately some files have both!
    if((meta == 1)||(meta == 5)) // text or lyrics
    {
      int len = n;
      unsigned char *b =  mp.file + mp.tk[trk].pos;
      char c = *b;
      if((mp.lyric_trk == 0) && ((c == '\\')||(c == '/')))
        mp.lyric_trk = trk;

      if((mp.lyric_trk == 0) || (mp.lyric_trk == trk))
      {
        char *p = mp.mplay_text + 1;                    // midiplay format
        char *end = mp.mplay_text + MPLAY_BUFF_LEN - 1; // midiplay format
        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

              mp.mplay_text[0] = 1; // midiplay format
              while(len-- > 2)
              {
                char a = *b++;
                text_out(a);
                if(p < end) // midiplay format
                  *p++ = a; // midiplay format
              }
              text_out('\n');
              *p = 0; // midiplay format
              break;
          }
        }

        else // lyrics
        {
          mp.mplay_text[0]++; // midiplay format
          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';
              text_out(c);
            }
            text_out(c);
            if(c == '\n')           // midiplay format
              mp.mplay_text[0] = 1; // midiplay format
            else if(p < end)        // midiplay format
              *p++ = c;             // midiplay format
          }
          *p = 0; // midiplay format
        }
      }
    }
  }
}


/*
 * read_data
 * ---------
 */
static void read_data(int trk, char *type, int n, int meta)
{
  if(mod.debug & (1<<DBG_MSG))
    if(mod.log)fprintf(mod.log, "Meta: track %d, %s\n", trk, type);

  unsigned char *b =  mp.file + mp.tk[trk].pos;
  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 (beat, 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
            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)))
                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, seeking backwards, and starting playback.
 * When seeking backwards to a position after the file start, the parameter 'reset' is
 * set FALSE as there is no need to reset the synth. Otherwise, when starting from the
 * beginning or looping, 'reset' is set TRUE and the synth is reset.
 */
static void replay_midi_file(int reset)
{
  int t;

  if(reset)
    mod.midi->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
  }
}


/*
 * midi_file_player
 * ----------------
 * handles the rescheduling, expects to be called at mod.master_clock rate.
 */
void midi_file_player(void)
{
  int t;

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

  mp.numerator = mod.master_clock << QBPS; // to cope with master_clock changes whilst playing

  if(mp.lead_out)
  {
    if(!--mp.lead_out)
      mp.flags = 0;
    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_RES<<QBPS)) >= mp.denominator)
    {
      mp.clock_timer -= mp.denominator;
      mp.position++;
    }

    // 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(1);
      else // if not looping, stop player, or start timer to stop player later
        if((mp.lead_out = mod.master_clock * EXTRA_SECS) == 0)
          mp.flags = 0;
    }
  }
}


/*
 * 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;

  mod.midi->all_notes_off(); // ensure no notes are left on when seeking

  // 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(!pos);

  if(pos)
  {
    sc_reset();

    // 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_RES<<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;
      }
    }

    sc_send();
  }
}


/*
 * 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;

  if(mod.debug & (1<<DBG_FN_CALLS))
    if(mod.log)fprintf(mod.log, "player_start(%d)\n", action);

  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-1)))
    //  return -INVALID_NUMBER_OF_TRACKS;

    // if the following cause problems, go back to the code above
    if(mp.format == 0)
      mp.tracks = 1; // try to play type 0 files with dodgy track numbers
    else if(mp.tracks > (NUM_TRACKS-1))
      mp.tracks = NUM_TRACKS-1; // 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 = mod.master_clock << QBPS;
    mp.denominator = mp.division * mp.bps;
    mp.long_gap = (LONG_GAP * CLK_RES * 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;
    }

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

  else if(mp.flags & (1<<FILE_PLAY)) // start playing the file from the start
    midiPlayer_fastseek(0);

  return NO_ERROR_;
}


/*
 * player_seek
 * -----------
 * pos = new postion, 0 = begining, duration = end
 * 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.
 */
int player_seek(int pos)
{
  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
  mp.flags &= ~(1<<PAUSED);

  if(mod.debug & (1<<DBG_FN_CALLS))
    if(mod.log)fprintf(mod.log, "player_seek(%d)\n", pos);

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

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

  mp.flags &= old_flags;  // restore flags

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


/*
 * midiPlayer_control
 * ------------------
 * Provides a command interface for the module swi's to control the player
 */
int midiPlayer_control(int cmd, int data, int *ret)
{
  int err = NO_ERROR_;

  switch(cmd)
  {
    case CTRL_LOAD: // load file to memory and initialise
      err = player_load((char *)data, (1<<QUIET));
      break;

    case CTRL_PLAY: // start playing from position, 0..song length
      if(data == 0)
      {
        text_out('\n');
        err = player_start(1<<FILE_PLAY);
      }
      else //if(!err && (data > 0))
        err = player_seek(data / (100/CLK_RES)); // convert from cs
      break;

    case CTRL_PAUSE: // pause playback
      if(mod.debug & (1<<DBG_FN_CALLS))
        if(mod.log)fprintf(mod.log, "player_pause\n");
      mp.flags |= (1<<PAUSED);
      mod.midi->all_notes_off();
      break;

    case CTRL_UNPAUSE: // continue playing
      if(mod.debug & (1<<DBG_FN_CALLS))
        if(mod.log)fprintf(mod.log, "player_continue\n");
      mp.flags &= ~(1<<PAUSED);
      break;

    case CTRL_TEMPO: // adjust overall tempo, data is 24.8 (Q8)
      if(mod.debug & (1<<DBG_FN_CALLS))
        if(mod.log)fprintf(mod.log, "player_tempo(&%X.%02X)\n", data >> 8, data & 0xff);
      // From inspection of the original assembler code, I believe this is a fractional mp.tempo multiplier.
      err = player_tempo(((100 << 8) * data) >> 16); // percent of tempo (rate)
      break;

    case CTRL_INFO: // provide information
      ret[0] = mp.duration * (100/CLK_RES);  // convert to cs
      ret[1] = mp.position * (100/CLK_RES);  // convert to cs
      ret[2] = (mp.flags >> FILE_PLAY) & 1;  // 1 = playing
      ret[3] = (int)mp.mplay_text; // text MIDIPlay format
      // use r4 for our additional return value as I don't think it's used by the original module
      ret[4] = (int)&mp.text;     // address of text buffer structure: size,read,write,buff[size]
      break;

    case CTRL_CLOSE: // stop player, free file memory
      if(mod.debug & (1<<DBG_FN_CALLS))
        if(mod.log)fprintf(mod.log, "player_close\n");
      mp.flags = 0;
      mp.mflen = 0;
      if(mp.file)
      {
        free(mp.file);
        mp.file = NULL;
      }
      mod.midi->reset();
      break;

    case CTRL_CONTROLS: // tempo and pitch controls, used by !MidiMan
      // ret[0] = command, 0 = read, 1 = tempo, 2 = pitch, 3 = options
      // ret[1] = data for commands 1,2,3
      // ret[2] = data for command 3
      if(ret[0] == 1)
        err = player_tempo(ret[1]);
      else if(ret[0] == 2)
        err = player_pitch(ret[1]);
      else if(ret[0] == 3)
        mp.config.flags = (mp.config.flags & ~(ret[1] & 3)) ^ (ret[2] & 3);
      ret[0] = mp.config.tempo;
      ret[1] = mp.config.transpose;
      ret[2] = mp.config.flags; // bit 0 = ignore sysex
      break;
  }
  return err;
}


/*
 * midiPlayer_info
 * ---------------
 * Display information about the currently loaded file
 */
void midiPlayer_info(int additional)
{
  // 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
    }
  };

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

    if((mp.mi.nn != 0) && (mp.mi.dd != 0))
      printf("    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))
        printf("    key signature: %s\n", key_sig[mp.mi.mi][sf + 7]);
    }

    unsigned int secs = mp.duration / CLK_RES;
    unsigned int tenths = mp.duration - (secs * 10);
    unsigned int mins = secs / 60;
    secs = secs - (mins * 60);
    printf("    duration: %d mins %d.%d secs\n", mins, secs, tenths);

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

  if(additional)
  {
    printf("Timing Details\n");
    if(!mp.mflen)
      printf("  No file loaded\n");
    else
    {
      printf("  delta rate: %d Hz\n", mp.denominator >> QBPS);
      printf("  division: %d\n", mp.division);
      printf("  tempo: %d\n", mp.tempo);
      printf("  scaled tempo: %d\n", mp.scaled_tempo);
      printf("  bps: &%X.%03X (bpm %d)\n", mp.bps >> QBPS, mp.bps & ((1<<QBPS)-1), ((mp.bps * 60)+(1<<(QBPS-1))) >> QBPS);
      printf("  numerator:   %10u\n", mp.numerator);
      printf("  denominator: %10u\n", mp.denominator);
    }
  }
}


/*
 * check_filename
 * --------------
 * Sets the top bit of spaces and replaces the first control character with a null.
 */
static void check_filename(char *str)
{
  while(*str >= 0x20)
  {
    if(*str == 0x20)
      *str = 0xa0;
    str++;
  }
  *str = 0;
}


/*
 * player_load
 * -----------
 * Loads a file into memory, scans file for song duration etc.
 */
int player_load(char *name, int options)
{
  int err = NO_ERROR_;
  int len;
  FILE *f;

  check_filename(name);

  if(mod.debug & (1<<DBG_FN_CALLS))
    if(mod.log)fprintf(mod.log, "player_load(%d) file: %s\n", options, name);

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

  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
      {
        char *p;
        if((p = strrchr(name, '/')) != NULL)
          *p = 0;
        p = strrchr(name, '.');
        strcpy(mp.name, (p) ? p+1 : name);
        mp.mflen = len;
        err = player_start((1<<FILE_INFO) | options); // scan file, find duration
      }
    }
    fclose(f);
  }
  return err;
}


/*
 * player_tempo
 * ------------
 * data = new  tempo, 10% to 1000%
 * Returns any error
 */
int player_tempo(int data)
{
      if(mod.debug & (1<<DBG_FN_CALLS))
        if(mod.log)fprintf(mod.log, "%d\n", 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_RES * 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;
  }
  return NO_ERROR_;
}


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

  mp.config.transpose = data;

  return NO_ERROR_;
}

