PB v3 max capability vs pattern bugs

So, I’ve finally finished a pattern I’ve been wanting to do for quite some time, implementing several new concepts. Unfortunately there weren’t many stepping stones functional in PB - I had a few concept patterns in PB, but mostly wrote it in notepad++ then pasted it in.

PBv3.12 (edit: also v3.16) seems to get stuck on ‘generating’ - am I asking too much, or is there some sort of a bug I’m not seeing?
This is intended for a 2D map - a loop of lights attached to a stable structure, with the PB attached to the centre.

I am using the accelerometer, and whenever a total magnitude over a threshold is reached, it sends a two ‘waves’ (pulses) starting from the nearest point to the impact and running each way. I’ve also dipped into the microphone, and if when a wave is being created, there is enough sound, it sets the color based on the max frequency.
In total, I wrote capability for 8 different pulses, 4 going each way.
Every beforeRender, it advances the angle of each pulse by the ‘speed’*time
with rendering, it checks if each pulse is within the wave distance from the angle of each pixel to center.
After it finds all the ‘active’ pulses for each given pixel, it largely averages the hues, averages and bumps up the v values, and pulls back on the saturation for each additional overlapping pulse, and returns this to the render function.

I’ve pasted the code below.
Thanks for your help!

/*
 Things that go bump (in the night)
 This pattern works best with a 2D circle/rectangle of lights.
 
 
 A pattern that uses the sensor expansion boards accellerometer and microphone.
 Whenever the PB is bumped, two waves are generated, both beginning near the projected point of impact, and one running each way.
 The color is locked in at wave creation time - if no sound, then it uses the base color.  If the sound is over the threshold, then
 the max frequency is used to set the hue.
 
 Brightness/v follows a wave pattern, however this (peak value) degrades over time.  Once the peak brightness is zero, it is no longer rendered/processed
 
 I opted to use a polar rendering system so as to allow easy calculation regardless of where the wave's origin was.
 
 Ideas for wave creation/integration/sensor usage from ZRanger1's Oasis and Roger Cheng's Accelerometer Test
 - so far I have yet to de

*/ 

// constant wave descriptor array indices.
// !!!Be extremely careful when changing or adding indices to this list!!!
// Not used in beforeRender() or render() -- it makes enough of a performance
// difference here to matter.  Static numbers are a hair faster.

var _vMax = 0;  // peak brightness (amplitude equivalent)
var _hue = 1;  // color of the wave
var _speed = 2;        // base wave movement speed 
var _curAngle = 3; // origin of the wave on the circle

var descriptorSize = 4;

// wave descriptors.  the *b waves are the backwards ones.
var wave1 = array(descriptorSize);
var wave1b = array(descriptorSize);
var wave2 = array(descriptorSize);
var wave2b = array(descriptorSize);
var wave3 = array(descriptorSize);
var wave3b = array(descriptorSize);
var wave4 = array(descriptorSize);
var wave4b = array(descriptorSize);

var waveFront = 0.5 // in radians, front half of the wave
var waveBack = 1 // radians, back half of the wave

export var aura = 0.66667

var baseSpeed = 0.6  // radians/second
var degradationRate = 0.4  // 0.4 would have a wave last 2.2 seconds
var lastAdvanced = 0

// the below variables are largely exported for debugging purposes
export var accelerometer  //3 element array with [x,y,z]
export var energyAverage // total audio volume
export var maxFrequency  // loudest frequency from sound board.  
export var magnitude  // need to find out what the realistic range of this is, to proportionally adjust it in the newWave function ***********
export var horizMagnitude

export var virtMagnitude
export var horizAngle
export var virtAngle
export var lastX
export var lastY
export var lastZ
export var deltaX
export var deltaY
export var deltaZ
export var deltaHMag
export var deltaVMag
export var deltaHAngle
export var deltaVAngle



// sound thresholds and adjustments
export var soundThreshold = 0.002  
var loudestSound = 0.06
var lowestSound = 150
var highestSound = 1000 //ranges 170-700 for talking between myself and Julia....
//this would give hues of  50-450/1000 or 0.05 for ben, 0.45 for julia

//accelerometer thresholds and adjustments
export var sensitivity = 0.02  // threshold for accelerometer reading to trigger event
var biggestMagnitude = 0.04 // tested max by simply tapping the accelerometer.  Will likely need to be adjusted after it is mounted.
// axis adjustments for mounting orientation.  +1 if upright, -1 if inverted
var adjustX = 1
var adjustY = -1
var adjustZ = 1

var debounceTime = 500  // time between events
var lastActivated = 0  // time of last event


// UI functions
export function sliderHue(v) {
  aura = v
}

export function readAccelerometer(delta) {
a = accelerometer  // left in for dampening equation below, should likely adapt
curX = accelerometer[0] * adjustX
curY = accelerometer[1] * adjustY
curZ = accelerometer[2] * adjustZ
// R = sqrt(x^2 + y^2 + z^2)
hyp = sqrt(pow(curX,2) + pow(curY,2) + pow(curZ,2) )
  
  // trigger only if magnitude greater than sensitivity setting
  if (hyp > sensitivity) {
    magnitude = hyp
    // initial calculation of momentary magnitudes/angles largely for debugging purposes
    horizMagnitude = sqrt(pow(curX,2) + pow(curY,2))
    horizAngle = (atan2(curY,curX) * 180)/PI;  // in degrees
	horizAngle = atan2(curY,curX)  // in radians
    //z and hypotinuse of x and y gives virt angle
    virtAngle = (atan2(horizMagnitude,curZ) * 180)/PI;  // in degrees
	virtAngle = atan2(horizMagnitude,curZ)   // in radians
    virtMagnitude = sqrt(pow(horizMagnitude,2) + pow (curZ,2))
    
    // calculate the delta values, intended to caluclate the impact angle and magnitude
    deltaX = curX-lastX
    deltaY = curY-lastY
    deltaZ = curZ-lastZ
    
    deltaHMag = sqrt(pow(deltaX,2) + pow(deltaY,2))
    //deltaHAngle = (atan2(deltaY,deltaX) * 180)/PI;  // in degrees
	deltaHAngle = atan2(deltaY,deltaX)  // in radians
    //deltaVAngle = (atan2(deltaHMag,deltaZ) * 180)/PI;  // in degrees
	deltaVAngle = atan2(deltaHMag, deltaZ)  // in radians.  Not currenly used in 2D implementation, but could be added for 3D purposes
    deltaVMag = sqrt(pow(deltaHMag,2) + pow (deltaZ,2))
    lastActivated = delta
	
	// create wave, with the horixontal angle, overall force, and timestamp
	var originAngle = (deltaHAngle + PI)%(PI*2)  // add 180 degrees or pi to the angle, then if over 2Pi, subtract 2Pi  (divide by 2Pi and give the remainder).  this gives the approximate angle the force came from.
	newWave(originAngle, magnitude, delta)
  }
  else {
    // continually set last values for sub-threshold readings, likely to be essentially the baseline
    lastX = curX
    lastY = curY
    lastZ = curZ
  }
}



var HALF_PI = PI/2
//NOTE: atan2 has a bug in V2.23, when fixed this can be replaces with atan2 directly
function arctan2(y, x) {
  if (x > 0) return atan(y/x)
  if (y > 0) return HALF_PI - atan(x/y)
  if (y < 0) return -HALF_PI - atan(x/y)
  if (x < 0) return PI + atan(y/x)
  return 1.0
}

//return the angle in radians, can be negative
function getAngleInRads(x, y) {
  //center the coordinate, then get the angle
  return arctan2(x - .5, y - .5)
}

//return the angle as a value between 0 and 1.0
//most of Pixelblaze's animation language uses this range
//it also happens to rotate the angle so that 0 is north
function getUnitAngle(x, y) {
  return (PI + arctan2(x - .5, y - .5))/PI2 
}

function newWave (HAngle, magnitude, delta)
{
	// read sound magnitude, if over threshold, then read max frequency and use those to create
	// search through waves, find one with lowest magnitude, and replace that one
	var goodWave = 1
	var currLow = wave1[0]
	var stop=false
	var newHue = aura
	var newV = magnitude/biggestMagnitude
	
	var newSpeed = baseSpeed
	var magSpeedThreshold = 0.6  // if magnitude over 60% of expected max, then increase speed above base rate
	
	if (newV > magSpeedThreshold) {
		newSpeed = baseSpeed * (1+newV)  // may need to adjust this, especially if using radians for angles
	}
	
	if (wave1[0] = 0) {  // if the first wave is off, just start with that one, skipping next evaluations)
		goodWave = 1
		curLow = 0
		stop = true
	}
	else if (wave2[0]<wave1[0]) {
		goodWave=2
		curLow=wave2[0]
		if (wave2[0]=0) stop = true
	}
	if ((stop=false) && (wave3[0]<curLow)) {  // if wave3 is the lowest
		goodWave=3
		curLow=wave3[0]
		if (wave3[0]=0) stop = true
	}
	if ((stop=false) && (wave4[0]<curLow)) {  // if wave3 is the lowest
		goodWave=4
		curLow=wave4[0]
		if (wave3[0]=0) stop = true
	}
	
    if (energyAverage>soundThreshold) {
		if (lowestSound>maxFrequency) newHue = 1 // to prevent values over 1 if a freq higher than expected is detected
		else newHue = (maxFrequency-lowestSound)/(highestSound-lowestSound) // gives a 0-1 value proportional to the space between lowst and highest expected maxFrequency.

		newV = (newV + (energyAverage/loudestSound))/2   // loud music has an energy average of 0.06, so average magnitude (0-1) with the proportion of energyAverage with its' max
	}
	else newHue = aura
	
	if (goodWave = 1) {
		initWave(wave1, magnitude, newHue, newSpeed, HAngle)
		initWave(wave1b, magnitude, newHue, -1*newSpeed, HAngle)
	}
	else if (goodWave = 2) {
		initWave(wave2, magnitude, newHue, newSpeed, HAngle)
		initWave(wave2b, magnitude, newHue, -1*newSpeed, HAngle)
	}
	else if (goodWave = 3) {
		initWave(wave3, magnitude, newHue, newSpeed, HAngle)
		initWave(wave3b, magnitude, newHue, -1*newSpeed, HAngle)
	}
	else if (goodWave = 4) {
		initWave(wave4, magnitude, newHue, newSpeed, HAngle)
		initWave(wave4b, magnitude, newHue, -1*newSpeed, HAngle)
	}
	
	lastActivated=delta
}

function initWave(w, vMax, hue, speed, curAngle) {
  w[_vMax] = vMax;
  w[_hue] = hue;
  w[_speed] = speed;
  w[_curAngle] = curAngle;
}

function advanceWaves (timeChange) {
	// advance position of waves and degrade waves, if not already deactivated
	//speed is in radians/sec, delta and timeChange is in miliseconds
	
	if (wave1[_vMax] >0) {
		wave1[_curAngle] = wave1[_curAngle] + wave1[_speed]* (timeChange/1000)
		wave1b[_curAngle] = wave1b[_curAngle] + wave1b[_speed]* (timeChange/1000)
		
		wave1[_vMax] = wave1[_vMax] - degradationRate* (timeChange/1000)
		wave1b[_vMax] = wave1b[_vMax] - degradationRate* (timeChange/1000)
	}
	
	if (wave2[_vMax] >0) {
		wave2[_curAngle] = wave2[_curAngle] + wave2[_speed]* (timeChange/1000)
		wave2b[_curAngle] = wave2b[_curAngle] + wave2b[_speed]* (timeChange/1000)
		
		wave2[_vMax] = wave2[_vMax] - degradationRate* (timeChange/1000)
		wave2b[_vMax] = wave2b[_vMax] - degradationRate* (timeChange/1000)
	}

	if (wave3[_vMax] >0) {
		wave3[_curAngle] = wave3[_curAngle] + wave3[_speed]* (timeChange/1000)
		wave3b[_curAngle] = wave3b[_curAngle] + wave3b[_speed]* (timeChange/1000)
		
		wave3[_vMax] = wave3[_vMax] - degradationRate* (timeChange/1000)
		wave3b[_vMax] = wave3b[_vMax] - degradationRate* (timeChange/1000)
	}
	
	if (wave4[_vMax] >0) {
		wave4[_curAngle] = wave4[_curAngle] + wave4[_speed]* (timeChange/1000)
		wave4b[_curAngle] = wave4b[_curAngle] + wave4b[_speed]* (timeChange/1000)
	
		wave4[_vMax] = wave4[_vMax] - degradationRate* (timeChange/1000)
		wave4b[_vMax] = wave4b[_vMax] - degradationRate* (timeChange/1000)
	}

	
}


function setup() {
  // initialize all waves
  initWave(wave1, 0, aura, baseSpeed, 0);  //wave, vMax, hue, speed, currentAngle
  initWave(wave1b, 0, aura, baseSpeed, 0); 
  initWave(wave2, 0, aura, baseSpeed, 0);
  initWave(wave2b, 0, aura, baseSpeed, 0);
  initWave(wave3, 0, aura, baseSpeed, 0);
  initWave(wave3b, 0, aura, baseSpeed, 0);
  initWave(wave4, 0, aura, baseSpeed, 0);
  initWave(wave4b, 0, aura, baseSpeed, 0);  
}


setup();

function waveStrength(wAngle,cAngle) {
	var vStrength = 0
	var backDist = -1
	var frontDist = -1
	var dAngle = cAngle-wAngle
	if ((dAngle >=0) && (dAngle <= waveFront)) { // if cAngle is ahead of wAngle, and within waveFront distance
		inFront = true
		frontDist = dAngle
	}
	else if ((cAngle < waveFront)&&((2*PI-wAngle+cAngle) < waveFront)) { // if cAngle just past zero, but within wafeFront distance form wAngle
		inFront= true
		frontDist = (2*PI-wAngle+cAngle)
	}
	else if ((dAngle < 0)&&(abs(dAngle) <= waveBack)) {  // if cAngle is behind wAngle, but within waveBack distance
		inBack = true
		backDist = abs(dAngle)
	}
	else if ((wAngle < waveBack)&&((2*PI-cAngle+wAngle) < waveBack)) { // if wAngle just past zero, but within waveBack distance of cAngle
		inBack = true
		backDist = (2*PI-cAngle+wAngle)
	}
	
	if (inFront) vStrength = sin((1/waveFront)*PI*frontDist)  // for the front half, use a sine wave modified to be 1 at zero, and sero at waveFront distance from zero radians
	if (inBack) vStrength = sin((1/waveBack)*PI*backDist)  // similar, except scaled to waveBack
	return vStrength
}


//integrator function:

function integratinator (delta, x, y) {
	var newHSV = array(3)
	var activeWaves =0
	var cumulativeHue =0
	var cumulativeV =0
	var tempAngle =0
	var tempStrength =0
	
	if (wave1[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave1[_vMax]*waveStrength(wave1[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave1[_hue]
			activeWaves=activeWaves+1
		}
	}
	
	if (wave1b[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave1b[_vMax]*waveStrength(wave1b[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave1b[_hue]
			activeWaves=activeWaves+1
		}
	}	
	
	if (wave2[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave2[_vMax]*waveStrength(wave2[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave2[_hue]
			activeWaves=activeWaves+1
		}
	}	
	
	if (wave2b[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave2b[_vMax]*waveStrength(wave2b[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave2b[_hue]
			activeWaves=activeWaves+1
		}
	}
	
	if (wave3[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave3[_vMax]*waveStrength(wave3[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave3[_hue]
			activeWaves=activeWaves+1
		}
	}	
	
	if (wave3b[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave3b[_vMax]*waveStrength(wave3b[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave3b[_hue]
			activeWaves=activeWaves+1
		}
	}
	
	if (wave4[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave4[_vMax]*waveStrength(wave4[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave4[_hue]
			activeWaves=activeWaves+1
		}
	}	
	
	if (wave4b[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave4b[_vMax]*waveStrength(wave4b[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave4b[_hue]
			activeWaves=activeWaves+1
		}
	}
	newHSV[0] = cumulativeHue/activeWaves //hue  - currently just a simple average of the hues.  Can consider other blends in the future.
	newHSV[1] = clamp((1-(activeWaves/10)),0,1)  // have saturation decrease with the more concurrent waves at a given point - essentially a white cap
	newHSV[2] = clamp((cumulativeV/activeWaves)+(activeWaves/8),0,1) // v - curently just a simple average with an additional 0.125 for every active wave.  Could consider quadratic easing or other methods.
	
	return newHSV

}

export function beforeRender(delta) {
	
	if (delta < lastActivated) {
		lastActivated = delta // if delta has wrapped around, set lastActivated to the new delta
		lastAdvanced = delta  // if delta has wrapped around, set lastAdvanced to the new delta
	}
	if (delta > (lastActivated + delta)) {  // only read accelerometer if debounce time has passed
		readAccelerometer(delta) // read accelerometer, and threshold reached a new wave will replace the smallest remaining wave
	}
	
	advanceWaves(delta-lastAdvanced)
	lastAdvanced = delta
	
}

export function render(index) {
	render2D(index, .5, index / pixelCount)
}

export function render2D(index, x, y) {
	var newHSV = array(3);
	newHSV = integratinator (x, y)
	hsv(newHSV[0],newHSV[1],newHSV[2])
}

export function render3D(index,x,y,z) {
	render2D(index,x,y)
}

I’m not surprised that’s stalling, that’s a huge amount of math.

Keeping in mind the two stroke engine analogy, and that you’re “stalling” it out, you might want figure out ways to reduce the calculations happening constantly. Could be done by reducing actual math (pre-calced tables?), for one example.

Even small things like 2*PI add up doing them repeatedly. Make those into pre-calced values (in this case, PI2 is an available constant, but if it wasn’t, use var twopi = PI * 2)and now it’s just a value instead of doing the math over and over.
Same with the “-wAngle+cAngle” math, you should precalc those at the top and then your logic doesn’t have to do the math over and over. You did that with dAngle, but don’t use it consistently. Do “-cAngle+wAngle” the same.

Get rid of the atan shim, and use the built-in.

And I’ve said this before, but this might be a case where polar coordinates isn’t the best choice, since adding polar vectors is more math-y than adding XY vectors (which are fairly simple).

I’ll play with this soon and see if I have any other tweaks, the above is just from code review.

Hey @bdm -

I’m not a low-level expert in how memory is allocated and freed on Pixelblaze, but I was able to get the preview to generate by taking your array declarations out of the functions and declare them once as globals that are reused.

So:

export function render2D(index, x, y) {
	var newHSV = array(3);
	newHSV = integratinator (x, y)
	hsv(newHSV[0],newHSV[1],newHSV[2])
}

Becomes:

var newHSV = array(3);
export function render2D(index, x, y) {
	newHSV = integratinator (x, y)
	hsv(newHSV[0],newHSV[1],newHSV[2])
}

You also need to do the same (and beware the naming conflict) for the

var newHSV = array(3)

around line 310, up top in the definition of integratinator()

I’m not seeing LED output after these changes, but it’s a start and the memory in use shown in the header bar drops significantly after this change.

1 Like

I’ll second Jeff’s and I did a LOT of math cleanup (example replacing timechanges/1000 with mtc, then degradationRate * mtc with dmtc, and so on…)

BUT I don’t get any pattern.

Suggestion:

Simplify

Get 1 wave working, remove ALL of the wave2/3/4 code
Get rid of the sound and acceleration stuff to start with.

Give us a working example, and maybe we can add things back in.
But it’s broken so far as I can see, and I can’t fix something I don’t understand how it works in the first place.

BTW, this code managed to break the Var Export and Generating sections (I’m unsure what broke first, maybe the PB just stalled out entirely?), but I ended up having to reset my PB3, cause it just locked things up. Once I cleaned up stuff, including Jeff’s change(s), that helped, but clearly, this ran over some limit in PB, and it just ‘died’. Maybe @wizard needs to add some sort of safety net or check for this?

Pixelblaze doesn’t have garbage collection yet, which is what many dynamic languages use to reclaim previously allocated memory that is no longer referenced so that it can be reused. This means you can only allocate a fixed number of arrays and/or elements for any given pattern lifetime.

The current count available is shown on the top right of the status area.

If you try to allocate too much, calls to array() will start to fail and will stop executing pattern code.

2 Likes

How do local vars declared in repeatedly called functions avoid this problem? Does the symbol exist scoped to a function namespace of sorts, such that it always reuses the same address in memory?

1 Like

Local vars and parameters are allocated on the stack, just like C and many other languages. There’s a decently sized stack, enough to do small recursive algorithms/fractals.

Global variables have their own special memory, and arrays are kept track of separately as well. Variables can store values, or in the case of an array or function, what is effectively a pointer to the object. So in this way arrays are passed by reference not by value.

PB Trivia: Pixelblaze’s virtual machine is a stack machine, so intermediate values calculated in expressions also go on the stack and are consumed by operators (like +) and built in functions.

One key idiomatic difference between Pixelblaze’s JavaScript light and full JavaScript is that you would rarely return an array from a function, rather you would pass the array to the function to fill in.

e.g.

var newHSV = array(3); //allocate array once
export function render2D(index, x, y) {
	integratinator (x, y, newHSV) //pass array instance to function
	hsv(newHSV[0],newHSV[1],newHSV[2])
}

Or perhaps commonly pass through a global, either a global array or a set of 3 global variables. e.g.

var newHSV = array(3); //allocate array once
export function render2D(index, x, y) {
	integratinator (x, y) //function will modify global newHSV as a side-effect
	hsv(newHSV[0],newHSV[1],newHSV[2])
}

With 3 discrete globals:

var newH, newS, newV
export function render2D(index, x, y) {
	integratinator (x, y) //function will modify global newH etc. as a side-effect
	hsv(newH,newS,newV)
}

Using globals too heavily can reduce readability/maintainability in large codebases. Functions without side-effects, or “pure” functions that only operate through parameters and return values are easier to reason about and debug.

Pixelblaze patterns are usually pretty simple and short, and the conveniences (and sometimes speed) of globals are applied generously.

3 Likes

Looking through the code, I think the timing/debounce stuff isn’t right. Delta is a small number, just the difference in time since the last render frame, not accumulated time.

If I move newHSV to a single global that is allocated once, and unconditionally call readAccelerometer and advanceWaves with delta then stuff happens on the display.

1 Like

All of the above is the sort of “deep dive” that many of us PBers love to learn, so thank you.

If we already need a “PB common tips” and “PB useful tricks” collection /page, we absolutely need a “PB - how it actually works” collected page of these tidbits.

1 Like

Thank-you all so much.
I’ve been slammed at work, so I’ve seen the replies but haven’t had the time to dig into them yet.
It looks like there were many (false) assumptions on my part, including equating delta with milis. I’ll need to revise a significant portion of the code, including the debounce and speed portions.
I also plan on reducing the operations substantially, and backing off fully to 1 wave at a time. I’ll move to globals where possible, especially arrays. It’s helpful to know what some realistic constraints are.
I will try to keep the polar coordinates for now (unlike my other thread where I was speculating on having expanding spheres, this is a much simpler approach). If it doesn’t work, I will probably go to a single polar calculation from the accelerometer and then try stay in the x/y/z system, or possibly even index, which should be fast even if a bit uneven around the corners.

I recognize this was quite buggy code - I had intended on working to clean it up, but with no errors thrown and the PB seemign to freeze up, I didn’t know where to start. My javascript skills are quite rusty indeed, so I’m sure I’m bringing Arduino/C / python / pascal bad habits and misinterpretations over as well.
I’m guessing I’ll be able to get into this on the weekend, so hopefully will have a much better next step to work from.

1 Like

Hi @Scruffynerf ,
I’m trying to look for mtc and dmtc, but can’t find documentation of this within the PB docs or more generally with regards to javascript. I do see separate js library called ‘mocktheclock’ .
What is mtc and dmtc? Is it something equivalent to millis?
in the PB clock functions doc, it has the days/hours/minutes/seconds functions, but I feel that accessing the high-level seconds functions would be even more work when all I want to do is track the passage of milliseconds.

You’re going to laugh:

var mtc = timechanges/1000;
var dmtc = degradationRate * mtc;

That’s all I meant. I was replacing the multiple times you did that repetitive math on multiple lines with doing the math once, and using a variable.

As for delta (and even that’s a convention), it’s actually this:

function beforerender(variablename commonly named delta)

It’s as @wizard referenced, literally the number of ticks from the last time you called the beforerender function. It gets shoved into a variable (often named delta), for you to use during beforerender (or to ignore)

Explain what you want to do with milliseconds, and we’ll explain a better way to do it.

Thanks for the clarification, @Scruffynerf
I was looking for a variable / function that reflects/returns the time passing in milliseconds. In the arduino world, this is millis , and it eventually loops around, which would explain some of the (now clearly) odd code to catch this. I imagine there is something to this extent under the surface, and it would be really nice if it could be uncovered, if it is not already. I don’t see anything to this extent in the docs…

I am used to using that for a debounce feature, and it would also be quite helpful if I want to limit new wave creation to a value far less frequent than the next pretender. Are there alternate / more appropriate ways of accomplishing these?

I’ll let @wizard explain why PB isn’t focused on millis. It’s absolutely a design choice.

That said, you can certainly check for and use the delta, which gives you elapsed time since the last beforerender. The correct way to do this is to add delta to a global variable, and then if that variable is larger than the amount you wish to measure, fire off something (usually with an if statement), to do whatever, and zero the accumulated variable so it can start again, next cycle.

You can get “millis” like so, but be warned that it is limited to the fixed point math range of ± 32,767, or just over 32 seconds.

var millis
export function beforeRender(delta) {
 millis += delta
}

Early prototypes/experiments with Pixelblaze had a t global that was millis()'s. By itself it wasn’t super useful in animations, so patterns would usually scale it to some interval.

For measuring the passage of time for e.g. a debounce timer or to prevent something from triggering too often, you can use a count-up or count-down timer. I wouldn’t recommend using timestamps and calculating deltas that way.

e.g. with exported vars so you can see it go:

export var upcountingTimer  = 0
export var upcountingTimerDone = false
export var downcountingTimer = 1000
export var downcountingTimerDone = false
export function beforeRender(delta) {
  if (upcountingTimer < 1000) {
    upcountingTimer += delta
    if (upcountingTimer >= 1000) {
      //timer is done!
      upcountingTimerDone = true
    }
  }

  if (downcountingTimer > 0) {
    downcountingTimer -= delta
    if(downcountingTimer <= 0) {
      //time is done!
      downcountingTimerDone = true
    }
  } 

  t1 = time(.1)
}

You can also scale delta if you don’t want to work in milliseconds.

There’s a seconds based version of setTimeout/clearTimeout API similar to JavaScript:

1 Like

Semi working pattern below. Thanks @wizard, @Scruffynerf and @jeff for your clarifications!
I’ve cleaned up some of the math, and cut down to a single wave. Understanding delta correctly (all in the name, clearly, I somehow ignored this in my incorrect associations), I’ve simplified the debounce and sinceLastActivated variables.
I’ve added a base pulse every two seconds. It appears that I need to sort out angles a bit (zero radians seems to have the waves start at x=0,y=1… perhaps an origin calculation issue), but the nuts and bolts are working, now to fine-tune… everything.
I think there is a good chance of this being able to be pulled off.

Here’s my v0.5 code:

/*
 Things that go bump (in the night)
 This pattern works best with a 2D circle/rectangle of lights.
 
 
 A pattern that uses the sensor expansion boards accellerometer and microphone.
 Whenever the PB is bumped, two waves are generated, both beginning near the projected point of impact, and one running each way.
 The color is locked in at wave creation time - if no sound, then it uses the base color.  If the sound is over the threshold, then
 the max frequency is used to set the hue.
 
 Brightness/v follows a wave pattern, however this (peak value) degrades over time.  Once the peak brightness is zero, it is no longer rendered/processed
 
 I opted to use a polar rendering system so as to allow easy calculation regardless of where the wave's origin was.
 
 Ideas for wave creation/integration/sensor usage from ZRanger1's Oasis and Roger Cheng's Accelerometer Test


*/ 

// constant wave descriptor array indices.
// !!!Be extremely careful when changing or adding indices to this list!!!
// Not used in beforeRender() or render() -- it makes enough of a performance
// difference here to matter.  Static numbers are a hair faster.

var _vMax = 0;  // peak brightness (amplitude equivalent)
var _hue = 1;  // color of the wave
var _speed = 2;        // base wave movement speed 
var _curAngle = 3; // origin of the wave on the circle

var descriptorSize = 4;

// wave descriptors.  the *b waves are the backwards ones.
var wave1 = array(descriptorSize);
var wave1b = array(descriptorSize);
var wave2 = array(descriptorSize);
var wave2b = array(descriptorSize);
//var wave3 = array(descriptorSize);
//var wave3b = array(descriptorSize);
//var wave4 = array(descriptorSize);
//var wave4b = array(descriptorSize);

var waveFront = 0.5 // in radians, front half of the wave
var waveBack = 1 // radians, back half of the wave

export var aura = 0.66667

var baseSpeed = 0.2  // radians/second
var degradationRate = 0.3  // 0.4 would have a wave last 2.2 seconds

// the below variables are largely exported for debugging purposes
export var accelerometer  //3 element array with [x,y,z]
export var energyAverage // total audio volume
export var maxFrequency  // loudest frequency from sound board.  
export var magnitude  // need to find out what the realistic range of this is, to proportionally adjust it in the newWave function ***********

//export var horizMagnitude
//export var virtMagnitude
//export var horizAngle
//export var virtAngle
var lastX
var lastY
var lastZ
export var deltaX
export var deltaY
export var deltaZ
export var deltaHMag
export var deltaVMag
export var deltaHAngle
export var deltaVAngle

export var originAngle

export var aWaveTracker=0
export var a2WaveTracker=0

export var waveCount = 0



// sound thresholds and adjustments
export var soundThreshold = 0.001
var loudestSound = 0.06
var lowestSound = 150
var highestSound = 1000 //ranges 170-700 for talking between myself and Julia....
//this would give hues of  50-450/1000 or 0.05 for ben, 0.45 for julia

//accelerometer thresholds and adjustments
export var sensitivity = 0.02  // threshold for accelerometer reading to trigger event
var biggestMagnitude = 0.04 // tested max by simply tapping the accelerometer.  Will likely need to be adjusted after it is mounted.
// axis adjustments for mounting orientation.  +1 if upright, -1 if inverted
var adjustX = 1
var adjustY = 1
var adjustZ = 1

var debounceTime = 500  // time between events
var idleWaveInterval = 8000 // idle wave interval
var sinceLastActivated = 0  // a counter leading up to debounceTime



// UI functions
export function sliderHue(v) {
  aura = v
}

export function readAccelerometer() {
a = accelerometer  // left in for dampening equation below, should likely adapt
curX = accelerometer[0] * adjustX
curY = accelerometer[1] * adjustY
curZ = accelerometer[2] * adjustZ
// R = sqrt(x^2 + y^2 + z^2)
hyp = sqrt(pow(curX,2) + pow(curY,2) + pow(curZ,2) )
  
  // trigger only if magnitude greater than sensitivity setting
  if (hyp > sensitivity) {
    magnitude = hyp
	
    // initial calculation of momentary magnitudes/angles largely for debugging purposes
    //horizMagnitude = sqrt(pow(curX,2) + pow(curY,2))
	//horizAngle = atan2(curY,curX)  // in radians
    //z and hypotinuse of x and y gives virt angle
	//virtAngle = atan2(horizMagnitude,curZ)   // in radians
    //virtMagnitude = sqrt(pow(horizMagnitude,2) + pow (curZ,2))
    
    // calculate the delta values, intended to caluclate the impact angle and magnitude
    deltaX = curX-lastX
    deltaY = curY-lastY
    deltaZ = curZ-lastZ
    
    deltaHMag = sqrt(pow(deltaX,2) + pow(deltaY,2))
	deltaHAngle = atan2(deltaY,deltaX)  // in radians
	deltaVAngle = atan2(deltaHMag, deltaZ)  // in radians.  Not currenly used in 2D implementation, but could be added for 3D purposes
    deltaVMag = sqrt(pow(deltaHMag,2) + pow (deltaZ,2))
    
	
	// create wave, with the horixontal angle, overall force
	originAngle = (deltaHAngle + PI)%(PI2)  // add 180 degrees or pi to the angle, then if over 2Pi, subtract 2Pi  (divide by 2Pi and give the remainder).  this gives the approximate angle the force came from.
	magnitude = clamp((magnitude*40),0,1)
	newWave(originAngle, magnitude)
  }
  else {
    // continually set last values for sub-threshold readings, likely to be essentially the baseline
    lastX = curX
    lastY = curY
    lastZ = curZ
  }
}

//return the angle in radians, can be negative
function getAngleInRads(x, y) {
  //center the coordinate, then get the angle
  return atan2(x - .5, y - .5)
}

//return the angle as a value between 0 and 1.0
//most of Pixelblaze's animation language uses this range
//it also happens to rotate the angle so that 0 is north
function getUnitAngle(x, y) {
  return (PI + atan2(x - .5, y - .5))/PI2 
}

function newWave (HAngle, magnitude)
{
	// read sound magnitude, if over threshold, then read max frequency and use those to create
	// search through waves, find one with lowest magnitude, and replace that one
	var goodWave = 1
	var currLow = wave1[0]
	var stop=false
	var newHue = aura
	var newV = magnitude/biggestMagnitude
	
	var newSpeed = baseSpeed
	var magSpeedThreshold = 0.6  // if magnitude over 60% of expected max, then increase speed above base rate
	
	if (newV > magSpeedThreshold) {
		newSpeed = baseSpeed * (1+newV)  // may need to adjust this, especially if using radians for angles
	}
	
	if (wave1[0] = 0) {  // if the first wave is off, just start with that one, skipping next evaluations)
		goodWave = 1
		curLow = 0
		stop = true
	}
	else if (wave2[0]<wave1[0]) {
		goodWave=2
		curLow=wave2[0]
		if (wave2[0]=0) stop = true
	}
//	if ((stop=false) && (wave3[0]<curLow)) {  // if wave3 is the lowest
//		goodWave=3
//		curLow=wave3[0]
//		if (wave3[0]=0) stop = true
//	}
//	if ((stop=false) && (wave4[0]<curLow)) {  // if wave3 is the lowest
//		goodWave=4
//		curLow=wave4[0]
//		if (wave3[0]=0) stop = true
//	}
	
    if (energyAverage>soundThreshold) {
		if (lowestSound>maxFrequency) newHue = 1 // to prevent values over 1 if a freq higher than expected is detected
		else newHue = (maxFrequency-lowestSound)/(highestSound-lowestSound) // gives a 0-1 value proportional to the space between lowst and highest expected maxFrequency.

		newV = (newV + (energyAverage/loudestSound))/2   // loud music has an energy average of 0.06, so average magnitude (0-1) with the proportion of energyAverage with its' max
	}
	else newHue = aura
	
	if (goodWave = 1) {
		initWave(wave1, magnitude, newHue, newSpeed, HAngle)
		initWave(wave1b, magnitude, newHue, -1*newSpeed, HAngle)
	}
	else if (goodWave = 2) {
		initWave(wave2, magnitude, newHue, newSpeed, HAngle)
		initWave(wave2b, magnitude, newHue, -1*newSpeed, HAngle)
	}
//	else if (goodWave = 3) {
//		initWave(wave3, magnitude, newHue, newSpeed, HAngle)
//		initWave(wave3b, magnitude, newHue, -1*newSpeed, HAngle)
//	}
//	else if (goodWave = 4) {
//		initWave(wave4, magnitude, newHue, newSpeed, HAngle)
//		initWave(wave4b, magnitude, newHue, -1*newSpeed, HAngle)
//	}
sinceLastActivated = 0
waveCount +=1
}

function initWave(w, vMax, hue, speed, curAngle) {
  w[_vMax] = vMax;
  w[_hue] = hue;
  w[_speed] = speed;
  w[_curAngle] = curAngle;
}

function advanceWaves (timeChange) {
	// advance position of waves and degrade waves, if not already deactivated
	//speed is in radians/sec, delta and timeChange is in miliseconds
	
	if (wave1[_vMax] >0) {
		wave1[_curAngle] = wave1[_curAngle] + wave1[_speed]* (timeChange/1000)
		aWaveTracker = wave1[_curAngle]
		if (wave1[_curAngle] >PI2) wave1[_curAngle] -=PI2
		wave1b[_curAngle] = wave1b[_curAngle] + wave1b[_speed]* (timeChange/1000)
		if (wave1b[_curAngle] <0) wave1b[_curAngle] +=PI2
		a2WaveTracker=wave1b[_curAngle]
		
		wave1[_vMax] = wave1[_vMax] - degradationRate* (timeChange/1000)
		wave1b[_vMax] = wave1b[_vMax] - degradationRate* (timeChange/1000)
	}
	
	if (wave2[_vMax] >0) {
		wave2[_curAngle] = wave2[_curAngle] + wave2[_speed]* (timeChange/1000)
		wave2b[_curAngle] = wave2b[_curAngle] + wave2b[_speed]* (timeChange/1000)
		
		wave2[_vMax] = wave2[_vMax] - degradationRate* (timeChange/1000)
		wave2b[_vMax] = wave2b[_vMax] - degradationRate* (timeChange/1000)
	}

//	if (wave3[_vMax] >0) {
//		wave3[_curAngle] = wave3[_curAngle] + wave3[_speed]* (timeChange/1000)
//		wave3b[_curAngle] = wave3b[_curAngle] + wave3b[_speed]* (timeChange/1000)
//		
//		wave3[_vMax] = wave3[_vMax] - degradationRate* (timeChange/1000)
//		wave3b[_vMax] = wave3b[_vMax] - degradationRate* (timeChange/1000)
//	}
	
//	if (wave4[_vMax] >0) {
//		wave4[_curAngle] = wave4[_curAngle] + wave4[_speed]* (timeChange/1000)
//		wave4b[_curAngle] = wave4b[_curAngle] + wave4b[_speed]* (timeChange/1000)
//	
//		wave4[_vMax] = wave4[_vMax] - degradationRate* (timeChange/1000)
//		wave4b[_vMax] = wave4b[_vMax] - degradationRate* (timeChange/1000)
//	}

	
}


function setup() {
  // initialize all waves
  initWave(wave1, 0, aura, baseSpeed, 0);  //wave, vMax, hue, speed, currentAngle
  initWave(wave1b, 0, aura, baseSpeed, 0); 
  initWave(wave2, 0, aura, baseSpeed, 0);
  initWave(wave2b, 0, aura, baseSpeed, 0);
 // initWave(wave3, 0, aura, baseSpeed, 0);
 // initWave(wave3b, 0, aura, baseSpeed, 0);
 // initWave(wave4, 0, aura, baseSpeed, 0);
 // initWave(wave4b, 0, aura, baseSpeed, 0);  
}


setup();

function waveStrength(wAngle,cAngle) {
	var vStrength = 0
	var backDist = -1
	var frontDist = -1
	var dAngle = cAngle-wAngle
	var idAngle = wAngle-cAngle
	// could further simplify to only calculate PI2+dAngle once and PI2+idAngle once...
	
	if ((dAngle >=0) && (dAngle <= waveFront)) { // if cAngle is ahead of wAngle, and within waveFront distance
		inFront = true
		frontDist = dAngle
	}
	else if ((cAngle < waveFront)&&((PI2+dAngle) < waveFront)) { // if cAngle just past zero, but within wafeFront distance form wAngle.  simplified second clause form PI2-wAngle+cAngle
		inFront= true
		frontDist = (PI2+dAngle)
	}
	else if ((dAngle < 0)&&(abs(dAngle) <= waveBack)) {  // if cAngle is behind wAngle, but within waveBack distance
		inBack = true
		backDist = abs(dAngle)
	}
	else if ((wAngle < waveBack)&&((PI2+idAngle) < waveBack)) { // if wAngle just past zero, but within waveBack distance of cAngle.  simplified second clause from PI2-cAngle+wAngle
		inBack = true
		backDist = (PI2+idAngle)
	}
	
	if (inFront) vStrength = sin((1/waveFront)*PI*frontDist)  // for the front half, use a sine wave modified to be 1 at zero, and sero at waveFront distance from zero radians
	if (inBack) vStrength = sin((1/waveBack)*PI*backDist)  // similar, except scaled to waveBack
	return vStrength
}


//integrator function:

function integratinator (x, y) {
	var activeWaves =0
	var cumulativeHue =0
	var cumulativeV =0
	var tempAngle =0
	var tempStrength =0
	
	if (wave1[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave1[_vMax]*waveStrength(wave1[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave1[_hue]
			activeWaves=activeWaves+1
		}
	}
	
	if (wave1b[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave1b[_vMax]*waveStrength(wave1b[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave1b[_hue]
			activeWaves=activeWaves+1
		}
	}	
	
	if (wave2[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave2[_vMax]*waveStrength(wave2[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave2[_hue]
			activeWaves=activeWaves+1
		}
	}	
	
	if (wave2b[_vMax] > 0) {
		tempAngle = getAngleInRads(x,y)
		tempStrength = wave2b[_vMax]*waveStrength(wave2b[_curAngle],tempAngle)
		if (tempStrength>0) {
			cumulativeV=cumulativeV+tempStrength
			cumulativeHue = cumulativeHue + wave2b[_hue]
			activeWaves=activeWaves+1
		}
	}
	
//	if (wave3[_vMax] > 0) {
//		tempAngle = getAngleInRads(x,y)
//		tempStrength = wave3[_vMax]*waveStrength(wave3[_curAngle],tempAngle)
//		if (tempStrength>0) {
//			cumulativeV=cumulativeV+tempStrength
//			cumulativeHue = cumulativeHue + wave3[_hue]
//			activeWaves=activeWaves+1
//		}
//	}	
	
//	if (wave3b[_vMax] > 0) {
//		tempAngle = getAngleInRads(x,y)
//		tempStrength = wave3b[_vMax]*waveStrength(wave3b[_curAngle],tempAngle)
//		if (tempStrength>0) {
//			cumulativeV=cumulativeV+tempStrength
//			cumulativeHue = cumulativeHue + wave3b[_hue]
//			activeWaves=activeWaves+1
//		}
//	}
	
//	if (wave4[_vMax] > 0) {
//		tempAngle = getAngleInRads(x,y)
//		tempStrength = wave4[_vMax]*waveStrength(wave4[_curAngle],tempAngle)
//		if (tempStrength>0) {
//			cumulativeV=cumulativeV+tempStrength
//			cumulativeHue = cumulativeHue + wave4[_hue]
//			activeWaves=activeWaves+1
//		}
//	}	
	
//	if (wave4b[_vMax] > 0) {
//		tempAngle = getAngleInRads(x,y)
//		tempStrength = wave4b[_vMax]*waveStrength(wave4b[_curAngle],tempAngle)
//		if (tempStrength>0) {
//			cumulativeV=cumulativeV+tempStrength
//			cumulativeHue = cumulativeHue + wave4b[_hue]
//			activeWaves=activeWaves+1
//		}
//	}
	mHSV[0] = cumulativeHue/activeWaves //hue  - currently just a simple average of the hues.  Can consider other blends in the future.
	mHSV[1] = clamp((1-(activeWaves/10)),0,1)  // have saturation decrease with the more concurrent waves at a given point - essentially a white cap
	mHSV[2] = clamp((cumulativeV/activeWaves)+(activeWaves/8),0,1) // v - curently just a simple average with an additional 0.125 for every active wave.  Could consider quadratic easing or other methods.
}


export function beforeRender(delta) {
	
	if (sinceLastActivated<debounceTime) { // use sinceLastActivated as an up-counting timer until it passes debounceTime
		sinceLastActivated += delta
	}
	else if (sinceLastActivated<idleWaveInterval){
		readAccelerometer()
		sinceLastActivated += delta
	}
	else {
		newWave(0,0.5) // have a baseline wave pulsing should also reset sinceLastActivated
	}

	
	advanceWaves(delta)	
}

export function render(index) {
	render2D(index, .5, index / pixelCount)
}

var mHSV = array(3); // Allocating the array once. simplification, global var to simplify passing array for momentaryHSV calculations
export function render2D(index, x, y) {

	integratinator (x, y)
	hsv(mHSV[0],mHSV[1],mHSV[2])
}

export function render3D(index,x,y,z) {
	render2D(index,x,y)
}

Interesting. Now that I see what you’re doing (In my head, it was entirely different), I think it’s a bit overcomplicated for the effect it produces. [I’d summarize as: it makes paired matching arcs of color, that originate from the center, and move towards each other (and potentially thru each other, rebounding a few times).]

I pictured (based on some original discussion), that it was making more circular waves than it is.

Let me ponder on this, and see if I can come up with a way to generate the same, without all of that work. (I’m thinking it’s primarily fading radial lines, without the sound/motion element, and then just adjusting based on those)

Nothing wrong with your version, but this is a good case for golfing: now that I understand the end result, is there a better way to do this?

Hi @scruffynerf
Yes, you have summarized it correctly. It doesn’t surprise me that it is a complicated way of approaching it, and I’m curious what alternate approaches you’d have for the same problem.

This week is looking heavy for work again, but hopefully I’ll be able to chip away at it some more towards the weekend. I’m pretty sure I know where the error is that results in almost a quarter of the circle not being rendered (my wave front/back math was done at a time I was quite tired), and I’m optimistic that after that, it’ll be a matter of adjusting thresholds. once the PB is fully fixed to the surface.

1 Like