icon
    The Millennium Falcon Audio Visualizer
    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
    1