omg-audio · the simulation stack

How the Sound
Is Made

Thirteen simulations run at once so that a piano behind a bookshelf sounds like a piano behind a bookshelf. Each layer answers one physical question, hands its result to the next, and every claim below is a passing test in the repository.

↓  read as a score — the layers build in order

Iarchitecture

Two clocks, one contract

Everything else lives inside this split.

Sound needs two speeds at once. The world changes slowly — you walk, a door swings — so a simulation tick every 50 ms (20 Hz, in a Web Worker or a native thread) is plenty. The waveform changes 48,000 times a second and tolerates zero lateness, so an AudioWorklet renders it sample by sample on the browser's real-time thread.

The only thing that crosses between them is a tap table: each acoustic path, of any origin, is one tap — key · delay · direction · gains[3]. Gains come in three bands (below 250 Hz, 250–2500, above 2500) because every physical effect in this engine is frequency-dependent. The key is a stable identity: as long as a path keeps its key across ticks, the renderer glides its delay — which is physically exact Doppler for free — and when a key vanishes, its tap fades out instead of clicking. Engines can be swapped live because they all speak tap.

SIMULATION · 20 Hz rays · mirrors · statistics AUDIO · per sample delay lines · HRTF · reverb taps: key · delay · dir · gains stable key → delay glide → Doppler · vanished key → crossfade
measured one tick budget 50 ms · typical traced tick 1.5–7 ms · a doorway can produce ~145 taps per source before the ceiling
IIearly · classic

Image sources

The exact solution for an empty box — the engine everything is measured against.

Reflect the source across a wall and the echo behaves exactly like a second source standing in the mirror room. Reflect the mirrors again for second-order bounces. For a shoebox room this is closed-form and exact: order 3 yields precisely 63 image sources, each with a machine-perfect delay, direction, and per-band wall loss.

Its weakness is its assumption: an empty box. Furniture breaks it, and other rooms are invisible to it — so the classic build routes sound between rooms through authored portals: virtual sources standing in doorways, blend zones while you cross, aperture re-radiation. That machinery worked, and it is still selectable as early = ism — but it is bookkeeping about geometry rather than geometry, and it made every doorway cost 2.5× a normal tick. The rest of this page is, in part, the story of deleting it.

source listener image the mirrored source's straight line IS the bounce path — length and angle exact
measured · trace_golden, world tests order-3 shoebox: 63 images, closed form · direct-path delay exact to 10⁻⁶ s · doorway tick in the portal era: 2.5× open ground
IIIgeometry

One world, made of surfaces with names

The mesh is the acoustic truth — and every plane keeps its identity.

The whole map — every wall, slab, roof, and the ground — is one triangle mesh under one BVH (a bounding-volume tree, so a ray finds its nearest hit in logarithmic time). Door and window holes are simply absent triangles. Each authored plane carries a stable surface id: a wall keeps one identity no matter how its door holes fracture it into patches, and each material carries per-band absorption, scattering, and transmission.

Identity is what makes paths durable. A reflection path is a chain of surface ids — [hall-east-wall], [floor, north-wall] — and a chain survives your movement even as its geometry glides underneath. That single property is what the next layer exploits.

measured · mesh_surfaces gate world mesh: 183,880 BVH primitives, 129,011 nodes · every surface's triangles provably coplanar · door holes share their wall's id
IVearly · traced

Path-traced early reflections

Rays discover that a path exists; geometry then supplies it exactly.

The traced engine splits the problem in a way most engines don't. A rotating golden fan of rays leaves the listener each tick and bounces over the world mesh, recording which surface chains it crosses — that is discovery, and it is cheap, statistical, and shared by every source (rays from your ears don't care what they're listening for). Each discovered chain enters a cache with a time-to-live.

Then, for every source, every cached chain is solved exactly: mirror the source across the chain's planes, draw the straight line, validate that each bounce lands on real mesh — a bounce that would land inside a door hole is refused. The solve is a handful of mirror operations, cheaper than one traced ray, and its delay and direction are machine-exact — so the sound is as stable as image sources, in geometry image sources could never handle. Every leg of the path also crosses the mesh honestly: a leg through a doorway hole passes free, a leg through masonry pays mass law (amplitude ∝ 1/thickness, bass surviving treble). Doorways stopped being a special case; they are just places where the wall isn't.

door = absent triangles source listener direct + ceiling bounce, both threading the hole zero portal code anywhere
measured · pt_mesh & world_traced gates mesh solver ≡ analytic solver in a golden box: 25/25 low-order paths, 57 records · through the open door −14.2 dB (free space) vs through masonry −59.3 dB, bass outliving treble · doorway tick now 0.83× open ground — cheaper than the field
Vdiffraction

Sound bends

Rays travel straight; hearing does not.

Step just past a doorway and the voice inside doesn't vanish — it dulls and seems to come from the door frame. That is knife-edge diffraction, and mirrors can't produce it. The engine prices it with the Kurze–Anderson approximation: the loss depends on the detour — how much longer the path over the edge is than the blocked straight line — measured in wavelengths, which is why shadows are bass-light and treble-dark.

The edges themselves are not authored. Door jambs, building corners and roof lines are auto-extracted from the world mesh as sharp creases, so a jamb and a skyline are the same object. Each shadowed source gets one tap from its dominant edge (summing four near-equal edges overcounts by ~10 dB — a measured mistake), scaled by the complement of the solved direct path so a clear sight line never double-counts. This layer is a live switch in the quality panel: turn it off and you get the hard shadows most game audio ships with.

source listener, in shadow blocked straight line detour δ → loss ≈ f(δ/λ) · bass bends, treble doesn't
measured · world_traced & module gates walk-past profile: a clean ~15 dB aperture bump, step-free at 0.5 m resolution · shadow boundary held to < −5 dB discontinuity · switch off → measurably harder shadow, restores exactly
VIoccluders

Furniture has ears-side consequences

The room isn't empty, so the engine stopped pretending it is.

Sofas, seminar tables, cathedral pillars, the club's bar — each is an acoustic box with a full material: per-band transmission, absorption, scattering. Every solved path leg checks what it passes through: a soft sofa eats treble and lets bass through, a bookshelf is nearly a wall. Significant pieces are also reflectors — their faces enter the surface table, discovery bounces off them, and a table-top slap is solved with the same machine-exact mirror as a wall bounce. And they shape the late field: reverb rays bounce off furniture with its absorption, so a furnished room measurably decays faster and more diffusely than the empty shell.

This is precisely where image sources broke and tracing wins on principle: the ray budget is constant no matter how cluttered the room is, while mirror enumeration explodes. It's also the second live switch — furniture: off makes every object acoustically transparent, which is the empty-shoebox assumption you can now hear as an absence.

measured · module_switches gate piano through the living-room bookshelf: ~14 dB of shadow, restored exactly on toggle · a table-top bounce solves to 10⁻⁵ s of the analytic mirror · furnished living-room RT60 0.43 s vs empty 0.80 s — the sofa halves the decay
VIIlate field

The reverberant tail is a statistic

Thousands of rays, measured — never authored.

After the first reflections, a room's response stops being individual paths and becomes a texture: thousands of overlapping arrivals decaying together. The engine measures it stochastically. Rays carry three-band energy from the source, bounce with material absorption and scattering (Lambert or specular, chosen per hit), and every crossing of a half-meter sphere around the listener deposits energy into a 10 ms-binned echogram. Schroeder backward integration over the echogram, fitted between −5 and −25 dB, yields the RT60 per band; energy after 80 ms yields the diffuse level. Those two numbers drive the renderer's reverb network.

Two disciplines keep it honest. No Russian roulette — stochastically killing rays corrupts the decay fit in stone rooms (measured, not theorized), so the honest budget lever is ray count, whose noise a running average absorbs. And a trace gate refuses to re-measure a scene that hasn't changed: motion, door swings and level changes re-trace instantly; stillness coasts on the average.

echogram: energy per 10 ms bin Schroeder fit −5…−25 dB → RT60 level = energy after 80 ms — deliberately NOT distance-scaled: the diffuse field doesn't get louder as you approach
measured · tracer & late gates concrete vs acoustic tile: RT60 ordering > · all-drywall Sabine check ≈ 1.6 s · late level distance-independent within ±3 dB over 1→6 m · mesh backend ≡ shoebox backend to 4 decimals
VIIIcoupled rooms

The wet field is measured, with a direction

Stand in the corridor: the living room's reverb reaches you through the door — because the rays did.

Since the late tracer runs on the world mesh, coupled rooms emerge instead of being routed. A source's rays leave its room through the doorway hole, decay in your room, and the echogram at your head simply contains it all: the coupled decay time, the level — and, because each energy deposit remembers its arrival direction, the tail's anisotropy. A reverberant room heard through a doorway reports a tail that mostly arrives from the doorway; the engine plays that share on a directional wet bus and the diffuse remainder in the reverb network. The last piece of portal bookkeeping — Lambert radiators, aperture routing — was deleted the day this measurement replaced it.

Doors themselves are panel overlays on the geometry: a leaf at 40% open covers 60% of its hole as a wood box that solved paths transmit through (mass law) and late rays bounce off. Swing it and every filter sweeps continuously.

measured · late_world gate corridor listener, piano in the living room: coupled RT60 0.82 s, tail ~29% directional toward the door · close the leaf → wet drops · reopen → restores · a panel over a test hole seals its through-energy to 0.000000
IXenvironment

The ambient dome

An audio skybox, sampled through real geometry.

Rain, wind and city rumble are not sources standing anywhere — they are a field arriving from the sky. Every tick, a fan of rays leaves your head and asks the world: which directions reach open sky, and through what? A ray escaping through a window carries the outdoor field from that window's direction, attenuated by the panes it crossed; a ray dying in masonry carries nothing. Binned by direction and smoothed over time, this produces the ambient image — walk from the field into the hall and the sky narrows to the doorway behind you, continuously, with no authored transition.

A parallel power balance handles what rays can't: through-shell seep (the storm as a bass presence inside a closed building), an enclosure factor that is geometric rather than reverberant, and roof exposure. Rain additionally anchors drop sounds on the glass panes themselves.

measured · ambience continuity gate walking through the hall door: total ambient energy step-free at 0.25 m resolution (every step < 3 dB) · enclosure ramps 0 → 1 smoothly across the threshold
Xbodies

Things that move, including you

Every moving thing is just a source; the layers above do the rest.

Your footsteps are a source in the world, at your feet. Steps fire on distance travelled (~1.5 m of stride, jittered so your feet aren't a metronome, gated to human cadence), pick their sample by the room's floor — wood, carpet, concrete, gravel — and then propagate like any other source: your steps echo in the cathedral and dull behind the bar, for free. Thrown balls and passing cars are dynamic sources with live positions; crouch, jump and sprint move the listener's actual ears, so ducking behind the club bar physically changes the solved paths. Face tracking, when enabled, steers the binaural head directly.

mechanics stride 1.35–1.65 m · cadence gate 320 ms · never the same sample twice in a row · sources: 11 (5 placed, feet, 3 balls, 2 cars)
XIbinaural

Rendering for two ears

Where taps become the feeling that the sound is *there*.

Every tap is binaural. All of them encode, at their arrival direction, into second-order ambisonics — a compact directional sound field — decoded through twenty virtual loudspeakers, each convolved with its measured KEMAR head-related response. That gives the full field correct interaural time and level differences, slightly blurred in angle. On top, the strongest N paths get a dedicated nearest-neighbor HRIR from a 710-direction grid: pin-sharp localization exactly where the precedence effect says hearing takes its bearings — from the loudest, earliest arrivals. The weak tail reads as envelopment either way.

Each tap runs through a per-sample fractional delay line (whose glide is the Doppler from layer I), per-band gain smoothing, and the diffuse and directional wet buses join at the end, before a slow AGC that protects your ears, not the mix. This is the one workload the GPU can never help with — it must produce the next sample now — which is why the governors of the next layer exist.

render path ambisonic order 220 virtual speakers × KEMAR HRIRs · point-tap grid 710 directions · budget: the N strongest paths (a live slider)
XIIgovernors

Shedding the right work first

Every quality lever trades variance or staleness — never a different sound.

Two governors watch two clocks. The sim governor starts every session at the lowest tier and earns its way up only after sustained calm, sheds instantly when a tick blows 40 ms or audio pressure persists. The audio governor watches render load per quantum: above 80% of realtime it sheds tap ceilings and point-HRIR count feed-forward — before the miss, because a missed audio deadline is the one failure you can hear. Every lever was chosen so that turning it down adds statistical noise (fewer rays), staleness (slower refresh), or spatial blur (fewer point HRIRs) — perceptually graceful degradations — and never bias.

The quality panel exposes all of it: pin any lever, or hand them back to the governors. The engine-module switches — ism/traced, cpu/gpu, diffraction, furniture — are the A/B instruments this page's claims can be checked with, live.

policy shed at load > 80% · climb after ~10 s calm · tap ceilings 160/112/64/32 · feed-forward shed on a > 420-tap surge, before the deadline is at risk
XIIIcompute

The same physics, on the GPU

Four WGSL kernels, one rule: silent fallback, parity-tested against the CPU.

The ray workloads are embarrassingly parallel, so they run as WebGPU compute — the same WGSL kernels on native wgpu and in the browser. K1 traces shoebox reverb; the PT kernel discovers box chains; K2 traces the world mesh (the BVH uploads once, ~9 MB; door leaves travel per-dispatch as 48-byte panel boxes); K3 runs chain discovery over the same buffers. The CPU implementations never leave: they are the fallback for machines without adapters and the oracle every kernel is statistically parity-tested against. A version handshake guards each bridge — on any mismatch the driver refuses and the sim keeps computing on CPU, because decoding garbage is worse than being slower.

What the GPU buys is density, not different sound: the same tick budget carries 8–16× the rays, so estimates are steadier — pure variance reduction, which is the only kind of free lunch this engine believes in.

measured · parity & speed gates K1: 4096 rays 1.0 ms vs CPU 7.3 ms · K2: 8192 world rays 9.2 ms vs CPU 512 in 11.6 ms16× density at equal wall-clock · K3 finds 97% of the CPU chain set at 5× density, 4.3 ms · levels agree to 3–4 decimals