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)
}
```