/*
 * Electrix Tweaker controller script 1.1.1 for Mixxx 1.12
 * Copyright (C) 2015 Be <be.0@gmx.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * See http://mixxx.org/wiki/doku.php/electrix_tweaker for instructions on how to use this mapping
 */

var ElectrixTweaker = function() {};

// ====================================================== USER OPTIONS =========================================================

// Set this to false if you do not want the volume of samples to be proportional to how hard you press the big buttons. When this is false, the sampler buttons will play the samplers at whatever volume the sample deck is set to.
ElectrixTweaker.samplerVelocityAsVolume = true;
// The higher this is, the less hard you have to strike the sample pads to play samples loudly (when ElectrixTweaker.samplerVelocityAsVolume is true).
ElectrixTweaker.samplerSensitivity = 4;

// Adjust sensitivity of EQs (range 1-7, only use integers)
ElectrixTweaker.eqSensitivity = 6;

// Set these to true to enable vinyl mode for that deck on startup. This will also enable vinyl control on startup.
ElectrixTweaker.vinylMode = {"[Channel1]": false, "[Channel2]": false, "[Channel3]": false, "[Channel4]": false};



// ==================================================== GLOBAL VARIABLES =======================================================

ElectrixTweaker.colorCodes = {
    "off": 0,
    "green": 1,
    "red": 4,
    "yellow": 8,
    "blue": 16,
    "cyan": 32,
    "magenta": 64,
    "white": 127
};
ElectrixTweaker.deckColor = {
    "[Channel1]": ElectrixTweaker.colorCodes.blue,
    "[Channel2]": ElectrixTweaker.colorCodes.blue,
    "[Channel3]": ElectrixTweaker.colorCodes.magenta,
    "[Channel4]": ElectrixTweaker.colorCodes.magenta
};
ElectrixTweaker.hotcueColors = [
    ElectrixTweaker.colorCodes.cyan,
    ElectrixTweaker.colorCodes.green,
    ElectrixTweaker.colorCodes.red,
    ElectrixTweaker.colorCodes.white
];

ElectrixTweaker.encoders = {
    "[Channel1]": {
        "High": {
            "cc": 57,
            "ring": 79,
            "button": 45
        },
        "Mid": {
            "cc": 58,
            "ring": 80,
            "button": 46
        },
        "Low": {
            "cc": 59,
            "ring": 81,
            "button": 47
        }
    },
    "[Channel2]": {
        "High": {
            "cc": 60,
            "ring": 82,
            "button": 48
        },
        "Mid": {
            "cc": 61,
            "ring": 83,
            "button": 49
        },
        "Low": {
            "cc": 62,
            "ring": 84,
            "button": 50
        }
    }
};
ElectrixTweaker.encoders["[Channel3]"] = ElectrixTweaker.encoders["[Channel1]"];
ElectrixTweaker.encoders["[Channel4]"] = ElectrixTweaker.encoders["[Channel2]"];
// each consecutive value in this array sets the encoder ring LEDs to the show the next LED
ElectrixTweaker.encoderRingSteps = [0, 10, 25, 35, 50, 60, 64, 75, 85, 95, 105, 115, 127];
ElectrixTweaker.encoderRingStepsFill = [0, 1, 20, 30, 40, 50, 60, 64, 75, 85, 95, 105, 120, 127];
ElectrixTweaker.buttons = {
    "[Channel1]": {
        "arrowSide": 42,

        "mode": 33,
        "pfl": 34,
        "play": 35,


        "hotcues": [
            [1, 2, 3, 4],
            [9, 10, 11, 12]
        ],

        "forward": 17,
        "slip": 18,
        "shift": 19,
        "deckToggle": 20,

        "back": 25,
        "quantize": 26,
        "key": 27,
        "sync": 28
    },
    "[Channel2]": {
        "mode": 36,
        "pfl": 37,
        "play": 38,

        "arrowSide": 43,


        "hotcues": [
            [5, 6, 7, 8],
            [13, 14, 15, 16]
        ],

        "forward": 21,
        "slip": 22,
        "shift": 23,
        "deckToggle": 24,

        "back": 29,
        "quantize": 30,
        "key": 31,
        "sync": 32

    }
};
ElectrixTweaker.buttons["[Channel3]"] = ElectrixTweaker.buttons["[Channel1]"];
ElectrixTweaker.buttons["[Channel4]"] = ElectrixTweaker.buttons["[Channel2]"];

ElectrixTweaker.shift = false;
ElectrixTweaker.topShift = false;
ElectrixTweaker.deckShift = {"[Channel1]": false, "[Channel2]": false, "[Channel3]": false, "[Channel4]": false};
ElectrixTweaker.deck = {"[Channel1]": "[Channel1]", "[Channel2]": "[Channel2]"};
ElectrixTweaker.mode = {"[Channel1]": "eq", "[Channel2]": "eq", "[Channel3]": "eq", "[Channel4]": "eq"};
ElectrixTweaker.loopMoveSize = {"[Channel1]": 1, "[Channel2]": 1, "[Channel3]": 1, "[Channel4]": 1};
ElectrixTweaker.loopSize = {"[Channel1]": 4, "[Channel2]": 4, "[Channel3]": 4, "[Channel4]": 4};
ElectrixTweaker.slipMode = {"[Channel1]": false, "[Channel2]": false, "[Channel3]": false, "[Channel4]": false};
ElectrixTweaker.deckShift = {"[Channel1]": false, "[Channel2]": false, "[Channel3]": false, "[Channel4]": false};
ElectrixTweaker.hotcuePage = {"[Channel1]": 0, "[Channel2]": 0, "[Channel3]": 0, "[Channel4]": 0};
ElectrixTweaker.hotcuesPressed = {"[Channel1]": 0, "[Channel2]": 0, "[Channel3]": 0, "[Channel4]": 0};
ElectrixTweaker.playPressedWhileCueJuggling = {"[Channel1]": false, "[Channel2]": false, "[Channel3]": false, "[Channel4]": false};
ElectrixTweaker.slipModeUnsetWhileLooping = {"[Channel1]": false, "[Channel2]": false, "[Channel3]": false, "[Channel4]": false};
ElectrixTweaker.midEncoderLEDTimer = {"[Channel1]": 0, "[Channel2]": 0, "[Channel3]": 0, "[Channel4]": 0};
ElectrixTweaker.midEncoderLEDTimer = {"[Channel1]": 0, "[Channel2]": 0, "[Channel3]": 0, "[Channel4]": 0};

