Code Walkthrough

First we import all the library elements needed.

import array
import math
import audiobusio
import board
import neopixel
from digitalio import DigitalInOut, Direction, Pull

from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.button_packet import ButtonPacket
from adafruit_bluefruit_connect.color_packet import ColorPacket
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService

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.animation.solid import Solid
from adafruit_led_animation.color import colorwheel
from adafruit_led_animation.color import (
    BLACK,
    RED,
    ORANGE,
    BLUE,
    PURPLE,
    WHITE,
)

The CircuitPython LED Animations Library will do most of the heavy lifting for the actual pixel animations. I've imported most of the basic colors I want to use. It's also easy to define or redefine your own colors. I wanted a dim yellow to use as the background in the sound reactive mode, so defined YELLOW with an RGB tuple:

YELLOW = (25, 15, 0)

Next we'll set up the bluetooth service and define some variables. It's hard to tell exactly how many pixels we have in this uber-high-density strip, but there appear to be around 30.

We'll also define the fairy light strands on pin A4.

# Setup BLE
ble = BLERadio()
uart = UARTService()
advertisement = ProvideServicesAdvertisement(uart)

# Color of the peak pixel.
PEAK_COLOR = (100, 0, 255)
# Number of total pixels - 10 build into Circuit Playground
NUM_PIXELS = 30


fairylights = DigitalInOut(board.A4)
fairylights.direction = Direction.OUTPUT
fairylights.value = True

Next we'll set up the sound reactive code. This function is based on the Playground Sound Meter guide.

# Exponential scaling factor.
# Should probably be in range -10 .. 10 to be reasonable.
CURVE = 2
SCALE_EXPONENT = math.pow(10, CURVE * -0.1)

# Number of samples to read at once.
NUM_SAMPLES = 160

brightness_increment = 0

# Restrict value to be between floor and ceiling.
def constrain(value, floor, ceiling):
    return max(floor, min(value, ceiling))


# Scale input_value between output_min and output_max, exponentially.
def log_scale(input_value, input_min, input_max, output_min, output_max):
    normalized_input_value = (input_value - input_min) / \
                             (input_max - input_min)
    return output_min + \
        math.pow(normalized_input_value, SCALE_EXPONENT) \
        * (output_max - output_min)


# Remove DC bias before computing RMS.
def normalized_rms(values):
    minbuf = int(mean(values))
    samples_sum = sum(
        float(sample - minbuf) * (sample - minbuf)
        for sample in values
    )

    return math.sqrt(samples_sum / len(values))


def mean(values):
    return sum(values) / len(values)


def volume_color(volume):
    return 200, volume * (255 // NUM_PIXELS), 0

Next, we set up our NeoPixel object on pin A1 and set them to "off" initially.

pixels = neopixel.NeoPixel(board.A1, NUM_PIXELS, brightness=0.1, auto_write=False)
pixels.fill(0)
pixels.show()

Then, set up the microphone input for our sound reaction.

mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA,
                       sample_rate=16000, bit_depth=16)

# Record an initial sample to calibrate. Assume it's quiet when we start.
samples = array.array('H', [0] * NUM_SAMPLES)
mic.record(samples, len(samples))
# Set lowest level to expect, plus a little.
input_floor = normalized_rms(samples) + 30
# OR: used a fixed floor
# input_floor = 50

# You might want to print the input_floor to help adjust other values.
print(input_floor)

# Corresponds to sensitivity: lower means more pixels light up with lower sound
# Adjust this as you see fit.
input_ceiling = input_floor + 100

peak = 0

Next we define our LED animations. You can learn more about how these animations work and which type of animations are available in the CircuitPython LED Animations guide

We've set up a handful of animations and then put them into an animation playlist. Later on in the code we can refer to these animations by number and assign them to different buttons in the Bluetooth control app.

Animations can also be layered using AnimationGroup. The LED Animations library is really powerful and easy to use, so dig in and customize here to your heart's content.

# Cusomize LED Animations  ------------------------------------------------------
rainbow = Rainbow(pixels, speed=0, period=6, name="rainbow", step=2.4)
rainbow_chase = RainbowChase(pixels, speed=0.1, size=5, spacing=5, step=5)
chase = Chase(pixels, speed=0.2, color=ORANGE, size=2, spacing=6)
rainbow_comet = RainbowComet(pixels, speed=0.1, tail_length=30, bounce=True)
rainbow_comet2 = RainbowComet(
    pixels, speed=0.1, tail_length=104, colorwheel_offset=80, bounce=True
    )
