It's always a good idea to get your software loaded onto your board before the build. That way you'll be able to test your solder joints at each step of the way, and you'll get instant gratification when you plug in the lights.
Getting the software loaded is a 3-step process:
- Install the operating system (CircuitPython) on the board
- Copy the required libraries into the /lib folder on the board
- Save the code.py file to the board
CircuitPython is a fairly new OS that's changing rapidly. New features are being added and bugs are being fixed all the time, so it's always best to get a fresh version of CircuitPython and the library files before coding.
Install CircuitPython
The Adafruit Feather Sense ships with CircuitPython, but let's go ahead and update it to the latest version. It's super easy with the circuitpython.org website. Follow the directions on the Feather Bluefruit Sense guide, or click the button below for a direct download link.
Download the file, plug your Feather Sense into your computer via the USB port, and double-click the reset button. You'll see a drive appear called FTHR840BOOT. Drag the .uf2 file you just downloaded onto this drive to install CircuitPython.
You'll know it worked if the FTHR840BOOT drive name changes to CIRCUITPY.
Adafruit Circuit Python Libraries
Download the CircuitPython library bundle per the Feather Sense guide instructions here. Unzip the files into a folder on your computer. Create a new folder on the CIRCUITPY drive and name it lib.
Open the library download folder and find the following files. Copy them into the lib folder on your CIRCUITPY drive.
- adafruit_bus_device (directory)
- adafruit_led_animation (directory)
- adafruit_lsm6ds.mpy
- adafruit_register (directory)
- neopixel.mpy
Upload Files
Click the link below to download the project zip – This contains the code. Upload the code.py file to the CIRCUITPY drive root (main) folder.
Check out the image above to see what your CIRCUITPY drive should look like when all the files are in place.
# SPDX-FileCopyrightText: 2020 Erin St. Blaine for Adafruit Industries # # SPDX-License-Identifier: MIT """ LED Ukulele with Feather Sense and PropMaker Wing Adafruit invests time and resources providing this open source code. Please support Adafruit and open source hardware by purchasing products from Adafruit! Written by Erin St Blaine & Limor Fried for Adafruit Industries Copyright (c) 2019-2020 Adafruit Industries Licensed under the MIT license. All text above must be included in any redistribution. MODES: 0 = off/powerup, 1 = sound reactive, 2 = non-sound reactive, 3 = tilt Pluck high A on the E string to toggle sound reactive mode on or off Pluck high A♭ on the E string to cycle through the animation modes """ import time import array import digitalio import audiobusio import board import neopixel try: from ulab.utils import spectrogram except ImportError: from ulab.scipy.signal import spectrogram from ulab import numpy as np from rainbowio import colorwheel from adafruit_lsm6ds import lsm6ds33 from adafruit_led_animation.helper import PixelMap from adafruit_led_animation.sequence import AnimationSequence from adafruit_led_animation.group import AnimationGroup from adafruit_led_animation.animation.sparkle import Sparkle from adafruit_led_animation.animation.rainbow import Rainbow from adafruit_led_animation.animation.rainbowchase import RainbowChase from adafruit_led_animation.animation.rainbowcomet import RainbowComet from adafruit_led_animation.animation.chase import Chase from adafruit_led_animation.animation.comet import Comet from adafruit_led_animation.color import ( BLACK, RED, ORANGE, BLUE, PURPLE, WHITE, ) MAX_BRIGHTNESS = 0.3 # set max brightness for sound reactive mode NORMAL_BRIGHTNESS = 0.1 # set brightness for non-reactive mode VOLUME_CALIBRATOR = 50 # multiplier for brightness mapping ROCKSTAR_TILT_THRESHOLD = 200 # shake threshold SOUND_THRESHOLD = 430000 # main strum or pluck threshold # Set to the length in seconds for the animations POWER_ON_DURATION = 1.3 ROCKSTAR_TILT_DURATION = 1 NUM_PIXELS = 104 # Number of pixels used in project NEOPIXEL_PIN = board.D5 POWER_PIN = board.D10 enable = digitalio.DigitalInOut(POWER_PIN) enable.direction = digitalio.Direction.OUTPUT enable.value = False i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller pixels = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False) pixels.fill(0) # NeoPixels off ASAP on startup pixels.show() # PIXEL MAPS: Used for reordering pixels so the animations can run in different configurations. # My LED strips inside the neck are accidentally swapped left-right, # so these maps also correct for that # fmt: off #Bottom up along both sides at once pixel_map_reverse = PixelMap(pixels, [ 0, 103, 1, 102, 2, 101, 3, 100, 4, 99, 5, 98, 6, 97, 7, 96, 8, 95, 9, 94, 10, 93, 11, 92, 12, 91, 13, 90, 14, 89, 15, 88, 16, 87, 17, 86, 18, 85, 19, 84, 20, 83, 21, 82, 22, 81, 23, 80, 24, 79, 25, 78, 26, 77, 27, 76, 28, 75, 29, 74, 30, 73, 31, 72, 32, 71, 33, 70, 34, 69, 35, 68, 36, 67, 37, 66, 38, 65, 39, 64, 40, 63, 41, 62, 42, 61, 43, 60, 44, 59, 45, 58, 46, 57, 47, 56, 48, 55, 49, 54, 50, 53, 51, 52, ], individual_pixels=True) #Starts at the bottom and goes around clockwise pixel_map_around = PixelMap(pixels, [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, ], individual_pixels=True) #Radiates from the center outwards like a starburst pixel_map_radiate = PixelMap(pixels, [ 75, 73, 76, 27, 28, 74, 77, 26, 29, 73, 78, 25, 30, 72, 79, 24, 31, 71, 80, 23, 32, 70, 81, 22, 33, 69, 82, 21, 34, 68, 83, 20, 35, 67, 84, 19, 36, 66, 85, 18, 37, 65, 38, 86, 17, 64, 39, 87, 16, 63, 40, 88, 15, 62, 41, 89, 14, 61, 42, 90, 13, 60, 43, 91, 12, 59, 44, 92, 11, 58, 45, 93, 10, 57, 46, 94, 9, 56, 47, 95, 8, 55, 48, 96, 7, 54, 49, 97, 6, 53, 50, 98, 5, 52, 51, 99, 4, 100, 3, 101, 2, 102, 1, 103, 0, ], individual_pixels=True) #Top down along both sides at once pixel_map_sweep = PixelMap(pixels, [ 51, 52, 50, 53, 49, 54, 48, 55, 47, 56, 46, 57, 45, 58, 44, 59, 43, 60, 42, 61, 41, 62, 40, 63, 39, 64, 38, 65, 37, 66, 36, 67, 35, 68, 34, 69, 33, 70, 32, 71, 31, 72, 30, 73, 29, 74, 28, 75, 27, 76, 27, 77, 26, 78, 25, 79, 24, 80, 23, 81, 22, 82, 21, 83, 20, 84, 19, 85, 18, 86, 17, 87, 16, 88, 15, 89, 14, 90, 13, 91, 12, 92, 11, 93, 10, 94, 9, 95, 8, 96, 7, 97, 6, 98, 5, 99, 4, 100, 3, 101, 2, 102, 1, 103, 0 ], individual_pixels=True) #Every other pixel, starting at the bottom and going upwards along both sides pixel_map_skip = PixelMap(pixels, [ 0, 103, 2, 101, 4, 99, 6, 97, 8, 95, 10, 93, 12, 91, 14, 89, 16, 87, 18, 85, 20, 83, 22, 81, 24, 79, 26, 77, 29, 74, 31, 72, 33, 70, 35, 68, 37, 66, 39, 64, 41, 62, 43, 60, 45, 58, 47, 56, 49, 54, 51, 52, ], individual_pixels=True) # fmt: on pixel_map = [ pixel_map_reverse, pixel_map_around, pixel_map_radiate, pixel_map_sweep, pixel_map_skip, ] # Set up accelerometer & mic sensor = lsm6ds33.LSM6DS33(i2c) mic = audiobusio.PDMIn( board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, sample_rate=16000, bit_depth=16 ) NUM_SAMPLES = 256 samples_bit = array.array("H", [0] * (NUM_SAMPLES + 3)) def power_on(duration): """ Animate NeoPixels for power on. """ start_time = time.monotonic() # Save start time while True: elapsed = time.monotonic() - start_time # Time spent if elapsed > duration: # Past duration? break # Stop animating powerup.animate() def rockstar_tilt(duration): """ Tilt animation - lightning effect with a rotating color :param duration: duration of the animation, in seconds (>0.0) """ tilt_time = time.monotonic() # Save start time while True: elapsed = time.monotonic() - tilt_time # Time spent if elapsed > duration: # Past duration? break # Stop animating pixels.brightness = MAX_BRIGHTNESS pixels.fill(TILT_COLOR) pixels.show() time.sleep(0.01) pixels.fill(BLACK) pixels.show() time.sleep(0.03) pixels.fill(WHITE) pixels.show() time.sleep(0.02) pixels.fill(BLACK) pixels.show() time.sleep(0.005) pixels.fill(TILT_COLOR) pixels.show() time.sleep(0.01) pixels.fill(BLACK) pixels.show() time.sleep(0.03) # Cusomize LED Animations ------------------------------------------------------ powerup = RainbowComet(pixel_map[3], speed=0, tail_length=25, bounce=False) rainbow = Rainbow(pixel_map[4], speed=0, period=6, name="rainbow", step=2.4) rainbow_chase = RainbowChase(pixel_map[3], speed=0, size=3, spacing=15, step=10) rainbow_chase2 = RainbowChase(pixel_map[2], speed=0, size=10, spacing=1, step=18) chase = Chase(pixel_map[1], speed=0.1, color=RED, size=1, spacing=6) rainbow_comet = RainbowComet(pixel_map[2], speed=0, tail_length=80, bounce=True) rainbow_comet2 = RainbowComet( pixel_map[0], speed=0, tail_length=104, colorwheel_offset=80, bounce=True ) rainbow_comet3 = RainbowComet( pixel_map[1], speed=0, tail_length=25, colorwheel_offset=80, step=4, bounce=False ) strum = RainbowComet( pixel_map[3], speed=0, tail_length=25, bounce=False, colorwheel_offset=50, step=4 ) lava = Comet(pixel_map[3], speed=0.01, color=ORANGE, tail_length=40, bounce=False) sparkle = Sparkle(pixel_map[4], speed=0.01, color=BLUE, num_sparkles=10) sparkle2 = Sparkle(pixel_map[1], speed=0.05, color=PURPLE, num_sparkles=4) # Animations Playlist - reorder as desired. AnimationGroups play at the same time animations = AnimationSequence( rainbow, rainbow_chase, rainbow_chase2, chase, lava, rainbow_comet, rainbow_comet2, AnimationGroup( sparkle, strum, ), AnimationGroup( sparkle2, rainbow_comet3, ), auto_clear=True, auto_reset=True, ) MODE = 0 LASTMODE = 1 # start up in sound reactive mode i = 0 # Main loop while True: i = (i + 0.5) % 256 # run from 0 to 255 TILT_COLOR = colorwheel(i) if MODE == 0: # If currently off... enable.value = True power_on(POWER_ON_DURATION) # Power up! MODE = LASTMODE elif MODE >= 1: # If not OFF MODE... mic.record(samples_bit, len(samples_bit)) samples = np.array(samples_bit[3:]) spectrum = spectrogram(samples) spectrum = spectrum[:128] spectrum[0] = 0 spectrum[1] = 0 peak_idx = np.argmax(spectrum) peak_freq = peak_idx * 16000 / 256 # print((peak_idx, peak_freq, spectrum[peak_idx])) magnitude = spectrum[peak_idx] # time.sleep(1) if peak_freq == 812.50 and magnitude > SOUND_THRESHOLD: animations.next() time.sleep(1) if peak_freq == 875 and magnitude > SOUND_THRESHOLD: if MODE == 1: MODE = 2 print("mode = 2") LASTMODE = 2 time.sleep(1) elif MODE == 2: MODE = 1 print("mode = 1") LASTMODE = 1 time.sleep(1) # Read accelerometer x, y, z = sensor.acceleration accel_total = x * x + y * y # x=tilt, y=rotate # print (accel_total) if accel_total > ROCKSTAR_TILT_THRESHOLD: MODE = 3 print("Tilted: ", accel_total) if MODE == 1: VOLUME = magnitude / (VOLUME_CALIBRATOR * 100000) if VOLUME > MAX_BRIGHTNESS: VOLUME = MAX_BRIGHTNESS # print(VOLUME) pixels.brightness = VOLUME # time.sleep(2) animations.animate() elif MODE == 2: pixels.brightness = NORMAL_BRIGHTNESS animations.animate() elif MODE == 3: rockstar_tilt(ROCKSTAR_TILT_DURATION) MODE = LASTMODE
Customizing Your Code
The best way to edit and upload your code is with the Mu Editor, a simple Python editor that works with Adafruit CircuitPython hardware. It's written in Python and works on Windows, MacOS, Linux and Raspberry Pi. The serial console is built right in so you get immediate feedback from your board's serial output. Instructions for installing Mu is here.
Edit Variables
Open the code in the Mu editor (or another text editor) and look near the top. You'll see a lot of variables that you can change to customize your instrument.
MAX_BRIGHTNESS = 0.3 # set max brightness for sound reactive mode NORMAL_BRIGHTNESS = 0.1 # set brightness for non-reactive mode VOLUME_CALIBRATOR = 50 # multiplier for brightness mapping ROCKSTAR_TILT_THRESHOLD = 200 # shake threshold SOUND_THRESHOLD = 430000 # main strum or pluck threshold
Set NORMAL_BRIGHTNESS to a number between 0 (off) and 1 (full brightness). My uke has a whole lot of densely packed lights, so 10% brightness is really plenty bright -- much more and I'd be blinding people.
MAX_BRIGHTNESS is the cutoff number for sound reactive mode. I've turned it up a little higher to give more variety with my strum, and since it's pulsing along with the volume, it's not quite as blinding.
The volume, tilt, and sound thresholds can also be adjusted here. Further down in the code we'll look at how to read these values from your uke to calibrate it to your own playing style.
# Set to the length in seconds for the animations POWER_ON_DURATION = 1.3 ROCKSTAR_TILT_DURATION = 1
Adjust these variables to change the duration of the power-up or tilt animations.
NUM_PIXELS = 104 # Number of pixels used in project NEOPIXEL_PIN = board.D5 POWER_PIN = board.D10
Change NUM_PIXELS
to reflect the actual number of LEDs in your project.
A neat function of the LED Animations library is Pixel Mapping: the ability to run the animations along pixels in whatever order you'd like. This is useful for a couple of reasons.
First, when I wrangled the LED strips inside my ukulele's neck, I somehow managed to get them reversed left-to-right. A light traveling along my four strips in sequence would go around the left side of the body, then up the right side of the neck and back down the left side, then around the right side of the body. This just looked messy. I'd already glued the strips in place, so there was no fixing this without a whole lot of destruction.
Pixel maps to the rescue! We can tell the code to run animations on the pixels in any order we want, so I was able to fix my mistake just by typing in a string of numbers.
Second, with our newfound pixel mapping power, we can set up a list of pixel maps and have a lot more control over the layout of our animations. I set up five different pixel maps:
- pixel_map_reverse: animations start at the base of the ukulele and travel upwards along both sides to the top, symmetrically
- pixel_map_around: animations start at the bottom and travel around the ukulele in a "circle", correcting for my swapped strips inside the neck
- pixel_map_radiate: animations start where the neck and body meet, and radiate out from the center of the ukulele symmetrically
- pixel_map_sweep: basically the inverse of pixel_map_reverse, going top-down along both sides
- pixel_map_skip: Same as pixel_map_reverse but lighting up every other pixel instead of all pixels
After setting up my maps, I made a list of all of them called pixel_map
. Further down in the code where I'm setting up animations, I can call a member of the list (i.e pixel_map[4]
) to make the LED animations run in that configuration.
pixel_map = [ pixel_map_reverse, pixel_map_around, pixel_map_radiate, pixel_map_sweep, pixel_map_skip, ]
You don't have to use the pixel maps -- you can simply call pixels.show()
instead of pixel_map[0].show()
in all the animations if you just want to keep it simple. But using pixel maps can add a whole new dimension to the animations in the LED Animations library.
Customize the Animations & Animation Playlist
Further down in the code, around line 181, you'll find the list of animations. I'm using several animations imported from the LED Animations library including:
- Rainbow: a lovely rainbow animation, with customizable spread and speed
- Comet: sends a single color "comet" of adjustable length down the strip
- RainbowComet: sends a rainbow colored "comet" of adjustable length along the strip
- RainbowChase: blocks of LEDs chasing each other around in varying colors
- Sparkle: twinkly lights!
There are more animation styles available - check the LED Animations Library Guide for a comprehensive list and use instructions.
To add more animations:
- Import them at the top of the code
- Define and customize them here
- Add them to the playlist at line 202.
If you want to use different colors, remember to scroll up to the import
section at the top of the code and add your colors to the color list. Here is a list of all available colors in the library.
# Cusomize LED Animations ------------------------------------------------------ powerup = RainbowComet(pixel_map[3], speed=0, tail_length=25, bounce=False) rainbow = Rainbow(pixel_map[4], speed=0, period=6, name="rainbow", step=2.4) rainbow_chase = RainbowChase(pixel_map[3], speed=0, size=3, spacing=15, step=10) rainbow_chase2 = RainbowChase(pixel_map[2], speed=0, size=10, spacing=1, step=18) chase = Chase(pixel_map[1], speed=0.1, color=RED, size=1, spacing=6) rainbow_comet = RainbowComet(pixel_map[2], speed=0, tail_length=80, bounce=True) rainbow_comet2 = RainbowComet( pixel_map[0], speed=0, tail_length=104, colorwheel_offset=80, bounce=True ) rainbow_comet3 = RainbowComet( pixel_map[1], speed=0, tail_length=25, colorwheel_offset=80, step=4, bounce=False ) strum = RainbowComet( pixel_map[3], speed=0, tail_length=25, bounce=False, colorwheel_offset=50, step=4 ) lava = Comet(pixel_map[3], speed=0.1, color=ORANGE, tail_length=70, bounce=False) sparkle = Sparkle(pixel_map[4], speed=0.01, color=BLUE, num_sparkles=10) sparkle2 = Sparkle(pixel_map[1], speed=0.05, color=PURPLE, num_sparkles=4) # Animations Playlist - reorder as desired. AnimationGroups play at the same time animations = AnimationSequence( rainbow, rainbow_chase, rainbow_chase2, chase, lava, rainbow_comet, rainbow_comet2, AnimationGroup( sparkle, strum, ), AnimationGroup( sparkle2, rainbow_comet3, ), auto_clear=True, auto_reset=True, )
Sensitivity
I set threshold variables at the top of the code, but if my values aren't quite what you're looking for, you may want to listen to what your ukulele is doing and adjust the numbers to fit. You can use the REPL in the Mu editor as a serial monitor to print out the sound and movement levels of your ukulele.
In Mu, click the Serial button and press D to enter the REPL. More about using the REPL here.
To find a good SOUND_THRESHOLD
, look for this code around line 245:
peak_idx = np.argmax(spectrum) peak_freq = peak_idx * 16000 / 256 # print((peak_idx, peak_freq, spectrum[peak_idx])) magnitude = spectrum[peak_idx] # time.sleep(1)
With your ukulele plugged into your computer, uncomment the print
and time.sleep
lines and save your code. In the Serial window, you'll see a series of 3 numbers printing out. The third number is spectrum[peak_idx]
and represents the overall volume of your sound. I chose 43000 for my SOUND_THRESHOLD
since that's pretty high inside the range between my "quiet" numbers and "loud" numbers. Play with some different numbers until you like the action.
Don't forget to comment out time.sleep
again when you're done adjusting or your animations will run really slowly.
Note Sensing
The middle number of the three is peak_freq
. This number represents the pitch, or frequency, of the note being strummed. The code normalizes the frequencies, "squishing" them into levels broken up every 62.5hz. This means you'll get pretty consistent numbers when you strum a particular note, even if your ukulele is a bit out of tune. A high A note played on your ukulele's E string comes out at 875hz, and an A♭ reads at 812.5hz.
This is fun because now we can tell the Feather to run some code whenever it hears that particular pitch. I chose the two highest notes on the instrument because they rarely get played (at least with my novice ability) and won't get strummed accidentally.
High A toggles sound-reactive mode on and off, and high A♭ cycles through the animation modes. Feel free to add more modes centered around different notes!
if peak_freq == 875 and magnitude > SOUND_THRESHOLD: animations.next() time.sleep(1) if peak_freq == 812.50 and magnitude > SOUND_THRESHOLD: if MODE == 1: MODE = 2 print("mode = 2") LASTMODE = 2 time.sleep(1) elif MODE == 2: MODE = 1 print("mode = 1") LASTMODE = 1 time.sleep(1)
Tilt Mode / Accelerometer Adjustment
Down around line 256 you'll find a similar print
line for your accelerometer readout. Uncomment print(accel_total)
and shake your ukulele around to get some data on which to base your ROCKSTAR_TILT_THRESHOLD
value.
# Read accelerometer x, y, z = sensor.acceleration accel_total = x * x + y * y # x=tilt, y=rotate # print (accel_total) if accel_total > ROCKSTAR_TILT_THRESHOLD: MODE = 3 print("Tilted: ", accel_total)
Troubleshooting
If it doesn't seem to be working, here are a few things to try:
- Double check you have all the correct libraries installed. Some of the names are really similar -- make sure you've got the right ones.
- Try reinstalling CircuitPython again
- Open the REPL in the Mu editor by clicking the "serial" button. Press <ctrl>D. This will error-check your code and let you know what line number may be the problem. More about the REPL here.
More tips and tricks can be found on the Intro to CircuitPython guide and the Feather Sense guide.
Text editor powered by tinymce.