7.11.16. STRUDEL-16 — HRTF: 3D Positional Override for Pan
daslang exposes a per-event HRTF position (azimuth, elevation) on
top of the standard mini-notation. When set, the equal-power
stereo pan render is replaced by a binaural-stereo HRTF:
the dry signal and the orbit-effect sends are run through the same
CIPIC-based ma_hrtf engine that audio_boost uses for 3D
sources. Pan still shapes source width / L/R balance feeding the
spatialiser; HRTF places the source in 3D.
Why bother — equal-power pan only places sources on a horizontal
stereo line between the speakers. HRTF adds:
depth / externalisation (sources feel “outside the head” on headphones),
front-back disambiguation (centre-front is no longer ambiguous with centre-rear),
elevation cues.
Cost — under the binaural-stereo render, each HRTF-positioned voice
carries two ma_hrtf instances (hrtf_l + hrtf_r,
lazy-allocated on first render: one per virtual-speaker channel at
azimuth -/+ 30°). Practical for 3-8 simultaneous HRTF voices on
modern hardware. Plain pan is far cheaper; reach for HRTF only
when you want spatial cues plain pan can’t provide.
7.11.16.1. API
pat |> hrtf_azimuth(deg) // numeric or pattern-valued, -180..180
pat |> hrtf_elevation(deg) // numeric or pattern-valued, -90..90
pat |> hrtf(az, el) // combined numeric setter
Setting any of the three flips the event’s hrtf_active flag.
Pan and HRTF are orthogonal under the binaural-stereo render:
pan controls the inherent stereo width / image of the source (it
shapes the L/R balance of the input feeding the spatialiser), and
hrtf positions the resulting source in 3D. Best heard on
headphones.
7.11.16.2. Part A: Static positions
hrtf(az, el) takes both axes at once. 0/0 is directly ahead,
+90/0 is right, -90/0 is left, 0/+45 is up-front:
let pat <- stack([
note("c4", "sine") |> hrtf(0.0, 0.0) |> attack(0.01) |> release(0.5) |> gain(0.4),
note("e4", "sine") |> hrtf(-90.0, 0.0) |> attack(0.01) |> release(0.5) |> gain(0.4),
note("g4", "sine") |> hrtf(+90.0, 0.0) |> attack(0.01) |> release(0.5) |> gain(0.4),
note("c5", "sine") |> hrtf(0.0, 45.0) |> attack(0.01) |> release(0.5) |> gain(0.4)
])
Four notes from four directions: ahead, hard-left, hard-right, up-front.
7.11.16.3. Part B: Animated azimuth
The setters accept patterns. Each event samples the modulation
pattern at its onset to get its azimuth. With
sine() |> range(-180, 180) |> slow(4), the source orbits the
listener once every 8 cycles (on a 0.5 cps stream):
var pat <- (
note("c4 e4 g4 c5", "triangle")
|> hrtf_azimuth(sine() |> range(-180.0, 180.0) |> slow(4.0lf))
|> attack(0.005) |> release(0.3) |> gain(0.4)
)
hrtf_elevation works the same way; combine the two for full
spherical motion.
7.11.16.4. Part C: Pan and HRTF combine — HRTF positions, pan widens
The render branch is selected per voice. When hrtf_active is
true the equal-power L/R mixer is replaced by a binaural-stereo HRTF:
the L and R input channels become virtual sources at azimuth - 30°
and azimuth + 30°, each through its own HRTF instance, summed at the
output. The pre-pan stereo image (whatever pan produced) is
preserved as source width; HRTF places the source in 3D. The same
binaural render is reused for the reverb / delay / chorus sends.
var pat <- stack([
note("c4", "sine") |> pan(0.5) |> gain(0.4),
note("e4", "sine") |> pan(0.5) |> hrtf(-90.0, 0.0) |> gain(0.4)
])
The second voice has both pan(0.5) and hrtf(-90, 0). The
voice is positioned to the left by HRTF; the slight L/R imbalance
that pan(0.5) produced is preserved as source width inside that
3D position (rather than being thrown away).
7.11.16.5. Part D: Mixing HRTF and plain pan
HRTF is heavier than pan and works best on headphones. For drum
machines or beats where you just want left/right separation, plain
pan is cheaper and works fine on speakers. Mix-and-match in the
same stack: HRTF the lead voice, pan the rhythm section:
var pat <- stack([
s("bd ~ sd ~") |> orbit(0) |> gain(0.6),
s("hh*8") |> orbit(0) |> pan(0.7) |> gain(0.3),
note("c4 e4 g4 e4", "triangle") |> orbit(1)
|> hrtf_azimuth(sine() |> range(-60.0, 60.0) |> slow(2.0lf))
|> room(0.4) |> roomsize(2.0) |> gain(0.35)
])
Because the lead is on a separate orbit, its reverb stays distinct from the (dry) drum bus. The reverb send for the lead voice is itself HRTF-positioned, so the room follows the source through space.
See also
Full source: tutorials/daStrudel/daStrudel_16_hrtf_position.das
Standalone examples: examples/daStrudel/features/hrtf_basic.das,
examples/daStrudel/features/hrtf_animated.das,
examples/daStrudel/features/hrtf_overrides_pan.das
Previous tutorial: STRUDEL-15 — Live-Reloading Patterns
Mini-notation compatibility: mini-notation compatibility
Full daStrudel reference: Strudel (Live Coding)