// ----
// ---- file   : linnstrument_arranger.tks
// ---- author : (c) 2018-2023 by bsp
// ---- legal  :
// ----          Permission is hereby granted, free of charge, to any person obtaining a copy
// ----          of this software and associated documentation files (the "Software"), to deal
// ----          in the Software without restriction, including without limitation the rights
// ----          to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// ----          copies of the Software, and to permit persons to whom the Software is
// ----          furnished to do so, subject to the following conditions:
// ----
// ----          The above copyright notice and this permission notice shall be included in all
// ----          copies or substantial portions of the Software.
// ----
// ----          THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// ----          IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// ----          FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// ----          AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// ----          LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// ----          OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// ----          SOFTWARE.
// ----
// ---- created: 01May2018
// ---- changed: 02May2018, 03May2018, 04May2018, 05May2018, 08May2018, 23May2018, 15Sep2018
// ----          17Sep2018, 14Oct2019, 16Oct2019, 17Oct2019, 18Oct2019, 19Oct2019, 11Nov2019
// ----          14Mar2020, 06Nov2020, 19Feb2021, 20Feb2021, 19Jun2021, 12Aug2021, 25Apr2022
// ----          09Jul2023, 02Dec2023, 03Dec2023
// ----
// ----
// ----
//
// Linnstrument Arranger utility script
//
//  (note) click "Send Configuration" to configure the Linnstrument, then
//          save the Preset to Slot#4 (global_setting_arranger+1).
//
//  (note) preset #1 is selected when the Arranger is turned off
//  (note) preset #4 is selected when it is turned on
//  (note) see global_setting_* variables below
//
//  (note) [14Mar2020]: default split point changed from 12 to 7 (more space for pattern selection)
//  (note) [14Mar2020]: changed default configuration to <play_d:8/9/10/11>
//  (note) [19Feb2021]: added scene-autosave mode; added scene copy
//  (note) [19Jun2021]: redraw all pads when track<=>node assignment has changed
//  (note) [12Aug2021]: add per-track port configuration
//
//
//  The left split is used for modulation, and also for playing TrigSeq notes
//   (When the "send notes" parameter is true (checkbox)).
//
//  The right split is used for switching patterns.
//   TIP: clicking (any) right-side pad will update the left-split track offset (tracks 1..8 or 9..16)
//
//  The right-most column is used as a toolbar. The functions are (from bottom to top):
//       Orange: Hold to discard any controller changes.
//                When collect mode is active, temporarily stop collecting new events.
//         Cyan: Hold to collect controller changes which will be sent when the pad is released again.
//        Green: Tap to switch between pattern pages 1 & 2 (11 patterns per page + the empty pattern => 23 in total)
//         Blue: Tap to switch between fader pages 1 & 2 (8 faders per page, mapped to CC#12 and 13 (effect control 1 & 2)).
//        White: Hold, then tap fader to clear its history (when cleared, fader value is not stored in scene)
//                or (in scene mode), tap a scene slot to clear the scene
//         Lime: Toggle node navigation mode. When enabled (magenta), hold-clicking a pattern takes you to its editor.
//        White: Swap pipe nodes A/B. Go to next arranger node
//       Yellow: Toggle scene selection mode. When enabled (magenta), the top two rows are used to select one of 22 scenes.
//                Each scene stores
//                 - the current pattern selection
//                 - the current (per-row) controller state generated by this script:
//                    - CC#1 : ModWheel          (left split fader)
//                    - CC#2 : BreathControl     (left split pressure)
//                    - CC#11: Expression        (left split y-pos)
//                    -        Channel Pressure  (pattern pad pressure)
//                    -        Pitchbend         (pattern pad bend)
//                    - CC#74: Brightness        (pattern pad y-pos)
//                    - CC#12: Effect Control 1  (right split fader page 1 fader)
//                    - CC#13: Effect Control 2  (right split fader page 2 fader)
//                    - CC#16: General Purpose 1 (right split fader page 1 pressure)
//                    - CC#17: General Purpose 2 (right split fader page 1 y-pos)
//                    - CC#18: General Purpose 3 (right split fader page 2 pressure)
//                    - CC#19: General Purpose 4 (right split fader page 2 y-pos)
//                    - CC#80: General Purpose 5 (ctl overview mode only: right side, row #5)
//                    - CC#81: General Purpose 6 (ctl overview mode only: right side, row #6)
//                    - CC#82: General Purpose 7 (ctl overview mode only: right side, row #7)
//                    - CC#83: General Purpose 8 (ctl overview mode only: right side, row #8)
//
//                 CCs 80..83 can only be reached by entering the controller overview mode (footswitch pedal).
//                 When the pedal is pressed, each controller of the last selected track can be edited with a fader.
//                 TIP: Hold the discard pad (bottom right) to select a track without changing a controller value.
//
//                 TIP: to save/restore ctlstate snapshots of any other MIDI controllers, use a CtlMorph node
//                       (e.g. assign it to the 2nd row from the top for easy access)
//
//                 TIP: also keep in mind that this script already gives you access to 16*8=128 different controllers
//                       which can also be rerouted by e.g. inserting a ModMatrix after this node.
//
//                 TIP: clear a fader before saving a scene to avoid controller state restores when the scene is reloaded
//
//                In order to save a scene, hold both the scene selection pad (right/top) and a scene slot pod.
//                In order to clear a scene, hold the lower white toolbar pad and tap a scene slot pad.
//                In order to copy a scene, hold a scene slot pad, then press another one.
//
//                Toggle scene-autosave mode with [SCENE]+[CLEAR] (row8+row5).
//                In scene-autosave mode, the previously selected scene will be saved before switching to another scene.
//                 TIP: save the current scene by reselecting it (in autosave-mode).
//
//                The scene selection pad turns orange(disabled) / red(enabled) while scene-autosave mode is active.
//
//                The last column of a left-hand side fader indicates whether a track has stored any scene data
//                 (white=no data, orange=pat/mw/bc/expr/..).
//
//
//
//  When the script node is muted, no event processing is taking place.
//
//  Nodes will periodically be scanned and if the number of used patterns has changed, the pad highlighting is updated.
//
//
//


// When enabled, auto-load global preset when node is muted/unmuted
//  (i.e. this is meant for quickly switching between regular linnstrument play-mode
//   and the arranger mode provided by this script)
// (note) setting #1 (0) is the bottom one !
int global_setting_default  = 3;  // 'Play-Mode' Global Setting (0..5, please edit as required)
int global_setting_arranger = 0;  // 'Arranger' Global Setting (0..5, please edit as required)

// Must match actual split point (12 or 7)
// int split_point = 12;
// int split_point = 7;
int split_point = 6;  // 6 columns on the left, 2x9+1=19 columns on the right

// Scene-select base note [default=0=C-0]
int scene_select_base_note = 0;

// Number of scene-select notes (in_sceneselect port). Max=34 (C-0..A-2)
//  (note) can be reduced in case it overlaps with a keyboard zone used for regular note input
//          (splits can also be set up in a MIDI scene)
int num_scene_select_notes = 34;

// Modulation MIDI ports (left split)
//  - note ons are converted to CC#1 (modwheel)
//  - pressure is mapped to CC#2 (breath control)
//  - y-pos is mapped to CC#11 (expression)
StringArray *left_modulation_ports;

// Note MIDI ports (left split)
//  - Note Ons are redirected to the these ports
//  - Can be used to trigger sounds in a TrigSeq instance, for example
StringArray *left_note_ports;

// (Recordable) Program Change (Pattern Select) MIDI ports (right split)
StringArray *right_prgchg_ports;

// Forced Program Change (Pattern Select) Page Offsets (right split). -1=use current patpg
IntArray *forced_patpg;

// Pattern Select mutex groups. -1=none
IntArray *mutex_groups;

// Event quantization (#16ths). 0=off, 1=1/16th, 2=2/16th, ..
IntArray *event_quantization;


// Port presets

// --------------------------------------------
// 16trk         (8xdrums+keys+bass+chords+lead) (starting at internal channel 8)
// --------------------------------------------
StringArray left_modulation_ports_preset_1 = [
   // dev       ch
   "<play_d>",  "1",  // bottom row (#1)
   "<play_d>",  "2",
   "<play_d>",  "3",
   "<play_d>",  "4",
   "<play_d>",  "5",
   "<play_d>",  "6",
   "<play_d>",  "7",
   "<play_d>",  "8",
   "<play_a>",  "1",  // bottom row (right side)
   "<play_a>",  "2",
   "<play_b>",  "1",
   "<play_b>",  "2",
   "<play_c>",  "1",
   "<play_c>",  "2",
   "<play_e>",  "1",
   "<play_e>",  "2"   // top row (#8)
                                  ];
StringArray left_modulation_ports_preset_2 = left_modulation_ports_preset_1;
StringArray left_modulation_ports_preset_3 = left_modulation_ports_preset_1;
StringArray left_modulation_ports_preset_4 = left_modulation_ports_preset_1;
StringArray left_modulation_ports_preset_5 = left_modulation_ports_preset_1;
StringArray left_modulation_ports_preset_6 = left_modulation_ports_preset_1;
StringArray left_modulation_ports_preset_7 = left_modulation_ports_preset_1;
StringArray left_modulation_ports_preset_8 = left_modulation_ports_preset_1;

StringArray left_note_ports_preset_1 = [
   // dev   ch
   "<play_d>",   "1",  // bottom row (#1)
   "<play_d>",   "2",
   "<play_d>",   "3",
   "<play_d>",   "4",
   "<play_d>",   "5",
   "<play_d>",   "6",
   "<play_d>",   "7",
   "<play_d>",   "8",
   "<play_a>",   "1",
   "<play_a>",   "2",
   "<play_b>",   "1",
   "<play_b>",   "2",
   "<play_c>",   "1",
   "<play_c>",   "2",
   "<play_e>",   "1",
   "<play_e>",   "2"   // top row (#8)
                                  ];

StringArray left_note_ports_preset_2 = left_note_ports_preset_1;
StringArray left_note_ports_preset_3 = left_note_ports_preset_1;
StringArray left_note_ports_preset_4 = left_note_ports_preset_1;
StringArray left_note_ports_preset_5 = left_note_ports_preset_1;
StringArray left_note_ports_preset_6 = left_note_ports_preset_1;
StringArray left_note_ports_preset_7 = left_note_ports_preset_1;
StringArray left_note_ports_preset_8 = left_note_ports_preset_1;


StringArray right_prgchg_ports_preset_1 = [
   // dev   ch
   "<play_d>",   "1",  // bottom row (#1)
   "<play_d>",   "2",
   "<play_d>",   "3",
   "<play_d>",   "4",
   "<play_d>",   "5",
   "<play_d>",   "6",
   "<play_d>",   "7",
   "<play_d>",   "8",
   "<play_a>",   "1",
   "<play_a>",   "2",
   "<play_b>",   "1",
   "<play_b>",   "2",
   "<play_c>",   "1",
   "<play_c>",   "2",
   "<play_e>",   "1",
   "<play_e>",   "2"   // top row (#8)
                                  ];

StringArray right_prgchg_ports_preset_2 = right_prgchg_ports_preset_1;
StringArray right_prgchg_ports_preset_3 = right_prgchg_ports_preset_1;
StringArray right_prgchg_ports_preset_4 = right_prgchg_ports_preset_1;
StringArray right_prgchg_ports_preset_5 = right_prgchg_ports_preset_1;
StringArray right_prgchg_ports_preset_6 = right_prgchg_ports_preset_1;
StringArray right_prgchg_ports_preset_7 = right_prgchg_ports_preset_1;
StringArray right_prgchg_ports_preset_8 = right_prgchg_ports_preset_1;


IntArray forced_patpg_preset_1 = [
   -1,  // bottom row (#1)
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,  // top row (#8)
                                    ];

IntArray forced_patpg_preset_2 = forced_patpg_preset_1;
IntArray forced_patpg_preset_3 = forced_patpg_preset_1;
IntArray forced_patpg_preset_4 = forced_patpg_preset_1;
IntArray forced_patpg_preset_5 = forced_patpg_preset_1;
IntArray forced_patpg_preset_6 = forced_patpg_preset_1;
IntArray forced_patpg_preset_7 = forced_patpg_preset_1;
IntArray forced_patpg_preset_8 = forced_patpg_preset_1;


IntArray mutex_groups_preset_1 = [
   -1,  // bottom row (#1)
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,
   -1,  // top row (#8)
                                    ];

IntArray mutex_groups_preset_2 = mutex_groups_preset_1;
IntArray mutex_groups_preset_3 = mutex_groups_preset_1;
IntArray mutex_groups_preset_4 = mutex_groups_preset_1;
IntArray mutex_groups_preset_5 = mutex_groups_preset_1;
IntArray mutex_groups_preset_6 = mutex_groups_preset_1;
IntArray mutex_groups_preset_7 = mutex_groups_preset_1;
IntArray mutex_groups_preset_8 = mutex_groups_preset_1;

IntArray event_quantization_preset_1 = [
   0,  // bottom row (#1)
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,  // top row (#8)
                                    ];

IntArray event_quantization_preset_2 = event_quantization_preset_1;
IntArray event_quantization_preset_3 = event_quantization_preset_1;
IntArray event_quantization_preset_4 = event_quantization_preset_1;
IntArray event_quantization_preset_5 = event_quantization_preset_1;
IntArray event_quantization_preset_6 = event_quantization_preset_1;
IntArray event_quantization_preset_7 = event_quantization_preset_1;
IntArray event_quantization_preset_8 = event_quantization_preset_1;


StringArray preset_names = [
   "Preset 1",
   "Preset 2",
   "Preset 3",
   "Preset 4",
   "Preset 5",
   "Preset 6",
   "Preset 7",
   "Preset 8",
                            ];

PointerArray left_modulation_ports_presets <= [
   left_modulation_ports_preset_1,
   left_modulation_ports_preset_2,
   left_modulation_ports_preset_3,
   left_modulation_ports_preset_4,
   left_modulation_ports_preset_5,
   left_modulation_ports_preset_6,
   left_modulation_ports_preset_7,
   left_modulation_ports_preset_8,
                                               ];


PointerArray left_note_ports_presets <= [
   left_note_ports_preset_1,
   left_note_ports_preset_2,
   left_note_ports_preset_3,
   left_note_ports_preset_4,
   left_note_ports_preset_5,
   left_note_ports_preset_6,
   left_note_ports_preset_7,
   left_note_ports_preset_8,
                                         ];

PointerArray right_prgchg_ports_presets <= [
   right_prgchg_ports_preset_1,
   right_prgchg_ports_preset_2,
   right_prgchg_ports_preset_3,
   right_prgchg_ports_preset_4,
   right_prgchg_ports_preset_5,
   right_prgchg_ports_preset_6,
   right_prgchg_ports_preset_7,
   right_prgchg_ports_preset_8,
                                            ];

PointerArray forced_patpg_presets <= [
   forced_patpg_preset_1,
   forced_patpg_preset_2,
   forced_patpg_preset_3,
   forced_patpg_preset_4,
   forced_patpg_preset_5,
   forced_patpg_preset_6,
   forced_patpg_preset_7,
   forced_patpg_preset_8,
                                            ];

PointerArray mutex_groups_presets <= [
   mutex_groups_preset_1,
   mutex_groups_preset_2,
   mutex_groups_preset_3,
   mutex_groups_preset_4,
   mutex_groups_preset_5,
   mutex_groups_preset_6,
   mutex_groups_preset_7,
   mutex_groups_preset_8,
                                            ];

