Arduino MIDI Glockenspiel with TIP120 Transistors
Motivation
This was a project done in collaboration with Enrico de Trizio, a New York based pianist, composer, and music producer. Similar in many ways to a project I did a couple years ago with a smaller glockenspiel, this iteration allowed me to improve on many ideas. Working closely with Enrico, a musician, helped me keep a focus on musicality and not just the engineering challenges.
Some of the notable differences from my previous build include:
Using “Hiduino” to make the Arduino Mega a “class compliant USB-MIDI device.” Meaning you just plug it in and it pops up as a MIDI instrument; no more using Loop MIDI and Hairless MIDI.
Using transistors to fire the solenoids versus relays, meaning much less clicking noise.
Adding multiple features to increase the musicality of the instrument, such as damping and dynamic control.
Process
Strikers
The components that strike the keys of the glockenspiel were very similar to the design used in my previous two builds. Small 5V solenoids from Digikey.com are clipped into 3D printed holders that then slide over an L-shaped piece of angle aluminum. Unlike in past builds where these were held to the aluminum with set screws, I just designed the slot smaller so it is held in place with friction. Since I switched from PLA plastic to PETG, there will be less deformation of the plastic overtime and this will hopefully remain secure.
Other differences include the inclusion of an LED on each of the strikers. These are wired, with a series resistor, in parallel with the two wires leading to each solenoid. The LED is mostly contained in the 3D printed plastic, so when they blink, the light is directed downward on to the key. The resistor and legs of the LED face up, so I printed a small cap that slips over the top, hiding the wiring. The inclusion of this LED allowed me to also make an arch in the middle of each component, which all the wires from the solenoids run through, keeping them from dropping down on the keys.
Transistors
In previous builds I’d used relay modules to trigger the solenoids, mostly for the simplicity of wiring. A huge downside of the relays however is the loud click they make each time they fire. For this build I used TIP120 transistors instead, eliminating all the noise, but increasing the build complexity.
Damper Bar
.
Hiduino
Google this and use the Github resources! It will allow the Arduino to be identified as a native midi device as soon as you plug it in to your computer.
.
Schematic
Code
#include <MIDI.h>
#include <Servo.h>
//M2 bolts to open electronics case
//Remove the top access the ISP header for uploading code (it's the header further from the USB port). Removing front can help as well.
//REMEMBER the 10uF capacitor between reset and GND of the programmer board (probably an Arduino Uno).
//Use the "old style" wiring for the ArduinoISP program (line 81 not commented out)
//Servo variables
Servo Rservo; //closer to electronics
Servo Lservo;
int RservoPin = 10; //orange wire
int LservoPin = 11; //yellow wire
int maxServoRotation = 34; //Rservo goes from 0 UP to maxServoRotation; Lservo goes from 90 DOWN to maxServoRotation
int currentServoPosition;
//Buttons:
int rampingButton = 3;
int rampingStyleButton = 4;
int killSwitchButton = 5;
//Kill switch variabls
int lastKillSwitchState = 0;
int killSwitchState = 0;
long lastKillSwitchDebounceTime = 0;
int killSwitchLedState = 0;
//Damper knob variables
int damperKnobPin = A0;
int lastDamperKnobReading = 0;
bool damperKnobOverride = true;
bool servoMoving = false;
int newServoPosition;
long lastServoMoveTime;
long servoTimeOut = 500;
int currentCC64 = 0;
//Dynamic knob variables
int dynamicKnobPin = A2;
int lastDynamicKnobReading = 0;
int currentCC11 = 127;
//Ramping variables
bool rampingOn = false;
int rampingLED = 12;
int rampingStyleLEDOne = 7;
int rampingStyleLEDTwo = 8;
int rampingStyleLEDThree = 9;
int rampStrikeTime;
long rampingStartTime;
long rampingStyleStartTime;
int rampingButtonState;
int rampingStyleButtonState;
int lastRampingButtonState = LOW;
int lastRampingStyleButtonState = LOW;
unsigned long lastRampingDebounceTime = 0;
unsigned long lastRampingStyleDebounceTime = 0;
unsigned long debounceDelay = 50;
int rampingPeriodPin = A1;
int rampingPeriod;
int rampingStyle = 0;
int rampingRange = 0;
//Default time solenoids fire for:
int strikeTime = 8;
int strikeTimeList[] = {2,3,4,5,8};
//byte c2,d2f,d2,e2f,e2,f2,g2f,g2,a2f,a2,b2f,b2,c3,d3f,d3,e3f,e3,f3,g3f,g3,a3f,a3,b3f,b3,c4;
//byte notes[] = {c2,d2f,d2,e2f,e2,f2,g2f,g2,a2f,a2,b2f,b2,c3,d3f,d3,e3f,e3,f3,g3f,g3,a3f,a3,b3f,b3,c4};
//byte pins[] = {42,23,48,29,36,38,33,40,35,44,25,46,41,27,43,31,37,39,34,26,30,24,32,28,22};
//Notes and their assigned Arduino pins:
byte c2= 42;
byte d2f= 23;
byte d2= 48;
byte e2f= 29;
byte e2= 36;
byte f2= 38;
byte g2f= 33;
byte g2= 40;
byte a2f= 35;
byte a2= 44;
byte b2f= 25;
byte b2= 46;
byte c3= 41;
byte d3f= 27;
byte d3= 43;
byte e3f= 31;
byte e3= 39;
byte f3= 37;
byte g3f= 34;
byte g3= 26;
byte a3f= 30;
byte a3= 24;
byte b3f= 32;
byte b3= 28;
byte c4= 22;
MIDI_CREATE_DEFAULT_INSTANCE();
//There are 128 notes in MIDI. 36 is the low C2 on the glockenspiel
//byte baseNote = 60; //The first C on the glockenspiel (60 is another C that Enrico requested)
//Swap the line below with the one above for FOLDING (the extra octave above and below):
byte baseNote = 48; //The C2 on the glockenspiel is actually 36, but I started an octave lower so more notes not on the glockenspiel still make sound, just an octave lower or higher;
//If the baseNote is 60, this should be 12 lower, or 48
//This is the array of notes that can be played on the glockenspiel
//byte noteList[] = {c2,d2f,d2,e2f,e2,f2,g2f,g2,a2f,a2,b2f,b2,c3,d3f,d3,e3f,e3,f3,g3f,g3,a3f,a3,b3f,b3,c4};
//Swap the line below with the one above for the extra octave above and below:
//Notice that there are two sets of the lowest and highest octaves to catch MIDI notes that are not on the keys.
byte noteList[] = {c2,d2f,d2,e2f,e2,f2,g2f,g2,a2f,a2,b2f,b2,c2,d2f,d2,e2f,e2,f2,g2f,g2,a2f,a2,b2f,b2,c3,d3f,d3,e3f,e3,f3,g3f,g3,a3f,a3,b3f,b3,c4,d3f,d3,e3f,e3,f3,g3f,g3,a3f,a3,b3f,b3,c4};
//Above this line are variables
// -----------------------------------------------------------------------------
//******************************************************************************
// -----------------------------------------------------------------------------
//Below this line are functions
//Define what action to take when a 'note on' byte is received
void handleNoteOn(byte inChannel, byte inNumber, byte inVelocity){
byte noteToStrike;
//transpose the inNumber note so that the noteToStrike is always in range.
//transpose down if above the highest note:
if(inNumber > baseNote + 25){ //There are 25 keys on the trillofono
while(inNumber > baseNote + 25){
inNumber -= 12;
}
noteToStrike = noteList[inNumber - baseNote];
}
//transpose up if below the lowest note:
else if(inNumber < baseNote){
while(inNumber < baseNote){
inNumber += 12;
}
noteToStrike = noteList[inNumber - baseNote];
}
//if the note was already in range, nothing more needs to be done:
else{
noteToStrike = noteList[inNumber - baseNote];
}
if(rampingOn == true){
strike(noteToStrike,rampStrikeTime);
}
else{
strike(noteToStrike,strikeTime);
}
//In setup() we are only listening to channel 1, so that info is not used here. Neither is velocity, since that is defined as a constant in strike()
}
void handleControlChange(byte channel, byte ccNumber, byte ccValue){
if(ccNumber == 11){ //dynamics
if(ccValue <= 0){
setStrikeTime(2);
}
else if(ccValue > 0 && ccValue <= 31){
setStrikeTime(3);
}
else if(ccValue > 31 && ccValue <= 63){
setStrikeTime(4);
}
else if(ccValue > 63 && ccValue <= 95){
setStrikeTime(5);
}
else{
setStrikeTime(8);
}
currentCC11 = ccValue;
}
if(ccNumber == 64){ //damping
newServoPosition = map(ccValue,0,127,maxServoRotation,0); //about 3.4 CC values to each degree
if(ccValue >= currentCC64 + 5 || ccValue <= currentCC64 - 5){ //don't attach servos unless CC changes by 5 or more
damperKnobOverride = false; //will ignore damper knob now until it is rotated again
servoMoving = true;
Rservo.attach(RservoPin);
Lservo.attach(LservoPin);
currentCC64 = ccValue;
}
}
if (ccNumber == 1) { //ramping
}
}
void moveServo(){
if(servoMoving == true && currentServoPosition != newServoPosition){
lastServoMoveTime = millis();
if(newServoPosition > currentServoPosition){
currentServoPosition++;
}
else if(newServoPosition < currentServoPosition){
currentServoPosition--;
}
Rservo.write(currentServoPosition);
Lservo.write(90 - currentServoPosition);
delay(10);
}
else if(currentServoPosition == newServoPosition && millis() - lastServoMoveTime >= servoTimeOut){
servoMoving = false;
Rservo.detach();
Lservo.detach();
}
}
void strike(byte stringnote, int timeToStrike){
digitalWrite(stringnote,HIGH);
delay(timeToStrike); //set to 8ms with the strikeTime variable.
digitalWrite(stringnote,LOW);
}
void setStrikeTime(byte ccValue){
strikeTime = ccValue;
}
void checkDamperKnob(){
int damperKnobReading = analogRead(damperKnobPin);
if(damperKnobReading <= lastDamperKnobReading - 10 || damperKnobReading >= lastDamperKnobReading + 10){ //only continue if the knob has turned a signigficant amount
newServoPosition = map(damperKnobReading,0,1023,maxServoRotation,0); //about 3.4 CC values to each degree
int scaledDamperReading = map(damperKnobReading,0,1023,0,127); //scale the knop value to a CC value
damperKnobOverride = true; //will now respond to the knob until new CC64 MIDI data comes in AND the knob is moved by a significant amount
if(damperKnobOverride == true && scaledDamperReading <= currentCC64 + 10 && scaledDamperReading >= currentCC64 -10){ //Only enable servos if close to the last MIDI position
Rservo.attach(RservoPin);
Lservo.attach(LservoPin);
currentCC64 = scaledDamperReading;
lastDamperKnobReading = damperKnobReading;
servoMoving = true;
}
}
}
void checkDynamicKnob(){
if(rampingOn == false){
int dynamicKnobReading = analogRead(dynamicKnobPin);
if(dynamicKnobReading <= lastDynamicKnobReading - 10 || dynamicKnobReading >= lastDynamicKnobReading + 10){ //Don't do anything unless the knob has changed. Allows MIDI to take back over.
int scaledDynamicReading = map(dynamicKnobReading, 0, 1023, 0, 127);
if(scaledDynamicReading >= currentCC11 - 10 && scaledDynamicReading <= currentCC11 + 10){ //Don't change dynamic until close to the last setting from MIDI
if(dynamicKnobReading <= 204){
setStrikeTime(2);
}
else if(dynamicKnobReading > 204 && dynamicKnobReading <= 408){
setStrikeTime(3);
}
else if(dynamicKnobReading > 408 && dynamicKnobReading <= 612){
setStrikeTime(4);
}
else if(dynamicKnobReading > 612 && dynamicKnobReading <= 816){
setStrikeTime(5);
}
else{
setStrikeTime(8);
}
lastDynamicKnobReading = dynamicKnobReading;
currentCC11 = scaledDynamicReading;
}
}
}
}
void ramping(){
int rampingButtonReading = digitalRead(rampingButton);
if(rampingButtonReading != lastRampingButtonState){
lastRampingDebounceTime = millis();
}
if((millis() - lastRampingDebounceTime) > debounceDelay){
if(rampingButtonReading != rampingButtonState){
rampingButtonState = rampingButtonReading;
if(rampingButtonState == LOW && rampingOn == false){
rampingOn = true;
digitalWrite(rampingLED,HIGH);
rampingStartTime = millis();
}
else if(rampingButtonState == LOW && rampingOn == true){
rampingOn = false;
digitalWrite(rampingLED,LOW);
digitalWrite(rampingStyleLEDOne,LOW);
digitalWrite(rampingStyleLEDTwo,LOW);
digitalWrite(rampingStyleLEDThree,LOW);
handleControlChange(1,11,map(analogRead(dynamicKnobPin),0,1023,0,127));
}
}
}
lastRampingButtonState = rampingButtonReading;
if(rampingOn == true){
rampingPeriod = map(analogRead(rampingPeriodPin),0,1023,200,5000);
int rampingRangeReading = analogRead(dynamicKnobPin);
if(rampingRangeReading <= 341){
rampingRange = 0;
}
else if(rampingRangeReading > 341 && rampingRangeReading <= 682){
rampingRange = 1;
}
else{
rampingRange = 2;
}
//Check for ramping style button press
int rampingStyleButtonReading = digitalRead(rampingStyleButton);
if(rampingStyleButtonReading != lastRampingStyleButtonState){
lastRampingStyleDebounceTime = millis();
}
if((millis() - lastRampingStyleDebounceTime) > debounceDelay){
if(rampingStyleButtonReading != rampingStyleButtonState){
rampingStyleButtonState = rampingStyleButtonReading;
if(rampingStyleButtonState == LOW){
rampingStyle++;
if(rampingStyle > 2){
rampingStyle = 0;
}
}
}
}
lastRampingStyleButtonState = rampingStyleButtonReading;
int rampingDeltaTime = millis() - rampingStartTime;
if(rampingStyle <= 0){ //sawtooth
digitalWrite(rampingStyleLEDOne,HIGH);
digitalWrite(rampingStyleLEDTwo,LOW);
digitalWrite(rampingStyleLEDThree,LOW);
if(rampingDeltaTime <= rampingPeriod/3){
rampStrikeTime = strikeTimeList[0 + rampingRange];
}
else if(rampingDeltaTime <= 2*rampingPeriod/3){
rampStrikeTime = strikeTimeList[1 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod){
rampStrikeTime = strikeTimeList[2 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod + rampingPeriod/3){
rampingStartTime = millis();
}
}
else if(rampingStyle == 1){ //reverse sawtooth
digitalWrite(rampingStyleLEDOne,LOW);
digitalWrite(rampingStyleLEDTwo,HIGH);
digitalWrite(rampingStyleLEDThree,LOW);
if(rampingDeltaTime <= rampingPeriod/3){
rampStrikeTime = strikeTimeList[2 + rampingRange];
}
else if(rampingDeltaTime <= 2*rampingPeriod/3){
rampStrikeTime = strikeTimeList[1 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod){
rampStrikeTime = strikeTimeList[0 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod + rampingPeriod/3){
rampingStartTime = millis();
}
}
else if(rampingStyle >= 2){ //triangle wave
digitalWrite(rampingStyleLEDOne,LOW);
digitalWrite(rampingStyleLEDTwo,LOW);
digitalWrite(rampingStyleLEDThree,HIGH);
if(rampingDeltaTime <= rampingPeriod/3){
rampStrikeTime = strikeTimeList[0 + rampingRange];
}
else if(rampingDeltaTime <= 2*rampingPeriod/3){
rampStrikeTime = strikeTimeList[1 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod){
rampStrikeTime = strikeTimeList[2 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod + rampingPeriod/3){
rampStrikeTime = strikeTimeList[1 + rampingRange];
}
else if(rampingDeltaTime <= rampingPeriod + 2*rampingPeriod/3){
rampStrikeTime = strikeTimeList[0 + rampingRange];
}
else if(rampingDeltaTime <= 2*rampingPeriod){
rampingStartTime = millis();
}
}
}
}
void killSwitch(){
//check state of kill switch button with debounce
int killSwitchReading = digitalRead(killSwitchButton);
if(killSwitchReading != lastKillSwitchState){
lastKillSwitchDebounceTime = millis();
}
if((millis() - lastKillSwitchDebounceTime) > debounceDelay){
if(killSwitchReading != killSwitchState){
killSwitchState = killSwitchReading;
if(killSwitchState == HIGH){
//turn on indicator LED
killSwitchLedState = !killSwitchLedState;
//write all solenoids low for good measure?
}
}
}
digitalWrite(rampingLED,killSwitchLedState);
lastKillSwitchState = killSwitchReading;
}
//Above this line are functions
// -----------------------------------------------------------------------------
//******************************************************************************
// -----------------------------------------------------------------------------
//Below this line are setup() and loop()
void setup(){
pinMode(rampingButton,INPUT_PULLUP);
pinMode(rampingLED,OUTPUT);
pinMode(rampingStyleButton,INPUT_PULLUP);
pinMode(rampingStyleLEDOne,OUTPUT);
pinMode(rampingStyleLEDTwo,OUTPUT);
pinMode(rampingStyleLEDThree,OUTPUT);
pinMode(killSwitchButton,INPUT_PULLUP);
pinMode(22,OUTPUT);
pinMode(23,OUTPUT);
pinMode(24,OUTPUT);
pinMode(25,OUTPUT);
pinMode(26,OUTPUT);
pinMode(27,OUTPUT);
pinMode(28,OUTPUT);
pinMode(29,OUTPUT);
pinMode(30,OUTPUT);
pinMode(31,OUTPUT);
pinMode(32,OUTPUT);
pinMode(33,OUTPUT);
pinMode(34,OUTPUT);
pinMode(35,OUTPUT);
pinMode(36,OUTPUT);
pinMode(37,OUTPUT);
pinMode(38,OUTPUT);
pinMode(39,OUTPUT);
pinMode(40,OUTPUT);
pinMode(41,OUTPUT);
pinMode(42,OUTPUT);
pinMode(43,OUTPUT);
pinMode(44,OUTPUT);
pinMode(46,OUTPUT);
pinMode(48,OUTPUT);
//Start the damping servos in their undamped position
Rservo.attach(RservoPin);
Lservo.attach(LservoPin);
Rservo.write(0);
Lservo.write(90);
delay(1500);
Rservo.detach();
Lservo.detach();
currentCC64 = 127; //undamped CC64 value
currentServoPosition = 0;
// Launch MIDI, by default listening to channel 1
MIDI.begin();
//Defines what to do when a 'note one' byte is detected during the read in the main loop
MIDI.setHandleNoteOn(handleNoteOn);
//Defines what to do when a 'control change' (CC) byte is detected during the read in the main loop
MIDI.setHandleControlChange(handleControlChange);
//Notice the higher baud rate than is often used (9600)
//Serial.begin(115200);
// (11/5/22) Removing this line made the code work. Can't recall why this was in the code to start with. Maybe when Hairless Midi was still used.
MIDI.turnThruOff();
}
void loop(){
killSwitch();
//killSwitchState = HIGH;
if(killSwitchLedState == LOW){
//do nothing
}
else{
ramping();
checkDamperKnob();
moveServo();
checkDynamicKnob();
MIDI.read();
}
}