This is an audio visualizer with an independent Bluetooth 5.0 audio module, both powered from a 5V3A charger. The visualizer use 45 NeoPixels (WS2812 RGB LEDs) and a 0.96" OLED to display strength levels at high/middle/bass frequencies, read by a MSGEQ7 analyzer module. It can be connected to any external speakers with a 3.5mm jack.
Everything is fitted inside the inner space of a 2004 Hasbro Millennium Falcon toy set, replacing rear engine lights and add cab/interior lights. The original front and laser LEDs are also connected to the SAMD21 microcontroller. Also, a MP3 playback module connects to the onboard 0.5W 8 Ohms speaker to play start-up/shutdown sound effects. A relay module turns the MP3 module off when it's not needed.
A capacitive touch sensor installed in the cockpit serves as the start-up/shutdown switch. Two potentiometers are used to adjust the levels of NeoPixel brightness level and onboard speaker volume.
The code is written in TinyGo, Golang for small places, and make use of TinyGo drivers as well as TinyDraw. The microcontroller is loaded with a Adafruit UF2 bootloader.
Thanks for DFRobot contacted me and send me samples to complete this project. Check out my post on Hascker.io for more details.
// The Millennium Falcon Audio Visualizer
// with TinyGo (written under TinyGo 0.16.0/Golang 1.15.8)
// by Alan Wang
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/ssd1306"
"tinygo.org/x/drivers/ws2812"
"tinygo.org/x/tinydraw"
)
const (
mainNeoPin = machine.D3
innerNeoPin = machine.D5
cabNeoPin = machine.D6
laserLEDsPin = machine.D7
frontLEDsPin = machine.D9
touchPadPin = machine.D10
audioStrobePin = machine.D11
audioResetPin = machine.D12
audioOutputPin = machine.A0
neoLevelPin = machine.A1
m3PowerPin = machine.A2
m3BusyPin = machine.A3
m3PlayPin1 = machine.A4
m3PlayPin2 = machine.A5
mainNeoNum = uint8(32)
innerNeoNum = uint8(12)
cabNeoNum = uint8(1)
padSkipTime = int64(750)
)
// NeoPixels struct
type NeoPixels struct {
neo ws2812.Device
num uint8
colors []color.RGBA
}
// MSGEQ7 autio analyzer struct
type MSGEQ7 struct {
strobe machine.Pin
reset machine.Pin
output machine.ADC
value [7]uint16
}
var (
display ssd1306.Device
mainNeo NeoPixels
innerNeo NeoPixels
cabNeo NeoPixels
audioAnlz MSGEQ7
touchPad = touchPadPin
neoLevel = machine.ADC{neoLevelPin}
)
func main() {
delayms(5000)
initialize() // Golang's init() dosen't work in TinyGo
for {
// waiting for starting up visualizer
for !touchPad.Get() {
cabNeo.fill(color.RGBA{R: 0, G: 32, B: 32})
cabNeo.show()
delayms(5)
}
timeStart := time.Now()
for touchPad.Get() {
}
timeEnd := time.Now()
if timeEnd.Sub(timeStart) < time.Millisecond*time.Duration(padSkipTime) {
startup()
} else {
startupSkipped() // skip startup effects if user pressed the pad long enough
}
var pos uint8
var cycle bool
for {
// read and convert audio level
audioAnlz.read()
currentNeoLevel := neoLevel.Get()
for i := 0; i < 7; i++ {
print(audioAnlz.value[i], " ")
}
println("")
// display audio level on NeoPixels
if cycle {
mainNeo.fillRange(wheel(pos+18, audioAnlz.value[3], currentNeoLevel), 0, 9)
mainNeo.fillRange(wheel(pos+9, audioAnlz.value[4], currentNeoLevel), 10, 22)
mainNeo.fillRange(wheel(pos, audioAnlz.value[2], currentNeoLevel), 22, 31)
innerNeo.fillRange(wheel(pos+85+9, audioAnlz.value[1], currentNeoLevel), 0, 5)
innerNeo.fillRange(wheel(pos+85, audioAnlz.value[0], currentNeoLevel), 6, 11)
cabNeo.fill(wheel(pos+85+18, audioAnlz.value[5], currentNeoLevel))
} else {
mainNeo.show()
innerNeo.show()
cabNeo.show()
pos++
}
// display audio level on SSD1306
display.ClearBuffer()
for i := int16(0); i < 7; i++ {
tinydraw.FilledRectangle(&display, i*18+2, 0, 16, int16(audioAnlz.value[6-i]/1024), color.RGBA{255, 255, 255, 255})
}
display.Display()
delayms(5)
cycle = !cycle
if touchPad.Get() {
break
}
}
timeStart = time.Now()
for touchPad.Get() {
}
timeEnd = time.Now()
if timeEnd.Sub(timeStart) < time.Millisecond*time.Duration(padSkipTime) {
shutdown()
} else {
shutdownSkipped() // skip shutdown effects if user pressed the pad long enough
}
delayms(1000)
}
}
// initialize pins and devices
func initialize() {
machine.InitADC()
machine.I2C0.Configure(machine.I2CConfig{Frequency: machine.TWI_FREQ_400KHZ})
// touch pad sensor
touchPad.Configure(pinMode("input"))
// NeoPixel light level potentiometer
neoLevel.Configure()
// LEDs
frontLEDsPin.Configure(pinMode("output"))
laserLEDsPin.Configure(pinMode("output"))
frontLEDsPin.High()
laserLEDsPin.High()
// M3 MP3 module
m3PowerPin.Configure(pinMode("output"))
m3PlayPin1.Configure(pinMode("output"))
m3PlayPin2.Configure(pinMode("output"))
m3PowerPin.Low()
m3PlayPin1.High()
m3PlayPin2.High()
delayms(250)
m3PowerPin.High() // turn on relay to power it up
// MSGEQ7
audioAnlz.setup(audioStrobePin, audioResetPin, audioOutputPin)
// SSD1306 OLED
display = ssd1306.NewI2C(machine.I2C0)
display.Configure(ssd1306.Config{
Address: ssd1306.Address_128_32,
Width: 128,
Height: 64,
})
display.ClearDisplay()
// NeoPixels
mainNeo.setup(mainNeoPin, mainNeoNum)
innerNeo.setup(innerNeoPin, innerNeoNum)
cabNeo.setup(cabNeoPin, cabNeoNum)
mainNeo.clear()
mainNeo.show()
innerNeo.clear()
innerNeo.show()
cabNeo.clear()
cabNeo.show()
delayms(1000)
cabNeo.fill(color.RGBA{R: 0, G: 32, B: 32})
cabNeo.show()
}
// start up visualizer with light and sound effects
func startup() {
m3PlayPin1.Low() // play startup music
cabNeo.fill(color.RGBA{R: 64, G: 64, B: 8})
cabNeo.show()
delayms(1500)
frontLEDsPin.Low()
delayms(500)
laserLEDsPin.Low()
delayms(2500)
for k := uint8(4); k <= 128; k++ {
mainNeo.fill(color.RGBA{R: k / 2, G: k / 2, B: k})
mainNeo.show()
innerNeo.fill(color.RGBA{R: k / 2, G: k / 2, B: k / 4})
innerNeo.show()
cabNeo.fill(color.RGBA{R: k, G: k, B: k / 8})
cabNeo.show()
delayms(5)
}
delayms(2500)
for k := uint8(128); k >= 65; k-- {
mainNeo.fill(color.RGBA{R: k / 3, G: k / 3, B: k})
mainNeo.show()
innerNeo.fill(color.RGBA{R: k / 2, G: k / 2, B: k / 8})
innerNeo.show()
cabNeo.fill(color.RGBA{R: k, G: k, B: k / 8})
cabNeo.show()
delayms(15)
}
delayms(9500)
for k := uint8(64); k >= 1; k-- {
mainNeo.fill(color.RGBA{R: k / 4, G: k / 4, B: k})
mainNeo.show()
innerNeo.fill(color.RGBA{R: k / 2, G: k / 2, B: k / 8})
innerNeo.show()
cabNeo.fill(color.RGBA{R: k, G: k, B: k / 8})
cabNeo.show()
delayms(25)
}
delayms(500)
mainNeo.clear()
mainNeo.show()
innerNeo.clear()
innerNeo.show()
cabNeo.clear()
cabNeo.show()
laserLEDsPin.High()
frontLEDsPin.High()
delayms(1500)
frontLEDsPin.Low()
delayms(250)
laserLEDsPin.Low()
delayms(750)
m3PlayPin1.High()
m3PowerPin.Low() // turn off MP3 module
}
// start up visualizer without effects
func startupSkipped() {
frontLEDsPin.Low()
laserLEDsPin.Low()
cabNeo.clear()
cabNeo.show()
m3PowerPin.Low()
}
// shut down visualizer with light and sound effects
func shutdown() {
m3PowerPin.High()
for k := uint8(4); k <= 128; k++ {
mainNeo.fill(color.RGBA{R: k / 2, G: k / 3, B: k})
mainNeo.show()
innerNeo.fill(color.RGBA{R: k / 2, G: k / 3, B: k / 8})
innerNeo.show()
cabNeo.fill(color.RGBA{R: k / 2, G: k / 3, B: k / 8})
cabNeo.show()
delayms(10)
}
frontLEDsPin.High()
laserLEDsPin.High()
mainNeo.clear()
mainNeo.show()
delayms(100)
frontLEDsPin.Low()
laserLEDsPin.Low()
mainNeo.fill(color.RGBA{R: 128 / 2, G: 128 / 4, B: 128 / 8})
mainNeo.show()
delayms(700)
for i := uint8(0); i < 2; i++ {
frontLEDsPin.High()
laserLEDsPin.High()
mainNeo.clear()
mainNeo.show()
delayms(50)
frontLEDsPin.Low()
laserLEDsPin.Low()
mainNeo.fill(color.RGBA{R: 128 / 2, G: 128 / 4, B: 128 / 8})
mainNeo.show()
delayms(200)
}
delayms(200)
m3PlayPin2.Low() // play shutdown sound effect
delayms(250)
display.ClearDisplay()
var cycle uint8
for i := uint8(8); i > 4; i-- {
for k := i * 16; k >= (i*16 - 64); k-- {
if cycle > 4 {
frontLEDsPin.Set(!frontLEDsPin.Get())
laserLEDsPin.Set(!laserLEDsPin.Get())
cycle = 0
} else {
cycle++
}
mainNeo.fill(color.RGBA{R: k, G: k / 8, B: 0})
mainNeo.show()
innerNeo.fill(color.RGBA{R: k, G: k / 8, B: 0})
innerNeo.show()
cabNeo.fill(color.RGBA{R: k, G: k / 8, B: 0})
cabNeo.show()
delayms(25)
}
}
delayms(250)
frontLEDsPin.High()
laserLEDsPin.High()
mainNeo.clear()
mainNeo.show()
innerNeo.clear()
innerNeo.show()
cabNeo.clear()
cabNeo.show()
delayms(1000)
m3PlayPin2.High()
}
// shut down visualizer without effects
func shutdownSkipped() {
m3PowerPin.High()
display.ClearDisplay()
frontLEDsPin.High()
laserLEDsPin.High()
mainNeo.clear()
mainNeo.show()
innerNeo.clear()
innerNeo.show()
cabNeo.clear()
cabNeo.show()
}
// === struct methods ===
// setup NeoPixels
func (ws *NeoPixels) setup(pin machine.Pin, neoNum uint8) {
pin.Configure(pinMode("output"))
ws.neo = ws2812.New(pin)
ws.num = neoNum
ws.colors = make([]color.RGBA, neoNum)
}
// fill NeoPixels with a specific color
func (ws *NeoPixels) fill(c color.RGBA) {
for i := range ws.colors {
ws.colors[i] = c
}
}
// fill certain NeoPixels with a specific color
func (ws *NeoPixels) fillRange(c color.RGBA, start, end uint8) {
for i := range ws.colors {
if uint8(i) >= start && uint8(i) <= end {
ws.colors[i] = c
}
}
}
// clear colors of NeoPixels
func (ws *NeoPixels) clear() {
ws.fill(color.RGBA{R: 0, G: 0, B: 0})
}
// write buffer into NeoPixels (for new colors to take effect)
func (ws *NeoPixels) show() {
ws.neo.WriteColors(ws.colors)
}
// setup MSGEQ7 audio analyzer
func (au *MSGEQ7) setup(strobePin, resetPin, outputPin machine.Pin) {
au.strobe = strobePin
au.reset = resetPin
au.output = machine.ADC{outputPin}
au.reset.Configure(pinMode("output"))
au.strobe.Configure(pinMode("output"))
au.output.Configure()
au.reset.Low()
au.strobe.Low()
}
// read from MSGEQ7 audio analyzer
func (au *MSGEQ7) read() {
au.reset.High()
delayus(100)
au.reset.Low()
delayus(72)
// get audio level at 63, 160, 400, 1K, 2.5K, 6.25K and 16KHz
for i := range au.value {
au.strobe.Low()
delayus(36)
au.value[i] = au.output.Get()
au.strobe.High()
delayus(36)
}
}
// === helper functions ===
// setup pin mode
func pinMode(mode string) machine.PinConfig {
if mode == "input" {
return machine.PinConfig{Mode: machine.PinInput}
} else if mode == "input_pullup" {
return machine.PinConfig{Mode: machine.PinInputPullup}
}
return machine.PinConfig{Mode: machine.PinOutput}
}
// return a rainbow color in a specific position
// this is based on Adafruit's example
func wheel(pos uint8, value uint16, level uint16) color.RGBA {
valueRatio := float32(value) / 65535
levelRatio := float32(uint16(level/1024)) / 64
var r, g, b uint8
switch {
case pos < 0 || pos > 255:
r = 0
g = 0
b = 0
case pos < 85:
r = 255 - pos*3
g = pos * 3
b = 0
case pos < 170:
pos -= 85
r = 0
g = 255 - pos*3
b = pos * 3
default:
pos -= 170
r = pos * 3
g = 0
b = 255 - pos*3
}
r = uint8(float32(r) * valueRatio * levelRatio)
g = uint8(float32(g) * valueRatio * levelRatio)
b = uint8(float32(b) * valueRatio * levelRatio)
return color.RGBA{R: r, G: g, B: b}
}
// equivalent to delay() in Arduino C++
func delayms(t time.Duration) {
time.Sleep(time.Millisecond * t)
}
// equivalent to delayMicroseconds() in Arduino C++
func delayus(t time.Duration) {
time.Sleep(time.Microsecond * t)
}