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, 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.
/*
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
Hi zranger1 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.