Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / C

Arduino-Based MIDI Expression Pedal

Rate me:
Please Sign up or sign in to vote.
4.99/5 (46 votes)
17 Jul 2009CPOL10 min read 160.5K   807   49   28
Create a MIDI expression pedal with an Arduino circuit board

Article 1: Introduction to the Arduino Hardware Platform
Article 2: Interfacing an Arduino with LCDs

Image 1

Introduction

This is my third article on the Arduino hardware platform and is the conclusion of the series. In this article, I will describe how to build a MIDI volume pedal using an Arduino microcontroller.

Background

I have an effects processor which I've built a song around where I vary one of the parameters as I play. The device has MIDI ports but it doesn't respond to MIDI continuous controller (CC) messages, however one can change parameters by sending the device MIDI system exclusive (SysEx) messages. I could have implemented a software-only solution but I wanted a stand-alone device and I didn't want to have to hook up a computer to my rig. I could have also implemented a hardware-only solution using off the shelf components including an expression pedal, a MIDI Solutions Pedal Controller, and a MIDI Solutions Event Processor, but this would still be a bit bulky and I wanted something more compact. One day I came across the Small Bear Electronics web site which sells expression pedal kits and I got the idea to combine a pedal enclosure with a microcontroller and build my first electronic device - a custom expression pedal that outputs SysEx. I realize this is a very specialized case and that a MIDI volume pedal would have a wider ranging appeal so I've included a sketch for a MIDI volume pedal as well.

Metronome2

Before I begin talking about the expression pedal sketches, let's revisit the metronome sketch I introduced in the first article. If you recall, the tempo was hardcoded within the program and if the tempo needed to be changed, the code had to be modified and recompiled. In Metronome2 I've added a 10K Ohm potentiometer, connected to Analog pin 0:

Image 2

(This image is a little hard to make out. If you view the image zoomed in, you will be able to see the pins on the Arduino better.)

Image 3

The analog pins take 10K Ohm devices and have a range of 0 - 1023. The following sketch displays the value of the potentiometer as it changes:

C++
// Raw potentiometer value viewer

// Constants
const int POT_PIN = 0;                  // Pot connected to analog pin 0
const int SERIAL_PORT_RATE = 9600;


void setup()                            // Run once, when the sketch starts
{
    Serial.begin(SERIAL_PORT_RATE);     // Starts communication with the serial port
}

void loop()                             // Run over and over again
{
    int nValue = analogRead(POT_PIN);
    Serial.println(nValue);
}

If you run with the serial monitor on, you will see we have a problem - even when you don't touch the potentiometer, the value still changes. This wasn't an issue with the Ski sketch because it only has five values (-2 through +2), but with the Metronome2 sketch (and later with the volume pedal sketch) this will be an issue - after you set the tempo, you'll want the tempo to be constant and not fluctuate. To overcome this, code was added to support a threshold and reject values too close to the last known good value:

C++
// Potentiometer viewer with threshold

// Constants
const int POT_PIN = 0;                  // Pot connected to analog pin 0
const int POT_THRESHOLD = 3;            // Threshold amount to guard against false values
const int SERIAL_PORT_RATE = 9600;


void setup()                            // Run once, when the sketch starts
{
    Serial.begin(SERIAL_PORT_RATE);     // Starts communication with the serial port
}

void loop()                             // Run over and over again
{
    static int s_nLastValue = 0;

    int nValue = analogRead(POT_PIN);
    if(abs(nValue - s_nLastValue) < POT_THRESHOLD)
        return;
    s_nLastValue = nValue;

    Serial.println(nValue);
}

That's better, but the code still needs to be taken one step further. The code needs to be mapped to a value that’s within a range we are looking for, and the code needs to guard against the same value being set:

C++
//
// Check floating potentiometer value against threshold
//

// Constants
const int POT_PIN = 0;                  // Pot connected to analog pin 0
const int POT_THRESHOLD = 3;            // Threshold amount to guard against false values
const int SERIAL_PORT_RATE = 9600;

void setup()                            // Run once, when the sketch starts
{
    Serial.begin(SERIAL_PORT_RATE);     // Starts communication with the serial port
}

