

The Falcon Audio Visualizer (a TinyGo project)
source link: https://www.hackster.io/alankrantas/the-falcon-audio-visualizer-a-tinygo-project-260360
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


The Falcon Audio Visualizer (a TinyGo project)
Convert a 2004 Hasbro Millennium Falcon set into a functional Bluetooth player/audio visualizer with Tiny Golang.
Things used in this project
Hardware components
DFRobot Firebeetle Board-M0×1










Software apps and online services
TinyGo
Story
What is this? (Quick Summery)
This is a semi-sponsored project, although the project idea and product opinions are entirely on my own.
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 "M3" MP3 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.
A bit background of how this started
In December 2020, I received a message from DFRobot, asking me if I'd like to try some of their new sensor modules. I said sure, and choose some stuff. And then they asked: will you make some projects with them?
I replied, "but I don't know what to do with them right now." And I don't want to make projects simply for the sensors' sake.
Then I had an idea: "I do have a project in mind. How about I choose something for this project?" And they agreed.
I picked a few things that are not easy to buy in Taiwan. So this has become my first (semi-)sponsored project.
Converting the Falcon
The 2004 Falcon is surprising easy to disassemble. Unscrew every screw underneath it, and it will open up easily.
MSGEQ7 makes analyzing audio simple
When I was fixing one of the Hasbro lightsaber with Arduino Nano last summer, I also found that one of our 2004 Millennium Falcon (My brothers and I own two) has broken engine lights. At that time, I have already planned to convert it into a music visualizer. But how?
One common solution is Fast Fourier Transform - translate audio signals into different frequency levels. There are FFT libraries, but how to use it is still way beyond my comprehension.
Only after DFRobot contacted me, I discovered the MSGEQ7 sensor on their website. This chip can measure audio strengths at 7 bands - 63Hz, 160Hz, 400Hz, 1kHz, 2.5kHz, 6.25kHz.
And the controlling protocol is quite simple (another source can be found here):
- Pull high RESET, wait 100 us, then pull down, wait 72 us.
- Pull high STROBE, read the analog value from OUTPUT, wait 36 us, then pull down STROBE, wait 36 us again. Repeat this 7 times to get readings of all 7 bands.
For Arduino IDE users, you can dwnlo their MSGEQ7 driver here. You'll need to manually move it into the Arduino library directory.
Bluetooth receiver
The audio source comes from a Bluetooth receiver module. Actually I asked for two of them (both are not made by DFRobot): MH-M38 and XY-WRBT. The MH-M38 (Bluetooth 4.2) can directly power two 5W speakers, but the signal is too messed up to be read by MSGEQ7.
So I decided to use the XY-WRBT with external speakers. An one-to-two 3.5mm output cable send the audio to both the speakers and MSGEQ7. I don't have the knowledge to build nice ones on my own, and there's not enough room anyway.
"M3" MP3 module
I have this "M3" MP3/WAV playback module from a couple of years ago made by some unknown Chinese maker. It's not sophisticated like the DFPlayer mini, but simple enough to be controlled by any languages. (Make a "01" directory in the micro SD card and name the song files as 001.xxx, 002.xxx, etc.)
It has two operating modes:
- Direct mode: pull down pin A1-A9 to play song 1-9.
- Binary mode: pull down A10 and use A5-A1 as binary signal (A5 is MSB). Pull all five pins up, then pull down the ones need to be 1. For example, 10110 (A5, A3, A2 pulled down) is 16 + 4 + 2 = 22nd song. So this mode supports total 31 songs.
Here I only need to play two songs, so I use the direct mode.
How I ended up choosing TinyGo
As for the microcontroller, I asked for a Firebeetle Board-M0, a beautifully-made SAMD21 board with castellated holes. Their ESP boards also looks nice...but SAMD21 boards are still uncommon in Asia.
But my main reason was (I'm sorry, DFRobot) - it looks kind of like the Adafruit Feather M0 Express. I wondered, is it possible to upload Feather M0's CircuitPython firemware onto it?
Yes you can.
(Of course, Firebeetle M0 is not a copy of Feather M0 Express -it's longer and has more GPIOs available.In fact, it's more similar to Arduino Zero than any of the Feather M0s.)
=================================================================
Firstly, I download a Feather M0 .ino UF2 bootloader and upload it via Arduino IDE.
To use Firebeetle M0 in Arduino IDE, you'll need to add the following link in preference: http://download.dfrobot.top/FireBeetle/package_DFRobot_index.json. More install detail here. Be noted that the English wiki listed the wrong URL, at least at the time I wrote this.
With the UF2 bootloader installed, I can now flash CP firmwares. Actually, now three new options have opened up:
- CircuitPython is really nice, with the best ecosystem of all three, but it is also the slowest. The audio visualizer has to be highly responsive, but time.sleep() in CP can only goes as far as 1 ms. (Also, only CP firmwares for Feather M0 Basic and Arduino Zero works well.)
- MakeCode Maker (TypeScript) is interesting but still highly experimental, even more so than TinyGo. I've tried to use it to control the MSGEQ7 with no success.
- ...And I always wanted to try building something with TinyGo. Arduino C++ is still reliable, but I just don't like it much these days. I've been learning Go for some time, and TinyGo is also fast, with pretty good support to SAMD21 boards.
There's another (accidental) advantage of TinyGo: its board definition of Adafruit ItsyBitsy M0 has all pins listed, including the GPIO 6 and 8 that is not on ItsyBitsy M0. (Firebeetle M0 does not have GPIO 8, but it is connected to an onboard NeoPixel).
Why this is important? Because as Arduino Zero, TinyGo would use BOSSA to upload the code, and upload time is a bit slower. As ItsyBitsy M0 with its pre-loaded UF2 bootloader, TinyGo simply copies the.bin file onto it.
So: after changing the bootloader, now I have an fully-functional ItsyBitsy M0 which is not an ItsyBitsy M0. Of course, I have to manually press reset twice every time before upload (not able to auto-reset), but that was fine by me.
Finishing up
However, the biggest challenge I've faced in this project is not TinyGo - but how exactly to convert audio level readings to RGB LED effects.
So I was stuck for a month, without knowing how to proceed. I also installed an OLED display (it was not in the original plan) to see how MSGEQ7 works. In the end I decided simply to use the video below as my example:
Without any audio input, the readings from MSGEQ7 are always has about 1/4-1/8 of the max values. And this turned out to be a good thing: since there are always some value, the NeoPixels will never truly be dark, maintaining a sense of continuity. So I don't even need to cut out the low value. Simply convert them to brightness levels would do.
The LEDs are mapped to MSGEQ7's 7 bands as the following:
- Main NeoPixels (engine exhaust, 32 leds) - bands 3-5 (middle)
- Inner NeoPixels (under turret window, 12 leds) - bands 1-2 (bass)
- Cockpit NeoPixel (1 led) - band 6 (high)
I decided to ignore band 7 since it's rarely used by songs. I added a slow rainbow rotation effect as well, based on Adafruit's example code.
The routine is very simple:
- Press the touch pad in the cockpit to "start it up". You can skip the light and sound effects by pressing the pad a bit longer.
- Now it's in the audio visualizer mode. The Bluetooth receiver works independently, with or without the visualizer. A relay turns of MP3 module's power during this mode.
- Press the pad again to "shut it down" (can be skipped as well).
This is the flashing command I used in the terminal:
tinygo flash -target itsybitsy-m0 -scheduler coroutines -port COMxx main
-scheduler coroutines is needed, otherwise it would cause a goroutine stack overflow error. This might be fixed in the future.
This project is compiled with TinyGo 0.16.0 and Golang 1.15.8. Future versions of TinyGo might not be compatible with this code.
BTW, Solo is a good Star Wars movie as long as you don't watch the part after the MAW.
The Falcon Audio Visualizer
Go// 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) }
Credits
Comments
Recommend
-
59
TinyGo - Go compiler for microcontrollers We never expected Go to be an embedded language and so it's got serious problems [...]. -- Rob Pike,
-
38
README.md TinyGo - Go compiler for microcontrollers
-
54
Release Release 0.8.0 · tinygo-org/tinygo · GitHub
-
50
This release adds experimental support for Windows, updates to support Go 1.13, improves flashing experience of many board...
-
10
With WebAssembly we can write a library in almost any language, compile it to WebAssembly (WASM) and use it from JavaScript. In this tutorial I will show you how...
-
8
Garbage collection in TinyGo 24 september 2020, by Ayke van Laethem Garbage collection is often seen like a kind of dark magic. And while it is possible to make i...
-
13
ESP32 and ESP8266 support in TinyGo 22 september 2020, by Ayke van Laethem As you might have heard, we've added ESP32 and ESP8266 support to TinyGo last week i...
-
9
How the TinyGo playground simulates hardware 17 juli 2019, by Ayke van Laethem You may have seen the recently launched
-
7
TL;DR: The concept of coding for IoT devices, CLIs and WebAssembly is not a new concept. However, what if I told you that it is possible to use Golang for all three. TinyGo is a specialized p...
-
5
@react-three/fiber equalizer r3f-equalizer is a 3d audio equalizer for React built using @react-three/fiber. Quickstart npm install r3f-equa...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK