A browser tool for binaural (HRTF) audio positioning. Runs entirely on the Web Audio API - no install, no plugins, no server.
Live demo: https://mmd-marcelo.github.io/lrAudioM1/
It started as the spatial-audio module for a private project that needed binaural positioning in the browser. We wrote the convolution pipeline ourselves instead of pulling in a library, mostly so we could control the audio graph directly. The standalone UI (visualizer, motion presets, EQ) was built around it for testing, and turned out usable enough to release on its own.
The honest summary: the core is a solid HRTF panner. Everything else ranges from useful to decorative. It's not a replacement for professional spatial-audio tools, and it isn't trying to be.
HRTF binaural positioning - the part worth using. Uses the SADIE II KU100 dataset (9201 measured positions, full sphere) with MIT KEMAR as a fallback. For any direction it blends the 4 nearest measurements (inverse-distance-squared weighting over great-circle distance) and convolves through ConvolverNode, crossfading between two convolvers so the source can move at 60 fps without clicking. The IR rebuild is throttled (~30 Hz, angle-gated) so calling setPosition every frame is cheap. This works well.
Early reflections - a 2nd-order image-source model in a fixed 16×16×10 m room, each reflection convolved with its own HRTF. Honestly, it's over-engineered for what you hear: the room is large enough that, after the amplitude threshold, usually only 2-3 reflections survive. There's a per-reflection lowpass that darkens paths which lost more energy - a rough approximation of high-frequency absorption, not a real per-band wall model. The net effect is subtle. It adds a faint sense of space, nothing more.
Camera head tracking - turn your head, the scene stays fixed. Webcam → MediaPipe FaceLandmarker (runs on-device in WASM) → yaw/pitch from the face transformation matrix → One Euro filter to kill jitter → the source position is counter-rotated before it hits the panner. Works. The model (~3.7 MB) loads from a CDN on first use; video frames never leave the browser. Roll is ignored (HRTF is indexed by azimuth/elevation only).
Motion - 15 parametric path presets, a procedural "Fun" mode with per-axis oscillators, and a custom-path editor (up to 5 waypoints, linear or Catmull-Rom). Cosmetic, but fun to demo.
EQ - a 5-band filter (two shelves, three peaks) with an FFT overlay, plus a bass-boost shelf. Basic. It's a standard BiquadFilterNode chain; nothing special.
WAV export - renders the full chain offline (HRTF + reflections + EQ) and downloads a stereo WAV. If a motion preset is running, the export follows the path; otherwise it bakes the current static position.
No reverb. There was a Freeverb-style one. It sounded bad at long decay times and we removed it rather than keep fighting it. Export the WAV and add reverb in a DAW with a real convolution plugin - it'll be better than anything synthesized here.
Everything in src/engine/ is plain TypeScript with no React, Zustand, or Three.js imports (verified - the UI depends on the engine, never the reverse). Take what you want.
import { BinauralPanner } from './engine/hrtf/BinauralPanner'
const ctx = new AudioContext()
const panner = new BinauralPanner(ctx)
await panner.init() // fetches the dataset relative to Vite's BASE_URL
yourSourceNode.connect(panner.input)
panner.output.connect(ctx.destination)
// degrees, degrees, metres - safe to call every frame
panner.setPosition({ azimuth: 45, elevation: 0, distance: 2 })The datasets are binary files in public/hrtf/ (sadie_ku100.bin ~18 MB, kemar_L.bin ~2.2 MB). Copy them into your own public directory. If SADIE fails to load it falls back to KEMAR; if both fail, audio passes through unconvolved.
import { EarlyReflections } from './engine/effects/EarlyReflections'
const earlyRef = new EarlyReflections(ctx)
yourSourceNode.connect(earlyRef.input)
earlyRef.output.connect(ctx.destination)
const dataset = panner.getDataset()
if (dataset) earlyRef.setHrtfDataset(dataset) // make reflections binaural too
// room dims in metres, order 1-3
earlyRef.update(position, { width: 16, length: 16, height: 10 }, 2)If you adopt this, use a smaller room than 16×16×10 - more reflections will survive the amplitude threshold and the effect becomes audible.
src/engine/tracking/ is self-contained - camera, pose estimation, filtering, and the head-frame rotation math.
import { HeadTracker } from './engine/tracking/HeadTracker'
import { rotateIntoHeadFrame } from './engine/tracking/headFrame'
const tracker = new HeadTracker(
pose => panner.setPosition(rotateIntoHeadFrame(sourcePos, pose.yaw, pose.pitch)),
status => console.log(status),
)
await tracker.start() // prompts for camera permissionSee src/app/utils/wav.ts - it renders the chain through an OfflineAudioContext and animates the source position during the render via suspend()/resume().
Use Resonance Audio or Omnitone. They're maintained, ambisonic-capable, and you won't have to read our code. This engine is only worth it if you specifically want direct control of the Web Audio graph.
npm install
npm run dev # http://localhost:5173
npm test # Vitest
npm run build # → dist/Deploy to GitHub Pages is automated (.github/workflows/deploy.yml, on push to master). The Vite base is /lrAudioM1/ and must match the repo name exactly (GitHub Pages paths are case-sensitive).
React 18 · TypeScript · Zustand · Three.js · Web Audio API · Vite · MediaPipe Tasks Vision
The measurement datasets are not ours; only the loading/interpolation/convolution code is. Check each dataset's terms before redistributing.