/** * American Audio VMS4 controller script v2.1 for Mixxx v2.1.x * Copyright (C) 2010 Anders Gunnarsson * Copyright (C) 2011-2018 Sean M. Pappalardo * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ VMS4 = new Controller(); VMS4.RateRanges = [0.08, 0.10, 0.30, 1.00]; //---- VMS4.id = ""; // The ID for the particular device being controlled for use in debugging, set at init time VMS4.numHotCues = 8; // Button control number to hot cue mapping VMS4.hotCues = { 0x12:1, 0x13:2, 0x14:3, 0x15:4, 0x17:5, 0x18:6, 0x19:7, 0x1A:8, 0x34:1, 0x35:2, 0x36:3, 0x37:4, 0x39:5, 0x3A:6, 0x3B:7, 0x3C:8 }; VMS4.initControls = [ ["Channel", "hotcue_x_enabled"], ["Channel", "quantize"], ["Channel", "sync_enabled"], ["Channel", "loop_in"], ["Channel", "loop_out"], ["Channel", "loop_enabled"], ["Channel", "loop_halve"], ["Channel", "loop_double"], ["Channel", "play"], ["Channel", "cue_default"], ["Channel", "back"], ["Channel", "fwd"], ["Channel", "keylock"], ["Channel", "rate_temp_up"], ["Channel", "rate_temp_down"], ["Sampler", "play"] ]; VMS4.init = function (id) { // called when the MIDI device is opened & set up VMS4.id = id; // Store the ID of this device for later use for (i=12; i<=77; i++) midi.sendShortMsg(0x80,i,0x00); // Extinquish all LEDs // Enable soft-takeover for all direct hardware controls // (Many of these are mapped in the XML directly so these have no effect. // Here for completeness in case any eventually move to script.) engine.softTakeover("[Master]","crossfader",true); engine.softTakeover("[Channel1]","rate",true); engine.softTakeover("[Channel1]","volume",true); engine.softTakeover("[Channel1]","pregain",true); engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]","parameter3",true); engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]","parameter2",true); engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]","parameter1",true); engine.softTakeover("[Channel2]","rate",true); engine.softTakeover("[Channel2]","volume",true); engine.softTakeover("[Channel2]","pregain",true); engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]","parameter3",true); engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]","parameter2",true); engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]","parameter1",true); engine.softTakeover("[Channel3]","rate",true); engine.softTakeover("[Channel3]","volume",true); engine.softTakeover("[Channel3]","pregain",true); engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]","parameter3",true); engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]","parameter2",true); engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]","parameter1",true); engine.softTakeover("[Channel4]","rate",true); engine.softTakeover("[Channel4]","volume",true); engine.softTakeover("[Channel4]","pregain",true); engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]","parameter3",true); engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]","parameter2",true); engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]","parameter1",true); engine.softTakeover("[Sampler1]","rate",true); engine.softTakeover("[Sampler1]","pregain",true); engine.softTakeover("[Sampler2]","rate",true); engine.softTakeover("[Sampler2]","pregain",true); engine.softTakeover("[EffectRack1_EffectUnit1]","mix",true); engine.softTakeover("[EffectRack1_EffectUnit1]","super1",true); engine.softTakeover("[EffectRack1_EffectUnit2]","mix",true); engine.softTakeover("[EffectRack1_EffectUnit2]","super1",true); engine.softTakeover("[EffectRack1_EffectUnit3]","mix",true); engine.softTakeover("[EffectRack1_EffectUnit4]","super1",true); engine.softTakeover("[EffectRack1_EffectUnit4]","mix",true); engine.softTakeover("[EffectRack1_EffectUnit4]","super1",true); print("American Audio "+VMS4.id+" initialized."); } VMS4.shutdown = function () { for (i=12; i<=77; i++) midi.sendShortMsg(0x80,i,0x00); // Extinquish all LEDs print("American Audio "+VMS4.id+" shut down."); } VMS4.Button = Button; VMS4.Button.prototype.setLed = function(ledState) { if(ledState == LedState.on) { midi.sendShortMsg(0x90,this.controlId,LedState.on); } else { midi.sendShortMsg(0x80,this.controlId,LedState.off); } } VMS4.Deck = Deck; VMS4.Deck.jogMsb = 0x00; VMS4.Deck.scratchncue = false; VMS4.Deck.hotCueDeleted = false; VMS4.Deck.keylockButton = false; VMS4.Deck.vinylButton = false; VMS4.Deck.cueButton = false; VMS4.Deck.hotCuePressed = false; VMS4.Deck.pitchLock = false; VMS4.Deck.controlEffectParameter = false; VMS4.Deck.effectSelect = -1; VMS4.Deck.sampleSelect = -1; VMS4.Deck.prototype.rateRangeHandler = function(value) { if(value == ButtonState.pressed) { this.Buttons.RateRange.setLed(LedState.on); // Round to two decimals to avoid double-precision comparison issues var currentRange = Math.round(engine.getValue(this.group, "rateRange")*100)/100; switch (true) { case (currentRange=VMS4.RateRanges[3]): engine.setValue(this.group,"rateRange",VMS4.RateRanges[0]); break; } // Update the screen display engine.trigger(this.group,"rate"); } else this.Buttons.RateRange.setLed(LedState.off); } VMS4.Deck.prototype.pitchCenterHandler = function(value) { // Reset pitch only on entrance to center position if(value == ButtonState.pressed) { this.pitchLock=true; engine.setValue(this.group, "rate", 0); } else { this.pitchLock=false; } } VMS4.Deck.prototype.jogTouchHandler = function(value) { if(value == ButtonState.pressed) { engine.scratchEnable(this.deckNumber, 3000, 45, 1.0/8, (1.0/8)/32); // Recall the cue point if in "scratch & cue" mode only when playing if (this.scratchncue && engine.getValue(this.group,"play")==1) { engine.setValue(this.group,"cue_goto",1); engine.setValue(this.group,"cue_goto",0); } } else { engine.scratchDisable(this.deckNumber); } } VMS4.Deck.prototype.jogMove = function(lsbValue) { var jogValue = (this.jogMsb << 7) + lsbValue; if(!isNaN(this.previousJogValue)) { var offset = jogValue - this.previousJogValue; if(offset > 8192) { offset = offset - 16384; } else if(offset < -8192) { offset = offset + 16384; } if(engine.isScratching(this.deckNumber)) { engine.scratchTick(this.deckNumber, offset); } else { engine.setValue(this.group,"jog", offset / 40.0); } } this.previousJogValue = jogValue; } VMS4.Deck.prototype.vinylButtonHandler = function(value) { if(value == ButtonState.pressed) this.vinylButton=true; else { this.vinylButton=false; // Force keylock up too since they're they same physical button // (This prevents keylock getting stuck down if shift is released first) this.keylockButton=false; } } VMS4.Deck.prototype.keyLockButtonHandler = function(value) { if(value == ButtonState.pressed) { this.keylockButton=true; this.hotCueDeleted=false; } else { // Toggle keylock only on release and only if a hot cue wasn't deleted if (!this.hotCueDeleted) { var currentKeylock = engine.getValue(this.group, "keylock"); engine.setValue(this.group, "keylock", !currentKeylock); } this.keylockButton=false; // Force vinyl up too since they're they same physical button // (This prevents vinyl getting stuck down if shift is pressed while // vinyl is held down) this.vinylButton=false; } } VMS4.Deck.prototype.effectParamButtonHandler = function(value) { // Couldn't get this to work. The buttons are mapped instead to VMS4.effectParameterButton // if(value == ButtonState.pressed) { // this.controlEffectParameter=!this.controlEffectParameter; // if (this.controlEffectParameter) { // // Super knob // this.Buttons.FXParam.setLed(LedState.on); // } // else { // // Wet/dry // this.Buttons.FXParam.setLed(LedState.off); // } // } } VMS4.Decks = {"Left":new VMS4.Deck(1,"[Channel1]"), "Right":new VMS4.Deck(2,"[Channel2]")}; VMS4.GroupToDeck = {"[Channel1]":"Left", "[Channel2]":"Right"}; VMS4.GroupToDeckNum = {"[Channel1]":1, "[Channel2]":2, "[Channel3]":3, "[Channel4]":4}; VMS4.StripToSide = {0x28:"Left", 0x2D:"Right"}; VMS4.touchStripPos = {"Left":null, "Right":null}; // Stores the position on touch // to convert the absolute sliders // to relative ones VMS4.GetDeck = function(group) { try { return VMS4.Decks[VMS4.GroupToDeck[group]]; } catch(ex) { return null; } } VMS4.GetDeckNum = function(group) { try { return VMS4.GroupToDeckNum[group]; } catch(ex) { return null; } } VMS4.Decks.Left.addButton("RateRange", new VMS4.Button(0x11), "rateRangeHandler"); VMS4.Decks.Left.addButton("PitchCenter", new VMS4.Button(), "pitchCenterHandler"); VMS4.Decks.Left.addButton("JogTouch", new VMS4.Button(), "jogTouchHandler"); VMS4.Decks.Left.addButton("Vinyl", new VMS4.Button(), "vinylButtonHandler"); VMS4.Decks.Left.addButton("KeyLock", new VMS4.Button(), "keyLockButtonHandler"); VMS4.Decks.Left.addButton("FXParam", new VMS4.Button(0x1C), "effectParamButtonHandler"); VMS4.Decks.Right.addButton("RateRange", new VMS4.Button(0x33), "rateRangeHandler"); VMS4.Decks.Right.addButton("PitchCenter", new VMS4.Button(), "pitchCenterHandler"); VMS4.Decks.Right.addButton("JogTouch", new VMS4.Button(), "jogTouchHandler"); VMS4.Decks.Right.addButton("Vinyl", new VMS4.Button(), "vinylButtonHandler"); VMS4.Decks.Right.addButton("KeyLock", new VMS4.Button(), "keyLockButtonHandler"); VMS4.Decks.Right.addButton("FXParam", new VMS4.Button(0x3E), "effectParamButtonHandler"); //Mapping functions VMS4.rate_range = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.Buttons.RateRange.handleEvent(value); } VMS4.pitch = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); if (!deck.pitchLock) { // engine.setValue(group, "rate", script.pitch(control,value,status)); // Can't use script.pitch() because the VMS4's sliders are inverted var value = (value << 7) | control; // Construct the 14-bit number // Range is 0x0000..0x3FFF center @ 0x2000, i.e. 0..16383 center @ 8192 var rate = (8192-value)/8191; engine.setValue(group, "rate", rate); } } VMS4.pitchCenter = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.Buttons.PitchCenter.handleEvent(value); } VMS4.effectSelect = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); if (deck.effectSelect == -1 || isNaN(deck.effectSelect)) deck.effectSelect = value; // Take wrap around into account var wrapCount = 0; if (value>=0 && value<10 && deck.effectSelect>117 && deck.effectSelect<=127) wrapCount+=1; if (value>117 && value<=127 && deck.effectSelect>=0 && deck.effectSelect<10) wrapCount-=1; var diff = value - deck.effectSelect; deck.effectSelect=value; diff += wrapCount*128; engine.setValue("[EffectRack1_EffectUnit"+VMS4.GetDeckNum(group)+"_Effect1]", "effect_selector",diff); } VMS4.effectSelectPress = function(channel, control, value, status, group) { var deckNum = VMS4.GetDeckNum(group); if (value > 0x40) { script.toggleControl("[EffectRack1_EffectUnit"+deckNum+"_Effect1]","enabled"); } } VMS4.effectControl = function(channel, control, value, status, group) { // If Parameter button is on, this becomes a super knob. Otherwise it controls wet/dry var deck = VMS4.GetDeck(group); var deckNum = VMS4.GetDeckNum(group); if (deck.controlEffectParameter) { engine.setParameter("[EffectRack1_EffectUnit"+deckNum+"]","super1",value/0x7F); } else { engine.setParameter("[EffectRack1_EffectUnit"+deckNum+"]","mix",value/0x7F); } } VMS4.effectParameterButton = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); // deck.Buttons.FXParam.handleEvent(group, value); if(value > 0x40) { var deckNum = VMS4.GetDeckNum(group); deck.controlEffectParameter=!deck.controlEffectParameter; if (deck.controlEffectParameter) { // Super knob deck.Buttons.FXParam.setLed(LedState.on); // Ignore the next wet/dry value engine.softTakeoverIgnoreNextValue("[EffectRack1_EffectUnit"+deckNum+"]","mix"); } else { // Wet/dry deck.Buttons.FXParam.setLed(LedState.off); // Ignore the next Super knob value engine.softTakeoverIgnoreNextValue("[EffectRack1_EffectUnit"+deckNum+"]","super1"); } } } VMS4.sampleSelect = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); if (deck.sampleSelect == -1) deck.sampleSelect = value; // Take wrap around into account var wrapCount = 0; if (value>=0 && value<10 && deck.sampleSelect>117 && deck.sampleSelect<=127) wrapCount+=1; if (value>117 && value<=127 && deck.sampleSelect>=0 && deck.sampleSelect<10) wrapCount-=1; var diff = value - deck.sampleSelect; deck.sampleSelect=value; diff += wrapCount*128; engine.setValue("[Playlist]","SelectTrackKnob",diff); } VMS4.jog_touch = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.Buttons.JogTouch.handleEvent(value); } VMS4.jog_move_lsb = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.jogMove(value); } VMS4.jog_move_msb = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.jogMsb = value; } VMS4.strip_fx_dw = function(channel, control, value, status, group) { var deck = VMS4.GetDeckNum(group); // When the deck is playing, adjust effect dry/wet. When it isn't, do needle drop. if (engine.getValue(group, "play") === 0) { engine.setValue(group, "playposition", value/0x7F); } else { engine.setParameter("[EffectRack1_EffectUnit"+deck+"]","mix",value/0x7F); } } VMS4.strip_touch = function(channel, control, value, status, group) { var deck = VMS4.GetDeckNum(group); if (deck === 1) { // Left side if ((status & 0xF0) === 0x80) { // If button up VMS4.touchStripPos["Left"] = null; } } else { // Right side if ((status & 0xF0) === 0x80) { // If button up VMS4.touchStripPos["Right"] = null; } } } VMS4.strip_scroll = function(channel, control, value, status, group) { var side = VMS4.StripToSide[control]; if (VMS4.touchStripPos[side] !== null) { // Higher on the strip gives a higher value, and up is negative on Library // scroll controls if (side === "Left") { engine.setValue("[Playlist]", "SelectPlaylist", VMS4.touchStripPos[side] - value); } else { engine.setValue("[Playlist]", "SelectTrackKnob", VMS4.touchStripPos[side] - value); } } VMS4.touchStripPos[side] = value; } VMS4.vinyl = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.Buttons.Vinyl.handleEvent(value); } VMS4.keylock = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); deck.Buttons.KeyLock.handleEvent(value); } VMS4.hotCue = function(channel, control, value, status, group) { var deck = VMS4.GetDeck(group); var hotCue = VMS4.hotCues[control]; if(value == ButtonState.pressed) { deck.hotCuePressed=true; if (deck.vinylButton || deck.keylockButton) { // print("vinyl="+deck.vinylButton+" keylock="+deck.keylockButton+" so clear hotcue"+hotCue); engine.setValue(group,"hotcue_"+hotCue+"_clear",1); deck.hotCueDeleted=true; } else { engine.setValue(group,"hotcue_"+hotCue+"_activate",1); } } else { deck.hotCuePressed=false; if (deck.vinylButton || deck.keylockButton) { engine.setValue(group,"hotcue_"+hotCue+"_clear",0); } else { engine.setValue(group,"hotcue_"+hotCue+"_activate",0); } } }