Skip to content

Android AutoMix: mix across sample rates, and correct the clock after a lead-in - #236

Merged
ingoau merged 3 commits into
mainfrom
fix/android-automix-sample-rate
Nov 10, 2026
Merged

ingoau merged 3 commits into
mainfrom
fix/android-automix-sample-rate

Conversation

@ingoau

@ingoau ingoau commented Nov 10, 2026

Copy link
Copy Markdown
Owner

From diagnostics report R-20261110-af7k ("automix cut from Ten to Perfect Pinterest Garden without fading").

Sample rate change dropped the mix

Every transition between a 44.1 kHz and a 48 kHz track in that log rendered its tail but never started. When the format changes, DefaultAudioSink can't join gaplessly. It drains A, flushes once still naming A (the new configuration is swapped in after that flush), then flushes for B with no end of stream in between. TransitionMixer read that second flush as a seek, so the mix ended with no log line and A cut off at its clip point.

Fix: every flush between an end of stream and the next frame belongs to the join (TransitionMixer.startItem). Once frames flow, a flush without an end of stream is a seek again.

ExoPlayer's clock after a mix with a lead-in

The same log has B reported at 14031 ms when it should have been 0. The sink's clock counts output frames, and a mix outputs incoming_at_ms of tail before B from incoming_from_ms, so the clock runs ahead by the difference (15.4 s for Ten). The backend mapped that back for B, but the lead carried into the next item. ExoPlayer moves on early by the lead at a gapless join. The renderer's position never goes back, so it held the next item at the lead.

Fix: MixClockSink (a ForwardingAudioSink) takes the lead out once the tail has sounded. The step is signalled as a position discontinuity so the renderer accepts it. The correction drops when the sink re-bases its clock at the next item's first frame (checkpoint or new AudioTrack), or at a flush. ExoBackend keeps adding the offset until ExoPlayer's position shows the step, so reports stay continuous. Leads under 100 ms are left alone.

Testing

  • New TransitionMixerTest.aFormatChangeAtTheJoinKeepsTheMix replays the sink's flush sequence. New MixClockSinkTest covers both directions of the lead, the re-base, pause and flush.
  • :playback:testDebugUnitTest: 158 tests pass.
  • Not yet checked on a device. The sink's flush order and clock behaviour come from reading Media3 1.11.1's DefaultAudioSink / MediaCodecAudioRenderer / ExoPlayerImplInternal.

🤖 Generated with Claude Code

ingoau and others added 3 commits November 10, 2026 11:36
When B's sample rate differs from A's, DefaultAudioSink can't join
gaplessly: it drains A, flushes once still naming A (the new
configuration is swapped in after that flush), then flushes for B with no
end of stream in between. TransitionMixer took that second flush as a
seek, so the mix was dropped without a log line and A cut off at its clip
point. Every flush between an end of stream and the next frame now
belongs to the join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sink's clock counts output frames, and a mix outputs the tail's first
incoming_at_ms before B from incoming_from_ms, so B's clock runs ahead by
the difference. The backend mapped that back for B, but the lead carried
into the next item: ExoPlayer moved on early (gapless) and the renderer,
whose position never goes back, held the next item at the lead (a 48 kHz
track reported 14 s in at its start).

MixClockSink wraps the sink: once the tail has sounded the backend asks
it to take the lead out, as a position discontinuity the renderer
accepts; the correction drops when the sink re-bases its clock at the
next item's first frame, or at a flush. The backend keeps adding the
offset until ExoPlayer's position shows the step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k switch-over

- A flush continues a join only at the item's start: a seek while the
  sink waits for A to play out (a format change) no longer starts the mix
  at the seek target.
- The backend switches to ExoPlayer's corrected position once the sink
  has applied the correction and ExoPlayer's own reading stepped by the
  lead, read every 40 ms until then, instead of guessing from wall time
  (a pause right after the correction could keep the wrong one).
- No correction within 3 s of B's end, where it could land on the next
  item after the sink's re-base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ingoau
ingoau merged commit f7e6408 into main Nov 10, 2026
13 checks passed
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.

1 participant