Batería virtual

  • Julen Justo Neira

Idea

Este proyecto está estrechamente ligado con el ámbito musical. Desde el primer momento, ha estado orientado a la creación de un dispositivo MIDI capaz de simular el comportamiento de una batería, pero evitando el coste espacial que este supone.

En primera instancia, se definieron unos objetivos ideales a los que aspirar. En el mejor de los casos, el dispositivo debería ser capaz de:

  • Detectar el supuesto golpeo a un tambor.
  • Producir una señal MIDI con sensibilidad a la velocidad (velocity).
  • Reconocer el tambor golpeado a partir de la posición.
  • Calibrar la ubicación de los tambores virtuales.
  • Interactuar con DAWs (Digital Audio Workstation).
  • Conectarse a dispositivos vía bluetooth.
  • Funcionar en plataformas móviles.
  • Diseño minimalista, cómodo y ergonómico.

Después de definir la situación ideal toca poner los pies sobre la tierra. A partir de esta idea ambiciosa, se decidió diseccionar en etapas donde el proyecto evolucionaría paso a paso. Esta práctica se corresponde con la primera etapa. En esta, el dispositivo debería ser capaz de:

  • Detectar el supuesto golpeo a un tambor.
  • Reconocer el tambor golpeado a partir de la posición.
  • Calibrar la ubicación de los tambores virtuales.
  • Reproducir el sonido vía Arduino.
  • Ofrecer 3 modos:
    • Play
    • Calibrate
    • Tempo

Componentes

Material Coste Proyecto Coste Total
Placa Arduino Uno Proporcionado por el profesor Proporcionado por el profesor
Botón x3 (x6) 0,10€/ud 0,60€ * 6 uds
Cables m-h 1,6€ 1,6€
Altavoz 2,46€ 2,46€
Módulo acelerómetro MPU6050 GY-521 x1 (x6) 4,01€/ud 4,01€/ud  * 6 uds
Total 8,67€ 31,72€

Para la realización del proyecto, se utilizará como componente principal el acelerómetro MPU6050 GY-521; un módulo acelerómetro y giroscopio de 3 ejes con 6 DOF en total. Este sensor será el encargado de monitorizar los movimientos del usuario y darle una respuesta que emule una batería.

                Mpu 6050 GY-521

El módulo acelerómetro mide aceleración lineal, aceleración angular y temperatura; los dos primeros devolviendo valores de entre -32767 y 32767. Este valor no está convertido al Sistema Métrico Internacional, por lo que deberemos realizar su conversión para entender mejor los datos.

Por otro lado, los valores medidos se almacenan en los registros internos del acelerómetro. Mientras que estos son de un tamaño de 8 bits, los valores medidos son de 16 bits; con lo que cada palabra será conformada por dos lecturas de registro.

Sin embargo, este módulo también cuenta con una imperfección. La medida del giroscopio y el acelerómetro se traspasa entre ejes con cierta sensibilidad, lo cual introduce imperfecciones en nuestros cálculos.

Diseño inicial

                Detección de golpeo

El diseño inicial se basaba en reconstruir la posición de la baqueta a partir del ángulo de giro y la aceleración por integración numérica:

En el momento en el que la variación de la posición sufriese un cambio repentino y después un parón, se identificaría como golpeo.

                Calibración de los tambores

Por otro lado, los tambores serían introducidos por el usuario cada vez que se reiniciase la aplicación. El sistema tiene soporte para tantos tambores como permita la memoria de la placa Arduino Uno, permitiéndole al usuario definir tantos tambores como desee, al igual que repartirlos por el espacio a su gusto.

Para definir un nuevo tambor, el usuario tendría que seguir los siguientes pasos:

  1. Acceder al modo de calibración.
  2. Opción: Introducir un nuevo tambor.
  3. Delimitar las esquinas del tambor:
    1. Poner la baqueta sobre la esquina izquierda y pulsar el botón 2.
    1. Poner la baqueta sobre la esquina derecha y pulsar el botón 2.
    1. Poner la baqueta sobre la esquina superior y pulsar el botón 2.
    1. Poner la baqueta sobre la esquina inferior y pulsar el botón 2.
  4. Volver a elegir la misma opción para continuar introduciendo tambores.

