Commit 504020a7 by ibaker Committed by Tianyi Feng

Use ceiling divide logic in `AudioTrackPositionTracker.hasPendingData`

This fixes a bug with playing very short audio files, introduced by
https://github.com/google/ExoPlayer/commit/fe710871aad3e4e6b4e0798f1cf762d5ecfebedb

The existing code using floor integer division results in playback never
transitioning to `STATE_ENDED` because at the end of playback for the
short sample clip provided `currentPositionUs=189937`,
`outputSampleRate=16000` and `(189937 * 16000) / 1000000 = 3038.992`,
while `writtenFrames=3039`. This is fixed by using `Util.ceilDivide`
so we return `3039`, which means
`AudioTrackPositionTracker.hasPendingData()` returns `false` (since
`writtenFrames ==
durationUsToFrames(getCurrentPositionUs(/* sourceEnded= */ false))`).

Issue: androidx/media#538
PiperOrigin-RevId: 554481782
(cherry picked from commit a9a2451ccbd08649da20936407076681fe3ad40f)
parent 212a912d
...@@ -1374,6 +1374,38 @@ public final class Util { ...@@ -1374,6 +1374,38 @@ public final class Util {
} }
/** /**
* Returns the total duration (in microseconds) of {@code sampleCount} samples of equal duration
* at {@code sampleRate}.
*
* <p>If {@code sampleRate} is less than {@link C#MICROS_PER_SECOND}, the duration produced by
* this method can be reversed to the original sample count using {@link
* #durationUsToSampleCount(long, int)}.
*
* @param sampleCount The number of samples.
* @param sampleRate The sample rate, in samples per second.
* @return The total duration, in microseconds, of {@code sampleCount} samples.
*/
public static long sampleCountToDurationUs(long sampleCount, int sampleRate) {
return (sampleCount * C.MICROS_PER_SECOND) / sampleRate;
}
/**
* Returns the number of samples required to represent {@code durationUs} of media at {@code
* sampleRate}, assuming all samples are equal duration except the last one which may be shorter.
*
* <p>The result of this method <b>cannot</b> be generally reversed to the original duration with
* {@link #sampleCountToDurationUs(long, int)}, due to information lost when rounding to a whole
* number of samples.
*
* @param durationUs The duration in microseconds.
* @param sampleRate The sample rate in samples per second.
* @return The number of samples required to represent {@code durationUs}.
*/
public static long durationUsToSampleCount(long durationUs, int sampleRate) {
return Util.ceilDivide(durationUs * sampleRate, C.MICROS_PER_SECOND);
}
/**
* Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
* *
* @param value The attribute value to decode. * @param value The attribute value to decode.
......
...@@ -27,6 +27,7 @@ import static com.google.android.exoplayer2.util.Util.parseXsDateTime; ...@@ -27,6 +27,7 @@ import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.parseXsDuration;
import static com.google.android.exoplayer2.util.Util.unescapeFileName; import static com.google.android.exoplayer2.util.Util.unescapeFileName;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
...@@ -833,6 +834,21 @@ public class UtilTest { ...@@ -833,6 +834,21 @@ public class UtilTest {
} }
@Test @Test
public void sampleCountToDuration_thenDurationToSampleCount_returnsOriginalValue() {
// Use co-prime increments, to maximise 'discord' between sampleCount and sampleRate.
for (long originalSampleCount = 0; originalSampleCount < 100_000; originalSampleCount += 97) {
for (int sampleRate = 89; sampleRate < 1_000_000; sampleRate += 89) {
long calculatedSampleCount =
Util.durationUsToSampleCount(
Util.sampleCountToDurationUs(originalSampleCount, sampleRate), sampleRate);
assertWithMessage("sampleCount=%s, sampleRate=%s", originalSampleCount, sampleRate)
.that(calculatedSampleCount)
.isEqualTo(originalSampleCount);
}
}
}
@Test
public void parseXsDuration_returnsParsedDurationInMillis() { public void parseXsDuration_returnsParsedDurationInMillis() {
assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L);
assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L);
......
...@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.audio; ...@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import static com.google.android.exoplayer2.util.Util.durationUsToSampleCount;
import static com.google.android.exoplayer2.util.Util.sampleCountToDurationUs;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
...@@ -244,7 +246,10 @@ import java.lang.reflect.Method; ...@@ -244,7 +246,10 @@ import java.lang.reflect.Method;
outputSampleRate = audioTrack.getSampleRate(); outputSampleRate = audioTrack.getSampleRate();
needsPassthroughWorkarounds = isPassthrough && needsPassthroughWorkarounds(outputEncoding); needsPassthroughWorkarounds = isPassthrough && needsPassthroughWorkarounds(outputEncoding);
isOutputPcm = Util.isEncodingLinearPcm(outputEncoding); isOutputPcm = Util.isEncodingLinearPcm(outputEncoding);
bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; bufferSizeUs =
isOutputPcm
? sampleCountToDurationUs(bufferSize / outputPcmFrameSize, outputSampleRate)
: C.TIME_UNSET;
rawPlaybackHeadPosition = 0; rawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0; rawPlaybackHeadWrapCount = 0;
passthroughWorkaroundPauseOffset = 0; passthroughWorkaroundPauseOffset = 0;
...@@ -280,7 +285,7 @@ import java.lang.reflect.Method; ...@@ -280,7 +285,7 @@ import java.lang.reflect.Method;
if (useGetTimestampMode) { if (useGetTimestampMode) {
// Calculate the speed-adjusted position using the timestamp (which may be in the future). // Calculate the speed-adjusted position using the timestamp (which may be in the future).
long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
long timestampPositionUs = framesToDurationUs(timestampPositionFrames); long timestampPositionUs = sampleCountToDurationUs(timestampPositionFrames, outputSampleRate);
long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs();
elapsedSinceTimestampUs = elapsedSinceTimestampUs =
Util.getMediaDurationForPlayoutDuration(elapsedSinceTimestampUs, audioTrackPlaybackSpeed); Util.getMediaDurationForPlayoutDuration(elapsedSinceTimestampUs, audioTrackPlaybackSpeed);
...@@ -426,7 +431,8 @@ import java.lang.reflect.Method; ...@@ -426,7 +431,8 @@ import java.lang.reflect.Method;
* @return Whether the audio track has any pending data to play out. * @return Whether the audio track has any pending data to play out.
*/ */
public boolean hasPendingData(long writtenFrames) { public boolean hasPendingData(long writtenFrames) {
return writtenFrames > durationUsToFrames(getCurrentPositionUs(/* sourceEnded= */ false)) long currentPositionUs = getCurrentPositionUs(/* sourceEnded= */ false);
return writtenFrames > durationUsToSampleCount(currentPositionUs, outputSampleRate)
|| forceHasPendingData(); || forceHasPendingData();
} }
...@@ -497,23 +503,18 @@ import java.lang.reflect.Method; ...@@ -497,23 +503,18 @@ import java.lang.reflect.Method;
} }
// Check the timestamp and accept/reject it. // Check the timestamp and accept/reject it.
long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs(); long timestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs();
long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
long playbackPositionUs = getPlaybackHeadPositionUs(); long playbackPositionUs = getPlaybackHeadPositionUs();
if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { if (Math.abs(timestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
listener.onSystemTimeUsMismatch( listener.onSystemTimeUsMismatch(
audioTimestampPositionFrames, timestampPositionFrames, timestampSystemTimeUs, systemTimeUs, playbackPositionUs);
audioTimestampSystemTimeUs,
systemTimeUs,
playbackPositionUs);
audioTimestampPoller.rejectTimestamp(); audioTimestampPoller.rejectTimestamp();
} else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) } else if (Math.abs(
sampleCountToDurationUs(timestampPositionFrames, outputSampleRate) - playbackPositionUs)
> MAX_AUDIO_TIMESTAMP_OFFSET_US) { > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
listener.onPositionFramesMismatch( listener.onPositionFramesMismatch(
audioTimestampPositionFrames, timestampPositionFrames, timestampSystemTimeUs, systemTimeUs, playbackPositionUs);
audioTimestampSystemTimeUs,
systemTimeUs,
playbackPositionUs);
audioTimestampPoller.rejectTimestamp(); audioTimestampPoller.rejectTimestamp();
} else { } else {
audioTimestampPoller.acceptTimestamp(); audioTimestampPoller.acceptTimestamp();
...@@ -545,14 +546,6 @@ import java.lang.reflect.Method; ...@@ -545,14 +546,6 @@ import java.lang.reflect.Method;
} }
} }
private long framesToDurationUs(long frameCount) {
return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate;
}
private long durationUsToFrames(long durationUs) {
return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND;
}
private void resetSyncParams() { private void resetSyncParams() {
smoothedPlayheadOffsetUs = 0; smoothedPlayheadOffsetUs = 0;
playheadOffsetCount = 0; playheadOffsetCount = 0;
...@@ -584,7 +577,7 @@ import java.lang.reflect.Method; ...@@ -584,7 +577,7 @@ import java.lang.reflect.Method;
} }
private long getPlaybackHeadPositionUs() { private long getPlaybackHeadPositionUs() {
return framesToDurationUs(getPlaybackHeadPosition()); return sampleCountToDurationUs(getPlaybackHeadPosition(), outputSampleRate);
} }
/** /**
...@@ -602,7 +595,7 @@ import java.lang.reflect.Method; ...@@ -602,7 +595,7 @@ import java.lang.reflect.Method;
long elapsedTimeSinceStopUs = (currentTimeMs * 1000) - stopTimestampUs; long elapsedTimeSinceStopUs = (currentTimeMs * 1000) - stopTimestampUs;
long mediaTimeSinceStopUs = long mediaTimeSinceStopUs =
Util.getMediaDurationForPlayoutDuration(elapsedTimeSinceStopUs, audioTrackPlaybackSpeed); Util.getMediaDurationForPlayoutDuration(elapsedTimeSinceStopUs, audioTrackPlaybackSpeed);
long framesSinceStop = durationUsToFrames(mediaTimeSinceStopUs); long framesSinceStop = durationUsToSampleCount(mediaTimeSinceStopUs, outputSampleRate);
return min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); return min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);
} }
if (currentTimeMs - lastRawPlaybackHeadPositionSampleTimeMs if (currentTimeMs - lastRawPlaybackHeadPositionSampleTimeMs
......
...@@ -2079,11 +2079,11 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -2079,11 +2079,11 @@ public final class DefaultAudioSink implements AudioSink {
} }
public long inputFramesToDurationUs(long frameCount) { public long inputFramesToDurationUs(long frameCount) {
return (frameCount * C.MICROS_PER_SECOND) / inputFormat.sampleRate; return Util.sampleCountToDurationUs(frameCount, inputFormat.sampleRate);
} }
public long framesToDurationUs(long frameCount) { public long framesToDurationUs(long frameCount) {
return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; return Util.sampleCountToDurationUs(frameCount, outputSampleRate);
} }
public AudioTrack buildAudioTrack( public AudioTrack buildAudioTrack(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment