diff --git a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js index e209ae1b3e6..b1185937c38 100644 --- a/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S2-MK1-hid-scripts.js @@ -1,1364 +1,1567 @@ /****************************************************************/ /* 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 */ /* but feel free to tweak this to your heart's content! */ /****************************************************************/ -// ==== Jog Wheel Touch Calibration ==== -// Set the threshold for scratching for each jog wheel. -// If it is always scratching increase the value -// If it never scratches decrease the value -// Bigger values mean more force necessary for it to scratch. -// The unpressed value is around 3100. -// The fully pressed value is around 3700. -var JogWheelTouchThreshold = { - "[Channel1]": 3328, - "[Channel2]": 3328, -}; - // ==== Friendly User Configuration ==== // The Cue button, when Shift is also held, can have two possible functions: // 1. "REWIND": seeks to the very start of the track. // 2. "REVERSEROLL": performs a temporary reverse or "censor" effect, where the track // is momentarily played in reverse until the button is released. -var ShiftCueButtonAction = "REWIND"; +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.gainEncoder = new Encoder(); + this.leftEncoder = new Encoder(); + this.rightEncoder = new Encoder(); + this.wheelPressInertiaTimer = 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 = NaN; + this.calibration = null; + } + registerInputs(config) { + // InputReport 0x01 + 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(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("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("!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(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; + 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)); + } + calibrate(calibration) { + this.calibration = calibration; + this.eq.calibrate(calibration.eq); + } + enableSoftTakeover() { + engine.softTakeover(this.channel, "rate", true); + engine.softTakeover(this.channel, "volume", true); + this.eq.enableSoftTakeover(); + } + registerButton(name, config, callback) { + if (callback !==undefined) { + callback = callback.bind(this); } - if (field.mapped_callback) { - var value = engine.getValue(coGroupName, field.name); - field.mapped_callback(value, coGroupName, field.name); + config.hidReport.addControl(this.channel, name, config.offset, "B", config.mask, false, callback); + } + registerScalar(name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); } - // No callback, no light! + config.hidReport.addControl(this.channel, name, config.offset, "H", 0xFFFF, false, callback); } -}; - -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"); + registerEncoder(name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); + } + config.hidReport.addControl(this.channel, name, config.offset, "B", config.mask, false, callback); } - - this.batchingLEDUpdate = false; - // And now send them all. - for (packetName in this.controller.OutputPackets) { - this.controller.OutputPackets[packetName].send(); + registerLed(name, config) { + config.hidReport.addOutput(this.channel, name, config.offset, "B"); } -}; - -TraktorS2MK1.init = function() { - if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) { - throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" + - "ShiftCueButtonAction is: " + ShiftCueButtonAction); + linkLed(name, callback) { + engine.makeConnection(this.channel, name, callback.bind(this)); } - 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); + volume(field) { + setFaderParameter(this.channel, "volume", field.value, this.calibration.volume); } - 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); + 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; + } } - if (ButtonBrightnessOn < ButtonBrightnessOff) { - throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" + - "ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" + - "ButtonBrightnessOff is: " + ButtonBrightnessOff); + shift(field) { + const shiftPressed = field.value > 0; + this.shiftPressed = shiftPressed; + this.controller.setOutput(this.channel, "!shift", + shiftPressed ? ButtonBrightnessOn : ButtonBrightnessOff, + !this.parent.batchingLEDUpdate); } + syncButton(field) { + const now = Date.now(); - 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(); + // 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); + } + } } - - 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; + 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); + } } - 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; + 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 finishJogPress 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.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); + loopInButton(field) { + engine.setValue(this.channel, "loop_in", field.value); } -}; -TraktorS2MK1.samplerGainKnob = function(field) { - for (var i = 1; i <= 8; i++) { - engine.setParameter("[Sampler" + i + "]", "pregain", field.value / 4095); + loopOutButton(field) { + engine.setValue(this.channel, "loop_out", field.value); } -}; -TraktorS2MK1.toggleButton = function(field) { - if (field.value > 0) { - script.toggleControl(field.group, field.name); + 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.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); + introOutroModeButton(field) { + if (field.value === 0) { + return; + } + 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 { + this.setPadMode(padModes.hotcue); + this.controller.setOutput(this.channel, "!reset_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]) { + 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; } - 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 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 (!engine.getValue(field.group, "sync_enabled")) { - // If disabled, and switching to disable... stay disabled. - engine.setValue(field.group, "sync_enabled", 0); - return; + if (loopEnabled) { + script.triggerControl(this.channel, "reloop_toggle"); + } else { + script.triggerControl(this.channel, "beatloop_activate"); } - // 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; + } + } + loadTrackButton(field) { + if (this.shiftPressed) { + engine.setValue(this.channel, "CloneFromDeck", 0); + } else { + engine.setValue(this.channel, "LoadSelectedTrack", field.value); + } + } + pflButton(field) { + if (field.value > 0) { + if (this.shiftPressed) { + script.toggleControl(this.channel, "quantize"); + } else { + script.toggleControl(this.channel, "pfl"); } - engine.setValue(field.group, "sync_enabled", 0); } } -}; + jogMove(field) { + const deltas = this.wheelDeltas(field.value); + let tickDelta = deltas[0]; + const timeDelta = deltas[1]; -TraktorS2MK1.cueButton = function(field) { - if (TraktorS2MK1.shiftPressed[field.group]) { - if (ShiftCueButtonAction === "REWIND") { - if (field.value === 0) { - return; + if (engine.getValue(this.channel, "scratch2_enable")) { + if (this.shiftPressed) { + tickDelta *= 10; } - engine.setValue(field.group, "start_stop", 1); - } else if (ShiftCueButtonAction === "REVERSEROLL") { - engine.setValue(field.group, "reverseroll", field.value); + engine.scratchTick(this.number, tickDelta); + } else { + const velocity = this.scalerJog(tickDelta, timeDelta); + engine.setValue(this.channel, "jog", velocity); } - } else { - engine.setValue(field.group, "cue_default", field.value); } -}; + leftEncoderCallback(field) { + const delta = this.leftEncoder.delta(field.value); -TraktorS2MK1.playButton = function(field) { - if (field.value === 0) { - return; - } - if (TraktorS2MK1.shiftPressed[field.group]) { - var locked = engine.getValue(field.group, "keylock"); - engine.setValue(field.group, "keylock", !locked); - } else { - var playing = engine.getValue(field.group, "play"); - var deckNumber = TraktorS2MK1.controller.resolveDeck(field.group); - // Failsafe to disable scratching in case the finishJogTouch timer has not executed yet - // after a backspin. - if (engine.isScratching(deckNumber)) { - engine.scratchDisable(deckNumber, false); + if (this.shiftPressed) { + if (delta === 1) { + script.triggerControl(this.channel, "pitch_up_small"); + } else if (delta === -1) { + script.triggerControl(this.channel, "pitch_down_small"); + } + } else { + if (this.leftEncoderPressed) { + let beatjumpSize = engine.getValue(this.channel, "beatjump_size"); + if (delta === 1) { + beatjumpSize *= 2; + } else if (delta === -1) { + beatjumpSize /= 2; + } + engine.setValue(this.channel, "beatjump_size", beatjumpSize); + } else { + if (delta === 1) { + script.triggerControl(this.channel, "beatjump_forward"); + } else if (delta === -1) { + script.triggerControl(this.channel, "beatjump_backward"); + } + } } - engine.setValue(field.group, "play", !playing); } -}; + rightEncoderCallback(field) { + const delta = this.rightEncoder.delta(field.value); -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; + if (this.shiftPressed) { + if (delta === 1) { + script.triggerControl(this.channel, "beatjump_1_forward"); + } else if (delta === -1) { + script.triggerControl(this.channel, "beatjump_1_backward"); + } } else { - inertiaTime = Math.pow(1.7, scratchRate) / 1.6; + if (delta === 1) { + script.triggerControl(this.channel, "loop_double"); + } else if (delta === -1) { + script.triggerControl(this.channel, "loop_halve"); + } } - if (inertiaTime < 100) { - // Just do it now. - TraktorS2MK1.finishJogTouch(field.group); + } + gainEncoderCallback(field) { + const delta = 0.03333 * this.gainEncoder.delta(field.value); + + if (this.shiftPressed) { + const currentPregain = engine.getParameter(this.channel, "pregain"); + engine.setParameter(this.channel, "pregain", currentPregain + delta); } else { - TraktorS2MK1.wheelTouchInertiaTimer[field.group] = engine.beginTimer( - inertiaTime, function() { - TraktorS2MK1.finishJogTouch(field.group); - }, true); + const quickEffectGroup = "[QuickEffectRack1_" + this.channel + "]"; + if (this.gainEncoderPressed) { + 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); + } } } -}; - -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); + jogPress(field) { + if (this.wheelPressInertiaTimer !== 0) { + // The wheel was touched again, reset the timer. + engine.stopTimer(this.wheelPressInertiaTimer); + this.wheelPressInertiaTimer = 0; + } + if (field.value > this.calibration.jogPress.pressed) { + engine.scratchEnable(this.number, 1024, 33.3333, 0.125, 0.125/8, true); } else { - // Check again soon. - TraktorS2MK1.wheelTouchInertiaTimer[group] = engine.beginTimer( - 1, function() { - TraktorS2MK1.finishJogTouch(group); - }, true); + // 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.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 + // 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.finishJogPress(); + } else { + this.wheelTouchInertiaTimer = engine.beginTimer( + inertiaTime, + this.finishJogPress.bind(this) + , true); + } } } -}; - -TraktorS2MK1.jogMove = function(field) { - var deltas = TraktorS2MK1.wheelDeltas(field.group, field.value); - var tickDelta = deltas[0]; - var timeDelta = deltas[1]; - - if (engine.getValue(field.group, "scratch2_enable")) { - var deckNumber = TraktorS2MK1.controller.resolveDeck(field.group); - if (TraktorS2MK1.shiftPressed[field.group]) { - tickDelta *= 10; + outputCallback(value, group, key) { + let ledValue = ButtonBrightnessOff; + if (value) { + ledValue = ButtonBrightnessOn; } - engine.scratchTick(deckNumber, tickDelta); - } else { - var velocity = TraktorS2MK1.scalerJog(tickDelta, timeDelta, field.group); - engine.setValue(field.group, "jog", velocity); + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); } -}; + outputCallbackLoop(value, group, key) { + let ledValue = ButtonBrightnessOff; + if (engine.getValue(group, "loop_enabled")) { + ledValue = 0x1F; + } + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); + } + outputCallbackDark(value, group, key) { + let ledValue = 0x00; + if (value) { + ledValue = 0x1F; + } + this.controller.setOutput(group, key, ledValue, !this.parent.batchingLEDUpdate); + } + onVuMeterChanged(value, group, _key) { + // This handler is called a lot so it should be as fast as possible. -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; + // Figure out number of fully-illuminated segments. + const scaledValue = value * 4.0; + const fullIllumCount = Math.floor(scaledValue); + + // Figure out how much the partially-illuminated segment is illuminated. + const partialIllum = (scaledValue - fullIllumCount) * 0x1F; + + 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(); } - if (previousTime > timeValue) { - // We looped around. Adjust current time so that subtraction works. - timeValue += 0x10000; + onLoopEnabledChanged(value, group, _key) { + this.outputCallbackLoop(value, group, "loop_in"); + this.outputCallbackLoop(value, group, "loop_out"); } - var timeDelta = timeValue - previousTime; - if (timeDelta === 0) { - // Spinning too fast to detect speed! By not dividing we are guessing it took 1ms. - timeDelta = 1; + 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; + } - 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; + let 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; + } + return [tickDelta, timeDelta]; } - //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; + scalerJog(tickDelta, timeDelta) { + if (engine.getValue(this.channel, "play")) { + return (tickDelta / timeDelta) / 3; + } else { + return (tickDelta / timeDelta) * 2.0; + } } -}; - -var introOutroKeys = [ - "intro_start", - "intro_end", - "outro_start", - "outro_end" -]; - -var introOutroColors = [ - {green: 0x1F, blue: 0}, - {green: 0x1F, blue: 0}, - {green: 0, blue: 0x1F}, - {green: 0, blue: 0x1F} -]; - -var introOutroColorsDim = [ - {green: 0x05, blue: 0}, - {green: 0x05, blue: 0}, - {green: 0, blue: 0x05}, - {green: 0, blue: 0x05} -]; - -TraktorS2MK1.setPadMode = function(group, padMode) { - TraktorS2MK1.padConnections[group].forEach(function(connection) { - connection.disconnect(); - }); - TraktorS2MK1.padConnections[group] = []; - - if (padMode === TraktorS2MK1.padModes.hotcue) { - for (var i = 1; i <= 4; i++) { - TraktorS2MK1.padConnections[group].push( - engine.makeConnection(group, "hotcue_" + i + "_enabled", TraktorS2MK1.outputHotcueCallback)); - } - } else if (padMode === TraktorS2MK1.padModes.introOutro) { - for (i = 1; i <= 4; i++) { - // This function to create callback functions is needed so the loop index variable - // i does not get captured in a closure within the callback. - var makeIntroOutroCallback = function(padNumber) { - return function(value, group, _control) { - if (value > 0) { - TraktorS2MK1.sendPadColor(group, padNumber, introOutroColors[padNumber-1]); - } else { - TraktorS2MK1.sendPadColor(group, padNumber, introOutroColorsDim[padNumber-1]); - } - }; - }; - TraktorS2MK1.padConnections[group].push(engine.makeConnection( - group, introOutroKeys[i-1] + "_enabled", makeIntroOutroCallback(i))); - } - } else if (padMode === TraktorS2MK1.padModes.sampler) { - for (i = 1; i <= 4; i++) { - var makeSamplerCallback = function(deckGroup, padNumber) { - var samplerNumber = deckGroup === "[Channel1]" ? padNumber : padNumber + 4; - var samplerGroup = "[Sampler" + samplerNumber + "]"; - return function(_value, _group, _control) { - if (engine.getValue(samplerGroup, "track_loaded")) { - if (engine.getValue(samplerGroup, "play") === 1) { - if (engine.getValue(samplerGroup, "repeat") === 1) { - TraktorS2MK1.sendPadColor(deckGroup, padNumber, - {green: 0x1F, blue: 0x1F}); - } else { - TraktorS2MK1.sendPadColor(deckGroup, padNumber, - {green: 0x1F, blue: 0}); - } - } else { - TraktorS2MK1.sendPadColor(deckGroup, padNumber, {green: 0x05, blue: 0x00}); - } - } else { - TraktorS2MK1.sendPadColor(deckGroup, padNumber, {green: 0, blue: 0}); - } - }; - }; - - var sNumber = group === "[Channel1]" ? i : i + 4; - var sGroup = "[Sampler" + sNumber + "]"; - TraktorS2MK1.padConnections[group].push(engine.makeConnection( - sGroup, "track_loaded", makeSamplerCallback(group, i))); - TraktorS2MK1.padConnections[group].push(engine.makeConnection( - sGroup, "play", makeSamplerCallback(group, i))); - TraktorS2MK1.padConnections[group].push(engine.makeConnection( - sGroup, "repeat", makeSamplerCallback(group, i))); + finishJogPress() { + this.wheelPressInertiaTimer = 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.wheelPressInertiaTimer = engine.beginTimer(20, this.finishJogPress.bind(this), true); + } } } +} - 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); +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 = []; } -}; - -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); + registerInputs(config) { + config.hidReport.addControl(this.deck.channel, "!pad" + this.number, config.offset, "B", config.mask, false, this.pressHandler.bind(this)); } -}; + registerOutputs(config) { + this.registerLed("!pad_" + this.number + "_G", config.green); + this.registerLed("!pad_" + this.number + "_B", config.blue); + } + registerLed(name, config) { + config.hidReport.addOutput(this.deck.channel, name, config.offset, "B"); + } + pressHandler(field) { + const padMode = this.deck.currentPadMode; -TraktorS2MK1.samplerButton = function(buttonNumber, group, value) { - if (value === 0) { - return; + 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); + } } - 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); + hotcueButton(value) { + if (this.deck.shiftPressed) { + engine.setValue(this.deck.channel, "hotcue_" + this.number + "_clear", value); } else { - script.triggerControl(samplerGroup, "eject"); + engine.setValue(this.deck.channel, "hotcue_" + this.number + "_activate", value); } - } else { - if (engine.getValue(samplerGroup, "track_loaded") === 0) { - script.triggerControl(samplerGroup, "LoadSelectedTrack"); + } + introOutroButton(value) { + if (this.deck.shiftPressed) { + engine.setValue(this.deck.channel, introOutroKeys[this.number-1] + "_clear", value); } else { - script.triggerControl(samplerGroup, "cue_gotoandplay"); + engine.setValue(this.deck.channel, introOutroKeys[this.number-1] + "_activate", value); } } -}; - -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); + 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"); + } + } } -}; - -TraktorS2MK1.samplerModeButton = function(field) { - if (field.value === 0) { - return; + 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); } - 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); + outputIntroOutroCallback(value) { + if (value > 0) { + this.sendPadColor(introOutroColors[this.number-1]); + } else { + this.sendPadColor(introOutroColorsDim[this.number-1]); + } } -}; - -TraktorS2MK1.introOutroModeButton = function(field) { - if (field.value === 0) { - return; + 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}); + } + } else { + this.sendPadColor({green: 0, blue: 0}); + } } - 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); + 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; -TraktorS2MK1.loopInButton = function(field) { - engine.setValue(field.group, "loop_in", field.value); -}; - -TraktorS2MK1.loopOutButton = function(field) { - engine.setValue(field.group, "loop_out", field.value); -}; + 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))); + } -// 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); + this.connections.forEach(function(connection) { + connection.trigger(); + }); + } +} + +class Equalizer { + constructor(deck) { + 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), }; - }; - - // FIXME: Why do the LEDs flicker? - TraktorS2MK1.batchingLEDUpdate = true; - for (var i = 0; i <= 2; i++) { - var effectGroup; - var key; - if (focusedEffect === 0) { - effectGroup = effectUnitGroup.slice(0, -1) + "_Effect" + (i+1) + "]"; - key = "enabled"; - } else { - effectGroup = effectUnitGroup.slice(0, -1) + "_Effect" + focusedEffect + "]"; - key = "button_parameter" + (i+1); - } - TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup][i] = engine.makeConnection( - effectGroup, key, makeButtonLEDcallback(i+1)); - TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup][i].trigger(); } - TraktorS2MK1.batchingLEDUpdate = false; - TraktorS2MK1.effectButtonLEDconnections[effectUnitGroup][2].trigger(); -}; - -// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this. -TraktorS2MK1.onShowParametersChange = function(value, group, _control) { - if (value === 0) { - if (engine.getValue(group, "show_focus") > 0) { - engine.setValue(group, "show_focus", 0); - TraktorS2MK1.previouslyFocusedEffect[group] = engine.getValue(group, "focused_effect"); - engine.setValue(group, "focused_effect", 0); + registerInputs(config) { + for (const param in this.params) { + this.params[param].registerInputs(config[param]); } - } else { - engine.setValue(group, "show_focus", 1); - if (TraktorS2MK1.previouslyFocusedEffect[group] !== null) { - engine.setValue(group, "focused_effect", TraktorS2MK1.previouslyFocusedEffect[group]); + } + calibrate(calibration) { + for (const param in this.params) { + this.params[param].calibrate(calibration[param]); } } - 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); - } + enableSoftTakeover() { + for (const param in this.params) { + this.params[param].enableSoftTakeover(); } - } else { - for (i = 1; i <= 2; i++) { - engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "meta"); + } +} +class EqualizerParameter { + constructor(equalizer, number) { + this.equalizer = equalizer; + this.group = equalizer.group; + this.number = number; + this.calibration = null; + } + registerInputs(config) { + this.registerKnob("!parameter" + this.number, config, this.knob); + } + calibrate(calibration) { + this.calibration = calibration; + } + enableSoftTakeover() { + engine.softTakeover(this.group, "parameter" + this.number, true); + } + 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); + } +} + +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), + ]; + this.calibration = null; + } + 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(config.params[i]); } } -}; - -// 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; + 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(config.params[i]); } - TraktorS2MK1.effectFocusLongPressTimer[field.group] = engine.beginTimer(TraktorS2MK1.longPressTimeoutMilliseconds, function() { - TraktorS2MK1.effectFocusChooseModeActive[field.group] = true; - TraktorS2MK1.effectButtonLEDconnections[field.group].forEach(function(connection) { - connection.disconnect(); - }); - var makeButtonLEDcallback = function(buttonNumber) { - return function(value, group, _control) { - TraktorS2MK1.controller.setOutput(group, "!effectbutton" + buttonNumber, - value === buttonNumber ? ButtonBrightnessOn : ButtonBrightnessOff, !TraktorS2MK1.batchingLEDUpdate); - }; - }; - TraktorS2MK1.batchingLEDUpdate = true; - for (var i = 0; i <= 2; i++) { - TraktorS2MK1.effectButtonLEDconnections[i] = engine.makeConnection( - field.group, "focused_effect", makeButtonLEDcallback(i+1)); - TraktorS2MK1.effectButtonLEDconnections[i].trigger(); - } - TraktorS2MK1.batchingLEDUpdate = false; - TraktorS2MK1.effectButtonLEDconnections[2].trigger(); - }); - if (!showParameters) { - engine.setValue(field.group, "show_parameters", 1); - TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group] = true; + } + 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(); + } + calibrate(calibration) { + this.calibration = calibration.mix; + for (let i = 0; i < 3; i++) { + this.params[i].calibrate(calibration.params[i]); } - } else { - if (TraktorS2MK1.effectFocusLongPressTimer[field.group] !== 0) { - engine.stopTimer(TraktorS2MK1.effectFocusLongPressTimer[field.group]); - TraktorS2MK1.effectFocusLongPressTimer[field.group] = 0; + } + enableSoftTakeover() { + engine.softTakeover(this.group, "!mix", true); + for (let i = 0; i < 3; i++) { + this.params[i].enableSoftTakeover(); } - - 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); + } + registerButton(name, config, callback) { + if (callback !==undefined) { + callback= callback.bind(this); } - - TraktorS2MK1.effectFocusButtonPressedWhenParametersHidden[field.group] = false; + config.hidReport.addControl(this.group, name, config.offset, "B", config.mask, false, callback); } -}; - -// 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); + registerKnob(name, config, callback) { + config.hidReport.addControl(this.group, name, config.offset, "H", 0xFFFF, false, callback.bind(this)); } -}; + registerLed(name, config) { + config.hidReport.addOutput(this.group, name, config.offset, "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, "loaded_chain_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) { + setKnobParameter(this.group, "mix", field.value, this.calibration); + } + 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; - - if (TraktorS2MK1.shiftPressed[field.group]) { - var currentPregain = engine.getParameter(field.group, "pregain"); - engine.setParameter(field.group, "pregain", currentPregain + delta); - } else { - var quickEffectGroup = "[QuickEffectRack1_" + field.group + "]"; - if (TraktorS2MK1.gainEncoderPressed[field.group]) { - script.triggerControl(quickEffectGroup, delta > 0 ? "next_chain" : "prev_chain"); - } else { - var currentQuickEffectSuperKnob = engine.getParameter(quickEffectGroup, "super1"); - engine.setParameter(quickEffectGroup, "super1", currentQuickEffectSuperKnob + delta); + 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; + this.calibration = null; + } + registerInputs(config) { + this.registerButton("!effectbutton" + this.number, config.button, this.effectButton); + this.registerKnob("!effectknob" + this.number, config.knob, this.effectKnob); + } + registerOutputs(config) { + config.hidReport.addOutput(this.group, "!effectbutton" + this.number, config.offset, "B"); + } + 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(name, config, callback) { + config.hidReport.addControl(this.group, name, config.offset, "B", config.mask, 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"); -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"); + if (field.value > 0) { + if (this.effectUnit.shiftPressed()) { + engine.setValue(this.group, "loaded_chain_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 { - script.triggerControl("[QuickEffectRack1_" + field.group + "]", "super1_set_default"); + engine.stopTimer(this.longPressTimer); + this.longPressTimer = 0; + if (this.isLongPressed) { + this.toggle(); + } + this.isLongPressed = false; } - } 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"); + toggle() { + const button = this.getButtonGroupAndKey(); + script.toggleControl(button.group, button.key); + } + effectKnob(field) { + const knob = this.getKnobGroupAndKey(); + setKnobParameter(knob.group, knob.key, field.value, this.calibration); + } + 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; - } - engine.setValue(field.group, "beatjump_size", beatjumpSize); + } + getKnobGroupAndKey() { + const focusedEffect = engine.getValue(this.group, "focused_effect"); + if (focusedEffect === 0) { + return { + group: this.groupPrefix + "_Effect" + this.number + "]", + key: "meta", + }; } else { - if (delta === 1) { - script.triggerControl(field.group, "beatjump_forward"); - } else { - script.triggerControl(field.group, "beatjump_backward"); - } + return { + group: this.groupPrefix + "_Effect" + focusedEffect + "]", + key: "parameter" + this.number, + }; } } -}; + 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; + + this.browseEncoder = new Encoder(); + + this.decks = [ + new DeckClass(this, 1), + new DeckClass(this, 2), + ]; + this.effectUnits = [ + 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. + // 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({ + 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: [ + {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: {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: {hidReport: InputReport0x02, offset: 0x11}, + mid: {hidReport: InputReport0x02, offset: 0x25}, + low: {hidReport: InputReport0x02, offset: 0x27}, + } + }); + 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: [ + {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: {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: {hidReport: InputReport0x02, offset: 0x21}, + mid: {hidReport: InputReport0x02, offset: 0x23}, + low: {hidReport: InputReport0x02, offset: 0x29}, + } + }); + this.effectUnits[0].registerInputs({ + focus: {hidReport: InputReport0x01, offset: 0x09, mask: 0x08}, + mix: {hidReport: InputReport0x02, offset: 0x0B}, + params: [ + { + 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: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x02}, + channel2: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x08}, + }); + this.effectUnits[1].registerInputs({ + focus: {hidReport: InputReport0x01, offset: 0x0A, mask: 0x80}, + mix: {hidReport: InputReport0x02, offset: 0x1B}, + params: [ + { + 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: {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)); + + 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("[Library]", "!browse", 0x02, "B", 0xF0, false, this.browseEncoderCallback.bind(this)); + + // Soft takeover for knobs + + // Set scalers + this.controller.setScaler("headMix", this.scalerSlider); + this.controller.setScaler("rate", this.scalerSlider); + + // Register packet + this.controller.registerInputPacket(InputReport0x01); + this.controller.registerInputPacket(InputReport0x02); + } + registerOutputPackets() { + const OutputReport0x80 = new HIDPacket("OutputReport0x80", 0x80); + + 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: {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({ + 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: {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({ + focus: {hidReport: OutputReport0x80, offset: 0x1C}, + params: [ + {hidReport: OutputReport0x80, offset: 0x1B}, + {hidReport: OutputReport0x80, offset: 0x1A}, + {hidReport: OutputReport0x80, offset: 0x19}, + ], + channel1: {hidReport: OutputReport0x80, offset: 0x3D}, + channel2: {hidReport: OutputReport0x80, offset: 0x3B}, + }); + this.effectUnits[1].registerOutputs({ + focus: {hidReport: OutputReport0x80, offset: 0x39}, + params: [ + {hidReport: OutputReport0x80, offset: 0x38}, + {hidReport: OutputReport0x80, offset: 0x37}, + {hidReport: OutputReport0x80, offset: 0x36}, + ], + channel1: {hidReport: OutputReport0x80, offset: 0x3C}, + channel2: {hidReport: OutputReport0x80, offset: 0x3A}, + }); -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"); - } -}; + OutputReport0x80.addOutput("[Master]", "!warninglight", 0x33, "B"); -TraktorS2MK1.rightEncoder = function(field) { - var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousRightEncoder[field.group]); - TraktorS2MK1.previousRightEncoder[field.group] = field.value; + this.controller.registerOutputPacket(OutputReport0x80); - if (TraktorS2MK1.shiftPressed[field.group]) { - if (delta === 1) { - script.triggerControl(field.group, "beatjump_1_forward"); - } else { - script.triggerControl(field.group, "beatjump_1_backward"); + 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); + } + 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]); } - } else { - if (delta === 1) { - script.triggerControl(field.group, "loop_double"); - } else { - script.triggerControl(field.group, "loop_halve"); + for (let i = 0; i < 2; i++) { + 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)); + // 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() { + engine.softTakeover("[Master]", "crossfader", true); + engine.softTakeover("[Master]", "headMix", true); + for (let i = 1; i <= 8; i++) { + engine.softTakeover("[Sampler" + i + "]", "pregain", true); + } -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"); + for (let i = 0; i < 2; i++) { + this.decks[i].enableSoftTakeover(); } - } else { - if (loopEnabled) { - script.triggerControl(field.group, "reloop_toggle"); - } else { - script.triggerControl(field.group, "beatloop_activate"); + 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" + + "ShiftCueButtonAction is: " + ShiftCueButtonAction); + } + if (typeof ButtonBrightnessOff !== "number" || ButtonBrightnessOff < 0 || ButtonBrightnessOff > 0x1f) { + throw new Error("ButtonBrightnessOff must be a number between 0 and 0x1f (31).\n" + + "ButtonBrightnessOff is: " + ButtonBrightnessOff); + } + if (typeof ButtonBrightnessOff !== "number" || ButtonBrightnessOff < 0 || ButtonBrightnessOff > 0x1f) { + throw new Error("ButtonBrightnessOn must be a number between 0 and 0x1f (31).\n" + + "ButtonBrightnessOn is: " + ButtonBrightnessOn); + } + if (ButtonBrightnessOn < ButtonBrightnessOff) { + throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" + + "ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" + + "ButtonBrightnessOff is: " + ButtonBrightnessOff); + } -TraktorS2MK1.browseEncoder = function(field) { - var delta = TraktorS2MK1.encoderDirection(field.value, TraktorS2MK1.previousBrowse); - TraktorS2MK1.previousBrowse = field.value; + this.calibrate(); + this.registerInputPackets(); + this.readCurrentPosition(); + this.enableSoftTakeover(); - if (TraktorS2MK1.shiftPressed["[Channel1]"] || TraktorS2MK1.shiftPressed["[Channel2]"]) { - delta *= 5; + 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 { + this.registerOutputPackets(); + } } - engine.setValue("[Playlist]", "SelectTrackKnob", delta); -}; - -TraktorS2MK1.scalerParameter = function(group, name, value) { - return script.absoluteLin(value, 0, 1, 16, 4080); -}; -TraktorS2MK1.scalerVolume = function(group, name, value) { - if (group === "[Master]") { - return script.absoluteNonLin(value, 0, 1, 4, 16, 4080); - } else { - return script.absoluteNonLin(value, 0, 0.25, 1, 16, 4080); + 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); } -}; -TraktorS2MK1.scalerSlider = function(group, name, value) { - return script.absoluteLin(value, -1, 1, 16, 4080); -}; + incomingData(data, length) { + this.controller.parsePacket(data, length); + } + // 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.outputChannelCallback = function(value, group, key) { - var ledValue = 0x05; - if (value) { - ledValue = 0x1F; + 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++) { + setKnobParameter("[Sampler" + i + "]", "pregain", field.value, this.calibration.sampler); + } } - TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate); -}; -TraktorS2MK1.outputChannelCallbackDark = function(value, group, key) { - var ledValue = 0x00; - if (value) { - ledValue = 0x1F; + toggleButton(field) { + if (field.value > 0) { + script.toggleControl(field.group, field.name); + } } - TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate); -}; -TraktorS2MK1.outputCallback = function(value, group, key) { - var ledValue = ButtonBrightnessOff; - if (value) { - ledValue = ButtonBrightnessOn; + browseEncoderCallback(field) { + const delta = this.browseEncoder.delta(field.value); + + if (this.shiftPressed()) { + engine.setValue("[Library]", "ScrollVertical", delta); + } else { + engine.setValue("[Library]", "MoveVertical", delta); + } } - 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; + browseEncoderPress(field) { + if (this.shiftPressed()) { + engine.setValue("[Library]", "MoveFocusBackward", field.value); + } else { + engine.setValue("[Library]", "GoToItem", field.value); + } + + } + crossfader(field) { + setFaderParameter("[Master]", "crossfader", field.value, this.calibration.crossfader); } - TraktorS2MK1.controller.setOutput(group, key, ledValue, !TraktorS2MK1.batchingLEDUpdate); -}; -TraktorS2MK1.outputCallbackDark = function(value, group, key) { - var ledValue = 0x00; - if (value) { - ledValue = 0x1F; + scalerParameter(group, name, value) { + const scaledValue = script.absoluteLin(value, 0, 1, 16, 4080); + return scaledValue; } - 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"); + scalerVolume(group, name, value) { + if (group === "[Master]") { + return script.absoluteNonLin(value, 0, 1, 4, 16, 4080); } else { - script.toggleControl(field.group, "pfl"); + return script.absoluteNonLin(value, 0, 0.25, 1, 16, 4080); } } -}; -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); -}; + scalerSlider(group, name, value) { + return script.absoluteLin(value, -1, 1, 16, 4080); + } + + 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]; + } -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. +class Encoder { + constructor() { + this.previousValue = -1; + } + /// return value 1 === right turn + /// return value -1 === left turn + /// return value 0 when something weird 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; + } +} - // 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); - } +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); } - TraktorS2MK1.controller.OutputPackets.OutputReport0x80.send(); + engine.setParameter(group, key, calibratedValue); }; - -TraktorS2MK1.onLoopEnabledChanged = function(value, group, _key) { - TraktorS2MK1.outputCallbackLoop(value, group, "loop_in"); - TraktorS2MK1.outputCallbackLoop(value, group, "loop_out"); +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 + // # Feature Report Description // // Feature Report `208` (`0xd0`)