Midi Controller Via Xbee S2

Introduction

MIDI is a standard protocol that in this project is going to be used for controlling a set of music parameters in a musical software application, e.g., using Pure Data or Ableton LIVE.

In this project the students will learn how to create a wireless MIDI controller using a microcontroller like arduino, and a communication device based on XBee S2.

Materials

The basic necessary hardware for Option 1

  • microcontroller arduino FIO, or arduino UNO.
  • communicator via Bluetooth,
  • communicator via ZigBee, XBee S2
  • Xbee explorer, that sends serial messages via USB port to the computer.
  • accelerometer, ADXL of two axis (X and Y).
  • piezoelectric sensor, as the used in Knock example.
  • switches and push buttons for detecting when the dancer pushes the sensor.

This is the basic necessary sotfware:

  • arduino IDE, for programming arduino FIO.
  • XCtung, for configurating Xbee S2 modules (Windows and MAC only; LINIX would have to use Wine).
  • Hairless Serial to MIDI converter, for connecting serial MIDI messages to MIDI inside the computer.

How To

The process has three main steps: hardware configuration, software programing and control configuration.

Hardware Configuration

Getting used with Arduino and Sensors.

As a beginning, you should learn how to connect different sensor to any arduino controller and how to read the values of each sensor. We are going to work with these sensors:

The student must test all of these sensors and print in the Serial Console the range of each sensor (maximum and minimum values).

The case of Arduino FIO with an accelerometer

Arduino FIO allows to connect the XBee module directly as it comes with a Xbee slot.

For powering the microcontroller an external battery has to be used. Be careful of the input voltage of the battery. A standard voltage is around 3.3V. The accelerometer could be powered with such voltage, even though its nominal voltage is 5V. The only limitation would be in the accuracy of the sensor.

Notice also that there is a battery holes that are parallel to the power slot of the board. Powering has to be connected in such holes.

The accelerometer has 5 pins:

  • Vcc has to be connected to 3.3V output from the arduinoFIO board.
  • GND has to be connected to GND in the arduinoFIO board.
  • X, Y and (if 3-axis acc) Z, would be connected to Analogous inputs: A1, A2 and A3.

X, Y and, if the accelerometer is kind of 3-axis, Z give a signal whose voltage is inside the range [GND+3%, Vcc-3%] and it is analogous to the movement in the respective axis.

Software Programming. Sensor reading and MIDI writing

Software has two different sections: sensor reading and MIDI messages delivery.

Input: Sensor Reading

Sensors reading would use the function analogRead. This function gives digital values between 0 and 1023 that are analog to voltage inputs between [0, VREF]. Therefore readings for X and Y axis would be as follows:

//  readSensors:
long int accX = 0;
long int accY = 0;
accX = analogRead( X_ACC_PIN ); // 0 - 1023
accY = analogRead( Y_ACC_PIN ); // 0 - 1023

However, the readings are not calibrated and each sensor can give different values for the same angles. Also readings have to be adapted to the range of the output of the signal. Therefore first of all, it is necessary to measure the range the accelerometer is giving. This type of debugging is made using serial writings:

Serial.print( "X = " );
Serial.print( accX );
Serial.print( "   Y = " );
Serial.println( accY );

When executed, in a serial monitor it is possible to see the values of the accelerometer readings. Move the accelerometer along its angle range and take note for the minimum and maximum values the accelerometer gives. These values would be used to "remap" the readings from one range to the output range. For instance if the minimum (when axis is vertical looking down) is 453 and the maximum (when axis is vertical looking up) is 574 the input range is [453, 574].

This is the whole code for calibrate the accelerometer sensor:

#define TAU 250 // tiempo de espera en el bucle de control

int xa = 0, ya = 0;
int xa_max = -1024, ya_max = -1024;
int xa_min = 1024, ya_min = 1024;

const int xpin = A1;   // Cada coordenada va referida a un pin de la placa
const int ypin = A0;

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

void loop() {
  read_acc();  
  normalize();

 print_sensor_values();

 delay( TAU );
}

void normalize() {
  xa = map( xa, xa_min, xa_max, 0, 127 );
  ya = map( ya, ya_min, ya_max, 0, 127 );
}

void calibrate() {
  Serial.println( "Callibration..." );
  Serial.println( "xa, xa_min, xa_max, ya, ya_min, ya_max" );

  for( int i = 0; i < 150; i++ ) {
    read_acc_mean_value( 10 );
    if( xa_max < xa ) xa_max = xa;
    if( ya_max < ya ) ya_max = ya;

    if( xa_min > xa ) xa_min = xa;
    if( ya_min > ya ) ya_min = ya;

    print_sensor_limits_values();

    delay(100);    
  }

  Serial.println( "Calibration OK!" );

  Serial.print( xa_min ); Serial.print( "\t" ); 
  Serial.print( xa_max ); Serial.print( "\t" ); 
  Serial.print( ya_min ); Serial.print( "\t" ); 
  Serial.print( ya_max ); Serial.print( "\t" ); 
}

void print_sensor_values() {
  Serial.print( xa );  Serial.print( "\t" );
  Serial.print( ya );  Serial.print( "\t" );
  Serial.println();
}

void print_sensor_limits_values() {
  Serial.print( xa );  Serial.print( "\t" );
  Serial.print( xa_min );  Serial.print( "\t" );
  Serial.print( xa_max );  Serial.print( "\t" );
  Serial.print( ya );  Serial.print( "\t" );
  Serial.print( ya_min );  Serial.print( "\t" );
  Serial.print( ya_max );  Serial.print( "\t" );
  Serial.println();
}

void read_acc_mean_value( int sample_size ) {
  long xa_mean = 0, ya_mean = 0;
  read_acc();
  delay(1);
  for (int i = 0; i < sample_size; i++) {
    xa_mean += xa;
    ya_mean += ya;
    read_acc();
  }

  xa = xa_mean/sample_size;
  ya = ya_mean/sample_size;
}

void read_acc() {
  //Acelerometro 2Axis
  xa = analogRead( xpin );
  ya = analogRead( ypin );

}

Output: MIDI writing

There are many MIDI possible messages the controller can send. It depends on the purpose of the program what message to send. MIDI can send a noteOn, noteOff, a Control Change for a continuous controller (CC) or a Program Change (PC), a BPM clock setting, and so.

This simple example shows how to do a CC on channel 1 using the values of the accelerometers. Following the MIDI protocol specification CC values are in the range [0, 127], therefore it is necessary to adapt the measured values from the accelerometers to such values. This is made using the map function as follows:

accX = map(accX, 453, 574, 0, 127);  // re-scale accelerometer 453-574 to 0-127

MIDI messages are just written through the serial port as bytes packages. The MIDI CC message is implemented in this function:

void controlChange( int controller, int value ) {
  Serial.write( 0xB0 ); // channel 1
  Serial.write( controller );
  Serial.write( value );
}

Communication via Zigbee Series 2

Introduction to Zigbee Modules

The purpose of the controller is to be wireless. As explained in this guide by sparkfun shop, Xbee modules are mainly of two types: S1 and S2. Xbee S1 modules, or just Xbee, are the oldest ones and are quite easy to configure. In the other side, they do not allow to configure complex sensor nets as it does Xbee S2 modules.

Xbee S2 configuration

Xbee S2 are more complex in the configuration, and it is less easy to find examples of using them with arduino boards, but they allow complex net configurations and are cheaper than Xbee (S1). In the official ardunio website it is possible to find instructions on how to configure Xbee S2 using X-CTU in windows for a Xbee on an arduino UNO.

There is also another clear example here

In the Digi website there are basic examples for getting started.

Xbee S2 are designed to form networks with star, cluster tree or a mesh topologies. It depend how you configure each Xbee S2. The roles of a XBee are: coordinator, router or endpoint. In all types of nets it has to be one and only one coordinator.

Radios which share the same PAN ID can communicate each other.

In our project, there are two Xbee S2 modules: one in the arduino FIO, and another in a Xbee-explorer board that is connected to the computer.

Each Xbee has to be configured separately using XCTung software. The net we want to configure has only two nodes: a Controller and a Router.

Reading Sensors and Writing MIDI. Complete Code.

/**
 * main_control_vX incluye un acelerometro ADXL345, que se deja en la siguiente versión por dar muchos problemas de comunicación y robustez.
 * La aplicación incluye:
 * - lectura de los sensores: IR_SHARP, 2 ADXL3xx, y 4 Switches
 * - calibración de los acelerometros
 * - escritura MIDI ad-hoc para configurar Ableton LIVE
 * - escritura MIDI en el control 
 * 
 * TODO: mejorar la calibración. Incluir estructuras complejas de control entre los sensores y mensajes MIDI
 */

#define NOTE_ON 0x90
#define NOTE_OFF 0x80
#define AFTER_POLY 0xA0
#define CONTROL_CHANGE 0xB0
#define PROGRAM_CHANGE 0xC0
#define CLOCK 0xF8
#define START 0xFA
#define CONTINUE 0xFB
#define STOP 0xFC

#define ACC_DER_THRES 30
#define TIME_FOR_PLAY 3000

#define TAU 20 // tiempo de bulce en ms

//Acelerometro2Axis
const int xapin = A1;   
const int yapin = A0;
const int xbpin = A4;  
const int ybpin = A3;

int xa = 0, ya = 0, xa_old = 0, ya_old = 0, Dxa = 0, Dya = 0; // acc values and its derivates
int xb = 0, yb = 0, xb_old = 0, yb_old = 0, Dxb = 0, Dyb = 0;  // acc values and its derivates

int xa_min = 288, ya_min = 294;
int xa_max = 383, ya_max = 395;
int xb_min = 293, yb_min = 300;
int xb_max = 443, yb_max = 398;

//IR
int IR_min = 60, IR_max = 400;
const int IR_PIN = A2;
int IR = 0;

//Switches
#define RESET_PIN 3
int S1 = 0, S2 = 0, S3 = 0, S4 = 0;
int S1_up = 0, S1_down = 0, S1_old = 0;

void setup() {

  Serial.begin( 115200 ); // MIDI communication

  //Switches
  //configure pin2 as an input and enable the internal pull-up resistor
  pinMode( 4, INPUT_PULLUP );
  pinMode( 5, INPUT_PULLUP );
  pinMode( 6, INPUT_PULLUP );
  pinMode( 7, INPUT_PULLUP );
  pinMode( RESET_PIN, OUTPUT );
  noteOn( 45, 127, 1 ); // Para iniciar los sonidos
//  ableton_config();
//  callibrate();
}

unsigned long timer = 0;
bool flag_playing = 0;
///////////////////////////////////////////////////////////////////////////////////
void loop() {
  read_acc_a();
  read_acc_b();
  IR = analogRead( IR_PIN );
  read_switches();

  normalize();
  derivate();

//  print_sensor_values();

//  if( S1_up ) controlChange( 14, 0, 1 );  
//  else if( S1_down ) controlChange( 14, 127, 1 );
  if( S1 ) controlChange( 14, 0, 1 );  
  else controlChange( 14, 127, 1 );

  int f = xa, q = xa;
  controlChange( 12, f, 1 ); // Acc21 Frequency
  controlChange( 12, q, 1 );  // Acc21 Resonator
  controlChange( 13, IR, 1 );  // Acc21 IRI

  if( ( Dxa > ACC_DER_THRES ) | ( Dya > ACC_DER_THRES )   ) {
    controlChange( 15, 127, 1 ); // Sube volumen sonido Acc31
    timer = millis();
    flag_playing = 1;
  }
  if( flag_playing & ( millis() - timer ) > TIME_FOR_PLAY ) {
      controlChange(15, 0, 1 ); // Baja volumen sonido Acc31
      flag_playing = 0;
  }

  delay( TAU );
}
///////////////////////////////////////////////////////////////////////////////////

void test_cc() {
  for( int i = 0; i < 127; i++ ) {
    controlChange( 0, i, 1 );  
    delay( 200 );
  }
}

void ableton_config(){
  delay( 3000 );
  noteOn( 69, 127, 1 );
  delay( 3000 );
  controlChange( 0, 0, 1 );
  delay( 3000 );
  controlChange( 10, 0, 1 );
  delay( 3000 );

}

void noteOn( int pitch, int velocity, int channel ) {
//  Serial.write( NOTE_ON + channel );
  Serial.write( NOTE_ON );
  Serial.write( pitch );
  Serial.write( velocity );
}

void noteOff( int pitch, int channel ) {
  Serial.write( NOTE_OFF );
  Serial.write( pitch );
  Serial.write( 0 );
}

void controlChange( int controllerID, int value, int channel) {
  Serial.write( CONTROL_CHANGE );
  Serial.write( controllerID );
  Serial.write( value );
}

void read_switches() {
  //Switches
  //read the pushbutton value into a variable
  S1_old = S1;
  S1 = digitalRead( 4 );
  if( S1 & !S1_old ) { S1_up = 1; S1_down = 0;}
  else if( !S1 & S1_old ) { S1_up = 0; S1_down = 1;}
  else{ S1_up = 0; S1_down = 0;} 

  S2 = digitalRead( 5 );
  S3 = digitalRead( 6 );
  S4 = digitalRead( 7 );

  reset_switches();
}

