#include "cwCommon.h"
#include "cwLog.h"
#include "cwCommonImpl.h"
#include "cwTest.h"
#include "cwMem.h"
#include "cwText.h"
#include "cwObject.h"
#include "cwFileSys.h"
#include "cwFile.h"
#include "cwTime.h"
#include "cwMidiDecls.h"
#include "cwMidi.h"
#include "cwMidiFile.h"
#include "cwUiDecls.h"
#include "cwIo.h"
#include "cwScoreFollowerPerf.h"
#include "cwIoMidiRecordPlay.h"
#include "cwMidiState.h"
#include "cwSvgMidi.h"

#define TIMER_LABEL "midi_record_play_timer"

namespace cw
{
  namespace midi_record_play
  {
    enum {
      kStoppedMidiStateId = 0,
      kPlayingMidiStateId,
      kStoppingMidiStateId
    };
    
    typedef struct midi_state_str
    {
      uint8_t keyVel[ midi::kMidiNoteCnt ];
      uint8_t ctlVal[ midi::kMidiCtlCnt ];
      unsigned state;
    } midi_state_t;
    
    typedef struct am_midi_msg_str
    {
      unsigned      devIdx;
      unsigned      portIdx;
      unsigned      microsec;

      unsigned      id;
      time::spec_t  timestamp;
      unsigned      loc;
      const void*   arg;
      uint8_t       ch;
      uint8_t       status;
      uint8_t       d0;
      uint8_t       d1;

      unsigned      chordNoteCnt; // count of notes in same chord as this note (only set on Note-on messages)
      
    } am_midi_msg_t;

    typedef struct vel_tbl_str
    {
      uint8_t*    velTblA;
      unsigned    velTblN;
      bool        enableFl;
      bool        defaultFl;
      char*       name;
      char*       device;
    } vel_tbl_t;

    typedef struct midi_device_str
    {
      char*          label;
      
      char*          midiOutDevLabel;
      char*          midiOutPortLabel;
      unsigned       midiOutDevIdx;
      unsigned       midiOutPortIdx;

      bool           enableFl;
      unsigned       velTableN;
      uint8_t*       velTableArray;
      bool           pedalMapEnableFl;
      
      unsigned       pedalDownVelId;
      unsigned       pedalDownVel;

      unsigned       pedalUpVelId;
      unsigned       pedalUpVel;
      
      unsigned       pedalDownHalfVelId;
      unsigned       pedalDownHalfVel;

      unsigned       pedalUpHalfVelId;
      unsigned       pedalUpHalfVel;

      unsigned       velHistogram[ midi::kMidiVelCnt ];

      bool           force_damper_down_fl;
      unsigned       force_damper_down_threshold;
      unsigned       force_damper_down_velocity;

      bool           damper_dead_band_enable_fl;
      unsigned       damper_dead_band_min_value;
      unsigned       damper_dead_band_max_value;

      bool           scale_chord_notes_enable_fl;
      double         scale_chord_notes_factor;

      midi_state_t   midi_state;
      
    } midi_device_t;

    enum
    {
      kHalfPedalDone,
      kWaitForBegin,
      kWaitForNoteOn,
      kWaitForNoteOff,
      kWaitForPedalUp,
      kWaitForPedalDown,
    };

    typedef struct midi_record_play_str
    {
      io::handle_t   ioH;
      
      am_midi_msg_t* msgArray;                    // msgArray[ msgArrayN ]
      unsigned       msgArrayN;                   // Count of messages allocated in msgArray.
      unsigned       msgArrayInIdx;               // Next available space for loaded MIDI messages (also the current count of msgs in msgArray[])
      unsigned       msgArrayOutIdx;              // Next message to transmit in msgArray[]     
      unsigned       midi_timer_period_micro_sec; // Timer period in microseconds
      unsigned       midi_timer_index;             // 
      unsigned       all_off_delay_ms;            // Wait this long before turning all notes off after the last note-on has played

      am_midi_msg_t* iMsgArray;                    // msgArray[ msgArrayN ]
      unsigned       iMsgArrayN;                   // Count of messages allocated in msgArray.
      unsigned       iMsgArrayInIdx;               // Next available space for incoming MIDI messages (also the current count of msgs in msgArray[])

      unsigned       lastStoreIndex;
      
      midi_device_t* midiDevA;
      unsigned       midiDevN;

      bool           startedFl;
      bool           recordFl;
      bool           muteFl;
      bool           thruFl;
      bool           logInFl;   // log incoming message when not in 'record' mode.
      bool           logOutFl;  // log outgoing messages
      bool           supressMidiXmitFl; // don't transmit MIDI

      unsigned       minDamperDownMs;
      bool           velHistogramEnableFl;
      
      bool     halfPedalFl;
      unsigned halfPedalState;
      unsigned halfPedalNextUs;
      unsigned halfPedalNoteDelayUs;
      unsigned halfPedalNoteDurUs;
      unsigned halfPedalUpDelayUs;
      unsigned halfPedalDownDelayUs;
      uint8_t  halfPedalMidiPitch;
      uint8_t  halfPedalMidiNoteVel;
      uint8_t  halfPedalMidiPedalVel;
            
      time::spec_t   play_time;                
      time::spec_t   start_time;
      time::spec_t   end_play_event_timestamp;
      time::spec_t   all_off_timestamp;

      event_callback_t cb;
      void*            cb_arg;

    } midi_record_play_t;
    
    enum
    {
      kMidiRecordPlayTimerId
    };

    midi_record_play_t* _handleToPtr( handle_t h )
    { return handleToPtr<handle_t,midi_record_play_t>(h); }

    rc_t _destroy( midi_record_play_t* p )
    {
      rc_t rc = kOkRC;
      
      if(p->midi_timer_index != kInvalidIdx )
        io::timerDestroy( p->ioH, p->midi_timer_index);

      for(unsigned i=0; i<p->midiDevN; ++i)
      {
        mem::release(p->midiDevA[i].label);
        mem::release(p->midiDevA[i].midiOutDevLabel);
        mem::release(p->midiDevA[i].midiOutPortLabel);
        mem::release(p->midiDevA[i].velTableArray);
      }

      mem::release(p->midiDevA);
      mem::release(p->msgArray);
      mem::release(p->iMsgArray);
      mem::release(p);
    
      return rc;
    }

    unsigned _label_to_device_index( midi_record_play_t* p, const char* label )
    {
      for(unsigned i=0; i<p->midiDevN; ++i)
        if( textCompare(p->midiDevA[i].label,label) == 0 )
          return i;
      
      return kInvalidIdx;
    }

    void _xmit_midi( midi_record_play_t* p, unsigned devIdx, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 )
    {
      if( !p->supressMidiXmitFl && p->midiDevA[devIdx].enableFl )
        io::midiDeviceSend( p->ioH, p->midiDevA[devIdx].midiOutDevIdx, p->midiDevA[devIdx].midiOutPortIdx, status + ch, d0, d1 );      
    }
        

    // initialze the midi state to all-notes-off and all controllers=0
    void _midi_state_clear( midi_record_play_t* p)
    {      
      for(unsigned i=0; i<p->midiDevN; ++i)
      {
        for(unsigned j=0; j<midi::kMidiNoteCnt; ++j)
          p->midiDevA[i].midi_state.keyVel[j] = 0;
      
        for(unsigned j=0; j<midi::kMidiCtlCnt; ++j)
          p->midiDevA[i].midi_state.ctlVal[j] = 0;

        p->midiDevA[i].midi_state.state = kPlayingMidiStateId;
      }      
    }

    void _midi_state_set_state( midi_record_play_t* p, unsigned stateId )
    {
      for(unsigned i=0; i<p->midiDevN; ++i)
        p->midiDevA[i].midi_state.state = stateId;
    }

    bool _midi_state_is_state( midi_record_play_t* p, unsigned stateId )
    {
      for(unsigned i=0; i<p->midiDevN; ++i)
        if( p->midiDevA[i].midi_state.state != stateId )
          return false;
      return true;
    }

    
    void _midi_state_set_stopping( midi_record_play_t* p )
    { _midi_state_set_state(p,kStoppingMidiStateId); }
    
    bool _midi_state_is_stopping( midi_record_play_t* p )
    { return _midi_state_is_state(p,kStoppingMidiStateId); }

    bool _midi_state_is_stopped( midi_record_play_t* p )
    { return _midi_state_is_state(p,kStoppedMidiStateId); }
  
    unsigned _midi_state_active_note_count( midi_record_play_t* p )
    {
      unsigned n = 0;
      for(unsigned i=0; i<p->midiDevN; ++i)
        for(unsigned j=0; j<midi::kMidiNoteCnt; ++j)
          n += p->midiDevA[i].midi_state.keyVel[j] > 0 ? 1 : 0;

      return n;
    }

    rc_t  _midi_state_update( midi_record_play_t* p, midi_device_t& dev, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 )
    {
      rc_t rc = kOkRC;

      if( ch != 0 )
      {
        rc = cwLogError(kInvalidArgRC,"Only the state of MIDI channel 0 tracked.");
        goto errLabel;
      }

      if( d0 > 127 || d1 > 127 )
      {
        rc = cwLogError(kInvalidArgRC,"Illegal MIDI byte value: st:%i d0:%i d1:%i",status,d0,d1);
        goto errLabel;
      }

      if( midi::isNoteOn(status,d1) )
      {
        if( dev.midi_state.state == kPlayingMidiStateId )
          dev.midi_state.keyVel[ d0 ] = d1;
        goto errLabel;
      }
      
      if( midi::isNoteOff(status,d1))
      {   
        dev.midi_state.keyVel[ d0 ] = 0;  
        goto errLabel;
      }

      if( midi::isCtl(status) )
      {
        dev.midi_state.ctlVal[ d0 ] = d1;
        goto errLabel;
      }
    
    errLabel:

      
      return rc;
    }

    bool _midi_state_are_all_notes_off( midi_record_play_t* p )
    {
      if( _midi_state_is_stopped( p ) )
        return true;
      
      if( _midi_state_is_stopping( p ) )
        if( _midi_state_active_note_count( p ) == 0 )
        {
          _midi_state_set_state(p,kStoppedMidiStateId);
          return true;
        }
      
      return false;
    }

    uint8_t _midi_state_ctl_value( midi_record_play_t* p, midi_device_t& dev, uint8_t d0 )
    {
      return dev.midi_state.ctlVal[d0];
    }

    void _midi_state_xmit_pedals( midi_record_play_t* p )
    {
      uint8_t ctlA[] = { midi::kSustainCtlMdId, midi::kSostenutoCtlMdId, midi::kSoftPedalCtlMdId };
      unsigned ctlN = sizeof(ctlA)/sizeof(ctlA[0]);
  
      for(unsigned i=0; i<p->midiDevN; ++i)
        for(unsigned j=0; j<ctlN; ++j)
        {
          uint8_t d1 = _midi_state_ctl_value( p, p->midiDevA[i], ctlA[j] );
          _xmit_midi(p, i, 0, midi::kCtlMdId, ctlA[j], d1 );
        }
    
    }
    
    void _set_chord_note_count( am_midi_msg_t* m0, const am_midi_msg_t* m, unsigned chordNoteCnt )
    {
      for(; m0<m; ++m0)
        if( midi::isNoteOn(m0->status,m0->d1) )
          m0->chordNoteCnt = chordNoteCnt;
      
    }
    
    void _set_chord_note_count( midi_record_play_t* p )
    {
      unsigned             chordNoteCnt = 1;
      am_midi_msg_t*       m0           = p->msgArray;
      am_midi_msg_t*       m            = nullptr;
      
      for(unsigned i=1; i<p->msgArrayN; ++i)
      {
        m = p->msgArray + i;
        if( midi::isNoteOn(m->status,m->d1) )
        {
          if( time::isEqual(m0->timestamp, m->timestamp) )
            ++chordNoteCnt;
          else
          {
            _set_chord_note_count(m0,m,chordNoteCnt);
          
            chordNoteCnt=1;
            m0 = m;
          }
        }
      }

      _set_chord_note_count(m0,m,chordNoteCnt);      
    }

    rc_t _parseCfg(midi_record_play_t* p, const object_t& cfg )
    {
      rc_t rc = kOkRC;
      const object_t* midiDevL = nullptr;
      if((rc = cfg.getv(
                        "max_midi_msg_count",          p->msgArrayN,
                        "midi_timer_period_micro_sec", p->midi_timer_period_micro_sec,
                        "all_off_delay_ms",            p->all_off_delay_ms,
                        "midi_device_list",            midiDevL,
                        "log_in_flag",                 p->logInFl,
                        "log_out_flag",                p->logOutFl,
                        "min_damper_down_time_ms",     p->minDamperDownMs,
                        "half_pedal_flag",             p->halfPedalFl)) != kOkRC )
      {
        rc = cwLogError(kSyntaxErrorRC,"MIDI record play configuration parse failed.");
        goto errLabel;
      }

      p->iMsgArrayN = p->msgArrayN;
      p->lastStoreIndex = kInvalidIdx;
      
      if( midiDevL->child_count() > 0 )
      {
        p->midiDevN = midiDevL->child_count();
        p->midiDevA = mem::allocZ<midi_device_t>(p->midiDevN);

        printf("Midi record play devices:%i\n",p->midiDevN);
        
        for(unsigned i=0; i<p->midiDevN; ++i)
        {
          const object_t* ele              = midiDevL->child_ele(i);
          const char*     label            = nullptr;
          const char*     midiOutDevLabel  = nullptr;
          const char*     midiOutPortLabel = nullptr;
          const object_t* pedalRecd        = nullptr;
          bool            enableFl         = false;
          
          if((rc = ele->getv( "label",             label,
                              "midi_out_device",   midiOutDevLabel,
                              "midi_out_port",     midiOutPortLabel,
                              "enableFl",          enableFl)) != kOkRC )
          {
            rc = cwLogError(kSyntaxErrorRC,"MIDI record play device list configuration parse failed.");
            goto errLabel;          
          }
          

          if((rc = ele->getv_opt(
                                 "pedal",     pedalRecd,
                                 "force_damper_down_fl",p->midiDevA[i].force_damper_down_fl,
                                 "force_damper_down_threshold",p->midiDevA[i].force_damper_down_threshold,
                                 "force_damper_down_velocity", p->midiDevA[i].force_damper_down_velocity,
                                 "damper_dead_band_enable_fl", p->midiDevA[i].damper_dead_band_enable_fl,
                                 "damper_dead_band_min_value", p->midiDevA[i].damper_dead_band_min_value,
                                 "damper_dead_band_max_value", p->midiDevA[i].damper_dead_band_max_value,
                                 "scale_chord_notes_enable_fl",p->midiDevA[i].scale_chord_notes_enable_fl,
                                 "scale_chord_notes_factor",   p->midiDevA[i].scale_chord_notes_factor)) != kOkRC )
          {
            rc = cwLogError(kSyntaxErrorRC,"MIDI record play device optional argument parsing failed.");
            goto errLabel;          
          }

          cwLogInfo("Force Pedal: enabled:%i thresh:%i veloc:%i dead band: enable:%i min:%i max:%i",
                    p->midiDevA[i].force_damper_down_fl,
                    p->midiDevA[i].force_damper_down_threshold,
                    p->midiDevA[i].force_damper_down_velocity,
                    p->midiDevA[i].damper_dead_band_enable_fl,
                    p->midiDevA[i].damper_dead_band_min_value,
                    p->midiDevA[i].damper_dead_band_max_value );

          p->midiDevA[i].label            = mem::duplStr( label );
          p->midiDevA[i].midiOutDevLabel  = mem::duplStr( midiOutDevLabel);
          p->midiDevA[i].midiOutPortLabel = mem::duplStr( midiOutPortLabel);
          p->midiDevA[i].enableFl         = enableFl;

          if( pedalRecd != nullptr )
          {
            if((rc = pedalRecd->getv( "down_id",  p->midiDevA[i].pedalDownVelId,
                                      "down_vel", p->midiDevA[i].pedalDownVel,
                                      "up_id",    p->midiDevA[i].pedalUpVelId,
                                      "up_vel",   p->midiDevA[i].pedalUpVel,
                                      "half_down_id",  p->midiDevA[i].pedalDownHalfVelId,
                                      "half_down_vel", p->midiDevA[i].pedalDownHalfVel,
                                      "half_up_id",  p->midiDevA[i].pedalUpHalfVelId,
                                      "half_up_vel", p->midiDevA[i].pedalUpHalfVel
                                      )) != kOkRC )
            {
              rc = cwLogError(kSyntaxErrorRC,"An error occured while parsing the pedal record for MIDI device:'%s' port:'%s'.",midiOutDevLabel,midiOutPortLabel);
              goto errLabel;              
            }
            else
            {
              p->midiDevA[i].pedalMapEnableFl = true;
            }
          }
          
          
        }
      }
      
      // allocate the MIDI msg buffer
      p->msgArray         = mem::allocZ<am_midi_msg_t>( p->msgArrayN );
      p->iMsgArray        = mem::allocZ<am_midi_msg_t>( p->iMsgArrayN );
      
    errLabel:
      return rc;
    }

    rc_t _stop( midi_record_play_t* p );

    
    const am_midi_msg_t*  _midi_store( midi_record_play_t* p, unsigned devIdx, unsigned portIdx, const time::spec_t& ts, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 )
    {
      am_midi_msg_t* am = nullptr;

      //if( !midi::isPedal(status,d0) )
      //  printf("MIDI store: %i : ch:%i st:%i d0:%i d1:%i\n",p->iMsgArrayInIdx,ch,status,d0,d1);

      p->lastStoreIndex = kInvalidIdx;
      
      // verify that space exists in the record buffer
      if( p->iMsgArrayInIdx < p->iMsgArrayN )
      {
        // MAKE THIS ATOMIC
        unsigned id       =  p->iMsgArrayInIdx;
        p->lastStoreIndex =  p->iMsgArrayInIdx;
        
        ++p->iMsgArrayInIdx;

        am = p->iMsgArray + id;
        
        am->id        = id;
        am->devIdx    = devIdx;
        am->portIdx   = portIdx;
        am->timestamp = ts;
        am->ch        = ch;
        am->status    = status;
        am->d0        = d0;
        am->d1        = d1;
        
      }

      return am;
    }

    rc_t _event_callback( midi_record_play_t* p, unsigned id, const time::spec_t timestamp, unsigned loc, const void* arg, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1, unsigned chordNoteCnt, bool log_fl=true )
    {
      rc_t rc = kOkRC;
      // if we have arrived at the stop time (but we may still be waiting for note-end messages)
      bool after_stop_time_fl = !time::isZero(p->end_play_event_timestamp) && time::isGT(timestamp,p->end_play_event_timestamp);

      if( after_stop_time_fl )
        _midi_state_set_stopping(p);

      // if we are after the stop time and after the all-notes-off time
      // bool after_all_off_fl   = after_stop_time_fl && time::isGT(timestamp,p->all_off_timestamp);

      // if we are after the stop time and there are no  more active notes
      bool after_all_off_fl  = after_stop_time_fl && _midi_state_are_all_notes_off(p);
      
      bool is_note_on_fl = midi::isNoteOn(status,d1);
      bool is_damper_fl = midi::isSustainPedal(status,d0); 
      bool supress_fl = (is_note_on_fl && after_stop_time_fl) || p->muteFl;
      bool is_pedal_fl = midi::isPedal( status, d0 );
      
      if( after_all_off_fl )
      {
        rc = _stop(p);
      }
      else        
      {

        if( p->halfPedalFl )
        {
          if( status==midi::kCtlMdId && d0 == midi::kSustainCtlMdId && d1 != 0 )
            d1 = p->halfPedalMidiPedalVel;
        }
        

        // for each midi device
        for(unsigned i=0; i<p->midiDevN; ++i)
          if(p->midiDevA[i].enableFl )
          {
            uint8_t out_d1 = d1;

            if( !p->halfPedalFl )
            {
              // if this is a note-on and a vel. table exists
              if( is_note_on_fl and p->midiDevA[i].velTableArray != nullptr )
              {
                // verify that the vel. is legal given the table
                if( d1 >= p->midiDevA[i].velTableN )
                  cwLogError(kInvalidIdRC,"A MIDI note-on velocity (%i) outside the velocity table (%i>%i) range was encountered on device:%s.",d1,(p->midiDevA[i].velTableN)-1,cwStringNullGuard(p->midiDevA[i].label));
                else                
                  out_d1 = p->midiDevA[i].velTableArray[ d1 ];

                // scale the note velocities of chords to account for their combined volumes
                if( p->midiDevA[i].scale_chord_notes_enable_fl && chordNoteCnt>1 )
                {
                  uint8_t delta = (uint8_t)lround(out_d1 * p->midiDevA[i].scale_chord_notes_factor * (chordNoteCnt-1) );
                  if( delta < out_d1 )
                  {
                    //printf("%i %i %i %i\n",chordNoteCnt,delta,out_d1,out_d1-delta);
                    out_d1 -= delta;
                  }
                }
                
              }

              // store the note-on velocity histogram data
              if( p->velHistogramEnableFl && is_note_on_fl && out_d1 < midi::kMidiVelCnt )
                p->midiDevA[i].velHistogram[ out_d1 ] += 1;

              // if the damper pedal velocity is in the dead band then don't send it
              if( p->midiDevA[i].damper_dead_band_enable_fl && is_pedal_fl && p->midiDevA[i].damper_dead_band_min_value <= out_d1 && out_d1 <= p->midiDevA[i].damper_dead_band_max_value )
                out_d1 = 0;
              
              // if the damper pedal velocity is over the 'forcing' threshold then force the damper down
              if( p->midiDevA[i].force_damper_down_fl && is_damper_fl && out_d1>p->midiDevA[i].force_damper_down_threshold )
                out_d1 = p->midiDevA[i].force_damper_down_velocity;

              // map the pedal  velocity
              if( status==midi::kCtlMdId && d0 == midi::kSustainCtlMdId && p->midiDevA[i].pedalMapEnableFl )
              {
                if( d1 == p->midiDevA[i].pedalUpVelId )
                  out_d1 = p->midiDevA[i].pedalUpVel;
                else
                  if( d1 == p->midiDevA[i].pedalDownVelId )
                    out_d1 = p->midiDevA[i].pedalDownVel;
                  else
                    if( d1 == p->midiDevA[i].pedalDownHalfVelId )
                      out_d1 = p->midiDevA[i].pedalDownHalfVel;
                    else
                    {
                      cwLogError(kInvalidIdRC,"Unexpected pedal down velocity (%i) during pedal velocity mapping. Remove the 'pedal' stanza from the MIDI device cfg to prevent pedal mapping.",d1);
                    }
              }
            }

            _midi_state_update(p, p->midiDevA[i], ch, status, d0, out_d1 );
      
            if( !supress_fl )
            {
              _xmit_midi( p, i, ch, status, d0, out_d1 );
        
            }
          }

        // don't inform the app if we are past the end time
        if( !after_stop_time_fl and p->cb )
          p->cb( p->cb_arg, kMidiEventActionId, id, timestamp, loc, arg, ch, status, d0, d1 );

        if( log_fl && p->logOutFl )
        {
          // Note: The device of outgoing messages is set to p->midiDevN + 1 to distinguish it from
          // incoming messages.
          _midi_store( p, p->midiDevN, 0, timestamp, ch, status, d0, d1 );
        }
        
      }
      
      return rc;
    }


    rc_t _transmit_msg( midi_record_play_t* p, const am_midi_msg_t* am, bool log_fl=true )
    {
      return _event_callback( p, am->id, am->timestamp, am->loc, am->arg, am->ch, am->status, am->d0, am->d1, am->chordNoteCnt, log_fl );
    }

    rc_t _transmit_note( midi_record_play_t* p, unsigned ch, unsigned pitch, unsigned vel, unsigned microsecs )
    {
      time::spec_t ts = {};
      time::microsecondsToSpec( ts, microsecs );
      return _event_callback( p, kInvalidId, ts, kInvalidId, nullptr, ch, midi::kNoteOnMdId, pitch, vel, 0 );
    }

    rc_t _transmit_ctl(  midi_record_play_t* p, unsigned ch, unsigned ctlId, unsigned ctlVal, unsigned microsecs )
    {
      time::spec_t ts = {};
      time::microsecondsToSpec( ts, microsecs );
      return _event_callback( p, kInvalidId, ts, kInvalidId, nullptr, ch, midi::kCtlMdId, ctlId, ctlVal, 0 );
    }
    
    rc_t _transmit_pedal( midi_record_play_t* p, unsigned ch, unsigned pedalCtlId, bool pedalDownFl, unsigned microsecs )
    {
      return _transmit_ctl( p, ch, pedalCtlId, pedalDownFl ? 127 : 0, microsecs);
    }

    void _half_pedal_update( midi_record_play_t* p, unsigned cur_time_us )
    {
      if( cur_time_us >= p->halfPedalNextUs )
      {
        unsigned midi_ch = 0;
        switch( p->halfPedalState )
        {
          
          case kWaitForBegin:
            printf("down: %i %i\n",cur_time_us/1000,p->halfPedalMidiPedalVel);
            _transmit_ctl( p, midi_ch, midi::kSustainCtlMdId, p->halfPedalMidiPedalVel, cur_time_us);
            p->halfPedalState = kWaitForNoteOn;
            p->halfPedalNextUs += p->halfPedalNoteDelayUs;
            break;
          
          case kWaitForNoteOn:
            printf("note: %i\n",cur_time_us/1000);
            _transmit_note( p, midi_ch, p->halfPedalMidiPitch, p->halfPedalMidiNoteVel, cur_time_us );
            p->halfPedalNextUs += p->halfPedalNoteDurUs;
            p->halfPedalState = kWaitForNoteOff;
            break;
              
          case kWaitForNoteOff:
            printf("off:  %i\n",cur_time_us/1000);
            _transmit_note( p, midi_ch, p->halfPedalMidiPitch, 0, cur_time_us );
            p->halfPedalNextUs += p->halfPedalUpDelayUs;
            p->halfPedalState = kWaitForPedalUp;
            break;
            
          case kWaitForPedalUp:
            printf("up:   %i\n",cur_time_us/1000);
            _transmit_ctl( p, midi_ch, midi::kSustainCtlMdId, 0, cur_time_us);
            p->halfPedalNextUs += p->halfPedalDownDelayUs;
            p->halfPedalState = kWaitForPedalDown;            
            break;

          case kWaitForPedalDown:
            //printf("down:   %i\n",cur_time_us/1000);
            //_transmit_ctl( p, midi_ch, midi::kSustainCtlMdId, p->halfPedalMidiPedalVel, cur_time_us);
            //_stop(p);
            p->halfPedalState = kHalfPedalDone;
            break;
            
          
          case kHalfPedalDone:
            break;
        }
      }
    }

    // Set the next location to store an incoming MIDI message
    void _set_midi_msg_next_index( midi_record_play_t* p, unsigned next_idx )
    {
      p->iMsgArrayInIdx = next_idx;
      if( next_idx == 0 )
        p->lastStoreIndex = kInvalidIdx;
    }

    // Set the next index of the next MIDI message to transmit
    void _set_midi_msg_next_play_index(midi_record_play_t* p, unsigned next_idx)
    {
      p->msgArrayOutIdx = next_idx;
    }


    cw::rc_t _am_file_read_version_0( const char* fn, file::handle_t fH, am_midi_msg_t* amMsgArray, unsigned msgN )
    {
      // version 0 record type
      typedef struct msg_str
      {
        unsigned dev_idx;
        unsigned port_idx;
        time::spec_t timestamp;
        uint8_t ch;
        uint8_t st;
        uint8_t d0;
        uint8_t d1;
        unsigned microsecs;
      } zero_msg_t;

      cw::rc_t    rc        = kOkRC;
      zero_msg_t* zMsgArray = mem::allocZ<zero_msg_t>(msgN);
      unsigned    fileByteN = msgN * sizeof(zero_msg_t);
      
      if((rc = file::read(fH,zMsgArray,fileByteN)) != kOkRC )
      {
        rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn );
        goto errLabel;
      }

      for(unsigned i=0; i<msgN; ++i)
      {
        am_midi_msg_t* am = amMsgArray + i;
        zero_msg_t*    zm = zMsgArray + i;
        
        am->devIdx        = zm->dev_idx;
        am->portIdx       = zm->port_idx;
        am->microsec      = zm->microsecs;
        am->id            = i;
        am->timestamp     = zm->timestamp;
        am->loc           = i;
        am->ch            = zm->ch;
        am->status        = zm->st;
        am->d0            = zm->d0;
        am->d1            = zm->d1;
      }
      
    errLabel:
      mem::release(zMsgArray);
      return rc;
    }

    // Read the am_midi_msg_t records from a file written by _midi_write()
    // If msgArrayCntRef==0 and msgArrayRef==NULL then an array will be allocated and it is up
    // to the caller to release it, otherwise the msgArrayCntRef should be set to the count
    // of available records in msgArrayRef[].  Note the if there are more records in the file
    // than there are record in msgArrayRef[] then a warning will be issued and only
    // msgArrayCntRef records will be returned.
    cw::rc_t _am_file_read( const char* fn, unsigned& msgArrayCntRef, am_midi_msg_t*& msgArrayRef )
    {
      
      rc_t           rc        = kOkRC;
      unsigned       recordN   = 0;      // count of records in the ifle
      unsigned       fileByteN = 0;      // count of bytes in the file
      int            version   = 0;      // version id (always a negative number)
      bool           alloc_fl  = false;
      bool           print_fl  = false;
      file::handle_t fH;

      if((rc = file::open(fH,fn,file::kReadFl)) != kOkRC )
      {
        rc = cwLogError(kOpenFailRC,"Unable to locate the AM file: '%s'.", fn );
        goto errLabel;
      }

      // read the first word - which is either a version id or the count of records in the file
      // if this is a version 0 file
      if((rc = file::read(fH,version)) != kOkRC )
      {
        rc = cwLogError(kReadFailRC,"Version read failed on Audio-MIDI file: '%s'.", fn );
        goto errLabel;
      }

      // if the version is greater than 0 then this is a version 0 file (and 'version' holds the record count.
      if( version > 0 )
      {
        recordN = (unsigned)version;
        version = 0;
      }
      else // otherwise the second word in the file holds the size of the file
      {
        if((rc = file::read(fH,recordN)) != kOkRC )
        {
          rc = cwLogError(kReadFailRC,"Header read failed on Audio-MIDI file: '%s'.", fn );
          goto errLabel;
        }
      }

      // if the output msg array was not allocated - then allocate it here 
      if( msgArrayCntRef == 0 || msgArrayRef == nullptr )
      {
        alloc_fl = true;
        msgArrayRef = mem::allocZ<am_midi_msg_t>(recordN);
      }
      else // if the msg array was allocated but is too small - then decrease the count of records to be read from the file
      {
        if( recordN > msgArrayCntRef )
        {
          cwLogWarning("The count of message in Audio-MIDI file '%s' reduced from %i to %i.", fn, recordN, msgArrayCntRef );
          recordN = msgArrayCntRef;          
        }
      }

      if( version == 0 )
      {
        // read the version 0 file into a temporary buffer then translate to am_midi_msg_t records
        if((rc = _am_file_read_version_0( fn, fH, msgArrayRef, recordN )) != kOkRC )
          goto errLabel;
      }
      else
      {
        fileByteN = recordN * sizeof(am_midi_msg_t);
        if((rc = file::read(fH,msgArrayRef,fileByteN)) != kOkRC )
        {
          rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn );
          goto errLabel;
        }
      }

      if( print_fl )
      {
        for(unsigned i=0; i<recordN; ++i)
        {
          am_midi_msg_t* m = msgArrayRef;        
          double dt = time::elapsedSecs( m[0].timestamp, m[i].timestamp );
          printf("%4i %4i : %6.2f %2x %2x %2x %2x : %6.2f\n", m[i].devIdx, m[i].portIdx, dt, m[i].ch, m[i].status, m[i].d0, m[i].d1, m[i].microsec/(1000.0*1000.0) );
        }
      }
      
      msgArrayCntRef = recordN;

    errLabel:

      if( rc != kOkRC and alloc_fl )
      {
        mem::release(msgArrayRef);
        msgArrayRef = nullptr;
        msgArrayCntRef = 0;
      } 
        
