andreagregorini
Published © CC BY-NC

ARKeytar - Arduino Based MIDI Controller Keytar

ARKeytar is an expressive Arduino based MIDI controller shaped as a maple wood keytar, with two softpots, a MIDI keyboard and CC controls.

AdvancedShowcase (no instructions)4,483
ARKeytar - Arduino Based MIDI Controller Keytar

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
SparkFun SoftPot Membrane Potentiometer - 500mm
Two softpots for the two ARKeytar "strings"
×2
Rotary potentiometer (generic)
Rotary potentiometer (generic)
×1
Potentiometer, Slide
Potentiometer, Slide
×1
Shift Register- Parallel to Serial
Texas Instruments Shift Register- Parallel to Serial
×2

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Soft pots

Soft pots connections to the analog pins.

MIDI in and MIDI out circuits

The switch on the RX port is needed so that the connection to the keyboard is interrupted when new code needs to be uploaded on the Nano, since the serial port is used in such a situation.

Shift registers

The switches are connected to the 8 central pins of each shift register as indicated in the example at the bottom.

Code

ARKeytar v5.21

Arduino
ARKeytar code version v5.21.
Date: 2022-05-01
/*
ARKeytar v5.21
2022-05-01, Andrea Gregorini
*/


#include <MIDI.h>
MIDI_CREATE_DEFAULT_INSTANCE();

#include <ShiftIn.h>
ShiftIn<2> shift;





/* panicButton */
const int panicButton = 2;
int panicButtonRead = 0;
int pressedPanicButton = 0;


/* ledPins */
const int ledVerde = 3; // Green
const int ledRosso = 4; // Red
int ledOnCount = 0;

/* shift register */
int num;
int incoming;
int midiChannel;
int ccPot[2] = {0, 1};
int ccPotOld[2] = {0,0};
const int listaControlChange[11] = {7, 74, 71, 11, 1, 5, 73, 91, 93, 72, 10};

/* octave up and octave down */
const int up = 6;
const int down = 5;	        

int transp[2] = {0, 0};
int transpSlide[2] = {0, 0};
int pressedUp = 0;		  int pressedDown = 0;
int pressedSlideUp = 0; int pressedSlideDown = 0;
int transpUp;		        int transpDown;
int transpSlideUp;      int transpSlideDown;




/* rotPot and slidePot */
int cicliMidiCC[2] = {6, 6};
int jj[2] = {0, 0};

/* CC messages variables */
int ccValue[2];
int ccRead[2];
int ccValue_Old[2] = {0, 0};


/* Note range and tuning */
const int threshold = 998; // read values discarded above threshold

int lowestNote;             // first soft pot
int keyRange;               // number of keys on a single softpot
      
int pitchRange;
int tuning[2];              // semitone shift between soft pots
int minNote[2];
int maxNote[2];
int octaveShift;
int semitoneShift;


/* pitch bending */
int analogPitchRange;
float intervallo;
float pitchRangeFloat;
int pitchNota[2];
int pitchSent[2] = {-1,-1};
int pitchNotaOld[2];
int pitchPriority = 0;

/* Pitch stabilization */

int pitchTolerance;
float DEFAULT_pitchSnap;
float pitchSnap;
int snapOn[2] = {0,0};
float calcFloat[2];   
float calcInt[2];   
float decPart[2];


/* A3 = softpot1, A4 = softpot2, A1 = slidepot, A0 = rotpot */
const int analogPins[4] = {A3, A4, A1, A0};
float lettura[2];
int letturaInt[2];
float lettura_Sent[2];

float midiNoteFloat[2];
int midiNote[2];
int midiNote_Sent[2];
int midiNote_Old[2];
int sensorPressed[2] = {0, 0};

int count[2] = {0, 0};

int timePressed[2];


/* Pitch values to be applied when notes are snapped */
const int range12[25] = 
{0, 683, 1365, 2048, 2731, 3413, 4096, 4778,
5461, 6144, 6826, 7509, 8192, 8874, 9557,
10239, 10922, 11605, 12287, 12970, 13653,
14335, 15018, 15700, 16383};

const int range24[49] = 
{0, 341, 683, 1024, 1365, 1707, 2048, 2389,
2731, 3072, 3413, 3754, 4096, 4437, 4778,
5120, 5461, 5802, 6144, 6485, 6826, 7168,
7509, 7850, 8192, 8533, 8874, 9215, 9557,
9898, 10239, 10581, 10922, 11263, 11605,
11946, 12287, 12629, 12970, 13311, 13653,
13994, 14335, 14676, 15018, 15359, 15700,
16042, 16383};













void setup() {

  MIDI.begin(MIDI_CHANNEL_OMNI);
  Serial.begin(31250);


  /* Buttons */
  pinMode(panicButton, INPUT);
  pinMode(up, INPUT);
  pinMode(down, INPUT);


  pinMode(ledVerde, OUTPUT);
  /* HIGH = led off. LOW = led on. This is due to the led type used */
  digitalWrite(ledVerde, HIGH);
  pinMode(ledRosso, OUTPUT);
  digitalWrite(ledRosso, HIGH);

  

  /* Analog ports initialization */
  for (int ii = 0; ii <= 1; ii++) {
     pinMode(analogPins[ii], INPUT_PULLUP);
  }
  for (int ii = 2; ii <= 3; ii++) {
     pinMode(analogPins[ii], INPUT);
  }
  
  /* Read shift registers */
  shift.begin(8, 9, 11, 12);
  if(shift.update()) // read in all values. returns true if any button has changed
  incoming = readShiftReg();
  saveShiftRegRead(); // Assign incoming values to variables


  /* Softpot settings */
  lowestNote = 60;
  keyRange = 32;
  
  tuning[0] = 0;
  tuning[1] = 5;
  
  octaveShift = 0;
  octaveShift = octaveShift*12;
  semitoneShift = 0;

  /* Apply transopose if read by shift registers or defined */
  for (int ii = 0; ii <= 1; ii++) {
	  minNote[ii] = lowestNote + tuning[ii] + semitoneShift + octaveShift;
	  maxNote[ii] = minNote[ii] + keyRange;
  }
  

  if (bitRead(incoming,14)== 1) {
	  pitchRange = 24; //(+-semitones)
  } else {
	  pitchRange = 12; //(+-semitones)
  }
	
  pitchTolerance = (pitchRange-12)/12 * (50-100) + 100;
  DEFAULT_pitchSnap = 0.2;
	pitchSnap = DEFAULT_pitchSnap;
  intervallo = 16383 / (2*pitchRange);
  pitchRangeFloat = float(pitchRange);


  analogPitchRange = 1023/(keyRange)*pitchRange;


  /* Nothing pressed at the beginning */
  sensorPressed[0] = 0;
  sensorPressed[1] = 0;
  midiNote_Old[0] = 0;
  midiNote_Old[1] = 0;


}




void loop() {
	
	panic();
	readPots();
	transpose();
	noPressure();
	newPressure();
	firstRelease();
	prolongedRelease();
	assignPitchPriority();
	prolongedPressure();
	potsReadSendCC();

} 





int readShiftReg() {
  for(int i = 0; i < shift.getDataWidth(); i++){
    bitWrite(num, i, shift.state(i));
  }

  int incomingNumber = 0;
  for (int bitNumber = 8; bitNumber <= 15; bitNumber++) {
    bitWrite(incoming, incomingNumber, bitRead(num,bitNumber));
    incomingNumber++;
  }
  for (int bitNumber = 0; bitNumber <= 7; bitNumber++) {
    bitWrite(incoming, incomingNumber, bitRead(num,bitNumber));
    incomingNumber++;
  }
  return incoming;
}

void saveShiftRegRead() {

  bitWrite(midiChannel, 0, bitRead(incoming,0));
  bitWrite(midiChannel, 1, bitRead(incoming,1));
  bitWrite(midiChannel, 2, bitRead(incoming,2));
  bitWrite(midiChannel, 3, bitRead(incoming,3));

  bitWrite(ccPot[0], 0, bitRead(incoming,4));
  bitWrite(ccPot[0], 1, bitRead(incoming,5));
  bitWrite(ccPot[0], 2, bitRead(incoming,6));
  bitWrite(ccPot[0], 3, bitRead(incoming,7));

  bitWrite(ccPot[1], 0, bitRead(incoming,8));
  bitWrite(ccPot[1], 1, bitRead(incoming,9));
  bitWrite(ccPot[1], 2, bitRead(incoming,10));
  bitWrite(ccPot[1], 3, bitRead(incoming,11));

  /*
  bitRead(incoming,12) Not assigned, unused
  bitRead(incoming,13) Not assigned, unused
  bitRead(incoming,14) used to select pitch range
  bitRead(incoming,15) used to select if transpose acts on both softpots or just second one.
  */

}

void noteOn(int pitch, int velocity) {
  Serial.write(144 + midiChannel);        // Note on command
  Serial.write(pitch);                    // Note pitch
  Serial.write(velocity);                 // Note velocity 0-127
}

void pitchBend(byte lsb, byte msb) {
  Serial.write(224 + midiChannel);        // Pitch bend command
  Serial.write(lsb);                      // Pitch lsb
  Serial.write(msb);                      // Pitch msb
}

void pitchBendMessage(int pitchNota) {
  int shiftedValue = pitchNota << 1;
  byte lsb = lowByte(shiftedValue) >> 1;  // Pitch lsb
  byte msb = highByte(shiftedValue);      // Pitch msb
  pitchBend(lsb, msb);
}

void midiCC(int CCnumber, int ccValue) {
  Serial.write(176 + midiChannel);        // Control change command
  Serial.write(CCnumber);                 // CC number
  Serial.write(ccValue);                  // Volume 0-127
}

void transposeButtonsAction() {
  if (transpUp == HIGH && pressedUp == 0) {
    pressedUp = 1;

    if (bitRead(incoming, 15) == 0) {
      transp[0] = transp[0] + 12;
      transp[1] = transp[1] + 12;
    } else if (bitRead(incoming, 15) == 1) {
      transp[1] = transp[1] + 12;
    }
  }

  if (transpDown == HIGH && pressedDown == 0) {
    pressedDown = 1;
    if (bitRead(incoming, 15) == 0) {
      transp[0] = transp[0] - 12;
      transp[1] = transp[1] - 12;
    } else if (bitRead(incoming, 15) == 1) {
      transp[1] = transp[1] - 12;
    }
  }

  if (transpUp == LOW && pressedUp == 1) {
    pressedUp = 0;
  }

  if (transpDown == LOW && pressedDown == 1) {
    pressedDown = 0;
  }
}

void transposeSlideAction() {

  if (transpSlideUp == HIGH && pressedSlideUp == 0) {
    pressedSlideUp = 1;

    if (bitRead(incoming, 15) == 0) {
      transpSlide[0] = transpSlide[0] + 12;
      transpSlide[1] = transpSlide[1] + 12;
    } else if (bitRead(incoming, 15) == 1) {
      transpSlide[1] = transpSlide[1] + 12;
    }
  }

  if (transpSlideDown == HIGH && pressedSlideDown == 0) {
    pressedSlideDown = 1;
    if (bitRead(incoming, 15) == 0) {
      transpSlide[0] = transpSlide[0] - 12;
      transpSlide[1] = transpSlide[1] - 12;
    } else if (bitRead(incoming, 15) == 1) {
      transpSlide[1] = transpSlide[1] - 12;
    }
  }

  if (transpSlideUp == LOW && pressedSlideUp == 1) {
    if (bitRead(incoming, 15) == 0) {
      transpSlide[0] = transpSlide[0] - 12;
      transpSlide[1] = transpSlide[1] - 12;
    } else if (bitRead(incoming, 15) == 1) {
      transpSlide[1] = transpSlide[1] - 12;
    }
    pressedSlideUp = 0;
  }

  if (transpSlideDown == LOW && pressedSlideDown == 1) {
    if (bitRead(incoming, 15) == 0) {
      transpSlide[0] = transpSlide[0] + 12;
      transpSlide[1] = transpSlide[1] + 12;
    } else if (bitRead(incoming, 15) == 1) {
      transpSlide[1] = transpSlide[1] + 12;
    }
    pressedSlideDown = 0;
  }
}

void ledCtrl(int ledNum) {
  if (ledNum == 0) {
    digitalWrite(ledRosso, LOW);
    ledOnCount = 0;
  }
  if (ledNum == 1) {
    digitalWrite(ledVerde, LOW);
    ledOnCount = 0;
  }
  if (ledNum > 1 && ledOnCount < 3) {
    ledOnCount = ledOnCount + 1;
  }
  if (ledNum > 1 && ledOnCount >= 3) {
    digitalWrite(ledRosso, HIGH);
    digitalWrite(ledVerde, HIGH);
    ledOnCount = 0;
  }
}



void panic() {

  panicButtonRead = digitalRead(panicButton);
  if (panicButtonRead == HIGH && pressedPanicButton == 0) {

	  for (int panicChannel = 1; panicChannel <= 16; panicChannel++) {
		MIDI.sendControlChange(123,0,panicChannel);
	  }
	  pressedPanicButton = 1;
  } else {
	  pressedPanicButton = 0;
  }
}

void readPots() {
	for (int ii = 0; ii <= 1; ii++) {
    lettura[ii] = analogRead(analogPins[ii]);
    lettura[ii] = analogRead(analogPins[ii]);
	
      unsigned long timeStart = micros();
      while(micros() - timeStart < 5000){
        MIDI.read();
      }
    
    letturaInt[ii] = int(lettura[ii]);

    midiNoteFloat[ii] = (lettura[ii]-0)*(maxNote[ii]-minNote[ii])/(1023-0) + minNote[ii];
    midiNote[ii] = int(midiNoteFloat[ii]) + transp[ii] + transpSlide[ii];
  }

  MIDI.read();
}

void transpose() {
  transpUp = digitalRead(up);
  transpDown = digitalRead(down);

  shift.begin(8, 9, 11, 12);
  if(shift.update())
  incoming = readShiftReg();

  transposeButtonsAction();
  MIDI.read();
}

void noPressure() {
	for (int ii = 0; ii <= 1; ii++) {

    if (letturaInt[ii] >= threshold ) {
      ledCtrl(23);
      sensorPressed[ii] = 0;
      timePressed[ii] = 0;
    }
	MIDI.read();
  }
}

void newPressure() {
	for (int ii = 0; ii <= 1; ii++) {
    if(letturaInt[ii]<=threshold && sensorPressed[ii]==0 && midiNote[ii]!=midiNote_Old[ii]){

      ledCtrl(1);

      pitchNota[ii] = 8192;
      pitchBendMessage(pitchNota[ii]);

      noteOn(midiNote[ii], 0x7F);

      sensorPressed[ii] = 1;
  	  count[ii] = 1;

      timePressed[ii] = 1;
      
      midiNote_Old[ii] = midiNote[ii];
      midiNote_Sent[ii] = midiNote[ii];
      lettura_Sent[ii] = lettura[ii];

      int kk;
      if (ii == 0) {kk = 1;} else if (ii == 1) {kk = 0;}
      
      if (timePressed[kk] >= 1 && timePressed[ii] < timePressed[kk]) {
        pitchPriority = ii + 1;
      }else if (timePressed[kk] == 0) {
        pitchPriority = ii + 1;
      }
      
      MIDI.read();
    }
  }
}

void firstRelease() {
	for (int ii = 0; ii <= 1; ii++) {
    if ((letturaInt[ii] >= threshold) && count[ii] == 1) {

      ledCtrl(0);
      noteOn(midiNote_Old[ii], 0x00);

      sensorPressed[ii] = 0;
      midiNote_Old[ii] = midiNote[ii];
      count[ii] = count[ii] + 1;
  
      MIDI.read();
    }
  }
}

void prolongedRelease() {
	for (int ii = 0; ii <= 1; ii++) {
    if (letturaInt[ii] >= threshold && count[ii] >=2) {
      ledCtrl(23);
      
      sensorPressed[ii] = 0;
      count[ii] = 0;
      timePressed[ii] = 0;
      
      MIDI.read();   
    }
  }
}

void assignPitchPriority() {

  if (timePressed[1] == 0 && sensorPressed[0] == 1 && timePressed[0] >= 0) {
    pitchPriority = 1;
  } else if (timePressed[0] == 0 && sensorPressed[1] == 1 && timePressed[1] >= 0) {
    pitchPriority = 2;
  }
}

void prolongedPressure() {
	for (int ii = 0; ii <= 1; ii++) {
    if (letturaInt[ii] <= threshold && sensorPressed[ii] == 1 && pitchPriority == ii+1) {

      ledCtrl(23);

      if (lettura[ii] <= lettura_Sent[ii] - analogPitchRange) {
        lettura[ii] = lettura_Sent[ii] - analogPitchRange + 1;
      }
      if (lettura[ii] >= lettura_Sent[ii] + analogPitchRange) {
        lettura[ii] = lettura_Sent[ii] + analogPitchRange + 1;
      }

      pitchNota[ii] = map(lettura[ii], lettura_Sent[ii] - analogPitchRange,
                                  lettura_Sent[ii] + analogPitchRange, 0,16383);
      
			/* Pitch stabilization */
      pitchStabilization(ii);

      /* Send stabilized pitch */
      if (pitchSent[ii] != pitchNota[ii]) {
        pitchBendMessage(pitchNota[ii]);
        pitchSent[ii] = pitchNota[ii];
      }
      
      timePressed[ii] = timePressed[ii] + 1;
      
      MIDI.read();
    }
  }
}

void pitchStabilization(int iii) {

      if ((pitchNota[iii] >= 8192 - pitchTolerance) && 
                                   (pitchNota[iii] <= 8192 + pitchTolerance)) {
       pitchNota[iii] = 8192;
      }

      calcFloat[iii] = pitchNota[iii]/intervallo;
      calcInt[iii] = (int)calcFloat[iii];
      decPart[iii] = calcFloat[iii] - calcInt[iii];
		
	  /* Snap pitch towards up (1), and down (2) */
	  /* (1) */ 
      if (decPart[iii] <= pitchSnap){
        snapOn[iii] = 0;
        if (bitRead(incoming,14) == 0) {
          pitchNota[iii] = range12[(int)calcInt[iii]];
        } else {
          pitchNota[iii] = range24[(int)calcInt[iii]];
        }
        pitchNotaOld[iii] = pitchNota[iii];
        
      } else {
        snapOn[iii] = snapOn[iii] + 1;
        if (snapOn[iii] <= 2 && abs(pitchNota[iii]-pitchNotaOld[iii])> 15) {
          pitchNota[iii] = pitchNotaOld[iii];
        }
      }
      
	  /* (2) */
      if (decPart[iii] >= (1 - pitchSnap)){
        snapOn[iii] = 0;
        if (bitRead(incoming,14) == 0) {
          pitchNota[iii] = range12[(int)calcInt[iii]+1];
        } else {
          pitchNota[iii] = range24[(int)calcInt[iii]+1];
        }
        pitchNotaOld[iii] = pitchNota[iii];
       
      } else {
        snapOn[iii] = snapOn[iii] + 1;
        if (snapOn[iii] <= 2 && abs(pitchNota[iii]-pitchNotaOld[iii])> 15) {
          pitchNota[iii] = pitchNotaOld[iii];
        }
      }

}

void potsReadSendCC() {
	for (int ii = 0; ii <= 1; ii++) { 

		if (ccPot[ii] == 13) {
			cicliMidiCC[ii] = 7;
		} else {
			cicliMidiCC[ii] = 7;
		}
 
    if ((jj[ii] % (cicliMidiCC[ii] - ii)) == 0) {
		if (ii == 0) {
			unsigned long timeStart = micros();
			while(micros() - timeStart < 4000){
			  MIDI.read();
			}
        }
      
      ccRead[ii] = analogRead(analogPins[ii+2]);

      jj[ii] = jj[ii] + 1;
      if (jj[ii] == cicliMidiCC[ii] - ii) { jj[ii] = 0; }
      MIDI.read();

      ccValue[ii] = map(ccRead[ii], 0, 1023, 0, 127);

      if (ccValue[ii] != ccValue_Old[ii] && ccPot[ii] != 12 && ccPot[ii] != 13){
          midiCC(listaControlChange[ccPot[ii]], ccValue[ii]);
      } else if (ccPot[ii] == 12) {

        if (ccRead[ii] <= 100) {
            transpSlideDown = HIGH;

        } else if (ccRead[ii] >= 800) {
            transpSlideUp = HIGH;

        } else {
            transpSlideDown = LOW;
            transpSlideUp = LOW;
        }
          
        transposeSlideAction();
      } else if (ccPot[ii] == 13) {
				/* Control pitch snap with rotPot */
				pitchSnap = (ccRead[ii])*0.4/1023 + 0.2;
				ccPotOld[ii] = 13;
			}
      ccValue_Old[ii] = ccValue[ii];
    } else {
      jj[ii] = jj[ii] + 1;

      if (jj[ii] == 2*cicliMidiCC[ii] - ii) { jj[ii] = 0; }
    }
		
		if (ccPotOld[ii] == 13 && ccPot[ii] != 13) {
			pitchSnap = DEFAULT_pitchSnap;
			ccPotOld[ii] = ccPot[ii];
		}
		
	}
 
}

Pitch Bend Tables

Python
This code computes, for a given pitch range, the pitch bend values corresponding to exact notes for pitch snapping. The code saves these values in a txt file. The content of the output file is ready to be copy-pasted in the Arduino IDE.
#Python 3.6
import numpy as np

# Pitch range value, interpreted, as in synthesizers, as +- number of semitones
pitchRange = 12

# Generate the array containing all the pitch bend values corresponding to
# exact notes for the given pitch range.
range12 = np.round(np.arange(-8192, 8192, 16383 / 2 / pitchRange) + 8192)

# Name of the variable containing exact notes pitch bend value in the
# Arduino code
varName = 'range' + str(int(pitchRange)) +\
          '[' + str(int(2 * pitchRange + 1)) + ']'

# Name of the txt file
fileName = 'pitchValues' + str(int(pitchRange)) + '.txt'

# Write the txt file with Arduino syntax, ready to be copy-pasted in the
# IDE
with open(fileName, 'w') as f:
    f.write('const int ' + varName + ' = \n')
    f.write('{')
    for pitchValue in range12[0:-1]:
        f.write(str(int(pitchValue)))
        f.write(', ')
    f.write(str(int(range12[-1])))
    f.write('};')

Credits

andreagregorini

andreagregorini

2 projects • 3 followers

Comments