Sunday, February 24, 2013

Wii Nunchuck and Ableton Live Scene Control

I've been playing around with some cheap knock-off Wii Nunchuck controllers I got from eBay. I wrote some code that allows the Nunchuck to be used for scene navigation and launch in Ableton Live.

Scene up/down is controlled by the two buttons, and to launch the currently selected scene, shake the controller.


The joystick is not used at this time. This will eventually be part of a more complicated controller, probably using two Nunchucks and a breath controller.

There's nothing too fancy going on here. Each button press sends a different MIDI note on/off pair on MIDI channel two, and a sudden change in the accelerometer readings sends another MIDI note on/off pair. Ableton's MIDI map mode makes it very easy to assign those messages to the scene controls.

Here's the code (requires a PJRC Teensy Microcontroller).

#include "Wire.h"
#include "WiiChuck.h"

// The MIDI channel we use for Ableton Live control
#define SCENE_MGMT_MIDI_CHANNEL 2
// The MIDI note for Live scene launch
#define SCENE_LAUNCH_MIDI_NOTE 0
// The MIDI note for the Live previous scene action
#define SCENE_PREV_MIDI_NOTE 1
// The MIDI note for the Live next scene action
#define SCENE_NEXT_MIDI_NOTE 2
// Suppress multiple scene lanches if < 100 ms apart
#define SCENE_LAUNCH_DELAY 100

// We need a change in Z-axis acceleration larger
// than this to launch the selected scene. You should
// adjust this value to fine-tune how hard you have
// to jerk the nunchuck to launch a scene.
#define Z_AXIS_ACCEL_THRESHOLD 500

WiiChuck chuck = WiiChuck(); // The nunchuck controller

int xVal, yVal, zVal, zSum, zAvg;
int zValues[10] = {0};
byte note;
boolean cButton, zButton;
int i;
unsigned long sceneChangeTime;

void setup() {
  // Initialize the nunchuck-related things
  cButton = zButton = false;
  chuck.begin();
}


void loop() {
  // Process nunchuck data
  chuck.update(); 
  delay(1);
  
  // Deal with accelerometer. Compute the average accelerometer
  // value over the last 10 samples. I've unfortunately forgotten
  // what internet reference lead me to using smoothed values
  // for this - apologies.
  zVal = chuck.readAccelZ();
  zSum -= zValues[i];
  zSum += zVal;
  zValues[i] = zVal;
  i = (i + 1) % 10;
  zAvg = zSum / 10;
  
  // If the average value is above some configured threshold, treat
  // the gesture as a scene launch
  if (zAvg > Z_AXIS_ACCEL_THRESHOLD) {
    if (millis() - sceneChangeTime > SCENE_LAUNCH_DELAY) {
      usbMIDI.sendNoteOn(SCENE_LAUNCH_MIDI_NOTE, 100, SCENE_MGMT_MIDI_CHANNEL);
      usbMIDI.sendNoteOff(SCENE_LAUNCH_MIDI_NOTE, 100, SCENE_MGMT_MIDI_CHANNEL);
      sceneChangeTime = millis();
    }
  } 
  
  if (chuck.cPressed()) {
    if (!cButton) {
      // Rising edge - launch current scene
      usbMIDI.sendNoteOn(SCENE_PREV_MIDI_NOTE, 100, SCENE_MGMT_MIDI_CHANNEL);
      usbMIDI.sendNoteOff(SCENE_PREV_MIDI_NOTE, 100, SCENE_MGMT_MIDI_CHANNEL);
      cButton = true;
    }
  } else {
    cButton = false;
  }
  if (chuck.zPressed()) {
    if (!zButton) {
      // Rising edge - select the next scene
      usbMIDI.sendNoteOn(SCENE_NEXT_MIDI_NOTE, 100, SCENE_MGMT_MIDI_CHANNEL);
      usbMIDI.sendNoteOff(SCENE_NEXT_MIDI_NOTE, 100, SCENE_MGMT_MIDI_CHANNEL);
      zButton = true;
    }
  } else {
    zButton = false;
  }
}


To set up Ableton Live, you need to enter MIDI mode (Command-M on a Mac), select the control to configure, then either press one of the buttons on the nunchuck or shake it, depending on which scene control you are configuring).

Depending on which Wii Nunchuck you have (knockoff or genuine Nintendo), you may need a different initialization sequence. Here's the WiiChuck.h I cobbled together from various internet sources (but most of the work comes from Tim Hirzel and Tod E. Kurt of ThingM):


/*
 * Nunchuck -- Use a Wii Nunchuck
 * Tim Hirzel http://www.growdown.com
 * 
 notes on Wii Nunchuck Behavior.
 This library provides an improved derivation of rotation angles from the nunchuck accelerometer data.
 The biggest different over existing libraries (that I know of ) is the full 360 degrees of Roll data
 from teh combination of the x and z axis accelerometer data using the math library atan2. 

 It is accurate with 360 degrees of roll (rotation around axis coming out of the c button, the front of the wii),
 and about 180 degrees of pitch (rotation about the axis coming out of the side of the wii).  (read more below)

 In terms of mapping the wii position to angles, its important to note that while the Nunchuck
 sense Pitch, and Roll, it does not sense Yaw, or the compass direction.  This creates an important
 disparity where the nunchuck only works within one hemisphere.  At a result, when the pitch values are 
 less than about 10, and greater than about 170, the Roll data gets very unstable.  essentially, the roll
 data flips over 180 degrees very quickly.   To understand this property better, rotate the wii around the
 axis of the joystick.  You see the sensor data stays constant (with noise).  Because of this, it cant know
 the difference between arriving upside via 180 degree Roll, or 180 degree pitch.  It just assumes its always
 180 roll.


 * 
 * This file is an adaptation of the code by these authors:
 * Tod E. Kurt, http://todbot.com/blog/
 *
 * The Wii Nunchuck reading code is taken from Windmeadow Labs
 * http://www.windmeadow.com/node/42
 * 
 * Conversion to Arduino 1.0 by Danjovic
 * http://hotbit.blogspot.com 
 * 
 */

#ifndef WiiChuck_h
#define WiiChuck_h


#include "Arduino.h"
#include <Wire.h>
#include <math.h>


// these may need to be adjusted for each nunchuck for calibration
#define ZEROX 510  
#define ZEROY 490
#define ZEROZ 460
#define RADIUS 210  // probably pretty universal

#define DEFAULT_ZERO_JOY_X 124
#define DEFAULT_ZERO_JOY_Y 132



class WiiChuck {
    private:
        uint8_t cnt;
        uint8_t status[6];  // array to store wiichuck output
        uint8_t averageCounter;
        //int accelArray[3][AVERAGE_N];  // X,Y,Z
        int i;
        int total;
        uint8_t zeroJoyX;   // these are about where mine are
        uint8_t zeroJoyY; // use calibrateJoy when the stick is at zero to correct
        int lastJoyX;
        int lastJoyY;
        int angles[3];

        bool lastZ, lastC;


    public:

        uint8_t joyX;
        uint8_t joyY;
        bool buttonZ;
        bool buttonC;
        void begin() 
        {
            Wire.begin();
            cnt = 0;
            averageCounter = 0;
            // instead of the common 0x40 -> 0x00 initialization, we
            // use 0xF0 -> 0x55 followed by 0xFB -> 0x00.
            // this lets us use 3rd party nunchucks (like cheap $4 ebay ones)
            // while still letting us use official oness.
            // only side effect is that we no longer need to decode bytes in _nunchuk_decode_byte
            // see http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1264805255
            //  
            Wire.beginTransmission(0x52); // device address
            Wire.write(0xF0);
            Wire.write(0x55);
            Wire.endTransmission();
  
            delay(1);
            Wire.beginTransmission(0x52);
            Wire.write(0xFB);
            Wire.write((uint8_t)0x00);
            

            Wire.endTransmission();
            update();            
            for (i = 0; i<3;i++) {
                angles[i] = 0;
            }
            zeroJoyX = DEFAULT_ZERO_JOY_X;
            zeroJoyY = DEFAULT_ZERO_JOY_Y;
        }


