Skip to content

Apply pre-render object positions and head rotation from the first frame - #22

Closed
trsonic wants to merge 11 commits into
mainfrom
fix/obr-initial-object-position
Closed

Apply pre-render object positions and head rotation from the first frame#22
trsonic wants to merge 11 commits into
mainfrom
fix/obr-initial-object-position

Conversation

@trsonic

@trsonic trsonic commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Fixes #21.

Problem

As analyzed in #21, OLR and OBR disagree on metadata applied before the first render call. When an audio element is added, OBR registers each object source with the ambisonic encoder at the default position (azimuth 0, elevation 0, distance 1). A position update arriving before the first Process() call only moves the ramp target, so the first rendered block audibly glides the object from front-center to its configured position. OLR applies pre-render metadata immediately, so loudspeaker and binaural output differ over the first block. The same pattern exists in the ambisonic rotator: a head pose set before the first render slerps in from identity.

Fix

Until a component has rendered any audio there is no previously audible state to interpolate from, so updates snap instead of scheduling a ramp:

  • AmbisonicEncoder tracks whether it has processed a block; while it hasn't, SetSource() sets current along with target. This covers every pre-render update path (object channels, loudspeaker-channel positions, gains) since they all funnel through SetSource().
  • AmbisonicRotator does the same for the target rotation on its first processed block.
  • Blocks rendered with head tracking disabled bypass the rotator but are audible at the identity rotation, so ProcessingGroup marks them via MarkAudioRendered(); enabling head tracking with a stored pose after such blocks still slerps instead of snapping.

Updates arriving after audio has been rendered still ramp/slerp across one block to avoid clicks, and mid-stream update semantics are unchanged.

Tests

  • AmbisonicEncoderTest.PreRenderPositionUpdateTakesEffectImmediately — reproduces the Question: Initial object position OLR vs OBR #21 glide on unfixed code (frame 0 carries the front-position coefficients instead of the configured position).
  • AmbisonicEncoderTest.PostRenderPositionUpdateRampsAcrossBlock — guards the click-avoidance ramp after audio has flowed.
  • AmbisonicRotatorTest.FirstProcessedBlockAppliesTargetRotationInFull, RotationAfterBypassedBlocksStillSlerps, RotationChangeAfterFirstBlockStillSlerps — same contract for the rotator, including the head-tracking-disabled case.

Each "first block" test fails without its fix; the existing encoder and rotator tests pass unchanged.

trsonic and others added 11 commits July 30, 2026 19:00
Ports the fork's MODULE.bazel, BUILD.bazel, extensions.bzl and
third_party/pffft.BUILD onto the public repo layout (code at the
repository root instead of liboar/).
Bazel globs do not cross package or module boundaries; the vendored OBR
subtree's BUILD and MODULE.bazel files would stop the top-level targets
from picking up its sources.
Azimuths outside (-180, 180] (e.g. +270 for -90) skipped the front-back
fold and hit the closest-speaker fallback, whose linear angle difference
picks the wrong speaker whenever the true separation crosses the +/-180
seam. Normalize azimuths once at metadata ingestion so every downstream
consumer sees canonical angles, and make the fallback distance circular.
Renders aliased azimuth pairs (+270 vs -90 etc.) to stereo and requires
bit-identical output, and checks lateral sources collapse onto opposite
speakers. Before the fix a source at +270 landed on the left speaker.
enable_testing() at the top level puts the obr unit tests and the liboar
examples in one ctest registry; the examples are registered under an
"oar" label (mirroring the obr subtree's "obr" label) derived from the
directory's target list so new examples cannot be silently left out. The
CI workflow's hardcoded example list and separate obr ctest invocation
collapse into a single label-filtered ctest run.
_open stored the address of the stack-local pout in
ear_renderer->out_sp_layout, which lives for the renderer's lifetime.
The pointer is only dereferenced during _open itself (matrix lookups),
so the bug is latent, but any future reader of
out_sp_layout.sp_layout.predefined_sp after _open returns would hit a
dangling stack pointer. Null it before returning, matching how the
input-layout paths already reset pin/cin.
The ARM NEON matrix-render, OBR resampler/sh_hrir DSP, and LFE filter
sample-rate fixes were merged upstream (AOMediaCodec/oar #14, #15 and
the matrix_render include commit), so they drop off the pending list.
The OLR azimuth wrapping, ctest registration, and EAR dangling-pointer
fixes take their place.
An object source is registered with the ambisonic encoder at its default
position (azimuth 0, elevation 0, distance 1) as soon as the audio
element is added. A position update arriving before the first render
call therefore only moved the ramp target, and the first processed block
audibly glided the object from front-center to its configured position.
Loudspeaker rendering (OLR) applies pre-render metadata immediately, so
binaural output disagreed with loudspeaker output over the first block.

Track whether the encoder has processed any audio yet; until it has,
SetSource() snaps the current parameters to the new target instead of
scheduling a ramp. There is no previously audible position to
interpolate from, so metadata applied before the first render now takes
effect from the first frame, matching OLR. Updates arriving after audio
has been rendered still ramp across one block to avoid clicks.

Reported upstream as #21.
The ambisonic rotator's current rotation starts at identity and only
the target rotation is supplied per Process() call, so a head pose set
before the first render call made the whole scene slerp from identity
to that pose across the first processed block.

Apply the same rule as for object positions: until any audio has been
rendered there is no previously audible rotation to interpolate from,
so the first processed block applies the target rotation in full from
the first frame. Rotation changes after audio has been rendered still
slerp across the block in 32-frame intervals.

Blocks rendered with head tracking disabled bypass the rotator but are
audible at the identity rotation, so the processing group marks them
via MarkAudioRendered(); enabling head tracking with a stored pose
after such blocks still slerps instead of snapping.
@trsonic

trsonic commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Withdrawing for now; will resubmit later.

@trsonic trsonic closed this Jul 30, 2026
@trsonic
trsonic deleted the fix/obr-initial-object-position branch July 30, 2026 20:40
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.

Question: Initial object position OLR vs OBR

1 participant