LED lighing project

Hi everyone,

I’m building a volumio system from a 1950s HMV radiogram, and the next stage is building a dynamic lighting effect similar to this: http://julip.co/2012/05/arduino-python-soundlight-spectrum/

I don’t have a sound card as yet, and am therefore using the audio jack output to drive my speakers. In order to process the audio data to control the lighting, I have created a audio LOOPBACK with a custom /etc/asound.conf file.

After much tweaking this works fine, playing wave files from the command line aplay test.wav . However, Volumio doesn’t appear to read the same /etc/asound.conf file, so I don’t get this looped back to my lighting script.

Any suggestions on how to do this? Can I write a custom preset for the Volumio UI?

Also, whilst working on this the serial connection to my Arduino appears to have gone down. The rpi and Arduino scripts appear to be running fine, but without any communication.

I will share all code and photos soon.

2 Likes

You have to configure Volumio to use the Loopback device as output. I suggest you to have a look in the plugin “volumio simple equalizer” to see how it can be done. :wink:

1 Like

Thank you very much balbuze, that was a great help.

I made some minor changes to the “volumio simple equaliser” plugin and it all works great together.

So the setup so far is:

  • a python script running on the raspberry pi, picks up the audio stream via a loopback
  • the audio is processed ‘near real-time’ using numpy’s fft package. This needs much more work to make the effect more dramatic.
  • the pi sends a byte of data for each LED over a serial link to an arduino mega
  • the arduino reads the data from the pi and sets PWM outputs to the appropriate values
  • the LEDs are hidden between and behind the old vacuum tubes from the radio, and are powered by the arduino outputs.

This all needs much improvement, but the idea works quite well. I’ll post all the code when I’ve tidied it up a little, and a video when I’m sure I’m not going to fall foul of music copyright infringements.

Thanks again!

20181027_163520.jpg

1 Like

Thanks for this presentation.

Would you be possible to share your plugin based on the “volumio simple equaliser”?

Hi David,

Thank you for your interest in this. Unfortunately I’ve disassembled the set-up intending to improve on it, but haven’t managed to put it all together again.

Is there specific items of code you would like?

Hi Colin,

Thanks for your quick enswer!
I want to use a WS2812B LED pixel stip to react from the music like this :https://github.com/scottlawsonbc/audio-reactive-led-strip

I would like to start from your implementation to see how Can I integrate and control a LED strip.

thanks.

Unfortunately I cannot remember exactly how I configured everything, but I have managed to locate the configuration files.

The first thing was to amend the asound.conf file to:

.asoundrc

ctl.equal {
type equal

}
pcm.plugequal1 {
type equal
slave.pcm “plughw:0,0”
#slave.pcm “turntable2” #no sound
}

pcm.equal {
type plug;
slave.pcm plugequal1;
#slave.pcm “turntable” #no difference
}

pcm.splitter1 {
type softvol
slave.pcm mono
control { name “PCM”; card 0;}
max_dB 50.0
}

pcm.mono {
type route
slave.pcm splitter2
ttable.0.0 1
ttable.1.1 1
}

pcm.splitter2 {
type plug;
slave.pcm {
type multi;
slaves.a.pcm “plugequal1”;
#slaves.a.pcm “turntable”; #no sound
slaves.b.pcm “loopout”;
#slaves.b.pcm “turntable2”; #no difference
slaves.a.channels 2;
slaves.b.channels 2;
bindings.0.slave a;
bindings.0.channel 0;
bindings.1.slave a;
bindings.1.channel 1;
bindings.2.slave b;
bindings.2.channel 0;
bindings.3.slave b;
bindings.3.channel 1;
}

ttable.0.0 1;
ttable.1.1 1;
ttable.0.2 1;
ttable.1.3 1;

}

pcm.output {
type hw
card ALSA
}

pcm.loopin {
type plug
slave.pcm “hw:Loopback,0”
#slave.pcm “turntable”
}

pcm.loopdevice {
type hw
card Loopback
device 1
}

