/*
  module.c
  --------
  Midi 4 port driver module code

  8/3/23 created
*/

#include <stdio.h>
#include <stdlib.h>
#include <kernel.h>
#include <swis.h>
#include "modhdr.h"
#include "module.h"
#include "queue.h"
#include "midi_spec.h"

/*
 The Acorn Midi User Guide is a bit confusing where Tx Timing messages are concerned. It states
 that the swi MIDI_SetTxChannel sets the channel (and port) to be used by ALL MIDI_Tx swi's
 except MIDI_TxCommand and MIDI_TxByte. But for the swi MIDI_TxStart it states "This affects
 all MIDI ports". It's not plain whether this refers to the whole swi description or just the
 last part to "disable reception of Start,Continue,Stop,Timing messages".
 So I have provided a compile switch to allow both options.

 The code does not include a MIDI Interpreter so all swi's and commands to control the internal
 sounds are not implemented.
*/

// Build Options ----------------------------
// These are mostly to give compliance with the Acorn Midi spec. if required.

// Define this to allow Tx Timing messages to be sent on the port defined by the swi MIDI_SetTxChannel.
// Comment out to send to all 4 ports.
#define INDEPENDENT_TX_TIMING

// To make Fast Clock timing mode the start up default make this = 1.
// To comply with the Acorn Midi spec. make it = 0.
#define DEFAULT_FAST_CLOCK 1
// note. defaults are also set by the swi MIDI_Init,0.

// This is the default tempo in beats per minute, it defines the rate of tx timing msgs when
// controlled by the Fast Clock, and needs defining if the Fast Clock is the default.
// If it is = 0, tx timing messages will never be sent.
#define DEFAULT_BPM 90
// note. In order to get correct timing of any BPM value, a fractional counter is used.
// When the msg rate is set by the swi MIDI_FastClock or the command *MidiStart, the rate is
// set as a period in ms so not all BPM values are obtainable.
//-------------------------------------------

// MIDISupport SWI numbers
#define MIDISupport_InstallDriver   0x4EE80
#define MIDISupport_RemoveDriver    0x4EE81
#define MIDISupport_DriverInfo      0x4EE82
#define MIDISupport_CreateDriver    0x4EE83
#define MIDISupport_Send            0x4EE84
#define MIDISupport_Receive         0x4EE85
#define MIDISupport_Connect         0x4EE86
#define MIDISupport_ConnectName     0x4EE87
#define MIDISupport_GetAddress      0x4EE88
#define MIDISupport_Insert          0x4EE89

// Timer SWI numbers
#define MTimer_Register             0xd25c0
#define MTimer_Remove               0xd25c1

// Serivice calls
#define Service_MIDI             0x58
// Sub reason codes
#define Service_MIDIAlive           0
#define Service_MIDIDying           1
#define Service_MIDISupportAlive    4
#define Service_MIDISupportDying    5

// misc
#define TickerV                  0x1c

// Events
#define Event_MIDI               0x11
// Sub reason codes
#define Event_MIDI_DataReceived     0
#define Event_MIDI_Error            1
#define Event_MIDI_QueueEmptying    2

// midi support driver flags
#define CAN_SEND                    1
#define CAN_RECEIVE                 2
#define REQUIRES_BUFFERING          4
#define CAN_RECEIVE_COMMAND         8

#define RXBUFF_SIZE               513 // midi command words (int's) , not bytes
#define ACTIVE_SENSE_TX_TIMEOUT   270 // period between transmitted active sense messages in ms
#define ACTIVE_SENSE_RX_TIMEOUT   330 // timeout for gap in received messages, including active sense, in ms
#define MIDI_CLK_RATE_Q             8 // tx timing msg rate, fractional bits
#define MIDI_BEATS_PER_SONG_POSN    6 // number of midi beats for each song position
#define MICROBEATS_PER_MIDI_BEAT   16 // number of microbeats in a midi beat

static struct mod_s
{
  int registered;     // a bit set for each port registered with Midi Support
  int driver_number[NUM_PORTS]; // Midi Support driver number
  unsigned int ticks; // absolute time in ms from Timer startup

  int tx_chan;        // channel & port for tx commands except MIDI_TxByte & MIDI_TxCommand
  int tx_rate;        // tx timing message rate in ms, fixed point fractional
  int tx_count;       // counter for above
  int midi_beat;      // midi beat timestamp
  unsigned int error; // error codes, one byte per port
  unsigned int flags; // see below for bit defs

  // sound system timing
  struct sys_s
  {
    int beats;        // sound system beats per bar, if zero the beat counter is disabled
    int beat;         // sound system beat count (microbeats)
    int bar;          // our bar counter
    unsigned int bar_beat; // bar/microbeat timestamp
    int clk;          // microbeat and bar counter (linear number), used to check if midi clock needs updating
  } sys;

  // communication with Midi Support
  struct support_s
  {
    int receive;      // call address
    int give;         // call address
    int pw;           // r12 value
  } support;

  // receive data, per port
  struct rx_s
  {
    unsigned int buff[RXBUFF_SIZE]; // midi commands
    unsigned int time[RXBUFF_SIZE]; // timestamps
    int rd;           // buffer read index
    int wr;           // buffer write index
    int shift;        // used when reading bytes from a multibyte command
    int gap_count;    // timer for gaps in received data
  } rx[NUM_PORTS];

  // transmit data, per port
  struct tx_s
  {
    int gap_count;    // timer for gaps in transmitted data
  } tx[NUM_PORTS];

  // debug
  FILE *log;
  unsigned int debug; // see below for bit defs

} mod;

// bits for mod.debug
#define DBG_MSG           0