void normalize() {
  xa = map( xa, xa_min, xa_max, 0, 127 );
  ya = map( ya, ya_min, ya_max, 0, 127 );

  xb = map( xb, xb_min, xb_max, 0, 127 );
  yb = map( yb, yb_min, yb_max, 0, 127 );

  IR = map( IR, IR_min, IR_max, 0, 127 );

  xa = constrain( xa, 0, 127 );
  ya = constrain( ya, 0, 127 );
  xb = constrain( xb, 0, 127 );
  yb = constrain( yb, 0, 127 );

  //IR = constrain( IR, 0, 127 );
  if( IR < 0 ) IR = 0;
  if( IR > 127 ) IR = 127;

}

void callibrate() {
  xa_max = -1024; xb_max = -1024;
  ya_max = -1024; yb_max = -1024;
  xa_min = 1024;  xb_min = 1024;
  ya_min = 1024;  yb_min = 1024;

  for ( int i = 0; i < 1500; i++ ) {
    read_acc_mean_value( 10 );
    if ( xa_max < xa ) xa_max = xa;
    if ( ya_max < ya ) ya_max = ya;

    if ( xa_min > xa ) xa_min = xa;
    if ( ya_min > ya ) ya_min = ya;

    if ( xb_max < xb ) xb_max = xb;
    if ( yb_max < yb ) yb_max = yb;

    if ( xb_min > xb ) xb_min = xb;
    if ( yb_min > yb ) yb_min = yb;

//    print_sensor_limits_values();

//    delay(1);
  }
}

void print_sensor_values() {
  Serial.print( xa );  Serial.print( "   " );
  Serial.print( ya );  Serial.print( "   " );
  Serial.print( Dxa ); Serial.print( "   " );
  Serial.print( Dya ); Serial.print( "   " );
  Serial.print( xb );  Serial.print( "\t" );
  Serial.print( yb );  Serial.print( "\t" );
  Serial.println();
}

void print_sensor_limits_values() {
  Serial.print( xa );  Serial.print( "\t" );
  Serial.print( xa_min );  Serial.print( "\t" );
  Serial.print( xa_max );  Serial.print( "\t" );
  Serial.print( ya );  Serial.print( "\t" );
  Serial.print( ya_min );  Serial.print( "\t" );
  Serial.print( ya_max );  Serial.print( "\t" );

  Serial.print( xb );  Serial.print( "\t" );
  Serial.print( xb_min );  Serial.print( "\t" );
  Serial.print( xb_max );  Serial.print( "\t" );
  Serial.print( yb );  Serial.print( "\t" );
  Serial.print( yb_min );  Serial.print( "\t" );
  Serial.print( yb_max );  Serial.print( "\t" );
  Serial.println();
}

void read_acc_mean_value( int sample_size ) {
  long xa_mean = 0, ya_mean = 0;
  long xb_mean = 0, yb_mean = 0;
  read_acc_a();
  read_acc_b();
  delay(1);
  for (int i = 0; i < sample_size; i++) {
    xa_mean += xa;
    ya_mean += ya;
    read_acc_a();
    xb_mean += xb;
    yb_mean += yb;
    read_acc_b();
  }

  xa = xa_mean / sample_size;
  ya = ya_mean / sample_size;
  xb = xb_mean / sample_size;
  yb = yb_mean / sample_size;
}

void derivate() {
  Dxa = abs( xa_old - xa );
  xa_old = xa;
}

void read_acc_a() {
  //Acelerometro 2Axis
  xa = analogRead( xapin );
  ya = analogRead( yapin );

}

void read_acc_b() {
  //Acelerometro 2Axis
  xb = analogRead( xbpin );
  yb = analogRead( ybpin );

}

void reset_switches() {
  if (S1 == HIGH) {
    digitalWrite( RESET_PIN, LOW);
  } else {
    digitalWrite( RESET_PIN, HIGH);
  }
  if (S2 == HIGH) {
    digitalWrite( RESET_PIN, LOW);
  } else {
    digitalWrite( RESET_PIN, HIGH);
  }
  if (S3 == HIGH) {
    digitalWrite( RESET_PIN, LOW);
  } else {
    digitalWrite( RESET_PIN, HIGH);
  }
  if (S4 == HIGH) {
    digitalWrite( RESET_PIN, LOW);
  } else {
    digitalWrite( RESET_PIN, HIGH);
  }
}

Control Configuration

There are two types of control appropriate for a MIDI controller. First is based on a Combinational System, where the outputs just depends from the inputs. Second is based on a Sequential System, where the outputs depends from the inputs and the state of the system.

The most simple control configuration is to do a Continuous Controller (CC) based on the values of the accelerometer. This case is very similar to any standard MIDI controller based on a keyboard with knobs. Here, the knobs are replaced by the accelerometer.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License