Neltner Labs

Fun Projects and Art

myki Prototype with Direct Audio Analysis

The myki prototype light doing direct audio reading with a relatively simple audio analysis program (meaning no frequency analysis). Algorithm is that the hue is constantly shifting, and when a sudden increase in the amplitude envelope is detected it triggers as a “beat” and sets the intensity to something related to the overall volume and then fades back down.

The idea here is that a soft beat flashes a dim light while a loud beat flashes a bright light. It works very nicely here; in reality my camera didn’t quite have the frame rate required to pick up some of the more subtle beats in the song. But I consider it a victory when the actual device outperforms my video camera!

This is using only a 20dB gain on the preamp, so it is not quite as sensitive as the final version of the myki will be (40-60dB with autocompression to prevent clipping). Code below the break.

This is the loop that actually does audio analysis. It automatically detects the DC offset using a lowpass filter, does enveloping to convert the AC signal around the DC offset into an RMS signal, implements autogain to scale appropriately regardless of input signal strength, and then does beat detection using a simple comparison of the current derivative against a lowpassed beat detection threshold.

void loop() {
  int audiovalue = analogRead(audioPin);
  DC_offset = DC_offset * (1 - 0.0001) + (float)audiovalue * 0.0001;
  float unscaledsignal = (audiovalue - (int)DC_offset);
  if(abs(unscaledsignal) > max_unscaledsignal) {
    max_unscaledsignal = max_unscaledsignal * 0.95 + abs(unscaledsignal) * 0.05;
    if (unscaledsignal < 0) unscaledsignal = -max_unscaledsignal;
    else unscaledsignal = max_unscaledsignal;
  }
  else if (abs(unscaledsignal) < noise_threshold) unscaledsignal = 0;
  max_unscaledsignal = max_unscaledsignal * (1 - 0.001) + min_threshold * 0.001;
  int signal = unscaledsignal/(max_unscaledsignal-noise_threshold) * 255;
  d_envelope = d_envelope * (1-0.1) + (abs(signal) - envelope) * 0.1;
  d_envelope_threshold = d_envelope_threshold * (1 - 0.001) + d_envelope * 0.001;
  envelope = envelope * (1-0.1) + abs(signal) * 0.1;
  
  if (d_envelope > (d_envelope_threshold+10)) {
    color.i = envelope/255;
    color.state = 1;
  }
  if (color.state == 1) {
    color.i = color.i-0.05;
    if (color.i < 1/255) {
      color.i = 0;
      color.state = 0;
    }
  }
  color.h = color.h + 0.1;
  sendcolor();
}

The complete arduino code set for the myki prototype is below. It requires removing the MSGEQ7 chip and jumpering the audio preamp directly to the analog input of the ATmega32U4.

#include "math.h"
#define DEG_TO_RAD(X) (M_PI*(X)/180)

#define redPin 9    // Red LED connected to digital pin 9
#define greenPin 10 // Green LED connected to digital pin 10
#define bluePin 11 // Blue LED connected to digital pin 11
#define whitePin 13 // White LED connected to digital pin 13
#define audioPin 0 // Analog Microphone Input
#define min_threshold 5 // Minimum value for unscaledsignal.

struct HSI {
  float h;
  float s;
  float i;
  float htarget;
  float starget;
  byte state;
} color;

float DC_offset = 518; // Initialize DC Offset to 1.5VDC
float max_unscaledsignal = min_threshold; // Initialize autoscale setting.
float envelope = 0;
float noise_threshold = 3; //Initialize noise threshold.
float d_envelope = 0; // Initialize envelope derivative.
float d_envelope_threshold = 100; // Starting threshold.

void setup() {
  pinMode(whitePin, OUTPUT);
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
  TCCR0B = _BV(CS00);
  TCCR1B = _BV(CS10);
  TCCR3B = _BV(CS30);
  TCCR4B = _BV(CS40);
  digitalWrite(whitePin, 0);
  digitalWrite(redPin, 0);
  digitalWrite(greenPin, 0);
  digitalWrite(bluePin, 0);
  
  color.h = 0;
  color.s = 1;
  color.i = 0;
  
  // Setup Serial Communication
  Serial.begin(57600);
  Serial.println("Booting up Audio Analysis -- Saikoduino v9");
  
  // Configure Audio Input
  pinMode(audioPin, INPUT);
  analogReference(DEFAULT);
}
  
void loop() {
  int audiovalue = analogRead(audioPin);
  DC_offset = DC_offset * (1 - 0.0001) + (float)audiovalue * 0.0001;
  float unscaledsignal = (audiovalue - (int)DC_offset);
  if(abs(unscaledsignal) > max_unscaledsignal) {
    max_unscaledsignal = max_unscaledsignal * 0.95 + abs(unscaledsignal) * 0.05;
    if (unscaledsignal < 0) unscaledsignal = -max_unscaledsignal;
    else unscaledsignal = max_unscaledsignal;
  }
  else if (abs(unscaledsignal) < noise_threshold) unscaledsignal = 0;
  max_unscaledsignal = max_unscaledsignal * (1 - 0.001) + min_threshold * 0.001;
  int signal = unscaledsignal/(max_unscaledsignal-noise_threshold) * 255;
  d_envelope = d_envelope * (1-0.1) + (abs(signal) - envelope) * 0.1;
  d_envelope_threshold = d_envelope_threshold * (1 - 0.001) + d_envelope * 0.001;
  envelope = envelope * (1-0.1) + abs(signal) * 0.1;
  
  if (d_envelope > (d_envelope_threshold+10)) {
    color.i = envelope/255;
    color.state = 1;
  }
  if (color.state == 1) {
    color.i = color.i-0.05;
    if (color.i < 1/255) {
      color.i = 0;
      color.state = 0;
    }
  }
  color.h = color.h + 0.1;
  sendcolor();
}

void sendcolor() {
  int rgbw[4];
  while (color.h >=360) color.h = color.h - 360;
  while (color.h < 0) color.h = color.h + 360;
  if (color.i > 1) color.i = 1;
  if (color.i < 0) color.i = 0;
  if (color.s > 1) color.s = 1;
  if (color.s < 0) color.s = 0;
  // Fix ranges (somewhat redundantly).
  hsi2rgbw(color.h, color.s, color.i, rgbw);
  analogWrite(redPin, rgbw[0]);
  analogWrite(greenPin, rgbw[1]);
  analogWrite(bluePin, rgbw[2]);
  analogWrite(whitePin, rgbw[3]);
}

void hsi2rgbw(float H, float S, float I, int* rgbw) {
  int r, g, b, w;
  float cos_h, cos_1047_h;
  H = fmod(H,360); // cycle H around to 0-360 degrees
  H = 3.14159*H/(float)180; // Convert to radians.
  S = S>0?(S<1?S:1):0; // clamp S and I to interval [0,1]
  I = I>0?(I<1?I:1):0;
  
  // This section is modified by the addition of white so that it assumes 
  // fully saturated colors, and then scales with white to lower saturation.
  //
  // Next, scale appropriately the pure color by mixing with the white channel.
  // Saturation is defined as "the ratio of colorfulness to brightness" so we will
  // do this by a simple ratio wherein the color values are scaled down by (1-S)
  // while the white LED is placed at S.
  
  // This will maintain constant brightness because in HSI, R+B+G = I. Thus, 
  // S*(R+B+G) = S*I. If we add to this (1-S)*I, where I is the total intensity,
  // the sum intensity stays constant while the ratio of colorfulness to brightness
  // goes down by S linearly relative to total Intensity, which is constant.

  if(H < 2.09439) {
    cos_h = cos(H);
    cos_1047_h = cos(1.047196667-H);
    r = S*255*I/3*(1+cos_h/cos_1047_h);
    g = S*255*I/3*(1+(1-cos_h/cos_1047_h));
    b = 0;
    w = 255*(1-S)*I;
  } else if(H < 4.188787) {
    H = H - 2.09439;
    cos_h = cos(H);
    cos_1047_h = cos(1.047196667-H);
    g = S*255*I/3*(1+cos_h/cos_1047_h);
    b = S*255*I/3*(1+(1-cos_h/cos_1047_h));
    r = 0;
    w = 255*(1-S)*I;
  } else {
    H = H - 4.188787;
    cos_h = cos(H);
    cos_1047_h = cos(1.047196667-H);
    b = S*255*I/3*(1+cos_h/cos_1047_h);
    r = S*255*I/3*(1+(1-cos_h/cos_1047_h));
    g = 0;
    w = 255*(1-S)*I;
  }
  
  rgbw[0]=r;
  rgbw[1]=g;
  rgbw[2]=b;
  rgbw[3]=w;
}