diff --git a/res/controllers/Traktor Kontrol S2 Mk1.hid.xml b/res/controllers/Traktor Kontrol S2 Mk1.hid.xml
new file mode 100644
index 00000000000..3ef428b8bdf
--- /dev/null
+++ b/res/controllers/Traktor Kontrol S2 Mk1.hid.xml
@@ -0,0 +1,18 @@
+
+
+
+ Native Instruments Traktor Kontrol S2 Mk1
+ leifhelm
+ Native Instruments Traktor Kontrol S2 MK1
+ native_instruments_traktor_kontrol_s2_mk1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js
new file mode 100644
index 00000000000..e209ae1b3e6
--- /dev/null
+++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js
@@ -0,0 +1,1467 @@
+/****************************************************************/
+/* Traktor Kontrol S2 MK1 HID controller script */
+/* Copyright (C) 2021, leifhelm */
+/* Based on: */
+/* Traktor Kontrol S2 MK2 HID controller script v1.00 */
+/* Copyright (C) 2020, Be */
+/* Copyright (C) 2017, z411 */
+/* but feel free to tweak this to your heart's content! */
+/****************************************************************/
+
+// ==== Jog Wheel Touch Calibration ====
+// Set the threshold for scratching for each jog wheel.
+// If it is always scratching increase the value
+// If it never scratches decrease the value
+// Bigger values mean more force necessary for it to scratch.
+// The unpressed value is around 3100.
+// The fully pressed value is around 3700.
+var JogWheelTouchThreshold = {
+ "[Channel1]": 3328,
+ "[Channel2]": 3328,
+};
+
+// ==== Friendly User Configuration ====
+// The Cue button, when Shift is also held, can have two possible functions:
+// 1. "REWIND": seeks to the very start of the track.
+// 2. "REVERSEROLL": performs a temporary reverse or "censor" effect, where the track
+// is momentarily played in reverse until the button is released.
+var ShiftCueButtonAction = "REWIND";
+
+// Set the brightness of button LEDs which are off and on. This uses a scale from 0 to 0x1F (31).
+// If you don't have the optional power adapter and are using the controller with USB bus power,
+var ButtonBrightnessOff = 0x00;
+var ButtonBrightnessOn = 0x1F;
+
+// eslint definitions
+var TraktorS2MK1 = new function() {
+ this.controller = new HIDController();
+
+ // When true, packets will not be sent to the controller.
+ // Used when updating multiple LEDs simultaneously.
+ this.batchingLEDUpdate = false;
+
+ // Previous values, used for calculating deltas for encoder knobs.
+ this.previousBrowse = 0;
+ this.previousPregain = {
+ "[Channel1]": 0,
+ "[Channel2]": 0
+ };
+ this.previousLeftEncoder = {
+ "[Channel1]": 0,
+ "[Channel2]": 0
+ };
+ this.previousRightEncoder = {
+ "[Channel1]": 0,
+ "[Channel2]": 0
+ };
+ this.wheelTouchInertiaTimer = {
+ "[Channel1]": 0,
+ "[Channel2]": 0
+ };
+
+ this.gainEncoderPressed = {
+ "[Channel1]": false,
+ "[Channel2]": false
+ };
+ this.leftEncoderPressed = {
+ "[Channel1]": false,
+ "[Channel2]": false
+ };
+ this.shiftPressed = {
+ "[Channel1]": false,
+ "[Channel2]": false
+ };
+
+ this.padModes = {
+ "hotcue": 0,
+ "introOutro": 1,
+ "sampler": 2
+ };
+ this.currentPadMode = {
+ "[Channel1]": this.padModes.hotcue,
+ "[Channel2]": this.padModes.hotcue
+ };
+ this.padConnections = {
+ "[Channel1]": [],
+ "[Channel2]": []
+ };
+
+ this.lastTickValue = [0, 0];
+ this.lastTickTime = [0.0, 0.0];
+ this.syncEnabledTime = {};
+
+ this.longPressTimeoutMilliseconds = 275;
+
+ this.effectButtonLongPressTimer = {
+ "[EffectRack1_EffectUnit1]": [0, 0, 0, 0],
+ "[EffectRack1_EffectUnit2]": [0, 0, 0, 0]
+ };
+ this.effectButtonIsLongPressed = {
+ "[EffectRack1_EffectUnit1]": [false, false, false, false],
+ "[EffectRack1_EffectUnit2]": [false, false, false, false]
+ };
+ this.effectFocusLongPressTimer = {
+ "[EffectRack1_EffectUnit1]": 0,
+ "[EffectRack1_EffectUnit2]": 0
+ };
+ this.effectFocusChooseModeActive = {
+ "[EffectRack1_EffectUnit1]": false,
+ "[EffectRack1_EffectUnit2]": false
+ };
+ this.effectFocusButtonPressedWhenParametersHidden = {
+ "[EffectRack1_EffectUnit1]": false,
+ "[EffectRack1_EffectUnit2]": false
+ };
+ this.previouslyFocusedEffect = {
+ "[EffectRack1_EffectUnit1]": null,
+ "[EffectRack1_EffectUnit2]": null
+ };
+ this.effectButtonLEDconnections = {
+ "[EffectRack1_EffectUnit1]": [],
+ "[EffectRack1_EffectUnit2]": []
+ };
+};
+
+TraktorS2MK1.registerInputPackets = function() {
+ var InputReport0x01 = new HIDPacket("InputReport0x01", 0x01, this.inputReport0x01Callback);
+ var InputReport0x02 = new HIDPacket("InputReport0x02", 0x02, this.inputReport0x02Callback);
+
+ // Values in input report 0x01 are all buttons, except the jog wheels.
+ // An exclamation point indicates a specially-handled function. Everything else is a standard
+ // Mixxx control object name.
+
+ InputReport0x01.addControl("[Channel1]", "!gain_encoder_press", 0x0E, "B", 0x01, false, this.gainEncoderPress);
+ InputReport0x01.addControl("[Channel1]", "!shift", 0x0D, "B", 0x80, false, this.shift);
+ InputReport0x01.addControl("[Channel1]", "!sync_enabled", 0x0D, "B", 0x40, false, this.syncButton);
+ InputReport0x01.addControl("[Channel1]", "!cue_default", 0x0D, "B", 0x20, false, this.cueButton);
+ InputReport0x01.addControl("[Channel1]", "!play", 0x0D, "B", 0x10, false, this.playButton);
+ InputReport0x01.addControl("[Channel1]", "!pad1", 0x0D, "B", 0x08, false, this.padButton);
+ InputReport0x01.addControl("[Channel1]", "!pad2", 0x0D, "B", 0x04, false, this.padButton);
+ InputReport0x01.addControl("[Channel1]", "!pad3", 0x0D, "B", 0x02, false, this.padButton);
+ InputReport0x01.addControl("[Channel1]", "!pad4", 0x0D, "B", 0x01, false, this.padButton);
+ InputReport0x01.addControl("[Channel1]", "!loop_in", 0x09, "B", 0x40, false, this.loopInButton);
+ InputReport0x01.addControl("[Channel1]", "!loop_out", 0x09, "B", 0x20, false, this.loopOutButton);
+ InputReport0x01.addControl("[Channel1]", "!samples_button", 0x0B, "B", 0x02, false, this.samplerModeButton);
+ InputReport0x01.addControl("[Channel1]", "!reset_button", 0x09, "B", 0x10, false, this.introOutroModeButton);
+ InputReport0x01.addControl("[Channel1]", "!left_encoder_press", 0x0E, "B", 0x02, false, this.leftEncoderPress);
+ InputReport0x01.addControl("[Channel1]", "!right_encoder_press", 0x0E, "B", 0x04, false, this.rightEncoderPress);
+ InputReport0x01.addControl("[Channel1]", "!jog_wheel", 0x01, "I", 0xFFFFFFFF, false, this.jogMove);
+ InputReport0x01.addControl("[Channel1]", "!load_track", 0x0B, "B", 0x08, false, this.loadTrackButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit1]", "!effect_focus_button",
+ 0x09, "B", 0x08, false, this.effectFocusButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit1]", "!effectbutton1", 0x09, "B", 0x04, false, this.effectButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit1]", "!effectbutton2", 0x09, "B", 0x02, false, this.effectButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit1]", "!effectbutton3", 0x09, "B", 0x01, false, this.effectButton);
+
+ InputReport0x01.addControl("[Channel2]", "!gain_encoder_press", 0x0E, "B", 0x10, false, this.gainEncoderPress);
+ InputReport0x01.addControl("[Channel2]", "!shift", 0x0C, "B", 0x80, false, this.shift);
+ InputReport0x01.addControl("[Channel2]", "!sync_enabled", 0x0C, "B", 0x40, false, this.syncButton);
+ InputReport0x01.addControl("[Channel2]", "!cue_default", 0x0C, "B", 0x20, false, this.cueButton);
+ InputReport0x01.addControl("[Channel2]", "!play", 0x0C, "B", 0x10, false, this.playButton);
+ InputReport0x01.addControl("[Channel2]", "!pad1", 0x0C, "B", 0x08, false, this.padButton);
+ InputReport0x01.addControl("[Channel2]", "!pad2", 0x0C, "B", 0x04, false, this.padButton);
+ InputReport0x01.addControl("[Channel2]", "!pad3", 0x0C, "B", 0x02, false, this.padButton);
+ InputReport0x01.addControl("[Channel2]", "!pad4", 0x0C, "B", 0x01, false, this.padButton);
+ InputReport0x01.addControl("[Channel2]", "!loop_in", 0x0B, "B", 0x40, false, this.loopInButton);
+ InputReport0x01.addControl("[Channel2]", "!loop_out", 0x0B, "B", 0x20, false, this.loopOutButton);
+ InputReport0x01.addControl("[Channel2]", "!samples_button", 0x0B, "B", 0x01, false, this.samplerModeButton);
+ InputReport0x01.addControl("[Channel2]", "!reset_button", 0x0B, "B", 0x10, false, this.introOutroModeButton);
+ InputReport0x01.addControl("[Channel2]", "!left_encoder_press", 0x0E, "B", 0x20, false, this.leftEncoderPress);
+ InputReport0x01.addControl("[Channel2]", "!right_encoder_press", 0x0E, "B", 0x40, false, this.rightEncoderPress);
+ InputReport0x01.addControl("[Channel2]", "!jog_wheel", 0x05, "I", 0xFFFFFFFF, false, this.jogMove);
+ InputReport0x01.addControl("[Channel2]", "!load_track", 0x0B, "B", 0x04, false, this.loadTrackButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit2]", "!effect_focus_button",
+ 0x0A, "B", 0x80, false, this.effectFocusButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit2]", "!effectbutton1", 0xA, "B", 0x40, false, this.effectButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit2]", "!effectbutton2", 0xA, "B", 0x20, false, this.effectButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit2]", "!effectbutton3", 0xA, "B", 0x10, false, this.effectButton);
+
+ InputReport0x01.addControl("[Channel1]", "!pfl", 0x09, "B", 0x80, false, this.pflButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", 0x0A, "B", 0x02);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", 0x0A, "B", 0x01);
+
+ InputReport0x01.addControl("[Channel2]", "!pfl", 0x0B, "B", 0x80, false, this.pflButton);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", 0x0A, "B", 0x08);
+ InputReport0x01.addControl("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", 0x0A, "B", 0x04);
+
+ // maximize the library on browse encoder press
+ InputReport0x01.addControl("[Master]", "maximize_library", 0x0E, "B", 0x08, false, this.toggleButton);
+
+ engine.makeConnection("[EffectRack1_EffectUnit1]", "show_parameters", TraktorS2MK1.onShowParametersChange);
+ engine.makeConnection("[EffectRack1_EffectUnit2]", "show_parameters", TraktorS2MK1.onShowParametersChange);
+
+ this.controller.registerInputPacket(InputReport0x01);
+
+ // Most items in the input report 0x02 are controls that go from 0-4095.
+ // There are also some 4 bit encoders.
+ InputReport0x02.addControl("[Channel1]", "rate", 0x0F, "H");
+ InputReport0x02.addControl("[Channel2]", "rate", 0x1F, "H");
+ InputReport0x02.addControl("[Channel1]", "!left_encoder", 0x01, "B", 0xF0, false, this.leftEncoder);
+ InputReport0x02.addControl("[Channel1]", "!right_encoder", 0x02, "B", 0x0F, false, this.rightEncoder);
+ InputReport0x02.addControl("[Channel2]", "!left_encoder", 0x03, "B", 0xF0, false, this.leftEncoder);
+ InputReport0x02.addControl("[Channel2]", "!right_encoder", 0x04, "B", 0x0F, false, this.rightEncoder);
+
+ InputReport0x02.addControl("[EffectRack1_EffectUnit1]", "mix", 0x0B, "H");
+ InputReport0x02.addControl("[EffectRack1_EffectUnit1]", "!effectknob1", 0x09, "H", 0xFFFF, false, this.effectKnob);
+ InputReport0x02.addControl("[EffectRack1_EffectUnit1]", "!effectknob2", 0x07, "H", 0xFFFF, false, this.effectKnob);
+ InputReport0x02.addControl("[EffectRack1_EffectUnit1]", "!effectknob3", 0x05, "H", 0xFFFF, false, this.effectKnob);
+
+ InputReport0x02.addControl("[EffectRack1_EffectUnit2]", "mix", 0x1B, "H");
+ InputReport0x02.addControl("[EffectRack1_EffectUnit2]", "!effectknob1", 0x19, "H", 0xFFFF, false, this.effectKnob);
+ InputReport0x02.addControl("[EffectRack1_EffectUnit2]", "!effectknob2", 0x17, "H", 0xFFFF, false, this.effectKnob);
+ InputReport0x02.addControl("[EffectRack1_EffectUnit2]", "!effectknob3", 0x15, "H", 0xFFFF, false, this.effectKnob);
+
+ InputReport0x02.addControl("[Channel1]", "volume", 0x2B, "H");
+ InputReport0x02.addControl("[EqualizerRack1_[Channel1]_Effect1]", "parameter3", 0x11, "H");
+ InputReport0x02.addControl("[EqualizerRack1_[Channel1]_Effect1]", "parameter2", 0x25, "H");
+ InputReport0x02.addControl("[EqualizerRack1_[Channel1]_Effect1]", "parameter1", 0x27, "H");
+ InputReport0x02.addControl("[Channel1]", "pregain", 0x01, "B", 0x0F, false, this.gainEncoder);
+ InputReport0x02.addControl("[Channel1]", "!jog_touch", 0x0D, "H", 0xFFFF, false, this.jogTouch);
+
+ InputReport0x02.addControl("[Channel2]", "volume", 0x2D, "H");
+ InputReport0x02.addControl("[EqualizerRack1_[Channel2]_Effect1]", "parameter3", 0x21, "H");
+ InputReport0x02.addControl("[EqualizerRack1_[Channel2]_Effect1]", "parameter2", 0x23, "H");
+ InputReport0x02.addControl("[EqualizerRack1_[Channel2]_Effect1]", "parameter1", 0x29, "H");
+ InputReport0x02.addControl("[Channel2]", "pregain", 0x03, "B", 0x0F, false, this.gainEncoder);
+ InputReport0x02.addControl("[Channel2]", "!jog_touch", 0x1D, "H", 0xFFFF, false, this.jogTouch);
+
+ InputReport0x02.addControl("[Master]", "crossfader", 0x2F, "H");
+ InputReport0x02.addControl("[Master]", "headMix", 0x31, "H");
+ InputReport0x02.addControl("[Master]", "!samplerGain", 0x13, "H");
+ InputReport0x02.setCallback("[Master]", "!samplerGain", this.samplerGainKnob);
+ InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoder);
+
+ // Soft takeover for knobs
+ engine.softTakeover("[Channel1]", "rate", true);
+ engine.softTakeover("[Channel2]", "rate", true);
+
+ engine.softTakeover("[Channel1]", "volume", true);
+ engine.softTakeover("[Channel2]", "volume", true);
+
+ engine.softTakeover("[Channel1]", "pregain", true);
+ engine.softTakeover("[Channel2]", "pregain", true);
+
+ engine.softTakeover("[Master]", "crossfader", true);
+ engine.softTakeover("[Master]", "headMix", true);
+ for (var i = 1; i <= 8; i++) {
+ engine.softTakeover("[Sampler" + i + "]", "pregain", true);
+ }
+
+ engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter3", true);
+ engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter2", true);
+ engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter1", true);
+
+ engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter3", true);
+ engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter2", true);
+ engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter1", true);
+
+ for (i = 1; i <= 3; i++) {
+ engine.softTakeover("[EffectRack1_EffectUnit1_Effect" + i + "]", "meta", true);
+ engine.softTakeover("[EffectRack1_EffectUnit2_Effect" + i + "]", "meta", true);
+ for (var j = 1; j <= 3; j++) {
+ engine.softTakeover("[EffectRack1_EffectUnit1_Effect" + i + "]", "parameter" + j, true);
+ engine.softTakeover("[EffectRack1_EffectUnit2_Effect" + i + "]", "parameter" + j, true);
+ }
+ }
+
+ // Set scalers
+ TraktorS2MK1.scalerParameter.useSetParameter = true;
+ this.controller.setScaler("volume", this.scalerVolume);
+ this.controller.setScaler("headMix", this.scalerSlider);
+ this.controller.setScaler("parameter1", this.scalerParameter);
+ this.controller.setScaler("parameter2", this.scalerParameter);
+ this.controller.setScaler("parameter3", this.scalerParameter);
+ this.controller.setScaler("super1", this.scalerParameter);
+ this.controller.setScaler("crossfader", this.scalerSlider);
+ this.controller.setScaler("rate", this.scalerSlider);
+ this.controller.setScaler("mix", this.scalerParameter);
+
+ // Register packet
+ this.controller.registerInputPacket(InputReport0x02);
+};
+
+TraktorS2MK1.registerOutputPackets = function() {
+ var OutputReport0x80 = new HIDPacket("OutputReport0x80", 0x80);
+
+ OutputReport0x80.addOutput("[Channel1]", "track_loaded", 0x1F, "B");
+ OutputReport0x80.addOutput("[Channel2]", "track_loaded", 0x1E, "B");
+
+ var VuOffsets = {
+ "[Channel1]": 0x15,
+ "[Channel2]": 0x11,
+ };
+ for (var ch in VuOffsets) {
+ for (var i = 0; i <= 0x03; i++) {
+ OutputReport0x80.addOutput(ch, "!" + "VuMeter" + i, VuOffsets[ch] + i, "B");
+ }
+ }
+
+ OutputReport0x80.addOutput("[Channel1]", "PeakIndicator", 0x01, "B");
+ OutputReport0x80.addOutput("[Channel2]", "PeakIndicator", 0x25, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!reset_button", 0x06, "B");
+ OutputReport0x80.addOutput("[Channel1]", "loop_in", 0x02, "B");
+ OutputReport0x80.addOutput("[Channel1]", "loop_out", 0x05, "B");
+
+ OutputReport0x80.addOutput("[Channel2]", "!reset_button", 0x26, "B");
+ OutputReport0x80.addOutput("[Channel2]", "loop_in", 0x22, "B");
+ OutputReport0x80.addOutput("[Channel2]", "loop_out", 0x21, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "pfl", 0x20, "B");
+ OutputReport0x80.addOutput("[Master]", "!warninglight", 0x31, "B");
+ OutputReport0x80.addOutput("[Channel2]", "pfl", 0x1D, "B");
+
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit1]", "!effect_focus_button", 0x1C, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton1", 0x1B, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton2", 0x1A, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton3", 0x19, "B");
+
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit2]", "!effect_focus_button", 0x39, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton1", 0x38, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton2", 0x37, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton3", 0x36, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!samples_button", 0x35, "B");
+ OutputReport0x80.addOutput("[Channel2]", "!samples_button", 0x34, "B");
+
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", 0x3D, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", 0x3C, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", 0x3B, "B");
+ OutputReport0x80.addOutput("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", 0x3A, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!shift", 0x08, "B");
+ OutputReport0x80.addOutput("[Channel1]", "sync_enabled", 0x04, "B");
+ OutputReport0x80.addOutput("[Channel1]", "cue_indicator", 0x07, "B");
+ OutputReport0x80.addOutput("[Channel1]", "play_indicator", 0x03, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!pad_1_G", 0x0C, "B");
+ OutputReport0x80.addOutput("[Channel1]", "!pad_1_B", 0x10, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!pad_2_G", 0x0B, "B");
+ OutputReport0x80.addOutput("[Channel1]", "!pad_2_B", 0x0F, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!pad_3_G", 0x0A, "B");
+ OutputReport0x80.addOutput("[Channel1]", "!pad_3_B", 0x0E, "B");
+
+ OutputReport0x80.addOutput("[Channel1]", "!pad_4_G", 0x09, "B");
+ OutputReport0x80.addOutput("[Channel1]", "!pad_4_B", 0x0D, "B");
+
+ OutputReport0x80.addOutput("[Channel2]", "!shift", 0x28, "B");
+ OutputReport0x80.addOutput("[Channel2]", "sync_enabled", 0x24, "B");
+ OutputReport0x80.addOutput("[Channel2]", "cue_indicator", 0x27, "B");
+ OutputReport0x80.addOutput("[Channel2]", "play_indicator", 0x23, "B");
+
+ OutputReport0x80.addOutput("[Channel2]", "!pad_1_G", 0x2C, "B");
+ OutputReport0x80.addOutput("[Channel2]", "!pad_1_B", 0x30, "B");
+
+ OutputReport0x80.addOutput("[Channel2]", "!pad_2_G", 0x2B, "B");
+ OutputReport0x80.addOutput("[Channel2]", "!pad_2_B", 0x2F, "B");
+
+ OutputReport0x80.addOutput("[Channel2]", "!pad_3_G", 0x2A, "B");
+ OutputReport0x80.addOutput("[Channel2]", "!pad_3_B", 0x2E, "B");
+
+ OutputReport0x80.addOutput("[Channel2]", "!pad_4_G", 0x29, "B");
+ OutputReport0x80.addOutput("[Channel2]", "!pad_4_B", 0x2D, "B");
+
+ this.controller.registerOutputPacket(OutputReport0x80);
+
+ // Link up control objects to their outputs
+ TraktorS2MK1.linkDeckOutputs("sync_enabled", TraktorS2MK1.outputCallback);
+ TraktorS2MK1.linkDeckOutputs("cue_indicator", TraktorS2MK1.outputCallback);
+ TraktorS2MK1.linkDeckOutputs("play_indicator", TraktorS2MK1.outputCallback);
+
+ TraktorS2MK1.setPadMode("[Channel1]", TraktorS2MK1.padModes.hotcue);
+ TraktorS2MK1.setPadMode("[Channel2]", TraktorS2MK1.padModes.hotcue);
+
+ TraktorS2MK1.linkDeckOutputs("loop_in", TraktorS2MK1.outputCallbackLoop);
+ TraktorS2MK1.linkDeckOutputs("loop_out", TraktorS2MK1.outputCallbackLoop);
+ TraktorS2MK1.linkDeckOutputs("LoadSelectedTrack", TraktorS2MK1.outputCallback);
+ TraktorS2MK1.linkDeckOutputs("slip_enabled", TraktorS2MK1.outputCallback);
+ TraktorS2MK1.linkChannelOutput("[Channel1]", "pfl", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[Channel2]", "pfl", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[Channel1]", "track_loaded", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[Channel2]", "track_loaded", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[Channel1]", "PeakIndicator", TraktorS2MK1.outputChannelCallbackDark);
+ TraktorS2MK1.linkChannelOutput("[Channel2]", "PeakIndicator", TraktorS2MK1.outputChannelCallbackDark);
+ TraktorS2MK1.linkChannelOutput("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", TraktorS2MK1.outputChannelCallback);
+ TraktorS2MK1.linkChannelOutput("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", TraktorS2MK1.outputChannelCallback);
+
+ engine.makeConnection("[EffectRack1_EffectUnit1]", "focused_effect", TraktorS2MK1.onFocusedEffectChange).trigger();
+ engine.makeConnection("[EffectRack1_EffectUnit2]", "focused_effect", TraktorS2MK1.onFocusedEffectChange).trigger();
+ TraktorS2MK1.connectEffectButtonLEDs("[EffectRack1_EffectUnit1]");
+ TraktorS2MK1.connectEffectButtonLEDs("[EffectRack1_EffectUnit2]");
+
+ engine.makeConnection("[Channel1]", "VuMeter", TraktorS2MK1.onVuMeterChanged).trigger();
+ engine.makeConnection("[Channel2]", "VuMeter", TraktorS2MK1.onVuMeterChanged).trigger();
+
+ engine.makeConnection("[Channel1]", "loop_enabled", TraktorS2MK1.onLoopEnabledChanged);
+ engine.makeConnection("[Channel2]", "loop_enabled", TraktorS2MK1.onLoopEnabledChanged);
+};
+
+TraktorS2MK1.linkDeckOutputs = function(key, callback) {
+ TraktorS2MK1.controller.linkOutput("[Channel1]", key, "[Channel1]", key, callback);
+ TraktorS2MK1.controller.linkOutput("[Channel2]", key, "[Channel2]", key, callback);
+};
+
+TraktorS2MK1.linkDeckCustomOutputs = function(key, callback) {
+ engine.makeConnection("[Channel1]", key, callback).trigger();
+ engine.makeConnection("[Channel2]", key, callback).trigger();
+};
+
+TraktorS2MK1.linkChannelOutput = function(group, key, callback) {
+ TraktorS2MK1.controller.linkOutput(group, key, group, key, callback);
+};
+
+TraktorS2MK1.lightGroup = function(packet, outputGroupName, coGroupName) {
+ var groupObject = packet.groups[outputGroupName];
+ for (var fieldName in groupObject) {
+ var field = groupObject[fieldName];
+ if (field.name[0] === "!") {
+ continue;
+ }
+ if (field.mapped_callback) {
+ var value = engine.getValue(coGroupName, field.name);
+ field.mapped_callback(value, coGroupName, field.name);
+ }
+ // No callback, no light!
+ }
+};
+
+TraktorS2MK1.lightDeck = function(group) {
+ // Freeze the lights while we do this update so we don't spam HID.
+ this.batchingLEDUpdate = true;
+ for (var packetName in this.controller.OutputPackets) {
+ var packet = this.controller.OutputPackets[packetName];
+ TraktorS2MK1.lightGroup(packet, group, group);
+ // These outputs show state managed by this script and do not react to ControlObject changes,
+ // so manually set them here.
+ TraktorS2MK1.outputCallback(0, group, "!shift");
+ TraktorS2MK1.outputCallback(0, group, "!reset_button");
+ TraktorS2MK1.outputCallback(0, group, "!samples_button");
+ }
+
+ this.batchingLEDUpdate = false;
+ // And now send them all.
+ for (packetName in this.controller.OutputPackets) {
+ this.controller.OutputPackets[packetName].send();
+ }
+};
+
+TraktorS2MK1.init = function() {
+ if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) {
+ throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" +
+ "ShiftCueButtonAction is: " + ShiftCueButtonAction);
+ }
+ if (typeof ButtonBrightnessOff !== "number" || ButtonBrightnessOff < 0 || ButtonBrightnessOff > 0x1f) {
+ throw new Error("ButtonBrightnessOff must be a number between 0 and 0x1f (31).\n" +
+ "ButtonBrightnessOff is: " + ButtonBrightnessOff);
+ }
+ if (typeof ButtonBrightnessOff !== "number" || ButtonBrightnessOff < 0 || ButtonBrightnessOff > 0x1f) {
+ throw new Error("ButtonBrightnessOn must be a number between 0 and 0x1f (31).\n" +
+ "ButtonBrightnessOn is: " + ButtonBrightnessOn);
+ }
+ if (ButtonBrightnessOn < ButtonBrightnessOff) {
+ throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" +
+ "ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" +
+ "ButtonBrightnessOff is: " + ButtonBrightnessOff);
+ }
+
+ TraktorS2MK1.registerInputPackets();
+
+ var debugLEDs = false;
+ if (debugLEDs) {
+ var data = [0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f];
+ controller.send(data, data.length, 0x80);
+ } else {
+ TraktorS2MK1.registerOutputPackets();
+ }
+
+ TraktorS2MK1.controller.setOutput("[Master]", "!warninglight", 0x00, true);
+ TraktorS2MK1.lightDeck("[Channel1]");
+ TraktorS2MK1.lightDeck("[Channel2]");
+ TraktorS2MK1.lightDeck("[EffectRack1_EffectUnit1]");
+ TraktorS2MK1.lightDeck("[EffectRack1_EffectUnit2]");
+};
+
+TraktorS2MK1.shutdown = function() {
+ var data = [];
+ for (var i = 0; i < 61; i++) {
+ data[i] = 0;
+ }
+ controller.send(data, data.length, 0x80);
+};
+
+TraktorS2MK1.incomingData = function(data, length) {
+ TraktorS2MK1.controller.parsePacket(data, length);
+};
+
+// The input report 0x01 handles buttons and jog wheels.
+TraktorS2MK1.inputReport0x01Callback = function(packet, data) {
+ for (var name in data) {
+ var field = data[name];
+ if (field.name === "!jog_wheel") {
+ TraktorS2MK1.controller.processControl(field);
+ continue;
+ }
+
+ TraktorS2MK1.controller.processButton(field);
+ }
+};
+
+// There are no buttons handled by input report 0x02, so this is a little simpler.
+TraktorS2MK1.inputReport0x02Callback = function(packet, data) {
+ for (var name in data) {
+ var field = data[name];
+ TraktorS2MK1.controller.processControl(field);
+ }
+};
+
+TraktorS2MK1.samplerGainKnob = function(field) {
+ for (var i = 1; i <= 8; i++) {
+ engine.setParameter("[Sampler" + i + "]", "pregain", field.value / 4095);
+ }
+};
+
+TraktorS2MK1.toggleButton = function(field) {
+ if (field.value > 0) {
+ script.toggleControl(field.group, field.name);
+ }
+};
+
+TraktorS2MK1.shift = function(field) {
+ var shiftPressed = field.value > 0;
+ TraktorS2MK1.shiftPressed[field.group] = shiftPressed;
+ TraktorS2MK1.controller.setOutput(field.group, "!shift",
+ shiftPressed ? ButtonBrightnessOn : ButtonBrightnessOff,
+ !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.loadTrackButton = function(field) {
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ engine.setValue(field.group, "CloneFromDeck", 0);
+ } else {
+ engine.setValue(field.group, "LoadSelectedTrack", field.value);
+ }
+};
+
+TraktorS2MK1.syncButton = function(field) {
+ var now = Date.now();
+
+ // If shifted, just toggle.
+ // TODO(later version): actually make this enable explicit master.
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ if (field.value === 0) {
+ return;
+ }
+ var synced = engine.getValue(field.group, "sync_enabled");
+ engine.setValue(field.group, "sync_enabled", !synced);
+ } else {
+ if (field.value === 1) {
+ TraktorS2MK1.syncEnabledTime[field.group] = now;
+ engine.setValue(field.group, "sync_enabled", 1);
+ } else {
+ if (!engine.getValue(field.group, "sync_enabled")) {
+ // If disabled, and switching to disable... stay disabled.
+ engine.setValue(field.group, "sync_enabled", 0);
+ return;
+ }
+ // was enabled, and button has been let go. maybe latch it.
+ if (now - TraktorS2MK1.syncEnabledTime[field.group] > 300) {
+ engine.setValue(field.group, "sync_enabled", 1);
+ return;
+ }
+ engine.setValue(field.group, "sync_enabled", 0);
+ }
+ }
+};
+
+TraktorS2MK1.cueButton = function(field) {
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ if (ShiftCueButtonAction === "REWIND") {
+ if (field.value === 0) {
+ return;
+ }
+ engine.setValue(field.group, "start_stop", 1);
+ } else if (ShiftCueButtonAction === "REVERSEROLL") {
+ engine.setValue(field.group, "reverseroll", field.value);
+ }
+ } else {
+ engine.setValue(field.group, "cue_default", field.value);
+ }
+};
+
+TraktorS2MK1.playButton = function(field) {
+ if (field.value === 0) {
+ return;
+ }
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ var locked = engine.getValue(field.group, "keylock");
+ engine.setValue(field.group, "keylock", !locked);
+ } else {
+ var playing = engine.getValue(field.group, "play");
+ var deckNumber = TraktorS2MK1.controller.resolveDeck(field.group);
+ // Failsafe to disable scratching in case the finishJogTouch timer has not executed yet
+ // after a backspin.
+ if (engine.isScratching(deckNumber)) {
+ engine.scratchDisable(deckNumber, false);
+ }
+ engine.setValue(field.group, "play", !playing);
+ }
+};
+
+TraktorS2MK1.jogTouch = function(field) {
+ if (TraktorS2MK1.wheelTouchInertiaTimer[field.group] !== 0) {
+ // The wheel was touched again, reset the timer.
+ engine.stopTimer(TraktorS2MK1.wheelTouchInertiaTimer[field.group]);
+ TraktorS2MK1.wheelTouchInertiaTimer[field.group] = 0;
+ }
+ if (field.value > JogWheelTouchThreshold[field.group]) {
+ var deckNumber = TraktorS2MK1.controller.resolveDeck(field.group);
+ engine.scratchEnable(deckNumber, 1024, 33.3333, 0.125, 0.125/8, true);
+ } else {
+ // The wheel touch sensor can be overly sensitive, so don't release scratch mode right away.
+ // Depending on how fast the platter was moving, lengthen the time we'll wait.
+ var scratchRate = Math.abs(engine.getValue(field.group, "scratch2"));
+ // inertiaTime was experimentally determined. It should be enough time to allow the user to
+ // press play after a backspin without normal playback starting before they can press the
+ // button, but not so long that there is an awkward delay before stopping scratching after
+ // a backspin.
+ var inertiaTime;
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ inertiaTime = Math.pow(1.7, scratchRate / 10) / 1.6;
+ } else {
+ inertiaTime = Math.pow(1.7, scratchRate) / 1.6;
+ }
+ if (inertiaTime < 100) {
+ // Just do it now.
+ TraktorS2MK1.finishJogTouch(field.group);
+ } else {
+ TraktorS2MK1.wheelTouchInertiaTimer[field.group] = engine.beginTimer(
+ inertiaTime, function() {
+ TraktorS2MK1.finishJogTouch(field.group);
+ }, true);
+ }
+ }
+};
+
+TraktorS2MK1.finishJogTouch = function(group) {
+ TraktorS2MK1.wheelTouchInertiaTimer[group] = 0;
+ var deckNumber = TraktorS2MK1.controller.resolveDeck(group);
+ var play = engine.getValue(group, "play");
+ if (play !== 0) {
+ // If we are playing, just hand off to the engine.
+ engine.scratchDisable(deckNumber, true);
+ } else {
+ // If things are paused, there will be a non-smooth handoff between scratching and jogging.
+ // Instead, keep scratch on until the platter is not moving.
+ var scratchRate = Math.abs(engine.getValue(group, "scratch2"));
+ if (scratchRate < 0.01) {
+ // The platter is basically stopped, now we can disable scratch and hand off to jogging.
+ engine.scratchDisable(deckNumber, true);
+ } else {
+ // Check again soon.
+ TraktorS2MK1.wheelTouchInertiaTimer[group] = engine.beginTimer(
+ 1, function() {
+ TraktorS2MK1.finishJogTouch(group);
+ }, true);
+ }
+ }
+};
+
+TraktorS2MK1.jogMove = function(field) {
+ var deltas = TraktorS2MK1.wheelDeltas(field.group, field.value);
+ var tickDelta = deltas[0];
+ var timeDelta = deltas[1];
+
+ if (engine.getValue(field.group, "scratch2_enable")) {
+ var deckNumber = TraktorS2MK1.controller.resolveDeck(field.group);
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ tickDelta *= 10;
+ }
+ engine.scratchTick(deckNumber, tickDelta);
+ } else {
+ var velocity = TraktorS2MK1.scalerJog(tickDelta, timeDelta, field.group);
+ engine.setValue(field.group, "jog", velocity);
+ }
+};
+
+TraktorS2MK1.wheelDeltas = function(group, value) {
+ // When the wheel is touched, four bytes change, but only the first behaves predictably.
+ // It looks like the wheel is 1024 ticks per revolution.
+ var tickval = value & 0xFF;
+ var timeValue = value >>> 16;
+ var previousTick = 0;
+ var previousTime = 0;
+
+ if (group[8] === "1" || group[8] === "3") {
+ previousTick = TraktorS2MK1.lastTickValue[0];
+ previousTime = TraktorS2MK1.lastTickTime[0];
+ TraktorS2MK1.lastTickValue[0] = tickval;
+ TraktorS2MK1.lastTickTime[0] = timeValue;
+ } else {
+ previousTick = TraktorS2MK1.lastTickValue[1];
+ previousTime = TraktorS2MK1.lastTickTime[1];
+ TraktorS2MK1.lastTickValue[1] = tickval;
+ TraktorS2MK1.lastTickTime[1] = timeValue;
+ }
+
+ if (previousTime > timeValue) {
+ // We looped around. Adjust current time so that subtraction works.
+ timeValue += 0x10000;
+ }
+ var timeDelta = timeValue - previousTime;
+ if (timeDelta === 0) {
+ // Spinning too fast to detect speed! By not dividing we are guessing it took 1ms.
+ timeDelta = 1;
+ }
+
+ var tickDelta = 0;
+ if (previousTick >= 200 && tickval <= 100) {
+ tickDelta = tickval + 256 - previousTick;
+ } else if (previousTick <= 100 && tickval >= 200) {
+ tickDelta = tickval - previousTick - 256;
+ } else {
+ tickDelta = tickval - previousTick;
+ }
+ //HIDDebug(group + " " + tickval + " " + previousTick + " " + tickDelta);
+ return [tickDelta, timeDelta];
+};
+
+TraktorS2MK1.scalerJog = function(tickDelta, timeDelta, group) {
+ if (engine.getValue(group, "play")) {
+ return (tickDelta / timeDelta) / 3;
+ } else {
+ return (tickDelta / timeDelta) * 2.0;
+ }
+};
+
+var introOutroKeys = [
+ "intro_start",
+ "intro_end",
+ "outro_start",
+ "outro_end"
+];
+
+var introOutroColors = [
+ {green: 0x1F, blue: 0},
+ {green: 0x1F, blue: 0},
+ {green: 0, blue: 0x1F},
+ {green: 0, blue: 0x1F}
+];
+
+var introOutroColorsDim = [
+ {green: 0x05, blue: 0},
+ {green: 0x05, blue: 0},
+ {green: 0, blue: 0x05},
+ {green: 0, blue: 0x05}
+];
+
+
+TraktorS2MK1.setPadMode = function(group, padMode) {
+ TraktorS2MK1.padConnections[group].forEach(function(connection) {
+ connection.disconnect();
+ });
+ TraktorS2MK1.padConnections[group] = [];
+
+ if (padMode === TraktorS2MK1.padModes.hotcue) {
+ for (var i = 1; i <= 4; i++) {
+ TraktorS2MK1.padConnections[group].push(
+ engine.makeConnection(group, "hotcue_" + i + "_enabled", TraktorS2MK1.outputHotcueCallback));
+ }
+ } else if (padMode === TraktorS2MK1.padModes.introOutro) {
+ for (i = 1; i <= 4; i++) {
+ // This function to create callback functions is needed so the loop index variable
+ // i does not get captured in a closure within the callback.
+ var makeIntroOutroCallback = function(padNumber) {
+ return function(value, group, _control) {
+ if (value > 0) {
+ TraktorS2MK1.sendPadColor(group, padNumber, introOutroColors[padNumber-1]);
+ } else {
+ TraktorS2MK1.sendPadColor(group, padNumber, introOutroColorsDim[padNumber-1]);
+ }
+ };
+ };
+ TraktorS2MK1.padConnections[group].push(engine.makeConnection(
+ group, introOutroKeys[i-1] + "_enabled", makeIntroOutroCallback(i)));
+ }
+ } else if (padMode === TraktorS2MK1.padModes.sampler) {
+ for (i = 1; i <= 4; i++) {
+ var makeSamplerCallback = function(deckGroup, padNumber) {
+ var samplerNumber = deckGroup === "[Channel1]" ? padNumber : padNumber + 4;
+ var samplerGroup = "[Sampler" + samplerNumber + "]";
+ return function(_value, _group, _control) {
+ if (engine.getValue(samplerGroup, "track_loaded")) {
+ if (engine.getValue(samplerGroup, "play") === 1) {
+ if (engine.getValue(samplerGroup, "repeat") === 1) {
+ TraktorS2MK1.sendPadColor(deckGroup, padNumber,
+ {green: 0x1F, blue: 0x1F});
+ } else {
+ TraktorS2MK1.sendPadColor(deckGroup, padNumber,
+ {green: 0x1F, blue: 0});
+ }
+ } else {
+ TraktorS2MK1.sendPadColor(deckGroup, padNumber, {green: 0x05, blue: 0x00});
+ }
+ } else {
+ TraktorS2MK1.sendPadColor(deckGroup, padNumber, {green: 0, blue: 0});
+ }
+ };
+ };
+
+ var sNumber = group === "[Channel1]" ? i : i + 4;
+ var sGroup = "[Sampler" + sNumber + "]";
+ TraktorS2MK1.padConnections[group].push(engine.makeConnection(
+ sGroup, "track_loaded", makeSamplerCallback(group, i)));
+ TraktorS2MK1.padConnections[group].push(engine.makeConnection(
+ sGroup, "play", makeSamplerCallback(group, i)));
+ TraktorS2MK1.padConnections[group].push(engine.makeConnection(
+ sGroup, "repeat", makeSamplerCallback(group, i)));
+ }
+ }
+
+ TraktorS2MK1.padConnections[group].forEach(function(connection) {
+ connection.trigger();
+ });
+
+ TraktorS2MK1.currentPadMode[group] = padMode;
+};
+
+TraktorS2MK1.hotcueButton = function(buttonNumber, group, value) {
+ if (TraktorS2MK1.shiftPressed[group]) {
+ engine.setValue(group, "hotcue_" + buttonNumber + "_clear", value);
+ } else {
+ engine.setValue(group, "hotcue_" + buttonNumber + "_activate", value);
+ }
+};
+
+TraktorS2MK1.introOutroButton = function(buttonNumber, group, value) {
+ if (TraktorS2MK1.shiftPressed[group]) {
+ engine.setValue(group, introOutroKeys[buttonNumber-1] + "_clear", value);
+ } else {
+ engine.setValue(group, introOutroKeys[buttonNumber-1] + "_activate", value);
+ }
+};
+
+TraktorS2MK1.samplerButton = function(buttonNumber, group, value) {
+ if (value === 0) {
+ return;
+ }
+ var samplerNumber = group === "[Channel1]" ? buttonNumber : buttonNumber + 4;
+ var samplerGroup = "[Sampler" + samplerNumber + "]";
+ if (TraktorS2MK1.shiftPressed[group]) {
+ if (engine.getValue(samplerGroup, "play") === 1) {
+ engine.setValue(samplerGroup, "play", 0);
+ } else {
+ script.triggerControl(samplerGroup, "eject");
+ }
+ } else {
+ if (engine.getValue(samplerGroup, "track_loaded") === 0) {
+ script.triggerControl(samplerGroup, "LoadSelectedTrack");
+ } else {
+ script.triggerControl(samplerGroup, "cue_gotoandplay");
+ }
+ }
+};
+
+TraktorS2MK1.padButton = function(field) {
+ var buttonNumber = parseInt(field.name[field.name.length - 1]);
+ var padMode = TraktorS2MK1.currentPadMode[field.group];
+
+ if (padMode === TraktorS2MK1.padModes.hotcue) {
+ TraktorS2MK1.hotcueButton(buttonNumber, field.group, field.value);
+ } else if (padMode === TraktorS2MK1.padModes.introOutro) {
+ TraktorS2MK1.introOutroButton(buttonNumber, field.group, field.value);
+ } else if (padMode === TraktorS2MK1.padModes.sampler) {
+ TraktorS2MK1.samplerButton(buttonNumber, field.group, field.value);
+ }
+};
+
+TraktorS2MK1.samplerModeButton = function(field) {
+ if (field.value === 0) {
+ return;
+ }
+ var padMode = TraktorS2MK1.currentPadMode[field.group];
+ if (padMode !== TraktorS2MK1.padModes.sampler) {
+ TraktorS2MK1.setPadMode(field.group, TraktorS2MK1.padModes.sampler);
+ TraktorS2MK1.controller.setOutput(field.group, "!samples_button", ButtonBrightnessOn, false);
+ TraktorS2MK1.controller.setOutput(field.group, "!reset_button", ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ } else {
+ TraktorS2MK1.setPadMode(field.group, TraktorS2MK1.padModes.hotcue);
+ TraktorS2MK1.controller.setOutput(field.group, "!samples_button", ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ }
+};
+
+TraktorS2MK1.introOutroModeButton = function(field) {
+ if (field.value === 0) {
+ return;
+ }
+ var padMode = TraktorS2MK1.currentPadMode[field.group];
+ if (padMode !== TraktorS2MK1.padModes.introOutro) {
+ TraktorS2MK1.setPadMode(field.group, TraktorS2MK1.padModes.introOutro);
+ TraktorS2MK1.controller.setOutput(field.group, "!reset_button", ButtonBrightnessOn, false);
+ TraktorS2MK1.controller.setOutput(field.group, "!samples_button", ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ } else {
+ TraktorS2MK1.setPadMode(field.group, TraktorS2MK1.padModes.hotcue);
+ TraktorS2MK1.controller.setOutput(field.group, "!reset_button", ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ }
+};
+
+TraktorS2MK1.loopInButton = function(field) {
+ engine.setValue(field.group, "loop_in", field.value);
+};
+
+TraktorS2MK1.loopOutButton = function(field) {
+ engine.setValue(field.group, "loop_out", field.value);
+};
+
+// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
+TraktorS2MK1.connectEffectButtonLEDs = function(effectUnitGroup) {
+ TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup].forEach(function(connection) {
+ connection.disconnect();
+ });
+
+ var focusedEffect = engine.getValue(effectUnitGroup, "focused_effect");
+ var makeButtonLEDcallback = function(effectNumber) {
+ return function(value, _group, _control) {
+ TraktorS2MK1.controller.setOutput(effectUnitGroup, "!effectbutton" + effectNumber,
+ value === 1 ? ButtonBrightnessOn : ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ };
+ };
+
+ // FIXME: Why do the LEDs flicker?
+ TraktorS2MK1.batchingLEDUpdate = true;
+ for (var i = 0; i <= 2; i++) {
+ var effectGroup;
+ var key;
+ if (focusedEffect === 0) {
+ effectGroup = effectUnitGroup.slice(0, -1) + "_Effect" + (i+1) + "]";
+ key = "enabled";
+ } else {
+ effectGroup = effectUnitGroup.slice(0, -1) + "_Effect" + focusedEffect + "]";
+ key = "button_parameter" + (i+1);
+ }
+ TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup][i] = engine.makeConnection(
+ effectGroup, key, makeButtonLEDcallback(i+1));
+ TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup][i].trigger();
+ }
+ TraktorS2MK1.batchingLEDUpdate = false;
+ TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup][2].trigger();
+};
+
+// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
+TraktorS2MK1.onShowParametersChange = function(value, group, _control) {
+ if (value === 0) {
+ if (engine.getValue(group, "show_focus") > 0) {
+ engine.setValue(group, "show_focus", 0);
+ TraktorS2MK1.previouslyFocusedEffect[group] = engine.getValue(group, "focused_effect");
+ engine.setValue(group, "focused_effect", 0);
+ }
+ } else {
+ engine.setValue(group, "show_focus", 1);
+ if (TraktorS2MK1.previouslyFocusedEffect[group] !== null) {
+ engine.setValue(group, "focused_effect", TraktorS2MK1.previouslyFocusedEffect[group]);
+ }
+ }
+ TraktorS2MK1.connectEffectButtonLEDs(group);
+};
+
+// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
+TraktorS2MK1.onFocusedEffectChange = function(value, group, _control) {
+ TraktorS2MK1.controller.setOutput(group, "!effect_focus_button", value > 0 ? ButtonBrightnessOn : ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ if (value === 0) {
+ for (var i = 1; i <= 2; i++) {
+ // The previously focused effect is not available here, so iterate over all effects' parameter knobs.
+ for (var j = 1; j < 3; j++) {
+ engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "parameter" + j);
+ }
+ }
+ } else {
+ for (i = 1; i <= 2; i++) {
+ engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "meta");
+ }
+ }
+};
+
+// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
+TraktorS2MK1.effectFocusButton = function(field) {
+ var showParameters = engine.getValue(field.group, "show_parameters");
+ if (field.value > 0) {
+ var effectUnitNumber = field.group.slice(-2, -1);
+ if (TraktorS2MK1.shiftPressed["[Channel" + effectUnitNumber + "]"]) {
+ engine.setValue(field.group, "load_preset", 1);
+ return;
+ }
+ TraktorS2MK1.effectFocusLongPressTimer[field.group] = engine.beginTimer(TraktorS2MK1.longPressTimeoutMilliseconds, function() {
+ TraktorS2MK1.effectFocusChooseModeActive[field.group] = true;
+ TraktorS2MK1.effectButtonLEDconnections[field.group].forEach(function(connection) {
+ connection.disconnect();
+ });
+ var makeButtonLEDcallback = function(buttonNumber) {
+ return function(value, group, _control) {
+ TraktorS2MK1.controller.setOutput(group, "!effectbutton" + buttonNumber,
+ value === buttonNumber ? ButtonBrightnessOn : ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate);
+ };
+ };
+ TraktorS2MK1.batchingLEDUpdate = true;
+ for (var i = 0; i <= 2; i++) {
+ TraktorS2MK1.effectButtonLEDconnections[i] = engine.makeConnection(
+ field.group, "focused_effect", makeButtonLEDcallback(i+1));
+ TraktorS2MK1.effectButtonLEDconnections[i].trigger();
+ }
+ TraktorS2MK1.batchingLEDUpdate = false;
+ TraktorS2MK1.effectButtonLEDconnections[2].trigger();
+ });
+ if (!showParameters) {
+ engine.setValue(field.group, "show_parameters", 1);
+ TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group] = true;
+ }
+ } else {
+ if (TraktorS2MK1.effectFocusLongPressTimer[field.group] !== 0) {
+ engine.stopTimer(TraktorS2MK1.effectFocusLongPressTimer[field.group]);
+ TraktorS2MK1.effectFocusLongPressTimer[field.group] = 0;
+ }
+
+ if (TraktorS2MK1.effectFocusChooseModeActive[field.group]) {
+ TraktorS2MK1.effectFocusChooseModeActive[field.group] = false;
+ TraktorS2MK1.connectEffectButtonLEDs(field.group);
+ } else if (showParameters && !TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group]) {
+ engine.setValue(field.group, "show_parameters", 0);
+ }
+
+ TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group] = false;
+ }
+};
+
+// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
+TraktorS2MK1.effectKnob = function(field) {
+ var knobNumber = parseInt(field.id.slice(-1));
+ var effectUnitGroup = field.group;
+ var focusedEffect = engine.getValue(effectUnitGroup, "focused_effect");
+ if (focusedEffect > 0) {
+ engine.setParameter(effectUnitGroup.slice(0, -1) + "_Effect" + focusedEffect + "]",
+ "parameter" + knobNumber,
+ field.value / 4095);
+ } else {
+ engine.setParameter(effectUnitGroup.slice(0, -1) + "_Effect" + knobNumber + "]",
+ "meta",
+ field.value / 4095);
+ }
+};
+
+// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
+TraktorS2MK1.effectButton = function(field) {
+ var buttonNumber = parseInt(field.id.slice(-1));
+ var effectUnitGroup = field.group;
+ var effectUnitNumber = field.group.match(script.effectUnitRegEx)[1];
+ var focusedEffect = engine.getValue(effectUnitGroup, "focused_effect");
+
+ var toggle = function() {
+ var group;
+ var key;
+ if (focusedEffect === 0) {
+ group = effectUnitGroup.slice(0, -1) + "_Effect" + buttonNumber + "]";
+ key = "enabled";
+ } else {
+ group = effectUnitGroup.slice(0, -1) + "_Effect" + focusedEffect + "]";
+ key = "button_parameter" + buttonNumber;
+ }
+ script.toggleControl(group, key);
+ };
+
+ if (field.value > 0) {
+ if (TraktorS2MK1.shiftPressed["[Channel" + effectUnitNumber + "]"]) {
+ engine.setValue(effectUnitGroup, "load_preset", buttonNumber+1);
+ } else {
+ if (TraktorS2MK1.effectFocusChooseModeActive[effectUnitGroup]) {
+ if (focusedEffect === buttonNumber) {
+ engine.setValue(effectUnitGroup, "focused_effect", 0);
+ } else {
+ engine.setValue(effectUnitGroup, "focused_effect", buttonNumber);
+ }
+ TraktorS2MK1.effectFocusChooseModeActive[effectUnitGroup] = false;
+ } else {
+ toggle();
+ TraktorS2MK1.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] =
+ engine.beginTimer(TraktorS2MK1.longPressTimeoutMilliseconds,
+ function() {
+ TraktorS2MK1.effectButtonIsLongPressed[effectUnitGroup][buttonNumber] = true;
+ TraktorS2MK1.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] = 0;
+ },
+ true
+ );
+ }
+ }
+ } else {
+ engine.stopTimer(TraktorS2MK1.effectButtonLongPressTimer[effectUnitGroup][buttonNumber]);
+ TraktorS2MK1.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] = 0;
+ if (TraktorS2MK1.effectButtonIsLongPressed[effectUnitGroup][buttonNumber]) {
+ toggle();
+ }
+ TraktorS2MK1.effectButtonIsLongPressed[effectUnitGroup][buttonNumber] = false;
+ }
+};
+
+/// return value 1 === right turn
+/// return value -1 === left turn
+TraktorS2MK1.encoderDirection = function(newValue, oldValue) {
+ var direction = 0;
+ var min = 0;
+ var max = 15;
+ if (oldValue === max && newValue === min) {
+ direction = 1;
+ } else if (oldValue === min && newValue === max) {
+ direction = -1;
+ } else if (newValue > oldValue) {
+ direction = 1;
+ } else {
+ direction = -1;
+ }
+ return direction;
+};
+
+TraktorS2MK1.gainEncoder = function(field) {
+ var delta = 0.03333 * TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousPregain[field.group]);
+ TraktorS2MK1.previousPregain[field.group] = field.value;
+
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ var currentPregain = engine.getParameter(field.group, "pregain");
+ engine.setParameter(field.group, "pregain", currentPregain + delta);
+ } else {
+ var quickEffectGroup = "[QuickEffectRack1_" + field.group + "]";
+ if (TraktorS2MK1.gainEncoderPressed[field.group]) {
+ script.triggerControl(quickEffectGroup, delta > 0 ? "next_chain" : "prev_chain");
+ } else {
+ var currentQuickEffectSuperKnob = engine.getParameter(quickEffectGroup, "super1");
+ engine.setParameter(quickEffectGroup, "super1", currentQuickEffectSuperKnob + delta);
+ }
+ }
+};
+
+TraktorS2MK1.gainEncoderPress = function(field) {
+ if (field.value > 0) {
+ TraktorS2MK1.gainEncoderPressed[field.group] = true;
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ script.triggerControl(field.group, "pregain_set_default");
+ } else {
+ script.triggerControl("[QuickEffectRack1_" + field.group + "]", "super1_set_default");
+ }
+ } else {
+ TraktorS2MK1.gainEncoderPressed[field.group] = false;
+ }
+};
+
+TraktorS2MK1.leftEncoder = function(field) {
+ var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousLeftEncoder[field.group]);
+ TraktorS2MK1.previousLeftEncoder[field.group] = field.value;
+
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ if (delta === 1) {
+ script.triggerControl(field.group, "pitch_up_small");
+ } else {
+ script.triggerControl(field.group, "pitch_down_small");
+ }
+ } else {
+ if (TraktorS2MK1.leftEncoderPressed[field.group]) {
+ var beatjumpSize = engine.getValue(field.group, "beatjump_size");
+ if (delta === 1) {
+ beatjumpSize *= 2;
+ } else {
+ beatjumpSize /= 2;
+ }
+ engine.setValue(field.group, "beatjump_size", beatjumpSize);
+ } else {
+ if (delta === 1) {
+ script.triggerControl(field.group, "beatjump_forward");
+ } else {
+ script.triggerControl(field.group, "beatjump_backward");
+ }
+ }
+ }
+};
+
+TraktorS2MK1.leftEncoderPress = function(field) {
+ TraktorS2MK1.leftEncoderPressed[field.group] = (field.value > 0);
+ if (TraktorS2MK1.shiftPressed[field.group] && field.value > 0) {
+ script.triggerControl(field.group, "pitch_adjust_set_default");
+ }
+};
+
+TraktorS2MK1.rightEncoder = function(field) {
+ var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousRightEncoder[field.group]);
+ TraktorS2MK1.previousRightEncoder[field.group] = field.value;
+
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ if (delta === 1) {
+ script.triggerControl(field.group, "beatjump_1_forward");
+ } else {
+ script.triggerControl(field.group, "beatjump_1_backward");
+ }
+ } else {
+ if (delta === 1) {
+ script.triggerControl(field.group, "loop_double");
+ } else {
+ script.triggerControl(field.group, "loop_halve");
+ }
+ }
+};
+
+TraktorS2MK1.rightEncoderPress = function(field) {
+ if (field.value === 0) {
+ return;
+ }
+ var loopEnabled = engine.getValue(field.group, "loop_enabled");
+ // The actions triggered below change the state of loop_enabled,
+ // so to simplify the logic, use script.triggerControl to only act
+ // on press rather than resetting ControlObjects to 0 on release.
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ if (loopEnabled) {
+ script.triggerControl(field.group, "reloop_andstop");
+ } else {
+ script.triggerControl(field.group, "reloop_toggle");
+ }
+ } else {
+ if (loopEnabled) {
+ script.triggerControl(field.group, "reloop_toggle");
+ } else {
+ script.triggerControl(field.group, "beatloop_activate");
+ }
+ }
+};
+
+TraktorS2MK1.browseEncoder = function(field) {
+ var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousBrowse);
+ TraktorS2MK1.previousBrowse = field.value;
+
+ if (TraktorS2MK1.shiftPressed["[Channel1]"] || TraktorS2MK1.shiftPressed["[Channel2]"]) {
+ delta *= 5;
+ }
+ engine.setValue("[Playlist]", "SelectTrackKnob", delta);
+};
+
+TraktorS2MK1.scalerParameter = function(group, name, value) {
+ return script.absoluteLin(value, 0, 1, 16, 4080);
+};
+
+TraktorS2MK1.scalerVolume = function(group, name, value) {
+ if (group === "[Master]") {
+ return script.absoluteNonLin(value, 0, 1, 4, 16, 4080);
+ } else {
+ return script.absoluteNonLin(value, 0, 0.25, 1, 16, 4080);
+ }
+};
+
+TraktorS2MK1.scalerSlider = function(group, name, value) {
+ return script.absoluteLin(value, -1, 1, 16, 4080);
+};
+
+TraktorS2MK1.outputChannelCallback = function(value, group, key) {
+ var ledValue = 0x05;
+ if (value) {
+ ledValue = 0x1F;
+ }
+ TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.outputChannelCallbackDark = function(value, group, key) {
+ var ledValue = 0x00;
+ if (value) {
+ ledValue = 0x1F;
+ }
+ TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.outputCallback = function(value, group, key) {
+ var ledValue = ButtonBrightnessOff;
+ if (value) {
+ ledValue = ButtonBrightnessOn;
+ }
+ TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.outputCallbackLoop = function(value, group, key) {
+ var ledValue = ButtonBrightnessOff;
+ if (engine.getValue(group, "loop_enabled")) {
+ ledValue = 0x1F;
+ }
+ TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.outputCallbackDark = function(value, group, key) {
+ var ledValue = 0x00;
+ if (value) {
+ ledValue = 0x1F;
+ }
+ TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.pflButton = function(field) {
+ if (field.value > 0) {
+ if (TraktorS2MK1.shiftPressed[field.group]) {
+ script.toggleControl(field.group, "quantize");
+ } else {
+ script.toggleControl(field.group, "pfl");
+ }
+ }
+};
+
+TraktorS2MK1.sendPadColor = function(group, padNumber, color) {
+ var padKey = "!pad_" + padNumber + "_";
+ var ColorBrightnessScaler = ButtonBrightnessOn / 0x1f;
+ var green = color.green * ColorBrightnessScaler;
+ var blue = color.blue * ColorBrightnessScaler;
+ if (color.green === 0 && color.blue === 0) {
+ green = ButtonBrightnessOff;
+ blue = ButtonBrightnessOff;
+ }
+ TraktorS2MK1.controller.setOutput(group, padKey + "G", green, false);
+ TraktorS2MK1.controller.setOutput(group, padKey + "B", blue, !TraktorS2MK1.batchingLEDUpdate);
+};
+
+TraktorS2MK1.outputHotcueCallback = function(value, group, key) {
+ var hotcueNumber = key.charAt(7);
+ var color;
+ if (engine.getValue(group, "hotcue_" + hotcueNumber + "_enabled")) {
+ color = {green: 0, blue: 0x1F};
+ } else {
+ color = {green: 0, blue: 0};
+ }
+ TraktorS2MK1.sendPadColor(group, hotcueNumber, color);
+};
+
+TraktorS2MK1.onVuMeterChanged = function(value, group, _key) {
+ // This handler is called a lot so it should be as fast as possible.
+
+ // Figure out number of fully-illuminated segments.
+ var scaledValue = value * 4.0;
+ var fullIllumCount = Math.floor(scaledValue);
+
+ // Figure out how much the partially-illuminated segment is illuminated.
+ var partialIllum = (scaledValue - fullIllumCount) * 0x1F;
+
+ for (var i = 0; i <= 3; i++) {
+ var key = "!VuMeter" + i;
+ if (i < fullIllumCount) {
+ // Don't update lights until they're all done, so the last term is false.
+ TraktorS2MK1.controller.setOutput(group, key, 0x1F, false);
+ } else if (i === fullIllumCount) {
+ TraktorS2MK1.controller.setOutput(group, key, partialIllum, false);
+ } else {
+ TraktorS2MK1.controller.setOutput(group, key, 0x00, false);
+ }
+ }
+ TraktorS2MK1.controller.OutputPackets.OutputReport0x80.send();
+};
+
+TraktorS2MK1.onLoopEnabledChanged = function(value, group, _key) {
+ TraktorS2MK1.outputCallbackLoop(value, group, "loop_in");
+ TraktorS2MK1.outputCallbackLoop(value, group, "loop_out");
+};
+
+// # Feature Report Description
+//
+// Feature Report `208` (`0xd0`)
+// | Byte | Endianness | Description |
+// |--------|------------|-----------------------------------------------------------|
+// | 0 | - | Always `d0` |
+// | 1..12 | - | I don't know/always `01 00 00 00 10 00 f0 0f 10 00 f0 0f` |
+// | 13..14 | LE | Channel 1 volume fader bottom |
+// | 15..16 | LE | Channel 1 volume fader top |
+// | 17..18 | LE | Channel 2 volume fader bottom |
+// | 19..20 | LE | Channel 2 volume fader top |
+// | 21..22 | LE | Crossfader left |
+// | 23..24 | LE | Crossfader right |
+// | 25..32 | - | Padding (`ff`) |
+
+// Feature Report `209` (`0xd1`)
+// | Byte | Endianness | Description |
+// |--------|------------|----------------------------------|
+// | 0 | - | Always `d1` |
+// | 1..2 | LE | Channel 1 FX dry/wet knob left |
+// | 3..4 | LE | Channel 1 FX dry/wet knob center |
+// | 5..6 | LE | Channel 1 FX dry/wet knob right |
+// | 7..8 | LE | Channel 1 FX 1 knob left |
+// | 9..10 | LE | Channel 1 FX 1 knob center |
+// | 11.12 | LE | Channel 1 FX 1 knob right |
+// | 13..14 | LE | Channel 1 FX 2 knob left |
+// | 15..16 | LE | Channel 1 FX 2 knob center |
+// | 17..18 | LE | Channel 1 FX 2 knob right |
+// | 19..20 | LE | Channel 1 FX 3 knob left |
+// | 21..22 | LE | Channel 1 FX 3 knob center |
+// | 23..24 | LE | Channel 2 FX 3 knob right |
+// | 25..26 | LE | Channel 1 EQ hi knob left |
+// | 27..28 | LE | Channel 1 EQ hi knob center |
+// | 29..30 | LE | Channel 1 EQ hi knob right |
+// | 31..32 | LE | Channel 1 EQ mid knob left |
+
+// Feature Report `210` (`0xd2`)
+// | Byte | Endianness | Description |
+// |--------|------------|------------------------------|
+// | 0 | - | Always `d2` |
+// | 1..2 | LE | Channel 1 EQ mid knob center |
+// | 3..4 | LE | Channel 1 EQ mid knob right |
+// | 5..6 | LE | Channel 1 EQ low knob left |
+// | 7..8 | LE | Channel 1 EQ low knob center |
+// | 9..10 | LE | Channel 1 EQ low knob right |
+// | 11..12 | LE | Channel 2 EQ hi knob left |
+// | 13..14 | LE | Channel 2 EQ hi knob center |
+// | 15..16 | LE | Channel 2 EQ hi knob right |
+// | 17..18 | LE | Channel 2 EQ mid knob left |
+// | 19..20 | LE | Channel 2 EQ mid knob center |
+// | 21..22 | LE | Channel 2 EQ mid knob right |
+// | 23..24 | LE | Channel 2 EQ low knob left |
+// | 25..26 | LE | Channel 2 EQ low knob center |
+// | 27..28 | LE | Channel 2 EQ low knob right |
+// | 29..30 | LE | Sample knob left |
+// | 31..32 | LE | Sample knob center |
+
+// Feature Report `211` (`0xd3`)
+// | Byte | Endianness | Description |
+// |--------|------------|----------------------------------|
+// | 0 | - | Always `d3` |
+// | 1..2 | LE | Sample knob right |
+// | 3..4 | LE | Channel 2 FX dry/wet knob left |
+// | 5..6 | LE | Channel 2 FX dry/wet knob center |
+// | 7..8 | LE | Channel 2 FX dry/wet knob right |
+// | 9..10 | LE | Channel 2 FX 1 knob left |
+// | 11..12 | LE | Channel 2 FX 1 knob center |
+// | 13..14 | LE | Channel 2 FX 1 knob right |
+// | 15..16 | LE | Channel 2 FX 2 knob left |
+// | 17..18 | LE | Channel 2 FX 2 knob center |
+// | 19..20 | LE | Channel 2 FX 2 knob right |
+// | 21..22 | LE | Channel 2 FX 3 knob left |
+// | 23..24 | LE | Channel 2 FX 3 knob center |
+// | 25..26 | LE | Channel 2 FX 3 knob right |
+// | 27..32 | - | Padding (`ff`) |
+
+// Feature Report `212` (`0xd4`)
+// | Byte | Endianness | Description |
+// |--------|------------|-----------------------------------------------------------|
+// | 0 | - | Always `d4` |
+// | 1..2 | BE | Left jogwheel unpressed |
+// | 3..4 | BE | Left jogwheel pressed |
+// | 5..6 | BE | Right jogwheel unpressed |
+// | 7..8 | BE | Right jogwheel pressed |
+// | 9 | - | Left jogwhell calibration type (`00` user, `ff` factory) |
+// | 10 | - | Right jogwhell calibration type (`00` user, `ff` factory) |
+// | 11..32 | - | Padding (`ff`) |
+
+// Feature Report `215` (`0xd7`) contains the factory calibration data for jogwheels:
+
+// Restore of both jogwheels to factory settings:
+// Read 215: `d7 00 00 00 00 4e c2 26 82 0c 1b 0d b2 0c 9a 0e 16 2a ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff`
+// Write 212: `d4 0c 1b 0c e6 0c 9a 0d 58 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff`
+
+// | Byte | Endianness | Description |
+// |--------|------------|------------------------------|
+// | 0 | - | Always `d7` |
+// | 1..8 | - | I don't know |
+// | 9..10 | BE | Left jogwheel unpressed |
+// | 11..12 | BE | Left jogwheel fully pressed |
+// | 13..14 | BE | Right jogwheel unpressed |
+// | 15..16 | BE | Right jogwheel fully pressed |
+// | 17 | - | `42` i guess |
+// | 18..32 | - | Padding (`ff`) |
+
+// `212`'s unpressed seems to be the same as unpressed. `212`'s pressed is `(pressed + unpressed) / 2`.