VOLTAreference

Reference / Effects and mixing

Position sounds in space

Effects and mixing

Open in the workbench →
On this page · 6 sections

Place a sound in a modeled listening space and explore how position affects its output.

padT  = play pad chords |> place 320 10        -- 40° left (constants wrap), 10° up
orbit = macro "orbit" 0 (lfoSaw @ 16c |> scale [0, 1])
arpT  = play pluck line |> place orbit 0 1.5   -- circles the head every 16 cycles
farT  = play bell riff  |> place 180 35 2      -- behind, above, twice the distance

1. Surface#

track |> place azimuth elevation [distance] — a track effect (one instance on the mixer bus, at its position in the chain, like panTrack/width).

SlotUnitsRangeNotes
azimuthdegrees−180 … 1800 = front, +90 = hard right, ±180 = behind. Constants wrap mod 360 — write 320 for −40° (the negative-literal-in-application workaround).
elevationdegrees−90 … 90clamped. 0 = ear level, +90 = overhead.
distanceunitless0 … 4clamped; optional, default 1 = reference. Gain 1/d beyond 1 (no boost inside); the native engine also low-passes with distance (air absorption).

Point source. place collapses the bus to mono before spatializing — that's what a position means. width/haas earlier in the chain are inert (allowed, not rejected). Reverb after a place stays a normal stereo tail around the placed source — usually what you want.

Live modulation. Each slot accepts a bare macro; the knob's [0, 1] travel maps across the slot's full range (az [−180, 180], el [−90, 90], dist [0, 4] — the width convention; macro 0.5 = az 0° / el 0° / dist 2). LFO- or pattern-driven placement is the macro-modulation idiom: macro "orbit" 0 (lfoSaw @ 16c |> scale [0, 1]) sweeps a full circle every 16 cycles. Azimuth smoothing takes the short way around the circle, so a saw-driven orbit crosses ±180° cleanly instead of swooshing back across the front.

2. IR#

{ "op": "Place", "azimuth": -40.0,                       // Param: const (degrees)
  "elevation": { "macro_ref": "lift", "initial": 0.7 },  // Param: live macro
  "distance": 1.0 }

Param::Const carries surface units; Param::Macro carries the [0, 1] knob initial — the engine applies the range map (including at build, so the first block starts where the knob sits). Lowering: compiler/src/lower/track.rs (BuiltinKind::Place) — azimuth wraps, elevation/distance clamp, distance defaults to Const(1.0).

3. Browser#

One Rust engine renders everywhere, so the browser gets exactly §4's structural binaural, via engine-wasm/ in an AudioWorklet. No platform HRTF panner, no distance-filter divergence: the Web Audio graph was deleted 2026-07-24.

4. Native engine (reference) — engine/src/binaural.rs#

A structural (parametric) binaural model — asset-free, deterministic, smooth under continuous modulation. Per sample, after the mono collapse:

  1. Distance — inverse gain 1/(1 + max(d−1, 0)) + an air-absorption one-pole (≈20 kHz at d ≤ 1 → ≈6 kHz at d = 4).
  2. ITD — Woodworth spherical head (a = 8.75 cm, c = 343 m/s), per-ear fractional delays, per-sample-ramped across each block and read with 4-point Hermite (dsp::hermite4) on a common +1-sample bias. The read was 2-point linear until the 2026-08-03 DSP audit §A7 measured the interpolator's own frac-dependent droop riding the (perfectly smooth) Woodworth delay: an orbit swished the shadowed ear 3.7 dB at 16 kHz, 6.0 dB across az 84°→90°. Now 1.9 / 3.2 dB, and 0.9 → 0.14 dB at 8 kHz. Those figures are at 48 kHz. The residual is the Hermite kernel's own droop, which is a function of f/fs, so it does not transfer: the same az 84°→90° spread at 16 kHz measures 4.58 dB at 44.1 kHz, 3.24 dB at 48, and 0.23 dB at 96 — a 96 kHz session gets what 48 kHz gives at 8 kHz.
  3. ILD — Brown–Duda head-shadow one-pole-one-zero per ear (H(s) = (αs + β)/(s + β), β = 2c/a, α = 1 + cosΓ): unity at DC, the far ear loses highs, the near ear gains up to +6 dB.
  4. Elevation — a pinna notch (−10 dB, Q 4) sweeping ~3.3 kHz → ~13 kHz as elevation rises −90° → +90°.

Targets smooth per block (~20 ms). Unit tests pin the ILD sign at az ±90, the Woodworth ITD magnitude, exact L/R mirror symmetry, notch movement with elevation, the inverse distance law, and bit determinism (engine/src/binaural.rs::tests); the offline smoke test covers the build/process wiring (offline.rs::new_track_effects_render_audible). engine/tests/stereo_interp.rs pins the two things an orbit can get wrong — binaural_orbit_has_no_interpolator_swish (the ITD read's frac-dependent droop) and place_delay_still_ramps_per_sample (block-grid sideband energy, which unlike seam-vs-inside |Δ²x| actually separates a ramped delay from a stepped one).

Why structural, not measured HRIRs: no embedded binary asset (size/licensing), no inter-direction interpolation artifacts, zipper-free under the orbit use case, ~200 lines. Measured-HRIR convolution is the fidelity ceiling → design spec Stage 3.

5. Touch points#

The standard track-effect anatomy (Contribute a language feature): BuiltinKind::Place + registry (compiler/src/builtins.rs), TrackEffect::Place (ir/src/ir.rs), lowering (compiler/src/lower/track.rs) + TrackRole::Effect in BuiltinKind::track_role, which drives both guards (compiler/src/lower.rs track-name detection, compiler/src/lower/expr.rs not-callable — they were two hand-maintained lists until 2026-08-15), the runtime/src/types.ts mirror, engine EffectState::Place (engine/src/effect.rs — build / process / reset / apply_macros), the catalogue entry in compiler/assets/docs-data.json, and examples/songs/orbit.volta (+ golden snapshot).

6. Gotchas#

  • Negative constants: application args can't take a bare negative literal; write place 320 10 (wrap) or place (0 - 40) 10.
  • Not a master insert: spatializing the summed master is a category error (place collapses its input to ONE point source). Above a single track its home is a sub-mix — submix flyers = [lead, pad, tone] with inserts = [place az 0 dist] places the sum of a few tracks, and the live keys of a patch, as one source (Mixing tracks and buses §1.3).
  • A sound track pipes through place like any track.
  • pulseT |> place 0 0 is not a no-op: the bus still collapses to a point source and gets the front-center HRTF coloration (that's the anchor cue).