PointerArray event_quantization_presets <= [
   event_quantization_preset_1,
   event_quantization_preset_2,
   event_quantization_preset_3,
   event_quantization_preset_4,
   event_quantization_preset_5,
   event_quantization_preset_6,
   event_quantization_preset_7,
   event_quantization_preset_8,
                                            ];

left_modulation_ports <= left_modulation_ports_preset_1;
left_note_ports       <=       left_note_ports_preset_1;
right_prgchg_ports    <=    right_prgchg_ports_preset_1;
forced_patpg          <=          forced_patpg_preset_1;
mutex_groups          <=          mutex_groups_preset_1;
event_quantization    <=    event_quantization_preset_1;

int prgchg_midi_base_note = STConfig.node_trig_seq_rec_base_note;  // C-3 (36) by default
// int prgchg_midi_base_note = STConfig.patternmgr_midi_base_note;  // C-4 (48) by default
// int prgchg_midi_base_note = STConfig.arranger_trigger_base_note;  // C-3 (36) by default

int pattern_rescan_interval = 300;  // in milliseconds

// ----------------- end of configuration --------------
boolean b_debug = false;
// boolean b_debug = true;

// (note) these tagged settings are saved per pattern
tag boolean b_autoload_global_setting = true;
tag int     p_send_notes = false;
tag int     p_send_cc_midimap = false;
tag int     p_send_notes_midimap = false;
tag boolean b_left_send_zy = true;
tag boolean b_right_send_zy = true;
tag boolean b_right_send_pat_xyz = true;
tag boolean b_right_footswitch_send_rpn_ctl = false;  // true=send RPN 100..107 instead of GP1..8 in footswitch mode
tag boolean b_invert_footswitch_polarity;
tag boolean b_footswitch_mode;  // 1=toggle, 0=momentary
tag int     p_loadsave_scene_mode = 2;  // 0=pattern, 1=controllers, 2=pattern+controllers
tag int     p_preset = 0;   // 0..4 => A..E
tag boolean b_scene_autosave = true;

StringArray loadsave_scene_mode_names = [
   "Patterns only",
   "Controllers only",
   "Patterns + Controllers"
                                         ];

int tick_nr;

boolean b_reset;

int COLOR_OFF     = 0;
int COLOR_RED     = 1;
int COLOR_YELLOW  = 2;
int COLOR_GREEN   = 3;
int COLOR_CYAN    = 4;
int COLOR_BLUE    = 5;
int COLOR_MAGENTA = 6;
int COLOR_BLACK   = 7;
int COLOR_WHITE   = 8;
int COLOR_ORANGE  = 9;  // flickers in low power mode
int COLOR_LIME    = 10;
int COLOR_PINK    = 11;

int CC_COLUMN_COORD  = 20;
int CC_ROW_COORD     = 21;
int CC_COLOR         = 22;

boolean b_last_mute_state;

boolean b_usermode = false;
boolean b_last_usermode_state = false;

IntArray row_colors = [
   //                 modulation  prgchg/patternselect
   COLOR_GREEN,    // <vst_d:1> / <d:1>
   COLOR_CYAN,     // <vst_d:2> / <d:2>
   COLOR_WHITE,    // <vst_d:3> / <d:3>
   COLOR_MAGENTA,  // <vst_d:4> / <d:4>

   COLOR_BLUE,     // <vst_b:1> / <b:1>
   COLOR_YELLOW,   // <vst_a:1> / <a:1>
   COLOR_PINK,     // <vst_a:2> / <a:9>
   COLOR_LIME      // <vst_c:1> / <c:1>
                            ];

int empty_pattern_color = COLOR_BLACK;
int selected_pattern_color = COLOR_RED;

IntArray last_selected_pattern_nrs;  // indexed by track (1..16)
IntArray last_num_used_patterns;  // indexed by track (1..16)
IntArray last_note_offsets;  // indexed by midiCh (1..8)

int last_pattern_rescan_ms;

int navigation_mode_off_color = COLOR_LIME;
int navigation_mode_on_color  = COLOR_MAGENTA;
boolean b_navigation_mode;

int scene_mode_off_color = COLOR_YELLOW;
int scene_mode_on_color  = COLOR_MAGENTA;
int scene_mode_off_autosave_color = COLOR_ORANGE;
int scene_mode_on_autosave_color  = COLOR_RED;
boolean b_scene;
boolean b_scene_pressed;
int scene_pressed_ms;
IntArray scene_colors = [
   COLOR_YELLOW,
   COLOR_GREEN,
   COLOR_CYAN,
   COLOR_ORANGE,
   COLOR_LIME
                         ];

IntArray toolbar_colors = [
   COLOR_ORANGE,              // 0: discardctl  (bottom row)
   COLOR_CYAN,                // 1: discardctl_collect
   COLOR_GREEN,               // 2: pattern page 1/2
   COLOR_BLUE,                // 3: fader page 1/2
   COLOR_WHITE,               // 4: clear fader history
   navigation_mode_off_color, // 5: navigation mode
   COLOR_WHITE,               // 6: Swap pipenodes A/B. (Go to next arranger)
   scene_mode_off_color       // 7: scene menu  (top row)
                           ];

int fader_overview_row_mode_off_color = COLOR_YELLOW;
int fader_overview_row_mode_on_color  = COLOR_MAGENTA;
boolean b_fader_overview_row_mode;

IntArray toolbar_colors_fader_overview = [
   COLOR_ORANGE,  // 0: discardctl  (bottom row)
   COLOR_CYAN,    // 1: discardctl_collect
   COLOR_BLACK,   // 2: -
   COLOR_BLACK,   // 3: -
   COLOR_WHITE,   // 4: clear fader history
   COLOR_BLACK,   // 5: -
   COLOR_BLACK,   // 6: -
   fader_overview_row_mode_off_color   // 7: track menu (top row)
                           ];

boolean b_discardctl;
boolean b_discardctl_collect;
boolean b_discardctl_collect_discard;  // true=temporarily stop collection
boolean b_discardctl_merge;
MIDIPipeFrame discardctl_collect_frame;

int current_pattern_page = 0;
int current_fader_page = -1;

int left_fader_track_offset = 0;  // offset of left-hand side faders (0=tracks 1..8, 8=tracks 9..16)

int CTL_PAT   =  0;  // pattern nr               (tap pattern pad)
int CTL_MW    =  1;  // CC#1 : ModWheel          (left fader)
int CTL_BC    =  2;  // CC#2 : BreathControl     (left pressure)
int CTL_EXPR  =  3;  // CC#11: Expression        (left y-pos)
int CTL_AT    =  4;  // Channel Pressure         (right pattern pad pressure)
int CTL_PB    =  5;  // Pitchbend                (right pattern pad bend)
int CTL_BRITE =  6;  // CC#74: Brightness        (right pattern pad y-pos)
int CTL_FX1   =  7;  // CC#12: Effect Control 1  (right fader page 1 fader)
int CTL_FX2   =  8;  // CC#13: Effect Control 2  (right fader page 2 fader)
int CTL_GP1   =  9;  // CC#16: General Purpose 1 (right fader page 1 pressure)
int CTL_GP2   = 10;  // CC#17: General Purpose 2 (right fader page 1 y-pos)
int CTL_GP3   = 11;  // CC#18: General Purpose 3 (right fader page 2 pressure)
int CTL_GP4   = 12;  // CC#19: General Purpose 4 (right fader page 2 y-pos)
int CTL_GP5   = 13;  // CC#80: General Purpose 5 (ctl overview mode only: right side, row #5)
int CTL_GP6   = 14;  // CC#81: General Purpose 6 (ctl overview mode only: right side, row #6)
int CTL_GP7   = 15;  // CC#82: General Purpose 7 (ctl overview mode only: right side, row #7)
int CTL_GP8   = 16;  // CC#83: General Purpose 8 (ctl overview mode only: right side, row #8)
int CTL_NUM   = 17;

// Controller destinations
IntArray ctl_ccs = [-999,    //  0: patnr
                    1,       //  1: modwheel
                    2,       //  2: breath control
                    11,      //  3: expression
                    -CTL_AT, //  4: channel pressure
                    -CTL_PB, //  5: pitchbend
                    74,      //  6: brightness
                    12,      //  7: effect control 1
                    13,      //  8: effect control 2
                    16,      //  9: general purpose 1
                    17,      // 10: general purpose 2
                    18,      // 11: general purpose 3
                    19,      // 12: general purpose 4
                    80,      // 13: general purpose 5
                    81,      // 14: general purpose 6
                    82,      // 15: general purpose 7
                    83       // 16: general purpose 8
                    ];

StringArray ctl_names = ["CTL_PAT",
                         "CTL_MW",
                         "CTL_BC",
                         "CTL_EXPR",
                         "CTL_AT",
                         "CTL_PB",
                         "CTL_BRITE",
                         "CTL_FX1",
                         "CTL_FX2",
                         "CTL_GP1",
                         "CTL_GP2",
                         "CTL_GP3",
                         "CTL_GP4",
                         "CTL_GP5",
                         "CTL_GP6",
                         "CTL_GP7",
                         "CTL_GP8"
                         ];


PointerArray ctl_states;  // 8x IntArray

PointerArray scene_ctl_states;  // 34xPointerArray 8x IntArray
int last_selected_scene_idx = -1;
int copy_src_scene_idx = -1;
int copy_src_scene_ms = 0;
int COPY_SRC_SCENE_THRESHOLD_MS = 500;

IntArray pad_colors; // 25*8
IntArray pad_colors_remote;

boolean b_clear_modifier;  // true=clear modifier pad is currently held down

int globalsettings_grace_ms;  // after loading global settings, CC#74 is sometimes reset to 0 (fw issue)

boolean b_footswitch;  // true=footswitch (fader overview mode) active

int last_selected_track_idx;

boolean b_update_pad_colors;  // true= pad color update pending

ui::XMLForm *ui_xfm;

int NUM_TRACKS = 16;

IntArray last_seen_node_gids;  // indexed by trackIdx

MIDIPipeFrame *[] future_frames;  // delayed / quantized frames. fr.timestamp stores target tick_nr. cleared on reset.


function SelectPreset_replay(int _preset) {
   // trace "xxx linnstrument:SelectPreset_replay("+_preset+")";
   if(_preset >= 0)
   {
      left_modulation_ports <= left_modulation_ports_presets.get(_preset);
      left_note_ports       <= left_note_ports_presets      .get(_preset);
      right_prgchg_ports    <= right_prgchg_ports_presets   .get(_preset);
      forced_patpg          <= forced_patpg_presets         .get(_preset);
      mutex_groups          <= mutex_groups_presets         .get(_preset);
      event_quantization    <= event_quantization_presets   .get(_preset);
   }
}

function OnReset() {
   // trace "[trc] linnstrument:OnReset: current_song="+@(st2::current_song);
   b_reset = true;

   future_frames.free();

   if(NUM_TRACKS != last_selected_pattern_nrs.numElements)
   {
      last_selected_pattern_nrs.allocAndFill(NUM_TRACKS, -1);
      last_num_used_patterns.allocAndFill(NUM_TRACKS, 0);
      last_note_offsets.allocAndFill(16, -1);

      IntArray *ctlState;
      loop(NUM_TRACKS)
      {
         ctlState <= new IntArray;
         ctlState.allocAndFill(CTL_NUM, -1);
         ctl_states.add(#(deref ctlState));
      }

      // (note) when the split point is set to 12, 22 scenes are available
      // (note) when the split point is moved to 7, 32 scenes will be available
      // (note) when the split point is moved to 6, all 34 scenes will be available
      loop(34/*NUM_SCENES*/)
      {
         PointerArray sceneCtls <= new PointerArray;
         loop(NUM_TRACKS)
         {
            ctlState <= new IntArray;
            ctlState.allocAndFill(CTL_NUM, -1);
            sceneCtls.add(#(deref ctlState));
         }
         scene_ctl_states.add(#(deref sceneCtls));
      }

      pad_colors.allocAndFill(25 * 8, -1);
      pad_colors_remote.allocAndFill(25 * 8, -1);
      b_update_pad_colors = true;

      last_seen_node_gids.allocAndFill(NUM_TRACKS, -1);
   }

   SelectPreset_replay(p_preset); // Reselect current preset (no UI update)

   trace "[dbg] Script<linnstrument_arranger>: prgchg_midi_base_note="+prgchg_midi_base_note;
}

function OnSongSelect() {
   // called after sub-song selection has changed => recolorize pads
   trace "[trc] linnstrument:onSongSelect: update colorization. current_song="+@(st2::current_song);
   pad_colors_remote.allocAndFill(25 * 8, -1);
   b_update_pad_colors = true;
   last_seen_node_gids.allocAndFill(NUM_TRACKS, -1);
}

function OnSeek(int ticks) {
   tick_nr = ticks;
}

function GetOrCreateFutureFrame(int _tickNr) {
   int i = 0;
   MIDIPipeFrame *fr;
   for(;;)
   {
      if(i < future_frames.numElements)
      {
         fr <= future_frames.get(i);
         if(fr.timeStamp == _tickNr)
            return fr;
         else if(fr.timeStamp > _tickNr)
         {
            // Insert before i
            fr <= new MIDIPipeFrame;
            fr.timeStamp = _tickNr;
            future_frames.insert(i, #(deref fr));
            return fr;
         }
         else // if(fr.timeStamp < _tickNr)
            i++;
      }
      else
      {
         // Add first
         fr <= new MIDIPipeFrame;
         fr.timeStamp = _tickNr;
         future_frames.add(#(deref fr));
         return fr;
      }
   }
}

function MergeFutureFrame(MIDIPipeFrame _frameOut) {
   MIDIPipeFrame fr <= future_frames.first;
   if(null != fr && fr.timeStamp == tick_nr)
   {
      _frameOut.mergeFrame(fr);
      future_frames.delete(0);
   }
}

function SetPadColor(int _colIdx, int _rowIdx, byte _color) {
   pad_colors[_colIdx + _rowIdx*25] = _color;
}

function UpdatePadColors(boolean _bProgressive) {

   STX_MIDIOutDevice dev <= MIDI.GetMIDIOutDeviceByIndex(linn_dev);

   if(null != dev)
   {
      int rowIdx = 0;
      int k = 0;
      int numSent = 0;
      loop(8)
      {
         dev.ctlChange(linn_ch, CC_ROW_COORD, rowIdx);
         int colIdx = 0;
         loop(25)
         {
            int color = pad_colors[k];
            if(-1 != color)
            {
               if(color != pad_colors_remote[k])
               {
                  pad_colors_remote[k] = color;

                  // (note) column 0 are the preset/volume/transpose/.. buttons
                  dev.ctlChange(linn_ch, CC_COLUMN_COORD, colIdx + 1);
                  dev.ctlChange(linn_ch, CC_COLOR, color);

                  numSent++;
                  if(_bProgressive && numSent > 10)
                     return;
               }
            }
            colIdx++;
            k++;
         }
         rowIdx++;
      }

      b_update_pad_colors = false;

      if(b_debug) trace "[dbg] Script<linnstrument_arranger>: updated "+numSent+" pad(s)";
   }
}

function SendNRPN(STX_MIDIOutDevice _dev, short _nrpn, short _value) {
   // used by SendConfiguration()
   _dev.ctlChange(linn_ch, 99, (_nrpn >> 7) & 16383); // NRPN MSB
   _dev.ctlChange(linn_ch, 98, (_nrpn & 16383));      // NRPN LSB
   _dev.ctlChange(linn_ch, 6,    0);                  // NRPN value MSB
   _dev.ctlChange(linn_ch, 6+32, _value);             // NRPN value LSB
   _dev.ctlChange(linn_ch, 101, 127);                 // RPN parameter number reset MSB
   _dev.ctlChange(linn_ch, 100, 127);                 // RPN parameter number reset LSB
   _dev.send(0);
   TKS.sleep(20);
}

function GetPrgChgNodeDevIdxCh(byte _trackIdx, Integer _retDevIdxOrNull, Byte _retMidiCh) {
   // trace "xxx GetPrgChgNodeDevIdxCh: trackIdx="+_trackIdx;
   // trace "xxx GetPrgChgNodeDevIdxCh: right_prgchg_ports="+#(right_prgchg_ports);
   String pcDevName <= right_prgchg_ports.get(_trackIdx*2 + 0);
   // if(b_debug) trace "xxx row["+_trackIdx+"] pcDevName=\""+pcDevName+"\"";
   if(null != pcDevName)
   {
      String pcMidiCh <= right_prgchg_ports.get(_trackIdx*2 + 1);
      // if(b_debug) trace "xxx row["+_trackIdx+"] pcMidiCh=\""+pcMidiCh+"\"";
      if(null != pcMidiCh)
      {
         STX_MIDIOutDevice pcDev <= MIDI.GetMIDIOutDeviceByAliasOrName(pcDevName);
         // if(b_debug) trace "xxx row["+_trackIdx+"] pcDev=\""+#(pcDev)+"\"";
         if(null != pcDev)
         {
            _retMidiCh = int(pcMidiCh) - 1;
            Node pcNode <= Node.FindFirstNodeThatListensTo(pcDev.dev_idx, _retMidiCh);
            // if(b_debug) trace "xxx row["+_trackIdx+"] pcNode=\""+#(pcNode)+"\"";
            if(null != pcNode)
            {
               if(null != _retDevIdxOrNull)
                  _retDevIdxOrNull = pcDev.dev_idx;
               return pcNode;
            }
         }
      }
   }
   return null;
}

function GetModDevIdxCh(byte _trackIdx, Byte _retMidiCh) {
   String devName <= left_modulation_ports.get(_trackIdx*2 + 0);
   if(null != devName)
   {
      String midiCh <= left_modulation_ports.get(_trackIdx*2 + 1);
      if(null != midiCh)
      {
         STX_MIDIOutDevice dev <= MIDI.GetMIDIOutDeviceByAliasOrName(devName);
         if(null != dev)
         {
            _retMidiCh = int(midiCh) - 1;
            return dev.dev_idx;
         }
      }
   }
   return -1;
}

function GetNoteDevIdxCh(byte _trackIdx, Byte _retMidiCh) {
   String devName <= left_note_ports.get(_trackIdx*2 + 0);
   if(null != devName)
   {
      String midiCh <= left_note_ports.get(_trackIdx*2 + 1);
      if(null != midiCh)
      {
         STX_MIDIOutDevice dev <= MIDI.GetMIDIOutDeviceByAliasOrName(devName);
         if(null != dev)
         {
            _retMidiCh = int(midiCh) - 1;
            return dev.dev_idx;
         }
      }
   }
   return -1;
}

function CopyScene(PointerArray _d, PointerArray _s, int _mode) {
   int trackIdx = 0;

   loop(NUM_TRACKS)
   {
      IntArray ctlD <= _d.get(trackIdx);
      IntArray ctlS <= _s.get(trackIdx);

      if(_mode >= 1)
      {
         int ctlIdx = (1 == _mode);
         loop(CTL_NUM - ctlIdx)
         {
            ctlD[ctlIdx] = ctlS[ctlIdx];
            ctlIdx++;
         }
      }
      else
      {
         ctlD[CTL_PAT] = ctlS[CTL_PAT];
      }

      trackIdx++;
   }
}

function ClearScene(int _idx) {
   PointerArray sceneCtls <= scene_ctl_states.get(_idx);
   if(null != sceneCtls)
   {
      IntArray *ctlState;
      foreach ctlState in sceneCtls
      {
         ctlState.fill(-1);
      }

      Global.PrintFromReplay("Clear scene #"+(_idx+1));
   }
}

function IsSceneEmpty(int _idx) {
   // (note) empty scenes are colored blue
   PointerArray sceneCtls <= scene_ctl_states.get(_idx);
   if(null != sceneCtls)
   {
      IntArray *ctlState;
      foreach ctlState in sceneCtls
      {
         int ctlVal;
         foreach ctlVal in ctlState
         {
            if(-1 != ctlVal)
               return false;
         }
      }
   }
   return true;
}

function SaveScene(int _idx, boolean _bCopy) {
   trace "[trc] linnstrument: SaveScene("+_idx+") bCopy="+_bCopy;
   PointerArray sceneCtls <= scene_ctl_states.get(_idx);
   if(null != sceneCtls)
   {
      int loadSaveMode = b_discardctl ? b_discardctl_collect ? 1/*controllers only*/ : 0/*patterns only*/ : p_loadsave_scene_mode;
      CopyScene(sceneCtls, ctl_states, loadSaveMode);

      if(_bCopy)
      {
         Global.PrintFromReplay("Copy to scene #"+(_idx+1)+" ("+loadsave_scene_mode_names.get(loadSaveMode)+")");
      }
      else
      {
         if(b_scene_autosave)
            Global.PrintFromReplay("Save scene #"+(_idx+1)+" ("+loadsave_scene_mode_names.get(loadSaveMode)+")");
         else
            Global.PrintFromReplay("Save scene #"+(_idx+1)+" ("+loadsave_scene_mode_names.get(loadSaveMode)+")");
      }
      last_selected_scene_idx = _idx;
   }
}

function ColorizePads_Default() {
   // (note) can't use the MIDIPipeFrame API here since the same CCs are set multiple times
   if(b_debug) trace "[dbg] Script<linnstrument_arranger>: ColorizePads_Default() dev="+linn_dev+" ch="+linn_ch;
   if(1)
   {
      // Left
      int rowIdx = 0;
      loop(8)
      {
         int colIdx = 0;
         loop(25)
         {
            SetPadColor(colIdx, rowIdx, COLOR_OFF);

            // Next column
            colIdx++;
         }

         // Next row
         rowIdx++;
      }
   }
}

function DrawCCFader(int _rowIdx, int _colIdx, byte _ccVal, int _numCels, byte _color, boolean _bHistory) {
   float valPerCell = 127.0 / _numCels;
   int numX = ((_ccVal / valPerCell)+0.5);
   if(numX > _numCels)
      numX = _numCels;
   // trace "xxx ccVal="+_ccVal+" numX="+numX;
   // trace "xxx DrawCCFader: yx=("+_rowIdx+";"+_colIdx+") ccVal="+_ccVal+" numCels="+_numCels+" color="+_color;

   int colIdx = _colIdx;
   loop(numX)
   {
      SetPadColor(colIdx++, _rowIdx, _color);
   }

   loop(_numCels - numX)
   {
      if(colIdx == (_colIdx + _numCels - 1))
      {
         SetPadColor(colIdx++, _rowIdx, _bHistory ? COLOR_ORANGE : COLOR_WHITE);
      }
      else
      {
         SetPadColor(colIdx++, _rowIdx, COLOR_BLACK);
      }
   }
}

function ColorizePads(int _fltTrackIdx, boolean _bUpdateLastSelected) {
   // (note) 'fltTrackIdx' is interpreted as rowIdx in (single-track) footswitch mode
   // (note) can't use the MIDIPipeFrame API here since the same CCs are set multiple times

   if(b_debug) trace "[dbg] Script<linnstrument_arranger>: ColorizePads() dev="+linn_dev+" ch="+linn_ch;
   if(1)
   {
      int trackIdx = 0;
      byte ccFaderCol;
      boolean bFaderHistory;
      int trackW = (25 - split_point - 1) / 2;  // == 9 columns/pads at split_point=6
      loop(NUM_TRACKS)
      {
         if( (-1 == _fltTrackIdx) || (trackIdx == _fltTrackIdx) )
         {
            int colOffset = (trackIdx >= 8) ? trackW : 0;
            int rowIdx = (trackIdx & 7);
            int colIdx = 0;
            IntArray ctlState <= ctl_states.get(trackIdx);

            if(!b_footswitch)
            {
               // Left
               bFaderHistory =
                  (-1 != ctlState.get(CTL_PAT))   ||   // [19Feb2021]
                  (-1 != ctlState.get(CTL_MW))    ||
                  (-1 != ctlState.get(CTL_BC))    ||
                  (-1 != ctlState.get(CTL_EXPR))  ||
                  (-1 != ctlState.get(CTL_AT))    ||
                  (-1 != ctlState.get(CTL_PB))    ||
                  (-1 != ctlState.get(CTL_BRITE))  ;

               if((0 == left_fader_track_offset) ^ (trackIdx >= 8))
               {
                  // trace "xxx left["+rowIdx+"]: bFaderHistory="+bFaderHistory+" ctlState="+#(ctlState);
                  DrawCCFader(rowIdx, 0, ctlState.get(CTL_MW), split_point, row_colors.get(rowIdx), bFaderHistory);
               }
               colIdx = split_point;

               if(b_scene && (rowIdx >= 6))
               {
                  if(0 == colOffset)
                  {
                     int sceneOff = (6 == rowIdx) ? (25 - split_point - 1) : 0;

                     // Right (scene select)
                     loop(25 - split_point - 1)
                     {
                        int sceneIdx = sceneOff + (colIdx - split_point);
                        if( ((tick_nr % 192) < 24) && (sceneIdx == last_selected_scene_idx))
                        {
                           SetPadColor(colIdx, rowIdx, COLOR_MAGENTA);
                        }
                        else
                        {
                           if(IsSceneEmpty(sceneIdx))
                           {
                              SetPadColor(colIdx, rowIdx, COLOR_BLUE);
                           }
                           else
                           {
                              SetPadColor(colIdx, rowIdx, scene_colors.get(((colIdx+(7==rowIdx)) % scene_colors.numElements)));
                           }
                        }
                        colIdx++;
                     }
                  }
               }
               else
               {
                  if(-1 != current_fader_page)
                  {
                     // Right (CC fader, per-track)
                     bFaderHistory =
                        (-1 != ctlState.get((0 == current_fader_page) ? CTL_FX1 : CTL_FX2)) ||
                        (-1 != ctlState.get((0 == current_fader_page) ? CTL_GP1 : CTL_GP3)) ||
                        (-1 != ctlState.get((0 == current_fader_page) ? CTL_GP2 : CTL_GP4)) ;
                     if(0 == ((rowIdx/2)&1))
                     {
                        if(0 == current_fader_page)
                           ccFaderCol = COLOR_BLUE;
                        else
                           ccFaderCol = COLOR_GREEN;
                     }
                     else
                     {
                        if(0 == current_fader_page)
                           ccFaderCol = COLOR_CYAN;
                        else
                           ccFaderCol = COLOR_MAGENTA;
                     }
                     DrawCCFader(rowIdx, split_point + colOffset,
                                 ctlState.get((0 == current_fader_page) ? CTL_FX1 : CTL_FX2),
                                 trackW,
                                 ccFaderCol,
                                 bFaderHistory
                                 );
                     colIdx = 25 - 1;
                  }
                  else
                  {
                     // Right (pattern select)
                     Byte pcMidiCh;
                     Node pcNode <= GetPrgChgNodeDevIdxCh(trackIdx, null/*devIdx*/, pcMidiCh);
                     // trace "xxx pcNode["+trackIdx+"]="+#(pcNode);
                     byte pcEmptyColor = COLOR_BLACK;

                     if(null != pcNode)
                     {
                        byte usedPatIdx = -1;
                        byte numUsedPatterns = pcNode.nodeGetNumUsedPatterns();

                        if(_bUpdateLastSelected)
                        {
                           last_selected_pattern_nrs[trackIdx] = pcNode.nodeGetCurrentPlayPatternNr();
                        }

                        loop(trackW)
                        {
                           byte pcColor = COLOR_BLACK;
                           // trace "xxx trackIdx="+trackIdx+" usedPatIdx="+usedPatIdx+" colOffset="+colOffset;

                           if(-1 == usedPatIdx)
                           {
                              // Split point (empty pattern select (127, "--/"))
                              if(colOffset == trackW)
                              {
                                 // tracks 9..16
                                 bFaderHistory =
                                    (-1 != ctlState.get(CTL_PAT))   ||   // [19Feb2021]
                                    (-1 != ctlState.get(CTL_MW))    ||
                                    (-1 != ctlState.get(CTL_BC))    ||
                                    (-1 != ctlState.get(CTL_EXPR))  ||
                                    (-1 != ctlState.get(CTL_AT))    ||
                                    (-1 != ctlState.get(CTL_PB))    ||
                                    (-1 != ctlState.get(CTL_BRITE))  ;

                                 if(Node.EMPTY_PAT_NR == last_selected_pattern_nrs.get(trackIdx))
                                    pcEmptyColor = bFaderHistory ? selected_pattern_color : COLOR_MAGENTA;
                                 else
                                    pcEmptyColor = bFaderHistory ? COLOR_ORANGE : (null != pcNode) ? COLOR_WHITE : COLOR_BLUE;
                              }
                              else if(Node.EMPTY_PAT_NR == last_selected_pattern_nrs.get(trackIdx))
                              {
                                 pcEmptyColor = selected_pattern_color;
                              }
                              else
                              {
                                 pcEmptyColor = (null != pcNode) ? empty_pattern_color : COLOR_BLUE;
                              }

                              pcColor = pcEmptyColor;
                           }
                           else
                           {
                              int patNr;

                              int forcedPatPg = forced_patpg.get(trackIdx);
                              if( (-1 != forcedPatPg) || ((1 == current_pattern_page) && (numUsedPatterns >= trackW)) )
                              {
                                 forcedPatPg = (-1 == forcedPatPg) ? 1 : forcedPatPg;
                                 patNr = pcNode.nodeGetPatternNrByUsedIdx(usedPatIdx + forcedPatPg*(trackW - 1));
                              }
                              else
                              {
                                 patNr = pcNode.nodeGetPatternNrByUsedIdx(usedPatIdx);
                              }

                              if(-1 != patNr)
                              {
                                 // trace "xxx patNr="+patNr+" lastSel="+last_selected_pattern_nrs.get(rowIdx);
                                 if(patNr == last_selected_pattern_nrs.get(trackIdx))
                                 {
                                    pcColor = selected_pattern_color;
                                 }
                                 else
                                 {
                                    pcColor = row_colors.get(rowIdx);
                                 }
                              }
                           }

                           SetPadColor(colIdx+colOffset, rowIdx, pcColor);
                           // trace "xxx set pat color x="+(colIdx+colOffset)+" y="+rowIdx+" color="+pcColor;
                           colIdx++;

                           usedPatIdx++;
                        } // loop right

                        last_num_used_patterns[trackIdx] = numUsedPatterns;

                     } // if pcNode
                     else
                     {
                        // Target node not available (show empty pattern select (127, "--/"))
                        if(colOffset == trackW)
                        {
                           // tracks 9..16
                           bFaderHistory =
                              (-1 != ctlState.get(CTL_PAT))   ||   // [19Feb2021]
                              (-1 != ctlState.get(CTL_MW))    ||
                              (-1 != ctlState.get(CTL_BC))    ||
                              (-1 != ctlState.get(CTL_EXPR))  ||
                              (-1 != ctlState.get(CTL_AT))    ||
                              (-1 != ctlState.get(CTL_PB))    ||
                              (-1 != ctlState.get(CTL_BRITE))  ;

                           if(Node.EMPTY_PAT_NR == last_selected_pattern_nrs.get(trackIdx))
                              pcEmptyColor = bFaderHistory ? selected_pattern_color : COLOR_MAGENTA;
                           else
                              pcEmptyColor = bFaderHistory ? COLOR_ORANGE : (null != pcNode) ? COLOR_WHITE : COLOR_BLUE;
                        }
                        else if(Node.EMPTY_PAT_NR == last_selected_pattern_nrs.get(trackIdx))
                        {
                           pcEmptyColor = selected_pattern_color;
                        }
                        else
                        {
                           pcEmptyColor = COLOR_BLUE;
                        }

                        SetPadColor(colIdx+colOffset, rowIdx, pcEmptyColor);
                        colIdx++;

                        loop(trackW-1)
                        {
                           SetPadColor(colIdx+colOffset, rowIdx, COLOR_BLACK);
                           colIdx++;
                        }
                     }
                  }
               }

               if(0 == colOffset)
                  SetPadColor(24, rowIdx, toolbar_colors.get(rowIdx));

            } // if !b_footswitch
            else
            {
               // Fader overview mode (single track)

               // Left:
               int ctlIdx = 1 + rowIdx;
               ctlState <= ctl_states.get(last_selected_track_idx);
               ccFaderCol = row_colors.get(rowIdx);
               DrawCCFader(rowIdx, 0,
                           ctlState.get(ctlIdx),
                           split_point,
                           ccFaderCol,
                           (-1 != ctlState.get(ctlIdx))/*bFaderHistory*/
                           );
               colIdx = split_point;

               // Right:
               if((7 == rowIdx) && b_fader_overview_row_mode)
               {
                  if(0 == colOffset)
                  {
                     // Draw track selection in top row
                     int trackPadIdx = 0;
                     loop(NUM_TRACKS)
                     {
                        if( ((tick_nr % 192) < 24) && (trackPadIdx == last_selected_track_idx))
                        {
                           SetPadColor(colIdx, rowIdx, COLOR_BLACK);
                        }
                        else
                        {
                           SetPadColor(colIdx, rowIdx, row_colors.get(trackIdx&7));
                        }
                        colIdx++;
                        trackIdx++;
                     }
                     loop(25 - NUM_TRACKS - 1)
                        SetPadColor(colIdx++, rowIdx, COLOR_BLACK);
                  }
               }
               else
               {
                  ctlIdx = 1 + rowIdx + 8;  // GP1..GP8
                  DrawCCFader(rowIdx, colIdx,
                              ctlState.get(ctlIdx),
                              (25 - split_point - 1),
                              row_colors.get(rowIdx),
                              (-1 != ctlState.get(ctlIdx))/*bFaderHistory*/
                              );
                  colIdx = 25 - 1;
               }

               if(0 == colOffset)
                  SetPadColor(24, rowIdx, toolbar_colors_fader_overview.get(rowIdx));
            }

         } // if fltTrackIdx

         // Next track (or single track fader overview row)
         trackIdx++;
      }
   }

   if(!b_footswitch)
   {
      SetPadColor(24, 5, b_navigation_mode ? navigation_mode_on_color : navigation_mode_off_color);
      if(b_scene_autosave)
         SetPadColor(24, 7, b_scene ? scene_mode_on_autosave_color : scene_mode_off_autosave_color);
      else
         SetPadColor(24, 7, b_scene ? scene_mode_on_color : scene_mode_off_color);
   }
   else
   {
      SetPadColor(24, 7, b_fader_overview_row_mode ? fader_overview_row_mode_on_color : fader_overview_row_mode_off_color);
   }

   b_update_pad_colors = true;

   // if(0)
   // {
   //    // Test
   //    Buffer b;
   //    b.size = 1+3+3;
   //    b.offset = 0;
   //    b.i8 = 0xFE;
   //    b.i8 = 0x90;
   //    b.i8 = 12*4;
   //    b.i8 = 70;
   //    dev.addBuffer(b);
   //    dev.send(milliSeconds());
   // }
}

function HasPatternChanged() {
   // Returns track idx
   int trackIdx = 0;
   loop(NUM_TRACKS)
   {
      Byte pcMidiCh;
      Node pcNode <= GetPrgChgNodeDevIdxCh(trackIdx, null/*devIdx*/, pcMidiCh);
      if(null != pcNode)
      {
         int nodeGID = pcNode.getNodeGID();
         if(last_seen_node_gids.get(trackIdx) != nodeGID)
         {
            last_seen_node_gids[trackIdx] = nodeGID;
            ColorizePads(-1, true/*bUpdateLastSelected*/);
            b_update_pad_colors = true;
         }

         // trace "xxx node="+#(pcNode)+" trackIdx="+trackIdx+" #usedpat="+pcNode.nodeGetNumUsedPatterns();
         // if(0 == trackIdx)
         //    trace "xxx HasPatternChanged: last_selected="+last_selected_pattern_nrs.get(trackIdx)+" new="+pcNode.nodeGetCurrentPlayPatternNr();
         if(pcNode.nodeGetNumUsedPatterns() != last_num_used_patterns.get(trackIdx))
         {
            return trackIdx;
         }
         else if(pcNode.nodeGetCurrentPlayPatternNr() != last_selected_pattern_nrs.get(trackIdx))
         {
            return trackIdx;
         }
      }
      else if(last_seen_node_gids.get(trackIdx) != -1)
      {
         last_seen_node_gids[trackIdx] = -1;
         ColorizePads(-1, true/*bUpdateLastSelected*/);
         b_update_pad_colors = true;
      }
      trackIdx++;
   }
   return -1;
}

function SendCtl(MIDIPipeFrame _frame, int _trackIdx, int _ctlIdx) {
   // (prgchg) called when loading scene

   Byte midiCh;
   int devIdx = GetModDevIdxCh(_trackIdx, midiCh);
   int ccNr = ctl_ccs.get(_ctlIdx);
   IntArray ctlState <= ctl_states.get(_trackIdx);
   short ctlVal = ctlState.get(_ctlIdx);

   boolean bSendCCMIDIMap = p_send_cc_midimap && !b_discardctl_collect;

   if(-1 != ctlVal)
   {
      // if(7 == _ctlIdx)
      //    trace "xxx lsarr: SendCtl(rowIdx="+_rowIdx+" ctlIdx="+_ctlIdx+") ctlVal="+ctlVal;

      switch(ccNr)
      {
         case -999:/*prg.chg / pattern*/
            Byte pcMidiCh;
            Integer pcDevIdx;
            Node pcNode <= GetPrgChgNodeDevIdxCh(_trackIdx, pcDevIdx, pcMidiCh);
            if(null != pcNode)
            {
               _frame.rpn(true/*bSet*/, pcDevIdx, pcMidiCh, Node.RPN_COMMON_UI_PRGCHG, ctlVal);
            }
            if(p_send_notes_midimap)
            {
               st2::replay.addSynMIDIMapEventUI_Note(devIdx, midiCh, ctlVal, 64/*velocity*/);
            }
            break;

         case -CTL_AT:
            _frame.chPressure(true/*bSet*/, devIdx, midiCh, ctlVal);
            if(bSendCCMIDIMap)
            {
               st2::replay.addSynMIDIMapEventUI_ChannelPressure(devIdx, midiCh, ctlVal);
            }
            break;

         case -CTL_PB:
            _frame.pitchbend(true/*bSet*/, devIdx, midiCh, ctlVal);
            if(bSendCCMIDIMap)
            {
               st2::replay.addSynMIDIMapEventUI_Pitchbend(devIdx, midiCh, ctlVal);
            }
            break;

         default:
            if(b_footswitch && b_right_footswitch_send_rpn_ctl && (_ctlIdx >= CTL_GP1))
            {
               _frame.rpn(true/*bSet*/, devIdx, midiCh, Node.RPN_CTL_BASE + (_ctlIdx - CTL_GP1), ctlVal*129);
            }
            else
            {
               _frame.cc(true/*bSet*/, devIdx, midiCh, ccNr, ctlVal);
               if(bSendCCMIDIMap)
               {
                  st2::replay.addSynMIDIMapEventUI_CC(devIdx, midiCh, ccNr, ctlVal);
               }
            }
            break;
      }
   }
}

function TraceCtl(String _msg) {
   if(NodeScript::IsEditorVisible())
   {
      Global.PrintFromReplay(_msg);
   }
}

function LoadScene(MIDIPipeFrame _framePlay, int _idx) {
   PointerArray sceneCtls <= scene_ctl_states.get(_idx);
   if(null != sceneCtls)
   {
      int loadSaveMode = b_discardctl ? b_discardctl_collect ? 1/*controllers only*/ : 0/*patterns only*/ : p_loadsave_scene_mode;
      CopyScene(ctl_states, sceneCtls, loadSaveMode);

      int trackIdx = 0;
      loop(NUM_TRACKS)
      {
         IntArray ctlState <= ctl_states.get(trackIdx);

         MIDIPipeFrame *frFuture;
         int evQuant = event_quantization.get(trackIdx);
         if(evQuant > 0 && st2::replay.b_playing)
         {
            evQuant = st2::current_song.quantizeToNext16ths(tick_nr, evQuant);
            frFuture <= GetOrCreateFutureFrame(evQuant);
            // trace "xxx linnstrument:LoadScene: tick_nr="+tick_nr+" future="+evQuant;
         }
         else
            frFuture <= _framePlay;

         if(b_debug) trace "[dbg] Script<linnstrument_arranger>: LoadScene ctlState["+trackIdx+"] = "+#(ctlState);

         if(null != ctlState)
         {
            if(loadSaveMode >= 1)
            {
               int ctlIdx = (1 == loadSaveMode);
               loop(CTL_NUM - ctlIdx)
               {
                  SendCtl(frFuture, trackIdx, ctlIdx++);
               }
            }
            else
            {
               SendCtl(frFuture, trackIdx, CTL_PAT);
            }
         }

         trackIdx++;
      }

      last_selected_pattern_nrs.fill(-1);

      ColorizePads(-1, true/*bUpdateLastSelected*/);
      if(b_scene_autosave)
         Global.PrintFromReplay("Load scene #"+(_idx+1)+" ("+loadsave_scene_mode_names.get(loadSaveMode)+")");
      else
         Global.PrintFromReplay("Load scene #"+(_idx+1)+" ("+loadsave_scene_mode_names.get(loadSaveMode)+")");
      last_selected_scene_idx = _idx;
   }
}

function SelectScene(MIDIPipeFrame _framePlay, int _sceneIdx) {
   // when scene-select pad is pressed or scene-select note-on is received
   if(b_debug) trace "[dbg] Script<linnstrument_arranger>: SelectScene("+_sceneIdx+")";

   if(b_scene_autosave && last_selected_scene_idx >= 0)
      SaveScene(last_selected_scene_idx, false/*bCopy*/);

   LoadScene(_framePlay, _sceneIdx);
   copy_src_scene_idx = last_selected_scene_idx;
   copy_src_scene_ms  = milliSeconds();
}

function LoadGlobalSetting(short _setting) {
   STX_MIDIOutDevice dev <= MIDI.GetMIDIOutDeviceByIndex(linn_dev);

   if(null != dev)
   {
      if(b_debug) trace "[dbg] Script<linnstrument_arranger>: LoadGlobalSetting("+_setting+")";

      // Send NRPN 243 (see Linnstrument midi.txt)
      //  (note) the device expects a very specific MIDI sequence so we can't use dev.nrpn() here
      globalsettings_grace_ms = milliSeconds();
      SendNRPN(dev, 243, _setting & 3);
   }
}

function SendConfiguration() {
   STX_MIDIOutDevice dev <= MIDI.GetMIDIOutDeviceByIndex(linn_dev);

   if(null != dev)
   {
      // User Firmware Mode OFF
      SendNRPN(dev, 245, 0);

      dev.send(0);
      TKS.sleep(250);

      // Select preset #4
      LoadGlobalSetting(global_setting_arranger);

      dev.send(0);
      TKS.sleep(500);

      trace "[dbg] Script<linnstrument_arranger>: send configuration";

      //    - split point: 6 columns left, 19 columns right  (note: actually this depends on split_point var)
      //    - row per channel mode (both left and right)
      //    - no row offset
      //       - first note left: C-3 (0x24)
      //       - first note right: C-3 (0x24)
      //    - channels left: 1..8
      //    - channels right: 9..16
      //    - transpose left: octave:+1, lights:+6
      //    - transpose right: octave:-1, lights:-6
      //    - pitchbend range left: 24 semitones
      //    - footswitch function set to CC65 (Portamento SW)
      //    - footswitch function set to CC65 (Portamento SW)
      //       (tested with a (regular) mono footswitch pedal)
      //       (note: if you don't have a foot pedal, just route/transform another MIDI event with
      //              a ModMatrix node, e.g. a note on/off or CC generated by a MIDI controller button)

      byte ch;

      // Channel Per Row MIDI Channel Order
      SendNRPN(dev, 60, 0);

      // Global Split Active
      SendNRPN(dev, 200, 1);
      SendNRPN(dev, 202, split_point+1);

      // Global Row Offset (off)
      SendNRPN(dev, 227, 127);

      // Left Footswitch = CC65
      SendNRPN(dev, 230, 3);

      int l = 0;
      int r = 100;

      //
      // Left Split
      //

      // Activate Channel Per Row mode
      SendNRPN(dev, l+0, 2);

      // Select MIDI channels 1..8
      ch = 0;
      loop(8)
      {
         SendNRPN(dev, l+2+ch, 1);
         ch++;
      }
      loop(8)
      {
         SendNRPN(dev, l+2+ch, 0);
         ch++;
      }

      // MIDI Bend Range
      SendNRPN(dev, l+19, 24);

      // X-axis
      SendNRPN(dev, l+20, 1);  // Enable send x
      SendNRPN(dev, l+21, 1);  // Pitch Quant
      SendNRPN(dev, l+22, 1);  // Pitch Quant Hold
      SendNRPN(dev, l+23, 0);  // Pitch Reset On Release

      // Y-axis mode (CC#74)
      SendNRPN(dev, l+24, 1);  // Enable send y
      SendNRPN(dev, l+39, 2);
      SendNRPN(dev, l+25, 74);

      // Z-axis enable + select Poly-Pressure mode
      SendNRPN(dev, l+27, 1);
      SendNRPN(dev, l+28, 0);

      // Turn off Low-Row mode
      SendNRPN(dev, l+34, 0);

      // Turn off Arp/Seq/..
      SendNRPN(dev, l+35, 0);

      // Octave/Pitch/Lights
      SendNRPN(dev, l+36, 5+1);  // oct +1
      SendNRPN(dev, l+37, 7);    // pitch +0   //  + (split_point-7)
      SendNRPN(dev, l+38, 7+6);  // lights +6   //  + (split_point-7)


      // dev.send(0);
      // TKS.sleep(250);


      //
      // Right Split
      //

      // Activate Channel Per Row mode
      SendNRPN(dev, r+0, 2);

      // Select MIDI channels 9..16
      ch = 0;
      loop(8)
      {
         SendNRPN(dev, r+2+ch, 0);
         ch++;
      }
      loop(8)
      {
         SendNRPN(dev, r+2+ch, 1);
         ch++;
      }

      // MIDI Bend Range
      SendNRPN(dev, r+19, 24);

      // X-axis
      SendNRPN(dev, r+20, 1);  // Enable send x
      SendNRPN(dev, r+21, 1);  // Pitch Quant
      SendNRPN(dev, r+22, 1);  // Pitch Quant Hold
      SendNRPN(dev, r+23, 0);  // Pitch Reset On Release

      // Y-axis mode (CC#74)
      SendNRPN(dev, r+24, 1);  // Enable send y
      SendNRPN(dev, r+39, 2);
      SendNRPN(dev, r+25, 74);

      // Z-axis enable + select Poly-Pressure mode
      SendNRPN(dev, r+27, 1);
      SendNRPN(dev, r+28, 0);

      // Turn off Low-Row mode
      SendNRPN(dev, r+34, 0);

      // Turn off Arp/Seq/..
      SendNRPN(dev, r+35, 0);

      // dev.send(0);
      // TKS.sleep(250);

      // Octave/Pitch/Lights
      if(12 == split_point)
      {
         SendNRPN(dev, r+36, 5-1);  // oct -1
         SendNRPN(dev, r+37, 7);    // pitch +0
         SendNRPN(dev, r+38, 7-6);  // lights -6
      }
      else if(7 == split_point)
      {
         SendNRPN(dev, r+36, 5+0);  // oct +0
         SendNRPN(dev, r+37, 7+1);  // pitch +1
         SendNRPN(dev, r+38, 7+2);  // lights +2
      }
      else // 6 == split_point
      {
         trace "xxx split_point="+split_point;
         SendNRPN(dev, r+36, 5+0);  // oct +0
         SendNRPN(dev, r+37, 7+1-2);  // pitch +1
         SendNRPN(dev, r+38, 7+2-3);  // lights +2
      }

      // dev.send(0);
      // TKS.sleep(250);

      // Workaround for Linnstrument bug: pressed note pad stays lit and color cannot be changed anymore
      SendNRPN(dev, l+32, COLOR_OFF);

      // Workaround for Linnstrument bug: pressed note pad stays lit and color cannot be changed anymore
      SendNRPN(dev, r+32, COLOR_OFF);

      dev.send(0);
      TKS.sleep(250);

      Global.Print("Global settings have been sent to Linnstrument");
   }
}

function UpdatePresetComboBoxes() {
   ui::ComboBox *cm;
   if(null != ui_xfm)
   {
      cm <= ui_xfm.findLayerById("p_preset");
      if(null != cm)
      {
         // trace "xxx linnstrument::UpdatePresetComboBoxes: preset_names="+#(preset_names);
         cm.setOptions(preset_names);
      }

      cm <= ui_xfm.findLayerById("cm_preset_arranger");
      if(null != cm)
      {
         cm.setSelectedOption(global_setting_arranger);
      }

      cm <= ui_xfm.findLayerById("cm_preset_default");
      if(null != cm)
      {
         cm.setSelectedOption(global_setting_default);
      }
   }
}

function SelectPreset(int _preset) {

   SelectPreset_replay(_preset);

   if(null != ui_xfm)  // can be null while loading project file
   {
      // Update UI
      int trackIdx = 0;
      ui::ComboBox *cm;
      ui::FloatParam *fp;
      loop(16)
      {
         // dev
         // trace "xxx ui_xfm="+#(ui_xfm);
         cm <= ui_xfm.findLayerById("trkcfg_cm_dev_trk"+(trackIdx+1));
         StringArray devOptions <= cm.getOptions();
         // trace "xxx devOptions="+#(devOptions);
         // trace "xxx left_modulation_ports["+trackIdx+"] = \""+left_modulation_ports.get(trackIdx*2+0)+"\"";
         cm.setSelectedOption(devOptions.indexOfObject(left_modulation_ports.get(trackIdx*2+0), 0));

         // ch
         cm <= ui_xfm.findLayerById("trkcfg_cm_ch_trk"+(trackIdx+1));
         cm.setSelectedOption(int(left_modulation_ports.get(trackIdx*2+1))-1);

         // forced_patpg
         cm <= ui_xfm.findLayerById("trkcfg_cm_pg_trk"+(trackIdx+1));
         cm.setSelectedOption(int(forced_patpg.get(trackIdx))+1);

         // mutex group
         cm <= ui_xfm.findLayerById("trkcfg_cm_mtx_trk"+(trackIdx+1));
         cm.setSelectedOption(int(mutex_groups.get(trackIdx))+1);

         // event quantization
         fp <= ui_xfm.findLayerById("trkcfg_fp_ev_quant_trk"+(trackIdx+1));
         fp.setValue(int(event_quantization.get(trackIdx)));

         // Next track
         trackIdx++;
      }
   }
}

function OnUIInit(ui::XMLForm xfm) {
   ui_xfm <= xfm;
   // trace "xxx linnstrument::OnUIInit";

   UpdatePresetComboBoxes();

   SelectPreset(p_preset); // Reselect current preset / update UI
}

function UpdateTrackConfigDev(int _trackIdx, String _devName) {
   left_modulation_ports[2*_trackIdx+0] = _devName;
   left_note_ports      [2*_trackIdx+0] = _devName;
   right_prgchg_ports   [2*_trackIdx+0] = _devName;
}

function UpdateTrackConfigCh(int _trackIdx, byte _midiCh) {
   left_modulation_ports[2*_trackIdx+1] = String(_midiCh + 1);
   left_note_ports      [2*_trackIdx+1] = String(_midiCh + 1);
   right_prgchg_ports   [2*_trackIdx+1] = String(_midiCh + 1);
}

function UpdateTrackConfigPg(int _trackIdx, byte _pg) {
   forced_patpg[_trackIdx] = _pg;
}

function UpdateTrackConfigMtx(int _trackIdx, byte _mtxGroupIdx) {
   mutex_groups[_trackIdx] = _mtxGroupIdx;
}

function UpdateTrackConfigEvQuant(int _trackIdx, int _evQuant) {
   event_quantization[_trackIdx] = _evQuant;
}

function OnAction(ui::Layer layer, String id, String acName, MIDIPipeFrame frameOut) {
   ui::ComboBox *cm;
   ui::FloatParam *fp;
   local String linnId = "<LINNSTRUMENT_"+linn_dev+">";
   if(b_debug)
      Global.Debug("script_linnstrument_arranger:OnAction: linnId=\""+linnId+"\"");

   if(id <= "trkcfg_")
   {
      cm <= layer;
      fp <= layer;
      int trackIdx = id.indexOf("_trk", 0);
      trackIdx = int(id.substring(trackIdx+4, 99)) - 1;
      // trace "xxx trackIdx="+trackIdx;
      if(id <= "trkcfg_cm_dev")
      {
         UpdateTrackConfigDev(trackIdx, cm.getSelectedOptionName());
         Global.Print("Track "+(trackIdx+1)+" device is \""+cm.getSelectedOptionName()+"\"");
      }
      else if(id <= "trkcfg_cm_ch")
      {
         UpdateTrackConfigCh(trackIdx, cm.getSelectedOption());
         Global.Print("Track "+(trackIdx+1)+" channel is \""+cm.getSelectedOptionName()+"\"");
      }
      else if(id <= "trkcfg_cm_pg")
      {
         UpdateTrackConfigPg(trackIdx, cm.getSelectedOption() - 1);
         Global.Print("Track "+(trackIdx+1)+" forced pattern page is \""+cm.getSelectedOptionName()+"\"");
      }
      else if(id <= "trkcfg_cm_mtx")
      {
         UpdateTrackConfigMtx(trackIdx, cm.getSelectedOption() - 1);
         Global.Print("Track "+(trackIdx+1)+" pattern mutex group is \""+cm.getSelectedOptionName()+"\"");
      }
      else if(id <= "trkcfg_fp_ev_quant")
      {
         UpdateTrackConfigEvQuant(trackIdx, fp.getFloatValue());
         Global.Print("Track "+(trackIdx+1)+" pattern event quantization is \""+int(fp.getFloatValue())+"\"");
      }
      return;
   }

   switch(id)
   {
      case linnId:
         switch(acName)
         {
            // Sent by NTS_Linnstrument TrigSeq plugin
            case "USERMODE_ON":
               b_usermode = true;
               break;

            case "USERMODE_OFF":
               b_usermode = false;
               STX_MIDIOutDevice outDev <= MIDI.GetMIDIOutDeviceByIndex(linn_dev);
               int inDevIdx = MIDI.FindInDeviceIdxForOutDevice(outDev.getAliasOrDeviceName());
               if(-1 != inDevIdx)
               {
                  st2::replay.excludeMIDIMapInDev(inDevIdx);
               }
               break;
         }
         break;


      case "bt_reset_default":
         ColorizePads_Default();
         UpdatePadColors(false/*bProgressive*/);
         break;

      case "bt_reset":
         b_reset = true;
         break;

      case "bt_sendcfg":
         SendConfiguration();
         break;

      case "cm_preset_arranger":
         cm <= ui_xfm.findLayerById("cm_preset_arranger");
         global_setting_arranger = cm.getSelectedOption();
         Global.Print("Arranger preset is "+cm.getSelectedOptionName());
         if(!b_last_mute_state)
            LoadGlobalSetting(global_setting_arranger);
         break;

      case "cm_preset_default":
         cm <= ui_xfm.findLayerById("cm_preset_default");
         global_setting_default = cm.getSelectedOption();
         Global.Print("Default preset is "+cm.getSelectedOptionName());
         if(b_last_mute_state)
            LoadGlobalSetting(global_setting_default);
         break;

      case "p_preset":
         // (todo) sync via =replay= mutex
         SelectPreset(p_preset);
         ColorizePads(-1, true/*bUpdateLastSelected*/);
         UpdatePadColors(false/*bProgressive*/);
         break;
   }
}

function HandleNotes(MIDIPipeFrame frameRec, MIDIPipeFrame framePlay) {

   int numEvents = frameRec.getNumEventsNoteOnByFlt(linn_mapped_dev, -1/*all ch*/);
   Byte noteMidiCh;
   int noteDevIdx;
   MIDIPipeEvent ev;
   int evIdx;
   int rowIdx;
   int noteOffset;
   int trackIdx;
   int colOffset;
   int trackW = (25 - split_point - 1) / 2;  // == 9 columns/pads at split_point=6

   if(0 != numEvents)
   {
      // trace "xxx HandleNotes: numEventsNoteOn="+numEvents;

      evIdx = 0;
      while(evIdx < numEvents)
      {
         frameRec.getEventByIdxAndFlt(evIdx, ev, MIDIPIPE_EVENT_TYPE_NOTE_ON, linn_mapped_dev, -1/*all ch*/);

         rowIdx = ev.midiCh & 7;

         if(!b_footswitch)
         {
            if(ev.midiCh < 8)
            {
               // Left split
               trackIdx = rowIdx + left_fader_track_offset;

               noteDevIdx = GetNoteDevIdxCh(trackIdx, noteMidiCh);
               if(-1 != noteDevIdx)
               {
                  if(p_send_notes)
                  {
                     frameRec.noteOn(true/*bSet*/, noteDevIdx, noteMidiCh, ev.note, ev.velocity, ev.duration);
                  }
                  if(p_send_notes_midimap)
                  {
                     st2::replay.addSynMIDIMapEventUI_Note(noteDevIdx, noteMidiCh, ev.note, ev.velocity);
                  }
               }
            }
            else
            {
               // Right split
               noteOffset = ev.note - prgchg_midi_base_note/*C-3*/;

               trackIdx = (noteOffset >= trackW) ? (rowIdx + 8) : rowIdx;

               if((24 - split_point) == noteOffset)
               {
                  // Toolbar pad
                  switch(rowIdx)
                  {
                     case 0: // Orange
                        if(b_discardctl_collect)
                        {
                           b_discardctl_collect_discard = true;
                        }
                        else
                        {
                           st2::replay.setEnableDiscardCtl(true);
                           st2::replay.excludeDiscardCtlDev(linn_mapped_dev);
                           b_discardctl = true;
                        }
                        Global.PrintFromReplay("Discard ctl");
                        break;

                     case 1: // Cyan
                        st2::replay.setEnableDiscardCtlCollect(true);
                        st2::replay.excludeDiscardCtlDev(linn_mapped_dev);
                        b_discardctl = true;
                        b_discardctl_collect = true;
                        b_discardctl_collect_discard = false;
                        Global.PrintFromReplay("Discard ctl (collect)");
                        break;

                     case 2: // Green
                        current_pattern_page = (current_pattern_page + 1) & 1;
                        current_fader_page = -1;
                        ColorizePads(-1, true/*bUpdateLastSelected*/);
                        Global.PrintFromReplay("Show Pattern Page "+(current_pattern_page+1));
                        break;

                     case 3: // Blue
                        current_fader_page = (current_fader_page + 1) & 1;
                        current_pattern_page = -1;
                        ColorizePads(-1, true/*bUpdateLastSelected*/);
                        Global.PrintFromReplay("Show Fader Page "+(current_fader_page+1));
                        break;

                     case 4: // White (lower)
                        if(b_scene_pressed)
                        {
                           b_scene_autosave = !b_scene_autosave;
                           ColorizePads(7, false/*bUpdateLastSelected*/);
                           Global.PrintFromReplay("Scene Autosave is "+Utils.GetEnableString(b_scene_autosave));
                        }
                        else
                        {
                           b_clear_modifier = true;
                           Global.PrintFromReplay("Clear: <tap fader or scene pad>");
                        }
                        break;

                     case 5: // Lime
                        b_navigation_mode = !b_navigation_mode;
                        ColorizePads(5, true/*bUpdateLastSelected*/);
                        Global.PrintFromReplay("Node Navigation mode is "+Utils.GetEnableString(b_navigation_mode));
                        break;

                     case 6: // White (upper)
                        // handled in note-off
                        //  (Linnstrument bug: pressed note permanently stays lit after switching to user mode (can't change pad color)
                        //                      while pad is pressed)
                        break;

                     case 7: // Yellow
                        b_scene_pressed = true;
                        scene_pressed_ms = milliSeconds();
                        if(b_scene)
                        {
                           Global.PrintFromReplay("Save Scene: <tap scene pad>");
                           ColorizePads(6, true/*bUpdateLastSelected*/);
                           ColorizePads(7, true/*bUpdateLastSelected*/);
                        }
                        break;
                  }
               }
            }
         }
         else
         {
            // Fader overview mode (current track)
            if(ev.midiCh >= 8)
            {
               // Right split
               noteOffset = ev.note - prgchg_midi_base_note/*C-3*/;

               if((24 - split_point) == noteOffset)
               {
                  // Toolbar pad
                  switch(rowIdx)
                  {
                     case 0: // Orange
                        if(b_discardctl_collect)
                        {
                           b_discardctl_collect_discard = true;
                        }
                        else
                        {
                           st2::replay.setEnableDiscardCtl(true);
                           st2::replay.excludeDiscardCtlDev(linn_mapped_dev);
                           b_discardctl = true;
                           Global.PrintFromReplay("Discard ctl");
                        }
                        break;

                     case 1: // Cyan
                        st2::replay.setEnableDiscardCtlCollect(true);
                        st2::replay.excludeDiscardCtlDev(linn_mapped_dev);
                        b_discardctl = true;
                        b_discardctl_collect = true;
                        b_discardctl_collect_discard = false;
                        Global.PrintFromReplay("Discard ctl (collect)");
                        break;

                     case 2: // Green
                        break;

                     case 3: // Blue
                        break;

                     case 4: // White (lower)
                        if(b_scene_pressed)
                        {
                           b_scene_autosave = !b_scene_autosave;
                           ColorizePads(7, false/*bUpdateLastSelected*/);
                           Global.PrintFromReplay("Scene Autosave is "+Utils.GetEnableString(b_scene_autosave));
                        }
                        else
                        {
                           b_clear_modifier = true;
                           Global.PrintFromReplay("Clear: <tap fader>");
                        }
                        break;

                     case 5: // Lime
                        break;

                     case 6: // White (upper)
                        break;

                     case 7: // Yellow
                        b_fader_overview_row_mode = !b_fader_overview_row_mode;
                        Global.PrintFromReplay("Fader Overview: "+(b_fader_overview_row_mode?"Activate":"Deactivate")+" row selection mode");
                        ColorizePads(7, true/*bUpdateLastSelected*/);
                        break;
                  }
               }
            }
         }

         evIdx++;
      }
   }

   numEvents = frameRec.getNumEventsNoteOffByFlt(linn_mapped_dev, -1/*all ch*/);
   if(0 != numEvents)
   {
      evIdx = 0;
      while(evIdx < numEvents)
      {
         frameRec.getEventByIdxAndFlt(evIdx, ev, MIDIPIPE_EVENT_TYPE_NOTE_OFF, linn_mapped_dev, -1/*all ch*/);

         copy_src_scene_idx = -1;

         last_note_offsets[ev.midiCh] = -1;
         rowIdx = ev.midiCh & 7;

         if(ev.midiCh < 8)
         {
            // left
            trackIdx = ev.midiCh;
            colOffset = 0;
         }
         else
         {
            // right
            noteOffset = ev.note - prgchg_midi_base_note/*C-3*/;
            if(noteOffset >= trackW)
            {
               trackIdx = ev.midiCh;
               colOffset = trackW;
            }
            else
            {
               trackIdx = ev.midiCh - 8;
               colOffset = 0;
            }
         }

         if(!b_footswitch)
         {
            if(ev.midiCh < 8)
            {
               // Left
               noteDevIdx = GetNoteDevIdxCh(trackIdx, noteMidiCh);
               if(-1 != noteDevIdx)
               {
                  if(p_send_notes)
                  {
                     frameRec.noteOff(true/*bSet*/, noteDevIdx, noteMidiCh, ev.note, ev.velocity);
                  }
                  if(p_send_notes_midimap)
                  {
                     st2::replay.addSynMIDIMapEventUI_Note(noteDevIdx, noteMidiCh, ev.note, 0/*velocity*/);
                  }
               }
            }
            else
            {
               // Right split
               if((24 - split_point) == noteOffset)
               {
                  // Toolbar pad
                  switch(rowIdx)
                  {
                     case 0:
                        if(b_discardctl_collect)
                        {
                           b_discardctl_collect_discard = false;
                        }
                        else
                        {
                           st2::replay.setEnableDiscardCtl(false);
                           st2::replay.includeDiscardCtlDev(linn_mapped_dev);
                           b_discardctl = false;
                        }
                        Global.PrintFromReplay("");
                        break;

                     case 1:
                        st2::replay.setEnableDiscardCtlCollect(false);
                        st2::replay.includeDiscardCtlDev(linn_mapped_dev);
                        b_discardctl = false;
                        b_discardctl_collect = false;
                        b_discardctl_merge = true;
                        b_discardctl_collect_discard = false;
                        Global.PrintFromReplay("");
                        break;

                     case 4: // White
                        b_clear_modifier = false;
                        Global.PrintFromReplay("");
                        break;

                     case 6:
                        // st2::root_form.showNextArranger();
                        Global.PrintFromReplay("Swap pipenodes A / B");
                        st2::root_form.queuePipeNodeABSwap();
                        break;

                     case 7: // Yellow
                        b_scene_pressed = false;
                        if((milliSeconds() - scene_pressed_ms) < 500)
                        {
                           b_scene = !b_scene;
                           ColorizePads(6,   true/*bUpdateLastSelected*/);
                           ColorizePads(6+8, true/*bUpdateLastSelected*/);
                           ColorizePads(7,   true/*bUpdateLastSelected*/);
                           ColorizePads(7+8, true/*bUpdateLastSelected*/);
                        }
                        Global.PrintFromReplay("");
                        break;
                  }
               }
            }
         }
         else
         {
            // Fader Overview Mode (current track)
            if(ev.midiCh >= 8)
            {
               // Right split
               noteOffset = ev.note - prgchg_midi_base_note/*C-3*/;

               if((24 - split_point) == noteOffset)
               {
                  // Toolbar pad
                  switch(rowIdx)
                  {
                     case 0:
                        if(b_discardctl_collect)
                        {
                           b_discardctl_collect_discard = false;
                        }
                        else
                        {
                           st2::replay.setEnableDiscardCtl(false);
                           st2::replay.includeDiscardCtlDev(linn_mapped_dev);
                           b_discardctl = false;
                        }
                        break;

                     case 1:
                        st2::replay.setEnableDiscardCtlCollect(false);
                        st2::replay.includeDiscardCtlDev(linn_mapped_dev);
                        b_discardctl = false;
                        b_discardctl_collect = false;
                        b_discardctl_merge = true;
                        b_discardctl_collect_discard = false;
                        break;

                     case 4: // White
                        b_clear_modifier = false;
                        Global.PrintFromReplay("");
                        break;

                     case 7: // Yellow
                        break;
                  }
               }
            }
         }

         frameRec.deleteEventByIdx(ev.index, -1);
         numEvents--;
      }
   }

}

function HandlePatternSelectAndCCFader(MIDIPipeFrame frameRec, MIDIPipeFrame framePlay) {

   MIDIPipeEvent ev;
   int evIdx;
   int ctlIdx;
   int numCels;
   boolean bCtlLeft;
   IntArray *ctlState;
   int rowIdx;
   int noteOffset;
   int trackIdx;
   int colOffset;
   int noteOffsetRel;

   int trackW = (25 - split_point - 1) / 2;  // == 9 columns/pads at split_point=6

   int numEvents = frameRec.getNumEventsNoteOnByFlt(linn_mapped_dev, -1/*all ch*/);
   // if(numEvents > 0)
   //    if(b_debug) trace "xxx Script<linnstrument_arranger>: numEventsNoteOn="+numEvents;
   if(0 != numEvents)
   {
      evIdx = 0;

      ctlIdx = -1;
      bCtlLeft = false;

      while(evIdx < numEvents)
      {
         frameRec.getEventByIdxAndFlt(evIdx, ev, MIDIPIPE_EVENT_TYPE_NOTE_ON, linn_mapped_dev, -1/*all ch*/);

         rowIdx = ev.midiCh;
         noteOffset = ev.note - prgchg_midi_base_note/*C-3*/;

         // defaults for right-hand side area (tracks 1..8 on the left, 9..16 on the right)
         trackIdx = (noteOffset >= trackW) ? ((rowIdx&7) + 8) : (rowIdx&7);
         colOffset = (trackIdx >= 8) ? trackW : 0;
         noteOffsetRel = (colOffset > 0) ? (noteOffset - trackW) : noteOffset;

         // trace "xxx lsarr: ev.midiCh="+ev.midiCh+" ev.note="+ev.note+" noteOffset="+noteOffset+" trackIdx="+trackIdx+" colOffset="+colOffset;

         // (note) right split base channel is #9
         if(rowIdx >= 8)
         {
            // Right split
            rowIdx -= 8;

            if(0 <= noteOffset < (25 - split_point - 1))
            {
               if(!b_footswitch)
               {
                  if(b_scene && (rowIdx >= 6))
                  {
                     int sceneOff = (6 == rowIdx) ? (25 - split_point - 1) : 0;
                     if(b_scene_pressed)
                     {
                        SaveScene(sceneOff + noteOffset, false/*bCopy*/);
                        ColorizePads(6, false/*bUpdateLastSelected*/);
                        ColorizePads(7, false/*bUpdateLastSelected*/);
                     }
                     else if(b_clear_modifier)
                     {
                        ClearScene(sceneOff + noteOffset);
                        ColorizePads(6, false/*bUpdateLastSelected*/);
                        ColorizePads(7, false/*bUpdateLastSelected*/);
                     }
                     else if(-1 != copy_src_scene_idx && ((milliSeconds() - copy_src_scene_ms) >= COPY_SRC_SCENE_THRESHOLD_MS))
                     {
                        // Hold scene <a> + press scene <b> pad
                        SaveScene(sceneOff + noteOffset, true/*bCopy*/);
                     }
                     else
                     {
                        // Press scene pad
                        SelectScene(framePlay, sceneOff + noteOffset);
                     }
                  }
                  else
                  {
                     if((0 == left_fader_track_offset) ^ (0 == colOffset))
                     {
                        left_fader_track_offset = (colOffset > 0) ? 8 : 0;
                        ColorizePads(-1, false/*bUpdateLastSelected*/);
                     }

                     if(-1 != current_fader_page)
                     {
                        // Right: CC fader
                        if(-1 != current_fader_page)
                        {
                           ctlIdx = (0 == current_fader_page) ? CTL_FX1 : CTL_FX2;
                           numCels = trackW;
                           bCtlLeft = false;
                           last_selected_track_idx = trackIdx;
                        }
                     }
                     else
                     {
                        // Right: prgchg

                        if(b_clear_modifier)
                        {
                           // Clear pattern selection (=> won't be saved to scene)
                           ctlState <= ctl_states.get(trackIdx);
                           ctlState[CTL_PAT]   = -1;
                           ctlState[CTL_AT]    = -1;  // [19Feb2021]
                           ctlState[CTL_PB]    = -1;  // [19Feb2021]
                           ctlState[CTL_BRITE] = -1;  // [19Feb2021]
                           ColorizePads(trackIdx, false/*bUpdateLastSelected*/);
                           Global.PrintFromReplay("Clear Fader["+(rowIdx+1)+"] PAT");
                        }
                        else
                        {
                           Byte pcMidiCh;
                           Integer pcDevIdx;
                           Node pcNode <= GetPrgChgNodeDevIdxCh(trackIdx, pcDevIdx, pcMidiCh);
                           if(null != pcNode)
                           {
                              int patNr;
                              int activePatNr = pcNode.nodeGetCurrentPlayPatternNr();

                              last_note_offsets[ev.midiCh] = noteOffset;

                              if((0 == noteOffset) || (trackW == noteOffset))
                              {
                                 patNr = Node.EMPTY_PAT_NR;  // 127, "--/"
                              }
                              else
                              {
                                 int numUsedPat = pcNode.nodeGetNumUsedPatterns();
                                 // trace "xxx numUsedPat="+numUsedPat+" current_pattern_page="+current_pattern_page;
                                 int forcedPatPg = forced_patpg.get(trackIdx);
                                 if( (-1 != forcedPatPg) || ((1 == current_pattern_page) && (numUsedPat >= trackW)) )
                                 {
                                    forcedPatPg = (-1 == forcedPatPg) ? 1 : forcedPatPg;
                                    patNr = pcNode.nodeGetPatternNrByUsedIdx(noteOffsetRel - 1 + (forcedPatPg * (trackW - 1)));
                                 }
                                 else
                                 {
                                    patNr = pcNode.nodeGetPatternNrByUsedIdx(noteOffsetRel - 1);
                                 }
                              }

                              if(-1 != patNr)
                              {
                                 // Send recordable program change RPN (524, see internal.msp)
                                 if(b_debug) trace "[dbg] Script<linnstrument_arranger>: HandlePatternSelectAndCCFader: send patNr="+patNr+" to dev="+pcDevIdx+" ch="+pcMidiCh+" node=\""+pcNode.nodeGetName()+"\"";
                                 MIDIPipeFrame *frFuture;
                                 int evQuant = event_quantization.get(trackIdx);
                                 if(evQuant > 0 && st2::replay.b_playing)
                                 {
                                    evQuant = st2::current_song.quantizeToNext16ths(tick_nr, evQuant);
                                    frFuture <= GetOrCreateFutureFrame(evQuant);
                                    // trace "xxx linnstrument: PrgChg tick_nr="+tick_nr+" future="+evQuant;
                                 }
                                 else
                                    frFuture <= framePlay;

                                 // Handle mutex
                                 int mtxGrp = mutex_groups.get(trackIdx);
                                 if(-1 != mtxGrp)
                                 {
                                    int trackIdxMtx = 0;
                                    loop(16)
                                    {
                                       if(trackIdxMtx != trackIdx)
                                       {
                                          if(mutex_groups.get(trackIdxMtx) == mtxGrp)
                                          {
                                             Byte pcMidiChMtx;
                                             Integer pcDevIdxMtx;
                                             Node pcNodeMtx <= GetPrgChgNodeDevIdxCh(trackIdxMtx, pcDevIdxMtx, pcMidiChMtx);
                                             if(null != pcNodeMtx)
                                             {
                                                frFuture.rpn(true/*bSet*/, pcDevIdxMtx, pcMidiChMtx, Node.RPN_COMMON_UI_PRGCHG, Node.EMPTY_PAT_NR);
                                             }
                                             last_selected_pattern_nrs[trackIdxMtx] = Node.EMPTY_PAT_NR;
                                             ColorizePads(trackIdxMtx, false/*bUpdateLastSelected*/); // update selection
                                          }
                                       }
                                       trackIdxMtx++;
                                    }
                                 } // if mtxGrp

                                 frFuture.rpn(true/*bSet*/, pcDevIdx, pcMidiCh, Node.RPN_COMMON_UI_PRGCHG, patNr);
                                 last_selected_pattern_nrs[trackIdx] = patNr;
                                 ctlState <= ctl_states.get(trackIdx);
                                 ctlState[CTL_PAT] = patNr;
                                 TraceCtl("Select pattern "+(Node.patnr_options.get(patNr)));
                                 last_selected_track_idx = trackIdx;
                                 ColorizePads(trackIdx, false/*bUpdateLastSelected*/); // update selection
                              }

                              if(b_navigation_mode && (patNr == activePatNr))
                              {
                                 st2::root_form.showNodeEditorFor(pcNode);
                              }
                              else
                              {
                                 st2::root_form.queueSelectPipeNodeButtonByIndices(pcNode.parent_pipe.getPipeUsedIdx(),
                                                                                   pcNode.parent_pipenode.getNodeIdx()
                                                                                   );
                              }
                            } // if pcNode
                        } // b_clear_modifier
                     }
                  }
               } // if !b_footswitch
               else
               {
                  // Footswitch Mode: Right Split Fader overview (single track GP1..GP8)
                  if(b_fader_overview_row_mode && (7 == rowIdx))
                  {
                     // Top row track selection
                     if(noteOffset < NUM_TRACKS)
                     {
                        last_selected_track_idx = noteOffset;
                        ColorizePads(-1, true/*bUpdateLastSelected*/);
                     }
                  }
                  else
                  {
                     // Edit fader
                     ctlIdx = 1 + rowIdx + 8;
                     numCels = 25 - split_point - 1;
                     bCtlLeft = false;
                     colOffset = 0;
                     noteOffsetRel = noteOffset;
                  }
               }
            }
            else
            {
               // Toolbar pad
               noteOffset = -1;
               last_note_offsets[ev.midiCh] = -1;
            }
         }
         else
         {
            // Left-hand side
            if(!b_footswitch)
            {
               // Track 1..8 MW faders
               ctlIdx = CTL_MW;
               numCels = split_point;
               bCtlLeft = true;
               trackIdx += left_fader_track_offset;
               last_selected_track_idx = trackIdx;
            }
            else
            {
               // Footswitch Mode: Current track MW/BC/EXPR/AT/PB/BRITE/FX1/FX2 faders
               ctlIdx = 1 + rowIdx;
               numCels = split_point;
               bCtlLeft = true;
               trackIdx = last_selected_track_idx;
            }
         }

         if((-1 != ctlIdx) && (-1 != noteOffset))
         {
            trackIdx = b_footswitch ? last_selected_track_idx : trackIdx;
            ctlState <= ctl_states.get(trackIdx);

            if(b_clear_modifier)
            {
               int clearCtlIdx;
               // trace "xxx b_clear_modifier, bCtlLeft="+bCtlLeft+" noteOffset="+noteOffset+" current_fader_page="+current_fader_page;

               if(bCtlLeft)
               {
                  if(!b_footswitch)
                  {
                     if(b_discardctl && b_discardctl_collect)
                     {
                        ctlState[CTL_MW]    = -1;
                        ctlState[CTL_BC]    = -1;
                        ctlState[CTL_EXPR]  = -1;
                        ctlState[CTL_AT]    = -1;  // [19Feb2021]
                        ctlState[CTL_PB]    = -1;  // [19Feb2021]
                        ctlState[CTL_BRITE] = -1;  // [19Feb2021]
                        Global.PrintFromReplay("Clear Track["+(trackIdx+1)+"] MW, BC, EXPR, AT, PB, BRITE");
                     }
                     else if(b_discardctl && !b_discardctl_collect)
                     {
                        ctlState[CTL_PAT]   = -1;
                        ctlState[CTL_AT]    = -1;  // [19Feb2021]
                        ctlState[CTL_PB]    = -1;  // [19Feb2021]
                        ctlState[CTL_BRITE] = -1;  // [19Feb2021]
                        Global.PrintFromReplay("Clear Track["+(trackIdx+1)+"] PAT");
                     }
                     else
                     {
                        ctlState[CTL_PAT]   = -1;
                        ctlState[CTL_MW]    = -1;
                        ctlState[CTL_BC]    = -1;
                        ctlState[CTL_EXPR]  = -1;
                        ctlState[CTL_AT]    = -1;  // [19Feb2021]
                        ctlState[CTL_PB]    = -1;  // [19Feb2021]
                        ctlState[CTL_BRITE] = -1;  // [19Feb2021]
                        Global.PrintFromReplay("Clear Track["+(trackIdx+1)+"] PAT, MW, BC, EXPR, AT, PB, BRITE");
                     }
                     ColorizePads(trackIdx, true/*bUpdateLastSelected*/);
                  }
                  else
                  {
                     // Footswitch mode (current track)
                     clearCtlIdx = 1 + rowIdx;
                     ctlState[clearCtlIdx] = -1;
                     ColorizePads(rowIdx, true/*bUpdateLastSelected*/);
                     Global.PrintFromReplay("Clear Ctl["+clearCtlIdx+"] "+ctl_names.get(clearCtlIdx));
                  }
               }
               else
               {
                  if(!b_footswitch)
                  {
                     // Clear FX1 or FX2
                     ctlState[(0 == current_fader_page) ? CTL_FX1 : CTL_FX2] = -1;
                     ColorizePads(trackIdx, true/*bUpdateLastSelected*/);
                     Global.PrintFromReplay("Clear Track["+(trackIdx+1)+"] "+((0 == current_fader_page)?"FX1":"FX2"));
                  }
                  else
                  {
                     // Clear current track CTL_GP1..9
                     clearCtlIdx = 1 + rowIdx + 8;
                     ctlState[clearCtlIdx] = -1;
                     ColorizePads(rowIdx, true/*bUpdateLastSelected*/);
                     Global.PrintFromReplay("Clear Fader["+clearCtlIdx+"] "+ctl_names.get(clearCtlIdx));
                  }
               }
            }
            else
            {
               // no clear modifier
               // trace "xxx lsarr: note-on noteOffsetRel="+noteOffsetRel+" numCels="+numCels+" trackIdx="+trackIdx+" ctlIdx="+ctlIdx;
               byte ccVal = noteOffsetRel * (127.0 / (numCels - 1));
               last_note_offsets[ev.midiCh] = noteOffset;
               // trace "xxx mwDevIdx="+mwDevIdx+" noteOffset="+noteOffset+" mwVal="+mwVal;
               if(ccVal > 127)
                  ccVal = 127;

               // trace "xxx rowIdx="+rowIdx+" ccVal="+ccVal;

               if(!b_discardctl)
               {
                  ctlState[ctlIdx] = ccVal;
                  SendCtl(framePlay, trackIdx, ctlIdx);
                  ColorizePads(b_footswitch ? rowIdx : trackIdx, true/*bUpdateLastSelected*/);
                  // if(![CTL_AT, CTL_PB, CTL_BRITE].contains(ctlIdx))
                     TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ccVal);
               }
               if(b_discardctl_collect)
               {
                  if(!b_discardctl_collect_discard)
                  {
                     ctlState[ctlIdx] = ccVal;
                     SendCtl(discardctl_collect_frame, trackIdx, ctlIdx);
                     ColorizePads(b_footswitch ? rowIdx : trackIdx, true/*bUpdateLastSelected*/);
                     // if(![CTL_AT, CTL_PB, CTL_BRITE].contains(ctlIdx))
                        TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ccVal);
                  }
               }
            }
         }

         frameRec.deleteEventByIdx(ev.index, -1);//MIDIPIPE_EVENT_TYPE_NOTE_ON);
         numEvents--;
      }
   }
}

function HandlePitchbend(MIDIPipeFrame frameRec, MIDIPipeFrame framePlay) {
   // trace "xxx rec numEv="+frameRec.numEvents;

   int trackIdx;
   int colOffset;
   int trackW = (25 - split_point - 1) / 2;  // == 9 columns/pads at split_point=6

   int numEvents = frameRec.getNumEventsPitchbendByFlt(linn_mapped_dev, -1/*all ch*/);
   // if(numEvents > 0)
   //    if(b_debug) trace "xxx Script<linnstrument_arranger>: numEventsPB="+numEvents;
   if(0 != numEvents)
   {
      MIDIPipeEvent ev;
      int evIdx = 0;
      while(evIdx < numEvents)
      {
         frameRec.getEventByIdxAndFlt(evIdx, ev, MIDIPIPE_EVENT_TYPE_PITCHBEND, linn_mapped_dev, -1/*all ch*/);

         int rowIdx = ev.midiCh & 7;
         float noteOffset;
         IntArray *ctlState;
         int ctlIdx = -1;
         byte ccVal;
         int numCels;

         if(ev.midiCh < 8)
         {
            // left
            trackIdx = ev.midiCh;
            colOffset = 0;
         }
         else
         {
            // right
            // trace "xxx pb last_note_offsets.get("+ev.midiCh+")="+last_note_offsets.get(ev.midiCh);
            if(last_note_offsets.get(ev.midiCh) >= trackW)
            {
               trackIdx = ev.midiCh;
               colOffset = trackW;
            }
            else
            {
               trackIdx = ev.midiCh - 8;
               colOffset = 0;
            }
         }

         if(!b_footswitch)
         {
            ctlState <= ctl_states.get(trackIdx);

            if(ev.midiCh < 8)
            {
               // Left:
               ctlIdx = CTL_MW;
               numCels = split_point;
               trackIdx += left_fader_track_offset;
               ctlState <= ctl_states.get(trackIdx);
            }
            else
            {
               // Right:
               if(-1 != current_fader_page)
               {
                  ctlIdx = (0 == current_fader_page) ? CTL_FX1 : CTL_FX2;
                  numCels = trackW;/////25 - split_point - 1;
               }
               else if(b_right_send_pat_xyz)
               {
                  if(-1 != last_note_offsets.get(ev.midiCh))
                  {
                     // trace "xxx pb mwDevIdx="+mwDevIdx+" noteOffset="+noteOffset+" ccVal="+ccVal;
                     short pbVal = mathClampi( ((ev.pitchbend-8192)*12)+8192, 0, 16383);
                     if(!b_discardctl)
                     {
                        ctlState[CTL_PB] = pbVal;
                        SendCtl(framePlay, trackIdx, CTL_PB);
                        TraceCtl(ctl_names.get(CTL_PB)+" is "+pbVal);
                     }
                     if(b_discardctl_collect)
                     {
                        if(!b_discardctl_collect_discard)
                        {
                           ctlState[CTL_PB] = pbVal;
                           SendCtl(discardctl_collect_frame, trackIdx, CTL_PB);
                           TraceCtl(ctl_names.get(CTL_PB)+" is "+pbVal);
                        }
                     }
                  }
               }
            }
         }
         else
         {
            // Footswitch / Fader overview mode (current track)
            ctlState <= ctl_states.get(last_selected_track_idx);
            ctlIdx = 1 + ev.midiCh;
            if(ev.midiCh < 8)
            {
               // left (MW/BC/EXPR/AT/PB/BRITE/FX1/FX2 faders)
               numCels = split_point;
            }
            else
            {
               // right (GP1..9 faders)
               numCels = 25 - split_point - 1;
            }
            colOffset = 0;
         }

         if(-1 != ctlIdx)
         {
            if(!b_clear_modifier)
            {
               // trace "xxx rowIdx="+rowIdx+" pb="+ev.pitchbend;
               noteOffset = last_note_offsets.get(ev.midiCh);

               if(-1 != noteOffset)
               {
                  float noteOffsetRel = noteOffset - colOffset;
                  noteOffsetRel += ((ev.pitchbend - 8192) / 8191.0) * 24;
                  // trace "xxx lsarr: pb noteOffset="+noteOffset;
                  ccVal =  mathClampi(noteOffsetRel * (127.0 / (numCels-1)), 0, 127);

                  // trace "xxx pb noteOffset="+noteOffset+" noteOffsetRel="+noteOffsetRel+" ccVal="+ccVal;

                  if(!b_discardctl)
                  {
                     ctlState[ctlIdx] = ccVal;
                     SendCtl(framePlay, last_selected_track_idx, ctlIdx);
                     ColorizePads(trackIdx, true/*bUpdateLastSelected*/);
                     // if(![CTL_AT, CTL_PB, CTL_BRITE].contains(ctlIdx))
                        TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ccVal);
                  }
                  if(b_discardctl_collect)
                  {
                     if(!b_discardctl_collect_discard)
                     {
                        ctlState[ctlIdx] = ccVal;
                        SendCtl(discardctl_collect_frame, last_selected_track_idx, ctlIdx);
                        ColorizePads(trackIdx, true/*bUpdateLastSelected*/);
                        // if(![CTL_AT, CTL_PB, CTL_BRITE].contains(ctlIdx))
                           TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ccVal);
                     }
                  }
               }
            }
         }

         evIdx++;
      }

      frameRec.deletePitchbendByFlt(linn_mapped_dev, -1);
      frameRec.deletePitchbendByFlt(linn_dev, -1);
   }
}

function HandlePolyAT(MIDIPipeFrame frameRec, MIDIPipeFrame framePlay) {

   int trackIdx;
   int colOffset;
   int trackW = (25 - split_point - 1) / 2;  // == 9 columns/pads at split_point=6

   // trace "xxx rec numEv="+frameRec.numEvents;
   int numEvents = frameRec.getNumEventsPolyPressureByFlt(linn_mapped_dev, -1/*all ch*/);
   // if(numEvents > 0)
   //    if(b_debug) trace "xxx Script<linnstrument_arranger>: numEvents="+numEvents;
   if(0 != numEvents)
   {
      MIDIPipeEvent ev;
      int evIdx = 0;
      while(evIdx < numEvents)
      {
         frameRec.getEventByIdxAndFlt(evIdx, ev, MIDIPIPE_EVENT_TYPE_POLYPRESSURE, linn_mapped_dev, -1/*all ch*/);

         int rowIdx = ev.midiCh & 7;
         IntArray *ctlState;

         if(ev.midiCh < 8)
         {
            // left
            trackIdx = ev.midiCh;
            colOffset = 0;
         }
         else
         {
            // right
            if((ev.polyPressureNote - prgchg_midi_base_note/*C-3*/) >= trackW)
            {
               trackIdx = ev.midiCh;
               colOffset = trackW;
            }
            else
            {
               trackIdx = ev.midiCh - 8;
               colOffset = 0;
            }
         }

         if(!b_footswitch)
         {
            ctlState <= ctl_states.get(trackIdx);

            if(ev.midiCh < 8)
            {
               // Left split
               if(b_left_send_zy)
               {
                  if(!b_clear_modifier)
                  {
                     if(!b_discardctl)
                     {
                        ctlState[CTL_BC] = ev.polyPressureValue;
                        SendCtl(framePlay, trackIdx, CTL_BC);
                        TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(CTL_BC)+" is "+ev.polyPressureValue);
                     }
                     if(b_discardctl_collect)
                     {
                        if(!b_discardctl_collect_discard)
                        {
                           ctlState[CTL_BC] = ev.polyPressureValue;
                           SendCtl(discardctl_collect_frame, trackIdx, CTL_BC);
                           TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(CTL_BC)+" is "+ev.polyPressureValue);
                        }
                     }
                  }
               }
            }
            else
            {
               // Right
               if(-1 != last_note_offsets[ev.midiCh])
               {
                  if(-1 != current_fader_page)
                  {
                     if(b_right_send_zy)
                     {
                        if(!b_clear_modifier)
                        {
                           int ctlIdx = (0 == current_fader_page) ? CTL_GP1 : CTL_GP3;
                           if(!b_discardctl)
                           {
                              ctlState[ctlIdx] = ev.polyPressureValue;
                              SendCtl(framePlay, trackIdx, ctlIdx);
                              TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ev.polyPressureValue);
                           }
                           if(b_discardctl_collect)
                           {
                              if(!b_discardctl_collect_discard)
                              {
                                 ctlState[ctlIdx] = ev.polyPressureValue;
                                 SendCtl(discardctl_collect_frame, trackIdx, ctlIdx);
                                 // TraceCtl(ctl_names.get(ctlIdx)+" is "+ev.polyPressureValue);
                              }
                           }
                        }
                     }
                  }
                  else if(b_right_send_pat_xyz)
                  {
                     if(!b_clear_modifier)
                     {
                        if(!b_discardctl)
                        {
                           ctlState[CTL_AT] = ev.polyPressureValue;
                           SendCtl(framePlay, trackIdx, CTL_AT);
                           // TraceCtl(ctl_names.get(CTL_AT)+" is "+ev.polyPressureValue);
                        }
                        if(b_discardctl_collect)
                        {
                           if(!b_discardctl_collect_discard)
                           {
                              ctlState[CTL_AT] = ev.polyPressureValue;
                              SendCtl(discardctl_collect_frame, trackIdx, CTL_AT);
                              // TraceCtl(ctl_names.get(CTL_AT)+" is "+ev.polyPressureValue);
                           }
                        }
                     }
                  }
               }
            }
         } // if !b_footswitch

         evIdx++;
      }

      frameRec.deletePolyPressureByFlt(linn_mapped_dev, -1);
      frameRec.deletePolyPressureByFlt(linn_dev, -1);
   }
}

function HandleCC(MIDIPipeFrame frameRec, MIDIPipeFrame framePlay) {

   int trackIdx;
   int colOffset;
   int trackW = (25 - split_point - 1) / 2;  // == 9 columns/pads at split_point=6

   // if(frameRec.numEventsCC > 0)
   //    trace "xxx rec numEv="+frameRec.numEventsCC;

   int numEvents = frameRec.getNumEventsCCByFlt(linn_mapped_dev, -1/*all ch*/);
   if(0 != numEvents)
   {
      // if(b_debug) trace "xxx Script<linnstrument_arranger>: numEvents="+numEvents;
      MIDIPipeEvent ev;
      int evIdx = 0;
      while(evIdx < numEvents)
      {
         frameRec.getEventByIdxAndFlt(evIdx, ev, MIDIPIPE_EVENT_TYPE_CC, linn_mapped_dev, -1/*all ch*/);

         int rowIdx = ev.midiCh & 7;

         if(ev.midiCh < 8)
         {
            // left
            trackIdx = ev.midiCh;
            colOffset = 0;
         }
         else
         {
            // right
            if(last_note_offsets.get(ev.midiCh) >= trackW)
            {
               trackIdx = ev.midiCh;
               colOffset = trackW;
            }
            else
            {
               trackIdx = ev.midiCh - 8;
               colOffset = 0;
            }
         }

         IntArray ctlState <= ctl_states.get(trackIdx);

         // (note) the linnstrument sends the switch CC on channels 0..7
         if((0 == ev.midiCh) && (65/*portamento sw*/ == ev.ccId))
         {
            boolean bSW = (ev.ccValue > 0);
            if(b_invert_footswitch_polarity)
               bSW = !bSW;
            // trace "xxx sustain ev.ccValue="+ev.ccValue+" b_footswitch="+b_footswitch+" mode="+b_footswitch_mode;

            if(0/*momentary*/ == b_footswitch_mode)
               b_footswitch = bSW;
            else if(bSW)
               b_footswitch = !b_footswitch;

            if(!b_footswitch)
            {
               // Leave fader overview mode
               last_note_offsets.fill(-1);
            }
            else
            {
               // Enter fader overview mode
            }
            ColorizePads(-1, true/*bUpdateLastSelected*/);
         }

         if(!b_footswitch)
         {
            if(ev.midiCh < 8)
            {
               // Left
               if(!b_clear_modifier)
               {
                  if(74/*brightness*/ == ev.ccId)
                  {
                     if(b_left_send_zy)
                     {
                        if(!b_discardctl)
                        {
                           ctlState[CTL_EXPR] = ev.ccValue;
                           SendCtl(framePlay, trackIdx, CTL_EXPR);
                           TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(CTL_EXPR)+" is "+ev.ccValue);
                        }
                        if(b_discardctl_collect)
                        {
                           if(!b_discardctl_collect_discard)
                           {
                              ctlState[CTL_EXPR] = ev.ccValue;
                              SendCtl(discardctl_collect_frame, trackIdx, CTL_EXPR);
                              TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(CTL_EXPR)+" is "+ev.ccValue);
                           }
                        }
                     }
                  }
               }
            }
            else
            {
               // Right
               if(74/*brightness*/ == ev.ccId)
               {
                  if(-1 != last_note_offsets[ev.midiCh])
                  {
                     if(-1 != current_fader_page)
                     {
                        if(!b_clear_modifier)
                        {
                           if(b_right_send_zy)
                           {
                              int ctlIdx = (0 == current_fader_page) ? CTL_GP2/*general purpose 2*/ : CTL_GP4/*general purpose 4*/;
                              if(!b_discardctl)
                              {
                                 ctlState[ctlIdx] = ev.ccValue;
                                 SendCtl(framePlay, trackIdx, ctlIdx);
                                 TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ev.ccValue);
                              }
                              if(b_discardctl_collect)
                              {
                                 if(!b_discardctl_collect_discard)
                                 {
                                    ctlState[ctlIdx] = ev.ccValue;
                                    SendCtl(discardctl_collect_frame, trackIdx, ctlIdx);
                                    TraceCtl("Track ["+(trackIdx+1)+"] "+ctl_names.get(ctlIdx)+" is "+ev.ccValue);
                                 }
                              }
                           }
                        }
                     }
                     else if(b_right_send_pat_xyz)
                     {
                        if(!b_discardctl)
                        {
                           ctlState[CTL_BRITE] = ev.ccValue;
                           SendCtl(framePlay, trackIdx, CTL_BRITE);
                           // TraceCtl(ctl_names.get(CTL_BRITE)+" is "+ev.ccValue);
                        }
                        if(b_discardctl_collect)
                        {
                           if(!b_discardctl_collect_discard)
                           {
                              ctlState[CTL_BRITE] = ev.ccValue;
                              SendCtl(discardctl_collect_frame, trackIdx, CTL_BRITE);
                              // TraceCtl(ctl_names.get(CTL_BRITE)+" is "+ev.ccValue);
                           }
                        }
                     }
                  }
               } // if 74==cc
            }
         }

         evIdx++;
      }

      frameRec.deleteCCByFlt(linn_mapped_dev, -1, -1);
      frameRec.deleteCCByFlt(linn_dev, -1, -1);
   }
}

