Skip to content

Give OBR's head-locked and world-locked passes their own encoders - #54

Open
trsonic wants to merge 1 commit into
mainfrom
fix/obr-mixed-head-locked
Open

trsonic wants to merge 1 commit into
mainfrom
fix/obr-mixed-head-locked

Conversation

@trsonic

@trsonic trsonic commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #53: in a binaural processing group with both head-locked and world-locked loudspeaker or object elements, channels were encoded with other channels' positions.

  • One encoder per pass (renderer/processing_group.{h,cc}). ProcessingGroup keeps an EncoderPass (encoder and input buffer) for the world-locked and for the head-locked channels. Each holds only its pass's channels as sources, in element order, so input i of a pass is encoded with the position of the pass's i-th channel, and the passes no longer share inputs or interpolation state. UpdateAmbisonicEncoder() sizes and sets both. The unfiltered GetAmbisonicEncoderSourceChannelIndices() overload had no other caller and is removed.
  • SetElementHeadLocked() (renderer/obr_impl.{h,cc}) calls UpdateAllAmbisonicEncoders(), because a change moves the element's channels to the other pass. Its documentation says that it can allocate when an encoder's source count changes.

A group whose elements are all world-locked or all head-locked uses one encoder with the same sources as before, so its output does not change. With the reproduction from #53, both runs now render element 2 on the right: L 6.577, R 227.456.

Tests:

  • tests/examples/test_binaural_mixed_lock.c renders, through oar.h, a world-locked object beside a silent head-locked one and requires the output of the world-locked object alone, and the reverse with the head-locked object alone.
  • ObrImplTest.TestSetElementHeadLockedUpdatesEncoders sets a configured group's element head-locked, and back to world-locked so that the head-locked pass is empty, and requires the output of the world-locked object alone.

Both fail on main.

Verified locally on macOS arm64:

  • CMake with the workflow's commands, ctest --test-dir build -L 'oar|obr' --output-on-failure: 32 of 32 pass, in Release and with -fsanitize=address.
  • Bazel 8.5.1: //tests/examples:all and @obr//obr/renderer/tests:all pass (13 pass, 1 skipped). The standalone Bazel build fails on this machine on main too, with the Xcode SDKSettings.json "absolute path inclusion" error in untouched files, so these targets were built as modules from a workspace that registers apple_support's toolchain.

Closes #53

ProcessingGroup::Process() encodes the loudspeaker layout and object
channels of a group in two passes, the world-locked elements' and the
head-locked elements', and copies each pass's channels to the
encoder's inputs 0 to k-1. One encoder held all the group's channels
as sources, in element order. In a group with both kinds of element a
pass's channels were therefore encoded with other channels' positions,
the encoder inputs past k still held the other pass's audio, and both
passes shared one encoder's interpolation state. Through oar.h, with
element 1 head-locked and silent at +90 degrees and element 2
world-locked noise at -90 degrees, element 2 is rendered at +90
degrees: left and right energy 226.695 and 6.799, against 6.577 and
227.456 with element 1 world-locked.

Each pass now has its own encoder and input buffer (EncoderPass), with
the pass's channels as sources in element order, so input i of a pass
is encoded with the position of the pass's i-th channel.
UpdateAmbisonicEncoder() sizes and sets both, and
SetElementHeadLocked() calls it through UpdateAllAmbisonicEncoders(),
because a change moves the element's channels to the other pass; its
documentation says that it can allocate. The unfiltered
GetAmbisonicEncoderSourceChannelIndices() overload had no other caller
and is removed. A group whose elements are all world-locked or all
head-locked uses one encoder with the same sources as before.

test_binaural_mixed_lock.c renders a world-locked object beside a
silent head-locked one and requires the output of the world-locked
object alone, and the reverse with the head-locked object alone.
ObrImplTest.TestSetElementHeadLockedUpdatesEncoders sets a configured
group's element head-locked, and back to world-locked so that the
head-locked pass is empty, and requires the output of the world-locked
object alone. Both fail on main.

ctest -L 'oar|obr' (CMake, Release and ASan): 32 pass.
@trsonic
trsonic force-pushed the fix/obr-mixed-head-locked branch from d9c1195 to e647b0b Compare October 19, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OBR: in a group with head-locked and world-locked elements, channels are encoded at other channels' positions

4 participants