code.py
1 # SPDX-FileCopyrightText: 2019 Collin Cunningham for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 """ 6 LED Disco Tie with Bluetooth 7 ========================================================= 8 Give your suit an sound-reactive upgrade with Circuit 9 Playground Bluefruit & Neopixels. Set color and animation 10 mode using the Bluefruit LE Connect app. 11 12 Author: Collin Cunningham for Adafruit Industries, 2019 13 """ 14 # pylint: disable=global-statement 15 16 import time 17 import array 18 import math 19 import audiobusio 20 import board 21 from rainbowio import colorwheel 22 import neopixel 23 24 from adafruit_ble import BLERadio 25 from adafruit_ble.advertising.standard import ProvideServicesAdvertisement 26 from adafruit_ble.services.nordic import UARTService 27 from adafruit_bluefruit_connect.packet import Packet 28 from adafruit_bluefruit_connect.color_packet import ColorPacket 29 from adafruit_bluefruit_connect.button_packet import ButtonPacket 30 31 ble = BLERadio() 32 uart_service = UARTService() 33 advertisement = ProvideServicesAdvertisement(uart_service) 34 35 # User input vars 36 mode = 0 # 0=audio, 1=rainbow, 2=larsen_scanner, 3=solid 37 user_color= (127,0,0) 38 39 # Audio meter vars 40 PEAK_COLOR = (100, 0, 255) 41 NUM_PIXELS = 10 42 NEOPIXEL_PIN = board.A1 43 # Use this instead if you want to use the NeoPixels on the Circuit Playground Bluefruit. 44 # NEOPIXEL_PIN = board.NEOPIXEL 45 CURVE = 2 46 SCALE_EXPONENT = math.pow(10, CURVE * -0.1) 47 NUM_SAMPLES = 160 48 49 # Restrict value to be between floor and ceiling. 50 def constrain(value, floor, ceiling): 51 return max(floor, min(value, ceiling)) 52 53 # Scale input_value between output_min and output_max, exponentially. 54 def log_scale(input_value, input_min, input_max, output_min, output_max): 55 normalized_input_value = (input_value - input_min) / \ 56 (input_max - input_min) 57 return output_min + \ 58 math.pow(normalized_input_value, SCALE_EXPONENT) \ 59 * (output_max - output_min) 60 61 # Remove DC bias before computing RMS. 62 def normalized_rms(values): 63 minbuf = int(mean(values)) 64 samples_sum = sum( 65 float(sample - minbuf) * (sample - minbuf) 66 for sample in values 67 ) 68 69 return math.sqrt(samples_sum / len(values)) 70 71 def mean(values): 72 return sum(values) / len(values) 73 74 def volume_color(volume): 75 return 200, volume * (255 // NUM_PIXELS), 0 76 77 # Set up NeoPixels and turn them all off. 78 pixels = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=0.1, auto_write=False) 79 pixels.fill(0) 80 pixels.show() 81 82 mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, 83 sample_rate=16000, bit_depth=16) 84 85 # Record an initial sample to calibrate. Assume it's quiet when we start. 86 samples = array.array('H', [0] * NUM_SAMPLES) 87 mic.record(samples, len(samples)) 88 # Set lowest level to expect, plus a little. 89 input_floor = normalized_rms(samples) + 10 90 # Corresponds to sensitivity: lower means more pixels light up with lower sound 91 input_ceiling = input_floor + 500 92 peak = 0 93 94 95 def rainbow_cycle(delay): 96 for j in range(255): 97 for i in range(NUM_PIXELS): 98 pixel_index = (i * 256 // NUM_PIXELS) + j 99 pixels[i] = colorwheel(pixel_index & 255) 100 pixels.show() 101 time.sleep(delay) 102 103 104 def audio_meter(new_peak): 105 mic.record(samples, len(samples)) 106 magnitude = normalized_rms(samples) 107 108 # Compute scaled logarithmic reading in the range 0 to NUM_PIXELS 109 c = log_scale(constrain(magnitude, input_floor, input_ceiling), 110 input_floor, input_ceiling, 0, NUM_PIXELS) 111 112 # Light up pixels that are below the scaled and interpolated magnitude. 113 pixels.fill(0) 114 for i in range(NUM_PIXELS): 115 if i < c: 116 pixels[i] = volume_color(i) 117 # Light up the peak pixel and animate it slowly dropping. 118 if c >= new_peak: 119 new_peak = min(c, NUM_PIXELS - 1) 120 elif new_peak > 0: 121 new_peak = new_peak - 1 122 if new_peak > 0: 123 pixels[int(new_peak)] = PEAK_COLOR 124 pixels.show() 125 return new_peak 126 127 pos = 0 # position 128 direction = 1 # direction of "eye" 129 130 def larsen_set(index, color): 131 if index < 0: 132 return 133 else: 134 pixels[index] = color 135 136 def larsen(delay): 137 global pos 138 global direction 139 color_dark = (int(user_color[0]/8), int(user_color[1]/8), 140 int(user_color[2]/8)) 141 color_med = (int(user_color[0]/2), int(user_color[1]/2), 142 int(user_color[2]/2)) 143 144 larsen_set(pos - 2, color_dark) 145 larsen_set(pos - 1, color_med) 146 larsen_set(pos, user_color) 147 larsen_set(pos + 1, color_med) 148 149 if (pos + 2) < NUM_PIXELS: 150 # Dark red, do not exceed number of pixels 151 larsen_set(pos + 2, color_dark) 152 153 pixels.write() 154 time.sleep(delay) 155 156 # Erase all and draw a new one next time 157 for j in range(-2, 2): 158 larsen_set(pos + j, (0, 0, 0)) 159 if (pos + 2) < NUM_PIXELS: 160 larsen_set(pos + 2, (0, 0, 0)) 161 162 # Bounce off ends of strip 163 pos += direction 164 if pos < 0: 165 pos = 1 166 direction = -direction 167 elif pos >= (NUM_PIXELS - 1): 168 pos = NUM_PIXELS - 2 169 direction = -direction 170 171 def solid(new_color): 172 pixels.fill(new_color) 173 pixels.show() 174 175 def map_value(value, in_min, in_max, out_min, out_max): 176 out_range = out_max - out_min 177 in_range = in_max - in_min 178 return out_min + out_range * ((value - in_min) / in_range) 179 180 speed = 6.0 181 wait = 0.097 182 183 def change_speed(mod, old_speed): 184 new_speed = constrain(old_speed + mod, 1.0, 10.0) 185 return(new_speed, map_value(new_speed, 10.0, 0.0, 0.01, 0.3)) 186 187 def animate(pause, top): 188 # Determine animation based on mode 189 if mode == 0: 190 top = audio_meter(top) 191 elif mode == 1: 192 rainbow_cycle(0.001) 193 elif mode == 2: 194 larsen(pause) 195 elif mode == 3: 196 solid(user_color) 197 return top 198 199 while True: 200 ble.start_advertising(advertisement) 201 while not ble.connected: 202 # Animate while disconnected 203 peak = animate(wait, peak) 204 205 # While BLE is connected 206 while ble.connected: 207 if uart_service.in_waiting: 208 try: 209 packet = Packet.from_stream(uart_service) 210 # Ignore malformed packets. 211 except ValueError: 212 continue 213 214 # Received ColorPacket 215 if isinstance(packet, ColorPacket): 216 user_color = packet.color 217 218 # Received ButtonPacket 219 elif isinstance(packet, ButtonPacket): 220 if packet.pressed: 221 if packet.button == ButtonPacket.UP: 222 speed, wait = change_speed(1, speed) 223 elif packet.button == ButtonPacket.DOWN: 224 speed, wait = change_speed(-1, speed) 225 elif packet.button == ButtonPacket.BUTTON_1: 226 mode = 0 227 elif packet.button == ButtonPacket.BUTTON_2: 228 mode = 1 229 elif packet.button == ButtonPacket.BUTTON_3: 230 mode = 2 231 elif packet.button == ButtonPacket.BUTTON_4: 232 mode = 3 233 234 # Animate while connected 235 peak = animate(wait, peak)