https://giav2030.github.io/giav2030.github.io_mxVisualizer/
I wanted to try the creation of fractals to make a visualizer that could send midi data to Ableton live and control some aspect of the music. Even though this could be basically a trial, I made it work.


For this project I sent MIDI data from my sketch to tie the animation with the remote movement as much as possible. I was able to attach this MIDI CC through channel #1 and control the filter cutoff of the bass instrument. Because of the bpm and the sync achieved with the animation, I composed the kick of the drums to be also attached to the circle expanding and retracting every beat. Internally in Ableton I also found the way to use the side chaining technique that allowed other instruments like the saw lead and the dreamy guitar to follow the attack of the kick.
let circlePoints = [];
let cx, cy;
let maxDepth = 11;
let frames = []; // pre-rendered buffer for each depth level
let midiOutput = null;
let lastDepth = -1;
let kickScale = 1.0; // current scale (1.0 = normal)
let clockStarted = false;
let bpm = 110;
let tickInterval = 60000 / bpm / 24; // ms between clock ticks (24 PPQ)
let clockTimer = null;
function setup() {
createCanvas(640,480);
setupMIDI();
let r = 100; // Radius
cx = width/2; // Center X
cy = height/2; // Center Y
// Calculate 360 points (one per degree)
for (let a = 0; a < 360; a++) {
let angle = radians(a); // Convert degrees to radians
let x = cx + r * cos(angle);
let y = cy + r * sin(angle);
circlePoints.push(createVector(x, y));
}
// Pre-render a buffer for each depth level (0 to maxDepth)
for (let d = 0; d <= maxDepth; d++) {
let pg = createGraphics(width, height);
pg.background(255);
// Draw the filled circle
pg.beginShape();
pg.fill(0);
pg.noStroke();
for (let v of circlePoints) {
pg.vertex(v.x, v.y);
}
pg.endShape(CLOSE);
// Draw branches at this depth
randomSeed(42);
for (let i = 0; i < circlePoints.length; i += 15) {
let v = circlePoints[i];
let angle = atan2(v.y - cy, v.x - cx);
branch(pg, v.x, v.y, angle, 20, d);
}
frames.push(pg);
}
}
function draw() {
// ping-pong synced to BPM — one beat per grow/shrink half
let framesPerBeat = round(60 * 60 / bpm); // 60fps * 60s / bpm
let cycleLength = framesPerBeat * 2; // full grow + shrink = 2 beats
let half = framesPerBeat;
let cycle = frameCount % cycleLength;
let currentDepth;
if (cycle < half) {
currentDepth = floor(map(cycle, 0, half, 0, maxDepth));
} else {
currentDepth = floor(map(cycle, half, cycleLength, maxDepth, 0));
}
// Trigger kick when depth hits min or max
if (currentDepth !== lastDepth) {
if (currentDepth === 0 || currentDepth === maxDepth) {
kickScale = 1.15; // 15% bigger on hit
}
sendMIDI(currentDepth);
lastDepth = currentDepth;
}
// Decay kick back to normal
kickScale = lerp(kickScale, 1.0, 0.15);
// Draw scaled from center
let w = width * kickScale;
let h = height * kickScale;
imageMode(CENTER);
image(frames[currentDepth], width / 2, height / 2, w, h);
imageMode(CORNER);
}
function branch(pg, x, y, angle, len, depth) {
if (depth === 0 || len < 2) return;
let endX = x + cos(angle) * len;
let endY = y + sin(angle) * len;
pg.stroke(0);
pg.strokeWeight(depth * 0.5);
pg.line(x, y, endX, endY);
let leftAngle = random(PI/8, PI/4);
let rightAngle = random(PI/8, PI/4);
let leftShrink = random(0.95, 1.0);
let rightShrink = random(0.95, 1.0);
branch(pg, endX, endY, angle - leftAngle, len * leftShrink, depth - 1);
branch(pg, endX, endY, angle + rightAngle, len * rightShrink, depth - 1);
}
function setupMIDI() {
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess({ sysex: false }).then(function(access) {
let outputs = Array.from(access.outputs.values());
if (outputs.length > 0) {
midiOutput = outputs[0]; // first available output (IAC Driver)
console.log("MIDI connected:", midiOutput.name);
startMIDIClock();
} else {
console.log("No MIDI outputs found. Enable IAC Driver in Audio MIDI Setup.");
}
});
}
}
function startMIDIClock() {
if (!midiOutput || clockStarted) return;
clockStarted = true;
midiOutput.send([0xFA]); // MIDI Start — Ableton transport begins
// Schedule clock ticks ahead of time in batches for jitter-free timing
let nextTickTime = performance.now();
// Dedicated timer running at ~2ms — much tighter than draw()'s 16ms
clockTimer = setInterval(function() {
let now = performance.now();
// Schedule ticks up to 50ms into the future for smooth delivery
while (nextTickTime <= now + 50) {
midiOutput.send([0xF8], nextTickTime);
nextTickTime += tickInterval;
}
}, 2);
}
function sendMIDI(depth) {
if (!midiOutput) return;
let ccValue = floor(map(depth, 0, maxDepth, 0, 127));
midiOutput.send([0xB0, 1, ccValue]);
}
It was a nice fun experiment that with further exploration could become a way more complex communication between the visual p5 sketch and the instruments within Ableton to create reactive animations live in the web. Could be interesting to explore this further with sounding music from the web when using API to play a song on my portfolio for example.