diff --git a/res/controllers/common-controller-scripts.js b/res/controllers/common-controller-scripts.js index 5b4b94ae0efa..cce9e53641aa 100644 --- a/res/controllers/common-controller-scripts.js +++ b/res/controllers/common-controller-scripts.js @@ -388,6 +388,8 @@ script.samplerRegEx = /^\[Sampler(\d+)\]$/ ; script.channelRegEx = /^\[Channel(\d+)\]$/ ; script.eqRegEx = /^\[EqualizerRack1_(\[.*\])_Effect1\]$/ ; script.quickEffectRegEx = /^\[QuickEffectRack1_(\[.*\])\]$/ ; +script.effectUnitRegEx = /^\[EffectRack1_EffectUnit(\d+)\]$/ ; +script.individualEffectRegEx = /^\[EffectRack1_EffectUnit(\d+)_Effect(\d+)\]$/ ; // ----------------- Object definitions -------------------------- diff --git a/res/controllers/midi-components-0.0.js b/res/controllers/midi-components-0.0.js index 906293d0a9a9..0620553e8b7b 100644 --- a/res/controllers/midi-components-0.0.js +++ b/res/controllers/midi-components-0.0.js @@ -161,6 +161,12 @@ onlyOnPress: true, on: 127, off: 0, + // Time in milliseconds to distinguish a short press from a long press + // In the provided Components, only EffectUnit.effectFocusButton uses + // this, but it is recommended to refer to it (as this.longPressTimeout) + // in any custom Buttons that act differently with short and long presses + // to keep the timeouts uniform. + longPressTimeout: 275, isPress: function (channel, control, value, status) { return value > 0; }, @@ -241,7 +247,8 @@ var HotcueButton = function (options) { if (options.number === undefined) { - print('WARNING: No hotcue number specified for new HotcueButton.'); + print('ERROR: No hotcue number specified for new HotcueButton.'); + return; } this.number = options.number; this.outKey = 'hotcue_' + this.number + '_enabled'; @@ -259,7 +266,8 @@ var SamplerButton = function (options) { if (options.number === undefined) { - print('WARNING: No sampler number specified for new SamplerButton.'); + print('ERROR: No sampler number specified for new SamplerButton.'); + return; } this.number = options.number; this.group = '[Sampler' + this.number + ']'; @@ -312,6 +320,13 @@ outKey: null, // hack to get Component constructor to call connect() }); + var EffectAssignmentButton = function (options) { + options.key = 'group_' + options.group + '_enable'; + options.group = '[EffectRack1_EffectUnit' + options.effectUnit + ']'; + Button.call(this, options); + }; + EffectAssignmentButton.prototype = new Button(); + var Pot = function (options) { Component.call(this, options); @@ -490,10 +505,21 @@ }; var Deck = function (deckNumbers) { - if (deckNumbers !== undefined && Array.isArray(deckNumbers)) { - // These must be unique to each instance, so they cannot be in the prototype. - this.currentDeck = '[Channel' + deckNumbers[0] + ']'; - this.deckNumbers = deckNumbers; + if (deckNumbers !== undefined) { + if (Array.isArray(deckNumbers)) { + // These must be unique to each instance, + // so they cannot be in the prototype. + this.currentDeck = '[Channel' + deckNumbers[0] + ']'; + this.deckNumbers = deckNumbers; + } else if (typeof deckNumbers === 'number' && + Math.floor(deckNumbers) === deckNumbers && + isFinite(deckNumbers)) { + this.currentDeck = '[Channel' + deckNumbers + ']'; + this.deckNumbers = [deckNumbers]; + } + } else { + print('ERROR! new Deck() called without specifying any deck numbers'); + return; } }; Deck.prototype = new ComponentContainer({ @@ -507,11 +533,19 @@ } else if (component.group.search(script.quickEffectRegEx) !== -1) { component.group = '[QuickEffectRack1_' + this.currentDeck + ']'; } - // Do not alter the Component's group if it does not match any of those RegExs because - // that could break effects Components. + // Do not alter the Component's group if it does not match any of those RegExs. + + if (component instanceof EffectAssignmentButton) { + // The ControlObjects for assinging decks to effect units + // indicate the effect unit with the group and the deck with the key, + // so change the key here instead of the group. + component.inKey = 'group_' + newGroup + '_enable'; + component.outKey = 'group_' + newGroup + '_enable'; + } }); }, toggle: function () { + // cycle through deckNumbers array var index = this.deckNumbers.indexOf(parseInt( script.channelRegEx.exec(this.currentDeck)[1] )); @@ -524,9 +558,113 @@ } }); - EffectUnit = function (unitNumber) { + EffectUnit = function (unitNumbers, allowFocusWhenParametersHidden) { var eu = this; - this.group = '[EffectRack1_EffectUnit' + unitNumber + ']'; + this.focusChooseModeActive = false; + + // This is only connected if allowFocusWhenParametersHidden is false. + this.onShowParametersChange = function (value) { + if (value === 0) { + // Prevent this from getting called twice (on button down and button up) + // when show_parameters button is clicked in skin. + // Otherwise this.previouslyFocusedEffect would always be set to 0 + // on the second call. + if (engine.getValue(this.group, 'show_focus') > 0) { + engine.setValue(this.group, 'show_focus', 0); + this.previouslyFocusedEffect = engine.getValue(this.group, + "focused_effect"); + engine.setValue(this.group, "focused_effect", 0); + } + } else { + engine.setValue(this.group, 'show_focus', 1); + if (this.previouslyFocusedEffect !== undefined) { + engine.setValue(this.group, 'focused_effect', + this.previouslyFocusedEffect); + } + } + }; + + this.setCurrentUnit = function (newNumber) { + this.currentUnitNumber = newNumber; + if (allowFocusWhenParametersHidden) { + engine.setValue(this.group, 'show_focus', 0); + } else { + if (this.showParametersConnection !== undefined) { + this.showParametersConnection.disconnect(); + } + delete this.previouslyFocusedEffect; + } + + this.group = '[EffectRack1_EffectUnit' + newNumber + ']'; + + if (allowFocusWhenParametersHidden) { + engine.setValue(this.group, 'show_focus', 1); + } else { + // Connect a callback to show_parameters changing instead of + // setting show_focus when effectFocusButton is pressed so + // show_focus is always in the correct state, even if the user + // presses the skin button for show_parameters. + this.showParametersConnection = engine.connectControl(this.group, + 'show_parameters', + this.onShowParametersChange); + this.showParametersConnection.trigger(); + } + + // Do not enable soft takeover upon EffectUnit construction + // so initial values can be loaded from knobs. + if (this.hasInitialized === true) { + for (var n = 1; n <= 3; n++) { + var effect = '[EffectRack1_EffectUnit' + this.currentUnitNumber + + '_Effect' + n + ']'; + engine.softTakeover(effect, 'meta', true); + engine.softTakeover(effect, 'parameter1', true); + engine.softTakeover(effect, 'parameter2', true); + engine.softTakeover(effect, 'parameter3', true); + } + } + + this.reconnectComponents(function (component) { + // update [EffectRack1_EffectUnitX] groups + var unitMatch = component.group.match(script.effectUnitRegEx); + if (unitMatch !== null) { + component.group = eu.group; + } else { + // update [EffectRack1_EffectUnitX_EffectY] groups + var effectMatch = component.group.match(script.individualEffectRegEx); + if (effectMatch !== null) { + component.group = '[EffectRack1_EffectUnit' + + eu.currentUnitNumber + + '_Effect' + effectMatch[2] + ']'; + } + } + }); + }; + + this.toggle = function () { + // cycle through unitNumbers array + var index = this.unitNumbers.indexOf(this.currentUnitNumber); + if (index === (this.unitNumbers.length - 1)) { + index = 0; + } else { + index += 1; + } + this.setCurrentUnit(this.unitNumbers[index]); + } + + if (unitNumbers !== undefined) { + if (Array.isArray(unitNumbers)) { + this.unitNumbers = unitNumbers; + this.setCurrentUnit(unitNumbers[0]); + } else if (typeof unitNumbers === 'number' && + Math.floor(unitNumbers) === unitNumbers && + isFinite(unitNumbers)) { + this.unitNumbers = [unitNumbers]; + this.setCurrentUnit(unitNumbers); + } + } else { + print('ERROR! new EffectUnit() called without specifying any unit numbers!'); + return; + } this.dryWetKnob = new Pot({ group: this.group, @@ -541,7 +679,11 @@ // for soft takeover this.disconnect(); this.connect(); - eu.knobs.reconnectComponents(); + // engine.softTakeoverIgnoreNextValue is called + // in the knobs' onFocusChange function + eu.knobs.forEachComponent(function (knob) { + knob.trigger(); + }); }, outConnect: false, }); @@ -554,110 +696,114 @@ outConnect: false, }); }; - this.enableOnChannelButtons.addButton('Channel1'); - this.enableOnChannelButtons.addButton('Channel2'); - this.enableOnChannelButtons.addButton('Channel3'); - this.enableOnChannelButtons.addButton('Channel4'); - this.enableOnChannelButtons.addButton('Headphone'); - this.enableOnChannelButtons.addButton('Master'); - this.enableOnChannelButtons.addButton('Microphone'); - this.enableOnChannelButtons.addButton('Auxiliary1'); this.EffectUnitKnob = function (number) { this.number = number; Pot.call(this); }; this.EffectUnitKnob.prototype = new Pot({ - onParametersHide: function () { - this.group = '[EffectRack1_EffectUnit' + unitNumber + '_Effect' + this.number + ']'; - this.inKey = 'meta'; + group: this.group, + unshift: function () { + this.input = function (channel, control, value, status, group) { + if (this.MSB !== undefined) { + value = (this.MSB << 7) + value; + } + this.inSetParameter(this.inValueScale(value)); + + if (this.previousValueReceived === undefined) { + var effect = '[EffectRack1_EffectUnit' + eu.currentUnitNumber + + '_Effect' + this.number + ']'; + engine.softTakeover(effect, 'meta', true); + engine.softTakeover(effect, 'parameter1', true); + engine.softTakeover(effect, 'parameter2', true); + engine.softTakeover(effect, 'parameter3', true); + } + this.previousValueReceived = value; + }; }, - onParametersShow: function () { - var focusedEffect = engine.getValue(eu.group, "focused_effect"); - if (focusedEffect === 0) { - // manipulate metaknobs - this.onParametersHide(); + shift: function () { + this.valueAtLastEffectSwitch = this.previousValueReceived; + // Floor the threshold to ensure that every effect can be selected + this.changeThreshold = Math.floor(this.max / + engine.getValue('[Master]', 'num_effectsavailable')); + + this.input = function (channel, control, value, status, group) { + if (this.MSB !== undefined) { + value = (this.MSB << 7) + value; + } + var change = value - this.valueAtLastEffectSwitch; + if (Math.abs(change) >= this.changeThreshold) { + var effectGroup = '[EffectRack1_EffectUnit' + + eu.currentUnitNumber + '_Effect' + + this.number + ']'; + engine.setValue(effectGroup, 'effect_selector', change); + this.valueAtLastEffectSwitch = value; + } + + this.previousValueReceived = value; + }; + }, + outKey: "focused_effect", + connect: function () { + this.connections[0] = engine.connectControl(eu.group, "focused_effect", + this.onFocusChange); + }, + disconnect: function () { + engine.softTakeoverIgnoreNextValue(this.group, this.inKey); + this.connections[0].disconnect(); + }, + trigger: function () { + this.connections[0].trigger(); + }, + onFocusChange: function (value, group, control) { + if (value === 0) { + this.group = '[EffectRack1_EffectUnit' + + eu.currentUnitNumber + '_Effect' + + this.number + ']'; + this.inKey = 'meta'; } else { - this.group = '[EffectRack1_EffectUnit' + unitNumber + '_Effect' + - focusedEffect + ']'; + this.group = '[EffectRack1_EffectUnit' + eu.currentUnitNumber + + '_Effect' + value + ']'; this.inKey = 'parameter' + this.number; } + engine.softTakeoverIgnoreNextValue(this.group, this.inKey); }, }); this.EffectEnableButton = function (number) { this.number = number; - this.group = '[EffectRack1_EffectUnit' + unitNumber + '_Effect' + number + ']'; + this.group = '[EffectRack1_EffectUnit' + eu.currentUnitNumber + + '_Effect' + this.number + ']'; Button.call(this); }; this.EffectEnableButton.prototype = new Button({ - onParametersHide: function () { - this.input = Button.prototype.input; - this.outKey = 'enabled'; - this.unshift = function () { - this.isShifted = false; - this.inKey = 'enabled'; - this.onlyOnPress = true; - }; - this.shift = function () { - this.isShifted = true; - this.inKey = 'next_effect'; - this.onlyOnPress = false; - }; - if (this.isShifted) { - this.shift(); - } else { - this.unshift(); - } - }, - onParametersShow: function () { + stopEffectFocusChooseMode: function () { this.inKey = 'enabled'; this.outKey = 'enabled'; - this.unshift = function () { - this.isShifted = false; - this.input = Button.prototype.input; - this.onlyOnPress = true; - }; - this.shift = function () { - this.isShifted = true; - this.input = function (channel, control, value, status, group) { - if (this.isPress(channel, control, value, status)) { - if (engine.getValue(eu.group, "focused_effect") === this.number) { - // unfocus and make knobs control metaknobs - engine.setValue(eu.group, "focused_effect", 0); - } else { - // focus this effect - engine.setValue(eu.group, "focused_effect", this.number); - } + this.input = Button.prototype.input; + this.connect = Button.prototype.connect; + this.output = Button.prototype.output; + }, + startEffectFocusChooseMode: function () { + this.input = function (channel, control, value, status, group) { + if (this.isPress(channel, control, value, status)) { + if (engine.getValue(eu.group, "focused_effect") === this.number) { + // unfocus and make knobs control metaknobs + engine.setValue(eu.group, "focused_effect", 0); + } else { + // focus this effect + engine.setValue(eu.group, "focused_effect", this.number); } - }; + } }; this.connect = function () { - this.connections[0] = engine.connectControl(this.group, "enabled", Button.prototype.output); - this.connections[1] = engine.connectControl(eu.group, "focused_effect", this.onFocusChanged); + this.connections[0] = engine.connectControl(eu.group, + "focused_effect", + this.output); }; - this.onFocusChanged = function (value, group, control) { - if (value === this.number) { - // make knobs control first 3 parameters of the focused effect - eu.knobs.reconnectComponents(function (knob) { - if (typeof knob.onParametersShow === 'function') { - knob.onParametersShow(); // to set new group property - } - }); - } else if (value === 0) { - // make knobs control metaknobs - eu.knobs.reconnectComponents(function (knob) { - if (typeof knob.onParametersShow === 'function') { - knob.onParametersHide(); // to set new group property - } - }); - } + this.output = function (value, group, control) { + this.send(value === this.number); }; - if (this.isShifted) { - this.shift(); - } else { - this.unshift(); - } }, }); @@ -668,41 +814,77 @@ this.enableButtons[n] = new this.EffectEnableButton(n); } - this.showParametersButton = new Button({ + this.effectFocusButton = new Button({ group: this.group, - key: 'show_parameters', - output: function (value, group, control) { - this.send((value > 0) ? this.on : this.off); - if (value === 0) { - engine.setValue(this.group, "show_focus", 0); - // NOTE: calling eu.reconnectComponents() here would cause an infinite loop when - // calling EffectUnit.reconnectComponents(). - eu.forEachComponent(function (c) { - if (typeof c.onParametersHide === 'function') { - c.disconnect(); - c.onParametersHide(); - c.connect(); - c.trigger(); + longPressed: false, + longPressTimer: 0, + pressedWhenParametersHidden: false, + previouslyFocusedEffect: 0, + startEffectFocusChooseMode: function () { + eu.focusChooseModeActive = true; + eu.enableButtons.reconnectComponents(function (button) { + button.startEffectFocusChooseMode(); + }); + }, + unshift: function () { + this.input = function (channel, control, value, status, group) { + var showParameters = engine.getValue(this.group, "show_parameters"); + if (this.isPress(channel, control, value, status)) { + this.longPressTimer = engine.beginTimer(this.longPressTimeout, + this.startEffectFocusChooseMode, + true); + if (!showParameters) { + if (!allowFocusWhenParametersHidden) { + engine.setValue(this.group, "show_parameters", 1); + // eu.onShowParametersChange will refocus the + // previously focused effect and show focus in skin + } + this.pressedWhenParametersHidden = true; } - }); - } else { - engine.setValue(this.group, "show_focus", 1); - eu.forEachComponent(function (c) { - if (typeof c.onParametersShow === 'function') { - c.disconnect(); - c.onParametersShow(); - c.connect(); - c.trigger(); + } else { + if (this.longPressTimer) { + engine.stopTimer(this.longPressTimer); } - }); - } + + if (eu.focusChooseModeActive) { + eu.enableButtons.reconnectComponents(function (button) { + button.stopEffectFocusChooseMode(); + }); + eu.focusChooseModeActive = false; + } else { + if (!showParameters && allowFocusWhenParametersHidden) { + engine.setValue(this.group, "show_parameters", 1); + } else if (showParameters && !this.pressedWhenParametersHidden) { + engine.setValue(this.group, "show_parameters", 0); + // eu.onShowParametersChange will save the focused effect, + // unfocus, and hide focus buttons in skin + } + } + this.pressedWhenParametersHidden = false; + } + }; + }, + shift: function () { + this.input = function (channel, control, value, status, group) { + if (this.isPress(channel, control, value, status)) { + eu.toggle(); + } + }; + }, + outKey: 'focused_effect', + output: function (value, group, control) { + this.send((value > 0) ? this.on : this.off); }, outConnect: false, }); this.init = function () { - this.showParametersButton.connect(); - this.showParametersButton.trigger(); + this.knobs.reconnectComponents(); + this.enableButtons.reconnectComponents(function (button) { + button.stopEffectFocusChooseMode(); + }); + this.effectFocusButton.connect(); + this.effectFocusButton.trigger(); this.enableOnChannelButtons.forEachComponent(function (button) { if (button.midi !== undefined) { @@ -717,6 +899,8 @@ component.group = eu.group; } }); + + this.hasInitialized = true; }; }; EffectUnit.prototype = new ComponentContainer(); @@ -730,6 +914,7 @@ exports.LoopToggleButton = LoopToggleButton; exports.HotcueButton = HotcueButton; exports.SamplerButton = SamplerButton; + exports.EffectAssignmentButton = EffectAssignmentButton; exports.Pot = Pot; exports.Encoder = Encoder; exports.ComponentContainer = ComponentContainer;