diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 25ed16b3af..49cd436624 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,7 @@ ([#1531](https://github.com/androidx/media/issues/1531)). * DataSource: * Audio: + * Fix pop sounds that may occur during seeks. * Video: * Add workaround for a device issue on Galaxy Tab S7 FE that causes 60fps secure H264 streams to be marked as unsupported diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 33ad3d00d4..b4a21d94c1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -18,6 +18,7 @@ package androidx.media3.exoplayer.audio; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.constrainValue; +import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.exoplayer.audio.AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES; import static androidx.media3.exoplayer.audio.AudioCapabilities.getCapabilities; import static java.lang.Math.max; @@ -1143,7 +1144,7 @@ public final class DefaultAudioSink implements AudioSink { if (!buffer.hasRemaining()) { return; } - outputBuffer = buffer; + outputBuffer = maybeRampUpVolume(buffer); } /** @@ -1848,6 +1849,25 @@ public final class DefaultAudioSink implements AudioSink { } } + private ByteBuffer maybeRampUpVolume(ByteBuffer buffer) { + if (configuration.outputMode != OUTPUT_MODE_PCM) { + return buffer; + } + long rampDurationUs = msToUs(AUDIO_TRACK_VOLUME_RAMP_TIME_MS); + int rampFrameCount = + (int) Util.durationUsToSampleCount(rampDurationUs, configuration.outputSampleRate); + long writtenFrames = getWrittenFrames(); + if (writtenFrames >= rampFrameCount) { + return buffer; + } + return PcmAudioUtil.rampUpVolume( + buffer, + configuration.outputEncoding, + configuration.outputPcmFrameSize, + (int) writtenFrames, + rampFrameCount); + } + private static void releaseAudioTrackAsync( AudioTrack audioTrack, @Nullable Listener listener, AudioTrackConfig audioTrackConfig) { // AudioTrack.release can take some time, so we call it on a background thread. The background diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/PcmAudioUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/PcmAudioUtil.java new file mode 100644 index 0000000000..661be34de3 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/PcmAudioUtil.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.audio; + +import androidx.media3.common.C; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** Utility methods for PCM audio data. */ +@UnstableApi +public final class PcmAudioUtil { + + /** + * Returns a new {@link ByteBuffer} with linear volume ramping applied. + * + * @param buffer The input buffer containing PCM frames. The buffer will be fully consumed by this + * method. + * @param pcmEncoding The {@link C.Encoding} of the PCM frames. + * @param pcmFrameSize The overall frame size of one PCM frame (including all channels). + * @param startFrameIndex The index of the first frame within the audio ramp duration (as + * specified by {@code rampFrameCount}). + * @param rampFrameCount The overall ramp duration in number of frames. + * @return The {@link ByteBuffer} containing the modified PCM data. + */ + public static ByteBuffer rampUpVolume( + ByteBuffer buffer, + @C.Encoding int pcmEncoding, + int pcmFrameSize, + int startFrameIndex, + int rampFrameCount) { + ByteBuffer outputBuffer = + ByteBuffer.allocateDirect(buffer.remaining()).order(ByteOrder.nativeOrder()); + int frameIndex = startFrameIndex; + int frameStartPosition = buffer.position(); + while (buffer.hasRemaining() && frameIndex < rampFrameCount) { + long pcm32Bit = readAs32BitIntPcm(buffer, pcmEncoding); + pcm32Bit = pcm32Bit * frameIndex / rampFrameCount; + write32BitIntPcm(outputBuffer, (int) pcm32Bit, pcmEncoding); + if (buffer.position() == frameStartPosition + pcmFrameSize) { + frameIndex++; + frameStartPosition = buffer.position(); + } + } + outputBuffer.put(buffer); + outputBuffer.flip(); + return outputBuffer; + } + + /** + * Reads a single-channel PCM value from the buffer and returns it as a 32-bit integer PCM value. + * + * @param buffer The {@link ByteBuffer} to read from. + * @param pcmEncoding The {@link C.Encoding} of the PCM data in the buffer. + * @return The 32-bit PCM value of the read buffer. + */ + public static int readAs32BitIntPcm(ByteBuffer buffer, @C.Encoding int pcmEncoding) { + switch (pcmEncoding) { + case C.ENCODING_PCM_8BIT: + return (buffer.get() & 0xFF) << 24; + case C.ENCODING_PCM_16BIT: + return ((buffer.get() & 0xFF) << 16) | ((buffer.get() & 0xFF) << 24); + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + return ((buffer.get() & 0xFF) << 24) | ((buffer.get() & 0xFF) << 16); + case C.ENCODING_PCM_24BIT: + return ((buffer.get() & 0xFF) << 8) + | ((buffer.get() & 0xFF) << 16) + | ((buffer.get() & 0xFF) << 24); + case C.ENCODING_PCM_24BIT_BIG_ENDIAN: + return ((buffer.get() & 0xFF) << 24) + | ((buffer.get() & 0xFF) << 16) + | ((buffer.get() & 0xFF) << 8); + case C.ENCODING_PCM_32BIT: + return (buffer.get() & 0xFF) + | ((buffer.get() & 0xFF) << 8) + | ((buffer.get() & 0xFF) << 16) + | ((buffer.get() & 0xFF) << 24); + case C.ENCODING_PCM_32BIT_BIG_ENDIAN: + return ((buffer.get() & 0xFF) << 24) + | ((buffer.get() & 0xFF) << 16) + | ((buffer.get() & 0xFF) << 8) + | (buffer.get() & 0xFF); + case C.ENCODING_PCM_FLOAT: + float floatValue = Util.constrainValue(buffer.getFloat(), /* min= */ -1f, /* max= */ 1f); + if (floatValue < 0) { + return (int) (-floatValue * Integer.MIN_VALUE); + } else { + return (int) (floatValue * Integer.MAX_VALUE); + } + default: + throw new IllegalStateException(); + } + } + + /** + * Writes a 32-bit integer PCM value to a buffer in the given target PCM encoding. + * + * @param buffer The {@link ByteBuffer} to write to. + * @param pcm32bit The 32-bit PCM value. + * @param pcmEncoding The target {@link C.Encoding} of the PCM data in the buffer. + */ + public static void write32BitIntPcm( + ByteBuffer buffer, int pcm32bit, @C.Encoding int pcmEncoding) { + switch (pcmEncoding) { + case C.ENCODING_PCM_32BIT: + buffer.put((byte) pcm32bit); + buffer.put((byte) (pcm32bit >> 8)); + buffer.put((byte) (pcm32bit >> 16)); + buffer.put((byte) (pcm32bit >> 24)); + return; + case C.ENCODING_PCM_24BIT: + buffer.put((byte) (pcm32bit >> 8)); + buffer.put((byte) (pcm32bit >> 16)); + buffer.put((byte) (pcm32bit >> 24)); + return; + case C.ENCODING_PCM_16BIT: + buffer.put((byte) (pcm32bit >> 16)); + buffer.put((byte) (pcm32bit >> 24)); + return; + case C.ENCODING_PCM_8BIT: + buffer.put((byte) (pcm32bit >> 24)); + return; + case C.ENCODING_PCM_32BIT_BIG_ENDIAN: + buffer.put((byte) (pcm32bit >> 24)); + buffer.put((byte) (pcm32bit >> 16)); + buffer.put((byte) (pcm32bit >> 8)); + buffer.put((byte) pcm32bit); + return; + case C.ENCODING_PCM_24BIT_BIG_ENDIAN: + buffer.put((byte) (pcm32bit >> 24)); + buffer.put((byte) (pcm32bit >> 16)); + buffer.put((byte) (pcm32bit >> 8)); + return; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + buffer.put((byte) (pcm32bit >> 24)); + buffer.put((byte) (pcm32bit >> 16)); + return; + case C.ENCODING_PCM_FLOAT: + if (pcm32bit < 0) { + buffer.putFloat(-((float) pcm32bit) / Integer.MIN_VALUE); + } else { + buffer.putFloat((float) pcm32bit / Integer.MAX_VALUE); + } + return; + default: + throw new IllegalStateException(); + } + } + + private PcmAudioUtil() {} +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/PcmAudioUtilTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/PcmAudioUtilTest.java new file mode 100644 index 0000000000..eb0c092c80 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/PcmAudioUtilTest.java @@ -0,0 +1,500 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.audio; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link PcmAudioUtil}. */ +@RunWith(AndroidJUnit4.class) +public final class PcmAudioUtilTest { + + @Test + public void readAs32BitIntPcm_read8Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(5); + buffer.put(hexToBytes("80" + "AB" + "00" + "12" + "7F")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_8BIT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_8BIT)).isEqualTo(0xAB000000); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_8BIT)).isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_8BIT)).isEqualTo(0x12000000); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_8BIT)) + .isWithin(0xFFFFFF) + .of(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_read16Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(10); + buffer.put(hexToBytes("0080" + "CDAB" + "0000" + "3412" + "FF7F")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT)).isEqualTo(0xABCD0000); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT)).isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT)).isEqualTo(0x12340000); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_read16BitBigEndian_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(10); + buffer.put(hexToBytes("8000" + "ABCD" + "0000" + "1234" + "7FFF")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT_BIG_ENDIAN)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT_BIG_ENDIAN)) + .isEqualTo(0xABCD0000); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT_BIG_ENDIAN)) + .isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT_BIG_ENDIAN)) + .isEqualTo(0x12340000); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_16BIT_BIG_ENDIAN)) + .isWithin(0xFFFF) + .of(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_read24Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(15); + buffer.put(hexToBytes("000080" + "EFCDAB" + "000000" + "563412" + "FFFF7F")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT)).isEqualTo(0xABCDEF00); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT)).isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT)).isEqualTo(0x12345600); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT)) + .isWithin(0xFF) + .of(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_read24BitBigEndian_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(15); + buffer.put(hexToBytes("800000" + "ABCDEF" + "000000" + "123456" + "7FFFFF")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT_BIG_ENDIAN)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT_BIG_ENDIAN)) + .isEqualTo(0xABCDEF00); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT_BIG_ENDIAN)) + .isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT_BIG_ENDIAN)) + .isEqualTo(0x12345600); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_24BIT_BIG_ENDIAN)) + .isWithin(0xFF) + .of(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_read32Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(20); + buffer.put(hexToBytes("00000080" + "12EFCDAB" + "00000000" + "78563412" + "FFFFFF7F")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT)).isEqualTo(0xABCDEF12); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT)).isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT)).isEqualTo(0x12345678); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT)) + .isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_read32BitBigEndian_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(20); + buffer.put(hexToBytes("80000000" + "ABCDEF12" + "00000000" + "12345678" + "7FFFFFFF")); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT_BIG_ENDIAN)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT_BIG_ENDIAN)) + .isEqualTo(0xABCDEF12); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT_BIG_ENDIAN)) + .isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT_BIG_ENDIAN)) + .isEqualTo(0x12345678); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_32BIT_BIG_ENDIAN)) + .isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void readAs32BitIntPcm_readFloat_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(40); + buffer.putFloat(Float.NEGATIVE_INFINITY); + buffer.putFloat(-2f); + buffer.putFloat(-1f); + buffer.putFloat(-0.5f); + buffer.putFloat(0f); + buffer.putFloat(0.5f); + buffer.putFloat(1f); + buffer.putFloat(2f); + buffer.putFloat(Float.POSITIVE_INFINITY); + buffer.putFloat(Float.NaN); + buffer.flip(); + + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isEqualTo(Integer.MIN_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isWithin(1) + .of(Integer.MIN_VALUE / 2); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)).isEqualTo(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isWithin(1) + .of(Integer.MAX_VALUE / 2); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isEqualTo(Integer.MAX_VALUE); + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isEqualTo(Integer.MAX_VALUE); + // Just make sure we don't crash for NaN. + assertThat(PcmAudioUtil.readAs32BitIntPcm(buffer, C.ENCODING_PCM_FLOAT)) + .isAnyOf(0, Integer.MAX_VALUE, Integer.MIN_VALUE); + } + + @Test + public void write32BitIntPcm_write8Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(5); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_8BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_8BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_8BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_8BIT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_8BIT); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)).isEqualTo("80" + "AB" + "00" + "12" + "7F"); + } + + @Test + public void write32BitIntPcm_write16Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(10); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_16BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_16BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_16BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_16BIT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_16BIT); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)).isEqualTo("0080" + "CDAB" + "0000" + "3412" + "FF7F"); + } + + @Test + public void write32BitIntPcm_write16BitBigEndian_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(10); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_16BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_16BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_16BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_16BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_16BIT_BIG_ENDIAN); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)).isEqualTo("8000" + "ABCD" + "0000" + "1234" + "7FFF"); + } + + @Test + public void write32BitIntPcm_write24Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(15); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_24BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_24BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_24BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_24BIT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_24BIT); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)) + .isEqualTo("000080" + "EFCDAB" + "000000" + "563412" + "FFFF7F"); + } + + @Test + public void write32BitIntPcm_write24BitBigEndian_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(15); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_24BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_24BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_24BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_24BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_24BIT_BIG_ENDIAN); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)) + .isEqualTo("800000" + "ABCDEF" + "000000" + "123456" + "7FFFFF"); + } + + @Test + public void write32BitIntPcm_write32Bit_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(20); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_32BIT); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)) + .isEqualTo("00000080" + "12EFCDAB" + "00000000" + "78563412" + "FFFFFF7F"); + } + + @Test + public void write32BitIntPcm_write32BitBigEndian_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(20); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_32BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0xABCDEF12, C.ENCODING_PCM_32BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_32BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, 0x12345678, C.ENCODING_PCM_32BIT_BIG_ENDIAN); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_32BIT_BIG_ENDIAN); + buffer.flip(); + + assertThat(byteBufferToHex(buffer)) + .isEqualTo("80000000" + "ABCDEF12" + "00000000" + "12345678" + "7FFFFFFF"); + } + + @Test + public void write32BitIntPcm_writeFloat_returnsExpectedValues() { + ByteBuffer buffer = ByteBuffer.allocate(20); + + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE, C.ENCODING_PCM_FLOAT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MIN_VALUE / 2, C.ENCODING_PCM_FLOAT); + PcmAudioUtil.write32BitIntPcm(buffer, 0, C.ENCODING_PCM_FLOAT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE / 2, C.ENCODING_PCM_FLOAT); + PcmAudioUtil.write32BitIntPcm(buffer, Integer.MAX_VALUE, C.ENCODING_PCM_FLOAT); + buffer.flip(); + + assertThat(buffer.getFloat()).isEqualTo(-1f); + assertThat(buffer.getFloat()).isEqualTo(-0.5f); + assertThat(buffer.getFloat()).isEqualTo(0f); + assertThat(buffer.getFloat()).isEqualTo(0.5f); + assertThat(buffer.getFloat()).isEqualTo(1f); + } + + @Test + public void rampUpVolume_fromZeroFullRamp_returnsCorrectlyScaledValues() { + ByteBuffer buffer = ByteBuffer.allocate(48); + // Intentionally use large values to check for overflows. + int a = Integer.MAX_VALUE; + int b = Integer.MIN_VALUE; + for (int i = 0; i < 6; i++) { + PcmAudioUtil.write32BitIntPcm(buffer, a, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, b, C.ENCODING_PCM_32BIT); + } + buffer.flip(); + + ByteBuffer output = + PcmAudioUtil.rampUpVolume( + buffer, + C.ENCODING_PCM_32BIT, + /* pcmFrameSize= */ 8, + /* startFrameIndex= */ 0, + /* rampFrameCount= */ 4); + + ImmutableList.Builder outputValues = ImmutableList.builder(); + while (output.hasRemaining()) { + outputValues.add(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_32BIT)); + } + int threeQuartersA = (int) ((long) a * 3 / 4); + int threeQuartersB = (int) ((long) b * 3 / 4); + assertThat(outputValues.build()) + .containsExactly( + 0, 0, a / 4, b / 4, a / 2, b / 2, threeQuartersA, threeQuartersB, a, b, a, b) + .inOrder(); + } + + @Test + public void rampUpVolume_fromNonZeroFullRamp_returnsCorrectlyScaledValues() { + ByteBuffer buffer = ByteBuffer.allocate(32); + // Intentionally use large values to check for overflows. + int a = Integer.MAX_VALUE; + int b = Integer.MIN_VALUE; + for (int i = 0; i < 4; i++) { + PcmAudioUtil.write32BitIntPcm(buffer, a, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, b, C.ENCODING_PCM_32BIT); + } + buffer.flip(); + + ByteBuffer output = + PcmAudioUtil.rampUpVolume( + buffer, + C.ENCODING_PCM_32BIT, + /* pcmFrameSize= */ 8, + /* startFrameIndex= */ 2, + /* rampFrameCount= */ 4); + + ImmutableList.Builder outputValues = ImmutableList.builder(); + while (output.hasRemaining()) { + outputValues.add(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_32BIT)); + } + int threeQuartersA = (int) ((long) a * 3 / 4); + int threeQuartersB = (int) ((long) b * 3 / 4); + assertThat(outputValues.build()) + .containsExactly(a / 2, b / 2, threeQuartersA, threeQuartersB, a, b, a, b) + .inOrder(); + } + + @Test + public void rampUpVolume_fromZeroPartialRamp_returnsCorrectlyScaledValues() { + ByteBuffer buffer = ByteBuffer.allocate(24); + // Intentionally use large values to check for overflows. + int a = Integer.MAX_VALUE; + int b = Integer.MIN_VALUE; + for (int i = 0; i < 3; i++) { + PcmAudioUtil.write32BitIntPcm(buffer, a, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, b, C.ENCODING_PCM_32BIT); + } + buffer.flip(); + + ByteBuffer output = + PcmAudioUtil.rampUpVolume( + buffer, + C.ENCODING_PCM_32BIT, + /* pcmFrameSize= */ 8, + /* startFrameIndex= */ 0, + /* rampFrameCount= */ 4); + + ImmutableList.Builder outputValues = ImmutableList.builder(); + while (output.hasRemaining()) { + outputValues.add(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_32BIT)); + } + assertThat(outputValues.build()).containsExactly(0, 0, a / 4, b / 4, a / 2, b / 2).inOrder(); + } + + @Test + public void rampUpVolume_fromNonZeroPartialRamp_returnsCorrectlyScaledValues() { + ByteBuffer buffer = ByteBuffer.allocate(48); + // Intentionally use large values to check for overflows. + int a = Integer.MAX_VALUE; + int b = Integer.MIN_VALUE; + for (int i = 0; i < 2; i++) { + PcmAudioUtil.write32BitIntPcm(buffer, a, C.ENCODING_PCM_32BIT); + PcmAudioUtil.write32BitIntPcm(buffer, b, C.ENCODING_PCM_32BIT); + } + buffer.flip(); + + ByteBuffer output = + PcmAudioUtil.rampUpVolume( + buffer, + C.ENCODING_PCM_32BIT, + /* pcmFrameSize= */ 8, + /* startFrameIndex= */ 1, + /* rampFrameCount= */ 4); + + ImmutableList.Builder outputValues = ImmutableList.builder(); + while (output.hasRemaining()) { + outputValues.add(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_32BIT)); + } + assertThat(outputValues.build()).containsExactly(a / 4, b / 4, a / 2, b / 2).inOrder(); + } + + @Test + public void rampUpVolume_non32Bit_returnsCorrectlyScaledValues() { + ByteBuffer buffer = ByteBuffer.allocate(48); + // Intentionally use large values to check for overflows. + int a = Integer.MAX_VALUE; + int b = Integer.MIN_VALUE; + for (int i = 0; i < 6; i++) { + PcmAudioUtil.write32BitIntPcm(buffer, a, C.ENCODING_PCM_16BIT); + PcmAudioUtil.write32BitIntPcm(buffer, b, C.ENCODING_PCM_16BIT); + } + buffer.flip(); + + ByteBuffer output = + PcmAudioUtil.rampUpVolume( + buffer, + C.ENCODING_PCM_16BIT, + /* pcmFrameSize= */ 4, + /* startFrameIndex= */ 0, + /* rampFrameCount= */ 4); + + int threeQuartersA = (int) ((long) a * 3 / 4); + int threeQuartersB = (int) ((long) b * 3 / 4); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)).isWithin(0xFFFF).of(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)).isWithin(0xFFFF).of(0); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(a / 4); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(b / 4); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(a / 2); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(b / 2); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(threeQuartersA); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)) + .isWithin(0xFFFF) + .of(threeQuartersB); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)).isWithin(0xFFFF).of(a); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)).isWithin(0xFFFF).of(b); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)).isWithin(0xFFFF).of(a); + assertThat(PcmAudioUtil.readAs32BitIntPcm(output, C.ENCODING_PCM_16BIT)).isWithin(0xFFFF).of(b); + } + + private byte[] hexToBytes(String hexString) { + byte[] bytes = new BigInteger(hexString, 16).toByteArray(); + // Remove or add leading zeros to match the expected length. + int expectedLength = hexString.length() / 2; + if (bytes.length > expectedLength) { + bytes = Arrays.copyOfRange(bytes, 1, bytes.length); + } else if (bytes.length < expectedLength) { + byte[] newBytes = new byte[expectedLength]; + System.arraycopy( + bytes, + /* srcPos= */ 0, + newBytes, + /* destPos= */ expectedLength - bytes.length, + bytes.length); + bytes = newBytes; + } + return bytes; + } + + private static String byteBufferToHex(ByteBuffer buffer) { + StringBuilder hexString = new StringBuilder(); + while (buffer.hasRemaining()) { + hexString.append(String.format("%02X", buffer.get())); + } + return hexString.toString(); + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/EndToEndGaplessTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/EndToEndGaplessTest.java index 14c161c57d..1429ea97ea 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/EndToEndGaplessTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/EndToEndGaplessTest.java @@ -119,8 +119,12 @@ public class EndToEndGaplessTest { // Track two is only trimmed at its beginning, but not its end. Arrays.copyOfRange( decoderOutputBytes, bytesPerAudioFile + delayBytes, decoderOutputBytes.length)); - byte[] audioTrackReceivedBytes = audioTrackListener.getAllReceivedBytes(); + + // The first few bytes can be modified to ramp up the volume. Exclude those from the comparison. + Arrays.fill(expectedTrimmedByteContent, 0, 2000, (byte) 0); + Arrays.fill(audioTrackReceivedBytes, 0, 2000, (byte) 0); + assertThat(audioTrackReceivedBytes).isEqualTo(expectedTrimmedByteContent); }