ElectrixTweaker.sysexPrefix = [240, 0, 1, 106, 1];
ElectrixTweaker.defaultSettings = [
    [240, 0, 1, 106, 1, 1, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 0, 11, 0, 12, 0, 13, 0, 14, 0, 15, 0, 16, 0, 17, 0, 18, 0, 19, 0, 20, 0, 21, 0, 22, 0, 23, 0, 24, 0, 25, 0, 26, 0, 27, 0, 28, 0, 29, 0, 30, 0, 31, 0, 32, 0, 33, 0, 34, 0, 35, 0, 36, 0, 37, 0, 38, 0, 39, 0, 40, 0, 41, 0, 42, 0, 43, 0, 44, 0, 45, 0, 46, 0, 47, 0, 48, 0, 49, 0, 50, 247],
    [240, 0, 1, 106, 1, 2, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 247],
    [240, 0, 1, 106, 1, 3, 0, 56, 0, 57, 0, 58, 0, 59, 0, 60, 0, 61, 0, 62, 247],
    [240, 0, 1, 106, 1, 4, 0, 63, 0, 64, 0, 65, 0, 66, 0, 67, 0, 68, 0, 69, 0, 70, 0, 71, 0, 72, 0, 73, 0, 74, 0, 75, 0, 76, 0, 77, 0, 78, 247],
    [240, 0, 1, 106, 1, 5, 126, 5, 1, 247],
    [240, 0, 1, 106, 1, 6, 127, 127, 15, 0, 7, 0, 9, 5, 247],
    [240, 0, 1, 106, 1, 7, 42, 42, 247],
    [240, 0, 1, 106, 1, 8, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 0, 11, 0, 12, 0, 13, 0, 14, 0, 15, 0, 16, 0, 17, 0, 18, 0, 19, 0, 20, 0, 21, 0, 22, 0, 23, 0, 24, 0, 25, 0, 26, 0, 27, 0, 28, 0, 29, 0, 30, 0, 31, 0, 32, 0, 33, 0, 34, 0, 35, 0, 36, 0, 37, 0, 38, 247],
    [240, 0, 1, 106, 1, 9, 0, 79, 0, 80, 0, 81, 0, 82, 0, 83, 0, 84, 247],
    [240, 0, 1, 106, 1, 13, 15, 247],
    [240, 0, 1, 106, 1, 14, 0, 247],
    [240, 0, 1, 106, 1, 15, 0, 247]
];
ElectrixTweaker.requestConfiguration = ElectrixTweaker.sysexPrefix.concat([126, 247]);

ElectrixTweaker.samplerRegEx = /\[Sampler(\d+)\]/;
ElectrixTweaker.channelRegEx = /\[Channel(\d+)\]/;

// ================================================= INITIALIZATION & SHUTDOWN ============================================

ElectrixTweaker.init = function() {
    if (engine.getValue("[App]", "num_samplers") < 8) {
        engine.setValue("[App]", "num_samplers", 8);
    }
    for (const group in ElectrixTweaker.encoders) { // loop over each [Channel]
        //         engine.softTakeover('[QuickEffectRack1_'+group+']', 'super1', true)
        //         engine.softTakeover(group, 'volume', true)
        // uncomment the line below when issue #8142 is fixed
        //         ElectrixTweaker.vinylMode[group] = engine.getValue(group, 'vinylcontrol_enabled')
        engine.setValue(group, "vinylcontrol_enabled", ElectrixTweaker.vinylMode[group]);
    }
    ElectrixTweaker.initDeck("[Channel1]");
    ElectrixTweaker.initDeck("[Channel2]");
    for (let i=1; i<=8; i++) {
        engine.connectControl("[Sampler"+i+"]", "track_samples", "ElectrixTweaker.oneShotLED");
        engine.trigger("[Sampler"+i+"]", "track_samples");
    }

    midi.sendShortMsg(0x90, 39, 127); // light up arrow
    midi.sendShortMsg(0x90, 40, 127); // light top shift button
    midi.sendShortMsg(0x90, 41, 127); // light down arrow
//     midi.sendSysexMsg(ElectrixTweaker.requestConfiguration, ElectrixTweaker.requestConfiguration.length)
//     for (var msg in ElectrixTweaker.defaultSettings) {
//         midi.sendSysexMsg(ElectrixTweaker.defaultSettings[msg], ElectrixTweaker.defaultSettings[msg].length)
//     }
};
// ElectrixTweaker.inboundSysex = function (data, length) {
//     print('========================== incoming sysex message ======================================')
//     print(length)
//     print(data)
//     if (length == 108) {
//         ElectrixTweaker.controllerConfiguration = data
//     }
// }

ElectrixTweaker.shutdown = function() {
    for (const group in ElectrixTweaker.encoders) {
        for (const encoder in ElectrixTweaker.encoders[group]) {
            // set encoder to absolute EQ mode with speed 5
            midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].cc, 118);
            // enable local control of LED ring
            midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].ring, 70);
            // set rings to center
            midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group][encoder].cc, 64);
            // turn off blue button lights
            midi.sendShortMsg(0x90, ElectrixTweaker.encoders[group][encoder].button, 0);
        }
    }
    // turn off all button LEDs
    for (let i = 0; i <= 70; i++) {
        midi.sendShortMsg(0x90, i, ElectrixTweaker.colorCodes.off);
    }
};

// ==================================================== MODE SWITCHING FUNCTIONS ================================================

ElectrixTweaker.initDeck = function(group) {
    let disconnectDeck = parseInt(ElectrixTweaker.channelRegEx.exec(group)[1]);
    if (disconnectDeck <= 2) {
        disconnectDeck += 2;
    } else {
        disconnectDeck -= 2;
    }
    disconnectDeck = "[Channel" + disconnectDeck + "]";
    ElectrixTweaker.connectDeckControls(disconnectDeck, true);
    // workaround for https://github.com/mixxxdj/mixxx/issues/8104
    for (let i=1; i <= 32; i++) {
        engine.setValue(disconnectDeck, "hotcue_"+i+"_activate", 0);
    }

    midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].shift, ElectrixTweaker.colorCodes.yellow);
    midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].deckToggle, ElectrixTweaker.deckColor[group]);
    midi.sendShortMsg(
        0x90,
        ElectrixTweaker.buttons[group].slip,
        ElectrixTweaker.slipMode[group] ? ElectrixTweaker.deckColor[group] : ElectrixTweaker.colorCodes.off
    );

    ElectrixTweaker.mode[group] = ElectrixTweaker.mode[disconnectDeck];

    ElectrixTweaker.connectDeckControls(group);
};

ElectrixTweaker.connectDeckControls = function(group, remove) {
    remove = (typeof remove !== "undefined") ? remove : false; // default value for remove is false

    const controlsToFunctions = {
        "pfl": "ElectrixTweaker.pflButtonLED",
        //         'track_samples': 'ElectrixTweaker.arrowSideLED', // the line below would overwrite this attribute
        "track_samples": "ElectrixTweaker.playButtonLED",
        "play": "ElectrixTweaker.playButtonLED",
        "playposition": "ElectrixTweaker.playButtonLED",
        "cue_indicator": "ElectrixTweaker.playButtonLED",
        "sync_enabled": "ElectrixTweaker.syncLED",
        "keylock": "ElectrixTweaker.keylockLED",
        "quantize": "ElectrixTweaker.quantizeLED"
    };
    engine.connectControl(group, "track_samples", "ElectrixTweaker.arrowSideLED", remove);
    for (const control in controlsToFunctions) {
        engine.connectControl(group, control, controlsToFunctions[control], remove);
        if (! remove) {
            engine.trigger(group, control);
        }
    }
    ElectrixTweaker.connectHotcuePage(group, remove);
    ElectrixTweaker.connectEncoderMode(group, ElectrixTweaker.mode[group], remove);

    if (ElectrixTweaker.vinylMode[group]) {
        ElectrixTweaker.connectVinylLEDs(group, remove);
    }
};

ElectrixTweaker.connectEncoderMode = function(group, mode, remove) {
    remove = (typeof remove !== "undefined") ? remove : false; // default value for remove is false
    switch (mode) {
    case "eq":
        midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].mode, ElectrixTweaker.colorCodes.white);
        for (const encoder in ElectrixTweaker.encoders[group]) {
            engine.connectControl(group, "filter" + encoder, "ElectrixTweaker.eqEncoderLEDs", remove);
            engine.connectControl(group, "filter" + encoder + "Kill", "ElectrixTweaker.eqEncoderKillButtonLED", remove);
            if (! remove) {
                // set encoder to absolute EQ mode with speed 5
                midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].cc, 70 + 8*ElectrixTweaker.eqSensitivity);
                // enable local control of LED ring
                midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].ring, 70);

                engine.trigger(group, "filter" + encoder);
                engine.trigger(group, "filter" + encoder + "Kill");
            }
        }
        break;
    case "loop":
        engine.connectControl(group, "loop_enabled", "ElectrixTweaker.loopButtonToggleLED", remove);
        if (! remove) {
            engine.trigger(group, "loop_enabled");
            midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].mode, ElectrixTweaker.colorCodes.magenta);
            for (const encoder in ElectrixTweaker.encoders[group]) {
                // set encoder to relative mode
                midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].cc, 64);
                // set LED ring to EQ mode with local control disabled
                midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].ring, 98);
            }

            midi.sendShortMsg(
                0xB0,
                ElectrixTweaker.encoders[group].High.ring,
                ElectrixTweaker.encoderRingSteps[
                    6 + Math.log(ElectrixTweaker.loopMoveSize[group]) / Math.log(2)
                ]
            );

            midi.sendShortMsg(
                0xB0,
                ElectrixTweaker.encoders[group].Mid.ring,
                64
            );

            midi.sendShortMsg(
                0xB0,
                ElectrixTweaker.encoders[group].Low.ring,
                ElectrixTweaker.encoderRingSteps[
                    6 + Math.log(ElectrixTweaker.loopSize[group]) / Math.log(2)
                ]
            );
        }
        break;
    }
};

ElectrixTweaker.connectHotcuePage = function(group, remove) {
    remove = (typeof remove !== "undefined") ? remove : false; // default value for remove is false

    const min = 1 + (ElectrixTweaker.hotcuePage[group] * 8);
    const max = min + 7;
    for (let i=min; i<=max; i++) {
        engine.connectControl(group, "hotcue_"+i+"_enabled", "ElectrixTweaker.hotcueLED", remove);
        if (! remove) {
            engine.trigger(group, "hotcue_"+i+"_enabled");
        }
    }
};

ElectrixTweaker.connectVinylLEDs = function(group, remove) {
    const controlsToFunctions = {
        "passthrough": "ElectrixTweaker.vinylStatusLED",
        "vinylcontrol_status": "ElectrixTweaker.vinylStatusLED",
        "vinylcontrol_mode": "ElectrixTweaker.vinylModeLED",
        "vinylcontrol_cueing": "ElectrixTweaker.vinylModeLED"
    };
    for (const control in controlsToFunctions) {
        engine.connectControl(group, control, controlsToFunctions[control], remove);
    }
    if (remove) {
        midi.sendShortMsg(
            0x90,
            ElectrixTweaker.buttons[group].back,
            (engine.getValue(group, "quantize")) ? ElectrixTweaker.colorCodes.white : ElectrixTweaker.colorCodes.green
        );
        midi.sendShortMsg(
            0x90,
            ElectrixTweaker.buttons[group].forward,
            (engine.getValue(group, "quantize")) ? ElectrixTweaker.colorCodes.white : ElectrixTweaker.colorCodes.green
        );
        midi.sendShortMsg(
            0x90,
            ElectrixTweaker.buttons[group].slip,
            (ElectrixTweaker.slipMode[group]) ? ElectrixTweaker.deckColor[group] : ElectrixTweaker.colorCodes.off
        );
    } else {
        for (const control in controlsToFunctions) {
            engine.trigger(group, control);
        }
    }
};

