summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUwe Klotz <uklotz@mixxx.org>2020-08-04 09:12:42 +0200
committerUwe Klotz <uklotz@mixxx.org>2020-08-04 09:12:42 +0200
commitd4febde01c65f89936be5db6fdbe9c462e192e59 (patch)
treef3a345a8ed919460591d864eb57e41df57a4c53e
parentbebd0a5d5318719f7e90f383a59660f4902f6f11 (diff)
parentcd7c3e0f29862e6eda3b378dd5096f7d1695a708 (diff)
Merge branch '2.3' of git@github.com:mixxxdj/mixxx.git
-rw-r--r--res/controllers/Traktor Kontrol S2 Mk2.hid.xml19
-rw-r--r--res/controllers/Traktor-Kontrol-S2-MK2-hid-scripts.js1381
-rw-r--r--res/controllers/common-hid-packet-parser.js21
-rw-r--r--src/library/autodj/autodjprocessor.cpp88
-rw-r--r--src/mixxx.cpp3
-rw-r--r--src/test/autodjprocessor_test.cpp2
6 files changed, 1460 insertions, 54 deletions
diff --git a/res/controllers/Traktor Kontrol S2 Mk2.hid.xml b/res/controllers/Traktor Kontrol S2 Mk2.hid.xml
new file mode 100644
index 0000000000..14138ab346
--- /dev/null
+++ b/res/controllers/Traktor Kontrol S2 Mk2.hid.xml
@@ -0,0 +1,19 @@
+<?xml version='1.0' encoding='utf-8'?>
+<MixxxControllerPreset mixxxVersion="2.3.0" schemaVersion="1">
+ <info>
+ <name>Native Instruments Traktor Kontrol S2 MK2</name>
+ <author>Be</author>
+ <description>Native Instruments Traktor Kontrol S2 MK2</description>
+ <wiki>https://github.com/mixxxdj/mixxx/wiki/Native-Instruments-Traktor-Kontrol-S2-Mk2</wiki>
+ <devices>
+ <product protocol="hid" vendor_id="0x17cc" product_id="0x1320"
+ usage_page="0x0" usage="0x0" interface_number="0x3" />
+ </devices>
+ </info>
+ <controller>
+ <scriptfiles>
+ <file functionprefix="" filename="common-hid-packet-parser.js"/>
+ <file functionprefix="TraktorS2MK2" filename="Traktor-Kontrol-S2-MK2-hid-scripts.js"/>
+ </scriptfiles>
+ </controller>
+</MixxxControllerPreset>
diff --git a/res/controllers/Traktor-Kontrol-S2-MK2-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK2-hid-scripts.js
new file mode 100644
index 0000000000..71a9e5a028
--- /dev/null
+++ b/res/controllers/Traktor-Kontrol-S2-MK2-hid-scripts.js
@@ -0,0 +1,1381 @@
+/****************************************************************/
+/* Traktor Kontrol S2 MK2 HID controller script */
+/* Copyright (C) 2020, Be <be@mixxx.org> */
+/* Copyright (C) 2017, douteiful */
+/* Based on: */
+/* Traktor Kontrol S4 MK2 HID controller script v1.00 */
+/* Copyright (C) 2015, the Mixxx Team */
+/* but feel free to tweak this to your heart's content! */
+/****************************************************************/
+
+
+// ==== 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 0x7f (127).
+// If you don't have the optional power adapter and are using the controller with USB bus power,
+// 0x09 is probably too dim to notice.
+var ButtonBrightnessOff = 0x01;
+var ButtonBrightnessOn = 0x7f;
+
+// KNOWN ISSUES:
+// * The effect button LEDs briefly flicker when pressing the effect focus button.
+
+// eslint definitions
+/* global controller, HIDController, HIDPacket */
+var TraktorS2MK2 = 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.topEncoderPressed = {
+ "[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]": []
+ };
+
+};
+
+TraktorS2MK2.registerInputPackets = function() {
+ var MessageShort = new HIDPacket("shortmessage", 0x01, this.shortMessageCallback);
+ var MessageLong = new HIDPacket("longmessage", 0x02, this.longMessageCallback);
+
+ // Values in the short message are all buttons, except the jog wheels.
+ // An exclamation point indicates a specially-handled function. Everything else is a standard
+ // Mixxx control object name.
+
+ MessageShort.addControl("[Channel1]", "!top_encoder_press", 0x0D, "B", 0x40, false, this.topEncoderPress);
+ MessageShort.addControl("[Channel1]", "!shift", 0x0B, "B", 0x08, false, this.shift);
+ MessageShort.addControl("[Channel1]", "!sync_enabled", 0x0B, "B", 0x04, false, this.syncButton);
+ MessageShort.addControl("[Channel1]", "!cue_default", 0x0B, "B", 0x02, false, this.cueButton);
+ MessageShort.addControl("[Channel1]", "!play", 0x0B, "B", 0x01, false, this.playButton);
+ MessageShort.addControl("[Channel1]", "!pad1", 0x0B, "B", 0x80, false, this.padButton);
+ MessageShort.addControl("[Channel1]", "!pad2", 0x0B, "B", 0x40, false, this.padButton);
+ MessageShort.addControl("[Channel1]", "!pad3", 0x0B, "B", 0x20, false, this.padButton);
+ MessageShort.addControl("[Channel1]", "!pad4", 0x0B, "B", 0x10, false, this.padButton);
+ MessageShort.addControl("[Channel1]", "!loop_in", 0x0C, "B", 0x40, false, this.loopInButton);
+ MessageShort.addControl("[Channel1]", "!loop_out", 0x0C, "B", 0x80, false, this.loopOutButton);
+ MessageShort.addControl("[Channel1]", "!remix_button", 0x0C, "B", 0x02, false, this.samplerModeButton);
+ MessageShort.addControl("[Channel1]", "!flux_button", 0x0C, "B", 0x20, false, this.introOutroModeButton);
+ MessageShort.addControl("[Channel1]", "!left_encoder_press", 0x0F, "B", 0x01, false, this.leftEncoderPress);
+ MessageShort.addControl("[Channel1]", "!right_encoder_press", 0x0F, "B", 0x02, false, this.rightEncoderPress);
+ MessageShort.addControl("[Channel1]", "!jog_touch", 0x0A, "B", 0x01, false, this.jogTouch);
+ MessageShort.addControl("[Channel1]", "!jog_wheel", 0x01, "I", null, false, this.jogMove);
+ MessageShort.addControl("[Channel1]", "!load_track", 0x0C, "B", 0x08, false, this.loadTrackButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effect_focus_button",
+ 0x0E, "B", 0x10, false, this.effectFocusButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectbutton1", 0x0E, "B", 0x80, false, this.effectButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectbutton2", 0x0E, "B", 0x40, false, this.effectButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectbutton3", 0x0E, "B", 0x20, false, this.effectButton);
+
+ MessageShort.addControl("[Channel2]", "!top_encoder_press", 0x0D, "B", 0x80, false, this.topEncoderPress);
+ MessageShort.addControl("[Channel2]", "!shift", 0x09, "B", 0x08, false, this.shift);
+ MessageShort.addControl("[Channel2]", "!sync_enabled", 0x09, "B", 0x04, false, this.syncButton);
+ MessageShort.addControl("[Channel2]", "!cue_default", 0x09, "B", 0x02, false, this.cueButton);
+ MessageShort.addControl("[Channel2]", "!play", 0x09, "B", 0x01, false, this.playButton);
+ MessageShort.addControl("[Channel2]", "!pad1", 0x09, "B", 0x80, false, this.padButton);
+ MessageShort.addControl("[Channel2]", "!pad2", 0x09, "B", 0x40, false, this.padButton);
+ MessageShort.addControl("[Channel2]", "!pad3", 0x09, "B", 0x20, false, this.padButton);
+ MessageShort.addControl("[Channel2]", "!pad4", 0x09, "B", 0x10, false, this.padButton);
+ MessageShort.addControl("[Channel2]", "!loop_in", 0x0A, "B", 0x40, false, this.loopInButton);
+ MessageShort.addControl("[Channel2]", "!loop_out", 0x0A, "B", 0x80, false, this.loopOutButton);
+ MessageShort.addControl("[Channel2]", "!remix_button", 0x0C, "B", 0x01, false, this.samplerModeButton);
+ MessageShort.addControl("[Channel2]", "!flux_button", 0x0A, "B", 0x20, false, this.introOutroModeButton);
+ MessageShort.addControl("[Channel2]", "!left_encoder_press", 0x0F, "B", 0x08, false, this.leftEncoderPress);
+ MessageShort.addControl("[Channel2]", "!right_encoder_press", 0x0F, "B", 0x10, false, this.rightEncoderPress);
+ MessageShort.addControl("[Channel2]", "!jog_touch", 0x0A, "B", 0x02, false, this.jogTouch);
+ MessageShort.addControl("[Channel2]", "!jog_wheel", 0x05, "I", null, false, this.jogMove);
+ MessageShort.addControl("[Channel2]", "!load_track", 0x0C, "B", 0x04, false, this.loadTrackButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effect_focus_button",
+ 0xD, "B", 0x04, false, this.effectFocusButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectbutton1", 0xD, "B", 0x20, false, this.effectButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectbutton2", 0xD, "B", 0x10, false, this.effectButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectbutton3", 0xD, "B", 0x08, false, this.effectButton);
+
+ MessageShort.addControl("[Channel1]", "!pfl", 0x0C, "B", 0x10, false, this.pflButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", 0x0E, "B", 0x08);
+ MessageShort.addControl("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", 0x0E, "B", 0x04);
+
+ MessageShort.addControl("[Channel2]", "!pfl", 0x0A, "B", 0x10, false, this.pflButton);
+ MessageShort.addControl("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", 0x0E, "B", 0x02);
+ MessageShort.addControl("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", 0x0E, "B", 0x01);
+
+ MessageShort.addControl("[Master]", "maximize_library", 0x0F, "B", 0x04, false, this.toggleButton);
+
+ MessageShort.addControl("[Microphone]", "talkover", 0x0A, "B", 0x08, false, this.toggleButton);
+
+ engine.makeConnection("[EffectRack1_EffectUnit1]", "show_parameters", TraktorS2MK2.onShowParametersChange);
+ engine.makeConnection("[EffectRack1_EffectUnit2]", "show_parameters", TraktorS2MK2.onShowParametersChange);
+
+ this.controller.registerInputPacket(MessageShort);
+
+ // Most items in the long message are controls that go from 0-4096.
+ // There are also some 4 bit encoders.
+ MessageLong.addControl("[Channel1]", "rate", 0x07, "H");
+ MessageLong.addControl("[Channel2]", "rate", 0x09, "H");
+ engine.softTakeover("[Channel1]", "rate", true);
+ engine.softTakeover("[Channel2]", "rate", true);
+ MessageLong.addControl("[Channel1]", "!left_encoder", 0x01, "B", 0x0F, false, this.leftEncoder);
+ MessageLong.addControl("[Channel1]", "!right_encoder", 0x01, "B", 0xF0, false, this.rightEncoder);
+ MessageLong.addControl("[Channel2]", "!left_encoder", 0x02, "B", 0xF0, false, this.leftEncoder);
+ MessageLong.addControl("[Channel2]", "!right_encoder", 0x03, "B", 0x0F, false, this.rightEncoder);
+
+ MessageLong.addControl("[EffectRack1_EffectUnit1]", "mix", 0x17, "H");
+ MessageLong.addControl("[EffectRack1_EffectUnit1]", "!effectknob1", 0x19, "H", null, false, this.effectKnob);
+ MessageLong.addControl("[EffectRack1_EffectUnit1]", "!effectknob2", 0x1B, "H", null, false, this.effectKnob);
+ MessageLong.addControl("[EffectRack1_EffectUnit1]", "!effectknob3", 0x1D, "H", null, false, this.effectKnob);
+
+ MessageLong.addControl("[EffectRack1_EffectUnit2]", "mix", 0x1F, "H");
+ MessageLong.addControl("[EffectRack1_EffectUnit2]", "!effectknob1", 0x21, "H", null, false, this.effectKnob);
+ MessageLong.addControl("[EffectRack1_EffectUnit2]", "!effectknob2", 0x23, "H", null, false, this.effectKnob);
+ MessageLong.addControl("[EffectRack1_EffectUnit2]", "!effectknob3", 0x25, "H", null, false, this.effectKnob);
+
+ for (var i = 1; i <= 2; i++) {
+ engine.softTakeover("[EffectRack1_EffectUnit1_Effect" + i + "]", "meta", true);
+ engine.softTakeover("[EffectRack1_EffectUnit2_Effect" + i + "]", "meta", true);
+ for (var j = 1; j <= 2; j++) {
+ engine.softTakeover("[EffectRack1_EffectUnit1_Effect" + i + "]", "parameter" + j, true);
+ engine.softTakeover("[EffectRack1_EffectUnit2_Effect" + i + "]", "parameter" + j, true);
+ }
+ }
+
+ MessageLong.addControl("[Channel1]", "volume", 0x13, "H");
+ MessageLong.addControl("[EqualizerRack1_[Channel1]_Effect1]", "parameter3", 0x27, "H");
+ MessageLong.addControl("[EqualizerRack1_[Channel1]_Effect1]", "parameter2", 0x29, "H");
+ MessageLong.addControl("[EqualizerRack1_[Channel1]_Effect1]", "parameter1", 0x2B, "H");
+ MessageLong.addControl("[Channel1]", "pregain", 0x03, "B", 0xF0, false, this.topEncoder);
+
+ MessageLong.addControl("[Channel2]", "volume", 0x15, "H");
+ MessageLong.addControl("[EqualizerRack1_[Channel2]_Effect1]", "parameter3", 0x2D, "H");
+ MessageLong.addControl("[EqualizerRack1_[Channel2]_Effect1]", "parameter2", 0x2F, "H");
+ MessageLong.addControl("[EqualizerRack1_[Channel2]_Effect1]", "parameter1", 0x31, "H");
+ MessageLong.addControl("[Channel2]", "pregain", 0x04, "B", 0x0F, false, this.topEncoder);
+
+ // The master gain knob controls the internal sound card volume, so if this was mapped
+ // the gain would be double-applied.
+ //MessageLong.addControl("[Master]", "volume", 0x11, "H");
+ MessageLong.addControl("[Master]", "crossfader", 0x05, "H");
+ MessageLong.addControl("[Master]", "headMix", 0x0B, "H");
+ MessageLong.addControl("[Master]", "!samplerGain", 0xD, "H");
+ MessageLong.setCallback("[Master]", "!samplerGain", this.samplerGainKnob);
+ MessageLong.addControl("[Playlist]", "!browse", 0x02, "B", 0x0F, false, this.browseEncoder);
+
+ TraktorS2MK2.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);
+ this.controller.registerInputPacket(MessageLong);
+};
+
+TraktorS2MK2.registerOutputPackets = function() {
+ var OutputTop = new HIDPacket("outputTop", 0x80);
+ var OutputBottom = new HIDPacket("outputBottom", 0x81);
+
+ OutputTop.addOutput("[Channel1]", "track_loaded", 0x19, "B");
+ OutputTop.addOutput("[Channel2]", "track_loaded", 0x1A, "B");
+
+ var VuOffsets = {
+ "[Channel1]": 0x01,
+ "[Channel2]": 0x06
+ };
+ for (var ch in VuOffsets) {
+ for (var i = 0; i <= 0x03; i++) {
+ OutputTop.addOutput(ch, "!" + "VuMeter" + i, VuOffsets[ch] + i, "B");
+ }
+ }
+
+ OutputTop.addOutput("[Channel1]", "PeakIndicator", 0x05, "B");
+ OutputTop.addOutput("[Channel2]", "PeakIndicator", 0x0A, "B");
+
+ OutputTop.addOutput("[Channel1]", "!flux_button", 0x20, "B");
+ OutputTop.addOutput("[Channel1]", "loop_in", 0x21, "B");
+ OutputTop.addOutput("[Channel1]", "loop_out", 0x22, "B");
+
+ OutputTop.addOutput("[Channel2]", "!flux_button", 0x25, "B");
+ OutputTop.addOutput("[Channel2]", "loop_in", 0x23, "B");
+ OutputTop.addOutput("[Channel2]", "loop_out", 0x24, "B");
+
+ OutputTop.addOutput("[Channel1]", "pfl", 0x1B, "B");
+ OutputTop.addOutput("[Master]", "!usblight", 0x1D, "B");
+ OutputTop.addOutput("[Channel2]", "pfl", 0x1F, "B");
+
+ OutputTop.addOutput("[EffectRack1_EffectUnit1]", "!effect_focus_button", 0xB, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton1", 0xC, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton2", 0xD, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton3", 0xE, "B");
+
+ OutputTop.addOutput("[EffectRack1_EffectUnit2]", "!effect_focus_button", 0x13, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton1", 0x14, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton2", 0x15, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton3", 0x16, "B");
+
+ OutputTop.addOutput("[Channel1]", "!remix_button", 0x17, "B");
+ OutputTop.addOutput("[Channel2]", "!remix_button", 0x18, "B");
+
+ OutputTop.addOutput("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", 0x0F, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", 0x10, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", 0x11, "B");
+ OutputTop.addOutput("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", 0x12, "B");
+
+ this.controller.registerOutputPacket(OutputTop);
+
+ OutputBottom.addOutput("[Channel1]", "!shift", 0x19, "B");
+ OutputBottom.addOutput("[Channel1]", "sync_enabled", 0x1A, "B");
+ OutputBottom.addOutput("[Channel1]", "cue_indicator", 0x1B, "B");
+ OutputBottom.addOutput("[Channel1]", "play_indicator", 0x1C, "B");
+
+ OutputBottom.addOutput("[Channel1]", "!pad_1_R", 0x01, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_1_G", 0x02, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_1_B", 0x03, "B");
+
+ OutputBottom.addOutput("[Channel1]", "!pad_2_R", 0x04, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_2_G", 0x05, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_2_B", 0x06, "B");
+
+ OutputBottom.addOutput("[Channel1]", "!pad_3_R", 0x07, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_3_G", 0x08, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_3_B", 0x09, "B");
+
+ OutputBottom.addOutput("[Channel1]", "!pad_4_R", 0x0A, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_4_G", 0x0B, "B");
+ OutputBottom.addOutput("[Channel1]", "!pad_4_B", 0x0C, "B");
+
+ OutputBottom.addOutput("[Channel2]", "!shift", 0x1D, "B");
+ OutputBottom.addOutput("[Channel2]", "sync_enabled", 0x1E, "B");
+ OutputBottom.addOutput("[Channel2]", "cue_indicator", 0x1F, "B");
+ OutputBottom.addOutput("[Channel2]", "play_indicator", 0x20, "B");
+
+ OutputBottom.addOutput("[Channel2]", "!pad_1_R", 0x0D, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_1_G", 0x0E, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_1_B", 0x0F, "B");
+
+ OutputBottom.addOutput("[Channel2]", "!pad_2_R", 0x10, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_2_G", 0x11, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_2_B", 0x12, "B");
+
+ OutputBottom.addOutput("[Channel2]", "!pad_3_R", 0x13, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_3_G", 0x14, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_3_B", 0x15, "B");
+
+ OutputBottom.addOutput("[Channel2]", "!pad_4_R", 0x16, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_4_G", 0x17, "B");
+ OutputBottom.addOutput("[Channel2]", "!pad_4_B", 0x18, "B");
+
+ this.controller.registerOutputPacket(OutputBottom);
+
+ // Link up control objects to their outputs
+ TraktorS2MK2.linkDeckOutputs("sync_enabled", TraktorS2MK2.outputCallback);
+ TraktorS2MK2.linkDeckOutputs("cue_indicator", TraktorS2MK2.outputCallback);
+ TraktorS2MK2.linkDeckOutputs("play_indicator", TraktorS2MK2.outputCallback);
+
+ TraktorS2MK2.setPadMode("[Channel1]", TraktorS2MK2.padModes.hotcue);
+ TraktorS2MK2.setPadMode("[Channel2]", TraktorS2MK2.padModes.hotcue);
+
+ TraktorS2MK2.linkDeckOutputs("loop_in", TraktorS2MK2.outputCallbackLoop);
+ TraktorS2MK2.linkDeckOutputs("loop_out", TraktorS2MK2.outputCallbackLoop);
+ TraktorS2MK2.linkDeckOutputs("keylock", TraktorS2MK2.outputCallbackDark);
+ TraktorS2MK2.linkDeckOutputs("LoadSelectedTrack", TraktorS2MK2.outputCallback);
+ TraktorS2MK2.linkDeckOutputs("slip_enabled", TraktorS2MK2.outputCallback);
+ TraktorS2MK2.linkChannelOutput("[Channel1]", "pfl", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[Channel2]", "pfl", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[Channel1]", "track_loaded", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[Channel2]", "track_loaded", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[Channel1]", "PeakIndicator", TraktorS2MK2.outputChannelCallbackDark);
+ TraktorS2MK2.linkChannelOutput("[Channel2]", "PeakIndicator", TraktorS2MK2.outputChannelCallbackDark);
+ TraktorS2MK2.linkChannelOutput("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", TraktorS2MK2.outputChannelCallback);
+ TraktorS2MK2.linkChannelOutput("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", TraktorS2MK2.outputChannelCallback);
+
+ engine.makeConnection("[EffectRack1_EffectUnit1]", "focused_effect", TraktorS2MK2.onFocusedEffectChange).trigger();
+ engine.makeConnection("[EffectRack1_EffectUnit2]", "focused_effect", TraktorS2MK2.onFocusedEffectChange).trigger();
+ TraktorS2MK2.connectEffectButtonLEDs("[EffectRack1_EffectUnit1]");
+ TraktorS2MK2.connectEffectButtonLEDs("[EffectRack1_EffectUnit2]");
+
+ engine.makeConnection("[Channel1]", "VuMeter", TraktorS2MK2.onVuMeterChanged).trigger();
+ engine.makeConnection("[Channel2]", "VuMeter", TraktorS2MK2.onVuMeterChanged).trigger();
+
+ engine.makeConnection("[Channel1]", "loop_enabled", TraktorS2MK2.onLoopEnabledChanged);
+ engine.makeConnection("[Channel2]", "loop_enabled", TraktorS2MK2.onLoopEnabledChanged);
+};
+
+TraktorS2MK2.linkDeckOutputs = function(key, callback) {
+ // Linking outputs is a little tricky because the library doesn't quite do what I want. But this
+ // method works.
+ TraktorS2MK2.controller.linkOutput("[Channel1]", key, "[Channel1]", key, callback);
+ engine.connectControl("[Channel3]", key, callback);
+ TraktorS2MK2.controller.linkOutput("[Channel2]", key, "[Channel2]", key, callback);
+ engine.connectControl("[Channel4]", key, callback);
+};
+
+TraktorS2MK2.linkChannelOutput = function(group, key, callback) {
+ TraktorS2MK2.controller.linkOutput(group, key, group, key, callback);
+};
+
+TraktorS2MK2.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!
+ }
+};
+
+TraktorS2MK2.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];
+ TraktorS2MK2.lightGroup(packet, group, group);
+ // These outputs show state managed by this script and do not react to ControlObject changes,
+ // so manually set them here.
+ TraktorS2MK2.outputCallback(0, group, "!shift");
+ TraktorS2MK2.outputCallback(0, group, "!flux_button");
+ TraktorS2MK2.outputCallback(0, group, "!remix_button");
+ }
+
+ this.batchingLEDUpdate = false;
+ // And now send them all.
+ for (packetName in this.controller.OutputPackets) {
+ this.controller.OutputPackets[packetName].send();
+ }
+};
+
+TraktorS2MK2.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 > 0x7f) {
+ throw new Error("ButtonBrightnessOff must be a number between 0 and 0x7f (127).\n" +
+ "ButtonBrightnessOff is: " + ButtonBrightnessOff);
+ }
+ if (typeof ButtonBrightnessOff !== "number" || ButtonBrightnessOff < 0 || ButtonBrightnessOff > 0x7f) {
+ throw new Error("ButtonBrightnessOn must be a number between 0 and 0x7f (127).\n" +
+ "ButtonBrightnessOn is: " + ButtonBrightnessOn);
+ }
+ if (ButtonBrightnessOn < ButtonBrightnessOff) {
+ throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" +
+ "ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" +
+ "ButtonBrightnessOff is: " + ButtonBrightnessOff);
+ }
+
+ TraktorS2MK2.registerInputPackets();
+
+ var debugLEDs = false;
+ if (debugLEDs) {
+ var data = [0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 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 {
+ TraktorS2MK2.registerOutputPackets();
+ }
+
+ TraktorS2MK2.controller.setOutput("[Master]", "!usblight", 0x7F, true);
+ TraktorS2MK2.lightDeck("[Channel1]");
+ TraktorS2MK2.lightDeck("[Channel2]");
+ TraktorS2MK2.lightDeck("[EffectRack1_EffectUnit1]");
+ TraktorS2MK2.lightDeck("[EffectRack1_EffectUnit2]");
+};
+
+TraktorS2MK2.shutdown = function() {
+ var data = [];
+ for (var i = 0; i <= 37; i++) {
+ data[i] = 0;
+ }
+ // Leave USB plug indicator light on.
+ data[0x1C] = ButtonBrightnessOn;
+ controller.send(data, data.length, 0x80);
+
+ for (i = 0; i <= 32; i++) {
+ data[i] = 0;
+ }
+ controller.send(data, data.length, 0x81);
+};
+
+TraktorS2MK2.incomingData = function(data, length) {
+ TraktorS2MK2.controller.parsePacket(data, length);
+};
+
+// The short message handles buttons and jog wheels.
+TraktorS2MK2.shortMessageCallback = function(packet, data) {
+ for (var name in data) {
+ var field = data[name];
+ if (field.name === "!jog_wheel") {
+ TraktorS2MK2.controller.processControl(field);
+ continue;
+ }
+
+ TraktorS2MK2.controller.processButton(field);
+ }
+};
+
+// There are no buttons handled by the long message, so this is a little simpler.
+TraktorS2MK2.longMessageCallback = function(packet, data) {
+ for (var name in data) {
+ var field = data[name];
+ TraktorS2MK2.controller.processControl(field);
+ }
+};
+
+TraktorS2MK2.samplerGainKnob = function(field) {
+ for (var i = 1; i <= 8; i++) {
+ engine.setParameter("[Sampler" + i + "]", "pregain", field.value / 4096);
+ }
+};
+
+TraktorS2MK2.toggleButton = function(field) {
+ if (field.value > 0) {
+ script.toggleControl(field.group, field.name);
+ }
+};
+
+TraktorS2MK2.shift = function(field) {
+ var group = field.id.split(".")[0];
+ TraktorS2MK2.shiftPressed[group] = field.value > 0;
+ TraktorS2MK2.controller.setOutput(group, "!shift",
+ TraktorS2MK2.shiftPressed[group] ? ButtonBrightnessOn : ButtonBrightnessOff, field.group, "!shift",
+ !TraktorS2MK2.batchingLEDUpdate);
+};
+
+TraktorS2MK2.loadTrackButton = function(field) {
+ var splitted = field.id.split(".");
+ var group = splitted[0];
+ if (TraktorS2MK2.shiftPressed[group]) {
+ engine.setValue(field.group, "eject", field.value);
+ } else {
+ engine.setValue(field.group, "LoadSelectedTrack", field.value);
+ }
+};
+
+TraktorS2MK2.syncButton = function(field) {
+ var now = Date.now();
+
+ var splitted = field.id.split(".");
+ var group = splitted[0];
+ // If shifted, just toggle.
+ // TODO(later version): actually make this enable explicit master.
+ if (TraktorS2MK2.shiftPressed[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) {
+ TraktorS2MK2.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 - TraktorS2MK2.syncEnabledTime[field.group] > 300) {
+ engine.setValue(field.group, "sync_enabled", 1);
+ return;
+ }
+ engine.setValue(field.group, "sync_enabled", 0);
+ }
+ }
+};
+
+TraktorS2MK2.cueButton = function(field) {
+ var splitted = field.id.split(".");
+ var group = splitted[0];
+ if (TraktorS2MK2.shiftPressed[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);
+ }
+};
+
+TraktorS2MK2.playButton = function(field) {
+ if (field.value === 0) {
+ return;
+ }
+ var splitted = field.id.split(".");
+ var group = splitted[0];
+ if (TraktorS2MK2.shiftPressed[group]) {
+ var locked = engine.getValue(field.group, "keylock");
+ engine.setValue(field.group, "keylock", !locked);
+ } else {
+ var playing = engine.getValue(field.group, "play");
+ var deckNumber = TraktorS2MK2.controller.resolveDeck(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);
+ }
+};
+
+TraktorS2MK2.jogTouch = function(field) {
+ if (TraktorS2MK2.wheelTouchInertiaTimer[field.group] !== 0) {
+ // The wheel was touched again, reset the timer.
+ engine.stopTimer(TraktorS2MK2.wheelTouchInertiaTimer[field.group]);
+ TraktorS2MK2.wheelTouchInertiaTimer[field.group] = 0;
+ }
+ if (field.value !== 0) {
+ var deckNumber = TraktorS2MK2.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 (TraktorS2MK2.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.
+ TraktorS2MK2.finishJogTouch(field.group);
+ } else {
+ TraktorS2MK2.wheelTouchInertiaTimer[field.group] = engine.beginTimer(
+ inertiaTime, function() {
+ TraktorS2MK2.finishJogTouch(field.group);
+ }, true);
+ }
+ }
+};
+
+TraktorS2MK2.finishJogTouch = function(group) {
+ TraktorS2MK2.wheelTouchInertiaTimer[group] = 0;
+ var deckNumber = TraktorS2MK2.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.
+ TraktorS2MK2.wheelTouchInertiaTimer[group] = engine.beginTimer(
+ 1, function() {
+ TraktorS2MK2.finishJogTouch(group);
+ }, true);
+ }
+ }
+};
+
+TraktorS2MK2.jogMove = function(field) {
+ var deltas = TraktorS2MK2.wheelDeltas(field.group, field.value);
+ var tickDelta = deltas[0];
+ var timeDelta = deltas[1];
+
+ if (engine.getValue(field.group, "scratch2_enable")) {
+ var deckNumber = TraktorS2MK2.controller.resolveDeck(field.group);
+ if (TraktorS2MK2.shiftPressed[field.group]) {
+ tickDelta *= 10;
+ }
+ engine.scratchTick(deckNumber, tickDelta);
+ } else {
+ var velocity = TraktorS2MK2.scalerJog(tickDelta, timeDelta, field.group);
+ engine.setValue(field.group, "jog", velocity);
+ }
+};
+
+TraktorS2MK2.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 = TraktorS2MK2.lastTickValue[0];
+ previousTime = TraktorS2MK2.lastTickTime[0];
+ TraktorS2MK2.lastTickValue[0] = tickval;
+ TraktorS2MK2.lastTickTime[0] = timeValue;
+ } else {
+ previousTick = TraktorS2MK2.lastTickValue[1];
+ previousTime = TraktorS2MK2.lastTickTime[1];
+ TraktorS2MK2.lastTickValue[1] = tickval;
+ TraktorS2MK2.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];
+};
+
+TraktorS2MK2.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 = [
+ {red: 0, green: 0x7f, blue: 0},
+ {red: 0, green: 0x7f, blue: 0},
+ {red: 0x7f, green: 0, blue: 0},
+ {red: 0x7f, green: 0, blue: 0}
+];
+
+var introOutroColorsDim = [
+ {red: 0, green: ButtonBrightnessOff, blue: 0},
+ {red: 0, green: ButtonBrightnessOff, blue: 0},
+ {red: ButtonBrightnessOff, green: 0, blue: 0},
+ {red: ButtonBrightnessOff, green: 0, blue: 0}
+];
+
+
+TraktorS2MK2.setPadMode = function(group, padMode) {
+ TraktorS2MK2.padConnections[group].forEach(function(connection) {
+ connection.disconnect();
+ });
+ TraktorS2MK2.padConnections[group] = [];
+
+ if (padMode === TraktorS2MK2.padModes.hotcue) {
+ for (var i = 1; i <= 4; i++) {
+ TraktorS2MK2.padConnections[group].push(
+ engine.makeConnection(group, "hotcue_" + i + "_enabled", TraktorS2MK2.outputHotcueCallback));
+ TraktorS2MK2.padConnections[group].push(
+ engine.makeConnection(group, "hotcue_" + i + "_color", TraktorS2MK2.outputHotcueCallback));
+ }
+ } else if (padMode === TraktorS2MK2.padModes.introOutro) {
+ for (i = 1; i <= 4; i++) {
+ // This function to create callback funct