Skip to content

[Android] E-AC-3 (DD+/Atmos) with AudioMode.Unity: capture delivers sample-doubled PCM — audio plays at 0.5×, one octave down #2498

Description

@lightsailvr

Unity version

6000.3.20f1

Unity editor platform

macOS

AVPro Video edition

Ultra

AVPro Video version

3.4.1

Device hardware

Meta Quest 3

Which Android OS version are you using?

14

Unity Graphics API

Vulkan

Video API

media3/ExoPlayer

Texture format

Y Cb Cr 420_OES

Audio output

System Direct

Any other Media Player component configuration required to reproduce the issue.

No response

Which output component(s) are you using?

No response

Any other component configuration required to reproduce the issue.

No response

The issue

Environment

  • AVPro Video 3.4.1 Ultra (native plugin reports 3.4.1-ultra); changelog for 3.4.2 (21 Aug 2026) shows no related fix
  • Unity 6000.3.20f1, Vulkan
  • Meta Quest 3, Horizon OS (Android 14 / API 34)
  • Bundled ExoPlayer: AndroidXMedia3/1.10.1
  • Unity DSP output sample rate: 48000 (verified at runtime, not just project settings)

Setup

  • MediaPlayer with PlatformOptionsAndroid.audioMode = AudioMode.Unity, set before the native player is created (fresh native player; mode baked at construction)
  • Six AudioOutput components, OneToAllChannels, ChannelMask = 1 << k (k = 0..5), SupportPositionalAudio = true
  • Local file playback (Path.Absolute), no streaming

Media

MP4: HEVC video + E-AC-3 5.1, 48 kHz, 768 kbps. dec3 box = 1800200f0000 (fscod=0 → 48 kHz, bsid=16, acmod=7, lfeon=1, no ec3 JOC extension flag), but the bitstream carries a Dolby Atmos (JOC) payload — Android selects its audio/eac3-joc C2 decoder for it (visible in CCodecConfig logs). This appears to be the trigger: see "Controls" below.

Symptom

All captured audio renders at exactly half speed, one octave flat, with an audible skip/pop every ~5 seconds. Video and GetCurrentTime() advance normally (measured 1.00× against wall clock).

Measurements

  1. Spectral proof of the 2× factor: we recorded the headset output and compared its long-term average spectrum to the source track on a log-frequency axis. Best alignment at frequency ratio 2.0010, correlation 0.984 — the entire source spectrum is present, shifted down one octave. Raw-waveform autocorrelation shows no peak at DSP-block or codec-buffer lags, so it is sample-level doubling (or an equivalent ×2 upsample), not repeated chunks.
  2. Ring arithmetic: GetAudioBufferedSampleCount() grows at +288,000 samples/s while Unity's DSP consumes 288,000 samples/s (6 ch × 48 kHz) — i.e. delivery into the capture buffer runs at exactly 2× real time. Depth peaks around ~1.38 M samples (~4.8 s) and then drops by ~1.2 M in one step — heard as the periodic pop.
  3. The decoder itself is clean: CCodecConfig reports raw.sample-rate = 48000, channel-count = 6 for the session.
  4. AudioSource.pitch = 1.0 throughout; measured DSP pull rate ≈ 48 kHz.

Controls

Path | Result -- | -- Same file, AudioMode.SystemDirect | Correct (pitch, speed, sync) Same video remuxed to AAC 5.1, identical capture rig | Correct (pitch, sync, no pops) Synthetic plain E-AC-3 5.1 (no Atmos payload in the bitstream) | Captured correctly in our channel-mapping tests

So the defect tracks the E-AC-3-with-JOC-payload decode specifically, on the capture path only.

Hypothesis

The numbers are consistent with the capture sink treating the decoded stream's input rate as 24000 Hz while the PCM is actually 48000 Hz, and upsampling ×2 to reach the output rate — possibly a sample-rate read from the eac3-joc format path rather than from the decoder's actual output format (the decoder's output-format change to 48000/6ch may not be reaching the sink's rate config).

Secondary issue, same area

After the media reaches end-of-stream and stops, GrabAudio() keeps returning data — stale circular-buffer contents wrapping about every ~1.5 s — while GetAudioBufferedSampleCount() returns 0. An app that keeps its AudioSources alive after FinishedPlaying renders the tail of the clip looping indefinitely. (We work around it by muting when IsPlaying() is false, but GrabAudio probably ought to return 0 once the ring is empty.)

Repro assets

We can privately share a ~10 MB bit-exact Atmos/JOC proxy clip that reproduces this, plus full logcat captures and the measurement scripts used above.

Media information

No response

Logcat output

Metadata

Metadata

Assignees

Labels

AndroidAndroid platformFixedA fix will be in the next release

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions