tttapa / Control-Surface

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

Selectors and banks #40

Closed JohnnyReggae closed 5 years ago

JohnnyReggae commented 5 years ago

Pieter, thank you for an awesome library and just what I need at the moment for my little project of building a midi controller. Please excuse the questions as I am not a developer. I can understand some programming languages when I read through them slowly but get quite lost in the more technical source code.

I have defined 2 banks, 1 to control the octave being played by a group of NoteButtons, and 1 to control the active midi channel. Octave bank is linked to an Increment/Decrement selector and works perfectly. Channel bank also linked to an Increment/Decrement selector that I'm connecting to pin13 to use as an object holder for the banks. I am successfully using the results from turning a potentiometer to set the active midi channel. So both banks work 100%. My problem is that I would like to use a pot as well to change the octave bank. Doing what I did with the other bank does not work however. In fact nothing works if I set the value of the bank using a variable. I can statically change the bank to a value using an actual int. The code uploads but nothing works, as in there are no midi signals going out and my 7 segment led display does not come on.

I'm pretty sure I'm not using the selectors as they are meant to be, but as mentioned I'm not a developer so I cut and paste a lot, and I work from examples to see how to use certain functions etc, and sometimes I can get what the source is trying to tell me.

I cannot for the life of me figure out what is different between the 2 banks where one would allow me to programatically set the bank and the other causes the entire system not to function ?

Apologies for the very lengthy post...

/* Alpaha Station Zero
 * -------------------
 * Midi controler unit using an Arduino to control buttons, slide potentiometers, rotary potentiometers, and a 4 digit 7 segment LED display 
 * 16 buttons will be direct midi inputs for instrument activation
 * 4 slide potentiometers to control effects
 * 4 rotary potentiometers to control changing of setting options which can be used to assign different settings to the effects sliders for eg...
 * 4 digit 7 segment LED display unit which is controlled by the rotary potentiometers giving each bank 10 settings
 * 
 * https://github.com/tttapa/Control-Surface
 */

#include <Control_Surface.h> // Include the Control Surface library
#include "LedControl.h" // Include library to control 4 Digit 7 Segment display

const int speedMultiplier = 1;

const uint8_t velocity = 0b1111111; // Maximum velocity (0b1111111 = 0x7F = 127)

USBMIDI_Interface midi; // Instantiate a MIDI Interface to use

using namespace MIDI_Notes; // Enable midi notes

CD74HC4067 buttonMux = { // Instantiate a multiplexer to connect all 16 buttons
  12,                    // input pin
  {11, 10, 9, 8},        // Address pins S0, S1, S2, S3
};

CD74HC4067 potMux = { // Instantiate a multiplexer to connect all 8 potentiometers (4 rotary, 4 slide)
  A0,                 // input pin
  {A4, A3, A2, A1},   // Address pins S0, S1, S2, S3
};

LedControl fourdig=LedControl(7, 6, 5, 1); // Instantiate a LED Display object, pin 7 = DataIn, pin 6 = CLK, pin 5 = LOAD, 1 max7219

Bank<16> channelBank(1); // Create 16 banks for the 16 midi channels available
IncrementDecrementSelector<16> switchChannel(channelBank, {13, 13}, Wrap::NoWrap);  // Instantiate an increment/decrement button selector to manipulate with a potentiometer as a channel Bank

Bankable::CCPotentiometer controlPots[] = { // Create an array of potentiometers that send out MIDI Control Change messages when you turn the potentiometers (testing Midi-cc and using pots to control banks)
  {{channelBank, BankType::CHANGE_CHANNEL}, potMux.pin(0), {MIDI_CC::Channel_Volume, CHANNEL_1}},
  {{channelBank, BankType::CHANGE_CHANNEL}, potMux.pin(1), {MIDI_CC::Pan, CHANNEL_1}},
  {{channelBank, BankType::CHANGE_CHANNEL}, potMux.pin(2), {MIDI_CC::Modulation_Wheel, CHANNEL_1}},
  {{channelBank, BankType::CHANGE_CHANNEL}, potMux.pin(3), {MIDI_CC::Effects_5, CHANNEL_1}},
//  {potMux.pin(4), {MIDI_CC::Effects_1, CHANNEL_1}},
//  {A3, {MIDI_CC::Effects_1, CHANNEL_1}},
//  {A5, {MIDI_CC::Modulation_Wheel, CHANNEL_1}},
};

Bankable::PBPotentiometer pbFader = { // Instantiate a PBPotentiometer object, Analog pin connected to potentiometer, MIDI Channel 1 
  {channelBank, BankType::CHANGE_CHANNEL}, potMux.pin(4), CHANNEL_1,                
};

Bank<9> octaveBank(12);  // Create 9 banks for the playable octaves with 12 tracks for the notes

IncrementDecrementSelector<9> switchOctave(octaveBank, {4, 3}, Wrap::NoWrap);  // Instantiate an increment/decrement button selector, increment on pin4 and decrement on pin 3, linked to octaveBank

Bankable::NoteButton buttons[] = {  // Instantiate the momentary push buttons that send out MIDI note events.
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(0), note(C, 0), 1}, // bankselector, bank type, digital input pin, note number, channel
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(1), note(Db, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(2), note(D, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(3), note(Eb, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(4), note(E, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(5), note(F, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(6), note(Gb, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(7), note(G, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(8), note(Ab, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(9), note(A, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(10), note(Bb, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(11), note(B, 0), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(12), note(C, 1), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(13), note(Db, 1), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(14), note(D, 1), 1},
    {{octaveBank, BankType::CHANGE_ADDRESS}, buttonMux.pin(15), note(Eb, 1), 1},
};

unsigned long delaytime=10; // set a time to use for global delay

void setup() {
  Control_Surface.begin(); // Setup the Control Surface
  Serial.begin(115200); // Setup Serial output
  buttonMux.begin();                  // Initialize multiplexer for buttons
  buttonMux.pinMode(0, INPUT_PULLUP); // Set the pin mode (setting it for one pin of the multiplexers sets it for all of them)
  potMux.begin();                     // Initialize multiplexer for potentiometers
  potMux.pinMode(0, INPUT_PULLUP);    // Set the pin mode (setting it for one pin of the multiplexers sets it for all of them)

  fourdig.shutdown(0,false);          //The MAX72XX is in power-saving mode on startup, we have to do a wakeup call
  fourdig.setIntensity(0,10);         // Set the brightness of the display -15 to 15
  fourdig.clearDisplay(0);            // Clear the display                       

  switchOctave.set(4);                // Set the starting octave at middle C (C4)

  switchChannel.set(map(controlPots[2].getValue(), 0, 124, 15, 0)); // Set starting channel to pot2 value
}

void loop() {
  switchChannel.set(map(controlPots[3].getValue(), 0, 124, 15, 0));

  //switchOctave.set(map(controlPots[2].getValue(), 0, 124, 15, 0));
  Serial.println(map(controlPots[2].getValue(), 0, 124, 15, 0));

  // Show LED display linked to the 4 rotary potentiometers, 1 digit for each individual potentiometer
  fourdig.setDigit(0,4, switchOctave.get(),false);  // set digit 4 to show current octave activated for the Note Buttons
//  fourdig.setChar(0,4, map(controlPots[2].getValue(), 0, 124, 15, 0),false);  // set digit 3 to pot(3) value
  fourdig.setChar(0,3, map(controlPots[3].getValue(), 0, 124, 15, 0),false);  // set digit 3 to pot(3) value
  fourdig.setDigit(0,2, map(controlPots[2].getValue(), 0, 124, 9, 0),false);  // set digit 2 to pot(2) value
  fourdig.setDigit(0,1, map(controlPots[1].getValue(), 0, 124, 9, 0),false);  // set digit 1 to pot(1) value
  fourdig.setDigit(0,0, map(controlPots[0].getValue(), 0, 124, 9, 0),false);  // set digit 0 to pot(0) value
  delay(delaytime);  // a delay is required before clearing the display otherwise the display will simply light up
  fourdig.clearDisplay(0);  // clear the LED display

  Control_Surface.loop();
}
JohnnyReggae commented 5 years ago

I have figured it out and is clearly a very noob mistake.

switchOctave.set(map(controlPots[2].getValue(), 0, 124, 15, 0));

I have only defined 9 banks, 0 to 8. Mapping a value even if it wasn't greater than 8 seemed to cause quite a crash. That's why this worked: switchOctave.set(3); for eg.

So I guess my next question is, what should I be using as a selector for changing banks if I am using a potentiometer as my physical selector ? So that I don't programatically make these simple mistakes of setting out of bounds values :-)

JohnnyReggae commented 5 years ago

Also, is there a way to have the change of channel banks affect both the midi notes channel and the effect channel ? So when changing the midi channel the midi notes travel with the midi cc to the next channel ?

tttapa commented 5 years ago

When you try to select a non-existent setting, you get an error. The on-board LED should start blinking in that case, and if you enable debugging in src/Settings/Settings.hpp, it'll print the error message:

[static CS::setting_t CS::Selectable<N>::validateSetting(CS::setting_t) [with unsigned char N = 9; CS::setting_t = unsigned char] @ line 24]:   Error: Setting 9 is not less than the number of settings (9) (0xFFFE)

So I guess my next question is, what should I be using as a selector for changing banks if I am using a potentiometer as my physical selector ? So that I don't programatically make these simple mistakes of setting out of bounds values :-)

Compile-time checking is done by specifying the number of settings as a template parameter (between the <>). You could create your own class for potentiometer selectors, and make it a template, so it can only be combined with a bank with the same number of settings.

This is the code I used for testing your code, by the way:

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

USBMIDI_Interface midi; // Instantiate a MIDI Interface to use

Bank<9> channelBank(1);

FilteredAnalog<7> controlPot = A2;

void setup() {
  Control_Surface.begin(); // Setup the Control Surface
  Serial.begin(115200); // Setup Serial output
}

void loop() {
  if (controlPot.update()) {
    uint8_t setting = map(controlPot.getValue(), 0, 124, 0, 15);
    Serial.println(setting);
    channelBank.select(setting);
  }
  Control_Surface.loop();
}

Also, is there a way to have the change of channel banks affect both the midi notes channel and the effect channel ? So when changing the midi channel the midi notes travel with the midi cc to the next channel ?

No, this is not supported at the moment.

JohnnyReggae commented 5 years ago

Thanks so much for the help, I do appreciate it. I will attempt to create a potentiometer selector class using one of the existing classes as a basis to work from. I'm not overly confident at this point but I'm willing to give it a go. Thanks for the pointers.

Is it possible to create another bank selector to change channels on the midi notes so that there is a bank to change the octave and a bank to change the midi channel ?

tttapa commented 5 years ago

I think something like this would work:

#define protected public  // Forgive me, Father :(

#include <Control_Surface.h>

BEGIN_CS_NAMESPACE

namespace Bankable {

class TwoBanks {
  public:
    TwoBanks(OutputBankConfig config1, OutputBankConfig config2, 
             MIDICNChannelAddress address)
        : bankable1{config1}, bankable2{config2}, address(address) {}

    void lock() {
        bankable1.lock();
        bankable2.lock();
    }

    void unlock() {
        bankable1.unlock();
        bankable2.unlock();
    }

    MIDICNChannelAddress getBaseAddress() const { return address; }

    MIDICNChannelAddress getActiveAddress() const {
        return getBaseAddress() 
             + bankable1.getAddressOffset() 
             + bankable2.getAddressOffset();
    }

  private:
    BankableMIDIOutput bankable1;
    BankableMIDIOutput bankable2;
    MIDICNChannelAddress address;
};

class TwoBanksNoteButton : public MIDIButton<TwoBanks, DigitalNoteSender> {
  public:
    TwoBanksNoteButton(const OutputBankConfig &config1, 
                       const OutputBankConfig &config2, 
                       pin_t pin,
                       const MIDICNChannelAddress &address,
                       const DigitalNoteSender &sender = {})
        : MIDIButton<TwoBanks, DigitalNoteSender>{
          {config1, config2, address}, pin, sender} {}
};

} // namespace Bankable

END_CS_NAMESPACE

USBDebugMIDI_Interface midi;

Bank<16> channelBank(1);
Bank<9> pitchBank(12);

using namespace MIDI_Notes;

Bankable::TwoBanksNoteButton button = {
  {channelBank, CHANGE_CHANNEL},
  {pitchBank, CHANGE_ADDRESS},
  2, // pin
  note(C, 0), // address
};

void setup() {
  Control_Surface.begin();
  channelBank.select(CHANNEL_10.getRaw());
  pitchBank.select(4);
}

void loop() {
  Control_Surface.loop();
}
JohnnyReggae commented 5 years ago

Wow, that was quick. You have really given me a lot to go through and use. Again I really do appreciate your time and help.

I will be looking into attempting to create the potentiometer selector class.

Great work on this library it really has saved me a lot of work and time in trying to figure things out myself :-)

tttapa commented 5 years ago

This seems to work:

#include <Control_Surface.h>

USBMIDI_Interface midi;

MAX7219SevenSegmentDisplay display = {10}; // MAX7219 connected to SPI bus, load pin to pin 10

template <setting_t N>
class PotSelector : public Updatable<> {
  public:
    PotSelector(Selectable<N> &selectable, pin_t analogPin, uint8_t displayPosition = 0) 
      : selectable(selectable), pot{analogPin}, displayPosition(displayPosition) {}

    void begin() override {}

    void update() override {
      if (pot.update()) {
        setting_t setting = map(pot.getValue(), 0, 64, 0, N);
        selectable.select(setting);
        static_assert(N <= 16, "Can only display 0-15");
        display.printHex(displayPosition, setting);
      }
    }

  private:
    Selectable<N> &selectable;
    FilteredAnalog<6, 3, 3> pot; // tweak filter and bit depth if necessary
    uint8_t displayPosition;
};

Bank<9> pitchBank(12);
Bank<16> channelBank(1);

PotSelector<9> pitchSel = {
  pitchBank, A2, 0,
};

PotSelector<16> channelSel = {
  channelBank, A3, 7,
};

void setup() {
  display.begin();
  Control_Surface.begin();
}

void loop() {
  Control_Surface.loop();
}

I added the printHex method in the latest commit: d8471baf2071ff756a05659d91dd0da283cb8501, so you might have to update.

JohnnyReggae commented 5 years ago

Pieter, you are a scholar and a gentleman ... thank you :-) You have helped immensely. I will try this out today. I am a few steps closer to having everything that I need working.

Your code for the TwoBanksNoteButton works perfectly and for now I can manipulate the banks using an Increment selector setting the banks manually reading from the pots. I started looking into the potSelector code and seeing how I can use that.

My last thing to figure out is to create banks of effects that I can apply to a group of slide potentiometers. eg.

Bank 1 = Reverb, Chorus, Tremelo, Phaser Bank 2 = Pitchbend, Modulation, Breath, Portamento Bank 3 = Release, Attack, Cutoff, Variation etc ...

tttapa commented 5 years ago

I'm not sure I understand what you mean. Does each effect have a certain CC number?

JohnnyReggae commented 5 years ago

Yes they would have their own Midi CC number except for Pitch Bend which seems to be on it's own. So the banks would look like this with midi cc:

Bank 1 = 91, 93, 92, 95 Bank 2 = ??, 1, 2, 5 Bank 3 = 72, 73, 74, 70

I'm not sure how many banks I could come up with, but grouping together certain effects that are related to each other in some way could give me a few more.

So for eg. when bank 1 is selected the 4 slide potentiometers are assigned Reverb, Chorus, Tremolo and Phaser. Bank 2 they get assigned Modulation, Breath, Portamento ... etc.

tttapa commented 5 years ago

Okay, I see. For the normal CC numbers, you can use Bankable::ManyAddresses:CCPotentiometer. I didn't add any examples yet, but maybe you can figure it out from the documentation: https://tttapa.github.io/Control-Surface/Doc/Doxygen/d5/d71/classBankable_1_1ManyAddresses_1_1CCPotentiometer.html
If I have time tonight, I'll try to add an example.

For the pitch bend, you'll have to check the bank setting, disable the CCPotentiometer, and enable the PBPotentiometer when appropriate. They have enable() and disable() methods, and you can call getSelection() on the Bank, IIRC.

If you get stuck, just let me know, and I'll try to look into it tonight.

tttapa commented 5 years ago

Turns out you don't even need enable and disable, you can just specify an invalid address, so that it won't send anything.

See the example I added in 4bddc91.

JohnnyReggae commented 5 years ago

Pieter, your code works perfectly. Exactly what I needed to get done. Using the bankable ManyAddresses works exactly like you described and a simple copy/paste of your example code is doing the job.

Thank you again for your time and effort in helping out with this. I really do appreciate it. I now need to put everything into a box so I can bash the buttons proper, as a friend said to me today. :-)

tttapa commented 5 years ago

Glad I could help :-)

JohnnyReggae commented 5 years ago

Created an include file to take your class definition for TwoBanks out of the main code. I don't have my project with me at work today so can't test if it works correctly however it seems to compile ok after adding the files to my Project directory. Not sure if I've done it correctly, but when I added the include #include "TwoBanks.h" while the original code was there the compiler did complain about a redeclaration, so I assume it had included it correctly.

TwoBanks.cpp

#ifdef TEST_COMPILE_ALL_HEADERS_SEPARATELY
#include "TwoBanks.h"
#endif

TwoBanks.h

#pragma once

#include <MIDI_Outputs/Abstract/MIDIButton.hpp>
#include <MIDI_Senders/DigitalNoteSender.hpp>

BEGIN_CS_NAMESPACE

namespace Bankable {

class TwoBanks {
  public:
    TwoBanks(OutputBankConfig config1, OutputBankConfig config2, 
             MIDICNChannelAddress address)
        : bankable1{config1}, bankable2{config2}, address(address) {}

    void lock() {
        bankable1.lock();
        bankable2.lock();
    }

    void unlock() {
        bankable1.unlock();
        bankable2.unlock();
    }

    MIDICNChannelAddress getBaseAddress() const { return address; }

    MIDICNChannelAddress getActiveAddress() const {
        return getBaseAddress() 
             + bankable1.getAddressOffset() 
             + bankable2.getAddressOffset();
    }

  private:
    BankableMIDIOutput bankable1;
    BankableMIDIOutput bankable2;
    MIDICNChannelAddress address;
};

class TwoBanksNoteButton : public MIDIButton<TwoBanks, DigitalNoteSender> {
  public:
    TwoBanksNoteButton(const OutputBankConfig &config1, 
                       const OutputBankConfig &config2, 
                       pin_t pin,
                       const MIDICNChannelAddress &address,
                       const DigitalNoteSender &sender = {})
        : MIDIButton<TwoBanks, DigitalNoteSender>{
          {config1, config2, address}, pin, sender} {}
};

} // namespace Bankable

END_CS_NAMESPACE
JohnnyReggae commented 5 years ago

Managed to test it all this evening, and it works perfectly just including the .h file in my project directory :-)

tttapa commented 5 years ago

Great, glad to hear!

JohnnyReggae commented 5 years ago

Thanks for all the help :-)