// bits for mod.flags
#define EXT_TIMING        0 // )
#define INT_TIMING        1 // )
#define FAST_CLOCK        2 // ) returned by MIDI_InqSongPositionPointer
#define FAST_CLK_CALLED   3 // )   (don't alter order)
#define STORE_REAL_TIME   4 // )
#define IGNORE_REAL_TIME  5 // )
#define IGNORE_TIMING     6 // set by MIDI_IgnoreTiming
#define ACTIVE_SENSE_TX   7 // 4 bits for ports 0..3 ) returned by MIDI_SetTxActiveSensing
#define ACTIVE_SENSE_RX  11 // 4 bits for ports 0..3 )   (don't alter order)
#define TIMING_CLK_TX    15 // 4 bits for ports 0..3 enable tx timing clock messages, set by MIDI_TxStart
#define TIMING_CLK_RX    19 // 4 bits for ports 0..3 enable rx timing clock messages, set by received START
#define MICROBEAT_TIMING 23 // Sound system timing is active

void module_call(int fn, _kernel_swi_regs *regs, int pw); // veneer.s
int stream_to_cmd(int port, int data); // midi.c
static void read_options(int argc, const char *arg_string, void *pw);

const char port_name[NUM_PORTS][12] = {"MIDIPort0","MIDIPort1","MIDIPort2","MIDIPort3"};


/*
 * err_rx_overflow
 * ---------------
 * Returns an error block for the receive buffer overflow condition.
 */
static _kernel_oserror *err_rx_overflow(int port)
{
  static struct
  {
    int num;
    char msg[32];
  }err = {0x20404, "Receive buffer overflow, Port?"};

  err.msg[29] = port + '0';

  return (_kernel_oserror *)&err;
}

/*
 * get_timestamp
 * -------------
 * Returns the timestamp for received data.
 * The type of timestamp depends on the timing mode.
 */
static int get_timestamp(int port)
{
  if(!(mod.flags & (1 << FAST_CLOCK)))
    if(mod.flags & (1 << INT_TIMING))
      return mod.midi_beat;         // internal timing mode

  if(mod.flags & (1 << EXT_TIMING))
    return mod.midi_beat;           // external timing mode

  if(mod.flags & (1 << FAST_CLOCK))
    return queue_clock(0);          // fast clock mode, ms timestamp

  return mod.sys.bar_beat;          // microbeat mode, bar/beat timestamp
}


/*
 * read_bar_beat
 * -------------
 * Reads the Sound System beat count and updates the bar counter
 * and bar_beat timestamp.
 * Checks if midi timing message would be due to send if enabled.
 * Returns:
 *  -1 if sound systen clock disabled
 *   1 if midi clock needs to be incremented
 *   else 0.
 */
static int read_bar_beat(void)
{
  _kernel_swi_regs regs;
  struct sys_s *s = &mod.sys;
  int ret = 0;

  // Read sound system beats per bar
  regs.r[0] = -1;
  _kernel_swi(Sound_QBeat, &regs, &regs);
  s->beats = regs.r[0];
  if(s->beats <= 0)
  {
    mod.flags &= ~(1 << MICROBEAT_TIMING);
    s->beat = s->bar = s->clk = s->bar_beat = 0;
    ret = -1; // disabled
  }
  else // sound system beat count running
  {
    mod.flags |= (1 << MICROBEAT_TIMING);

    regs.r[0] = 0; // Read sound system beat count
    _kernel_swi(Sound_QBeat, &regs, &regs);
    if(regs.r[0] != s->beat)
    {
      // update bar, beat
      if(regs.r[0] < s->beat)
        s->bar++;
      s->beat = regs.r[0];

      s->bar_beat = (s->bar << 16) | s->beat; // update bar/beat timestamp

      // check if midi clock needs incrementing
      int clk = (s->bar * s->beats) + s->beat;
      if((clk - s->clk) >= MICROBEATS_PER_MIDI_BEAT)
      {
        ret = 1;
        s->clk = clk;
      }

      // for checking microbeats, bars, beats, and ticks
      if(mod.debug & 8)
        if(mod.log)fprintf(mod.log, "%u: %d %d %d %d\n", mod.ticks, s->beats, s->bar, s->beat, ret);
    }
  }
  return ret;
}


/*
 * init_data
 * ---------
 * Initialises variables to the startup state
 */
static void init_data(void)
{
#define MS_PER_MIN  60000
  int i;

  mod.flags = (DEFAULT_FAST_CLOCK << FAST_CLOCK); // default to fast clock mode
  mod.tx_chan = 0;
#if(DEFAULT_BPM == 0)
  mod.tx_rate = 0;
#else
  mod.tx_rate = ((MS_PER_MIN / MCLK_PER_QUARTER) << MIDI_CLK_RATE_Q) / DEFAULT_BPM; // ms, fixed point fractional
#endif
  mod.tx_count = 0;
  mod.midi_beat = 0;
  mod.error = 0;
  queue_set(0); // clear millisecond timer
  stream_to_cmd(-1,0); // clear running status
  mod.sys.beats = 0;
  mod.sys.beat = 0;
  mod.sys.bar = 0;
  mod.sys.clk = 0;
  mod.sys.bar_beat = 0;
  for(i=0; i<NUM_PORTS; i++)
  {
    mod.rx[i].rd = mod.rx[i].wr = mod.rx[i].shift = 0;
    mod.rx[i].gap_count = 0;
    mod.tx[i].gap_count = 0;
    queue_clear(i); // clear scheduled message queue
  }
}


/*
 * generate_event
 * --------------
 * Generate the provided MIDI event
 */
static void generate_event(int event)
{
  _kernel_swi_regs regs;

  regs.r[0] = Event_MIDI;
  regs.r[1] = event;
  _kernel_swi(OS_GenerateEvent, &regs, &regs);
}


/*
 * tx_command
 * ----------
 * Sends a midi command of up to 3 bytes directly to the midi support module.
 * cmd format:
 *  data byte 1 usually the status, bits 0-7
 *  data byte 2, bits 8-15
 *  data byte 3, bits 16-23
 *  number of bytes, bits 24,25 (1..3)
 *  port, bits 28,29 (0..3)
 */
void tx_command(unsigned int cmd)
{
  _kernel_swi_regs regs;
  int port = (cmd >> 28) & 3;

  if(mod.flags & (1 << (ACTIVE_SENSE_TX + port)))
    mod.tx[port].gap_count = 0; // reset inactivity count on any message sent

  if(mod.debug & (1<<DBG_MSG))
    if(mod.log)fprintf(mod.log, "%u: %08X\n", mod.ticks, cmd);

  regs.r[0] = cmd & 0x0fffffff; // remove port number
  regs.r[1] = mod.driver_number[port]; // midi support driver number for this port

  if(mod.support.receive)
    module_call(mod.support.receive, &regs, mod.support.pw);
}


/*
 * find_length
 * -----------
 * Returns the number of midi message bytes in the passed 32 bit word.
 */
static int find_length(unsigned int data)
{
  switch(data & 0xf0)
  {
    case NOTE_OFF:
    case NOTE_ON:
    case KEY_PRESSURE:
    case CONTROL:
    case PITCH_WHEEL: return 3;

    case PROGRAM:
    case CHAN_PRESSURE: return 2;

    case 0xf0:
      switch(data & 0xff)
      {
        case SONG_POSITION: return 3;

        case SONG_SELECT: return 2;

        case TUNE_REQUEST:
        case TIMING_CLOCK:
        case START:
        case CONTINUE:
        case STOP:
        case ACTIVE_SENSING:
        case SYSTEM_RESET: return 1;
      }
      break;
  }

  return 0;
}

/*
 * driver_register
 * ---------------
 * Registers us with the midi support module
 */
static void reg1port(int port, void *info, void *init, void *receive, void *pw)
{
  _kernel_swi_regs regs;

  regs.r[0] = CAN_SEND | CAN_RECEIVE | CAN_RECEIVE_COMMAND;
  regs.r[1] = (int)info;
  regs.r[2] = (int)init;
  regs.r[3] = (int)receive;
  regs.r[4] = (int)pw;
  if(_kernel_swi(MIDISupport_InstallDriver, &regs, &regs) == NULL)
  {
    mod.driver_number[port] = regs.r[0];
    mod.support.receive = regs.r[1];
    mod.support.give = regs.r[2];
    mod.support.pw = regs.r[3];
    mod.registered |= (1 << port);
  }
}
static void driver_register(void *pw)
{
  reg1port(0, driver1_info, driver1_init, driver1_receive, pw);
  reg1port(1, driver2_info, driver2_init, driver2_receive, pw);
  reg1port(2, driver3_info, driver3_init, driver3_receive, pw);
  reg1port(3, driver4_info, driver4_init, driver4_receive, pw);
}


/*
 * driver_deregister
 * -----------------
 */
static void driver_deregister(void *pw)
{
  _kernel_swi_regs regs;
  int i;

  for(i=0; i<NUM_PORTS; i++)
  {
    regs.r[1] = mod.driver_number[i];
    _kernel_swi(MIDISupport_RemoveDriver, &regs, &regs);
  }
  mod.registered = 0;
}


/*
 * module_init
 * -----------
 */
_kernel_oserror *module_init(const char *tail, int podule_base, void *pw)
{
  _kernel_oserror *err = NULL;
  _kernel_swi_regs regs;

  init_data();

  // Register with timer to drive the scheduler
  regs.r[0] = (int)timer_callback;
  regs.r[1] = (int)pw;
  regs.r[2] = 1; // period, 1ms
  regs.r[3] = (int)Module_Title;
  if((err = _kernel_swi(MTimer_Register, &regs, &regs)))
    return err;

  // If Midi Support is loaded, register with it, if not we will
  // register when we receive its startup service call.
  regs.r[0] = 18;
  regs.r[1] = (int)"MIDISupport";
  if(!_kernel_swi(OS_Module, &regs, &regs))
    driver_register(pw);

  // Enable MIDI events
  regs.r[0] = 14;
  regs.r[1] = Event_MIDI;
  _kernel_swi(OS_Byte, &regs, &regs);

  // Issue startup service call
  regs.r[0] = Service_MIDIAlive;
  regs.r[1] = Service_MIDI;
  _kernel_swi(OS_ServiceCall, &regs, &regs);

  return NULL;
}

/*
 * module_term
 * -----------
 */
_kernel_oserror *module_term(int fatal, int podule_base, void *pw)
{
  _kernel_swi_regs regs;

  // If we are registered with Midi Support, remove ourselves
  if(mod.registered)
    driver_deregister(pw);

  // Remove our timer entry
  regs.r[0] = (int)timer_callback;
  _kernel_swi(MTimer_Remove, &regs, &regs);

  // Disable MIDI events
  regs.r[0] = 13;
  regs.r[1] = Event_MIDI;
  _kernel_swi(OS_Byte, &regs, &regs);

  // Issue shutdown service call
  regs.r[0] = Service_MIDIDying;
  regs.r[1] = Service_MIDI;
  _kernel_swi(OS_ServiceCall, &regs, &regs);

  return NULL;
}

/*
 * module_command
 * --------------
 */
_kernel_oserror *module_command(const char *arg_string, int argc, int number, void *pw)
{
  int i;

  switch(number)
  {
    case CMD_MidiSound: // not implemented
      break;

    case CMD_MidiTouch: // not implemented
      break;

    case CMD_MidiChannel: // not implemented
      break;

    case CMD_MidiMode: // not implemented
      break;

    case CMD_MidiStart:
      if(argc > 0)
      {
        while(*arg_string == ' ')arg_string++;
        i = atoi(arg_string);
        if((i > 0) && (i < 0x10000))
          mod.tx_rate = i << MIDI_CLK_RATE_Q;
      }
      mod.midi_beat = 0;
      mod.flags &= ~((15 << TIMING_CLK_RX) | (1 << EXT_TIMING)); // disable external timing
#ifdef INDEPENDENT_TX_TIMING
      mod.flags |= (1 << (TIMING_CLK_TX + ((mod.tx_chan >> 4) & 3))) | (1 << INT_TIMING) | (1 << FAST_CLOCK);
      tx_command(START | ((mod.tx_chan & 0xf0) << 24) | (1 << 24));
#else
      mod.flags |= (15 << TIMING_CLK_TX) | (1 << INT_TIMING) | (1 << FAST_CLOCK);
      for(i=0; i<NUM_PORTS; i++)
        tx_command(START | (i << 28) | (1 << 24));
#endif
      break;

    case CMD_MidiStop:
#ifdef INDEPENDENT_TX_TIMING
      mod.flags &= ~(1 << (TIMING_CLK_TX + ((mod.tx_chan >> 4) & 3)));
      if(!(mod.flags & (15 << TIMING_CLK_TX))) // if all off, clear internal timing mode
        mod.flags &= ~(1 << INT_TIMING);
      for(i=0; i<NUM_PORTS; i++)
        tx_command(STOP | (i << 28) | (1 << 24));
#else
      mod.flags &= ~((15 << TIMING_CLK_TX) | (1 << INT_TIMING));
      tx_command(STOP | ((mod.tx_chan & 0xf0) << 24) | (1 << 24));
#endif
      break;

    case CMD_MidiContinue:
      mod.flags &= ~((15 << TIMING_CLK_RX) | (1 << EXT_TIMING)); // disable external timing
#ifdef INDEPENDENT_TX_TIMING
      mod.flags |= (15 << TIMING_CLK_TX) | (1 << INT_TIMING) | (1 << FAST_CLOCK);
      for(i=0; i<NUM_PORTS; i++)
        tx_command(CONTINUE | (i << 28) | (1 << 24));
#else
      mod.flags |= (1 << (TIMING_CLK_TX + ((mod.tx_chan >> 4) & 3))) | (1 << INT_TIMING) | (1 << FAST_CLOCK);
      tx_command(CONTINUE | ((mod.tx_chan & 0xf0) << 24) | (1 << 24));
#endif
      break;

    case CMD_MidiTxChannel:
      if(argc > 0)
      {
        while(*arg_string == ' ')arg_string++;
        i = atoi(arg_string);
        if((i >= 1) && (i <= 64))
          mod.tx_chan = i - 1;
      }
      break;

    case CMD_Midi4debug:
      if(argc > 0)
        read_options(argc, arg_string, pw);
      break;
  }

  return NULL;
}

/*
 * module_swi
 * ----------
 */
