Repository navigation
Android: keep one output rate so joins across sample rates stay gapless - #237
Merged
Merged
Conversation
ExoPlayer's DefaultAudioSink can only reuse its AudioTrack when the output format is unchanged. At a join between tracks at different sample rates it played A out to silence, released the track and built a new one, which left a short gap: between tracks, and in the middle of A's audio when AutoMix mixed across rates. MixProcessor now leaves at one rate (the first stream's when it is 44.1 or 48 kHz, else the device's), converting other streams after the mixer with a new streaming resampler in hocket-android (JNI): a Kaiser-windowed sinc with exact per-phase taps, output frame j at input j * in / out, ~1.5 ms lookahead. - A gapless join at the same format continues through the same resampler. - When the next stream is at another format, A's end of stream emits A's last frames (with the armed tail's continuation of A as lookahead), and B's resampler is primed with the tail's lead, so the switch from A to the tail has no seam. - prepare_tail decodes from 30 ms before the tail start and returns the lead and lookahead alongside the tail; it now uses the same resampler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The end of the playlist (MixClockSink.playToEndOfStream) finishes a resampled stream, so its held-back last ~1.5 ms is played. - A lead still primes a resampler that hasn't had input (a change of channel count flushes twice before the mix starts). - The finish path reads the output buffer after it may have grown. - prepare_tail floors the context in frames like the decoder and the clip point, sizes its buffer in u64, and the tail render decodes 60 ms of context so short-step rate pairs still get a lead. - The resampler JNI refuses one buffer as both input and output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
AutoMix across sample rates still left a short gap now and then. The cause is ExoPlayer's
DefaultAudioSink, not the mixer: it reuses itsAudioTrackonly when the output format is unchanged (canReuseAudioOutput). At a 44.1 ↔ 48 kHz join it played A out to silence, released the track and built a new one. That leaves a gap between tracks, and in the middle of A's audio when the join is a mix.What
MixProcessornow always outputs at one rate, so every join is an ordinary gapless join and the sink keeps its track.OutputRate.kt): the first stream's rate after a reset when it is 44.1 or 48 kHz, else the device's (PROPERTY_OUTPUT_SAMPLE_RATE). A library at one rate is never resampled and stays bit-exact. There's no extra CPU and no double resampling over 44.1 kHz Bluetooth.crates/hocket-android/src/resampler.rs, JNI): a Kaiser-windowed sinc with exact taps per output phase (160 rows for 44.1 ↔ 48 kHz, ~80 dB stopband). Output framejis input framej × in / outexactly. It holds back only the kernel's ~1.5 ms lookahead, not a 1024-frame chunk. It runs after the mixer, so the mixer's sample-exact item accounting is unchanged.after(A's real continuation) as lookahead. B's resampler is primed with the tail'slead, so the switch from A to the tail is as clean as at one rate.automix_prepare_taildecodes from 60 ms before the tail start. That gives the tail's first frames filter history; before, they were resampled from silence. It returnsleadandafteralong with the tail.Testing
cargo test -p hocket-android(26 tests). New resampler tests cover:finishwith lookahead andprimewith history both matching an uncut streamprepare_tail:playback:testDebugUnitTest: newOutputRateTest(11 tests) covers:MixProcessor: a fixed output format across items, end of stream holdingisEndeduntil A's last frames are out, and the resampler staying open at a same-format end of streamKnown limits
AudioTrack.🤖 Generated with Claude Code