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.
Page last edited March 08, 2024
Text editor powered by tinymce.