_kernel_oserror *module_swi(int number, _kernel_swi_regs *r, void *pw)
{
  int i;
  int len, r0 = r->r[0], r1 = r->r[1];

  switch(number + MIDI_00)
  {
    case MIDI_SoundEnable: // not implemented
      break;

    case MIDI_SetMode: // not implemented
      break;

    case MIDI_SetTxChannel:
      if(r0)
        mod.tx_chan = r0 - 1;
      r->r[0] = mod.tx_chan + 1;
      break;

    case MIDI_SetTxActiveSensing:
      {
        int tx_bit = ACTIVE_SENSE_TX + ((r0 >> 1) & 3);
        mod.flags = (mod.flags & ~(1 << tx_bit)) | ((r0 & 1) << tx_bit);
      }
      r->r[0] = (mod.flags >> ACTIVE_SENSE_TX) & 0xff; // return both Tx and Rx Active Sense state
      break;

    case MIDI_InqSongPositionPointer:
      r->r[0] = mod.midi_beat / MIDI_BEATS_PER_SONG_POSN;
      r->r[1] = (mod.flags >> EXT_TIMING) & 0x3f; // return 6 flag bits
      break;

    case MIDI_InqBufferSize:
      if(r0 & 1) // Tx buffer
        r->r[0] = 512; // data passed directly on without buffering, so "Tx buffer" is always empty
      else // Rx buffer
      {
        i = (r0 >> 1) & 3; // port
        struct rx_s *b = &mod.rx[i];
        int used = b->wr - b->rd;
        if(used < 0)
          used += RXBUFF_SIZE;
        r->r[0] = RXBUFF_SIZE - 1 - used; // return number of free locations
      }
      break;

    case MIDI_InqError:
      r->r[0] = mod.error;
      mod.error = 0;
      break;

    case MIDI_IgnoreTiming:
      mod.flags = (mod.flags & ~(1<<IGNORE_TIMING)) | ((r0 & 1) << IGNORE_TIMING);
      break;

    case MIDI_SynchSoundScheduler: // not implemented
      r->r[0] = 0; // always return "normal mode"
      break;

    case MIDI_FastClock:
      {
        mod.flags |= (1 << FAST_CLK_CALLED);
        int clk = queue_clock(0);
        if(r0 < 0)
          r->r[0] = clk;
        else if(r0 == 0)
          mod.flags &= ~(1 << FAST_CLOCK);
        else // (r0 > 0)
        {
          mod.flags |= (1 << FAST_CLOCK);
          queue_set(r1);
          mod.tx_rate = r0 << MIDI_CLK_RATE_Q;
        }
        r->r[1] = clk;
      }
      break;

    case MIDI_Init:
      if(r0 == 0) // internal system reset
        init_data();
      else
      {
        if(r0 & (1<<0)) // clear running status
          stream_to_cmd(-1,0);

        if(r0 & (1<<1)) // clear receive buffers
          for(i=0; i<NUM_PORTS; i++)
            mod.rx[i].rd = mod.rx[i].wr = mod.rx[i].shift = 0;

        if(r0 & (1<<3)) // clear scheduler
          for(i=0; i<NUM_PORTS; i++)
            queue_clear(i);

        if(r0 & (1<<4)) // clear current error
          mod.error = 0;

        if(r0 & (1<<30)) // store received system real time messages
          mod.flags |= (1 << STORE_REAL_TIME);

        if(r0 & (1<<31)) // ignore received system real time messages
        {
          mod.flags |= (1 << IGNORE_REAL_TIME);
          mod.flags &= ~((15 << TIMING_CLK_RX) | (1 << EXT_TIMING)); // don't get stuck in an ext timing mode
        }
      }
      r->r[0] = NUM_PORTS; // number of active ports we provide
      break;

    case MIDI_SetBufferSize: // sizes are fixed but are reported
      if(r0 == 0) // Rx
        r->r[0] = RXBUFF_SIZE - 1; // report the number of usable locations
      else if(r0 == 1) // Tx
        r->r[0] = 512;
      r->r[1] = 0; // no RMA claimed
      break;

    case MIDI_Interface: // not implemented
      break;

    case MIDI_RxByte:
      if(r0 == -1)
      {
        for(i=0; i<NUM_PORTS; i++)
          if(mod.rx[i].rd != mod.rx[i].wr)
            break;
        if(i >= NUM_PORTS)
        { // all empty
          r->r[0] = (r0 << 28);
          r->r[1] = 0;
          break;
        }
        else
          r0 = i;
      }
      if((r0 >= 0) && (r0 <= 3))
      {
        struct rx_s *b = &mod.rx[r0];
        if(b->rd == b->wr)
        { // empty
          r->r[0] = (r0 << 28);
          r->r[1] = 0;
        }
        else
        {
          int cmd = b->buff[b->rd];
          r->r[0] = ((cmd >> b->shift) & 0xff) | (1 << 24) | (r0 << 28);
          r->r[1] = b->time[b->rd];
          b->shift += 8;
          int len = (cmd >> 24) & 3;
          if(b->shift > ((len - 1) * 8))
          { // read all bytes in this command
            b->rd = (b->rd < (RXBUFF_SIZE-1)) ? b->rd + 1 : 0;
            b->shift = 0; // next byte to read from next command
          }
          if(mod.debug & (1<<DBG_MSG))
            if(mod.log)fprintf(mod.log, "%u: %08X %u RxB\n", mod.ticks, r->r[0], r->r[1]);
          if(((mod.error >> (r0 * 8)) & 0xff) == 'B')
            return err_rx_overflow(r0);
        }
      }
      break;

    case MIDI_RxCommand:
      if(r0 == -1)
      {
        for(i=0; i<NUM_PORTS; i++)
          if(mod.rx[i].rd != mod.rx[i].wr)
            break;
        if(i >= NUM_PORTS)
        {
          r->r[0] = r->r[1] = 0; // all empty
          break;
        }
        else
          r0 = i;
      }
      if((r0 >= 0) && (r0 <= 3))
      {
        struct rx_s *b = &mod.rx[r0];
        if(b->rd == b->wr)
          r->r[0] =  r->r[1] = 0; // empty
        else
        {
          r->r[0] = b->buff[b->rd] | (r0 << 28);
          r->r[1] = b->time[b->rd];
          b->rd = (b->rd < (RXBUFF_SIZE-1)) ? b->rd + 1 : 0;
          b->shift = 0; // next byte to read from next command
          if(mod.debug & (1<<DBG_MSG))
            if(mod.log)fprintf(mod.log, "%u: %08X %u RxC\n", mod.ticks, r->r[0], r->r[1]);
          if(((mod.error >> (r0 * 8)) & 0xff) == 'B')
            return err_rx_overflow(r0);
        }
      }
      break;

    case MIDI_TxByte:
      if(mod.debug & (1<<DBG_MSG))
        if(mod.log)fprintf(mod.log, "%u: %08X TxB\n", mod.ticks, r0);

      if((i = stream_to_cmd(r0 >> 28, r0)) != 0) // assemble commands before sending them
        tx_command((r0 & 0xf0000000) | i);
      break;

    case MIDI_TxCommand:
      if(mod.debug & (1<<DBG_MSG))
        if(mod.log)fprintf(mod.log, "%u: %08X TxC\n", mod.ticks, r0);

      len = (r0 >> 24) & 3;
      if(len == 0)
      {
        len = find_length(r0); // ensure length is always defined
        r0 = (r0 & 0xf0ffffff) | (len << 24);
      }

      if((mod.flags & (1 << FAST_CLOCK)) && (r1 > 0))
        r->r[0] = queue_write(r0, r1);
      else
        tx_command(r0);
      break;

    case MIDI_TxNoteOff:
      tx_command(NOTE_OFF | (mod.tx_chan & 0xf) | // status
                 (r0 << 8) |  (r1 << 16) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxNoteOn:
      tx_command(NOTE_ON | (mod.tx_chan & 0xf) | // status
                 (r0 << 8) | (r1 << 16) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxPolyKeyPressure:
      tx_command(KEY_PRESSURE | (mod.tx_chan & 0xf) | // status
                 (r0 << 8) | (r1 << 16) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxControlChange:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 (r0 << 8) | (r1 << 16) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxLocalControl:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 ((LOCAL_KEYBOARD_on << 8) | (r0 << 16)) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxAllNotesOff:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 (ALL_NOTES_OFF << 8) | // data byte
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxOmniModeOff:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 (OMNI_MODE_OFF << 8) | // data byte
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxOmniModeOn:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 (OMNI_MODE_ON << 8) | // data byte
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxMonoModeOn:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 (MONO_OPERATION << 8) | (r0 << 16) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxPolyModeOn:
      tx_command(CONTROL | (mod.tx_chan & 0xf) | // status
                 (POLY_OPERATION << 8) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxProgramChange:
      tx_command(PROGRAM | (mod.tx_chan & 0xf) | // status
                 (r0 << 8) | // data byte
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxChannelPressure:
      tx_command(CHAN_PRESSURE | (mod.tx_chan & 0xf) | // status
                 (r0 << 8) | // data byte
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxPitchWheel:
      tx_command(PITCH_WHEEL | (mod.tx_chan & 0xf) | // status
                 ((r0 & 0x7f) << 8) | ((r0 & 0x3f80) << 9) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      break;

    case MIDI_TxSongPositionPointer:
      tx_command(SONG_POSITION | // status
                 ((r0 & 0x7f) << 8) | ((r0 & 0x3f80) << 9) | // data bytes
                 ((mod.tx_chan & 0xf0) << 24) | (3 << 24)); // port/length
      mod.midi_beat = r0 * MIDI_BEATS_PER_SONG_POSN;
      break;

    case MIDI_TxSongSelect:
      tx_command(SONG_SELECT | // status
                 ((r0 & 0x7f) << 8) | // data byte
                 ((mod.tx_chan & 0xf0) << 24) | (2 << 24)); // port/length
      break;

    case MIDI_TxTuneRequest:
      tx_command(TUNE_REQUEST | // status
                 ((mod.tx_chan & 0xf0) << 24) | (1 << 24)); // port/length
      break;

    case MIDI_TxStart:
      mod.midi_beat = 0;
      mod.flags &= ~((15 << TIMING_CLK_RX) | (1 << EXT_TIMING));
#ifdef INDEPENDENT_TX_TIMING
      mod.flags |= (1 << (TIMING_CLK_TX + ((mod.tx_chan >> 4) & 3))) | (1 << INT_TIMING);
      tx_command(START | ((mod.tx_chan & 0xf0) << 24) | (1 << 24));
#else
      mod.flags |= (15 << TIMING_CLK_TX) | (1 << INT_TIMING);
      for(i=0; i<NUM_PORTS; i++)
        tx_command(START | (i << 28) | (1 << 24));
#endif
      break;

    case MIDI_TxContinue:
      mod.flags &= ~((15 << TIMING_CLK_RX) | (1 << EXT_TIMING));
#ifdef INDEPENDENT_TX_TIMING
      mod.flags |= (1 << (TIMING_CLK_TX + ((mod.tx_chan >> 4) & 3))) | (1 << INT_TIMING);
      tx_command(CONTINUE | ((mod.tx_chan & 0xf0) << 24) | (1 << 24));
#else
      mod.flags |= (15 << TIMING_CLK_TX) | (1 << INT_TIMING);
      for(i=0; i<NUM_PORTS; i++)
        tx_command(CONTINUE | (i << 28) | (1 << 24));
#endif
      break;

    case MIDI_TxStop:
#ifdef INDEPENDENT_TX_TIMING
      mod.flags &= ~(1 << (TIMING_CLK_TX + ((mod.tx_chan >> 4) & 3)));
      if(!(mod.flags & (15 << TIMING_CLK_TX))) // if all off, clear internal timing mode
        mod.flags &= ~(1 << INT_TIMING);
      tx_command(STOP | ((mod.tx_chan & 0xf0) << 24) | (1 << 24));
#else
      mod.flags &= ~((15 << TIMING_CLK_TX) | (1 << INT_TIMING));
      for(i=0; i<NUM_PORTS; i++)
        tx_command(STOP | (i << 28) | (1 << 24));
#endif
      break;

    case MIDI_TxSystemReset:
      tx_command(SYSTEM_RESET | // status
                 ((mod.tx_chan & 0xf0) << 24) | (1 << 24)); // port/length
      break;

    // These are to provide compatibility with apps expecting to be using the USB Midi module

    case MIDI_USBInfo:
      if(r0 == -1) // module indentification
      {
        r->r[0] = *(int *)"Pete"; // (0x65746550)
        r->r[1] = 0;
      }
      else if(r0 == 0)
      {
        r->r[0] = 4;
        r->r[1] = 15;
      }
      else if((r0 >= 1) && (r0 <= 4))
      {
        r->r[0] = 0;
        r->r[1] = (int)"MIDI";
        r->r[2] = (int)port_name[r0 - 1];
        r->r[3] = 0;
        r->r[4] = 0;
        r->r[5] = 0;
        r->r[6] = 0;
        r->r[7] = 0;
      }
      break;

    case MIDI_Options:
      r->r[0] = 0;
      r->r[1] = 0;
      break;

    case MIDI_Remap:
      break;
  }

  return NULL;
}


/*
 * driver_info_handlers
 * --------------------
 */
static _kernel_oserror *driver_info_handler(int port, _kernel_swi_regs *r)
{
  r->r[0] = (int)port_name[port];
  r->r[1] = Module_VersionNumber;
  r->r[2] = (int)Module_Date;

  return NULL;
}
_kernel_oserror *driver1_info_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_info_handler(0, r);
}
_kernel_oserror *driver2_info_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_info_handler(1, r);
}
_kernel_oserror *driver3_info_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_info_handler(2, r);
}
_kernel_oserror *driver4_info_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_info_handler(3, r);
}


/*
 * driver_init_handlers
 * --------------------
 */
_kernel_oserror *driver1_init_handler(_kernel_swi_regs *r, void *pw)
{
  return NULL;
}
_kernel_oserror *driver2_init_handler(_kernel_swi_regs *r, void *pw)
{
  return NULL;
}
_kernel_oserror *driver3_init_handler(_kernel_swi_regs *r, void *pw)
{
  return NULL;
}
_kernel_oserror *driver4_init_handler(_kernel_swi_regs *r, void *pw)
{
  return NULL;
}


/*
 * driver_receive_handlers
 * -----------------------
 */
static _kernel_oserror *driver_receive_handler(int port, unsigned int cmd)
{
  struct rx_s *b = &mod.rx[port];
  int status = cmd & 0xff;
  int timing_msg = (status == TIMING_CLOCK) || (status == START) || (status == CONTINUE) || (status == STOP);

  if(mod.flags & (1 << (ACTIVE_SENSE_RX + port)))
    b->gap_count = 0; // reset inactivity count on any received message

  if(timing_msg && (mod.flags & (1 << IGNORE_TIMING)))
    return NULL;

  // action system real time messages and SONG_POSITION but not SYSTEM_RESET
  if(timing_msg || (status == ACTIVE_SENSING) || (status == SONG_POSITION))
  {
    if(!(mod.flags & (1 << IGNORE_REAL_TIME)))
      switch(status)
      {
        case START:
          if(!(mod.flags & (1 << INT_TIMING)))
          {
            mod.flags |= (1 << (TIMING_CLK_RX + port)) | (1 << EXT_TIMING);
            mod.midi_beat = 0;
          }
          break;

        case TIMING_CLOCK:
          if(mod.flags & (1 << (TIMING_CLK_RX + port)))
            mod.midi_beat++;  // increment midi clock
          break;

        case STOP:
          mod.flags &= ~(1 << (TIMING_CLK_RX + port));
          if(!(mod.flags & (15 << TIMING_CLK_RX))) // if all off, clear external timing
            mod.flags &= ~(1 << EXT_TIMING);
          break;

        case CONTINUE:
          if(!(mod.flags & (1 << INT_TIMING)))
            mod.flags |= (1 << (TIMING_CLK_RX + port)) | (1 << EXT_TIMING);
          break;

        case ACTIVE_SENSING:
          mod.flags |= (1 << (ACTIVE_SENSE_RX + port));
          b->gap_count = 0;
          break;

        case SONG_POSITION:
          if(mod.flags & (1 << (TIMING_CLK_RX + port)))
          {
            int posn = ((cmd >> 8) & 0x7f) | ((cmd >> 9) & 0x3f80);
            mod.midi_beat = MIDI_BEATS_PER_SONG_POSN * posn;
          }
          break;
      }

    if(!(mod.flags & (1 << STORE_REAL_TIME)))
      return NULL;
  }

  if(b->wr == b->rd) // empty?
  {
    generate_event(Event_MIDI_DataReceived);

    if(mod.debug & (1<<DBG_MSG))
      if(mod.log)fprintf(mod.log, "%u: %d Event RxData\n", mod.ticks, port);
  }

  int next = (b->wr < (RXBUFF_SIZE-1)) ? b->wr + 1 : 0;
  if(next != b->rd) // not full
  {
    b->buff[b->wr] = cmd; // store command
    b->time[b->wr] = get_timestamp(port); // store timestamp

    if(b->rd == b->wr)
      b->shift = 0; // if empty set next byte to read from next command to read
    b->wr = next;
  }
  else // error, buffer full
  {
    mod.error = (mod.error & ~(0xff << (port * 8))) | ('B' << (port * 8));
    generate_event(Event_MIDI_Error);
  }

  if(mod.debug & (1<<DBG_MSG))
    if(mod.log)fprintf(mod.log, "%u: %d -> %08X rx in\n", mod.ticks, port, cmd);

  return NULL;
}
_kernel_oserror *driver1_receive_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_receive_handler(0, r->r[0]);
}
_kernel_oserror *driver2_receive_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_receive_handler(1, r->r[0]);
}
_kernel_oserror *driver3_receive_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_receive_handler(2, r->r[0]);
}
_kernel_oserror *driver4_receive_handler(_kernel_swi_regs *r, void *pw)
{
  return driver_receive_handler(3, r->r[0]);
}