Al elegir este orden de calibración en las esquinas de los tambores, somos capaces de obtener el vector que describe la normal del mismo. ¿De qué nos sirve esto? Bien.

Detección de tambor golpeado

Una vez se ha detectado un golpe, se compara la posición de la baqueta en el momento del impacto con la zona delimitada de cada uno de los tambores. Sin embargo, surge un problema: ¿Qué pasa si quiero tocar un tambor sobre una pared?

En este momento entra en juego la normal de cada tambor. En función de este vector, los tambores se almacenan en estructuras diferentes. Después, al detectar el golpeo, se extrae el sentido del golpeo de la diferencia de la posición y se busca únicamente en aquella estructura cuyo vector sea similar.

Se definen 3 estructuras diferentes: Down, Mid y Up. Down se correspondería con los bombos que tocamos normalmente en una batería, Mid se corresponde con el acto de golpear una pared y Up; con tocar mientras estamos tumbados.

Problemas – Deriva en el cálculo de la aceleración

Como ya se ha mencionado antes, el acelerómetro presenta un error natural a la hora de medir aceleración y giro. Estas imperfecciones hacen que reconstruir la posición de forma fiel y consistente sea una tarea imposible.

Al obtener los valores de la aceleración en el eje Z, comprobamos que este dato está cerca del valor real de la gravedad (9.8), sin embargo tiene imprecisiones de más de 2 décimas en algunos instantes.

Esto introduce un error demasiado elevado como para reconstruir la posición a partir de esta componente, ya que la deriva iría aumentando progresivamente hasta distorsionar por completo la posición de la baqueta y mostrar valores no representativos.

Solución 1: Cálculo del giro a partir de la tangente

Este método aprovecha la descomposición del valor de la gravedad para calcular los giros en X e Y en función de la magnitud de la componente de dicha aceleración.

Como ya he mencionado en el apartado anterior, la medición de la aceleración cuenta con un error mínimo, pero apreciable; lo cual resta valor a este método.

El argumento de peso para descartar este método es que calcula el giro a partir de la gravedad. Para ello, no debe haber presente ninguna otra aceleración en el sistema. El nuestro se mueve constantemente.

                Solución 2: Combinación del método de la tangente con la integración del giro

Nuevamente se utiliza el giro obtenido por el método de la tangente, pero en este caso se usa como apoyo. Este método consiste en integrar los valores de la velocidad angular obtenidos por el giroscopio y combinarlos linealmente con los datos obtenidos por el método de la tangente.

Matemáticamente, este método se describe como:

ángulo x = 0.98 * (angulo x + gx * dt) + 0.02 * accel_tan_x

ángulo y = 0.98 * (angulo y + gy * dt) + 0.02 * accel_tan_y

De esta manera, conseguimos suavizar la onda y conseguimos una señal más fiel al movimiento real.

El inconveniente de este método es que calculamos los giros X e Y a partir de la componente gravitacional y su descomposición, lo cual hace que sea imposible calcular la rotación en Z ya que en un inicio es paralela al suelo.

Este percance introduce una deriva que trastorna el dato hasta sacarlo fuera de rango.

Para solucionarlo, introducimos un valor corrector que hace que el giro en Z tienda siempre hacia el centro.

Diseño final

                Selección de tambor

Tras observar que la reconstrucción de la posición no era una medida ni fiable ni factible, se remodeló el sistema por completo.

En lugar de posicionar áreas en el espacio para emular la zona de los tambores, se define un rango angular para cada sonido. De esta manera, podemos realizar la selección del tambor mediante la medida del eje Z anteriormente citada.

                Calibración del entorno de batería

Al cambiar la forma en la que se representan las áreas de golpeo, la forma de definirlas también debe hacer lo mismo. Se remodela el sistema desde el principio.

En este caso, el sistema cuenta con un límite angula lateral en -90 grados, a partir del cual no se pueden definir áreas. Es a partir de este ángulo a partir del cual se definen la sucesión de fronteras que delimitan las áreas y, por tanto, los tambores.

Detección de golpeo

Paralelamente, el golpeo se medirá con la aceleración en el eje Z. Esta decisión de diseño viene condicionada por el movimiento que los baterías acostumbran a realizar.

Los baterías no levantan la baqueta hasta que su punta se dirige al cielo para dar un golpe a un tambor, sino que realizan un golpe de muñeca. Esto se traduce en que el valor que mejor refleja los golpes es la aceleración y no la rotación, pero entonces tenemos un problema. ¿Toda aceleración en z es un golpe?

Si observamos cuidadosamente, cuando tocamos la batería, cogemos carrerilla antes de atizar un tambor. A partir de este comportamiento, deduje que el algoritmo debería constar de dos estados: cargado cuando preparamos el golpe y golpeado cuando descargamos el brazo.

En amarillo puede observarse la variación de la aceleración en el eje Z.

Problemas finales

A causa de un error desconocido, el sistema de calibración deja de funcionar al intentar definir una nueva área. Existe una alta probabilidad de que se trate de un error de tratamiento de punteros, ya que, al cambiar de diseño, hubo que remodelar todo el sistema.

Debido a este error, no se puede calibrar correctamente los tambores y, por tanto, no se pueden crear múltiples áreas de golpeo de forma manual.

Objetivos cumplidos

En comparación con los objetivos iniciales, considero el trabajo como satisfactorio, sobre todo después de haber conseguido llegar a una solución tras tantas trabas.

El prototipo final es capaz de detectar golpeo con sensibilidad y reproducir sonidos perfectamente. Considero que ambos apartados están desarrollados con solidez y que están a la altura de los objetivos iniciales que me marqué.

Sin embargo, el pequeño fallo en la calibración del entorno hace que este apartado no sea funcional, lo cual hace invisible un apartado que considero suficientemente robusto.

Mejoras a implementar

Como primera mejora, se tratará de solucionar el error en el método de calibración para desbloquear dicha funcionalidad. Además, se podría incluir una pequeña mejora que consista en fijar la rotación en Z a la media del ángulo de la zona golpeada. Esto haría que la corrección que hace que el giro en Z tienda a 0 sea menos importante.

Por otro lado, la implementación en el DAW haría que el proyecto se tornase mucho más atractivo, ya que podríamos reproducir sonidos de batería controlados por el dispositivo con paso de sensibilidad. Además, añadir una segunda baqueta daría mucho más realismo y libertad al usuario.

La implementación de un diseño ergonómico y comunicación por bluetooth queda pendiente como mejor a largo plazo, ya que se tratan de mejoras de comodidad.

Por último, realizar una adaptación de la aplicación para móviles daría el toque final al proyecto.

Presentación

Código

#include "DrumSet.h"
//#include "Drum.h"
//#include "Vector3.h"
#include "DrumStick.h"
#include "Button.h"
#include "AudioManager.h"
#include "Sound.h"
#include "Tempo.h"

//Global objects
  //Managers
  AudioManager audioManager;  //The speaker must be connected to the 9th pin.
  Tempo tempoManager;

  //Hardware
  DrumStick drumStick;
  Button buttons[3] = { //Button. Underneath: each button's pin.
    4,
    5,
    6
  };  

  //Middleware
  DrumSet drumSet;

//Global variables
  int mode = 1;
  Hit hit;

void setup() {
  //Init objects
  drumStick.init(&buttons[0], &buttons[1], &buttons[2]);
  Serial.begin(9600);
  tempoManager.init(audioManager);

  //Mostrar menú
  Serial.println("Menú:\n\t- Mode 1: Play mode.\n\t- Mode 2: Set and calibrate DrumSet.\n\t- Mode 3: Calibrate tempo.\nYou can switch between modes with the 1st button.\nTo continue, press the 1st button.");

  while(!buttons[0].readValue()){
  
  }

  Serial.print("Mode: ");
  Serial.println(mode);
}

void loop() {
  //Actualizar angulo de la baqueta
  drumStick.updateAngle(); 
    
  //Actualizar valores de los botones
  if(buttons[0].readValue()){
    mode=(mode+1)%3;
    Serial.print("Mode: ");
    Serial.println(mode);

    if(mode==0){
      Serial.println("Press the 2nd button to enable tempoFunctions or press the 1st one to change between modes.");
    }
  }

  //Dependiendo del modo... (Botón 1)
  switch(mode){
    
    //0 - Play
    case 0:
      //Reproducir tempo
      if(buttons[1].readValue()){
        tempoManager.switchActivate();
      }
      
      if(tempoManager.isActivated()){
        tempoManager.playTempo();
      }

      //Detectar golpe
      //Llamada a función --> Input: Drumstick pos. Output: Hit.
      if(drumStick.checkHit(&hit)){
        //Debug
        audioManager.playNiapa(&hit);
        
        //Identificar area golpeado
        drumStick.setHitArea(&hit);
        if(drumSet.findDrumArea(&hit)){
          
          //Reproducir sonido
          Serial.print("DrumArea ");
          Serial.print(hit.getArea());
          Serial.println(" hit.");
          audioManager.playSound(drumSet.getDrumArea(hit.getArea())->getSound());
          
        }else{
          //(else ---> mensaje por consola "MISSED")
          Serial.println("Find drum area: MISSED.");
        }
      }
      break;

    //1 - Calibrate
    case 1:
      //Crea un set desde 0
      drumSet.restart();
      
      Serial.println("Press the 2nd button to add a new DrumArea or press the 1st one to change between modes.\n Mode: ");
      Serial.println(mode);
      while(mode == 1){
        //Capturar movimiento de la baqueta
        drumStick.updateAngle();
        
        //Capturar cambio de modo
        if(buttons[0].readValue()){
          mode=(mode+1)%3;
        }

        //Añadir bombo (Botón 2)
        if(buttons[1].readValue()){
          //Calibrar posición
          Serial.println("Crear DrumArea.");
          DrumArea* d = drumSet.createDrumArea(drumStick);
                    
          //Asignar sonido (Botón 3) (mientras que no se pulse ningún otro botón)
          Serial.println("Options:");
          Serial.println("  - Button 1: Change mode.");
          Serial.println("  - Button 2: Insert new Drum.");
          Serial.println("  - Button 3: Change Drum's sound.");
          
          while(mode==1 && buttons[1].readValue()==false){
            if(buttons[0].readValue()){
              mode=(mode+1)%3;
            }
            
            if(buttons[2].readValue()){
              Serial.println("Sound changed.");
              d->changeSound();
              audioManager.playSound(d->getSound());
            }
          }
        }
      }
      break;

    //2 - Set tempo
    case 2:
      //Primera marca de timepo
      int counter = 0;
      
      Serial.println("Press the 2nd button to start the tempo calibration or press the 1st one to change between modes.");
      while(buttons[1].readValue() && mode==2){
        if(buttons[0].readValue()){
          mode=(mode+1)%3;
        }
      }

      tempoManager.setTimeStamp(counter);
      
      while(mode == 2){
        counter++;
        
        //Capturar cambio de modo
        if(buttons[0].readValue()){
          mode=(mode+1)%3;
        }

        //Nueva marca de tiempo (Botón 2)
        if(buttons[1].readValue()){
          tempoManager.setTimeStamp(counter);
          counter=0;
          Serial.print("Tempo: ");
          Serial.println(tempoManager.getCalibrationCounter());
        }
      }

      tempoManager.updateTempo();
      break;
   
    default:
      Serial.println("Mode: Default.");
      break;
  }
}
#ifndef __Acelerometer__
#define __Acelerometer__

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

class Acelerometer{
  private:
    //Dirección de acceso a memoria del MPU
    #define MPU 0X68
    
    //Ratios de conversión
    #define A_R 16384.0   //aceleración a g/s^2
    #define G_R 131.0     //rotación a grados/s
    
    //Conversión de radianes a grados
    #define RAD_A_DEG = 57.295779
    
    //MPU-6050 tiene registros de 8 bits, pero los datos son de 16 bits (MEMORIA)
      //Valores RAW
      //int16_t reg_acc [3];  //AcX, AcY, AcZ
    
      //Detección de golpeo
      bool charged=false;
      //float acelZ;            //Se recoje primero el RAW, despues se convierte a SI. Local.
      const float threshHoldCharge = -2.9;
      const float threshHoldSound = 0.9;

      long previous_time_hit;
      float dt_hit;
    
      long previous_time_loop_checkHit;
      float dt_loop_checkHit;
    
      float posZ;
      float velZ;
      //float zHitLength;
      //float velocity;
      //float mappedVelocity; //Local

      //Deteccion area
      //float giroZ;  //Local
      //float giroZ_SI; //Se calcula sobre giroZ
      float angleZ;

      long previous_time_loop_area;
      float dt_loop_area;
    
      //Debug
      //String valores;