rainbow_comet3 = RainbowComet(
    pixels, speed=0, tail_length=25, colorwheel_offset=80, step=4, bounce=False
    )
strum = RainbowComet(
    pixels, speed=0.1, tail_length=25, bounce=False, colorwheel_offset=50, step=4
    )
sparkle = Sparkle(pixels, speed=0.1, color=BLUE, num_sparkles=10)
sparkle2 = Sparkle(pixels, speed=0.5, color=PURPLE, num_sparkles=4)
off = Solid(pixels, color=BLACK)

# Animations Playlist - reorder as desired. AnimationGroups play at the same time
animations = AnimationSequence(

    rainbow_comet2, #
    rainbow_comet, #
    chase, #
    rainbow_chase, #
    rainbow, #

    AnimationGroup(
        sparkle,
        strum,
        ),
    AnimationGroup(
        sparkle2,
        rainbow_comet3,
        ),
    off,
    auto_clear=True,
    auto_reset=True,
)


MODE = 1
LASTMODE = 1 # start up in sound reactive mode
i = 0
# Are we already advertising?
advertising = False

Finally, we get to the main program loop. The pixels will turn on and play the first animation in the Animation Sequence when they're powered up. The bluetooth will begin advertising. 

This is the area where you can define which modes play when you press each of the buttons in the Bluetooth Control App. Just change the number for each mode button called by animations.animate().

Mode 4 turns on Sound Reactive mode, and I've assigned button 4 to activate it.

while True:
    animations.animate()
    if not ble.connected and not advertising:
        ble.start_advertising(advertisement)
        advertising = True

    # Are we connected via Bluetooth now?
    if ble.connected:
        # Once we're connected, we're not advertising any more.
        advertising = False
        # Have we started to receive a packet?
        if uart.in_waiting:
            packet = Packet.from_stream(uart)
            if isinstance(packet, ColorPacket):
                # Set all the pixels to one color and stay there.
                pixels.fill(packet.color)
                pixels.show()
                MODE = 2
            elif isinstance(packet, ButtonPacket):
                if packet.pressed:
                    if packet.button == ButtonPacket.BUTTON_1:
                        animations.activate(1)
                    elif packet.button == ButtonPacket.BUTTON_2:
                        MODE = 1
                        animations.activate(2)
                    elif packet.button == ButtonPacket.BUTTON_3:
                        MODE = 1
                        animations.activate(3)
                    elif packet.button == ButtonPacket.BUTTON_4:
                        MODE = 4

                    elif packet.button == ButtonPacket.UP:
                        pixels.brightness = pixels.brightness + 0.1
                        pixels.show()
                        if pixels.brightness > 1:
                            pixels.brightness = 1
                    elif packet.button == ButtonPacket.DOWN:
                        pixels.brightness = pixels.brightness - 0.1
                        pixels.show()
                        if pixels.brightness < 0.1:
                            pixels.brightness = 0.1
                    elif packet.button == ButtonPacket.RIGHT:
                        MODE = 1
                        animations.next()
                    elif packet.button == ButtonPacket.LEFT:
                        animations.activate(7)
            animations.animate()
        
        if MODE == 2:
            animations.freeze()
        if MODE == 4:
            animations.freeze()
            pixels.fill(YELLOW)
            mic.record(samples, len(samples))
            magnitude = normalized_rms(samples)
            # You might want to print this to see the values.
            #print(magnitude)

            # Compute scaled logarithmic reading in the range 0 to NUM_PIXELS
            c = log_scale(constrain(magnitude, input_floor, input_ceiling),
                          input_floor, input_ceiling, 0, NUM_PIXELS)

            # Light up pixels that are below the scaled and interpolated magnitude.
            #pixels.fill(0)
            for i in range(NUM_PIXELS):
                if i < c:
                    pixels[i] = volume_color(i)
                # Light up the peak pixel and animate it slowly dropping.
                if c >= peak:
                    peak = min(c, NUM_PIXELS - 1)
                elif peak > 0:
                    peak = peak - 0.01
                if peak > 0:
                    pixels[int(peak)] = PEAK_COLOR
            pixels.show()

Troubleshooting

If you're having trouble getting the code to load, head over to the Circuit Playground Bluefruit guide for more detailed instructions and things to try.

The Mu Editor has a very handy feature called the REPL, which will help you with debugging and give you feedback about what may be going wrong. Click the Serial button in the toolbar to access the REPL. Here's a lot more info about this process -- it's invaluable in debugging your code.

This guide was first published on Apr 28, 2021. It was last updated on Apr 28, 2021.

This page (Code Walkthrough) was last updated on Apr 28, 2021.

Text editor powered by tinymce.