Skip to content

Instantly share code, notes, and snippets.

@cowboy
Last active September 20, 2023 05:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cowboy/6c2230d54aea58577c5b7953ea86aebe to your computer and use it in GitHub Desktop.
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
// ===============================================================
// 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);
}
// 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
};
@cowboy
Copy link
Author

cowboy commented Sep 28, 2021

Photos

Inside

2021-09-27 19 05 23

Front

2021-09-27 19 08 18

Back

2021-09-27 19 08 23

Video

https://www.youtube.com/watch?v=iSE7Q92W-eg

@TheAudioDabbler
Copy link

Awesome dude. Really wanting to do this for my Akai Force. Even with the newest update there still seems to be no way to use a foot pedal and trigger audio to stop at the end of the phrase.

@cowboy
Copy link
Author

cowboy commented Jan 6, 2023

@TheAudioDabbler agreed. If AKAI added Deluge-like footswitch control to starting/stopping loop recording and undo, and it worked with regular clips and not special "looper" clips, that would be HUGE.

@TheAudioDabbler
Copy link

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.

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