      //Functions
      float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
        return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
      }

  public:
    Acelerometer(){
      
    }
  
    void init(){
      Wire.begin();
      Wire.beginTransmission(MPU);
      Wire.write(0x6B);             //Power management register ()
      Wire.write(0);                //Poner valores a 0 reinicia el mpu
      Wire.endTransmission(true);
    }
    
    bool checkHit(Hit* hit){
      bool isHit=false;
      float acelZ;
      float mappedVelocity;
        
        //Aceleración (solo z)
      Wire.beginTransmission(MPU);
      Wire.write(0x3F);               //Dirección a partir de la cual empezamos a leer.
      Wire.endTransmission(false);
      Wire.requestFrom(MPU,2,true);   //Le pide 6 registros al MPU a partir de la dirección especificada arriba.
      
      acelZ=Wire.read()<<8|Wire.read();    //Pide 2 registros y los junta cada vez. El primero es high y el segundo low.
      
      //Detección de golpeo (en función de aceleración en z)
      acelZ = acelZ/A_R -1;

      //Serial.println("IS CHARGED?");
      
      if(acelZ<=threshHoldCharge && !charged){
        Serial.println("Charged.");
        charged = true;
        previous_time_hit = micros();
        velZ=0;
        posZ=0;
      }
    
      if(acelZ >= threshHoldSound && charged){
        Serial.println("Hit.");
        charged=false;
        dt_hit=(micros()-previous_time_hit)*1e-6;   //El último valor depende de tu delay al final. (x ms * 100.0) (En este caso son 10ms).
        //zHitLength = -posZ;
        //velocity = -posZ/dt_hit;
        mappedVelocity = mapFloat(-posZ/dt_hit, 0.6, 1.2, 20.0, 127.0);
        mappedVelocity = constrain(mappedVelocity, 0.0, 127.0);
    
        //Serial.print("Z: "); Serial.println(zHitLength);  //m
        //Serial.print("dt: "); Serial.println(dt_hit);   // s
        //Serial.print("Velocity: "); Serial.println(velocity);  // m/s
        Serial.print("Midi velocity: "); Serial.println(mappedVelocity);

        hit->setVelocity(mappedVelocity);
        isHit=true;
      }

      //Calculate vel and pos
      dt_loop_checkHit= micros()-previous_time_loop_checkHit;
      previous_time_loop_checkHit = micros();
      
      velZ= velZ + acelZ * 9.8 * dt_loop_checkHit * 1e-6; //m/s
      posZ= posZ + velZ * dt_loop_checkHit * 1e-6;  //m

      return isHit;
    }

    void setHitArea(Hit* hit){
      //Clasificar area en hit
      //Serial.print("Venga porfa: "); Serial.println(angleZ);
      hit->setAngle(angleZ);
    }

    void updateAngle(){
      float giroZ;
      
        //Read data - Giro (solo z)
      Wire.beginTransmission(MPU);
      Wire.write(0x47);               //Dirección a partir de la cual empezamos a leer.
      Wire.endTransmission(false);
      Wire.requestFrom(MPU,2,true);   //Le pide 6 registros al MPU a partir de la dirección especificada arriba.

      giroZ = Wire.read()<<8|Wire.read();
      giroZ = giroZ/G_R;
      //Serial.println(giroZ_SI);

        //Obtain elapsed time
      dt_loop_area= (millis()-previous_time_loop_area)*1e-3;
      previous_time_loop_area = millis();

        //Integrate angleZ
      angleZ= angleZ + giroZ*dt_loop_area - angleZ*0.005; 
      //Serial.println(angleZ);
        //Sacar por pantalla
      //valores = "90, " + String(angleZ) + ", -90";
      //Serial.println(valores);
    }

    float getAngle(){
      return angleZ;
    }
  };

#endif
#ifndef __AudioManager__
#define __AudioManager__

#include "Sound.h"

// Frecuencia en hercios de las notas musicales
#define DO3   261.626 //DO3
//#define REb3  277.183
#define RE3   293.665
//#define MIb3  311.127
#define MI3   293.628
#define FA3   349.228
//#define SOLb3  369.994
#define SOL3  391.995
//#define LAb3 415.305
#define LA3   440.000
//#define SIb3  466.164
#define SI3   493.883
#define DO4   523.251

class AudioManager{
  private:
    int pin;
    //double notas[13]={DO3, REb3, RE3, MIb3, MI3, FA3, SOLb3, SOL3, LAb3, LA3, SIb3, SI3, DO4};
    //double notas[8]={DO3, RE3, MI3, FA3, SOL3, LA3, SI3, DO4};
    double notas[3]={DO3, SOL3, SI3};
    //double tempoSound = DO3;
    /*TMRpcm Audio;*/
    
  
  public:    
    AudioManager(){
      pin = 9;
      /*Audio.speakerPin(9);
      Audio.quality(1);
      Audio.setVolume(5);*/
    }

    void playSound(Sound s){
      //Serial.print("Audio: "); Serial.println(s.getAudioIndex());
      tone(pin, notas[s.getAudioIndex()], 100);
    }

    void playTempo(){
      tone(pin, DO3, 100);
    }

    void playNiapa(Hit* hit){
      tone(pin, notas[5], 20);
    }
};

#endif
#ifndef __Button__
#define __Button__

class Button{
  private: 
    int pin;
    bool value = false;

  public:
    Button(int inputPin){
      pin = inputPin;
      pinMode(pin, INPUT);
    }
    
    bool readValue(){
      if(digitalRead(pin)==HIGH){
          Serial.println("Please, release the button.");
          while(digitalRead(pin)==HIGH){    //Evita falsas lecturas (pulsar el botón solo se realiza en un ciclo de arduino)  //MEMORIA
          }
          value = true;
        }else{
          value = false;
        }
      return value;
    }
    
    /*bool readValue(){
      return false;
    }*/
};


#endif
#ifndef __DrumArea__
#define __DrumArea__

#include "DrumStick.h"
#include "Sound.h"
#include "Hit.h"


class DrumArea{
  private:
    float iniAngle;   //Max 90  (izquierda)
    float endAngle;   //Max -90 (derecha)
    Sound sound;

  public:
    DrumArea(){
  }

  void init(DrumStick drumStick, float previousAngle){
    
    setIniAngle(previousAngle);
    Serial.println("New Drum Created."); Serial.println(previousAngle);Serial.println(iniAngle);
    
    calibrateZone(drumStick);
  }

  void initZero(){
    Serial.println("First Drum Created.");
    iniAngle=90;
    endAngle=90;
  }

  private:
    
    void calibrateZone(DrumStick drumStick){
      do{
        Serial.print("Posiciona la baqueta en la orientación deseada y pulsa el botón 2.");

        //Mientras que el botón esté en false, nada. Cuando se accione:
        while(!drumStick.getButton(1)->readValue()){
          drumStick.updateAngle();       //La posición de la baqueta se debe actualizar aunque la ejecución esté en este bucle. HACER PRUEBAS
          Serial.println("Vamos.");
        }
        
       //Guardar posición de la baqueta en pos. 
        endAngle = drumStick.getAngle();
        
        if(endAngle>iniAngle){
          Serial.println("Error. You have set a negative area.");
          showAngles();
        }  
      }while(endAngle>iniAngle);
      Serial.println("Drum calibrated: ");
      showAngles();
      
    }

  public:
    //Setters
    void setIniAngle(float newIniAngle){
      iniAngle= newIniAngle;
    }
    
    void changeSound(){
      sound.changeSound();
    }

    //Getters
    float getEndAngle(){
      return endAngle;
    }

    float getMidPoint(){
      return (endAngle+iniAngle)/2;
    }
    
    Sound getSound(){
      return sound;
    }
    
    //Methods
    bool checkArea(Hit* hit){
      showAngles();
      return (hit->getAngle() < iniAngle
           && hit->getAngle() > endAngle);
    }

    void showAngles(){
       Serial.print("Ini angle: "); Serial.println(iniAngle);
       Serial.print("End angle: "); Serial.println(endAngle);
    }
};

#endif
#ifndef __DrumSet__
#define __DrumSet__

#include "DrumArea.h"

class DrumSet{
  private:
    DrumArea drumArea[4];
    int nDrumAreas=1;

  public:
    static const int arrayLength=4;
  
    DrumSet(){
    }

    void restart(){
      nDrumAreas=1;
      drumArea[0]= *new DrumArea();
      drumArea[0].initZero();
      drumArea[0].showAngles();
    }

    //Getters
    DrumArea* getDrumArea(int index){
      return &drumArea[index];
    }
    
    //Methods
    DrumArea* createDrumArea(DrumStick drumStick){
      if(nDrumAreas == arrayLength-1){
        Serial.println("Error. Max DrumAreas already created.");
        exit;
      }
      nDrumAreas=nDrumAreas+1;
      drumArea[nDrumAreas]= *new DrumArea();
      drumArea[nDrumAreas].init(drumStick, drumArea[nDrumAreas-1].getEndAngle());

      return &drumArea[nDrumAreas];
    }

    bool findDrumArea(Hit* hit){
      bool found = false;
      int i=0;
      Serial.println(nDrumAreas);

      while(!found && i!=nDrumAreas){
        if(drumArea[i].checkArea(hit)){
          found = true;
        }else{
          i++;
        }
      }

      if(!found){
        Serial.print("Hit Angle: "); Serial.println(hit->getAngle());
      }

      hit->setArea(i);
      return found;
    }
};

#endif
#ifndef __DrumStick__
#define __DrumStick__

#include "Acelerometer.h"
#include "Hit.h"
#include "Button.h"

class DrumStick{
  private:
    Acelerometer accel;
    Button* buttons [3];

  public: 
    Drumstick(){
    }

    void init(Button* one, Button* two, Button* three){
      accel.init();
      buttons[0]=one;
      buttons[1]=two;
      buttons[2]=three;
    }

    bool checkHit(Hit* hit){
      return accel.checkHit(hit);
    }

    float getAngle(){
      return accel.getAngle();
    }

    void updateAngle(){
      accel.updateAngle();
    }

    Button* getButton(int index){
      return buttons[index];
    }

    bool setHitArea(Hit* hit){
      accel.setHitArea(hit);
    }
};

#endif
#ifndef __Hit__
#define __Hit__

//#define MAX_TIME 500
      
class Hit{
  private:
    float velocity;
    float angle;
    int area = 0;
  
  public:
    Hit(){}

  //Setters
    void setVelocity(float newVelocity){
      velocity = newVelocity;
    }

    void setAngle(float angleZ){
      angle = angleZ;
    }

    void setArea(int index){
      area = index;
    }
    
  //Getters
    float getAngle(){
      return angle;
    }
    
    float getVelocity(){
      return velocity;
    }

    int getArea(){
      return area; 
    }
};

#endif
#ifndef __Sound__
#define __Sound__

class Sound{
  private:
    int audioIndex = 0;

  public:
    Sound(){
      int audioIndex=0;
    }

    Sound(int index){
      audioIndex=index;
    }

    void changeSound(){
      audioIndex=(audioIndex+1)%3;
    }
    
    int getAudioIndex(){
      return audioIndex;
    }
};

#endif
#ifndef __Tempo__
#define __Tempo__

#include "AudioManager.h"
#include "Sound.h"

class Tempo{
  int tempoValue = 100;   //Cuanto equivale a 100 BPM (¿?)
  bool tempoActivated = false;

  int calibrationCounter = 0;
  int nTimeStamps = 0;
  
  int playCounter = 0;

  AudioManager audioManager;
  
  public:
    Tempo(){
      tempoActivated = false;
      tempoValue = 100;
    }

    void init(AudioManager newAudioManager){
      this->audioManager=newAudioManager;
    }

//Getters
    bool isActivated(){
      return tempoActivated;
    }

    int getCalibrationCounter(){
      return calibrationCounter;
    }
    
//Setters
    void setTimeStamp(int counter){
      nTimeStamps++;
      calibrationCounter = (calibrationCounter*(nTimeStamps-1) + counter)/nTimeStamps;
    }

    void resetTimeStamp(){
      calibrationCounter=0;
      nTimeStamps = 0;
    }

    void updateTempo(){
      tempoValue = calibrationCounter;
    }
    
//Functions
    void activateTempo(){
      tempoActivated = true;
      playCounter = 0;
    }

    void deactivateTempo(){
      tempoActivated = false;
    } 

    void switchActivate(){
      if(isActivated()){
        deactivateTempo();
      }else{
        activateTempo();
      }
    }

    void playTempo(){
      playCounter++;
      if(playCounter >= calibrationCounter){
        playCounter=0;
        audioManager.playTempo();
      }
    }
};

#endif

También te podría gustar...

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *