|
| 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 | +# pylint: disable=global-statement |
| 11 | + |
| 12 | +import time |
| 13 | +import array |
| 14 | +import math |
| 15 | +import audiobusio |
| 16 | +import board |
| 17 | +import neopixel |
| 18 | +from adafruit_ble.uart_server import UARTServer |
| 19 | +from adafruit_bluefruit_connect.packet import Packet |
| 20 | +from adafruit_bluefruit_connect.color_packet import ColorPacket |
| 21 | +from adafruit_bluefruit_connect.button_packet import ButtonPacket |
| 22 | + |
| 23 | +uart_server = UARTServer() |
| 24 | + |
| 25 | +# User input vars |
| 26 | +mode = 0 # 0=audio, 1=rainbow, 2=larsen_scanner, 3=solid |
| 27 | +user_color= (127,0,0) |
| 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(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 wheel_pos < 0 or wheel_pos > 255: |
| 85 | + r = g = b = 0 |
| 86 | + elif wheel_pos < 85: |
| 87 | + r = int(wheel_pos * 3) |
| 88 | + g = int(255 - wheel_pos*3) |
| 89 | + b = 0 |
| 90 | + elif wheel_pos < 170: |
| 91 | + wheel_pos -= 85 |
| 92 | + r = int(255 - wheel_pos*3) |
| 93 | + g = 0 |
| 94 | + b = int(wheel_pos*3) |
| 95 | + else: |
| 96 | + wheel_pos -= 170 |
| 97 | + r = 0 |
| 98 | + g = int(wheel_pos*3) |
| 99 | + b = int(255 - wheel_pos*3) |
| 100 | + return (r, g, b) |
| 101 | + |
| 102 | +def rainbow_cycle(delay): |
| 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(delay) |
| 109 | + |
| 110 | +def audio_meter(new_peak): |
| 111 | + mic.record(samples, len(samples)) |
| 112 | + magnitude = normalized_rms(samples) |
| 113 | + |
| 114 | + # Compute scaled logarithmic reading in the range 0 to NUM_PIXELS |
| 115 | + c = log_scale(constrain(magnitude, input_floor, input_ceiling), |
| 116 | + input_floor, input_ceiling, 0, NUM_PIXELS) |
| 117 | + |
| 118 | + # Light up pixels that are below the scaled and interpolated magnitude. |
| 119 | + pixels.fill(0) |
| 120 | + for i in range(NUM_PIXELS): |
| 121 | + if i < c: |
| 122 | + pixels[i] = volume_color(i) |
| 123 | + # Light up the peak pixel and animate it slowly dropping. |
| 124 | + if c >= new_peak: |
| 125 | + new_peak = min(c, NUM_PIXELS - 1) |
| 126 | + elif new_peak > 0: |
| 127 | + new_peak = new_peak - 1 |
| 128 | + if new_peak > 0: |
| 129 | + pixels[int(new_peak)] = PEAK_COLOR |
| 130 | + pixels.show() |
| 131 | + return new_peak |
| 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(delay): |
| 143 | + global pos |
| 144 | + global direction |
| 145 | + color_dark = (int(user_color[0]/8), int(user_color[1]/8), |
| 146 | + int(user_color[2]/8)) |
| 147 | + color_med = (int(user_color[0]/2), int(user_color[1]/2), |
| 148 | + int(user_color[2]/2)) |
| 149 | + |
| 150 | + larsen_set(pos - 2, color_dark) |
| 151 | + larsen_set(pos - 1, color_med) |
| 152 | + larsen_set(pos, user_color) |
| 153 | + larsen_set(pos + 1, color_med) |
| 154 | + |
| 155 | + if (pos + 2) < NUM_PIXELS: |
| 156 | + # Dark red, do not exceed number of pixels |
| 157 | + larsen_set(pos + 2, color_dark) |
| 158 | + |
| 159 | + pixels.write() |
| 160 | + time.sleep(delay) |
| 161 | + |
| 162 | + # Erase all and draw a new one next time |
| 163 | + for j in range(-2, 2): |
| 164 | + larsen_set(pos + j, (0, 0, 0)) |
| 165 | + if (pos + 2) < NUM_PIXELS: |
| 166 | + larsen_set(pos + 2, (0, 0, 0)) |
| 167 | + |
| 168 | + # Bounce off ends of strip |
| 169 | + pos += direction |
| 170 | + if pos < 0: |
| 171 | + pos = 1 |
| 172 | + direction = -direction |
| 173 | + elif pos >= (NUM_PIXELS - 1): |
| 174 | + pos = NUM_PIXELS - 2 |
| 175 | + direction = -direction |
| 176 | + |
| 177 | +def solid(new_color): |
| 178 | + pixels.fill(new_color) |
| 179 | + pixels.show() |
| 180 | + |
| 181 | +def map_value(value, in_min, in_max, out_min, out_max): |
| 182 | + out_range = out_max - out_min |
| 183 | + in_range = in_max - in_min |
| 184 | + return out_min + out_range * ((value - in_min) / in_range) |
| 185 | + |
| 186 | +speed = 6.0 |
| 187 | +wait = 0.097 |
| 188 | + |
| 189 | +def change_speed(mod, old_speed): |
| 190 | + new_speed = constrain(old_speed + mod, 1.0, 10.0) |
| 191 | + return(new_speed, map_value(new_speed, 10.0, 0.0, 0.01, 0.3)) |
| 192 | + |
| 193 | +while True: |
| 194 | + # While BLE is *not* connected |
| 195 | + if not uart_server.connected: |
| 196 | + # OK to call again even if already advertising |
| 197 | + uart_server.start_advertising() |
| 198 | + |
| 199 | + # While BLE is connected |
| 200 | + else: |
| 201 | + if uart_server.in_waiting: |
| 202 | + packet = Packet.from_stream(uart_server) |
| 203 | + |
| 204 | + # Received ColorPacket |
| 205 | + if isinstance(packet, ColorPacket): |
| 206 | + user_color = packet.color |
| 207 | + |
| 208 | + # Received ButtonPacket |
| 209 | + elif isinstance(packet, ButtonPacket): |
| 210 | + if packet.pressed: |
| 211 | + if packet.button == ButtonPacket.UP: |
| 212 | + speed, wait = change_speed(1, speed) |
| 213 | + elif packet.button == ButtonPacket.DOWN: |
| 214 | + speed, wait = change_speed(-1, speed) |
| 215 | + elif packet.button == ButtonPacket.BUTTON_1: |
| 216 | + mode = 0 |
| 217 | + elif packet.button == ButtonPacket.BUTTON_2: |
| 218 | + mode = 1 |
| 219 | + elif packet.button == ButtonPacket.BUTTON_3: |
| 220 | + mode = 2 |
| 221 | + elif packet.button == ButtonPacket.BUTTON_4: |
| 222 | + mode = 3 |
| 223 | + |
| 224 | + # Determine animation based on mode |
| 225 | + if mode == 0: |
| 226 | + peak = audio_meter(peak) |
| 227 | + elif mode == 1: |
| 228 | + rainbow_cycle(0.001) |
| 229 | + elif mode == 2: |
| 230 | + larsen(wait) |
| 231 | + elif mode == 3: |
| 232 | + solid(user_color) |
0 commit comments