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:

  1. Install the operating system (CircuitPython) on the board
  2. Copy the required libraries into the /lib folder on the board
  3. 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
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


#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)

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.

Pixel Mapping

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:

  1. pixel_map_reverse: animations start at the base of the ukulele and travel upwards along both sides to the top, symmetrically
  2. pixel_map_around: animations start at the bottom and travel around the ukulele in a "circle", correcting for my swapped strips inside the neck
  3. pixel_map_radiate: animations start where the neck and body meet, and radiate out from the center of the ukulele symmetrically
  4. pixel_map_sweep: basically the inverse of pixel_map_reverse, going top-down along both sides
  5. 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:

  1. Import them at the top of the code
  2. Define and customize them here
  3. 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:

  1. Double check you have all the correct libraries installed. Some of the names are really similar -- make sure you've got the right ones.
  2. Try reinstalling CircuitPython again
  3. 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.

This guide was first published on Jul 22, 2020. It was last updated on Jul 22, 2020.

This page (Software) was last updated on May 30, 2023.

Text editor powered by tinymce.