ElectrixTweaker.topShiftButton = function(channel, control, value, status, group) {
//     ElectrixTweaker.shift = ! ElectrixTweaker.shift
    ElectrixTweaker.topShift = ! ElectrixTweaker.topShift;

    ElectrixTweaker.connectEncoderMode(ElectrixTweaker.deck["[Channel1]"], ElectrixTweaker.mode[group], value/127);
    ElectrixTweaker.connectEncoderMode(ElectrixTweaker.deck["[Channel2]"], ElectrixTweaker.mode[group], value/127);
    if (value) {
        for (group in ElectrixTweaker.deck) {
            for (const encoder in ElectrixTweaker.encoders[group]) {
                // set encoder to absolute EQ mode with speed 5
                midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].cc, 70 + 8*ElectrixTweaker.eqSensitivity);
                // enable local control of LED ring
                midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group][encoder].ring, 70);
            }
            engine.connectControl(ElectrixTweaker.deck[group], "pregain", "ElectrixTweaker.gainLEDs");
            engine.trigger(ElectrixTweaker.deck[group], "pregain");
        }
    } else {
        for (group in ElectrixTweaker.deck) {
            engine.connectControl(ElectrixTweaker.deck[group], "pregain", "ElectrixTweaker.gainLEDs", true);
        }
    }

    const controlsToFunctions = {
        "volume": "ElectrixTweaker.masterGainLEDs",
        "balance": "ElectrixTweaker.masterBalanceLEDs",
        "headVolume": "ElectrixTweaker.headGainLEDs",
        "headMix": "ElectrixTweaker.headMixLEDs",
        "headSplit": "ElectrixTweaker.headSplitLED"
    };
    for (const control in controlsToFunctions) {
        engine.connectControl("[Master]", control, controlsToFunctions[control], ! value/127);
        if (value) {
            engine.trigger("[Master]", control);
        }
    }
};

ElectrixTweaker.deckShiftButton = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    ElectrixTweaker.deckShift[group] = ! ElectrixTweaker.deckShift[group];
    if (value) {
        // set low encoder to relative mode
        midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group].Low.cc, 64);
        // set low LED ring to walk mode with local control disabled
        midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group].Low.ring, 96);
        midi.sendShortMsg(
            0xB0,
            ElectrixTweaker.encoders[group].Low.ring,
            ElectrixTweaker.encoderRingStepsFill[ElectrixTweaker.hotcuePage[group]+1]
        );
        // set mid encoder to relative mode
        midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group].Mid.cc, 64);
        // set mid encoder LED ring to EQ mode with local control disabled
        // There seems to be a bug in the Tweaker firmware when local control is enabled one LED ring but not another. If local control is enabled here, the other rings behave confusingly.
        midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group].Mid.ring, 98);
        midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 64);
        // set high encoder to absolute EQ mode with sensitivity level 1
        // 1 step = 1 MIDI value for fine pitch control
        midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group].High.cc, 78);
        // set high LED ring to EQ mode with local control disabled
        midi.sendShortMsg(0xBF, ElectrixTweaker.encoders[group].High.ring, 78);
        engine.connectControl(group, "rate", "ElectrixTweaker.rateEncoderLEDs");
        engine.trigger(group, "rate");
        if (ElectrixTweaker.topShift && ElectrixTweaker.deckShift[group]) {
            ElectrixTweaker.connectVinylLEDs(group, ElectrixTweaker.vinylMode[group]);
            ElectrixTweaker.vinylMode[group] = ! ElectrixTweaker.vinylMode[group];
        }
    } else {
        engine.stopTimer(ElectrixTweaker.midEncoderLEDTimer[group]);
        engine.connectControl(group, "rate", "ElectrixTweaker.rateEncoderLEDs", true);
    }
    ElectrixTweaker.connectEncoderMode(group, ElectrixTweaker.mode[group], value/127);
};

ElectrixTweaker.rateEncoderLEDs = function(value, group, _control) {
    midi.sendShortMsg(
        0xB0,
        ElectrixTweaker.encoders[group].High.cc,
        script.absoluteLinInverse(value, -1, 1)
    );
};

// ================================================== ARROWS + BIG ENCODER ====================================================

ElectrixTweaker.bigEncoder = function(channel, control, value, _status, _group) {
    if (ElectrixTweaker.topShift) {
        for (let i=0; i<35; i++) {
            engine.setValue("[Playlist]", (value === 1) ? "SelectNextTrack" : "SelectPrevTrack", 1);
        }
    } else {
        engine.setValue("[Playlist]", (value === 1) ? "SelectNextTrack" : "SelectPrevTrack", 1);
    }
};
ElectrixTweaker.bigEncoderButton = function(channel, control, value, _status, _group) {
    if (value) {
        if (ElectrixTweaker.topShift) {
            engine.setValue("[Playlist]", "LoadSelectedIntoFirstStopped", 1);
        } else {
            engine.setValue("[Skin]", "show_maximized_library", ! engine.getValue("[Skin]", "show_maximized_library"));
        }
    }
};
ElectrixTweaker.arrowSide = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.topShift) {
            engine.setValue(group, "eject", 1);
            engine.beginTimer(250, () => engine.setValue(group, eject, 0), true);
        } else {
            engine.setValue(group, "LoadSelectedTrack", 1);
        }
    }
};
ElectrixTweaker.arrowSideLED = function(value, group, _control) {
    midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].arrowSide, (value) ? 127 : 0);
};
ElectrixTweaker.arrowUp = function(channel, control, value, _status, _group) {
    if (value) {
        if (ElectrixTweaker.topShift) {
            engine.setValue("[Playlist]", "ToggleSelectedSidebarItem", 1);
        } else {
            engine.setValue("[Playlist]", "SelectPrevPlaylist", 1);
        }
    }
};
ElectrixTweaker.arrowDown = function(channel, control, value, _status, _group) {
    if (value) {
        if (ElectrixTweaker.topShift) {
            engine.setValue("[Playlist]", "ToggleSelectedSidebarItem", 1);
        } else {
            engine.setValue("[Playlist]", "SelectNextPlaylist", 1);
        }
    }
};

// ===================================================== SAMPLERS ===========================================================

ElectrixTweaker.oneShot = function(channel, control, value, status, group) {
    if (value) {
        if (engine.getValue(group, "track_samples")) {
            if (ElectrixTweaker.topShift) {
                engine.setValue(group, "key", 0);
                engine.setValue(group, "sync_enabled", 0);
                engine.setValue(group, "repeat", 0);
                engine.setValue(group, "play", 0);
                engine.setValue(group, "eject", 1);
                engine.beginTimer(250, () => engine.setValue(group, eject, 0), true);
            } else {
                if (ElectrixTweaker.samplerVelocityAsVolume) {
                    engine.setValue(group, "volume", script.absoluteNonLin(value * ElectrixTweaker.samplerSensitivity, 0, .25, 1));
                }
                engine.setValue(group, "playposition", 0);
                engine.setValue(group, "play", 1);
            }
        } else {
            if (ElectrixTweaker.samplerVelocityAsVolume) {
                engine.setValue(group, "volume", script.absoluteNonLin(value, 0, .25, 1));
            }
            engine.setValue(group, "LoadSelectedTrackAndPlay", 1);
        }
    } else {
        engine.setValue(group, "play", 0);
    }
};
ElectrixTweaker.oneShotLED = function(value, group, _control) {
    midi.sendShortMsg(0x90, 62 + parseInt(ElectrixTweaker.samplerRegEx.exec(group)[1]), (value) ? 127 : 0);
};

// ================================================= CHANNEL STRIPS ===========================================================

ElectrixTweaker.leftKnob = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    // soft takeover
    if (Math.abs(script.absoluteLin(value, 0, 1) - engine.getValue("[QuickEffectRack1_"+group+"]", "super1")) < .1) {
        engine.setValue("[QuickEffectRack1_"+group+"]", "super1", script.absoluteLin(value, 0, 1));
    }
};
ElectrixTweaker.rightKnob = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    // soft takeover
    if (Math.abs(script.absoluteLin(value, 0, 1) - engine.getValue("[QuickEffectRack1_"+group+"]", "super1")) < .1) {
        engine.setValue("[QuickEffectRack1_"+group+"]", "super1", script.absoluteLin(value, 0, 1));
    }
};

ElectrixTweaker.eqEncoderLEDs = function(value, group, control) {
    const encoder = control.replace("filter", "");
    midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group][encoder].cc, script.absoluteNonLinInverse(value, 0, 1, 4));
};

ElectrixTweaker.eqEncoderKillButtonLED = function(value, group, control) {
    let encoder = control.replace("filter", "");
    encoder = encoder.replace("Kill", "");
    midi.sendShortMsg(0x90, ElectrixTweaker.encoders[group][encoder].button, value * 127);
};

ElectrixTweaker.masterGainLEDs = function(value, _group, _control) {
    midi.sendShortMsg(0xB0, ElectrixTweaker.encoders["[Channel2]"].High.cc, script.absoluteNonLinInverse(value, 0, 1, 4));
};

ElectrixTweaker.masterBalanceLEDs = function(value, _group, _control) {
    midi.sendShortMsg(0xB0, ElectrixTweaker.encoders["[Channel2]"].Mid.cc, script.absoluteLinInverse(value, -1, 1));
};

ElectrixTweaker.headGainLEDs = function(value, _group, _control) {
    midi.sendShortMsg(0xB0, ElectrixTweaker.encoders["[Channel1]"].High.cc, script.absoluteNonLinInverse(value, 0, 1, 4));
};

ElectrixTweaker.headMixLEDs = function(value, _group, _control) {
    midi.sendShortMsg(0xB0, ElectrixTweaker.encoders["[Channel1]"].Mid.cc, script.absoluteLinInverse(value, -1, 1));
};

ElectrixTweaker.headSplitLED = function(value, _group, _control) {
    midi.sendShortMsg(0x90, ElectrixTweaker.encoders["[Channel1]"].Mid.button, value * 127);
};

ElectrixTweaker.gainLEDs = function(value, group, _control) {
    midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Low.cc, script.absoluteNonLinInverse(value, 0, 1, 4));
};

ElectrixTweaker.highEncoder = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (ElectrixTweaker.topShift) {
        if (control === ElectrixTweaker.encoders["[Channel1]"].High.cc) {
            engine.setValue("[Master]", "headVolume", script.absoluteNonLin(value, 0, 1, 5));
        } else {
            engine.setValue("[Master]", "volume", script.absoluteNonLin(value, 0, 1, 5));
        }
    } else if (ElectrixTweaker.deckShift[group]) {
        engine.setValue(group, "rate", script.absoluteLin(value, -1, 1, 0, 126));
    } else {
        switch (ElectrixTweaker.mode[group]) {
        case "eq":
            engine.setValue(group, "filterHigh", script.absoluteNonLin(value, 0, 1, 4));
            break;
        case "loop":
            if ((value === 127) && (ElectrixTweaker.loopMoveSize[group] >= Math.pow(2, -5))) {
                ElectrixTweaker.loopMoveSize[group] = ElectrixTweaker.loopMoveSize[group] / 2;
            } else if ((value === 1) && (ElectrixTweaker.loopMoveSize[group] <= Math.pow(2, 5))) {
                ElectrixTweaker.loopMoveSize[group] = ElectrixTweaker.loopMoveSize[group] * 2;
            }
            midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].High.ring, ElectrixTweaker.encoderRingSteps[6 + Math.log(ElectrixTweaker.loopMoveSize[group]) / Math.log(2)]);
            break;
        }
    }
};
ElectrixTweaker.highEncoderPress = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.topShift) {
            if (control === ElectrixTweaker.encoders["[Channel1]"].High.button) {
                engine.setValue("[Master]", "headVolume", 1);
            } else {
                engine.setValue("[Master]", "volume", 1);
            }
        } else {
            switch (ElectrixTweaker.mode[group]) {
            case "eq":
                if (ElectrixTweaker.deckShift[group]) {
                    engine.setValue(group, "filterHigh", 1);
                } else {
                    engine.setValue(group, "filterHighKill", ! engine.getValue(group, "filterHighKill"));
                }
                break;
            case "loop":
                // What to do with this?
                break;
            }
        }
    }
};
ElectrixTweaker.midEncoder = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (ElectrixTweaker.topShift) {
        if (control === ElectrixTweaker.encoders["[Channel1]"].Mid.cc) {
            engine.setValue("[Master]", "headMix", script.absoluteLin(value, -1, 1));
        } else {
            engine.setValue("[Master]", "balance", script.absoluteLin(value, -1, 1));
        }
    } else if (ElectrixTweaker.deckShift[group]) {
        engine.stopTimer(ElectrixTweaker.midEncoderLEDTimer[group]);
        if (value === 127) {
            engine.setValue(group, "beatjump_32_backward", 1);
            midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 0);
        } else {
            engine.setValue(group, "beatjump_32_forward", 1);
            midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 127);
        }
        ElectrixTweaker.midEncoderLEDTimer[group] = engine.beginTimer(1000, () => midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 64), true);
    } else {
        switch (ElectrixTweaker.mode[group]) {
        case "eq":
            engine.setValue(group, "filterMid", script.absoluteNonLin(value, 0, 1, 4));
            break;
        case "loop":
            engine.stopTimer(ElectrixTweaker.midEncoderLEDTimer[group]);
            if (value === 127) {
                engine.setValue(group, "loop_move_" + ElectrixTweaker.loopMoveSize[group] + "_backward", 1);
                midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 0);

            } else {
                engine.setValue(group, "loop_move_" + ElectrixTweaker.loopMoveSize[group] + "_forward", 1);
                midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 127);
            }
            ElectrixTweaker.midEncoderLEDTimer[group] = engine.beginTimer(1000, () => midi.sendShortMsg(0xB0, ElectrixTweaker.encoders[group].Mid.ring, 64), true);
            break;
        }
    }
};
ElectrixTweaker.midEncoderPress = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.topShift) {
            if (control === ElectrixTweaker.encoders["[Channel1]"].Mid.button) {
                engine.setValue("[Master]", "headSplit", ! engine.getValue("[Master]", "headSplit"));
            } else {
                engine.setValue("[Master]", "balance", 0);
            }
        } else {
            switch (ElectrixTweaker.mode[group]) {
            case "eq":
                if (ElectrixTweaker.deckShift[group]) {
                    engine.setValue(group, "filterMid", 1);
                } else {
                    engine.setValue(group, "filterMidKill", ! engine.getValue(group, "filterMidKill"));
                }
                break;
            case "loop":
                    // What to do with this?
            }
        }
    }
};
ElectrixTweaker.lowEncoder = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (ElectrixTweaker.topShift) {
        engine.setValue(group, "pregain", script.absoluteNonLin(value, 0, 1, 4));
    } else if (ElectrixTweaker.deckShift[group]) {
        if (value === 1 && ElectrixTweaker.hotcuePage[group] < 3) {
            ElectrixTweaker.connectHotcuePage(group, true);
            ElectrixTweaker.hotcuePage[group]++;
            ElectrixTweaker.connectHotcuePage(group);
        } else if (value === 127 && ElectrixTweaker.hotcuePage[group] > 0) {
            ElectrixTweaker.connectHotcuePage(group, true);
            ElectrixTweaker.hotcuePage[group]--;
            ElectrixTweaker.connectHotcuePage(group);
        }
        midi.sendShortMsg(
            0xB0,
            ElectrixTweaker.encoders[group].Low.ring,
            ElectrixTweaker.encoderRingStepsFill[ElectrixTweaker.hotcuePage[group]+1]
        );
    } else {
        switch (ElectrixTweaker.mode[group]) {
        case "eq":
            engine.setValue(group, "filterLow", script.absoluteNonLin(value, 0, 1, 4));
            break;
        case "loop":
            if ((value === 127) && (ElectrixTweaker.loopSize[group] >= Math.pow(2, -5))) {
                ElectrixTweaker.loopSize[group] = ElectrixTweaker.loopSize[group] / 2;
                engine.setValue(group, "loop_halve", 1);
                engine.setValue(group, "loop_halve", 0);
            } else if ((value === 1) && (ElectrixTweaker.loopSize[group] <= Math.pow(2, 5))) {
                ElectrixTweaker.loopSize[group] = ElectrixTweaker.loopSize[group] * 2;
                engine.setValue(group, "loop_double", 1);
                engine.setValue(group, "loop_double", 0);
            }
            midi.sendShortMsg(
                0xB0,
                ElectrixTweaker.encoders[group].Low.ring,
                ElectrixTweaker.encoderRingSteps[
                    6 + Math.log(ElectrixTweaker.loopSize[group]) / Math.log(2)
                ]
            );
            break;
        }
    }
};
ElectrixTweaker.lowEncoderPress = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.topShift) {
            engine.setValue(group, "pregain", 1);
        } else {
            switch (ElectrixTweaker.mode[group]) {
            case "eq":
                if (ElectrixTweaker.deckShift[group]) {
                    engine.setValue(group, "filterLow", 1);
                } else {
                    engine.setValue(group, "filterLowKill", ! engine.getValue(group, "filterLowKill"));
                }
                break;
            case "loop":
                if (ElectrixTweaker.slipMode[group]) {
                    engine.setValue(group, "slip_enabled", ! engine.getValue(group, "slip_enabled"));
                }
                if (engine.getValue(group, "loop_enabled")) {
                    engine.setValue(group, "reloop_exit", 1);
                } else {
                    engine.setValue(group, "beatloop_" + ElectrixTweaker.loopSize[group] + "_activate", 1);
                }
                break;
            }
        }
    } else if (ElectrixTweaker.mode[group] === "loop" && (ElectrixTweaker.slipMode[group] || ElectrixTweaker.slipModeUnsetWhileLooping[group])) {
        engine.setValue(group, "slip_enabled", ! engine.getValue(group, "slip_enabled"));
        if (engine.getValue(group, "loop_enabled")) {
            engine.setValue(group, "reloop_exit", 1);
        }
        ElectrixTweaker.slipModeUnsetWhileLooping[group] = false;
    }
};
ElectrixTweaker.loopButtonToggleLED = function(value, group, _control) {
    midi.sendShortMsg(0x90, ElectrixTweaker.encoders[group].Low.button, value * 127);
};

ElectrixTweaker.modeButton = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        ElectrixTweaker.connectEncoderMode(group, ElectrixTweaker.mode[group], true);
        switch (ElectrixTweaker.mode[group]) {
        case "eq":
            ElectrixTweaker.mode[group] = "loop";
            break;
        case "loop":
            ElectrixTweaker.mode[group] = "eq";
            break;
        }
        ElectrixTweaker.connectEncoderMode(group, ElectrixTweaker.mode[group]);
    }
};

ElectrixTweaker.fader = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    // soft takeover. This is necessary for toggling between decks 1/3 or 2/4
    // because the fader can't be moved to the new deck's value by this script
    if (Math.abs(value - script.absoluteNonLinInverse(engine.getValue(group, "volume"), 0, .25, 1)) < 30) {
        engine.setValue(group, "volume", script.absoluteNonLin(value, 0, .25, 1));
    }
};

ElectrixTweaker.pflButton = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.deckShift[group]) {
            engine.setValue(group, "reloop_exit", 1);
        } else {
            engine.setValue(group, "pfl", ! engine.getValue(group, "pfl"));
        }
    }
};
ElectrixTweaker.pflButtonLED = function(value, group, control) {
    midi.sendShortMsg(
        0x90,
        ElectrixTweaker.buttons[group][control],
        (value) ? ElectrixTweaker.colorCodes.green : ElectrixTweaker.colorCodes.off
    );
};

ElectrixTweaker.playButton = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (ElectrixTweaker.deckShift[group]) {
        engine.setValue(group, "cue_default", value);
    } else if (ElectrixTweaker.topShift) {
        engine.setValue(group, "cue_gotoandstop", 1);
    } else if (value) {
        if (ElectrixTweaker.hotcuesPressed[group]) {
            ElectrixTweaker.playPressedWhileCueJuggling[group] = true;
        }

        engine.setValue(group, "play", ! engine.getValue(group, "play"));
    }
};
ElectrixTweaker.playButtonLED = function(value, group, control) {
    if (control === "cue_indicator" && value === 1) {
        midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].play, ElectrixTweaker.colorCodes.yellow);
    } else {
        if (engine.getValue(group, "play")) {
            if (
                (control !== "playposition") // do not spam MIDI signals with each update in playposition while playing
                && (
                    (! ElectrixTweaker.hotcuesPressed[group])
                    || ElectrixTweaker.playPressedWhileCueJuggling[group]
                )
            ) {
                midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].play, ElectrixTweaker.colorCodes.green);
            }
        } else if (engine.getValue(group, "track_samples")) {
            midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].play, ElectrixTweaker.colorCodes.red);
        } else {
            midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].play, ElectrixTweaker.colorCodes.off);
        }
    }
};


//===================================================================== BUTTON GRID =========================================================

ElectrixTweaker.hotcue = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    const row = (control < ElectrixTweaker.buttons[group].hotcues[1][0]) ? 0 : 1;
    const cueButton = 4 + row*4 - (ElectrixTweaker.buttons[group].hotcues[row][3] - control);
    const cue = cueButton + (8 * ElectrixTweaker.hotcuePage[group]);
    if (value) {
        if (engine.getValue(group, "hotcue_"+cue+"_enabled")) {
            if (ElectrixTweaker.deckShift[group]) {
                engine.setValue(group, "hotcue_"+cue+"_set", 1);
            } else if (ElectrixTweaker.topShift) {
                engine.setValue(group, "hotcue_"+cue+"_clear", 1);
            } else {
                if (ElectrixTweaker.slipMode[group]) {
                    if (engine.getValue(group, "play") && (! ElectrixTweaker.hotcuesPressed[group])) {
                        engine.setValue(group, "slip_enabled", 1);
                    }
                    engine.setValue(group, "hotcue_"+cue+"_activate", 1);
                    ElectrixTweaker.hotcuesPressed[group]++;
                } else {
                    engine.setValue(group, "hotcue_"+cue+"_goto", 1);
                }
            }
        } else {
            engine.setValue(group, "hotcue_"+cue+"_set", 1);
        }
    } else {
        if (ElectrixTweaker.hotcuesPressed[group]) { // do not go below 0
            ElectrixTweaker.hotcuesPressed[group]--;
        }
        engine.setValue(group, "hotcue_"+cue+"_activate", 0);

        if (! ElectrixTweaker.hotcuesPressed[group]) {
            ElectrixTweaker.playPressedWhileCueJuggling[group] = false;
            engine.setValue(group, "slip_enabled", 0);
        }
    }
};
ElectrixTweaker.hotcueLED = function(value, group, control) {
    const cue = parseInt(control.split("_")[1]) - (8 * ElectrixTweaker.hotcuePage[group]);
    const row = (cue <= 4) ? 0 : 1;
    midi.sendShortMsg(
        0x90,
        ElectrixTweaker.buttons[group].hotcues[row][cue - 1 - 4*row],
        value * ElectrixTweaker.hotcueColors[ElectrixTweaker.hotcuePage[group]]
    );
};

ElectrixTweaker.slipButton = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.deckShift[group]) {
            engine.setValue(group, "loop_in", 1);
        } else {
            if (ElectrixTweaker.slipMode[group]) {
                if (ElectrixTweaker.hotcuesPressed[group] && ! engine.getValue(group, "slip_enabled")) {
                    engine.setValue(group, "play", 0);
                }
                if (engine.getValue(group, "loop_enabled")) {
                    ElectrixTweaker.slipModeUnsetWhileLooping[group] = true;
                }
                //engine.setValue(group, 'slip_cancel', 1)
                ElectrixTweaker.hotcuesPressed[group] = 0;
            }


            ElectrixTweaker.slipMode[group] = ! ElectrixTweaker.slipMode[group];
            midi.sendShortMsg(
                0x90,
                ElectrixTweaker.buttons[group].slip,
                ElectrixTweaker.slipMode[group] ? ElectrixTweaker.deckColor[group] : ElectrixTweaker.colorCodes.off
            );
        }
    }
};

ElectrixTweaker.deckToggle = function(channel, control, value, status, group) {
    if (value) {
        if (ElectrixTweaker.deckShift[group]) {
            engine.setValue(ElectrixTweaker.deck[group], "loop_out", 1);
        } else {
            let deckNumber = parseInt(
                ElectrixTweaker.channelRegEx.exec(
                    ElectrixTweaker.deck[group]
                )[1]
            );
            if (deckNumber <= 2) {
                deckNumber += 2;
            } else {
                deckNumber -= 2;
            }
            ElectrixTweaker.deck[group] = "[Channel" + deckNumber + "]";
            ElectrixTweaker.initDeck(ElectrixTweaker.deck[group]);
        }
    }
};

ElectrixTweaker.sync = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.deckShift[group]) {
            engine.setValue(group, "rate", 0);
        } else {
            engine.setValue(group, "sync_enabled", ! engine.getValue(group, "sync_enabled"));
        }
    }
};
ElectrixTweaker.syncLED = function(value, group, _control) {
    midi.sendShortMsg(
        0x90,
        ElectrixTweaker.buttons[group].sync,
        (value) ? ElectrixTweaker.deckColor[group] : ElectrixTweaker.colorCodes.off
    );
};

ElectrixTweaker.key = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.deckShift[group]) {
            if (engine.getValue(group, "file_key") !== engine.getValue(group, "key")) {
                engine.setValue(group, "reset_key", 1);
            } else {
                engine.setValue(group, "sync_key", 1);
            }
        } else {
            engine.setValue(group, "keylock", ! engine.getValue(group, "keylock"));
        }
    }
};
ElectrixTweaker.keylockLED = function(value, group, _control) {
    midi.sendShortMsg(
        0x90,
        ElectrixTweaker.buttons[group].key,
        (value) ? ElectrixTweaker.deckColor[group] : ElectrixTweaker.colorCodes.off
    );
};

ElectrixTweaker.quantize = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (value) {
        if (ElectrixTweaker.deckShift[group]) {
            engine.setValue(group, "beats_translate_curpos", 1);
        } else {
            engine.setValue(group, "quantize", ! engine.getValue(group, "quantize"));
        }
    }
};
ElectrixTweaker.quantizeLED = function(value, group, _control) {
    midi.sendShortMsg(
        0x90,
        ElectrixTweaker.buttons[group].quantize,
        (value) ? ElectrixTweaker.deckColor[group] : ElectrixTweaker.colorCodes.off
    );
    if (! ElectrixTweaker.vinylMode[group]) {
        midi.sendShortMsg(
            0x90,
            ElectrixTweaker.buttons[group].back,
            (value) ? ElectrixTweaker.colorCodes.white : ElectrixTweaker.colorCodes.green
        );
        midi.sendShortMsg(
            0x90,
            ElectrixTweaker.buttons[group].forward,
            (value) ? ElectrixTweaker.colorCodes.white : ElectrixTweaker.colorCodes.green
        );
    }
};

ElectrixTweaker.vinylModeLED = function(value, group, _control) {
    let color;
    switch (engine.getValue(group, "vinylcontrol_mode")) {
    // absolute mode
    case 0: color = "off"; break;
        // relative mode
    case 1:
        switch (engine.getValue(group, "vinylcontrol_cueing")) {
        case 0: color = "white"; break;
        case 1: color = "yellow"; break;
        case 2: color = "green"; break;
        }
        break;
        // constant mode
    case 2: color = "red"; break;
    }
    midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].forward, ElectrixTweaker.colorCodes[color]);
};
ElectrixTweaker.vinylStatusLED = function(value, group, _control) {
    if (engine.getValue(group, "passthrough")) {
        midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].back, ElectrixTweaker.colorCodes.white);
    } else {
        let color;
        switch (engine.getValue(group, "vinylcontrol_status")) {
        case 0: color = "off"; break;
        case 1: color = "green"; break;
        case 2: color = "yellow"; break;
        case 3: color = "red"; break;
        }
        midi.sendShortMsg(0x90, ElectrixTweaker.buttons[group].back, ElectrixTweaker.colorCodes[color]);
    }
};

ElectrixTweaker.forward = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (ElectrixTweaker.vinylMode[group]) {
        if (value) {
            switch (engine.getValue(group, "vinylcontrol_mode")) {
            // absolute mode
            case 0: engine.setValue(group, "vinylcontrol_mode", 1); break;
                // relative mode
            case 1:
                if (engine.getValue(group, "play") && ! ElectrixTweaker.shift) {
                    switch (engine.getValue(group, "vinylcontrol_cueing")) {
                    case 0: engine.setValue(group, "vinylcontrol_cueing", 1); break;
                    case 1: engine.setValue(group, "vinylcontrol_cueing", 2); break;
                    case 2: engine.setValue(group, "vinylcontrol_cueing", 0); break;
                    }
                } else {
                    engine.setValue(group, "vinylcontrol_mode", 2);
                }
                break;
                // constant mode
            case 2: engine.setValue(group, "vinylcontrol_mode", 0); break;
            }
        }
    } else {
        if (engine.getValue(group, "quantize")) {
            if (value) {
                if (ElectrixTweaker.deckShift[group]) {
                    engine.setValue(group, "beatjump_1_forward", 1);
                } else {
                    engine.setValue(group, "beatjump_4_forward", 1);
                }
            }
        } else {
            if (ElectrixTweaker.deckShift[group]) {
                engine.setValue(group, "rate_temp_up", value / 127);
            } else {
                engine.setValue(group, "fwd", value);
            }
        }
    }
};
ElectrixTweaker.back = function(channel, control, value, status, group) {
    group = ElectrixTweaker.deck[group];
    if (ElectrixTweaker.vinylMode[group]) {
        if (value) {
            if (ElectrixTweaker.deckShift[group] || engine.getValue(group, "passthrough")) {
                engine.setValue(group, "passthrough", ! engine.getValue(group, "passthrough"));
            } else {
                engine.setValue(group, "vinylcontrol_enabled", ! engine.getValue(group, "vinylcontrol_enabled"));
            }
        }
    } else {
        if (engine.getValue(group, "quantize")) {
            if (value) {
                if (ElectrixTweaker.deckShift[group]) {
                    engine.setValue(group, "beatjump_1_backward", 1);
                } else {
                    engine.setValue(group, "beatjump_4_backward", 1);
                }
            }
        } else {
            if (ElectrixTweaker.deckShift[group]) {
                engine.setValue(group, "rate_temp_down", value / 127);
            } else {
                engine.setValue(group, "back", value);
            }
        }
    }
};
