Project in progress
MIDI to QWERTY with Arduino

MIDI to QWERTY with Arduino © Apache-2.0

Control your PC musical software from a MIDI keyboard.

  • 253 views
  • 1 comment
  • 2 respects

Components and supplies

Apps and online services

About this project

My friend and colleague ValenS is a big fan of chiptune music, and a few months ago discovered what he claims to be the ultimate chiptune tracker, especially because it allows FM synthesis: klystrack.

Problem is, he wanted to input his music onto the PC using a conventional piano keyboard, while apparently the software does not support MIDI input.

Originally ValenS contacted me to help him to scavenge a conventional PC keyboard, and make it into a musical keyboard. However, that would have involved some mechanical hacking that be both seemed to fear. Moreover, we did not even had an USB keyboard to scavenge at hand, which made the task of buying a keyboard in purpose for that, a little bit silly (at least to me).

After some brainstorming, that passed through also reinventing the C64 keyboard overlay, and buying a fatar keybed, I proposed instead to go the other way round: taking an existing musical keyboard, and translate its output into a conventional QWERTY. This could have been done with a microcontroller with a MIDI input, and an USB output: sounds more like MY type of tinkering.

After some little research, I found that the Arduino Micro had the needed USB output, and some compatible Arduino PRO Micro were available for cheap.

While arduino MIDI shields are available, I find them to be a bit expensive and bulky, therefore I decided to go for making my own board.

MIDI protocol has turned 36 (being born in 1983 like those hands that are writing), and it is essentially an UART port, with a non-PC standard speed of 31.25 kbit/s, and an opto-coupled circuit to avoid ground-loops.

The circuit to implement is found easily everywhere, and I report it here.

I could buy all the parts for very reasonable price on a Chinese market site.

NB: probably other optocouplers (cheaper) than 6N138 could be used, however care must be taken in verifying the commutation speed. Long story short: I stuck with the "safe" choice.

I have started prototyping the circuit with a breadboard, as you see below.

I have enjoyed using again my rockband 3 keyboard controller... and it worked!

So now a few technical details: I have created an enumerator to contain the received MIDI pitch, and mapped in a global variable the corresponding letter to be generated.

enum midi_pitch {
MIDI_B2 = 47,
MIDI_C3 = 48,
MIDI_D3b,
MIDI_D3,
MIDI_E3b,
MIDI_E3,
MIDI_F3,
MIDI_G3b,
MIDI_G3,
MIDI_A3b,
MIDI_A3,
MIDI_B3b,
MIDI_B3,
MIDI_C4,
MIDI_D4b,
MIDI_D4,
MIDI_E4b,
MIDI_E4,
MIDI_F4,
MIDI_G4b,
MIDI_G4,
MIDI_A4b,
MIDI_A4,
MIDI_B4b,
MIDI_B4,
MIDI_C5,
MIDI_D5b
};

char midi_map[128];
void midi_map_init()
{
midi_map[MIDI_C3] = 'z';
midi_map[MIDI_D3] = 'x';
midi_map[MIDI_E3] = 'c';
midi_map[MIDI_F3] = 'v';
midi_map[MIDI_G3] = 'b';
midi_map[MIDI_A3] = 'n';
midi_map[MIDI_B3] = 'm';
// ROW1
midi_map[MIDI_D3b] = 's';
midi_map[MIDI_E3b] = 'd';
midi_map[MIDI_G3b] = 'g';
midi_map[MIDI_A3b] = 'h';
midi_map[MIDI_B3b] = 'j';
// ROW2
midi_map[MIDI_C4] = 'q';
midi_map[MIDI_D4] = 'w';
midi_map[MIDI_E4] = 'e';
midi_map[MIDI_F4] = 'r';
midi_map[MIDI_G4] = 't';
midi_map[MIDI_A4] = 'y';
midi_map[MIDI_B4] = 'u';
midi_map[MIDI_C5] = 'i';
// ROW3
midi_map[MIDI_D4b] = '2';
midi_map[MIDI_E4b] = '3';
midi_map[MIDI_G4b] = '5';
midi_map[MIDI_A4b] = '6';
midi_map[MIDI_B4b] = '7';
// SPACE BAR
midi_map[MIDI_B2] = ' ';
midi_map[MIDI_D5b] = ' ';
}

The loop only deals with the Note On and Note Off messages.

case midi::NoteOff:
// Keyboard.print("Note Off\n");
pitch = MIDIUART.getData1();
midi_note_on[ pitch ] = false;
break;
case midi::NoteOn:
// Keyboard.print("Note On\n");
pitch = MIDIUART.getData1();
Keyboard.print( midi_map[ pitch ] );
midi_note_on[ pitch ] = true;
break;

So at this point a keypress was generated when pushing a key, but unfortunately that is NOT how a PC keyboard works: actually if you keep a key presse, several key-strikes are generated at some rate, and apparently this has an impact in usability of the tracker program.

In order to imitate this behavior, I have created a map of the notes pressed, and called a function that regenerates the key-press at each iteration of the loop.

bool midi_note_on[128]  = {false};
void midi_retrigger(void *)
{
uint8_t pitch;
for(pitch = MIDI_B2; pitch <= MIDI_D5b; pitch++)
{
if(midi_note_on[pitch])
Keyboard.print( midi_map[ pitch ] );
}
}

void loop()
{
uint8_t pitch;
delay(100); // wait for 100ms
midi_retrigger();

...

}

However, in order to reduce the rate of the generation of the key-strokes, I had to include a delay function, that in practice also generates a latency in the responsivity of the software.

I think the right way to solve this is to use a non-blocking timer instead, that only takes care of re-triggering the key presses, while the main loop reads the MIDI input. Fortunately arduino supports the timer library that just does that. However when I have included the timer library, I started having a weird warning about memory space, and the arduino got bricked at next reboot, so I will stick with the first implementation, and live with the latency!

Below a photo of the final assembly.

Last but not least, I have worked on a MIDI IN or OUT expansion board that could also support the wemos D1 mini.


Code

MIDI to qwerty arduinoArduino
/* 
   Arduino Pro Micro MIDI to QWERTY conversion.
   by: Marco Merlin 2019
   
   Original code:
   Pro Micro Test Code
   by: Nathan Seidle
   modified by: Jim Lindblom
   SparkFun Electronics
   date: September 16, 2013
   license: Public Domain - please use this code however you'd like.
   It's provided as a learning tool.

   This code is provided to show how to control the SparkFun
   ProMicro's TX and RX LEDs within a sketch. It also serves
   to explain the difference between Serial.print() and
   Serial1.print().
*/

#include <MIDI.h>
#include "Keyboard.h"
// #include <timer.h>

#define RXLED_ON  digitalWrite(RXLED, LOW)   // set the LED on
#define RXLED_OFF digitalWrite(RXLED, HIGH)  // set the LED off

// 1 turns on debug, 0 off
#define DBGSERIAL if (0) SERIAL_PORT_MONITOR
#ifdef USBCON
#define MIDI_SERIAL_PORT Serial1
#else
#define MIDI_SERIAL_PORT Serial
#endif

//button pressbutton unpressbutton pressbutton unpress
int RXLED = 17;  // The RX LED has a defined Arduino pin
// The TX LED was not so lucky, we'll need to use pre-defined
// macros (TXLED1, TXLED0) to control that.
// (We could use the same macros for the RX LED too -- RXLED1,
//  and RXLED0.)

const int buttonPin = 4;          // input pin for pushbutton
int previousButtonState = HIGH;   // for checking the state of a pushButton
// auto timer = timer_create_default(); // create a timer with default settings

struct MySettings : public midi::DefaultSettings
{
  static const bool Use1ByteParsing = false;
  static const unsigned SysExMaxSize = 1026; // Accept SysEx messages up to 1024 bytes long.
  static const long BaudRate = 31250;
};

MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, MIDI_SERIAL_PORT, MIDIUART, MySettings);

inline uint8_t writeUARTwait(uint8_t *p, uint16_t size)
{
  // Apparently, not needed. write blocks, if needed
  //  while (MIDI_SERIAL_PORT.availableForWrite() < size) {
  //    delay(1);
  //  }
  return MIDI_SERIAL_PORT.write(p, size);
}

uint16_t sysexSize = 0;

void sysex_end(uint8_t i)
{
  sysexSize += i;
  DBGSERIAL.print(F("sysexSize="));
  DBGSERIAL.println(sysexSize);
  sysexSize = 0;
}

const uint8_t MIDI_passthru_pin=2;
bool MIDI_passthru;

enum midi_pitch {
  MIDI_B2 = 47,
  MIDI_C3 = 48,
  MIDI_D3b,
  MIDI_D3,
  MIDI_E3b,
  MIDI_E3,
  MIDI_F3,
  MIDI_G3b,
  MIDI_G3,
  MIDI_A3b,
  MIDI_A3,
  MIDI_B3b,
  MIDI_B3,
  MIDI_C4,
  MIDI_D4b,
  MIDI_D4,
  MIDI_E4b,
  MIDI_E4,
  MIDI_F4,
  MIDI_G4b,
  MIDI_G4,
  MIDI_A4b,
  MIDI_A4,
  MIDI_B4b,
  MIDI_B4,
  MIDI_C5,
  MIDI_D5b
  };

/*
char midi_map[] = {
  // ROW0
  [MIDI_C3]  = 'z',
  [MIDI_D3]  = 'x',
  [MIDI_E3]  = 'c',
  [MIDI_F3]  = 'v',
  [MIDI_G3]  = 'b',
  [MIDI_A3]  = 'n',
  [MIDI_B3]  = 'm',
  // ROW1
  [MIDI_D3b]  = 's',
  [MIDI_E3b]  = 'd',
  [MIDI_G3b]  = 'g',
  [MIDI_A3b]  = 'h',
  [MIDI_B3b]  = 'j',
  // ROW2
  [MIDI_C4]  = 'q',
  [MIDI_D4]  = 'w',
  [MIDI_E4]  = 'e',
  [MIDI_F4]  = 'r',
  [MIDI_G4]  = 't',
  [MIDI_A4]  = 'y',
  [MIDI_B4]  = 'u',
  [MIDI_C5]  = 'i',
  // ROW3
  [MIDI_D4b]  = '2',
  [MIDI_E4b]  = '3',
  [MIDI_G4b]  = '5',
  [MIDI_A4b]  = '6',
  [MIDI_B4b]  = '7',
  // space bar
  [MIDI_B2]  = ' ',
  [MIDI_D5b] = ' '  
  };
*/

char midi_map[128];
void midi_map_init()
{
  midi_map[MIDI_C3]  = 'z';
  midi_map[MIDI_D3]  = 'x';
  midi_map[MIDI_E3]  = 'c';
  midi_map[MIDI_F3]  = 'v';
  midi_map[MIDI_G3]  = 'b';
  midi_map[MIDI_A3]  = 'n';
  midi_map[MIDI_B3]  = 'm';
  // ROW1
  midi_map[MIDI_D3b]  = 's';
  midi_map[MIDI_E3b]  = 'd';
  midi_map[MIDI_G3b]  = 'g';
  midi_map[MIDI_A3b]  = 'h';
  midi_map[MIDI_B3b]  = 'j';
  // ROW2
  midi_map[MIDI_C4]  = 'q';
  midi_map[MIDI_D4]  = 'w';
  midi_map[MIDI_E4]  = 'e';
  midi_map[MIDI_F4]  = 'r';
  midi_map[MIDI_G4]  = 't';
  midi_map[MIDI_A4]  = 'y';
  midi_map[MIDI_B4]  = 'u';
  midi_map[MIDI_C5]  = 'i';
  // ROW3
  midi_map[MIDI_D4b]  = '2';
  midi_map[MIDI_E4b]  = '3';
  midi_map[MIDI_G4b]  = '5';
  midi_map[MIDI_A4b]  = '6';
  midi_map[MIDI_B4b]  = '7';
  // SPACE BAR
  midi_map[MIDI_B2]   = ' ';
  midi_map[MIDI_D5b]  = ' ';
}

void setup()
{
 pinMode(RXLED, OUTPUT);  // Set RX LED as an output
 // TX LED is set as an output behind the scenes

 //Serial.begin(9600); //This pipes to the serial monitor

 // make the pushButton pin an input:
 pinMode(buttonPin, INPUT);

 //if(keyboard_en)
  // initialize control over the keyboard:
 Keyboard.begin();

 // Pin 0 LOW selects MIDI pass through on
  pinMode(MIDI_passthru_pin, INPUT_PULLUP);
  MIDI_passthru = (digitalRead(MIDI_passthru_pin) == LOW);

  MIDIUART.begin(MIDI_CHANNEL_OMNI);
  if (MIDI_passthru) {
    DBGSERIAL.println("MIDI thru on");
  }
  else {
    DBGSERIAL.println("MIDI thru off");
    MIDIUART.turnThruOff();


  // retrigger every 100ms
  //timer.every(100, midi_retrigger);

}

midi_map_init();
 
}

void button_update() {
  // read the pushbutton:
  int buttonState = digitalRead(buttonPin);
  // if the button state has changed,
  if (buttonState != previousButtonState)
  {
    if (buttonState == HIGH)  // rising edge
      {
      RXLED_ON;
      Keyboard.print("button press\n");
      }
    else
      {
      RXLED_OFF;
      Keyboard.print("button unpress\n");
      }
  }
  // save the current button state for comparison next time:
  previousButtonState = buttonState;
}

bool midi_note_on[128]  = {false};

void midi_retrigger(void *)
{
  uint8_t pitch;
  for(pitch = MIDI_B2; pitch <= MIDI_D5b; pitch++)
    {
      if(midi_note_on[pitch])
        Keyboard.print( midi_map[ pitch ] );

    }
}

void loop()
{
  // button_update();
  uint8_t pitch;

  delay(100);              // wait for 100ms 
  midi_retrigger();
  delay(100);              // wait for 100ms 

  //timer.tick();

/* MIDI UART -> MIDI USB */
  if (MIDIUART.read()) 
  {
    midi::MidiType msgType = MIDIUART.getType();
    DBGSERIAL.print(F("UART "));
    DBGSERIAL.print(msgType, HEX);
    DBGSERIAL.print(' ');
    DBGSERIAL.print(MIDIUART.getData1(), HEX);
    DBGSERIAL.print(' ');
    DBGSERIAL.println(MIDIUART.getData2(), HEX);
    switch (msgType) {
      case midi::InvalidType:
        break;
      case midi::NoteOff:
            // Keyboard.print("Note Off\n");
            pitch = MIDIUART.getData1();
            midi_note_on[ pitch ] = false;
            break;
      case midi::NoteOn:
            // Keyboard.print("Note On\n");
            pitch = MIDIUART.getData1();
            //velocity = MIDIUART.getData2();
            // Keyboard.print( midi_map[ MIDIUART.getData1() ] );
            // Keyboard.println(velocity);
            Keyboard.print( midi_map[ pitch ] );
            midi_note_on[ pitch ] = true;
            break;
      case midi::AfterTouchPoly:
      case midi::ControlChange:
      case midi::ProgramChange:
      case midi::AfterTouchChannel:
      case midi::PitchBend:
        { 
          //  MIDIUART.getData1(),
          //  MIDIUART.getData2()
          break;
        }
      case midi::SystemExclusive:
        DBGSERIAL.print("sysex size ");
        DBGSERIAL.println(MIDIUART.getSysExArrayLength());
        break;
      case midi::TuneRequest:
      case midi::Clock:
      case midi::Start:
      case midi::Continue:
      case midi::Stop:
      case midi::ActiveSensing:
      case midi::SystemReset:
      case midi::TimeCodeQuarterFrame:
      case midi::SongSelect:
      case midi::SongPosition:
      default:
        break;
    }
  }
}

Comments

Similar projects you might like

Midi Keypad

Project tutorial by Team labsud

  • 3,149 views
  • 0 comments
  • 6 respects

USB-BLE Wireless MIDI Adapters

Project tutorial by Joe Bowbeer

  • 4,756 views
  • 0 comments
  • 5 respects

Arduino LED MIDI Controller with FL Studio

Project tutorial by reyadeetopee

  • 3,740 views
  • 0 comments
  • 14 respects

MIDI Slide Whistle "MEMIDION" Next Stage

Project tutorial by HomeMadeGarbage

  • 2,747 views
  • 0 comments
  • 15 respects

XY MIDI Pad With Arduino and TFT

Project tutorial by Silvius

  • 2,762 views
  • 1 comment
  • 14 respects

Turn an Arduino Uno into a MIDI Controller: Guitar Pedals

Project tutorial by Johan van Vugt

  • 5,777 views
  • 1 comment
  • 11 respects
Add projectSign up / Login