// see "A5000E_manual.PDF":4.1:SAMPLE DUMP STANDARD
// also see <http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/sds.htm>

// see http://www.midi.org/techspecs/ca19.pdf for extended header docs

use tksampler;
use tkmidi;


// boolean b_debug = true;
boolean b_debug = false;

boolean b_upload = true;


class Utils {

   static SplitPathname(String name, path, file) {
      // Split last used file name into directory/file components
      
      int idx = name.lastIndexOf("/");
      int idxDos = name.lastIndexOf("\\");
      if(idxDos > idx)
      {
         idx = idxDos;
      }

      if(-1 != idx)
      {
         name.substring(0, idx) => path;
         name.substring(idx+1, -1) => file;
      }
      else
      {
         path = null;
         file = name;
      }

      ////trace "xxx SplitPathname: name=\""+name+"\" path=\""+path+"\" file=\""+file+"\".";
   }
}


// String inDevName = "MIDISPORT 4x4 Anniversary In D"; // Yamaha A5000
// String outDevName = "MIDISPORT 4x4 Anniversary Out D"; // Yamaha A5000

String inDevName = "Elektron Analog Rytm";
String outDevName = "Elektron Analog Rytm";

// String inDevName = "In From MIDI Yoke:  1";
// String outDevName = "Out To MIDI Yoke:  2";

// String inDevName = "In From MIDI Yoke:  2";
// String outDevName = "Out To MIDI Yoke:  1";


int sysexChannel = 0;

function Usage() {
   trace "[...] Usage: tks app:sds [-chain <length>] [-varichain] [-autodir] [-dir <dirprefix>] <filelist.txt> [-single <file.wav>] [-name <samplename>] [-pad <chainpadsize>] [-minpad <varichainminpadsize>] [-d]";
   exit(5);
}

if(Arguments.numElements < 1)
{
   Usage();
}

String single_filename = "";
int chain_size = 0;
boolean b_varichain = false;
boolean b_chain = false;

int argIdx = 0;
String filelistFileName = "";

String dir_prefix = "";
boolean b_autodir = false;

String force_sample_name = "";
int extra_padding = 0;
int min_padding = 0;


while(argIdx < Arguments.numElements)
{
   switch(Arguments[argIdx])
   {
      default:
         filelistFileName = Arguments[argIdx];
         break;

      case "-dir":
         dir_prefix = Arguments[argIdx] + "/";
         b_autodir = false;
         trace "[dbg] Manual dir prefix set to \""+dir_prefix+"\".";
         break;

      case "-autodir":
         b_autodir = true;
         trace "[dbg] Enable automatic dir prefix";
         break;
         
      case "-single":
         if( (argIdx+1) < Arguments.numElements)
         {
            single_filename = Arguments[++argIdx];
            trace "[dbg] Upload single file, name=\""+single_filename+"\"";
         }
         else Usage();
         break;

      case "-chain":
         if( (argIdx+1) < Arguments.numElements)
         {
            chain_size = Arguments[++argIdx];

            if(1 <= chain_size <= 120)
            {
               if(120 == ((120 / chain_size) * chain_size))
               {
                  b_chain = true;
                  trace "[dbg] Enable fixed length chain mode, length="+chain_size+" => step="+(120/chain_size);
               }
               else
               {
                  trace "[dbg] Invalid chain size ("+chain_size+"): (120 / "+chain_size+") is not an integer.";
                  Usage();
               }
            }
            else
            {
               trace "[---] invalid chain size \""+chain_size+"\" (the valid range is 1..120)";
               Usage();
            }
         }
         else Usage();
         break;

      case "-varichain":
         b_varichain = true;
         b_chain = true;
         trace "[dbg] Enable varichain mode";
         break;

      case "-noupload":
         trace "[dbg] Disable all uploads";
         b_upload = false;
         break;

      case "-name":
         if( (argIdx+1) < Arguments.numElements)
         {
            force_sample_name = Arguments[++argIdx];
            trace "[dbg] single/chain sample name set to \""+force_sample_name+"\"";
         }
         else Usage();
         break;

      case "-d":
         b_debug = true;
         break;

      case "-pad":
         if( (argIdx+1) < Arguments.numElements)
         {
            extra_padding = Arguments[++argIdx];
            if(0 <= extra_padding <= (1024*1024))
            {
               trace "[dbg] extra chain padding set to \""+extra_padding+"\" sample fragments";
            }
            else
            {
               trace "[---] invalid pad size ("+extra_padding+") (the valid range is 0..1048576).";
               exit(5);
            }
         }
         else Usage();
         break;

      case "-minpad":
         if( (argIdx+1) < Arguments.numElements)
         {
            min_padding = Arguments[++argIdx];
            if(0 <= min_padding <= (1024*1024))
            {
               trace "[dbg] min chain padding set to \""+min_padding+"\" sample fragments";
            }
            else
            {
               trace "[---] invalid min pad size ("+min_padding+") (the valid range is 0..1048576).";
               exit(5);
            }
         }
         else Usage();
         break;
   }

   argIdx++;
}


if(single_filename.isBlank())
{
   if(!filelistFileName.isBlank())
   {
      String filelist;
      if(!filelist.loadLocal(filelistFileName, true/*bRemoveCR*/)) 
      {
         trace "[---] failed to load filelist from \""+filelistFileName+"\".";
         exit(5);
      }
   }
   else
   {
      trace "[---] please set the filelist filename";
      Usage();
   }
}

// trace "xxx filelist=\""+filelist+"\"";
// exit(10);


MIDIIn midiin;
MIDIOut midiout;



// List device names
int devIdx;

if(b_debug)
{
   trace "[dbg] found "+MIDIIn.GetNumDevices()+" input devices:";
   devIdx = 0;
   loop(MIDIIn.GetNumDevices())
   {
      trace "[dbg]  ["+devIdx+"]: \""+MIDIIn.GetDeviceNameByIdx(devIdx)+"\".";
      devIdx++;
   }

   trace "";
}


if(b_debug)
{
   trace "[dbg] found "+MIDIOut.GetNumDevices()+" output devices:";
   devIdx = 0;
   loop(MIDIOut.GetNumDevices())
   {
      trace "[dbg]  ["+devIdx+"]: \""+MIDIOut.GetDeviceNameByIdx(devIdx)+"\".";
      devIdx++;
   }
   // end List Device names
}


class SDS_Test {

   Buffer in_buf;
   Buffer out_buf;

   /* if true, ignore WAIT msg from sampler.
    * this considerably speeds up sample upload to the Elektron Analog Rytm but only works for small samples
    */
   // boolean b_ignore_waits = false;
   boolean b_ignore_waits = true;

    // boolean b_waitack = true;
   boolean b_waitack = false;

   boolean b_allow_resend = false; // does not work with Analog Rytm drum computer
   // boolean b_allow_resend = true; // does not work with Analog Rytm drum computer

   // int packet_sleep_interval = 0; // 32
   // int packet_sleep_ms       = 0; // 30

   // int packet_sleep_interval = 14; // 32
   // int packet_sleep_ms       = 22; // 30

   // works with ~242k sample:
   // int packet_sleep_interval = 7; // 32
   // int packet_sleep_ms       = 12; // 30

   // works with 1.7mb sample (ignore_waits=true, b_waitack=true):
   // int packet_sleep_interval = 6; // 32
   // int packet_sleep_ms       = 13; // 30

   // works with 1.7mb sample (ignore_waits=true, b_waitack=true): (=>19779.6 bytes/sec)
   //  (but not reliably)
   // int packet_sleep_interval = 5; // 32
   // int packet_sleep_ms       = 10; // 30

   // works with 1.7mb sample (ignore_waits=true, b_waitack=true): (=>10595.7 bytes/sec)
   // int packet_sleep_interval = 1; // 32
   // int packet_sleep_ms       = 3; // 30

   int packet_sleep_interval = 23; // 32
   int packet_sleep_ms       = 5; // 30


   IntArray ack_buf;

   protected StSample *sam;
   protected StWaveform *wav;
   protected FloatArray *dat;

   protected short sample_nr;
   protected byte sysex_channel;

   protected int packet_nr;
   protected int num_packets;

   define int STATE_TIMEOUT = -1;
   define int STATE_WAIT   = 0x7C;
   define int STATE_CANCEL = 0x7D;
   define int STATE_NAK    = 0x7E;
   define int STATE_ACK    = 0x7F;


   protected method sendDumpHeader() {
      // Send dump header
      out_buf.offset = 0;

      out_buf.i8 = 0xF0;
      out_buf.i8 = 0x7E;
      out_buf.i8 = sysex_channel;
      out_buf.i8 = 0x01;
      out_buf.i8 = (sample_nr & 127);
      out_buf.i8 = (sample_nr >> 7) & 127;
      out_buf.i8 = 16; // bits per sample (8..28)

      float rateNanoSec = (1000000.0 / (wav.sampleRate / 1000.0f));
      out_buf.i8 = int(rateNanoSec) & 127;
      out_buf.i8 = (int(rateNanoSec) >> 7) & 127;
      out_buf.i8 = (int(rateNanoSec) >> 14) & 127;

      int numWords = dat.numElements;
      out_buf.i8 = numWords & 127;
      out_buf.i8 = (numWords >> 7) & 127;
      out_buf.i8 = (numWords >> 14) & 127;

      int loopStart = numWords;
      out_buf.i8 = loopStart & 127;
      out_buf.i8 = (loopStart >> 7) & 127;
      out_buf.i8 = (loopStart >> 14) & 127;

      int loopEnd = numWords;
      out_buf.i8 = loopEnd & 127;
      out_buf.i8 = (loopEnd >> 7) & 127;
      out_buf.i8 = (loopEnd >> 14) & 127;

      out_buf.i8 = 0x7F; // loop type (0x00=forward, 0x01=pingpong)

      out_buf.i8 = 0xF7;

      // Send dump header to device
      midiout.sendBuffer(out_buf);
   }

   protected method sendDumpHeaderExt() {

      if(b_debug)
      {
         trace "[dbg] sendDumpHeaderExt";
      }

      // Send dump header
      out_buf.offset = 0;

      out_buf.i8 = 0xF0;
      out_buf.i8 = 0x7E;
      out_buf.i8 = sysex_channel;
      out_buf.i8 = 0x05; // sub-id #1
      out_buf.i8 = 0x05; // sub-id #2
      out_buf.i8 = (sample_nr & 127);
      out_buf.i8 = (sample_nr >> 7) & 127;
      out_buf.i8 = 16; // bits per sample (8..28)

      int srHzInt = wav.sampleRate;
      out_buf.i8 = srHzInt & 127;
      out_buf.i8 = (srHzInt >> 7) & 127;
      out_buf.i8 = (srHzInt >> 14) & 127;
      out_buf.i8 = (srHzInt >> 21) & 127;

      int srHzFrac = frac(wav.sampleRate) * 10000000;
      out_buf.i8 = srHzFrac & 127;
      out_buf.i8 = (srHzFrac >> 7) & 127;
      out_buf.i8 = (srHzFrac >> 14) & 127;
      out_buf.i8 = (srHzFrac >> 21) & 127;

      int numWords = dat.numElements;
      out_buf.i8 = numWords & 127;
      out_buf.i8 = (numWords >> 7) & 127;
      out_buf.i8 = (numWords >> 14) & 127;
      out_buf.i8 = (numWords >> 21) & 127;
      out_buf.i8 = (numWords >> 28) & 127;

      int loopStart = numWords;
      out_buf.i8 = loopStart & 127;
      out_buf.i8 = (loopStart >> 7) & 127;
      out_buf.i8 = (loopStart >> 14) & 127;
      out_buf.i8 = (loopStart >> 21) & 127;
      out_buf.i8 = (loopStart >> 28) & 127;

      int loopEnd = numWords;
      out_buf.i8 = loopEnd & 127;
      out_buf.i8 = (loopEnd >> 7) & 127;
      out_buf.i8 = (loopEnd >> 14) & 127;
      out_buf.i8 = (loopEnd >> 21) & 127;
      out_buf.i8 = (loopEnd >> 28) & 127;

      out_buf.i8 = 0; // loop type

      out_buf.i8 = wav.numChannels;

      out_buf.i8 = 0xF7;

      // don't send out buffer immediately
   }

   public method enableElektronARTurbo() {
      out_buf.offset = 0;

      // out_buf.i8 = 0xF0;
      // out_buf.i8 = 0x00;
      // out_buf.i8 = 0x20;
      // out_buf.i8 = 0x3C;
      // out_buf.i8 = 0x04;
      // out_buf.i8 = 0x00;
      // out_buf.i8 = 0x01;
      // out_buf.i8 = 0xF7;

      out_buf.i8 = 0xF0;
      out_buf.i8 = 0x00;
      out_buf.i8 = 0x20;
      out_buf.i8 = 0x3C;
      out_buf.i8 = 0x04;
      out_buf.i8 = 0x00;
      out_buf.i8 = 0x02;
      out_buf.i8 = 0x08;
      out_buf.i8 = 0xF7;

      midiout.sendBuffer(out_buf);
   }

   static public GetStateName(int state) : String {
      switch(state)
      {
         default:
            return "<UNKNOWN>";

         case STATE_TIMEOUT:
            return "<TIMEOUT>";

         case STATE_WAIT:
            return "WAIT";

         case STATE_CANCEL:
            return "CANCEL";

         case STATE_ACK:
            return "ACK";

         case STATE_NAK:
            return "NAK";
      }
   }

   protected method waitAck(Integer _retPacketNr, boolean _bForceWait) : int {

      explain "Wait for ACK from sampler. Returns state 0=ACK, 1=NAK, 2=CANCEL, 3=WAIT.";

      int timeout = milliSeconds() + 2000;
      
      int numTimeouts = 0;

      return = 0;

      int numWaits = 0;

      RecordedMIDIEvent *ev;

      boolean bHaveAck = false;

      while(!bHaveAck)
      {
         ev <= midiin.nextEvent;

         if(null == ev)
         {
            ev <= midiin.waitNextEvent(100);
         }

         if(null != ev)
         {
            if(ev.isLongMessage())
            {
               //trace "xxx rcv'd sysex ev.size="+ev.size;

               if(4 == ev.size)
               {
                  in_buf.offset = 0;
                  ev.copyToStream(in_buf);

                  if(0x7E == in_buf[0])
                  {
                     return = in_buf[2];

                     _retPacketNr = in_buf[3]; // 0=dump header

                     if(b_debug)
                     {
                        trace "[dbg] ok, recv'd state "+GetStateName(in_buf[2])+". packetNr="+_retPacketNr;
                     }

                     if(STATE_NAK == in_buf[2])
                     {
                        trace "[~~~] NAK, resending packet "+packet_nr+"/"+num_packets;
                        sendNextPacket();
                     }
                     else if(STATE_ACK == in_buf[2])
                     {
                        ack_buf[_retPacketNr] = true;
                        bHaveAck = (_retPacketNr == (packet_nr & 127));
                     }
                     else if(STATE_WAIT == in_buf[2])
                     {
                        if(b_ignore_waits && !_bForceWait)
                        {
                           
                        }
                        else
                        {
                           numWaits++;
                           
                           if(numWaits > 1)
                           {
                              if(b_debug)
                              {
                                 trace "[dbg] waiting..";
                              }
                              TKS.sleep(20);
                           }
                           
                           if(numWaits > 500)
                           {
                              trace "[~~~] more than 500 WAIT replies recv'd";
                              break;
                           }
                        }
                     }
                     else
                     {
                        // Cancel
                        trace "[~~~] ***** CANCEL ****";
                        return 2;
                     }
                  }
               }
            }
         }

         if(milliSeconds() >= timeout)
         {
            numTimeouts++;
            trace "[~~~] SDS::waitAck: timeout after 2 sec (#timeouts="+numTimeouts+")";
            if(numTimeouts >= 15)
            {
               return STATE_TIMEOUT;
            }
            timeout = milliSeconds() + 2000;
         }
      }

      if(b_ignore_waits && !_bForceWait)
      do
      {
         ev <= midiin.nextEvent;
         if(null != ev)
         {
            in_buf.offset = 0;
            ev.copyToStream(in_buf);
            
            if(0x7E == in_buf[0])
            {
               if(STATE_ACK == in_buf[2])
               {
                  int packetNr = in_buf[3];
                  ack_buf[packetNr] = true;
                  
                  if(b_debug)
                  {
                     trace "[dbg] <queue> ok, recv'd state "+GetStateName(in_buf[2])+". packetNr="+packetNr;
                  }
               }
               else if(STATE_WAIT == in_buf[2])
               {
                  if(b_debug)
                  {
                     trace "[dbg] <queue> ok, recv'd state "+GetStateName(in_buf[2])+". packetNr="+packetNr;
                  }
                  //TKS.sleep(20);
               }
               else if(STATE_NAK == in_buf[2])
               {
                  trace "[~~~] NAK, resending packet "+packet_nr+"/"+num_packets;
                  sendNextPacket();
               }
               else if(STATE_CANCEL == in_buf[2])
               {
                  return 2;
               }

            }
         }
      } while(null != ev);
   }

   protected method sendNextPacket() {

      // if( b_debug || (0 == packet_nr) || (packet_nr == (num_packets-1)) || (0 == (packet_nr % 10)) )
      if( b_debug )
      {
         trace "[dbg] sending packet #"+packet_nr+" / "+num_packets+" (modpn="+(packet_nr&127)+")";
      }
      else
      {
         stdout ".";
      }

      out_buf.offset = 0;

      out_buf.i8 = 0xF0;
      out_buf.i8 = 0x7E;
      out_buf.i8 = sysex_channel;
      out_buf.i8 = 0x02;
      out_buf.i8 = (packet_nr & 127);

      int chksum = 0x7E ^ sysex_channel ^ 0x02 ^ (packet_nr & 127);

      int smpOff = packet_nr * (120 / 3);

      loop(120/3)
      {
         int smp = int(dat.get(smpOff) * 32767) + 0x8000;

//01234567 01234567

         // (note) left justified (MSB first)
         byte b1 = (smp >> 9) & 127;
         byte b2 = (smp >> 2) & 127;
         byte b3 = (smp & 3);
         
         chksum = chksum ^ b1 ^ b2 ^ b3;

         out_buf.i8 = b1;
         out_buf.i8 = b2;
         out_buf.i8 = b3;

         smpOff++;
      }

      out_buf.i8 = (chksum & 127);

      out_buf.i8 = 0xF7;

      // Send data packet to device
      midiout.sendBuffer(out_buf);
   }

   public method upload(StSample _sam, short _sampleNr, byte _sysexChannel, String _nameOrNull) : boolean {
      in_buf.size = 4096;
      out_buf.size = 4096;

      ack_buf.alloc(128);
      ack_buf.numElements = 128;
      ack_buf.fill(false);

      sam <= _sam;
      wav <= sam.waveform;
      dat <= wav.sampleData;

      sample_nr = _sampleNr;  // (note) +1000 to load into AR project (does this really work ?????)
      sysex_channel = _sysexChannel;

      if(null != _nameOrNull)
      {
         sendDumpHeaderExt();
         sendSampleNameExtPriv(_nameOrNull);

         if(b_debug)
         {
            File f; f.openLocal("dbghdr.dat", IOS_OUT);
            f.writeBuffer(out_buf, 0, out_buf.offset);
            f.close();
         }

         // Send extended dump header to device
         midiout.sendBuffer(out_buf);
      }
      else
      {
         sendDumpHeader();
      }

      Integer packetNrRecv;

      // if(2 == waitAck(packetNrRecv, true))
      // {
      //    return false;
      // }

      // (note) 40 16bit samples fit into one data packet
      num_packets = ((dat.numElements + 39) / (120/3));
      packet_nr = 0;

      loop(num_packets)
      {
         sendNextPacket();
         // TKS.sleep(1);

         if(b_waitack)
         {
            if(2 == waitAck(packetNrRecv, false))
            {
               return false;
            }
         }

         // if(0 == packet_nr)
         // {
         //    TKS.sleep(10000);
         // }

         packet_nr++;

         if(0 == (packet_nr & 127))
         {
            // Wait until all previous packages have been acknowledged
            // int waitCnt = 0;
            // while(-1 != ack_buf.indexOf(0, 0) && (waitCnt < 128))
            // {
            //    TKS.sleep(10);
            //    waitAck(packetNrRecv, false);
            //    waitCnt++;
            // }

            if(-1 != ack_buf.indexOf(0, 0))
            {
               if(b_allow_resend)
               {
                  if(b_debug)
                  {
                     trace "[dbg] wait ack all  buf="+ack_buf.string;
                  }

                  // resend packets
                  packet_nr -= 128;

                  int acki = 0;
                  loop(128)
                  {
                     if(0 == ack_buf[acki])
                     {
                        if(b_debug)
                        {
                           trace "[dbg] resend packet "+packet_nr+"/"+num_packets;
                        }

                        sendNextPacket();
                        if(2 == waitAck(packetNrRecv, true/*forceWait*/))
                        {
                           return false;
                        }
                     }
                     acki++;
                     packet_nr++;
                  }
               }
               else
               {
                  if(b_debug)
                  {
                     trace "[~~~] not all packets ack'd  buf="+ack_buf.string;
                  }
               }
            }

            ack_buf.fill(false);
         }

         if(0 != packet_sleep_interval)
         {
            if(0 == (packet_nr % packet_sleep_interval))
            {
               TKS.sleep(packet_sleep_ms);
            }
         }

      }

      trace "";

      return true;
   }

   protected method sendSampleNameExtPriv(String _name) {
      // F0 7E 01 05 03 00 01 00 0E 52 65 6E 61 6D 65 64 20 53 61 6D 70 6C 65 F7

      if(b_debug)
      {
         trace "[dbg] sendSampleNameExt";
      }

      // see http://www.midi.org/techspecs/ca19.pdf
      //  (extended header)

      int nlen = _name.length;

      if(nlen > 0)
      {
         nlen--; // don't send ASCIIZ

         if(nlen > 127)
            nlen = 127;

         out_buf.i8 = 0xF0;
         out_buf.i8 = 0x7E;
         out_buf.i8 = sysex_channel;
         out_buf.i8 = 5;  // Sample Dump Extensions Command (sub-ID#1)
         out_buf.i8 = 3;  // Sample Name Transmission Sub Command (sub-ID#2)
         out_buf.i8 = (sample_nr & 127);
         out_buf.i8 = (sample_nr >> 7) & 127;
         out_buf.i8 = 0; // Sample Name Language Tag Length (default: 00)
         out_buf.i8 = nlen; // Sample Name Length (up to 127 characters)

         // Sample Name Data bytes (string of length nn):
         int ci = 0;
         loop(nlen)
         {
            out_buf.i8 = _name.getc(ci++);
         }

         out_buf.i8 = 0xF7;
      }
   }

   public method sendSampleNameExt(int _sampleNr, int _sysexCh, String _name) {
      // F0 7E 01 05 03 00 01 00 0E 52 65 6E 61 6D 65 64 20 53 61 6D 70 6C 65 F7

      if(b_debug)
      {
         trace "[dbg] sendSampleNameExt";
      }

      // see http://www.midi.org/techspecs/ca19.pdf
      //  (extended header)

      sysex_channel = _sysexCh;
      sample_nr = _sampleNr;

      if(_name.length > 0)
      {
         out_buf.offset = 0;

         sendSampleNameExtPriv(_name);
         
         // Send data packet to device
         midiout.sendBuffer(out_buf);
      }
   }

}


StSample g_sam;
StWaveform g_wav;
Integer g_rate;
Integer g_numCh;
String g_fileInfo;

g_wav.alloc(1/*numCh*/, 1/*numFrames*/);
g_sam.setWaveform(g_wav);

class ChainEntry {
   StSample *sam;
   StWaveform *wav;
   StWaveform *twav;
   Integer rate;
   Integer numCh;
   String fileInfo;
   String fnPath;
   String fnFile;

   int orig_sz;

   int pad_sz;

   float slice_pct;


   public method init() {
      sam <= new StSample;
      wav <= new StWaveform;
      wav.alloc(1/*numCh*/, 1/*numFrames*/);
      // trace "xxx init wav.sampleData="+#(wav.sampleData);
      sam.setWaveform(wav);

      orig_sz = 1;

      // twav <= new StWaveform;
      // twav.alloc(1/*numCh*/, 1/*numFrames*/);
      // trace "xxx init twav.sampleData="+#(twav.sampleData);
   }

   public method load(String _pathname) : boolean {
      boolean ret = false;

      // trace "xxx wav.sampleData="+#(wav.sampleData);

      if(WavIO.LoadLocal(_pathname, wav.sampleData, rate, numCh, fileInfo, sam))
      {
         if(b_debug)
         {
            trace "[dbg] wav.sampleData.numElements="+(wav.sampleData.numElements);
         }

         Utils.SplitPathname(_pathname, fnPath, fnFile);
         fnFile.replace(".wav", "");
         fnFile.replace(".WAV", "");

         orig_sz = getSampleSize();

         ret = true;
      }
      else
      {
         trace "[---] failed to open sample file \""+_pathname+"\".";
      }

      return ret;
   }

   public method getSampleSizeInBytes() : int {
      return wav.sampleData.numElements * 2;
   }

   public method getSampleSize() : int {
      return wav.sampleData.numElements;
   }

   public method upload() {

      if(b_upload)
      {
         SDS_Test sds <= new SDS_Test;
      
         int tStart = milliSeconds();

         String remoteName = fnFile;

         // sds.upload(sam, 0/*sampleNr*/, sysexChannel, "test2__name");
         if(!single_filename.isBlank() || b_chain)
         {
            if(!force_sample_name.isBlank())
            {
               remoteName = force_sample_name;
            }
         }

         sds.upload(sam, 0/*sampleNr*/, sysexChannel, remoteName);
         // sds.upload(sam, 1127/*sampleNr*/, sysexChannel, remoteName);

         int tDelta = milliSeconds() - tStart;
      
         trace "[...] file transfered in "+(tDelta/1000.0f)+" seconds ("+((2*wav.sampleData.numElements) / (tDelta/1000.0f))+" bytes/sec).";
      }
      else
      {
         trace "[...] skipping upload because of -noupload option";
      }
   }

   public method resizeTo(int _sz) {
      FloatArray dat <= wav.sampleData;
      if(dat.numElements != _sz)
      {
         FloatArray nd;
         nd.realloc(_sz);
         nd.useAll();
         nd.fill(0.0f);
         nd.copyFrom(dat, 0, dat.numElements, 0);
         dat = nd;
      }
   }
}

PointerArray chain; // ChainEntry instances
ChainEntry out_chain;
out_chain.init();

class ChainUtils {

   static GetMaxSampleSize() : int {
      // in fragments
      int ret = 0;
      ChainEntry *ch;
      foreach ch in chain
      {
         int sz = ch.getSampleSize();
         if(sz > ret)
            ret = sz;
      }
      return ret;
   }

   static GetTotalSampleSize() : int {
      int ret = 0;
      ChainEntry *ch;
      foreach ch in chain
      {
         ret += ch.getSampleSize();
      }
      return ret;
   }

   static GetTotalSampleSizeInBytes() : int {
      int ret = 0;
      ChainEntry *ch;
      foreach ch in chain
      {
         ret += ch.getSampleSizeInBytes();
      }
      return ret;
   }

   static GetAvgSlicePad() : float {

      float r = 0;

      ChainEntry *ch;
      foreach ch in chain
      {
         r += ch.pad_sz;
      }

      return r / chain.numElements;
   }

   static ResizeEntriesTo(int _sz) {

      ChainEntry *ch;
      foreach ch in chain
      {
         ch.resizeTo(_sz);
      }
   }

   static AddPadding(int _pad) {

      if(_pad > 0)
      {
         ChainEntry *ch;
         foreach ch in chain
         {
            ch.resizeTo(ch.getSampleSize() + _pad);
            ch.pad_sz = _pad;
         }
      }
   }

   static ArePadSizesGreaterThan(int _minPad) : boolean {
      ChainEntry *ch;
      foreach ch in chain
      {
         if(ch.pad_sz < _minPad)
            return false;
      }
      return true;
   }

   static RestoreOrigSizes() : boolean {
      ChainEntry *ch;
      foreach ch in chain
      {
         ch.resizeTo(ch.orig_sz);
         ch.pad_sz = 0;
      }
   }

   static CalcSlicePct1(float _maxSmpSz) {

      ChainEntry *ch;

      foreach ch in chain
      {
         int chSz = ch.getSampleSize();

         ch.slice_pct = chSz / _maxSmpSz;
      }
   }

   static float cur_sta = 0; // should always be int

   static AlignEntrySizesTo(int _sz) {

      ChainEntry *ch;

      foreach ch in chain
      {
         int chSz = ch.getSampleSize();

         if(chSz != ((chSz / _sz) * _sz))
         {
            chSz = ((chSz / _sz) + 1) * _sz;

            ch.pad_sz = chSz - ch.orig_sz;

            Float ioSta = cur_sta;
            Integer ioOrigSz = ch.orig_sz;
            Integer ioPadSz = ch.pad_sz;
            Integer ioChSz = chSz;

            // trace "xxx STA="+cur_sta+" origSz="+ch.orig_sz+" padSz="+ch.pad_sz+" chSz="+chSz+" stepSz="+_sz;
            trace "[trc] STA="+ioSta.printf("%3.3f")+" origSz="+ioOrigSz.printf("%10u")+" padSz="+ioPadSz.printf("%8d")+" chSz="+ioChSz.printf("%10d")+" stepSz="+_sz;

            ch.resizeTo(chSz);
         }

         cur_sta += (float(chSz) / _sz);
      }
   }

   static AlignPaddedEntrySizesTo(int _sz) {

      ChainEntry *ch;

      foreach ch in chain
      {
         int chSz = ch.getSampleSize();

         if(chSz != ((chSz / _sz) * _sz))
         {
            chSz = ((chSz / _sz) + 0) * _sz;

            ch.pad_sz = chSz - ch.orig_sz;

            Float ioSta = cur_sta;
            Integer ioOrigSz = ch.orig_sz;
            Integer ioPadSz = ch.pad_sz;
            Integer ioChSz = chSz;

            // trace "xxx STA="+cur_sta+" origSz="+ch.orig_sz+" padSz="+ch.pad_sz+" chSz="+chSz+" stepSz="+_sz;
            trace "[trc] STA="+ioSta.printf("%6.2f")+" origSz="+ioOrigSz.printf("%8u")+" padSz="+ioPadSz.printf("%8d")+" chSz="+ioChSz.printf("%8d")+" stepSz="+_sz;

            ch.resizeTo(chSz);
         }

         cur_sta += (float(chSz) / _sz);
      }
   }

