Skip to content

Instantly share code, notes, and snippets.

@codeandmake
Created January 7, 2023 21:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codeandmake/a2a5672fd49b748823cb352ba72d804a to your computer and use it in GitHub Desktop.
Save codeandmake/a2a5672fd49b748823cb352ba72d804a to your computer and use it in GitHub Desktop.
/*
* Copyright 2023 Code and Make (codeandmake.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* Arduino-powered Adafruit MacroPad RP2040 Midi Controller
* by Code and Make (https://codeandmake.com/)
*
* This code accompanies the following tutorial:
* https://codeandmake.com/post/arduino-adafruit-macropad-rp2040-midi-controller
*
* Arduino-powered Adafruit MacroPad RP2040 Midi Controller v1.0 (7 January 2023)
*/
#define ARRAY_SIZE(arr) (sizeof((arr)) / sizeof((arr)[0]))
#define FIRST_NOTE 60
#define LINE_GAP 1
#define MIDI_OUT_CHANNEL 1
#define MIDI_IN_CHANNEL 1
#define NUMBER_OF_KEYS 12
#define NUMBER_OF_KEYS_PER_ROW 3
#define SECTION_GAP 3
#define NUMBER_OF_NOTES 128
#define UNDERLINE_HEIGHT 2
#include <Adafruit_NeoPixel.h>
#include <Adafruit_SH110X.h>
#include <Adafruit_TinyUSB.h>
#include <Arduino.h>
#include <MIDI.h>
#include <RotaryEncoder.h>
// MIDI
Adafruit_USBD_MIDI usbdMidi;
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usbdMidi, MIDI);
// NeoPixel
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUM_NEOPIXEL, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
// OLED display
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &SPI1, OLED_DC, OLED_RST, OLED_CS);
boolean displayUpdateNeeded = true;
// Rotary encoder
RotaryEncoder encoder(PIN_ROTA, PIN_ROTB, RotaryEncoder::LatchMode::FOUR3);
void checkPosition() {
encoder.tick();
}
int encoderPosition = 0;
boolean encoderPressed = false;
struct Option
{
String name;
byte min;
byte max;
byte value;
byte range;
};
Option options[3] = {
{ "Range", 0, NUMBER_OF_NOTES - NUMBER_OF_KEYS, FIRST_NOTE, NUMBER_OF_KEYS, },
{ "Attack", 0, 127, 64, 1, },
{ "Decay", 0, 127, 0, 1, },
};
byte selectedOption = 0;
const String noteNames[12] = {
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
};
uint32_t keyColours[NUMBER_OF_KEYS] = { };
boolean internalNoteStates[NUMBER_OF_NOTES] = { };
boolean externalNoteStates[NUMBER_OF_NOTES] = { };
void setup() {
// Delay recommended for RP2040
delay(100);
// Set keys to inputs
for (int i = 0; i < NUMBER_OF_KEYS; i++) {
pinMode(i + 1, INPUT_PULLUP);
}
// Set rotary encoder inputs and interrupts
pinMode(PIN_ROTA, INPUT_PULLUP);
pinMode(PIN_ROTB, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ROTA), checkPosition, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ROTB), checkPosition, CHANGE);
// Generate key colours
for (int i = 0; i < NUMBER_OF_KEYS; i++) {
keyColours[i] = pixels.ColorHSV(65536 / NUMBER_OF_KEYS * i);
}
// Initialise note state arrays
for (int i = 0; i < NUMBER_OF_NOTES; i++) {
internalNoteStates[i] = false;
externalNoteStates[i] = false;
}
// Start pixels
pixels.begin();
pixels.setBrightness(40);
pixels.show();
// Start OLED display
display.begin(0, true);
display.setTextWrap(false);
display.setTextColor(SH110X_WHITE, SH110X_BLACK);
display.display();
// Initialise MIDI
MIDI.begin(MIDI_IN_CHANNEL);
MIDI.setHandleNoteOn(noteOnHandler);
MIDI.setHandleNoteOff(noteOffHandler);
// Wait until the device is mounted
while (!TinyUSBDevice.mounted()) {
delay(1);
}
}
void loop() {
processEncoder();
processMIDI();
updatePixels();
updateDisplay();
}
void processMIDI() {
for (int i = 0; i < NUMBER_OF_KEYS; i++) {
if (!digitalRead(i + 1)) {
if(!internalNoteStates[options[0].value + i]) {
internalNoteStates[options[0].value + i] = true;
MIDI.sendNoteOn(options[0].value + i, options[1].value, MIDI_OUT_CHANNEL);
}
} else {
if(internalNoteStates[options[0].value + i]) {
internalNoteStates[options[0].value + i] = false;
MIDI.sendNoteOff(options[0].value + i, options[2].value, MIDI_OUT_CHANNEL);
}
}
}
MIDI.read();
}
void processEncoder() {
// Check whether encoder is pressed
if (!digitalRead(PIN_SWITCH)) {
if (!encoderPressed) {
encoderPressed = true;
selectedOption = (selectedOption + ARRAY_SIZE(options) + 1) % ARRAY_SIZE(options);
displayUpdateNeeded = true;
}
} else {
encoderPressed = false;
}
// Check the encoder's position
encoder.tick();
int newEncoderPosition = encoder.getPosition();
if (encoderPosition != newEncoderPosition) {
if(selectedOption == 0) {
resetNotes();
}
int delta = mod(0 - (newEncoderPosition - encoderPosition), ARRAY_SIZE(options));
options[selectedOption].value = constrain(options[selectedOption].value + delta, options[selectedOption].min, options[selectedOption].max);
encoderPosition = newEncoderPosition;
displayUpdateNeeded = true;
}
}
void updatePixels() {
for (int i = 0; i < NUMBER_OF_KEYS; i++) {
if (internalNoteStates[options[0].value + i] || externalNoteStates[options[0].value + i]) {
pixels.setPixelColor(i, 0xffffff);
} else {
int keyColourIndex = (options[0].value + i) % ARRAY_SIZE(keyColours);
pixels.setPixelColor(i, keyColours[keyColourIndex]);
}
}
pixels.show();
}
void updateDisplay() {
if (displayUpdateNeeded) {
const int16_t upperSectionWidth = display.width() / 3;
int16_t x1, y1;
uint16_t w, h;
display.clearDisplay();
display.setTextSize(1);
for (int i = 0; i < ARRAY_SIZE(options); i++) {
display.getTextBounds(options[i].name, 0, 0, &x1, &y1, &w, &h);
display.setCursor((upperSectionWidth * i) + (upperSectionWidth / 2) - (w / 2), 0);
display.println(options[i].name);
}
display.getTextBounds(options[0].name, 0, 0, &x1, &y1, &w, &h);
uint16_t secondLineY = h + LINE_GAP;
display.setTextSize(1);
for (int i = 0; i < ARRAY_SIZE(options); i++) {
String text;
// If it is a ranged value
if (options[i].range > 1) {
text = String(options[i].value) + "-" + String(options[i].value + options[i].range - 1);
} else {
text = String(options[i].value);
}
display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
display.setCursor((upperSectionWidth * i) + (upperSectionWidth / 2) - (w / 2), secondLineY);
display.println(text);
}
display.getTextBounds(String(options[0].value), 0, 0, &x1, &y1, &w, &h);
uint16_t thirdLineY = secondLineY + h + LINE_GAP;
display.fillRect(selectedOption * upperSectionWidth, thirdLineY, upperSectionWidth, UNDERLINE_HEIGHT, SH110X_WHITE);
uint16_t fourthLineY = thirdLineY + UNDERLINE_HEIGHT + SECTION_GAP;
const int16_t lowerSectionWidth = display.width() / NUMBER_OF_KEYS_PER_ROW;
display.setTextSize(1);
for (int i = 0; i < (NUMBER_OF_KEYS / NUMBER_OF_KEYS_PER_ROW); i++) {
for (int j = 0; j < NUMBER_OF_KEYS_PER_ROW; j++) {
int keyNumber = (i * NUMBER_OF_KEYS_PER_ROW) + j;
if (keyNumber < NUMBER_OF_KEYS) {
int noteNameIndex = (options[0].value + keyNumber) % ARRAY_SIZE(noteNames);
String text = String(noteNames[noteNameIndex]) + (((options[0].value + keyNumber) / 12) - 1);
display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
display.setCursor((lowerSectionWidth * j) + (lowerSectionWidth / 2) - (w / 2), fourthLineY + (i * h));
display.println(text);
}
}
}
display.setTextSize(1);
String url = "codeandmake.com";
display.getTextBounds(url, 0, 0, &x1, &y1, &w, &h);
display.setCursor((display.width() / 2) - (w / 2), display.height() - h);
display.println(url);
// Display oled
display.display();
}
displayUpdateNeeded = false;
}
void resetNotes() {
for (int i = 0; i < NUMBER_OF_NOTES; i++) {
if(internalNoteStates[i]) {
MIDI.sendNoteOff(i, options[2].value, MIDI_OUT_CHANNEL);
internalNoteStates[i] = false;
}
externalNoteStates[i] = false;
}
}
void noteOnHandler(byte channel, byte note, byte velocity) {
if(channel == MIDI_IN_CHANNEL) {
externalNoteStates[note] = true;
}
}
void noteOffHandler(byte channel, byte note, byte velocity) {
if(channel == MIDI_IN_CHANNEL) {
externalNoteStates[note] = false;
}
}
int mod(int x, int m) {
return (x % (m + m)) % m;
}
@davedarko
Copy link

just tried it with my macropad clone and it worked very well with "garage band", had a good time just jamming :) thanks for sharing!

@mrlab0489
Copy link

hows it going @davedarko i wanted to use my adafruit as a midi controller but after watching the tutorial,im still having issue with going through the process. is there any way you can possibly help me out
than you

@davedarko
Copy link

@mrlab0489 Not sure I can, since I don't have a macropad myself. But if you could describe your problem here, then maybe @codeandmake can help out? Where are you stuck?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment