diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioMixerImpl.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioMixerImpl.java index 8bee6e9172..683b7f2591 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioMixerImpl.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioMixerImpl.java @@ -228,7 +228,10 @@ import java.nio.ByteOrder; return EMPTY_BUFFER; } - long minSourcePosition = sources.size() == 0 ? maxPositionOfRemovedSources : endPosition; + long minSourcePosition = endPosition; + if (sources.size() == 0) { + minSourcePosition = min(minSourcePosition, maxPositionOfRemovedSources); + } for (int i = 0; i < sources.size(); i++) { minSourcePosition = min(minSourcePosition, sources.valueAt(i).position); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/AudioMixerImplTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/AudioMixerImplTest.java index 7be16e79ce..c49cf5303a 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/AudioMixerImplTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/AudioMixerImplTest.java @@ -32,11 +32,25 @@ import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; -/** Unit tests for {@link AudioMixerImpl}. */ +/** + * Unit tests for {@link AudioMixerImpl}. + * + *

The duration of a given buffer can be calculated with the {@link AudioFormat}: + * + *

+ * + * For example, a buffer containing 4 float values (of {@link #AUDIO_FORMAT_STEREO_PCM_FLOAT}) would + * be 2000us of data (4 values = 2 frames). + */ +// TODO(b/290002720): Expand and generalize parameterized test cases. @RunWith(ParameterizedRobolectricTestRunner.class) public final class AudioMixerImplTest { - @Parameters(name = "{0}") + @Parameters(name = "outputSilenceWithNoSources={0}") public static ImmutableList parameters() { return ImmutableList.of(false, true); } @@ -290,6 +304,88 @@ public final class AudioMixerImplTest { assertThat(mixer.isEnded()).isTrue(); } + @Test + public void output_withOneSource_queueSetEnd_outputsToEndTime() throws Exception { + mixer.configure(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* bufferSizeMs= */ 4, /* startTimeUs= */ 0); + + int sourceId = mixer.addSource(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* startTimeUs= */ 0); + ByteBuffer sourceBuffer = + createByteBuffer(new float[] {0.1f, -0.1f, 0.2f, -0.2f, 0.3f, -0.3f, 0.4f, -0.4f}); + + mixer.queueInput(sourceId, sourceBuffer); + mixer.setEndTimeUs(3000); + + // All input consumed because queued before end time set. + assertThat(sourceBuffer.hasRemaining()).isFalse(); + // Last frame not in output due to end time. + assertThat(createFloatArray(mixer.getOutput())) + .isEqualTo(new float[] {0.1f, -0.1f, 0.2f, -0.2f, 0.3f, -0.3f}); + assertThat(mixer.isEnded()).isTrue(); + } + + @Test + public void output_withOneSource_queueSetEndRemove_outputsToEndTime() throws Exception { + mixer.configure(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* bufferSizeMs= */ 4, /* startTimeUs= */ 0); + int sourceId = mixer.addSource(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* startTimeUs= */ 0); + ByteBuffer sourceBuffer = + createByteBuffer(new float[] {-0.5f, -0.5f, 0.25f, 0.25f, -0.25f, -0.25f, 0.25f, 0.25f}); + + mixer.queueInput(sourceId, sourceBuffer); + mixer.setEndTimeUs(3000); + mixer.removeSource(sourceId); + + // All input consumed because queued before end time set. + assertThat(sourceBuffer.hasRemaining()).isFalse(); + // Last frame not in output due to end time. + assertThat(createFloatArray(mixer.getOutput())) + .usingTolerance(1f / Short.MAX_VALUE) + .containsExactly(new float[] {-0.5f, -0.5f, 0.25f, 0.25f, -0.25f, -0.25f}) + .inOrder(); + assertThat(mixer.isEnded()).isTrue(); + } + + @Test + public void output_withOneSource_queueRemoveSetEnd_outputsToEndTime() throws Exception { + mixer.configure(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* bufferSizeMs= */ 4, /* startTimeUs= */ 0); + int sourceId = mixer.addSource(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* startTimeUs= */ 0); + ByteBuffer sourceBuffer = + createByteBuffer(new float[] {-0.5f, -0.5f, 0.25f, 0.25f, -0.25f, -0.25f, 0.25f, 0.25f}); + + mixer.queueInput(sourceId, sourceBuffer); + mixer.removeSource(sourceId); + mixer.setEndTimeUs(3000); + + // All input consumed because queued before end time set. + assertThat(sourceBuffer.hasRemaining()).isFalse(); + // Last frame not in output due to end time. + assertThat(createFloatArray(mixer.getOutput())) + .usingTolerance(1f / Short.MAX_VALUE) + .containsExactly(new float[] {-0.5f, -0.5f, 0.25f, 0.25f, -0.25f, -0.25f}) + .inOrder(); + assertThat(mixer.isEnded()).isTrue(); + } + + @Test + public void output_withOneSource_setEndQueueRemove_outputsToEndTime() throws Exception { + mixer.configure(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* bufferSizeMs= */ 4, /* startTimeUs= */ 0); + int sourceId = mixer.addSource(AUDIO_FORMAT_STEREO_PCM_FLOAT, /* startTimeUs= */ 0); + ByteBuffer sourceBuffer = + createByteBuffer(new float[] {-0.5f, -0.5f, 0.25f, 0.25f, -0.25f, -0.25f, 0.25f, 0.25f}); + + mixer.setEndTimeUs(3000); + mixer.queueInput(sourceId, sourceBuffer); + mixer.removeSource(sourceId); + + // Last frame of input not consumed because end time set before queue. + assertThat(sourceBuffer.remaining()).isEqualTo(AUDIO_FORMAT_STEREO_PCM_FLOAT.bytesPerFrame); + // All queued input (3 frames) in output. + assertThat(createFloatArray(mixer.getOutput())) + .usingTolerance(1f / Short.MAX_VALUE) + .containsExactly(new float[] {-0.5f, -0.5f, 0.25f, 0.25f, -0.25f, -0.25f}) + .inOrder(); + assertThat(mixer.isEnded()).isTrue(); + } + @Test public void input_whileIsEnded_isNotConsumed() throws Exception { mixer.configure(