void loop()                             // Run over and over again
{
    static int s_nLastPotValue = 0;
    static int s_nLastMappedValue = 0;

    int nCurrentPotValue = analogRead(POT_PIN);
    if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
        return;
    s_nLastPotValue = nCurrentPotValue;

    int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 255);  // Map the value to 0-255
    if(nMappedValue == s_nLastMappedValue)
        return;
    s_nLastMappedValue = nMappedValue;

    Serial.println(nMappedValue);
}

If you find this to be still too noisy (I did, but I was running a 200K wah potentiometer in a pin that wants 10K), an alternate approach is to check the value against a history buffer:

C++
//
// Check floating potentiometer value against history buffer
//

// Constants
const int POT_PIN = 0;                  // Pot connected to analog pin 0
const int POT_THRESHOLD = 3;            // Threshold amount to guard against false values
const int HISTORY_BUFFER_LENGTH = 6;    // History buffer length 
                   // (to guard against noise being sent)
const int SERIAL_PORT_RATE = 9600;

// Globals
static int s_history[HISTORY_BUFFER_LENGTH];

void setup()                            // Run once, when the sketch starts
{
    Serial.begin(SERIAL_PORT_RATE);     // Starts communication with the serial port

    // Initialize he buffer
    for(int i=0; i<HISTORY_BUFFER_LENGTH; i++)
    {
        s_history[i] = -1;
    }
}

void loop()                             // Run over and over again
{
    static int s_nLastPotValue = 0;
    static int s_nLastMappedValue = 0;

    int nCurrentPotValue = analogRead(POT_PIN);
    if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
        return;
    s_nLastPotValue = nCurrentPotValue;

    int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 255);    // Map the value 
                            // to 0-255
    if(nMappedValue == s_nLastMappedValue)
        return;

    for(int i=0; i<HISTORY_BUFFER_LENGTH; i++)
    {
        if(s_history[i] == nMappedValue)
            return;
    }

    memcpy(&s_history[0], &s_history[1], sizeof(int) * (HISTORY_BUFFER_LENGTH - 1));
    s_history[HISTORY_BUFFER_LENGTH - 1] = nMappedValue;
    s_nLastMappedValue = nMappedValue;
    Serial.println(nMappedValue);
}

(The current value being resent isn't an issue with the metronome sketch but later on we'll need this for the expression pedal.)

Now, adding this support for the potentiometer in the metronome sketch, the code now looks like:

C++
/*
 * Metronome2
 *
 * Based on the basic Arduino example, Blink:
 *   http://www.arduino.cc/en/Tutorial/Blink
 * Operates as a visual metronome.
 */

// Constants
const int LED_PIN = 13;                 // LED connected to digital pin 13
const int POT_PIN = 0;                  // Pot connected to analog pin 0
const int POT_THRESHOLD = 3;            // Threshold amount to guard against false values

void setup()                            // Run once, when the sketch starts
{
    pinMode(LED_PIN, OUTPUT);           // Sets the LED as output
}

void loop()                             // Run over and over again
{
    static int s_nLastPotValue = 0;
    static int s_nTempo = 0;

    int nCurrentPotValue = analogRead(POT_PIN);            // Has a range of 0 - 1023
    if(abs(nCurrentPotValue - s_nLastPotValue) >= POT_THRESHOLD)
    {
        s_nLastPotValue = nCurrentPotValue;
        int nTempo = map(nCurrentPotValue, 0, 1023, 50, 255); // Map the value to 50-255
        if(nTempo != s_nTempo)
        {
            s_nTempo = nTempo;
        }
    }

    // Delay in milliseconds = 1 minute    60 seconds   1000 milliseconds
    //                         --------- * ---------- * -----------------
    //                         (X) beats   minute       second
    int nDelay = (int)((60.0 * 1000.0) / (float)s_nTempo);
    PlayNote(nDelay);
}

