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}:
+ *
+ *
+ * - {@link #SAMPLE_RATE} of 1000Hz means a frame is 1000us long (100Hz would mean frames are
+ * 10_000us each).
+ *
- Channel count of stereo means there are two values for each frame.
+ *
+ *
+ * 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(