What is Freeside?

Freeside is a Georgia nonprofit corporation, organized to develop a community of coders, makers, artists, and researchers in order to promote collaboration and community outreach. Learn more »

A Capacitive-Touch Janko Keyboard: What I Did at the 2017 Georgia Tech Moog Hackathon

Last weekend (February 10-12, 2017) I made a Janko-layout capacitive-touch keyboard for the Moog Werkstatt at the Georgia Tech Moog Hackathon. The day after (Monday the 13th), I made this short video of the keyboard being played:

"Capacitive Touch Janko Keyboard for Moog Werkstatt"



(Text from the video doobly doo)

This is a Janko-layout touch keyboard I made at the 2017 Moog Hackathon at Georgia Tech, February 10-12. I'm playing a few classic bass and melody lines from popular and classic tunes. I only have one octave (13 notes) connected so far.

The capacitive touch sensors use MPR121 capacitive-touch chips, on breakout boards from Adafruit (Moog Hackathon sponsor Sparkfun makes a similar board for the same chip). The example code from Adafruit was modified to read four boards (using the Adafruit library and making four sensor objects and initializing each to one of the four I2C addresses is remarkably easy for anyone with moderate familiarity with C++), and code was written to send a gate (key down) signal to the Werkstatt, and to write a binary representation of the pressed key (low note priority) to an Arduino port connected to a precision R-2R ladder to generate the voltage for the VCO exponential input.

The capacitive touch sensors can be used to make a touch keyboard with any configuration, not just the Janko. With these sensors it's remarkably easy to make a functioning electronic musical keyboard, as no mechanical switches or moving parts are needed. The feeling is at least as responsive as a "real" keyboard, as response to touch and release feels instant as far as I can tell. If anything, there's a "problem" in that if you accidentally, even slightly, touch a key it will sound, whereas with a mechanical keyboard you have to "accidentally" press a key down for it to sound.

A traditional seven-natural-and-five-sharp-keys layout would have been just as easy, but less "interesting." I chose the Janko layout after having read about it for many years (see Paul Vandervoot's piano video "Demonstration of 4-Row Janko Keyboard" - he describes the layout at 4:06). The Janko has, from left to right, six whole steps per octave, thus is one less key wide per octave than the traditional keyboard, so with the same key spacings the Janko octave is a shorter distance. Going up or down diagonally is a half step, so a chromatic scale of all 12 notes is a zig-zag pattern. A major scale is the first three notes in a line (whole steps), diagonally up or down to the next key (a half step), this and the next three keys across (whole steps), and then diagonally again (a half step) to get to the octave key. You can start on any key and the major scale is the same description. This is the remarkable property of the Janko layout, there are very few patterns to memorize for the different scales and chords.

(End text from the doobly doo)


I used an Arduino Mega 2560 (actually the Inland brand compatible board from Micro Center), because I thought I would use more I/O pins than on an Uno. This project can be done on an Uno, but the direct write to the Mega DDRC and PORTC registers (and perhaps other I/O pin assignments) may need to be changed for the Uno. If you don't know how to use the AVR port registers directly, you may be better off just using a Mega 2560 rather than trying to change the code for an Uno.

No direct work for this project was done at Freeside Atlanta (nor at Georgia Tech's Invention Studio - I cut these pieces of wood to size at home using a circular saw just before going to the hackathon, then hot-glued everything together at the hackathon), but I did some preliminary work done at Freeside. I had been wanting to make some sort of Janko keyboard for a while, and in recent months I've 3d-printed a couple of rounded-rectangle "keys" to help get the feel of what I wanted. (The short time of a one-weekend build kept me from using anything other than a rectangle shape on this project, and even then I only had one octave done by 5PM Sunday.) I decided on key spacing the same as "standard" piano keys, which are about about 165mm (6.5 inches) per octave. Since the Janko layout has six (whole-step) keys per octave instead of the traditional seven (major scale) keys, this octave is about 141.4mm or 5.57 inches wide. The distance from one row of keys to the next above it is 1.8 inches, and each row up is 0.53 inches (the approximate heigth of a sharp note on a standard keyboard) higher than the previous. These numbers are mostly just "good guesses" as to what the dimensions of such a keyboard should be for good ergonomics. If you make one of these, feel free to make whatever changes you like, even a traditional key layout or something totally different.

The keys are made of brass strips. I had a brass sheet, dimensioned 6 inches by 24 inches by 0.004 inches. I cut this into rectangles of 1.5 inches by 0.75 inches. I soldered wires to one side and glued the soldered side down to a plywood board with hot glue. Each vertical pair arranged (first-and-third row, or second-and-fourth row) were connected together and connected to a sensor input on the MPR121 breakout board.

For greater versatility, each key could be connected to a separate sensor input (doubling the number of sensor inputs required). This would allow the vertical pairs to be "wired together" in software for the Janko layout, or for each key to generate a different note. This would be ideal for generating microtonal scales such as 24 notes per octave.

The current code implements a monophonic keyboard for a single voice analog synthesizer. The keyboard priority is for the lowest note played, and retriggering is off (you have to lift off all keys and press a key again to get a new gate signal). Many enhancements can be done, such as highest or last note priority, retriggering, and sending polyphonic MIDI data, and adding adding modulation wheels on the left side for pitch bend, LFO modulation amount, and other possible performance parameters (I think there should be at least three such wheels, with the third one changing the filter cutoff frequency). These are, as always, left as an exercise for the student.

Blatant Blurb for Synthesizer Class

This Tuesday, February 21 2017, I'll be putting on a class at Freeside:
"Introduion to Electronic Musical Instruments."
I'll cover analog music synthesizers, and have this Janko keyboard instrument and others in the Synth Petting Zoo after the class. There is a $10 charge, this covers the time and cost of setting up and of using Freeside to put on this class. Sign up here:
https://www.meetup.com/Freeside-Atlanta/events/236883195/

Schematic (power supply connections for Werkstatt and Arduino not shown):

Arduino code:

// tkey - read capacitive touch keys and control Werkstatt
// Ben Bradley Feb. 11-12, 2017
// for Moog Hackathon

// substantial code taken from the MPR121test program from the
// Adafruit library.


// From other keyscan program for the Mega2560:

// AVRpin AVR name   Arduino name
//   1    PG5         D4
//   2    PE0         D0
//   3    PE1         D1
//   4    PE2
//   5    PE3         D5
//   6    PE4         D2
//   7    PE5         D3
//   8    PE6
//   9    PE7
//  12-18 PH0-PH6     D17-D16,X,D6-D9
//  19-26 PB0-PB7     D52-D50,D10-D13
//  27    PH7
//  28-29 PG3-PG4
//  35-42 PL0-PL7     D49-D42                   // out to r-2r ladder
//  43-50 PD0-PD7     D21-D18,X,X,X,D38
//  51-52 PG0-PG1     D40-D41
//  53-60 PC0-PC7     D37-D30             ***  Voltage control output, port C
//  63-69 PJ0-PJ6     D15-D14,X,X,X,X,X
//  70    PG2         D39
//  71-78 PA7-PA0     D29-D22             ***
//  79    PJ7
//  82-89 PK7-PK0     A15-A8
//  90-97 PF7-PF0     A7-A0
//  98    AREF



/*********************************************************
This is a library for the MPR121 12-channel Capacitive touch sensor

Designed specifically to work with the MPR121 Breakout in the Adafruit shop
  ----> https://www.adafruit.com/products/

These sensors use I2C communicate, at least 2 pins are required
to interface

Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing
products from Adafruit!

Written by Limor Fried/Ladyada for Adafruit Industries. 
BSD license, all text above must be included in any redistribution
**********************************************************/

#include <Wire.h>
#include "Adafruit_MPR121.h"

// You can have up to 4 on one i2c bus but one is enough for testing!
Adafruit_MPR121 chip1 = Adafruit_MPR121();
Adafruit_MPR121 chip2 = Adafruit_MPR121();
Adafruit_MPR121 chip3 = Adafruit_MPR121();
Adafruit_MPR121 chip4 = Adafruit_MPR121();

// Keeps track of the last pins touched
// so we know when buttons are 'released'
uint16_t lasttouched1 = 0;
uint16_t currtouched1 = 0;
uint16_t lasttouched2 = 0;
uint16_t currtouched2 = 0;
uint16_t lasttouched3 = 0;
uint16_t currtouched3 = 0;
uint16_t lasttouched4 = 0;
uint16_t currtouched4 = 0;



const int GateOut = 48;   // Mega digital output

void setup()
{
  Serial.begin(9600);

  while (!Serial) { // needed to keep leonardo/micro from starting too fast!
    delay(10);
  }
 
//  Serial.println("Adafruit MPR121 Capacitive Touch sensor test");

//   The MPR121 ADDR pin is pulled to ground and has a default I2C address of 0x5A
// You can adjust the I2C address by connecting ADDR to other pins:
// ADDR not connected: 0x5A
// ADDR tied to 3V: 0x5B
// ADDR tied to SDA: 0x5C
// ADDR tied to SCL: 0x5D

  // Default address is 0x5A, if tied to 3.3V its 0x5B
  // If tied to SDA its 0x5C and if SCL then 0x5D
  if (!chip1.begin(0x5A))
  {
    Serial.println("MPR121 chip1 not found, check wiring?");
    while (1);
  }
//  Serial.println("MPR121 chip1 found!");


  if (!chip2.begin(0x5B))
  {
    Serial.println("MPR121 chip2 not found, check wiring?");
    while (1);
  }
//  Serial.println("MPR121 chip2 found!");

  if (!chip3.begin(0x5C))
  {
    Serial.println("MPR121 chip3 not found, check wiring?");
    while (1);
  }
//  Serial.println("MPR121 chip3 found!");

  if (!chip4.begin(0x5D))
  {
    Serial.println("MPR121 chip4 not found, check wiring?");
    while (1);
  }
//  Serial.println("MPR121 chip4 found!");

  Serial.println("All chips found.");

  DDRC = 0xff;
  PORTC = 0;
  pinMode (GateOut, OUTPUT);
  digitalWrite(GateOut, 0);
} // void setup()

void loop()
{

  int notepressed = -1;
  // Get the currently touched pads
  currtouched1 = chip1.touched();
 
#ifdef __print_touched_
  for (uint8_t i=0; i<12; i++) {
   // it if *is* touched and *wasnt* touched before, alert!

    if ((currtouched1 & _BV(i)) && !(lasttouched1 & _BV(i)) )
    {
      Serial.print("c1 "); Serial.print(i); Serial.println(" touched");
    }
    // if it *was* touched and now *isnt*, alert!
    if (!(currtouched1 & _BV(i)) && (lasttouched1 & _BV(i)) )
    {
      Serial.print("c1 "); Serial.print(i); Serial.println(" released");
    }
  }
#endif #ifdef __print_touched_


  currtouched2 = chip2.touched();
#ifdef __print_touched_
  for (uint8_t i=0; i<12; i++)
  {
    // it if *is* touched and *wasnt* touched before, alert!
    if ((currtouched2 & _BV(i)) && !(lasttouched2 & _BV(i)) ) {
      Serial.print("c2 "); Serial.print(i); Serial.println(" touched");
    }
    // if it *was* touched and now *isnt*, alert!
    if (!(currtouched2 & _BV(i)) && (lasttouched2 & _BV(i)) )
    {
      Serial.print("c2 "); Serial.print(i); Serial.println(" released");
    }
  }
#endif #ifdef __print_touched_

  currtouched3 = chip3.touched();
 
#ifdef __print_touched_
  for (uint8_t i=0; i<12; i++)
  {
    // it if *is* touched and *wasnt* touched before, alert!
    if ((currtouched3 & _BV(i)) && !(lasttouched3 & _BV(i)) ) {
      Serial.print("c3 "); Serial.print(i); Serial.println(" touched");
    }
    // if it *was* touched and now *isnt*, alert!
    if (!(currtouched3 & _BV(i)) && (lasttouched3 & _BV(i)) )
    {
      Serial.print("c3 "); Serial.print(i); Serial.println(" released");
    }
  }
#endif #ifdef __print_touched_


  currtouched4 = chip4.touched();
 
  for (uint8_t i=0; i<12; i++)
  {
    // it if *is* touched and *wasnt* touched before, alert!
    if ((currtouched4 & _BV(i)) && !(lasttouched4 & _BV(i)) ) {
      Serial.print("c4 "); Serial.print(i); Serial.println(" touched");
    }
    // if it *was* touched and now *isnt*, alert!
    if (!(currtouched4 & _BV(i)) && (lasttouched4 & _BV(i)) )
    {
      Serial.print("c4 "); Serial.print(i); Serial.println(" released");
    }
  }


  if ((lasttouched1 != currtouched1) ||
      (lasttouched2 != currtouched2) ||
      (lasttouched3 != currtouched3) ||
      (lasttouched4 != currtouched4))
  {
    // find lowest note.

 
    if (currtouched1)
    {
      for (int8_t i=11; i>=0; i--)
      {
        if (currtouched1 & _BV(i))
          notepressed = i;
      }
    }
    else
    if (currtouched2)
    {
      for (int8_t i=11; i>=0; i--)
      {
        if (currtouched2 & _BV(i))
          notepressed = 12 + i;
      }
    }
    else
    if (currtouched3)
    {
      for (int8_t i=11; i>=0; i--)
      {
        if (currtouched3 & _BV(i))
          notepressed = 24 + i;
      }
    }
    else
    if (currtouched4 & 0x01)
      notepressed = 36;     // highest key
//    Serial.print("lowest note ");
    if (notepressed != -1)
    {
      PORTC = 37 - notepressed; // invert bits for negative sum
      Serial.print(notepressed);
      Serial.print (' ');
    }
    if (currtouched1 | currtouched2 | currtouched3 | currtouched4)
      digitalWrite(GateOut, 1);
   else
      digitalWrite(GateOut, 0);
  } // if ((lasttouched1 != // note changed

  // reset our state
  lasttouched1 = currtouched1;
  lasttouched2 = currtouched2;
  lasttouched3 = currtouched3;
  lasttouched4 = currtouched4;

} // void loop()