Spectrum Analyser pattern

I’ve made a 24x16 matrix of LEDs that I’ve been having fun creating lots of patterns for. Here’s one that’s a spectrum analyser with peak level indicators and automatic gain. I’m not quite sure I have the auto gain fully correct, though it does seem to work OK in my testing so far. Also note that currently the code requires the width of the matrix to be explicitly defined. Any feedback or suggestions for improvements appreciated, otherwise feel free to use this in your own projects and let me know how it goes!

// Get frequency info from the sensor expansion board
export var frequencyData

width = 24
height = pixelCount / width

peaks = array(width)  // peak values for each bar, in the range [0, height)
fy = array(width)     // current frequency values for each bar, in the range [0, height)
peakDropMs = 0

// Automatic gain / PIController
targetMax = 0.9       // aim for a maximum bar of 90% full
averageMax = 0.0      // approx rolling average of the maximum bar, for feedback into the PIController
pic = makePIController(0.25, 1.8, 30, 0, 200)

function makePIController(kp, ki, start, min, max) {
  var pic = array(5)
  pic[0] = kp
  pic[1] = ki
  pic[2] = start
  pic[3] = min
  pic[4] = max
  return pic

function calcPIController(pic, err) {
  pic[2] = clamp(pic[2] + err, pic[3], pic[4])
  return pic[0] * err + pic[1] * pic[2]

export function beforeRender(delta) {
  // Calculate sensitivity based on how far away we are from our target maximum
  sensitivity = max(1, calcPIController(pic, targetMax - averageMax))

  t1 = time(.01)
  peakDropMs += delta

  // Drop all the peaks every 100ms
  if (peakDropMs > 100) {
      peakDropMs = 0
      for (i = 0; i < width; i++) {
        peaks[i] -= 1
  currentMax = 0.0
  for (i = 0; i < width; i++) {
    logy = log(i / width + 1)
    // Determine the portion of the bar that is filled based on the current sound level.
    // We use the PIController sensitivity to try and keep this at the targetMax
    powerLevel = frequencyData[logy * 32] * sensitivity
    fy[i] = floor(min(1, powerLevel) * (height - 1))
    peaks[i] = max(peaks[i], fy[i])
    currentMax = max(currentMax, powerLevel)
  averageMax = averageMax - (averageMax / 50) + (currentMax / 50)

export function render2D(index, x, y) {
  yPixel = floor(y * (width - 1) + 0.5)
  xPixel = height - 1 - floor(x * (height - 1) + 0.5)

  h = t1 + y - floor(t1 + y)
  s = peaks[yPixel] == xPixel ? 0 : 1
  v = fy[yPixel] > xPixel || peaks[yPixel] == xPixel ? 1 : 0
  hsv(h, s, v)

So I woke up today and decided today was the day I was finally going to port my old Arduino spectrum analyzer code over to PixelBlaze, so I opened my PixelBlaze editor and thought no, wait, I’ll check the forums real quick first. Then I find this was posted just 30 minutes prior. Took me a while to get the Mapper working (never tried it before) and rotated properly but it all works. Great post! Thanks for sharing!


Thanks for sharing this I will have a play later, not being the greatest at coding fun stuff like this is always a bonus and really appreciate, thank you. Tony

1 Like

This post makes me want to build a matrix!

Very cool @ChrisNZ! I can’t wait to try this out! Would you like the honor of publishing it to the pattern site? I think this will be a popular pattern for sure :slight_smile:

Thanks for sharing!

Thanks everyone! I’d be happy to publish it on the pattern site but I was hoping to make a few changes and improvements first based on any feedback, plus a couple of ideas I still have. If I publish it now can it be updated later, or is it better I wait until it’s a bit more polished?

I’m very pleased to hear Sunandmooncouture managed to get it working OK. Your comment about getting the rotation right made me realise my variables in render2D() are around the wrong way for the axis which no doubt you found confusing - I’ll fix that! (and I guess my Mapper logic needs correcting too)

One thing I don’t like is that when multiple bars clip past the maximum, all of their peak level indicators then drop back parallel with each other. That looks a bit awkward to me, so I was planning to try out a couple of other approaches to try and stagger them a bit more.

One other thing I was thinking of improving is the colouring of the bars so they’re a bit more interesting than the current generic rainbow.

I only get time to work on this at the weekends unfortunately, but hopefully I can get most of the above (along with any other suggestions?) done this Saturday.

Hi Chris!

This is really cool. I’ve been playing with it on a 32 wide by 8 high. It was pretty easy to follow.

Like you mentioned, I swapped the axes as you’ll see in my snippets below. Second, I see how you’re calculating the yPixel to the center frequency, but when you apply the floor I think it’s equivalent to this:

  xPixel = floor(x * width)
  yPixel = height - 1 - floor(y * height)

You can leave our your ternaries if you like - PB casts comparators to 1 and 0. So:
  peaks[yPixel] == xPixel ? 1 : 0

is equivalent to:

  peaks[yPixel] == xPixel

I also noticed my top row was only being triggered by the peaks, not the frequencyData. I changed this line in `beforeRender()`:
    fy[i] = floor(min(1, powerLevel) * (height - 1))

to this:

    fy[i] = floor(min(1, powerLevel) * height)

and then started to see a fuller vertical range.

To verify the issue and change, I commented out the peaks in render():

v = fy[xPixel] > yPixel // || peaks[xPixel] == yPixel

I also wanted the peaks to go away when everything is quiet - they were hanging around the bottom row for me. So I changed this:
    peaks[i] = max(peaks[i], fy[i])


    peaks[i] = max(peaks[i], fy[i] - 1)

Here's the final output:

I’m pretty sure that what I see going on in the low end with a sine wave tone is a combo of:

  1. Laptop speakers’ poor low end response
  2. You can see harmonics pretty clearly, maybe from physical resonances in the laptop and sensor board.
  3. The low end FFTs are the most sensitive to having the tone centered.

Thanks for posting!


Thanks for all the helpful fixes and improvements Jeff, really appreciated and they have saved me a fair bit of time today solving some of the very same issues! I’ve incorporated them into what I have plus fixed the x/y mapping logic. I’ll pulish what I have to the pattern site as it’s hopefully in good enough shape now for people to use as-is, or as a starting point for their own customisations.


1 Like