I made Audio Reactive Xmas Lights and you should too!

Audio Reactive Xmas Lights

December is here. Happy Holidays Everyone!

:christmas_tree: :confetti_ball: :christmas_tree: :tada: :christmas_tree:

Let’s all make our Christmas lights audio reactive in the most over designed way possible.
Today we’re going to listen to and real time analyze some music :loud_sound:, learn about my love/hate relationship with OSC protocol :level_slider:, and punch out some sweet sweet 5V power to an LED String! :sparkler: :fireworks:

DEMO VIDEO!

What did I do?

* Controlling standard 4.5v Xmas lights that flicker to music.*

  1. Music is sent from my computer to my sound system.
  2. A duplicate of the music is sent to MaxMSP for analysis.
  3. MaxMSP checks for “Transient Events” in the music and expresses that as an integer.
  4. MaxMSP pumps out the int as a 10bit value via WIFI using OSC protocol.
  5. A Raspberry PI 400 listens for the OSC packets.
  6. On receiving a packet, the PI400 sets a GPIO pin to HIGH if the 10bit int is above a threshold.
  7. The GPIO pin is connected to the base of a transistor which opens the emitter.
  8. The emitter in connected to fairy lights positive pin, completing a circuit, and turning them on.

How did I do it?

You will need:

  1. Fairy lights! :fairy: I just found one that used 3 AA batteries and snipped off the battery case exposing the wires.
  2. Some kind of micro controller or SBC with at least one controllable digital GPIO pin.
  3. A computer for playing and analyzing the audio.
  4. A router (wifi optional).
  5. A BC639 Transistor or similar.
  6. A large capacitor (at least 1000uf).
  7. An LED (for testing).
  8. A 100ohm resistor (for testing).
  9. A small breadboard (or not, you could just freestyle solder like the champ you are).
  10. Some jumpers or a some female to male header pins.

There are three ingredients in this potion:

  1. The analyser and OSC transmitter (I used my desktop computer and MAXMSP).
  2. The listener (I used a PI400 with a super simple rust script).
  3. The circuit for driving the Leds.

Let’s look at each of them in turn.

The MaxMSP patch.

Intuitively us monkeys can feel the pulse of music. We can tell when a “musical moment” is important and when it’s filler. We want our lights to flicker to the pulse and to capture the subtle feeling of the music.

Lights that just pulse to the loudness of a signal betray our musical expectations and come off feeling technical rather than sublime :black_heart: That’s not what we want.

Problem is audio signals are weak and boring and it’s hard to extract the pulse out of it. The method I’ll be using today is called a discrete transient follower. It’s very simple and robust because it narrows its focus to changes in just a small part of our spectrum. Today, I’m going to be looking to isolate the percussive elements of snare and bass from our wave form. :musical_note:

This patch below is just asking for the difference between each successive sample in a signal. A sample is just a number that represents the amplitude of an audio wave at a specific moment.
The sample rate matters here since faster rates will lead to finer difference, so we need some scaling to compensate. (I’m running on 48Ksamps/s).

Here is the equation this patch compiles to.

formula
Where
k is a sample of audio
a is the scaling factor,
b is the cut off frequency of the low pass filter.

The rampsmooth~ is just a finesse that holds the output signal at level for 900 samples.
After that, the rest is just data-wrangling our signal output to a 10bit binary for OSC. Easy!

udpsend is the object responsible for sending the OSC message. Its first argument is the IP4 address you’re sending to. If you’re not sure what the IP4addr of your receiving device is, a good bash command to be familiar with is ip addr which displays all your IP info.

OSC commands come in three parts. First, an address, second a type, and lastly a value.
e.g /oscillator_amp/float/0.32.
Max handles the type, but it is still good practice to prepend an address and check for that address in code at the raspberry pi (or whatever micro-controller is listening for the packets).

Alternative to MaxMSP in python

Note: MaxMSP was my first love but it is proprietary software. I just used it because It’s fast, easy, and I was familiar with it.

Everything done here could be done with SciPy, JUCE or even Audacity.
All you need is a way to filter the audio to isolate the spectrum you’re interested in and a way to take the derivative of the audio vector. All of the above libraries can do that.
Here is an example in python :dragon_face:. Tested and beautiful.

If you’re looking for quick and dirty I see no reason why you couldn’t loop through the samples of your audio and check for custom keyframes. Then just punch the keyframes over OSC. Voila! :fr:
Here is some untested dummy python code showing how I’d approach it. :small_airplane:

import pyaudio
import wave

# Setup
chunk = 1024  
wf = wave.open(filename, 'rb')
p = pyaudio.PyAudio()

# Open a .Stream object to write the WAV file to play
stream = p.open(format = p.get_format_from_width(wf.getsampwidth()),
                channels = wf.getnchannels(),
                rate = wf.getframerate(),
                output = True)

# Read data in chunks
data = wf.readframes(chunk)

# Play the sound by writing the audio data to the stream
while data != '':
    stream.write(data)
    data = wf.readframes(chunk)

   #DETECT ZERO CROSSING AS AUDIO PLAYS!
    for samp in chunk:
      if samp == 0.0 then:
        print("Detected Zero Crossing. Send 1023 via OSCpacket")
      else:
        print("Release GPIO")
   #Maybe you could also check the derivative here?

# Close and terminate the stream
stream.close()
p.terminate()

Code for OSC receiver and GPIO management. (Raspberry Pi).

Notes : The code only has two parts :

  1. Listening and parsing an OSC Packet.
  2. Setting a GPIO pin.

I know many people on this community prefer python and I am confident both 1.OSC and 2.GPIO can be re-written in python with no noticeable performance loss.

If this is your first time working with OSC I wrote a simple OSC server in python below and you’re welcome to use it for inspiration.

If this your first time working with the GPIOs on Raspberry PI the Core Electronics team have you covered with a guide below.

If you do port the below code to python I’d love to see it! :partying_face:
I’m also happy to try and help if you need assistance.

For those who want to plug in and play here is all the rust code you need.

:crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab: :crab:

I’ve left comments on the code so you can follow along.
Here we get an IP4 to listen to from the command line arguments, open up an OSC client on that socket, and the parse any incoming packets for our GPIO pins.

/*Cargo.toml*/

[package]
name = "osc_gpio_xmas23"
version = "0.1.0"
edition = "2021"

[dependencies]
rosc = "~0.10"
gpio = "0.4.1"
ctrlc = "3.4.1"
/*main.rs*/

use rosc::OscPacket;
use rosc::OscMessage;
use rosc::decoder::MTU;
use gpio::GpioOut;
use std::env;
use std::net::{SocketAddrV4, UdpSocket};
use std::str::FromStr;
use std::any::type_name;
use ctrlc;

#[allow(dead_code)]
fn type_of<T>(_: T) -> &'static str {
    type_name::<T>()
}

fn get_args() -> Vec<String> {
    let args: Vec<String> = env::args().collect();
    // Check to see if we actually... gave arguments
    if args.len() < 2 {
        println!("Required: valid osc address as first commandline argument");
        ::std::process::exit(1)
    }
    return args;
}

fn extract_oscmsg(packet: OscPacket) -> OscMessage {
    match packet {
        OscPacket::Message(msg) => {
            return msg;
        }
        OscPacket::Bundle(bundle) => {
            panic!("OSCBundles not implemented -- received OSC Bundle: {:?}", bundle);
        }
    }
}

fn parse_oscarg_from_maxmsp_asf32(msg: &OscMessage) -> Result<f32, &str> {
    /* Expect
        1. Keyword "fromMaxMsp"
        2. A signed 32 bit float. 
        3. Between -1024 and +1024 */

    let bad_addr : &str =  "Received OscMessage from unrecognised address";
    let bad_args : &str = "No argument provided";
    let not_f32 : &str = "Argument must be a 32bit signed floating point number";
    let not_inrange : &str = "Argument expected to be between -2^10 and +2^10";

    if msg.addr.trim_start_matches('/') != "fromMaxMsp" {
        return Err(bad_addr);
    }
    if msg.args.is_empty() {return Err(bad_args)}

    match msg.args[0].clone().float() {
        Some(f) => match f {
            x if (0.0..1024.0).contains(&x) => return Ok(f), 
            _ => return Err(not_inrange),
        }
        None => return Err(not_f32),
    }
}

fn is_extremerange_f32(f : f32, r : f32)  -> bool {
    let is_high : bool = f > 1024.0 * r;
    let is_low : bool = f < -1024.0 * r;
    return is_low | is_high;
}

fn main() {
    // Kill GPIOs on Keyboard Interupt
    ctrlc::set_handler(move || {
        let mut gpio23 = gpio::sysfs::SysFsGpioOutput::open(23).unwrap();
	    let mut gpio18 = gpio::sysfs::SysFsGpioOutput::open(18).unwrap();
        gpio23.set_value(false).expect("could not set gpio23");
	    gpio18.set_value(false).expect("could not set gpio18");
        println!("\r\nGoodbye");
	    ::std::process::exit(1);
    }).expect("Error setting Ctrl-C handler");

    // Open GPIOs
    println!("Opening GPIO Ports");
    let mut gpio23 = gpio::sysfs::SysFsGpioOutput::open(23).unwrap();
    let mut gpio18 = gpio::sysfs::SysFsGpioOutput::open(18).unwrap();
    let mut value : bool = false;

    // Open a small buffer to receive inputs
    println!("Opening mutable buffer for reading incoming data");
    let mut buf = [0u8; MTU];

    // Attempt to parse first cmd arg as IPv4 
    let addr : SocketAddrV4 = match SocketAddrV4::from_str(&get_args()[1]) {
        Ok(addr) => addr,
        Err(_) => panic!("Invalid IPAddrV4"),
    };

    // Open a Udp listener
    let sock : UdpSocket = UdpSocket::bind(addr).unwrap();
    println!("Listening to {}", addr);


    loop {
        // Listen for an extract an osc message
        let packet : OscPacket;
        match sock.recv_from(&mut buf) {
            Ok((size, addr)) => {
                print!("Received Packet! size: {} from: {} ", size, addr);
                let (_, pk) = rosc::decoder::decode_udp(&buf[..size]).unwrap();
                packet = pk;
            }
            Err(e) => {
                panic!("Error receiving from socket: {}", e);
            }
        }
        let msg : OscMessage = extract_oscmsg(packet);

        /* We are specifically looking for integers from max so we match on that.
            Unless we are debugging, I'm happy to discard any incorrect messages */
        match parse_oscarg_from_maxmsp_asf32(&msg) {
            Ok(n) => {
                print!("addr: {} value: {} ", msg.addr, n);
                // The second argument is the fudge factor here. Try stuff till it works.
                value = is_extremerange_f32(n, 0.72);
                println!("GPIO: {}", value);
            }
            Err(e) => println!("{}",e),
        }

        gpio23.set_value(value).expect("could not set gpio23");
	    gpio18.set_value(value).expect("could not set gpio18");
    }
}
#to use (bash)
cargo run - IP4tolisten:channel

Circuit Diagram. :zap:

Note: swapping C1 for a higher value capacitor will extend the decay time of the fairy lights. I think that’s because the cap is charging almost instantaneously from the emitter of the BC639 but discharging slowly because the Light Emitting Diodes are kinda acting as a resistor.
D1 and R1 are just for debugging.


401774093_7614125775281515_1870917349062046236_n-Animated Image (Small)

With thanks :slight_smile:

If you make this please show me. I’d love to see it.
Thanks to all the people on this community that take the time to answer my questions.
Have a merry xmas :christmas_tree:!

Pix :heavy_heart_exclamation:

5 Likes

Hi Pix,

Always love seeing your work on here. Many thanks to you too for tirelessly helping others here also!

3 Likes

Hi Pix
Looking at your schematic from an analog point of view I think there are a couple of things you should look at.

They both involve current limiting.
You have to consider C1 in the discharged condition. At Q1 switch on it will be a short circuit to ground.
From the data sheet Q1 has a continuous rating of 1A so I think you should include a 5Ω or a bit more resistor in the collector circuit or the transistor might not survive more that a couple of switch ons.
With C1 discharged, at Q1 switch on the base will only be 0.6V above ground. I don’t know what the RP44 I/O pin current capability is but it certainly won’t be anything like the base current of Q1 without a limiting resistor. I think the RP I/O is 3.3V so your resistor will have to drop 2.7V at whatever you want the RP I/O current to be. I would suggest you work on about half or a bit less of the rated maximum current for safety reasons. No need to stress the I/O any more than necessary.
A few Ω in series with your fairy lights might not go astray either. Depends on the characteristics and how they are wired. Might be OK as they are but trial will tell.
Cheers Bob

EDIT:
If the transistor has a problem switching fully on with a suitable base resistor (hfe too low) you could consider a Mosfet in place of it. and put your fairy lights in the drain circuit and switch the low side (source) to ground. In other words a low side switch. You would need about 1kΩ resistor in series with the gate. The thing you would miss however is the decaying effect due to C1 discharge

5 Likes

Hi Robert.

Thanks for your reply. I think I learned alot.

So I need…

  1. A resistor before my transistor to protect my GPIO ports.
  2. A resistor before my capacitor to protect against short circuits.
  3. A tiny resistor in series with the fairy lights.

Here is my updated diagram below. How did I do?

2 Likes

Hi Pix
You could try this but I think you will still have a problem using a transistor. It will not simply switch 5V on to the LEDs. The emitter can only get to Base voltage minus 0.6V which would be 3.3 - 0.6 = 2.7V. It can’t go any higher than this.

I think your solution is to replace the transistor with a high side switch. You can roll your own using that transistor and a P channel Mosfet or I am pretty sure Core have a ready made device. BUT unless you know exactly what is called it is a nightmare trolling looking for it. Perhaps if Core are monitoring this they might be able to help out.

Leave the capacitor, faith lights and resistors as is just replace the transistor with the switch.

Alternatively you could just transplant that whole network of cap, lights and resistors just as is to the collector side of the transistor then you would get the (almost) whole 5V across the lights/cap combination. This converts the transistor to a low side switch (at the moment it is an emitter follower)
Cheers Bob

3 Likes

OH!!! That’s such a cool idea. I’ll try that tonight :slight_smile:

3 Likes

Hi Pix
Like this

Keep the polarity the same. Junction of Fairy Lights and R4 to 5V and the bottom of the network to collector. Emitter to ground.
Cheers Bob

2 Likes

Like this?

3 Likes

Yes

But you have omitted R2. I don’t know what these LEDs are but they might need some current limiting
Cheers Bob

5 Likes

Reporting back.

I did prototype and experiment with bobs suggestion and the new design above.

Worked great!

I personally found that the decay time of the leds (i,e, the discharge period of the capacitor), was a little longer for some reason.
I also felt that including the resistor in series with the LEDs made them too dim for my liking so I have left it off.
Bobs advice is good advice, and I’d recommend implementing it.
My LEDS are going to be sacrificed in the same of brightness. The LEDs certainly won’t last to Christmas and I’m ok with that.

3 Likes

Hi Pix

Probably because it would charge to nearly 5V instead of about 2.6V.

If the decay time is too long, reduce the size of the capacitor.
Cheers Bob

4 Likes