function HandleSceneSelectNotes(MIDIPipeFrame _frameRec, MIDIPipeFrame _framePlay) {
   int numEv = _frameRec.getNumEventsNoteOnByFlt(in_sceneselect_dev, in_sceneselect_ch);
   if(numEv > 0)
   {
      int evIdx = 0;
      MIDIPipeEvent pev;
      loop(numEv)
      {
         _frameRec.getEventByIdxAndFlt(evIdx, pev, MIDIPIPE_EVENT_TYPE_NOTE_ON, in_sceneselect_dev, in_sceneselect_ch);

         if(scene_select_base_note <= pev.note < (scene_select_base_note + num_scene_select_notes))
         {
            SelectScene(_framePlay, (pev.note - scene_select_base_note)/*scene idx*/);
         }

         // Next note-on
         evIdx++;
      }
   }
}

function SaveStatePreset(Stream ofs, int _presetIdx) {
   // ver
   ofs.i16 = 2;

   // Name
   Utils.WriteString(ofs, preset_names.get(_presetIdx));

   int trackIdx = 0;
   StringArray portCfg     <= left_modulation_ports_presets.get(_presetIdx);
   IntArray    forcedPatPg <=          forced_patpg_presets.get(_presetIdx);
   IntArray    mtxGrps     <=          mutex_groups_presets.get(_presetIdx);
   IntArray    evQuant     <=    event_quantization_presets.get(_presetIdx);
   loop(16)
   {
      // Device
      Utils.WriteString(ofs, portCfg.get(trackIdx*2+0));

      // Ch
      ofs.i8 = int(portCfg.get(trackIdx*2+1))-1;

      // Forced PatPg
      ofs.i8 = forcedPatPg.get(trackIdx);

      // Mutex Group
      ofs.i8 = mtxGrps.get(trackIdx);

      // Event quantization
      ofs.i16 = evQuant.get(trackIdx);

      // Next track
      trackIdx++;
   }
}

function LoadStatePreset(Stream ifs, int _presetIdx) {
   short ver = ifs.u16;
   // trace "xxx linnstrument::LoadStatePreset: presetIdx="+_presetIdx+" ver="+ver;
   if(ver >= 1)
   {
      // Name
      Utils.ReadString(ifs, preset_names[_presetIdx]);

      int trackIdx = 0;
      StringArray portCfgMod    <= left_modulation_ports_presets.get(_presetIdx);
      StringArray portCfgNote   <=       left_note_ports_presets.get(_presetIdx);
      StringArray portCfgPrgChg <=    right_prgchg_ports_presets.get(_presetIdx);
      IntArray    forcedPatPg   <=          forced_patpg_presets.get(_presetIdx);
      IntArray    mtxGrps       <=          mutex_groups_presets.get(_presetIdx);
      IntArray    evQuant       <=    event_quantization_presets.get(_presetIdx);
      loop(16)
      {
         // Device
         Utils.ReadString(ifs, portCfgMod.get(trackIdx*2+0));
         portCfgNote[trackIdx*2+0] = portCfgMod.get(trackIdx*2+0);
         portCfgPrgChg[trackIdx*2+0] = portCfgMod.get(trackIdx*2+0);

         // Ch
         portCfgMod[(trackIdx*2+1)] = String(ifs.u8 + 1);
         portCfgNote[trackIdx*2+1] = portCfgMod[(trackIdx*2+1)];
         portCfgPrgChg[trackIdx*2+1] = portCfgMod[(trackIdx*2+1)];

         // Forced PatPg
         forcedPatPg[trackIdx] = ifs.s8;

         // Mutex Group
         mtxGrps[trackIdx] = ifs.s8;

         if(ver >= 2)
         {
            // Event Quantization (v2+)
            evQuant[trackIdx] = ifs.s16;
         }

         // Next track
         trackIdx++;
      }

      return true;
   }
   return false;
}

function OnSaveState(Stream ofs) {
   // Version
   ofs.i16 = 3;

   // Write scenes
   ofs << scene_ctl_states;

   // Write current (unsaved) scene
   ofs << ctl_states;

   // Write footswitch (fader overview) state
   ofs.i8 = b_footswitch;

   // Write fader overview row selection mode state
   ofs.i8 = b_fader_overview_row_mode;

   // Write global_setting_arranger (v2+)
   ofs.i8 = global_setting_arranger;

   // Write global_setting_default (v2+)
   ofs.i8 = global_setting_default;

   // Write presets (v3+)
   ofs.i8 = preset_names.numElements;
   int presetIdx = 0;
   loop(preset_names.numElements)
      SaveStatePreset(ofs, presetIdx++);
}

function OnLoadState(Stream ifs) {
   // Version
   short ver = ifs.i16;

   if(ver >= 1)
   {
      // Read scenes
      scene_ctl_states << ifs;

      // Read current (unsaved) scene
      ctl_states << ifs;

      // Read footswitch (fader overview) state
      b_footswitch = ifs.b8;

      // Read fader overview row selection mode state
      b_fader_overview_row_mode = ifs.b8;

      if(ver >= 2)
      {
         // Read global_setting_arranger (v2+)
         global_setting_arranger = ifs.u8;

         // Read global_setting_default (v2+)
         global_setting_default = ifs.u8;

         if(ver >= 3)
         {
            // Load presets (v3+)
            int numPresets = ifs.u8;
            int presetIdx = 0;
            loop(numPresets)
               LoadStatePreset(ifs, presetIdx++);
         }

         UpdatePresetComboBoxes();
      }

      SelectPreset(p_preset);
   }
}

function OnProcess(MIDIPipeFrame framePlay,
                   MIDIPipeFrame frameRec,
                   boolean       bMuted,
                   boolean       bPlaySeq
                   ) {
   // if(frameRec.numEvents > 0)
   //    trace "xxx frameRec.numEvents="+frameRec.numEvents+" b_usermode="+b_usermode;
   // if(framePlay.numEvents > 0)
   //    trace "xxx framePlay.numEvents="+framePlay.numEvents;

   if(b_usermode ^ b_last_usermode_state)
   {
      b_last_usermode_state = b_usermode;
      trace "[dbg] script_linnstrument_arranger: usermode is now "+b_usermode;
   }

   if(b_usermode)
      return;

   if(bMuted ^ b_last_mute_state)
   {
      // trace "xxx lsarr: mute changed";
      if(bMuted)
      {
         ColorizePads_Default();
         UpdatePadColors(false/*bProgressive*/);

         if(b_autoload_global_setting)
         {
            LoadGlobalSetting(global_setting_default);
         }
      }
      else
      {
         if(b_autoload_global_setting)
         {
            LoadGlobalSetting(global_setting_arranger);
         }

         ColorizePads(-1, true/*bUpdateLastSelected*/);
      }
      b_reset = false;
   }

   b_last_mute_state = bMuted;

   int inDevIdx;
   STX_MIDIOutDevice *outDev;

   if(!bMuted)
   {
      if((milliSeconds() - last_pattern_rescan_ms) >= pattern_rescan_interval)
      {
         int changedTrackIdx = HasPatternChanged();
         if(-1 != changedTrackIdx)
         {
            ColorizePads(changedTrackIdx, true/*bUpdateLastSelected*/);
            // (note) don't update rescan_ms so all rows are rescanned
         }
         else
         {
            last_pattern_rescan_ms = milliSeconds();
         }
      }

      if(b_reset)
      {
         b_reset = false;
         ColorizePads(-1, true/*bUpdateLastSelected*/);
         outDev <= MIDI.GetMIDIOutDeviceByIndex(linn_dev);
         if(null != outDev)
         {
            inDevIdx = MIDI.FindInDeviceIdxForOutDevice(outDev.getAliasOrDeviceName());
            if(-1 != inDevIdx)
            {
               st2::replay.excludeMIDIMapInDev(inDevIdx);
            }
            else
            {
               trace "[~~~] Linnstrument: script: failed to find input device idx for outDev=\""+outDev.getAliasOrDeviceName()+"\" ! MIDI learn will not work correctly.";
            }
         }
      }

      MIDIPipeFrame fr; fr.empty();

      boolean bHandleEv = (0 == globalsettings_grace_ms);

      if(!bHandleEv)
      {
         // Workaround for CC#74 glitch in Linnstrument FW (sometimes resets to 0 after loading settings)
         if(milliSeconds() >= (globalsettings_grace_ms + 500))
         {
            globalsettings_grace_ms = 0;
            bHandleEv = true;
         }
      }

      if(bHandleEv)
      {
         HandleNotes(frameRec, fr);
         HandlePatternSelectAndCCFader(frameRec, fr);
         HandlePitchbend(frameRec, fr);
         HandlePolyAT(frameRec, fr);
         HandleCC(frameRec, fr);
         HandleSceneSelectNotes(frameRec, fr);  // in_sceneselect port
      }

      frameRec.mergeFrame(fr);
      MergeFutureFrame(frameRec);

      if(!b_discardctl && b_discardctl_merge)
      {
         if(b_debug) trace "[dbg] Script<linnstrument_arranger>: merge ctl frame ("+discardctl_collect_frame.numEvents+" event(s))";
         frameRec.mergeFrame(discardctl_collect_frame);
         discardctl_collect_frame.empty();
         b_discardctl_merge = false;
      }

      tick_nr++;

      if(b_scene || (b_footswitch && b_fader_overview_row_mode))
      {
         if( (0 == (tick_nr % 192)) || (24 == (tick_nr % 192)) )
         {
            // Highlight last selected scene
            ColorizePads(6, false/*bUpdateLastSelected*/);
            ColorizePads(7, false/*bUpdateLastSelected*/);
         }
      }

      if(b_update_pad_colors)
      {
         UpdatePadColors(true/*bProgressive*/);
      }
   }
   else
   {
      // muted
      if(b_reset)
      {
         b_reset = false;
         outDev <= MIDI.GetMIDIOutDeviceByIndex(linn_dev);
         if(null != outDev)
         {
            inDevIdx = MIDI.FindInDeviceIdxForOutDevice(outDev.getAliasOrDeviceName());
            if(-1 != inDevIdx)
            {
               st2::replay.includeMIDIMapInDev(inDevIdx);
            }
         }
      }
   }
}
