// ----
// ---- file   : NodeAuto.tks
// ---- author : Bastian Spiegel <bs@tkscript.de>
// ---- legal  : (c) 2015-2018 by Bastian Spiegel. 
// ----          Distributed under terms of the GNU LESSER GENERAL PUBLIC LICENSE (LGPL). See 
// ----          http://www.gnu.org/licenses/licenses.html#LGPL or COPYING for further information.
// ----
// ---- info   : This is part of the "syntracker" sequencer.
// ----
// ---- changed: 14Mar2015, 15Mar2015, 16Mar2015, 17Mar2015, 18Mar2015, 20Mar2015, 21Mar2015
// ----          22Mar2015, 23Mar2015, 24Mar2015, 25Mar2015, 26Mar2015, 27Mar2015, 28Mar2015
// ----          06Apr2015, 14Apr2015, 17Apr2015, 18Apr2015, 19Apr2015, 20Apr2015, 21Apr2015
// ----          27Apr2015, 29Apr2015, 11May2015, 13May2015, 14May2015, 16May2015, 04Oct2015
// ----          26Oct2015, 08Jan2016, 17Jan2016, 13Mar2016, 09Apr2016, 09Jul2016, 12Jul2016
// ----          16Sep2016, 21Jan2017, 04Feb2017, 05Feb2017, 12Aug2017, 04Jan2018, 07Mar2018
// ----          27May2018, 17Jun2018
// ----
// ----
// ----

module MNodeAuto;

use namespace ui;
use namespace st2;


// <class.png>
class NodeAutoLane {

 
   int lane_idx;

   NodeAutoTrack *parent_track;
   NodeAuto *parent_node;

   define int MODE_REPLACE    = 0;
   define int MODE_OVERLAY    = 1;
   define int MODE_ADD        = 2;
   define int MODE_SUB        = 3;
   define int MODE_MIN        = 4;
   define int MODE_MAX        = 5;
   define int MODE_IN_DELTA   = 6;  // treat start/end values as "0"-point for incoming ctl values
   define int MODE_OUT_DELTA  = 7;  // treat start value as "0"-point for automation ctl values
   define int MODE_CENTER     = 8;
   define int MODE_WARP       = 9;
   define int MODE_WARP2      = 10;

   int mode;

   boolean b_muted;
   boolean b_locked;
   boolean b_unfold;

   boolean b_sh;   // true=sample&hold envelope value

   boolean b_start;
   short start_value;

   // // boolean b_end;
   // // short end_value;

   short amt_value;  // for SCALE, OVERLAY, ..
   short ctr_value;  // for CENTER, WARP
   
   int loop_len;  // 0=infinite / oneshot, >0: num pulses

   boolean b_sync_pat_loop;

   // delta time / value pairs. delta time unit is #pulses.
   //  for EVTYPE_NOTES, the value is an index into the note_data array
   Envelope env;

   PointerArray note_data; // MIDIPipeFrame instances

   int num_recorded;  // num events recorded since recording started
   int autoclear_timeout;
   boolean b_autoclear_after_loop; // true=env has already been cleared after loop

   float last_env_value;
   boolean b_force_lastev;  // true= ev was recorded after recposition has already been played

   float rec_value;
   boolean b_force_recev;  // true= use recorded event value in next play() call, overriding envelope vals

   int last_record_pulse_nr;
   float last_record_val;

   boolean b_have_new_recorded_event;

   IntArray last_rec_note_qp;
   IntArray last_rec_note_rawp;  // Raw position, to correctly calculate note duration
   IntArray last_rec_note_off_delay; // Note-Off delay for notes that will be played Q-delayed, don't let manual note-off cut note too early if it occurs shortly after sequenced note-on (a bit tricky)
   IntArray last_rec_delayed_note_offs; // List of notes that have to be note-off delayed after recording. 'last_rec_note_off_delay' is used as a countdown in this case
   int rec_note_frame_idx;  // -1 or index to note_data array



   // <method_init.png>
   public method init(NodeAutoTrack _parent, int _laneIdx) {
      parent_track <= _parent;

      parent_node <= parent_track.parent_pat.parent_node;

      lane_idx = _laneIdx;

      mode = MODE_REPLACE;

      last_env_value = NodeAuto.INVALID_VALUE;

      amt_value = 127;
      ctr_value = 64;

      env.interpolation = ENV_SHRESET;  // for isNewEvent()
      b_sh = true;

      b_sync_pat_loop = true;

      last_rec_note_qp.alloc(128);
      last_rec_note_qp.useAll();

      last_rec_note_rawp.alloc(128);
      last_rec_note_rawp.useAll();

      last_rec_note_off_delay.alloc(128);
      last_rec_note_off_delay.useAll();
   }

   // <method_set.png>
   public =replay= method setEnableLock(boolean _bEnable) {
      b_locked = _bEnable;
   }

   // <method_set.png>
   public =replay= method setEnableMute(boolean _bEnable) {
      b_muted = _bEnable;
   }

   // <method_set.png>
   public =replay= method setMode(int _mode) {
      mode = _mode;
   }

   // <method_set.png>
   public =replay= method setEnableSH(boolean _bEnable) {
      b_sh = _bEnable;
   }

   // <method_set.png>
   public =replay= method setLastEnvValue(float _val) {
      last_env_value = _val;
   }

   // <method_get.png>
   public =replay= method getLastEnvValue() : float {
      return last_env_value;
   }


   // <method_get.png>
   public =replay= method getLastPlayedPosition() : int {
      return env.time;
   }

   // <method_set.png>
   public =replay= method setEnableStart(boolean _bEnable) {
      b_start = _bEnable;
   }

   // <method_set.png>
   public =replay= method setStartValue(short _val) {
      start_value = _val;
   }

   // // // <method_set.png>
   // // public =replay= method setEndValue(short _val) {
   // //    end_value = _val;
   // // }

   // <method_set.png>
   public =replay= method setAmtValue(short _val) {
      amt_value = _val;
   }

   // <method_set.png>
   public =replay= method setCtrValue(short _val) {
      ctr_value = _val;
   }

   // <method_set.png>
   public =replay= method setLoopLen(int _len) {
      loop_len = _len;
   }

   // <method_set.png>
   public =replay= method setEnableSyncPatLoop(boolean _bEnable) {
      b_sync_pat_loop = _bEnable;
   }

   // <method_set.png>
   public =replay= method getNumEvents() {
      return env.numElements / 2;
   }

   // <method_get.png>
   public method getNumPulses() : int {
      int r = 0;

      if(loop_len > 0)
      {
         r = loop_len;
      }

      if(b_sync_pat_loop)
      {
         NodeAutoPattern pat <= parent_track.parent_pat;
         int pr = pat.getRes() * pat.getSigBeats() * pat.getNumBars();

         if((0 == r) || (pr < r))
         {
            r = pr;
         }
      }
      
      if(0 == r)
      {
         if(env.numElements >= 2)
         {
            int lastIdx = env.numElements - 2;
            r = env.indexToTime(lastIdx) + 1;
         }
         else
         {
            r = 999999;
         }
      }

      return r;
   }

   // <method_get.png>
   public method isInfinite() : boolean {
      return (0 == loop_len) && !b_sync_pat_loop;
   }

   // <method_get.png>
   public method getQuantizedRecordPosition() : int {
      // Called during record

      NodeAutoPattern pat <= parent_track.parent_pat;

      int wt = pat.rec_winsz;
      float pt = pat.pulse_ticks;

      // (note) tick_cnt is already incremented by now, revert that
      int ptc = ((pat.parent_node.pulse_tick_cnt) + (pat.pulse_ticks - 1.0)) % (pat.parent_node.pulse_tick_cnt);

      // trace "xxx getQuantizedRecordPosition:\n\t\tenv.time="+env.time+" pt="+pt+" tick_cnt="+(pat.parent_node.pulse_tick_cnt)+" ptc="+ptc;

      // float qp = (env.time * pt) + (pat.parent_node.pulse_tick_cnt) + (wt * pt * 0.5);
      float qp = (env.time * pt) + ptc + (wt * pt * 0.5);

      return int(qp / (wt * pt)) * wt;
   }

   // <method_set.png>
   protected method setHaveNewRecordedEvent() {
      b_have_new_recorded_event = true;

      parent_track.parent_pat.parent_node.setHaveNewRecordedEvent();
   }

   // <method_get.png>
   public =replay= method checkResetHaveNewRecordedEvent() : boolean {
      boolean ret = b_have_new_recorded_event;

      b_have_new_recorded_event = false;

      return ret;
   }

   // <method.png>
  public =replay= method setEventWindow(int _pulse, float _val, int _winSz) {
      // called by NA_EnvelopeView
      if(NodeAutoTrack.EVTYPE_NOTES != parent_track.io_evtype)
      {
         env.insertReplaceEvent(_pulse, _val, _winSz);
      }
      else
      {
         MIDIPipeFrame fr <= getNoteFrameByPulse(_pulse);
         if(null != fr)
         {
            // Update velocities
            int noteIdx = 0;
            loop(16)
            {
               fr.setEventArg2ByIdx(noteIdx++, _val);
            }
         }
      }
   }
      
   // <method.png>
   public =replay= method setEvent(int _pulse, float _val) {
      setEventWindow(_pulse, _val, 1);
   }

   // <method.png>
   public =replay= method setEventWindowed(int _pulse, float _val) {
      NodeAutoPattern pat <= parent_track.parent_pat;

      // // _pulse = int((_pulse + pat.rec_winsz*0.5) / pat.rec_winsz) * pat.rec_winsz;
      _pulse = int(_pulse / pat.rec_winsz) * pat.rec_winsz;

      if(NodeAutoTrack.EVTYPE_NOTES != parent_track.io_evtype)
      {
         env.removeRange(_pulse, pat.rec_winsz);
         env.shiftEvents(_pulse, pat.rec_winsz);
      }

      setEventWindow(_pulse, _val, pat.rec_winsz);
   }

   // <method.png>
   public =replay= method optimizeNoteData() {
      if(NodeAutoTrack.EVTYPE_NOTES == parent_track.io_evtype)
      {
         // Called after index env was edited and events were removed
         //  Optimize note_data array and only keep the frames that are actually used
         int idx = 0;
         int newDataIdx = 0;
         PointerArray newNoteData <= new PointerArray;

         loop(env.numElements / 2)
         {
            MIDIPipeFrame fr <= note_data.getDeref(env[idx + 1]);
            if(null != fr)
            {
               newNoteData.add(#(deref fr));
               env[idx + 1] = newDataIdx++;
            }

            idx += 2;
         }

         note_data <= deref newNoteData;
      }
   }

   // <method.png>
   public =replay= method clearEvent(int _pulse) {
      // called by NA_EnvelopeView
      env.removeRange(_pulse, 1);
      env.shiftEvents(_pulse, 1);

      optimizeNoteData();
   }

   // <method.png>
   public method clearEvents() : boolean {
      // Called while =replay= mutex is locked

      if(!b_locked)
      {
         env.free();
         note_data.free();
         last_record_pulse_nr = -1;
         return true;
      }
      return false;
   }

   // <method.png>
   public method clearEventWindow() {
      // Called while =replay= mutex is locked
      if(!b_locked)
      {
         NodeAutoPattern pat <= parent_track.parent_pat;

         int oldNumEv = env.numElements / 2;

         // Window ticks
         float wt = pat.rec_winsz;

         // Quantized position
         //int qp = int((env.time + (wt*0.5)) / wt) * wt;
         int qp = getQuantizedRecordPosition();
         
         env.removeRange(qp, wt);
         env.shiftEvents(qp, wt);

         optimizeNoteData();

         last_record_pulse_nr = -1;

         int numClear = (oldNumEv - (env.numElements/2));

         if(numClear > 0)
         {
            setHaveNewRecordedEvent();

            Global.Debug2("clearEventWindow:  qp="+qp+" wt="+wt+": cleared "+numClear+" event(s).");
         }

      }
   }

   // <save.png>
   public method laneSaveState(Stream ofs) {

      // Version
      ofs.i16 = 7;

      ofs.i8 = mode;
      ofs.i8 = b_muted;
      ofs.i8 = b_unfold;
      ofs.i8 = b_locked; // v2+

      ofs.f32 = last_env_value; // v6+

      ofs.i16 = start_value;
      ofs.i16 = ctr_value;  // pre v5: end_value;

      ofs.i8 = b_start; // v3+
      ofs.i8 = b_sh;    // v3+   prev: b_end (pre v5)

      ofs.i16 = amt_value;

      ofs.i32 = loop_len; // v4+

      ofs.i8 = b_sync_pat_loop; // v5+

      // Save env
      int envStartOff = ofs.offset;
      ofs.i32 = 0;
      Utils.WriteEnvelope(ofs, env);
      Utils.FixChunkSize(ofs, envStartOff);

      // Save note_data (v7+)
      int ndStartOff = ofs.offset;
      ofs.i32 = 0;
      MIDIPipeFrame *fr;

      foreach fr in note_data
      {
         fr.ioFilter = MIDIPIPE_EVENT_TYPE_NOTES;
         fr.saveState(ofs);
      }
      Utils.FixChunkSize(ofs, ndStartOff);
   }

   // <load.png>
   public method laneLoadState(Stream ifs, IntArray _outDevIdxMap, IntArray _outDevUseCount) : boolean {

      short ver = ifs.i16;
      int t;

      if(ver >= 1)
      {
         mode     = ifs.i8;
         b_muted  = ifs.i8;
         b_unfold = ifs.i8;
         
         if(ver >= 2)
         {
            b_locked = ifs.i8;
         }

         if(ver >= 6)
         {
            last_env_value = ifs.f32;
         }

         start_value  = ifs.i16;
         ctr_value    = ifs.i16; // pre v5: end_value

         if(ver >= 3)
         {
            b_start = ifs.i8;
            b_sh    = ifs.i8;  // pre v5: b_end
         }

         amt_value = ifs.i16;

         if(ver >= 4)
         {
            loop_len = ifs.i32;

            if(ver >= 5)
            {
               b_sync_pat_loop = ifs.u8;
            }
         }

         // Read envelope
         int envStartOff = ifs.offset;
         int envSize = ifs.i32;
         if(Utils.ReadEnvelope(ifs, envSize - 4, env) == (envSize - 4))
         {
            if(ver >= 7)
            {
               // Read note data
               int ndStartOff = ifs.offset;
               int ndTotalBytes = ifs.i32;
               int ndBytesLeft = ndTotalBytes - 4;
               int frameIdx = 0;

               while(ndBytesLeft > 0)
               {
                  MIDIPipeFrame fr <= new MIDIPipeFrame;
                  fr.ioFilter = MIDIPIPE_EVENT_TYPE_NOTES;
                  fr.usrFrameIdx = frameIdx;
                  int frameBytesRead = ifs.offset;
                  if(!fr.loadState(ifs))
                  {
                     trace "[---] NodeAuto::laneLoadState: failed to read frame #"+frameIdx;
                     return false;
                  }

                  note_data.add(#(deref fr));

                  frameBytesRead = ifs.offset - frameBytesRead;

                  ndBytesLeft -= frameBytesRead;

                  frameIdx++;
               }

               if(0 != ndBytesLeft)
               {
                  trace "[---] NodeAuto::laneLoadState: invalid note_data read size (expected "+ndTotalBytes+", have "+(ifs.offset - ndStartOff)+")";
               }
               else
               {
                  // Succeeded
                  return true;
               }
            }
            else
            {
               // Succeeded
               return true;
            }
         }
         else
         {
            trace "[---] NodeAuto::laneLoadState: invalid env read size (expected "+envSize+", have "+(ifs.offset - envStartOff)+")";
         }

      } // if ver >= 1

      return false;
   }

   // <replay.png>
   public method reset(boolean _bSoft) {
      // (note) do NOT unset last_env_value, e.g. when envelope does not have any events and the lanes
      //         are just used to modify constant controller values (saved with project)
      // // last_env_value = NodeAuto.INVALID_VALUE;
      b_force_lastev = false;
      b_force_recev = false;

      last_record_pulse_nr = -1;

      autoclear_timeout = 0;
      b_autoclear_after_loop = false;

      b_have_new_recorded_event = false;

      last_rec_note_qp.fill(-1);

      last_rec_note_off_delay.fill(0);
      last_rec_delayed_note_offs.empty();
   }

   // <replay.png>
   public method seek(int _pulses, boolean _bPatLoop) {

      // bPatLoop: true=seek due to pattern loop

      int numPulses = getNumPulses();
      if(numPulses < 999999)
      {
         _pulses = _pulses % numPulses;
      }

      if(_bPatLoop)
      {
         if(b_sync_pat_loop)
         {
            env.time = _pulses;
            num_recorded = 0;
            b_autoclear_after_loop = false;
         }
      }
      else
      {
         env.time = _pulses;
      }

      // trace "xxx seek: _pulses="+_pulses+" env.time="+env.time;
   }

   // <method.png>
   public method getNoteFrameByPulse(int _pulse) : MIDIPipeFrame {
      var frIdx = env.valueAtTimeRaster(_pulse, 1);

      MIDIPipeFrame *ret;

      if(typeid(frIdx) != YAC_TYPE_VOID)
      {
         ret <= note_data.get(frIdx);
      }
      else
      {
         ret <= null;
      }

      return ret;
   }

   // <method.png>
   public method getOrCreateNoteFrameByPulse(int _pulse) : MIDIPipeFrame {
      var frIdx = env.valueAtTimeRaster(_pulse, 1);

      MIDIPipeFrame *ret;

      if(typeid(frIdx) != YAC_TYPE_VOID)
      {
         ret <= note_data.get(frIdx);
      }
      else
      {
         ret <= null;
      }

      // trace "xxx get: frIdx="+frIdx+" fr="+#(ret);

      if(null == ret)
      {
         ret <= new MIDIPipeFrame;
         note_data.add(#(deref ret));
         frIdx = note_data.numElements - 1;
         env.insertReplaceEvent(_pulse, frIdx, 1);
         ret.usrFrameIdx = frIdx;
         // trace "xxx get: add frIdx="+frIdx;
      }

      return ret;
   }

   // <method_get.png>
   public =replay= method getPrimaryEnvValueByIdx(int _evIdx) {
      float f = env[_evIdx * 2 + 1];
      if(NodeAutoTrack.EVTYPE_NOTES == parent_track.io_evtype)
      {
         MIDIPipeFrame fr <= note_data.get(f);
         if(null != fr)
         {
            f = fr.getEventArg2ByIdx(0); // velocity

            if(f < 0)
               f = 0; // should not be reached (-1 if event 0 does not exist)
         }
         else
         {
            f = 0;
         }
      }
      return f;
   }

   // <method.png>
   public =replay= method clear() {
      env.free();
   }

   // <method.png>
   public method syncPlayPosition(NodeAutoLane o) {
      env.time = o.env.time;
   }

   // <replay.png>
   public method record(float _val, int _note) : boolean {
      // (note) _val is always in 0..127 range
      boolean bPassThru = false;

      NodeAutoPattern pat <= parent_track.parent_pat;

      if(NodeAutoPattern.RECMODE_REPLACE == pat.rec_mode)
      {
         if(0 == num_recorded)
         {
            if(!b_autoclear_after_loop)
            {
               if(0 == autoclear_timeout)
               {
                  env.free();
                  note_data.free();
                  last_record_pulse_nr = -1;
                  b_autoclear_after_loop = true;
                  Global.Debug2("RECMODE_REPLACE: autoclear lane envelope");
               }
            }
         }
      }

      // Window ticks
      float wt = pat.rec_winsz;
      
      // Quantized position
      int rawP = env.time;
      // int qp = int((rawP + (wt*0.5)) / wt) * wt;
      int qp = getQuantizedRecordPosition();

      int numPulses;
      boolean bWrapAroundRecord;

      if(isInfinite())
      {
         numPulses = 999999;
         bWrapAroundRecord = false;
      }
      else
      {
         numPulses = getNumPulses(); // returns 999999 if lane is infinite and empty

         if((0 == num_recorded) && (rawP >= (numPulses - pat.rec_winend)))
         {
            qp = 0;
            bWrapAroundRecord = true;
         }
         else
         {
            bWrapAroundRecord = (qp >= numPulses);
         }

         qp = qp % numPulses;
      }

      Global.Debug2("NodeAutoLane::record: rawP="+rawP+" t="+qp+" last="+last_record_pulse_nr+" wt="+wt+" val="+_val+" numPulses="+numPulses);

      // Handle controller interpolation
      if(-1 != last_record_pulse_nr)
      {
         int pulseDelta = (qp - last_record_pulse_nr);
         boolean bWrapAroundRemove = false;

         if(pulseDelta < 0)
         {
            // Loop wrap-around
            pulseDelta = (numPulses - last_record_pulse_nr) + qp;
            Global.Debug2("NodeAutoLane::record: clear range: wraparound: qp="+qp+" last_rec="+last_record_pulse_nr+" numPulses="+numPulses);
            bWrapAroundRemove = true;
         }

         if(pulseDelta > 0)
         {
            if(NodeAutoPattern.RECMODE_REPLACE == pat.rec_mode)
            {
               if(pulseDelta > 1)
               {
                  Global.Debug2("NodeAutoLane::record: clear range: qp="+qp+" last_record_pulse_nr="+last_record_pulse_nr+" pulseDelta="+pulseDelta+" time="+env.time+" bWrapAroundRemove="+bWrapAroundRemove);

                  if(bWrapAroundRemove)
                  {
                     env.removeRange(last_record_pulse_nr + 1, numPulses - 1 - last_record_pulse_nr);
                     env.shiftEvents(last_record_pulse_nr + 1, numPulses - 1 - last_record_pulse_nr);

                     env.removeRange(0, qp);
                     env.shiftEvents(0, qp);
                  }
                  else
                  {
                     env.removeRange(last_record_pulse_nr + 1, pulseDelta -1);
                     env.shiftEvents(last_record_pulse_nr + 1, pulseDelta -1);
                  }

                  optimizeNoteData();
                  // trace "xxx clear range: POST time="+env.time;
               }
            }

            if(NodeAutoTrack.EVTYPE_NOTES != parent_track.io_evtype)
            {
               if(1 == wt)
               {
                  if(pulseDelta >= 2)
                  {
                     int ipolTicks = current_song.getCtlIpolNumTicks();
                     if(ipolTicks > 0)
                     {
                        // 100ms = ~3 pulses at 125bpm / 96 ppq
                        int ipolPulses = ipolTicks / (parent_track.parent_pat.pulse_ticks);

                        if(pulseDelta <= ipolPulses)
                        {
                           int ip = (last_record_pulse_nr + 1) % numPulses;
                           float ivStep = (_val - last_record_val) / pulseDelta;
                           float iv = last_record_val + ivStep;

                           loop(pulseDelta - 1)
                           {
                              //trace "xxx ****************** IPOL ip="+ip+" iv="+iv;
                              env.insertReplaceEvent(ip, iv, 1);
                              ip = (ip + 1) % numPulses;
                              iv += ivStep;
                           }
                        }
                     }
                  }
               }
            }
         }
      }

      if(NodeAutoTrack.EVTYPE_NOTES != parent_track.io_evtype)
      {
         env.insertReplaceEvent(qp, _val, wt/*windowSize*/);

         // // bAllowForce = (env.time < qp);
         b_force_lastev = (env.time >= qp) && !bWrapAroundRecord;    
         setHaveNewRecordedEvent();
      }
      else
      {
         MIDIPipeFrame *fr;

         if(_note < 0)
         {
            // Note-Off (only note duration is updated)
            _note = -_note;

            int recQP = last_rec_note_qp[_note];
            int recRawP = last_rec_note_rawp[_note];

            last_rec_note_qp[_note] = -1;

            if(-1 != recQP)
            {
               int dur;
               if(recRawP > rawP)
               {
                  // Wrap-around
                  dur = (numPulses - recRawP) + rawP;
               }
               else
               {
                  dur = (rawP - recRawP);
               }

               // // Quantize duration
               // dur = int((dur + wt*0.5) / wt) * wt;

               if(dur < 1)
               {
                  dur = 1;
               }
               
               // Update note duration
               fr <= getNoteFrameByPulse(recQP);

               if(null != fr)
               {
                  fr.updateNoteDuration(0,0, _note, dur);

                  Global.Debug2("NodeAutoLane::record: update recQP="+recQP+" note="+_note+" dur="+dur);
               }
               // else: should not be reached

            }

            if(last_rec_note_off_delay[_note] > 0)
            {
               // Delay note-off to compensate for quantized offset
               last_rec_delayed_note_offs.add(_note);
               bPassThru = false;
            }
            else
            {
               // Play note-off immediately
               bPassThru = true;
            }
         }
         else
         {
            // Note-On
            last_rec_note_qp[_note] = qp;
            last_rec_note_rawp[_note] = rawP;

            if(bWrapAroundRecord)
            {
               if(qp < rawP)
               {
                  // Will be played delayed
                  last_rec_note_off_delay[_note] = numPulses - rawP;
               }
               else
               {
                  // Will be played immediately (no note off delay)
                  last_rec_note_off_delay[_note] = 0;
               }
            }
            else
            {
               if(qp > rawP)
               {
                  // Will be played delayed
                  last_rec_note_off_delay[_note] = qp - rawP;
               }
               else
               {
                  // Will be played immediately (no note off delay)
                  last_rec_note_off_delay[_note] = 0;
               }
            }

            fr <= getOrCreateNoteFrameByPulse(qp);
            fr.noteOn(true/*bSet*/, 0,0, _note,
                      _val/*velocity*/,
                      0/*duration placeholder*/);

            env.insertReplaceEvent(qp, fr.usrFrameIdx, wt/*windowSize*/);

            if((qp < env.time) && !bWrapAroundRecord)
            {
               // Env time already passed (quantization). Make sure note is played this tick.
               //  (todo) consider wrap-around
               rec_note_frame_idx = fr.usrFrameIdx;
            }
            else
            {
               // Note is played when quantized pulse is reached
               rec_note_frame_idx = -1;
            }

            // trace "xxx record: \ttime="+env.time+" note="+_note+" vel="+_val+" num_note_data="+note_data.numElements;

            // b_force_lastev = (env.time >= qp); // xxx rly needed??
            b_force_lastev = (env.time >= qp) && !bWrapAroundRecord; // xxx rly needed??
            // bPassThru = (env.time <= qp);
            bPassThru = (env.time >= qp) && !bWrapAroundRecord;

            setHaveNewRecordedEvent();
         }
      }



      num_recorded++;

      autoclear_timeout = STConfig.node_auto_lane_autoclear_timeout;
      b_autoclear_after_loop = true; // don't autoclear once sth has been recorded after loop cycle

      Global.Debug2("NodeAutoLane::record: record: qp="+qp+" last_record_pulse_nr="+last_record_pulse_nr);
      last_record_pulse_nr = qp;
      last_record_val = _val;

      // trace "xxx record: bPassThru="+bPassThru;

      return bPassThru;
   }

   // <replay.png>
   public method play(MIDIPipeFrame _framePlay,
                      MIDIPipeFrame _frameRec,
                      boolean       _bMuted,
                      boolean       _bAdvance,
                      float         _inVal,
                      float         _altInVal,
                      Integer       _retNoteFrameIdx
                      ) : float {
      float r = _inVal;////NodeAuto.INVALID_VALUE;

      // trace "xxx play: getQuantizedRecordPosition="+getQuantizedRecordPosition();

      _retNoteFrameIdx = -1;

      // if(_bAdvance)
      //    trace "xxx ===================== play ================= time="+env.time;

      // if(env.isNewEvent())
      //    trace "xxx  newevent";

      if(!(b_muted || _bMuted))
      {
         float v = NodeAuto.INVALID_VALUE;

         if(b_force_recev)
         {
            // trace "xxx b_force_recev is set. rec_note_frame_idx="+rec_note_frame_idx;
            v = rec_value;
            last_env_value = v;

            if(NodeAutoTrack.EVTYPE_NOTES == parent_track.io_evtype)
            {
               _retNoteFrameIdx = rec_note_frame_idx;
            }
         }
         else
         {
            if(b_start && (0 == env.time))
            {
               v = start_value;
               last_env_value = v; 
            }

            if(env.isNewEvent())
            {
               v = env.get();
               last_env_value = v;

               if(NodeAutoTrack.EVTYPE_NOTES == parent_track.io_evtype)
               {
                  MIDIPipeFrame fr <= note_data.get(v);
                  if(null != fr)
                  {
                     _retNoteFrameIdx = fr.usrFrameIdx;
                  }
               }
            }
            else if(b_sh || b_force_lastev)
            {
               v = last_env_value;  // last read env value or freshly recorded value
            }
         }

         if((true == parent_track.b_alt_in) && (0 == lane_idx))
         {
            if(NodeAuto.INVALID_VALUE != _altInVal)
            {
               v = _altInVal;
               last_env_value = _altInVal;
            }
         }

         float amtNorm = amt_value / 127.0;

         if(NodeAuto.INVALID_VALUE != v)
         {
            if(NodeAuto.INVALID_VALUE != r)
            {
               switch(mode)
               {
                  case MODE_REPLACE:
                     r = v * amtNorm;
                     break;

                  case MODE_OVERLAY:
                     r = r + ((v - r) * amtNorm);
                     break;

                  case MODE_ADD:
                     trace "xxx auto: ADD: r="+r+" v="+v+" amtNorm="+amtNorm;
                     r = r + (v * amtNorm);
                     break;

                  case MODE_SUB:
                     r = r - (v * amtNorm);
                     break;

                  case MODE_MIN:
                     r = (r * (1.0 - amtNorm)) + amtNorm * mathMinf(r, v);
                     break;

                  case MODE_MAX:
                     r = (r * (1.0 - amtNorm)) + amtNorm * mathMaxf(r, v);
                     break;

                  case MODE_IN_DELTA:
                     r = (r - ctr_value) * amtNorm + v;
                     break;

                  case MODE_OUT_DELTA:
                     r = (v - ctr_value) * amtNorm + r;
                     break;

                  case MODE_CENTER:
                     r = r + (((ctr_value - r) * v * amtNorm) / 127.0);
                     break;
                     
                  case MODE_WARP:
                     r = ctr_value + (((r - ctr_value) * v) / amt_value);
                     break;

                  case MODE_WARP2:
                     r = v + (((r - v) * ctr_value) / amt_value);
                     break;
               }
            }
            else
            {
               // Env value but no input value
               switch(mode)
               {
                  case MODE_REPLACE:
                  case MODE_ADD:
                  case MODE_SUB:
                  case MODE_MIN:
                  case MODE_MAX:
                  case MODE_CENTER:
                     r = v * amtNorm;
                     break;

                  case MODE_OUT_DELTA:
                     r = (v - ctr_value) * amtNorm + 0;
                     break;

                  default:
                     // No output
                     break;
               }
            }
         }
         else
         {
            if(NodeAuto.INVALID_VALUE != _inVal)
            {
               // Input value but no env value

               switch(mode)
               {
                  default:
                     // Return input value
                     break;

                  case MODE_IN_DELTA:
                     r = (r - ctr_value) * amtNorm + 0;
                     break;

                  case MODE_WARP:
                     r = NodeAuto.INVALID_VALUE;
                     break;

                  case MODE_WARP2:
                     r = 0 + (((r - 0) * ctr_value) / amt_value);
                     break;
               }
            }
            else
            {
               // No valid env or input values (==> No output)
            }
         }
      } // if not muted

      if(_bAdvance)
      {
         env.tickPrecise(1.0f);
         ///trace "xxx env.time after tick="+env.time+" env.speed="+env.speed;

         if(0 != loop_len)
         {
            if(env.time >= loop_len)
            {
               env.time = 0;

               num_recorded = 0;

               b_autoclear_after_loop = false;
            }
         }
      }

      if(autoclear_timeout > 0)
      {
         autoclear_timeout--;

         if(0 == autoclear_timeout)
         {
            // Make envelope view refresh its display (value color from red => black/grayish)
            setHaveNewRecordedEvent();
         }
      }

      b_force_lastev = false;
      b_force_recev = false;

      rec_note_frame_idx = -1;

      // Handle delayed note-off (cornercase after fwd-quantized recording)
      int delayedNoteOff;
      int delayedNoteOffIdx = 0;
      while(delayedNoteOffIdx < last_rec_delayed_note_offs.numElements)
      {
         delayedNoteOff = last_rec_delayed_note_offs[delayedNoteOffIdx];
         int cd = last_rec_note_off_delay[delayedNoteOff];
         if(cd > 0)
         {
            if(0 == --cd)
            {
               MIDIPipeFrame frTmp <= parent_track.tmp_note_frame;
               frTmp.noteOff(true/*bSet*/, 0,0, delayedNoteOff, 0/*vel*/);
               // trace "xxx delayed note off";

               last_rec_delayed_note_offs.delete(delayedNoteOffIdx);
               delayedNoteOffIdx--;
            }
         }
         else
         {
            // Oops (should not be reached)
            last_rec_delayed_note_offs.delete(delayedNoteOffIdx);
            delayedNoteOffIdx--;
         }
         last_rec_note_off_delay[delayedNoteOff] = cd;
         delayedNoteOffIdx++;
      }

      return r;
   }

}


// <class.png>
class NodeAutoTrack {

   NodeAutoPattern *parent_pat;
   NodeAuto *parent_node;

   define int EVTYPE_CC        = 0;
   define int EVTYPE_RPN       = 1;
   define int EVTYPE_NRPN      = 2;
   define int EVTYPE_CHAT      = 3;
   define int EVTYPE_PITCHBEND = 4;
   define int EVTYPE_NOTES     = 5;
   define int EVTYPE_POLYAT    = 6;
   define int EVTYPE_VELOCITY  = 7;

   static StringArray evtype_names = [
      "CC", "RPN", "NRPN", "Channel AT", "Pitchbend", "Note On/Off", "Poly AT", "Velocity"
                                      ];

   int track_idx;

   boolean b_unfold;
   boolean b_muted;
   boolean b_record;
   boolean b_record_frameplay;  // true=(also) record output of previous node(s), false=only record external controller input

   int track_length; // 0=use pattern length, >0 = #pulses

   int   io_dev;
   int   io_ch;   // 0..15
   int   io_evtype; // EVTYPE_xxx
   byte  io_ev_cc;
   short io_ev_rpn;
   short io_ev_nrpn;

   boolean b_alt_in; // true=replace first lane envelope by alternative input events.  maybe= output track value only if note-on occured on alternative input

   int   alt_in_dev;
   int   alt_in_ch;  // 0..15
   int   alt_in_evtype; // EVTYPE_xxx
   byte  alt_in_ev_cc;
   short alt_in_ev_rpn;
   short alt_in_ev_nrpn;

   PointerArray lanes;  // NodeAutoLane instances

   int current_lane_idx; // -1 = none selected

   float frameplay_cur_val1;  // s&h input from first input (output from previous node(s)). normalized to 0..127
   float frameplay_cur_val2;  // s&h input from second input ("alt_in")

   float framerec_cur_val1;  // s&h input from first external controller input. normalized to 0..127
   float framerec_cur_val2;  // s&h input from second external controller input

   boolean b_discard_rec;  // true=always discard matching events from frameRec. false=discard only if automation track did output sth

   MIDIPipeFrame tmp_note_frame;

   boolean tmp_force_discard;

   MIDIPipeFrame gated_out_frame; // used when alt input is in gate mode


   // <method_init.png>
   public method init(NodeAutoPattern _parent, int _trackIdx) {
      parent_pat <= _parent;
      parent_node <= parent_pat.parent_node;

      track_idx = _trackIdx;

      io_evtype = EVTYPE_CC;
      alt_in_evtype = EVTYPE_CC;

      b_alt_in = false;
      
      b_record_frameplay = false;

      current_lane_idx = -1;
   }

   // <method_get.png>
   public method getLaneByIdx(int _idx) : NodeAutoLane {
      return lanes.get(_idx);
   }

   // <method_get.png>
   public method getCurrentLane() : NodeAutoLane {
      return lanes.get(current_lane_idx);
   }

   // <method.png>
   public method swapLanes(int _a, int _b) {
      NodeAutoLane *lane;

      // Update indices
      lane <= lanes.get(_a);
      lane.lane_idx = _b;

      lane <= lanes.get(_b);
      lane.lane_idx = _a;

      // Swap lanes
      lanes.swap(_a, _b);

      // Update current_lane_idx
      if(current_lane_idx == _a)
      {
         current_lane_idx = _b;
      }
      else if(current_lane_idx == _b)
      {
         current_lane_idx = _a;
      }
   }

   // <method_get.png>
   public =replay= method getAltInLastValue() : float {
      return frameplay_cur_val2;
   }

   // <method_set.png>
   public =replay= method setEnableAltIn(boolean _bEnable) {
      b_alt_in = _bEnable;
   }

   // <method_set.png>
   public =replay= method setEnableMute(boolean _bEnable) {
      b_muted = _bEnable;
   }

   // <method_set.png>
   public =replay= method setEnableRecord(boolean _bEnable) {
      b_record = _bEnable;
   }

   // <method_set.png>
   public =replay= method setEnableRecordFramePlay(boolean _bEnable) {
      b_record_frameplay = _bEnable;
   }

   // <method_set.png>
   public =replay= method setIODev(int _dev) {
      io_dev = _dev;
   }

   // <method_set.png>
   public =replay= method setIOCh(int _ch) {
      io_ch = _ch;
   }

   // <method_set.png>
   public =replay= method setIOEvType(int _evType) {
      io_evtype = _evType;
   }

   // <method_set.png>
   public =replay= method setIOEvCC(int _evCC) {
      io_ev_cc = _evCC;
   }

   // <method_set.png>
   public =replay= method setIOEvRPN(int _evRPN) {
      io_ev_rpn = _evRPN;
   }

   // <method_set.png>
   public =replay= method setIOEvNRPN(int _evNRPN) {
      io_ev_nrpn = _evNRPN;
   }

   // <method_set.png>
   public =replay= method setAltInDev(int _dev) {
      alt_in_dev = _dev;
   }

   // <method_set.png>
   public =replay= method setAltInCh(int _ch) {
      alt_in_ch = _ch;
   }

   // <method_set.png>
   public =replay= method setAltInEvType(int _evType) {
      alt_in_evtype = _evType;
   }

   // <method_set.png>
   public =replay= method setAltInEvCC(int _evCC) {
      alt_in_ev_cc = _evCC;
   }

   // <method_set.png>
   public =replay= method setAltInEvRPN(int _evRPN) {
      alt_in_ev_rpn = _evRPN;
   }

   // <method_set.png>
   public =replay= method setAltInEvNRPN(int _evNRPN) {
      alt_in_ev_nrpn = _evNRPN;
   }

   // <method_enum.png>
   public =replay= method enumerateLanes() {
      NodeAutoLane *lane;
      int laneIdx = 0;
      foreach lane in lanes
      {
         lane.lane_idx = laneIdx++;
      }
   }

   // <method_add.png>
   public =replay= method addLane() : NodeAutoLane {
      local NodeAutoLane lane;

      lane.init(this, lanes.numElements);

      lanes.add(#(deref lane));

      return lane;
   }

   // <method_remove.png>
   public =replay= method removeLaneByIdx(int _laneIdx) {
      if(0 <= _laneIdx < lanes.numElements)
      {
         lanes.delete(_laneIdx);

         enumerateLanes();

         if(current_lane_idx >= lanes.numElements)
         {
            current_lane_idx = lanes.numElements - 1;
         }
      }
   }

   // <method.png>
   public static MapFromMMEVType(local MIDIMapEvent _ev) : int {
      switch(_ev.type)
      {
         case MIDIMapEventType.TYPE_POLY_PRESSURE:
            return EVTYPE_POLYAT;

         case MIDIMapEventType.TYPE_CC:
            return EVTYPE_CC;

         case MIDIMapEventType.TYPE_CHANNEL_PRESSURE:
            return EVTYPE_CHAT;

         case MIDIMapEventType.TYPE_PITCHBEND:
            return EVTYPE_PITCHBEND;

         case MIDIMapEventType.TYPE_RPN:
            return EVTYPE_RPN;

         case MIDIMapEventType.TYPE_NRPN:
            return EVTYPE_NRPN;

         case MIDIMapEventType.TYPE_NOTE_ON:
         case MIDIMapEventType.TYPE_NOTE_OFF:
            return EVTYPE_NOTES;
      }
      return -1;
   }

   // <method.png>
   public method copyIOCtlNrFromMMEV(MIDIMapEvent _ev) {
      switch(io_evtype)
      {
         case EVTYPE_CC:
            io_ev_cc = _ev.ext_type;
            break;

         case NodeAutoTrack.EVTYPE_RPN:
            io_ev_rpn = _ev.ext_type;
            break;

         case NodeAutoTrack.EVTYPE_NRPN:
            io_ev_nrpn = _ev.ext_type;
            break;

         // case NodeAutoTrack.EVTYPE_POLYAT:
         // case NodeAutoTrack.EVTYPE_CHAT:
         // case NodeAutoTrack.EVTYPE_PITCHBEND:
         // case NodeAutoTrack.EVTYPE_NOTES:
      } // match ctlnr
   }

   // <save.png>
   public method trackSaveState(Stream ofs) {

      // Version
      ofs.i16 = 2;

      ofs.i8 = b_unfold;
      ofs.i8 = b_muted;
      ofs.i8 = b_record;
      ofs.i8 = b_record_frameplay; // v2+

      ofs.i32 = track_length;

      ofs.i8 = io_dev;
      ofs.i8 = io_ch;
      ofs.i8 = io_evtype;
      ofs.i8 = io_ev_cc;
      ofs.i8 = io_ev_rpn;
      ofs.i8 = io_ev_nrpn;

      ofs.i8 = b_alt_in;

      ofs.i8 = alt_in_dev;
      ofs.i8 = alt_in_ch;
      ofs.i8 = alt_in_evtype;
      ofs.i8 = alt_in_ev_cc;
      ofs.i8 = alt_in_ev_rpn;
      ofs.i8 = alt_in_ev_nrpn;

      // Save lanes
      int allLanesStartOff = ofs.offset;
      ofs.i32 = 0;

      NodeAutoLane *lane;
      foreach lane in lanes
      {
         lane.laneSaveState(ofs);
      }

      Utils.FixChunkSize(ofs, allLanesStartOff); 

      ofs.i8 = current_lane_idx;
   }

   // <load.png>
   public method trackLoadState(Stream ifs, IntArray _outDevIdxMap, IntArray _outDevUseCount) : boolean {

      short ver = ifs.u16;

      if(ver >= 1)
      {
         b_unfold = ifs.i8;
         b_muted  = ifs.i8;
         b_record = ifs.i8;

         if(ver >= 2)
         {
            b_record_frameplay = ifs.i8;
         }

         track_length = ifs.i32;

         io_dev     = ifs.s8;
         _outDevUseCount.inc(io_dev);
         io_dev = _outDevIdxMap.get(io_dev);
         
         io_ch      = ifs.i8;
         io_evtype  = ifs.i8;
         io_ev_cc   = ifs.i8;
         io_ev_rpn  = ifs.i8;
         io_ev_nrpn = ifs.i8;

         b_alt_in = ifs.i8;

         alt_in_dev     = ifs.i8;
         _outDevUseCount.inc(alt_in_dev);
         alt_in_dev = _outDevIdxMap.get(alt_in_dev);

         alt_in_ch      = ifs.i8;
         alt_in_evtype  = ifs.i8;
         alt_in_ev_cc   = ifs.i8;
         alt_in_ev_rpn  = ifs.i8;
         alt_in_ev_nrpn = ifs.i8;

         // Read lanes
         int allLanesStartOff = ifs.offset;
         int allLanesBytes = ifs.i32;
         int allLanesBytesLeft = allLanesBytes - 4;

         int laneIdx = 0;

         while(allLanesBytesLeft > 0)
         {
            int laneStartOff = ifs.offset;

            NodeAutoLane lane <= new NodeAutoLane;
         
            lane.init(this, laneIdx);
         
            if(!lane.laneLoadState(ifs, _outDevIdxMap, _outDevUseCount))
            {
               trace "[---] NodeAutoTrack::trackLoadState: failed to read lane #"+laneIdx;
               return false;
            }

            lanes.add(#(deref lane));

            int laneBytesRead = ifs.offset - laneStartOff;

            allLanesBytesLeft -= laneBytesRead;

            // Next lane
            laneIdx++;
         }

         if(0 != allLanesBytesLeft)
         {
            trace "[---] NodeAutoTrack::trackLoadState: invalid lanes read size (expected "+allLanesBytes+", have "+(ifs.offset - allLanesStartOff)+")";
            return false;
         }

         current_lane_idx = ifs.s8;

         return true;
      }

      return false;
   }

   // <replay.png>
   public method reset(boolean _bSoft) {
      frameplay_cur_val1 = NodeAuto.INVALID_VALUE;
      frameplay_cur_val2 = NodeAuto.INVALID_VALUE;

      framerec_cur_val1 = NodeAuto.INVALID_VALUE;
      framerec_cur_val2 = NodeAuto.INVALID_VALUE;

      NodeAutoLane *lane;
      foreach lane in lanes
      {
         lane.reset(_bSoft);
      }

      gated_out_frame.free();
   }

   // <replay.png>
   public method seek(int _pulses, boolean _bPatLoop) {
      NodeAutoLane *lane;
      foreach lane in lanes
      {
         lane.seek(_pulses, _bPatLoop);
      }
   }

   // <method.png>
   public =replay= method clear() {
      NodeAutoLane *lane;
      foreach lane in lanes
      {
         lane.clear();
      }
   }

   // <method.png>
   public method syncPlayPosition(NodeAutoTrack o) {
      NodeAutoLane *lane;
      int laneIdx = 0;
      foreach lane in lanes
      {
         NodeAutoLane oLane <= o.lanes.get(laneIdx);
         lane.syncPlayPosition(oLane);
         laneIdx++;
      }
   }

   // <method_handle.png>
   public method handleStartRecording() {
      NodeAutoLane *lane;
      foreach lane in lanes
      {
         lane.num_recorded = 0;
         lane.autoclear_timeout = 0;
      }
   }      

   // <replay.png>
   protected method record(float _val, boolean _bAllowRec, boolean _bFramePlay, boolean _bSec, int _note) {

      NodeAutoLane recLane <= lanes.get(current_lane_idx);

      boolean bPassThru = true;

      if(null != recLane)
      {
         if(_bAllowRec)
         {
            if(!recLane.b_locked)
            {
               bPassThru = recLane.record(_val, _note);
            }
         }

         if(!_bFramePlay || (_bFramePlay && b_record_frameplay))
         {
            recLane.last_env_value = _val;
            recLane.rec_value = _val;

            if(bPassThru)
            {
               // Pass-through
               if(_note < 0)
               {
                  tmp_note_frame.noteOff(true/*bSet*/, 0,0, -_note, _val/*vel*/);
               }
               else if(_note > 0) // 22Oct2015
               {
                  tmp_note_frame.noteOn(true/*bSet*/, 0,0, _note,
                                        _val/*vel*/,
                                        0/*dur*/);
               }
               ////recLane.rec_note = _note;
            }

            // recLane.b_force_recev = true;
            recLane.b_force_recev = bPassThru;
         }
      }

      if(_bFramePlay)
      {
         if(_bSec)
         {
            frameplay_cur_val2 = _val;
         }
         else
         {
            frameplay_cur_val1 = _val;
         }
      }
      else
      {
         if(_bSec)
         {
            framerec_cur_val2 = _val;
         }
         else
         {
            framerec_cur_val1 = _val;
         }
      }

      // Force discard to e.g. avoid double note trigger if event was rec and will soon be played
      tmp_force_discard = !bPassThru;
   }

   // <replay.png>
   protected method handleRecordA(MIDIPipeFrame _frame,
                                  boolean _bAllowRec,
                                  boolean _bFramePlay
                                  ) {
      int numEv;
      int evIdx = 0;
      MIDIPipeEvent pev;
     
      // trace "xxx processFrame record clane="+current_lane_idx;
      
      switch(io_evtype)
      {
         case EVTYPE_CC:
            numEv = _frame.numEventsCC;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_CC))
               {
                  // trace "xxx ev CC devIdx="+pev.devIdx+" ch="+pev.midiCh;
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     if(pev.ccId == io_ev_cc)
                     {
                        record(pev.ccValue, _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_RPN:
            numEv = _frame.numEventsRPN;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_RPN))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     if(pev.rpn == io_ev_rpn)
                     {
                        record(pev.rpnValue / (16383.0 / 127), _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_NRPN:
            numEv = _frame.numEventsNRPN;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_NRPN))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     if(pev.rpn == io_ev_nrpn)
                     {
                        record(pev.nrpnValue / (16383.0 / 127), _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_POLYAT:
            numEv = _frame.numEventsPolyPressure;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_POLYPRESSURE))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.polyPressureValue, _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_CHAT:
            numEv = _frame.numEventsChPressure;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_CHPRESSURE))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.chPressure, _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_PITCHBEND:
            numEv = _frame.numEventsPitchbend;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_PITCHBEND))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.pitchbend / (16383.0 / 127), _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_NOTES:
            numEv = _frame.numEventsNoteOff;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_NOTE_OFF))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.velocity, _bAllowRec, _bFramePlay, false/*bSec*/, -pev.note);
                  }
               }
               evIdx++;
            }

            numEv = _frame.numEventsNoteOn;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_NOTE_ON))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.velocity, _bAllowRec, _bFramePlay, false/*bSec*/, pev.note);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_VELOCITY:
            // ModWheel => Velocity
            // // numEv = _frame.numEventsCC;
            // // loop(numEv)
            // // {
            // //    if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_CC))
            // //    {
            // //       // trace "xxx ev CC devIdx="+pev.devIdx+" ch="+pev.midiCh;
            // //       if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
            // //       {
            // //          if(pev.ccId == 1/*modwheel*/)
            // //          {
            // //             record(pev.ccValue, _bAllowRec, _bFramePlay, false/*bSec*/, 0/*note*/);
            // //          }
            // //       }
            // //    }
            // //    evIdx++;
            // // }
            break;

      } // switch io_evtype
   }

   // <replay.png>
   protected method handleRecordB(MIDIPipeFrame _frame,
                                  boolean _bAllowRec,
                                  boolean _bFramePlay
                                  ) {
      int numEv;
      int evIdx = 0;
      MIDIPipeEvent pev;
      
      // trace "xxx processFrame record clane="+current_lane_idx;
      
      switch(alt_in_evtype)
      {
         case EVTYPE_CC:
            numEv = _frame.numEventsCC;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_CC))
               {
                  // trace "xxx ev CC devIdx="+pev.devIdx+" ch="+pev.midiCh;
                  if( (pev.devIdx == alt_in_dev) && (pev.midiCh == alt_in_ch) )
                  {
                     if(pev.ccId == alt_in_ev_cc)
                     {
                        record(pev.ccValue, _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_RPN:
            numEv = _frame.numEventsRPN;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_RPN))
               {
                  if( (pev.devIdx == alt_in_dev) && (pev.midiCh == alt_in_ch) )
                  {
                     if(pev.rpn == alt_in_ev_rpn)
                     {
                        record(pev.rpnValue / (16383.0 / 127), _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_NRPN:
            numEv = _frame.numEventsNRPN;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_NRPN))
               {
                  if( (pev.devIdx == alt_in_dev) && (pev.midiCh == alt_in_ch) )
                  {
                     if(pev.rpn == alt_in_ev_nrpn)
                     {
                        record(pev.nrpnValue / (16383.0 / 127), _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_POLYAT:
            numEv = _frame.numEventsPolyPressure;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_POLYPRESSURE))
               {
                  if( (pev.devIdx == alt_in_dev) && (pev.midiCh == alt_in_ch) )
                  {
                     record(pev.polyPressureValue, _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_CHAT:
            numEv = _frame.numEventsChPressure;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_CHPRESSURE))
               {
                  if( (pev.devIdx == alt_in_dev) && (pev.midiCh == alt_in_ch) )
                  {
                     record(pev.chPressure, _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_PITCHBEND:
            numEv = _frame.numEventsPitchbend;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_PITCHBEND))
               {
                  if( (pev.devIdx == alt_in_dev) && (pev.midiCh == alt_in_ch) )
                  {
                     record(pev.pitchbend / (16383.0 / 127), _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_NOTES:
            numEv = _frame.numEventsNoteOff;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_NOTE_OFF))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.velocity, _bAllowRec, _bFramePlay, true/*bSec*/, -pev.note);
                  }
               }
               evIdx++;
            }

            numEv = _frame.numEventsNoteOn;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_NOTE_ON))
               {
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     record(pev.velocity, _bAllowRec, _bFramePlay, true/*bSec*/, pev.note);
                  }
               }
               evIdx++;
            }
            break;

         case EVTYPE_VELOCITY:
            numEv = _frame.numEventsCC;
            loop(numEv)
            {
               if(_frame.getEventByIdx(evIdx, pev, MIDIPIPE_EVENT_TYPE_CC))
               {
                  // trace "xxx ev CC devIdx="+pev.devIdx+" ch="+pev.midiCh;
                  if( (pev.devIdx == io_dev) && (pev.midiCh == io_ch) )
                  {
                     if(pev.ccId == 1/*modwheel*/)
                     {
                        record(pev.ccValue, _bAllowRec, _bFramePlay, true/*bSec*/, 0/*note*/);
                     }
                  }
               }
               evIdx++;
            }
            break;
      } // switch alt_in_evtype
   }

   // <replay.png>
   public method processFrame(MIDIPipeFrame _framePlay,
                              MIDIPipeFrame _frameRec,
                              boolean _bMuted,
                              boolean _bPlaySeq,
                              boolean _bRecord,
                              boolean _bAdvance
                              ) {

      // if(_frameRec.numEvents > 0)
      // {
      //    trace "xxxx _frameRec.numEvents="+_frameRec.numEvents;
      // }

      _bMuted = _bMuted || b_muted;

      tmp_note_frame.empty(); // notes may be added by handleRecord*()
      tmp_force_discard = false;

      handleRecordA(_framePlay, _bRecord && b_record && b_record_frameplay/*bRecord*/, true/*bFramePlay*/);
      handleRecordA(_frameRec, _bRecord && b_record, false/*bFramePlay*/);

      handleRecordB(_framePlay, false, true/*bFramePlay*/);
      handleRecordB(_frameRec, _bRecord && b_record, false/*bFramePlay*/);

      boolean bOutput = false;

      if(_bPlaySeq)
      {
         NodeAutoLane *lane;
         float cVal;

         if(EVTYPE_VELOCITY == io_evtype)
         {
            cVal = _frameRec.getFirstNoteOnVelocity(io_dev, io_ch);

            if(cVal < 0)
            {
               cVal = _framePlay.getFirstNoteOnVelocity(io_dev, io_ch);

               if(cVal < 0)
                  cVal = 0;
            }
         }
         else
         {
            cVal = frameplay_cur_val1;
         }

         MIDIPipeFrame *fr;

         int laneIdx = 0;
         foreach lane in lanes
         {
            Integer cNoteIdx = -1;

            float nv = lane.play(_framePlay,
                                 _frameRec,
                                 _bMuted,
                                 _bAdvance,
                                 cVal,
                                 (0 == laneIdx) ? frameplay_cur_val2 : NodeAuto.INVALID_VALUE,
                                 cNoteIdx
                                 );

            if(-1 != cNoteIdx)
            {
               fr <= lane.note_data.get(cNoteIdx);
               if(null != fr)
               {
                  trace "xxx merge frame cNoteIdx="+cNoteIdx+" #on="+fr.numEventsNoteOn+" #off="+fr.numEventsNoteOff;
                  tmp_note_frame.mergeFrame(fr);
               }
            }

            cVal = nv;
            laneIdx++;
         }

         if(NodeAuto.INVALID_VALUE != nv)
         {
            MIDIPipeFrame *frOut;

            if(maybe == b_alt_in)
            {
               frOut <= gated_out_frame;
            }
            else
            {
               frOut <= _framePlay;
            }

            switch(io_evtype)
            {
               case EVTYPE_CC:
                  frOut.cc(true/*bSet*/, io_dev, io_ch, io_ev_cc, nv);
                  bOutput = true;
                  break;
               case EVTYPE_RPN:
                  frOut.rpn(true/*bSet*/, io_dev, io_ch, io_ev_rpn, nv * (16383.0/127));
                  bOutput = true;
                  break;
               case EVTYPE_NRPN:
                  frOut.nrpn(true/*bSet*/, io_dev, io_ch, io_ev_nrpn, nv * (16383.0/127));
                  bOutput = true;
                  break;
               case EVTYPE_CHAT:
                  frOut.chPressure(true/*bSet*/, io_dev, io_ch, nv);
                  bOutput = true;
                  break;
               case EVTYPE_PITCHBEND:
                  frOut.pitchbend(true/*bSet*/, io_dev, io_ch, nv * (16383.0/127));
                  bOutput = true;
                  break;
               case EVTYPE_NOTES:
                  tmp_note_frame.scaleNoteDurations(0,0,
                                                    parent_pat.pulse_ticks * parent_node.rpn_com_dur_scl,
                                                    parent_node.rpn_com_dur_off
                                                    );
                  tmp_note_frame.scaleNoteVelocities(0,0,
                                                     parent_node.rpn_com_vel_scl,
                                                     parent_node.rpn_com_vel_off
                                                     );
                  tmp_note_frame.setDevIdxAndMidiCh(io_dev, io_ch);
                  trace "xxx frOut merge frame #on="+tmp_note_frame.numEventsNoteOn+" #off="+tmp_note_frame.numEventsNoteOff;
                  frOut.mergeFrame(tmp_note_frame);
                  bOutput = true;
                  break;
               case EVTYPE_POLYAT:
                  // (todo)
                  break;
               case EVTYPE_VELOCITY:
                  frOut.updateNoteOnVelocities(io_dev, io_ch, nv);
                  bOutput = true;
                  break;
                 
            }
         } // if valid env output
         else if(EVTYPE_NOTES == io_evtype)
         {
            /*for delayed note offs*/
            if(tmp_note_frame.numEvents)
            {
               tmp_note_frame.scaleNoteDurations(0,0,
                                                 parent_pat.pulse_ticks * parent_node.rpn_com_dur_scl,
                                                 parent_node.rpn_com_dur_off
                                                 );
               tmp_note_frame.scaleNoteVelocities(0,0,
                                                  parent_node.rpn_com_vel_scl,
                                                  parent_node.rpn_com_vel_off
                                                  );
               tmp_note_frame.setDevIdxAndMidiCh(io_dev, io_ch);
               _framePlay.mergeFrame(tmp_note_frame);
            }
         }

         // Handle alt-in gate mode
         if(maybe == b_alt_in)
         {
            if(gated_out_frame.numEvents > 0)
            {
               if(_frameRec.getNumEventsNoteOnByFlt(alt_in_dev, alt_in_ch) ||
                  _framePlay.getNumEventsNoteOnByFlt(alt_in_dev, alt_in_ch)
                  )
               {
                  _framePlay.mergeFrame(gated_out_frame);
                  gated_out_frame.free();
               }
            }
         }

      } // if bPlaySeq

      // Discard framerec if new value has been output and track record is enabled
      if(bOutput || b_discard_rec || tmp_force_discard && b_record)
      {
         switch(io_evtype)
         {
            case EVTYPE_CC:
               _frameRec.deleteCCByFlt(io_dev, io_ch, io_ev_cc);
               break;
            case EVTYPE_RPN:
               _frameRec.deleteRPNByFlt(io_dev, io_ch, io_ev_rpn);
               break;
            case EVTYPE_NRPN:
               _frameRec.deleteRPNByFlt(io_dev, io_ch, io_ev_nrpn);
               break;
            case EVTYPE_CHAT:
               _frameRec.deleteChPressureByFlt(io_dev, io_ch);
               break;
            case EVTYPE_PITCHBEND:
               _frameRec.deletePitchbendByFlt(io_dev, io_ch);
               break;
            case EVTYPE_NOTES:
               _frameRec.deleteNoteOffsByFlt(io_dev, io_ch);
               _frameRec.deleteNoteOnsByFlt(io_dev, io_ch);
               break;
            case EVTYPE_POLYAT:
               break;
            case EVTYPE_VELOCITY:
               break;
         }
      }
   }
}


// <class.png>
class NodeAutoPattern : NodePattern {

   define int RECMODE_REPLACE = 0;
   define int RECMODE_OVERDUB = 1;
   // define int RECMODE_MULTI   = 2;

   NodeAuto *parent_node;

   module int sig_beats_def;   // >=1
   module int sig_notelen_def; // >=1
   module int sig_beats;   // >=1
   module int sig_notelen; // >=1

   module int num_bars; // >=1
   module int res;      // >=1   num pulses per beat
   float speed;

   // public float duration_ticks;
   public float pulse_ticks;

   public int pulse_nr;

   PointerArray tracks;  // NodeAutoTrack instances

   public int current_track_idx;

   public int rec_winsz;   // quant/clear window size (#pulses)
   public int rec_winend;  // End-of-env quantization window size (#pulses)

   public int rec_mode;  // RECMODE_xxx



   // <init.png>
   public virtual patInit(NodeAuto _parent, int _patNr) {

      NodePattern::patInit(_parent, _patNr);

      parent_node <= _parent;

      sig_beats_def   = 4;
      sig_notelen_def = 4;
      sig_beats   = sig_beats_def;
      sig_notelen = sig_notelen_def;

      num_bars    = 1;
      res         = 32;  //  (ppq=96 => 96/32 = 3 ticks per pulse)
      speed       = 1.0;

      rec_mode  = RECMODE_OVERDUB;
      rec_winsz = 1;
      rec_winend = 4;

      pulse_nr = 0;

      calcTicks();

      current_track_idx = -1;

      tracks.free();
   }

   // <ui_init.png>
   public method uiInit() {
      // NodeAutoTrack track <= addTrack();
      // track.b_unfold = true;
      // NodeAutoLane lane <= track.addLane();
      // track.current_lane_idx = 0;

      // track <= addTrack();
      // lane <= track.addLane();

      // current_track_idx = 0;
   }

   // <method_set.png>
   public =replay= method setSignature(int _beats, int _notelen) {
      sig_beats_def   = _beats;
      sig_notelen_def = _notelen;

      sig_beats   = _beats;
      sig_notelen = _notelen;

      calcTicks();

      if(replay.b_playing)
      {
         parent_node.nodeSeek(parent_node.tick_nr + parent_node.nodeGetLastPatternStartSongOffset());
      }

   }

   // <method_get.png>
   public method getPatternLengthInTicks() : int {
      return num_bars * sig_beats * res * pulse_ticks;
   }

   // <method_get.png>
   public method getTrackByIdx(int _idx) : NodeAutoTrack {
      return tracks.get(_idx);
   }

   // <method_get.png>
   public method getCurrentTrack() : NodeAutoTrack {
      return tracks.get(current_track_idx);
   }

   // <method.png>
   public method swapTracks(int _a, int _b) {
      NodeAutoTrack *track;

      // Update indices
      track <= tracks.get(_a);
      track.track_idx = _b;

      track <= tracks.get(_b);
      track.track_idx = _a;

      // Swap tracks
      tracks.swap(_a, _b);

      // Update current_track_idx
      if(current_track_idx == _a)
      {
         current_track_idx = _b;
      }
      else if(current_track_idx == _b)
      {
         current_track_idx = _a;
      }
   }

   // <method_get.png>
   public method getSigBeats() : int {
      return sig_beats_def;
   }

   // <method_get.png>
   public method getSigNoteLen() : int {
      return sig_notelen_def;
   }

   // <method_set.png>
   public =replay= method setNumBars(int _numBars) {
      num_bars = _numBars;

      calcTicks();
   }

   // <method_get.png>
   public method getNumBars() : int {
      return num_bars;
   }

   // <method_set.png>
   public =replay= method setRes(int _res) {
      res = _res;

      calcTicks();

      if(replay.b_playing)
      {
         parent_node.nodeSeek(parent_node.tick_nr + parent_node.nodeGetLastPatternStartSongOffset());
      }
   }

   // <method_get.png>
   public method getRes() : int {
      return res;
   }

   // <method_set.png>
   public =replay= method setSpeed(float _speed) {
      speed = _speed;

      calcTicks();

      if(replay.b_playing)
      {
         parent_node.nodeSeek(parent_node.tick_nr + parent_node.nodeGetLastPatternStartSongOffset());
      }
   }

   // <method_get.png>
   public method getSpeed() : float {
      return speed;
   }

   // <method_set.png>
   public =replay= method setRecMode(int _mode) {
      rec_mode = _mode;
   }

   // <method_set.png>
   public =replay= method setRecWinSz(int _winSz) {
      rec_winsz = _winSz;
   }

   // <method_set.png>
   public =replay= method setRecWinEnd(int _winEnd) {
      rec_winend = _winEnd;
   }

   // <method_handle.png>
   public virtual patHandleSongSpeedChanged() {
      calcTicks();
   }

   // <method_update.png>
   module method calcTicks() {
      int numTicksPerNote = current_song.ppq * 4;

      float numTicksPerNoteLen = (numTicksPerNote / float(sig_notelen)) / speed;

      // duration_ticks = numTicksPerNoteLen * sig_beats * num_bars;

      pulse_ticks = numTicksPerNoteLen / res;

      Global.Debug2("NodeAuto::calcTicks: numTicksPerNote="+numTicksPerNote);
      Global.Debug2("NodeAuto::calcTicks: numTicksPerNoteLen="+numTicksPerNoteLen);
      Global.Debug2("NodeAuto::calcTicks: res="+res);
      Global.Debug2("NodeAuto::calcTicks: pulse_ticks="+pulse_ticks);
      // Global.Debug2("NodeAuto::calcTicks: duration_ticks="+duration_ticks);
   }

   // <method.png>
   public method pulsesToBeats(int _pulses) : float {
      int numTicksPerNote = current_song.ppq * 4;
      float numTicksPerNoteLen = (numTicksPerNote / float(sig_notelen)) / speed;
      return (_pulses * pulse_ticks) / numTicksPerNoteLen;
   }

   // <method.png>
   public =replay= method clear() {
      NodeAutoTrack *track;
      foreach track in tracks
      {
         track.clear();
      }
   }

   // <method.png>
   protected method syncPlayPosition(NodeAutoPattern o) {

      pulse_nr = o.pulse_nr;

      // Called after pattern copy
      NodeAutoTrack *track;
      int trackIdx = 0;
      foreach track in tracks
      {
         NodeAutoTrack oTrack <= o.tracks.get(trackIdx);
         track.syncPlayPosition(oTrack);
         trackIdx++;
      }      
   }

   // <method.png>
   public =replay= virtual patCopyFrom(NodeAutoPattern o, boolean _bClear) {

      // (todo) move to Node class (generic copy)

      // Determine pattern size
      DummyStream ds;
      ds.offset = 0;
      ds.size = 0;
      o.patSaveState(ds);

      // Save to memory buffer
      Buffer b <= new Buffer;
      b.size = ds.offset;
      o.patSaveState(b);

      SongFileReader.InitIdentityOutDevIdxMap();

      patInit(parent_node, pat_nr);

      b.offset = 0;
      short ver = b.i16;
      if(!patLoadState(b,
                       ver,
                       MSongFileReader.piperoot_outdevidxmap,
                       MSongFileReader.piperoot_outdevusecount
                       ))
      {
         trace "[---] patCopyFrom: internal error: auto pattern loadState failed.";
      }
      else
      {
         if(_bClear)
         {
            clear();
         }

         // Calc ticks, ..
         reset(false/*bSoft*/);

         syncPlayPosition(o);
      }

      b <= null;
   }

   // <method_enum.png>
   public =replay= method enumerateTracks() {
      NodeAutoTrack *track;
      int trackIdx = 0;
      foreach track in tracks
      {
         track.track_idx = trackIdx++;
      }
   }