   static BuildOutChain() {

      FloatArray d <= out_chain.wav.sampleData;

      ChainEntry *ch;
      foreach ch in chain
      {
         trace "xxx ch.sz="+((ch.wav.sampleData.numElements)*2);
         d.join(d, ch.wav.sampleData);
      }
   }
}

if(b_autodir)
{
   String fnPathDir;
   String fnFileDir;
   Utils.SplitPathname(filelistFileName, fnPathDir, fnFileDir);
   dir_prefix = fnPathDir + "/";
}


if(!b_upload || midiin.openByName(inDevName))
{
   if(b_upload)
      midiin.start();
   
   if(!b_upload || midiout.openByName(outDevName))
   {
      String fileName;
      int fileNr = 1;

      StringArray fileNames;

      if(single_filename.isBlank())
      {
         fileNames <= filelist.splitChar('\n');
      }
      else
      {
         fileNames.add(single_filename);
      }

      // trace "xxx fileNames="+#(fileNames);

      foreach fileName in fileNames
      {
         fileName.trim();

         fileName = dir_prefix+fileName;

         if(b_chain)
         {
            ChainEntry en <= new ChainEntry;
            en.init();
            if(en.load(fileName))
            {
               trace "[trc] add chain entry \""+fileName+"\"";
               chain.add(#(deref en));
            }
         }
         else
         {
            // Original upload code
            if(fileNames.numElements > 1)
            {
               force_sample_name = "";
            }

            if(WavIO.LoadLocal(fileName, g_wav.sampleData, g_rate, g_numCh, g_fileInfo, g_sam))
            {
               if(b_debug)
               {
                  trace "[dbg] wav.sampleData.numElements="+(g_wav.sampleData.numElements);
               }

               if(b_upload)
               {
                  SDS_Test sds <= new SDS_Test;

                  int tStart = milliSeconds();

                  // (note) on Yamaha AxK sampler, sampleNr determines the sample name:
                  //   sampleNr=0 => "MIDI 00001"
                  //   sampleNr=1 => "MIDI 00002"
                  //   ...
                  // (note) Yamaha AxK can handle 128 samples max., 127 per samplebank
                  // sds.upload(sam, 1/*sampleNr*/, sysexChannel, null);

                  /* NOTE: when uploading samples to the Elektron Analog Rhythm, sampleNr 0 _must_ be used! */
                  /*  (recv CANCEL otherwise) */
                  // sds.enableElektronARTurbo();

                  String g_fnPath;
                  String g_fnFile;
                  Utils.SplitPathname(fileName, g_fnPath, g_fnFile);
                  g_fnFile.replace(".wav", "");
                  g_fnFile.replace(".WAV", "");

                  String g_remoteName = g_fnFile;

                  // sds.upload(sam, 0/*sampleNr*/, sysexChannel, "test2__name");
                  if(!single_filename.isBlank())
                  {
                     if(!force_sample_name.isBlank())
                     {
                        g_remoteName = force_sample_name;
                     }
                  }
         
                  // sds.upload(sam, 0/*sampleNr*/, sysexChannel, "test2__name");
                  sds.upload(g_sam, 0/*sampleNr*/, sysexChannel, g_remoteName);
                  // sds.upload(g_sam, 0/*sampleNr*/, sysexChannel, g_fnFile);

                  // (note) 17.223 seconds for 424 packets (16960 samples)
                  //          => 24.618 packets/sec
                  //          => 984.729 samples/sec
                  int tDelta = milliSeconds() - tStart;

                  trace "[...] file "+fileNr+"/"+(fileNames.numElements)+" transfered in "+(tDelta/1000.0f)+" seconds ("+((2*g_wav.sampleData.numElements) / (tDelta/1000.0f))+" bytes/sec).";
               }
            
            }
            else
            {
               trace "[---] failed to open sample file \""+fileName+"\".";
            }
         } // if b_chain

         fileNr++;
      } // foreach file

      if(b_chain)
      {
         if(chain.numElements > 0)
         {
            int maxSmpSz;

            if(0 != chain_size)
            {
               if(chain.numElements > chain_size)
               {
                  trace "[~~~] warning: number of files ("+chain.numElements+") exceeds the chain size ("+chain_size+"), some files will not be uploaded!";
               }
               else if(chain.numElements < chain_size)
               {
                  trace "[...] chain size ("+chain_size+") exceeds the number of files ("+chain.numElements+"), adding silence to compensate";
                  loop(chain_size - chain.numElements)
                  {
                     ChainEntry padChEn <= new ChainEntry;
                     padChEn.init();
                     chain.add(#(deref padChEn));
                  }

               }

               maxSmpSz = ChainUtils.GetMaxSampleSize();
               ChainUtils.ResizeEntriesTo(maxSmpSz + extra_padding);
            }
            else if(b_varichain)
            {
               // Varichain

               int origTotalSmpSz = ChainUtils.GetTotalSampleSize();

               trace "[...] origTotalSmpSz="+origTotalSmpSz;

               if(extra_padding < 2000)
                  extra_padding = 2000;

               int iter = 1;

               for(;;)
               {
                  ChainUtils.AddPadding(extra_padding);

                  int varStepSmpSz;
                  int chainStep;
                  int totalSmpSz;

                  chainStep = 120 / chain.numElements;
                  trace "xxx chainStep="+chainStep;

                  // 1:
                  totalSmpSz = ChainUtils.GetTotalSampleSize();

                  int origPadTotalSmpSz = totalSmpSz;

                  trace "xxx start totalSmpSz="+totalSmpSz;

                  maxSmpSz = ChainUtils.GetMaxSampleSize();

                  trace "xxx start maxSmpSz="+maxSmpSz;

                  ChainUtils.CalcSlicePct1(maxSmpSz);

                  float maxPct = float(maxSmpSz) / totalSmpSz;

                  int maxNumSlices = (120 * maxPct) + 0.5;
               
                  trace "xxx maxPct="+maxPct+" maxNumSlices="+maxNumSlices;

                  float slcSz = maxSmpSz / maxNumSlices;
                  trace "xxx slcSz="+slcSz;

                  ChainUtils.cur_sta = 0;
                  ChainUtils.AlignEntrySizesTo(slcSz);

                  totalSmpSz = ChainUtils.GetTotalSampleSize();
                  trace "[...] newTotalSmpSz="+totalSmpSz;
                  float newNumSlices = totalSmpSz / slcSz;
                  trace "[...] newNumSlices="+newNumSlices+" int="+int(newNumSlices+0.5);

                  slcSz = totalSmpSz / 120.0;
                  trace "[...] newSlcSz="+slcSz+" int="+int(slcSz+0.5);

                  slcSz = int(slcSz+0.5);

                  ChainUtils.cur_sta = 0;
                  ChainUtils.AlignPaddedEntrySizesTo(slcSz);

                  if(ChainUtils.ArePadSizesGreaterThan(min_padding))
                     break;
                  else
                  {
                     ChainUtils.RestoreOrigSizes();
                     extra_padding += 100;
                     iter++;
                  }
               }

               totalSmpSz = ChainUtils.GetTotalSampleSize();
               float padNewNumSlices = totalSmpSz / slcSz;
               trace "[...] padNewNumSlices="+padNewNumSlices+" int="+int(padNewNumSlices+0.5);

               // Add pad entry
               ChainEntry enVariPad <= new ChainEntry;
               enVariPad.init();
               chain.add(#(deref enVariPad));
               enVariPad.resizeTo( (120 - padNewNumSlices) * slcSz );

               totalSmpSz = ChainUtils.GetTotalSampleSize();
               float finalNumSlices = totalSmpSz / slcSz;
               trace "[...] finalNumSlices="+finalNumSlices+" int="+int(finalNumSlices+0.5);
               trace "[...] origTotalSmpSz="+origTotalSmpSz;

               trace "[...] total padding:"+(totalSmpSz - origTotalSmpSz)+" ratio to unpadded orig="+((float(totalSmpSz)/origTotalSmpSz)*100)+"%";
               trace "[...] ratio to padded orig="+((float(totalSmpSz)/origPadTotalSmpSz)*100)+"%";

               trace "[...] avg slice padding="+ChainUtils.GetAvgSlicePad();

               totalSmpSz = ChainUtils.GetTotalSampleSize();

               trace "[...] totalSmpSz="+totalSmpSz+"  /120="+(float(totalSmpSz)/120);

               trace "[...] ("+(totalSmpSz*2)+" bytes)";

              
               trace "[dbg] num iterations="+iter;
            }
            else
            {
               trace "[---] chain is empty, nothing to upload.";
               exit(5);
            }

            trace "[...] Building sample chain..";
            ChainUtils.BuildOutChain();

            if(out_chain.getSampleSizeInBytes() >= (16*1024*1024))
            {
               trace "[---] the sample chain is larger than 16 MBytes, please reduce the chain size or the number of samples.";
               exit(5);
            }

            trace "[...] Sample chain memory size is "+float(out_chain.getSampleSizeInBytes() / 1024.0f)+" kBytes"; ///, sample start/end step is "+(120/chain.numElements);

            if(b_debug)
            {
               trace "[...] writing generated sample chain to \"debug.wav\"";
               WavIO.SaveLocal("debug.wav", out_chain.wav.sampleData, out_chain.wav.sampleRate, 1/*numCh*/);
            }

            out_chain.upload();
         }
      }

      if(b_upload)
      {
         midiout.close();

         if(b_debug)
         {
            trace "[dbg] closed MIDI device";
         }
      }
   }
   else
   {
      trace "[---] failed to open MIDI output device \""+outDevName+"\".";
   }
   
   if(b_upload)
   {
      midiin.stop();
      midiin.close();
   }
}
else
{
   trace "[---] failed to open MIDI input device \""+inDevName+"\".";
}

// trace "[dbg] exiting";