tttapa / Control-Surface

Arduino library for creating MIDI controllers and other MIDI devices.
GNU General Public License v3.0
1.23k stars 138 forks source link

Changing the midi address of CCRotaryEncoder with a push button #140

Open ParametricToroid opened 4 years ago

ParametricToroid commented 4 years ago

Good evening everyone. I am trying to create a midi controller with several encoders and pushbuttons. For one part of the build, I would like a button to be able to change the function of a particular encoder. I would expect to do this by changing the midi address using an if statement once a button is pressed. I am currently using 3 encoders and 8 buttons, but plan on scaling the project up to 22 encoders and about 50 buttons. I am going to use multiplexers (CD74HC4067) for the buttons, and a Teensy 3.6 for its large number of I/O.

I am not particularly experienced with arduino programming, but i'm really just looking at trying to change the midi address of an encoder once a button is pressed. Note: the midi addresses themselves do not matter for the time being.

Also (not as important) If there is a better way to multiplex buttons than what I have please let me know. I saw the example with potentiometers, but couldn't make it work with digital buttons.


//#include <Encoder.h> // Include the Encoder library.
// This must be done before the Control Surface library.
#include <Control_Surface.h> // Include the Control Surface library

// Instantiate a MIDI over USB interface.
USBMIDI_Interface midi;

// Instantiate a CCRotaryEncoder object

//ENCODER Setup
CCRotaryEncoder enc = {
  {0, 1},       // pins
  MCU::REC_RDY_1, // THIS IS WHAT I WANT TO CHANGE WITH A PUSH BUTTON MIDI address (CC number + optional channel)
  1,            // optional multiplier if the control isn't fast enough
};
CCRotaryEncoder enc2 = {
  {2, 3},       // pins
  MCU::REC_RDY_2, // MIDI address (CC number + optional channel)
  1,            // optional multiplier if the control isn't fast enough
};
CCRotaryEncoder enc3 = {
  {4, 5},       // pins
  MCU::REC_RDY_3, // MIDI address (CC number + optional channel)
  1,            // optional multiplier if the control isn't fast enough
};
//

//MUX setup
int pin_Out_S0 = 7;
int pin_Out_S1 = 8;
int pin_Out_S2 = 9;
int pin_In_Mux1 = 33;
int Mux1_State[8] = {0,0,0,0,0,0,0,0};
//

void setup() {
  RelativeCCSender::setMode(relativeCCmode::TWOS_COMPLEMENT);
  Control_Surface.begin(); // Initialize Control Surface

  //MUX SETUP
  pinMode(pin_Out_S0, OUTPUT);
  pinMode(pin_Out_S1, OUTPUT);
  pinMode(pin_Out_S2, OUTPUT);
  pinMode(pin_In_Mux1, INPUT);
  //
}

void loop() {
  Control_Surface.loop(); // Update the Control Surface
  //UPDATE MUX
  updateMux1();
  if (Mux1_State[0] == 1){
    usbMIDI.sendControlChange(10,1,1);
  }
  if (Mux1_State[1] == 1){
    usbMIDI.sendControlChange(11,1,1);
  }
  if (Mux1_State[2] == 1){
    usbMIDI.sendControlChange(12,1,1);
  }
  if (Mux1_State[3] == 1){
    usbMIDI.sendControlChange(13,1,1);
  }
  if (Mux1_State[4] == 1){
    usbMIDI.sendControlChange(14,1,1);
  }
  if (Mux1_State[5] == 1){
    usbMIDI.sendControlChange(15,1,1);
  }
  if (Mux1_State[6] == 1){
    usbMIDI.sendControlChange(16,1,1);
  }
  if (Mux1_State[7] == 1){
    usbMIDI.sendControlChange(17,1,1);
  }
  //
}

//read the multiplexer to obtain button changes
void updateMux1 () {
  digitalWrite(pin_Out_S0, LOW);
  digitalWrite(pin_Out_S1, LOW);
  digitalWrite(pin_Out_S2, LOW);
  delayMicroseconds(10);
  Mux1_State[0] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, LOW);
  digitalWrite(pin_Out_S1, LOW);
  digitalWrite(pin_Out_S2, HIGH);
  delayMicroseconds(10);
  Mux1_State[1] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, LOW);
  digitalWrite(pin_Out_S1, HIGH);
  digitalWrite(pin_Out_S2, LOW);
  delayMicroseconds(10);
  Mux1_State[2] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, LOW);
  digitalWrite(pin_Out_S1, HIGH);
  digitalWrite(pin_Out_S2, HIGH);
  delayMicroseconds(10);
  Mux1_State[3] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, HIGH);
  digitalWrite(pin_Out_S1, LOW);
  digitalWrite(pin_Out_S2, LOW);
  delayMicroseconds(10);
  Mux1_State[4] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, HIGH);
  digitalWrite(pin_Out_S1, LOW);
  digitalWrite(pin_Out_S2, HIGH);
  delayMicroseconds(10);
  Mux1_State[5] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, HIGH);
  digitalWrite(pin_Out_S1, HIGH);
  digitalWrite(pin_Out_S2, LOW);
  delayMicroseconds(10);
  Mux1_State[6] = digitalRead(pin_In_Mux1);
  digitalWrite(pin_Out_S0, HIGH);
  digitalWrite(pin_Out_S1, HIGH);
  digitalWrite(pin_Out_S2, HIGH);
  delayMicroseconds(10);
  Mux1_State[7] = digitalRead(pin_In_Mux1);

}

Thank you very much in advance for your help!

tttapa commented 4 years ago

Changing MIDI addresses

Changing MIDI addresses of MIDI Elements is done using Banks. The bankable versions of the available MIDI Output Elements are in the Bankable and Bankable::ManyAddresses namespaces.
Bankable allows you to change the MIDI address by multiples of a fixed offset (e.g. Bank 1 → channel 1 = 1 + 0×4, Bank 2 → channel 5 = 1 + 1×4, Bank 3 → channel 9 = 1 + 2×4, Bank 4 → channel 13 = 1 + 3×4). ManyAddresses allows you to specify a list of arbitrary addresses (e.g. Bank 1 → CC#11, channel 1, Bank 2 → CC#45, channel 10, Bank 3 → CC#45, channel 2, Bank 4 → CC#1, channel 16).

Banks are controlled using a Selector, which reads input from a push button, for example, and selects the right bank accordingly.

For your use case, you can use both Bankable and ManyAddresses:

Bankable

#include <Encoder.h> // Include the Encoder library.
// This must be done before the Control Surface library.
#include <Control_Surface.h> // Include the Control Surface library

// Instantiate a MIDI over USB interface.
USBMIDI_Interface midi;

Bank<2> bank(MCU::MUTE_1 - MCU::REC_RDY_1);
//           ^~.~.~.~.~.~.~.~.~.~.~.~.~.~ tracks per bank / offset
SwitchSelector selector = {bank, 11}; // switch between pin 11 and ground

// Instantiate a CCRotaryEncoder object
Bankable::CCRotaryEncoder enc = {
  bank,
  {0, 1},         // pins
  MCU::REC_RDY_1, // base address, offset is added when second bank is selected
  1,              // optional multiplier if the control isn't fast enough
};

void setup() {
  RelativeCCSender::setMode(relativeCCmode::TWOS_COMPLEMENT);
  Control_Surface.begin(); // Initialize Control Surface
}

void loop() {
  Control_Surface.loop(); // Update the Control Surface
}

ManyAddresses

#include <Encoder.h> // Include the Encoder library.
// This must be done before the Control Surface library.
#include <Control_Surface.h> // Include the Control Surface library

// Instantiate a MIDI over USB interface.
USBMIDI_Interface midi;

Bank<2> bank;
SwitchSelector selector = {bank, 11}; // switch between pin 11 and ground

// Instantiate a CCRotaryEncoder object
Bankable::ManyAddresses::CCRotaryEncoder<2> enc = {
  bank,
  {0, 1},         // pins
  {
    MCU::REC_RDY_1, // address for Bank 1
    MCU::MUTE_1,    // address for Bank 2
  },
  1,              // optional multiplier if the control isn't fast enough
};

void setup() {
  RelativeCCSender::setMode(relativeCCmode::TWOS_COMPLEMENT);
  Control_Surface.begin(); // Initialize Control Surface
}

void loop() {
  Control_Surface.loop(); // Update the Control Surface
}

I just fixed some missing default arguments for Bankable::CCPotentiometer, and I pushed the Bankable::ManyAddresses::CCPotentiometer that wasn't included yet, so you'll have to pull the latest version of the library before you can run the examples above.

Multiplexers for buttons

You can use the multiplexer classes for buttons as well, the usage is exactly the same as for analog inputs.

// Include the library
#include <Control_Surface.h>

// Instantiate a MIDI Interface to use
USBMIDI_Interface midi;

// Instantiate an analog multiplexer
CD74HC4051 mux = {
  33,       // Input pin (signal pin)
  {7, 8, 9} // Address pins S0, S1, S2
};

// Create an array of buttons that send out MIDI Control Change messages when 
// pressed.
// The buttons are connected to the eight input pins of the multiplexer.
CCButton buttons[] = {
  {mux.pin(0), {10, CHANNEL_1}}, // {button pin, {CC#, MIDI channel}}
  {mux.pin(1), {11, CHANNEL_2}},
  {mux.pin(2), {12, CHANNEL_3}},
  {mux.pin(3), {13, CHANNEL_4}},
  {mux.pin(4), {14, CHANNEL_5}},
  {mux.pin(5), {15, CHANNEL_6}},
  {mux.pin(6), {16, CHANNEL_7}},
  {mux.pin(7), {17, CHANNEL_8}},
};

// Initialize the Control Surface
void setup() {
  Control_Surface.begin();
}

// Update the Control Surface
void loop() {
  Control_Surface.loop();
}

It looks like your buttons are wired incorrectly, though. Buttons should be wired between the input pin and ground. If you're writing code manually, enable the internal pull-up resistor manually, if you're using buttons in Control Surface, this will be done automatically.

benwadub commented 4 years ago

I haven’t seen that encoders are usable in banks nice!!!

ParametricToroid commented 4 years ago

Thank you for the fast reply tttapa! Using banks with encoders to change the address makes sense. Also, I'll try the method you listed to multiplex buttons (it seems much better than mine). I guess my biggest question would then be: How would I use a multiplexed button to switch the bank of an encoder? It seems easy enough for multiplexed buttons to send MIDI CC commands, but is there a way to allow them to change the bank of an encoder instead? Again I apologize, I'm relatively new to arduino, and especially new to MIDI interfacing.

As a side note, would anyone know of a good way to multiplex (I2C?) many more encoders? I'm not sure if the common methods work with this library.

Thank you again for your help!

tttapa commented 4 years ago

I guess my biggest question would then be: How would I use a multiplexed button to switch the bank of an encoder? It seems easy enough for multiplexed buttons to send MIDI CC commands, but is there a way to allow them to change the bank of an encoder instead?

Almost all classes and functions in the Control Surface library allow you to use Extended Input/Output pins, like the pins of a multiplexer:

// Instantiate an analog multiplexer
CD74HC4051 mux = {
  33,       // Input pin (signal pin)
  {7, 8, 9} // Address pins S0, S1, S2
};
// ...
SwitchSelector selector = {bank, mux.pin(0)}; // switch between the first pin of the mux and ground

As a side note, would anyone know of a good way to multiplex (I2C?) many more encoders? I'm not sure if the common methods work with this library.

Encoders are the main exception to the rule above: it's not possible to multiplex rotary encoders, because their timing requirements are too tight. You really need interrupts if you want to use encoders. For a small number of encoders, you can try polling, but IIRC, @benwadub tried this with multiple encoders on an Arduino Mega, without success. Polling might be viable on a much faster Teensy with timer interrupts, for instance, but I've never tried it.

There are I2C port expanders with interrupt capabilities, but the standard Arduino I2C libraries don't allow you to use I2C in interrupt service routines. In theory, this should be possible, but I don't feel like rolling, and (more importantly) maintaining my own I2C driver for all different architectures that Control Surface supports.

The Control Surface library is very modular, so it's easy enough to add your own control elements (also see the RelativeCCSender class). If you have a platform-specific way to get encoders through multiplexers/expanders working, you can integrate it with the library that way. (Feel free to post it here, so others can benefit from it as well.)
I feel that's a better way of dealing with it than including features with the library that are hard to maintain across platforms and impossible to get working in all cases.

ParametricToroid commented 4 years ago

Thank you for your reply. I have made some progress, but still have an issue. What I am currently trying to do is use three buttons to switch the function for three encoders, so that pressing a button allows each encoder to do three different things. At this point, however, I would also like to add the push button of the encoder to the list of things that can be changed with the three selector buttons. This would be such that the encoder and its push button are tied to three specific functions, and by pressing one of the selector buttons, I can change what each encoder push button combination does. The issue I'm running into right now is switching the function of the encoder buttons. I am trying to use a format similar to Bankable::ManyAddresses::CCRotaryEncoder<3>, but it does not seem to be working:


// Bankable::ManyAddresses::CCButton<3> {
  bank,
  mux1.pin(15),
  {
    {10, CHANNEL_1},
    {11, CHANNEL_1},
    {12, CHANNEL_1},
  },
};

The error that appears is: Lightroom_controller_test:66: error: expected unqualified-id before '{' token Bankable::ManyAddresses::CCButton<3> {

                                  ^

expected unqualified-id before '{' token

My entire project is this:


////Test
#include <Encoder.h> // Include the Encoder library.
// This must be done before the Control Surface library.
#include <Control_Surface.h> // Include the Control Surface library
//#include <Bounce.h> // include bounce button library
//#include <MIDI.h>

// Instantiate a MIDI over USB interface.
USBMIDI_Interface midi;

// Create Bank Object
Bank<3> bank;
//Create bank switching button

// Instantiate a CCRotaryEncoder object using banks
//ENCODER Setup
// Instantiate a CCRotaryEncoder object
Bankable::ManyAddresses::CCRotaryEncoder<3> enc0 = {
  bank,
  {0, 1},         // pins
  {
    MCU::REC_RDY_1, // address for Bank 1
    MCU::REC_RDY_4,    // address for Bank 2
    MCU::REC_RDY_7,        // address for Bank 3
  },
  1,              // optional multiplier if the control isn't fast enough
};
Bankable::ManyAddresses::CCRotaryEncoder<3> enc1 = {
  bank,
  {2, 3},         // pins
  {
    MCU::REC_RDY_2, // address for Bank 1
    MCU::REC_RDY_5,    // address for Bank 2
    MCU::REC_RDY_8,        // address for Bank 3
  },
  1,              // optional multiplier if the control isn't fast enough
};
Bankable::ManyAddresses::CCRotaryEncoder<3> enc2 = {
  bank,
  {4, 5},         // pins
  {
    MCU::REC_RDY_3, // address for Bank 1
    MCU::REC_RDY_6,    // address for Bank 2
    MCU::SOLO_1,        // address for Bank 3
  },
  1,              // optional multiplier if the control isn't fast enough
};

//Mux setup
// Instantiate an analog multiplexer
CD74HC4067 mux1 = {
  6,            // input pin
  {10, 9, 8, 7}, // Address pins S0, S1, S2, S3
  // 7, // Optionally, specify the enable pin
};

//Create bank switching button
ManyButtonsSelector<3> selector = {
  bank,
  {{mux1.pin(0),mux1.pin(1),mux1.pin(2)}},
};

// Create an array of buttons that send out MIDI Control Change messages when 
// pressed.
// The buttons are connected to the sixteen input pins of the multiplexer.
Bankable::ManyAddresses::CCButton<3> {  //This is the current issue...
  bank,
  mux1.pin(15),
  {
    {10, CHANNEL_1},
    {11, CHANNEL_1},
    {12, CHANNEL_1},
  },
};
//CCButton buttons[] = {
//  {mux1.pin(3), {3, CHANNEL_1}},
//  {mux1.pin(4), {4, CHANNEL_1}},
//  {mux1.pin(5), {5, CHANNEL_1}},
//  {mux1.pin(6), {6, CHANNEL_1}},
//  {mux1.pin(7), {7, CHANNEL_1}},
//  {mux1.pin(8), {8, CHANNEL_1}}, // {button pin, {CC#, MIDI channel}}
//  {mux1.pin(9), {9, CHANNEL_1}},
//  {mux1.pin(10), {10, CHANNEL_1}},
//  {mux1.pin(11), {11, CHANNEL_1}},
//  {mux1.pin(12), {12, CHANNEL_1}},
//};

//Setup LED pins
const int onLED = 35;
const int satLED = 23;
int satLEDstatus = 1;
const int hueLED = 22;
const int lumLED = 21;

void setup() {
  RelativeCCSender::setMode(relativeCCmode::TWOS_COMPLEMENT);
  Control_Surface.begin(); // Initialize Control Surface
};

void loop() {
  analogWrite(onLED, 5);
  Control_Surface.loop(); // Update the Control Surface
  if (digitalRead(mux1.pin(0)) == 0 || satLEDstatus == 1) {
    analogWrite(satLED, 5);
    analogWrite(hueLED, 0);
    analogWrite(lumLED, 0);
  }
  if (digitalRead(mux1.pin(1)) == 0) {
    analogWrite(hueLED, 5);
    analogWrite(satLED, 0);
    analogWrite(lumLED, 0);  
    satLEDstatus = 0;  
  }
  if (digitalRead(mux1.pin(2)) == 0) {
    analogWrite(lumLED, 5);
    analogWrite(hueLED, 0);
    analogWrite(satLED, 0);
    satLEDstatus = 0;
  }

};

I appreciate your continued support, thank you very much!!

tttapa commented 4 years ago

The problem is that you're trying to initialize a variable without providing a variable name:

Bankable::ManyAddresses::CCButton<3> myButton = {
//                                   ^~~~~~~~
  bank,
  mux1.pin(15),
  {
    {10, CHANNEL_1},
    {11, CHANNEL_1},
    {12, CHANNEL_1},
  },
};
ParametricToroid commented 4 years ago

Ok, yes, my bad, that should've been obvious, thanks. I have named the button, unfortunately, I still receive an error:


//Bankable::ManyAddresses::CCButton<3> enc0button = {
//
  bank,
  mux1.pin(15),
  {
    {10, CHANNEL_1},
    {11, CHANNEL_1},
    {12, CHANNEL_1},
  },
};

The error is:

Lightroom_controller_test:74: error: could not convert '{bank, mux1.AH::AnalogMultiplex<4u>::.AH::StaticSizeExtendedIOElement<16u>::.AH::ExtendedIOElement::pin(15u), {{10, CS::CHANNEL_1}, {11, CS::CHANNEL_1}, {12, CS::CHANNEL_1}}}' from '' to 'CS::Bankable::ManyAddresses::CCButton<3u>' };

^

could not convert '{bank, mux1.AH::AnalogMultiplex<4u>::.AH::StaticSizeExtendedIOElement<16u>::.AH::ExtendedIOElement::pin(15u), {{10, CS::CHANNEL_1}, {11, CS::CHANNEL_1}, {12, CS::CHANNEL_1}}}' from '' to 'CS::Bankable::ManyAddresses::CCButton<3u>'

Is there something else that I am missing in defining the button of the encoder in this way? I noticed that there is a parameter called DigitalCCSender, present at the end of the example of using Bankable with a CCButton definition. I didn't need this for an encoder, is it required to define a button with multiple states depending on the bank, like I'm trying to do? Thanks again for your help!

tttapa commented 4 years ago

I should have tested the code in my previous reply.

This is a quirk in C++, if the elements of the address list are initialized using braces, you have to add another pair of braces around the list:

Bankable::ManyAddresses::CCButton<3> myButton = {
  bank,
  mux1.pin(15),
  {{
    {10, CHANNEL_1},
    {11, CHANNEL_1},
    {12, CHANNEL_1},
  }},
};

If the addresses themselves are specified without braces, this isn't necessary:

Bankable::ManyAddresses::CCButton<3> myButton = {
  bank,
  mux1.pin(15),
  {
    10,
    11,
    12,
  },
};
ParametricToroid commented 4 years ago

Ok, great, that worked for me, thanks! I need to get better at C++, so hopefully I can catch some of these quirks in advance. I'll let you know if I have any other issues related to this. Please keep up the great work, this is an excellent library!

ParametricToroid commented 4 years ago

So this is a bit of an odd issue, but it is related to using the encoders in Twos Complement mode. Currently they send a value of 1 or 127 when turned. However, in the software that I'm using there is an issue where there are 100 values possible in each direction. This means that each rotation increments by 100/127 = 0.78. I would like to change the output of the encoders to max out at 100, or 101, so that the increment is a whole number, so it does not increase by a decimal amount. Is it possible to change the max output of the encoder when using CCRotaryEncoder to something other than 127?

Thanks!

tttapa commented 4 years ago

I don't really understand what you mean, but I don't think it's possible.
Control Surface only sends relative messages, don't think of them as (1, 127), but as (+1, -1). It's up to the software on your computer to assign meaning to these relative numbers.

Sending 100 in two's complement has a meaning of -28. It has nothing to do with the maximum value in your DAW software, nor with the delta between two values.

ParametricToroid commented 4 years ago

I see, thanks for the reply. The software that I am using is a plugin for a photo editing software that takes midi input and changes parameters of the software according to how they're set. There is an option in this plugin to set the maximum value of the relative two's complement, but this change must also be made in the software of the midi controller. It is interesting because I can see a 1 or 127 given when I rotate the encoder CW or CCW, in the plugin midi monitor. Would there potentially be a way to change what the arduino/teensy sends as a midi cc message in the CCRotaryEncoder function. I guess what I mean is that it sends a 1111 1111 for 1, and 1000 0001 for 127. So would there be a way to edit some called function such that the CCRotaryEncoder sends 0110 0101, for 101? I'm sorry if I don't really understand this, of if I make it hard to understand, I just wanted to clarify. Thanks!

tttapa commented 4 years ago

Would there potentially be a way to change what the arduino/teensy sends as a midi cc message in the CCRotaryEncoder function.

Sure, you can add a fourth relative CC mode in the RelativeCCSender class:

https://tttapa.github.io/Control-Surface-doc/Doxygen/d9/d24/RelativeCCSender_8hpp_source.html

https://github.com/tttapa/Control-Surface/blob/master/src/MIDI_Senders/RelativeCCSender.hpp

ParametricToroid commented 4 years ago

Ah, I see. Ok I will take a look at that. I'll post if it works. Thanks!