/*
  player.c
  --------
  MIDI file player

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

#include "main.h"
#include "lib.h"
#include "kbd.h"
#include "wave_spec.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   0x2000   // 8K, 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
  int bps;       // bytes per sample
} 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;

// lyrics text
typedef struct mp_lyrics_s
{
  int size;      // length of buffer in bytes
  int read;      // read buffer index (not used in this implementation)
  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_record_t rec;           // recording control

  mp_info_t mi;              // file information

  mp_track_t tk[NUM_TRACKS]; // track data

  mp_lyrics_t text;          // lyrics text

} midi_player_t;

#define QBPS          12 // number of fractional bits in mp.bps

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

// mp.config.flags bits
enum
{
  IGNORE_SYSEX,  // discard system exclusive messages
  MP_MONO        // only used by midiPlayer_recording
};

// 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     2 // Extra seconds to play after the file has finished to allow notes to conclude.

int master_clock = 1000; // This is the rate in Hz that the player is called at. It should be 100 when
                         // driven by the system ticker, 1000 gives more accurate timing.

unsigned int song_duration; // duration in 1/CLK_RES seconds, with EXTRA_SECS added, and rounded up.

/*
 * midiPlayer_init
 * ---------------
 */
void midiPlayer_init(opt_t *o)
{
  mp.config.flags = (1<<IGNORE_SYSEX) | (o->mono << MP_MONO);
  mp.config.bps = (2 - o->mono) << 1; // bytes per sample
  mp.config.tempo = o->tempo; // 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((status & 0xf) != (PERCUSSION_CHAN-1))
    if(((status & 0xf0) == NOTE_ON) || ((status & 0xf0) == NOTE_OFF))
      buff[0] += mp.config.transpose;

  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 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.
 */
static void text_out(char c)
{
  if(mp.text.write >= mp.text.size)
    return;

  mp.text.buff[mp.text.write++] = c;
}


/*
 * read_text
 * ---------
 * Reads a text event from the midi file. Sends text to the output buffer.
 */
static void read_text(int trk, char *type, int n, int meta)
{
  if((mp.flags & ((1<<QUIET)|(1<<FILE_INFO))) == ((0<<QUIET)|(1<<FILE_INFO)))
  {
    // 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))
      {
        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)
              {
                char a = *b++;
                text_out(a);
              }
              text_out('\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';
              text_out(c);
            }
            text_out(c);
          }
        }
      }
    }
  }
}


/*
 * read_data
 * ---------
 */
static void read_data(int trk, char *type, int n, int meta)
{
  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)
     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
  }
}


/*
 * player_stop
 * -----------
 */
void player_stop(void)
{
  mp.flags = 0;
  mp.rec.active = FALSE;
  slider_value(WIN_MAIN, ICON_BAR_BACK, 0);
  file_icon_state(0,0,-1,1);
}


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

  if(mp.lead_out)
  {
    if(!--mp.lead_out)
      player_stop();
    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++;
      static int old_percent;
      int percent = (mp.position * 1000) / mp.duration;
      if(old_percent != percent)
        slider_value(WIN_MAIN, ICON_BAR_BACK, old_percent = percent);
    }

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


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

  reset_all_gates(0); // 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)
  {
    // 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;
      }
    }
  }
}


/*
 * 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.
 */
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-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 = 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
  {
    file_icon_state(1,1,-1,0);
    midiPlayer_fastseek(0);
  }

  return NO_ERROR_;
}


/*
 * player_save_txt
 * ---------------
 */
int player_save_txt(char *filename)
{
  FILE *f;
  int err;

  strcat(filename, "/txt");

  err = check_disc_space(filename, mp.text.write);
  if(err != 0) // not enough disc space or user cancelled
    return err;


  if((f = fopen(filename, "w")) == NULL)
    return -OUTPUT_OPEN_PROBLEM;

  fwrite(mp.text.buff, 1, mp.text.write, f);
  fclose(f);

  return NO_ERROR_;
}



/*
 * rec_active
 * ----------
 */
int rec_active(void)
{
  return mp.rec.active;
}


/*
 * set_rec_active
 * --------------
 */
void set_rec_active(void)
{
  mp.rec.active = TRUE;
}


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


/*
 * display_file_info
 * ------------------
 * Displays information about the currently loaded Midi file, or blanks the
 * info display if the file failed to load correctly.
 */
static void display_file_info(int err)
{
  // 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
    }
  };

  char str[40];
  int i, win = ro.win_data[WIN_MAIN].win_handle;

  if(err >= NO_ERROR_)
  {
    file_icon_state(0,0,mp.text.write == 0,1);
    // line 1
    int duration = mp.duration + (EXTRA_SECS * CLK_RES);
    int secs = duration / CLK_RES;
    int tenths = duration - (secs * 10);
    int mins = secs / 60;
    secs = secs - (mins * 60);
    sprintf(str, "duration: %d mins %d.%d secs", mins, secs, tenths);
    icon_text_change(str, win, ICON_INFO1);

    // line 2
    i = sprintf(str, "type %d", mp.format);
    if(mp.format != 0)
      sprintf(str+i, ", %d tracks", mp.tracks);
    icon_text_change(str, win, ICON_INFO2);

    // line 3
    str[0] = 0;
    if((mp.mi.nn != 0) && (mp.mi.dd != 0))
      i = sprintf(str, "time sig: %d/%d   ", 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))
        sprintf(str+i, "key: %s", key_sig[mp.mi.mi][sf + 7]);
    }
    icon_text_change(str, win, ICON_INFO3);

    // line 4
    sprintf(str, "initial tempo: %d bpm", 60000000 / mp.tempo);
    icon_text_change(str, win, ICON_INFO4);

    // line 5
    sprintf(str, "text, lyrics: %d bytes", mp.text.write);
    icon_text_change(str, win, ICON_INFO5);
  }
  else // file failed to load, clear display
  {
    file_icon_state(1,1,1,1);
    str[0] = 0;
    for(i=ICON_FILENAME; i<=ICON_INFO5; i++)
      icon_text_change(str, win, i);
  }
}


/*
 * player_rescan
 * -------------
 * used if the tempo is changed to update the duration
 */
int player_rescan(void)
{
  mp.text.write = 0; // flush text buffer
  int err = player_start((1<<FILE_INFO)); // scan file, find duration etc.
  display_file_info(err);
  song_duration = mp.duration + 1 + (EXTRA_SECS * CLK_RES);
  return err;
}


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

  check_filename(filename);

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

  if((f = fopen(filename, "rb")) == NULL)
    return -INPUT_OPEN_PROBLEM;

  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
    {
      mp.mflen = len;
      mp.text.write = 0; // flush text buffer
      err = player_start((1<<FILE_INFO) | options); // scan file, find duration etc.
      display_file_info(err);
      // The song duration is used to calculate the disc space required to save the file
      // mp.duration has a resolution of 1/10 second and is rounded down, the fraction is ignored.
      // To ensure that the estimated file size is always larger than the actual size, round up to
      // the next 1/10 second. The lead out must also be added. Any further additions due to file
      // format are added by the format specific save functions, which calculate the required disc
      // space for the file format.
      song_duration = mp.duration + 1 + (EXTRA_SECS * CLK_RES);
    }
  }
  fclose(f);

  return err;
}


/*
 * player_save_wav
 * ---------------
 * Saves the created audio of a loaded MIDI file.
 * A wav file will be created in the given directory.
 */
int player_save_wav(char *filename)
{
  int err;

  check_filename(filename);
  strcat(filename, "/wav");

  // check if enough room on disc
  unsigned int size = (((Uint64)song_duration * syn.sample_rate * mp.config.bps) / CLK_RES) + sizeof(wave_pcm_t);
  err = check_disc_space(filename, size);
  if(err != 0) // not enough disc space or user cancelled
    return err;

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

  if((mp.rec.f = fopen(filename, "wb")) == NULL)
    return -OUTPUT_OPEN_PROBLEM;
  else
  {
    set_filetype(filename, ro.converting = WAVE);
    mp.config.loop = 0;
    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_;
}


/*
 * player_close
 * ------------
 * Write header and close file
 */
void player_close(void)
{
  if(mp.rec.f)
  {
    int chans = (mp.config.flags & (1<<MP_MONO)) ? 1 : 2;
    mp.rec.h = (wave_pcm_t){{"RIFF",36,"WAVE"},{"fmt ",16,1,0,0,0,0,16},{"data",0}};
    mp.rec.h.fmt.channels = chans;
    mp.rec.h.fmt.samp_rate = syn.sample_rate;
    mp.rec.h.fmt.byte_rate = 2 * chans * syn.sample_rate;
    mp.rec.h.fmt.byte_samp = 2 * chans;
    mp.rec.h.data.len = mp.rec.n * sizeof(int);
    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);
    fclose(mp.rec.f);
    mp.rec.f = NULL;
    mp.rec.active = 0;
    ro.converting = 0;
  }
}


/*
 * 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 i;
  #define BUFF_SIZE 0x2000 // 32kB
  static int buff[BUFF_SIZE];
  if(mp.config.flags & (1<<MP_MONO))
    for(i=0; (i < BUFF_SIZE) && mp.rec.active; i++)
    {
      buff[i] = midiSynth_sample() & 0xffff;
      buff[i] |= midiSynth_sample() & 0xffff0000;
    }
  else
    for(i=0; (i < BUFF_SIZE) && mp.rec.active; i++)
      buff[i] = midiSynth_sample();
  int j = fwrite(buff, sizeof(unsigned int), i, mp.rec.f);
  mp.rec.n += j;

  if((!mp.rec.active) || (j < i)) // file finished or disc full
    player_close();
    // note. if fwrite returns an error, the header cannot be written so the file will not be playable.
}


/*
 * player_tempo
 * ------------
 * data = new  tempo, 10% to 1000%
 * Returns any error
 */
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_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_;
}

