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); } }
1 Answer 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:
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);
}
}
Explore related questions
See similar questions with these tags.
updateFader()
being called when it is supposed to be called?updateFader()
is only called whenfaderPos == lastfaderPosition
. This is unlikely to be the case because there will be noise (electrical due to quantisisation and vibration) onint faderPos = analogRead(mainFaderPin)
. Try something likeif (abs(faderPos - lastfaderPosition) < some_threshold)...)
position
is less thanfaderMin
e.g.incomingVelocity
is zero? Or ifposition
is greater thanfaderMax
, e.g.incomingVelocity
is 1016 (or 127 * 8) whenfaderMax
is about 1023 - 40 = 983?