   // <method_add.png>
   public =replay= method addTrack() : NodeAutoTrack {
      local NodeAutoTrack track;

      track.init(this, tracks.numElements);

      tracks.add(#(deref track));

      return track;
   }

   // <method_remove.png>
   public =replay= method removeTrackByIdx(int _trackIdx) {
      if(0 <= _trackIdx < tracks.numElements)
      {
         tracks.delete(_trackIdx);

         enumerateTracks();

         if(current_track_idx >= tracks.numElements)
         {
            current_track_idx = tracks.numElements - 1;
         }
      }
   }

   // <method_find.png>
   public method findTrackForEvent(local MIDIMapEvent _ev) : NodeAutoTrack {

      local int devIdx = _ev.getDevIdx();
      local int evType = NodeAutoTrack.MapFromMMEVType(_ev);

      if( (-1 != devIdx) && (-1 != evType) )
      {
         local NodeAutoTrack *track;
         foreach track in tracks
         {
            if(track.io_dev == devIdx)
            {
               if(track.io_ch == _ev.dev_ch)
               {
                  if(track.io_evtype == evType)
                  {
                     switch(evType)
                     {
                        case NodeAutoTrack.EVTYPE_CC:
                           if(_ev.ext_type == track.io_ev_cc)
                           {
                              return track;
                           }
                           break;

                        case NodeAutoTrack.EVTYPE_RPN:
                           if(_ev.ext_type == track.io_ev_rpn)
                           {
                              return track;
                           }
                           break;

                        case NodeAutoTrack.EVTYPE_NRPN:
                           if(_ev.ext_type == track.io_ev_nrpn)
                           {
                              return track;
                           }
                           break;

                        case NodeAutoTrack.EVTYPE_POLYAT:
                           return track;

                        case NodeAutoTrack.EVTYPE_CHAT:
                           return track;

                        case NodeAutoTrack.EVTYPE_PITCHBEND:
                           return track;

                        case NodeAutoTrack.EVTYPE_NOTES:
                           return track;
                     } // match ctlnr
                  } // match ev.type
               } // match ch
            } // match dev
         } // foreach track
      } // if valid devIdx / evType

      // Not found
      return null;
   }

   // <method.png>
   public =replay= method clearCurrentLaneEvents() : int {

      NodeAutoTrack *track;
      NodeAutoLane *lane;

      if(parent_node.b_modall)
      {
         int r = 0;
         foreach track in tracks
         {
            lane <= track.lanes.get(track.current_lane_idx);
            if(null != lane)
            {
               r += lane.clearEvents();
            }
         }
         return r;
      }
      else
      {
         track <= tracks.get(current_track_idx);
         if(null != track)
         {
            lane <= track.lanes.get(track.current_lane_idx);
            if(null != lane)
            {
               return lane.clearEvents();
            }
         }
      }
      return 0;
   }

   // <method.png>
   public method handleClearCurrentLaneEvents() : boolean {

      // called from replay thread

      NodeAutoTrack *track;
      NodeAutoLane *lane;

      if(parent_node.b_modall)
      {
         boolean r = false;
         foreach track in tracks
         {
            lane <= track.lanes.get(track.current_lane_idx);
            if(null != lane)
            {
               lane.clearEventWindow();
               r = true;
            }
         }
         return r;
      }
      else
      {
         track <= tracks.get(current_track_idx);
         if(null != track)
         {
            lane <= track.lanes.get(track.current_lane_idx);
            if(null != lane)
            {
               lane.clearEventWindow();
               return true;
            }
         }
      }
   }

   // // <replay.png>
   // public method record(MIDIPipeFrame _frame) {
   // }

   // // <replay.png>
   // public method play(MIDIPipeFrame _frame) {
   // }

   // <save.png>
   public virtual patSaveState(Stream ofs) {

      // Version
      ofs.i16 = 4;

      NodePattern::patSaveState(ofs);

      ofs.i8 = sig_beats_def;
      ofs.i8 = sig_notelen_def;
      
      ofs.i8 = num_bars;
      ofs.i8 = res;

      ofs.f32 = speed; // v2+

      ofs.i8 = rec_winsz; // v3+
      ofs.i8 = rec_mode;  // v3+
      ofs.i8 = rec_winend; // v4+

      // Save tracks
      int allTracksStartOff = ofs.offset;
      ofs.i32 = 0;

      NodeAutoTrack *track;
      foreach track in tracks
      {
         track.trackSaveState(ofs);
      }

      Utils.FixChunkSize(ofs, allTracksStartOff);

      ofs.i8 = current_track_idx;
   }

   // <load.png>
   public virtual patLoadState(Stream ifs, short ver, IntArray _outDevIdxMap, IntArray _outDevUseCount) : boolean {

      if(!NodePattern::patLoadState(ifs, ver, _outDevIdxMap, _outDevUseCount))
      {
         return false;
      }

      sig_beats_def   = ifs.u8;
      sig_notelen_def = ifs.u8;
      
      sig_beats = sig_beats_def;
      sig_notelen = sig_notelen_def;
      
      num_bars = ifs.u8;
      res      = ifs.u8;
      
      speed = ifs.f32;

      if(ver >= 3)
      {
         rec_winsz = ifs.u8;
         rec_mode  = ifs.u8;

         if(ver >= 4)
         {
            rec_winend = ifs.u8;
         }
      }

      // Read tracks
      int allTracksStartOff = ifs.offset;
      int allTracksBytes = ifs.i32;
      int allTracksBytesLeft = allTracksBytes - 4;

      int trackIdx = 0;

      while(allTracksBytesLeft > 0)
      {
         int trackStartOff = ifs.offset;

         NodeAutoTrack track <= new NodeAutoTrack;
         
         track.init(this, trackIdx);
         
         if(!track.trackLoadState(ifs, _outDevIdxMap, _outDevUseCount))
         {
            trace "[---] NodeAutoPattern::patLoadState: failed to read track #"+trackIdx;
            return false;
         }

         tracks.add(#(deref track));

         int trackBytesRead = ifs.offset - trackStartOff;

         allTracksBytesLeft -= trackBytesRead;

         // Next track
         trackIdx++;
      }

      if(0 != allTracksBytesLeft)
      {
         trace "[---] NodeAutoPattern::patLoadState: invalid tracks read size (expected "+allTracksBytes+", have "+(ifs.offset - allTracksStartOff)+")";
         return false;
      }

      current_track_idx = ifs.s8;
      
      return true;
   }

   // <replay.png>
   public method reset(boolean _bSoft) {

      sig_beats = sig_beats_def;
      sig_notelen = sig_notelen_def;

      calcTicks();

      NodeAutoTrack *track;
      foreach track in tracks
      {
         track.reset(_bSoft);
      }
   }

   // <replay.png>
   public method seek(int _ticks, boolean _bPatLoop) {

      pulse_nr = _ticks / pulse_ticks;

      NodeAutoTrack *track;
      foreach track in tracks
      {
         track.seek(pulse_nr, _bPatLoop);
      }

      pulse_nr = pulse_nr % (num_bars * sig_beats * res);
   }

   // <method_handle.png>
   public method handleStartRecording() {
      NodeAutoTrack *track;
      foreach track in tracks
      {
         track.handleStartRecording();
      }
   }

   // <replay.png>
   public method processFrame(MIDIPipeFrame _framePlay,
                              MIDIPipeFrame _frameRec,
                              boolean _bMuted,
                              boolean _bPlaySeq,
                              boolean _bRecord,
                              boolean _bAdvance
                              ) {

      // Time signature update
      if((_framePlay.numEventsRPN > 0) || (_frameRec.numEventsRPN > 0))
      {
         int tSigBeats = parent_node.getRPN(_framePlay, _frameRec, Node.RPN_COMMON_SIG_BEATS);
         int tSigNoteLen = parent_node.getRPN(_framePlay, _frameRec, Node.RPN_COMMON_SIG_NOTELEN);

         if(-1 != tSigBeats)
         {
            sig_beats = tSigBeats;
         }

         if(-1 != tSigNoteLen)
         {
            sig_notelen = tSigNoteLen;
         }

         if( (-1 != tSigBeats) || (-1 != tSigNoteLen) )
         {
            calcTicks();

            parent_node.nodeSeek(parent_node.tick_nr + parent_node.nodeGetLastPatternStartSongOffset());
         }
      }

      if(_bRecord && parent_node.b_clear)
      {
         handleClearCurrentLaneEvents();
      }

      NodeAutoTrack *track;
      int trackNr = 0;
      foreach track in tracks
      {
         track.processFrame(_framePlay,
                            _frameRec,
                            _bMuted || (0 != (parent_node.node_track_mute_mask & (1 << trackNr))),
                            _bPlaySeq,
                            _bRecord && (parent_node.b_modall || (current_track_idx == track.track_idx)),
                            _bAdvance
                            );

         trackNr++;
      }

      if(_bAdvance)
      {
         pulse_nr++;

         // trace "xxx pulse_nr="+pulse_nr;
         
         if(pulse_nr >= (num_bars * sig_beats * res))
         {
            // Pattern loop
            Global.Debug2("auto pattern loop (after "+(num_bars * sig_beats * res)+" pulses)");
            pulse_nr = 0;
            foreach track in tracks
            {
               track.seek(pulse_nr, true/*bPatLoop*/);
            }
         }
      }

   }
}


// <class.png>
class NodeAuto : Node {

   define float INVALID_VALUE = -999999;

   // (note) auto_* fields inherited from Node

   PointerArray patterns;

   int current_play_pattern;
   int current_edit_pattern;

   static NodeAutoEditor *editor;

   public int tick_nr; // ticks relative to pattern start, never wraps around (maybe after half a year)
   public float pulse_tick_cnt;  // 0..pat.pulse_ticks

   public boolean b_record;

   public boolean b_modall;  // true=record/clear/scrub events of all current lane envelopes

   public boolean b_follow;  // true=track selection follows incoming MIDI events

   public boolean b_clear;  // true=clear rec_winsz pulses in current lane envelope

   public boolean b_have_new_recorded_event;

   public boolean b_seq_run; // true=play+advance seq


   // <init.png>
   public virtual nodeInit(MIDIPipe _pipe, MIDIPipeNode _node) : boolean {

      Node::nodeInit(_pipe, _node);

      patterns.alloc(128);
      patterns.useAll();

      auto_dev_filter_a = -1;
      auto_ch_filter_a  = 7;

      auto_dev_filter_b = -1;
      auto_ch_filter_b  = 7;

      b_modall = false;
      b_follow = true;

      return true;
   }

   // <ui_init.png>
   public virtual nodeUIInit() {
      local NodeAutoPattern pat;

      Node::nodeUIInit();

      // Allocate first pattern
      pat.patInit(this, 0);

      pat.uiInit();

      nodeSetPattern(0, deref pat);
   }

   // <method_get.png>
   public virtual nodeGetEditor() : NodeEditor {
      if(null == editor)
      {
         editor <= new NodeAutoEditor;

         if(!editor.init())
         {
            editor <= null;
            return false;
         }
      }
      return editor;
   }

   // <method_get.png>
   public virtual nodeGetEditorIfExists() : NodeEditor {
      return editor;
   }

   // <method_get.png>
   public virtual nodeGetIcon() : Icon {
      return UI.GetIcon("node_auto");
   }

   // <method.png>
   public virtual nodeGetPatternClassName() : String {
      return "NodeAutoPattern";
   }

   // <replay.png>
   public virtual nodeReset(boolean _bSoft) {

      Node::nodeReset(_bSoft);

      NodeAutoPattern pat <= nodeGetCurrentPlayPattern();

      if(null != pat)
      {
         pat.reset(_bSoft);
      }

      if(maybe == _bSoft)
      {
         return;
      }

      b_seq_run = true;

      node_track_mute_mask = 0;

      tick_nr = 0;

      pulse_tick_cnt = 0;
   }

   // <replay.png>
   public virtual nodeRestart() {
      nodeSetLastPatternStartSongOffset(current_song.song_offset);

      nodeSeek(nodeGetLastPatternStartSongOffset() + 0);
   }

   // <replay.png>
   public =replay= virtual nodeSeek(int _ticks) {

      // trace "xxx auto: nodeSeek: last_pattern_start_editor_song_offset="+last_pattern_start_editor_song_offset+" last_pattern_start_song_offset="+last_pattern_start_song_offset;

      tick_nr = nodeCalcRelativeTicks(_ticks);  // modulo by current pattern length. result is always clamped to 0.

      // // // tick_nr = _ticks - nodeGetLastPatternStartSongOffset();

      // trace "xxx auto: nodeSeek: _ticks="+_ticks+" tick_nr="+tick_nr+" last_pattern_start="+last_pattern_start_song_offset;

      // // if(tick_nr < 0)
      // //    tick_nr = 0;

      NodeAutoPattern pat <= nodeGetCurrentPlayPattern();

      if(null != pat)
      {
         pulse_tick_cnt = tick_nr % pat.pulse_ticks;

         pat.seek(tick_nr, false/*bPatLoop*/);
      }
      else
      {
         pulse_tick_cnt = 0;
      }
   }

   // <method_set.png>
   public method setHaveNewRecordedEvent() {
      b_have_new_recorded_event = true;
   }

   // <method_get.png>
   public =replay= method checkResetHaveNewRecordedEvent() : boolean {
      boolean ret = b_have_new_recorded_event;

      b_have_new_recorded_event = false;

      return ret;
   }

   // <method_set.png>
   public =replay= virtual nodeSetPattern(int _idx, NodeAutoPattern _pat) {
      patterns[_idx & 127] = deref _pat;
   }

   // <method_get.png>
   public virtual nodeGetPatternsGeneric() : PointerArray {
      return patterns;
   }

   // <method_set.png>
   public =replay= virtual nodeSetCurrentPlayPatternNr(int _patNr) {

      Node::nodeSetCurrentPlayPatternNr(_patNr);

      NodeAutoPattern pat <= nodeGetCurrentPlayPattern();

      // // int playPos;
      // // if(null != pat)
      // // {
      // //    playPos = pat.pulse_nr;
      // // }
      // // else
      // // {
      // //    playPos = -1;
      // // }

      current_play_pattern = _patNr & 127;

      pat <= nodeGetCurrentPlayPattern();
      if(null != pat)
      {
         // // if(-1 != playPos)
         // // {
         // //    pat.calcTicks();
         // //    pat.seek(playPos, false/*bPatLoop*/);
         // // }
         pat.calcTicks();
         pat.seek(tick_nr, false/*bPatLoop*/);
      }
   }

   // <method_set.png>
   public =replay= virtual nodeSetCurrentEditPatternNr(int _patNr) {
      current_edit_pattern = _patNr & 127;
   }

   // <method_get.png>
   public virtual nodeIsPatternEmpty(int _patNr) : boolean {
      return (null == patterns.get(_patNr));
   }

   // <method_get.png>
   public =replay= virtual nodeGetCurrentPlayPatternNr() : int {
      return current_play_pattern;
   }

   // <method_get.png>
   public method nodeGetCurrentPlayPatternNumTicks() : int {
      NodeAutoPattern pat <= patterns.get(current_play_pattern);
      if(null != pat)
      {
         return pat.getPatternLengthInTicks(); // do _not_ scale by rpn_com_speed_scl;
      }
      return 0;
   }

   // <method_get.png>
   public virtual nodeGetPatternNumTicks(byte _patternNr) : int {
      NodeAutoPattern pat <= patterns.get(_patternNr);
      if(null != pat)
      {
         return pat.getPatternLengthInTicks(); // do _not_ scale by rpn_com_speed_scl;
      }
      return 0;
   }

   // <method_get.png>
   public =replay= virtual nodeGetCurrentEditPatternNr() : int {
      return current_edit_pattern;
   }

   // <method_get.png>
   public virtual nodeGetPatternByIdx(int _idx) : NodeAutoPattern {
      return patterns.get(_idx);
   }

   // <method_get.png>
   public =replay= virtual nodeGetCurrentPlayPattern() : NodeAutoPattern {
      return patterns.get(current_play_pattern);
   }

   // <method_get.png>
   public =replay= virtual nodeGetCurrentEditPattern() : NodeAutoPattern {
      return patterns.get(current_edit_pattern);
   }

   // <method_get.png>
   public method findFirstUnusedPattern(int _startOff) : int {
      int patIdx = patterns.indexOfPointer(null, _startOff);

      return patIdx;
   }

   // <method.png>
   public =replay= method startRecording() {
      b_record = true;

      NodeAutoPattern pat <= nodeGetCurrentEditPattern();
      if(null != pat)
      {
         pat.handleStartRecording();
      }
   }

   // <method.png>
   public method isRecording() : boolean {
      return b_record;
   }

   // <method.png>
   public =replay= method stopRecording() {
      b_record = false;
      b_clear = false;
   }

   // <method_set.png>
   public =replay= method setEnableModAll(boolean _bEnable) {
      b_modall = _bEnable;
   }

   // <method_set.png>
   public =replay= method setEnableFollow(boolean _bEnable) {
      b_follow = _bEnable;
   }

   // <method_set.png>
   public =replay= method setEnableClearMode(boolean _bClear) {
      b_clear = _bClear;
   }

   // <save.png>
   public virtual nodeSaveState(Stream ofs) {
      ofs.i16 = 6; // Version

      Node::nodeSaveState(ofs);

      ofs.i8 = current_play_pattern;
      ofs.i8 = current_edit_pattern;

      ///// ofs.i32 = last_pattern_start_song_offset; // v2+, pre v5

      // pre v6:
      // // ofs.i8 = b_auto_filter_a;
      // // ofs.i8 = auto_dev_filter_a;
      // // ofs.i8 = auto_ch_filter_a;
      // // ofs.i8 = b_auto_filter_b;
      // // ofs.i8 = auto_dev_filter_b;
      // // ofs.i8 = auto_ch_filter_b;

      ofs.i8 = b_modall; // v3+
      ofs.i8 = b_follow; // v4+

      // Write patterns
      int patIdx = 0;
      loop(128)
      {
         NodeAutoPattern p <= patterns.get(patIdx);

         if(null != p)
         {
            p.patSaveState(ofs);
         }
         else
         {
            // Unused
            ofs.i16 = 0; // (version)
         }

         patIdx++;
      }
   }

   // <load.png>
   public virtual nodeLoadState(Stream ifs, IntArray _outDevIdxMap, IntArray _outDevUseCount) : boolean {
      short ver = ifs.i16;

      if(ver >= 1)
      {
         if(!Node::nodeLoadState(ifs, _outDevIdxMap, _outDevUseCount))
         {
            return false;
         }

         current_play_pattern = ifs.i8;
         current_edit_pattern = ifs.i8;

         if(ver >= 2)
         {
            if(ver < 5)
            {
               last_pattern_start_song_offset = ifs.i32;  // v2+, pre v5
            }
         }

         if(ver < 6)
         {
            b_auto_filter_a   = ifs.i8;
            auto_dev_filter_a = ifs.s8;
            auto_ch_filter_a  = ifs.s8;

            // trace "xxx read auto_dev_filter="+auto_dev_filter;
            // trace "xxx _outDevIdxMap="+#(_outDevIdxMap);

            if(-1 != auto_dev_filter_a)
            {
               _outDevUseCount.inc(auto_dev_filter_a);

               auto_dev_filter_a = _outDevIdxMap.get(auto_dev_filter_a);
            }

            b_auto_filter_b   = ifs.i8;
            auto_dev_filter_b = ifs.s8;
            auto_ch_filter_b  = ifs.s8;
               
            // trace "xxx read auto_dev_filter="+auto_dev_filter;
            // trace "xxx _outDevIdxMap="+#(_outDevIdxMap);
         
            if(-1 != auto_dev_filter_b)
            {
               _outDevUseCount.inc(auto_dev_filter_b);
            
               auto_dev_filter_b = _outDevIdxMap.get(auto_dev_filter_b);
            }
         }

         if(ver >= 3)
         {
            b_modall = ifs.u8;

            if(ver >= 4)
            {
               b_follow = ifs.u8;
            }
         }

         // Read patterns
         int patIdx = 0;
         loop(128)
         {
            short patVer = ifs.i16;

            // trace "xxx try read patIdx="+patIdx+" patVer="+patVer;

            if(patVer > 0)
            {
               NodeAutoPattern p <= new NodeAutoPattern;

               p.patInit(this, patIdx);

               patterns[patIdx] = deref p;

               if(!p.patLoadState(ifs, patVer, _outDevIdxMap, _outDevUseCount))
               {
                  return false;
               }
            }
            // else: unused

            patIdx++;
         }

         return true;

      } // if ver >= 1

      return false;
   }

   // <method_handle.png>
   protected method handlePrgChg(MIDIPipeFrame _frame) {
      int numPC = _frame.getNumEventsPrgChg();

      if(numPC > 0)
      {
         int pcIdx = 0;

         NodeAutoPattern *pat;

         loop(numPC)
         {
            MIDIPipeEvent ev;
            _frame.getEventByIdx(pcIdx, ev, MIDIPIPE_EVENT_TYPE_PRGCHG);

            if(b_auto_filter_a)
            {
               if( (-1 == auto_dev_filter_a) || (auto_dev_filter_a == ev.devIdx) )
               {
                  if( (-1 == auto_ch_filter_a) || (auto_ch_filter_a == ev.midiCh) )
                  {
                     nodeSetCurrentPlayPatternNr(ev.arg1);
                     ui_b_node_pattern_changed = true;
                     Events.SendPlayPat();

                     Global.Debug3("auto: changed to pattern "+current_play_pattern+" via PrgChg A");
                  }
               }
            }

            if(b_auto_filter_b)
            {
               if( (-1 == auto_dev_filter_b) || (auto_dev_filter_b == ev.devIdx) )
               {
                  if( (-1 == auto_ch_filter_b) || (auto_ch_filter_b == ev.midiCh) )
                  {
                     nodeSetCurrentPlayPatternNr(ev.arg1);
                     ui_b_node_pattern_changed = true;
                     Events.SendPlayPat();

                     Global.Debug3("auto: changed to pattern "+current_play_pattern+" via PrgChg B");
                  }
               }
            }

            pcIdx++;
         }
      }

      // Handle timeline recordable program change
      short t = getFrameRPN(_frame, RPN_COMMON_UI_PRGCHG);
      if(t >= 0)
      {
         nodeSetCurrentPlayPatternNr(t);
         ui_b_node_pattern_changed = true;
         Events.SendPlayPat();
         replay.addUIPrgChgEntry(getNodeGID(), t);
      }
   }

   // <replay.png>
   public virtual nodeProcessFrame(MIDIPipeFrame _framePlay,
                                   MIDIPipeFrame _frameRec,
                                   boolean       _bMuted,
                                   boolean       _bPlaySeq
                                   ) {

      handlePrgChg(_framePlay);
      handlePrgChg(_frameRec);

      nodeSendQueuedPrgChgOut(_framePlay);

      NodeAutoPattern pat <= patterns.get(current_play_pattern);

      if((_framePlay.numEventsRPN > 0) || (_frameRec.numEventsRPN > 0))
      {
         int t;
         
         // Reset
         t = getRPN(_framePlay, _frameRec, Node.RPN_COMMON_RESET);
         if(t >= 1)
         {
            nodeReset(true/*bSoft*/);

            //////last_pattern_start_song_offset = current_song.song_offset;
         }

         nodeParseRPN_Vel_Dur_Speed(_framePlay, _frameRec);

         t = getRPN(_framePlay, _frameRec, RPN_COMMON_RESTART);
         if(t > 0)
         {
            nodeRestart();
         }

         // Handle MUTE, MUTE_TOGGLE, MUTE_TEMP, MUTE_TEMP_TOGGLE
         nodeParseRPN_Mute(_framePlay, _frameRec);
         // // t = getRPN(_framePlay, _frameRec, RPN_COMMON_MUTE);
         // // if(-1 != t)
         // // {
         // //    track_mute_mask = t;
         // // }

         // // t = getRPN(_framePlay, _frameRec, RPN_COMMON_MUTE_TEMP);
         // // if(-1 != t)
         // // {
         // //    b_mute_temp = t;
         // //    _bMuted |= b_mute_temp;
         // // }

         t = getRPN(_framePlay, _frameRec, RPN_COMMON_RUN);
         if(-1 != t)
         {
            b_seq_run = (t > 0);
         }

         t = getRPN(_framePlay, _frameRec, RPN_COMMON_SEEK_OFFSET);
         if(-1 != t)
         {
            Global.Debug3("NodeAuto: RPN_COMMON_SEEK_OFFSET to "+t);
            nodeSeek(t);
         }

      }

      _bMuted |= nodeHandleQueuedMuteTemp();

      if(null != pat)
      {
         boolean bAdvanced = false;

         if(_bPlaySeq && b_seq_run)
         {
            pulse_tick_cnt += rpn_com_speed_scl;

            do
            {
               if(pulse_tick_cnt >= pat.pulse_ticks)
               {
                  pulse_tick_cnt -= pat.pulse_ticks;
                  // trace "xxx pulse_tick_cnt="+pulse_tick_cnt;

                  pat.processFrame(_framePlay, _frameRec, _bMuted, _bPlaySeq, b_record && !bAdvanced, true/*bAdvance*/);

                  bAdvanced = true;
               }
            }
            while(pulse_tick_cnt >= pat.pulse_ticks);
         }

         if(!bAdvanced)
         {
            pat.processFrame(_framePlay, _frameRec, _bMuted || !b_seq_run, _bPlaySeq && b_seq_run, b_record, false/*bAdvance*/);
         }
      }

      // if(_framePlay.numEventsNoteOn)
      //    trace "xxx _framePlay.numEventsNoteOn="+_framePlay.numEventsNoteOn;

      if(_bPlaySeq && b_seq_run)
      {
         tick_nr++;
      }
   }


}

