icon

The Millennium Falcon Audio Visualizer

2 50275 Medium
projectImage
HARDWARE LIST
1 Firebeetle Board-M0 (V1.0)
1 Bluetooth 5.0 Audio Receiver Board-Controllable Volume
1 Audio Analyzer Module
1 Gravity: Digital Capacitive Touch Sensor For Arduino
2 Gravity: Analog Rotation Potentiometer Sensor for Arduino - Rotation 300°
1 Monochrome 0.96" 128x64 I2C/SPI OLED Display
1 Gravity: Digital 5A Relay Module
1 Gravity: Digital RGB LED Module
1 NeoPixel Ring - 12 x 5050 RGB LED with Integrated Drivers
4 NeoPixel Stick - 8 x 5050 RGB LED with Integrated Drivers
1 Voice playback module

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.

projectImage
CODE
// 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)
}
projectImage
projectImage
projectImage
projectImage
projectImage
projectImage
projectImage
License
All Rights
Reserved
licensBg
2