/ Disco_Tie / code.py
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)