Skip to content

Instantly share code, notes, and snippets.

@hkolbeck
Last active June 30, 2023 06:16
Show Gist options
  • Save hkolbeck/9d9332f1a8f86ac8f1b5637d2b48a3d0 to your computer and use it in GitHub Desktop.
Save hkolbeck/9d9332f1a8f86ac8f1b5637d2b48a3d0 to your computer and use it in GitHub Desktop.
/*
Circular Equalizer
Copyright (C) 2023 Hannah Kolbeck
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export var frequencyData;
export var energyAverage, maxFrequency;
var hArr = array(pixelCount + 1);
BUCKET_WIDTH = PI2 / frequencyData.length;
export var rotationRate = 0.0
export var rotation = 0.0
export var 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;
}
export function sliderRotation(v) {
rotationRate = PI * v / 100
}
export var maxFrequencyMagnitude = -1
soundLevelVal = 0
pic = makePIController(.05, .35, 30, 0, 400)
soundLevelPowFactor = 1.2
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]
}
var msSinceUpdate = 0;
export function beforeRender(delta) {
processBass(delta);
rotation = (rotation + rotationRate) % PI2
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];
} 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 + rotation) % PI2 ;
}
function beatDetected() {
decay = 0;
var sensitivity = calcPIController(pic, .5 - soundLevelVal)
soundLevelVal = pow(maxFrequencyMagnitude * sensitivity,
soundLevelPowFactor)
for (var bucket = 0; bucket < frequencyData.length; bucket++) {
adjustedBuckets[bucket] = sqrt(sqrt(frequencyData[bucket] / soundLevelVal));
}
var maxBucket = -1
var maxAdjusted = -1
for (var bucket = 0; bucket < adjustedBuckets.length; bucket++) {
if (adjustedBuckets[bucket] > maxAdjusted) {
maxBucket = bucket
maxAdjusted = adjustedBuckets[bucket]
}
}
adjustedBuckets[maxBucket] *= 0.6
}
// ****************************************************************************
// * 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 = 2 // 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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment