It worked! I was able to get all the followers to source their ground-truth beat count from the leader and stay synced flawlessly. Here’s the updated script:
/*---------------------------------------------------/< Credits >/----------------------------------------------------\\
Much appreciation to Jeff Vyduna for his very extensive Music Sequencer posted here:
https://forum.electromage.com/t/music-sequencer-choreography/1549
This pattern slowly fades through the color spectrum and jumps to the next nearest color upon detection of a bass beat.
This version is specifically tailored to facilitate synchronization across a fleet of followers. It accomplishes this by
connecting IO pins 16, 17, and 19 on the leader Pixelblaze to the analog inputs "A0," "A1," and "A2" of its sensor
board. This allows the leader to handle all the beat detection and broadcast a state variable to all the followers
to prevent them from drifting out of sync with one another.
>> THIS PATTERN WILL NOT RUN PROPERLY UNLESS YOUR LEADER PIXELBLAZE HAS ITS IO PADS CONNECTED TO THE ANALOG <<
>> INPUTS ON ITS SENSOR BOARD AND YOU HAVE ASSIGNED IT A NODE NUMBER DIFFERENT THAN ITS FOLLOWERS <<
-MyMathematicalMind, 2023. MIT License
//---------------------------------------------------\< Credits >\----------------------------------------------------*/
//-----------------------------------------------/< Global Variables >/-----------------------------------------------\\
var leaderNode = 2 //**** MAKE SURE YOUR LEADER IS ASSIGNED A DIFFERENT NODE NUMBER THAN THE FOLLOWERS
//GPIO pin variables
var digitalOutputPins = [16, 17, 19, 21, 22] // IO pins: IO16(pin16), IO17(pin17), IO19(pin19), IO21(pin21), IO22(pin22)
for (i = 0; i < 5; i++) {pinMode(digitalOutputPins[i], OUTPUT)}
export var analogInputs
// Sensor board variables
export var light = -1 // If this remains at the impossible value of -1, a sensor board is not connected.
export var frequencyData = array(32) //Get all the frequencies samples in an array
//Color hue variables
var red = 0, yellow = 0.1, green = 0.33, cyan = 0.5, blue = 0.66, magenta = 0.9
export var colors = [red, yellow, green, cyan, blue, magenta]
export var currentColor = 0
//Timing/State Machine variables
export var BPM = 130 // Debounce timer is set to enable algorithm to catch 16th notes at this BPM
export var globalDelta = 0
export var elapsedTime = 0, currentTime = 0, prevTime = 0
export var beatCount = 0, trueBeatCount = 0
//Debounce variables
var minBeatRetrigger = .2 /* How much of a currently defined quarter note beat must pass before a detected instrument
will retrigger? E.g. use .2 to allow .25 retrigger (e.g. to catch sixteenth note drums)*/
var debounceTimer = beatsToMs(minBeatRetrigger)
//Bass variables
var bass, maxBass, bassOn /* Bass and beats. (bassOn==true) is what triggers the debounce calculation and ultimately
whether or not beatDetected() is called*/
var bassSlowEMA = .001, bassFastEMA = .001 // Exponential moving averages to compare to each other
var bassThreshold = .02 // Raise this if very soft music with no beats is still triggering the beat detector
var maxBass = bassThreshold // Maximum bass detected recently (while any bass above threshold was present)
var bassVelocitiesSize = 2*pow(beatSensitivity, 2) + 11*beatSensitivity + 2 /* bassVelocitiesSize=5 seems right for
most. Up to 15 for infrequent bass beats (slower reaction, longer decay), down to 2 for very fast triggering on doubled
kicks like in drum n bass */
var bassVelocities = array(bassVelocitiesSize) /* Circular buffer to store the last 5 first derivatives of the
`fast exponential avg/MaxSample`, used to calculate a running average */
var lastBassFastEMA = .5, bassVelocitiesAvg = .5
var bassVelocitiesPointer = 0 // Pointer for circular buffer
//Tempo inference variables
// Store the last 8 intervals between beats. Longest = 50BPM on beat 1 (4800 ms = 60 / 50BPM * 1000 * 4)
var beatIntervalSamples = 8, beatIntervalPtr = 0, beatIntervalTimer = 0
var beatIntervals = array(beatIntervalSamples)
//UI controlled variables
export var beatSensitivity = 0.5
export var fadeSpeed = 8, displayedFadeSpeed = 0.5
//-----------------------------------------------\< Global Variables >\-----------------------------------------------\\
//---------------------------------------------/< UI Control Functions >/---------------------------------------------\\
export function sliderBeatSensitivity(_v) { beatSensitivity = _v }
export function showNumberBeatSensitivity() { return beatSensitivity }
export function sliderFadeSpeed(_v) {
displayedFadeSpeed = _v
fadeSpeed = 4 + 8*_v
}
export function showNumberFadeSpeed() { return displayedFadeSpeed }
//---------------------------------------------\< UI Control Functions >\---------------------------------------------\\
//-------------------------------------------/< Beat Detection Functions >/-------------------------------------------\\
function processSound(delt) {
processInstruments(delt)
inferTempo(delt)
}
// Debounce detector
function debounce(trigger, fn, duration, elapsed) {
if (trigger && debounceTimer <= 0) {
fn()
debounceTimer = duration
} else {
debounceTimer = max(-3e4, debounceTimer - elapsed)
}
}
function processInstruments(delt) {
// Assume Sensor Board updates at 40Hz (25ms); Max BPM 180 = 333ms or 13 samples; Typical BPM 500ms, 20 samples
// Kickdrum fundamental 40-80Hz. https://www.bhencke.com/pixelblaze-sensor-expansion
bass = frequencyData[1] + frequencyData[2] + frequencyData[3]
maxBass = max(maxBass, bass)
if (maxBass > 10 * bassSlowEMA && maxBass > bassThreshold) maxBass *= .99 // AGC - Auto gain control
bassSlowEMA = (bassSlowEMA * 999 + bass) / 1000
bassFastEMA = (bassFastEMA * 9 + bass) / 10
}
function inferTempo(delt) {
bassVelocities[bassVelocitiesPointer] = (bassFastEMA - lastBassFastEMA) / maxBass /* Normalized first derivative of
fast moving expo avg */
bassVelocitiesAvg += bassVelocities[bassVelocitiesPointer] / bassVelocitiesSize
bassVelocitiesPointer = (bassVelocitiesPointer + 1) % bassVelocitiesSize
bassVelocitiesAvg -= bassVelocities[bassVelocitiesPointer] / bassVelocitiesSize
bassOn = bassVelocitiesAvg > .51 // `bassOn` is true when bass is rising
debounce(bassOn, beatDetectedWrapper, beatsToMs(minBeatRetrigger), delt)
beatIntervalTimer += delt
// Longest = 50BPM on beat 1 (4800 ms = 60 / 50BPM * 1000 * 4)
if (beatIntervalTimer > 5000) beatIntervalTimer = 5000 // No-beat ms threshold to reset beat detection
lastBassFastEMA = bassFastEMA
}
function beatDetectedWrapper() {
if (beatIntervalTimer >= 5000) { // Clear beat intervals, it's been too long since a beat
beatIntervals.mutate(() => 0)
beatIntervalTimer = beatIntervalPtr = 0
}
beatIntervals[beatIntervalPtr] = beatIntervalTimer
beatIntervalTimer = 0
beatIntervalPtr = (beatIntervalPtr + 1) % beatIntervalSamples
beatDetected() // Calls a user-customized function that happens whenever a beat is detected
}
// Do this whenever a beat is detected
function beatDetected() {
if (nodeId() == leaderNode) {
if (currentColor < colors[1]) {
beatCount = 1
} else if (currentColor < colors[2]) {
beatCount = 2
} else if (currentColor < colors[3]) {
beatCount = 3
} else if (currentColor < colors[4]) {
beatCount = 4
} else if (currentColor < colors[5]) {
beatCount = 5
} else {
beatCount = 0
}
trueBeatCount = beatCount
setBeatColor()
}
}
//-------------------------------------------\< Beat Detection Functions >\-------------------------------------------\\
//----------------------------------------/< Timing/State Machine Functions >/----------------------------------------\\
function updateDeltaTimer() {
currentTime = 1000*time(1/65.536)
if (currentTime >= prevTime) {
elapsedTime += currentTime-prevTime
} else {
elapsedTime += 1 - (prevTime - currentTime)
}
prevTime = currentTime
globalDelta = elapsedTime
elapsedTime = 0
}
function txTrueBeatCnt() {
if (trueBeatCount == 0) {
digitalWrite(digitalOutputPins[0], 0)
digitalWrite(digitalOutputPins[1], 0)
digitalWrite(digitalOutputPins[2], 0)
} else if (trueBeatCount == 1) {
digitalWrite(digitalOutputPins[0], 1)
digitalWrite(digitalOutputPins[1], 0)
digitalWrite(digitalOutputPins[2], 0)
} else if (trueBeatCount == 2) {
digitalWrite(digitalOutputPins[0], 0)
digitalWrite(digitalOutputPins[1], 1)
digitalWrite(digitalOutputPins[2], 0)
} else if (trueBeatCount == 3) {
digitalWrite(digitalOutputPins[0], 1)
digitalWrite(digitalOutputPins[1], 1)
digitalWrite(digitalOutputPins[2], 0)
} else if (trueBeatCount == 4) {
digitalWrite(digitalOutputPins[0], 0)
digitalWrite(digitalOutputPins[1], 0)
digitalWrite(digitalOutputPins[2], 1)
} else {
digitalWrite(digitalOutputPins[0], 1)
digitalWrite(digitalOutputPins[1], 0)
digitalWrite(digitalOutputPins[2], 1)
}
}
function rxTrueBeatCnt() {
trueBeatCount = round(analogInputs[0]) + round(analogInputs[1])*2 + round(analogInputs[2])*4
}
//----------------------------------------\< Timing/State Machine Functions >\----------------------------------------\\
//------------------------------------------------/< Misc Functions >/------------------------------------------------\\
function SB() { return light != -1 } //Function to check if sensor board is connected: if(SB())==true if detected
function beatsToMs(_beats) { return (1000 / BPM * 60 * _beats) }
function progressColor() {
currentColor += deltaColor(beatCount, 0.001)
}
function setBeatColor() {currentColor = colors[beatCount]}
// Calculate color increment based upon beatCount and timeStep
function deltaColor(i, dt) {
if (i != 5) {
return (
dt*(colors[i + 1] - colors[i])*fadeSpeed
)
} else {
return (
dt*(1 - colors[i])*fadeSpeed
)
}
}
function round(_v) {
return ceil(_v - 0.5)
}
//------------------------------------------------\< Misc Functions >\------------------------------------------------\\
//----------------------------------------------/< Runtime Functions >/-----------------------------------------------\\
export function beforeRender(globalDelta) {
updateDeltaTimer()
if (SB()) processSound(globalDelta) //Update all sound related variables to be used by the patterns
if (time(0.001/65.536) == 0) progressColor()
if (nodeId() == leaderNode) {
txTrueBeatCnt()
} else {
rxTrueBeatCnt()
if (beatCount != trueBeatCount) {
beatCount = trueBeatCount
setBeatColor()
}
}
}
export function render2D(index, x, y) {
hsv(currentColor, 1, 1)
}
//----------------------------------------------\< Runtime Functions >\-----------------------------------------------\\
Only needed 8 of the 32 bits to accomplish it too, lol.
However, in my excitement to share my success with my friends, I brought a couple of the LED bars out to an event last night and when I went to fire up the first bar (the one with the leader attached) I ran into some problems. As it turns out, without any strain relief on the connections soldered to the LED output, the ground connector had finally had enough and it snapped off as I was pulling the PB out of its enclosure.
Right before this happened, I had been connecting to the board’s WiFi and it threw out a message that Pixelblaze had recovered from a fail-state and had re-booted into fail-safe mode. After getting back home, re-soldering the ground connection, and trying to fire it up again, now the code won’t run properly .
I’m thinking maybe I need to do a full factory-firmware reset, but I’m not sure how I go about flashing the board and I can’t find instructions on how to do so anywhere.
Besides that, I suppose it’s possible that when the ground connection snapped it came into contact with the data connection and shorted, but there’s no visible indicators on the board that would point to that.
Any further advice you guys (@wizard @jeff) have for me would be greatly appreciated.