Mapping a spectrum analyser on to an irregular matrix of alternating size

Hi, Is there a way to map the spectrum analyser 1D 2D code onto an irregular matrix alternating between 32 and 24 pixels?

It’s a zig-zag matrix like this.

Hi and welcome!
The answer is, it is indeed possible to map the spectrum analyzer to pretty much any shape you need, if you have a mapping function for your matrix set up on your Pixelblaze first.

It’ll take a handful of modifications to the pattern to do this. What you’ll need to do is change it so that it uses the normalized Y coordinates (0.0 to 1.0) provided by your map instead of height (which is calculated in a way that assumes a rectangular matrix). This stuff is all found in the beforeRender() and render2D() functions.

This sounds trickier than it is – there are only a couple of places where height is used in the code, and it’s just a multiplier for the y coordinate.

Once you’ve done that, you have to fix the peak indicators so that instead of comparing to an absolute pixel number, they’re compared to the “real” y coordinate. Change the peak indicator clearing rate to your taste, and you’re done.

If you need to, click the “Source Code” item below to see my quick and dirty version. It could probably use additional polish, but I wound up changing only about 5 lines of the original.

Source Code
/*
  Sound - spectrum analyser 1D/2D
  
  Output demo: https://youtu.be/sZIZiAt9l4o
  (You can connect multiple Pixelblaze to a single sensor expansion board.)
  
  This pattern uses the sensor expansion board and a 2D LED matrix. It displays
  a spectrum analyser based on the frequency data from the microphone. This is a
  real time graph where the low frequencies are plotted on the left hand side,
  and higher frequencies are on the right.
  
  This pattern is meant to be displayed on an LED matrix or other 2D surface 
  defined in the Mapper tab. Using the computer graphics convention, (x, y) = 
  (0, 0) is the top left (positive y advances downwards). You will need to set
  the 'width' variable below to match the width of your matrix.
  
  There's also a 1D fallback and a spectrum simulator used when the sensor board
  is not detected.
  
  Generously contributed by ChrisNZ (Chris) from the Pixelblaze forums.
    https://forum.electromage.com/u/chrisnz
*/

// Set this to the width of your 2D display, or number of frequency bars to plot
width = 16
height = pixelCount / width

// Set the hue, saturation, and value for peak value indicators.
// E.g. For white peaks, set peakHSV[1] = 0. No peaks, set peakHSV[2] = 0
peakHSV = array(3)  // [h, s, v]
peakHSV[0] = 0; peakHSV[1] = 1; peakHSV[2] = 1


// Get frequency information from the sensor expansion board
export var frequencyData = array(32)
// Start with an impossible value to detect if the sensor board is connected
export var light = -1 

// Peak values for each bar, in the range 0..`height`
peaks = array(width)
// Current frequency values for each bar, in the range 0..`height`
fy = array(width)     
peakDropMs = 0  // This will accumulate `delta` to drop our peaks by a pixel

// Automatic gain / PI controller. See comments in "sound - blinkfade".
targetMax = .9       // Aim for a maximum bar of 90% full
// Approx rolling average of the maximum bar, for feedback into the PIController
averageMax = 0
pic = makePIController(.25, 1.8, 30, 0, 100)

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

  hueT = time(1 / 65.536)  // 1 second hue rotation
  
  peakDropMs += delta

  // Drop all the peaks every 100ms
  if (peakDropMs > 100) {
    peakDropMs = 0
    for (i = 0; i < width; i++) peaks[i] -= .1
  }

  if (light == -1) simulateSound() // `light` is >= 0 if the SB is connected
  
  currentMax = 0
  for (i = 0; i < width; i++) {
    logy = log(i / width + 1) // Plot lower bins (log of 2 = bottom 30%)
    // Determine the portion of the bar 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] = min(1, powerLevel)
    peaks[i] = max(peaks[i], fy[i] - .01)

    currentMax = max(currentMax, powerLevel)
  }
  averageMax = averageMax - (averageMax / 50) + (currentMax / 50)
}

export function render2D(index, x, y) {
  xPixel = floor(x * width)  // Converts 0..1 'world units' x into pixel width
  yPixel = 1-y //height - 1 - floor(y * height) // Invert so baseline is yPixel == 0

  h = hueT + x // Cycle the bar color through the rainbow. hsv() 'wraps' h.
  s = 1
  v = fy[xPixel] > yPixel  // Fill bars from 0..fy[xPixel]
  
  // If this is a peak pixel, apply the peakHSV color
  if (abs(peaks[xPixel] - yPixel) <= 0.035) {
    h = peakHSV[0]; s = peakHSV[1]; v = peakHSV[2]
  }
  
  hsv(h, s, v)
}

// The 1D fallback plots the raw 32-bin spectrum across all pixels in a strip
export function render(index) {
  h = hueT + index/pixelCount // Cycle bar color. Remember, hsv() 'wraps' h.
  
  // Spread all 32 bins across the strip and interpolate
  binPixelWidth = pixelCount / 31
  LBin = floor(index / binPixelWidth)
  RBinPct = (index % binPixelWidth) / binPixelWidth
  v = (1 - RBinPct) * frequencyData[LBin] + RBinPct * frequencyData[LBin + 1]
  v *= sensitivity // Scale by PI controller's sensitivity

  hsv(h, 1, v * v)
}


/*
  Simulate the sensor board variables used in this pattern, if no sensor board
  is detected. The values and waveforms were chosen to approximate the look when
  real sound is sensed for a basic 4-on-the-floor loop.
*/
BPM = 120
var measurePeriod = 4 * 60 / BPM / 65.536

function simulateSound() {
  tM = time(measurePeriod) // 2 seconds per measure @120 BPM
  tP = time(8 * measurePeriod) // 8 measures per phrase
  for (i = 0; i < 32; i++) frequencyData[i] = 0
  
  beat = (-4 * tM + 5) % 1 // 4 attacks per measure
  beat *= .02 * pow(beat, 4)  // Scale magnitude and make concave-up
  // Splay energy out, most energy at lowest frequency bins
  for (i = 0; i < 10; i++) frequencyData[i] += beat * (10 - i) / 10

  claps = .01 * square(2 * tM - .5, .10) // "&" of every beat
  for (i = 9; i < 14 + random(10); i++) 
    frequencyData[i] += claps * (.7 + .6 * random(i % 2))

  highHat = .003 * square(4 * tM - .5, .05) // Beats 2 and 4
  for (i = 20; i < 30; i++) {
    frequencyData[i] += highHat * (.8 + random(.4)) * (i % 3 < 2)
  }

  lead = 4 + floor(16 * wander(tP))  // Wandering fundamental synth's freq bin
  for (i = 4; i < 20; i++)
    // Excite the fundamental and, 20% of the time, 4 bins up
    frequencyData[i] += .005 * (lead == i || lead == (i - 4) * r(.2))
}

// Random-ish perlin-esque walk for t in 0..1, outputs 0..1
// https://www.desmos.com/calculator/enggm6rcrm
function wander(t) {
  t *= 49.261 // Selected so t's wraparound will have continuous output
  return (wave(t / 2) * wave(t / 3) * wave(t / 5) + wave(t / 7)) / 2
}

function r(p) { return random(1) < p } // Randomly true with probability p
1 Like

Hi zranger1 :slight_smile: thank you so much for your welcome and help with that. I also realised that the leds aren’t wired up as a zig-zag matrix, which I incorrectly stated in the OP.