/* global controller */ /** Common HID script debugging function. Just to get logging with 'HID' prefix. */ var 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. */ var 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 */ var 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("Unknown 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. */ var 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 (var 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 (var 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) { control_group = this.groups[group_name]; for (var field_name in control_group) { var field = control_group[field_name]; if (field === undefined || field.type !== "bitvector") continue; for (var 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 (var 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 * @param callback callback function for the control */ HIDPacket.prototype.addControl = function(group, name, offset, pack, bitmask, isEncoder, callback) { 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); if (callback !== undefined) { if (typeof callback !== "function") { HIDDebug("ERROR callback provided for " + group + "." + name + " is not a function."); return; } this.setCallback(group, name, callback); } 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 var 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; if (callback !== undefined) { if (typeof callback !== "function") { HIDDebug("ERROR callback provided for " + group + "." + name + " is not a function."); return; } this.setCallback(group, name, callback); } }; /** 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 var 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) { var 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.id] = 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 (var 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 */ var 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); } var 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) { var 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; var 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; 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")); 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 definition) * * 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; } } var 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]; 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; 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; 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); 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.connectControl(controlgroup, m_name, callback); 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(); };