Skip to content

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

Merged
trsonic merged 1 commit into
mainfrom
fix/obr-mixed-head-locked
Oct 21, 2026
Merged

trsonic merged 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.
  • 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: 40 of 40 pass, in Release and with -fsanitize=address.
  • Bazel 8.5.1: //tests/examples:all and @obr//obr/renderer/tests:all pass (21 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

@trsonic
trsonic force-pushed the fix/obr-mixed-head-locked branch from d9c1195 to e647b0b Compare October 19, 2026 16:43
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. 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): 40 pass.
@trsonic
trsonic force-pushed the fix/obr-mixed-head-locked branch from e647b0b to 94df884 Compare October 21, 2026 07:11
@trsonic

trsonic commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main after #55 merged (551f974). #55 and this PR both change renderer/processing_group.{h,cc}. In the resolution, UpdateEncoderPass() reserves the index list of its own pass (the world-locked or the head-locked one) for that pass's source count, where #55's UpdateAmbisonicEncoder() reserved both lists for the group's total, and Process() fills each list within its pass's capacity as in #55. #55 already removed the unfiltered GetAmbisonicEncoderSourceChannelIndices() overload, so I dropped that sentence from the commit message and the description. The test registrations in tests/examples are appended after #52's and #58's. Locally 40 of 40 tests pass in Release and with AddressSanitizer, and the Bazel tests //tests/examples:all and @obr//obr/renderer/tests:all pass (21 pass, 1 skipped).

@trsonic
trsonic merged commit e7152e8 into main Oct 21, 2026
9 checks passed
@trsonic
trsonic deleted the fix/obr-mixed-head-locked branch October 21, 2026 07:29
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