Created
May 29, 2026 14:02
-
-
Save deeplook/94ff1ed8bd7dd5c21ae7db3015899698 to your computer and use it in GitHub Desktop.
Audio Visualizer Shader (raw WebGL/GLSL)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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