void PlayNote(int nDuration)
{
    nDuration = (nDuration / 2);
    digitalWrite(LED_PIN, HIGH);        // Set the LED on
    delay(nDuration);                   // Wait for half the (original) duration
    digitalWrite(LED_PIN, LOW);         // Set the LED off
    delay(nDuration);                   // Wait for half the (original) duration
}

VolumePedal

There are several MIDI foot controllers currently on the market, like the Roland FC-300 and the Behringer FCB1010, but these foot controllers are bulky and if you only want a single expression pedal they are a bit overkill. Unfortunately, an individual MIDI volume pedal doesn't exist on the market at this time, so I decided to create one using an Arduino.

Sending a MIDI CC volume change message is very easy and only three bytes need to be sent:

0xB0        - CC change command (0xB0 - 0xBF depending on the MIDI channel)
0x07        - Volume command
the value   - The value of the volume, between 0 and 127

...so the actual MIDI commands sent aren't very complicated or long, however the supporting hardware and software did take a little bit of work... (not too much, but still some work and some trial-and-error)

Before looking at the source code, let's go over the parts. At the minimum, you will need:

  • An Arduino processor. For my expression pedal, I used an Arduino Pro Mini (the 5V version)
  • An expression pedal enclosure
  • A wah potentiometer (it doesn't come with the pedal enclosure)
  • A 3-pole footswitch
  • A LED (though this is optional)
  • A DC power plug
  • A 9VDC power supply
  • A MIDI plug
  • A 220 Ohm resister for the MIDI port
  • A 1K Ohm resister for the LED
  • A breadboard to pre-assemble the circuit on and verify that the sketch works
  • A blank circuit board to solder everything onto
  • A set of headers to solder the Arduino to the circuit board
  • Wire

The expression pedal enclosure that ships from Small Bear Electronics is unfinished and is bare aluminium. Before assembling your project, you will probably want to paint your enclosure. I sent mine to PedalEnclosures.com and had the enclosure painted in a red hammertone finish.

As far as the wiring goes, we've already covered how to wire the LED and potentiometer. The only components remaining are the 9VDC input, the footswitch, and the MIDI Out port. For the MIDI Out port, you will need to connect:

  • Pin 5 to TX0
  • Pin 2 to GND
  • Pin 4 to a 220 Ohm resister, which is then connected to a +5VDC power source (VCC)

Image 4

For the 9VDC input plug and the footswitch, solder the footswitch so it breaks the circuit after the +V on the input plug:

Image 5

...and now connect everything to the breadboard:

Image 6

Image 7

The camera I have wasn't able to take a very good picture of final assembly, so here is a rundown of each pin on the Arduino Mini:

Top Row
RAW Footswitch +V
GND Power supply plug GND
RST  
VCC +V rail
A3  
A2  
A1  
A0 Pot variable resistor (middle connector)
13 LED
12  
11  
10  

 

Bottom Row
TX0 MIDI Pin 5
RXI  
RST  
GND Ground rail
2  
3  
4  
5  
6  
7  
8  
9  

 

+V Rail 220 Ohm resistor (connected to MIDI Pin 4)
  Pot +V

 

GND Rail MIDI Pin 2
  1K resistor (connected to LED ground)

The source code for the MIDI volume pedal is as follows:

C++
//#define DEBUG                 1

// Constants
const int LED_PIN = 13;             // LED connected to digital pin 13
const int POT_PIN = 0;              // Pot connected to analog pin 0
const int POT_THRESHOLD = 7;        // Threshold amount to guard against false values
const int MIDI_CHANNEL = 0;         // MIDI Channel 1

#ifdef DEBUG
const int DEBUG_RATE = 9600;        // Serial debugging communicates at 9600 baud
const int SERIAL_PORT_RATE = DEBUG_RATE;
#else
const int MIDI_BAUD_RATE = 31250;   // MIDI communicates at 31250 baud
const int SERIAL_PORT_RATE = MIDI_BAUD_RATE;
#endif


void setup()
{
    pinMode(LED_PIN, OUTPUT);          // Sets the digital pin as output
    digitalWrite(LED_PIN, HIGH);       // Turn the LED on
    Serial.begin(SERIAL_PORT_RATE);     // Starts communication with the serial port
}

void loop()
{
    static int s_nLastPotValue = 0;
    static int s_nLastMappedValue = 0;

    int nCurrentPotValue = analogRead(POT_PIN);
    if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
        return;
    s_nLastPotValue = nCurrentPotValue;

    int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 127); // Map the value to 0-127
    if(nMappedValue == s_nLastMappedValue)
        return;
    s_nLastMappedValue = nMappedValue;

    MidiVolume(MIDI_CHANNEL, nMappedValue);
}

void MidiVolume(byte channel, byte volume)
{
#ifdef DEBUG
    Serial.println(volume, DEC);
#else
    Serial.print(0xB0 | (channel & 0x0F), BYTE);    // Control change command
    Serial.print(0x07, BYTE);                       // Volume command
    Serial.print(volume & 0x7F, BYTE);              // Volume 0-127
#endif
}

Going through the code, first there is:

C++
//#define DEBUG                    1

I use this constant to define whether or not the build is a debug build. To run in debug mode, simply uncomment this line.

Next, there are the constants:

C++
// Constants
const int LED_PIN = 13;             // LED connected to digital pin 13
const int POT_PIN = 0;              // Pot connected to analog pin 0
const int POT_THRESHOLD = 7;        // Threshold amount to guard against false values
const int MIDI_CHANNEL = 0;         // MIDI Channel 1

#ifdef DEBUG
const int DEBUG_RATE = 9600;        // Serial debugging communicates at 9600 baud
const int SERIAL_PORT_RATE = DEBUG_RATE;
#else
const int MIDI_BAUD_RATE = 31250;   // MIDI communicates at 31250 baud
const int SERIAL_PORT_RATE = MIDI_BAUD_RATE;
#endif

The LED indicates that the unit is on. It's optional but if you attach a LED, the LED needs to be connected to digital pin 13 (and don't forget the resistor and the orientation of the LED).

The potentiometer is connected to analog pin 0. The Arduino specs state that the analog pins want devices with only 10K Ohm resistance, however wah potentiometers (which is what we will be using with the expression pedal) are only available in 100K and 200K. I purchased a 200K potentiometer and found that although it was a bit noisy, it still worked, and needed to set the threshold to 7:

C++
const int POT_THRESHOLD = 7;        // Threshold amount to guard against false values

The next value, the MIDI channel, is left as a hardcoded constant. Just like the metronome sketch, we could add a potentiometer to select the MIDI channel but for my purposes I only needed the MIDI channel fixed to one channel.

The next group of constants pertains to the serial port rate. Both the MIDI port and the print() commands used for debugging use the serial port and don't coexist well. One needs to either be in debug mode or release mode so I switch between the two using the #ifdef DEBUG check.

Next is the code. The setup() function is minimal and is typical of what we've already seen so far:

C++
void setup()
{
    pinMode(LED_PIN, OUTPUT);           // Sets the digital pin as output
    digitalWrite(LED_PIN, HIGH);        // Turn the LED on
    Serial.begin(SERIAL_PORT_RATE);     // Starts communication with the serial port
}

The next function, loop(), contains the bulk of the code:

C++
void loop()
{
    static int s_nLastPotValue = 0;
    static int s_nLastMappedValue = 0;

    int nCurrentPotValue = analogRead(POT_PIN);
    if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
        return;
    s_nLastPotValue = nCurrentPotValue;

    int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 127); // Map the value to 0-127
    if(nMappedValue == s_nLastMappedValue)
        return;
    s_nLastMappedValue = nMappedValue;

    MidiVolume(MIDI_CHANNEL, nMappedValue);
}

s_nLastPotValue and the if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD) check guards against a noisy potentiometer, and s_nLastMappedValue and the if(nMappedValue == s_nLastMappedValue) check guard against sending the value if it was the last value sent.

MidiVolume() is where the bytes are sent. As I previously mentioned, just three bytes are needed to send the MIDI CC volume message:

