235 lines
7 KiB
Python
Executable file
235 lines
7 KiB
Python
Executable file
# SPDX-FileCopyrightText: 2019 Collin Cunningham for Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
LED Disco Tie with Bluetooth
|
|
=========================================================
|
|
Give your suit an sound-reactive upgrade with Circuit
|
|
Playground Bluefruit & Neopixels. Set color and animation
|
|
mode using the Bluefruit LE Connect app.
|
|
|
|
Author: Collin Cunningham for Adafruit Industries, 2019
|
|
"""
|
|
# pylint: disable=global-statement
|
|
|
|
import time
|
|
import array
|
|
import math
|
|
import audiobusio
|
|
import board
|
|
from rainbowio import colorwheel
|
|
import neopixel
|
|
|
|
from adafruit_ble import BLERadio
|
|
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
|
|
from adafruit_ble.services.nordic import UARTService
|
|
from adafruit_bluefruit_connect.packet import Packet
|
|
from adafruit_bluefruit_connect.color_packet import ColorPacket
|
|
from adafruit_bluefruit_connect.button_packet import ButtonPacket
|
|
|
|
ble = BLERadio()
|
|
uart_service = UARTService()
|
|
advertisement = ProvideServicesAdvertisement(uart_service)
|
|
|
|
# User input vars
|
|
mode = 0 # 0=audio, 1=rainbow, 2=larsen_scanner, 3=solid
|
|
user_color= (127,0,0)
|
|
|
|
# Audio meter vars
|
|
PEAK_COLOR = (100, 0, 255)
|
|
NUM_PIXELS = 10
|
|
NEOPIXEL_PIN = board.A1
|
|
# Use this instead if you want to use the NeoPixels on the Circuit Playground Bluefruit.
|
|
# NEOPIXEL_PIN = board.NEOPIXEL
|
|
CURVE = 2
|
|
SCALE_EXPONENT = math.pow(10, CURVE * -0.1)
|
|
NUM_SAMPLES = 160
|
|
|
|
# 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
|
|
|
|
# Set up NeoPixels and turn them all off.
|
|
pixels = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=0.1, auto_write=False)
|
|
pixels.fill(0)
|
|
pixels.show()
|
|
|
|
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) + 10
|
|
# Corresponds to sensitivity: lower means more pixels light up with lower sound
|
|
input_ceiling = input_floor + 500
|
|
peak = 0
|
|
|
|
|
|
def rainbow_cycle(delay):
|
|
for j in range(255):
|
|
for i in range(NUM_PIXELS):
|
|
pixel_index = (i * 256 // NUM_PIXELS) + j
|
|
pixels[i] = colorwheel(pixel_index & 255)
|
|
pixels.show()
|
|
time.sleep(delay)
|
|
|
|
|
|
def audio_meter(new_peak):
|
|
mic.record(samples, len(samples))
|
|
magnitude = normalized_rms(samples)
|
|
|
|
# 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 >= new_peak:
|
|
new_peak = min(c, NUM_PIXELS - 1)
|
|
elif new_peak > 0:
|
|
new_peak = new_peak - 1
|
|
if new_peak > 0:
|
|
pixels[int(new_peak)] = PEAK_COLOR
|
|
pixels.show()
|
|
return new_peak
|
|
|
|
pos = 0 # position
|
|
direction = 1 # direction of "eye"
|
|
|
|
def larsen_set(index, color):
|
|
if index < 0:
|
|
return
|
|
else:
|
|
pixels[index] = color
|
|
|
|
def larsen(delay):
|
|
global pos
|
|
global direction
|
|
color_dark = (int(user_color[0]/8), int(user_color[1]/8),
|
|
int(user_color[2]/8))
|
|
color_med = (int(user_color[0]/2), int(user_color[1]/2),
|
|
int(user_color[2]/2))
|
|
|
|
larsen_set(pos - 2, color_dark)
|
|
larsen_set(pos - 1, color_med)
|
|
larsen_set(pos, user_color)
|
|
larsen_set(pos + 1, color_med)
|
|
|
|
if (pos + 2) < NUM_PIXELS:
|
|
# Dark red, do not exceed number of pixels
|
|
larsen_set(pos + 2, color_dark)
|
|
|
|
pixels.write()
|
|
time.sleep(delay)
|
|
|
|
# Erase all and draw a new one next time
|
|
for j in range(-2, 2):
|
|
larsen_set(pos + j, (0, 0, 0))
|
|
if (pos + 2) < NUM_PIXELS:
|
|
larsen_set(pos + 2, (0, 0, 0))
|
|
|
|
# Bounce off ends of strip
|
|
pos += direction
|
|
if pos < 0:
|
|
pos = 1
|
|
direction = -direction
|
|
elif pos >= (NUM_PIXELS - 1):
|
|
pos = NUM_PIXELS - 2
|
|
direction = -direction
|
|
|
|
def solid(new_color):
|
|
pixels.fill(new_color)
|
|
pixels.show()
|
|
|
|
def map_value(value, in_min, in_max, out_min, out_max):
|
|
out_range = out_max - out_min
|
|
in_range = in_max - in_min
|
|
return out_min + out_range * ((value - in_min) / in_range)
|
|
|
|
speed = 6.0
|
|
wait = 0.097
|
|
|
|
def change_speed(mod, old_speed):
|
|
new_speed = constrain(old_speed + mod, 1.0, 10.0)
|
|
return(new_speed, map_value(new_speed, 10.0, 0.0, 0.01, 0.3))
|
|
|
|
def animate(pause, top):
|
|
# Determine animation based on mode
|
|
if mode == 0:
|
|
top = audio_meter(top)
|
|
elif mode == 1:
|
|
rainbow_cycle(0.001)
|
|
elif mode == 2:
|
|
larsen(pause)
|
|
elif mode == 3:
|
|
solid(user_color)
|
|
return top
|
|
|
|
while True:
|
|
ble.start_advertising(advertisement)
|
|
while not ble.connected:
|
|
# Animate while disconnected
|
|
peak = animate(wait, peak)
|
|
|
|
# While BLE is connected
|
|
while ble.connected:
|
|
if uart_service.in_waiting:
|
|
try:
|
|
packet = Packet.from_stream(uart_service)
|
|
# Ignore malformed packets.
|
|
except ValueError:
|
|
continue
|
|
|
|
# Received ColorPacket
|
|
if isinstance(packet, ColorPacket):
|
|
user_color = packet.color
|
|
|
|
# Received ButtonPacket
|
|
elif isinstance(packet, ButtonPacket):
|
|
if packet.pressed:
|
|
if packet.button == ButtonPacket.UP:
|
|
speed, wait = change_speed(1, speed)
|
|
elif packet.button == ButtonPacket.DOWN:
|
|
speed, wait = change_speed(-1, speed)
|
|
elif packet.button == ButtonPacket.BUTTON_1:
|
|
mode = 0
|
|
elif packet.button == ButtonPacket.BUTTON_2:
|
|
mode = 1
|
|
elif packet.button == ButtonPacket.BUTTON_3:
|
|
mode = 2
|
|
elif packet.button == ButtonPacket.BUTTON_4:
|
|
mode = 3
|
|
|
|
# Animate while connected
|
|
peak = animate(wait, peak)
|