Skip to content

Instantly share code, notes, and snippets.

@stotes
Created November 5, 2023 01:26
Show Gist options
  • Save stotes/17e2c7b9f8b2d002ed6358bf8d16f05b to your computer and use it in GitHub Desktop.
Save stotes/17e2c7b9f8b2d002ed6358bf8d16f05b to your computer and use it in GitHub Desktop.
Mixxx 2.5 compatible HID controller mapping scripts for Traktor Kontrol Z1 and X1 MK2
/** Common HID script debugging function. Just to get logging with 'HID' prefix. */
HIDDebug = function (message) {
print("HID " + message)
}
/** HID Bit Vector Class
*
* Collection of bits in one parsed packet field. These objects are
* created by HIDPacket addControl and addOutput and should not be
* created manually. */
HIDBitVector = function () {
this.size = 0
this.bits = {}
}
/** Return bit offset based on bitmask */
HIDBitVector.prototype.getOffset = function (bitmask) {
for (var i = 0; i < 32; i++)
if ((1 & bitmask >> i) != 0)
return i
return 0
}
/** Add a control bitmask to the HIDBitVector */
HIDBitVector.prototype.addBitMask = function (group, name, bitmask) {
var bit = {}
bit.type = "button"
bit.packet = undefined
bit.id = group + "." + name
bit.group = group
bit.name = name
bit.mapped_group = undefined
bit.mapped_name = undefined
bit.bitmask = bitmask
bit.bitmask = bitmask
bit.bit_offset = this.getOffset(bitmask)
bit.callback = undefined
bit.value = undefined
bit.auto_repeat = undefined
bit.auto_repeat_interval = undefined
this.bits[bit.id] = bit
}
/** Add a Output control bitmask to the HIDBitVector */
HIDBitVector.prototype.addOutputMask = function (group, name, bitmask) {
var bit = {}
bit.type = "output"
bit.packet = undefined
bit.id = group + "." + name
bit.group = group
bit.name = name
bit.mapped_group = undefined
bit.mapped_name = undefined
bit.bitmask = bitmask
bit.bit_offset = this.getOffset(bitmask)
bit.callback = undefined
bit.value = undefined
bit.toggle = undefined
this.bits[bit.id] = bit
}
/** HID Modifiers object
*
* Wraps all defined modifiers to one object with uniform API.
* Don't call directly, this is available as HIDController.modifiers */
HIDModifierList = function () {
this.modifiers = Object()
this.callbacks = Object()
}
/** Add a new modifier to controller. */
HIDModifierList.prototype.add = function (name) {
if (name in this.modifiers) {
HIDDebug("Modifier already defined: " + name)
return
}
this.modifiers[name] = undefined
}
/** Set modifier value */
HIDModifierList.prototype.set = function (name, value) {
if ((!name in this.modifiers)) {
HIDDebug("Unknown modifier: " + name)
return
}
this.modifiers[name] = value
if (name in this.callbacks) {
var callback = this.callbacks[name]
callback(value)
}
}
/** Get modifier value */
HIDModifierList.prototype.get = function (name) {
if (!(name in this.modifiers)) {
HIDDebug("Unknown modifier: " + name)
return false
}
return this.modifiers[name]
}
/** Set modifier callback (update function after modifier state changes) */
HIDModifierList.prototype.setCallback = function (name, callback) {
if ((!name in this.modifiers)) {
HIDDebug("Unknonwn modifier: " + name)
return
}
this.callbacks[name] = callback
}
/**
* HID Packet object
*
* One HID input/output packet to register to HIDController
* @param name name of packet
* @param reportId report ID of the packet. If the device only uses
* one report type, this must be 0.
* @param callback function to call when this packet type is input
* and is received. If packet callback is set, the
* packet is not parsed by delta functions.
* callback is not meaningful for output packets
* @param header (optional) list of bytes to match from beginning
* of packet. Do NOT put the report ID in this; use
* the reportId parameter instead. */
HIDPacket = function (name, reportId, callback, header) {
this.name = name
this.header = header
this.callback = callback
this.reportId = 0
if (reportId !== undefined) {
this.reportId = reportId
}
this.groups = {}
// Size of various 'pack' values in bytes
this.packSizes = { b: 1, B: 1, h: 2, H: 2, i: 4, I: 4 }
this.signedPackFormats = ["b", "h", "i"]
}
/** Pack a field value to the packet.
* Can only pack bits and byte values, patches welcome. */
HIDPacket.prototype.pack = function (data, field) {
var value
if (!(field.pack in this.packSizes)) {
HIDDebug("ERROR parsing packed value: invalid pack format " + field.pack)
return
}
var bytes = this.packSizes[field.pack]
var signed = false
if (this.signedPackFormats.indexOf(field.pack) != -1)
signed = true
if (field.type == "bitvector") {
// TODO - fix multi byte bit vector outputs
if (bytes > 1) {
HIDDebug("ERROR: packing multibyte bit vectors not yet supported")
return
}
for (bit_id in field.value.bits) {
var bit = field.value.bits[bit_id]
data[field.offset] = data[field.offset] | bit.value
}
return
}
value = (field.value != undefined) ? field.value : 0
if (value < field.min || value > field.max) {
HIDDebug("ERROR " + field.id + " packed value out of range: " + value)
return
}
for (var byte_index = 0; byte_index < bytes; byte_index++) {
var index = field.offset + byte_index
if (signed) {
if (value >= 0) {
data[index] = (value >> (byte_index * 8)) & 255
} else {
data[index] = 255 - ((-(value + 1) >> (byte_index * 8)) & 255)
}
} else {
data[index] = (value >> (byte_index * 8)) & 255
}
}
}
/** Parse and return field value matching the 'pack' field from field attributes.
* Valid values are:
* b signed byte
* B unsigned byte
* h signed short
* H unsigned short
* i signed integer
* I unsigned integer */
HIDPacket.prototype.unpack = function (data, field) {
var value = 0
if (!(field.pack in this.packSizes)) {
HIDDebug("ERROR parsing packed value: invalid pack format " + field.pack)
return
}
var bytes = this.packSizes[field.pack]
var signed = false
if (this.signedPackFormats.indexOf(field.pack) != -1)
signed = true
for (field_byte = 0; field_byte < bytes; field_byte++) {
if (data[field.offset + field_byte] == 255 && field_byte == 4)
value += 0
else
value += data[field.offset + field_byte] * Math.pow(2, (field_byte * 8))
}
if (signed) {
var max_value = Math.pow(2, bytes * 8)
var split = max_value / 2 - 1
if (value > split)
value = value - max_value
}
return value
}
/** Find HID packet group matching name.
* Create group if create is true */
HIDPacket.prototype.getGroup = function (name, create) {
if (this.groups == undefined)
this.groups = {}
if (name in this.groups)
return this.groups[name]
if (!create)
return undefined
this.groups[name] = {}
return this.groups[name]
}
/** Lookup HID packet field matching given offset and pack type
* Returns undefined if no patching field can be found. */
HIDPacket.prototype.getFieldByOffset = function (offset, pack) {
if (!(pack in this.packSizes)) {
HIDDebug("Unknown pack string " + pack)
return undefined
}
var end_offset = offset + this.packSizes[pack]
var group
var field
for (var group_name in this.groups) {
group = this.groups[group_name]
for (var field_id in group) {
field = group[field_id]
// Same field offset
if (field.offset == offset)
return field
// 7-8 8-9
// Offset for smaller packet inside multibyte field
if (field.offset < offset && field.end_offset >= end_offset)
return field
// Packet offset starts inside field, may overflow
if (field.offset < offset && field.end_offset > offset)
return field
// Packet start before field, ends or overflows field
if (field.offset > offset && field.offset < end_offset)
return field
}
}
return undefined
}
/** Return a field by group and name from the packet,
* Returns undefined if field could not be found */
HIDPacket.prototype.getField = function (group, name) {
var field_id = group + "." + name
if (!(group in this.groups)) {
HIDDebug("PACKET " + this.name + " group not found " + group)
return undefined
}
var control_group = this.groups[group]
if (field_id in control_group)
return control_group[field_id]
// Lookup for bit fields in bitvector matching field name
for (var group_name in this.groups) {
var control_group = this.groups[group_name]
for (field_name in control_group) {
var field = control_group[field_name]
if (field == undefined || field.type != "bitvector")
continue
for (bit_name in field.value.bits) {
var bit = field.value.bits[bit_name]
if (bit.id == field_id) {
return field
}
}
}
}
// Field not found
return undefined
}
/** Return reference to a bit in a bitvector field */
HIDPacket.prototype.lookupBit = function (group, name) {
var field = this.getField(group, name)
if (field == undefined) {
HIDDebug("Bitvector match not found: " + group + "." + name)
return undefined
}
var bit_id = group + "." + name
for (bit_name in field.value.bits) {
var bit = field.value.bits[bit_name]
if (bit.id == bit_id)
return bit
}
HIDDebug("BUG: bit not found after successful field lookup")
return undefined
}
/** Remove a control registered. Normally not needed */
HIDPacket.prototype.removeControl = function (group, name) {
var control_group = this.getGroup(group)
if (!(name in control_group)) {
HIDDebug("Field not in control group " + group + ": " + name)
return
}
delete control_group[name]
}
/** Register a numeric value to parse from input packet
*
* @param group control group name
* @param name name of the field
* @param offset field offset inside packet (bytes)
* @param pack control packing format for unpack(), one of b/B, h/H, i/I
* @param bitmask bitmask size, undefined for byte(s) controls
* NOTE: Parsing bitmask with multiple bits is not supported yet.
* @param isEncoder indicates if this is an encoder which should be wrapped and delta reported */
HIDPacket.prototype.addControl = function (group, name, offset, pack, bitmask, isEncoder) {
var control_group = this.getGroup(group, true)
var bitvector = undefined
if (control_group == undefined) {
HIDDebug("ERROR creating HID packet group " + group)
return
}
if (!(pack in this.packSizes)) {
HIDDebug("Unknown pack value " + pack)
return
}
var field = this.getFieldByOffset(offset, pack)
if (field != undefined) {
if (bitmask == undefined) {
HIDDebug("ERROR registering offset " + offset + " pack " + pack)
HIDDebug(
"ERROR trying to overwrite non-bitmask control " + group + " " + name
)
return
}
bitvector = field.value
bitvector.addBitMask(group, name, bitmask)
return
}
field = {}
field.packet = undefined
field.id = group + "." + name
field.group = group
field.name = name
field.mapped_group = undefined
field.mapped_name = undefined
field.pack = pack
field.offset = offset
field.end_offset = offset + this.packSizes[field.pack]
field.bitmask = bitmask
field.isEncoder = isEncoder
field.callback = undefined
field.soft_takeover = false
field.ignored = false
field.auto_repeat = undefined
field.auto_repeat_interval = undefined
var packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8)
if (this.signedPackFormats.indexOf(pack) != -1) {
field.min = 0 - (packet_max_value / 2) + 1
field.max = (packet_max_value / 2) - 1
} else {
field.min = 0
field.max = packet_max_value - 1
}
if (bitmask == undefined || bitmask == packet_max_value) {
field.type = "control"
field.value = undefined
field.delta = 0
field.mindelta = 0
} else {
if (this.signedPackFormats.indexOf(pack) != -1) {
HIDDebug("ERROR registering bitvector: signed fields not supported")
return
}
// Create a new bitvector field and add the bit to that
// TODO - accept controls with bitmask < packet_max_value
field_name = "bitvector_" + offset
field.type = "bitvector"
field.name = field_name
field.id = group + "." + field_name
bitvector = new HIDBitVector(field.max)
bitvector.size = field.max
bitvector.addBitMask(group, name, bitmask)
field.value = bitvector
field.delta = undefined
field.soft_takeover = undefined
field.mindelta = undefined
}
// Add the new field to the packet
control_group[field.id] = field
}
/** Register a Output control field or Output control bit to output packet
* Output control field:
* Output field with no bitmask, controls Output with multiple values
* Output control bit:
* Output with with bitmask, controls Output with a single bit
*
* It is recommended to define callbacks after packet creation with
* setCallback instead of adding it directly here. But you can do it.
*
* @param offset the offset of the byte(s) to use, starting at 1
* (0 is automatically populated with the reportId)
* @param pack control packing format for pack(), one of b/B, h/H, i/I */
HIDPacket.prototype.addOutput = function (group, name, offset, pack, bitmask, callback) {
var control_group = this.getGroup(group, true)
var field
var bitvector = undefined
var field_id = group + "." + name
if (control_group == undefined) {
return
}
if (!(pack in this.packSizes)) {
HIDDebug("ERROR: unknown Output control pack value " + pack)
return
}
// Adjust offset by 1 because the reportId was previously considered part of the payload
// but isn't anymore and we can't be bothered to adjust every single script manually
offset -= 1
// Check if we are adding a Output bit to existing bitvector
field = this.getFieldByOffset(offset, pack)
if (field != undefined) {
if (bitmask == undefined) {
HIDDebug(
"ERROR: overwrite non-bitmask control " + group + "." + name
)
return
}
bitvector = field.value
bitvector.addOutputMask(group, name, bitmask)
return
}
field = {}
field.id = field_id
field.group = group
field.name = name
field.mapped_group = undefined
field.mapped_name = undefined
field.pack = pack
field.offset = offset
field.end_offset = offset + this.packSizes[field.pack]
field.bitmask = bitmask
field.callback = callback
field.toggle = undefined
var packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8)
if (this.signedPackFormats.indexOf(pack) != -1) {
field.min = 0 - (packet_max_value / 2) + 1
field.max = (packet_max_value / 2) - 1
} else {
field.min = 0
field.max = packet_max_value - 1
}
if (bitmask == undefined || bitmask == packet_max_value) {
field.type = "output"
field.value = undefined
field.delta = undefined
field.mindelta = undefined
} else {
// Create new Output bitvector control field, add bit to it
// rewrite name to use bitvector instead
field_name = "bitvector_" + offset
field.type = "bitvector"
field.id = group + "." + field_name
field.name = field_name
bitvector = new HIDBitVector()
bitvector.size = field.max
bitvector.addOutputMask(group, name, bitmask)
field.value = bitvector
field.delta = undefined
field.mindelta = undefined
}
// Add Output to HID packet
control_group[field.id] = field
}
/** Register a callback to field or a bit vector bit.
* Does not make sense for Output fields but you can do that. */
HIDPacket.prototype.setCallback = function (group, name, callback) {
var field = this.getField(group, name)
var field_id = group + "." + name
if (callback == undefined) {
HIDDebug("Callback to add was undefined for " + field_id)
return
}
if (field == undefined) {
HIDDebug("setCallback: field for " + field_id + " not found"
)
return
}
if (field.type == "bitvector") {
for (var bit_id in field.value.bits) {
var bit = field.value.bits[bit_id]
if (bit_id != field_id)
continue
bit.callback = callback
return
}
HIDDebug("ERROR: BIT NOT FOUND " + field_id)
} else {
field.callback = callback
}
}
/** Set 'ignored' flag for field to given value (true or false)
* If field is ignored, it is not reported in 'delta' objects. */
HIDPacket.prototype.setIgnored = function (group, name, ignored) {
var field = this.getField(group, name)
if (field == undefined) {
HIDDebug("ERROR setting ignored flag for " + group + " " + name)
return
}
field.ignored = ignored
}
/** Adjust field's minimum delta value.
* Input value changes smaller than this are not reported in delta */
HIDPacket.prototype.setMinDelta = function (group, name, mindelta) {
field = this.getField(group, name)
if (field == undefined) {
HIDDebug("ERROR adjusting mindelta for " + group + " " + name)
return
}
if (field.type == "bitvector") {
HIDDebug("ERROR setting mindelta for bitvector packet does not make sense")
return
}
field.mindelta = mindelta
}
/** Parse bitvector field values, returning object with the named bits set.
* Value must be a valid unsigned byte to parse, with enough bits.
* Returns list of modified bits (delta) */
HIDPacket.prototype.parseBitVector = function (field, value) {
var bits = {}
var bit
var new_value
for (var bit_id in field.value.bits) {
bit = field.value.bits[bit_id]
new_value = (bit.bitmask & value) >> bit.bit_offset
if (bit.value != undefined && bit.value != new_value)
bits[bit_id] = bit
bit.value = new_value
}
return bits
}
/** Parse input packet fields from data.
* Data is expected to be a Packet() received from HID device.
* Returns list of changed fields with new value.
* BitVectors are returned as bits you can iterate separately. */
HIDPacket.prototype.parse = function (data) {
var field_changes = {}
var group
var group_name
var field
var field_id
for (group_name in this.groups) {
group = this.groups[group_name]
for (field_id in group) {
field = group[field_id]
if (field == undefined)
continue
var value = this.unpack(data, field)
if (value == undefined) {
HIDDebug("Error parsing packet field value for " + field_id)
return
}
if (field.type == "bitvector") {
// Bitvector deltas are checked in parseBitVector
var changed_bits = this.parseBitVector(field, value)
for (var bit_name in changed_bits)
field_changes[bit_name] = changed_bits[bit_name]
} else if (field.type == "control") {
if (field.value == value && field.mindelta != undefined)
continue
if (field.ignored || field.value == undefined) {
field.value = value
continue
}
var change
if (field.isEncoder) {
if (field.value == field.max && value == field.min) {
change = 1
field.delta = 1
} else if (value == field.max && field.value == field.min) {
change = 1
field.delta = -1
} else {
change = 1
field.delta = value - field.value
}
field.value = value
} else {
change = Math.abs(value - field.value)
field.delta = value - field.value
}
if (field.mindelta == undefined || change > field.mindelta) {
field_changes[field.name] = field
field.value = value
}
}
}
}
return field_changes
}
/** Send this HID packet to device.
* First the header bytes are copied to beginning of packet, then
* field object values are packed to the HID packet according to the
* field type. */
HIDPacket.prototype.send = function (debug) {
var data = []
if (this.header !== undefined) {
for (header_byte = 0; header_byte < this.header.length; header_byte++) {
data[header_byte] = this.header[header_byte]
}
}
for (var group_name in this.groups) {
var group = this.groups[group_name]
for (var field_name in group) {
this.pack(data, group[field_name])
}
}
if (debug) {
var packet_string = ""
for (var d in data) {
if (data[d] < 0x10) {
// Add padding for bytes smaller than 10
packet_string += "0"
}
packet_string += data[d].toString(16) + " "
}
HIDDebug("Sending packet with Report ID " + this.reportId + ": " + packet_string)
}
controller.send(data, data.length, this.reportId)
}
/**
* HID Controller Class
*
* HID Controller with packet parser
* Global attributes include:
*
* initialized by default false, you should set this to true when
* controller is found and everything is OK
* activeDeck by default undefined, used to map the virtual deck
* names 'deck','deck1' and 'deck2' to actual [ChannelX]
* isScratchEnabled set to true, when button 'jog_touch' is active
* buttonStates valid state values for buttons, should contain fields
* released (default 0) and pressed (default 1)
* LEDColors possible Output colors named, must contain 'off' value
* deckOutputColors Which colors to use for each deck. Default 'on' for first
* four decks. Values are like {1: 'red', 2: 'green' }
* and must reference valid OutputColors fields.
* OutputUpdateInterval By default undefined. If set, it's a value for timer
* executed every n ms to update Outputs with updateOutputs()
* modifiers Reference to HIDModifierList object
* toggleButtons List of button names you wish to act as 'toggle', i.e.
* pressing the button and releasing toggles state of the
* control and does not set it off again when released.
*
* Scratch variables (initialized with 'common' defaults, you can override):
* scratchintervalsPerRev Intervals value for scratch_enable
* scratchRPM RPM value for scratch_enable
* scratchAlpha Alpha value for scratch_enable
* scratchBeta Beta value for scratch_enable
* scratchRampOnEnable If 'ramp' is used when enabling scratch
* scratchRampOnDisable If 'ramp' is used when disabling scratch
*/
HIDController = function () {
this.initialized = false
this.activeDeck = undefined
this.InputPackets = {}
this.OutputPackets = {}
// Default input control packet name: can be modified for controllers
// which can swap modes (wiimote for example)
this.defaultPacket = "control"
// Callback functions called by deck switching. Undefined by default
this.disconnectDeck = undefined
this.connectDeck = undefined
// Scratch parameter defaults for this.scratchEnable function
// override for custom control
this.isScratchEnabled = false
this.scratchintervalsPerRev = 128
this.scratchRPM = 33 + 1 / 3
this.scratchAlpha = 1.0 / 8
this.scratchBeta = this.scratchAlpha / 32
this.scratchRampOnEnable = false
this.scratchRampOnDisable = false
// Button states available
this.buttonStates = { released: 0, pressed: 1 }
// Output color values to send
this.LEDColors = { off: 0x0, on: 0x7f }
// Toggle buttons
this.toggleButtons = ["play", "pfl", "keylock", "quantize", "reverse", "slip_enabled",
"group_[Channel1]_enable", "group_[Channel2]_enable",
"group_[Channel3]_enable", "group_[Channel4]_enable"]
// Override to set specific colors for multicolor button Output per deck
this.deckOutputColors = { 1: "on", 2: "on", 3: "on", 4: "on" }
// Mapping of automatic deck switching with deckSwitch function
this.virtualDecks = ["deck", "deck1", "deck2", "deck3", "deck4"]
this.deckSwitchMap = { 1: 2, 2: 1, 3: 4, 4: 3, undefined: 1 }
// Standard target groups available in mixxx. This is used by
// HID packet parser to recognize group parameters we should
// try sending to mixxx.
this.valid_groups = [
"[Channel1]", "[Channel2]", "[Channel3]", "[Channel4]",
"[Sampler1]", "[Sampler2]", "[Sampler3]", "[Sampler4]",
"[Sampler5]", "[Sampler6]", "[Sampler7]", "[Sampler8]",
"[Master]", "[PreviewDeck1]", "[Effects]", "[Playlist]", "[Flanger]",
"[Microphone]", "[EffectRack1_EffectUnit1]", "[EffectRack1_EffectUnit2]",
"[EffectRack1_EffectUnit3]", "[EffectRack1_EffectUnit4]",
"[InternalClock]"]
// Set to value in ms to update Outputs periodically
this.OutputUpdateInterval = undefined
this.modifiers = new HIDModifierList()
this.scalers = {}
this.timers = {}
this.auto_repeat_interval = 100
}
/** Function to close the controller object cleanly */
HIDController.prototype.close = function () {
for (var name in this.timers) {
var timer = this.timers[name]
if (timer == undefined)
continue
engine.stopTimer(timer)
this.timers[name] = undefined
}
}
/** Initialize our packet data and callbacks. This does not seem to
* work when executed from here, but we keep a stub just in case. */
HIDController.prototype.initializePacketData = function () {
}
/** Return deck number from deck name. Deck name can't be virtual deck name
* in this function call. */
HIDController.prototype.resolveDeck = function (group) {
if (group == undefined)
return undefined
var result = group.match(/\[Channel[0-9]+\]/)
if (!result)
return undefined
var str = group.replace(/\[Channel/, "")
return str.substring(0, str.length - 1)
}
/** Return the group name from given deck number. */
HIDController.prototype.resolveDeckGroup = function (deck) {
if (deck == undefined)
return undefined
return "[Channel" + deck + "]"
}
/** Map virtual deck names to real deck group. If group is already
* a real mixxx group value, just return it as it without mapping. */
HIDController.prototype.resolveGroup = function (group) {
var channel_name = /\[Channel[0-9]+\]/
if (group != undefined && group.match(channel_name))
return group
if (this.valid_groups.indexOf(group) != -1) {
return group
}
if (group == "deck" || group == undefined) {
if (this.activeDeck == undefined)
return undefined
return "[Channel" + this.activeDeck + "]"
}
if (this.activeDeck == 1 || this.activeDeck == 2) {
if (group == "deck1") return "[Channel1]"
if (group == "deck2") return "[Channel2]"
}
if (this.activeDeck == 3 || this.activeDeck == 4) {
if (group == "deck1") return "[Channel3]"
if (group == "deck2") return "[Channel4]"
}
return undefined
}
/** Find Output control matching give group and name
* Returns undefined if output field can't be found. */
HIDController.prototype.getOutputField = function (m_group, m_name) {
for (var packet_name in this.OutputPackets) {
var packet = this.OutputPackets[packet_name]
for (var group_name in packet.groups) {
var group = packet.groups[group_name]
for (var field_name in group) {
var field = group[field_name]
if (field.type == "bitvector") {
for (var bit_id in field.value.bits) {
var bit = field.value.bits[bit_id]
if (bit.mapped_group == m_group && bit.mapped_name == m_name)
return bit
if (bit.group == m_group && bit.name == m_name)
return bit
}
continue
}
if (field.mapped_group == m_group && field.mapped_name == m_name)
return field
if (field.group == m_group && field.name == m_name)
return field
}
}
}
return undefined
}
/** Find input packet matching given name.
* Returns undefined if input packet name is not registered. */
HIDController.prototype.getInputPacket = function (name) {
if (!(name in this.InputPackets))
return undefined
return this.InputPackets[name]
}
/** Find output packet matching given name
* Returns undefined if output packet name is not registered. */
HIDController.prototype.getOutputPacket = function (name) {
if (!(name in this.OutputPackets))
return undefined
return this.OutputPackets[name]
}
/** Set input packet callback afterwards */
HIDController.prototype.setPacketCallback = function (packet, callback) {
var input_packet = this.getInputPacket(packet)
input_packet.callback = callback
}
/** Register packet field's callback.
* If packet has callback, it is still parsed but no field processing is done,
* callback is called directly after unpacking fields from packet. */
HIDController.prototype.setCallback = function (packet, group, name, callback) {
var input_packet = this.getInputPacket(packet)
if (input_packet == undefined) {
HIDDebug("Input packet not found " + packet)
return
}
input_packet.setCallback(group, name, callback)
}
/** Register scaling function for a control name
* This does not check if given control name is valid */
HIDController.prototype.setScaler = function (name, callback) {
if (name in this.scalers)
return
this.scalers[name] = callback
}
/** Lookup scaling function for control
* Returns undefined if function is not registered. */
HIDController.prototype.getScaler = function (name, callback) {
if (!(name in this.scalers))
return undefined
return this.scalers[name]
}
/** Change type of a previously defined field to modifier and register it */
HIDController.prototype.linkModifier = function (group, name, modifier) {
var packet = this.getInputPacket(this.defaultPacket)
if (packet == undefined) {
HIDDebug(
"ERROR creating modifier: input packet " + this.defaultPacket + " not found"
)
return
}
var bit_id = group + "." + name
var field = packet.lookupBit(group, name)
if (field == undefined) {
HIDDebug("BIT field not found: " + bit_id)
return
}
field.group = "modifiers"
field.name = modifier
this.modifiers.set(modifier)
}
/** TODO - implement unlinking of modifiers */
HIDController.prototype.unlinkModifier = function (group, name, modifier) {
HIDDebug("Unlinking of modifiers not yet implemented")
}
/** Link a previously declared HID control to actual mixxx control */
HIDController.prototype.linkControl = function (group, name, m_group, m_name, callback) {
var field
var packet = this.getInputPacket(this.defaultPacket)
if (packet == undefined) {
HIDDebug("ERROR creating modifier: input packet " + this.defaultPacket + " not found")
return
}
field = packet.getField(group, name)
if (field == undefined) {
HIDDebug("Field not found: " + group + "." + name)
return
}
if (field.type == "bitvector") {
field = packet.lookupBit(group, name)
if (field == undefined) {
HIDDebug("bit not found: " + group + "." + name)
return
}
}
field.mapped_group = m_group
field.mapped_name = m_name
if (callback != undefined)
field.callback = callback
}
/** TODO - implement unlinking of controls */
HIDController.prototype.unlinkControl = function (group, name) {
}
/** Register HID input packet type to controller.
* Input packets can be responses from device to queries, or control
* data details. The default control data packet must be named in
* variable this.defaultPacket to allow automatic processing. */
HIDController.prototype.registerInputPacket = function (packet) {
// Find modifiers and other special cases from packet fields
for (var group_name in packet.groups) {
var group = packet.groups[group_name]
for (var field_name in group) {
var field = group[field_name]
field.packet = packet
if (field.type == "bitvector") {
for (var bit_id in field.value.bits) {
var bit = field.value.bits[bit_id]
bit.packet = packet
if (bit.group == "modifiers")
this.modifiers.add(bit.name)
}
} else {
if (field.group == "modifiers")
this.modifiers.add(field.name)
}
}
}
this.InputPackets[packet.name] = packet
}
/** Register HID output packet type to controller
* There are no special Output control output packets, just register Outputs to any
* valid packet and we detect them here.
* This module only supports sending bitvector values and byte fields to device.
* If you need other data structures, patches are welcome, or you can just do it
* manually in your script without registering the packet. */
HIDController.prototype.registerOutputPacket = function (packet) {
this.OutputPackets[packet.name] = packet
// Link packet to all fields
for (var group_name in packet.groups) {
var group = packet.groups[group_name]
for (var field_name in group) {
var field = group[field_name]
field.packet = packet
if (field.type == "bitvector") {
for (var bit_id in field.value.bits) {
var bit = field.value.bits[bit_id]
bit.packet = packet
}
}
}
}
}
/** Parse a received input packet fields with "unpack" calls to fields
* Calls packet callback and returns, if packet callback was defined
* Calls processIncomingPacket and processes automated events there.
* If defined, calls processDelta for results after processing automated fields */
HIDController.prototype.parsePacket = function (data, length) {
var packet
var changed_data
if (this.InputPackets == undefined) {
return
}
for (var name in this.InputPackets) {
packet = this.InputPackets[name]
// When the device uses multiple report types with report IDs, hidapi
// prepends the report ID to the data sent to Mixxx. If the device
// only has a single report type, the HIDPacket constructor sets the
// reportId as 0. In this case, hidapi only sends the data of the
// report to Mixxx without a report ID.
if (packet.reportId !== 0 && packet.reportId !== data[0]) {
continue
}
if (packet.header !== undefined) {
for (var header_byte = 0; header_byte < packet.header.length; header_byte++) {
if (packet.header[header_byte] != data[header_byte]) {
packet = undefined
break
}
}
if (packet === undefined)
continue
}
changed_data = packet.parse(data)
if (packet.callback != undefined) {
packet.callback(packet, changed_data)
return
}
// Process named group controls
if (packet.name == this.defaultPacket)
this.processIncomingPacket(packet, changed_data)
// Process generic changed_data packet, if callback is defined
if (this.processDelta != undefined)
this.processDelta(packet, changed_data)
if (this.postProcessDelta != undefined)
this.postProcessDelta(packet, changed_data)
return
}
HIDDebug("Received unknown packet of " + length + " bytes")
for (var i in data) HIDDebug("BYTE " + data[i])
}
/** Process the modified field values (delta) from input packet fields for
* input control packet, if packet name is in this.defaultPacket.
*
* Button field processing:
* - Sets modifiers from buttons
* - Calls button callbacks, if defined
* - Finally tries to run matching engine.setValue() function for buttons
* in default mixxx groups, honoring toggleButtons and other button
* details. Not done if a callback was defined for button.
*
* Control field processing
* - Calls scaling functions for control fields, if defined for field.
* Scaling function for encoders (isEncoder attribute is true) scales
* field delta instead of raw value.
* - Calls callback functions for control fields, if defined for field
* - Finally tries run matching engine.setValue() function for control
* fields in default mixxx groups. Not done if a callback was defined. */
HIDController.prototype.processIncomingPacket = function (packet, delta) {
var field
for (var name in delta) {
if (this.ignoredControlChanges != undefined
&& this.ignoredControlChanges.indexOf(name) != -1)
continue
field = delta[name]
if (field.type == "button")
this.processButton(field)
else if (field.type == "control")
this.processControl(field)
else
HIDDebug("Unknown field " + field.name + " type " + field.type)
}
}
/** Get active group for this field */
HIDController.prototype.getActiveFieldGroup = function (field) {
if (field.mapped_group != undefined) {
return this.resolveGroup(field.mapped_group)
}
group = field.group
if (group == undefined) {
if (this.activeDeck != undefined)
return "[Channel" + this.activeDeck + "]"
}
if (this.valid_groups.indexOf(group) != -1) {
//HIDDebug("Resolving group " + group);
return this.resolveGroup(group)
}
return group
}
/** Get active control name from field */
HIDController.prototype.getActiveFieldControl = function (field) {
if (field.mapped_name != undefined)
return field.mapped_name
return field.name
}
/** Process given button field, triggering events */
HIDController.prototype.processButton = function (field) {
var group = this.getActiveFieldGroup(field)
var control = this.getActiveFieldControl(field)
if (group == undefined) {
HIDDebug("processButton: Could not resolve group from "
+ field.group + " " + field.mapped_group + " "
+ field.name + " " + field.mapped_name
)
return
}
if (group == "modifiers") {
if (field.value != 0)
this.modifiers.set(control, true)
else
this.modifiers.set(control, false)
return
}
if (field.auto_repeat) {
timer_id = "auto_repeat_" + field.id
if (field.value) {
this.startAutoRepeatTimer(timer_id, field.auto_repeat_interval)
} else {
this.stopAutoRepeatTimer(timer_id)
}
}
if (field.callback != undefined) {
field.callback(field)
return
}
if (control == "jog_touch") {
if (group != undefined) {
if (field.value == this.buttonStates.pressed)
this.enableScratch(group, true)
else
this.enableScratch(group, false)
}
return
}
if (this.toggleButtons.indexOf(control) != -1) {
if (field.value == this.buttonStates.released)
return
if (engine.getValue(group, control)) {
if (control == "play")
engine.setValue(group, "stop", true)
else
engine.setValue(group, control, false)
} else {
engine.setValue(group, control, true)
}
return
}
if (field.auto_repeat && field.value == this.buttonStates.pressed) {
HIDDebug("Callback for " + field.group)
engine.setValue(group, control, field.auto_repeat(field))
} else if (engine.getValue(group, control) == false) {
engine.setValue(group, control, true)
} else {
engine.setValue(group, control, false)
}
}
/** Process given control field, triggering events */
HIDController.prototype.processControl = function (field) {
var value
var group = this.getActiveFieldGroup(field)
var control = this.getActiveFieldControl(field)
if (group == undefined) {
HIDDebug("processControl: Could not resolve group from "
+ field.group + " " + field.mapped_group + " "
+ field.name + " " + field.mapped_name
)
return
}
if (field.callback != undefined) {
value = field.callback(field)
return
}
if (group == "modifiers") {
this.modifiers.set(control, field.value)
return
}
if (control == "jog_wheel") {
// Handle jog wheel scratching transparently
this.jog_wheel(field)
return
}
// Call value scaler if defined and send mixxx signal
value = field.value
scaler = this.getScaler(control)
if (field.isEncoder) {
var field_delta = field.delta
if (scaler != undefined)
field_delta = scaler(group, control, field_delta)
engine.setValue(group, control, field_delta)
} else {
if (scaler != undefined) {
value = scaler(group, control, value)
// See the Traktor S4 script for how to use this. If the scaler function has this
// parameter set to true, we use the effects-engine setParameter call instead of
// setValue.
if (scaler.useSetParameter) {
engine.setParameter(group, control, value)
return
}
}
engine.setValue(group, control, value)
}
}
/** Toggle control state from toggle button */
HIDController.prototype.toggle = function (group, control, value) {
if (value == this.buttonStates.released)
return
var status = (engine.getValue(group, control) == true) ? false : true
engine.setValue(group, control, status)
}
/** Toggle play/pause state */
HIDController.prototype.togglePlay = function (group, field) {
if (field.value == this.buttonStates.released)
return
var status = (engine.getValue(group, "play")) ? false : true
if (!status)
engine.setValue(group, "stop", true)
else
engine.setValue(group, "play", true)
}
/** Processing of the 'jog_touch' special button name, which is used to detect
* when scratching should be enabled.
* Deck is resolved from group with 'resolveDeck'
*
* Enabling scratching (press 'jog_touch' button)
* Sets the internal 'isScratchEnabled' attribute to true, and calls scratchEnable
* with the scratch attributes (see class defination)
*
* Disabling scratching (release 'jog_touch' button)
* Sets the internal 'isScratchEnabled attribute to false, and calls scratchDisable
* to end scratching mode */
HIDController.prototype.enableScratch = function (group, status) {
var deck = this.resolveDeck(group)
if (status) {
this.isScratchEnabled = true
engine.scratchEnable(deck,
this.scratchintervalsPerRev,
this.scratchRPM,
this.scratchAlpha,
this.scratchBeta,
this.rampedScratchEnable
)
if (this.enableScratchCallback != undefined) this.enableScratchCallback(true)
} else {
this.isScratchEnabled = false
engine.scratchDisable(deck, this.rampedScratchDisable)
if (this.enableScratchCallback != undefined) this.enableScratchCallback(false)
}
}
/** Default jog scratching function. Used to handle jog move events from special
* input control field called 'jog_wheel'. Handles both 'scratch' and 'jog' mixxx
* functions, depending on isScratchEnabled value above (see enableScratch())
*
* Since most controllers require value scaling for jog and scratch functions,
* you are warned if following scaling function names are not registered:
*
* jog
* Scaling function from 'jog_wheel' for rate bend events with mixxx 'jog'
* function. Should return value range suitable for 'jog', whatever you
* wish it to do.
* jog_scratch
* Scaling function from 'jog_wheel' for scratch movements with mixxx
* 'scratchTick' function. Should return -1,0,1 or small ranges of integers
* both negative and positive values.
*/
HIDController.prototype.jog_wheel = function (field) {
var scaler = undefined
var active_group = this.getActiveFieldGroup(field)
var value = undefined
if (field.isEncoder)
value = field.delta
else
value = field.value
if (this.isScratchEnabled) {
var deck = this.resolveDeck(active_group)
if (deck == undefined)
return
scaler = this.getScaler("jog_scratch")
if (scaler != undefined)
value = scaler(active_group, "jog_scratch", value)
else
HIDDebug("WARNING non jog_scratch scaler, you likely want one")
engine.scratchTick(deck, value)
} else {
if (active_group == undefined)
return
scaler = this.getScaler("jog")
if (scaler != undefined)
value = scaler(active_group, "jog", value)
else
HIDDebug("WARNING non jog scaler, you likely want one")
engine.setValue(active_group, "jog", value)
}
}
HIDController.prototype.stopAutoRepeatTimer = function (timer_id) {
if (this.timers[timer_id]) {
engine.stopTimer(this.timers[timer_id])
delete this.timers[timer_id]
} else {
//HIDDebug("No such autorepeat timer: " + timer_id);
}
}
/** Toggle field autorepeat on or off */
HIDController.prototype.setAutoRepeat = function (group, name, callback, interval) {
var packet = this.getInputPacket(this.defaultPacket)
var field = packet.getField(group, name)
if (field == undefined) {
HIDDebug("setAutoRepeat: field not found " + group + "." + name)
return
}
field.auto_repeat = callback
if (interval)
field.auto_repeat_interval = interval
else
field.auto_repeat_interval = controller.auto_repeat_interval
if (callback)
callback(field)
}
/** Callback for auto repeat timer to send again the values for
* buttons and controls marked as 'auto_repeat'
* Timer must be defined from actual controller side, because of
* callback call namespaces and 'this' reference */
HIDController.prototype.autorepeatTimer = function () {
var group_name
var group
var field
var field_name
var bit_name
var bit
var packet = this.InputPackets[this.defaultPacket]
for (group_name in packet.groups) {
group = packet.groups[group_name]
for (field_name in group) {
field = group[field_name]
if (field.type != "bitvector") {
if (field.auto_repeat)
this.processControl(field)
continue
}
for (bit_name in field.value.bits) {
bit = field.value.bits[bit_name]
if (bit.auto_repeat)
this.processButton(bit)
}
}
}
}
/** Toggle active deck and update virtual output field control mappings */
HIDController.prototype.switchDeck = function (deck) {
var packet
var field
var controlgroup
if (deck == undefined) {
if (this.activeDeck == undefined) {
deck = 1
} else {
// This is unusable: num_decks has always minimum 4 decks
// var totalDecks = engine.getValue("[Master]","num_decks");
// deck = (this.activeDeck+1) % totalDecks;
deck = this.deckSwitchMap[this.activeDeck]
if (deck == undefined)
deck = 1
}
}
new_group = this.resolveDeckGroup(deck)
HIDDebug("Switching to deck " + deck + " group " + new_group)
if (this.disconnectDeck != undefined)
this.disconnectDeck()
for (var packet_name in this.OutputPackets) {
packet = this.OutputPackets[packet_name]
var send_packet = false
for (var group_name in packet.groups) {
var group = packet.groups[group_name]
for (var field_name in group) {
field = group[field_name]
if (field.type == "bitvector") {
for (var bit_id in field.value.bits) {
var bit = field.value.bits[bit_id]
if (this.virtualDecks.indexOf(bit.mapped_group) == -1)
continue
send_packet = true
controlgroup = this.resolveGroup(bit.mapped_group)
engine.connectControl(controlgroup, bit.mapped_name, bit.mapped_callback, true)
engine.connectControl(new_group, bit.mapped_name, bit.mapped_callback)
var value = engine.getValue(new_group, bit.mapped_name)
HIDDebug("BIT " + bit.group + "." + bit.name + " value " + value)
if (value)
this.setOutput(
bit.group, bit.name,
this.LEDColors[this.deckOutputColors[deck]]
)
else
this.setOutput(
bit.group, bit.name,
this.LEDColors.off
)
}
continue
}
// Only move outputs of virtual decks
if (this.virtualDecks.indexOf(field.mapped_group) == -1)
continue
send_packet = true
controlgroup = this.resolveGroup(field.mapped_group)
engine.connectControl(controlgroup, field.mapped_name, field.mapped_callback, true)
engine.connectControl(new_group, field.mapped_name, field.mapped_callback)
var value = engine.getValue(new_group, field.mapped_name)
if (value)
this.setOutput(
field.group, field.name,
this.LEDColors[this.deckOutputColors[deck]]
)
else
this.setOutput(
field.group, field.name,
this.LEDColors.off
)
}
}
}
this.activeDeck = deck
if (this.connectDeck != undefined)
this.connectDeck()
}
/** Link a virtual HID Output to mixxx control */
HIDController.prototype.linkOutput = function (group, name, m_group, m_name, callback) {
var controlgroup
var field = this.getOutputField(group, name)
if (field == undefined) {
HIDDebug("Linked output not found: " + group + "." + name)
return
}
if (field.mapped_group != undefined) {
HIDDebug("Output already linked: " + field.mapped_group)
return
}
controlgroup = this.resolveGroup(m_group)
field.mapped_group = m_group
field.mapped_name = m_name
field.mapped_callback = callback
engine.makeConnection(controlgroup, m_name, callback) //Switched to makeConnection deprecated connectControl
if (engine.getValue(controlgroup, m_name))
this.setOutput(m_group, m_name, "on")
else
this.setOutput(m_group, m_name, "off")
}
/** Unlink a virtual HID Output from mixxx control */
HIDController.prototype.unlinkOutput = function (group, name, callback) {
var field = this.getOutputField(group, name)
var controlgroup
if (field == undefined) {
HIDDebug("Unlinked output not found: " + group + "." + name)
return
}
if (field.mapped_group == undefined || field.mapped_name == undefined) {
HIDDebug("Unlinked output not mapped: " + group + "." + name)
return
}
controlgroup = this.resolveGroup(field.mapped_group)
engine.connectControl(controlgroup, field.mapped_name, callback, true)
field.mapped_group = undefined
field.mapped_name = undefined
field.mapped_callback = undefined
}
/** Set output state to given value */
HIDController.prototype.setOutput = function (group, name, value, send_packet) {
var field = this.getOutputField(group, name)
if (field == undefined) {
HIDDebug("setOutput: unknown field: " + group + "." + name)
return
}
field.value = value << field.bit_offset
field.toggle = value << field.bit_offset
if (send_packet)
field.packet.send()
}
/** Set Output to toggle between two values. Reset with setOutput(name,'off') */
HIDController.prototype.setOutputToggle = function (group, name, toggle_value) {
var field = this.getOutputField(group, name)
if (field == undefined) {
HIDDebug("setOutputToggle: unknown field " + group + "." + name)
return
}
field.value = toggle_value << field.bit_offset
field.toggle = toggle_value << field.bit_offset
field.packet.send()
}
HIDController.prototype.softTakeover = function () {
for (var i = 1; i < arguments.length; i++) {
engine.softTakeover(arguments[0], arguments[i], true)
}
}
HIDController.prototype.softTakeoverAll = function () {
for (var channel = 1; channel < 5; channel++) {
this.softTakeover('[EqualizerRack1_[Channel' + channel + ']_Effect1]', 'parameter1', 'parameter2', 'parameter3')
this.softTakeover('[QuickEffectRack1_[Channel' + channel + ']]', 'super1')
this.softTakeover('[Channel' + channel + ']', 'pregain', 'volume', 'rate')
for (var i = 1; i < 4; i++) {
this.softTakeover('[EffectRack1_EffectUnit' + channel + '_Effect' + i + ']', 'meta')
}
}
this.softTakeover('[Master]', 'headMix', 'crossfader')
}
HIDPacket.prototype.clearControls = function () {
for (var groupName in this.groups) {
var group = this.groups[groupName]
for (var control in group) {
group[control].value = 0
}
}
this.send()
}
HIDController.prototype.getLightsPacket = function (packetName) {
return this.getOutputPacket(packetName ? packetName : 'lights')
}
HIDController.prototype.sendLightsUpdate = function (packetName) {
this.getLightsPacket(packetName).send()
}
HIDController.prototype.connectLight = function (group, name, setter) {
setter(engine.getValue(group, name), this.getLightsPacket(), group, name)
var fun = function (value, group, name) {
setter(value, this.getLightsPacket(), group, name)
this.sendLightsUpdate()
}.bind(this) // Add .bind(this) to make sure the function is called in the context of the controller
engine.makeConnection(group, name, fun)
this.sendLightsUpdate()
return fun
}
<?xml version="1.0" encoding="UTF-8"?>
<MixxxControllerPreset mixxxVersion="2.2.0" schemaVersion="1">
<info>
<name>Traktor Kontrol X1 MK2</name>
<author>infiniteloop</author>
<description>HID Mapping for Traktor Kontrol X1 MK2</description>
<forums>https://mixxx.discourse.group/t/solved-native-instruments-traktor-kontrol-x1-mk1/18471/11</forums>
<manual>native_instruments_traktor_kontrol_x1_mk2</manual>
<devices>
<product protocol="hid" vendor_id="0x17cc" product_id="0x1220" usage_page="0xff01" usage="0x0" interface_number="0x4" />
</devices>
</info>
<controller id="Traktor">
<scriptfiles>
<file filename="common-hid-packet-parser.js" functionprefix="" />
<file filename="Traktor-Kontrol-X1-MK2-hid-scripts.js" functionprefix="TraktorX1MK2" />
</scriptfiles>
</controller>
</MixxxControllerPreset>
/****************************************************************/
/* Traktor Kontrol X1 MK2 HID controller script */
/* Copyright (C) 2021, infiniteloop */
/* Based on: */
/* Traktor Kontrol S2 MK2 HID controller script v1.00 */
/* Copyright (C) 2015, the Mixxx Team */
/* but feel free to tweak this to your heart's content! */
/****************************************************************/
// ==== Friendly User Configuration ====
// The Cue button, when Shift is also held, can have two possible functions:
// 1. "REWIND": seeks to the very start of the track.
// 2. "REVERSEROLL": performs a temporary reverse or "censor" effect, where the track
// is momentarily played in reverse until the button is released.
var ShiftCueButtonAction = "REWIND";
var OutputBrightnessMax = 0x7f;
// Set the brightness of button LEDs which are off and on. This uses a scale from 0 to 0x7f (127).
// If you don't have the optional power adapter and are using the controller with USB bus power,
// 0x09 is probably too dim to notice.
var ButtonBrightnessOff = 0x03;
var ButtonBrightnessOn = OutputBrightnessMax;
// KNOWN ISSUES:
// * The effect button LEDs briefly flicker when pressing the effect focus button.
// eslint definitions
/* global controller, HIDController, HIDPacket */
var TraktorX1MK2 = 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.previousDeckEncoder = {
"[Channel1]": 0,
"[Channel2]": 0
};
this.stripeTouchInertiaTimer = {
"[Channel1]": 0,
"[Channel2]": 0
};
this.stripeTouchTime = 0;
this.stripeCenterPressTimer = 0;
this.deckEncoderPressed = {
"[Channel1]": false,
"[Channel2]": false
};
this.syncPressed = {
"[Channel1]": false,
"[Channel2]": false
};
this.browsePressed = false;
this.shiftPressed = false;
this.stripeCenterPressed = false;
this.rateModified = false;
this.padModes = {
"hotcue": 0,
"introOutro": 1,
"sampler": 2
};
this.currentPadMode = {
"[Channel1]": this.padModes.hotcue,
"[Channel2]": this.padModes.hotcue
};
this.padConnections = {
"[Channel1]": [],
"[Channel2]": []
};
this.lastTouchChannel = [-1, -1];
this.lastTickValue = [0, 0];
this.lastTickTime = [0.0, 0.0];
this.syncEnabledTime = {
"[Channel1]": 0,
"[Channel2]": 0
};
this.rateResetTime = {
"[Channel1]": 0,
"[Channel2]": 0
};
this.longPressTimeoutMilliseconds = 275;
this.stripePressTimeoutMilliseconds = 50;
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]": []
};
};
TraktorX1MK2.registerInputPackets = function () {
var MessageShort = new HIDPacket("shortmessage", 0x01, this.shortMessageCallback);
// An exclamation point indicates a specially-handled function. Everything else is a standard
// Mixxx control object name.
MessageShort.addControl("[Playlist]", "!browse", 0x11, "B", 0xF0);
MessageShort.addControl("[Master]", "!shift", 0x14, "B", 0x04);
MessageShort.addControl("[Master]", "!browse_press", 0x17, "B", 0x02);
MessageShort.addControl("[Master]", "!stripe_touch_timestamp", 0x19, "H", 0xFFFF, false, this.stripeTouchTimestamp);
MessageShort.addControl("[Master]", "!stripe_touch0", 0x1B, "H", 0x07FF, false, this.stripeTouch);
MessageShort.addControl("[Master]", "!stripe_touch1", 0x1D, "H", 0x07FF, false, this.stripeTouch);
MessageShort.addControl("[Channel1]", "!deck_encoder_press", 0x17, "B", 0x01, false, this.deckEncoderPress);
MessageShort.addControl("[Channel1]", "!load_track", 0x14, "B", 0x08, false, this.loadTrackButton);
MessageShort.addControl("[Channel1]", "!pad1", 0x16, "B", 0x80, false, this.padButton);
MessageShort.addControl("[Channel1]", "!pad2", 0x16, "B", 0x40, false, this.padButton);
MessageShort.addControl("[Channel1]", "!pad3", 0x16, "B", 0x20, false, this.padButton);
MessageShort.addControl("[Channel1]", "!pad4", 0x16, "B", 0x10, false, this.padButton);
MessageShort.addControl("[Channel1]", "!flux_button", 0x16, "B", 0x08, false, this.introOutroModeButton);
MessageShort.addControl("[Channel1]", "!sync_enabled", 0x16, "B", 0x04, false, this.syncButton);
MessageShort.addControl("[Channel1]", "!cue_default", 0x16, "B", 0x02, false, this.cueButton);
MessageShort.addControl("[Channel1]", "!play", 0x16, "B", 0x01, false, this.playButton);
MessageShort.addControl("[Channel2]", "!deck_encoder_press", 0x14, "B", 0x01, false, this.deckEncoderPress);
MessageShort.addControl("[Channel2]", "!load_track", 0x14, "B", 0x02, false, this.loadTrackButton);
MessageShort.addControl("[Channel2]", "!pad1", 0x15, "B", 0x80, false, this.padButton);
MessageShort.addControl("[Channel2]", "!pad2", 0x15, "B", 0x40, false, this.padButton);
MessageShort.addControl("[Channel2]", "!pad3", 0x15, "B", 0x20, false, this.padButton);
MessageShort.addControl("[Channel2]", "!pad4", 0x15, "B", 0x10, false, this.padButton);
MessageShort.addControl("[Channel2]", "!flux_button", 0x15, "B", 0x08, false, this.introOutroModeButton);
MessageShort.addControl("[Channel2]", "!sync_enabled", 0x15, "B", 0x04, false, this.syncButton);
MessageShort.addControl("[Channel2]", "!cue_default", 0x15, "B", 0x02, false, this.cueButton);
MessageShort.addControl("[Channel2]", "!play", 0x15, "B", 0x01, false, this.playButton);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effect_focus_button", 0x13, "B", 0x80, false, this.effectFocusButton);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectbutton1", 0x13, "B", 0x40, false, this.effectButton);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectbutton2", 0x13, "B", 0x20, false, this.effectButton);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectbutton3", 0x13, "B", 0x10, false, this.effectButton);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effect_focus_button", 0x13, "B", 0x08, false, this.effectFocusButton);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectbutton1", 0x13, "B", 0x04, false, this.effectButton);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectbutton2", 0x13, "B", 0x02, false, this.effectButton);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectbutton3", 0x13, "B", 0x01, false, this.effectButton);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", 0x14, "B", 0x80);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", 0x14, "B", 0x40);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", 0x14, "B", 0x20);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", 0x14, "B", 0x10);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "mix", 0x01, "H", 0x0FFF);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectknob1", 0x03, "H", 0x0FFF, false);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectknob2", 0x05, "H", 0x0FFF, false);
MessageShort.addControl("[EffectRack1_EffectUnit1]", "!effectknob3", 0x07, "H", 0x0FFF, false);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "mix", 0x09, "H", 0x0FFF);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectknob1", 0x0B, "H", 0x0FFF, false);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectknob2", 0x0D, "H", 0x0FFF, false);
MessageShort.addControl("[EffectRack1_EffectUnit2]", "!effectknob3", 0x0F, "H", 0x0FFF, false);
MessageShort.addControl("[Channel1]", "!deck_encoder", 0x11, "B", 0x0F, false, this.deckEncoder);
MessageShort.addControl("[Channel2]", "!deck_encoder", 0x12, "B", 0x0F, false, this.deckEncoder);
// ADD Callbacks
MessageShort.setCallback("[Playlist]", "!browse", this.browseEncoder);
MessageShort.setCallback("[Master]", "!shift", this.shift);
MessageShort.setCallback("[Master]", "!browse_press", this.browsePress);
MessageShort.setCallback("[Master]", "!stripe_touch_timestamp", this.stripeTouchTimestamp);
MessageShort.setCallback("[Master]", "!stripe_touch0", this.stripeTouch);
MessageShort.setCallback("[Master]", "!stripe_touch1", this.stripeTouch);
MessageShort.setCallback("[Channel1]", "!deck_encoder_press", this.deckEncoderPress);
MessageShort.setCallback("[Channel1]", "!load_track", this.loadTrackButton);
MessageShort.setCallback("[Channel1]", "!pad1", this.padButton);
MessageShort.setCallback("[Channel1]", "!pad2", this.padButton);
MessageShort.setCallback("[Channel1]", "!pad3", this.padButton);
MessageShort.setCallback("[Channel1]", "!pad4", this.padButton);
MessageShort.setCallback("[Channel1]", "!flux_button", this.introOutroModeButton);
MessageShort.setCallback("[Channel1]", "!sync_enabled", this.syncButton);
MessageShort.setCallback("[Channel1]", "!cue_default", this.cueButton);
MessageShort.setCallback("[Channel1]", "!play", this.playButton);
MessageShort.setCallback("[Channel2]", "!load_track", this.loadTrackButton);
MessageShort.setCallback("[Channel2]", "!deck_encoder_press", this.deckEncoderPress);
MessageShort.setCallback("[Channel2]", "!pad1", this.padButton);
MessageShort.setCallback("[Channel2]", "!pad2", this.padButton);
MessageShort.setCallback("[Channel2]", "!pad3", this.padButton);
MessageShort.setCallback("[Channel2]", "!pad4", this.padButton);
MessageShort.setCallback("[Channel2]", "!flux_button", this.introOutroModeButton);
MessageShort.setCallback("[Channel2]", "!sync_enabled", this.syncButton);
MessageShort.setCallback("[Channel2]", "!cue_default", this.cueButton);
MessageShort.setCallback("[Channel2]", "!play", this.playButton);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effect_focus_button", this.effectFocusButton);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effectbutton1", this.effectButton);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effectbutton2", this.effectButton);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effectbutton3", this.effectButton);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effect_focus_button", this.effectFocusButton);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effectbutton1", this.effectButton);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effectbutton2", this.effectButton);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effectbutton3", this.effectButton);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effectknob1", this.effectKnob);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effectknob2", this.effectKnob);
MessageShort.setCallback("[EffectRack1_EffectUnit1]", "!effectknob3", this.effectKnob);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effectknob1", this.effectKnob);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effectknob2", this.effectKnob);
MessageShort.setCallback("[EffectRack1_EffectUnit2]", "!effectknob3", this.effectKnob);
MessageShort.setCallback("[Channel1]", "!deck_encoder", this.deckEncoder);
MessageShort.setCallback("[Channel2]", "!deck_encoder", this.deckEncoder);
engine.makeConnection("[EffectRack1_EffectUnit1]", "show_parameters", TraktorX1MK2.onShowParametersChange);
engine.makeConnection("[EffectRack1_EffectUnit2]", "show_parameters", TraktorX1MK2.onShowParametersChange);
for (var 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
TraktorX1MK2.scalerParameter.useSetParameter = true;
this.controller.setScaler("mix", this.scalerParameter);
this.controller.setScaler("parameter1", this.scalerParameter);
this.controller.setScaler("parameter2", this.scalerParameter);
this.controller.setScaler("parameter3", this.scalerParameter);
// Register packet
this.controller.registerInputPacket(MessageShort);
};
TraktorX1MK2.registerOutputPackets = function () {
var OutputButtons = new HIDPacket("outputButtons", 0x80);
var OutputDisplay = new HIDPacket("outputDisplay", 0x81);
/*OutputButtons.addOutput("[Master]", "!usblight", 0x1D, "B");*/
OutputButtons.addOutput("[Master]", "!shift", 0x12, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "!effect_focus_button", 0x01, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton1", 0x02, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton2", 0x03, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "!effectbutton3", 0x04, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "!effect_focus_button", 0x05, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton1", 0x06, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton2", 0x07, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "!effectbutton3", 0x08, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", 0x09, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", 0x0A, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", 0x0F, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", 0x10, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "!arrow_left", 0x0B, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit1]", "!arrow_right", 0x0C, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "!arrow_left", 0x0D, "B");
OutputButtons.addOutput("[EffectRack1_EffectUnit2]", "!arrow_right", 0x0E, "B");
OutputButtons.addOutput("[Channel1]", "track_loaded", 0x11, "B");
OutputButtons.addOutput("[Channel1]", "!pad_1_R", 0x14, "B");
OutputButtons.addOutput("[Channel1]", "!pad_1_G", 0x15, "B");
OutputButtons.addOutput("[Channel1]", "!pad_1_B", 0x16, "B");
OutputButtons.addOutput("[Channel1]", "!pad_2_R", 0x17, "B");
OutputButtons.addOutput("[Channel1]", "!pad_2_G", 0x18, "B");
OutputButtons.addOutput("[Channel1]", "!pad_2_B", 0x19, "B");
OutputButtons.addOutput("[Channel1]", "!pad_3_R", 0x20, "B");
OutputButtons.addOutput("[Channel1]", "!pad_3_G", 0x21, "B");
OutputButtons.addOutput("[Channel1]", "!pad_3_B", 0x22, "B");
OutputButtons.addOutput("[Channel1]", "!pad_4_R", 0x23, "B");
OutputButtons.addOutput("[Channel1]", "!pad_4_G", 0x24, "B");
OutputButtons.addOutput("[Channel1]", "!pad_4_B", 0x25, "B");
OutputButtons.addOutput("[Channel1]", "!flux_button", 0x2C, "B");
OutputButtons.addOutput("[Channel1]", "sync_enabled", 0x2D, "B");
OutputButtons.addOutput("[Channel1]", "cue_indicator", 0x30, "B");
OutputButtons.addOutput("[Channel1]", "play_indicator", 0x31, "B");
OutputButtons.addOutput("[Channel2]", "track_loaded", 0x13, "B");
OutputButtons.addOutput("[Channel2]", "!pad_1_R", 0x1A, "B");
OutputButtons.addOutput("[Channel2]", "!pad_1_G", 0x1B, "B");
OutputButtons.addOutput("[Channel2]", "!pad_1_B", 0x1C, "B");
OutputButtons.addOutput("[Channel2]", "!pad_2_R", 0x1D, "B");
OutputButtons.addOutput("[Channel2]", "!pad_2_G", 0x1E, "B");
OutputButtons.addOutput("[Channel2]", "!pad_2_B", 0x1F, "B");
OutputButtons.addOutput("[Channel2]", "!pad_3_R", 0x26, "B");
OutputButtons.addOutput("[Channel2]", "!pad_3_G", 0x27, "B");
OutputButtons.addOutput("[Channel2]", "!pad_3_B", 0x28, "B");
OutputButtons.addOutput("[Channel2]", "!pad_4_R", 0x29, "B");
OutputButtons.addOutput("[Channel2]", "!pad_4_G", 0x2A, "B");
OutputButtons.addOutput("[Channel2]", "!pad_4_B", 0x2B, "B");
OutputButtons.addOutput("[Channel2]", "!flux_button", 0x2E, "B");
OutputButtons.addOutput("[Channel2]", "sync_enabled", 0x2F, "B");
OutputButtons.addOutput("[Channel2]", "cue_indicator", 0x32, "B");
OutputButtons.addOutput("[Channel2]", "play_indicator", 0x33, "B");
this.controller.registerOutputPacket(OutputButtons);
for (i = 0; i < 24; i++) {
OutputDisplay.addOutput("[Channel1]", "!display_segment" + i,
0x01 + i, "B");
OutputDisplay.addOutput("[Channel2]", "!display_segment" + i,
0x19 + i, "B");
}
for (i = 0; i < 21; i++) {
OutputDisplay.addOutput("[Master]", "!stripe_segment" + i + "_R",
0x31 + i * 2, "B");
OutputDisplay.addOutput("[Master]", "!stripe_segment" + i + "_B",
0x32 + i * 2, "B");
}
this.controller.registerOutputPacket(OutputDisplay);
// Link up control objects to their outputs
TraktorX1MK2.linkDeckOutputs("sync_enabled", TraktorX1MK2.outputCallback);
TraktorX1MK2.linkDeckOutputs("cue_indicator", TraktorX1MK2.outputCallback);
TraktorX1MK2.linkDeckOutputs("play_indicator", TraktorX1MK2.outputCallback);
TraktorX1MK2.setPadMode("[Channel1]", TraktorX1MK2.padModes.hotcue);
TraktorX1MK2.setPadMode("[Channel2]", TraktorX1MK2.padModes.hotcue);
TraktorX1MK2.linkDeckOutputs("keylock", TraktorX1MK2.outputCallbackDark);
TraktorX1MK2.linkDeckOutputs("LoadSelectedTrack", TraktorX1MK2.outputCallback);
TraktorX1MK2.linkDeckOutputs("slip_enabled", TraktorX1MK2.outputCallback);
TraktorX1MK2.linkChannelOutput("[Channel1]", "track_loaded", TraktorX1MK2.outputChannelCallback);
TraktorX1MK2.linkChannelOutput("[Channel2]", "track_loaded", TraktorX1MK2.outputChannelCallback);
TraktorX1MK2.linkChannelOutput("[EffectRack1_EffectUnit1]", "group_[Channel1]_enable", TraktorX1MK2.outputChannelCallback);
TraktorX1MK2.linkChannelOutput("[EffectRack1_EffectUnit2]", "group_[Channel1]_enable", TraktorX1MK2.outputChannelCallback);
TraktorX1MK2.linkChannelOutput("[EffectRack1_EffectUnit1]", "group_[Channel2]_enable", TraktorX1MK2.outputChannelCallback);
TraktorX1MK2.linkChannelOutput("[EffectRack1_EffectUnit2]", "group_[Channel2]_enable", TraktorX1MK2.outputChannelCallback);
engine.makeConnection("[EffectRack1_EffectUnit1]", "focused_effect", TraktorX1MK2.onFocusedEffectChange).trigger();
engine.makeConnection("[EffectRack1_EffectUnit2]", "focused_effect", TraktorX1MK2.onFocusedEffectChange).trigger();
TraktorX1MK2.connectEffectButtonLEDs("[EffectRack1_EffectUnit1]");
TraktorX1MK2.connectEffectButtonLEDs("[EffectRack1_EffectUnit2]");
engine.makeConnection("[Channel1]", "rate", TraktorX1MK2.onRateChanged);
engine.makeConnection("[Channel2]", "rate", TraktorX1MK2.onRateChanged);
engine.makeConnection("[Channel1]", "loop_enabled", TraktorX1MK2.onLoopEnabledChanged);
engine.makeConnection("[Channel2]", "loop_enabled", TraktorX1MK2.onLoopEnabledChanged);
engine.makeConnection("[Channel1]", "beatloop_size", TraktorX1MK2.onBeatloopSizeChanged);
engine.makeConnection("[Channel2]", "beatloop_size", TraktorX1MK2.onBeatloopSizeChanged);
engine.makeConnection("[Channel1]", "beatjump_size", TraktorX1MK2.onBeatjumpSizeChanged);
engine.makeConnection("[Channel2]", "beatjump_size", TraktorX1MK2.onBeatjumpSizeChanged);
};
TraktorX1MK2.linkDeckOutputs = function (key, callback) {
// Linking outputs is a little tricky because the library doesn't quite do what I want. But this
// method works.
TraktorX1MK2.controller.linkOutput("[Channel1]", key, "[Channel1]", key, callback);
engine.makeConnection("[Channel3]", key, callback);
TraktorX1MK2.controller.linkOutput("[Channel2]", key, "[Channel2]", key, callback);
engine.makeConnection("[Channel4]", key, callback);
};
TraktorX1MK2.linkChannelOutput = function (group, key, callback) {
TraktorX1MK2.controller.linkOutput(group, key, group, key, callback);
};
TraktorX1MK2.lightGroup = function (packet, outputGroupName, coGroupName) {
var groupObject = packet.groups[outputGroupName];
for (var fieldName in groupObject) {
var field = groupObject[fieldName];
if ((field.name === "!arrow_left") ||
(field.name === "!arrow_right")) {
TraktorX1MK2.controller.setOutput(outputGroupName, field.name,
OutputBrightnessMax,
!TraktorX1MK2.batchingLEDUpdate);
}
if (field.name[0] === "!") {
continue;
}
if (field.mapped_callback) {
var value = engine.getValue(coGroupName, field.name);
field.mapped_callback(value, coGroupName, field.name);
}
// No callback, no light!
}
};
TraktorX1MK2.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];
TraktorX1MK2.lightGroup(packet, group, group);
// These outputs show state managed by this script and do not react to ControlObject changes,
// so manually set them here.
TraktorX1MK2.outputCallback(0, group, "!flux_button");
}
this.batchingLEDUpdate = false;
// And now send them all.
for (packetName in this.controller.OutputPackets) {
this.controller.OutputPackets[packetName].send();
}
};
TraktorX1MK2.lightStripe = function () {
var blueSegments = [1, 2, 3, 4, 5, 6, 7, 8,
12, 13, 14, 15, 16, 17, 18, 19];
var redSegments = [0, 9,
11, 20];
for (var i in blueSegments) {
this.controller.setOutput("[Master]",
"!stripe_segment" + blueSegments[i] + "_B",
OutputBrightnessMax, false);
}
for (var i in redSegments) {
this.controller.setOutput("[Master]",
"!stripe_segment" + redSegments[i] + "_R",
OutputBrightnessMax, false);
}
this.controller.OutputPackets["outputDisplay"].send();
}
TraktorX1MK2.init = function () {
if (!(ShiftCueButtonAction === "REWIND" || ShiftCueButtonAction === "REVERSEROLL")) {
throw new Error("ShiftCueButtonAction must be either \"REWIND\" or \"REVERSEROLL\"\n" +
"ShiftCueButtonAction is: " + ShiftCueButtonAction);
}
if (typeof ButtonBrightnessOff !== "number" ||
ButtonBrightnessOff < 0 ||
ButtonBrightnessOff > OutputBrightnessMax) {
throw new Error("ButtonBrightnessOff must be a number between 0 and 0x7f (127).\n" +
"ButtonBrightnessOff is: " + ButtonBrightnessOff);
}
if (typeof ButtonBrightnessOff !== "number" ||
ButtonBrightnessOff < 0 ||
ButtonBrightnessOff > OutputBrightnessMax) {
throw new Error("ButtonBrightnessOn must be a number between 0 and 0x7f (127).\n" +
"ButtonBrightnessOn is: " + ButtonBrightnessOn);
}
if (ButtonBrightnessOn < ButtonBrightnessOff) {
throw new Error("ButtonBrightnessOn must be greater than ButtonBrightnessOff.\n" +
"ButtonBrightnessOn is: " + ButtonBrightnessOn + "\n" +
"ButtonBrightnessOff is: " + ButtonBrightnessOff);
}
TraktorX1MK2.registerInputPackets();
var debugLEDs = false;
if (debugLEDs) {
var data0 = [0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f
];
var data1 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
];
controller.send(data0, data0.length, 0x82);
controller.send(data1, data1.length, 0x81);
} else {
TraktorX1MK2.registerOutputPackets();
}
/*TraktorX1MK2.controller.setOutput("[Master]", "!usblight", OutputBrightnessMax, true);*/
TraktorX1MK2.controller.setOutput("[Master]", "!shift", ButtonBrightnessOff, true);
TraktorX1MK2.lightDeck("[Channel1]");
TraktorX1MK2.lightDeck("[Channel2]");
TraktorX1MK2.lightDeck("[EffectRack1_EffectUnit1]");
TraktorX1MK2.lightDeck("[EffectRack1_EffectUnit2]");
TraktorX1MK2.lightStripe();
TraktorX1MK2.displayLoopSize("[Channel1]");
TraktorX1MK2.displayLoopSize("[Channel2]");
};
TraktorX1MK2.shutdown = function () {
var data = [];
for (var i = 0; i < 51; i++) {
data[i] = 0;
}
// Leave USB plug indicator light on.
/*data[0x1C] = ButtonBrightnessOn;*/
controller.send(data, data.length, 0x80);
for (i = 0; i < 90; i++) {
data[i] = 0;
}
controller.send(data, data.length, 0x81);
};
TraktorX1MK2.incomingData = function (data, length) {
TraktorX1MK2.controller.parsePacket(data, length);
};
TraktorX1MK2.shortMessageCallback = function (packet, data) {
for (var name in data) {
var field = data[name];
if ((field.name === "!stripe_touch_timestamp") ||
(field.name === "!stripe_touch0") ||
(field.name === "!stripe_touch1") ||
(field.name === "!effectknob1") ||
(field.name === "!effectknob2") ||
(field.name === "!effectknob3") ||
(field.name === "!deck_encoder") ||
(field.name === "!browse") ||
(field.name === "mix")) {
TraktorX1MK2.controller.processControl(field);
continue;
}
TraktorX1MK2.controller.processButton(field);
}
};
TraktorX1MK2.toggleButton = function (field) {
if (field.value > 0) {
script.toggleControl(field.group, field.name);
}
};
TraktorX1MK2.shift = function (field) {
var group = field.group;
TraktorX1MK2.shiftPressed = field.value > 0;
TraktorX1MK2.controller.setOutput(group, "!shift",
TraktorX1MK2.shiftPressed ? ButtonBrightnessOn : ButtonBrightnessOff, group, "!shift",
!TraktorX1MK2.batchingLEDUpdate);
if (TraktorX1MK2.shiftPressed) {
for (var i = 1; i <= 2; i++) {
group = "[Channel" + i + "]";
if (!TraktorX1MK2.syncPressed[group]) {
TraktorX1MK2.displayBeatjumpSize(group);
}
}
} else {
for (var i = 1; i <= 2; i++) {
group = "[Channel" + i + "]";
if (!TraktorX1MK2.syncPressed[group]) {
TraktorX1MK2.displayLoopSize(group);
}
}
}
};
TraktorX1MK2.loadTrackButton = function (field) {
if (TraktorX1MK2.shiftPressed) {
engine.setValue(field.group, "eject", field.value);
} else {
engine.setValue(field.group, "LoadSelectedTrack", field.value);
}
};
TraktorX1MK2.syncButton = function (field) {
var now = Date.now();
var group = field.group;
TraktorX1MK2.syncPressed[group] = field.value > 0;
// If shifted, make this channel explicit master.
if (TraktorX1MK2.shiftPressed) {
if (TraktorX1MK2.syncPressed[group]) {
var sync_master = engine.getValue(group, "sync_master");
engine.setValue(group, "sync_master", !sync_master);
TraktorX1MK2.displayBpm(group);
} else {
TraktorX1MK2.displayBeatjumpSize(group);
}
} else {
if (TraktorX1MK2.syncPressed[group]) {
TraktorX1MK2.syncEnabledTime[group] = now;
TraktorX1MK2.displayBpm(group);
} else {
if (engine.getValue(group, "sync_enabled")) {
// was enabled, and button has been let go. maybe latch it.
engine.setValue(group, "sync_enabled", 0);
} else {
if (!TraktorX1MK2.rateModified &&
((now - TraktorX1MK2.syncEnabledTime[group]) > 300)) {
engine.setValue(group, "sync_enabled", 1);
}
TraktorX1MK2.rateModified = false;
}
TraktorX1MK2.displayLoopSize(group);
}
}
};
TraktorX1MK2.cueButton = function (field) {
if (TraktorX1MK2.shiftPressed) {
if (ShiftCueButtonAction === "REWIND") {
if (field.value === 0) {
return;
}
engine.setValue(field.group, "start_stop", 1);
} else if (ShiftCueButtonAction === "REVERSEROLL") {
engine.setValue(field.group, "reverseroll", field.value);
}
} else {
engine.setValue(field.group, "cue_default", field.value);
}
};
TraktorX1MK2.playButton = function (field) {
if (field.value === 0) {
return;
}
if (TraktorX1MK2.shiftPressed) {
var locked = engine.getValue(field.group, "keylock");
engine.setValue(field.group, "keylock", !locked);
} else {
var playing = engine.getValue(field.group, "play");
var deckNumber = TraktorX1MK2.controller.resolveDeck(field.group);
// Failsafe to disable scratching in case the finishStripeTouch timer has not executed yet
// after a backspin.
if (engine.isScratching(deckNumber)) {
engine.scratchDisable(deckNumber, false);
}
engine.setValue(field.group, "play", !playing);
}
};
TraktorX1MK2.finishStripeCenterTouch = function (timestamp) {
TraktorX1MK2.stripeCenterPressTimer = 0;
TraktorX1MK2.stripeCenterPressed = false;
var previousTime = TraktorX1MK2.stripeCenterPressTime;
if (previousTime > timestamp) {
// We looped around. Adjust current time so that subtraction works.
timestamp += 0x10000;
}
var timeDelta = timestamp - previousTime;
if (timeDelta >= 0x0200) {
}
}
TraktorX1MK2.stripeTouchTimestamp = function (field) {
TraktorX1MK2.stripeTouchTime = field.value;
}
TraktorX1MK2.stripeTouch = function (field) {
var timestamp = TraktorX1MK2.stripeTouchTime;
var group = "[Master]";
var value = field.value;
var index = -1;
if (field.name[13] === "0") {
index = 0;
} else if (field.name[13] === "1") {
index = 1;
}
if ((index < 0) && (index >= TraktorX1MK2.lastTouchChannel.length)) {
return;
}
if (value == 0x0000) {
TraktorX1MK2.stripeTouchRelease(index);
} else if (value <= 0x01C0) {
TraktorX1MK2.lastTouchChannel[index] = 0;
value = 0x01C0 - value;
} else if (value < 0x0240) {
TraktorX1MK2.stripeTouchRelease(index);
if (!TraktorX1MK2.stripeCenterPressed) {
TraktorX1MK2.stripeCenterPressed = true;
TraktorX1MK2.stripeCenterPressTime = timestamp;
}
if (TraktorX1MK2.stripeCenterPressTimer !== 0) {
engine.stopTimer(TraktorX1MK2.stripeCenterPressTimer);
}
TraktorX1MK2.stripeCenterPressTimer = engine.beginTimer(TraktorX1MK2.stripePressTimeoutMilliseconds,
function () {
TraktorX1MK2.finishStripeCenterTouch(timestamp);
}, true);
} else if (value <= 0x0400) {
TraktorX1MK2.lastTouchChannel[index] = 1;
value = 0x0400 - value;
} else {
TraktorX1MK2.stripeTouchRelease(index);
}
var channelIndex = TraktorX1MK2.lastTouchChannel[index];
if ((channelIndex >= 0) &&
(channelIndex < TraktorX1MK2.lastTickValue.length)) {
if (TraktorX1MK2.lastTickValue[channelIndex] == 0) {
TraktorX1MK2.lastTickValue[channelIndex] = value;
} else {
group = "[Channel" + (channelIndex + 1) + "]";
}
}
if (group !== "[Master]") {
var deltas = TraktorX1MK2.stripeDeltas(group, value, timestamp);
var tickDelta = deltas[0];
var timeDelta = deltas[1];
if (engine.getValue(group, "scratch2_enable")) {
var deckNumber = TraktorX1MK2.controller.resolveDeck(group);
if (TraktorX1MK2.shiftPressed) {
tickDelta *= 10;
}
engine.scratchTick(deckNumber, tickDelta);
} else {
var velocity = TraktorX1MK2.scalerStripe(tickDelta, timeDelta, group);
engine.setValue(group, "jog", velocity);
}
}
};
TraktorX1MK2.stripeTouchRelease = function (index) {
var channelIndex = TraktorX1MK2.lastTouchChannel[index];
if ((channelIndex >= 0) &&
(channelIndex < TraktorX1MK2.lastTickValue.length)) {
TraktorX1MK2.lastTickValue[channelIndex] = 0;
}
TraktorX1MK2.lastTouchChannel[index] = -1;
}
TraktorX1MK2.stripeDeltas = function (group, value, timestamp) {
var previousTick = 0;
var previousTime = 0;
if (group[8] === "1" || group[8] === "3") {
previousTick = TraktorX1MK2.lastTickValue[0];
previousTime = TraktorX1MK2.lastTickTime[0];
TraktorX1MK2.lastTickValue[0] = value;
TraktorX1MK2.lastTickTime[0] = timestamp;
} else {
previousTick = TraktorX1MK2.lastTickValue[1];
previousTime = TraktorX1MK2.lastTickTime[1];
TraktorX1MK2.lastTickValue[1] = value;
TraktorX1MK2.lastTickTime[1] = timestamp;
}
if (previousTime > timestamp) {
// We looped around. Adjust current time so that subtraction works.
timestamp += 0x10000;
}
var timeDelta = timestamp - 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 >= 400 && value <= 100) {
tickDelta = value + 448 - previousTick;
} else if (previousTick <= 100 && value >= 400) {
tickDelta = value - previousTick - 448;
} else {
tickDelta = value - previousTick;
}
//HIDDebug(group + " " + value + " " + previousTick + " " + tickDelta);
return [tickDelta, timeDelta];
};
TraktorX1MK2.scalerStripe = function (tickDelta, timeDelta, group) {
if (engine.getValue(group, "play")) {
return (tickDelta / timeDelta) / 3;
} else {
return (tickDelta / timeDelta) * 2.25;
}
};
var introOutroKeys = [
"intro_start",
"intro_end",
"outro_start",
"outro_end"
];
var introOutroColors = [
{ red: 0, green: ButtonBrightnessOn, blue: 0 },
{ red: 0, green: ButtonBrightnessOn, blue: 0 },
{ red: ButtonBrightnessOn, green: 0, blue: 0 },
{ red: ButtonBrightnessOn, green: 0, blue: 0 }
];
var introOutroColorsDim = [
{ red: 0, green: ButtonBrightnessOff, blue: 0 },
{ red: 0, green: ButtonBrightnessOff, blue: 0 },
{ red: ButtonBrightnessOff, green: 0, blue: 0 },
{ red: ButtonBrightnessOff, green: 0, blue: 0 }
];
TraktorX1MK2.setPadMode = function (group, padMode) {
TraktorX1MK2.padConnections[group].forEach(function (connection) {
connection.disconnect();
});
TraktorX1MK2.padConnections[group] = [];
if (padMode === TraktorX1MK2.padModes.hotcue) {
for (var i = 1; i <= 4; i++) {
TraktorX1MK2.padConnections[group].push(
engine.makeConnection(group, "hotcue_" + i + "_status", TraktorX1MK2.outputHotcueCallback));
TraktorX1MK2.padConnections[group].push(
engine.makeConnection(group, "hotcue_" + i + "_color", TraktorX1MK2.outputHotcueCallback));
}
} else if (padMode === TraktorX1MK2.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) {
TraktorX1MK2.sendPadColor(group, padNumber, introOutroColors[padNumber - 1]);
} else {
TraktorX1MK2.sendPadColor(group, padNumber, introOutroColorsDim[padNumber - 1]);
}
};
};
TraktorX1MK2.padConnections[group].push(engine.makeConnection(
group, introOutroKeys[i - 1] + "_enabled", makeIntroOutroCallback(i)));
}
} else if (padMode === TraktorX1MK2.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) {
TraktorX1MK2.sendPadColor(deckGroup, padNumber,
{ red: 0x7f, green: 0x7f, blue: 0 });
} else {
TraktorX1MK2.sendPadColor(deckGroup, padNumber,
{ red: 0xAF, green: 0x00, blue: 0xCC });
}
} else {
TraktorX1MK2.sendPadColor(deckGroup, padNumber, { red: 0x7f, green: 0x7f, blue: 0x7f });
}
} else {
TraktorX1MK2.sendPadColor(deckGroup, padNumber, { red: 0, green: 0, blue: 0 });
}
};
};
var sNumber = group === "[Channel1]" ? i : i + 4;
var sGroup = "[Sampler" + sNumber + "]";
TraktorX1MK2.padConnections[group].push(engine.makeConnection(
sGroup, "track_loaded", makeSamplerCallback(group, i)));
TraktorX1MK2.padConnections[group].push(engine.makeConnection(
sGroup, "play", makeSamplerCallback(group, i)));
TraktorX1MK2.padConnections[group].push(engine.makeConnection(
sGroup, "repeat", makeSamplerCallback(group, i)));
}
}
TraktorX1MK2.padConnections[group].forEach(function (connection) {
connection.trigger();
});
TraktorX1MK2.currentPadMode[group] = padMode;
};
TraktorX1MK2.hotcueButton = function (buttonNumber, group, value) {
if (TraktorX1MK2.shiftPressed) {
engine.setValue(group, "hotcue_" + buttonNumber + "_clear", value);
} else {
engine.setValue(group, "hotcue_" + buttonNumber + "_activate", value);
}
};
TraktorX1MK2.introOutroButton = function (buttonNumber, group, value) {
if (TraktorX1MK2.shiftPressed) {
engine.setValue(group, introOutroKeys[buttonNumber - 1] + "_clear", value);
} else {
engine.setValue(group, introOutroKeys[buttonNumber - 1] + "_activate", value);
}
};
TraktorX1MK2.samplerButton = function (buttonNumber, group, value) {
if (value === 0) {
return;
}
var samplerNumber = group === "[Channel1]" ? buttonNumber : buttonNumber + 4;
var samplerGroup = "[Sampler" + samplerNumber + "]";
if (TraktorX1MK2.shiftPressed) {
if (engine.getValue(samplerGroup, "play") === 1) {
engine.setValue(samplerGroup, "play", 0);
} else {
script.triggerControl(samplerGroup, "eject");
}
} else {
if (engine.getValue(samplerGroup, "track_loaded") === 0) {
script.triggerControl(samplerGroup, "LoadSelectedTrack");
} else {
script.triggerControl(samplerGroup, "cue_gotoandplay");
}
}
};
TraktorX1MK2.padButton = function (field) {
var buttonNumber = parseInt(field.name[field.name.length - 1]);
var padMode = TraktorX1MK2.currentPadMode[field.group];
if (padMode === TraktorX1MK2.padModes.hotcue) {
TraktorX1MK2.hotcueButton(buttonNumber, field.group, field.value);
} else if (padMode === TraktorX1MK2.padModes.introOutro) {
TraktorX1MK2.introOutroButton(buttonNumber, field.group, field.value);
} else if (padMode === TraktorX1MK2.padModes.sampler) {
TraktorX1MK2.samplerButton(buttonNumber, field.group, field.value);
}
};
TraktorX1MK2.introOutroModeButton = function (field) {
if (field.value === 0) {
return;
}
var padMode = TraktorX1MK2.currentPadMode[field.group];
if (padMode !== TraktorX1MK2.padModes.introOutro) {
TraktorX1MK2.setPadMode(field.group, TraktorX1MK2.padModes.introOutro);
TraktorX1MK2.controller.setOutput(field.group, "!flux_button", ButtonBrightnessOn, !TraktorX1MK2.batchingLEDUpdate);
} else {
TraktorX1MK2.setPadMode(field.group, TraktorX1MK2.padModes.hotcue);
TraktorX1MK2.controller.setOutput(field.group, "!flux_button", ButtonBrightnessOff, !TraktorX1MK2.batchingLEDUpdate);
}
};
// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
TraktorX1MK2.connectEffectButtonLEDs = function (effectUnitGroup) {
TraktorX1MK2.effectButtonLEDconnections[effectUnitGroup].forEach(function (connection) {
connection.disconnect();
});
var focusedEffect = engine.getValue(effectUnitGroup, "focused_effect");
var makeButtonLEDcallback = function (effectNumber) {
return function (value, _group, _control) {
TraktorX1MK2.controller.setOutput(effectUnitGroup, "!effectbutton" + effectNumber,
value === 1 ? ButtonBrightnessOn : ButtonBrightnessOff, !TraktorX1MK2.batchingLEDUpdate);
};
};
// FIXME: Why do the LEDs flicker?
TraktorX1MK2.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);
}
TraktorX1MK2.effectButtonLEDconnections[effectUnitGroup][i] = engine.makeConnection(
effectGroup, key, makeButtonLEDcallback(i + 1));
TraktorX1MK2.effectButtonLEDconnections[effectUnitGroup][i].trigger();
}
TraktorX1MK2.batchingLEDUpdate = false;
TraktorX1MK2.effectButtonLEDconnections[effectUnitGroup][2].trigger();
};
// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
TraktorX1MK2.onShowParametersChange = function (value, group, _control) {
if (value === 0) {
if (engine.getValue(group, "show_focus") > 0) {
engine.setValue(group, "show_focus", 0);
TraktorX1MK2.previouslyFocusedEffect[group] = engine.getValue(group, "focused_effect");
engine.setValue(group, "focused_effect", 0);
}
} else {
engine.setValue(group, "show_focus", 1);
if (TraktorX1MK2.previouslyFocusedEffect[group] !== null) {
engine.setValue(group, "focused_effect", TraktorX1MK2.previouslyFocusedEffect[group]);
}
}
TraktorX1MK2.connectEffectButtonLEDs(group);
};
// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
TraktorX1MK2.onFocusedEffectChange = function (value, group, _control) {
TraktorX1MK2.controller.setOutput(group, "!effect_focus_button",
value > 0 ? ButtonBrightnessOn : ButtonBrightnessOff,
!TraktorX1MK2.batchingLEDUpdate);
if (value === 0) {
for (var i = 1; i <= 2; i++) {
// The previously focused effect is not available here, so iterate over all effects' parameter knobs.
for (var j = 1; j < 3; j++) {
engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "parameter" + j);
}
}
} else {
for (i = 1; i <= 2; i++) {
engine.softTakeoverIgnoreNextValue(group.slice(0, -1) + "_Effect" + i + "]", "meta");
}
}
};
// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
TraktorX1MK2.effectFocusButton = function (field) {
var showParameters = engine.getValue(field.group, "show_parameters");
if (field.value > 0) {
var effectUnitNumber = field.group.slice(-2, -1);
if (TraktorX1MK2.shiftPressed) {
engine.setValue(field.group, "load_preset", 1);
return;
}
TraktorX1MK2.effectFocusLongPressTimer[field.group] = engine.beginTimer(TraktorX1MK2.longPressTimeoutMilliseconds, function () {
TraktorX1MK2.effectFocusChooseModeActive[field.group] = true;
TraktorX1MK2.effectButtonLEDconnections[field.group].forEach(function (connection) {
connection.disconnect();
});
var makeButtonLEDcallback = function (buttonNumber) {
return function (value, group, _control) {
TraktorX1MK2.controller.setOutput(group, "!effectbutton" + buttonNumber,
value === buttonNumber ? ButtonBrightnessOn : ButtonBrightnessOff, !TraktorX1MK2.batchingLEDUpdate);
};
};
TraktorX1MK2.batchingLEDUpdate = true;
for (var i = 0; i <= 2; i++) {
TraktorX1MK2.effectButtonLEDconnections[i] = engine.makeConnection(
field.group, "focused_effect", makeButtonLEDcallback(i + 1));
TraktorX1MK2.effectButtonLEDconnections[i].trigger();
}
TraktorX1MK2.batchingLEDUpdate = false;
TraktorX1MK2.effectButtonLEDconnections[2].trigger();
});
if (!showParameters) {
engine.setValue(field.group, "show_parameters", 1);
TraktorX1MK2.effectFocusButtonPressedWhenParametersHidden[field.group] = true;
}
} else {
if (TraktorX1MK2.effectFocusLongPressTimer[field.group] !== 0) {
engine.stopTimer(TraktorX1MK2.effectFocusLongPressTimer[field.group]);
TraktorX1MK2.effectFocusLongPressTimer[field.group] = 0;
}
if (TraktorX1MK2.effectFocusChooseModeActive[field.group]) {
TraktorX1MK2.effectFocusChooseModeActive[field.group] = false;
TraktorX1MK2.connectEffectButtonLEDs(field.group);
} else if (showParameters && !TraktorX1MK2.effectFocusButtonPressedWhenParametersHidden[field.group]) {
engine.setValue(this.group, "show_parameters", 0);
}
TraktorX1MK2.effectFocusButtonPressedWhenParametersHidden[field.group] = false;
}
};
// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
TraktorX1MK2.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 / 4096);
} else {
engine.setParameter(effectUnitGroup.slice(0, -1) + "_Effect" + knobNumber + "]",
"meta",
field.value / 4096);
}
};
// Refer to https://github.com/mixxxdj/mixxx/wiki/standard-effects-mapping for how to use this.
TraktorX1MK2.effectButton = function (field) {
var buttonNumber = parseInt(field.id.slice(-1));
var effectUnitGroup = field.group;
var effectUnitNumber = field.group.match(script.effectUnitRegEx)[1];
var focusedEffect = engine.getValue(effectUnitGroup, "focused_effect");
var toggle = function () {
var group;
var key;
if (focusedEffect === 0) {
group = effectUnitGroup.slice(0, -1) + "_Effect" + buttonNumber + "]";
key = "enabled";
} else {
group = effectUnitGroup.slice(0, -1) + "_Effect" + focusedEffect + "]";
key = "button_parameter" + buttonNumber;
}
script.toggleControl(group, key);
};
if (field.value > 0) {
if (TraktorX1MK2.shiftPressed) {
engine.setValue(effectUnitGroup, "load_preset", buttonNumber + 1);
} else {
if (TraktorX1MK2.effectFocusChooseModeActive[effectUnitGroup]) {
if (focusedEffect === buttonNumber) {
engine.setValue(effectUnitGroup, "focused_effect", 0);
} else {
engine.setValue(effectUnitGroup, "focused_effect", buttonNumber);
}
TraktorX1MK2.effectFocusChooseModeActive[effectUnitGroup] = false;
} else {
toggle();
TraktorX1MK2.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] =
engine.beginTimer(TraktorX1MK2.longPressTimeoutMilliseconds,
function () {
TraktorX1MK2.effectButtonIsLongPressed[effectUnitGroup][buttonNumber] = true;
TraktorX1MK2.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] = 0;
},
true
);
}
}
} else {
engine.stopTimer(TraktorX1MK2.effectButtonLongPressTimer[effectUnitGroup][buttonNumber]);
TraktorX1MK2.effectButtonLongPressTimer[effectUnitGroup][buttonNumber] = 0;
if (TraktorX1MK2.effectButtonIsLongPressed[effectUnitGroup][buttonNumber]) {
toggle();
}
TraktorX1MK2.effectButtonIsLongPressed[effectUnitGroup][buttonNumber] = false;
}
};
/// return value 1 === right turn
/// return value -1 === left turn
TraktorX1MK2.encoderDirection = function (newValue, oldValue) {
var direction = 0;
var min = 0;
var max = 15;
if (oldValue === max && newValue === min) {
direction = 1;
} else if (oldValue === min && newValue === max) {
direction = -1;
} else if (newValue > oldValue) {
direction = 1;
} else {
direction = -1;
}
return direction;
};
TraktorX1MK2.deckEncoder = function (field) {
var group = field.group;
var delta = TraktorX1MK2.encoderDirection(field.value, TraktorX1MK2.previousDeckEncoder[group]);
TraktorX1MK2.previousDeckEncoder[group] = field.value;
if (TraktorX1MK2.syncPressed[group]) {
if (delta === 1) {
script.triggerControl(group, TraktorX1MK2.deckEncoderPressed[group] ?
"rate_perm_up" : "rate_perm_up_small");
} else {
script.triggerControl(group, TraktorX1MK2.deckEncoderPressed[group] ?
"rate_perm_down" : "rate_perm_down_small");
}
TraktorX1MK2.rateModified = true;
} else {
if (TraktorX1MK2.shiftPressed) {
if (TraktorX1MK2.deckEncoderPressed[group]) {
var beatjumpSize = engine.getValue(group, "beatjump_size");
if (delta === 1) {
beatjumpSize *= 2;
} else {
beatjumpSize /= 2;
}
engine.setValue(group, "beatjump_size", beatjumpSize);
} else {
if (delta === 1) {
script.triggerControl(group, "beatjump_forward");
} else {
script.triggerControl(group, "beatjump_backward");
}
}
} else {
if (delta === 1) {
script.triggerControl(group, "loop_double");
} else {
script.triggerControl(group, "loop_halve");
}
}
}
};
TraktorX1MK2.deckEncoderPress = function (field) {
var now = Date.now();
var group = field.group;
var loopEnabled = engine.getValue(group, "loop_enabled");
TraktorX1MK2.deckEncoderPressed[group] = (field.value > 0);
if (TraktorX1MK2.deckEncoderPressed[group]) {
if (TraktorX1MK2.syncPressed[group]) {
TraktorX1MK2.rateResetTime[group] = now;
} else {
if (!TraktorX1MK2.shiftPressed) {
if (loopEnabled) {
script.triggerControl(group, "reloop_toggle");
} else {
script.triggerControl(group, "beatloop_activate");
}
}
}
} else {
if (TraktorX1MK2.syncPressed[group] &&
((now - TraktorX1MK2.rateResetTime[group]) <= 300)) {
engine.setValue(group, "rate", 0);
TraktorX1MK2.rateModified = true;
}
}
};
TraktorX1MK2.browseEncoder = function (field) {
var delta = TraktorX1MK2.encoderDirection(field.value, TraktorX1MK2.previousBrowse);
TraktorX1MK2.previousBrowse = field.value;
if (TraktorX1MK2.shiftPressed) {
engine.setValue("[Playlist]", "SelectPlaylist", delta);
} else {
engine.setValue("[Playlist]", "SelectTrackKnob", delta);
}
};
TraktorX1MK2.browsePress = function (field) {
TraktorX1MK2.browsePressed = (field.value > 0);
if (TraktorX1MK2.browsePressed) {
if (TraktorX1MK2.shiftPressed) {
script.triggerControl("[Library]", "GoToItem");
} else {
script.toggleControl("[Master]", "maximize_library");
}
}
};
TraktorX1MK2.scalerParameter = function (group, name, value) {
return script.absoluteLin(value, 0, 1, 16, 4080);
};
TraktorX1MK2.outputChannelCallback = function (value, group, key) {
var ledValue = 0x05;
if (value) {
ledValue = ButtonBrightnessOn;
}
TraktorX1MK2.controller.setOutput(group, key, ledValue, !TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.outputChannelCallbackDark = function (value, group, key) {
var ledValue = 0x00;
if (value) {
ledValue = ButtonBrightnessOn;
}
TraktorX1MK2.controller.setOutput(group, key, ledValue, !TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.outputCallback = function (value, group, key) {
var ledValue = ButtonBrightnessOff;
if (value) {
ledValue = ButtonBrightnessOn;
}
TraktorX1MK2.controller.setOutput(group, key, ledValue, !TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.outputCallbackLoop = function (value, group, key) {
var ledValue = ButtonBrightnessOff;
if (engine.getValue(group, "loop_enabled")) {
ledValue = ButtonBrightnessOn;
}
TraktorX1MK2.controller.setOutput(group, key, ledValue, !TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.outputCallbackDark = function (value, group, key) {
var ledValue = 0x00;
if (value) {
ledValue = ButtonBrightnessOn;
}
TraktorX1MK2.controller.setOutput(group, key, ledValue, !TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.sendPadColor = function (group, padNumber, color) {
var padKey = "!pad_" + padNumber + "_";
var ColorBrightnessScaler = ButtonBrightnessOn / OutputBrightnessMax;
var red = color.red * ColorBrightnessScaler;
var green = color.green * ColorBrightnessScaler;
var blue = color.blue * ColorBrightnessScaler;
if (color.red === 0 && color.green === 0 && color.blue === 0) {
red = ButtonBrightnessOff;
green = ButtonBrightnessOff;
blue = ButtonBrightnessOff;
}
TraktorX1MK2.controller.setOutput(group, padKey + "R", red, false);
TraktorX1MK2.controller.setOutput(group, padKey + "G", green, false);
TraktorX1MK2.controller.setOutput(group, padKey + "B", blue, !TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.outputHotcueCallback = function (value, group, key) {
var hotcueNumber = key.charAt(7);
var color;
if (engine.getValue(group, "hotcue_" + hotcueNumber + "_status")) {
color = colorCodeToObject(engine.getValue(group, "hotcue_" + hotcueNumber + "_color"));
// hotcue_X_color reports red, green, and blue on a 0-255 scale, but the controller LEDs
// only support values 0 - 127.
color.red /= 2;
color.green /= 2;
color.blue /= 2;
} else {
color = { red: 0, green: 0, blue: 0 };
}
TraktorX1MK2.sendPadColor(group, hotcueNumber, color);
};
TraktorX1MK2.displayCharDigit = function (group, charPos, character) {
// charPost is 0 or 1 for first or second character on the display
// the display is placed like this:
// o -- 4 --
// | |
// 5 3
// | |
// -- 1 --
// | |
// 6 2
// | |
// -- 7 --
// Where the numbers respresent each segment of the display
// and the dot before it is 0 (but this is dealt with in displayCharDot)
var numArray = {
"": [], //empty
0: [2, 3, 4, 5, 6, 7],
1: [2, 3],
2: [1, 3, 4, 6, 7],
3: [1, 2, 3, 4, 7],
4: [1, 2, 3, 5],
5: [4, 5, 1, 2, 7],
6: [4, 5, 1, 2, 6, 7],
7: [4, 3, 2],
8: [1, 2, 3, 4, 5, 6, 7],
9: [5, 4, 1, 3, 2, 7],
// Add a few special characters
"h": [5, 1, 6, 2],
"n": [6, 1, 2],
"o": [6, 1, 2, 7],
"-": [1],
"b": [5, 6, 7, 2, 1],
"c": [1, 6, 7],
"u": [6, 2, 7],
};
for (var j = 0; j < 8; j++) {
var seg = 8 * charPos + j;
var key = "!display_segment" + seg;
TraktorX1MK2.controller.setOutput(
group, key, (numArray[character].indexOf(j) > -1) * OutputBrightnessMax, // if it's in the array turn it on, off otherwise
!TraktorX1MK2.batchingLEDUpdate);
}
};
TraktorX1MK2.displayCharDot = function (group, charPos, on) {
var key = "!display_segment" + 8 * charPos;
TraktorX1MK2.controller.setOutput(group, key, on * OutputBrightnessMax,
!TraktorX1MK2.batchingLEDUpdate);
};
TraktorX1MK2.sendDisplayMessage = function (group,
firstChar, secondChar, thirdChar,
firstDot, secondDot, thirdDot) {
TraktorX1MK2.batchingLEDUpdate = true;
TraktorX1MK2.displayCharDigit(group, 0, firstChar);
TraktorX1MK2.displayCharDigit(group, 1, secondChar);
TraktorX1MK2.displayCharDigit(group, 2, thirdChar);
TraktorX1MK2.displayCharDot(group, 0, firstDot);
TraktorX1MK2.displayCharDot(group, 1, secondDot);
TraktorX1MK2.displayCharDot(group, 2, thirdDot);
TraktorX1MK2.batchingLEDUpdate = false;
TraktorX1MK2.controller.OutputPackets["outputDisplay"].send();
};
TraktorX1MK2.displayValue = function (group, value) {
// handle single digit values
if (value.toString().length === 1) {
TraktorX1MK2.sendDisplayMessage(group,
"", "", value,
false, false, false);
// handle values which have fraction part
} else if (1 > value > 0) {
var inverse_value = 1 / value;
if (inverse_value % 1 === 0) {
if (inverse_value.toString().length === 1) {
TraktorX1MK2.sendDisplayMessage(group, "", "", inverse_value,
false, false, true);
} else if (inverse_value.toString().length === 2) {
TraktorX1MK2.sendDisplayMessage(group,
"",
inverse_value.toString().split("")[0],
inverse_value.toString().split("")[1],
false, true, false);
} else if (inverse_value.toString().length === 3) {
TraktorX1MK2.sendDisplayMessage(group,
inverse_value.toString().split("")[0],
inverse_value.toString().split("")[1],
inverse_value.toString().split("")[2],
true, false, false);
}
}
// values with two digits
} else if (value.toString().length === 2) {
TraktorX1MK2.sendDisplayMessage(group,
"",
value.toString().split("")[0],
value.toString().split("")[1],
false, false, false);
// values with three digits
} else if (value.toString().length === 3) {
var firstChar = value.toString().split("")[0];
var secondChar = value.toString().split("")[1];
var thirdChar = value.toString().split("")[2];
var thirdDot = false;
if (secondChar === ".") {
thirdDot = true;
thirdChar = secondChar;
secondChar = firstChar;
firstChar = "";
}
TraktorX1MK2.sendDisplayMessage(group, firstChar, secondChar, thirdChar,
false, false, thirdDot);
// handle larger values
} else if (value.toString().length > 3) {
var firstChar = value.toString().split("")[0];
var secondChar = value.toString().split("")[1];
var thirdChar = value.toString().split("")[2];
var firstDot = true;
var secondDot = true;
var thirdDot = true;
if (thirdChar === ".") {
firstDot = false;
secondDot = false;
thirdChar = value.toString().split("")[3];
} else if (secondChar === ".") {
firstDot = false;
thirdDot = false;
secondChar = thirdChar;
thirdChar = value.toString().split("")[3];
}
TraktorX1MK2.sendDisplayMessage(group, firstChar, secondChar, thirdChar,
firstDot, secondDot, thirdDot);
}
}
TraktorX1MK2.displayBpm = function (group) {
TraktorX1MK2.displayValue(group, engine.getValue(group, "bpm"));
};
TraktorX1MK2.displayLoopSize = function (group) {
TraktorX1MK2.displayValue(group, engine.getValue(group, "beatloop_size"));
};
TraktorX1MK2.displayBeatjumpSize = function (group) {
TraktorX1MK2.displayValue(group, engine.getValue(group, "beatjump_size"));
};
TraktorX1MK2.onRateChanged = function (value, group, _key) {
if (TraktorX1MK2.syncPressed[group]) {
TraktorX1MK2.displayBpm(group);
}
};
TraktorX1MK2.onLoopEnabledChanged = function (value, group, _key) {
};
TraktorX1MK2.onBeatloopSizeChanged = function (value, group, _key) {
if (!TraktorX1MK2.syncPressed[group] && !TraktorX1MK2.shiftPressed) {
TraktorX1MK2.displayLoopSize(group);
}
};
TraktorX1MK2.onBeatjumpSizeChanged = function (value, group, _key) {
if (!TraktorX1MK2.syncPressed[group] && TraktorX1MK2.shiftPressed) {
TraktorX1MK2.displayBeatjumpSize(group);
}
};
function KontrolZ1Controller () {
this.controller = new HIDController()
this.controller.softTakeoverAll()
this.mode = 'default'
// region CONTROLS
this.registerInputPackets = function () {
var packet = new HIDPacket('control', 0x1)
for (var c = 1; c < 3; c++) {
var o = c * 10
packet.addControl('hid', c + '_gain', o - 9, 'H')
packet.addControl('hid', c + '_hi', o - 7, 'H')
packet.addControl('hid', c + '_mid', o - 5, 'H')
packet.addControl('hid', c + '_low', o - 3, 'H')
packet.addControl('hid', c + '_fx', o - 1, 'H')
}
packet.addControl('hid', 'cue_mix', 21, 'H')
packet.addControl('hid', '1_vol', 23, 'H')
packet.addControl('hid', '2_vol', 25, 'H')
packet.addControl('hid', 'crossfader', 27, 'H')
var button = function (name, code) {
packet.addControl('hid', name, 29, 'B', code)
}
button('mode', 0x2)
button('1_headphone', 0x10)
button('2_headphone', 0x1)
button('1_button_fx', 0x4)
button('2_button_fx', 0x8)
this.controller.registerInputPacket(packet)
}
this.registerCallbacks = function () {
HIDDebug('Registering HID callbacks')
var controller = this.controller
controller.linkModifier('hid', 'mode', 'mode')
for (var channel = 1; channel < 3; channel++) {
controller.setCallback('control', 'hid', channel + '_headphone', function (button) {
var ch = button.name.substr(0, 1)
if (controller.modifiers.get('mode')) {
engine.setParameter('[Channel' + ch + ']', 'cue_default', button.value === controller.buttonStates.pressed)
} else {
if (button.value === controller.buttonStates.pressed) {
controller.toggle('[Channel' + ch + ']', 'pfl')
}
}
})
controller.setCallback('control', 'hid', channel + '_button_fx', function (button) {
var ch = button.name.substr(0, 1)
if (button.value === controller.buttonStates.pressed) {
if (controller.modifiers.get('mode')) {
controller.toggle('[Channel' + ch + ']', 'play')
} else {
var newVal = engine.getParameter('[QuickEffectRack1_[Channel' + ch + ']_Effect1]', 'enabled') ? 0 : 1
engine.setParameter('[EqualizerRack1_[Channel' + ch + ']_Effect1]', 'enabled', newVal)
engine.setParameter('[QuickEffectRack1_[Channel' + ch + ']_Effect1]', 'enabled', newVal)
}
}
})
this.linkKnob('default', channel + '_gain', '[Channel' + channel + ']', 'pregain')
this.linkKnob('default', channel + '_hi', '[EqualizerRack1_[Channel' + channel + ']_Effect1]', 'parameter3')
this.linkKnob('default', channel + '_mid', '[EqualizerRack1_[Channel' + channel + ']_Effect1]', 'parameter2')
this.linkKnob('default', channel + '_low', '[EqualizerRack1_[Channel' + channel + ']_Effect1]', 'parameter1')
this.linkKnob('default', channel + '_fx', '[QuickEffectRack1_[Channel' + channel + ']]', 'super1')
controller.setCallback('control', 'hid', channel + '_gain', this.knob)
controller.setCallback('control', 'hid', channel + '_hi', this.knob)
controller.setCallback('control', 'hid', channel + '_mid', this.knob)
controller.setCallback('control', 'hid', channel + '_low', this.knob)
controller.setCallback('control', 'hid', channel + '_fx', this.knob)
this.linkKnob('default', channel + '_vol', '[Channel' + channel + ']', 'volume')
controller.setCallback('control', 'hid', channel + '_vol', this.knob)
}
this.linkKnob('default', 'cue_mix', '[Master]', 'headMix')
controller.setCallback('control', 'hid', 'cue_mix', this.knob)
this.linkKnob('default', 'crossfader', '[Master]', 'crossfader')
controller.setCallback('control', 'hid', 'crossfader', this.knob)
}
// endregion
// region LIGHTS
this.registerOutputPackets = function () {
var packet = new HIDPacket('lights', 0x80)
for (var c = 1; c < 3; c++) {
for (var i = 1; i < 8; i++) {
packet.addOutput('hid', 'ch' + c + '_meter_segment' + i, i + (c - 1) * 7, 'B')
}
packet.addOutput('hid', c + '_headphone', 14 + c, 'B')
packet.addOutput('hid', c + '_button_fx_red', 14 + c * 3, 'B')
packet.addOutput('hid', c + '_button_fx_blue', 15 + c * 3, 'B')
}
packet.addOutput('hid', 'mode', 19, 'B')
this.controller.registerOutputPacket(packet)
}
this.brightness = 0x7f
this.brightnessRange = 1.0 / 7
this.refreshVolumeLights = function (value, group, key) {
var packet = this.controller.getLightsPacket()
var channel = group.substr(8, 1)
for (var i = 0; i < 7; i++) {
var br = Math.max(Math.min((value - i * this.brightnessRange) * 7, 1), 0) * this.brightness
packet.getField('hid', 'ch' + channel + '_meter_segment' + (i + 1)).value = br
}
this.controller.sendLightsUpdate()
}
// endregion
}
var KontrolZ1 = new KontrolZ1Controller()
KontrolZ1.init = function (id) {
KontrolZ1.id = id
KontrolZ1.registerInputPackets()
KontrolZ1.registerOutputPackets()
KontrolZ1.registerCallbacks()
for (var c = 1; c < 3; c++) {
engine.makeConnection('[Channel' + c + ']', 'vu_meter', KontrolZ1.refreshVolumeLights.bind(this))
KontrolZ1.controller.connectLight('[Channel' + c + ']', 'pfl', function (value, packet, group, name) {
var channel = group.substr(8, 1)
packet.getField('hid', channel + '_headphone').value = value * 0x7F
})
KontrolZ1.controller.connectLight('[QuickEffectRack1_[Channel' + c + ']_Effect1]', 'enabled', function (value, packet, group, name) {
var channel = group.substr(26, 1)
packet.getField('hid', channel + '_button_fx_red').value = value * 0x7F
packet.getField('hid', channel + '_button_fx_blue').value = value * 0x7F
})
}
print('NI Traktor Kontrol Z1 ' + KontrolZ1.id + ' initialized!')
}
// region knobs
KontrolZ1.knobs = {}
KontrolZ1.linkKnob = function (mode, knob, group, name) {
if (!(mode in KontrolZ1.knobs))
KontrolZ1.knobs[mode] = {}
KontrolZ1.knobs[mode][knob] = {
'mode': mode,
'knob': knob,
'group': group,
'name': name,
}
}
KontrolZ1.control = function (control, field) {
if (control.callback !== undefined) {
control.callback(control, field)
return
}
engine.setParameter(control.group, control.name, field.value / 4096)
}
KontrolZ1.knob = function (field) {
var mode = KontrolZ1.knobs[KontrolZ1.mode]
if (mode === undefined) {
HIDDebug('Knob group not mapped in mode ' + KontrolZ1.mode)
return
}
var knob = mode[field.name]
if (knob === undefined) {
HIDDebug('Fader ' + field.name + ' not mapped in ' + KontrolZ1.mode)
return
}
return KontrolZ1.control(knob, field)
}
// endregion
KontrolZ1.shutdown = function () {
KontrolZ1.controller.getLightsPacket().clearControls()
print('NI Traktor Kontrol Z1 ' + KontrolZ1.id + ' shut down!')
}
KontrolZ1.incomingData = function (data, length) {
KontrolZ1.controller.parsePacket(data, length)
}
<?xml version='1.0' encoding='utf-8'?>
<MixxxControllerPreset mixxxVersion="2.0+" schemaVersion="1">
<info>
<name>Native Instruments Traktor Kontrol Z1</name>
<author>Janek Fischer</author>
<description>HID mapping for Native Instruments Traktor Kontrol Z1</description>
</info>
<controller id="Traktor">
<scriptfiles>
<file functionprefix="" filename="common-hid-packet-parser.js"/>
<file filename="common-hid-utils.js" functionprefix=""/>
<file functionprefix="KontrolZ1" filename="Traktor-Kontrol-Z1-scripts.js"/>
</scriptfiles>
</controller>
</MixxxControllerPreset>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment