Final Documentation: William Schwartz


Psycho (the name of the my project) was a super fun project to work on. Psycho is an Audio Visualizer. There is a teensy 3.2 in the back of the neck along with a microphone, the teensy preforms the FFT with the help of teensy’s audio library. the different frequency bins are then grouped according to base, mid, and treble, and those bins are then sent over to the lights, with the dB of each bin corresponding to the brightness, and the hue determined by a random value with in the range of the bin. This may seem a bit confusing but the way if works is that when the FFT is preformed, we then get 512 bins all with 40Hz of band per bin. This covers the full audio range of human hearing 20Hz to 20KHz. for this project only the frequency from 40Hz to 3.6 kHz are used, and the bands are defined as follows 1-6, 6-12, 13-20, 21-30, 31-40, 40-90, in terms of the “bins”. I then iterate though the bins in each band, and look for the bin with the highest amplitude, and the that amplitude gets mapped to the brightness, and the bin number gets mapped to the hue, with a small degree of randomness just to get a little more variance in the color.

example videos of the head working šŸ™‚


Teensy 3.2

Lithium ion battery: 18650 2600 mAH or equivalent

charge controller


Natural PLA 3d printer filament (or any other clear filament that is easy to print)

Neo-pixel lights

Bypass capacitor (330 microF)

On/Off switch

hot glue

3D printing:

here is a rendered photo of the head:

the STL file(s) for the head are hosted here

all the parts were sliced in Cura with no infill, and no bottom layers

the parts need to be positioned so that there are minimum overhands

All of the electronics went into part #6, (in the folder linked above )


the wiring is that, the battery was connected to the batter ports of the charge controller

the + output of the charge controller had the switch connected to it, and the other end of the switch was connected to the + of the capacitor

the – output of the charge controller was connected to the – terminal of the capacitor.

the + of the capacitor was connected to the Vin of the Teensy, and the input of the Neo-Pixel

the 3.3v pin (on the teensy) was connected to Vdd of the microphone

the – terminal of the capacitor was connected to the ground of the teensy, the ground of the microphone, and the ground of the Neo-Pixel

the data line of the Neo pixel was connected to pin 5 on the teensy

the output of the mic was connected to A0 (pin 14) on the teensy

3 lights were put into this back piece, and an Ethernet cord was cut to connect the wire the lights together in-between the pieces


a hot glue gun was used to melt holes between the parts to wire though, about a foot of wire was used between pieces to give so extra room for soldering. the parts were then hot glued together. this part took the longest, around 30 hours to assemble.


I started with the teensy Audio library to perform the FFT, as well as theĀ Adafruit_NeoPixel library to control the lights.

I used a separate function to convert the RGB input of the Neo-pixels to HSV so that it was more intuitive to set the color and brightness.


the code is as follows:

#include <Adafruit_NeoPixel.h>
#include <Audio.h>
//#include <Wire.h>
//#include <SerialFlash.h>
#ifdef __AVR__
// #include <avr/power.h>

void setLedColorHSV(byte h, byte s, byte v);

// Create the Audio components. These should be created in the
// order data flows, inputs/sources -> processing -> outputs
AudioInputAnalog adc1(A0); // ADC0 input
AudioAnalyzeFFT1024 myFFT;

// Connect either the live input or synthesized sine wave
AudioConnection patchCord1(adc1, 0, myFFT, 0);

// Which pin on the Arduino is connected to the NeoPixels?
// On a Trinket or Gemma we suggest changing this to 1
#define PIN 5
const int POWER_LED_PIN = 13; // Output pin for power LED (pin 13 to use Teensy 3.0's onboard LED).
byte h;
byte s;
byte v;
byte RedLight;
byte GreenLight;
byte BlueLight;
int currentlight;

float avg_fft[512]; 
int avg_fft_cout = 1;

#define NUMPIXELS 41
#define NUMCELLS 25
int cells[NUMCELLS] = {3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 1, 1, 3, 4, 3, 2};

// ----------- ALL THE DIFFERENT MAPPINGS THAT I HAVE MADE!!! ---------------------------------------
// even maping base at the neck
int mapping[NUMCELLS] = {0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0};
// random mapping
//int mapping[NUMCELLS] = {0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0};
// random mapping base at the left ear
//int mapping[NUMCELLS] = {0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 5, 5, 5};
// random mapping base in the neck
//int mapping[NUMCELLS] = {0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 0, 5, 5, 5, 5, 1, 2, 3};

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

#define freq_bands 6

// defines the frequency ranges for each band 
int freq_high[freq_bands] = {90, 40, 30, 20, 12, 6};
int freq_low[freq_bands] = {40, 31, 21, 13, 6, 1};

// stores the results of going through each band
int freq_max_bin[freq_bands] = {};
float freq_max_amp[freq_bands] = {};
float prev_freq_max_amp[freq_bands] = {};
byte freq_hue[freq_bands] = {};
byte prev_freq_hue[freq_bands] = {};

// When we setup the NeoPixel library, we tell it how many pixels, and which pin to use to send signals.
// Note that for older NeoPixel strips you might need to change the third parameter--see the strandtest
// example for more information on possible values.
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

void setup() {
// Turn on the power indicator LED.
digitalWrite(POWER_LED_PIN, HIGH);

pixels.begin(); // This initializes the NeoPixel library.
// Audio connections require memory to work. For more
// detailed information, see the MemoryAndCpuUsage example

// Configure the window algorithm to use

void loop() {
float n;
int i;
int fft_count = 0;


// ________________________________________________________ 
// copy initial sample
if (myFFT.available()) {
for (i = 0; i < 512; i++) {
avg_fft[i] =;
// averge other samples
while (fft_count < avg_fft_cout) {
if (myFFT.available()) {
for (i = 0; i < 512; i++) {
avg_fft[i] = (avg_fft[i] +;
// ________________________________________________________

int f; // used as intex for band we are in
int bin = 0; // hold on to the current bin

for (f = 0; f < freq_bands; f++) {

freq_max_amp[f] = 0;
bin = 0;

for (i=freq_high[f]; i > freq_low[f]; i--) {
n = avg_fft[i];
if ( n > 0.01) {
freq_max_amp[f] = max(freq_max_amp[f], n);
// recently added might make things worse? we will see...
//if (freq_max_amp[f] == n) {
bin = i;

if (bin != 0 && bin != freq_max_bin[f]){
freq_max_bin[f] = bin;
// TODO: the bins are off!! bin 5 should be RED!! currently bin 0 is red
freq_hue[f] = 252 - ((f+1%6)*42 + random((freq_max_bin[f]-freq_low[f])*(42/(freq_high[f]-freq_low[f]))));


if (freq_max_amp[f] == 0){
freq_max_bin[f] = 0;

// set the hue to the bin hue
if (freq_max_bin[f] != 0){
Serial.print("BAND ");
Serial.print(" amplitude is ");
Serial.print(freq_max_amp[f], 20);
Serial.print("the bin is ");

// ________________________________________________________ 
currentlight = 0;
for(int i=0;i<NUMCELLS;i++) {

h = freq_hue[mapping[i]];
if (h != 0 && prev_freq_hue[mapping[i]] != 0) {
h = (h - prev_freq_hue[mapping[i]])/3 + prev_freq_hue[mapping[i]];
prev_freq_hue[mapping[i]] = h;

s = 255;
if (freq_max_amp[mapping[i]] != 0.0){
// v = (int)(freq_max_amp[mapping[i]]*1000.0);
v = (int)(freq_max_amp[mapping[i]]*1000.0);
v = (int) abs(v - prev_freq_max_amp[mapping[i]])/2 + prev_freq_max_amp[mapping[i]];

prev_freq_max_amp[mapping[i]] = v/1.1-1;
} else {
//v = ((int)(prev_freq_max_amp[mapping[i]]*1000.0))/2-1;
v = prev_freq_max_amp[mapping[i]];
prev_freq_max_amp[mapping[i]] = (int) prev_freq_max_amp[mapping[i]]/1.1-1;
for (int j=0;j<cells[i];j++) {
// pixels.Color takes RGB values, from 0,0,0 up to 255,255,255
pixels.setPixelColor(currentlight, pixels.Color(RedLight, GreenLight, BlueLight));
currentlight = currentlight + 1;
for (int f=0; f < freq_bands; f++){
if (freq_max_bin[f] != 0){
Serial.print("THIS IS THE LIGHT DATA!!, band is ");
Serial.print(" hue is ");
Serial.print(freq_max_amp[f], 20);
Serial.print("the bin is ");
}; // This sends the updated pixel color to the hardware.
// ________________________________________________________


void setLedColorHSV(byte h, byte s, byte v) {
// this is the algorithm to convert from RGB to HSV
h = (h * 192) / 256; // 0..191
unsigned int i = h / 32; // We want a value of 0 thru 5
unsigned int f = (h % 32) * 8; // 'fractional' part of 'i' 0..248 in jumps

unsigned int sInv = 255 - s; // 0 -> 0xff, 0xff -> 0
unsigned int fInv = 255 - f; // 0 -> 0xff, 0xff -> 0
byte pv = v * sInv / 256; // pv will be in range 0 - 255
byte qv = v * (256 - s * f / 256) / 256;
byte tv = v * (256 - s * fInv / 256) / 256;

switch (i) {
case 0:
RedLight = v;
GreenLight = tv;
BlueLight = pv;
case 1:
RedLight = qv;
GreenLight = v;
BlueLight = pv;
case 2:
RedLight = pv;
GreenLight = v;
BlueLight = tv;
case 3:
RedLight = pv;
GreenLight = qv;
BlueLight = v;
case 4:
RedLight = tv;
GreenLight = pv;
BlueLight = v;
case 5:
RedLight = v;
GreenLight = pv;
BlueLight = qv;

Design Iterations:

I started with an initial test to see how the 3D printed parts would work to defuse the light

this was a success, for this test I used 2 neo-pixles, and and the Bottom front two pieces of the neck.

Next I tested out preforming the FFT:

for this iteration I did not use the teensy Audio library, and so the FFT was not as good, the lower frequencies were too sensitive. And I believe the mic was always picking up the DC offset of the microphone, for this part I only have a picture showing the different frequency bins, as a test.

with these two tests complete, and some nice results I felt like I was ready to start assembling the head after all the pieces were printed

images of the assembly are above ^


after everything was put together it was time to get the teensy to work:

there were a lot of iterations in the Code

1 first I just had a script going through and randomly assigning colors to all the lights in the head

then I started playing with the teensy Audio library, which is an amazingly powerful library for audio processing. I found the FFT example, and ran with it.

the documentation was a little limited from what I found online, what was best was going directly into the source files, and from there the code is well documented, and I was able to figure everything out.

In the lab:

I found some contradictory information online, the ADC input for the audio library was stated online to require a DC bias of .6 and a max voltage of 1.2 and a min voltage of 0. So I tested this on the board by brining it in to the ECE lab.

I used the function generator and attached it to the analog input that I was using, I try initially with a 10kHz wave of 1.2 V peak-peak, and ran my script, I then tested with a 3.3 V peak-peak and 1.25v offset, and found that the ADC was able to digitize the whole waveform from 0 to 3.3V which is what I expected from the beginning.

with those tests done, I also tests the microphone in different settings, playing with the gain, and the Attack/Release ratio (not quite sure what it is but it has settings). and found that the default setting gave me the best results.


now that all that nonsense was out of the way, started dividing up the frequency bins in the code

initially I had 6 bins from 40Hz to 20kHz but quickly found that there was not much being picked up at the higher frequencies.

I had by bands divided according to this guide for sound:

Frequency Range Frequency Values
Sub-bass 20 to 60 Hz
Bass 60 to 250 Hz
Low midrange 250 to 500 Hz
Midrange 500 Hz to 2 kHz
Upper midrange 2 to 4 kHz
Presence 4 to 6 kHz
Brilliance 6 to 20 kHz

but I was getting very little presence and brilliance so I scaled all by bands down to make it look better.

and ended up going from 40 Hz to about 3.5KHz and got some good results


I was trying to average multiple FFT together to try to get rid of some noise, but what I found was that I forgot to check if I was getting the new sample before I was averaging it in, so what this equated to was wasted CPU time that was just sort of a delay, but it looked nice.

So I fixed the code and then I found that by doing this, the lights were flickering a lot, and I tried to apply a moving average to all the samples, but still wasn’t giving the results that I wanted, so eventually I gave up and put back in the delay. this is something that I plan on fixing in the future, because I want the head to me more responsive to the audio coming it and be able to fade appropriately, I think that is the one thing really missing from the project.

light layout:

I played around a bit with the light layout, at first I just mapped all the lights to the different bins in order from 0-5 and repeating till the end of the cells. It gave a pretty nice look, but I like the base, so I decided to make half of the face just dedicated to the base, and then I was happy with the results and think that it looks really cool. So the left half of the face glows red with the base.


finally what was left was to find the songs that I thought make the head look best, for this I recruited my roommate Steve (he has some nice speakers) and we sat in his room listening to music and watching how the head responds. Great experience would recommend

Future improvements:

better attempt at the moving average

re-ordering the lights

dividing up the frequency bands a bit better

making more heads

adding the entire process to my GitHub.

Leave a Reply