From 9a7cf36c34ce997942f05caef16b2a0070a8459e Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Mon, 30 Jan 2023 17:01:22 +0100 Subject: [PATCH 01/16] Traktor S2 MK1: Refactor mapping --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 2359 +++++++++-------- 1 file changed, 1181 insertions(+), 1178 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index e209ae1b3e6..d2f0cba9482 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -15,9 +15,9 @@ // 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, +const JogWheelTouchThreshold = { + "[Channel1]": 3222, + "[Channel2]": 3444, }; // ==== Friendly User Configuration ==== @@ -25,1339 +25,1342 @@ var JogWheelTouchThreshold = { // 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"; +const 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]": [] - }; -}; +const ButtonBrightnessOff = 0x00; +const ButtonBrightnessOn = 0x1F; -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); +const padModes = { + "hotcue": 0, + "introOutro": 1, + "sampler": 2 }; -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"); +class DeckClass { + constructor(parent, number) { + this.parent = parent; + this.controller = this.parent.controller; + this.number = number; + this.channel = "[Channel" + number + "]"; + this.previousPregain = 0; + this.previousLeftEncoder = 0; + this.previousRightEncoder = 0; + this.wheelTouchInertiaTimer = 0; + this.gainEncoderPressed = false; + this.leftEncoderPressed = false; + this.rightEncoderPressed = false; + this.shiftPressed = false; + this.currentPadMode = padModes.hotcue; + this.pads = [ + new PadButton(this, 1), + new PadButton(this, 2), + new PadButton(this, 3), + new PadButton(this, 4), + ]; + this.eq = new Equalizer(this); + this.lastTickTime = 0; + this.lastTickValue = 0; + this.syncEnabledTime = {}; + } + registerInputs(inputReport0x01, inputReport0x02, config) { + // InputReport 0x01 + this.registerButton(inputReport0x01, "!gain_encoder_press", config.gainEncoderPress, this.gainEncoderPress); + this.registerButton(inputReport0x01, "!shift", config.shift, this.shift); + this.registerButton(inputReport0x01, "!sync_enabled", config.sync, this.syncButton); + this.registerButton(inputReport0x01, "!cue_default", config.cue, this.cueButton); + this.registerButton(inputReport0x01, "!play", config.play, this.playButton); + for (let i = 0; i < 4; i++) { + this.pads[i].registerInputs(inputReport0x01, config.pads[i]); } - } - - 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; + this.registerButton(inputReport0x01, "!loop_in", config.loopIn, this.loopInButton); + this.registerButton(inputReport0x01, "!loop_out", config.loopOut, this.loopOutButton); + this.registerButton(inputReport0x01, "!samples_button", config.samples, this.samplerModeButton); + this.registerButton(inputReport0x01, "!reset_button", config.reset, this.introOutroModeButton); + this.registerButton(inputReport0x01, "!left_encoder_press", config.leftEncoderPress, this.leftEncoderPress); + this.registerButton(inputReport0x01, "!right_encoder_press", config.rightEncoderPress, this.rightEncoderPress); + this.registerButton(inputReport0x01, "!load_track", config.loadTrack, this.loadTrackButton); + this.registerButton(inputReport0x01, "!pfl", config.pfl, this.pflButton); + inputReport0x01.addControl(this.channel, "!jog_wheel", config.jogWheel, "I", 0xFFFFFFFF, false, this.jogMove.bind(this)); + // InputReport 0x02 + this.registerScalar(inputReport0x02, "rate", config.rate); + this.registerEncoder(inputReport0x02, "!left_encoder", config.leftEncoder, this.leftEncoder); + this.registerEncoder(inputReport0x02, "!right_encoder", config.rightEncoder, this.rightEncoder); + this.registerScalar(inputReport0x02, "volume", config.volume); + this.registerEncoder(inputReport0x02, "!pregain", config.gain, this.gainEncoder); + this.registerScalar(inputReport0x02, "!jog_touch", config.jogTouch, this.jogTouch); + this.eq.registerInputs(inputReport0x02, config.eq); + // configure soft takeover + engine.softTakeover(this.group, "rate", true); + engine.softTakeover(this.group, "volume", true); + } + registerOutputs(outputReport0x80, config) { + this.registerLed(outputReport0x80, "track_loaded", config.trackLoaded); + for (let i = 0; i < 4; i++) { + this.registerLed(outputReport0x80, "!VuMeter" + i, config.vuMeter + i); } - if (field.mapped_callback) { - var value = engine.getValue(coGroupName, field.name); - field.mapped_callback(value, coGroupName, field.name); + this.registerLed(outputReport0x80, "PeakIndicator", config.peak); + this.registerLed(outputReport0x80, "!reset_button", config.reset); + this.registerLed(outputReport0x80, "loop_in", config.loopIn); + this.registerLed(outputReport0x80, "loop_out", config.loopOut); + this.registerLed(outputReport0x80, "pfl", config.pfl); + this.registerLed(outputReport0x80, "!samples_button", config.samples); + this.registerLed(outputReport0x80, "!shift", config.shift); + this.registerLed(outputReport0x80, "sync_enabled", config.sync); + this.registerLed(outputReport0x80, "cue_indicator", config.cue); + this.registerLed(outputReport0x80, "play_indicator", config.play); + for (let i = 0; i < 4; i++) { + this.pads[i].registerOutputs(outputReport0x80, config.pads[i]); } - // 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"); + linkOutputs() { + this.linkLed("sync_enabled", this.outputCallback); + this.linkLed("cue_indicator", this.outputCallback); + this.linkLed("play_indicator", this.outputCallback); + this.linkLed("loop_in", this.outputCallbackLoop); + this.linkLed("loop_out", this.outputCallbackLoop); + this.linkLed("pfl", this.outputCallback); + this.linkLed("track_loaded", this.outputCallback); + this.linkLed("PeakIndicator", this.outputCallback); + engine.makeConnection(this.channel, "VuMeter", this.onVuMeterChanged.bind(this)).trigger(); + engine.makeConnection(this.channel, "loop_enabled", this.onLoopEnabledChanged.bind(this)); + } + registerButton(hidReport, name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); + } + hidReport.addControl(this.channel, name, config[0], "B", config[1], false, callback); } - - this.batchingLEDUpdate = false; - // And now send them all. - for (packetName in this.controller.OutputPackets) { - this.controller.OutputPackets[packetName].send(); + registerScalar(hidReport, name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); + } + hidReport.addControl(this.channel, name, config, "H", 0xFFFF, false, callback); } -}; - -TraktorS2MK1.init = function() { - if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) { - throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" + - "ShiftCueButtonAction is: " + ShiftCueButtonAction); + registerEncoder(hidReport, name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); + } + hidReport.addControl(this.channel, name, config[0], "B", config[1], false, callback); } - 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); + registerLed(hidReport, name, config) { + hidReport.addOutput(this.channel, name, config, "B"); } - 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); + linkLed(name, callback) { + this.controller.linkOutput(this.channel, name, this.channel, name, callback.bind(this)); } - if (ButtonBrightnessOn < ButtonBrightnessOff) { - throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" + - "ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" + - "ButtonBrightnessOff is: " + ButtonBrightnessOff); + gainEncoderPress(field) { + if (field.value > 0) { + this.gainEncoderPressed = true; + if (this.shiftPressed) { + script.triggerControl(this.channel, "pregain_set_default"); + } else { + script.triggerControl("[QuickEffectRack1_" + this.channel + "]", "super1_set_default"); + } + } else { + this.gainEncoderPressed = false; + } } - - 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(); + shift(field) { + const shiftPressed = field.value > 0; + this.shiftPressed = shiftPressed; + this.controller.setOutput(this.channel, "!shift", + shiftPressed ? ButtonBrightnessOn : ButtonBrightnessOff, + !this.parent.batchingLEDUpdate); + engine.setValue("[Library]", "focused_widget", shiftPressed ? 2: 3); } + syncButton(field) { + const now = Date.now(); - 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; + // If shifted, just toggle. + // TODO(later version): actually make this enable explicit master. + if (this.shiftPressed) { + if (field.value === 0) { + return; + } + const synced = engine.getValue(this.channel, "sync_enabled"); + engine.setValue(this.channel, "sync_enabled", !synced); + } else { + if (field.value === 1) { + this.syncEnabledTime = now; + engine.setValue(this.channel, "sync_enabled", 1); + } else { + if (!engine.getValue(this.channel, "sync_enabled")) { + // If disabled, and switching to disable... stay disabled. + engine.setValue(this.channel, "sync_enabled", 0); + return; + } + // was enabled, and button has been let go. maybe latch it. + if (now - this.syncEnabledTime > 300) { + engine.setValue(this.channel, "sync_enabled", 1); + return; + } + engine.setValue(this.channel, "sync_enabled", 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; + cueButton(field) { + if (this.shiftPressed) { + if (ShiftCueButtonAction === "REWIND") { + if (field.value === 0) { + return; + } + engine.setValue(this.channel, "start_stop", 1); + } else if (ShiftCueButtonAction === "REVERSEROLL") { + engine.setValue(this.channel, "reverseroll", field.value); + } + } else { + engine.setValue(this.channel, "cue_default", field.value); } - - 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); + playButton(field) { + if (field.value === 0) { + return; + } + if (this.shiftPressed) { + const locked = engine.getValue(this.channel, "keylock"); + engine.setValue(this.channel, "keylock", !locked); + } else { + const playing = engine.getValue(this.channel, "play"); + // Failsafe to disable scratching in case the finishJogTouch timer has not executed yet + // after a backspin. + if (engine.isScratching(this.number)) { + engine.scratchDisable(this.number, false); + } + engine.setValue(this.channel, "play", !playing); + } } -}; - -TraktorS2MK1.samplerGainKnob = function(field) { - for (var i = 1; i <= 8; i++) { - engine.setParameter("[Sampler" + i + "]", "pregain", field.value / 4095); + loopInButton(field) { + engine.setValue(this.channel, "loop_in", field.value); } -}; -TraktorS2MK1.toggleButton = function(field) { - if (field.value > 0) { - script.toggleControl(field.group, field.name); + loopOutButton(field) { + engine.setValue(this.channel, "loop_out", field.value); } -}; - -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); + samplerModeButton(field) { + if (field.value === 0) { + return; + } + const padMode = this.currentPadMode; + if (padMode !== padModes.sampler) { + this.setPadMode(padModes.sampler); + this.controller.setOutput(this.channel, "!samples_button", ButtonBrightnessOn, false); + this.controller.setOutput(this.channel, "!reset_button", ButtonBrightnessOff, !this.parent.batchingLEDUpdate); + } else { + this.setPadMode(padModes.hotcue); + this.controller.setOutput(this.channel, "!samples_button", ButtonBrightnessOff, !this.parent.batchingLEDUpdate); + } } -}; - -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]) { + introOutroModeButton(field) { 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); + const padMode = this.currentPadMode; + if (padMode !== padModes.introOutro) { + this.setPadMode(padModes.introOutro); + this.controller.setOutput(this.channel, "!reset_button", ButtonBrightnessOn, false); + this.controller.setOutput(this.channel, "!samples_button", ButtonBrightnessOff, !this.parent.batchingLEDUpdate); } 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); + this.setPadMode(padModes.hotcue); + this.controller.setOutput(this.channel, "!reset_button", ButtonBrightnessOff, !this.parent.batchingLEDUpdate); } } -}; - -TraktorS2MK1.cueButton = function(field) { - if (TraktorS2MK1.shiftPressed[field.group]) { - if (ShiftCueButtonAction === "REWIND") { - if (field.value === 0) { - return; + leftEncoderPress(field) { + this.leftEncoderPressed = field.value > 0; + if (this.shiftPressed && this.leftEncoderPressed) { + script.triggerControl(this.channel, "pitch_adjust_set_default"); + } + } + rightEncoderPress(field) { + if (field.value === 0) { + return; + } + const loopEnabled = engine.getValue(this.channel, "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 (this.shiftPressed) { + if (loopEnabled) { + script.triggerControl(this.channel, "reloop_andstop"); + } else { + script.triggerControl(this.channel, "reloop_toggle"); + } + } else { + if (loopEnabled) { + script.triggerControl(this.channel, "reloop_toggle"); + } else { + script.triggerControl(this.channel, "beatloop_activate"); } - 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; + loadTrackButton(field) { + if (this.shiftPressed) { + engine.setValue(this.channel, "CloneFromDeck", 0); + } else { + engine.setValue(this.channel, "LoadSelectedTrack", field.value); + } } - 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); + pflButton(field) { + if (field.value > 0) { + if (this.shiftPressed) { + script.toggleControl(this.channel, "quantize"); + } else { + script.toggleControl(this.channel, "pfl"); + } } - engine.setValue(field.group, "play", !playing); } -}; + jogMove(field) { + const deltas = this.wheelDeltas(field.value); + let tickDelta = deltas[0]; + const timeDelta = deltas[1]; -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); + if (engine.getValue(this.channel, "scratch2_enable")) { + if (this.shiftPressed) { + tickDelta *= 10; + } + engine.scratchTick(this.number, tickDelta); } else { - TraktorS2MK1.wheelTouchInertiaTimer[field.group] = engine.beginTimer( - inertiaTime, function() { - TraktorS2MK1.finishJogTouch(field.group); - }, true); + const velocity = this.scalerJog(tickDelta, timeDelta, this.channel); + engine.setValue(this.channel, "jog", velocity); } } -}; + leftEncoder(field) { + const delta = encoderDirection(field.value, this.previousLeftEncoder); + this.previousLeftEncoder = field.value; -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); + if (this.shiftPressed) { + if (delta === 1) { + script.triggerControl(this.channel, "pitch_up_small"); + } else { + script.triggerControl(this.channel, "pitch_down_small"); + } } else { - // Check again soon. - TraktorS2MK1.wheelTouchInertiaTimer[group] = engine.beginTimer( - 1, function() { - TraktorS2MK1.finishJogTouch(group); - }, true); + if (this.leftEncoderPressed) { + let beatjumpSize = engine.getValue(this.channel, "beatjump_size"); + if (delta === 1) { + beatjumpSize *= 2; + } else { + beatjumpSize /= 2; + } + engine.setValue(this.channel, "beatjump_size", beatjumpSize); + } else { + if (delta === 1) { + script.triggerControl(this.channel, "beatjump_forward"); + } else { + script.triggerControl(this.channel, "beatjump_backward"); + } + } } } -}; - -TraktorS2MK1.jogMove = function(field) { - var deltas = TraktorS2MK1.wheelDeltas(field.group, field.value); - var tickDelta = deltas[0]; - var timeDelta = deltas[1]; + rightEncoder(field) { + const delta = encoderDirection(field.value, this.previousRightEncoder); + this.previousRightEncoder = field.value; - if (engine.getValue(field.group, "scratch2_enable")) { - var deckNumber = TraktorS2MK1.controller.resolveDeck(field.group); - if (TraktorS2MK1.shiftPressed[field.group]) { - tickDelta *= 10; + if (this.shiftPressed) { + if (delta === 1) { + script.triggerControl(this.channel, "beatjump_1_forward"); + } else { + script.triggerControl(this.channel, "beatjump_1_backward"); + } + } else { + if (delta === 1) { + script.triggerControl(this.channel, "loop_double"); + } else { + script.triggerControl(this.channel, "loop_halve"); + } } - engine.scratchTick(deckNumber, tickDelta); - } else { - var velocity = TraktorS2MK1.scalerJog(tickDelta, timeDelta, field.group); - engine.setValue(field.group, "jog", velocity); } -}; + gainEncoder(field) { + const delta = 0.03333 * encoderDirection(field.value, this.previousPregain); + this.previousPregain = field.value; -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 (this.shiftPressed) { + const currentPregain = engine.getParameter(this.channel, "pregain"); + engine.setParameter(this.channel, "pregain", currentPregain + delta); + } else { + const quickEffectGroup = "[QuickEffectRack1_" + this.channel + "]"; + if (this.gainEncoderPressed) { + script.triggerControl(quickEffectGroup, delta > 0 ? "next_chain" : "prev_chain"); + } else { + const currentQuickEffectSuperKnob = engine.getParameter(quickEffectGroup, "super1"); + engine.setParameter(quickEffectGroup, "super1", currentQuickEffectSuperKnob + delta); + } + } } - - if (previousTime > timeValue) { - // We looped around. Adjust current time so that subtraction works. - timeValue += 0x10000; + jogTouch(field) { + if (this.wheelTouchInertiaTimer !== 0) { + // The wheel was touched again, reset the timer. + engine.stopTimer(this.wheelTouchInertiaTimer); + this.wheelTouchInertiaTimer = 0; + } + if (field.value > JogWheelTouchThreshold[this.channel]) { + engine.scratchEnable(this.number, 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. + const scratchRate = Math.abs(engine.getValue(this.number, "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. + let inertiaTime; + if (this.shiftPressed) { + 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. + this.finishJogTouch(); + } else { + this.wheelTouchInertiaTimer = engine.beginTimer( + inertiaTime, + this.finishJogTouch.bind(this) + , true); + } + } } - var timeDelta = timeValue - previousTime; - if (timeDelta === 0) { - // Spinning too fast to detect speed! By not dividing we are guessing it took 1ms. - timeDelta = 1; + outputCallback(value, group, key) { + let ledValue = ButtonBrightnessOff; + if (value) { + ledValue = ButtonBrightnessOn; + } + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); } - - 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; + outputCallbackLoop(value, group, key) { + let ledValue = ButtonBrightnessOff; + if (engine.getValue(group, "loop_enabled")) { + ledValue = 0x1F; + } + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); } - //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; + outputCallbackDark(value, group, key) { + let ledValue = 0x00; + if (value) { + ledValue = 0x1F; + } + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); } -}; - -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} -]; + onVuMeterChanged(value, group, _key) { + // This handler is called a lot so it should be as fast as possible. + // Figure out number of fully-illuminated segments. + const scaledValue = value * 4.0; + const fullIllumCount = Math.floor(scaledValue); -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}); - } - }; - }; + // Figure out how much the partially-illuminated segment is illuminated. + const partialIllum = (scaledValue - fullIllumCount) * 0x1F; - 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))); + for (let i = 0; i <= 3; i++) { + const key = "!VuMeter" + i; + if (i < fullIllumCount) { + // Don't update lights until they're all done, so the last term is false. + this.controller.setOutput(group, key, 0x1F, false); + } else if (i === fullIllumCount) { + this.controller.setOutput(group, key, partialIllum, false); + } else { + this.controller.setOutput(group, key, 0x00, false); + } } + this.controller.OutputPackets.OutputReport0x80.send(); } - 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); + onLoopEnabledChanged(value, group, _key) { + this.outputCallbackLoop(value, group, "loop_in"); + this.outputCallbackLoop(value, group, "loop_out"); } -}; - -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); + setPadMode(padMode) { + this.currentPadMode = padMode; + for (const padButton in this.pads) { + this.pads[padButton].padModeChanged(); + } } -}; + wheelDeltas(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. + const tickval = value & 0xFF; + let timeValue = value >>> 16; + const previousTick = this.lastTickValue; + const previousTime = this.lastTickTime; + this.lastTickValue = tickval; + this.lastTickTime = timeValue; + + if (previousTime > timeValue) { + // We looped around. Adjust current time so that subtraction works. + timeValue += 0x10000; + } + let timeDelta = timeValue - previousTime; + if (timeDelta === 0) { + // Spinning too fast to detect speed! By not dividing we are guessing it took 1ms. + timeDelta = 1; + } -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); + let tickDelta = 0; + if (previousTick >= 200 && tickval <= 100) { + tickDelta = tickval + 256 - previousTick; + } else if (previousTick <= 100 && tickval >= 200) { + tickDelta = tickval - previousTick - 256; } else { - script.triggerControl(samplerGroup, "eject"); + tickDelta = tickval - previousTick; } - } else { - if (engine.getValue(samplerGroup, "track_loaded") === 0) { - script.triggerControl(samplerGroup, "LoadSelectedTrack"); + //HIDDebug(group + " " + tickval + " " + previousTick + " " + tickDelta); + return [tickDelta, timeDelta]; + } + + scalerJog(tickDelta, timeDelta) { + if (engine.getValue(this.channel, "play")) { + return (tickDelta / timeDelta) / 3; } else { - script.triggerControl(samplerGroup, "cue_gotoandplay"); + return (tickDelta / timeDelta) * 2.0; } } -}; -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); + finishJogTouch() { + this.wheelTouchInertiaTimer = 0; + const play = engine.getValue(this.channel, "play"); + if (play !== 0) { + // If we are playing, just hand off to the engine. + engine.scratchDisable(this.number, 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. + const scratchRate = Math.abs(engine.getValue(this.channel, "scratch2")); + if (scratchRate < 0.01) { + // The platter is basically stopped, now we can disable scratch and hand off to jogging. + engine.scratchDisable(this.number, true); + } else { + // Check again soon. + this.wheelTouchInertiaTimer = engine.beginTimer(1, this.finishJogTouch.bind(this), true); + } + } } -}; +} -TraktorS2MK1.samplerModeButton = function(field) { - if (field.value === 0) { - return; +class PadButton { + constructor(deck, number) { + this.deck = deck; + this.controller = deck.controller; + this.number = number; + const samplerNumber = (this.deck.number -1) * 4 + this.number; + this.samplerGroup = "[Sampler" + samplerNumber + "]"; + this.connections = []; } - 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); + registerInputs(inputReport0x01, config) { + inputReport0x01.addControl(this.deck.channel, "!pad" + this.number, config[0], "B", config[1], false, this.pressHandler.bind(this)); } -}; - -TraktorS2MK1.introOutroModeButton = function(field) { - if (field.value === 0) { - return; + registerOutputs(outputReport0x80, config) { + this.registerLed(outputReport0x80, "!pad_" + this.number + "_G", config.green); + this.registerLed(outputReport0x80, "!pad_" + this.number + "_B", config.blue); } - 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); + registerLed(hidReport, name, config) { + hidReport.addOutput(this.deck.channel, name, config, "B"); } -}; + pressHandler(field) { + const padMode = this.deck.currentPadMode; -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); + if (padMode === padModes.hotcue) { + this.hotcueButton(field.value); + } else if (padMode === padModes.introOutro) { + this.introOutroButton(field.value); + } else if (padMode === padModes.sampler) { + this.samplerButton(field.value); } - 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); + hotcueButton(value) { + if (this.deck.shiftPressed) { + engine.setValue(this.deck.channel, "hotcue_" + this.number + "_clear", value); + } else { + engine.setValue(this.deck.channel, "hotcue_" + this.number + "_activate", value); } - } else { - engine.setValue(group, "show_focus", 1); - if (TraktorS2MK1.previouslyFocusedEffect[group] !== null) { - engine.setValue(group, "focused_effect", TraktorS2MK1.previouslyFocusedEffect[group]); + } + introOutroButton(value) { + if (this.deck.shiftPressed) { + engine.setValue(this.deck.channel, introOutroKeys[this.number-1] + "_clear", value); + } else { + engine.setValue(this.deck.channel, introOutroKeys[this.number-1] + "_activate", value); } } - 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); + samplerButton(value) { + if (value === 0) { + return; + } + if (this.deck.shiftPressed) { + if (engine.getValue(this.samplerGroup, "play") === 1) { + engine.setValue(this.samplerGroup, "play", 0); + } else { + script.triggerControl(this.samplerGroup, "eject"); + } + } else { + if (engine.getValue(this.samplerGroup, "track_loaded") === 0) { + script.triggerControl(this.samplerGroup, "LoadSelectedTrack"); + } else { + script.triggerControl(this.samplerGroup, "cue_gotoandplay"); } } - } else { - for (i = 1; i <= 2; i++) { - engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "meta"); + } + outputHotcueCallback() { + let color; + if (engine.getValue(this.deck.channel, "hotcue_" + this.number + "_enabled")) { + color = {green: 0, blue: 0x1F}; + } else { + color = {green: 0, blue: 0}; } + this.sendPadColor(color); } -}; - -// 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; + outputIntroOutroCallback(value) { + if (value > 0) { + this.sendPadColor(introOutroColors[this.number-1]); + } else { + this.sendPadColor(introOutroColorsDim[this.number-1]); } - 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(); + } + outputSamplerCallback() { + if (engine.getValue(this.samplerGroup, "track_loaded")) { + if (engine.getValue(this.samplerGroup, "play") === 1) { + if (engine.getValue(this.samplerGroup, "repeat") === 1) { + this.sendPadColor({green: 0x1F, blue: 0x1F}); + } else { + this.sendPadColor({green: 0x1F, blue: 0}); + } + } else { + this.sendPadColor({green: 0x05, blue: 0x00}); } - TraktorS2MK1.batchingLEDUpdate = false; - TraktorS2MK1.effectButtonLEDconnections[2].trigger(); - }); - if (!showParameters) { - engine.setValue(field.group, "show_parameters", 1); - TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group] = true; + } else { + this.sendPadColor({green: 0, blue: 0}); } - } else { - if (TraktorS2MK1.effectFocusLongPressTimer[field.group] !== 0) { - engine.stopTimer(TraktorS2MK1.effectFocusLongPressTimer[field.group]); - TraktorS2MK1.effectFocusLongPressTimer[field.group] = 0; + } + sendPadColor(color) { + const padKey = "!pad_" + this.number + "_"; + const ColorBrightnessScaler = ButtonBrightnessOn / 0x1f; + let green = color.green * ColorBrightnessScaler; + let blue = color.blue * ColorBrightnessScaler; + if (color.green === 0 && color.blue === 0) { + green = ButtonBrightnessOff; + blue = ButtonBrightnessOff; } + this.controller.setOutput(this.deck.channel, padKey + "G", green, false); + this.controller.setOutput(this.deck.channel, padKey + "B", blue, !this.deck.parent.batchingLEDUpdate); + } + padModeChanged() { + const padMode = this.deck.currentPadMode; - 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); + this.connections.forEach(function(connection) { + connection.disconnect(); + }); + this.connections = []; + + if (padMode === padModes.hotcue) { + this.connections.push( + engine.makeConnection(this.deck.channel, "hotcue_" + this.number + "_enabled", this.outputHotcueCallback.bind(this))); + } else if (padMode === padModes.introOutro) { + this.connections.push(engine.makeConnection( + this.deck.channel, introOutroKeys[this.number-1] + "_enabled", this.outputIntroOutroCallback.bind(this))); + } else if (padMode === padModes.sampler) { + this.connections.push(engine.makeConnection( + this.samplerGroup, "track_loaded", this.outputSamplerCallback.bind(this))); + this.connections.push(engine.makeConnection( + this.samplerGroup, "play", this.outputSamplerCallback.bind(this))); + this.connections.push(engine.makeConnection( + this.samplerGroup, "repeat", this.outputSamplerCallback.bind(this))); } - TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group] = false; + this.connections.forEach(function(connection) { + connection.trigger(); + }); } -}; - -// 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); +} + +class Equalizer { + constructor(deck) { + this.deck = deck; + this.controller = this.deck.controller; + this.group = "[EqualizerRack1_" + this.deck.channel + "_Effect1]"; + } + registerInputs(inputReport0x02, config) { + this.registerKnob(inputReport0x02, "parameter3", config.hi); + this.registerKnob(inputReport0x02, "parameter2", config.mid); + this.registerKnob(inputReport0x02, "parameter1", config.low); + // soft takeover + engine.softTakeover(this.group, "parameter3", true); + engine.softTakeover(this.group, "parameter2", true); + engine.softTakeover(this.group, "parameter1", true); + } + registerKnob(hidReport, name, config) { + hidReport.addControl(this.group, name, config, "H"); + } +} + +const longPressTimeoutMilliseconds = 275; +class EffectUnit { + constructor(parent, number) { + this.parent = parent; + this.controller = parent.controller; + this.number = number; + this.group = "[EffectRack1_EffectUnit" + number + "]"; + this.effectButtonLongPressTimer= [0, 0, 0, 0]; + this.effectButtonIsLongPressed = [false, false, false, false]; + this.effectFocusLongPressTimer = 0; + this.effectFocusChooseModeActive = false; + this.effectFocusButtonPressedWhenParametersHidden =false; + this.previouslyFocusedEffect = null; + this.params = [ + new EffectParameter(this, 1), + new EffectParameter(this, 2), + new EffectParameter(this, 3), + ]; + } + registerInputs(inputReport0x01, inputReport0x02, config) { + this.registerButton(inputReport0x01, "!effect_focus_button", config.focus, this.effectFocusButton); + this.registerButton(inputReport0x01, "group_[Channel1]_enable", config.channel1); + this.registerButton(inputReport0x01, "group_[Channel2]_enable", config.channel2); + this.registerKnob(inputReport0x02, "!mix", config.mix, this.mixKnob); + for (let i = 0; i < 3; i++) { + this.params[i].registerInputs(inputReport0x01, inputReport0x02, config.params[i]); + } } -}; + registerOutputs(outputReport0x80, config) { + this.registerLed(outputReport0x80, "!effect_focus_button", config.focus); + this.registerLed(outputReport0x80, "group_[Channel1]_enable", config.channel1); + this.registerLed(outputReport0x80, "group_[Channel2]_enable", config.channel2); + for (let i = 0; i < 3; i++) { + this.params[i].registerOutputs(outputReport0x80, config.params[i]); + } + } + linkOutputs() { + engine.makeConnection(this.group, "show_parameters", this.onShowParametersChange.bind(this)); + engine.makeConnection(this.group, "focused_effect", this.onFocusedEffectChange.bind(this)).trigger(); + engine.makeConnection(this.group, "group_[Channel1]_enable", this.outputCallback.bind(this)).trigger(); + engine.makeConnection(this.group, "group_[Channel2]_enable", this.outputCallback.bind(this)).trigger(); + } + registerButton(hidReport, name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); + } + hidReport.addControl(this.group, name, config[0], "B", config[1], false, callback); + } + registerKnob(hidReport, name, config, callback) { + hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); + } + registerLed(hidReport, name, config) { + hidReport.addOutput(this.group, name, config, "B"); + } + effectFocusButton(field) { + const showParameters = engine.getValue(this.group, "show_parameters"); + if (field.value > 0) { + if (this.parent.decks[this.number -1].shiftPressed) { + engine.setValue(this.group, "load_preset", 1); + return; + } + this.effectFocusLongPressTimer = engine.beginTimer(longPressTimeoutMilliseconds, function() { + this.effectFocusChooseModeActive = true; + this.connectEffectButtonLedsFocused(); + }.bind(this)); + if (!showParameters) { + engine.setValue(this.group, "show_parameters", 1); + this.effectFocusButtonPressedWhenParametersHidden = true; + } + } else { + if (this.effectFocusLongPressTimer !== 0) { + engine.stopTimer(this.effectFocusLongPressTimer); + this.effectFocusLongPressTimer = 0; + } -// 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"); + if (this.effectFocusChooseModeActive) { + this.effectFocusChooseModeActive = false; + this.connectEffectButtonLedsNormal(); + } else if (showParameters && !this.effectFocusButtonPressedWhenParametersHidden) { + engine.setValue(this.group, "show_parameters", 0); + } - 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; + this.effectFocusButtonPressedWhenParametersHidden = false; } - script.toggleControl(group, key); - }; - - if (field.value > 0) { - if (TraktorS2MK1.shiftPressed["[Channel" + effectUnitNumber + "]"]) { - engine.setValue(effectUnitGroup, "load_preset", buttonNumber+1); + } + mixKnob(field) { + engine.setParameter(this.group, "mix", field.value/4095); + } + onShowParametersChange(value, group, _control) { + if (value === 0) { + if (engine.getValue(group, "show_focus") > 0) { + engine.setValue(group, "show_focus", 0); + this.previouslyFocusedEffect = engine.getValue(group, "focused_effect"); + engine.setValue(group, "focused_effect", 0); + } } else { - if (TraktorS2MK1.effectFocusChooseModeActive[effectUnitGroup]) { - if (focusedEffect === buttonNumber) { - engine.setValue(effectUnitGroup, "focused_effect", 0); - } else { - engine.setValue(effectUnitGroup, "focused_effect", buttonNumber); + engine.setValue(group, "show_focus", 1); + if (this.previouslyFocusedEffect !== null) { + engine.setValue(group, "focused_effect", this.previouslyFocusedEffect); + } + } + this.connectEffectButtonLedsNormal(); + } + onFocusedEffectChange(value, group, _control) { + this.controller.setOutput(this.group, "!effect_focus_button", value > 0 ? ButtonBrightnessOn : ButtonBrightnessOff, !this.parent.batchingLEDUpdate); + if (value === 0) { + for (let i = 1; i <= 2; i++) { + // The previously focused effect is not available here, so iterate over all effects' parameter knobs. + for (let j = 1; j < 3; j++) { + engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "parameter" + j); } - 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 { + for (let i = 1; i <= 2; i++) { + engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "meta"); } } - } else { - engine.stopTimer(TraktorS2MK1.effectButtonLongPressTimer[effectUnitGroup][buttonNumber]); - TraktorS2MK1.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] = 0; - if (TraktorS2MK1.effectButtonIsLongPressed[effectUnitGroup][buttonNumber]) { - toggle(); + } + outputCallback(value, group, key) { + let ledValue = ButtonBrightnessOff; + if (value) { + ledValue = ButtonBrightnessOn; } - TraktorS2MK1.effectButtonIsLongPressed[effectUnitGroup][buttonNumber] = false; + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); } -}; - -/// 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; + shiftPressed() { + return this.parent.decks[this.number - 1].shiftPressed; } - return direction; -}; - -TraktorS2MK1.gainEncoder = function(field) { - var delta = 0.03333 * TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousPregain[field.group]); - TraktorS2MK1.previousPregain[field.group] = field.value; + connectEffectButtonLedsNormal() { + this.connectEffectButtonLeds(this.params[0].connectLedNormal); + } + connectEffectButtonLedsFocused() { + this.connectEffectButtonLeds(this.params[0].connectLedFocused); + } + connectEffectButtonLeds(fn) { + this.parent.batchingLEDUpdate = true; + for (let i = 0; i < 2; i++) { + fn.bind(this.params[i])(); + } + this.parent.batchingLEDUpdate = false; + fn.bind(this.params[2])(); + } +} + +class EffectParameter { + constructor(effectUnit, number) { + this.effectUnit = effectUnit; + this.groupPrefix = effectUnit.group.slice(0, -1); + this.group = effectUnit.group; + this.controller = effectUnit.controller; + this.number = number; + this.longPressTimer = 0; + this.isLongPressed = false; + this.ledConnection = null; + } + registerInputs(inputReport0x01, inputReport0x02, config) { + this.registerButton(inputReport0x01, "!effectbutton" + this.number, config.button, this.effectButton); + this.registerKnob(inputReport0x02, "!effectknob" + this.number, config.knob, this.effectKnob); + // soft takeover + const group = this.groupPrefix + "_Effect" + this.number + "]"; + engine.softTakeover(group, "meta", true); + for (let i = 1; i <= 3; i++) { + engine.softTakeover(group, "parameter" + i, true); + } + } + registerOutputs(outputReport0x80, config) { + outputReport0x80.addOutput(this.group, "!effectbutton" + this.number, config, "B"); + } + registerButton(hidReport, name, config, callback) { + hidReport.addControl(this.group, name, config[0], "B", config[1], false, callback.bind(this)); + } + registerKnob(hidReport, name, config, callback) { + hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); + } + effectButton(field) { + const focusedEffect = engine.getValue(this.group, "focused_effect"); - 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"); + if (field.value > 0) { + if (this.effectUnit.shiftPressed()) { + engine.setValue(this.group, "load_preset", this.number+1); + } else { + if (this.effectUnit.effectFocusChooseModeActive) { + if (focusedEffect === this.number) { + engine.setValue(this.group, "focused_effect", 0); + } else { + engine.setValue(this.group, "focused_effect", this.number); + } + this.effectUnit.effectFocusChooseModeActive = false; + } else { + this.toggle(); + this.longPressTimer = engine.beginTimer(longPressTimeoutMilliseconds, + function() { + this.isLongPressed = true; + this.longPressTimer= 0; + }.bind(this), + true + ); + } + } } else { - var currentQuickEffectSuperKnob = engine.getParameter(quickEffectGroup, "super1"); - engine.setParameter(quickEffectGroup, "super1", currentQuickEffectSuperKnob + delta); + engine.stopTimer(this.longPressTimer); + this.longPressTimer = 0; + if (this.isLongPressed) { + this.toggle(); + } + this.isLongPressed = false; } } -}; - -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"); + toggle() { + const button = this.getButtonGroupAndKey(); + script.toggleControl(button.group, button.key); + } + effectKnob(field) { + const focusedEffect = engine.getValue(this.group, "focused_effect"); + const scaledValue = field.value / 4095; + if (focusedEffect > 0) { + engine.setParameter(this.groupPrefix + "_Effect" + focusedEffect + "]", + "parameter" + this.number, + scaledValue); } else { - script.triggerControl("[QuickEffectRack1_" + field.group + "]", "super1_set_default"); + engine.setParameter(this.groupPrefix + "_Effect" + this.number + "]", + "meta", + scaledValue); } - } 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"); + connectLedNormal() { + const button = this.getButtonGroupAndKey(); + this.connectLed(button.group, button.key, this.ledCallbackNormal); + } + connectLedFocused() { + this.connectLed(this.group, "focused_effect", this.ledCallbackFocused); + } + connectLed(group, key, callback) { + if (this.ledConnection !== null) { + this.ledConnection.disconnect(); + } + this.ledConnection = engine.makeConnection(group, key, callback.bind(this)); + this.ledConnection.trigger(); + } + getButtonGroupAndKey() { + const focusedEffect = engine.getValue(this.group, "focused_effect"); + if (focusedEffect === 0) { + return { + group: this.groupPrefix + "_Effect" + this.number + "]", + key: "enabled", + }; } else { - script.triggerControl(field.group, "pitch_down_small"); + return { + group: this.groupPrefix + "_Effect" + focusedEffect + "]", + key: "button_parameter" + this.number, + }; } - } else { - if (TraktorS2MK1.leftEncoderPressed[field.group]) { - var beatjumpSize = engine.getValue(field.group, "beatjump_size"); - if (delta === 1) { - beatjumpSize *= 2; - } else { - beatjumpSize /= 2; + } + ledCallbackNormal(value) { + this.ledCallback(value === 1); + } + ledCallbackFocused(value) { + this.ledCallback(value === this.number); + } + ledCallback(value) { + this.controller.setOutput(this.group, "!effectbutton" + this.number, + value ? ButtonBrightnessOn : ButtonBrightnessOff, !this.effectUnit.parent.batchingLEDUpdate); + } +} + +class TraktorS2MK1Class { + constructor() { + 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.decks = [ + new DeckClass(this, 1), + new DeckClass(this, 2), + ]; + this.effectUnits = [ + new EffectUnit(this, 1), + new EffectUnit(this, 2), + ]; + } + registerInputPackets() { + // 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. + const InputReport0x01 = new HIDPacket("InputReport0x01", 0x01, this.inputReport0x01Callback.bind(this)); + // Most items in the input report 0x02 are controls that go from 0-4095. + // There are also some 4 bit encoders. + const InputReport0x02 = new HIDPacket("InputReport0x02", 0x02, this.inputReport0x02Callback.bind(this)); + + this.decks[0].registerInputs(InputReport0x01, InputReport0x02, { + gainEncoderPress: [0x0E, 0x01], + shift: [0x0D, 0x80], + sync: [0x0D, 0x40], + cue: [0x0D, 0x20], + play: [0x0D, 0x10], + pads: [ + [0x0D, 0x08], + [0x0D, 0x04], + [0x0D, 0x02], + [0x0D, 0x01], + ], + loopIn: [0x09, 0x40], + loopOut: [0x09, 0x20], + samples: [0x0B, 0x02], + reset: [0x09, 0x10], + leftEncoderPress: [0x0E, 0x02], + rightEncoderPress: [0x0E, 0x04], + jogWheel: 0x01, + loadTrack: [0x0B, 0x08], + pfl: [0x09, 0x80], + rate: 0x0F, + leftEncoder: [0x01, 0xF0], + rightEncoder: [0x02, 0x0F], + volume: 0x2B, + gain: [0x01, 0x0F], + jogTouch: 0x0D, + eq: { + hi: 0x11, + mid: 0x25, + low: 0x27, } - engine.setValue(field.group, "beatjump_size", beatjumpSize); - } else { - if (delta === 1) { - script.triggerControl(field.group, "beatjump_forward"); - } else { - script.triggerControl(field.group, "beatjump_backward"); + }); + this.decks[1].registerInputs(InputReport0x01, InputReport0x02, { + gainEncoderPress: [0x0E, 0x10], + shift: [0x0C, 0x80], + sync: [0x0C, 0x40], + cue: [0x0C, 0x20], + play: [0x0C, 0x10], + pads: [ + [0x0C, 0x08], + [0x0C, 0x04], + [0x0C, 0x02], + [0x0C, 0x01], + ], + loopIn: [0x0B, 0x40], + loopOut: [0x0B, 0x20], + samples: [0x0B, 0x01], + reset: [0x0B, 0x10], + leftEncoderPress: [0x0E, 0x20], + rightEncoderPress: [0x0E, 0x40], + jogWheel: 0x05, + loadTrack: [0x0B, 0x04], + pfl: [0x0B, 0x80], + rate: 0x1F, + leftEncoder: [0x03, 0xF0], + rightEncoder: [0x04, 0x0F], + volume: 0x2D, + gain: [0x03, 0x0F], + jogTouch: 0x1D, + eq: { + hi: 0x21, + mid: 0x23, + low: 0x29, } + }); + this.effectUnits[0].registerInputs(InputReport0x01, InputReport0x02, { + focus: [0x09, 0x08], + mix: 0x0B, + params: [ + {button: [0x09, 0x04], knob: 0x09}, + {button: [0x09, 0x02], knob: 0x07}, + {button: [0x09, 0x01], knob: 0x05}, + ], + channel1: [0x0A, 0x02], + channel2: [0x0A, 0x08], + }); + this.effectUnits[1].registerInputs(InputReport0x01, InputReport0x02, { + focus: [0x0A, 0x80], + mix: 0x1B, + params: [ + {button: [0x0A, 0x40], knob: 0x19}, + {button: [0x0A, 0x20], knob: 0x17}, + {button: [0x0A, 0x10], knob: 0x15}, + ], + channel1: [0x0A, 0x01], + channel2: [0x0A, 0x04], + }); + InputReport0x01.addControl("[Master]", "!browse_encoder_press", 0x0E, "B", 0x08, false, this.browseEncoderPress.bind(this)); + + InputReport0x02.addControl("[Master]", "crossfader", 0x2F, "H"); + InputReport0x02.addControl("[Master]", "headMix", 0x31, "H"); + InputReport0x02.addControl("[Master]", "!samplerGain", 0x13, "H"); + InputReport0x02.setCallback("[Master]", "!samplerGain", this.samplerGainKnob.bind(this)); + InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoder.bind(this)); + + // Soft takeover for knobs + engine.softTakeover("[Master]", "crossfader", true); + engine.softTakeover("[Master]", "headMix", true); + for (let i = 1; i <= 8; i++) { + engine.softTakeover("[Sampler" + i + "]", "pregain", true); } - } -}; -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"); - } -}; + // Set scalers + // this.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(InputReport0x01); + this.controller.registerInputPacket(InputReport0x02); + } + registerOutputPackets() { + const OutputReport0x80 = new HIDPacket("OutputReport0x80", 0x80); + + this.decks[0].registerOutputs(OutputReport0x80, { + trackLoaded: 0x1F, + vuMeter: 0x15, + peak: 0x01, + reset: 0x06, + loopIn: 0x02, + loopOut: 0x05, + pfl: 0x20, + samples: 0x35, + shift: 0x08, + sync: 0x04, + cue: 0x07, + play: 0x03, + pads: [ + {green: 0x0C, blue: 0x10}, + {green: 0x0B, blue: 0x0F}, + {green: 0x0A, blue: 0x0E}, + {green: 0x09, blue: 0x0D}, + ], + }); + this.decks[1].registerOutputs(OutputReport0x80, { + trackLoaded: 0x1E, + vuMeter: 0x11, + peak: 0x25, + reset: 0x26, + loopIn: 0x22, + loopOut: 0x21, + pfl: 0x1D, + samples: 0x34, + shift: 0x28, + sync: 0x24, + cue: 0x27, + play: 0x23, + pads: [ + {green: 0x2C, blue: 0x30}, + {green: 0x2B, blue: 0x2F}, + {green: 0x2A, blue: 0x2E}, + {green: 0x29, blue: 0x2D}, + ], + }); + this.effectUnits[0].registerOutputs(OutputReport0x80, { + focus: 0x1C, + params: [ + 0x1B, + 0x1A, + 0x19, + ], + channel1: 0x3D, + channel2: 0x3B, + }); + this.effectUnits[1].registerOutputs(OutputReport0x80, { + focus: 0x39, + params: [ + 0x38, + 0x37, + 0x36, + ], + channel1: 0x3C, + channel2: 0x3A, + }); -TraktorS2MK1.rightEncoder = function(field) { - var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousRightEncoder[field.group]); - TraktorS2MK1.previousRightEncoder[field.group] = field.value; + OutputReport0x80.addOutput("[Master]", "!warninglight", 0x33, "B"); - if (TraktorS2MK1.shiftPressed[field.group]) { - if (delta === 1) { - script.triggerControl(field.group, "beatjump_1_forward"); - } else { - script.triggerControl(field.group, "beatjump_1_backward"); + this.controller.registerOutputPacket(OutputReport0x80); + + this.decks.forEach(function(deck) { + deck.linkOutputs(); + }); + this.effectUnits.forEach(function(effectUnit) { + effectUnit.linkOutputs(); + }); + this.decks.forEach(function(deck) { + deck.setPadMode(padModes.hotcue); + }); + this.controller.setOutput("[Master]", "!warninglight", 0x00, true); + } + init() { + if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) { + throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" + + "ShiftCueButtonAction is: " + ShiftCueButtonAction); } - } else { - if (delta === 1) { - script.triggerControl(field.group, "loop_double"); - } else { - script.triggerControl(field.group, "loop_halve"); + 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); } - } -}; - -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"); + 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); } - } else { - if (loopEnabled) { - script.triggerControl(field.group, "reloop_toggle"); + if (ButtonBrightnessOn < ButtonBrightnessOff) { + throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" + + "ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" + + "ButtonBrightnessOff is: " + ButtonBrightnessOff); + } + + this.registerInputPackets(); + + const debugLEDs = false; + if (debugLEDs) { + const data = []; + for (let i = 0; i < 61; i++) { + data[i] = ButtonBrightnessOn; + } + data[0x31 - 1] = 0; + data[0x32 - 1] = 0; + controller.send(data, data.length, 0x80); } else { - script.triggerControl(field.group, "beatloop_activate"); + this.registerOutputPackets(); } } -}; -TraktorS2MK1.browseEncoder = function(field) { - var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousBrowse); - TraktorS2MK1.previousBrowse = field.value; + shutdown() { + const data = []; + for (let i = 0; i < 61; i++) { + data[i] = 0; + } + // light up warning light + data[0x33 - 1] = ButtonBrightnessOn; + controller.send(data, data.length, 0x80); + } - if (TraktorS2MK1.shiftPressed["[Channel1]"] || TraktorS2MK1.shiftPressed["[Channel2]"]) { - delta *= 5; + incomingData(data, length) { + this.controller.parsePacket(data, length); } - engine.setValue("[Playlist]", "SelectTrackKnob", delta); -}; + // The input report 0x01 handles buttons and jog wheels. + inputReport0x01Callback(packet, data) { + for (const name in data) { + const field = data[name]; + if (field.name === "!jog_wheel") { + this.controller.processControl(field); + continue; + } -TraktorS2MK1.scalerParameter = function(group, name, value) { - return script.absoluteLin(value, 0, 1, 16, 4080); -}; + this.controller.processButton(field); + } + } + // There are no buttons handled by input report 0x02, so this is a little simpler. + inputReport0x02Callback(packet, data) { + for (const name in data) { + const field = data[name]; + this.controller.processControl(field); + } + } + samplerGainKnob(field) { + for (let i = 1; i <= 8; i++) { + engine.setParameter("[Sampler" + i + "]", "pregain", field.value / 4095); + } + } -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); + toggleButton(field) { + if (field.value > 0) { + script.toggleControl(field.group, field.name); + } } -}; -TraktorS2MK1.scalerSlider = function(group, name, value) { - return script.absoluteLin(value, -1, 1, 16, 4080); -}; + browseEncoder(field) { + const delta = encoderDirection(field.value, this.previousBrowse); + this.previousBrowse = field.value; -TraktorS2MK1.outputChannelCallback = function(value, group, key) { - var ledValue = 0x05; - if (value) { - ledValue = 0x1F; + engine.setValue("[Library]", "MoveVertical", delta); } - TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate); -}; -TraktorS2MK1.outputChannelCallbackDark = function(value, group, key) { - var ledValue = 0x00; - if (value) { - ledValue = 0x1F; + browseEncoderPress(field) { + if (this.shiftPressed()) { + engine.setValue("[Library]", "GoToItem", field.value); + } else { + if (field.value > 0) { + script.toggleControl("[Master]", "maximize_library"); + } + } } - TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate); -}; -TraktorS2MK1.outputCallback = function(value, group, key) { - var ledValue = ButtonBrightnessOff; - if (value) { - ledValue = ButtonBrightnessOn; + scalerParameter(group, name, value) { + const scaledValue = script.absoluteLin(value, 0, 1, 16, 4080); + return scaledValue; } - 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; + scalerVolume(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.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate); -}; -TraktorS2MK1.outputCallbackDark = function(value, group, key) { - var ledValue = 0x00; - if (value) { - ledValue = 0x1F; + scalerSlider(group, name, value) { + return script.absoluteLin(value, -1, 1, 16, 4080); } - 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"); + outputChannelCallback(value, group, key) { + let ledValue = 0x05; + if (value) { + ledValue = 0x1F; } + this.controller.setOutput(group, key, ledValue, !this.batchingLEDUpdate); } -}; -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); -}; + outputChannelCallbackDark(value, group, key) { + let ledValue = 0x00; + if (value) { + ledValue = 0x1F; + } + this.controller.setOutput(group, key, ledValue, !this.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}; + shiftPressed() { + return this.decks[0].shiftPressed || this.decks[1].shiftPressed; } - 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); +const introOutroKeys = [ + "intro_start", + "intro_end", + "outro_start", + "outro_end" +]; + +const introOutroColors = [ + {green: 0x1F, blue: 0}, + {green: 0x1F, blue: 0}, + {green: 0, blue: 0x1F}, + {green: 0, blue: 0x1F} +]; - // Figure out how much the partially-illuminated segment is illuminated. - var partialIllum = (scaledValue - fullIllumCount) * 0x1F; +const introOutroColorsDim = [ + {green: 0x05, blue: 0}, + {green: 0x05, blue: 0}, + {green: 0, blue: 0x05}, + {green: 0, blue: 0x05} +]; - 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); - } +/// return value 1 === right turn +/// return value -1 === left turn +const encoderDirection = function(newValue, oldValue) { + let direction = 0; + const min = 0; + const 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; } - TraktorS2MK1.controller.OutputPackets.OutputReport0x80.send(); + return direction; }; -TraktorS2MK1.onLoopEnabledChanged = function(value, group, _key) { - TraktorS2MK1.outputCallbackLoop(value, group, "loop_in"); - TraktorS2MK1.outputCallbackLoop(value, group, "loop_out"); -}; + +var TraktorS2MK1 = new TraktorS2MK1Class(); // eslint-disable-line no-var, no-unused-vars // # Feature Report Description // From d85309d38600b2e13c2e5653557fbe000f11c569 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 1 Feb 2023 18:17:04 +0100 Subject: [PATCH 02/16] Traktor S2 Mk1: Read any apply calibration --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 273 +++++++++++++----- 1 file changed, 196 insertions(+), 77 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index d2f0cba9482..0198159663c 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -8,18 +8,6 @@ /* 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. -const JogWheelTouchThreshold = { - "[Channel1]": 3222, - "[Channel2]": 3444, -}; - // ==== 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. @@ -48,7 +36,7 @@ class DeckClass { this.previousPregain = 0; this.previousLeftEncoder = 0; this.previousRightEncoder = 0; - this.wheelTouchInertiaTimer = 0; + this.wheelPressInertiaTimer = 0; this.gainEncoderPressed = false; this.leftEncoderPressed = false; this.rightEncoderPressed = false; @@ -64,6 +52,7 @@ class DeckClass { this.lastTickTime = 0; this.lastTickValue = 0; this.syncEnabledTime = {}; + this.calibration = null; } registerInputs(inputReport0x01, inputReport0x02, config) { // InputReport 0x01 @@ -88,9 +77,10 @@ class DeckClass { this.registerScalar(inputReport0x02, "rate", config.rate); this.registerEncoder(inputReport0x02, "!left_encoder", config.leftEncoder, this.leftEncoder); this.registerEncoder(inputReport0x02, "!right_encoder", config.rightEncoder, this.rightEncoder); - this.registerScalar(inputReport0x02, "volume", config.volume); + // this.registerScalar(inputReport0x02, "volume", config.volume); + this.registerScalar(inputReport0x02, "!volume", config.volume, this.volume); this.registerEncoder(inputReport0x02, "!pregain", config.gain, this.gainEncoder); - this.registerScalar(inputReport0x02, "!jog_touch", config.jogTouch, this.jogTouch); + this.registerScalar(inputReport0x02, "!jog_press", config.jogPress, this.jogPress); this.eq.registerInputs(inputReport0x02, config.eq); // configure soft takeover engine.softTakeover(this.group, "rate", true); @@ -127,6 +117,10 @@ class DeckClass { engine.makeConnection(this.channel, "VuMeter", this.onVuMeterChanged.bind(this)).trigger(); engine.makeConnection(this.channel, "loop_enabled", this.onLoopEnabledChanged.bind(this)); } + calibrate(calibration) { + this.calibration = calibration; + this.eq.calibrate(calibration.eq); + } registerButton(hidReport, name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); @@ -151,6 +145,9 @@ class DeckClass { linkLed(name, callback) { this.controller.linkOutput(this.channel, name, this.channel, name, callback.bind(this)); } + volume(field) { + setFaderParameter(this.channel, "volume", field.value, this.calibration.volume); + } gainEncoderPress(field) { if (field.value > 0) { this.gainEncoderPressed = true; @@ -224,7 +221,7 @@ class DeckClass { engine.setValue(this.channel, "keylock", !locked); } else { const playing = engine.getValue(this.channel, "play"); - // Failsafe to disable scratching in case the finishJogTouch timer has not executed yet + // Failsafe to disable scratching in case the finishJogPress timer has not executed yet // after a backspin. if (engine.isScratching(this.number)) { engine.scratchDisable(this.number, false); @@ -390,18 +387,18 @@ class DeckClass { } } } - jogTouch(field) { - if (this.wheelTouchInertiaTimer !== 0) { + jogPress(field) { + if (this.wheelPressInertiaTimer !== 0) { // The wheel was touched again, reset the timer. - engine.stopTimer(this.wheelTouchInertiaTimer); - this.wheelTouchInertiaTimer = 0; + engine.stopTimer(this.wheelPressInertiaTimer); + this.wheelPressInertiaTimer = 0; } - if (field.value > JogWheelTouchThreshold[this.channel]) { + if (field.value > this.calibration.jogPress.pressed) { engine.scratchEnable(this.number, 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. - const scratchRate = Math.abs(engine.getValue(this.number, "scratch2")); + const scratchRate = Math.abs(engine.getValue(this.channel, "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 @@ -414,11 +411,11 @@ class DeckClass { } if (inertiaTime < 100) { // Just do it now. - this.finishJogTouch(); + this.finishJogPress(); } else { this.wheelTouchInertiaTimer = engine.beginTimer( inertiaTime, - this.finishJogTouch.bind(this) + this.finishJogPress.bind(this) , true); } } @@ -506,7 +503,6 @@ class DeckClass { } else { tickDelta = tickval - previousTick; } - //HIDDebug(group + " " + tickval + " " + previousTick + " " + tickDelta); return [tickDelta, timeDelta]; } @@ -518,8 +514,8 @@ class DeckClass { } } - finishJogTouch() { - this.wheelTouchInertiaTimer = 0; + finishJogPress() { + this.wheelPressInertiaTimer = 0; const play = engine.getValue(this.channel, "play"); if (play !== 0) { // If we are playing, just hand off to the engine. @@ -533,7 +529,7 @@ class DeckClass { engine.scratchDisable(this.number, true); } else { // Check again soon. - this.wheelTouchInertiaTimer = engine.beginTimer(1, this.finishJogTouch.bind(this), true); + this.wheelPressInertiaTimer = engine.beginTimer(20, this.finishJogPress.bind(this), true); } } } @@ -678,18 +674,42 @@ class Equalizer { this.deck = deck; this.controller = this.deck.controller; this.group = "[EqualizerRack1_" + this.deck.channel + "_Effect1]"; + this.params = { + hi: new EqualizerParameter(this, 3), + mid: new EqualizerParameter(this, 2), + low: new EqualizerParameter(this, 1), + }; } registerInputs(inputReport0x02, config) { - this.registerKnob(inputReport0x02, "parameter3", config.hi); - this.registerKnob(inputReport0x02, "parameter2", config.mid); - this.registerKnob(inputReport0x02, "parameter1", config.low); - // soft takeover - engine.softTakeover(this.group, "parameter3", true); - engine.softTakeover(this.group, "parameter2", true); - engine.softTakeover(this.group, "parameter1", true); + for (const param in this.params) { + this.params[param].registerInputs(inputReport0x02, config[param]); + } + } + calibrate(calibration) { + for (const param in this.params) { + this.params[param].calibrate(calibration[param]); + } + } +} +class EqualizerParameter { + constructor(equalizer, number) { + this.equalizer = equalizer; + this.group = equalizer.group; + this.number = number; + this.calibration = null; + } + registerInputs(inputReport0x02, config) { + this.registerKnob(inputReport0x02, "!parameter" + this.number, config, this.knob); + engine.softTakeover(this.group, "parameter" + this.number, true); + } + calibrate(calibration) { + this.calibration = calibration; } - registerKnob(hidReport, name, config) { - hidReport.addControl(this.group, name, config, "H"); + registerKnob(hidReport, name, config, callback) { + hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); + } + knob(field) { + setKnobParameter(this.group, "parameter" + this.number, field.value, this.calibration); } } @@ -711,6 +731,7 @@ class EffectUnit { new EffectParameter(this, 2), new EffectParameter(this, 3), ]; + this.calibration = null; } registerInputs(inputReport0x01, inputReport0x02, config) { this.registerButton(inputReport0x01, "!effect_focus_button", config.focus, this.effectFocusButton); @@ -735,6 +756,12 @@ class EffectUnit { engine.makeConnection(this.group, "group_[Channel1]_enable", this.outputCallback.bind(this)).trigger(); engine.makeConnection(this.group, "group_[Channel2]_enable", this.outputCallback.bind(this)).trigger(); } + calibrate(calibration) { + this.calibration = calibration.mix; + for (let i = 0; i < 3; i++) { + this.params[i].calibrate(calibration.params[i]); + } + } registerButton(hidReport, name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); @@ -779,7 +806,7 @@ class EffectUnit { } } mixKnob(field) { - engine.setParameter(this.group, "mix", field.value/4095); + setKnobParameter(this.group, "mix", field.value, this.calibration); } onShowParametersChange(value, group, _control) { if (value === 0) { @@ -847,6 +874,7 @@ class EffectParameter { this.longPressTimer = 0; this.isLongPressed = false; this.ledConnection = null; + this.calibration = null; } registerInputs(inputReport0x01, inputReport0x02, config) { this.registerButton(inputReport0x01, "!effectbutton" + this.number, config.button, this.effectButton); @@ -861,6 +889,9 @@ class EffectParameter { registerOutputs(outputReport0x80, config) { outputReport0x80.addOutput(this.group, "!effectbutton" + this.number, config, "B"); } + calibrate(calibration) { + this.calibration = calibration; + } registerButton(hidReport, name, config, callback) { hidReport.addControl(this.group, name, config[0], "B", config[1], false, callback.bind(this)); } @@ -906,17 +937,8 @@ class EffectParameter { script.toggleControl(button.group, button.key); } effectKnob(field) { - const focusedEffect = engine.getValue(this.group, "focused_effect"); - const scaledValue = field.value / 4095; - if (focusedEffect > 0) { - engine.setParameter(this.groupPrefix + "_Effect" + focusedEffect + "]", - "parameter" + this.number, - scaledValue); - } else { - engine.setParameter(this.groupPrefix + "_Effect" + this.number + "]", - "meta", - scaledValue); - } + const knob = this.getKnobGroupAndKey(); + setKnobParameter(knob.group, knob.key, field.value, this.calibration); } connectLedNormal() { const button = this.getButtonGroupAndKey(); @@ -946,6 +968,20 @@ class EffectParameter { }; } } + getKnobGroupAndKey() { + const focusedEffect = engine.getValue(this.group, "focused_effect"); + if (focusedEffect === 0) { + return { + group: this.groupPrefix + "_Effect" + this.number + "]", + key: "meta", + }; + } else { + return { + group: this.groupPrefix + "_Effect" + focusedEffect + "]", + key: "parameter" + this.number, + }; + } + } ledCallbackNormal(value) { this.ledCallback(value === 1); } @@ -977,6 +1013,8 @@ class TraktorS2MK1Class { new EffectUnit(this, 1), new EffectUnit(this, 2), ]; + this.rawCalibration = {}; + this.calibration = null; } registerInputPackets() { // Values in input report 0x01 are all buttons, except the jog wheels. @@ -1013,7 +1051,7 @@ class TraktorS2MK1Class { rightEncoder: [0x02, 0x0F], volume: 0x2B, gain: [0x01, 0x0F], - jogTouch: 0x0D, + jogPress: 0x0D, eq: { hi: 0x11, mid: 0x25, @@ -1046,7 +1084,7 @@ class TraktorS2MK1Class { rightEncoder: [0x04, 0x0F], volume: 0x2D, gain: [0x03, 0x0F], - jogTouch: 0x1D, + jogPress: 0x1D, eq: { hi: 0x21, mid: 0x23, @@ -1077,10 +1115,9 @@ class TraktorS2MK1Class { }); InputReport0x01.addControl("[Master]", "!browse_encoder_press", 0x0E, "B", 0x08, false, this.browseEncoderPress.bind(this)); - InputReport0x02.addControl("[Master]", "crossfader", 0x2F, "H"); + InputReport0x02.addControl("[Master]", "!crossfader", 0x2F, "H", 0xFFFF, false, this.crossfader.bind(this)); InputReport0x02.addControl("[Master]", "headMix", 0x31, "H"); - InputReport0x02.addControl("[Master]", "!samplerGain", 0x13, "H"); - InputReport0x02.setCallback("[Master]", "!samplerGain", this.samplerGainKnob.bind(this)); + InputReport0x02.addControl("[Master]", "!samplerGain", 0x13, "H", 0xFFFF, false, this.samplerGainKnob.bind(this)); InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoder.bind(this)); // Soft takeover for knobs @@ -1091,16 +1128,8 @@ class TraktorS2MK1Class { } // Set scalers - // this.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(InputReport0x01); @@ -1185,6 +1214,21 @@ class TraktorS2MK1Class { }); this.controller.setOutput("[Master]", "!warninglight", 0x00, true); } + calibrate() { + this.rawCalibration.faders = new Uint8Array(controller.getFeatureReport(0xD0)); + this.rawCalibration.knobs = new Uint8Array(0x20 * 3); + this.rawCalibration.knobs.set(new Uint8Array(controller.getFeatureReport(0xD1)), 0x00); + this.rawCalibration.knobs.set(new Uint8Array(controller.getFeatureReport(0xD2)), 0x20); + this.rawCalibration.knobs.set(new Uint8Array(controller.getFeatureReport(0xD3)), 0x40); + this.rawCalibration.jogWheels = new Uint8Array(controller.getFeatureReport(0xD4)); + this.calibration = this.parseRawCalibration(); + for (let i = 0; i < 2; i++) { + this.decks[i].calibrate(this.calibration.decks[i]); + } + for (let i = 0; i < 2; i++) { + this.effectUnits[i].calibrate(this.calibration.effectUnits[i]); + } + } init() { if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) { throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" + @@ -1204,6 +1248,7 @@ class TraktorS2MK1Class { "ButtonBrightnessOff is: " + ButtonBrightnessOff); } + this.calibrate(); this.registerInputPackets(); const debugLEDs = false; @@ -1254,7 +1299,7 @@ class TraktorS2MK1Class { } samplerGainKnob(field) { for (let i = 1; i <= 8; i++) { - engine.setParameter("[Sampler" + i + "]", "pregain", field.value / 4095); + setKnobParameter("[Sampler" + i + "]", "pregain", field.value, this.calibration.sampler); } } @@ -1280,6 +1325,9 @@ class TraktorS2MK1Class { } } } + crossfader(field) { + setFaderParameter("[Master]", "crossfader", field.value, this.calibration.crossfader); + } scalerParameter(group, name, value) { const scaledValue = script.absoluteLin(value, 0, 1, 16, 4080); @@ -1298,20 +1346,77 @@ class TraktorS2MK1Class { return script.absoluteLin(value, -1, 1, 16, 4080); } - outputChannelCallback(value, group, key) { - let ledValue = 0x05; - if (value) { - ledValue = 0x1F; - } - this.controller.setOutput(group, key, ledValue, !this.batchingLEDUpdate); - } - - outputChannelCallbackDark(value, group, key) { - let ledValue = 0x00; - if (value) { - ledValue = 0x1F; - } - this.controller.setOutput(group, key, ledValue, !this.batchingLEDUpdate); + parseRawCalibration() { + return { + decks: [ + { + volume: this.parseFaderCalibration(0x0C), + eq: { + hi: this.parseKnobCalibration(0x18), + mid: this.parseKnobCalibration(0x1E), + low: this.parseKnobCalibration(0x24), + }, + jogPress: this.parseJogPressCalibration(0x00), + }, + { + volume: this.parseFaderCalibration(0x10), + eq: { + hi: this.parseKnobCalibration(0x2A), + mid: this.parseKnobCalibration(0x30), + low: this.parseKnobCalibration(0x36), + }, + jogPress: this.parseJogPressCalibration(0x04), + }, + ], + effectUnits: [ + { + mix: this.parseKnobCalibration(0x00), + params: [ + this.parseKnobCalibration(0x06), + this.parseKnobCalibration(0x0C), + this.parseKnobCalibration(0x12), + ], + }, + { + mix: this.parseKnobCalibration(0x42), + params: [ + this.parseKnobCalibration(0x48), + this.parseKnobCalibration(0x4E), + this.parseKnobCalibration(0x54), + ] + } + ], + crossfader: this.parseFaderCalibration(0x14), + sampler: this.parseKnobCalibration(0x3C), + }; + } + parseKnobCalibration(index) { + const data = this.rawCalibration.knobs; + return { + min: this.parseUint16Le(data, index), + center: this.parseUint16Le(data, index+2), + max: this.parseUint16Le(data, index+4), + }; + } + parseFaderCalibration(index) { + const data = this.rawCalibration.faders; + return { + min: this.parseUint16Le(data, index), + max: this.parseUint16Le(data, index+2), + }; + } + parseJogPressCalibration(index) { + const data = this.rawCalibration.jogWheels; + return { + unpressed: this.parseUint16Be(data, index), + pressed: this.parseUint16Be(data, index+2), + }; + } + parseUint16Le(data, index) { + return data[index] + (data[index+1]<<8); + } + parseUint16Be(data, index) { + return (data[index]<<8) + data[index+1]; } shiftPressed() { @@ -1359,6 +1464,20 @@ const encoderDirection = function(newValue, oldValue) { return direction; }; +const setKnobParameter = function(group, key, value, calibration) { + let calibratedValue; + if (value <= calibration.center) { + calibratedValue = script.absoluteLin(value, 0, 0.5, calibration.min, calibration.center); + } else { + calibratedValue = script.absoluteLin(value, 0.5, 1, calibration.center, calibration.max); + } + engine.setValue(group, key, calibratedValue); +}; +const setFaderParameter = function(group, key, value, calibration) { + const calibratedValue = script.absoluteLin(value, 0, 1, calibration.min, calibration.max); + engine.setParameter(group, key, calibratedValue); +}; + var TraktorS2MK1 = new TraktorS2MK1Class(); // eslint-disable-line no-var, no-unused-vars From 624145f4ce04b1710828b6a2f9e7ed7cd27d0f0b Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Sun, 2 Jul 2023 17:40:25 +0200 Subject: [PATCH 03/16] Traktor S2 Mk1: update copyright notice --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 0198159663c..15a89b8028f 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -1,7 +1,7 @@ /****************************************************************/ /* Traktor Kontrol S2 MK1 HID controller script */ -/* Copyright (C) 2021, leifhelm */ -/* Based on: */ +/* Copyright (C) 2023, leifhelm */ +/* Initially Based on: */ /* Traktor Kontrol S2 MK2 HID controller script v1.00 */ /* Copyright (C) 2020, Be */ /* Copyright (C) 2017, z411 */ From add0f497bf1176b95486ccc0e68f8f776ec58727 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Sun, 2 Jul 2023 18:15:34 +0200 Subject: [PATCH 04/16] Traktor S2 Mk1: load_preset -> loaded_chain_preset --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 15a89b8028f..f091501095b 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -778,7 +778,7 @@ class EffectUnit { const showParameters = engine.getValue(this.group, "show_parameters"); if (field.value > 0) { if (this.parent.decks[this.number -1].shiftPressed) { - engine.setValue(this.group, "load_preset", 1); + engine.setValue(this.group, "loaded_chain_preset", 1); return; } this.effectFocusLongPressTimer = engine.beginTimer(longPressTimeoutMilliseconds, function() { @@ -903,7 +903,7 @@ class EffectParameter { if (field.value > 0) { if (this.effectUnit.shiftPressed()) { - engine.setValue(this.group, "load_preset", this.number+1); + engine.setValue(this.group, "loaded_chain_preset", this.number+1); } else { if (this.effectUnit.effectFocusChooseModeActive) { if (focusedEffect === this.number) { From 4e9298add90b69935fca1d1584301f6ef1027894 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Sun, 2 Jul 2023 18:16:35 +0200 Subject: [PATCH 05/16] Traktor S2 Mk1: remove linkOutput --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index f091501095b..02f40575d90 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -143,7 +143,7 @@ class DeckClass { hidReport.addOutput(this.channel, name, config, "B"); } linkLed(name, callback) { - this.controller.linkOutput(this.channel, name, this.channel, name, callback.bind(this)); + engine.makeConnection(this.channel, name, callback.bind(this)); } volume(field) { setFaderParameter(this.channel, "volume", field.value, this.calibration.volume); From a91b007297f21656b334218cb8fadd19eeb0a65e Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Tue, 4 Jul 2023 21:13:50 +0200 Subject: [PATCH 06/16] Traktor S2 Mk1: fix eq knobs scaling for biquad --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 02f40575d90..dca8be185da 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -709,7 +709,7 @@ class EqualizerParameter { hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); } knob(field) { - setKnobParameter(this.group, "parameter" + this.number, field.value, this.calibration); + setKnobParameterEq(this.group, "parameter" + this.number, field.value, this.calibration); } } @@ -1473,6 +1473,15 @@ const setKnobParameter = function(group, key, value, calibration) { } engine.setValue(group, key, calibratedValue); }; +const setKnobParameterEq = function(group, key, value, calibration) { + let calibratedValue; + if (value <= calibration.center) { + calibratedValue = script.absoluteLin(value, 0, 1, calibration.min, calibration.center); + } else { + calibratedValue = script.absoluteLin(value, 1, 2, calibration.center, calibration.max); + } + engine.setValue(group, key, calibratedValue); +}; const setFaderParameter = function(group, key, value, calibration) { const calibratedValue = script.absoluteLin(value, 0, 1, calibration.min, calibration.max); engine.setParameter(group, key, calibratedValue); From 4e2dc7134dc72aa6d445d583a45e4ae45919bbb7 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Tue, 4 Jul 2023 21:34:02 +0200 Subject: [PATCH 07/16] Traktor S2 Mk1: Adjust browse knob behavior --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index dca8be185da..09fd58e8ad6 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -166,7 +166,6 @@ class DeckClass { this.controller.setOutput(this.channel, "!shift", shiftPressed ? ButtonBrightnessOn : ButtonBrightnessOff, !this.parent.batchingLEDUpdate); - engine.setValue("[Library]", "focused_widget", shiftPressed ? 2: 3); } syncButton(field) { const now = Date.now(); @@ -1313,17 +1312,20 @@ class TraktorS2MK1Class { const delta = encoderDirection(field.value, this.previousBrowse); this.previousBrowse = field.value; - engine.setValue("[Library]", "MoveVertical", delta); + if (this.shiftPressed()) { + engine.setValue("[Library]", "ScrollVertical", delta); + } else { + engine.setValue("[Library]", "MoveVertical", delta); + } } browseEncoderPress(field) { if (this.shiftPressed()) { - engine.setValue("[Library]", "GoToItem", field.value); + engine.setValue("[Library]", "MoveFocusBackward", field.value); } else { - if (field.value > 0) { - script.toggleControl("[Master]", "maximize_library"); - } + engine.setValue("[Library]", "GoToItem", field.value); } + } crossfader(field) { setFaderParameter("[Master]", "crossfader", field.value, this.calibration.crossfader); From 1568bca96638962df55109449b76c485b584114d Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Tue, 4 Jul 2023 22:21:40 +0200 Subject: [PATCH 08/16] Traktor S2 Mk1: Read initial state of controls --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 09fd58e8ad6..44d618679ae 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -77,14 +77,10 @@ class DeckClass { this.registerScalar(inputReport0x02, "rate", config.rate); this.registerEncoder(inputReport0x02, "!left_encoder", config.leftEncoder, this.leftEncoder); this.registerEncoder(inputReport0x02, "!right_encoder", config.rightEncoder, this.rightEncoder); - // this.registerScalar(inputReport0x02, "volume", config.volume); this.registerScalar(inputReport0x02, "!volume", config.volume, this.volume); this.registerEncoder(inputReport0x02, "!pregain", config.gain, this.gainEncoder); this.registerScalar(inputReport0x02, "!jog_press", config.jogPress, this.jogPress); this.eq.registerInputs(inputReport0x02, config.eq); - // configure soft takeover - engine.softTakeover(this.group, "rate", true); - engine.softTakeover(this.group, "volume", true); } registerOutputs(outputReport0x80, config) { this.registerLed(outputReport0x80, "track_loaded", config.trackLoaded); @@ -121,6 +117,11 @@ class DeckClass { this.calibration = calibration; this.eq.calibrate(calibration.eq); } + enableSoftTakeover() { + engine.softTakeover(this.group, "rate", true); + engine.softTakeover(this.group, "volume", true); + this.eq.enableSoftTakeover(); + } registerButton(hidReport, name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); @@ -689,6 +690,11 @@ class Equalizer { this.params[param].calibrate(calibration[param]); } } + enableSoftTakeover() { + for (const param in this.params) { + this.params[param].enableSoftTakeover(); + } + } } class EqualizerParameter { constructor(equalizer, number) { @@ -699,11 +705,13 @@ class EqualizerParameter { } registerInputs(inputReport0x02, config) { this.registerKnob(inputReport0x02, "!parameter" + this.number, config, this.knob); - engine.softTakeover(this.group, "parameter" + this.number, true); } calibrate(calibration) { this.calibration = calibration; } + enableSoftTakeover() { + engine.softTakeover(this.group, "parameter" + this.number, true); + } registerKnob(hidReport, name, config, callback) { hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); } @@ -761,6 +769,12 @@ class EffectUnit { this.params[i].calibrate(calibration.params[i]); } } + enableSoftTakeover() { + engine.softTakeover(this.group, "!mix", true); + for (let i = 0; i < 3; i++) { + this.params[i].enableSoftTakeover(); + } + } registerButton(hidReport, name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); @@ -878,12 +892,6 @@ class EffectParameter { registerInputs(inputReport0x01, inputReport0x02, config) { this.registerButton(inputReport0x01, "!effectbutton" + this.number, config.button, this.effectButton); this.registerKnob(inputReport0x02, "!effectknob" + this.number, config.knob, this.effectKnob); - // soft takeover - const group = this.groupPrefix + "_Effect" + this.number + "]"; - engine.softTakeover(group, "meta", true); - for (let i = 1; i <= 3; i++) { - engine.softTakeover(group, "parameter" + i, true); - } } registerOutputs(outputReport0x80, config) { outputReport0x80.addOutput(this.group, "!effectbutton" + this.number, config, "B"); @@ -891,6 +899,13 @@ class EffectParameter { calibrate(calibration) { this.calibration = calibration; } + enableSoftTakeover() { + const group = this.groupPrefix + "_Effect" + this.number + "]"; + engine.softTakeover(group, "meta", true); + for (let i = 1; i <= 3; i++) { + engine.softTakeover(group, "parameter" + i, true); + } + } registerButton(hidReport, name, config, callback) { hidReport.addControl(this.group, name, config[0], "B", config[1], false, callback.bind(this)); } @@ -1120,11 +1135,6 @@ class TraktorS2MK1Class { InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoder.bind(this)); // Soft takeover for knobs - engine.softTakeover("[Master]", "crossfader", true); - engine.softTakeover("[Master]", "headMix", true); - for (let i = 1; i <= 8; i++) { - engine.softTakeover("[Sampler" + i + "]", "pregain", true); - } // Set scalers this.controller.setScaler("headMix", this.scalerSlider); @@ -1228,6 +1238,27 @@ class TraktorS2MK1Class { this.effectUnits[i].calibrate(this.calibration.effectUnits[i]); } } + readCurrentPosition() { + const report0x01 = new Uint8Array(controller.getInputReport(0x01)); + this.controller.parsePacket([0x01, ...Array.from(report0x01)]); + const report0x02 = new Uint8Array(controller.getInputReport(0x02)); + this.controller.parsePacket([0x02, ...Array.from(report0x02.map(x => ~x))]); + this.controller.parsePacket([0x02, ...Array.from(report0x02)]); + } + enableSoftTakeover() { + engine.softTakeover("[Master]", "crossfader", true); + engine.softTakeover("[Master]", "headMix", true); + for (let i = 1; i <= 8; i++) { + engine.softTakeover("[Sampler" + i + "]", "pregain", true); + } + + for (let i = 0; i < 2; i++) { + this.decks[i].enableSoftTakeover(); + } + for (let i = 0; i < 2; i++) { + this.effectUnits[i].enableSoftTakeover(); + } + } init() { if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) { throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" + @@ -1249,6 +1280,8 @@ class TraktorS2MK1Class { this.calibrate(); this.registerInputPackets(); + this.readCurrentPosition(); + this.enableSoftTakeover(); const debugLEDs = false; if (debugLEDs) { From b148da47be7c0dcc91de08671594fdb119f04a28 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 5 Jul 2023 16:28:32 +0200 Subject: [PATCH 09/16] Traktor S2 Mk1: Fix encoder initialization --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 96 ++++++++++--------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 44d618679ae..f7c52a963e6 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -33,9 +33,9 @@ class DeckClass { this.controller = this.parent.controller; this.number = number; this.channel = "[Channel" + number + "]"; - this.previousPregain = 0; - this.previousLeftEncoder = 0; - this.previousRightEncoder = 0; + this.gainEncoder = new Encoder(); + this.leftEncoder = new Encoder(); + this.rightEncoder = new Encoder(); this.wheelPressInertiaTimer = 0; this.gainEncoderPressed = false; this.leftEncoderPressed = false; @@ -75,10 +75,10 @@ class DeckClass { inputReport0x01.addControl(this.channel, "!jog_wheel", config.jogWheel, "I", 0xFFFFFFFF, false, this.jogMove.bind(this)); // InputReport 0x02 this.registerScalar(inputReport0x02, "rate", config.rate); - this.registerEncoder(inputReport0x02, "!left_encoder", config.leftEncoder, this.leftEncoder); - this.registerEncoder(inputReport0x02, "!right_encoder", config.rightEncoder, this.rightEncoder); + this.registerEncoder(inputReport0x02, "!left_encoder", config.leftEncoder, this.leftEncoderCallback); + this.registerEncoder(inputReport0x02, "!right_encoder", config.rightEncoder, this.rightEncoderCallback); this.registerScalar(inputReport0x02, "!volume", config.volume, this.volume); - this.registerEncoder(inputReport0x02, "!pregain", config.gain, this.gainEncoder); + this.registerEncoder(inputReport0x02, "!pregain", config.gain, this.gainEncoderCallback); this.registerScalar(inputReport0x02, "!jog_press", config.jogPress, this.jogPress); this.eq.registerInputs(inputReport0x02, config.eq); } @@ -324,14 +324,13 @@ class DeckClass { engine.setValue(this.channel, "jog", velocity); } } - leftEncoder(field) { - const delta = encoderDirection(field.value, this.previousLeftEncoder); - this.previousLeftEncoder = field.value; + leftEncoderCallback(field) { + const delta = this.leftEncoder.delta(field.value); if (this.shiftPressed) { if (delta === 1) { script.triggerControl(this.channel, "pitch_up_small"); - } else { + } else if (delta === -1) { script.triggerControl(this.channel, "pitch_down_small"); } } else { @@ -339,40 +338,38 @@ class DeckClass { let beatjumpSize = engine.getValue(this.channel, "beatjump_size"); if (delta === 1) { beatjumpSize *= 2; - } else { + } else if (delta === -1) { beatjumpSize /= 2; } engine.setValue(this.channel, "beatjump_size", beatjumpSize); } else { if (delta === 1) { script.triggerControl(this.channel, "beatjump_forward"); - } else { + } else if (delta === -1) { script.triggerControl(this.channel, "beatjump_backward"); } } } } - rightEncoder(field) { - const delta = encoderDirection(field.value, this.previousRightEncoder); - this.previousRightEncoder = field.value; + rightEncoderCallback(field) { + const delta = this.rightEncoder.delta(field.value); if (this.shiftPressed) { if (delta === 1) { script.triggerControl(this.channel, "beatjump_1_forward"); - } else { + } else if (delta === -1) { script.triggerControl(this.channel, "beatjump_1_backward"); } } else { if (delta === 1) { script.triggerControl(this.channel, "loop_double"); - } else { + } else if (delta === -1) { script.triggerControl(this.channel, "loop_halve"); } } } - gainEncoder(field) { - const delta = 0.03333 * encoderDirection(field.value, this.previousPregain); - this.previousPregain = field.value; + gainEncoderCallback(field) { + const delta = 0.03333 * this.gainEncoder.delta(field.value); if (this.shiftPressed) { const currentPregain = engine.getParameter(this.channel, "pregain"); @@ -380,7 +377,11 @@ class DeckClass { } else { const quickEffectGroup = "[QuickEffectRack1_" + this.channel + "]"; if (this.gainEncoderPressed) { - script.triggerControl(quickEffectGroup, delta > 0 ? "next_chain" : "prev_chain"); + if (delta === 1) { + script.triggerControl(quickEffectGroup, "next_chain"); + } else if (delta === -1) { + script.triggerControl(quickEffectGroup, "prev_chain"); + } } else { const currentQuickEffectSuperKnob = engine.getParameter(quickEffectGroup, "super1"); engine.setParameter(quickEffectGroup, "super1", currentQuickEffectSuperKnob + delta); @@ -1016,8 +1017,7 @@ class TraktorS2MK1Class { // Used when updating multiple LEDs simultaneously. this.batchingLEDUpdate = false; - // Previous values, used for calculating deltas for encoder knobs. - this.previousBrowse = 0; + this.browseEncoder = new Encoder(); this.decks = [ new DeckClass(this, 1), @@ -1132,7 +1132,7 @@ class TraktorS2MK1Class { InputReport0x02.addControl("[Master]", "!crossfader", 0x2F, "H", 0xFFFF, false, this.crossfader.bind(this)); InputReport0x02.addControl("[Master]", "headMix", 0x31, "H"); InputReport0x02.addControl("[Master]", "!samplerGain", 0x13, "H", 0xFFFF, false, this.samplerGainKnob.bind(this)); - InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoder.bind(this)); + InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoderCallback.bind(this)); // Soft takeover for knobs @@ -1242,7 +1242,8 @@ class TraktorS2MK1Class { const report0x01 = new Uint8Array(controller.getInputReport(0x01)); this.controller.parsePacket([0x01, ...Array.from(report0x01)]); const report0x02 = new Uint8Array(controller.getInputReport(0x02)); - this.controller.parsePacket([0x02, ...Array.from(report0x02.map(x => ~x))]); + // The first packet is ignored by HIDConstroller + this.controller.parsePacket([0x02, ...Array.from(report0x02.map(x => x ^ 0xFF))]); this.controller.parsePacket([0x02, ...Array.from(report0x02)]); } enableSoftTakeover() { @@ -1341,9 +1342,8 @@ class TraktorS2MK1Class { } } - browseEncoder(field) { - const delta = encoderDirection(field.value, this.previousBrowse); - this.previousBrowse = field.value; + browseEncoderCallback(field) { + const delta = this.browseEncoder.delta(field.value); if (this.shiftPressed()) { engine.setValue("[Library]", "ScrollVertical", delta); @@ -1459,6 +1459,28 @@ class TraktorS2MK1Class { } } +class Encoder { + constructor() { + this.previousValue = -1; + } + /// return value 1 === right turn + /// return value -1 === left turn + /// retun value 0 when something wierd happens/first delta + delta(value) { + if (this.previousValue === -1) { + this.previousValue = value; + return 0; + } + let dir = 0; + if ((value + 1) % 16 === this.previousValue) { + dir = -1; + } else if ((this.previousValue + 1) % 16 === value) { + dir = 1; + } + this.previousValue = value; + return dir; + } +} const introOutroKeys = [ "intro_start", @@ -1481,24 +1503,6 @@ const introOutroColorsDim = [ {green: 0, blue: 0x05} ]; -/// return value 1 === right turn -/// return value -1 === left turn -const encoderDirection = function(newValue, oldValue) { - let direction = 0; - const min = 0; - const 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; -}; - const setKnobParameter = function(group, key, value, calibration) { let calibratedValue; if (value <= calibration.center) { From 1a127060e54d05cdf792303ebddc3650dbcb8327 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 5 Jul 2023 16:29:13 +0200 Subject: [PATCH 10/16] Traktor S2 Mk1: setValue -> setParameter --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index f7c52a963e6..c0a7af27ad1 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -717,7 +717,7 @@ class EqualizerParameter { hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); } knob(field) { - setKnobParameterEq(this.group, "parameter" + this.number, field.value, this.calibration); + setKnobParameter(this.group, "parameter" + this.number, field.value, this.calibration); } } @@ -1465,7 +1465,7 @@ class Encoder { } /// return value 1 === right turn /// return value -1 === left turn - /// retun value 0 when something wierd happens/first delta + /// return value 0 when something weird happens/first delta delta(value) { if (this.previousValue === -1) { this.previousValue = value; @@ -1510,16 +1510,7 @@ const setKnobParameter = function(group, key, value, calibration) { } else { calibratedValue = script.absoluteLin(value, 0.5, 1, calibration.center, calibration.max); } - engine.setValue(group, key, calibratedValue); -}; -const setKnobParameterEq = function(group, key, value, calibration) { - let calibratedValue; - if (value <= calibration.center) { - calibratedValue = script.absoluteLin(value, 0, 1, calibration.min, calibration.center); - } else { - calibratedValue = script.absoluteLin(value, 1, 2, calibration.center, calibration.max); - } - engine.setValue(group, key, calibratedValue); + engine.setParameter(group, key, calibratedValue); }; const setFaderParameter = function(group, key, value, calibration) { const calibratedValue = script.absoluteLin(value, 0, 1, calibration.min, calibration.max); From 697e9ebb4d2af7034578304f0c43f80e75378567 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Sun, 9 Jul 2023 20:39:47 +0200 Subject: [PATCH 11/16] Traktor S2 Mk1: Playlist -> Library --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index c0a7af27ad1..ab1519e3b2e 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -1132,7 +1132,7 @@ class TraktorS2MK1Class { InputReport0x02.addControl("[Master]", "!crossfader", 0x2F, "H", 0xFFFF, false, this.crossfader.bind(this)); InputReport0x02.addControl("[Master]", "headMix", 0x31, "H"); InputReport0x02.addControl("[Master]", "!samplerGain", 0x13, "H", 0xFFFF, false, this.samplerGainKnob.bind(this)); - InputReport0x02.addControl("[Playlist]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoderCallback.bind(this)); + InputReport0x02.addControl("[Library]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoderCallback.bind(this)); // Soft takeover for knobs From 18007a85f2fc7ac5ac2f4d7c57b533b0d1413ae7 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Tue, 29 Aug 2023 10:20:57 +0200 Subject: [PATCH 12/16] Traktor S2 Mk1: Clarify registerInput/Output --- .../Traktor-Kontrol-S2-MK1-hid-scripts.js | 460 ++++++++++-------- 1 file changed, 252 insertions(+), 208 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index ab1519e3b2e..1a05845f242 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -54,51 +54,51 @@ class DeckClass { this.syncEnabledTime = {}; this.calibration = null; } - registerInputs(inputReport0x01, inputReport0x02, config) { + registerInputs(config) { // InputReport 0x01 - this.registerButton(inputReport0x01, "!gain_encoder_press", config.gainEncoderPress, this.gainEncoderPress); - this.registerButton(inputReport0x01, "!shift", config.shift, this.shift); - this.registerButton(inputReport0x01, "!sync_enabled", config.sync, this.syncButton); - this.registerButton(inputReport0x01, "!cue_default", config.cue, this.cueButton); - this.registerButton(inputReport0x01, "!play", config.play, this.playButton); + this.registerButton("!gain_encoder_press", config.gainEncoderPress, this.gainEncoderPress); + this.registerButton("!shift", config.shift, this.shift); + this.registerButton("!sync_enabled", config.sync, this.syncButton); + this.registerButton("!cue_default", config.cue, this.cueButton); + this.registerButton("!play", config.play, this.playButton); for (let i = 0; i < 4; i++) { - this.pads[i].registerInputs(inputReport0x01, config.pads[i]); - } - this.registerButton(inputReport0x01, "!loop_in", config.loopIn, this.loopInButton); - this.registerButton(inputReport0x01, "!loop_out", config.loopOut, this.loopOutButton); - this.registerButton(inputReport0x01, "!samples_button", config.samples, this.samplerModeButton); - this.registerButton(inputReport0x01, "!reset_button", config.reset, this.introOutroModeButton); - this.registerButton(inputReport0x01, "!left_encoder_press", config.leftEncoderPress, this.leftEncoderPress); - this.registerButton(inputReport0x01, "!right_encoder_press", config.rightEncoderPress, this.rightEncoderPress); - this.registerButton(inputReport0x01, "!load_track", config.loadTrack, this.loadTrackButton); - this.registerButton(inputReport0x01, "!pfl", config.pfl, this.pflButton); - inputReport0x01.addControl(this.channel, "!jog_wheel", config.jogWheel, "I", 0xFFFFFFFF, false, this.jogMove.bind(this)); + this.pads[i].registerInputs(config.pads[i]); + } + this.registerButton("!loop_in", config.loopIn, this.loopInButton); + this.registerButton("!loop_out", config.loopOut, this.loopOutButton); + this.registerButton("!samples_button", config.samples, this.samplerModeButton); + this.registerButton("!reset_button", config.reset, this.introOutroModeButton); + this.registerButton("!left_encoder_press", config.leftEncoderPress, this.leftEncoderPress); + this.registerButton("!right_encoder_press", config.rightEncoderPress, this.rightEncoderPress); + this.registerButton("!load_track", config.loadTrack, this.loadTrackButton); + this.registerButton("!pfl", config.pfl, this.pflButton); + config.jogWheel.hidReport.addControl(this.channel, "!jog_wheel", config.jogWheel.offset, "I", 0xFFFFFFFF, false, this.jogMove.bind(this)); // InputReport 0x02 - this.registerScalar(inputReport0x02, "rate", config.rate); - this.registerEncoder(inputReport0x02, "!left_encoder", config.leftEncoder, this.leftEncoderCallback); - this.registerEncoder(inputReport0x02, "!right_encoder", config.rightEncoder, this.rightEncoderCallback); - this.registerScalar(inputReport0x02, "!volume", config.volume, this.volume); - this.registerEncoder(inputReport0x02, "!pregain", config.gain, this.gainEncoderCallback); - this.registerScalar(inputReport0x02, "!jog_press", config.jogPress, this.jogPress); - this.eq.registerInputs(inputReport0x02, config.eq); - } - registerOutputs(outputReport0x80, config) { - this.registerLed(outputReport0x80, "track_loaded", config.trackLoaded); + this.registerScalar("rate", config.rate); + this.registerEncoder("!left_encoder", config.leftEncoder, this.leftEncoderCallback); + this.registerEncoder("!right_encoder", config.rightEncoder, this.rightEncoderCallback); + this.registerScalar("!volume", config.volume, this.volume); + this.registerEncoder("!pregain", config.gain, this.gainEncoderCallback); + this.registerScalar("!jog_press", config.jogPress, this.jogPress); + this.eq.registerInputs(config.eq); + } + registerOutputs(config) { + this.registerLed("track_loaded", config.trackLoaded); for (let i = 0; i < 4; i++) { - this.registerLed(outputReport0x80, "!VuMeter" + i, config.vuMeter + i); - } - this.registerLed(outputReport0x80, "PeakIndicator", config.peak); - this.registerLed(outputReport0x80, "!reset_button", config.reset); - this.registerLed(outputReport0x80, "loop_in", config.loopIn); - this.registerLed(outputReport0x80, "loop_out", config.loopOut); - this.registerLed(outputReport0x80, "pfl", config.pfl); - this.registerLed(outputReport0x80, "!samples_button", config.samples); - this.registerLed(outputReport0x80, "!shift", config.shift); - this.registerLed(outputReport0x80, "sync_enabled", config.sync); - this.registerLed(outputReport0x80, "cue_indicator", config.cue); - this.registerLed(outputReport0x80, "play_indicator", config.play); + this.registerLed("!VuMeter" + i, {hidReport: config.vuMeter.hidReport, offset: config.vuMeter.offset + i}); + } + this.registerLed("PeakIndicator", config.peak); + this.registerLed("!reset_button", config.reset); + this.registerLed("loop_in", config.loopIn); + this.registerLed("loop_out", config.loopOut); + this.registerLed("pfl", config.pfl); + this.registerLed("!samples_button", config.samples); + this.registerLed("!shift", config.shift); + this.registerLed("sync_enabled", config.sync); + this.registerLed("cue_indicator", config.cue); + this.registerLed("play_indicator", config.play); for (let i = 0; i < 4; i++) { - this.pads[i].registerOutputs(outputReport0x80, config.pads[i]); + this.pads[i].registerOutputs(config.pads[i]); } } linkOutputs() { @@ -122,26 +122,28 @@ class DeckClass { engine.softTakeover(this.group, "volume", true); this.eq.enableSoftTakeover(); } - registerButton(hidReport, name, config, callback) { + registerButton(name, config, callback) { if (callback !==undefined) { - callback= callback.bind(this); + callback = callback.bind(this); } - hidReport.addControl(this.channel, name, config[0], "B", config[1], false, callback); + config.hidReport.addControl(this.channel, name, config.offset, "B", config.mask, false, callback); } - registerScalar(hidReport, name, config, callback) { + registerScalar(name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); } - hidReport.addControl(this.channel, name, config, "H", 0xFFFF, false, callback); + config.hidReport.addControl(this.channel, name, config.offset, "H", 0xFFFF, false, callback); } - registerEncoder(hidReport, name, config, callback) { + registerEncoder(name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); } - hidReport.addControl(this.channel, name, config[0], "B", config[1], false, callback); + config.hidReport.addControl(this.channel, name, config.offset, "B", config.mask, false, callback); } - registerLed(hidReport, name, config) { - hidReport.addOutput(this.channel, name, config, "B"); + registerLed(name, config) { + console.log(name); + console.log(config.hidReport); + config.hidReport.addOutput(this.channel, name, config.offset, "B"); } linkLed(name, callback) { engine.makeConnection(this.channel, name, callback.bind(this)); @@ -545,15 +547,15 @@ class PadButton { this.samplerGroup = "[Sampler" + samplerNumber + "]"; this.connections = []; } - registerInputs(inputReport0x01, config) { - inputReport0x01.addControl(this.deck.channel, "!pad" + this.number, config[0], "B", config[1], false, this.pressHandler.bind(this)); + registerInputs(config) { + config.hidReport.addControl(this.deck.channel, "!pad" + this.number, config.offset, "B", config.mask, false, this.pressHandler.bind(this)); } - registerOutputs(outputReport0x80, config) { - this.registerLed(outputReport0x80, "!pad_" + this.number + "_G", config.green); - this.registerLed(outputReport0x80, "!pad_" + this.number + "_B", config.blue); + registerOutputs(config) { + this.registerLed("!pad_" + this.number + "_G", config.green); + this.registerLed("!pad_" + this.number + "_B", config.blue); } - registerLed(hidReport, name, config) { - hidReport.addOutput(this.deck.channel, name, config, "B"); + registerLed(name, config) { + config.hidReport.addOutput(this.deck.channel, name, config.offset, "B"); } pressHandler(field) { const padMode = this.deck.currentPadMode; @@ -681,9 +683,9 @@ class Equalizer { low: new EqualizerParameter(this, 1), }; } - registerInputs(inputReport0x02, config) { + registerInputs(config) { for (const param in this.params) { - this.params[param].registerInputs(inputReport0x02, config[param]); + this.params[param].registerInputs(config[param]); } } calibrate(calibration) { @@ -704,8 +706,8 @@ class EqualizerParameter { this.number = number; this.calibration = null; } - registerInputs(inputReport0x02, config) { - this.registerKnob(inputReport0x02, "!parameter" + this.number, config, this.knob); + registerInputs(config) { + this.registerKnob("!parameter" + this.number, config, this.knob); } calibrate(calibration) { this.calibration = calibration; @@ -713,8 +715,8 @@ class EqualizerParameter { enableSoftTakeover() { engine.softTakeover(this.group, "parameter" + this.number, true); } - registerKnob(hidReport, name, config, callback) { - hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); + registerKnob(name, config, callback) { + config.hidReport.addControl(this.group, name, config.offset, "H", 0xFFFF, false, callback.bind(this)); } knob(field) { setKnobParameter(this.group, "parameter" + this.number, field.value, this.calibration); @@ -741,21 +743,21 @@ class EffectUnit { ]; this.calibration = null; } - registerInputs(inputReport0x01, inputReport0x02, config) { - this.registerButton(inputReport0x01, "!effect_focus_button", config.focus, this.effectFocusButton); - this.registerButton(inputReport0x01, "group_[Channel1]_enable", config.channel1); - this.registerButton(inputReport0x01, "group_[Channel2]_enable", config.channel2); - this.registerKnob(inputReport0x02, "!mix", config.mix, this.mixKnob); + registerInputs(config) { + this.registerButton("!effect_focus_button", config.focus, this.effectFocusButton); + this.registerButton("group_[Channel1]_enable", config.channel1); + this.registerButton("group_[Channel2]_enable", config.channel2); + this.registerKnob("!mix", config.mix, this.mixKnob); for (let i = 0; i < 3; i++) { - this.params[i].registerInputs(inputReport0x01, inputReport0x02, config.params[i]); + this.params[i].registerInputs(config.params[i]); } } - registerOutputs(outputReport0x80, config) { - this.registerLed(outputReport0x80, "!effect_focus_button", config.focus); - this.registerLed(outputReport0x80, "group_[Channel1]_enable", config.channel1); - this.registerLed(outputReport0x80, "group_[Channel2]_enable", config.channel2); + registerOutputs(config) { + this.registerLed("!effect_focus_button", config.focus); + this.registerLed("group_[Channel1]_enable", config.channel1); + this.registerLed("group_[Channel2]_enable", config.channel2); for (let i = 0; i < 3; i++) { - this.params[i].registerOutputs(outputReport0x80, config.params[i]); + this.params[i].registerOutputs(config.params[i]); } } linkOutputs() { @@ -776,17 +778,17 @@ class EffectUnit { this.params[i].enableSoftTakeover(); } } - registerButton(hidReport, name, config, callback) { + registerButton(name, config, callback) { if (callback !==undefined) { callback= callback.bind(this); } - hidReport.addControl(this.group, name, config[0], "B", config[1], false, callback); + config.hidReport.addControl(this.group, name, config.offset, "B", config.mask, false, callback); } - registerKnob(hidReport, name, config, callback) { - hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); + registerKnob(name, config, callback) { + config.hidReport.addControl(this.group, name, config.offset, "H", 0xFFFF, false, callback.bind(this)); } - registerLed(hidReport, name, config) { - hidReport.addOutput(this.group, name, config, "B"); + registerLed(name, config) { + config.hidReport.addOutput(this.group, name, config.offset, "B"); } effectFocusButton(field) { const showParameters = engine.getValue(this.group, "show_parameters"); @@ -890,12 +892,12 @@ class EffectParameter { this.ledConnection = null; this.calibration = null; } - registerInputs(inputReport0x01, inputReport0x02, config) { - this.registerButton(inputReport0x01, "!effectbutton" + this.number, config.button, this.effectButton); - this.registerKnob(inputReport0x02, "!effectknob" + this.number, config.knob, this.effectKnob); + registerInputs(config) { + this.registerButton("!effectbutton" + this.number, config.button, this.effectButton); + this.registerKnob("!effectknob" + this.number, config.knob, this.effectKnob); } - registerOutputs(outputReport0x80, config) { - outputReport0x80.addOutput(this.group, "!effectbutton" + this.number, config, "B"); + registerOutputs(config) { + config.hidReport.addOutput(this.group, "!effectbutton" + this.number, config.offset, "B"); } calibrate(calibration) { this.calibration = calibration; @@ -907,11 +909,11 @@ class EffectParameter { engine.softTakeover(group, "parameter" + i, true); } } - registerButton(hidReport, name, config, callback) { - hidReport.addControl(this.group, name, config[0], "B", config[1], false, callback.bind(this)); + registerButton(name, config, callback) { + config.hidReport.addControl(this.group, name, config.offset, "B", config.mask, false, callback.bind(this)); } - registerKnob(hidReport, name, config, callback) { - hidReport.addControl(this.group, name, config, "H", 0xFFFF, false, callback.bind(this)); + registerKnob(name, config, callback) { + config.hidReport.addControl(this.group, name, config.offset, "H", 0xFFFF, false, callback.bind(this)); } effectButton(field) { const focusedEffect = engine.getValue(this.group, "focused_effect"); @@ -1039,93 +1041,111 @@ class TraktorS2MK1Class { // There are also some 4 bit encoders. const InputReport0x02 = new HIDPacket("InputReport0x02", 0x02, this.inputReport0x02Callback.bind(this)); - this.decks[0].registerInputs(InputReport0x01, InputReport0x02, { - gainEncoderPress: [0x0E, 0x01], - shift: [0x0D, 0x80], - sync: [0x0D, 0x40], - cue: [0x0D, 0x20], - play: [0x0D, 0x10], + this.decks[0].registerInputs({ + gainEncoderPress: {hidReport: InputReport0x01, offset: 0x0E, mask: 0x01}, + shift: {hidReport: InputReport0x01, offset: 0x0D, mask: 0x80}, + sync: {hidReport: InputReport0x01, offset: 0x0D, mask: 0x40}, + cue: {hidReport: InputReport0x01, offset: 0x0D, mask: 0x20}, + play: {hidReport: InputReport0x01, offset: 0x0D, mask: 0x10}, pads: [ - [0x0D, 0x08], - [0x0D, 0x04], - [0x0D, 0x02], - [0x0D, 0x01], + {hidReport: InputReport0x01, offset: 0x0D, mask: 0x08}, + {hidReport: InputReport0x01, offset: 0x0D, mask: 0x04}, + {hidReport: InputReport0x01, offset: 0x0D, mask: 0x02}, + {hidReport: InputReport0x01, offset: 0x0D, mask: 0x01}, ], - loopIn: [0x09, 0x40], - loopOut: [0x09, 0x20], - samples: [0x0B, 0x02], - reset: [0x09, 0x10], - leftEncoderPress: [0x0E, 0x02], - rightEncoderPress: [0x0E, 0x04], - jogWheel: 0x01, - loadTrack: [0x0B, 0x08], - pfl: [0x09, 0x80], - rate: 0x0F, - leftEncoder: [0x01, 0xF0], - rightEncoder: [0x02, 0x0F], - volume: 0x2B, - gain: [0x01, 0x0F], - jogPress: 0x0D, + loopIn: {hidReport: InputReport0x01, offset: 0x09, mask: 0x40}, + loopOut: {hidReport: InputReport0x01, offset: 0x09, mask: 0x20}, + samples: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x02}, + reset: {hidReport: InputReport0x01, offset: 0x09, mask: 0x10}, + leftEncoderPress: {hidReport: InputReport0x01, offset: 0x0E, mask: 0x02}, + rightEncoderPress: {hidReport: InputReport0x01, offset: 0x0E, mask: 0x04}, + jogWheel: {hidReport: InputReport0x01, offset: 0x01}, + loadTrack: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x08}, + pfl: {hidReport: InputReport0x01, offset: 0x09, mask: 0x80}, + rate: {hidReport: InputReport0x02, offset: 0x0F}, + leftEncoder: {hidReport: InputReport0x02, offset: 0x01, mask: 0xF0}, + rightEncoder: {hidReport: InputReport0x02, offset: 0x02, mask: 0x0F}, + volume: {hidReport: InputReport0x02, offset: 0x2B}, + gain: {hidReport: InputReport0x02, offset: 0x01, mask: 0x0F}, + jogPress: {hidReport: InputReport0x02, offset: 0x0D}, eq: { - hi: 0x11, - mid: 0x25, - low: 0x27, + hi: {hidReport: InputReport0x02, offset: 0x11}, + mid: {hidReport: InputReport0x02, offset: 0x25}, + low: {hidReport: InputReport0x02, offset: 0x27}, } }); - this.decks[1].registerInputs(InputReport0x01, InputReport0x02, { - gainEncoderPress: [0x0E, 0x10], - shift: [0x0C, 0x80], - sync: [0x0C, 0x40], - cue: [0x0C, 0x20], - play: [0x0C, 0x10], + this.decks[1].registerInputs({ + gainEncoderPress: {hidReport: InputReport0x01, offset: 0x0E, mask: 0x10}, + shift: {hidReport: InputReport0x01, offset: 0x0C, mask: 0x80}, + sync: {hidReport: InputReport0x01, offset: 0x0C, mask: 0x40}, + cue: {hidReport: InputReport0x01, offset: 0x0C, mask: 0x20}, + play: {hidReport: InputReport0x01, offset: 0x0C, mask: 0x10}, pads: [ - [0x0C, 0x08], - [0x0C, 0x04], - [0x0C, 0x02], - [0x0C, 0x01], + {hidReport: InputReport0x01, offset: 0x0C, mask: 0x08}, + {hidReport: InputReport0x01, offset: 0x0C, mask: 0x04}, + {hidReport: InputReport0x01, offset: 0x0C, mask: 0x02}, + {hidReport: InputReport0x01, offset: 0x0C, mask: 0x01}, ], - loopIn: [0x0B, 0x40], - loopOut: [0x0B, 0x20], - samples: [0x0B, 0x01], - reset: [0x0B, 0x10], - leftEncoderPress: [0x0E, 0x20], - rightEncoderPress: [0x0E, 0x40], - jogWheel: 0x05, - loadTrack: [0x0B, 0x04], - pfl: [0x0B, 0x80], - rate: 0x1F, - leftEncoder: [0x03, 0xF0], - rightEncoder: [0x04, 0x0F], - volume: 0x2D, - gain: [0x03, 0x0F], - jogPress: 0x1D, + loopIn: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x40}, + loopOut: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x20}, + samples: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x01}, + reset: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x10}, + leftEncoderPress: {hidReport: InputReport0x01, offset: 0x0E, mask: 0x20}, + rightEncoderPress: {hidReport: InputReport0x01, offset: 0x0E, mask: 0x40}, + jogWheel: {hidReport: InputReport0x01, offset: 0x05}, + loadTrack: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x04}, + pfl: {hidReport: InputReport0x01, offset: 0x0B, mask: 0x80}, + rate: {hidReport: InputReport0x02, offset: 0x1F}, + leftEncoder: {hidReport: InputReport0x02, offset: 0x03, mask: 0xF0}, + rightEncoder: {hidReport: InputReport0x02, offset: 0x04, mask: 0x0F}, + volume: {hidReport: InputReport0x02, offset: 0x2D}, + gain: {hidReport: InputReport0x02, offset: 0x03, mask: 0x0F}, + jogPress: {hidReport: InputReport0x02, offset: 0x1D}, eq: { - hi: 0x21, - mid: 0x23, - low: 0x29, + hi: {hidReport: InputReport0x02, offset: 0x21}, + mid: {hidReport: InputReport0x02, offset: 0x23}, + low: {hidReport: InputReport0x02, offset: 0x29}, } }); - this.effectUnits[0].registerInputs(InputReport0x01, InputReport0x02, { - focus: [0x09, 0x08], - mix: 0x0B, + this.effectUnits[0].registerInputs({ + focus: {hidReport: InputReport0x01, offset: 0x09, mask: 0x08}, + mix: {hidReport: InputReport0x02, offset: 0x0B}, params: [ - {button: [0x09, 0x04], knob: 0x09}, - {button: [0x09, 0x02], knob: 0x07}, - {button: [0x09, 0x01], knob: 0x05}, + { + button: {hidReport: InputReport0x01, offset: 0x09, mask: 0x04}, + knob: {hidReport: InputReport0x02, offset: 0x09}, + }, + { + button: {hidReport: InputReport0x01, offset: 0x09, mask: 0x02}, + knob: {hidReport: InputReport0x02, offset: 0x07}, + }, + { + button: {hidReport: InputReport0x01, offset: 0x09, mask: 0x01}, + knob: {hidReport: InputReport0x02, offset: 0x05}, + }, ], - channel1: [0x0A, 0x02], - channel2: [0x0A, 0x08], + channel1: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x02}, + channel2: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x08}, }); - this.effectUnits[1].registerInputs(InputReport0x01, InputReport0x02, { - focus: [0x0A, 0x80], - mix: 0x1B, + this.effectUnits[1].registerInputs({ + focus: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x80}, + mix: {hidReport: InputReport0x02, offset: 0x1B}, params: [ - {button: [0x0A, 0x40], knob: 0x19}, - {button: [0x0A, 0x20], knob: 0x17}, - {button: [0x0A, 0x10], knob: 0x15}, + { + button: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x40}, + knob: {hidReport: InputReport0x02, offset: 0x19}, + }, + { + button: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x20}, + knob: {hidReport: InputReport0x02, offset: 0x17}, + }, + { + button: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x10}, + knob: {hidReport: InputReport0x02, offset: 0x15}, + }, ], - channel1: [0x0A, 0x01], - channel2: [0x0A, 0x04], + channel1: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x01}, + channel2: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x04}, }); InputReport0x01.addControl("[Master]", "!browse_encoder_press", 0x0E, "B", 0x08, false, this.browseEncoderPress.bind(this)); @@ -1147,65 +1167,89 @@ class TraktorS2MK1Class { registerOutputPackets() { const OutputReport0x80 = new HIDPacket("OutputReport0x80", 0x80); - this.decks[0].registerOutputs(OutputReport0x80, { - trackLoaded: 0x1F, - vuMeter: 0x15, - peak: 0x01, - reset: 0x06, - loopIn: 0x02, - loopOut: 0x05, - pfl: 0x20, - samples: 0x35, - shift: 0x08, - sync: 0x04, - cue: 0x07, - play: 0x03, + this.decks[0].registerOutputs({ + trackLoaded: {hidReport: OutputReport0x80, offset: 0x1F}, + vuMeter: {hidReport: OutputReport0x80, offset: 0x15}, + peak: {hidReport: OutputReport0x80, offset: 0x01}, + reset: {hidReport: OutputReport0x80, offset: 0x06}, + loopIn: {hidReport: OutputReport0x80, offset: 0x02}, + loopOut: {hidReport: OutputReport0x80, offset: 0x05}, + pfl: {hidReport: OutputReport0x80, offset: 0x20}, + samples: {hidReport: OutputReport0x80, offset: 0x35}, + shift: {hidReport: OutputReport0x80, offset: 0x08}, + sync: {hidReport: OutputReport0x80, offset: 0x04}, + cue: {hidReport: OutputReport0x80, offset: 0x07}, + play: {hidReport: OutputReport0x80, offset: 0x03}, pads: [ - {green: 0x0C, blue: 0x10}, - {green: 0x0B, blue: 0x0F}, - {green: 0x0A, blue: 0x0E}, - {green: 0x09, blue: 0x0D}, + { + green: {hidReport: OutputReport0x80, offset: 0x0C}, + blue: {hidReport: OutputReport0x80, offset: 0x10}, + }, + { + green: {hidReport: OutputReport0x80, offset: 0x0B}, + blue: {hidReport: OutputReport0x80, offset: 0x0F}, + }, + { + green: {hidReport: OutputReport0x80, offset: 0x0A}, + blue: {hidReport: OutputReport0x80, offset: 0x0E}, + }, + { + green: {hidReport: OutputReport0x80, offset: 0x09}, + blue: {hidReport: OutputReport0x80, offset: 0x0D}, + }, ], }); - this.decks[1].registerOutputs(OutputReport0x80, { - trackLoaded: 0x1E, - vuMeter: 0x11, - peak: 0x25, - reset: 0x26, - loopIn: 0x22, - loopOut: 0x21, - pfl: 0x1D, - samples: 0x34, - shift: 0x28, - sync: 0x24, - cue: 0x27, - play: 0x23, + this.decks[1].registerOutputs({ + trackLoaded: {hidReport: OutputReport0x80, offset: 0x1E}, + vuMeter: {hidReport: OutputReport0x80, offset: 0x11}, + peak: {hidReport: OutputReport0x80, offset: 0x25}, + reset: {hidReport: OutputReport0x80, offset: 0x26}, + loopIn: {hidReport: OutputReport0x80, offset: 0x22}, + loopOut: {hidReport: OutputReport0x80, offset: 0x21}, + pfl: {hidReport: OutputReport0x80, offset: 0x1D}, + samples: {hidReport: OutputReport0x80, offset: 0x34}, + shift: {hidReport: OutputReport0x80, offset: 0x28}, + sync: {hidReport: OutputReport0x80, offset: 0x24}, + cue: {hidReport: OutputReport0x80, offset: 0x27}, + play: {hidReport: OutputReport0x80, offset: 0x23}, pads: [ - {green: 0x2C, blue: 0x30}, - {green: 0x2B, blue: 0x2F}, - {green: 0x2A, blue: 0x2E}, - {green: 0x29, blue: 0x2D}, + { + green: {hidReport: OutputReport0x80, offset: 0x2C}, + blue: {hidReport: OutputReport0x80, offset: 0x30}, + }, + { + green: {hidReport: OutputReport0x80, offset: 0x2B}, + blue: {hidReport: OutputReport0x80, offset: 0x2F}, + }, + { + green: {hidReport: OutputReport0x80, offset: 0x2A}, + blue: {hidReport: OutputReport0x80, offset: 0x2E}, + }, + { + green: {hidReport: OutputReport0x80, offset: 0x29}, + blue: {hidReport: OutputReport0x80, offset: 0x2D}, + }, ], }); - this.effectUnits[0].registerOutputs(OutputReport0x80, { - focus: 0x1C, + this.effectUnits[0].registerOutputs({ + focus: {hidReport: OutputReport0x80, offset: 0x1C}, params: [ - 0x1B, - 0x1A, - 0x19, + {hidReport: OutputReport0x80, offset: 0x1B}, + {hidReport: OutputReport0x80, offset: 0x1A}, + {hidReport: OutputReport0x80, offset: 0x19}, ], - channel1: 0x3D, - channel2: 0x3B, + channel1: {hidReport: OutputReport0x80, offset: 0x3D}, + channel2: {hidReport: OutputReport0x80, offset: 0x3B}, }); - this.effectUnits[1].registerOutputs(OutputReport0x80, { - focus: 0x39, + this.effectUnits[1].registerOutputs({ + focus: {hidReport: OutputReport0x80, offset: 0x39}, params: [ - 0x38, - 0x37, - 0x36, + {hidReport: OutputReport0x80, offset: 0x38}, + {hidReport: OutputReport0x80, offset: 0x37}, + {hidReport: OutputReport0x80, offset: 0x36}, ], - channel1: 0x3C, - channel2: 0x3A, + channel1: {hidReport: OutputReport0x80, offset: 0x3C}, + channel2: {hidReport: OutputReport0x80, offset: 0x3A}, }); OutputReport0x80.addOutput("[Master]", "!warninglight", 0x33, "B"); From 70dc1ce29a5e565126ca5317748a231f9889146a Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 13 Sep 2023 19:58:56 +0200 Subject: [PATCH 13/16] Traktor S2 Mk1: Remove console.log statements --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 1a05845f242..e3e51917b05 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -141,8 +141,6 @@ class DeckClass { config.hidReport.addControl(this.channel, name, config.offset, "B", config.mask, false, callback); } registerLed(name, config) { - console.log(name); - console.log(config.hidReport); config.hidReport.addOutput(this.channel, name, config.offset, "B"); } linkLed(name, callback) { From 905f8baef35b70fd900ac65a8456040cedb28a9f Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 13 Sep 2023 19:59:22 +0200 Subject: [PATCH 14/16] Traktor S2 Mk1: Fix missing rename. --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index e3e51917b05..6257e381a4f 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -118,8 +118,8 @@ class DeckClass { this.eq.calibrate(calibration.eq); } enableSoftTakeover() { - engine.softTakeover(this.group, "rate", true); - engine.softTakeover(this.group, "volume", true); + engine.softTakeover(this.channel, "rate", true); + engine.softTakeover(this.channel, "volume", true); this.eq.enableSoftTakeover(); } registerButton(name, config, callback) { From 3d8e0438978e2101ab4090a65aab7d40be4e17d2 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 13 Sep 2023 20:00:07 +0200 Subject: [PATCH 15/16] Traktor S2 Mk1: Remove superfluous arguments --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 6257e381a4f..9357c618968 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -320,7 +320,7 @@ class DeckClass { } engine.scratchTick(this.number, tickDelta); } else { - const velocity = this.scalerJog(tickDelta, timeDelta, this.channel); + const velocity = this.scalerJog(tickDelta, timeDelta); engine.setValue(this.channel, "jog", velocity); } } From 19535a2d173dc8391476a54066e1581f43a67590 Mon Sep 17 00:00:00 2001 From: Jakob Leifhelm Date: Wed, 13 Sep 2023 20:02:22 +0200 Subject: [PATCH 16/16] Traktor S2 Mk1: Use NaN as initial value --- res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index 9357c618968..b1185937c38 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -51,7 +51,7 @@ class DeckClass { this.eq = new Equalizer(this); this.lastTickTime = 0; this.lastTickValue = 0; - this.syncEnabledTime = {}; + this.syncEnabledTime = NaN; this.calibration = null; } registerInputs(config) {