Free shipping within Canada on orders over $150

Maker Festival Projects: Giant NeoPixel VU Meter

We had a fair number of blinky projects at Toronto Maker Festival. While the Flying Toasters and the Clap-o-meter were popular,  they didn't have the sheer sale of this, our four metre double VU meter:

This is a volume-activated full strip of 240 NeoPixels bopping along to some fine Montreal power-pop from Simon I. Just as well no-one else was about at Elmwood Towers that time of night …

Although this is a big, power hungry and not exactly cheap project, the wiring couldn't be simpler:

The project is based on Adafruit's LED Ampli-Tie, except with waaay more LEDs. The main components are:

You'll also need to find a way to power the Arduino. I hacked apart an old USB B printer cable and connected its red and black wires across the terminals of the power supply. This allowed me to avoid a second, higher-voltage power supply to tun the Arduino, but still gave a little protection to the Arduino by passing the 5 V power through the board's USB protection fuses.

The code is a trivial modification of the Ampli-Tie to use more pixels, and to start the loudness meter from the centre of the strip:

 

/*
   neo_noise - scruss - 2017-07 for elmwood.to
   centred vu meter of 240 neopixel strip
   based on ampli-tie:

  LED VU meter for Arduino and Adafruit NeoPixel LEDs. More info: http://learn.adafruit.com/led-ampli-tie/

  Hardware requirements:
  - Most Arduino or Arduino-compatible boards (ATmega 328P or better).
  - Adafruit Electret Microphone Amplifier (ID: 1063)
  - Adafruit Flora RGB Smart Pixels (ID: 1260)
  OR
  - Adafruit NeoPixel Digital LED strip (ID: 1138)
  - Optional: battery for portable use (else power through USB or adapter)
  Software requirements:
  - Adafruit NeoPixel library

  Connections:
  - 3.3V to mic amp +
  - GND to mic amp -
  - Analog pin to microphone output (configurable below)
  - Digital pin to LED data input (configurable below)
  See notes in setup() regarding 5V vs. 3.3V boards - there may be an
  extra connection to make and one line of code to enable or disable.

  Written by Adafruit Industries.  Distributed under the BSD license.
  This paragraph must be included in any redistribution.

  fscale function:
  Floating Point Autoscale Function V0.1
  Written by Paul Badger 2007
  Modified from code by Greg Shakar

*/

#include <Adafruit_NeoPixel.h>
#include <math.h>

#define N_PIXELS  240  // Number of pixels in strand
#define MIC_PIN   A1  // Microphone is attached to this analog pin
#define LED_PIN    6  // NeoPixel LED strand is connected to this pin
#define SAMPLE_WINDOW   10  // Sample window for average level
#define PEAK_HANG 24 //Time of pause before peak dot falls
#define PEAK_FALL 4 //Rate of falling peak dot
#define INPUT_FLOOR 10 //Lower range of analogRead input
#define INPUT_CEILING 300 //Max range of analogRead input, the lower the value the more sensitive (1023 = max)

byte peak = 16;      // Peak level of column; used for falling dots
unsigned int sample;

byte dotCount = 0;  //Frame counter for peak dot
byte dotHangCount = 0; //Frame counter for holding peak dot

Adafruit_NeoPixel strip = Adafruit_NeoPixel(N_PIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);

void setup()
{
  // This is only needed on 5V Arduinos (Uno, Leonardo, etc.).
  // Connect 3.3V to mic AND TO AREF ON ARDUINO and enable this
  // line.  Audio samples are 'cleaner' at 3.3V.
  // COMMENT OUT THIS LINE FOR 3.3V ARDUINOS (FLORA, ETC.):
  //analogReference(EXTERNAL);

  // Serial.begin(9600);
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'

}

// FIXME:
// * left end works fine, right end doesn't blank


void loop()
{
  unsigned long startMillis = millis(); // Start of sample window
  float peakToPeak = 0;   // peak-to-peak level

  unsigned int signalMax = 0;
  unsigned int signalMin = 1023;
  unsigned int c, y;


  // collect data for length of sample window (in mS)
  while (millis() - startMillis < SAMPLE_WINDOW)
  {
    sample = analogRead(MIC_PIN);
    if (sample < 1024)  // toss out spurious readings
    {
      if (sample > signalMax)
      {
        signalMax = sample;  // save just the max levels
      }
      else if (sample < signalMin)
      {
        signalMin = sample;  // save just the min levels
      }
    }
  }
  peakToPeak = signalMax - signalMin;  // max - min = peak-peak amplitude

  // Serial.println(peakToPeak);

  /*
    //Fill the strip with rainbow gradient
    for (int i = 0; i <= strip.numPixels() - 1; i++) {
      strip.setPixelColor(i, Wheel(map(i, 0, strip.numPixels() - 1, 30, 150)));
    }
  */

  //Fill the strip with rainbow gradient
  for (int i = 0; i <= strip.numPixels() / 2; i++) {
    // left - works
    strip.setPixelColor(strip.numPixels() / 2 - i, Wheel(map(i, 0, strip.numPixels() / 2, 30, 150)));
    // right - maybe fixme
    strip.setPixelColor(i + strip.numPixels() / 2, Wheel(map(i, 0, strip.numPixels() / 2, 30, 150)));
  }

  //Scale the input logarithmically instead of linearly
  c = fscale(INPUT_FLOOR, INPUT_CEILING, strip.numPixels() / 2, 0, peakToPeak, 2);

  if (c < peak) {
    peak = c;        // Keep dot on top
    dotHangCount = 0;    // make the dot hang before falling
  }
  if (c <= strip.numPixels() / 2) { // Fill partial column with off pixels
    // left - okay
    drawLine(0, c, strip.Color(0, 0, 0));
    // right - maybe fixed?
    drawLine(strip.numPixels() - c, strip.numPixels(), strip.Color(0, 0, 0));
  }

  // Set the peak dot to match the rainbow gradient
  y = strip.numPixels() / 2 - peak;

  // left - okay
  strip.setPixelColor(strip.numPixels() / 2 - (y - 1), Wheel(map(y, 0, strip.numPixels() / 2, 30, 150)));
  // right - maybe fixme
  strip.setPixelColor(strip.numPixels() / 2 + y - 1, Wheel(map(y, 0, strip.numPixels() / 2, 30, 150)));

  strip.show();

  // Frame based peak dot animation
  if (dotHangCount > PEAK_HANG) { //Peak pause length
    if (++dotCount >= PEAK_FALL) { //Fall rate
      peak++;
      dotCount = 0;
    }
  }
  else {
    dotHangCount++;
  }
}

//Used to draw a line between two points of a given color
void drawLine(uint8_t from, uint8_t to, uint32_t c) {
  uint8_t fromTemp;
  if (from > to) {
    fromTemp = from;
    from = to;
    to = fromTemp;
  }
  for (int i = from; i <= to; i++) {
    strip.setPixelColor(i, c);
  }
}


float fscale( float originalMin, float originalMax, float newBegin, float
              newEnd, float inputValue, float curve) {

  float OriginalRange = 0;
  float NewRange = 0;
  float zeroRefCurVal = 0;
  float normalizedCurVal = 0;
  float rangedValue = 0;
  boolean invFlag = 0;


  // condition curve parameter
  // limit range

  if (curve > 10) curve = 10;
  if (curve < -10) curve = -10;

  curve = (curve * -.1) ; // - invert and scale - this seems more intuitive - postive numbers give more weight to high end on output
  curve = pow(10, curve); // convert linear scale into lograthimic exponent for other pow function

  /*
    Serial.println(curve * 100, DEC);   // multply by 100 to preserve resolution
    Serial.println();
  */

  // Check for out of range inputValues
  if (inputValue < originalMin) {
    inputValue = originalMin;
  }
  if (inputValue > originalMax) {
    inputValue = originalMax;
  }

  // Zero Refference the values
  OriginalRange = originalMax - originalMin;

  if (newEnd > newBegin) {
    NewRange = newEnd - newBegin;
  }
  else
  {
    NewRange = newBegin - newEnd;
    invFlag = 1;
  }

  zeroRefCurVal = inputValue - originalMin;
  normalizedCurVal  =  zeroRefCurVal / OriginalRange;   // normalize to 0 - 1 float

  // Check for originalMin > originalMax  - the math for all other cases i.e. negative numbers seems to work out fine
  if (originalMin > originalMax ) {
    return 0;
  }

  if (invFlag == 0) {
    rangedValue =  (pow(normalizedCurVal, curve) * NewRange) + newBegin;

  }
  else     // invert the ranges
  {
    rangedValue =  newBegin - (pow(normalizedCurVal, curve) * NewRange);
  }

  return rangedValue;
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  if (WheelPos < 85) {
    return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  }
  else if (WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  else {
    WheelPos -= 170;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
}

 

There's a slight bug in the code that the two meters cross when it's very, very quiet, but most people won't ever see that in a normally-noisy gathering.


Stewart Russell
Stewart Russell

Author

mostly harmless



Leave a comment

Comments will be approved before showing up.