MIDI controller on a SparkFun Pro Micro
Two faders, a knob, four drum pads and an encoder on a Pro Micro, sending real MIDI bytes the 5-pin way, with every message spelled out in the Serial Monitor. The whole build, a SparkFun Pro Micro and 11 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
The parts retail for about $20. On Mokxi they're free.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// MIDI controller: two faders, a knob, four pads and an encoder.
//
// MIDI is three-byte messages at 31250 baud. A status byte says what and on
// which channel (0xB0 is Control Change on channel 1, 0x99 is Note On on
// channel 10), then one or two data bytes from 0 to 127.
//
// This sketch sends real MIDI bytes out of TX1 (pin 1), the classic 5-pin DIN
// way, and prints every message in words on the USB Serial Monitor, with the
// bytes it sent in brackets. On a real build, TX1 goes through 220 ohms to
// pin 5 of a DIN socket and 5 V through 220 ohms to pin 4. Here the lamp in
// that loop stands in for the receiver's optocoupler: it flickers with every
// byte, exactly as the opto inside a synth would.
//
// (USB MIDI, where the board shows up as a MIDI device on its own, needs the
// MIDIUSB library, which Mokxi does not have yet. These are the same messages
// over the older wire.)
//
// Wiring (SparkFun Pro Micro):
// faders A2 A1 -> CC 20 and 21 knob A3 -> CC 22
// pads 5 6 7 8 to GND -> notes 36 38 42 39 on channel 10 (kick, snare,
// closed hat, clap in General MIDI drums)
// encoder CLK 10, DT 16 -> program change; its button 14 -> All Notes Off
// MIDI out: TX1 (pin 1)
//
// The encoder is read by polling: loop() goes round in well under a
// millisecond, far faster than a hand can turn a knob.
#include <Encoder.h>
const uint8_t ANALOG_PINS[3] = {A2, A1, A3};
const uint8_t PAD_PINS[4] = {5, 6, 7, 8};
const uint8_t PAD_NOTES[4] = {36, 38, 42, 39};
const uint8_t ENCODER_BUTTON = 14;
const uint8_t FIRST_CC = 20; // 20 to 31 are left undefined by the MIDI spec, free to map
const uint8_t CHANNEL = 0; // channel 1, counted from 0 on the wire
const uint8_t DRUMS = 9; // channel 10, where General MIDI keeps the drums
const uint8_t VELOCITY = 100;
Encoder encoder(10, 16);
long encoderAt = 0;
uint8_t program = 0;
int lastRaw[3];
uint8_t lastValue[3];
bool padDown[4];
unsigned long padAt[4];
bool buttonDown = false;
unsigned long buttonAt = 0;
// ---- the wire -----------------------------------------------------------------
void hex(uint8_t b) {
if (b < 0x10) Serial.print('0');
Serial.print(b, HEX);
}
// Send the bytes on TX1 and show them, so what you read is what went out.
void send(uint8_t status, uint8_t d1, int d2) {
Serial1.write(status);
Serial1.write(d1);
if (d2 >= 0) Serial1.write((uint8_t)d2);
Serial.print(" (");
hex(status);
Serial.print(' ');
hex(d1);
if (d2 >= 0) {
Serial.print(' ');
hex((uint8_t)d2);
}
Serial.println(')');
}
void noteName(uint8_t note) {
static const char NAMES[12][3] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
Serial.print(NAMES[note % 12]);
Serial.print(note / 12 - 1);
Serial.print(" (");
Serial.print(note);
Serial.print(')');
}
// ---- faders and knobs -----------------------------------------------------------
void readAnalog(uint8_t i) {
int raw = analogRead(ANALOG_PINS[i]);
// A pot's reading wobbles by a count or two. Only a move of 8 counts, one
// whole MIDI step, is news, so a fader at rest stays quiet.
if (abs(raw - lastRaw[i]) < 8) return;
uint8_t value = (uint8_t)(raw >> 3);
lastRaw[i] = raw;
if (value == lastValue[i]) return;
lastValue[i] = value;
if (i < 2) {
Serial.print("Fader ");
Serial.print(i + 1);
} else {
Serial.print("Knob");
}
Serial.print(" CC ");
Serial.print(FIRST_CC + i);
Serial.print(" = ");
Serial.print(value);
send(0xB0 | CHANNEL, FIRST_CC + i, value);
}
// ---- pads ---------------------------------------------------------------------------
void readPad(uint8_t i) {
bool down = digitalRead(PAD_PINS[i]) == LOW;
// Count a change at once, then ignore the pin for 20 ms of contact bounce.
if (down == padDown[i] || millis() - padAt[i] < 20) return;
padDown[i] = down;
padAt[i] = millis();
Serial.print("Pad ");
Serial.print(i + 1);
Serial.print(down ? " Note On " : " Note Off ");
noteName(PAD_NOTES[i]);
if (down) {
Serial.print(" vel ");
Serial.print(VELOCITY);
send(0x90 | DRUMS, PAD_NOTES[i], VELOCITY);
} else {
send(0x80 | DRUMS, PAD_NOTES[i], 0);
}
}
// ---- the encoder ------------------------------------------------------------------------
void readEncoder() {
// Four counts for every click of a KY-040.
long now = encoder.read();
int step = 0;
if (now - encoderAt >= 4) step = 1;
if (encoderAt - now >= 4) step = -1;
if (step == 0) return;
encoderAt += step * 4;
program = (uint8_t)((program + 128 + step) % 128);
Serial.print("Encoder Program Change ");
Serial.print(program + 1); // synths count programs from 1
send(0xC0 | CHANNEL, program, -1);
}
void readEncoderButton() {
bool down = digitalRead(ENCODER_BUTTON) == LOW;
if (down == buttonDown || millis() - buttonAt < 30) return;
buttonDown = down;
buttonAt = millis();
if (down) {
// The panic button: CC 123 tells every voice on the channel to stop.
Serial.print("Encoder button All Notes Off");
send(0xB0 | DRUMS, 123, 0);
}
}
void setup() {
Serial.begin(115200);
Serial1.begin(31250); // the MIDI baud rate, the same since 1983
for (uint8_t i = 0; i < 4; i++) pinMode(PAD_PINS[i], INPUT_PULLUP);
pinMode(ENCODER_BUTTON, INPUT_PULLUP);
for (uint8_t i = 0; i < 3; i++) lastRaw[i] = -100;
Serial.println("MIDI controller ready. Move a fader, hit a pad.");
}
void loop() {
for (uint8_t i = 0; i < 3; i++) readAnalog(i);
for (uint8_t i = 0; i < 4; i++) readPad(i);
readEncoder();
readEncoderButton();
}
Parts list
13 parts, plus the jumper wires. They retail for about $20; on Mokxi they're free.
- 1 × SparkFun Pro Micro
- 1 × Full-size breadboard
- 2 × Resistor, 220 Ω
- 1 × LED, green
- 2 × Slide potentiometer
- 4 × Pushbutton
- 1 × Potentiometer
- 1 × Rotary encoder, KY-040
How it is wired
15 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- SparkFun Pro Micro pin 1; Resistor, 220 Ω (2) pin 2
- SparkFun Pro Micro pin 5; Pushbutton (1) pin 1a; Pushbutton (1) pin 1b
- SparkFun Pro Micro pin 6; Pushbutton (2) pin 1a; Pushbutton (2) pin 1b
- SparkFun Pro Micro pin 7; Pushbutton (3) pin 1a; Pushbutton (3) pin 1b
- SparkFun Pro Micro pin 8; Pushbutton (4) pin 1a; Pushbutton (4) pin 1b
- Ground: SparkFun Pro Micro pin GND; Slide potentiometer (1) pin 1; Slide potentiometer (2) pin 1; Pushbutton (1) pin 2a; Pushbutton (1) pin 2b; Pushbutton (2) pin 2a; Pushbutton (2) pin 2b; Pushbutton (3) pin 2a; Pushbutton (3) pin 2b; Pushbutton (4) pin 2a; Pushbutton (4) pin 2b; Potentiometer pin 1; Rotary encoder, KY-040 pin GND
- SparkFun Pro Micro pin VCC; Resistor, 220 Ω (1) pin 1; Slide potentiometer (1) pin 3; Slide potentiometer (2) pin 3; Potentiometer pin 3; Rotary encoder, KY-040 pin VCC
- SparkFun Pro Micro pin A3; Potentiometer pin 2
- SparkFun Pro Micro pin A2; Slide potentiometer (1) pin 2
- SparkFun Pro Micro pin A1; Slide potentiometer (2) pin 2
- SparkFun Pro Micro pin 14; Rotary encoder, KY-040 pin SW
- SparkFun Pro Micro pin 16; Rotary encoder, KY-040 pin DT
- SparkFun Pro Micro pin 10; Rotary encoder, KY-040 pin CLK
- Resistor, 220 Ω (1) pin 2; LED, green pin A
- LED, green pin C; Resistor, 220 Ω (2) pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.