Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 4 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
This project is my variation of Arduino Controlled Sound Responsive LED Display by teslahed. The changes made are:
- Replaced Arduino Nano with a ATtiny1614 microprocessor
- Added a PCB for the components
- Created a new smaller base to hold the electronics and display
- Added a cover around the display to hide the LED boards and wiring
- New software with more animations
Open the "Display Form.stl" file in your slicer software. Because this part is printed using transparent filament, the infill you choose will have an impact on the final display and look. I used a 10% grid infill.
The LED boards should fit snugly into the display form. In teslahed's original design, the wires that connected the LED boards ran on the outside of the display form. I used a hot pin and made holes through the corners so the wires stayed inside the display form.
3D print the "Display Base Top.stl" in transparent filament with supports that touch the build plate. Once printed, use this to position where to create the hole in the base of the display form for the wires that go from the base to 5V, DIN and GND.
3D print "Display Cover.stl" in black. This will go over the LED boards to hide the wiring. Don't glue this in place until you have tested everything works.
Modifying the MAX9814 AGC microphone moduleRemove the microphone from the board. Solder some thin wires to the microphone pins which will ultimately be reattached to the board when the base is assembled. The microphone board is a bit too long so you need to cut off the top. Use the picture below to see where you need to make the cut.
The Eagle files for the PCB have been attached should you wish to have the boards commercially made or you can do as I did and make them yourself. I used the Toner method. Start by adding the SMD components.
Use pin headers on the top side of the board. It will make it easier to wire up the base assembly.
The ATtiny1614 is a new breed of ATtiny microprocessor. These are programmed using the RESET/UPDI pin. I have written the following tutorial on how to program these chips. See Using the New ATtiny Processors with Arduino IDE.
The unconnected pin header shown above has the pins for VCC, UPDI and GND. Connect these to your programmer.
Updateteslahed (author of Arduino Controlled Sound Responsive LED Display) suggested that if you print the display part with a hexagonal infill and zero top layers then the LEDs will make prettier patterns in the internal fill structure and you'll have 2 different looks depending on which way around you place the device.
/*-----------------------------------------------------------------------------
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.
Comments