Basic overview
The Processing sketch takes the audio input and runs a Fast Fourier Transform to break the signal up by frequency, and then aggregates that data and returns a breakdown of the signal by how much energy is in each band (I'm using 12 bands with half-octave widths starting at 55 Hz because it seems to make for a good display. This is entirely configurable). The band energy data is then compared against the recent peak energy in each band. If it is near or above the peak, the light assigned to that band is set to full brightness. In the middle, the light is set to partial brightness. Far from the peak, the light is turned off. The recent peaks are shaped a bit in an attempt to keep the display interesting (see the code for details). Required hardware
1 computer that can run Processing (Mac, Windows, Linux, maybe others). 1 Arduino (you can buy this lots of places-- here for example). 1 strand of 15-bit RGB LEDs that update on a SPI compatible protocol (I used these, but these may work too, and are better documented. You can drive whatever you want, but you'll need to figure out your own drivers on the Arduino side if the protocol differs). 1 5V power supply with a high enough current rating (~1A per 25, more won't hurt) to drive the strand(s) ( this should work for ~50 lights if you don't have a suitable supply sitting around already). Some way to hook everything together. I used male header and a 2.1 mm power supply jack, but the exact method isn't critical. Computer setup The Processing sketch looks for audio using the Java Sound Library, so it will probably use whatever the default sound input is for your system. This presents a bit of a problem, since many people will want to make their computer's sound output drive the display. One non-ideal way to do that is to physically wire your computer's output to its input. If you are using a Mac, a better way is to use Soundflower and Soundflowerbed. You would then set your Mac's sound input and output to Soundflower 2ch, and use Soundflowerbed to route output from 2ch to your normal output device. The price for this is a very small delay in sound output which depends on your buffer size. There are probably similar ways to do this for Windows and Linux. If you know a way, let me know and I will add an update. Wiring
The wiring for this project is simple: Connect the RGB LED strand to the power supply ground and the Ardunio ground, and power supply +5V to the RGB LED strand hot. Connect pin 13 to the RGB LED clock line, and pin 11 to the data line (we're using the Atmega chip's built-in SPI facility for speed, so the pins cannot be changed). For the lights I'm using, red is hot, white is ground, blue is clock, green is data. Code An updated version of this sketch is now available on GitHub. The Processing code is here, the Arduino code is here.
Processing /* 12 RGB Color Organ Controller
Analyzes sound frequency and sends color information over serial
to an Arduino with RGB LEDs attached to display. Color information
is intended to synchronize with music into an interesting display.
Copyright (C)
2010, 2013 Douglas A. Telfer
This source code is released simultaneously under the GNU GPL v2 and the Mozilla Public License, v. 2.0; derived works may use either license, a compatible license, or both licenses as suits the needs of the derived work.
Additional licensing terms may be available; contact the author with your proposal.
*** GNU General Public License, version 2 notice:
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*** Mozilla Public License, v. 2.0 notice:
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/ */
// Requires Minim: http://code.compartmental.net/tools/minim/
import ddf.minim.analysis.*; import ddf.minim.*; import processing.serial.*;
// Sound Input and processing objects Serial myPort; Minim minim; AudioInput myInput; AudioOutput myOutput; BeatDetect beat; FFT fftL; FFT fftR; int bufferSize = 2048; int minBeatPeriod = 300; // if new "beat" is < 300 ms after last beat, ignore it.
// Frequency analysis float decay = 0.99f; float thresBot = 0.3; float thresTop = 0.9; float[] peaks; float[] peakSinceUpdate; float[] noiseLvl; float minPeak = 0.1; // Used to stop lights flickering at start due to inaudible noise boolean trackNoiseLvl = false; float maxPeak = 0; int maxPeakIdx = 0; int bandNumber;
// Color state int posOffset = 0; byte rr; byte gg; byte bb;
// Communications boolean inputReady=false; long lastUpdate; byte[] drawState; // I'm assuming 15-bit light data byte lowByte; byte highByte;
// Constants to configure int ledCount = 50; //How many LEDs in your string. int[] colorIndex = { 0xff0000, 0xff0000, 0xff0000, 0xffff00, 0x00ff00, 0x00ff00, 0x00ff00, 0x0000ff, 0x0000ff, 0x0000ff, 0xff00ff, 0xff00ff }; // Standard HTML 24-bit RGB hex color notation. int bandLimit = 12; int startingQ = 55; int octaveDivisions = 2;
// ********** BEGIN *********** void setup() { // Init all the sound objects minim = new Minim(this); myInput = minim.getLineIn(Minim.STEREO, bufferSize); fftL = new FFT(myInput.bufferSize(), myInput.sampleRate()); fftL.logAverages(startingQ,octaveDivisions); fftL.window(FFT.HAMMING); fftR = new FFT(myInput.bufferSize(), myInput.sampleRate()); fftR.logAverages(startingQ,octaveDivisions); fftR.window(FFT.HAMMING); beat = new BeatDetect(myInput.bufferSize(), myInput.sampleRate()); beat.setSensitivity(minBeatPeriod);
// Init tracking data drawState = new byte[ledCount*2 + 2]; drawState[ledCount*2] = 0; drawState[ledCount*2 + 1] = 0; // Terminating bytes bandNumber = min(bandLimit, fftL.avgSize()); peaks = new float[bandNumber]; peakSinceUpdate = new float[bandNumber]; noiseLvl = new float[bandNumber]; for (int i = 0; i < bandNumber; ++i) peaks[i] = minPeak;
// Init communications String portName = Serial.list()[0]; println(portName); myPort = new Serial(this, portName, 57600); lastUpdate = millis(); }
void draw() { beat.detect(myInput.mix); fftL.forward(myInput.left); fftR.forward(myInput.right);
checkPeaks();
colorOrgan();
updateScreen(); }
void checkPeaks() { boolean newPeak = false; boolean newMaxPeak = false;
// Grab the new level data. Check to see if it represents a new peak. // Also check to see if there is a new max peak. // If there are no new peaks, decay the levels of the current peaks. // (this acts as a primitive auto-level control, and helps emphasize // changes in volume) for (int i=0; i < bandNumber; i++) { if (fftL.getAvg(i) + fftR.getAvg(i) > peaks[i]) { peaks[i] = fftL.getAvg(i) + fftR.getAvg(i); if (peaks[i] > maxPeak) { newMaxPeak = true; maxPeak = peaks[i]; maxPeakIdx = i; } } if (!newPeak) { peaks[i] *= decay; if (peaks[i] < minPeak) peaks[i] = minPeak; } } if (!newMaxPeak) { maxPeak *= decay; if (maxPeak < minPeak) maxPeak = minPeak; }
// Raise the other peaks based on the max peak. This allows a few // fequency bands to dominate the display when those frequencies also // dominate the sound spectrum. The power function makes more distant // frequency bands less affected by this shaping. The value of 0.8 // (and heck, the function) was the result of crude experimentation. // There are probably better methods for this, but it seems to do // about what I want. for (int i = 0; i < bandNumber; i++) { float peakTop = maxPeak*(pow(0.8,abs(i-maxPeakIdx))); if (peaks[i] < peakTop) peaks[i] = peakTop; }
if (trackNoiseLvl) setNoiseFloor(); // I'm not sure I'm totally sold on this. It seems a little busy. if (beat.isKick()) posOffset++; if (posOffset >= bandNumber) posOffset = 0; }
void colorOrgan() {
for (int i=0; i < bandNumber; i++) { int col = colorIndex[i%colorIndex.length]; float amp = fftL.getAvg(i) + fftR.getAvg(i); // Check noise threshold. If above, normalize amp to [0-1]. if (amp > noiseLvl[i]) amp = (amp)/peaks[i]; else amp = 0;
// Shape the band levels. Peg values above or below the upper and lower // bounds. Remap the middle so that it covers the full range. Less space // between the bounds makes things blinkier. if (amp < thresBot) amp = 0; else if (amp > thresTop) amp = 1; else amp = amp/(thresTop - thresBot) - thresBot; if (amp < 0) amp = 0; else if (amp > 1) amp = 1;
// Hold on the biggest amplitudes we've seen since the last update. This // is so that we don't lose transients if it takes too long to communicate // with the lights. I'm not sure how much of a difference this makes // though. if (amp > peakSinceUpdate[i]) peakSinceUpdate[i]=amp; else amp=peakSinceUpdate[i];
// Set the colors from the amplitudes rr = (byte)( ((col&0xff0000) >> 16)*amp ); gg = (byte)( ((col&0x00ff00) >> 8)*amp ); bb = (byte)( ((col&0x0000ff) )*amp );
// Set the communications byte array from the colors. lowByte = (byte)(rgbTo15bit(rr, gg, bb) >>> 8); highByte = (byte)(rgbTo15bit(rr, gg, bb) &0x00ff);
// Place the bytes in the array. If there fewer bands than lights, // repeat until we run out of lights. // (There is almost certainly a better way to do this...) for (int j=0; ((i+posOffset)%bandNumber)*2+j+1 < ledCount*2; j+=bandNumber*2) { drawState[((i+posOffset)%bandNumber)*2+j] = lowByte; drawState[((i+posOffset)%bandNumber)*2+j+1] = highByte; } } }
void updateScreen() { // Wait until the controller sends back a byte to indicate that it is ready, then // send the current state. if (myPort.available() > 0) { myPort.clear(); myPort.write(0); myPort.write(0); myPort.write(drawState); clearPSU(); lastUpdate = millis(); } // else println(millis() - lastUpdate); }
public void stop() { // always close Minim audio classes when you are done with them myInput.close(); myOutput.close(); minim.stop();
super.stop(); }
int rgbTo15bit( byte rr, byte gg, byte bb ) { return ((rr&0xf8)<<7)|((gg&0xf8)<<2)|((bb&0xf8)>>>3)|0x8000; }
void clearPSU() { for (int i = 0; i<bandNumber; ++i) { peakSinceUpdate[i] = 0; } }
// This is used primarily when taking audio from an external input. Since // I automatically reset levels based on recent input volume, even a // small amount of noise from the external source will eventually light // up some of the lights, which can ruin the effect of quiet passages // in the music. The somewhat crude solution is to set a noise threshold // when no music is playing. Sound must exceed the volume of the noise in // order to be recognized. This check is done on a per-band basis, so a // lot of noise in one band (e.g. a 60Hz hum) won't interfere with the // sensitivity of other bands. // // Anyways, to set the noise threshold, hold down 'n' when no music is // playing to sample the noise. void keyPressed() { if ( key == 'n' ) { if (!trackNoiseLvl) { for (int i=0; i < fftL.avgSize(); i++) {
noiseLvl[i] = 0; } trackNoiseLvl = true; } } }
void keyReleased() { if ( key == 'n' ) { trackNoiseLvl = false; } }
void setNoiseFloor() { for (int i=0; i < bandNumber; i++) { if (fftL.getAvg(i)+fftR.getAvg(i) > noiseLvl[i]) { noiseLvl[i] = fftL.getAvg(i)+fftR.getAvg(i); } } } Arduino /* D705 RGB LED serial controller
Reads color information from a serial connection and sends that
color information to
attached RGB LEDs controlled by the D705
chipset.
Copyright (C) 2010, 2013 Douglas A. Telfer
This source code is released simultaneously under the GNU GPL v2 and the Mozilla Public License, v. 2.0; derived works may use either license, a compatible license, or both licenses as suits the needs of the derived work.
Additional licensing terms may be available; contact the author with your proposal.
*** GNU General Public License, version 2 notice:
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*** Mozilla Public License, v. 2.0 notice:
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/ */
// Requires the SPI library found here: http://arduino.cc/playground/Code/Spi
#include <Spi.h>
#define MAX_LED_COUNT 1000
byte counter; byte inByte; byte zeroCount = 0;
void setup() { Spi.transfer(0); Spi.transfer(0); for (int i = 0; i < MAX_LED_COUNT; ++i) { Spi.transfer(0x80); Spi.transfer(0x00); } Spi.transfer(0); Spi.transfer(0); Serial.begin(57600); counter = 0; }
void loop() { if (Serial.available() > 0) { // get incoming byte inByte = Serial.read(); counter++; if (inByte == 0) zeroCount++; else zeroCount = 0; Spi.transfer(inByte); if (zeroCount >= 2) { Spi.transfer(0); Spi.transfer(0); counter = 0; Serial.write(17); } } else if (counter == 0) { Serial.write(17); } }
|