Control_Surface.h - Is it possible to make dynamic the MIDI command of an Encoder?

Newbie here. Building a Midi control surface which allows me to scroll through tracks with a knob, and then work on tracks like mute, solo, volume and pan, ONE track at a time. (instead of having 8 channels of hardware control I will have only 1)

Code is working great for mute, solo, and record arm by having the code keep track of the current track's position in the virtual 8-fader bank. The DAW sends midi output which signifies where current track is in this 1-8 position. And then for instance, if the current track is imagining itself as 'track 3 of 8' then the code knows to apply 'MUTE_3' on Mute button press.

Where I hit a snag is when trying to apply this approach to the Pan knob encoder. It seems the encoder's midi command is sort of hard coded as a global, and I'm trying to figure out - Is it possible and good practice to dynamically change the encoderPan's midi command during runtime like I'm doing with the buttons?

The Code:


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

USBMIDI_Interface midi;  // Instantiate Control Surface connection in the DAW.

// Mackie visualizes 8 tracks at a time. the current track's bank slot (1-8). set the track buttons such as mute and solo to work with the selected track
static int positionInBank = 1;

// PARSE MIDI data from DAW, and keep positionInBank updated
struct MyMIDI_Callbacks : FineGrainedMIDI_Callbacks<MyMIDI_Callbacks> {

  // Function that is called whenever a MIDI Note Off message is received.
  void onNoteOff(Channel channel, uint8_t note, uint8_t velocity, Cable cable) {
    Serial << "Note Off: " << channel << ", note " << note << ", velocity "
           << velocity << ", " << cable << endl;
  }

  // Function that is called whenever a MIDI Note On message is received.
  void onNoteOn(Channel channel, uint8_t note, uint8_t velocity, Cable cable) {
    Serial << "Note On: " << channel << ", note " << note << ", velocity "
           << velocity << ", " << cable << endl;

    //keep positionInBank updated. These are the note numbers that the DAW outputs indicating which 1-8 bank slot the current track belongs
    if (note == 24 && velocity == 127) { positionInBank = 1; }  
    if (note == 25 && velocity == 127) { positionInBank = 2; }  
    if (note == 26 && velocity == 127) { positionInBank = 3; }  
    if (note == 27 && velocity == 127) { positionInBank = 4; }  
    if (note == 28 && velocity == 127) { positionInBank = 5; }  
    if (note == 29 && velocity == 127) { positionInBank = 6; }  
    if (note == 30 && velocity == 127) { positionInBank = 7; }  
    if (note == 31 && velocity == 127) { positionInBank = 8; }  
  }
} callback;  // Instantiate a callback

// Send MIDI commands to DAW
class CustomNoteSender {
public:
  CustomNoteSender(uint8_t onVelocity, uint8_t offVelocity)
    : onVelocity(onVelocity), offVelocity(offVelocity) {}

  void sendOn(MIDIAddress address) {
    Control_Surface.sendNoteOn(address, onVelocity);
  }

  void sendOff(MIDIAddress address) {
    Control_Surface.sendNoteOff(address, offVelocity);
  }

private:
  uint8_t onVelocity, offVelocity;
};

struct CustomNoteButton : MIDIButton<CustomNoteSender> {
  // Constructor
  CustomNoteButton(pin_t pin, MIDIAddress address, uint8_t onVelocity,
                   uint8_t offVelocity)
    : MIDIButton(pin, address, { onVelocity, offVelocity }) {}
  //  ^~~~~~~~~~ Initialization of the base class MIDIButton
};

Encoder encoderTracks(4, 5); // scrool through tracks
Pushbutton buttonTrackMute(8); // track mute button
Pushbutton buttonTrackSolo(9); // track solo button
Pushbutton buttonTrackRecordArm(6); // track record arm button

// Pan knob - needs to be dynamic though
CCRotaryEncoder encoderPan{
  { 11, 12 },                             // pins
  MIDI_CC::General_Purpose_Controller_1,  // need to make the '1' a dynamic number between 1-8 like the mute and solo buttons
  1,                                      // optional multiplier if the control isn't fast enough
};

