John Bradnam
Published © GPL3+

Sound Responsive LED Display

A 3D printed 64 LED display with 10 built in audio and sequenced patterns controlled by a ATtiny microprocessor.

IntermediateFull instructions provided12 hours524
Sound Responsive LED Display

Things used in this project

Hardware components

Microchip ATtiny1614
×1
Resistor 330 ohm 1206 SMD
×1
SparkFun MAX9814 AGC Microphone module
×1
Tactile Switch, Side Actuated
Tactile Switch, Side Actuated
Long shaft
×1
DC power socket
×1
8 Channel WS2812 5050 RGB Built-in Full Color LED Lights Development Board
×4

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
UPDI programmer
DIY programmer. See https://www.hackster.io/john-bradnam/create-your-own-updi-programmer-1e55f1

Story

Read more

Custom parts and enclosures

STL files

Files for your 3D printer

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

SoundResponsiveDisplayV4.ino

C/C++
/*-----------------------------------------------------------------------------
  Sound Responsive Display
  by John Bradnam (jbrad2089@gmail.com)
  
  ATTiny1614 Pins mapped to Ardunio Pins

             +--------+
         VCC + 1   14 + GND
 (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
       1 PA5 + 3   12 + PA2 9  (MISO)
 (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
       3 PA7 + 5   10 + PA0 11 (UPDI)
 (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
             +--------+

PA0 to PA7 can be analog or digital
PWM on D0, D1, D6, D7, D10

  BOARD: ATtiny1614/1604/814/804/414/404/214/204
  Chip: ATtiny1614
  Clock Speed: 16MHz
  Programmer: jtag2updi (megaTinyCore)

  Pin mapping: https://github.com/SpenceKonde/megaTinyCore/blob/master/megaavr/extras/ATtiny_x14.md

Credits:
  This code is a mixture from various sources as well as some of my own.
  ChrisParkerTech (https://www.instructables.com/id/Arduino-FFT-Visualizer-With-Addressable-LEDs/)
  teslahed (https://www.instructables.com/id/Arduino-Controlled-Sound-Responsive-LED-Display/)

-----------------------------------------------------------------------------*/

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define GAIN 2    //(PA6)
#define OUT 3     //PA7
#define MIC_IN 3  //Alias of OUT pin
#define AR 5      //PB2
#define SWITCH 6  //PB1
#define LED_PIN 7 //PB0

#include <arduinoFFT.h>
//#include <FastLED.h>

#define SAMPLES 64        // Must be a power of 2
#define BRIGHTNESS  150   // LED information 
double vReal[SAMPLES];
double vImag[SAMPLES];

#define X_RES 4            // Total number of  columns in the display
#define Y_RES 8            // Total number of  rows in the display
  
int Intensity[X_RES] = { }; // initialize Frequency Intensity to zero
int Displacement = 1;

#define NUMBER_PIXEL 32
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMBER_PIXEL, LED_PIN, NEO_GRB + NEO_KHZ800);

arduinoFFT FFT = arduinoFFT();  // Create FFT object

enum MODES { FALLING_DOT, VISUALISER, TRAFFIC, PAINTBALL, PULSE, WIPE_COLOR, CHASE_COLOR, RAINBOW_PLAIN, RAINBOW_CYCLE, RAINBOW_CHASE, START_OVER };
MODES mode = FALLING_DOT;

enum PALETTE { RAINBOW, SUNSET, OCEAN, PINA_COLADA, SULFUR, NO_GREEN, RESET_PALETTE };

byte
  peak      = 0,      // Used for falling dot
  dotCount  = 0,      // Frame counter for delaying dot-falling speed
  volCount  = 0;      // Frame counter for storing past volume data
int
  vol[SAMPLES],       // Collection of prior volume samples
  lvl       = 0,      // Current "dampened" audio level
  minLvlAvg = 0,      // For dynamic adjustment of graph low & high
  maxLvlAvg = 512;

#define LED_HALF  NUMBER_PIXEL/2
#define KNOB_VALUE 1023.0   //Sets maximum brightness
uint16_t gradient = 0; //Used to iterate and loop through each color palette gradually

//IMPORTANT:
//  This array holds the "threshold" of each color function (i.e. the largest number they take before repeating).
//  The values are in the same order as in ColorPalette()'s switch case (Rainbow() is first, etc). This is simply to
//  keep "gradient" from overflowing, the color functions themselves can take any positive value. For example, the
//  largest value Rainbow() takes before looping is 1529, so "gradient" should reset after 1529, as listed.
//     Make sure you add/remove values accordingly if you add/remove a color function in the switch-case in ColorPalette().
uint16_t thresholds[] = {1529, 1019, 764, 764, 764, 1274};

