Last active
September 20, 2023 05:43
-
-
Save cowboy/6c2230d54aea58577c5b7953ea86aebe to your computer and use it in GitHub Desktop.
Akai Force Rec Pedal w/ 8x8 Matrix Display (USB MIDI) - for Teensy LC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// =============================================================== | |
// Akai Force Rec Pedal w/ 8x8 Matrix Display (USB MIDI) - for Teensy LC | |
// "Cowboy" Ben Alman, 2021 | |
// https://gist.github.com/cowboy/6c2230d54aea58577c5b7953ea86aebe | |
// =============================================================== | |
// Why does this exist? | |
// | |
// I designed this to circumvent an issue with the Akai Force where receiving | |
// an MMC Rec message signals that the loop should end at the end of the | |
// current measure BUT it also stops recording the current clip at the moment | |
// MMC Rec is received, NOT at the end of the measure, which makes it very hard | |
// to end loop recording via foot pedal in a musical way. | |
// | |
// Usage | |
// | |
// While recording: | |
// - Press the pedal to queue an MMC Rec message to be sent at the end of the | |
// current measure. This will a) tell Force that it should set the loop length | |
// to the end of the measure and b) turn recording mode off at the end of the | |
// measure. | |
// - If you hold the pedal past the end of the measure, it will do "a" above | |
// but keep recording mode on, instead of turing it off. | |
// - If you press the pedal again before the end of the current measure, it | |
// will cancel the previous queued behavior. | |
// | |
// While not recording: | |
// - Press the pedal to queue an MMC Rec message to be sent at the end of | |
// the current measure. | |
// - If you press the pedal again before the end of the current measure, it | |
// will cancel the previous queued behavior. | |
// | |
// In order for this to work, you need to ensure the Force is set to send "MIDI | |
// Clock" and "Receive MMC" and be sure to enable "Sync" on the "Cowboy Akai | |
// Force Rec Pedal" Output Port. Also, note that because MIDI clock is dumb, | |
// this device can't know about any other time signature than what is hard-coded | |
// into it, which in this case is 4. I don't think Force supports anything other | |
// than 4/4 time, but if you want to change this, see BEATS_PER_MEASURE. | |
// | |
// How do I build this? | |
// | |
// This requires a momentary (normally open) foot pedal, like the Yamaha FC5, as | |
// well as an MAX7219-controlled 8x8 LED Matrix. See the photos in the comments | |
// to get an idea of how I built it. Here are the parts I used: | |
// | |
// * Teensy LC https://www.pjrc.com/store/teensylc.html | |
// * 1/4" Mono Jack https://www.switchcraft.com/Category_Multi.aspx?Parent=952 | |
// * Yamaha FC5 https://smile.amazon.com/gp/product/B00005ML71 | |
// * LED Matrix https://smile.amazon.com/gp/product/B07VM6HXN5 | |
// * Project Box https://smile.amazon.com/gp/product/B0895HPW1T | |
// * USB adapter https://www.amazon.com/gp/product/B07FL3MKLK | |
// | |
// How do I program this? | |
// | |
// Program with the Arduino IDE and Teensyduino. Their instructions are pretty | |
// comprehensive, but you may also want to read some guides or tutorials. | |
// | |
// * Arduino IDE https://www.arduino.cc/en/software | |
// * Teensyduino https://www.pjrc.com/teensy/teensyduino.html | |
// * MAX7219 Lib https://github.com/GyverLibs/GyverMAX7219 | |
// * (in English) https://translate.google.com/translate?hl=en&sl=en&tl=en&u=https%3A%2F%2Fgithub.com%2FGyverLibs%2FGyverMAX7219 | |
// * Bitmaps https://bitmap-code-generator.benalman.com/ | |
// | |
// Have fun! | |
#include <Bounce.h> | |
#include <GyverMAX7219.h> | |
// Uncomment the next line to simulate pressing play + running on boot | |
//#define RUN_TEST | |
// Change these pins if desired | |
const int SWITCH_PIN = 0; | |
const int LED_PIN = LED_BUILTIN; | |
const int MATRIX_DATA_PIN = 23; | |
const int MATRIX_CLK_PIN = 21; | |
const int MATRIX_CS_PIN = 22; | |
const int MATRIX_WIDTH = 8; | |
const int MATRIX_HEIGHT = 8; | |
const int MATRIX_BRIGHTNESS = 8; | |
MAX7219 < MATRIX_WIDTH, MATRIX_HEIGHT, MATRIX_CS_PIN, MATRIX_DATA_PIN, MATRIX_CLK_PIN > mtrx; | |
const uint8_t number_1_icon[] PROGMEM = {0x06, 0x0e, 0x16, 0x06, 0x06, 0x06, 0x06, 0x1f}; | |
const uint8_t number_2_icon[] PROGMEM = {0x0e, 0x1b, 0x03, 0x03, 0x06, 0x0c, 0x18, 0x1f}; | |
const uint8_t number_3_icon[] PROGMEM = {0x1e, 0x03, 0x03, 0x0e, 0x03, 0x03, 0x03, 0x1e}; | |
const uint8_t number_4_icon[] PROGMEM = {0x03, 0x07, 0x0b, 0x1b, 0x1f, 0x03, 0x03, 0x03}; | |
const uint8_t* const numbers[] PROGMEM = {number_1_icon, number_2_icon, number_3_icon, number_4_icon}; | |
const uint8_t play_icon[] PROGMEM = {0xff, 0x81, 0xb1, 0xbd, 0xbd, 0xb1, 0x81, 0xff}; | |
const uint8_t stop_icon[] PROGMEM = {0xff, 0x81, 0xbd, 0xbd, 0xbd, 0xbd, 0x81, 0xff}; | |
const uint8_t ok_icon[] PROGMEM = {0x65, 0x95, 0x95, 0x96, 0x96, 0x95, 0x95, 0x65}; | |
const uint8_t err_icon[] PROGMEM = {0x3d, 0x42, 0x85, 0x89, 0x91, 0xa1, 0x42, 0xbc}; | |
const uint8_t robot_0_icon[] PROGMEM = {0x42, 0x7e, 0x81, 0xa5, 0x81, 0x7e, 0x3c, 0xff}; | |
const uint8_t robot_1_icon[] PROGMEM = {0x42, 0x7e, 0x81, 0x81, 0x81, 0x7e, 0x3c, 0xff}; | |
const uint8_t* const idle_screen_icons[] PROGMEM = { | |
robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, | |
robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, | |
robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, robot_0_icon, | |
robot_1_icon, robot_0_icon, robot_1_icon, robot_0_icon, | |
}; | |
const int idle_screen_icon_count = 28; | |
const int idle_screen_frame_delay = 20000; | |
const int stop_icon_frame_delay = 100000; | |
// Assume 4/4 time | |
int BEATS_PER_MEASURE = 4; | |
int PPQN = 24; | |
int PPSN = PPQN / 4; | |
int MAX_PULSES = PPQN * BEATS_PER_MEASURE; | |
Bounce button0 = Bounce(SWITCH_PIN, 5); | |
int counter = 0; | |
int error = 0; | |
int idle_screen_counter = 0; | |
int button_pressed = 0; | |
int rec_toggle_pending = 0; | |
int clock_running = 0; | |
int led_lit = 0; | |
int just_started = 0; | |
int just_stopped = 0; | |
void setup() { | |
Serial.begin(115200); | |
// Setup pins | |
pinMode(SWITCH_PIN, INPUT_PULLUP); | |
pinMode(LED_PIN, OUTPUT); | |
// MIDI message handlers | |
usbMIDI.setHandleClock(onClock); | |
usbMIDI.setHandleStart(onStart); | |
usbMIDI.setHandleContinue(onContinue); | |
usbMIDI.setHandleStop(onStop); | |
mtrx.begin(); | |
mtrx.setBright(MATRIX_BRIGHTNESS); | |
mtrx.setRotation(3); | |
draw_icon(ok_icon); | |
blink(5); | |
init_idle_screen(); | |
#ifdef RUN_TEST | |
onStart(); | |
#endif | |
} | |
void led_on() { | |
led_lit = 1; | |
digitalWrite(LED_PIN, HIGH); | |
} | |
void led_off() { | |
led_lit = 0; | |
digitalWrite(LED_PIN, LOW); | |
} | |
void led_toggle() { | |
if (led_lit) { | |
led_off(); | |
} else { | |
led_on(); | |
} | |
} | |
void blink(int count) { | |
for (int i = 0; i < count; i++) { | |
led_on(); | |
delay(50); | |
led_off(); | |
delay(50); | |
} | |
} | |
void test_matrix() { | |
mtrx.clear(); | |
byte x, y; | |
// light one LED at a time, scanning left to right | |
// and top to bottom... useful for testing the matrix | |
for (y = 0; y < MATRIX_HEIGHT; y++) { | |
for (x = 0; x < MATRIX_WIDTH; x++) { | |
mtrx.dot(x, y); | |
mtrx.update(); | |
delay(100); | |
} | |
} | |
} | |
void loop() { | |
if (just_stopped) { | |
just_stopped++; | |
if (just_stopped == stop_icon_frame_delay) { | |
just_stopped = 0; | |
init_idle_screen(); | |
} | |
} else if (!clock_running && !error) { | |
idle_screen(); | |
} | |
button0.update(); | |
// Pedal pressed | |
if (button0.risingEdge()) { | |
button_pressed = 1; | |
if (clock_running) { | |
// Toggle pending state | |
rec_toggle_pending = 1 - rec_toggle_pending; | |
// Turn the LED on or off accordingly | |
if (rec_toggle_pending) { | |
led_on(); | |
} else { | |
led_off(); | |
} | |
} else { | |
// Complain if pressed when the clock is not running | |
blink(2); | |
} | |
} | |
// Pedal released | |
else if (button0.fallingEdge()) { | |
button_pressed = 0; | |
} | |
usbMIDI.read(); | |
#ifdef RUN_TEST | |
onClock(); | |
delay(50); | |
#endif | |
} | |
void send_rec() { | |
static uint8_t mmc_rec[6] = {0xF0, 0x7F, 0x7F, 0x06, 0x06, 0xF7}; | |
usbMIDI.sendSysEx(6, mmc_rec); | |
} | |
void clear_matrix() { | |
mtrx.clear(); | |
mtrx.update(); | |
} | |
void draw_icon(uint8_t *icon) { | |
mtrx.clear(); | |
mtrx.drawBitmap(0, 0, icon, 8, 8); | |
mtrx.update(); | |
} | |
void draw_number(int i) { | |
mtrx.clear(); | |
uint16_t ptr = pgm_read_word(&(numbers[i])); | |
mtrx.drawBitmap(-3, 0, ptr, 8, 8); | |
} | |
void update_matrix() { | |
int beat = counter / PPQN; | |
int sixteenth = counter / PPSN; | |
if (counter % PPQN == 0) { | |
draw_number(beat); | |
} | |
for (int i = 0; i <= sixteenth % 4; i++) { | |
int y1 = i * 2; | |
int y2 = y1 + 1; | |
if (rec_toggle_pending) { | |
mtrx.fastLineH(y1, 6, 7); | |
mtrx.fastLineH(y2, 6, 7); | |
} else { | |
int x1 = beat % 2 ? 6 : 7; | |
int x2 = beat % 2 ? 7 : 6; | |
mtrx.dot(x1, y1, 1); | |
mtrx.dot(x2, y1, 0); | |
mtrx.dot(x1, y2, 0); | |
mtrx.dot(x2, y2, 1); | |
} | |
} | |
mtrx.update(); | |
} | |
void init_idle_screen() { | |
idle_screen_counter = 0; | |
} | |
void idle_screen() { | |
if (++idle_screen_counter % idle_screen_frame_delay == 0) { | |
int i = idle_screen_counter / idle_screen_frame_delay; | |
if (i == idle_screen_icon_count) { | |
i = 0; | |
idle_screen_counter = 0; | |
} | |
draw_icon(pgm_read_word(&(idle_screen_icons[i]))); | |
} | |
} | |
void onClock() { | |
if (!clock_running) { | |
if (!error) { | |
error = 1; | |
draw_icon(err_icon); | |
} | |
return; | |
} | |
if (++counter == MAX_PULSES) { | |
counter = 0; | |
// Send rec message if pending | |
if (rec_toggle_pending) { | |
rec_toggle_pending = 0; | |
send_rec(); | |
// Since this is the beginning of the measure, send again | |
// if the pedal is held down | |
if (button_pressed) { | |
send_rec(); | |
} | |
} | |
} | |
if (counter % PPQN == 0 && just_started) { | |
just_started = 0; | |
} | |
if (!just_started && counter % PPSN == 0) { | |
update_matrix(); | |
} | |
if (counter == 0) { | |
led_on(); | |
} else if (counter % PPQN == 0 || counter == 2) { | |
led_toggle(); | |
} else if (counter % PPQN == 1 || counter == 3) { | |
led_toggle(); | |
} else if (counter == MAX_PULSES - 2) { | |
led_off(); | |
} | |
} | |
void onStart() { | |
Serial.println("Start"); | |
led_on(); | |
counter = 0; | |
clock_running = 1; | |
rec_toggle_pending = 0; | |
just_started = 1; | |
just_stopped = 0; | |
draw_icon(play_icon); | |
error = 0; | |
} | |
void onContinue() { | |
Serial.println("Continue"); | |
clock_running = 1; | |
error = 0; | |
} | |
void onStop() { | |
Serial.println("Stop"); | |
led_off(); | |
clock_running = 0; | |
rec_toggle_pending = 0; | |
just_started = 0; | |
just_stopped = 1; | |
error = 0; | |
draw_icon(stop_icon); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// To give your project a unique name, this code must be | |
// placed into a .c file (its own tab). It can not be in | |
// a .cpp file or your main sketch (the .ino file). | |
#include "usb_names.h" | |
// Edit these lines to create your own name. The length must | |
// match the number of characters in your custom name. | |
#define MIDI_NAME {'C', 'o', 'w', 'b', 'o', 'y', ' ', 'A', 'k', 'a', 'i', ' ', 'F', 'o', 'r', 'c', 'e', ' ', 'R', 'e', 'c', ' ', 'P', 'e', 'd', 'a', 'l'} | |
#define MIDI_NAME_LEN 27 | |
// Do not change this part. This exact format is required by USB. | |
struct usb_string_descriptor_struct usb_string_product_name = { | |
2 + MIDI_NAME_LEN * 2, | |
3, | |
MIDI_NAME | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The Force is almost perfect. Just a few tweaks. I watched a YouTuber named Youngr. He uses the Force to do some amazing loops and he seems to use foot pedals or something to stop a loop.