2

I'm using an Arduino Leonardo for sending MIDI In and Out messages to Ableton Live.

Components include a Potentiometer, a Capacitive Touch Sensor, and a DC Motor, which I'm powering with a L298N Motor Driver.

Right now I have it set up so that I can record automation in by moving the potentiometer, send a MIDI note in when I'm touching the fader, and follow automation using the DC Motor.

The problem that I'm having is that when I go to play my tracks, the automation that I recorded with the potentiometer is deactivated, and therefore my motor doesn't respond. I believe this is because I'm sending MIDI Input and Output at the same time. Here's the current version of the code:

#include <Control_Surface.h>
#include <CapacitiveSensor.h>

int minimumCp = 200;
int lastfaderPosition = 0;     // previous state of the button

const byte touchSend = 7;
const byte touchReceive = 8;

CapacitiveSensor touchSensor = CapacitiveSensor(touchReceive, touchSend);
USBMIDI_Interface midi;
CCPotentiometer faderPosition { A0, MIDI_CC::General_Purpose_Controller_1 };

int incomingChannel;
int incomingNote;
int incomingVelocity;
const byte mainFaderPin = 0;
const byte motorUp      = 4;
const byte motorDown    = 5;
const byte motorPWM     = 6;
byte motorSpeed = 150;
byte tolerance  = 40;
double faderMax = 0;
double faderMin = 0;

struct MyMIDI_Callbacks : FineGrainedMIDI_Callbacks<MyMIDI_Callbacks> {

  void onControlChange(Channel channel, uint8_t controller, uint8_t value, Cable cable) {
    incomingNote = controller;
    incomingVelocity = value;
  }
  void onProgramChange(Channel channel, uint8_t program, Cable cable) {
    Serial << "Program Change: " << channel << ", program " << program << ", "
           << cable << endl;
  }
  void onAfterTouchChannel(Channel channel, uint8_t pressure, Cable cable) {
    Serial << "Channel Pressure: " << channel << ", pressure " << pressure
           << ", " << cable << endl;
  }
  void onPitchBend(Channel channel, uint16_t bend, Cable cable) {
    Serial << "Pitch Bend: " << channel << ", bend " << bend << ", " << cable
           << endl;
  }
  void onSystemExclusive(SysExMessage se) {
    Serial << F("System Exclusive: [") << se.length << "] "
           << AH::HexDump(se.data, se.length) << ", " << se.cable << endl;
  }
  void onTimeCodeQuarterFrame(uint8_t data, Cable cable) {
    Serial << "MTC Quarter Frame: " << data << ", " << cable << endl;
  }
  void onSongPosition(uint16_t beats, Cable cable) {
    Serial << "Song Position Pointer: " << beats << ", " << cable << endl;
  }
  void onSongSelect(uint8_t songnumber, Cable cable) {
    Serial << "Song Select: " << songnumber << ", " << cable << endl;
  }
  void onTuneRequest(Cable cable) {
    Serial << "Tune Request: " << cable << endl;
  }
  void onClock(Cable cable) { Serial << "Timing Clock: " << cable << endl; }
  void onStart(Cable cable) { Serial << "Start: " << cable << endl; }
  void onContinue(Cable cable) { Serial << "Continue: " << cable << endl; }
  void onStop(Cable cable) { Serial << "Stop: " << cable << endl; }
  void onActiveSensing(Cable cable) {
    Serial << "Active Sensing: " << cable << endl;
  }
  void onSystemReset(Cable cable) {
    Serial << "System Reset: " << cable << endl;
  }

} callback;

void setup() {
  touchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF);
  midi.setCallbacks(callback);
  Control_Surface.begin();
  pinMode(motorDown, OUTPUT);
  pinMode(motorUp, OUTPUT);
  pinMode(motorPWM, OUTPUT);
  Serial.begin(250000);
  calibrateFader();
}

void calibrateFader() {
  digitalWrite(motorUp, HIGH);
  analogWrite(motorPWM, motorSpeed);
  delay(300);
  digitalWrite(motorUp, LOW);
  faderMax = analogRead(mainFaderPin) - tolerance;
  digitalWrite(motorDown, HIGH);
  analogWrite(motorPWM, motorSpeed);
  delay(300);
  digitalWrite(motorDown, LOW);
  faderMin = analogRead(mainFaderPin) + tolerance;
}

