#define SND_ADC_CHANNEL 0
#define LDR_ADC_CHANNEL 1
#define RND_ADC_CHANNEL 2
#define DEFAULT_INPUT 1
#define LED_PIN 2
#define INPUT1_PIN 3
#define INPUT2_PIN 4
#define INPUT3_PIN 5
#define INPUT4_PIN 6
#define MODE_PIN 7
#define OUTPUT1_PIN 8
#define OUTPUT2_PIN 9
#define OUTPUT3_PIN 10
#define OUTPUT4_PIN 11
#define XRES 32
#define YRES 8
#include <SoftTimers.h>
#include <InputDebounce.h>
// MEM
#define BUFFER_LEN 10
#define BUFFER_ADR 0x10
#include <eewl.h>
// FHT
#define LIN_OUT 1
#define FHT_N 64
#include <FHT.h>
// LDR
#define LDR_MAX 600
#define LDR_MIN 100
#define LDR_FILTER_WEIGHT 6
// SND
#define SND_SAMPLES FHT_N
#define NOISE_THRESHOLD 3
#define NUM_BINS FHT_N/2
#define NUM_BANDS XRES/2
#define SILENCE_MS 10000
#define SND_FILTER_WEIGHT 2
// LED
#define NUM_LEDS XRES*YRES
#define BRIGHTNESS 64
#define YRES_HUE 65536/YRES
#define YRES_HUE_STEP YRES_HUE/YRES
#include <Adafruit_NeoPixel.h>
// GFX
#define UPDATE_MS 24
#define PEAK_HOLD 16
#define MAX_DIM 192
#define SATURATION 255
// used to amplify bins 5 dB / oct
const long BIN_EQ_LUT[NUM_BINS] = {
11264, 23429, 41564, 58122, 74004, 88986, 103291, 117258,
131564, 145080, 158147, 170537, 183716, 196895, 208497, 220887,
233953, 244992, 258058, 268646, 281262, 292864, 303227, 315617,
326769, 338258, 350085, 360335, 370811, 381624, 392776, 404265
};
const byte EQ_MAX = BIN_EQ_LUT[0] >> 6;
// used to hold data per band
struct BandStruct {
byte avg;
byte level;
byte peak;
byte hold;
} bands[NUM_BANDS];
// used for silence indicator
struct StarStruct {
byte x;
byte y;
unsigned int hue;
byte sat;
byte val;
} star;
byte sndMax = 0;
byte dimValue = 0;
byte selectedInput = DEFAULT_INPUT;
bool altMode = false;
InputDebounce btnMode;
InputDebounce btnInput1;
InputDebounce btnInput2;
InputDebounce btnInput3;
InputDebounce btnInput4;
SoftTimer silenceTimer;
SoftTimer updateTimer;
EEWL eewl(selectedInput, BUFFER_LEN, BUFFER_ADR);
Adafruit_NeoPixel leds(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// return input selection for pin
byte pinToSelection(byte pin) {
byte selection;
switch (pin) {
case INPUT2_PIN:
selection = 2;
break;
case INPUT3_PIN:
selection = 3;
break;
case INPUT4_PIN:
selection = 4;
break;
default:
selection = 1;
break;
}
return selection;
}
// return pin for input selection
byte selectionToPin(byte selection) {
byte pin;
switch (selection) {
case 2:
pin = INPUT2_PIN;
break;
case 3:
pin = INPUT3_PIN;
break;
case 4:
pin = INPUT4_PIN;
break;
default:
pin = INPUT1_PIN;
break;
}
return pin;
}
// set selected output and save to eeprom
void setOutput(byte selection) {
(selection == 1) ? digitalWrite(OUTPUT1_PIN, HIGH) : digitalWrite(OUTPUT1_PIN, LOW);
(selection == 2) ? digitalWrite(OUTPUT2_PIN, HIGH) : digitalWrite(OUTPUT2_PIN, LOW);
(selection == 3) ? digitalWrite(OUTPUT3_PIN, HIGH) : digitalWrite(OUTPUT3_PIN, LOW);
(selection == 4) ? digitalWrite(OUTPUT4_PIN, HIGH) : digitalWrite(OUTPUT4_PIN, LOW);
if (selection != selectedInput) {
selectedInput = selection;
eewl.put(selectedInput);
}
}
// input selection callback
void btnInputCallback(byte pin) {
byte selection = pinToSelection(pin);
if (selection != selectedInput) {
setOutput(selection);
}
}
// handle button debouce
void processButtons() {
unsigned long now = millis();
btnInput1.process(now);
btnInput2.process(now);
btnInput3.process(now);
btnInput4.process(now);
btnMode.process(now);
altMode = btnMode.isPressed();
}
// map x,y to pixel offset
int pixel(byte x, byte y) {
int i = XRES * (YRES - y);
return (y & 0x01) ? i - XRES + x : i - 1 - x;
}
// integer filter where weight factor is a power of two
int filter(int value, unsigned long avg, byte weight) {
return ((avg << weight) - avg + value) >> weight;
}
// integer min-max scaling to resolution
byte scaleMinMax(byte value, byte minValue, byte maxValue, byte resolution) {
return (minValue < maxValue) ? map(value, minValue, maxValue, 0, resolution) : 0;
}
// init band data
void initBands() {
for (byte i = 0; i < NUM_BANDS; i++) {
bands[i].avg = 0;
bands[i].level = 0;
bands[i].peak = 0;
bands[i].hold = 0;
}
}
// generate new random star
void nova() {
star.x = random(XRES);
star.y = random(YRES);
star.hue = random(0xffff);
star.sat = 32;
star.val = 255 - dimValue;
}
// get rainbow hue for band
unsigned int getRainbowHue(byte band) {
return band << 12;
}
// get hue from color cycle
unsigned int getNextHue() {
static unsigned int hue = 0;
hue--;
return hue;
}
// draw level bar for band
void drawBar(byte band, unsigned int hue, bool dark) {
byte x = band << 1;
byte shade = 255 - dimValue;
if (dark) {
shade >>= 1;
}
for (byte i = 0; i < YRES; i++) { // bar
if (bands[band].level > 0 && i < bands[band].level) {
unsigned long normColor = leds.ColorHSV(hue, SATURATION, shade);
leds.setPixelColor(pixel(x, i), normColor);
}
if (bands[band].peak > 0 && i < bands[band].peak) {
unsigned long peakColor = leds.ColorHSV(hue + 2048, SATURATION, shade >> 1);
leds.setPixelColor(pixel(x + 1, i), peakColor);
}
hue += YRES_HUE_STEP;
}
}
// draw alternative level bar for band
void drawAltBar(byte band, unsigned int hue, bool dark) {
byte x = band << 1;
byte shade = 255 - dimValue;
if (dark) {
shade >>= 1;
}
for (byte i = 0; i < YRES; i++) { // bar
if (bands[band].peak > 0 && i == bands[band].peak - 1) {
unsigned long peakColor = leds.ColorHSV(hue + 2048, SATURATION, shade);
leds.setPixelColor(pixel(x, i), peakColor);
leds.setPixelColor(pixel(x + 1, i), peakColor);
} else if (bands[band].level > 0 && i < bands[band].level && i < bands[band].peak - 1) {
unsigned long normColor = leds.ColorHSV(hue, SATURATION, shade >> 1);
leds.setPixelColor(pixel(x, i), normColor);
leds.setPixelColor(pixel(x + 1, i), normColor);
}
hue += YRES_HUE_STEP;
}
}
// update led display
void updateDisplay() {
leds.clear();
if (silenceTimer.hasTimedOut()) { // no sound, indicate it with a "twinkeling star"
if (star.sat < 255) {
star.sat++;
} else if (star.val > 0) {
star.val--;
} else {
nova();
}
unsigned long color = leds.ColorHSV(star.hue, star.sat, star.val);
leds.setPixelColor(pixel(star.x, star.y), color);
} else {
unsigned int hue;
bool dark = (dimValue == MAX_DIM);
for (byte i = 0; i < NUM_BANDS; i++) {
hue = getNextHue();
if (altMode) {
drawAltBar(i, hue, dark);
} else {
drawBar(i, hue, dark);
}
}
}
leds.show();
}
// get 5dB/octave amplified bin
byte eqBin(byte bin) {
int value;
value = fht_lin_out[bin];
value = map(value, 0, BIN_EQ_LUT[0], 0, BIN_EQ_LUT[bin]) >> 6;
return constrain(value, 0, 255);
}
// eq and smoothen response, return updated avg value
byte processBand(byte band) {
int value = eqBin(band + 1); // skip first bin
value = filter(value, bands[band].avg, SND_FILTER_WEIGHT); // filter value
bands[band].avg = constrain(value, 0, 255); // update avg
return bands[band].avg;
}
// process sound sample
void processSnd() {
bool sound = false;
byte value;
byte i;
fht_window(); // window data for better frequency response
fht_reorder(); // reorder data before FHT
fht_run(); // process FHT data
fht_mag_lin(); // get FHT output
for (i = 0; i < NUM_BANDS; i++) {
value = processBand(i); // eq and filter band value
sndMax = max(value, sndMax); // update max value
}
// scale, manage peaks and sound detection
for (i = 0; i < NUM_BANDS; i++) {
value = bands[i].avg; // use filtered value
value = scaleMinMax(value, 0, sndMax, YRES); // min-max scale to YRES
if (value > 0) {
sound = true;
}
bands[i].level = value;
if (bands[i].level > bands[i].peak) {
bands[i].peak = bands[i].level;
bands[i].hold = PEAK_HOLD;
} else if (bands[i].hold > 0) {
bands[i].hold--;
} else if (bands[i].peak > 0) {
bands[i].peak--;
}
}
if (sndMax > 0) {
sndMax--;
}
if (sound) {
silenceTimer.reset();
}
}
// set adc channel
void adcSet(byte channel) {
static byte adcChannel = 0xFF;
if (adcChannel == 0xFF) {
adcChannel = channel;
DIDR0 |= B00111111; // disable digital input pins
ADMUX = B00000000 | adcChannel; // AREF, right-adjust, channel
ADCSRA = B10100101; // enable ADC, auto-trigger, prescaler 32
} else if (channel != adcChannel) {
adcChannel = channel;
ADMUX = B00000000 | adcChannel; // AREF, right-adjust, channel
ADCSRA |= B01000000; // start ADC conversion
while (!(ADCSRA & B00010000)); // wait for adc to be ready
ADCL | (ADCH << 8); // skip sample from old channel
}
}
// return one adc sample
int adcGet() {
ADCSRA |= B01000000; // start ADC conversion
while (!(ADCSRA & B00010000)); // wait for adc to be ready
return ADCL | (ADCH << 8); // must read low first
}
// take sound samples
void sampleSnd() {
int i;
int value;
long avg = 0;
adcSet(SND_ADC_CHANNEL);
for (i = 0; i < SND_SAMPLES; i++) {
value = adcGet(); // get sample
fht_input[i] = value; // put into FHT input
avg += value; // add for avg
}
avg /= SND_SAMPLES; // avg -> voltage offset
for (i = 0; i < SND_SAMPLES; i++) {
value = fht_input[i]; // get from FHT input
value -= avg; // remove voltage offset
value = abs(value) > NOISE_THRESHOLD ? value : 0; // filter some noise
value <<= 6; // to FHT max resolution
fht_input[i] = value; // put into FHT input
}
}
// take ldr sample
void sampleLdr() {
static int avg = 0;
int value;
adcSet(LDR_ADC_CHANNEL);
value = adcGet(); // read ldr value
avg = filter(value, avg, LDR_FILTER_WEIGHT); // smoothen ldr reading
value = constrain(avg, LDR_MIN, LDR_MAX); // clamp to ldr min-max
value = map(value, LDR_MIN, LDR_MAX + 1, 0, MAX_DIM + 1); // map value to min-max
dimValue = MAX_DIM - value; // update based on ldr value
}
void setup() {
initBands();
// init output pins
pinMode(OUTPUT1_PIN, OUTPUT);
pinMode(OUTPUT2_PIN, OUTPUT);
pinMode(OUTPUT3_PIN, OUTPUT);
pinMode(OUTPUT4_PIN, OUTPUT);
// get persisted data
eewl.get(selectedInput);
// output selected input
setOutput(selectedInput);
// init buttons
btnMode.setup(MODE_PIN);
btnInput1.registerCallbacks(btnInputCallback, NULL);
btnInput2.registerCallbacks(btnInputCallback, NULL);
btnInput3.registerCallbacks(btnInputCallback, NULL);
btnInput4.registerCallbacks(btnInputCallback, NULL);
btnInput1.setup(INPUT1_PIN);
btnInput2.setup(INPUT2_PIN);
btnInput3.setup(INPUT3_PIN);
btnInput4.setup(INPUT4_PIN);
// init LEDs
leds.begin();
leds.show();
leds.setBrightness(BRIGHTNESS);
// init silence display
adcSet(RND_ADC_CHANNEL);
randomSeed(adcGet()); // seed with adc noise
nova(); // create new star ;)
// init timers
silenceTimer.setTimeOutTime(SILENCE_MS);
silenceTimer.reset();
updateTimer.setTimeOutTime(UPDATE_MS);
updateTimer.reset();
}
void loop() {
processButtons();
if (updateTimer.hasTimedOut()) {
updateTimer.reset();
sampleLdr();
sampleSnd();
processSnd();
updateDisplay();
}
}