I took advantage of @jeff’s beat detection code to make an equalizer pattern that displays the various frequency buckets as radii from the center of the board. It should render as round-ish on non ring displays as well. Current primary WIP is figure out what do do during song fade-in and out when there’s still audio but beatDetected() hasn’t fired for a bit. I’m also going to do something basic for render(). Ideas welcome!
export var frequencyData;
export var energyAverage, maxFrequency, maxFrequencyMagnitude;
export var hArr = array(pixelCount + 1);
BUCKET_WIDTH = PI2 / frequencyData.length;
//rotating display
adjustedBuckets = array(frequencyData.length);
var outerRadius = 0.4;
var whiteRadius = 0.05
var decayRate = 250;
var decay = 0;
export function sliderOuterRadius(v) {
outerRadius = v / 2;
}
export function sliderWhiteRadius(v) {
whiteRadius = v / 2;
}
export function sliderDecayRate(v) {
decayRate = abs(1 - v) * 1000;
}
var msSinceUpdate = 0;
export function beforeRender(delta) {
processBass(delta);
if (msSinceUpdate >= decayRate) {
decay += 0.01
if (decay < 0) {
decay = 0;
}
msSinceUpdate = 0;
} else {
msSinceUpdate += delta;
}
}
export function render(index) {
hsv(index / pixelCount, 1, 0.33);
}
export function render2D(index, x, y) {
var angle = computeAngle(x, y);
var radius = hypot(x - 0.5, y - 0.5) + decay;
if (radius < whiteRadius) {
hsv(0, 0, 0.5);
return;
}
var bucketAngle = angle / BUCKET_WIDTH;
var lowerBucket = floor(bucketAngle);
var upperBucket = ceil(bucketAngle) % frequencyData.length;
var energy;
if (lowerBucket === upperBucket) {
energy = adjustedBuckets[lowerBucket] / 2;
} else {
if (upperBucket >= frequencyData.length || lowerBucket === frequencyData.length) {
upperBucket = 0;
lowerBucket = frequencyData.length - 1;
}
var lowerWeight = (angle - lowerBucket * BUCKET_WIDTH) / BUCKET_WIDTH;
var upperWeight = 1.0 - lowerWeight;
energy = adjustedBuckets[lowerBucket] * lowerWeight + adjustedBuckets[upperBucket] * upperWeight;
}
if (radius > energy) {
hsv(0, 0, 0)
return
}
hsv((radius - whiteRadius) / energy * 0.9, 1, 1)
}
export function render3D(index, x, y, z) {
render2D(index, x, y);
}
function computeAngle(x, y) {
var originX = x - 0.5;
var originY = y - 0.5;
if (!originX && !originY) {
return 0;
}
var angle = atan2(originY, originX);
if (angle < 0) {
angle += PI2
}
return angle;
}
function beatDetected() {
var freqCeil = maxFrequencyMagnitude || 0.001;
decay = 0;
for (var bucket = 0; bucket < frequencyData.length; bucket++) {
adjustedBuckets[bucket] = sqrt(sqrt(frequencyData[bucket] / freqCeil));
}
}
// ****************************************************************************
// * SOUND, BEAT AND TEMPO DETECTION by https://forum.electromage.com/u/jeff *
// ****************************************************************************
var bass, maxBass, bassOn // Bass and beats
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 = 5 // 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
var bassDebounceTimer = 0
function processBass(delta) {
// 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
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
if (bassOn && bassDebounceTimer <= 0) {
beatDetected();
bassDebounceTimer = 100 // ms
} else {
bassDebounceTimer = max(-3e4, bassDebounceTimer - delta)
}
lastBassFastEMA = bassFastEMA
}