void loop() {
  int faderPos = analogRead(mainFaderPin);
  int totalCp =  touchSensor.capacitiveSensor(9);
  Control_Surface.loop();

  if ((faderPos == lastfaderPosition) && (totalCp <= minimumCp) && (incomingNote == 16)) {
    updateFader(incomingVelocity * 8); 
    }

  if ((faderPos == lastfaderPosition) && (totalCp > minimumCp)) {
    midi.sendNoteOn({37, CHANNEL_2}, 127);
    digitalWrite(motorUp, LOW);
    digitalWrite(motorDown, LOW);
  }

  if (((faderPos < lastfaderPosition - 1) || (faderPos > lastfaderPosition + 1)) && (totalCp > minimumCp)) {
    digitalWrite(motorUp, LOW);
    digitalWrite(motorDown, LOW);
  }
  if (((faderPos < lastfaderPosition - 1) || (faderPos > lastfaderPosition + 1)) && (totalCp <=minimumCp)) {
// Stop Sending MIDI, and receive it only
  }
  lastfaderPosition = faderPos;
}

void updateFader(int position) {     //Function to move fader to a specific position between 0-1023 if it's not already there
  if (position < analogRead(mainFaderPin) - tolerance && position > faderMin) {
    digitalWrite(motorDown, HIGH);
    while (position < analogRead(mainFaderPin) - tolerance) {}; //Loops until motor is done moving
    digitalWrite(motorDown, LOW);
  }
  else if (position > analogRead(mainFaderPin) + tolerance && position < faderMax) {
    digitalWrite(motorUp, HIGH);
    while (position > analogRead(mainFaderPin) + tolerance) {}; //Loops until motor is done moving
    digitalWrite(motorUp, LOW);
  }
}

Note that when I use this version with an Arduino UNO, the whole thing works. I've tried discussing this with a few who are far more experienced than I, but surprisingly they were unable to provide insight as to why.

#include <SoftwareSerial.h> // MIDI Input
#include <MIDI.h> // MIDI Output
#include <CapacitiveSensor.h>

#define rxPin 0 // Input  (Grey)
#define txPin 1 // Output (Yellow)

SoftwareSerial midiSerial (rxPin, txPin);

const byte wiper        = 0; //Position of fader relative to GND (Analog 0)
const byte motorUp      = 4;
const byte motorDown    = 5;
const byte motorPWM     = 6;
const byte touchSend    = 7;//Send pin for Capacitance Sensing Circuit (Digital 7)
const byte touchReceive = 8; //Receive pin for Capacitance Sensing Circuit (Digital 8)
unsigned int incomingCommand;
unsigned int incomingNote;
unsigned int incomingVelocity;
byte motorSpeed = 150;  // Raise if the fader is too slow (0-255)
byte minimumCp  = 200; // Raise if the fader is too sensitive (0-16383)
byte tolerance  = 10;  // Raise if the fader is too shaky (0-1023)
double faderMax = 1023; //Value read by fader's maximum position (0-1023)
double faderMin = 0; //Value read by fader's minimum position (0-1023)
long lastDebounceTime = 0;  // the last time the output pin was toggled
long debounceDelay    = 15; // the debounce time; increase if the output flickers
int lastfaderValue    = 0;
int targetPosition;
byte ch1Vol = 176;
byte fullVel = 127;
byte ch_1 = 1;
byte Db_1 = 1;

CapacitiveSensor touch = CapacitiveSensor(touchReceive, touchSend);
MIDI_CREATE_DEFAULT_INSTANCE();

void setup() {
  touch.set_CS_AutocaL_Millis(0xFFFFFFFF);
  midiSerial.begin(31250);
  MIDI.begin();
  pinMode(motorDown, OUTPUT);
  pinMode(motorUp, OUTPUT);
  pinMode(motorPWM, OUTPUT);
  calibrateFader(); }

void calibrateFader() {
  digitalWrite(motorUp, HIGH);
  analogWrite(motorPWM, motorSpeed);
  delay(300);
  digitalWrite(motorUp, LOW);
  faderMax = analogRead(wiper) - tolerance;
  digitalWrite(motorDown, HIGH);
  analogWrite(motorPWM, motorSpeed);
  delay(300);
  digitalWrite(motorDown, LOW);
  faderMin = analogRead(wiper) + tolerance; }

void loop() {
  int faderPos = analogRead(wiper);
  int faderHiResMIDI = ((faderPos *16.0146627566) - 8191.5);

  while ( midiSerial.available()) {
    incomingCommand = midiSerial.read();
    incomingNote = midiSerial.read();
    incomingVelocity = midiSerial.read()*8.05511811024; 
  if ((incomingVelocity  <=1023) && (incomingCommand >127) && (incomingNote <128)) {
    midiSerial.write(incomingCommand);
    midiSerial.write(incomingNote);
    midiSerial.write(incomingVelocity); } }
    
 { int totalCp =  touch.capacitiveSensor(30); 
 if ( (millis() - lastDebounceTime) > debounceDelay) {
    if (totalCp <= minimumCp) { 
      //if (incomingCommand == ch1Vol) { 
        updateFader(incomingVelocity); }// }    // Not Touching fader
    if ((faderPos == lastfaderValue) && (totalCp > minimumCp)) { 
      MIDI.sendNoteOn(Db_1, fullVel, ch_1); // Touching Fader
      digitalWrite(motorDown, LOW);
      digitalWrite(motorUp, LOW); }
    if (((faderPos > lastfaderValue+1) or (faderPos < lastfaderValue-1)) && (totalCp > minimumCp)) {   // Moving Fader
      MIDI.sendPitchBend(faderHiResMIDI, ch_1);
      digitalWrite(motorDown, LOW);
      digitalWrite(motorUp, LOW); }
    lastfaderValue = faderPos;  
    lastDebounceTime = millis(); } } }

void updateFader(int position) {     //Function to move fader to a specific position between 0-1023 if it's not already there
  if (position < analogRead(wiper) - tolerance && position > faderMin) {
    digitalWrite(motorDown, HIGH);
    while (position < analogRead(wiper) - tolerance) {}; //Loops until motor is done moving
    digitalWrite(motorDown, LOW); }
  else if (position > analogRead(wiper) + tolerance && position < faderMax) {
    digitalWrite(motorUp, HIGH);
    while (position > analogRead(wiper) + tolerance) {}; //Loops until motor is done moving
    digitalWrite(motorUp, LOW); } }
7
  • Is updateFader() being called when it is supposed to be called? updateFader() is only called when faderPos == lastfaderPosition. This is unlikely to be the case because there will be noise (electrical due to quantisisation and vibration) on int faderPos = analogRead(mainFaderPin). Try something like if (abs(faderPos - lastfaderPosition) < some_threshold)...)
    – tim
    Jul 2, 2021 at 0:46
  • Yea, that seems to work pretty well. I added coupling/decoupling capacitors to the MIDI In/Out circuit, and that fixed the noise right up. It's actually very stable now Jul 2, 2021 at 1:22
  • What happens if position is less than faderMin e.g. incomingVelocity is zero? Or if position is greater than faderMax, e.g. incomingVelocity is 1016 (or 127 * 8) when faderMax is about 1023 - 40 = 983?
    – tim
    Jul 2, 2021 at 1:51
  • Hi Tim, when the motor is within the tolerance for error, it simply stops moving. Without this tolerance for error, the motor will move erratically back and forth, trying to find the exact value to position itself in. Give it a tolerance, and it works right out. This is a necessary step for motorized faders Jul 2, 2021 at 13:29
  • The second version in your updated question works by fluke.
    – tim
    Jul 2, 2021 at 14:49

1 Answer 1

1

Firstly, you need a robust way to synchronise to and parse MIDI messages which can be 1 byte, 2 bytes or 3 bytes or more... See this Summary of MIDI 1.0 Messages by the MIDI Association.

A good way to achieve this is by using a state machine. I've written a simple one below that parses 3-byte MIDI messages. It could be expanded to parse different types of commands.

typedef enum State
{
    Idle,
    Command,
    Data1
};

State state = State::Idle;

const float INCOMING_VELOCITY_SCALE = 1023.0 / 127.0;

byte incomingCommand;
byte incomingNote;
byte incomingVelocity;

int targetPosition;

void loop()
{
    if (midiSerial.available())  // Use the `if` statement to get the next available byte.
    {
        byte incoming = midiSerial.read();

        // Synchronise to and parse the 3-byte MIDI message packet, i.e. 1nnnnnnn 0nnnnnnn 0nnnnnnn
        switch (state)
        {
        case State::Idle:
            if (IsMidiCommand(incoming))
            {
                Serial.println("Received command byte.");
                incomingCommand = incoming;
                state = State::Command;
            }
            else if (IsMidiData(incoming))
            {
                Serial.println("Received data byte.");
            }
            break;
        case State::Command:
            if (IsMidiCommand(incoming))  // Double check for an out of sync command.
            {
                Serial.println("Received command byte.");
                incomingCommand = incoming;
                state = State::Command;
            }
            else if (IsMidiData(incoming))
            {
                Serial.println("Received data1 byte.");
                incomingNote = incoming;
                state = State::Data1;
            }
            break;
        case State::Data1:
            if (IsMidiCommand(incoming))  // Triple check for an out of sync command.
            {
                Serial.println("Received command byte.");
                incomingCommand = incoming;
                state = State::Command;
            }
            else if (IsMidiData(incoming))
            {
                Serial.println("Received data2 byte.");
                incomingVelocity = incoming;
                midiSerial.write(incomingCommand);
                midiSerial.write(incomingNote);
                midiSerial.write(incomingVelocity);
                Serial.write(incomingCommand);
                Serial.write(incomingNote);
                Serial.write(incomingVelocity);
                Serial.println();
                targetPosition = incomingVelocity * INCOMING_VELOCITY_SCALE;
                if (targetPosition > faderMax)      // Clip if out of bounds.
                {
                    targetPosition = faderMax;
                }
                else if (targetPosition < faderMin)
                {
                    targetPosition = faderMin;
                }
                state = State::Idle;
            }
            break;
        default:
            state = State::Idle;
            break;
        }
    }
    . . .
    updateFader(targetPosition);
    . . .
}

bool IsMidiCommand(const byte b)
{
    return b >= 128;  // For MIDI command bytes, MSB is 1.
}

bool IsMidiData(const byte b)
{
    return b <= 127;  // For MIDI data bytes, MSB is 0.
}

Secondly, there is an issue with the updateFader() function regarding the tolerance. It's 40 in your first version and 10 in your second version. Consider the following values for the variables:

// In setup()
digitalWrite(motorDown, HIGH);
analogWrite(motorPWM, motorSpeed);
delay(300);
faderMin = analogRead(wiper) + tolerance;
faderMin = 0                 + 40;         // Bottom of range.
faderMin = 40;

// In loop()
incomingVelocity = midiSerial.read() * 8.05511811024;
incomingVelocity = 1                 * 8.05511811024;  // A quiet note, e.g. between 0 and 5, will a give a value of 0 to 40.
incomingVelocity = 8;

// In updateFader()
if (position < analogRead(wiper) - tolerance && position > faderMin)
if (8        < 512               - 40        && 8        > 40      )
if (8        < 472                           && 8        > 40      )
if (true                                     && false              )
if (false                                                          )

Result: the fader will not budge if a quiet note is received that puts position below faderMin and will leave the fader jamming somewhere in the middle of its scale rather than boogieing on down to faderMin. This effect would be exaggerated if faderMin were higher than 40 when analogRead(wiper) reads greater than zero in setup().

Similarly, this also happens with very loud notes and faderMax at the other end of the scale:

// In updateFader()
else if (position > analogRead(wiper) + tolerance && position < faderMax)

A better way would be to clip the targetPosition to the valid range before calling updateFader() and remove the second condition from the if statements in updateFader():

targetPosition = incomingVelocity * INCOMING_VELOCITY_SCALE;
if (targetPosition > faderMax)      // Clip if out of bounds.
{
    targetPosition = faderMax;
}
else if (targetPosition < faderMin)
{
    targetPosition = faderMin;
}
. . .
updateFader(targetPosition);
. . .
if (position < analogRead(wiper) - tolerance)
. . .
else if (position > analogRead(wiper) + tolerance)
. . .

Thirdly, updateFader() blocks until the fader motor has finished moving. If notes are being received faster than the fader can move, the tempo will rallentando, so consider using a non-blocking style like Blink Without Delay.

Fourthly, faderMin and faderMax should be just that – the minimum and maximum extent of fader travel. But by adding/substracting tolerance from min/max it is unnecessarily reducing the range. The tolerance is effectively being doubled by adjusting for it in both setup() and adjustFader(). The extent of fader travel can be obtained by taking an average of several values in calibrateFader(). Then the tolerance can be adjusted for only once in adjustFader() as described above.

void calibrateFader()
{
    const byte NUM_SAMPLES = 10;
    digitalWrite(motorUp, HIGH);
    analogWrite(motorPWM, motorSpeed);
    delay(300);
    digitalWrite(motorUp, LOW);
    faderMax = AnalogueAverage(wiper, NUM_SAMPLES);
    Serial.println(faderMax);
    digitalWrite(motorDown, HIGH);
    analogWrite(motorPWM, motorSpeed);
    delay(300);
    digitalWrite(motorDown, LOW);
    faderMin = AnalogueAverage(wiper, NUM_SAMPLES);
    Serial.println(faderMin);
}

unsigned int AnalogueAverage(const byte pin, const byte num_samples)
{
    const byte ROUND_OFF = num_samples / 2;
    unsigned long total = 0;
    for (byte i = 0; i < num_samples; i++)
    {
        total += analogRead(pin);
    }
    return (total + ROUND_OFF) / num_samples;
}

On a different note...

...I've been reading through the documentation and source code for the libraries you referenced in your code, which I think are these ones:

  1. Control Surface
  2. Arduino MIDI Library

These already implement the MIDI message parsing using callbacks, e.g.:

That example stresses the importance of swift non-blocking code. This means that updateFader() should also be non-blocking. You have the option to write your own custom state machine parser or use the libraries' parsers.

Here is how your code could be re-written to use the MIDI library's parser with non-blocking callbacks and non-blocking updateFader(). You may want to double-check which pins you are using for midiSerial because pins 0 and 1 are used for the default Serial monitor or you could use MIDI.sendNoteOn() and MIDI.sendNoteOff().

#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();

unsigned int faderMax = 0;
unsigned int faderMin = 0;
unsigned int faderTargetPosition = 0;

void doNoteOn(byte channel, byte note, byte velocity)
{
    midiSerial.write(channel);
    midiSerial.write(note);
    midiSerial.write(velocity);
    faderTargetPosition = velocity * INCOMING_VELOCITY_SCALE;

    // Clip if out of bounds.
    if (faderTargetPosition > faderMax)
    {
        faderTargetPosition = faderMax;
    }
    else if (faderTargetPosition < faderMin)
    {
        faderTargetPosition = faderMin;
    }
}

void doNoteOff(byte channel, byte note, byte velocity)
{
    midiSerial.write(channel);
    midiSerial.write(note);
    midiSerial.write(velocity);
}

void setup()
{
    MIDI.setHandleNoteOn(doNoteOn);
    MIDI.setHandleNoteOff(doNoteOff);
    MIDI.begin(MIDI_CHANNEL_OMNI);
    . . .
}

void loop()
{
    // Parse the MIDI messages and call the callbacks.
    MIDI.read();

    if ((millis() - lastDebounceTime) > debounceDelay)
    {
        int totalCp = touch.capacitiveSensor(30);
        int faderPos = analogRead(FADER_PIN);
        int faderHiResMIDI = faderPos * 16.0146627566 - 8191.5;

        if (totalCp <= minimumCp)
        {
            // Not touching fader.
            //if (incomingCommand == ch1Vol)
            //{
            updateFader(faderTargetPosition);
            //}
        }
        else
        {
            if (faderPos == lastfaderValue)
            {
                // Touching fader.
                MIDI.sendNoteOn(Db_1, fullVel, ch_1);
                digitalWrite(MOTOR_DOWN_PIN, LOW);
                digitalWrite(MOTOR_UP_PIN, LOW);
            }
            else if ((faderPos > lastfaderValue + 1) or (faderPos < lastfaderValue - 1))
            {
                // Moving fader.
                MIDI.sendPitchBend(faderHiResMIDI, ch_1);
                digitalWrite(MOTOR_DOWN_PIN, LOW);
                digitalWrite(MOTOR_UP_PIN, LOW);
            }
        }
        lastfaderValue = faderPos;
        lastDebounceTime += debounceDelay;  // Constant interval.
    }
}

void updateFader(const unsigned int& faderTargetPosition)
{
    const unsigned int currentPosition = analogRead(FADER_PIN);
    if (faderTargetPosition < currentPosition - tolerance)      // Below the tolerance band.
    {
        digitalWrite(MOTOR_UP_PIN, LOW);                        // Switch the up control off before switching the down control on.
        digitalWrite(MOTOR_DOWN_PIN, HIGH);
    }
    else if (faderTargetPosition > currentPosition + tolerance) // Above the tolerance band.
    {
        digitalWrite(MOTOR_DOWN_PIN, LOW);                      // Switch the down control off before switching the up control on.
        digitalWrite(MOTOR_UP_PIN, HIGH);
    }
    else                                                        // Within the tolerance band.
    {
        digitalWrite(MOTOR_DOWN_PIN, LOW);
        digitalWrite(MOTOR_UP_PIN, LOW);
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.