Github: jorticus/audiovis

Overview

I developed a sound reactive system to go with a Chinese 5m RGB light strip, which easily lights up the whole room in any colour I want.

It detects the noisiness of the room, and changes colour depending on how noisy it is. When the room is quiet, the room remains a deep blue colour, but it progresses to yellow and red as the room gets noisier.

The system consists of an Arduino and a Python script running on my computer, which listens to an audio input and performs some processing on it to determine the loudness of the room.

Electronics

The electronics are relatively simple, as all they need to do is receive commands from the computer and drive the LEDs

Arduino

The arduino code is really simple, it just reads 3 binary values from the computer over a serial port, and updates the RGB PWM outputs. I'm using a very simple protocol consisting of 4 bytes, using the first byte to sync the packet in case it gets out of sync.

// Update LED brightness
void loop()  { 
  if (Serial.available()) {
    if (Serial.read() == 'X') { // sync byte

      while (!Serial.available());
      analogWrite(red_led, Serial.read());

      while (!Serial.available());
      analogWrite(green_led, Serial.read());

      while (!Serial.available());
      analogWrite(blue_led, Serial.read());
    }
  }
}

Python

The python code does all the heavy audio DSP, and contains the algorithm for determining the colour of the LEDs.

This process is explained below...

Capturing Audio

To capture audio using python, I'm using pyAudio. It makes it very easy to connect to an audio device and stream data.

To open an audio stream using pyAudio:

import pyaudio

SAMPLE_RATE = 44100
BUFFER_SIZE = 2**11

p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16,channels=1,rate=SAMPLE_RATE,\
                input=True,output=False,frames_per_buffer=BUFFER_SIZE)

Where BUFFER_SIZE will determine how long the sample is, and also the frequncy bands generated from the FFT. Thus, changing it will also change the response of the DSP algorithm below, and will need to be adjusted.

DSP

First we read an audio sample, and convert it to something python can use

import struct

# Get audio sample
buf = stream.read(BUFFER_SIZE)
data = scipy.array(struct.unpack("%dh"%(BUFFER_SIZE),buf)) #h: unsigned short, 2 bytes per sample

Then we generate an FFT using scipy's FFT library. I'm only interested in the amplitudes, so calling abs() on the samples will produce an array of amplitudes.

from scipy.fftpack import fft, fftfreq
import scipy

def get_fft(data):
    """ Run the sample through a FFT """
    FFT = fft(data)                                # Returns an array of complex numbers
    freqs = fftfreq(BUFFER_SIZE, 1.0/SAMPLE_RATE)  # Returns an array containing the frequency values

    y = abs(FFT[0:len(FFT)/2])/1000                # Get amplitude and scale
    y = scipy.log(y) - 2                           # Subtract noise floor (empirically determined)
    return (freqs,y)

freqs,y = get_fft(fdata)

Note that only half of the resulting array has unique data, as the other half is a mirror.

The FFT is then averaged into smaller chunks, which makes it easier to display and process.

# Average into chunks of N
N = 200
yy = [scipy.average(y[n:n+N]) for n in range(0, len(y), N)]
yy = yy[50:]  # Discard half of the samples, as they are mirrored

Sidenote: You could probably skip using FFT and just take the absolute average of the signal, depending on how you want the system to react.

RGB Algorithm

The loudness level is determined using a threshold detection system

def thresh(val, threshold):
    val -= threshold
    if val < 0: val = 0
    val *= (1.0/threshold)
    return val

CHANNEL = 1      # which FFT frequency to respond to
GAIN = 1.5       # audio gain
THRESHOLD = 0.15 # audio trigger threshold

loudness = thresh(yy[CHANNEL] * GAIN, THRESHOLD)

The loudness level is based on an attack-decay system, in that it will decay over quiet periods, and build up over louder periods.

ATTACK = 0.004  # amount of increase with loudness
DECAY = 0.003   # amount of decay

noisiness -= DECAY
noisiness += loudness * ATTACK
noisiness = limit(noisiness, 0.0, 1.0)

The noisiness value is now the mapping that will decide which hue to use. I wanted it to spend less time at the red end, and more time in the green/yellow end, so I applied a power function to it:

mapping = (10 ** limit(noisiness, 0.0, 1.0)) / 10.0                 # Power function
mapping = mapping * 1.1 - 0.11                                      # Linear correction

Finally this is turned into a hue value

MIN_HUE = 200  # Aqua
MAX_HUE = 0    # Red
hue = mapval(limit(noisiness, 0.0, 1.0), 0.0, 1.0, MIN_HUE, MAX_HUE)

Where mapval() is a linear mapping between values, and limit() truncates values outside the specified values. See the source code for the actual implementation of it.

Now we can update the LEDs

red,green,blue = hsv2rgb(hue,1.0,brightness)
rgb_update([int(red),int(green),int(blue)])

I obtained hsv2rgb from here: Python RGB and HSV Conversion

Communication

I'm using a very simple protocol, and pyserial to communicate with the arduino.

import struct
import serial

ser = serial.Serial('COM3') # Change as appropriate

def tobyte(i):
    i = int(i)
    if i < 0: i = 0
    if i > 255: i = 255
    return i

def rgb_update(color): 
    r = tobyte(color[0])
    g = tobyte(color[1])
    b = tobyte(color[2])
    packet = struct.pack('cBBB', 'X', r,g,b)
    #print packet
    self.ser.write(packet)

Footnote

If you'd like to know more about this project, feel free to contact me!

Complete source code is available on github:

jorticus/audiovis


comments powered by Disqus