/*
 * timer_callback_handler
 * ----------------------
 * This is called every ms by the MTimer module.
 */
_kernel_oserror *timer_callback_handler(_kernel_swi_regs *r, void *pw)
{
  int i;

  mod.ticks = r->r[1];
  queue_clock(r->r[2]); // advance clock by the number of ticks (ms) since last call

  // update the microbeat bar/beat counter
  i = read_bar_beat(); // returns 1 if 16 microbeats have elapsed

  // check for timing messages tp send
  if(mod.flags & (1 << INT_TIMING))
  {
    int send_timing_msg = 0;
    if(!(mod.flags & (1 << FAST_CLOCK)))
    {
      if(i == 1)
      {
        mod.midi_beat++;
        send_timing_msg = 1; // send a microbeat timed timing message if enabled
      }
    }
    else // Fast Clock mode
    {  // check for ms timed timing messages to send
      if(mod.tx_rate > 0)
      {
        mod.tx_count += r->r[2] << MIDI_CLK_RATE_Q;
        while(mod.tx_count >= mod.tx_rate)
        {
          mod.tx_count -= mod.tx_rate;
          mod.midi_beat++;
          send_timing_msg = 1; // send a fast clock timed timing message if enabled

          { // this was for checking the fractional tx midi clock rate
            static unsigned int old_ticks;
            if(mod.debug & 4)
              if(mod.log)fprintf(mod.log, "%d ", r->r[1] - old_ticks);
            old_ticks = r->r[1];
          }
        }
      }
    }
    if(send_timing_msg)
      for(i=0; i<NUM_PORTS; i++)
#ifdef INDEPENDENT_TX_TIMING
        if(mod.flags & (1 << (TIMING_CLK_TX + i)))
#endif
          tx_command(TIMING_CLOCK | (1<<24) | (i<<28));
  }

  // check scheduled command queue
  static int was_emptying;
  int emptying = 0;
  for(i=0; i<NUM_PORTS; i++)
  {
    queue_read(i);  // check and send any scheduled events that are due
    if(queue_check(i)) // check if queue is nearly empty
      emptying = 1;
  }
  if(emptying && !was_emptying)
    generate_event(Event_MIDI_QueueEmptying);
  was_emptying = emptying;

  // Active sense
  for(i=0; i<NUM_PORTS; i++)
  {
    // check received active sense counters
    if(mod.flags & (1 << (ACTIVE_SENSE_RX + i)))
      if(++mod.rx[i].gap_count == ACTIVE_SENSE_RX_TIMEOUT)
      {
        mod.error = (mod.error & ~(0xff << (i * 8))) | ('A' << (i * 8));
        generate_event(Event_MIDI_Error);
      }

    // check transmitted active sense counters
    if(mod.flags & (1 << (ACTIVE_SENSE_TX + i)))
      if(++mod.tx[i].gap_count == ACTIVE_SENSE_TX_TIMEOUT)
        tx_command(ACTIVE_SENSING | (1<<24) | (i<<28));
  }

  // this was for checking the callback delay from the 1ms interrupt handler in MTimer
  if(mod.debug & 2)
    if(mod.log)fprintf(mod.log, "%d ", r->r[2]);

  return NULL;
}


/*
 * service_callback_handler
 * ------------------------
 */
_kernel_oserror *service_callback_handler(_kernel_swi_regs *r, void *pw)
{
  if(!mod.registered)
    driver_register(pw);

  return NULL;
}


/*
 * service_midi
 * ------------
 * Handles MIDI service calls
 */
void service_midi(int service, _kernel_swi_regs *r, void *pw)
{
  _kernel_swi_regs regs;

  if(service == Service_MIDI)
  {
    if((r->r[0] == Service_MIDISupportAlive) && !mod.registered)
    {
      regs.r[0] = (int)service_callback;
      regs.r[1] = (int)pw;
      _kernel_swi(OS_AddCallBack, &regs, &regs);
    }
    else if((r->r[0] == Service_MIDISupportDying) && mod.registered)
    {
      mod.support.receive = 0;
      mod.support.give = 0;
      mod.registered = 0;
    }
  }
}

// helpers for displaying the module status

// returns a string of up to 4 numbers for 4 flag bits if set
static char *flags2str(int start)
{
  static char str[12];
  char *s = str;
  int flags = mod.flags;

  if(!(flags & (15 << start)))
    return "(none)";

  int i;
  for(i=0; i<4; i++)
  {
    if(flags & (1 << (start + i)))
    {
      *s++ = i + '0';
      *s++ = ' ';
    }
  }
  *s = 0;
  return str;
}

// returns a string description of the supplied error character
static char *err2str(int err)
{
  if(err == 'A')
    return "Active Sense failure (signal loss)";
  if(err == 'B')
    return "Receive buffer overflow";
  return "(none)";
}

// returns a printable decimal number for a binary fraction
static int frac_to_int(int num, int start)
{
  int frac = 500; // start with a half (0.5)
  int acc = 0;
  int mask = (1 << start); // first fractional bit
  while(mask && frac)
  {
    if(num & mask)
      acc += frac;
    mask >>= 1;
    frac >>= 1;
  }
  return acc;
}

/*
 * read_options
 * ------------
 * Reads options for the MidiSynth command
 */
static void read_options(int argc, const char *arg_string, void *pw)
{
  char c;
  int i;

  do
  {
    while((c = *arg_string++) == ' ');
    if(c == '-')
      switch((c = *arg_string++) & ~0x20)
      {
        case 'I': // display module status
          printf("\nDefaults: Tx port = %d, Tx channel = %d\n",
                  mod.tx_chan >> 4, (mod.tx_chan & 15) + 1);
          printf("Timing mode: %s\n", (mod.flags&(1<<INT_TIMING)) ? "Internal" :
                                      (mod.flags&(1<<EXT_TIMING)) ? "External" :
                                      "Unset");
          printf("Fast Clock %s\n", (mod.flags & (1<<FAST_CLOCK)) ? "enabled" : "disabled");
          printf("Sound system microbeat timing %s\n",
                  (mod.flags & (1<<MICROBEAT_TIMING)) ? "active" : "inactive");
          printf("Received System Real Time messages: %sstored, %s\n",
                  (mod.flags & (1<<STORE_REAL_TIME)) ? "" : "not ",
                  (mod.flags & (1<<IGNORE_REAL_TIME)) ? "ignored" : "actioned");
          printf("Received Timing messages: %s\n",
                  (mod.flags & (1<<IGNORE_TIMING)) ? "ignored" : "actioned");
          printf("Active Sensing:\n");
          printf("  Sent on port %s\n", flags2str(ACTIVE_SENSE_TX));
          printf("  Received on port %s\n", flags2str(ACTIVE_SENSE_RX));
          printf("Timing Clock messages:\n");
          printf("  Sent on port %s\n", flags2str(TIMING_CLK_TX));
          printf("  Received on port %s\n", flags2str(TIMING_CLK_RX));
          printf("Transmitted timing clock rate when in Fast Clock mode: %d.%02dms, %d bpm\n",
                   mod.tx_rate >> MIDI_CLK_RATE_Q, frac_to_int(mod.tx_rate, MIDI_CLK_RATE_Q-1) / 10,
                   ((2500 << MIDI_CLK_RATE_Q) + (mod.tx_rate / 2)) / mod.tx_rate); // rounded
          printf("Errors:\n");
          for(i=0; i<4; i++)
            printf("  Port %d: %s\n", i, err2str((mod.error >> (i * 8)) & 0xff));
          printf("\n");
          break;

        case 'O': // open log file
          if(mod.log)
            printf("Log file already open\n");
          else
          {
            mod.log = fopen("logM", "w"); // log file opened in the current directory
            if(!mod.log)
              printf("Couldn't open Log file\n");
            else
              printf("Log file opened\n");
          }
          break;

        case 'C': // close log file
          if(mod.log)
          {
            fclose(mod.log);
            printf("Log file closed\n");
            mod.log = 0;
          }
          else
            printf("Log file not open\n");
          break;

        case 'D': // debug options
          mod.debug = atoi(arg_string);
          break;

        default: printf("unknown option (-%c)\n", c);
      }
  }
  while(c >= ' ');
}