      return rc;        
    }

    // Fill the play buffer from a previously store AM file.
    rc_t _midi_read( midi_record_play_t* p, const char* fn )
    {
      rc_t           rc = kOkRC;
      unsigned       n  = 0;
      int            version;
      file::handle_t fH;

      if((rc = file::open(fH,fn,file::kReadFl)) != kOkRC )
      {
        rc = cwLogError(kOpenFailRC,"Unable to locate the file: '%s'.", fn );
        goto errLabel;
      }

      if((rc = file::read(fH,version)) != kOkRC )
      {
        rc = cwLogError(kReadFailRC,"Version read failed on Audio-MIDI file: '%s'.", fn );
        goto errLabel;
      }
      
      if((rc = file::read(fH,n)) != kOkRC )
      {
        rc = cwLogError(kReadFailRC,"Header read failed on Audio-MIDI file: '%s'.", fn );
        goto errLabel;
      }

      if( n > p->msgArrayN )
      {
        cwLogWarning("The count of message in Audio-MIDI file '%s' reduced from %i to %i.", fn, n, p->msgArrayN );
        n = p->msgArrayN;
      }

      if((rc = file::read(fH,p->msgArray,n*sizeof(am_midi_msg_t))) != kOkRC )
      {
        rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn );
        goto errLabel;
      }

      p->msgArrayInIdx = n;

      cwLogInfo("Read %i from '%s'.",n,fn);

    errLabel:

      file::close(fH);
        
      return rc;        
    }

    // Write the record buffer to an AM file
    rc_t _midi_write( midi_record_play_t* p, const char* fn )
    {
      rc_t           rc = kOkRC;
      file::handle_t fH;
      // NOTE: version must be a small negative number to differentiate from file version that
      // whose first word is the count of records in the file, rather than the version number
      int version = -1; 
      
      if( p->iMsgArrayInIdx == 0 )
      {
        cwLogWarning("Nothing to write.");
        return rc;
      }

      // open the file
      if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC )
      {
        rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(fn));
        goto errLabel;
      }

      // write the file version
      if((rc = write(fH,version)) != kOkRC )
      {
        rc = cwLogError(kWriteFailRC,"Version write to '%s' failed.",cwStringNullGuard(fn));
        goto errLabel;          
      }
      
      // write the file header
      if((rc = write(fH,p->iMsgArrayInIdx)) != kOkRC )
      {
        rc = cwLogError(kWriteFailRC,"Header write to '%s' failed.",cwStringNullGuard(fn));
        goto errLabel;          
      }

      // write the file data
      if((rc = write(fH,p->iMsgArray,sizeof(am_midi_msg_t)*p->iMsgArrayInIdx)) != kOkRC )
      {
        rc = cwLogError(kWriteFailRC,"Data write to '%s' failed.",cwStringNullGuard(fn));
        goto errLabel;                    
      }

    errLabel:
      file::close(fH);

      cwLogInfo("Saved %i events to '%s'.", p->iMsgArrayInIdx, fn );
        
      return rc;
    }

    rc_t _write_msgs_to_csv( const char* fn, const am_midi_msg_t* msgArray, unsigned msgArrayCnt )
    {
      rc_t rc = kOkRC;
      file::handle_t fH;

      // open the file
      if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC )
      {
        rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(fn));
        goto errLabel;
      }

      file::printf(fH,"dev,port,microsec,id,sec,ch,status,sci_pitch,d0,d1\n");
      
      for(unsigned i=0; i<msgArrayCnt; ++i)
      {
        const am_midi_msg_t* m = msgArray + i;

        double secs = time::elapsedSecs( msgArray[0].timestamp, msgArray[i].timestamp );
        
        char sciPitch[ midi::kMidiSciPitchCharCnt + 1 ];
        if( m->status == midi::kNoteOnMdId )
          midi::midiToSciPitch( m->d0, sciPitch, midi::kMidiSciPitchCharCnt );
        else
          strcpy(sciPitch,"");

        if((rc = file::printf(fH, "%3i,%3i,%8i,%3i,%8.4f,%2i,0x%2x,%5s,%3i,%3i\n",
                              m->devIdx, m->portIdx, m->microsec, m->id, secs,
                              m->ch, m->status, sciPitch, m->d0, m->d1 )) != kOkRC )
        {
          rc  = cwLogError(rc,"Write failed on line:%i", i+1 );
          goto errLabel;
        }
      }

    errLabel:
      file::close(fH);

      return rc;
      
    }

    rc_t _write_csv( midi_record_play_t* p, const char* fn )
    {
      rc_t           rc = kOkRC;

      if( p->iMsgArrayInIdx == 0 )
      {
        cwLogWarning("Nothing to write.");
        return rc;
      }

      if((rc = _write_msgs_to_csv( fn, p->iMsgArray, p->iMsgArrayInIdx )) != kOkRC )
        goto errLabel;

      cwLogInfo("Saved %i events to '%s'.", p->iMsgArrayInIdx, fn );
      
    errLabel:
      if(rc != kOkRC )
        rc = cwLogError(rc,"Write to '%s' failed.", fn );
      
      return rc;
    }

    rc_t _write_sync_csv( midi_record_play_t* p, const char* fn )
    {
      rc_t           rc = kOkRC;
      file::handle_t fH;

      if( p->iMsgArrayInIdx == 0 )
      {
        cwLogWarning("Nothing to write.");
        return rc;
      }

      // open the file
      if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC )
      {
        rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(fn));
        goto errLabel;
      }

      // write the header line
      file::printf(fH,"meas,index,voice,loc,tick,sec,dur,rval,dots,sci_pitch,dmark,dlevel,status,d0,d1,bar,section,bpm,grace,pedal\n");
      
      for(unsigned i=0; i<p->iMsgArrayInIdx; ++i)
      {
        const am_midi_msg_t* m = p->iMsgArray + i;

        double secs = time::elapsedSecs( p->iMsgArray[0].timestamp, p->iMsgArray[i].timestamp );
        
        // write the event line
        if( midi::isNoteOn(m->status,m->d1) )
        {
          char sciPitch[ midi::kMidiSciPitchCharCnt + 1 ];
          unsigned bar = 0;
          midi::midiToSciPitch( m->d0, sciPitch, midi::kMidiSciPitchCharCnt );
          rc = file::printf(fH, "%i,%i,%i,%i,0,%f,0.0,0.0,0,%s,,,%i,%i,%i,,,,,\n",
                            bar,i,1,m->loc,secs,sciPitch,m->status,m->d0,m->d1);
        }        
        else
        {
          rc = file::printf(fH, ",%i,,,%i,%f,,,,,,,%i,%i,%i,,,,,\n",i,0,secs,m->status,m->d0,m->d1);
        }
        
        if( rc != kOkRC )
        {
          rc  = cwLogError(rc,"Write failed on line:%i", i+1 );
          goto errLabel;
        }
      }

    errLabel:
      file::close(fH);

      cwLogInfo("Saved %i events to '%s'.", p->iMsgArrayInIdx, fn );

      return rc;
    }
    
    rc_t _write_midi_meta_file(const char* meta_fn, const char* player_name, const char* take_label, unsigned sess_numb, unsigned take_numb, unsigned beg_loc, unsigned end_loc)
    {
      rc_t rc = kOkRC;
      const unsigned bufCharN = 256;
      char buf[ bufCharN+1 ];
      int bN;
      
      if((bN = snprintf(buf,bufCharN,"{ player_name: \"%s\", take_label: \"%s\", session_number: %i, take_number: %i, beg_loc: %i, end_loc: %i, skip_score_follow_fl: false }",player_name,take_label,sess_numb,take_numb,beg_loc,end_loc)) == 0)
      {
        rc = cwLogError(kOpFailRC,"The meta data buffer formation failed.");
        goto errLabel;
      }
      
      if((rc = file::fnWrite(meta_fn,buf,bN)) != kOkRC )
        goto errLabel;

    errLabel:
      if( rc != kOkRC )
        rc = cwLogError(rc,"MIDI meta file write failed on '%s'.",cwStringNullGuard(meta_fn));
      
      return rc;
    }

    rc_t _midi_file_write( const char* fn, const am_midi_msg_t* msgArray, unsigned msgArrayCnt )
    {
      rc_t                 rc                 = kOkRC;
      const unsigned       midiFileTrackCnt   = 1;
      const unsigned       midiFileTicksPerQN = 192;
      const unsigned       midiFileTempoBpm   = 120;
      const unsigned       midiFileTrkIdx     = 0;
      file::handle_t       fH;
      midi::file::handle_t mfH;
      time::spec_t         t0;      


      if( msgArrayCnt == 0 )
      {
        cwLogWarning("Nothing to write.");
        return rc;
      }

      if((rc = midi::file::create( mfH, midiFileTrackCnt,  midiFileTicksPerQN )) != kOkRC )
      {
        rc = cwLogError(rc,"MIDI file create failed. File:'%s'", cwStringNullGuard(fn));
        goto errLabel;                  
      }

      if((rc = midi::file::insertTrackTempoMsg( mfH, midiFileTrkIdx, 0, midiFileTempoBpm )) != kOkRC )
      {
        rc = cwLogError(rc,"MIDI file tempo message insert failed. File:'%s'", cwStringNullGuard(fn));
        goto errLabel;                  
      }

      t0 = msgArray[0].timestamp;
      
      for(unsigned i=0; i<msgArrayCnt; ++i)
      {

        double secs = time::elapsedMicros( t0, msgArray[i].timestamp ) / 1000000.0;
        unsigned atick = secs * midiFileTicksPerQN * midiFileTempoBpm / 60.0;
        
        if((rc = insertTrackChMsg( mfH, midiFileTrkIdx, atick, msgArray[i].ch + msgArray[i].status, msgArray[i].d0, msgArray[i].d1 )) != kOkRC )
        {
          rc = cwLogError(rc,"MIDI file message insert failed. File: '%s'.",cwStringNullGuard(fn));
          goto errLabel;                  
          
        }

      }
      
      if((rc = midi::file::write( mfH, fn )) != kOkRC )
      {
        rc = cwLogError(rc,"MIDI file write failed on '%s'.",cwStringNullGuard(fn));
        goto errLabel;                  
      }

      if((rc = midi::file::close( mfH )) != kOkRC )
      {
        rc = cwLogError(rc,"MIDI file close failed on '%s'.",cwStringNullGuard(fn));
        goto errLabel;                  
      }

    errLabel:
      return rc;
    }

    
    void _print_midi_msg( const am_midi_msg_t* mm )
    {
      printf("%i %i : %10i : %2i 0x%02x 0x%02x 0x%02x %i\n", mm->devIdx, mm->portIdx, mm->microsec, mm->ch, mm->status, mm->d0, mm->d1, mm->chordNoteCnt );
    }
      
    void _report_midi( midi_record_play_t* p )
    {

      for(unsigned i=0; i<p->midiDevN; ++i)
      {
        printf("vel table: %s:%s\n", cwStringNullGuard(p->midiDevA[i].midiOutDevLabel),cwStringNullGuard(p->midiDevA[i].midiOutPortLabel));
        for(unsigned j=0; j<p->midiDevA[i].velTableN; ++j)
          printf("%i ",(unsigned)p->midiDevA[i].velTableArray[j]);

        printf("\n");
      }
      
      printf("omsg cnt:%i\n",p->msgArrayInIdx);
      for(unsigned i=0; i<p->msgArrayInIdx; ++i)
      {
        am_midi_msg_t* mm = p->msgArray + i;
        _print_midi_msg(mm);
      }


      printf("imsg cnt:%i\n",p->iMsgArrayInIdx);
      for(unsigned i=0; i<p->iMsgArrayInIdx; ++i)
      {
        am_midi_msg_t* mm = p->iMsgArray + i;
        _print_midi_msg(mm);
      }
      
    }

    rc_t _write_vel_histogram( midi_record_play_t* p )
    {
      const char* fname = "/home/kevin/temp/vel_histogram.txt";
      rc_t rc = kOkRC;
      
      if( !p->velHistogramEnableFl )
        return rc;
            
      file::handle_t h;
      if((rc = file::open(h,fname,file::kWriteFl)) != kOkRC )
        return rc;

      for(unsigned i=0; i<p->midiDevN; ++i)
        if(p->midiDevA[i].enableFl )
        {
          for(unsigned j=0; j<midi::kMidiVelCnt; ++j)
            if((rc = file::printf(h,"%i,",p->midiDevA[i].velHistogram[j])) != kOkRC )
            {
              rc = cwLogError(rc,"Histogram output file (%s) write failed.",fname);
              goto errLabel;
            }
          
          file::printf(h,"\n");
        }

    errLabel:
      file::close(h);
      return rc;
    }

    // Fill the play buffer (msgArray) from the record buffer (iMsgArray)
    void _iMsgArray_to_msgArray(midi_record_play_t* p)
    {
      if( p->msgArrayN < p->iMsgArrayN)
      {
        mem::resize(p->msgArray,p->iMsgArrayN,mem::kZeroAllFl);
        p->msgArrayN = p->iMsgArrayN;
      }
      p->msgArrayOutIdx = 0;
      p->msgArrayInIdx = p->iMsgArrayInIdx;
      memcpy(p->msgArray,p->iMsgArray,p->iMsgArrayInIdx*sizeof(am_midi_msg_t));
    }
    
    
    rc_t _stop( midi_record_play_t* p )
    {
      rc_t rc = kOkRC;

      if( p->startedFl )
      {
        p->startedFl = false;

        time::spec_t t1;
        time::get(t1);

        _write_vel_histogram( p );
      
        // if we were recording
        if( p->recordFl )
        {

          // set the 'microsec' value for each MIDI msg as an offset from the first message[]
          for(unsigned i=0; i<p->iMsgArrayInIdx; ++i)
          {
            p->iMsgArray[i].microsec = time::elapsedMicros(p->iMsgArray[0].timestamp,p->iMsgArray[i].timestamp);
          }

          // copy the recorded messages from the input buffer to the output buffer
          _iMsgArray_to_msgArray(p);

          cwLogInfo("MIDI messages recorded: %i",p->msgArrayInIdx );
          
        }
        else
        {          
          io::timerStop( p->ioH, io::timerIdToIndex(p->ioH, kMidiRecordPlayTimerId) );



          // TODO:
          // BUG BUG BUG: should work for all channels

          // all notes off
          _transmit_ctl(   p, 0, 121, 0, 0 ); // reset all controllers
          _transmit_ctl(   p, 0, 123, 0, 0 ); // all notes off
          _transmit_ctl(   p, 0,   0, 0, 0 ); // switch to bank 0
        
        }

        _midi_state_clear(p);
      
        if( p->cb != nullptr )
          p->cb( p->cb_arg, kPlayerStoppedActionId, kInvalidId, t1, kInvalidId, nullptr, 0, 0, 0, 0 );
      }
      
      return rc;
    }
    
    rc_t _midi_receive( midi_record_play_t* p, const io::midi_msg_t& m )
    {
      rc_t                  rc  = kOkRC;
      const midi::packet_t* pkt = m.pkt;

      // for each midi msg
      for(unsigned j=0; j<pkt->msgCnt; ++j)
      {
        // if this is a sys-ex msg
        if( pkt->msgArray == NULL )
        {
          cwLogError(kNotImplementedRC,"Sys-ex recording not implemented.");
        }
        else // this is a triple
        {

          //if( !midi::isPedal(pkt->msgArray[j].status,pkt->msgArray[j].d0) )
          //  printf("IN: st:%i rec:%i : 0x%x 0x%x 0x%x\n", p->startedFl, p->recordFl, pkt->msgArray[j].status, pkt->msgArray[j].d0, pkt->msgArray[j].d1 );
          
          if( (p->recordFl || p->logInFl) && p->startedFl )
          {

            // verify that space exists in the record buffer
            if( p->iMsgArrayInIdx >= p->iMsgArrayN )
            {
              _stop(p);
              rc = cwLogError(kBufTooSmallRC,"MIDI message record buffer is full. % messages.",p->iMsgArrayN);
              goto errLabel;
            }
            else
            {
              // copy the msg into the record buffer
              midi::msg_t*   mm = pkt->msgArray + j;

              if( midi::isChStatus(mm->status) )
              {
                //printf("R:%i %i %i\n",mm->status, mm->d0, mm->d1);
        
                const am_midi_msg_t* am = _midi_store( p, pkt->devIdx, pkt->portIdx, mm->timeStamp, mm->ch, mm->status, mm->d0, mm->d1 );

                if( p->thruFl && am != nullptr )
                  _transmit_msg( p, am, false );

              }
            }
          }
        }

      }

    errLabel:
      return rc;
    }


    rc_t _timer_callback(midi_record_play_t* p, io::timer_msg_t& m)
    {
      rc_t rc = kOkRC;

      // if the MIDI player is started and in 'play' mode and msg remain to be played
      if( p->startedFl && (p->recordFl==false) && (p->msgArrayOutIdx < p->msgArrayInIdx))
      {
        time::spec_t t;
        time::get(t);

        unsigned cur_time_us = time::elapsedMicros(p->play_time,t);

        if( p->halfPedalFl )
          _half_pedal_update( p, cur_time_us );
        else  
          while( p->msgArray[ p->msgArrayOutIdx ].microsec <= cur_time_us )
          {
            
            am_midi_msg_t* mm = p->msgArray + p->msgArrayOutIdx;

            //_print_midi_msg(mm);

            _transmit_msg( p, mm );
            
            _set_midi_msg_next_play_index(p, p->msgArrayOutIdx+1 );

            // if all MIDI messages have been played
            if( p->msgArrayOutIdx >= p->msgArrayInIdx )
            {
              _stop(p);
              break;
            }
          }

        if( p->msgArrayOutIdx < p->msgArrayInIdx )
        {
          // the next callback should happen at now() + (next_msg_us - cur_us)
          time::advanceMicros(t, p->msgArray[ p->msgArrayOutIdx ].microsec - cur_time_us );
          io::timerSetNextTime( p->ioH, p->midi_timer_index,t);
        }
      }
        
      return rc;
    }

    rc_t _get_take_number( const char* dir, unsigned& take_numb_ref )
    {
      rc_t rc = kOkRC;
      
      take_numb_ref = -1;

      const char* suffix;
      if((suffix = lastMatchChar(dir,'_')) == nullptr )
      {
        rc = kSyntaxErrorRC;
        goto errLabel;
      }
      
      if(sscanf(suffix+1,"%i",&take_numb_ref) != 1)
      {
        rc = kSyntaxErrorRC;
        goto errLabel;
      }

    errLabel:
      if( rc != kOkRC )
        rc = cwLogError(rc,"The suffix of the directory name containing the 'am' file is not an underscore separated integer. (%s).",cwStringNullGuard(dir));

      return rc;
    }

    
  }
}
    
    
cw::rc_t cw::midi_record_play::create( handle_t& hRef,
                                       io::handle_t ioH,
                                       const object_t& cfg,
                                       const char* velTableFname,
                                       event_callback_t cb,
                                       void* cb_arg )
{
  bool asyncFl = true;
  midi_record_play_t* p = nullptr;
  rc_t rc;
  object_t* velTblCfg = nullptr;
  
  if((rc = destroy(hRef)) != kOkRC )
    return rc;

  p = mem::allocZ<midi_record_play_t>();

  p->ioH = ioH; // p->ioH is used in _destory() so initialize it here

  // parse the cfg 
  if((rc = _parseCfg(p,cfg)) != kOkRC )
    goto errLabel;

  
  p->midi_timer_index = kInvalidIdx;
  p->cb  = cb;
  p->cb_arg = cb_arg;
  p->halfPedalState        = kHalfPedalDone;
  p->halfPedalNextUs       = 0;
  p->halfPedalNoteDelayUs  = 100 * 1000;
  p->halfPedalNoteDurUs    = 1000 * 1000;
  p->halfPedalUpDelayUs    = 1000 * 1000;
  p->halfPedalDownDelayUs  = 1000 * 1000;
  p->halfPedalMidiPitch    = 64;
  p->halfPedalMidiNoteVel  = 64;
  p->halfPedalMidiPedalVel = 127;
  p->velHistogramEnableFl  = true;

  for( unsigned i=0; i<p->midiDevN; ++i)    
  {
    midi_device_t* dev = p->midiDevA + i;

    if( !p->midiDevA[i].enableFl )
      continue;
    
    if((dev->midiOutDevIdx = io::midiDeviceIndex(p->ioH,dev->midiOutDevLabel)) == kInvalidIdx )
    {
      rc = cwLogError(kInvalidArgRC,"The MIDI output device: '%s' was not found.", cwStringNullGuard(dev->midiOutDevLabel) );
      goto errLabel;
    }
    
    if((dev->midiOutPortIdx = io::midiDevicePortIndex(p->ioH,dev->midiOutDevIdx,false,dev->midiOutPortLabel)) == kInvalidIdx )
    {
      rc = cwLogError(kInvalidArgRC,"The MIDI output port: '%s' was not found.", cwStringNullGuard(dev->midiOutPortLabel) );
      goto errLabel;
    }

    printf("%s %s : %i %i\n",dev->midiOutDevLabel, dev->midiOutPortLabel, dev->midiOutDevIdx, dev->midiOutPortIdx );
  }

  // create the MIDI playback timer
  if((rc = timerCreate( p->ioH, TIMER_LABEL, kMidiRecordPlayTimerId, p->midi_timer_period_micro_sec, asyncFl)) != kOkRC )
  {
    cwLogError(rc,"Audio-MIDI timer create failed.");
    goto errLabel;
  }

  if((p->midi_timer_index = io::timerLabelToIndex( p->ioH, TIMER_LABEL )) == kInvalidIdx )
  {
    cwLogError(rc,"Audio-MIDI timer index access failed.");
    goto errLabel;
  }

  _midi_state_clear(p);
  
 errLabel:
  if( rc == kOkRC )
    hRef.set(p);
  else
    _destroy(p);

  if(velTblCfg != nullptr )
    velTblCfg->free();
  
  return rc;
}

