diff options
author | Uwe Klotz <uklotz@mixxx.org> | 2020-08-04 09:12:42 +0200 |
---|---|---|
committer | Uwe Klotz <uklotz@mixxx.org> | 2020-08-04 09:12:42 +0200 |
commit | d4febde01c65f89936be5db6fdbe9c462e192e59 (patch) | |
tree | f3a345a8ed919460591d864eb57e41df57a4c53e | |
parent | bebd0a5d5318719f7e90f383a59660f4902f6f11 (diff) | |
parent | cd7c3e0f29862e6eda3b378dd5096f7d1695a708 (diff) |
Merge branch '2.3' of git@github.com:mixxxdj/mixxx.git
-rw-r--r-- | res/controllers/Traktor Kontrol S2 Mk2.hid.xml | 19 | ||||
-rw-r--r-- | res/controllers/Traktor-Kontrol-S2-MK2-hid-scripts.js | 1381 | ||||
-rw-r--r-- | res/controllers/common-hid-packet-parser.js | 21 | ||||
-rw-r--r-- | src/library/autodj/autodjprocessor.cpp | 88 | ||||
-rw-r--r-- | src/mixxx.cpp | 3 | ||||
-rw-r--r-- | src/test/autodjprocessor_test.cpp | 2 |
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 |