Skip to content

Instantly share code, notes, and snippets.

@deeplook
Created May 29, 2026 14:02
Show Gist options
  • Select an option

  • Save deeplook/94ff1ed8bd7dd5c21ae7db3015899698 to your computer and use it in GitHub Desktop.

Select an option

Save deeplook/94ff1ed8bd7dd5c21ae7db3015899698 to your computer and use it in GitHub Desktop.
Audio Visualizer Shader (raw WebGL/GLSL)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Visualizer Shader</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
#btn {
position: fixed; top: 20px; left: 20px;
padding: 10px 18px; z-index: 10;
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.2);
color: #fff; font-family: 'SF Mono', monospace; font-size: 11px;
letter-spacing: 0.1em; text-transform: uppercase;
cursor: pointer; border-radius: 4px;
}
#btn:hover { background: rgba(255,255,255,0.15); }
</style>
</head>
<body>
<button id="btn">Start Microphone</button>
<canvas id="canvas"></canvas>
<script>
(function() {
var canvas = document.getElementById('canvas');
var btn = document.getElementById('btn');
var gl = canvas.getContext('webgl', { alpha: false, antialias: false });
if (!gl) { btn.textContent = 'WebGL not supported'; return; }
var vertSrc = 'attribute vec2 a_pos; void main(){gl_Position=vec4(a_pos,0,1);}';
var fragSrc = [
'precision highp float;',
'uniform float u_time;',
'uniform vec2 u_res;',
'uniform sampler2D u_audio;', // 256×1 texture, freq bins 0-1
'uniform float u_level;', // average volume 0-1
'uniform float u_active;', // 1 = mic on, 0 = idle
'',
'#define PI 3.14159265359',
'#define BINS 256.0',
'',
'float hash(vec2 p){',
' p=fract(p*vec2(127.1,311.7));',
' p+=dot(p,p+45.32);',
' return fract(p.x*p.y);',
'}',
'',
// Sample frequency at bin fraction [0,1]
'float freq(float x){',
' return texture2D(u_audio, vec2(x, 0.5)).r;',
'}',
'',
'// ── Vertical bars ───────────────────────────',
'vec3 bars(vec2 uv){',
' float bin = pow(uv.x, 1.8);', // log-ish scale: more space for bass
' float level = freq(bin);',
'',
' // Colour: bass=red/orange, mids=green, highs=cyan/blue',
' vec3 barCol = mix(',
' mix(vec3(1.0,0.25,0.0), vec3(0.1,1.0,0.3), smoothstep(0.0,0.45,bin)),',
' vec3(0.0,0.6,1.0), smoothstep(0.45,1.0,bin)',
' );',
'',
' float bar = step(uv.y, level);',
'',
' // Soft glow above bar top',
' float glow = exp(-max(uv.y-level,0.0)*18.0)*level;',
' vec3 col = barCol*(bar*0.9 + glow*0.6);',
'',
' // Bright pixel at bar top',
' float tip = smoothstep(0.012,0.0,abs(uv.y-level))*level;',
' col += vec3(1.0)*tip*0.8;',
'',
' return col;',
'}',
'',
'// ── Circular waveform ───────────────────────',
'vec3 circle(vec2 uv, float t){',
' vec2 p = uv - 0.5;',
' float angle = atan(p.y, p.x); // -PI..PI',
' float bin = (angle + PI) / (2.0*PI); // 0..1',
' float r = length(p);',
'',
' float level = freq(bin);',
' float radius = 0.18 + level * 0.22;',
'',
' // Ring outline',
' float ring = exp(-abs(r - radius)*80.0)*level;',
'',
' // Colour rotates with angle and time',
' vec3 col = 0.5+0.5*cos(vec3(0,2.1,4.2) + bin*6.28 + t*0.5);',
' return col * ring * 1.5;',
'}',
'',
'// ── Background pulse ────────────────────────',
'vec3 bgPulse(vec2 uv, float t){',
' float bass = freq(0.04);', // low bin ≈ bass energy
' float treble = freq(0.85);',
' float d = length(uv - 0.5);',
' float pulse = bass * exp(-d * 4.0) * 0.5;',
' vec3 col = mix(vec3(0.05,0.0,0.12), vec3(0.0,0.08,0.20), treble);',
' return col + vec3(0.3,0.05,0.0)*pulse;',
'}',
'',
'void main(){',
' vec2 uv = gl_FragCoord.xy / u_res;',
' float t = u_time;',
'',
' // Split screen: bottom 45% = bars, top = circular + bg',
' float split = 0.45;',
'',
' vec3 col;',
' if (uv.y < split) {',
' // Bars fill the bottom strip (uv.y remapped to 0..1)',
' vec2 barUV = vec2(uv.x, uv.y / split);',
' col = bgPulse(uv, t) * 0.4;',
' col += bars(barUV);',
'',
' // Mirror reflection: bars are flipped vertically below split',
' vec2 refUV = vec2(uv.x, (split - uv.y) / split);',
' col += bars(refUV) * 0.18 * (uv.y / split);',
' } else {',
' col = bgPulse(uv, t);',
' col += circle(uv, t);',
' // Stars',
' float s = hash(floor(uv * vec2(u_res.x/u_res.y, 1.0) * 90.0));',
' col += vec3(step(0.97, s) * 0.5);',
' }',
'',
' // Idle animation when mic not active',
' if (u_active < 0.5) {',
' float idle = sin(uv.x*12.0 + t*1.5)*0.5+0.5;',
' idle *= sin(t*0.7)*0.5+0.5;',
' col = mix(col, vec3(0.0,0.15,0.12)*idle, 0.6);',
' }',
'',
' // Tone map + gamma',
' col = col / (col + 0.4) * 1.4;',
' col = pow(max(col, 0.0), vec3(0.4545));',
'',
' // Vignette',
' vec2 vUV = uv - 0.5;',
' col *= 1.0 - dot(vUV, vUV) * 0.7;',
'',
' gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);',
'}',
].join('\n');
function compile(type, src) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error('Shader error:', gl.getShaderInfoLog(s));
return s;
}
var prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS))
console.error('Link error:', gl.getProgramInfoLog(prog));
gl.useProgram(prog);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
var aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
var uTime = gl.getUniformLocation(prog, 'u_time');
var uRes = gl.getUniformLocation(prog, 'u_res');
var uAudio = gl.getUniformLocation(prog, 'u_audio');
var uLevel = gl.getUniformLocation(prog, 'u_level');
var uActive = gl.getUniformLocation(prog, 'u_active');
// 256×1 LUMINANCE texture to hold frequency data
var BIN_COUNT = 256;
var audioTex = gl.createTexture();
var audioData = new Uint8Array(BIN_COUNT);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, audioTex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, BIN_COUNT, 1, 0,
gl.LUMINANCE, gl.UNSIGNED_BYTE, audioData);
gl.uniform1i(uAudio, 0);
var level = 0.0;
var active = 0.0;
var analyser = null;
var analyserBuf = null;
function getAverageVolume(array) {
var sum = 0;
for (var i = 0; i < array.length; i++) sum += array[i];
return sum / array.length / 255;
}
btn.addEventListener('click', function startAudio() {
btn.textContent = 'Connecting…';
btn.disabled = true;
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(function(stream) {
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source = audioCtx.createMediaStreamSource(stream);
analyser = audioCtx.createAnalyser();
analyser.fftSize = BIN_COUNT * 2; // frequencyBinCount = BIN_COUNT
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
analyserBuf = new Uint8Array(analyser.frequencyBinCount);
active = 1.0;
btn.style.display = 'none';
})
.catch(function(err) {
btn.textContent = 'Mic denied: ' + err.message;
btn.disabled = false;
});
});
var running = true;
var needsResize = true;
var dpr = Math.min(window.devicePixelRatio || 1, 2);
function resize() {
needsResize = false;
var w = Math.round(canvas.clientWidth * dpr);
var h = Math.round(canvas.clientHeight * dpr);
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w; canvas.height = h;
gl.viewport(0, 0, w, h);
}
}
function render(now) {
if (!running) return;
if (needsResize) resize();
// Upload latest frequency data
if (analyser) {
analyser.getByteFrequencyData(analyserBuf);
audioData.set(analyserBuf);
level = getAverageVolume(analyserBuf);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, audioTex);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, BIN_COUNT, 1,
gl.LUMINANCE, gl.UNSIGNED_BYTE, audioData);
}
gl.uniform1f(uTime, now * 0.001);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uLevel, level);
gl.uniform1f(uActive, active);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render);
}
window.addEventListener('resize', function() { needsResize = true; });
document.addEventListener('visibilitychange', function() {
if (document.hidden) { running = false; }
else { running = true; requestAnimationFrame(render); }
});
resize();
requestAnimationFrame(render);
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment