tttapa / Control-Surface

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

Rotary Encoders over MCPs with pitchBend #147

Open jmym opened 4 years ago

jmym commented 4 years ago

I'have the code below, for use multiples MCP23017, with rotary encoders and switches, and work correctly, but i'm interested about the integration with your system and my code. I use an Arduino Mega, 16 Encoders, a fews MCPs and i like to use with my Presonus Studio One DAW, I would like to use 8 volumen, and 8 pan with the encoders. And integrate with your banks, etc.


#ifndef SRC_ROTARYENCOVERMCP_H_
#define SRC_ROTARYENCOVERMCP_H_

/* Describes new objects based on the Rotary and Adafruit MCP23017 library */
#include <Adafruit_MCP23017.h>
#include <Rotary.h>

/* function pointer definition */
typedef void (*rotaryActionFunc)(bool clockwise, int id);

/* We describe an object in which we instantiate a rotary encoder
 * over an I2C expander.
 * It holds the information:
 *  * to which MCP object it's connected
 *  * which pin it is connected to
 *  * what function to call when there is a change
 * */
class RotaryEncOverMCP {
public:
    RotaryEncOverMCP(Adafruit_MCP23017* mcp, byte pinA, byte pinB, rotaryActionFunc actionFunc = nullptr, int id = 0)
    : rot(pinA, pinB), mcp(mcp),
      pinA(pinA), pinB(pinB),
      actionFunc(actionFunc), id(id) {
    }

    /* Initialize object in the MCP */
    void init() {
        if(mcp != nullptr) {
            mcp->pinMode(pinA, INPUT);
            mcp->pullUp(pinA, 0); //disable pullup on this pin
            mcp->setupInterruptPin(pinA,CHANGE);
            mcp->pinMode(pinB, INPUT);
            mcp->pullUp(pinB, 0); //disable pullup on this pin
            mcp->setupInterruptPin(pinB,CHANGE);
        }
    }

    /* On an interrupt, can be called with the value of the GPIOAB register (or INTCAP) */
    void feedInput(uint16_t gpioAB) {
        uint8_t pinValA = bitRead(gpioAB, pinA);
        uint8_t pinValB = bitRead(gpioAB, pinB);
        uint8_t event = rot.process(pinValA, pinValB);
        if(event == DIR_CW || event == DIR_CCW) {
            //clock wise or counter-clock wise
            bool clockwise = event == DIR_CW;
            //Call into action function if registered
            if(actionFunc) {
                actionFunc(clockwise, id);
            }
        }
    }

    /* Poll the encoder. Will cause an I2C transfer. */
    void poll() {
        if(mcp != nullptr) {
            feedInput(mcp->readGPIOAB());
        }
    }

    Adafruit_MCP23017* getMCP() {
        return mcp;
    }

private:
    Rotary rot;                         /* the rotary object which will be created*/
    Adafruit_MCP23017* mcp = nullptr;   /* pointer the I2C GPIO expander it's connected to */
    uint8_t pinA = 0;
    uint8_t pinB = 0;           /* the pin numbers for output A and output B */
    rotaryActionFunc actionFunc = nullptr;  /* function pointer, will be called when there is an action happening */
    int id = 0;                             /* optional ID for identification */
};

#endif /* SRC_ROTARYENCOVERMCP_H_ */
tttapa commented 4 years ago

There are certainly cleaner ways to do the integration, but if you just want to reuse the code you posted, you can do something like this:

#include <Control_Surface.h>
#include <Adafruit_MCP23017.h>

typedef void (*rotaryActionFunc)(bool clockwise, int id);

class RotaryEncOverMCP {
  public:
    RotaryEncOverMCP(Adafruit_MCP23017* mcp, byte pinA, byte pinB, rotaryActionFunc actionFunc = nullptr, int id = 0) {}

    // ...
};

// --------------------------------------------------------------------------

USBMIDI_Interface midi;

Bank<3> bank(4);

Bankable::SingleAddress encAddresses[8] = {
  {bank, 16},
  {bank, 17},
  {bank, 18},
  {bank, 19},
  {bank, 20},
  {bank, 21},
  {bank, 22},
  {bank, 23},
};

void handler(bool clockwise, int id) {
  MIDIAddress address = encAddresses[id].getActiveAddress();
  RelativeCCSender::send(clockwise ? +1 : -1, address);
}

Adafruit_MCP23017 mcp;

RotaryEncOverMCP rotaryEncoders[] = {
  {&mcp, 0, 1, handler, 0}, // Pins GPA0,GPA1, register with encAddresses id 0
  {&mcp, 2, 3, handler, 1},
  {&mcp, 4, 5, handler, 2},
  // ...
};

