
2000s Visualization Expert
- 124 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Build charts, dashboards, and graphics with early-2000s web aesthetics for retro demos, nostalgia campaigns, or distinctive data storytelling.
About
Specializes in designing and implementing data visualizations and interfaces that evoke early-2000s web culture—beveled UI, neon gradients, skeuomorphic cues—for engaging retro-styled analytics, dashboards, and presentation experiences.
- Retro color palettes
- Glossy chart styling
- Period typography
- Dashboard mockups
- Nostalgic UX motifs
2000s Visualization Expert by the numbers
- 124 all-time installs (skills.sh)
- Ranked #1,059 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill 2000s-visualization-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 124 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Build charts, dashboards, and graphics with early-2000s web aesthetics for retro demos, nostalgia campaigns, or distinctive data storytelling.
Files
2000s Music Visualization Expert
Expert in recreating the legendary 2000s music visualization era - Milkdrop, AVS, Geiss - using modern WebGL and Web Audio APIs.
When to Use
✅ Use for:
- Implementing Milkdrop-style psychedelic visualizations
- Butterchurn library integration (WebGL Milkdrop)
- Web Audio API AnalyserNode FFT/waveform extraction
- GLSL fragment shaders for audio-reactive effects
- Full-screen immersive music experiences
- Real-time beat detection and audio analysis
- Preset systems and visualization transitions
❌ NOT for:
- Simple spectrum bar charts (use Canvas 2D)
- Static audio waveform displays
- Video editing or processing
- Non-audio generative art
- Audio playback/streaming issues (use audio-engineer skills)
The Golden Era (Summary)
| Era | Key Software | Innovation |
|---|---|---|
| 1998-2000 | Geiss | Simple plasma effects, DirectX |
| 2001-2007 | Milkdrop 1 & 2 | Per-pixel equations, preset system |
| 2007-2015 | Decline | Streaming services rise |
| 2018-Present | Butterchurn | WebGL renaissance |
Milkdrop's magic: Layering simple effects - blur, zoom, rotation, color shift - with audio-reactive parameters.
→ See references/butterchurn-guide.md for full history and integration.
Core Technologies
Butterchurn (WebGL Milkdrop)
- 1.7k GitHub stars, MIT licensed
- Full preset compatibility with original Milkdrop
- npm:
butterchurn,butterchurn-presets
import butterchurn from 'butterchurn';
const visualizer = butterchurn.createVisualizer(audioContext, canvas, {
width: window.innerWidth,
height: window.innerHeight,
pixelRatio: window.devicePixelRatio || 1,
});
visualizer.connectAudio(audioNode);
visualizer.loadPreset(preset, 2.0); // 2s blendWeb Audio API FFT
const analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);Critical: FFT bins are linear but hearing is logarithmic! Use logarithmic mapping.
→ See references/web-audio-fft.md for frequency band extraction.
GLSL Shaders
Pass audio data as 1D texture, use uniforms for bass/mid/treble:
uniform float u_bass;
float glow = smoothstep(0.5 - u_bass * 0.3, 0.0, dist);→ See references/glsl-shaders.md for complete patterns.
Anti-Patterns to Avoid
1. Ignoring AudioContext State
What it looks like: Visualization silently fails Why it's wrong: AudioContext starts suspended, needs user interaction Fix: Resume on click: await audioContext.resume()
2. Linear Frequency Display
What it looks like: Bass dominates, treble invisible Why it's wrong: FFT bins are linear; first 100 bins might be 0-2kHz Fix: Use logarithmic bin mapping (code in references)
3. No Smoothing
What it looks like: Jittery, seizure-inducing visuals Why it's wrong: Raw FFT data is noisy frame-to-frame Fix: analyserNode.smoothingTimeConstant = 0.7
4. requestAnimationFrame Without Cleanup
What it looks like: Memory leaks, multiple render loops Fix: Store animation ID, call cancelAnimationFrame on unmount
5. Hardcoded Canvas Size
What it looks like: Blurry on retina, wrong aspect ratio Fix: Multiply by devicePixelRatio, handle resize events
6. Blocking Main Thread
What it looks like: Choppy audio, dropped frames Why it's wrong: Heavy shader compilation on UI thread Fix: Compile shaders during loading, not during playback
Preset Recommendations
Psychedelic/Trippy:
Flexi, martin + geiss - dedicated to the sherwin maxawowRovastar - Fractopia
Smooth/Chill:
Flexi - predator-prey-spiralsGeiss - Cosmic Strings 2
High Energy:
Flexi + Martin - disconnectedshifter - tumbling cubes
Integration Checklist
- [ ] AudioContext created and resumed on user interaction
- [ ] AnalyserNode connected to audio source
- [ ] Canvas sized correctly (account for devicePixelRatio)
- [ ] Render loop with requestAnimationFrame
- [ ] Cleanup on unmount (cancelAnimationFrame)
- [ ] Preset loading with blend time
- [ ] Resize handling
- [ ] Full-screen support with ESC to exit
- [ ] Track info overlay (z-index above canvas)
- [ ] Cursor hiding after inactivity
Performance Tips
1. Lower texture ratio for older GPUs: textureRatio: 0.5 2. Reduce fftSize if not needed: 512 or 1024 vs 2048 3. Use `will-change: transform` on canvas 4. Avoid DOM updates during render loop 5. Profile with Chrome DevTools GPU timeline
References
→ references/butterchurn-guide.md - Complete Butterchurn integration → references/web-audio-fft.md - FFT extraction and frequency analysis → references/glsl-shaders.md - Audio-reactive shader patterns
---
This skill covers: Butterchurn/Milkdrop | Web Audio FFT | GLSL shaders | Full-screen visualization | Audio-reactive art
Changelog
All notable changes to the 2000s-visualization-expert skill.
[2.0.0] - 2024-12-14
Changed
- BREAKING: Restructured skill following progressive disclosure pattern
- Reduced SKILL.md from 360 lines to ~165 lines for faster loading
- Extracted detailed content to reference files
Added
references/butterchurn-guide.md- Complete Butterchurn integration guidereferences/web-audio-fft.md- FFT extraction and frequency analysisreferences/glsl-shaders.md- Audio-reactive shader patterns
Improved
- Clearer activation triggers and boundary definitions
- Expanded anti-patterns section (4 → 6 items)
- Better historical context presentation
- Streamlined integration checklist
[1.0.0] - 2024-11-26
Added
- Initial skill creation
- Milkdrop/AVS/Geiss historical context
- Butterchurn integration examples
- Web Audio API FFT documentation
- GLSL shader examples for audio visualization
- Full-screen visualization best practices
Butterchurn Integration Guide
Complete guide to implementing Milkdrop-style visualizations with Butterchurn.
---
Basic Setup
import butterchurn from 'butterchurn';
import butterchurnPresets from 'butterchurn-presets';
// Setup
const visualizer = butterchurn.createVisualizer(audioContext, canvas, {
width: window.innerWidth,
height: window.innerHeight,
pixelRatio: window.devicePixelRatio || 1,
textureRatio: 1, // Lower for performance
});
// Connect audio source
visualizer.connectAudio(audioNode); // Can be MediaElementSource, Oscillator, etc.
// Load preset
const presets = butterchurnPresets.getPresets();
const presetKeys = Object.keys(presets);
visualizer.loadPreset(presets[presetKeys[0]], 2.0); // 2 second blend
// Render loop
function render() {
visualizer.render();
requestAnimationFrame(render);
}
render();---
Preset Recommendations
Psychedelic/Trippy
Flexi, martin + geiss - dedicated to the sherwin maxawowRovastar - FractopiaUnchained - Unified DragZylot - Psychedelic Flowermartin - acid warp
Smooth/Chill
Flexi - predator-prey-spiralsGeiss - Cosmic Strings 2Martin - liquid scienceRovastar - Harlequin's Fruit Saladshifter - liquid glass
High Energy
Flexi + Martin - disconnectedshifter - tumbling cubesZylot - Clouds (Tunnel Mix)martin - fire stormUnchained - Demon's Gate
Minimal/Clean
Geiss - Explosion 3Martin - Acid Warp SimpleRovastar - Twilight
---
Preset Blending
class PresetManager {
private visualizer: any;
private presets: Record<string, any>;
private presetKeys: string[];
private currentIndex: number = 0;
private blendTime: number = 2.0;
constructor(visualizer: any) {
this.visualizer = visualizer;
this.presets = butterchurnPresets.getPresets();
this.presetKeys = Object.keys(this.presets);
}
next() {
this.currentIndex = (this.currentIndex + 1) % this.presetKeys.length;
this.load(this.presetKeys[this.currentIndex]);
}
previous() {
this.currentIndex = (this.currentIndex - 1 + this.presetKeys.length) % this.presetKeys.length;
this.load(this.presetKeys[this.currentIndex]);
}
random() {
const randomIndex = Math.floor(Math.random() * this.presetKeys.length);
this.currentIndex = randomIndex;
this.load(this.presetKeys[randomIndex]);
}
load(presetName: string, blendTime?: number) {
const preset = this.presets[presetName];
if (preset) {
this.visualizer.loadPreset(preset, blendTime ?? this.blendTime);
}
}
// Auto-advance every N seconds
startAutoAdvance(intervalSeconds: number = 30) {
return setInterval(() => this.random(), intervalSeconds * 1000);
}
}---
Full-Screen Setup
function setupFullscreen(canvas: HTMLCanvasElement, visualizer: any) {
// Handle resize
function handleResize() {
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
visualizer.setRendererSize(width, height);
}
window.addEventListener('resize', handleResize);
handleResize();
// Full-screen toggle
async function toggleFullscreen() {
if (!document.fullscreenElement) {
await canvas.requestFullscreen();
} else {
await document.exitFullscreen();
}
}
canvas.addEventListener('dblclick', toggleFullscreen);
// Hide cursor after inactivity
let cursorTimeout: number;
document.addEventListener('mousemove', () => {
document.body.style.cursor = 'default';
clearTimeout(cursorTimeout);
cursorTimeout = window.setTimeout(() => {
document.body.style.cursor = 'none';
}, 3000);
});
return { handleResize, toggleFullscreen };
}---
Performance Optimization
Lower texture ratio for older GPUs
const visualizer = butterchurn.createVisualizer(audioContext, canvas, {
width: window.innerWidth,
height: window.innerHeight,
textureRatio: 0.5, // Half resolution for textures
});Reduce fftSize if not needed
analyserNode.fftSize = 512; // 256, 512, 1024, 2048 (default)CSS Performance Hints
canvas.visualizer {
will-change: transform;
contain: strict;
}Profile with Chrome DevTools
1. Open DevTools → Performance tab 2. Enable GPU timeline 3. Record during visualization 4. Look for dropped frames, GPU memory spikes
---
Cleanup Pattern
class VisualizerController {
private animationId: number | null = null;
private visualizer: any;
private autoAdvanceInterval: number | null = null;
start() {
const render = () => {
this.visualizer.render();
this.animationId = requestAnimationFrame(render);
};
render();
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
if (this.autoAdvanceInterval) {
clearInterval(this.autoAdvanceInterval);
this.autoAdvanceInterval = null;
}
}
destroy() {
this.stop();
// Additional cleanup if needed
}
}---
React Hook
import { useEffect, useRef, useCallback } from 'react';
import butterchurn from 'butterchurn';
import butterchurnPresets from 'butterchurn-presets';
export function useButterchurn(audioContext: AudioContext | null, audioNode: AudioNode | null) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const visualizerRef = useRef<any>(null);
const animationRef = useRef<number>();
useEffect(() => {
if (!canvasRef.current || !audioContext || !audioNode) return;
const canvas = canvasRef.current;
const visualizer = butterchurn.createVisualizer(audioContext, canvas, {
width: canvas.width,
height: canvas.height,
pixelRatio: window.devicePixelRatio || 1,
});
visualizer.connectAudio(audioNode);
visualizerRef.current = visualizer;
// Load initial preset
const presets = butterchurnPresets.getPresets();
const keys = Object.keys(presets);
visualizer.loadPreset(presets[keys[0]], 0);
// Render loop
const render = () => {
visualizer.render();
animationRef.current = requestAnimationFrame(render);
};
render();
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [audioContext, audioNode]);
const loadPreset = useCallback((presetName: string, blendTime = 2.0) => {
const presets = butterchurnPresets.getPresets();
if (visualizerRef.current && presets[presetName]) {
visualizerRef.current.loadPreset(presets[presetName], blendTime);
}
}, []);
return { canvasRef, loadPreset };
}GLSL Shaders for Audio Visualization
Custom shader implementations for audio-reactive effects.
---
Basic Audio-Reactive Fragment Shader
precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform sampler2D u_audioData; // FFT as 1D texture
uniform float u_bass;
uniform float u_mid;
uniform float u_treble;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 center = uv - 0.5;
// Audio-reactive radius
float dist = length(center);
float audioSample = texture2D(u_audioData, vec2(dist, 0.0)).r;
// Psychedelic color cycling
float hue = u_time * 0.1 + audioSample * 0.5;
vec3 color = 0.5 + 0.5 * cos(6.28 * (hue + vec3(0.0, 0.33, 0.67)));
// Pulsing glow based on bass
float glow = smoothstep(0.5 - u_bass * 0.3, 0.0, dist);
gl_FragColor = vec4(color * glow, 1.0);
}---
Tunnel Effect
precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_bass;
uniform float u_speed;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 center = uv - 0.5;
// Convert to polar coordinates
float angle = atan(center.y, center.x);
float radius = length(center);
// Tunnel distortion
float tunnel = 0.1 / radius;
// Scrolling texture coordinates
float u = angle / 3.14159;
float v = tunnel + u_time * u_speed + u_bass * 0.5;
// Create stripe pattern
float stripes = sin(u * 10.0) * sin(v * 20.0);
stripes = smoothstep(0.0, 0.1, stripes);
// Color based on depth
vec3 color = vec3(0.2, 0.5, 1.0) * stripes;
color *= 1.0 - radius * 1.5; // Fade at edges
gl_FragColor = vec4(color, 1.0);
}---
Plasma Effect
precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_bass;
uniform float u_mid;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
// Multiple sine waves
float v1 = sin(uv.x * 10.0 + u_time);
float v2 = sin(10.0 * (uv.x * sin(u_time / 2.0) + uv.y * cos(u_time / 3.0)) + u_time);
float cx = uv.x + 0.5 * sin(u_time / 5.0);
float cy = uv.y + 0.5 * cos(u_time / 3.0);
float v3 = sin(sqrt(100.0 * (cx * cx + cy * cy) + 1.0) + u_time);
float v = v1 + v2 + v3;
// Audio modulation
v *= 1.0 + u_bass * 0.5;
// Color palette
vec3 color;
color.r = sin(v * 3.14159 + u_mid);
color.g = sin(v * 3.14159 + 2.094 + u_mid);
color.b = sin(v * 3.14159 + 4.188 + u_mid);
color = color * 0.5 + 0.5;
gl_FragColor = vec4(color, 1.0);
}---
Kaleidoscope Effect
precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform sampler2D u_audioData;
uniform float u_bass;
#define PI 3.14159265359
#define SEGMENTS 8.0
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 center = uv - 0.5;
// Convert to polar
float angle = atan(center.y, center.x);
float radius = length(center);
// Kaleidoscope fold
float segment = PI * 2.0 / SEGMENTS;
angle = mod(angle, segment);
if (mod(floor(atan(center.y, center.x) / segment), 2.0) == 1.0) {
angle = segment - angle;
}
// Convert back to cartesian
vec2 kaleido = vec2(cos(angle), sin(angle)) * radius;
// Sample audio at radius
float audio = texture2D(u_audioData, vec2(radius * 2.0, 0.0)).r;
// Create pattern
float pattern = sin(kaleido.x * 20.0 + u_time * 2.0);
pattern *= sin(kaleido.y * 20.0 + u_time);
pattern = pattern * 0.5 + 0.5;
// Color
vec3 color = vec3(pattern) * (audio + 0.2);
color *= 0.5 + 0.5 * cos(6.28 * (u_time * 0.1 + vec3(0.0, 0.33, 0.67)));
// Vignette
color *= 1.0 - radius;
gl_FragColor = vec4(color, 1.0);
}---
Spectrum Bars (Classic)
precision mediump float;
uniform vec2 u_resolution;
uniform sampler2D u_audioData;
uniform float u_barCount;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
// Which bar are we in?
float barIndex = floor(uv.x * u_barCount);
float barCenter = (barIndex + 0.5) / u_barCount;
// Sample audio at bar position
float audio = texture2D(u_audioData, vec2(barCenter, 0.0)).r;
// Bar shape
float barWidth = 0.8 / u_barCount;
float inBar = step(abs(uv.x - barCenter), barWidth * 0.5);
// Height
float barHeight = audio;
float inHeight = step(uv.y, barHeight);
// Color gradient (blue to red based on height)
vec3 color = mix(vec3(0.2, 0.5, 1.0), vec3(1.0, 0.2, 0.5), uv.y);
// Combine
float alpha = inBar * inHeight;
gl_FragColor = vec4(color * alpha, alpha);
}---
Waveform Display
precision mediump float;
uniform vec2 u_resolution;
uniform sampler2D u_waveformData;
uniform float u_lineWidth;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
// Sample waveform
float waveform = texture2D(u_waveformData, vec2(uv.x, 0.0)).r;
waveform = waveform * 2.0 - 1.0; // Convert from 0-1 to -1 to 1
// Center the waveform
float y = waveform * 0.4 + 0.5;
// Distance from waveform line
float dist = abs(uv.y - y);
// Line with glow
float line = smoothstep(u_lineWidth, 0.0, dist);
float glow = smoothstep(u_lineWidth * 10.0, 0.0, dist) * 0.5;
vec3 color = vec3(0.3, 1.0, 0.5) * (line + glow);
gl_FragColor = vec4(color, 1.0);
}---
WebGL Shader Setup (TypeScript)
function createShaderProgram(
gl: WebGLRenderingContext,
vertexSource: string,
fragmentSource: string
): WebGLProgram {
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram()!;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error('Shader program failed to link');
}
return program;
}
function compileShader(
gl: WebGLRenderingContext,
type: number,
source: string
): WebGLShader {
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
throw new Error(`Shader compilation error: ${info}`);
}
return shader;
}
// Basic vertex shader (fullscreen quad)
const VERTEX_SHADER = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Create fullscreen quad
function createFullscreenQuad(gl: WebGLRenderingContext): WebGLBuffer {
const buffer = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW
);
return buffer;
}Web Audio API FFT Reference
Comprehensive guide to extracting and using audio frequency data.
---
Basic FFT Setup
// Create analyser
const analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 2048; // Power of 2, 32-32768
analyserNode.smoothingTimeConstant = 0.8; // 0-1, higher = smoother
// Connect to audio chain
source.connect(analyserNode);
analyserNode.connect(audioContext.destination);
// Get frequency data (spectrum)
const frequencyData = new Uint8Array(analyserNode.frequencyBinCount);
analyserNode.getByteFrequencyData(frequencyData); // 0-255 values
// Get waveform data (time domain)
const waveformData = new Uint8Array(analyserNode.fftSize);
analyserNode.getByteTimeDomainData(waveformData); // 128 = silence---
FFT Size Tradeoffs
| fftSize | Bins | Frequency Resolution | Time Resolution | Use Case |
|---|---|---|---|---|
| 256 | 128 | 172 Hz | 5.8 ms | Beat detection |
| 512 | 256 | 86 Hz | 11.6 ms | Fast visuals |
| 1024 | 512 | 43 Hz | 23.2 ms | Balanced |
| 2048 | 1024 | 22 Hz | 46.4 ms | Detailed spectrum |
| 4096 | 2048 | 11 Hz | 92.9 ms | High-res analysis |
Rule of thumb: Higher fftSize = better frequency resolution but slower response.
---
Logarithmic Frequency Bands
Critical knowledge: FFT bins are linear in frequency, but human hearing is logarithmic!
function getLogarithmicBands(
frequencyData: Uint8Array,
numBands: number,
sampleRate: number = 44100
): number[] {
const bands = new Array(numBands).fill(0);
const nyquist = sampleRate / 2;
for (let i = 0; i < numBands; i++) {
// Logarithmic frequency mapping (20Hz - Nyquist)
const lowFreq = 20 * Math.pow(nyquist / 20, i / numBands);
const highFreq = 20 * Math.pow(nyquist / 20, (i + 1) / numBands);
const lowBin = Math.floor(lowFreq / nyquist * frequencyData.length);
const highBin = Math.floor(highFreq / nyquist * frequencyData.length);
let sum = 0;
let count = 0;
for (let j = lowBin; j < highBin && j < frequencyData.length; j++) {
sum += frequencyData[j];
count++;
}
bands[i] = count > 0 ? sum / count : 0;
}
return bands;
}---
Common Frequency Ranges
function getFrequencyRanges(
frequencyData: Uint8Array,
sampleRate: number = 44100
): { bass: number; mid: number; treble: number } {
const nyquist = sampleRate / 2;
const binWidth = nyquist / frequencyData.length;
function getRange(lowHz: number, highHz: number): number {
const lowBin = Math.floor(lowHz / binWidth);
const highBin = Math.min(
Math.floor(highHz / binWidth),
frequencyData.length - 1
);
let sum = 0;
for (let i = lowBin; i <= highBin; i++) {
sum += frequencyData[i];
}
return sum / (highBin - lowBin + 1);
}
return {
bass: getRange(20, 250), // Sub-bass + bass
mid: getRange(250, 4000), // Low-mid + mid + high-mid
treble: getRange(4000, 20000) // Presence + brilliance
};
}---
Beat Detection
class BeatDetector {
private history: number[] = [];
private historySize = 43; // ~1 second at 60fps
private threshold = 1.3;
detect(frequencyData: Uint8Array): boolean {
// Focus on bass frequencies (first 1/8 of spectrum)
const bassEnd = Math.floor(frequencyData.length / 8);
let bassEnergy = 0;
for (let i = 0; i < bassEnd; i++) {
bassEnergy += frequencyData[i] * frequencyData[i];
}
bassEnergy = Math.sqrt(bassEnergy / bassEnd);
// Add to history
this.history.push(bassEnergy);
if (this.history.length > this.historySize) {
this.history.shift();
}
// Compare to average
const average = this.history.reduce((a, b) => a + b, 0) / this.history.length;
// Beat if current energy exceeds threshold * average
return bassEnergy > average * this.threshold;
}
}---
Smoothing Techniques
Built-in Smoothing
analyserNode.smoothingTimeConstant = 0.8; // 0 = no smoothing, 1 = frozenManual Exponential Smoothing
class SmoothingFilter {
private smoothed: Float32Array;
private alpha: number;
constructor(size: number, alpha: number = 0.3) {
this.smoothed = new Float32Array(size);
this.alpha = alpha;
}
apply(raw: Uint8Array): Float32Array {
for (let i = 0; i < raw.length; i++) {
this.smoothed[i] = this.smoothed[i] * (1 - this.alpha) + raw[i] * this.alpha;
}
return this.smoothed;
}
}Attack/Release Smoothing
class AttackRelease {
private current: number = 0;
private attack: number; // Fast rise
private release: number; // Slow fall
constructor(attack: number = 0.9, release: number = 0.3) {
this.attack = attack;
this.release = release;
}
apply(target: number): number {
const alpha = target > this.current ? this.attack : this.release;
this.current = this.current * (1 - alpha) + target * alpha;
return this.current;
}
}---
Audio Texture for Shaders
function createAudioTexture(
gl: WebGLRenderingContext,
frequencyData: Uint8Array
): WebGLTexture {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Upload as 1D texture (width = data length, height = 1)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
frequencyData.length,
1,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
frequencyData
);
// Filtering
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);
return texture!;
}
// Update each frame
function updateAudioTexture(
gl: WebGLRenderingContext,
texture: WebGLTexture,
frequencyData: Uint8Array
) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0, 0,
frequencyData.length, 1,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
frequencyData
);
}---
AudioContext State Handling
async function ensureAudioContext(
audioContext: AudioContext
): Promise<void> {
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
}
// Require user interaction to start
function setupAudioActivation(
audioContext: AudioContext,
element: HTMLElement
) {
const activate = async () => {
await ensureAudioContext(audioContext);
element.removeEventListener('click', activate);
element.removeEventListener('touchstart', activate);
};
element.addEventListener('click', activate);
element.addEventListener('touchstart', activate);
}