PALETTE palette = RAINBOW;  //Holds the current color palette.
//PATTERN visual = PULSE; //Holds the current visual being displayed.
uint8_t volume = 0;   //Holds the volume level read from the sound detector.
uint8_t last = 0;     //Holds the value of volume from the previous loop() pass.
uint8_t x = 0;
float maxVol = 5;    //Holds the largest volume recorded thus far to proportionally adjust the visual's responsiveness.
float avgBump = 0;    //Holds the "average" volume-change to trigger a "bump."
float avgVol = 0;     //Holds the "average" volume-level to proportionally adjust the visual experience.
float shuffleTime = 0;  //Holds how many seconds of runtime ago the last shuffle was (if shuffle mode is on).
bool shuffle = true;  //Toggles shuffle mode.
bool bump = false;     //Used to pass if there was a "bump" in volume
const int sampleWindow = 50; // Sample window width in mS (50 mS = 20Hz)
unsigned int sample;

//For Traffic() visual
int8_t pos[NUMBER_PIXEL] = { -2};    //Stores a population of color "dots" to iterate across the LED strip.
uint8_t rgb[NUMBER_PIXEL][3] = {0};  //Stores each dot's specific RGB values.

//For Snake() visual
bool left = false;  //Determines the direction of iteration. Recycled in PaletteDance()
int8_t dotPos = 0;  //Holds which LED in the strip the dot is positioned at. Recycled in most other visuals.
float timeBump = 0; //Holds the time (in runtime seconds) the last "bump" occurred.
float avgTime = 0;  //Holds the "average" amount of time between each "bump" (used for pacing the dot's movement).

//For fallingDot()
uint8_t dotOffset = 0;
uint32_t dotTimeout = 0;
#define DOT_TIMEOUT 5000;

uint16_t debounceTimeout = 0;
bool buttonPressed = false;

// Switch interrupt
void buttonInterrupt()
{
  if (digitalRead(SWITCH) == LOW)
  {
    debounceTimeout = millis() + 10;
  }
  else if (debounceTimeout > 0 && millis() > debounceTimeout)
  {
    debounceTimeout = 0;
    buttonPressed = true;
  }
}

void setup() 
{
  //Tri-Level Amplifier Gain Control.
  // GAIN = VDD, gain set to 40dB.
  //    pinMode(GAIN, OUTPUT);
  //    digitalWrite(GAIN, HIGH);
  // GAIN = GND, gain set to 50dB.
  //    pinMode(GAIN, OUTPUT);
  //    digitalWrite(GAIN, LOW);
  // GAIN = Unconnected, uncompressed gain set to 60dB.
        pinMode(GAIN, INPUT);

  //Tri-Level Attack and Release Ratio Select. Controls the ratio of attack time to release time for the AGC circuit.
  // A/R = GND: Attack/Release Ratio is 1:500
  //    pinMode(AR, OUTPUT);
  //    digitalWrite(AR, LOW);
  // A/R = VDD: Attack/Release Ratio is 1:2000
      pinMode(AR, OUTPUT);
      digitalWrite(AR, HIGH);
  // A/R = Unconnected: Attack/Release Ratio is 1:4000  
  //    pinMode(AR, INPUT);

  //Amplifier Output
  pinMode(MIC_IN, INPUT);

  //SWITCH - Active LOW
  pinMode(SWITCH, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SWITCH),buttonInterrupt,CHANGE);

  //WS2812B Strip
  pinMode(LED_PIN, OUTPUT);
  
	delay(3000);                  // power-up safety delay
  strip.begin();//initialises neopixels
  strip.setBrightness(BRIGHTNESS);// set brightness from 0 to max is 255
  strip.show();//clears any previous data in the strip
}

void loop() 
{
  bool firstTime = buttonPressed;
  if (buttonPressed)
  {
    buttonPressed = false;
    mode = (MODES)((int)mode + 1);
  }
  switch (mode)
  {
    case FALLING_DOT: fallingDot(firstTime); break;
    case VISUALISER: visualiser(); break;
    case TRAFFIC: traffic(firstTime); break;
    case PAINTBALL: paintball(firstTime); break;
    case PULSE: palettePulse(firstTime); break;
    case WIPE_COLOR: colorChange(50); break;
    case CHASE_COLOR: colorChase(50); break;
    case RAINBOW_PLAIN: rainbowPlain(20); break;
    case RAINBOW_CYCLE: rainbowCycle(20); break;
    case RAINBOW_CHASE: theaterChaseRainbow(50); break;
    case START_OVER: mode = FALLING_DOT; break;
  }
}

//-------------------------------------------------------------------

void visualiser()
{
	//Collect Samples
	getSamples();
	//Update Display
	displayUpdate();
	//FastLED.show();
  strip.show();
}

void getSamples()
{
  #define NOISE_LEVEL 250  // Noise/hum/interference in mic signal
  int n;
  
	for(int i = 0; i < SAMPLES; i++)
	{
    n = analogRead(MIC_IN);             // Raw reading from mic
    n = (n <= NOISE_LEVEL) ? 0 : (n - NOISE_LEVEL); // Remove noise/hum
		vReal[i] = constrain(n * 2, 0, 1023);
		vImag[i] = 0;
	}

	//FFT
	FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
	FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
	FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);

	//Update Intensity Array
	for(int i = 2; i < (X_RES*Displacement)+2; i+=Displacement)
	{
		vReal[i] = constrain(vReal[i],0 ,2047);            // set max value for input data
		vReal[i] = map(vReal[i], 0, 2047, 0, Y_RES);        // map data to fit our display

		Intensity[(i/Displacement)-2] --;                      // Decrease displayed value
		if (vReal[i] > Intensity[(i/Displacement)-2])          // Match displayed value to measured value
			Intensity[(i/Displacement)-2] = vReal[i];
	}
}

void displayUpdate()
{
	uint16_t color;
  int k, m;
	for(int i = 0; i < X_RES; i++)
	{
		for(int j = 0; j < Y_RES; j++)
		{
      k = (j%2 == 0) ? (X_RES*(j+1))-i-1 : (X_RES*j)+i;
      m = (k & 0x01) ? 31 - (k >> 1) : k >> 1;
      color = (k << 8) / 5;
      strip.setPixelColor(m, toHsv(color, 255, (j <= Intensity[i]) ? BRIGHTNESS : 0));
		}
	}
}

//Convert HSV value to RGB color
uint32_t toHsv(uint16_t h, uint8_t s, uint8_t v) 
{
  uint8_t r, g, b;

  uint8_t sextant = h >> 8;
  if(sextant > 5)
    sextant = 5;  // Limit hue sextants to defined space

  g = v;    // Top level

  // Perform actual calculations

  /*
   * Bottom level:
   * --> (v * (255 - s) + error_corr + 1) / 256
   */
  uint16_t ww;        // Intermediate result
  ww = v * (uint8_t)(~s);
  ww += 1;            // Error correction
  ww += ww >> 8;      // Error correction
  b = ww >> 8;

  uint8_t h_fraction = h & 0xff;  // Position within sextant
  uint32_t d;      // Intermediate result

  if(!(sextant & 1)) 
  {
    // r = ...slope_up...
    // --> r = (v * ((255 << 8) - s * (256 - h)) + error_corr1 + error_corr2) / 65536
    d = v * (uint32_t)(0xff00 - (uint16_t)(s * (256 - h_fraction)));
    d += d >> 8;  // Error correction
    d += v;       // Error correction
    r = d >> 16;
  } 
  else 
  {
    // r = ...slope_down...
    // --> r = (v * ((255 << 8) - s * h) + error_corr1 + error_corr2) / 65536
    d = v * (uint32_t)(0xff00 - (uint16_t)(s * h_fraction));
    d += d >> 8;  // Error correction
    d += v;       // Error correction
    r = d >> 16;
  }

  // Swap RGB values according to sextant. This is done in reverse order with
  // respect to the original because the swaps are done after the
  // assignments.
  if (!(sextant & 6)) 
  {
    if (!(sextant & 1)) 
    {
      uint8_t tmp = r;
      r = g;
      g = tmp;
    }
  } 
  else if (sextant & 1) 
  {
    uint8_t tmp = r;
    r = g;
    g = tmp;
  }
  if (sextant & 4) 
  {
    uint8_t tmp = g;
    g = b;
    b = tmp;
  }
  if(sextant & 2) 
  {
    uint8_t tmp = r;
    r = b;
    b = tmp;
  }

  // At this point, RGB values are assigned.
  uint8_t brightness = strip.getBrightness();
  if (brightness)
  {
    r = (r * brightness) >> 8;
    g = (g * brightness) >> 8;
    b = (b * brightness) >> 8;
  }
  return strip.Color(r, g, b);
}

//-----------------------------------------------------------------------------

void fallingDot(bool firstTime)
{
  #define DC_OFFSET 0  // DC offset in mic signal - if unusure, leave 0
  #define NOISE     345  // Noise/hum/interference in mic signal
  #define SAMPLES   40  // Length of buffer for dynamic level adjustment
  #define MAX_COUNT 16
  #define TOP       (MAX_COUNT + 2) // Allow dot to go slightly off scale
  #define PEAK_FALL 40  // Rate of peak falling dot

  if (firstTime)
  {
    dotOffset = 0;
    dotTimeout = millis() + DOT_TIMEOUT;
  }
  else if (millis() > dotTimeout)
  {
    dotOffset = (dotOffset + 32) & 255;
    dotTimeout = millis() + DOT_TIMEOUT;
  }
  uint8_t i;
  uint16_t minLvl, maxLvl;
  uint32_t c;
  int n, height;

  n = analogRead(MIC_IN);             // Raw reading from mic
  n = abs(n - 512 - DC_OFFSET);       // Center on zero
  n = (n <= NOISE) ? 0 : (n - NOISE); // Remove noise/hum
  lvl = ((lvl * 7) + n) >> 3;         // "Dampened" reading (else looks twitchy)

  // Calculate bar height based on dynamic min/max levels (fixed point):
  height = TOP * (lvl - minLvlAvg) / (long)(maxLvlAvg - minLvlAvg);

  if (height < 0L)
    height = 0;      // Clip output
  else if (height > TOP) 
    height = TOP;
  if(height > peak)
    peak = height; // Keep 'peak' dot at top


  // Color pixels based on rainbow gradient
  for (i=0; i<MAX_COUNT; i++) 
  {
    c = ((i >= height) ? 0 : Wheel((map(i,0,MAX_COUNT-1,30,150) + dotOffset) & 0xff));
    strip.setPixelColor(i, c);
    strip.setPixelColor(31 - i, c);
  }

  // Draw peak dot 
  if (peak > 0 && peak <= MAX_COUNT-1)
  {
    c = Wheel((map(peak,0,MAX_COUNT-1,30,150) + dotOffset) & 0xff);
    strip.setPixelColor(peak,c);
    strip.setPixelColor(31 - peak,c);
  }

  strip.show(); // Update strip

  // Every few frames, make the peak pixel drop by 1:
  if (++dotCount >= PEAK_FALL) 
  { //fall rate
    if(peak > 0) 
      peak--;
    dotCount = 0;
  }

  vol[volCount] = n; //Save sample for dynamic leveling
  if(++volCount >= SAMPLES) 
    volCount = 0; // Advance/rollover sample counter

  // Get volume range of prior frames
  minLvl = maxLvl = vol[0];
  for (i=1; i<SAMPLES; i++) 
  {
    if (vol[i] < minLvl)
      minLvl = vol[i];
    else if (vol[i] > maxLvl) 
      maxLvl = vol[i];
  }
  if ((maxLvl - minLvl) < TOP) 
    maxLvl = minLvl + TOP;
  minLvlAvg = (minLvlAvg * 63 + minLvl) >> 6; // Dampen min/max levels
  maxLvlAvg = (maxLvlAvg * 63 + maxLvl) >> 6; // (fake rolling average)
}

//-----------------------------------------------------------------------

void startPattern()
{
  //This is where the magic happens. This loop produces each frame of the visual.
  //open void loop
  //first run the sound sampling
  unsigned long startMillis = millis(); // Start of sample window
  unsigned int peakToPeak = 0;   // peak-to-peak level

  unsigned int signalMax = 0;
  unsigned int signalMin = 1024;

  // collect data for 50 mS
  while (millis() - startMillis < sampleWindow)
  {
    //open while loop
    sample = analogRead(MIC_IN);
    if (sample < 1024)  // toss out spurious readings
    {
      //open 1st if loop in while
      if (sample > signalMax)
      {
        //open 2nd if
        signalMax = sample;  // save just the max levels
      }//close 2nd if
      else if (sample < signalMin)
      {
        //open 3rd if
        signalMin = sample;  // save just the min levels
      }//close 3rd if
    }//close 1st if
  }//close while loop
  peakToPeak = signalMax - signalMin;  // max - min = peak-peak amplitude
  double volts = (peakToPeak * 3.3) / 1024;  // convert to volts
  int sound = (volts * 10);
  volume = map(sound, 1, 10, 0, 255);

  //Sets a threshold for volume.
  //  In practice I've found noise can get up to 15, so if it's lower, the visual thinks it's silent.
  //  Also if the volume is less than average volume / 2 (essentially an average with 0), it's considered silent.
  if (volume < avgVol / 2.0 || volume < 15) 
    volume = 0;
  else 
    avgVol = (avgVol + volume) / 2.0; //If non-zeo, take an "average" of volumes.

  //If the current volume is larger than the loudest value recorded, overwrite
  if (volume > maxVol) 
    maxVol = volume;

  cyclePalette();  //Changes palette for shuffle mode or button press.

  //This is where "gradient" is modulated to prevent overflow.
  if (gradient > thresholds[palette]) 
  {
    gradient %= thresholds[palette] + 1;

    //Everytime a palette gets completed is a good time to readjust "maxVol," just in case
    //  the song gets quieter; we also don't want to lose brightness intensity permanently
    //  because of one stray loud sound.
    maxVol = (maxVol + volume) / 2.0;
  }

  //If there is a decent change in volume since the last pass, average it into "avgBump"
  if (volume - last > 10) 
    avgBump = (avgBump + (volume - last)) / 2.0;

  //If there is a notable change in volume, trigger a "bump"
  //  avgbump is lowered just a little for comparing to make the visual slightly more sensitive to a beat.
  bump = (volume - last > avgBump * .9);  

  //If a "bump" is triggered, average the time between bumps
  if (bump) 
  {
    avgTime = (((millis() / 1000.0) - timeBump) + avgTime) / 2.0;
    timeBump = millis() / 1000.0;
  }
}

void endPattern()
{
  gradient++;    //Increments gradient
  last = volume; //Records current volume for next pass
  delay(30);     //Paces visuals so they aren't too fast to be enjoyable
}

void cyclePalette() 
{
  //If shuffle mode is on, and it's been 30 seconds since the last shuffle, and then a modulo
  //  of gradient to get a random decision between palette or visualization shuffle
  if (shuffle && millis() / 1000.0 - shuffleTime > 30 && gradient % 2) 
  {

    shuffleTime = millis() / 1000.0; //Record the time this shuffle happened.

    palette == (PALETTE)((int)palette + 1);
    if ((int)palette >= sizeof(thresholds) / 2) palette = RESET_PALETTE;
    gradient %= thresholds[(int)palette];
    maxVol = avgVol;  //Set the max volume to average for a fresh experience.
  }
}

//This function calls the appropriate color palette based on "palette"
//  If a negative value is passed, returns the appropriate palette withe "gradient" passed.
//  Otherwise returns the color palette with the passed value (useful for fitting a whole palette on the strip).
uint32_t colorPalette(float num) 
{
  switch (palette) {
    case RAINBOW: return (num < 0) ? Rainbow(gradient) : Rainbow(num);
    case SUNSET: return (num < 0) ? Sunset(gradient) : Sunset(num);
    case OCEAN: return (num < 0) ? Ocean(gradient) : Ocean(num);
    case PINA_COLADA: return (num < 0) ? PinaColada(gradient) : PinaColada(num);
    case SULFUR: return (num < 0) ? Sulfur(gradient) : Sulfur(num);
    case NO_GREEN: return (num < 0) ? NoGreen(gradient) : NoGreen(num);
    default: return Rainbow(gradient);
  }
}

//------------------------------------------------------------------------------

//PULSE
//Pulse from center of the strip
void pulse(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
  }
  startPattern();
  fade(0.75);   //Listed below, this function simply dims the colors a little bit each pass of loop()

  //Advances the palette to the next noticeable color if there is a "bump"
  if (bump) gradient += thresholds[palette] / 24;

  //If it's silent, we want the fade effect to take over, hence this if-statement
  if (volume > 0) 
  {
    uint32_t col = colorPalette(-1); //Our retrieved 32-bit color

    //These variables determine where to start and end the pulse since it starts from the middle of the strip.
    //  The quantities are stored in variables so they only have to be computed once (plus we use them in the loop).
    int start = LED_HALF - (LED_HALF * (volume / maxVol));
    int finish = LED_HALF + (LED_HALF * (volume / maxVol)) + strip.numPixels() % 2;
    //Listed above, LED_HALF is simply half the number of LEDs on your strip.  this part adjusts for an odd quantity.

    for (int i = start; i < finish; i++) 
    {

      //"damp" creates the fade effect of being dimmer the farther the pixel is from the center of the strip.
      //  It returns a value between 0 and 1 that peaks at 1 at the center of the strip and 0 at the ends.
      float damp = sin((i - start) * PI / float(finish - start));

      //Squaring damp creates more distinctive brightness.
      damp = pow(damp, 2.0);

      //Fetch the color at the current pixel so we can see if it's dim enough to overwrite.
      uint32_t col2 = strip.getPixelColor(i);

      //Takes advantage of one for loop to do the following:
      // Appropriatley adjust the brightness of this pixel using location, volume, and "knob"
      // Take the average RGB value of the intended color and the existing color, for comparison
      uint8_t colors[3];
      float avgCol = 0, avgCol2 = 0;
      for (int k = 0; k < 3; k++) 
      {
        colors[k] = split(col, k) * damp * KNOB_VALUE * pow(volume / maxVol, 2);
        avgCol += colors[k];
        avgCol2 += split(col2, k);
      }
      avgCol /= 3.0, avgCol2 /= 3.0;

      //Compare the average colors as "brightness". Only overwrite dim colors so the fade effect is more apparent.
      if (avgCol > avgCol2) 
        strip.setPixelColor(i, strip.Color(colors[0], colors[1], colors[2]));
    }
  }
  //This command actually shows the lights. If you make a new visualization, don't forget this!
  strip.show();

  endPattern();
}

//------------------------------------------------------------------------------

//PALETTEPULSE
//Same as Pulse(), but colored the entire pallet instead of one solid color
void palettePulse(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
    memset(pos, -2, sizeof(pos));
  }
  startPattern();
  
  fade(0.75);
  if (bump) gradient += thresholds[palette] / 24;
  if (volume > 0) 
  {
    int start = LED_HALF - (LED_HALF * (volume / maxVol));
    int finish = LED_HALF + (LED_HALF * (volume / maxVol)) + strip.numPixels() % 2;
    for (int i = start; i < finish; i++) 
    {
      float damp = sin((i - start) * PI / float(finish - start));
      damp = pow(damp, 2.0);

      //This is the only difference from Pulse(). The color for each pixel isn't the same, but rather the
      //  entire gradient fitted to the spread of the pulse, with some shifting from "gradient".
      int val = thresholds[palette] * (i - start) / (finish - start);
      val += gradient;
      uint32_t col = colorPalette(val);

      uint32_t col2 = strip.getPixelColor(i);
      uint8_t colors[3];
      float avgCol = 0, avgCol2 = 0;
      for (int k = 0; k < 3; k++) 
      {
        colors[k] = split(col, k) * damp * KNOB_VALUE * pow(volume / maxVol, 2);
        avgCol += colors[k];
        avgCol2 += split(col2, k);
      }
      avgCol /= 3.0, avgCol2 /= 3.0;
      if (avgCol > avgCol2) 
        strip.setPixelColor(i, strip.Color(colors[0], colors[1], colors[2]));
    }
  }
  strip.show();
  
  endPattern();
}

//------------------------------------------------------------------------------

//TRAFFIC
//Dots racing into each other
void traffic(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
    randomSeed(volume);
    dotPos = random(strip.numPixels());
  }
  startPattern();

  //fade() actually creates the trail behind each dot here, so it's important to include.
  fade(0.8);

  //Create a dot to be displayed if a bump is detected.
  if (bump) 
  {

    //This mess simply checks if there is an open position (-2) in the pos[] array.
    int8_t slot = 0;
    for (slot; slot < sizeof(pos); slot++) 
    {
      if (pos[slot] < -1) 
        break;
      else if (slot + 1 >= sizeof(pos)) 
      {
        slot = -3;
        break;
      }
    }

    //If there is an open slot, set it to an initial position on the strip.
    if (slot != -3) 
    {

      //Evens go right, odds go left, so evens start at 0, odds at the largest position.
      pos[slot] = (slot % 2 == 0) ? -1 : strip.numPixels();

      //Give it a color based on the value of "gradient" during its birth.
      uint32_t col = colorPalette(-1);
      gradient += thresholds[palette] / 24;
      for (int j = 0; j < 3; j++) 
      {
        rgb[slot][j] = split(col, j);
      }
    }
  }

  //Again, if it's silent we want the colors to fade out.
  if (volume > 0) 
  {

    //If there's sound, iterate each dot appropriately along the strip.
    for (int i = 0; i < sizeof(pos); i++) 
    {

      //If a dot is -2, that means it's an open slot for another dot to take over eventually.
      if (pos[i] < -1)
        continue;

      //As above, evens go right (+1) and odds go left (-1)
      pos[i] += (i % 2) ? -1 : 1;

      //Odds will reach -2 by subtraction, but if an even dot goes beyond the LED strip, it'll be purged.
      if (pos[i] >= strip.numPixels()) pos[i] = -2;

      //Set the dot to its new position and respective color.
      //  I's old position's color will gradually fade out due to fade(), leaving a trail behind it.
      strip.setPixelColor( pos[i], strip.Color(
        float(rgb[i][0]) * pow(volume / maxVol, 2.0) * KNOB_VALUE,
        float(rgb[i][1]) * pow(volume / maxVol, 2.0) * KNOB_VALUE,
        float(rgb[i][2]) * pow(volume / maxVol, 2.0) * KNOB_VALUE)
        );
    }
  }
  strip.show(); //Again, don't forget to actually show the lights!
  
  endPattern();
}


//------------------------------------------------------------------------------

//SNAKE
//Dot sweeping back and forth to the beat
void snake(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
    randomSeed(volume);
    dotPos = random(strip.numPixels());
  }
  startPattern();
  
  if (bump) 
  {

    //Change color a little on a bump
    gradient += thresholds[palette] / 30;

    //Change the direction the dot is going to create the illusion of "dancing."
    left = !left;
  }

  fade(0.975); //Leave a trail behind the dot.

  uint32_t col = colorPalette(-1); //Get the color at current "gradient."

  //The dot should only be moved if there's sound happening.
  //  Otherwise if noise starts and it's been moving, it'll appear to teleport.
  if (volume > 0) 
  {

    //Sets the dot to appropriate color and intensity
    strip.setPixelColor(dotPos, strip.Color(
      float(split(col, 0)) * pow(volume / maxVol, 1.5) * KNOB_VALUE,
      float(split(col, 1)) * pow(volume / maxVol, 1.5) * KNOB_VALUE,
      float(split(col, 2)) * pow(volume / maxVol, 1.5) * KNOB_VALUE)
      );

    //This is where "avgTime" comes into play.
    //  That variable is the "average" amount of time between each "bump" detected.
    //  So we can use that to determine how quickly the dot should move so it matches the tempo of the music.
    //  The dot moving at normal loop speed is pretty quick, so it's the max speed if avgTime < 0.15 seconds.
    //  Slowing it down causes the color to update, but only change position every other amount of loops.
    if (avgTime < 0.15)                                               dotPos += (left) ? -1 : 1;
    else if (avgTime >= 0.15 && avgTime < 0.5 && gradient % 2 == 0)   dotPos += (left) ? -1 : 1;
    else if (avgTime >= 0.5 && avgTime < 1.0 && gradient % 3 == 0)    dotPos += (left) ? -1 : 1;
    else if (gradient % 4 == 0)                                       dotPos += (left) ? -1 : 1;
  }

  strip.show(); // Display the lights

  //Check if dot position is out of bounds.
  if (dotPos < 0)
    dotPos = strip.numPixels() - 1;
  else if (dotPos >= strip.numPixels())  
    dotPos = 0;
  
  endPattern();
}

//------------------------------------------------------------------------------

//PALETTEDANCE
//Projects a whole palette which oscillates to the beat, similar to the snake but a whole gradient instead of a dot
void paletteDance(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
  }
  startPattern();
  
  //This is the most calculation-intensive visual, which is why it doesn't need delayed.

  if (bump) left = !left; //Change direction of iteration on bump

  //Only show if there's sound.
  if (volume > avgVol) 
  {

    //This next part is convoluted, here's a summary of what's happening:
    //  First, a sin wave function is introduced to change the brightness of all the pixels (stored in "sinVal")
    //      This is to make the dancing effect more obvious. The trick is to shift the sin wave with the color so it all appears
    //      to be the same object, one "hump" of color. "dotPos" is added here to achieve this effect.
    //  Second, the entire current palette is proportionally fitted to the length of the LED strip (stored in "val" each pixel).
    //      This is done by multiplying the ratio of position and the total amount of LEDs to the palette's threshold.
    //  Third, the palette is then "shifted" (what color is displayed where) by adding "dotPos."
    //      "dotPos" is added to the position before dividing, so it's a mathematical shift. However, "dotPos"'s range is not
    //      the same as the range of position values, so the function map() is used. It's basically a built in proportion adjuster.
    //  Lastly, it's all multiplied together to get the right color, and intensity, in the correct spot.
    //      "gradient" is also added to slowly shift the colors over time.
    for (int i = 0; i < strip.numPixels(); i++) 
    {

      float sinVal = abs(sin(
        (i + dotPos) *
        (PI / float(strip.numPixels() / 1.25) )
        ));
      sinVal *= sinVal;
      sinVal *= volume / maxVol;
      sinVal *= KNOB_VALUE;

      unsigned int val = float(thresholds[palette] + 1)
        //map takes a value between -NUM_LEDS and +NUM_LEDS and returns one between 0 and NUM_LEDS
        * (float(i + map(dotPos, -1 * (strip.numPixels() - 1), strip.numPixels() - 1, 0, strip.numPixels() - 1))
        / float(strip.numPixels()))
        + (gradient);

      val %= thresholds[palette]; //make sure "val" is within range of the palette

      uint32_t col = colorPalette(val); //get the color at "val"

      strip.setPixelColor(i, strip.Color(
        float(split(col, 0))*sinVal,
        float(split(col, 1))*sinVal,
        float(split(col, 2))*sinVal)
        );
    }

    //After all that, appropriately reposition "dotPos."
    dotPos += (left) ? -1 : 1;
  }

  //If there's no sound, fade.
  else  fade(0.8);

  strip.show(); //Show lights.

  //Loop "dotPos" if it goes out of bounds.
  if (dotPos < 0) 
    dotPos = strip.numPixels() - strip.numPixels() / 6;
  else if (dotPos >= strip.numPixels() - strip.numPixels() / 6)  
    dotPos = 0;
  
  endPattern();
}

//------------------------------------------------------------------------------

//GLITTER
//Creates white sparkles on a color palette to the beat
void glitter(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
  }
  startPattern();

  //This visual also fits a whole palette on the entire strip
  //  This just makes the palette cycle more quickly so it's more visually pleasing
  gradient += thresholds[palette] / 204;

  //"val" is used again as the proportional value to pass to ColorPalette() to fit the whole palette.
  for (int i = 0; i < strip.numPixels(); i++) 
  {
    unsigned int val = float(thresholds[palette] + 1) *
      (float(i) / float(strip.numPixels()))
      + (gradient);
    val %= thresholds[palette];
    uint32_t  col = colorPalette(val);

    //We want the sparkles to be obvious, so we dim the background color.
    strip.setPixelColor(i, strip.Color(
      split(col, 0) / 6.0 * KNOB_VALUE,
      split(col, 1) / 6.0 * KNOB_VALUE,
      split(col, 2) / 6.0 * KNOB_VALUE)
      );
  }

  //Create sparkles every bump
  if (bump) 
  {

    //Random generator needs a seed, and micros() gives a large range of values.
    //  micros() is the amount of microseconds since the program started running.
    randomSeed(micros());

    //Pick a random spot on the strip.
    dotPos = random(strip.numPixels() - 1);

    //Draw  sparkle at the random position, with appropriate brightness.
    strip.setPixelColor(dotPos, strip.Color(
      255.0 * pow(volume / maxVol, 2.0) * KNOB_VALUE,
      255.0 * pow(volume / maxVol, 2.0) * KNOB_VALUE,
      255.0 * pow(volume / maxVol, 2.0) * KNOB_VALUE
      ));
  }
  bleed(dotPos);
  strip.show(); //Show the lights.
  
  endPattern();
}

//------------------------------------------------------------------------------

//PAINTBALL
//Recycles Glitter()'s random positioning; simulates "paintballs" of
//  color splattering randomly on the strip and bleeding together.
void paintball(bool firstTime) 
{
  if (firstTime)
  {
    gradient = 0;
    maxVol = avgVol;
  }
  startPattern();

  //If it's been twice the average time for a "bump" since the last "bump," start fading.
  if ((millis() / 1000.0) - timeBump > avgTime * 2.0) fade(0.99);

  //Bleeds colors together. Operates similarly to fade. For more info, see its definition below
  bleed(dotPos);

  //Create a new paintball if there's a bump (like the sparkles in Glitter())
  if (bump) {

    //Random generator needs a seed, and micros() gives a large range of values.
    //  micros() is the amount of microseconds since the program started running.
    randomSeed(micros());

    //Pick a random spot on the strip. Random was already reseeded above, so no real need to do it again.
    dotPos = random(strip.numPixels() - 1);

    //Grab a random color from our palette.
    uint32_t col = colorPalette(random(thresholds[palette]));

    //Array to hold final RGB values
    uint8_t colors[3];

    //Relates brightness of the color to the relative volume and potentiometer value.
    for (int i = 0; i < 3; i++) colors[i] = split(col, i) * pow(volume / maxVol, 2.0) * KNOB_VALUE;

    //Splatters the "paintball" on the random position.
    strip.setPixelColor(dotPos, strip.Color(colors[0], colors[1], colors[2]));

    //This next part places a less bright version of the same color next to the left and right of the
    //  original position, so that the bleed effect is stronger and the colors are more vibrant.
    for (int i = 0; i < 3; i++) colors[i] *= .8;
    strip.setPixelColor(dotPos - 1, strip.Color(colors[0], colors[1], colors[2]));
    strip.setPixelColor(dotPos + 1, strip.Color(colors[0], colors[1], colors[2]));
  }
  strip.show(); //Show lights.
  
  endPattern();
}
...

This file has been truncated, please download it to see its full contents.

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to teslahed.

Comments