Introduction: Guitar Hero Arduino Project

Wij zijn Maarten Vrebos, Justin Cavanas en Wannes Stroobandt en we studeren multimedia & communicatietechnologie. Voor een groepsproject voor het vak Audiovisual & IT Principles hebben wij een Guitar Hero-gitaar gehackt en gebruikt als behuizing voor onze MIDI-controller. Het was onze bedoeling om de bestaande knoppen op de gitaar intern te vervangen. Onze controller zal vastgehouden en bespeeld worden als een normale gitaar. Aangezien we iets hebben gehackt hebben we er niet veel extra materiaal in moeten verwerken.

In de afbeelding kan u onze allereerste schets op papier zien van hoe het eindproduct er zou moeten uitzien met daarnaast een foto van de gitaar die als behuizing zal worden gebruikt.

Wij hebben ons voor dit project gebaseerd op volgende bronnen:

https://slapyak.wordpress.com/guitar-hero-midi-con...

https://www.instructables.com/id/Converting-a-rescu...

https://gizmodo.com/391834/turn-your-guitar-hero-g...

Benodigdheden voor dit project

  • 6 kleine pushbuttons
  • 7 1kohm resistors
  • 1 gele LED 1
  • blauwe LED
  • 1 Arduino Uno R3
  • 1 groene LED
  • 2 rode LEDs
  • 1 schuifschakelaar
  • 1 breadboard
  • 1 potentiometer
  • 1 protobord
  • 1 Guitar Hero gitaar
  • Voldoende bedrading
  • Materiaal om te solderen/dremelen/
  • Schroevendraaier

Step 1: Componenten Verzamelen

Voor ons prototype (op breadboard) hebben we volgende componenten gebruikt:

6 Pushbuttons

7 1kohm Resistors

1 Yellow LED

1 Blue LED

1 Arduino Uno R3

1 Green LED

2 Red LED

1 Schuifschakelaar

1 Breadboard

1 Potentiometer

Step 2: Prototype Bouwen

Om ons prototype te bouwen hebben we al onze componenten gebruikt op een breadboard, deze breadboard dient dan als testobject zodat we niet meteen in de behuizing te werk moeten gaan. Dit prototype hebben we dan ook gedigitaliseerd via tinkercad.com, op deze manier hadden we een duidelijk overzicht van ons prototype dat elk groepslid ook kon bewerken.

Er worden 5 kleine pushbuttons gebruikt die fungeren als 5 snaren en een grote pushbutton die in combinatie met één of meerdere 'snaren' moet worden ingedrukt om een auditief effect te krijgen. De verschillende LED-lampjes dienen gewoon als visuele controle om er zeker van te zijn dat de interactie succesvol werkt.

Step 3: Code Prototype

Globale variabelen

In het eerste deel van de code initialiseer je globale variabelen voor de pins van arduino uno waar alle pushbuttons mee verbonden zijn.

// zet pin numbers waar mainButton(snaar) en andere buttons aan verbonden zijn:<br>const int mainButton = A1;     // gitaar snaar
const int lightSensor = A0;
const int buttonPin1 = 2;     // nummer van pushbutton1
const int buttonPin2 = 3;     // nummer van pushbutton2<br>const int buttonPin3 = 4;     // nummer van pushbutton3<br>const int buttonPin4 = 5;     // nummer van pushbutton4<br>const int buttonPin5 = 6;     // nummer van pushbutton5<br>

Hierna worden er twee arrays aangemaakt voor de namen van de pushbuttons en hun pinnummer.

const int aantalKnoppen = 5;<br>const String namenKnoppen[aantalKnoppen] = {"knop 1", "knop 2", "knop 3", "knop 4", "knop 5"};
const int knopPinnen[aantalKnoppen] = {2, 3, 4, 5, 6};

En dan nog variabelen voor de pins van de LED lichtjes.

const int ledPin1 =  13;      // the number of the LED pin 13
const int ledPin2 = 12;       // the number of LED pin 12
const int ledPin3 = 11;       // the number of LED pin 11
const int ledPin4 = 10;       // the number of LED pin 10
const int ledPin5 = 9;        // the number of LED pin 9
const int potPin = A5;        // the number of LED pin A5

De laatste globale variabelen dienen als 'states' voor de sensors ( zijn de pushbuttons ingedrukt of niet? potentiometer, lichtsensor).

// initialiseer buttonStates voor de knoppen (ingedrukt of niet)<br>int mainButtonState = 0;
int buttonState1 = 0;
int buttonState2 = 0;
int buttonState3 = 0;
int buttonState4 = 0;
int buttonState5 = 0;
int lightSensorState = 0;
int potValue = 0;
int lightValue = 0;

Setup

Nu volgt de void setup functie. Deze is van het type void (geeft geen waarde terug) en de instructies hierin worden maar 1 keer uitgevoerd.

Bij elke functie is commentaar geschreven wat er concreet gedaan wordt. Extra uitleg over wat een specifieke functie concreet doet is te vinden in de arduino reference

void setup() {<br>  // data rate per seconde (baud) voor seriele data transmissie
  Serial.begin(9600);
  
  // Initialiseer de ledPin variabelen als output
  pinMode(ledPin1, OUTPUT);
  pinMode(ledPin2, OUTPUT);
  pinMode(ledPin3, OUTPUT);
  pinMode(ledPin4, OUTPUT);
  pinMode(ledPin5, OUTPUT);
  
  
  // initialiseer alle pushbuttons als input:
  pinMode(mainButton, INPUT);
  pinMode(buttonPin1, INPUT);
  pinMode(buttonPin2, INPUT);
  pinMode(buttonPin3, INPUT);
  pinMode(buttonPin4, INPUT);
  pinMode(buttonPin5, INPUT);
  pinMode(potPin, INPUT);
  pinMode(lightSensor, INPUT);
}

Void functie

Na de setup() functie volgt de loop() functie, de instructies die hierin staan gaan herhaald uitgevoerd worden.

void loop() {<br>  // lees de staat van de pushbuttons uit (ingedrukt of niet)
  mainButtonState = digitalRead(mainButton);
  buttonState1 = digitalRead(buttonPin1);
  buttonState2 = digitalRead(buttonPin2);
  buttonState3 = digitalRead(buttonPin3);
  buttonState4 = digitalRead(buttonPin4);
  buttonState5 = digitalRead(buttonPin5);
  // alle pushbutton statusen in een array
  int buttonStates[] = {buttonState1, buttonState2, buttonState3, buttonState4, buttonState5};
// leest de waarde uit van de potentiometer en de lichtsensor
potValue = analogRead(potPin);
lightValue = analogRead(lightSensor);
  
// declareer een array mainStates en geef die de standaard waarden 0 in.
int mainStates[] = {0, 0, 0, 0, 0};
// loop over de array aantalKnoppen 
for(int i = 0; i < aantalKnoppen; i++){
    pinMode(knopPinnen[i], INPUT);	// initialiseer alle knopPinnen als input
    digitalRead(knopPinnen[i]);		// lees de waarde van alle knoppinnen uit

// indien de mainswitch (snaar) ingedrukt is, print alle knopnamen , alle buttonstates
    if(mainButtonState == HIGH){
      Serial.print(namenKnoppen[i]);
      Serial.print(",");
      Serial.println(buttonStates[i]);
    }
  }

Step 4: Prototype Uittesten

Nadat het prototype gebouwd is volgens ons model en de code geschreven is in Processing, is het tijd om het prototype uit te testen. Op de video is te zien dat alle knoppen een reactie geven op de bijhorende ledjes en dat ook combinaties van knoppen mogelijk zijn.

In de tweede video is te zien hoe onze tremolo werkt aan de hand van een potentiometer in de gitaar en hoe de waardes worden uitgelezen in Processing.

Step 5: Behuizing "ontmantelen" En Kijken Welke Componenten Gebruikt Gaan Worden

Als de code correct werkte op het prototype zijn we begonnen met het "ontmantelen" van onze Guitar Hero-gitaar. We hebben de gitaar opengemaakt met een schroevendraaier en bekeken welke originele componenten we eventueel nog zouden kunnen hergebruiken voor onze controller. Uiteindelijk hebben we onze eigen pushbuttons in de bestaande buttons gekregen (zie volgende stap). We hebben de tremolo ook gebruikt voor ons eindproduct en voor onze hoofdbutton (initiële button om als een combinatie af te spelen) hebben we ook de originele twee buttons gebruikt(zie vierde foto). De LEDjes zullen verdwijnen (deze waren enkel ter indicatie zodat we zagen dat alle knoppen correct werkten.

Step 6: Werking Originele Buttons + Dremelen

Op de bijhorende video is de wijze te zien waarop de twee originele knoppen werken als een soort van schakelaar die wij gebruiken om een effect te genereren bij combinatie van knoppen.

Om onze eigen buttons te verwerken in de originele knoppen hebben we de binnenkant van de originelen er grotendeels uitgehaald zoals te zien is op de foto.

Step 7: Bedrading Solderen + Buttons Vastlijmen

Omdat we niet meer met een breadboard werken moeten de draden gesoldeerd worden om zo de verschillende componenten met elkaar te verbinden. Nadat dit gebeurd is kunnen we de buttons vastlijmen zoals te zien is op de foto's. Eens dit gebeurd is kunnen we doorgaan naar de volgende stap.

Step 8: Plaats Maken in De Behuizing

Omdat dit Guitar Hero-model redelijk krap was om mee te werken hebben we extra plaats moeten maken d.m.v. dremelen. Zo hebben we uit de achterkant van de gitaar een hele strook verwijderd zodat er meer plaats ontstaat voor de bedrading in de gitaar. Omdat er overal in de binnenkant obstakels waren, waaronder veel buisjes om de vijzen in te bevestigen, hebben we die ook verwijderd om optimaal van de gegeven ruimte gebruik te kunnen maken. Op de vierde en vijfde foto is te zien dat we in de achterkant van de gitaar een doorgang hebben gecreëerd voor de draden die naar de buttons gaan omdat de gitaar anders niet meer te sluiten was. En op de laatste foto is te zien dat we de draden die rechtstreeks verbonden worden met de Arduino door een gat in de onderkant van de gitaar de behuizing verlaten.

Step 9: Bedrading Aansluiten Op Protobord

Om alle componenten met elkaar te verbinden hebben we gebruik gemaakt van een protobord. Dit is een bordje dat eigenlijk op net dezelfde manier werkt als een breadbord, maar dan betrouwbaarder en efficiënter. We hebben de bedrading aan het bordje gesoldeerd zoals te zien is op de derde foto. Dit bord is het centrale punt van waaruit al onze verbindingen vertrekken en samenkomen(zie foto 2).

Step 10: Verstevigen

Als finishing touch is het verstandig om de losse delen te verstevigen voor extra stabiliteit. Op deze foto is te zien hoe we het deel dat we er hebben uitgehaald d.m.v. dremelen achteraan de buttons verstevigen met stukjes karton.

Step 11: Code Voor Het Communiceren Met Reaper

Deze code is opgedeeld in twee delen, het eerste deel is in de arduino IDE (interactive development enviroment) geschreven. Die code wordt geüpload naar arduino zelf en dient om alle waarden van de sensors van de midi controller uit te lezen en door te sturen naar processing.

Processing is het tweede gedeelte. Deze code dient om alles wat arduino doorstuurt te ontvangen en door te sturen naar Reaper.

Arduino

/*  This code is a basic sketch to communicate with Processing through Serial.<br>
     It is a blueprint in which you can put your own code
     specified for your own buttons, potentiometers or sensors.
     It has a handshake to make sure we have contact
     and the format in which we are communicating is decided
     It is important to construct the message this same way,
     so that Processing knows how to deconstruct it
     and send correct OSC-messages to our DAW
     made for werkcollege AV&IT
     oct 2017
*
/ baud rate
const long baudRate = 115200;
// time to wait in ms between polls to the pins
const int loopPauseTime =  200; // milli seconds
// start and end values for the message sent on Serial
const String startString = "*",
             endString   = "#";
const char contactCharacter = '|'; 
// pin id's
// other global variables
const int aantalKnoppen = 5;
const String namenKnoppen[aantalKnoppen] = {"knop 1", "knop 2", "knop 3", "knop 4", "knop 5"};
const int knopPinnen[aantalKnoppen] = {2, 3, 4, 5, 6};
const int mainButton = A1;
int mainButtonState = 0;
int potValue = 0;
// analoge sensors
const int potPin = A5;        // pin voor tremolo
// We need this function to establish contact with the Processing sketch
// Keep it here
void establishContact() {
  while (Serial.available() <= 0) {
    Serial.print(contactCharacter);   // send a char and wait for a response...
    delay(loopPauseTime);
  }
  Serial.read();
}
void setup() {
  // set the pinModes for all the pins
  for(int i = 0; i < aantalKnoppen; i++){
    pinMode(knopPinnen[i], INPUT);
  }
  pinMode(mainButton, INPUT);
  
  // uncomment if you use sensors that work on 3V instead of 5V
  // you will have to wire the 'ext' pin to 3.3V as well
  // analogReference(EXTERNAL);
  // initialize Serial comms
  Serial.begin(baudRate);
  while (!Serial);
  
  // wait for handshake
  establishContact();
}
void loop() {
  // STEP 1: READ BUTTONS
  // poll all the pins and map the reading to the appropriate range
  int buttonStates[aantalKnoppen];
  /*
  buttonStates[0] = digitalRead(knopPinnen[0]);
  buttonStates[1] = digitalRead(knopPinnen[1]);
  buttonStates[2] = digitalRead(knopPinnen[2]);
  buttonStates[3] = digitalRead(knopPinnen[3]);
  buttonStates[4] = digitalRead(knopPinnen[4]);
  */
  
  mainButtonState = digitalRead(mainButton); 
  for(int i = 0; i < aantalKnoppen; i++){
    buttonStates[i] = digitalRead(knopPinnen[i]);
  }
  
  
  potValue = analogRead(potPin);
  // examples:
  // float v0 = map(bpm, 0, 1023, 60, 250);
  // if you want to use a normalized float (eg. for volume)
  // float v1 = map(analogRead(pin2), fromMin, fromMax, 0, 100) / 100.0;
  // STEP 2: WRITE MESSAGE
  Serial.print(startString);  // start a message sequence
  for(int i = 0; i < aantalKnoppen; i++){
    if(mainButtonState == HIGH){
      Serial.print(namenKnoppen[i]);
      Serial.print(",");
      Serial.print(buttonStates[i]);
      if(i < aantalKnoppen - 1){
        Serial.print(",");
      }
    }else{
      buttonStates[i] = 0;
      Serial.print(namenKnoppen[i]);
      Serial.print(",");
      Serial.print(buttonStates[i]);
      if(i < aantalKnoppen - 1){
        Serial.print(",");
      }
    }
  }
  Serial.print(",");
  Serial.print("tremolo");
  Serial.print(",");
  Serial.print(map(potValue, 0, 1023, 0, 100));
  
  // write the end of message
  Serial.print(endString);
  // wait for a while..
  delay(loopPauseTime);
}

Processing

Disclaimer: Niet alle code van de processing sketch staat hier in geschreven, voor de volledige code zie het bestand: ProcessingSoundControl_handout_v6_1.pde in bijlage

De volgende instructies moeten aangepast worden (indien nodig):

// Baudrate moet hetzelfde zijn zoals in de arduino sketch
final int baudRate = 115200;
// Zoek naar het IP adress in reaper (zie screenshots in bijlage)
// Processing stuurt naar dit andres en reaper luistert hier naar
//
//final String remoteIP = "192.168.1.43"; //eg. "127.0.0.1";
final String remoteIP = "10.3.209.60";
// Take note of the sendPort and fill this in in Reaper.
// This is the port Processing sends to and Reaper listens to.
final int listenPort = 12000, 
          sendPort   = 12000;
// The listenPort here is to actively debug.
// the portNames are here to debug as well.
//final String portName = "/dev/ttyACM0";
 final String portName = "COM5"; // "/dev/ttyUSB0";
///////////////////// END of USER PARAMETERS /////////////////////////////
import processing.serial.*;
import java.util.*;
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress myRemoteLocation;
Serial commsPort;       // The serial port
boolean messageArrived = false; 
String incoming = "", 
       IncomingOSCMessage = "";
final char startChar = '*', 
           endChar   = '#';
           
final char contactCharacter = '|';
// To make sure we only send the parameters (values) that change
// these global variabls are delcared here but should
// not be initialized here!
HashMap oldParams,
                       newParams,
                       toSendParams;
// We need to split the message at every comma
void processIncoming () {
  String resVec[]  = incoming.split(",");
  // we get name + value pairs
  // so for every name (+2)...
  try{
    for (int i = 0; i< resVec.length; i+=2) { 
      float value = Float.parseFloat(resVec[i+1]);
      // put them in the new Hashtable
      newParams.put(resVec[i], value);
    }
  }
  // if an error occurs, let's catch it display and exit.
  catch(Exception ex){
    println("Exception Message: " + ex);
    printArray(resVec);
    exit();
  }
}
// To filter our messages
/* We make sure there is only an OSC-out message when 
 * the input message (Serial) changes
 * That is: if we turn/push the button and it changes value.
 * So we filter out the incoming values that actually change
 * note: we won't avoid jumping values
 * as come from eg accelerometers or distance sensors
 * you will need to smooth those yourself in Arduino 
 */
void filterParams () {
  toSendParams = new HashMap();
  for (String key : newParams.keySet()) {
    // if the key is already present
    if (oldParams.containsKey(key)) {
      // key present and value not the same, then update
      if (!oldParams.get(key).equals(newParams.get(key))) {    
        toSendParams.put(key, newParams.get(key));
      }
    }
    else{ // key is not present in old params, so put it!
        toSendParams.put(key, newParams.get(key));
    }
    oldParams.put(key, newParams.get(key));
  }
}
void makeOSC() {
  for (String key : toSendParams.keySet()) {
    OscMessage myMessage = new OscMessage("/"+ key);
    myMessage.add(toSendParams.get(key));
    /* send the message */
    oscP5.send(myMessage, myRemoteLocation);
  }
}
void translateMessage() {
  processIncoming();
  filterParams();
  makeOSC();
}
// When we want to print to the window
void ShowIncoming() {
  // to see incoming message, as set in the HashMap
  text("Incoming from Arduino", 20, 20);
  int y = 20;
  for (String key : newParams.keySet()) {
    y = y+20;
    text(key, 20, y);
    text(newParams.get(key), 300, y);
  }
}
void showOsc() {
  text(IncomingOSCMessage, 300, 200);
  IncomingOSCMessage ="";
}
void setup() {
  size(1000, 800);  // Stage size
  fill(255);
  background(0);
  oldParams = new HashMap();
  newParams = new HashMap();
  
  //printArray(Serial.list());
  commsPort = new Serial(this, portName, baudRate);
  /* start oscP5, listening for incoming messages */
  oscP5 = new OscP5(this, listenPort);
  /* myRemoteLocation is a NetAddress. a NetAddress takes 2 parameters,
   * an ip address and a port number. myRemoteLocation is used as parameter in
   * oscP5.send() when sending osc packets to another computer, device, 
   * application. usage see below. for testing purposes the listening port
   * and the port of the remote location address are the same, hence you will
   * send messages back to this sketch.
   */
  myRemoteLocation = new NetAddress(remoteIP, sendPort);
}
void draw() {
  if (messageArrived) {
    background(0);
    translateMessage();
    ShowIncoming();
    messageArrived= false;
  }
  showOsc();
}
void serialEvent(Serial commsPort) {
  // read a byte from the serial port:
  char inChar = commsPort.readChar();
  switch (inChar) {
    case contactCharacter:
      commsPort.write(contactCharacter);       // ask for more
      println("starting...");
      break;
    case startChar:
      incoming= "";
      break;
    case endChar:
      messageArrived = true;
      //println("end of msg");
      break;
    default:
      incoming += inChar;
      break;
  }
}
/* incoming osc message are forwarded to the oscEvent method. */
void oscEvent(OscMessage theOscMessage) {  
  float value = theOscMessage.get(0).floatValue(); // get the 1st osc argument
  IncomingOSCMessage += "\n" + 
                        String.format("### received an osc message: " + 
                        " addrpattern: " + 
                        theOscMessage.addrPattern() + 
                        " :  %f", 
                        value);
  println(IncomingOSCMessage);
}

Step 12: Controller Uittesten

Nu alles is aangesloten, alle code is geschreven en alles is gedubbelcheckt is het eindelijk tijd om de controller z'n werk te laten doen. Zoek een paar leuke effecten op Reaper en geniet van de voltooide Guitar Hero MIDI Controller!