        void calibrateJoy() {
            zeroJoyX = joyX;
            zeroJoyY = joyY;
        }

        void update() {

            Wire.requestFrom (0x52, 6); // request data from nunchuck
            while (Wire.available ()) {
                // receive byte as an integer
                status[cnt] = _nunchuk_decode_byte (Wire.read()); //
                cnt++;
            }
            if (cnt > 5) {
                lastZ = buttonZ;
                lastC = buttonC;
                lastJoyX = readJoyX();
                lastJoyY = readJoyY();
                //averageCounter ++;
                //if (averageCounter >= AVERAGE_N)
                //    averageCounter = 0;

                cnt = 0;
                joyX = (status[0]);
                joyY = (status[1]);
                for (i = 0; i < 3; i++) 
                    //accelArray[i][averageCounter] = ((int)status[i+2] << 2) + ((status[5] & (B00000011 << ((i+1)*2) ) >> ((i+1)*2))); 
                    angles[i] = (status[i+2] << 2) + ((status[5] & (B00000011 << ((i+1)*2) ) >> ((i+1)*2))); 

                //accelYArray[averageCounter] = ((int)status[3] << 2) + ((status[5] & B00110000) >> 4); 
                //accelZArray[averageCounter] = ((int)status[4] << 2) + ((status[5] & B11000000) >> 6); 

                buttonZ = !( status[5] & B00000001);
                buttonC = !((status[5] & B00000010) >> 1);
                _send_zero(); // send the request for next bytes

            }
        }


    // UNCOMMENT FOR DEBUGGING
    //byte * getStatus() {
    //    return status;
    //}

    float readAccelX() {
       // total = 0; // accelArray[xyz][averageCounter] * FAST_WEIGHT;
        return (float)angles[0] - ZEROX;
    }
    float readAccelY() {
        // total = 0; // accelArray[xyz][averageCounter] * FAST_WEIGHT;
        return (float)angles[1] - ZEROY;
    }
    float readAccelZ() {
        // total = 0; // accelArray[xyz][averageCounter] * FAST_WEIGHT;
        return (float)angles[2] - ZEROZ;
    }

    bool zPressed() {
        return (buttonZ && ! lastZ);
    }
    bool cPressed() {
        return (buttonC && ! lastC);
    }

    // for using the joystick like a directional button
    bool rightJoy(int thresh=60) {
        return (readJoyX() > thresh and lastJoyX <= thresh);
    }

    // for using the joystick like a directional button
    bool leftJoy(int thresh=60) {
        return (readJoyX() < -thresh and lastJoyX >= -thresh);
    }


    int readJoyX() {
        return (int) joyX - zeroJoyX;
    }

    int readJoyY() {
        return (int)joyY - zeroJoyY;
    }


    // R, the radius, generally hovers around 210 (at least it does with mine)
   // int R() {
   //     return sqrt(readAccelX() * readAccelX() +readAccelY() * readAccelY() + readAccelZ() * readAccelZ());  
   // }


    // returns roll degrees
    int readRoll() {
        return (int)(atan2(readAccelX(),readAccelZ())/ M_PI * 180.0);
    }

    // returns pitch in degrees
    int readPitch() {        
        return (int) (acos(readAccelY()/RADIUS)/ M_PI * 180.0);  // optionally swap 'RADIUS' for 'R()'
    }

    private:
        uint8_t _nunchuk_decode_byte (uint8_t x)
        {
            //decode is only necessary with certain initializations 
            //x = (x ^ 0x17) + 0x17;
            return x;
        }

        void _send_zero()
        {
            Wire.beginTransmission (0x52); // transmit to device 0x52
            Wire.write ((uint8_t)0x00);  // sends one byte
            Wire.endTransmission (); // stop transmitting
        }

};


#endif