LED Music Cube

An audio receiver with support for Bluetooth (A2DP), AirPlay, and UPnP that visualizes the spectrum on a LED cube.

The setup is based on a Raspberry Pi running Raspbian Stretch as a headless audio sink and an Arduino used to control an RGB LED cube. This will allow your phone, laptop or any other device to play audio wirelessly while having the audio spectrum visualized on the LEDs.

Components

  • An old Raspberry Pi 1 Model B
  • Keyestudio Ks0177 RGB LED CUBE KIT
  • Ednet USB 2.0 Hub with 4 Ports
  • Bluetooth dongle
  • WiFi dongle
  • Push button

Assembling

I started with the LED cube following the instructions from the manufacturer. After that I made a case for the other parts out of wood and added the cube on top.

Case assembling

Software

Basic setup

As a base I installed the latest Raspbian Stretch Lite and made sure the system is up to date using the following commands

sudo apt-get update
sudo apt-get dist-upgrade

Then I rebooted the Pi to ensure the latest kernel is loaded

sudo reboot

AirPlay

I installed Shairport Sync using the following command

sudo apt-get shairport-sync

and changed the name parameter in /etc/shairport-sync.conf. By default Shairport Sync will use the ALSA backend and the default output device.

UPnP

I installed GMediaRenderer using the following command

sudo apt-get gmediarender

and changed some parameter in /etc/default/gmediarender as follows

ENABLED=1
DAEMON_USER="pi:audio"
UPNP_DEVICE_NAME="Sound Dude [UPnP]"
ALSA_DEVICE="default"
DAEMON_EXTRA_ARGS="--gst-debug-level=3"

A2DP

I installed BlueALSA and the python dbus interface

sudo apt-get install bluealsa python-dbus

Then I changed some parameters in /etc/bluetooth/main.conf. First the name

Name = Sound Dude [Bt]

Then I set the device class to a Loudspeaker

Class = 0x200414

Since this is a headless setup the device has to stay discoverable forever so I set

DiscoverableTimeout = 0

I found out that the SAP and Hostname plugin overwrite the device class and the name so I had to disable them in /lib/systemd/system/bluetooth.service by adding

ExecStart=/usr/lib/bluetooth/bluetoothd --noplugin=hostname,sap

I also had to enable the a2dp audio sink in /lib/systemd/system/bluealsa.service by adding

ExecStart=/usr/bin/bluealsa -p a2dp-sink 

Then I enabled discovery on the bluetooth controller by executing

sudo bluetoothctl
power on
discoverable on
exit

A2DP Bluetooth Agent

A bluetooth agent is a piece of software that handles pairing and authorization of bluetooth devices. The following agent allows the Raspberry Pi to automatically pair and accept A2DP connections from bluetooth devices. All other bluetooth services are rejected.

I created /usr/local/bin/a2dp-agent and made the file executable with

sudo chmod +x /usr/local/bin/a2dp-agent

To make the A2DP bluetooth agent run on boot I added a systemd service /etc/systemd/system/bt-agent-a2dp.service and enabled it with

sudo systemctl enable bt-agent-a2dp.service
sudo systemctl start bt-agent-a2dp.service

I was now able to pair and connect to the Raspberry Pi without any user intervention.

A2DP Audio Playback

To forward audio from the bluetooth device to the ALSA I used bluealsa-aplay. To make the tool run on boot I added a systemd service /etc/systemd/system/a2dp-playback.service and enabled it with

sudo systemctl enable a2dp-playback.service
sudo systemctl start a2dp-playback.service

Audio Visualizer

First, I installed git

sudo apt-get install git

I then cloned and build C.A.V.A. following the official documentation on the GitHub page.

To capture audio I created an ALSA loopback interface by adding snd-aloop to /etc/modules. To prevent it from being loaded as the first soundcard I added the line options snd-aloop index=1 enable=1 pcm_substreams=4 id=Loopback to /etc/modprobe.d/alsa-base.conf.

Playing audio through the Loopback interface makes it possible for cava to to capture it, but there will be no sound in your speakers. In order to play audio on the loopback interface and the actual interface I used an ALSA multi channel configured in /etc/asound.conf as follows

pcm.multi {
    type route
    slave.pcm {
        type multi
        slaves {
            a { channels 2 pcm "output" }  # the real device
            b { channels 2 pcm "loopout" }  # the loopback driver
        }
        bindings {
            0 { slave a channel 0 }
            1 { slave a channel 1 }
            2 { slave b channel 0 }
            3 { slave b channel 1 }
        }
    }
    ttable [
        [ 1 0 1 0 ]   # left  -> a.left,  b.left
        [ 0 1 0 1 ]   # right -> a.right, b.right
    ]
}

pcm.!default {
    type plug
    slave.pcm "multi"
}

pcm.output {
  type hw
  card 0
  device 0
}

pcm.loopout {
  type plug
  #ipc_key 1234
  slave.pcm "hw:Loopback,0,0"
}

pcm.loopin {
  type plug
  #ipc_key 1235
  slave.pcm "hw:Loopback,0,1"
}

With this setup I can send audio to the default interface and capture from hw:1,1.

RGB LED Cube

Using the Arduino IDE I flashed rgb_cube.ino on the driver board. The code creates a simple serial interface to control the LEDs and accepts a COBS encoded byte array containing the RGB values of each LED. I used PacketSerial to decode the data and Colorduino to control the LEDs.

On the Raspberry Pi I created a python script (main.py) that starts a C.A.V.A. subprocess to captures audio from the loopback device and outputs binary data to stdout. The audio spectrum data gets send through the serial interface to the Arduino. I also added a systemd service /etc/systemd/system/rgb-cube.service to run the script on boot and enabled it with

sudo systemctl enable a2dp-playback.service
sudo systemctl start a2dp-playback.service

Push Button

To shutdown the Raspberry Pi with the button I used the gpio-shutdown overlay. To enable it I added

dtoverlay=gpio-shutdown

to /boot/config.txt.

Result

All the referenced code can be found on my GitHub page.

Previous Post Next Post