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