C++
void MidiVolume(byte channel, byte volume)
{
#ifdef DEBUG
    Serial.println(volume, DEC);
#else
    Serial.print(0xB0 | (channel & 0x0F), BYTE);    //  Control change command
    Serial.print(0x07, BYTE);                       //  Volume command
    Serial.print(volume & 0x7F, BYTE);              //  Volume 0-127
#endif
}

And that's it. If you wired the hardware correctly, everything should work at this point. On Windows, there is a freeware program available called MIDI-OX which allows you to see all incoming MIDI messages, and on Mac OSX there is a freeware program available called MIDI Monitor which does the same thing. If you run either of these programs (and have your expression pedal's MIDI Out port connected to your computer) you should see MIDI CC volume messages come in as you move the wah potentiometer.

Assembling the Final Components

At this point, I was ready to commit the design to the final circuit board and assemble the components. I transferred the design on the breadboard to the circuit board and added connectors so I could easily add and remove the component not directly on the circuit board:

Image 8

This picture was taken at close range. In doing so, the image produced was distorted. The green wire at the bottom connects pin 5 on the MIDI connector to TX0, but in this image it looks like the connection is to RXI, not TX0.

Image 9

Circuit board flipped horizontally to show back side (the top rail is +V and the bottom rail is GND).

Image 10

Circuit board with components

Image 11

Close-up of the circuit board connections

Image 12

Internal assembly of components

Adjusting the Range

After the hardware was fully assembled, the wah pot no longer had its full range of motion (due to the limited range of the top plate) so a minor code change was needed. When the wah pot had its full range of motion, s_nLastPotValue had a range of 0-1023 and s_nLastMappedValue had a range of 0-127. With the limited range s_nLastPotValue now had a range of 0-1002 and s_nLastMappedValue had a range of 0-125. I updated loop() with a minor change and it became:

C++
void loop()
{
    static int s_nLastPotValue = 0;
    static int s_nLastMappedValue = 0;

    int nCurrentPotValue = analogRead(POT_PIN);
    if(abs(nCurrentPotValue - s_nLastPotValue) < POT_THRESHOLD)
        return;
    s_nLastPotValue = nCurrentPotValue;

    //int nMappedValue = map(nCurrentPotValue, 0, 1023, 0, 127); // Map the value to 0-127
    int nMappedValue = map(nCurrentPotValue, 0, 1002, 0, 127);   // Map the value to 0-127
    if(nMappedValue > 127)
        nMappedValue = 127;
    if(nMappedValue == s_nLastMappedValue)
        return;
    s_nLastMappedValue = nMappedValue;

    MidiVolume(MIDI_CHANNEL, nMappedValue);
}

SysExPedal

Some MIDI messages like the MIDI Note On/Off command and the Program Change command are specific, closed, and strictly defined. However manufactures needed a mechanism to transfer custom data from one device to another, so the open-ended System Exclusive (SysEx) message was added to the MIDI specification. A SysEx message starts with 0xF0 and ends with 0xF7. Typically the format within these markers is:

0xF0 - SysEx start
  xx - Manufacturer ID
  xx - Model ID
  xx - MIDI channel
  xx - data 1
  xx - data 2
  ...
  xx - data N
0xF7 - SysEx end

...but the format is left up to the manufacturer to define. As I mentioned at the beginning of this article, the whole reason for this project and getting into hardware programming is that I have an effects processor that doesn't respond to MIDI CC messages but does respond to SysEx messages. Migrating from sending CC messages to sending SysEx messages is very straight forward. If you compare the SysExPedal sketch with the VolumePedal sketch, you will see that the two sketches are almost identical. Where MidiVolume() was called when the potentiometer changed is now SendSysEx():

C++
void SendSysEx(byte channel, byte volume)
{
#ifdef DEBUG
    Serial.println(volume, DEC);
#else
    SerialOutput(0xF0);         // SysEx start
    SerialOutput(0x00);         // Manufacturer 0
    SerialOutput(0x01);         // Model 1
    SerialOutput(channel);      // MIDI channel
    SerialOutput(0x42);         // Data
    SerialOutput(0x42);         // Data
    SerialOutput(volume);       // Data
    SerialOutput(0x42);         // Data
    SerialOutput(0x42);         // Data
    SerialOutput(0xF7);         // SysEx end
#endif
}