void setup() {
  Serial.begin(9600);
  midi.begin();
  midi.setCallbacks(callback);  // Attach the custom callback
  Control_Surface.begin();      // Initialize Control Surface
}

long oldPositionTracks = -999;

void loop() {

  if (buttonTrackMute.getSingleDebouncedPress()) {
    if (positionInBank == 1) { midi.sendNoteOn(0x10, 0x7f); }
    if (positionInBank == 2) { midi.sendNoteOn(0x11, 0x7f); }
    if (positionInBank == 3) { midi.sendNoteOn(0x12, 0x7f); }
    if (positionInBank == 4) { midi.sendNoteOn(0x13, 0x7f); }
    if (positionInBank == 5) { midi.sendNoteOn(0x14, 0x7f); }
    if (positionInBank == 6) { midi.sendNoteOn(0x15, 0x7f); }
    if (positionInBank == 7) { midi.sendNoteOn(0x16, 0x7f); }
    if (positionInBank == 8) { midi.sendNoteOn(0x17, 0x7f); }
  }

  if (buttonTrackSolo.getSingleDebouncedPress()) {
    if (positionInBank == 1) { midi.sendNoteOn(0x08, 0x7f); }
    if (positionInBank == 2) { midi.sendNoteOn(0x09, 0x7f); }
    if (positionInBank == 3) { midi.sendNoteOn(0x0A, 0x7f); }
    if (positionInBank == 4) { midi.sendNoteOn(0x0B, 0x7f); }
    if (positionInBank == 5) { midi.sendNoteOn(0x0C, 0x7f); }
    if (positionInBank == 6) { midi.sendNoteOn(0x0D, 0x7f); }
    if (positionInBank == 7) { midi.sendNoteOn(0x0E, 0x7f); }
    if (positionInBank == 8) { midi.sendNoteOn(0x0F, 0x7f); }
  }

  if (buttonTrackRecordArm.getSingleDebouncedPress()) {
    if (positionInBank == 1) { midi.sendNoteOn(0x00, 0x7f); }
    if (positionInBank == 2) { midi.sendNoteOn(0x01, 0x7f); }
    if (positionInBank == 3) { midi.sendNoteOn(0x02, 0x7f); }
    if (positionInBank == 4) { midi.sendNoteOn(0x03, 0x7f); }
    if (positionInBank == 5) { midi.sendNoteOn(0x04, 0x7f); }
    if (positionInBank == 6) { midi.sendNoteOn(0x05, 0x7f); }
    if (positionInBank == 7) { midi.sendNoteOn(0x06, 0x7f); }
    if (positionInBank == 8) { midi.sendNoteOn(0x07, 0x7f); }
  }

  long newPositionTracks = encoderTracks.read();

  if (newPositionTracks != oldPositionTracks && newPositionTracks % 2 == 0) {

    if (newPositionTracks < oldPositionTracks) {
      midi.sendNoteOn(0x60, 0x7f);  // UP - goto previous track
    } else {
      midi.sendNoteOn(0x61, 0x7f);  // DOWN - goto next track
    }
    oldPositionTracks = newPositionTracks;
  }

  Control_Surface.loop();
  midi.update();
  MIDI_Interface::updateAll();
}

Please see:

1 Like

Awesome thanks for the incredibly quick help!! .setAddress worked perfectly. Here is the updated code in case anyone is looking for this functionality. Although a new issue in this code is the pan knob requires many more turns to go right than it does to go left. Is this a known issue with MCU protocol?

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

USBMIDI_Interface midi;  // Instantiate Control Surface connection in the DAW.

// Mackie visualizes 8 tracks at a time. the current track's bank slot (1-8). set the track buttons such as mute and solo to work with the selected track
static int positionInBank = 1;

// PARSE MIDI data from DAW, and keep positionInBank updated
struct MyMIDI_Callbacks : FineGrainedMIDI_Callbacks<MyMIDI_Callbacks> {

  // Function that is called whenever a MIDI Note On message is received.
  void onNoteOn(Channel channel, uint8_t note, uint8_t velocity, Cable cable) {
    Serial << "Note On: " << channel << ", note " << note << ", velocity "
           << velocity << ", " << cable << endl;

    //keep positionInBank updated. These are the note numbers that the DAW outputs indicating which 1-8 bank slot the current track belongs
    if (velocity == 127 && note >= 24 && note <= 31) {
      int noteMap[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
      positionInBank = noteMap[note - 24];
    }
  }
} callback;  // Instantiate a callback

// Send MIDI commands to DAW
class CustomNoteSender {
public:
  CustomNoteSender(uint8_t onVelocity, uint8_t offVelocity)
    : onVelocity(onVelocity), offVelocity(offVelocity) {}

  void sendOn(MIDIAddress address) {
    Control_Surface.sendNoteOn(address, onVelocity);
  }

  void sendOff(MIDIAddress address) {
    Control_Surface.sendNoteOff(address, offVelocity);
  }

private:
  uint8_t onVelocity, offVelocity;
};

struct CustomNoteButton : MIDIButton<CustomNoteSender> {
  // Constructor
  CustomNoteButton(pin_t pin, MIDIAddress address, uint8_t onVelocity,
                   uint8_t offVelocity)
    : MIDIButton(pin, address, { onVelocity, offVelocity }) {}
  //  ^~~~~~~~~~ Initialization of the base class MIDIButton
};

void sendMidiNote(int noteOffset) {
  if (positionInBank >= 1 && positionInBank <= 8) {
    int midiNote = noteOffset + positionInBank - 1;
    midi.sendNoteOn(midiNote, 0x7f);
  }
}

// Pan knob
CCRotaryEncoder encoderPan{
  { 11, 12 },                             // pins
  MIDI_CC::General_Purpose_Controller_1,  // initial CC number
  1,                                      // optional multiplier if the control isn't fast enough
};

Encoder encoderTracks(4, 5);         // scroll through tracks
Pushbutton buttonTrackMute(8);       // track mute button
Pushbutton buttonTrackSolo(9);       // track solo button
Pushbutton buttonTrackRecordArm(6);  // track record arm button

// Define constants for dynamic assignment of MIDI CC numbers
const uint8_t baseCCNumber = MIDI_CC::General_Purpose_Controller_1;  // Start MIDI CC number
const uint8_t numCCNumbers = 8;                                      // Number of CC numbers

void setup() {
  Serial.begin(9600);
  midi.begin();
  midi.setCallbacks(callback);  // Attach the custom callback
  Control_Surface.begin();      // Initialize Control Surface
}

long oldPositionTracks = -999;

void loop() {
  // Check if positionInBank is within the valid range
  if (positionInBank >= 1 && positionInBank <= numCCNumbers) {
    // Set the encoderPan address based on positionInBank
    encoderPan.setAddress(baseCCNumber + positionInBank - 1);
  }

  if (buttonTrackMute.getSingleDebouncedPress()) {
    sendMidiNote(0x10);
  }

  if (buttonTrackSolo.getSingleDebouncedPress()) {
    sendMidiNote(0x08);
  }

  if (buttonTrackRecordArm.getSingleDebouncedPress()) {
    sendMidiNote(0x00);
  }

  long newPositionTracks = encoderTracks.read();

  if (newPositionTracks != oldPositionTracks && newPositionTracks % 2 == 0) {

    if (newPositionTracks < oldPositionTracks) {
      midi.sendNoteOn(0x60, 0x7f);  // UP - go to the previous track
    } else {
      midi.sendNoteOn(0x61, 0x7f);  // DOWN - go to the next track
    }
    oldPositionTracks = newPositionTracks;
  }

  Control_Surface.loop();
  midi.update();
  MIDI_Interface::updateAll();
}

Make sure you select the right relativeCCmode, see Control Surface: RotaryEncoder.ino.

1 Like

Perfect thanks! Updated code attached and the pan knob is working great. The multiplier is really handy as well. Added another encoder for track volume which isn't yet working. I think maybe because it is expecting a potentiometer and not an encoder?



#include <Encoder.h>
#include <Control_Surface.h>
#include <Pushbutton.h>

USBMIDI_Interface midi;
static int positionInBank = 1;
static int noteMapSize = 8;

struct MyMIDI_Callbacks : FineGrainedMIDI_Callbacks<MyMIDI_Callbacks> {
  void onNoteOn(Channel ch, uint8_t note, uint8_t velocity, Cable cable) {
    if (velocity == 127 && note >= 24 && note <= 31) {
      int noteMap[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
      positionInBank = noteMap[note - 24];
    }
  }
} callback;

class CustomNoteSender {
public:
  CustomNoteSender(uint8_t onVelocity, uint8_t offVelocity)
    : onVelocity(onVelocity), offVelocity(offVelocity) {}

  void sendOn(MIDIAddress address) {
    Control_Surface.sendNoteOn(address, onVelocity);
  }

  void sendOff(MIDIAddress address) {
    Control_Surface.sendNoteOff(address, offVelocity);
  }

private:
  uint8_t onVelocity, offVelocity;
};

struct CustomNoteButton : MIDIButton<CustomNoteSender> {
  CustomNoteButton(pin_t pin, MIDIAddress address, uint8_t onVelocity,
                   uint8_t offVelocity)
    : MIDIButton(pin, address, { onVelocity, offVelocity }) {}
};

void sendMidiNote(int noteOffset) {
  if (positionInBank >= 1 && positionInBank <= 8) {
    int midiNote = noteOffset + positionInBank - 1;
    midi.sendNoteOn(midiNote, 0x7f);
  }
}

// Track Pan knob
CCRotaryEncoder encoderTrackPan{
  { 11, 12 },                             // pins
  MIDI_CC::General_Purpose_Controller_1,  // initial CC number for pan
  4,                                      // optional multiplier if the control isn't fast enough
};

// Track Volume knob
CCRotaryEncoder encoderTrackVolume{
  { 10, 3 },      // pins
  MCU::VOLUME_1,  // initial CC number for volume
  1,              // optional multiplier if the control isn't fast enough
};

Encoder encoderTracks(4, 5);         // scroll through tracks
Pushbutton buttonTrackMute(8);       // track mute button
Pushbutton buttonTrackSolo(9);       // track solo button
Pushbutton buttonTrackRecordArm(6);  // track record arm button

const uint8_t numCCNumbers = 8;  // Number of CC numbers

void setup() {
  Serial.begin(9600);
  midi.begin();
  midi.setCallbacks(callback);
  RelativeCCSender::setMode(relativeCCmode::MACKIE_CONTROL_RELATIVE);
  Control_Surface.begin();
}

long oldPositionTracks = -999;

void loop() {
  // Check if positionInBank is within the valid range
  if (positionInBank >= 1 && positionInBank <= numCCNumbers) {
    // Set the encoderPan address based on positionInBank
    encoderTrackPan.setAddress(MIDI_CC::General_Purpose_Controller_1 + positionInBank - 1);
    // Set the encoderVolume address based on positionInBank
    encoderTrackVolume.setAddress(MCU::VOLUME_1 + positionInBank - 1);
  }

  if (buttonTrackMute.getSingleDebouncedPress()) {
    sendMidiNote(0x10);
  }

  if (buttonTrackSolo.getSingleDebouncedPress()) {
    sendMidiNote(0x08);
  }

  if (buttonTrackRecordArm.getSingleDebouncedPress()) {
    sendMidiNote(0x00);
  }

  long newPositionTracks = encoderTracks.read();

  if (newPositionTracks != oldPositionTracks && newPositionTracks % 2 == 0) {

    if (newPositionTracks < oldPositionTracks) {
      midi.sendNoteOn(0x60, 0x7f);  // UP - go to the previous track
    } else {
      midi.sendNoteOn(0x61, 0x7f);  // DOWN - go to the next track
    }
    oldPositionTracks = newPositionTracks;
  }

  Control_Surface.loop();
  midi.update();
  MIDI_Interface::updateAll();
}

Indeed, the MCU protocol uses MIDI Pitch Bend for the track volume.

See Control Surface: PBAbsoluteEncoder Class Reference.

1 Like

Thanks, I've been trying PBAbsoluteEncoder, but still no track volume. Is PBAE expecting an analog pin? I realize typically a potentiometer fader would be used for track volume, but it does seem from the documentation that the PBAE class is meant for a rotary encoder.

Track Pan works with this code:

CCRotaryEncoder encoderTrackPan{
  { 3, 10 }, // pins
  {MIDI_CC::General_Purpose_Controller_1, CHANNEL_1}, // PAN
  4,                                     
};

Using the same encoder connection to Arduino, the following code will compile but doesn't appear to do anything in the DAW. Also included in the comments are all my attempts at different commands:

PBAbsoluteEncoder encoderTrackVolume = {
{3, 10}, // pins
CHANNEL_1, // Compiles, but no response in DAW
//MCU::VOLUME_1, // Compiles, but no response in DAW
//MCU::V_POT_1,// compile error: could not convert
//{MIDI_CC::General_Purpose_Controller_1, CHANNEL_1}, // compile error: could not convert
//MIDI_CC::General_Purpose_Controller_1, // compile error: could not convert
//General_Purpose_Controller_1, // compile error: not declared in scope
4, // large multiplier because Pitch Bend has high resolution
};

Is it possible to tell if I'm simply missing the right CC or MCU command, or would I need different hardware from encoder like a potentiometer with an analog pin?

Let's start with a simpler sketch to isolate the problem:

#include <Control_Surface.h>

USBDebugMIDI_Interface midi{115200};

PBAbsoluteEncoder enc{
  {3, 10},
  MCU::VOLUME_1,
  127,
};

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

Connect a rotary encoder to pins 3 and 10, with the common pin wired to ground. Then upload this sketch and open the serial monitor at 115200 baud.
When turning the encoder, you should see Pitch Bend messages on channel 1 with increasing values.

To try it with your DAW, replace USBDebugMIDI_Interface midi{115200}; by USBMIDI_Interface midi;

Unfortunately, no response in DAW. I am using UAD Luna as my DAW if this helps.

I tried the two i/o pins both ways. Just one encoder currently connected to board, without the SW connected.

So the test with the serial monitor works?
In that case, the correct MIDI messages are being sent, and it seems that the DAW does not understand them. I'm not familiar with the particular DAW you're using, but you'll have to check the UAD Luna documentation to see which MIDI messages it expects.

Yes the serial monitor is showing me all the pitch bend data sent when turning the encoder.

And yes, I have looked for such Luna documentation but couldn't find, that would be REALLY helpful, but didn't know if it would be public.

I really appreciate your help here confirming the MCU command. I will consult the Luna docs and settings and tackle it from that end for a bit and see what I come up with. Thanks!!

Getting closer. From the midi monitor, it appears the pitch bend values are pretty different between affecting track volume fader from inside daw versus from my arduino encoder:

|09:30:52.502|To Arduino Leonardo|Pitch Wheel|1|3008|
|09:30:52.507|To Arduino Leonardo|Pitch Wheel|1|2919|
|09:30:52.508|To Arduino Leonardo|Pitch Wheel|1|3008|
|09:30:52.516|To Arduino Leonardo|Pitch Wheel|1|3098|
|09:30:56.476|From Arduino Leonardo|Pitch Wheel|1|-7338|
|09:30:56.499|From Arduino Leonardo|Pitch Wheel|1|-7314|
|09:30:56.548|From Arduino Leonardo|Pitch Wheel|1|-7291|
|09:30:56.569|From Arduino Leonardo|Pitch Wheel|1|-7267|

The top 4 entries are from when I move the fader from INSIDE THE DAW.

The bottom 4 entries are from when I moved the encoder with following code:

PBAbsoluteEncoder encoderTrackVolume{
  { 10, 3 },      
  MCU::VOLUME_1,  
  1,            
};

Could this difference in range be causing the issue, and is the solution to somehow shift the pitch bend range up to a range that resembles more the DAWs values?

You're using a very small multiplier, try increasing it, and you should more easily get the full range (-8192 - +8191).

1 Like

Thanks. Yes, tried various multipliers, all with the same outcome. And turned the encoder to where the pitch bend values are similar to the DAW and still not working.

You can try sending a Note On message for MCU::FADER_TOUCH_1 every now and then.

1 Like

YESSSSS!!! Thank you :slight_smile:

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.