Making Music Tangible: Connecting Web Audio API to Matter.js
How I bridged the gap between digital frequency data and rigid body physics to create a visualizer that feels alive.


On a Tuesday night in mid-February, I was staring at a grid of neon bars rising and falling in perfect sync with a techno track I’d been listening to for four hours straight. It was the standard implementation: an AnalyserNode from the Web Audio API feeding a requestAnimationFrame loop, drawing rectangles on an HTML5 Canvas. Functionally, it was correct. Emotionally, it was dead. The music had punch, groove, and a physical weight that the visualizer completely failed to capture. The bass hit felt like a thump in the chest, but on screen, it was just a green line moving upward by 40 pixels.
I realized that the problem wasn't the data; the FFT (Fast Fourier Transform) was working fine. The problem was the representation. Frequencies are abstract, but we experience music as a physical phenomenon. I wanted the visuals to have mass, momentum, and collision. I wanted the snare hit to actually smack into something. That night, I scrapped the canvas drawing logic and decided to hook the Web Audio API directly into Matter.js, a 2D rigid body physics engine for the web. The goal was to turn frequency bands into physical forces.

The Limitation of Standard Frequency Bars
Before diving into the solution, I need to explain why the standard approach—draw loop clear, draw loop render—felt insufficient for this specific project. Standard visualizers are immediate. When the frequency value goes from 0 to 255, the bar height changes instantly. This precision is great for scientific analysis or utility, but it lacks the "messiness" of the real world. In the real world, objects don't teleport to a new height; they accelerate, encounter friction, and bounce.
I had been experimenting with shaders recently, exploring how GPUs handle massive parallel calculations. While shaders are incredibly fast at manipulating pixels, they don't inherently understand concepts like "gravity" or "elastic collision" without writing complex custom physics logic in GLSL. That is a fascinating path, but for this particular experiment, I needed a physics environment that worked out of the box so I could focus on the audio-reactivity logic. I needed objects that would fall, stack, and tumble on their own, only getting interrupted when the music demanded it.
How to Hook an AnalyserNode to a Physics Engine
The architecture became clear: the Web Audio API would act as the sensor, and Matter.js would act as the nervous system and muscle.
Setting up the audio context is standard fare. I created an AudioContext, grabbed the source (a file input), and routed it through an AnalyserNode. The crucial configuration here is the fftSize. I settled on 512, which gives me 256 data points. This is a sweet spot; high enough resolution to separate the kick drum from the hi-hats, but low enough to keep the CPU overhead manageable.
The real magic happens in the update loop, which runs every physics tick. Matter.js has its own runner, but for audio reactivity, it is often better to drive the physics forces from the animation frame loop where the audio data is updated.
Inside the loop, I call analyser.getByteFrequencyData(dataArray). This populates my array with values between 0 and 255. I didn't want every single frequency bin to control a body; 256 bouncing balls would be chaotic noise. Instead, I grouped the data into "buckets" corresponding to the bass, mids, and highs.
I created a stack of static rectangles and a field of dynamic circular bodies (the "particles") resting on top. The logic for interaction was a direct force application:
// Pseudo-code logic for the force application
const bass = dataArray[10]; // grabbing the low-end
const forceMagnitude = bass * 0.002;
// Apply force to bodies near the center
bodies.forEach(body => {
if (body.position.x < 200 && body.position.x > 100) {
Body.applyForce(body, body.position, {
x: 0,
y: -forceMagnitude // Upward force
});
}
});
When the bass kicked, the value in dataArray[10] spiked. That spike translated to a significant upward force vector applied to the bodies in the center of the pile. The result wasn't just a bar rising; the entire stack of particles physically exploded upward, rained back down, and collided with each other. The "physics" of the visualization was handling the decay, the gravity, and the random scattering. I didn't have to code the "falling" animation; gravity did that for me.
Why the Simulation Felt "Heavy" at First
The initial implementation was disappointing. It felt sluggish, like the particles were moving through molasses. I checked the frame rate, and it was hovering around 30 FPS on my M2 MacBook Air. For a generative art piece, that is unacceptable. The lag was caused by a conflict in update rates.
The audio analysis was updating as fast as the browser could paint (60Hz+), but the physics solver was struggling to resolve collisions for 150 bodies every single frame. Every time a particle moved, Matter.js had to check it against every other particle to calculate a collision response. That is an $O(N^2)$ complexity problem.
I noticed a similar issue when analyzing Why Your Interactive Art Installation is Lagging. The bottleneck was rarely the graphics, but the logic running in the background.
To fix this, I had to make two specific trade-offs. First, I reduced the particle count from 150 to 80. Visual density decreased, but the fluidity of motion improved drastically. Second, I switched from using complex polygon bodies to simple circles. Collision detection between two circles is mathematically cheaper than between arbitrary polygons. Suddenly, the simulation snapped back to 60 FPS, and the audio reactivity felt instantaneous.
The Specificity of Smoothing Time Constants
Once the performance was sorted, the visuals still looked erratic. Raw audio data is jittery. A frequency value might jump from 100 to 200 and back to 100 in the span of three frames. Visually, this looked like the particles were vibrating uncontrollably rather than pulsing rhythmically.
I needed to tame the data before it hit the physics engine. The Web Audio API provides a property specifically for this: analyser.smoothingTimeConstant.
I tweaked this value for hours. A value of 0.1 meant the visuals reacted instantly to every micro-change in the sound—too nervous. A value of 0.9 introduced a heavy lag, where the visuals would jump seconds after the beat. I finally settled on 0.85 for the bass frequencies and 0.6 for the higher frequencies.
The bass needs to "boom" and sustain, hence the higher smoothing. The highs need to "sparkle," hence the lower smoothing. This differentiation added a new layer of expression. The heavy circles at the bottom would thump and slowly settle, while the lighter particles at the top would dance frantically.
There is a distinct beauty in using a physics engine for audio visualization because of the unpredictability. In a standard canvas visualization, if you play the same song twice, the animation looks identical every time. With this physics setup, because the collision resolution depends on the precise order of operations and floating-point math, the visual result is never exactly the same. The particles might stack differently on the second chorus. They might tumble left instead of right on the final drop. The system becomes a live performer rather than a playback device.
Choosing the right stack is critical for this kind of experimentation. While many traditional creative coders swear by Processing or JavaScript, the browser ecosystem offers unique advantages for audio integration that desktop environments sometimes struggle with, particularly regarding the native AudioContext.
The Unintended Beauty of Resting States
The most interesting discovery didn't happen during the loud drops, but during the quiet breakdowns. In a standard visualizer, silence equals empty space. In this physics-based version, silence meant the particles would finally come to rest.
I added a dampening factor to the walls and floor of the container. When the music stopped abruptly, the particles would settle into a pile, physically touching each other, creating a unique geometric pattern determined by how they landed. When the music kicked back in, they would explode out of that specific pattern. The visuals were remembering the history of the song through the physical arrangement of the bodies.
This "memory" effect is impossible with simple frequency bars. It transforms the listening experience into something tactile. You aren't just hearing the music; you are watching a system struggle and recover from the energy injected into it.
If you are looking to move beyond basic frequency analysis, try introducing a simulation layer. Whether it is Matter.js, a custom spring system, or even basic verlet integration, adding "weight" to your data changes the audience's relationship with the sound. It makes the digital feel analog, and that is a powerful illusion to pull off in a browser window.