What's presented here is an example for a fictional device. Since the format of a SysEx message is unique to a specific device (and in my case, a specific patch), I decided to just post the raw outline of what a SysEx message would look like.

Conclusion

This wraps up the third article and concludes my series on the Arduino hardware platform. We've gone from hooking up a simple LED to developing a fully functioning product. Hopefully you will be able to take some of the ideas presented here and build your own unique devices based on an Arduino microcontroller.

History

  • 16th July, 2009: Initial post
  • 16th July, 2009: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
In a nutshell, my forte is Windows, Macintosh, and cross-platform development, and my interests are in UI, image processing, and MIDI application development.

Comments and Discussions

 
QuestionMidi wah Pin
Member 1543923121-Nov-21 10:47
Member 1543923121-Nov-21 10:47 
QuestionHow to Change the code for The BYTE keyword is no longer supported Pin
Member 147024731-Jan-20 2:04
Member 147024731-Jan-20 2:04 
QuestionWhat is the total cost of all the parts? Pin
Member 1108773616-Sep-14 16:28
Member 1108773616-Sep-14 16:28 
AnswerRe: What is the total cost of all the parts? Pin
jeffb4216-Sep-14 18:41
jeffb4216-Sep-14 18:41 
GeneralMy vote of 5 Pin
Abinash Bishoyi21-Feb-13 20:10
Abinash Bishoyi21-Feb-13 20:10 
Questionreplacement for reserved word BYTE Pin
Member 92293239-Jul-12 2:46
Member 92293239-Jul-12 2:46 
GeneralRe: replacement for reserved word BYTE Pin
jeffb429-Jul-12 17:36
jeffb429-Jul-12 17:36 
GeneralRe: replacement for reserved word BYTE Pin
Member 922932310-Jul-12 3:46
Member 922932310-Jul-12 3:46 
GeneralMy vote of 5 Pin
mauriciobarros11-Jun-12 6:29
mauriciobarros11-Jun-12 6:29 
GeneralRe: My vote of 5 Pin
jeffb429-Jul-12 17:38
jeffb429-Jul-12 17:38 
GeneralThe code rocks! Pin
munky_300023-Jan-11 8:42
munky_300023-Jan-11 8:42 
GeneralRe: The code rocks! Pin
jeffb429-Jul-12 17:44
jeffb429-Jul-12 17:44 
GeneralArduino-based vs software Pin
waveland17-Jun-10 17:10
waveland17-Jun-10 17:10 
GeneralRe: Arduino-based vs software Pin
jeffb4229-Jun-10 10:15
jeffb4229-Jun-10 10:15 
GeneralGood Job Pin
ProfessorDan18-Aug-09 9:54
ProfessorDan18-Aug-09 9:54 
GeneralThis series gets my 5 Pin
David MacDermot23-Jul-09 6:41
David MacDermot23-Jul-09 6:41 
GeneralWow! Pin
Modulrob22-Jul-09 6:32
Modulrob22-Jul-09 6:32 
GeneralRe: Wow! Pin
jeffb4225-Jul-09 14:02
jeffb4225-Jul-09 14:02 
GeneralExcellent series Pin
mel_6718-Jul-09 7:51
mel_6718-Jul-09 7:51 
GeneralThanks everyone Pin
jeffb4217-Jul-09 14:04
jeffb4217-Jul-09 14:04 
QuestionWhy the three? Pin
jeffb4217-Jul-09 14:00
jeffb4217-Jul-09 14:00 
AnswerRe: Why the three? Pin
Jim Crafton22-Jul-09 8:37
Jim Crafton22-Jul-09 8:37 
GeneralSuperb Pin
Sacha Barber17-Jul-09 5:41
Sacha Barber17-Jul-09 5:41 
GeneralAWESOME! Pin
JeffBall16-Jul-09 16:40
JeffBall16-Jul-09 16:40 
GeneralReminds me of a sustain-pedal project... Pin
supercat916-Jul-09 13:25
supercat916-Jul-09 13:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.