A desktop tool for debugging positional audio in a space. Place audio emitters and walls (with material types) on a top-down 2D canvas, drag a listener around, and hear a physically-modelled spatial mix of what you'd hear standing at that point.
Built in Rust with egui for the interface, Valve's Steam Audio (via audionimbus) for acoustics, and cpal for real-time output.
The point of the tool is to approximate how sound really behaves in a room. Each emitter's audio reaches the listener through a chain of physical models:
- Distance falloff — sources get quieter with distance by the inverse-distance law (−6 dB every time the distance doubles), as a point source does in open air.
- Propagation delay — sound travels at ~343 m/s, so each source is delayed by its real time-of-flight. This produces the precedence (Haas) effect and comb filtering between overlapping sources, plus a gentle Doppler glide as the listener moves.
- Air absorption — high frequencies fade faster than lows over distance, so far-away sources sound duller, not just quieter.
- Binaural spatialization (HRTF) — you hear each source from its actual direction via head-related filtering (for headphones); a speaker-panning mode is also available.
- Source directivity — emitters can be aimed: each has a facing direction and a beam width (angle of emission), so a speaker pointed away from the listener sounds dimmer, like a real loudspeaker rather than an omnidirectional point.
- Occlusion & transmission — walls block the direct path and muffle whatever passes through, depending on material, frequency, and thickness (glass leaks more highs than concrete; a thicker wall transmits less).
- Directional reflections / reverb — ray-traced reverb that tracks room size and surface materials and arrives from the right direction (a hard wall on your left reflects from your left).
- Diffraction — low frequencies bend around walls instead of stopping at a hard shadow edge, found by tracing sound paths around the geometry.
- Material acoustics — six wall materials (concrete, brick, glass, wood, carpet, curtain), each with its own absorption, scattering, and transmission.
Everything is in real units: emitter levels in dB, wall lengths in millimetres, a metric canvas. See How the simulation is wired for the details.
cargo runHeadphones recommended. Spatialization defaults to HRTF binaural rendering, which assumes headphone playback. A speaker/panning output mode is available in the side panel for loudspeaker listening.
Steam Audio native library.
build.rsembeds anrpathto the auto-installedlibphonon.dylibin the build tree, so./target/debug/positional-audio-toolruns directly. A relocatable.appbundle (dylib copied in,@executable_pathrpath, codesigning) is still future work.
Two seamless demo tones are in assets/ (400 Hz and 600 Hz) for loading
onto emitters.
- Place emitter — click on the canvas to drop an emitter, then Load audio (WAV or MP3) in the side panel to give it a looping clip and a dB level slider. Each emitter also has facing and emission (beam width) sliders; the canvas draws the resulting cone, and at 360° emission it is omnidirectional.
- Draw wall — pick a material, then click two points to lay a wall segment. Each wall's length is shown in millimetres on the canvas and in the side panel, where it can be typed to an exact value and given a thickness (drawn to scale, and affecting how much sound transmits through).
- Move listener — the listener (ear icon) follows your cursor over the canvas; click to toggle following on/off.
- Room dimensions — set the room's width × depth in millimetres; the canvas is sized to show that room (drawn as a boundary rectangle).
Four concerns, split as the code is:
core/— pure, no IO or threads: the 2D scene model, the 2D→3D geometry mapping (walls extruded to vertical quads), material acoustics, and the per-source mix computation (compute_mix). Unit-tested.debug/— the egui window (owns the macOS main thread) and the 2D canvas.sim/— the background simulation thread that ray-traces reflections and bakes / runs pathing on the same Steam AudioSimulator, shipping params to the audio thread.audio/— the cpal output stream and the real-time mixer. Per voice it runs a propagationDelayLine→DirectEffect→BinauralEffect, plus aReflectionEffectand aPathEffect, each decoded to stereo through anAmbisonicsDecodeEffect.
The UI computes a SimSnapshot (per-source direction, distance, attenuation, air
absorption, gain) each frame and publishes it through an ArcSwap. The audio callback
reads the latest snapshot lock-free and never blocks or allocates on the hot path; new
clip samples arrive over a single-producer rtrb ring, and reflection / pathing params
arrive over two more rtrb rings from the simulation thread.
Working today: the 2D editor (place emitters, draw walls with per-material editing, drag the listener, settable canvas scale), looping WAV/MP3 emitters with dB levels, scene save/load, headphone and speaker output modes, and the full real-time acoustic chain from What it simulates. Runs as a standalone binary.
The heavier pieces of the chain run off the audio thread and feed it lock-free.
Each voice runs through a variable fractional DelayLine sized to
its true time-of-flight (distance / 343 m·s⁻¹) before the direct effect. Because every
source arrives at its real time, the precedence (Haas) effect and comb filtering between
overlapping sources fall out for free; moving the listener glides the delay, giving mild
Doppler. The delay is applied only to the direct path — reflections and paths carry their
own propagation times from the simulation.
A background thread (sim/thread.rs) runs Steam Audio's
ray-traced reflection simulation (Convolution, ambisonic order 1) at ~15 Hz: it builds
a Scene/StaticMesh from the walls (plus an optional floor/ceiling enclosure), traces
rays from the listener, and computes a directional (4-channel ambisonic) impulse response
per source from each surface's absorption/scattering. The ReflectionEffectParams are
shipped to the audio thread over a lock-free rtrb ring (params are Send and retain
their source, keeping the IR valid), where each voice convolves through a
ReflectionEffect and decodes the ambisonic reverb to stereo with an
AmbisonicsDecodeEffect (binaural for headphones, panning for speakers). Because the
reverb is directional you can hear where the room is — a hard wall on your left
reflects from your left. Its presence is determined entirely by geometry and materials,
not a manual control. Draw walls, toggle Floor & ceiling, and the reverb appears;
swap a wall from concrete to curtain and the room goes from live to dead.
The same simulator also runs Steam Audio's pathing simulation. On each geometry
change it generates a grid of probes at ear height (sim/pathing.rs)
and bakes the shortest sound paths between them around the walls. At runtime it finds the
path from each source to the listener — including paths that bend around obstacles — and
ships an owned PathEffectParams (3-band EQ + ambisonic directionality) to the audio
thread, where a PathEffect + AmbisonicsDecodeEffect render it. This restores the
low-frequency energy that leaks around a wall, instead of treating occlusion as a hard
shadow. Pathing needs a floor to place probes on, so it is most meaningful with the
Floor & ceiling enclosure on.
A headless test (enclosed_room_simulates_and_renders_reverb)
exercises the whole chain end to end: scene → run_reflections/run_pathing → params →
ReflectionEffect/PathEffect apply → AmbisonicsDecodeEffect decode.
Future upgrades: higher ambisonic orders for sharper directionality, individualised
HRTFs, and a relocatable macOS .app bundle.
cargo fmt
cargo clippy --all-targets -- -D warnings
cargo test