cw::rc_t cw::midi_record_play::destroy( handle_t& hRef )
{
  rc_t rc = kOkRC;
  
  if( !hRef.isValid() )
    return kOkRC;

  midi_record_play_t* p = _handleToPtr(hRef);

  if((rc = _destroy(p)) != kOkRC )
    return rc;

  hRef.clear();
  
  return rc;
}

cw::rc_t cw::midi_record_play::start( handle_t h, bool rewindFl, const time::spec_t* end_play_event_timestamp )
{
  midi_record_play_t* p = _handleToPtr(h);
  p->startedFl = true;

  if( p->velHistogramEnableFl )
    for(unsigned i=0; i<p->midiDevN; ++i)
      if(p->midiDevA[i].enableFl )
        memset(p->midiDevA[i].velHistogram,0,sizeof(p->midiDevA[i].velHistogram));

  
  // set the end play time
  if( end_play_event_timestamp == nullptr or time::isZero(*end_play_event_timestamp) )
    time::setZero(p->end_play_event_timestamp);
  else
  {
    p->end_play_event_timestamp = *end_play_event_timestamp;
    //p->all_off_timestamp = *end_play_event_timestamp;
    //time::advanceMs( p->all_off_timestamp, p->all_off_delay_ms);  }
  }
  
  time::get(p->start_time);
        
  if( p->recordFl || p->logInFl or p->logOutFl )
  {
    _set_midi_msg_next_index(p, 0 );
  }

  if( !p->recordFl )
  {
    time::get(p->play_time);
    
    if( rewindFl )
      _set_midi_msg_next_play_index(p,0);
    else
    {

      io::timerSetNextTime( p->ioH, p->midi_timer_index,p->play_time);
      
      // Set the begin play time back by the time offset of the current output event.
      // This will cause that event to be played back immediately.
      time::subtractMicros(p->play_time, p->msgArray[ p->msgArrayOutIdx ].microsec );

    }

    if( p->halfPedalFl )
    {
      p->halfPedalNextUs = 0;
      p->halfPedalState  = kWaitForBegin;
    }

    io::timerStart( p->ioH, io::timerIdToIndex(p->ioH, kMidiRecordPlayTimerId) );
  }

  
  return kOkRC;
}

cw::rc_t cw::midi_record_play::stop( handle_t h )
{
  midi_record_play_t* p = _handleToPtr(h);
  return _stop(p);
}

bool cw::midi_record_play::is_started( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);
  return p->startedFl;
}


cw::rc_t cw::midi_record_play::clear( handle_t h )
{
  rc_t                rc = kOkRC;
  midi_record_play_t* p  = _handleToPtr(h);
  _set_midi_msg_next_index(p,0);
  return rc;
}

cw::rc_t cw::midi_record_play::set_record_state( handle_t h, bool record_fl )
{
  rc_t                rc = kOkRC;
  midi_record_play_t* p  = _handleToPtr(h);
  p->recordFl = record_fl;
  return rc;  
}

bool cw::midi_record_play::record_state( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);
  return p->recordFl;
}

cw::rc_t cw::midi_record_play::set_mute_state( handle_t h, bool mute_fl )
{
  rc_t                rc = kOkRC;
  midi_record_play_t* p  = _handleToPtr(h);
  p->muteFl = mute_fl;
  return rc;  
}

bool cw::midi_record_play::mute_state( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);
  return p->muteFl;
}


cw::rc_t cw::midi_record_play::set_thru_state( handle_t h, bool thru_fl )
{
  rc_t                rc = kOkRC;
  midi_record_play_t* p  = _handleToPtr(h);
  p->thruFl = thru_fl;
  return rc;  
}

bool cw::midi_record_play::thru_state( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);
  return p->thruFl;
}

cw::rc_t cw::midi_record_play::save( handle_t h, const char* fn )
{
  midi_record_play_t* p  = _handleToPtr(h);  
  return _midi_write(p,fn);
}

cw::rc_t cw::midi_record_play::save_csv( handle_t h, const char* fn )
{
  midi_record_play_t* p  = _handleToPtr(h);  
  return _write_csv(p,fn);
}

cw::rc_t cw::midi_record_play::save_synced_csv( handle_t h, const char* fn, const score_follower::ssf_note_on_t* syncA, unsigned syncN )
{
  rc_t rc = kOkRC;
  midi_record_play_t* p  = _handleToPtr(h);  

  for(unsigned i=0; i<p->iMsgArrayInIdx; ++i)
  {
    const am_midi_msg_t* m = p->iMsgArray + i;

    if( midi::isNoteOn(m->status,m->d1) )
    {
      for(unsigned j=0; j<syncN; ++j)
        if( syncA[j].muid == p->iMsgArray[i].id )
        {
          if( p->iMsgArray[i].d0 != syncA[j].pitch )
          {
            rc = cwLogError(kInvalidStateRC,"MIDI sync failed. Pitch mismatch. i:%i j:%i : %i %i",i,j, p->iMsgArray[i].d0 != syncA[j].pitch);
            //goto errLabel;
            continue;
          }

          p->iMsgArray[i].loc = syncA[j].loc;
        }
    }
    
  }

  if((rc = _write_sync_csv( p, fn )) != kOkRC )
  {
    rc = cwLogError(rc,"MIDI sync'd CSV write failed.");
    goto errLabel;
  }

 errLabel:
  return rc;
}


cw::rc_t cw::midi_record_play::write_svg( handle_t h, const char* fn )
{
  rc_t rc;
  svg_midi::handle_t svgH;

  midi_record_play_t* p = _handleToPtr(h);
  
  if((rc = svg_midi::create( svgH )) != kOkRC )
  {
    rc = cwLogError(rc,"svg_midi object create failed.");
    goto errLabel;
  }

  for(unsigned i=0; i<p->iMsgArrayInIdx; ++i)
  {
    const am_midi_msg_t* m = p->iMsgArray + i;

    double sec = time::specToSeconds( m->timestamp );

    if((rc = svg_midi::setMidiMsg(svgH,sec,m->id,m->ch,m->status,m->d0,m->d1)) != kOkRC )
    {
      rc = cwLogError(rc,"svg_midi set midi msg failed.");
      goto errLabel;
    }
  }

 errLabel:
  svg_midi::destroy(svgH);
  
  return rc;
}


cw::rc_t cw::midi_record_play::open( handle_t h, const char* fn )
{
  midi_record_play_t* p  = _handleToPtr(h);  
  return _midi_read(p,fn);
}

cw::rc_t cw::midi_record_play::load( handle_t h, const midi_msg_t* msg, unsigned msg_count )
{
  rc_t                rc = kOkRC;
  midi_record_play_t* p  = _handleToPtr(h);

  unsigned extraMs = 0;
  unsigned curMicros = 0;
  
  
  if( msg_count > p->msgArrayN )
  {  
    mem::release(p->msgArray);
    p->msgArray = mem::allocZ<am_midi_msg_t>( msg_count );
    p->msgArrayN = msg_count;
  }

  for(unsigned i=0; i<msg_count; ++i)
  {

    if( i> 0)
    {
      unsigned deltaMicros = time::elapsedMicros(msg[i-1].timestamp,msg[i].timestamp);
      curMicros += deltaMicros + (extraMs*1000);
    }
    
    p->msgArray[i].id        = msg[i].id;
    p->msgArray[i].timestamp = msg[i].timestamp;
    p->msgArray[i].loc       = msg[i].loc;
    p->msgArray[i].arg       = msg[i].arg;
    p->msgArray[i].ch        = msg[i].ch;
    p->msgArray[i].status    = msg[i].status;
    p->msgArray[i].d0        = msg[i].d0;
    p->msgArray[i].d1        = msg[i].d1;
    p->msgArray[i].devIdx    = kInvalidIdx;
    p->msgArray[i].portIdx   = kInvalidIdx;
    //p->msgArray[i].microsec  = time::elapsedMicros(p->msgArray[0].timestamp,p->msgArray[i].timestamp);
    p->msgArray[i].microsec  = curMicros;

    //hprintf("%i %i\n",curMicros,time::elapsedMicros(p->msgArray[0].timestamp,p->msgArray[i].timestamp) );

    extraMs = 0;
    if( midi::isSustainPedalUp(msg[i].status,msg[i].d0,msg[i].d1))
      extraMs = p->minDamperDownMs;
    
  }

  p->msgArrayInIdx =  msg_count;
  p->msgArrayOutIdx = 0;

  _set_chord_note_count(p);
  
  return rc;
}

unsigned cw::midi_record_play::label_to_device_index( handle_t h, const char* devLabel )
{
  midi_record_play_t* p = _handleToPtr(h);  
  return _label_to_device_index( p, devLabel );
}

const char* cw::midi_record_play::device_index_to_label( handle_t h, unsigned devIdx )
{
  midi_record_play_t* p = _handleToPtr(h);
  return  devIdx < p->midiDevN ? p->midiDevA[devIdx].label : nullptr;
}

cw::rc_t cw::midi_record_play::seek( handle_t h, time::spec_t seek_timestamp )
{
  rc_t rc           = kOkRC;
  midi_record_play_t* p = _handleToPtr(h);

  // clear the MIDI state
  _midi_state_clear(p); 

  // supress MIDI transmission during the seek 
  p->supressMidiXmitFl = true;

  // iterate throught the MIDI msg array
  for(unsigned i=0; i<p->msgArrayInIdx; ++i)
  {
    am_midi_msg_t* mm = p->msgArray + i;

    // if this msg is at or after the seek target
    if( time::isLTE(seek_timestamp,mm->timestamp) )
    {
      p->msgArrayOutIdx = i;
      break;
    }

    // transmit the msg so that the dev._midi_state is updated,
    // but 'supressMidiXmitFl' is set so no MIDI will actually
    // be transmitted
    _transmit_msg( p, mm, false );
  }

  p->supressMidiXmitFl = false;

  _midi_state_xmit_pedals(p);

  
  return rc;
}

unsigned cw::midi_record_play::elapsed_micros( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);
  time::spec_t t;
  time::get(t);

  return time::elapsedMicros( p->start_time, t );
}

unsigned cw::midi_record_play::event_count( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);    
  return p->msgArrayInIdx;
}

unsigned cw::midi_record_play::event_index( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);    
  return p->recordFl ? p->iMsgArrayInIdx : p->msgArrayOutIdx;
}

unsigned cw::midi_record_play::last_store_index( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);    
  return p->recordFl ? p->lastStoreIndex : kInvalidIdx;
}

unsigned cw::midi_record_play::event_loc( handle_t h )
{
  midi_record_play_t* p  = _handleToPtr(h);
  
  if( !p->recordFl && p->msgArrayOutIdx <  p->msgArrayN )
    return p->msgArray[ p->msgArrayOutIdx ].loc;
  
  return kInvalidId;
}


cw::rc_t  cw::midi_record_play::exec( handle_t h, const io::msg_t& m )
{
  rc_t rc = kOkRC;
  midi_record_play_t* p = _handleToPtr(h);
  
  switch( m.tid )
  {
    case io::kTimerTId:
      if( m.u.timer != nullptr )
        rc = _timer_callback(p,*m.u.timer);
      break;
    
    case io::kMidiTId:
      if( m.u.midi != nullptr )
        _midi_receive(p,*m.u.midi);
      break;

    default:
      rc = kOkRC;
  
  }

  return rc;
}

unsigned cw::midi_record_play::device_count( handle_t h )
{
  midi_record_play_t* p = _handleToPtr(h);
  return p->midiDevN;
}

bool cw::midi_record_play::is_device_enabled( handle_t h, unsigned devIdx )
{
  midi_record_play_t* p = _handleToPtr(h);
  bool fl = false;
  
  if( devIdx >= p->midiDevN )
    cwLogError(kInvalidArgRC,"The MIDI record-play device index '%i' is invalid.",devIdx );
  else
    fl = p->midiDevA[devIdx].enableFl;
  
  return fl;
}

void cw::midi_record_play::enable_device( handle_t h, unsigned devIdx, bool enableFl )
{
  midi_record_play_t* p = _handleToPtr(h);
  
  if( devIdx >= p->midiDevN )
    cwLogError(kInvalidArgRC,"The MIDI record-play device index '%i' is invalid.",devIdx );
  else
  {
    p->midiDevA[devIdx].enableFl = enableFl;
    //printf("Enable: %i = %i\n",devIdx,enableFl);
  }
}

cw::rc_t cw::midi_record_play::send_midi_msg( handle_t h, unsigned devIdx, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 )
{
  midi_record_play_t* p = _handleToPtr(h);
  
  if( devIdx >= p->midiDevN )
    return cwLogError(kInvalidArgRC,"The MIDI record-play device index '%i' is invalid.",devIdx );
  
  if( p->midiDevA[devIdx].enableFl )
    io::midiDeviceSend( p->ioH, p->midiDevA[devIdx].midiOutDevIdx, p->midiDevA[devIdx].midiOutPortIdx, status + ch, d0, d1 );

  return kOkRC;
}


void cw::midi_record_play::half_pedal_params( handle_t h, unsigned noteDelayMs, unsigned pitch, unsigned vel, unsigned pedal_vel, unsigned noteDurMs, unsigned downDelayMs )
{
  midi_record_play_t* p = _handleToPtr(h);
  p->halfPedalNoteDelayUs = noteDelayMs * 1000;
  p->halfPedalNoteDurUs   = noteDurMs   * 1000;
  p->halfPedalDownDelayUs = downDelayMs   * 1000;
  p->halfPedalMidiPitch   = pitch;
  p->halfPedalMidiNoteVel = vel;
  p->halfPedalMidiPedalVel= pedal_vel;
      
}

cw::rc_t cw::midi_record_play::am_to_midi_file( const char* am_filename, const char* midi_filename, const char* csv_filename )
{  
  rc_t           rc          = kOkRC;
  unsigned       msgArrayCnt = 0;
  am_midi_msg_t* msgArray    = nullptr;

  if(!filesys::isFile(am_filename))
  {
    cwLogError(kOpenFailRC,"The AM file '%s' does not exist.",am_filename);
    goto errLabel;
  }

  if((rc = _am_file_read( am_filename, msgArrayCnt, msgArray )) != kOkRC )
  {
    rc = cwLogError(rc,"Unable to read AM file '%s'.", cwStringNullGuard(am_filename));
    goto errLabel;
  }

  if( midi_filename != nullptr )
    if((rc = _midi_file_write( midi_filename, msgArray, msgArrayCnt )) != kOkRC )
    {
      rc = cwLogError(rc,"Unable to write AM file '%s' to '%s'.", cwStringNullGuard(am_filename),cwStringNullGuard(midi_filename));
      goto errLabel;
    }

  if( csv_filename != nullptr )
    if((rc = _write_msgs_to_csv( csv_filename, msgArray, msgArrayCnt )) != kOkRC )
    {
      rc = cwLogError(rc,"Unable to write AM file '%s' to '%s'.", cwStringNullGuard(am_filename),cwStringNullGuard(csv_filename));
      goto errLabel;
    }

 errLabel:
  mem::release(msgArray);

  return rc;
  
}

cw::rc_t cw::midi_record_play::am_to_midi_dir( const char* player_name, const char* prefix_label, unsigned sess_numb, unsigned beg_loc, unsigned end_loc, const char* inDir )
{  
  rc_t                 rc            = kOkRC;
  filesys::dirEntry_t* dirEntryArray = nullptr;
  unsigned             dirEntryCnt   = 0;
  char*                take_label    = nullptr; 

  if( !filesys::isDir(inDir) )
  {
    rc = cwLogError(kInvalidArgRC,"The directory '%s' does not exist.",cwStringNullGuard(inDir));
    goto errLabel;
  }
  
  if(( dirEntryArray = filesys::dirEntries( inDir, filesys::kDirFsFl | filesys::kFullPathFsFl, &dirEntryCnt )) == nullptr )
    goto errLabel;

  for(unsigned i=0; i<dirEntryCnt and rc==kOkRC; ++i)
  {
    unsigned take_numb;
    char* am_fn      = filesys::makeFn(  dirEntryArray[i].name, "midi", "am", NULL);
    char* midi_fn    = filesys::makeFn(  dirEntryArray[i].name, "midi", "mid", NULL);    
    char* csv_fn     = filesys::makeFn(  dirEntryArray[i].name, "midi", "csv", NULL);
    char* meta_fn    = filesys::makeFn(  dirEntryArray[i].name, "meta", "cfg", NULL);
    
    if((rc = _get_take_number( dirEntryArray[i].name, take_numb )) != kOkRC )
      goto errLabel;

    take_label = mem::printf(take_label,"%s_%i",prefix_label,take_numb);
    
    cwLogInfo("0x%x AM:%s MIDI:%s", dirEntryArray[i].flags, dirEntryArray[i].name, midi_fn);
    
    if((rc = am_to_midi_file( am_fn, midi_fn, csv_fn)) != kOkRC )
      goto errLabel;

    if((rc = _write_midi_meta_file(meta_fn,player_name,take_label,sess_numb,take_numb,beg_loc,end_loc)) != kOkRC )
      goto errLabel;

    mem::release(am_fn);
    mem::release(midi_fn);
    mem::release(csv_fn);
    mem::release(meta_fn);

  }
  
 errLabel:
  mem::release(dirEntryArray);
  mem::release(take_label);

  if( rc != kOkRC )
    rc = cwLogError(rc,"AM to MIDI conversion failed on '%s'.",inDir);
  
  return rc;  
}

cw::rc_t cw::midi_record_play::am_to_midi_file( const object_t* cfg )
{
  rc_t        rc    = kOkRC;
  
  // verify that the cfg is a list
  if( cfg == nullptr || cfg->is_list() == false)
  {
    rc = cwLogError(kInvalidArgRC,"AM to MIDI file: invalid cfg.");
    goto errLabel;
  }

  // for each job specified in the list
  for(unsigned i=0; i<cfg->child_count(); ++i)
  {
    const object_t* job_cfg      = nullptr;
    bool            enable_fl    = false;
    const char*     prefix_label = nullptr;
    const char*     player_name  = nullptr;
    unsigned        sess_numb    = 0;
    unsigned        beg_loc      = kInvalidId;
    unsigned        end_loc      = kInvalidId;
    const char*     dir          = nullptr;

    // verify that each job spec is a dict
    if(( job_cfg = cfg->child_ele(i)) == nullptr || job_cfg->is_dict() == false )
    {
      rc = cwLogError(kSyntaxErrorRC,"The AM to MIDI job spec. at index %i is invalid.",i);
      goto errLabel;
    }

    // read the job spec
    if((rc = job_cfg->getv("enable_fl",    enable_fl,
                           "player_name",  player_name,
                           "prefix_label", prefix_label,
                           "sess_numb",    sess_numb,
                           "beg_loc",      beg_loc,
                           "end_loc",      end_loc,
                           "dir",          dir)) != kOkRC )
    {
      rc = cwLogError(rc,"AM to MIDI file: Unable to parse input arg's at index %i",i);
      goto errLabel;
    }

    if( enable_fl )
    {
      if((rc = am_to_midi_dir(player_name,prefix_label,sess_numb,beg_loc,end_loc,dir)) != kOkRC )
      {
        rc = cwLogError(rc,"AM to MIDI file conversion on directory:'%s' failed.", cwStringNullGuard(dir));
        goto errLabel;
      }
    }    
  }

 errLabel:

  if( rc != kOkRC )
    rc = cwLogError(rc,"AM to MIDI file conversion failed.");
  
  return rc;
  
}

void cw::midi_record_play::report( handle_t h )
{
  midi_record_play_t* p = _handleToPtr(h);
  _report_midi(p);
}


unsigned cw::midi_record_play::dev_count( handle_t h )
{
  midi_record_play_t* p = _handleToPtr(h);
  return p->midiDevN;
}

cw::rc_t cw::midi_record_play::vel_table_disable( handle_t h, unsigned devIdx )
{
  rc_t                rc        = kOkRC;  
  midi_record_play_t* p         = _handleToPtr(h);
  unsigned            begDevIdx = devIdx==kInvalidIdx ? 0           : devIdx;
  unsigned            endDevIdx = devIdx==kInvalidIdx ? p->midiDevN : begDevIdx+1;

  if( devIdx != kInvalidIdx && devIdx > p->midiDevN )
  {
    rc = cwLogError(kInvalidArgRC,"The device index '%i'is invalid.",devIdx);
    goto errLabel;
  }

  for(unsigned i=begDevIdx; i<endDevIdx; ++i)
  {
    mem::release(p->midiDevA[i].velTableArray);
    p->midiDevA[i].velTableN = 0;
  }

  
 errLabel:
  return rc;
}

unsigned cw::midi_record_play::vel_table_count( handle_t h, unsigned devIdx )
{
  midi_record_play_t* p = _handleToPtr(h);
  return p->midiDevA[ devIdx ].velTableN;   
}

const uint8_t*  cw::midi_record_play::vel_table( handle_t h, unsigned devIdx )
{
  midi_record_play_t* p = _handleToPtr(h);
  return p->midiDevA[ devIdx ].velTableArray;   
}

cw::rc_t cw::midi_record_play::vel_table_set( handle_t h, unsigned devIdx, const uint8_t* tbl, unsigned tblN )
{
  midi_record_play_t* p = _handleToPtr(h);

  midi_device_t* dev = p->midiDevA + devIdx;
  
  if( tblN != dev->velTableN )
  {
    dev->velTableArray = mem::resize<uint8_t>(dev->velTableArray,tblN);
    dev->velTableN     = tblN;
  }

  printf("Apply Vel Table to %s : ", cwStringNullGuard(p->midiDevA[devIdx].label));
  
  for(unsigned i=0; i<tblN; ++i)
  {
    dev->velTableArray[i] = tbl[i];
    printf("%i->%i  ",i,tbl[i]);
  }
  printf("\n");
  
  return kOkRC;
}