pcm.turntable {
type hw
card Controller
device 0
}

pcm.turntable2 {
type plug
#slave.pcm turntable
#slave.channels 1
#ttable.0.0 0.5
#ttable.0.0 0.5
#ttable.1.0 0.5
#ttable.1.0 0.5
#type dsnoop
slave.pcm turntable
#slave.channels 2
#slave.period_size 1024
#slave.buffer_size 4096
#slave.rate 44100
#slave.periods 0
#slave.period_time 0
}

pcm.loopout {
type plug
slave.pcm “loopdevice”
#slave.pcm “turntable”
}

Then I wrote a python script to read the audio and produce an output.

#!/usr/bin/env python3
# BEGIN INIT INFO
# Provides:          dickie_audio_service.py
# Required-Start:    $all
# Required-Stop:     $remote_fs $syslog
# Default-Start:     5
# Default-Stop:      0 1 6
# Short-Description: LED lighting script
# Description:       DICKIE innovation addon to volumio Simple Equaliser Plugin
### END INIT INFO
# -*- coding: utf-8 -*-
"""
ÐICKIE innovation

Created on Tue Oct 23 20:55:56 2018

Copyright Dickie Innovation Ltd
"""


import numpy
import math
import pyaudio
import wave
import serial
import sys
import time
import struct
import datetime

freq_low = 20 #Hz
freq_high =20000 #Hz



scale      = 50    # Change if too dim/bright
exponent   = 5     # Change if too little/too much difference between loud and quiet sounds



main_exit = False

def serial_init():
    print('Openning port at {0} baud'.format(19200))
    try:
        ser = serial.Serial('/dev/ttyACM0',19200, timeout=1 )
        print('\tOpen on port /dev/ttyACM0')

    except serial.SerialException:
        ser = serial.Serial('/dev/ttyACM1', 19200, timeout=1 )
        print('\tOpen on port /dev/ttyACM1')
        
    time.sleep(1)
    if not ser.isOpen():
        ser.open()

    ser.flushInput()
    ser.flushOutput()
    ser.write(b'\n')
    #inline = ser.readline()
    return ser

class Lighting():
    def __init__(self, serial_link):
        self.serial_link = serial_link
        self.chunk_n     = 11
        self.chunk       = 2**self.chunk_n # Change if too fast/slow, never less than 2**11    
        self.device      = 1
        self.samplerate  = 22050 #44100
        self.band_no     = 8
        self.frame_count = 0

        self.freqs = numpy.fft.fftfreq(self.chunk,1/self.samplerate)
        self.idxs = numpy.argsort(self.freqs)
        self.drange = 1600
        #self.drange[1] *= 0
        #self.drange[2] *= 100
        try:
            print('\nOpenning Device:', self.device)
            self.p = pyaudio.PyAudio()
            self.dev = self.p.get_device_info_by_index(self.device)

            print('No of Input channels', self.dev['maxInputChannels'])
            print('No of Output channels', self.dev['maxOutputChannels'])
            self.stream = self.p.open(format= pyaudio.paInt16,
		    #channels=dev['maxInputChannels'],
		    channels=1,
		    rate=44100,
		    input=True,
		    frames_per_buffer = self.chunk,
		    input_device_index = self.device,
		    stream_callback=self.callback)
        except:
            self.device = 2
            print('\nOpenning Device:', self.device)
            self.p = pyaudio.PyAudio()
            self.dev = self.p.get_device_info_by_index(self.device)

            print('No of Input channels', self.dev['maxInputChannels'])
            print('No of Output channels', self.dev['maxOutputChannels'])
            self.stream = self.p.open(format= pyaudio.paInt16,
		    #channels=dev['maxInputChannels'],
		    channels=1,
		    rate=44100,
		    input=True,
		    frames_per_buffer = self.chunk,
		    input_device_index = self.device,
		    stream_callback=self.callback)

        start = datetime.datetime.now()
        self.stream.start_stream()

        while self.stream.is_active() :
            time.sleep(1)
        

        end = datetime.datetime.now()
        rate = (end-start).seconds/self.frame_count
        print('elapsed {0:5.4f} seconds per run, or {1:5.2f}Hz'.format(rate, 1/rate))
        print('total count', self.frame_count)
        #print('min/max', self.drange[1:])
   
    def callback(self,in_data, frame_size, time_info, status):
        self.frame_size = frame_size
        #print('current status', status, len(in_data))
        #data = wf.readframes(frame_count)
        self.data = in_data
        #print('frame count', frame_count)
        self.calculate_levels()
        levels_list = self.levels.tolist()
        if status == pyaudio.paComplete:
            print('pyaudio terminated')
        elif status == pyaudio.paAbort:
            print('pyaudio aborted- ignoring')
            status = pyaudio.paContinue
        if sum(levels_list) == 0:
            print('levels zero')
            main_exit = True
            status = pyaudio.paComplete
        levels_list.append(10)
        #print(levels_list)
        self.serial_link.write(bytearray(levels_list))
        #inline = ser.readline()[:-2]
        return (self.data, status)
    
    def calculate_levels(self):
        # Use FFT to calculate volume for each frequency
        datalen = len(self.data)
        # Convert raw sound data to Numpy array
        fmt = "{0}H".format(datalen//2)
        data2 = struct.unpack(fmt, self.data)
        data2 = numpy.array(data2, dtype=numpy.int32)

        # Apply FFT
        fourier = numpy.fft.fft(data2)
        #print(len(data),fourier.shape, chunk)

        gaps = [0] + [2**x for x in range(self.chunk_n-self.band_no, self.chunk_n)]
        fft = numpy.abs(fourier[:self.chunk]) #/ (128*chunk)
        fft = fft[self.idxs]
    
        levels = numpy.zeros((self.band_no+4), dtype=numpy.int32)
        for band in range(self.band_no):
            levels[band] = numpy.mean(fft[gaps[band]:gaps[band+1]])
            levels[band] = numpy.max([1,levels[band]])
        levels[8] = levels[0]
        levels[9] = levels[1]
        levels[10] = levels[0]
        levels[11] = levels[2]
        levels_a = (numpy.log(levels)-12)*150
        #print([int(a) for a in levels_a])
        #self.drange[1] = numpy.min(self.drange, axis=0)
        #self.drange[2] = numpy.max(self.drange, axis=0)

        #range_running = self.drange[2]-self.drange[1]
        #levels = (levels_a)*255/self.drange
        levels = numpy.array(levels_a, dtype=numpy.int16)
        levels[levels<10] = 0
        levels[levels>255 ] =255
        
        #levels = levels.astype(numpy.uint8)
        levels[levels==10] = 9
        self.levels = levels
        self.frame_count +=1
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('stopping stream')
        self.stream.stop_stream()
        self.stream.close()
        self.p.terminate()
        del self.stream
        del self.p
        #del self.callback



if __name__ == "__main__":
    ser  = serial_init()
    while True:
        with Lighting(ser) as buray:
            while buray.stream.is_active():
                print('Buray running')
                time.sleep(1)

Which is run as a service as soon as the raspberry Pi/ Volumio is started:

[Unit]
Description=DICKIEs lighting
After=volsimpleequal.service

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/volumio/dickie_audio_service.py > /home/volumio/log.text 2>&1
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=volumio
User=volumio
Group=volumio

[Install]
WantedBy=multi-user.target

I hope this helps.

This is based on an idea that’s been floating around in my head for a while

Did you manage to end this project? I know someone who has had a similar project, but he wasn’t able to do it.

He said the old radiogram just doesn’t fit for that. However, the radiogram that he tried to use was much older than the one you tried to use. He actually tried to do something similar to these lights https://www.vont.com/product/smart-strip-lights-led-strip-lights/, using a newer radiogram from the 1980s, and it worked much better. He even bought a newer smart strip just to take it as a sample.