Ramp up volume after AudioTrack flush to avoid pop sound

AudioTrack doesn't automatically ramp up the volume after a flush
(only when resuming with play after a pause), which causes audible
pop sounds in most cases. The issue can be avoided by manually
applying a short 20ms volume ramp, the same duration used by the
platform for the automatic volume ramping where available.

Together with the already submitted 6147050b90, this fixes the
unwanted pop sounds for most cases in the desired way. It only
leaves two cases that are not handled perfectly:
 - If the media file itself contains a volume ramp at the beginning,
   we wouldn't need this additional ramping. Given the extremely
   short duration, this seems ignorable and we can treat it as a
   future feature request to mark the beginning of media in a special
   way that can then disable the volume ramping.
 - For seamless period transitions where we keep using the same
   AudioTrack, we may still get a pop sound at the transition. To
   solve this, we'd need a dedicated audio processor to either ramp
   the end of media down and the beginning of the next item up, or
   apply a very short cross-fade. Either way, we need new signalling
   to identify cases where the media originates from the same source
   and this effect should not be applied (e.g. when re-concatenating
   clipped audio snippets from the same file).

PiperOrigin-RevId: 676860234
This commit is contained in:
tonihei 2024-09-20 08:53:03 -07:00 committed by Copybara-Service
parent e887614246
commit 5e3dcea1bf
5 changed files with 692 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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() {}
}

View File

@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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();
}
}

View File

@ -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);
}