code.py
  1  # SPDX-FileCopyrightText: 2020 Jeff Epler for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  """Waterfall FFT demo adapted from
  6  https://teaandtechtime.com/fft-circuitpython-library/
  7  to work with ulab on Adafruit CLUE"""
  8  
  9  import array
 10  
 11  import board
 12  import audiobusio
 13  import displayio
 14  from ulab import numpy as np
 15  from ulab.scipy.signal import spectrogram
 16  
 17  display = board.DISPLAY
 18  
 19  # Create a heatmap color palette
 20  palette = displayio.Palette(52)
 21  for i, pi in enumerate((0xff0000, 0xff0a00, 0xff1400, 0xff1e00,
 22                          0xff2800, 0xff3200, 0xff3c00, 0xff4600,
 23                          0xff5000, 0xff5a00, 0xff6400, 0xff6e00,
 24                          0xff7800, 0xff8200, 0xff8c00, 0xff9600,
 25                          0xffa000, 0xffaa00, 0xffb400, 0xffbe00,
 26                          0xffc800, 0xffd200, 0xffdc00, 0xffe600,
 27                          0xfff000, 0xfffa00, 0xfdff00, 0xd7ff00,
 28                          0xb0ff00, 0x8aff00, 0x65ff00, 0x3eff00,
 29                          0x17ff00, 0x00ff10, 0x00ff36, 0x00ff5c,
 30                          0x00ff83, 0x00ffa8, 0x00ffd0, 0x00fff4,
 31                          0x00a4ff, 0x0094ff, 0x0084ff, 0x0074ff,
 32                          0x0064ff, 0x0054ff, 0x0044ff, 0x0032ff,
 33                          0x0022ff, 0x0012ff, 0x0002ff, 0x0000ff)):
 34      palette[51-i] = pi
 35  
 36  class RollingGraph(displayio.TileGrid):
 37      def __init__(self, scale=2):
 38          # Create a bitmap with heatmap colors
 39          self._bitmap = displayio.Bitmap(display.width//scale,
 40                                         display.height//scale, len(palette))
 41          super().__init__(self._bitmap, pixel_shader=palette)
 42  
 43          self.scroll_offset = 0
 44  
 45      def show(self, data):
 46          y = self.scroll_offset
 47          bitmap = self._bitmap
 48  
 49          board.DISPLAY.auto_refresh = False
 50          offset = max(0, (bitmap.width-len(data))//2)
 51          for x in range(min(bitmap.width, len(data))):
 52              bitmap[x+offset, y] = int(data[x])
 53  
 54          board.DISPLAY.auto_refresh = True
 55  
 56          self.scroll_offset = (y + 1) % self.bitmap.height
 57  
 58  group = displayio.Group(scale=3)
 59  graph = RollingGraph(3)
 60  fft_size = 256
 61  
 62  # Add the TileGrid to the Group
 63  group.append(graph)
 64  
 65  # Add the Group to the Display
 66  display.show(group)
 67  
 68  # instantiate board mic
 69  mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA,
 70                         sample_rate=16000, bit_depth=16)
 71  
 72  #use some extra sample to account for the mic startup
 73  samples_bit = array.array('H', [0] * (fft_size+3))
 74  
 75  # Main Loop
 76  def main():
 77      max_all = 10
 78  
 79      while True:
 80          mic.record(samples_bit, len(samples_bit))
 81          samples = np.array(samples_bit[3:])
 82          spectrogram1 = spectrogram(samples)
 83          # spectrum() is always nonnegative, but add a tiny value
 84          # to change any zeros to nonzero numbers
 85          spectrogram1 = np.log(spectrogram1 + 1e-7)
 86          spectrogram1 = spectrogram1[1:(fft_size//2)-1]
 87          min_curr = np.min(spectrogram1)
 88          max_curr = np.max(spectrogram1)
 89  
 90          if max_curr > max_all:
 91              max_all = max_curr
 92          else:
 93              max_curr = max_curr-1
 94  
 95          print(min_curr, max_all)
 96          min_curr = max(min_curr, 3)
 97          # Plot FFT
 98          data = (spectrogram1 - min_curr) * (51. / (max_all - min_curr))
 99          # This clamps any negative numbers to zero
100          data = data * np.array((data > 0))
101          graph.show(data)
102  
103  main()