Created
November 5, 2023 01:26
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/****************************************************************/ | |
/* 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); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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