IncrementDecrementSelector<3> selector = {bank, {2, 3}};

void setup() {
  Control_Surface.begin();
  // Initialize mcp here and attach interrupt
  // see https://github.com/maxgerhardt/rotary-encoder-over-mcp23017/blob/master/examples/Interrupt/InterruptExample.cpp
}

void loop() {
  Control_Surface.loop();
  // Update mcp and rotaryEncoders here if interrupt occured
  // see https://github.com/maxgerhardt/rotary-encoder-over-mcp23017/blob/master/examples/Interrupt/InterruptExample.cpp
}
jmym commented 4 years ago

Thank you, I'll try your code in my project.

Thanks for your help.

Best regards. JMY

El jue., 26 mar. 2020 22:28, tttapa notifications@github.com escribió:

There are certainly cleaner ways to do the integration, but if you just want to reuse the code you posted, you can do something like this:

include

include

typedef void (rotaryActionFunc)(bool clockwise, int id); class RotaryEncOverMCP { public: RotaryEncOverMCP(Adafruit_MCP23017 mcp, byte pinA, byte pinB, rotaryActionFunc actionFunc = nullptr, int id = 0) {}

// ...

}; // --------------------------------------------------------------------------

USBMIDI_Interface midi;

Bank<3> bank(4);

Bankable::SingleAddress encAddresses[8] = { {bank, 16}, {bank, 17}, {bank, 18}, {bank, 19}, {bank, 20}, {bank, 21}, {bank, 22}, {bank, 23}, }; void handler(bool clockwise, int id) { MIDIAddress address = encAddresses[id].getActiveAddress(); RelativeCCSender::send(clockwise ? +1 : -1, address); }

Adafruit_MCP23017 mcp;

RotaryEncOverMCP rotaryEncoders[] = { {&mcp, 0, 1, handler, 0}, // Pins GPA0,GPA1, register with encAddresses id 0 {&mcp, 2, 3, handler, 1}, {&mcp, 4, 5, handler, 2}, // ... };

IncrementDecrementSelector<3> selector = {bank, {2, 3}}; void setup() { Control_Surface.begin(); // Initialize mcp here and attach interrupt // see https://github.com/maxgerhardt/rotary-encoder-over-mcp23017/blob/master/examples/Interrupt/InterruptExample.cpp } void loop() { Control_Surface.loop(); // Update mcp and rotaryEncoders here if interrupt occured // see https://github.com/maxgerhardt/rotary-encoder-over-mcp23017/blob/master/examples/Interrupt/InterruptExample.cpp }

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/tttapa/Control-Surface/issues/147#issuecomment-604696450, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHZPWCYDRTET4TN7SR6E7I3RJPCJHANCNFSM4LUPB42A .

jmym commented 4 years ago

Hello again, my volume encoders works with pitchbends values, in channels from 1 to 8, How I could send a pitchbend value in a specific channel from banks?

    MIDIAddress address = encAddresses[id].getActiveAddress();
    PitchBendSender<RESOLUTION>::send(val, addr);//clockwise ? +1 : -1
tttapa commented 4 years ago

What kind of pitch bend messages does your DAW expect? Relative or absolute?

jmym commented 4 years ago

Could you explain me the difference? I send the pitchbendsender and only work in the first fader from my daw. I use the Mackie control driver to send the data from Arduino to my daw.

El sáb., 28 mar. 2020 16:33, tttapa notifications@github.com escribió:

What kind of pitch bend messages does your DAW expect? Relative or absolute?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/tttapa/Control-Surface/issues/147#issuecomment-605463186, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHZPWC2TNFQI2RX2HJBG6YLRJYKELANCNFSM4LUPB42A .

tttapa commented 4 years ago

In absolute mode, you send a value between 0 and 16,384, which represents a value between 0% and 100%. This is how potentiometers work.

For encoders, relative messages are often used. Instead of sending an absolute value, you send relative deltas, e.g. "+1" or "-1". This adds or subtracts one from the current value in the DAW, there's no way to set the absolute value of a control.

If you want to go from the (relative) input of an encoder to an absolute value, you'll have to keep track of the absolute position on the Arduino, so you'll have to add an array, one position variable for each encoder.

I send the pitchbendsender and only work in the first fader from my daw.

If you want to use pitch bend, you have to specify the channels:

Bankable::SingleAddress encAddresses[8] = {
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_1},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_2},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_3},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_4},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_5},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_6},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_7},
  {{bank, BankType::CHANGE_CHANNEL}, CHANNEL_8},
};
jmym commented 4 years ago

Thank you again. The Daw works in absolute mode. I write my code to increment or decrement values and work correctly. But, when change the bank (in the DAW has got more than 8 tracks), the channels start in 9 and ends in 16 in my controller (don't work in studio one). I test with TouchDaw app in my tablet and emulates a Mackie for Studio One, and when I press the bank, send the tablet "noteOn A# 127-0" or "noteOn B 127-0" (two buttons bank) and the channels begins the next 8 channels in channel 1 to 8. I don't know, how it's work internally my DAW or the "mackie emulator" but, could you help me, please?

tttapa commented 4 years ago

The MCU protocol has banking functionality built-in. This is different from the banking implemented by Control Surface.
You can change the MCU banks using the note numbers MCU::BANK_LEFT and MCU::BANK_RIGHT. IIRC, BANK_LEFT/BANK_RIGHT shift the bank by 8 tracks, and CHANNEL_LEFT/CHANNEL_RIGHT shift the bank by 1 track. These can be used with the NoteButton class, for instance.
If you use MCU banks, you probably don't need Control Surface banks, the encoders always send on the same track.

You'll need some extra housekeeping in your sketch to determine what bank you're in, and to keep track of the encoder value for each track (which doesn't necessarily correspond to the physical encoder).

Another approach is to use the Control Surface BankType::CHANGE_CABLENB, but this only works if your MIDI interface has multiple cables (such as Teensy boards). In that case, you can bank to Mackie Control Extenders.
I'm just leaving this here for reference, it won't work with your Arduino Mega.

jmym commented 4 years ago

Thank you for you quickly response. I'm trying your comment, but I don't know how to put all the ideas in the code. I write the code below, but i don't know how to insert MCU::BANK_LEFT:

NoteButton noteButton[2]={  
  14,                       // Push button on pin 14
  {note(Ab, 1), CHANNEL_1}, // Note Ab on MIDI channel 1
  15,                       // Push button on pin 15
  {note(B, 1),  CHANNEL_1}, // Note B on MIDI channel 1
  };

And but after that, how could I write my code to work, with my encoders in your banks, please?

Best regards, JMY

tttapa commented 4 years ago

You can do it like this:

NoteButton noteButton[]={
  {
    14,                       // Push button on pin 14
    MCU::BANK_LEFT,
  },
  {
    15,                       // Push button on pin 15
    MCU::BANK_RIGHT
  },
};

Note that there's one pair of braces around the entire array, and another pair of braces around each element.

And but after that, how could I write my code to work, with my encoders in your banks, please?

If you're using MCU banks, you don't need, Control Surface banks.

jmym commented 4 years ago

How Could I link my RotaryEncOverMCP to your MIDIsend or PitchBendSender? I'm trying all the options, without asking you. But I still, don't know how it's work MCU, and your code. I use MCU banks, and in the code bellow, i write MCU::Bankable::VPotRing array. But how could I update the rotary encoders with the vpotRing.

#include <Control_Surface.h> // Include the Control Surface library
#include <Wire.h>
#include <Adafruit_MCP23017.h>  // https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library
#include <Rotary.h>
#include "RotaryEncOverMCP.h"

USBMIDI_Interface midi;
void RotaryEncoderChanged(bool clockwise, int id);

// setup the port expander
Adafruit_MCP23017 mcp0;
Adafruit_MCP23017 mcp1;

//Array of pointers of all MCPs if there is more than one
Adafruit_MCP23017* allMCPs[] = { &mcp0, &mcp1 };
constexpr int numMCPs = (int)(sizeof(allMCPs) / sizeof(*allMCPs));

#define addr1 7 // 7 = A1 high, A2 high, A3 high
#define addr2 0 

Bank<4> bank(8);
NoteButton noteButton[]={
  {
    14,                       // Push button on pin 14
    MCU::BANK_LEFT,
  },
  {
    15,                       // Push button on pin 15
    MCU::BANK_RIGHT
  },
};

// VPot rings
MCU::Bankable::VPotRing<4> vpot[] = {
  {bank,1,CHANNEL_1},
{bank,2,CHANNEL_2},
{bank,3,CHANNEL_3},
{bank,4,CHANNEL_4},
{bank,5,CHANNEL_5},
{bank,6,CHANNEL_6},
{bank,7,CHANNEL_7},
{bank,8,CHANNEL_8}
};

RotaryEncOverMCP rotaryEncoders[] = {
        // outputA,B on GPA7,GPA6, register with callback and ID=1
         RotaryEncOverMCP(&mcp0, 0, 1, &RotaryEncoderChanged, 1),
         RotaryEncOverMCP(&mcp0, 3, 4, &RotaryEncoderChanged, 2),
         RotaryEncOverMCP(&mcp0, 8, 9, &RotaryEncoderChanged, 3),
       RotaryEncOverMCP(&mcp0, 11, 12, &RotaryEncoderChanged, 4),
           RotaryEncOverMCP(&mcp1, 0, 1, &RotaryEncoderChanged, 5),
           RotaryEncOverMCP(&mcp1, 3, 4, &RotaryEncoderChanged, 6),
           RotaryEncOverMCP(&mcp1, 8, 9, &RotaryEncoderChanged, 7),
           RotaryEncOverMCP(&mcp1, 11,12, &RotaryEncoderChanged, 8)
};
void RotaryEncoderChanged(bool clockwise, int id) {

    if(clockwise){
      RotarysValues[id-1] = RotarysValues[id-1]+1;//0.2;

    }else{
      RotarysValues[id-1] = RotarysValues[id-1]-1;//0.2;

    }
    digitalWrite(led, !digitalRead(led));
    RotarysValues[id-1] = constrain(RotarysValues[id-1], 0, 127);
//  MIDIAddress address = vpot[id-1].getActiveAddress(); //??????????????????????????
//  PitchBendSender<RESOLUTION>::send(RotarysValues[id-1], address);//???????????????????
}
void setup(){

  //light.blink(50,50);
  RelativeCCSender::setMode(MACKIE_CONTROL_RELATIVE);
  Control_Surface.begin(); // Initialize Control Surface
   Wire.begin();
   mcp0.begin();      // use default address 0
   mcp1.begin(7);
   //Initialize input encoders (pin mode, interrupt)
    for(int i=0; i < numEncoders; i++) {
        rotaryEncoders[i].init();
    }
}
void loop() {

  Control_Surface.loop(); // Update the Control Surface

  pollAll();

}

Thank you for your patience, but I think we are all going to learn something from all this.

tttapa commented 4 years ago

I don't have the time to write and test code right now, but it might be useful to read the MCU specification regarding banks.

https://images.thomann.de/pics/prod/151261_manual.pdf

The interesting part is "Fader Bank Zone" on page 64.

You need an array of encoder positions for all tracks in your DAW, let's say 24.
If you're on the first MCU bank, tracks 1-8 are active, so when the encoders are turned, you update elements 0-7 in the array. Then when you receive a "Bank Right" message, tracks 9-16 are active, so when the encoders are turned, you update elements 8-15 in the array. If you receive a "Channel Left" message, tracks 8-15 are active, so you update elements 7-14 in the array.

If you are using MCU Banks, you don't need Control Surface banks, so use MCU::VPotRing, not MCU::Bankable::VPotRing.

The addresses of the pitch bend messages don't change, they are tied to one of the encoder IDs. (They are independent of the MCU Bank or active tracks.)

PitchBendSender<10>::send(RotarysValues[id-1], Channel(id-1));

You'll also need to clamp the RotarysValues to match the resolution of the PitchBendSender. In this case, the resolution is 10 bits, so you need to make sure that the values are between 0 and 1023 = 210 - 1.

jmym commented 4 years ago

Thank you very much.

Best regards, JMY

El mar., 31 mar. 2020 12:02, tttapa notifications@github.com escribió:

I don't have the time to write and test code right now, but it might be useful to read the MCU specification regarding banks.

https://images.thomann.de/pics/prod/151261_manual.pdf

The interesting part is "Fader Bank Zone" on page 64.

You need an array of encoder positions for all tracks in your DAW, let's say 24. If you're on the first MCU bank, tracks 1-8 are active, so when the encoders are turned, you update elements 0-7 in the array. Then when you receive a "Bank Right" message, tracks 9-16 are active, so when the encoders are turned, you update elements 8-15 in the array. If you receive a "Channel Left" message, tracks 8-15 are active, so you update elements 7-14 in the array.

If you are using MCU Banks, you don't need Control Surface banks, so use MCU::VPotRing, not MCU::Bankable::VPotRing.

The addresses of the pitch bend messages don't change, they are tied to one of the encoder IDs. (They are independent of the MCU Bank or active tracks.)

PitchBendSender<10>::send(RotarysValues[id-1], Channel(id-1));

You'll also need to clamp the RotarysValues to match the resolution of the PitchBendSender. In this case, the resolution is 10 bits, so you need to make sure that the values are between 0 and 1023 = 210 - 1.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/tttapa/Control-Surface/issues/147#issuecomment-606525201, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHZPWCY4LB4JG6Z7LDIL5ATRKG5T7ANCNFSM4LUPB42A .