Add tests to validate FLAC decoder output

PiperOrigin-RevId: 289091494
This commit is contained in:
olly 2020-01-10 15:48:09 +00:00 committed by Oliver Woodman
parent f4271f55bc
commit ca11e56fe6
8 changed files with 582 additions and 10 deletions

View File

@ -0,0 +1,91 @@
config:
encoding = 2 (16 bit)
channel count = 2
sample rate = 48000
buffer:
time = 1000
data = 1217833679
buffer:
time = 97000
data = 558614672
buffer:
time = 193000
data = -709714787
buffer:
time = 289000
data = 1367870571
buffer:
time = 385000
data = -141229457
buffer:
time = 481000
data = 1287758361
buffer:
time = 577000
data = 1125289147
buffer:
time = 673000
data = -1677383475
buffer:
time = 769000
data = 2130742861
buffer:
time = 865000
data = -1292320253
buffer:
time = 961000
data = -456587163
buffer:
time = 1057000
data = 748981534
buffer:
time = 1153000
data = 1550456016
buffer:
time = 1249000
data = 1657906039
buffer:
time = 1345000
data = -762677083
buffer:
time = 1441000
data = -1343810763
buffer:
time = 1537000
data = 1137318783
buffer:
time = 1633000
data = -1891318229
buffer:
time = 1729000
data = -472068495
buffer:
time = 1825000
data = 832315001
buffer:
time = 1921000
data = 2054935175
buffer:
time = 2017000
data = 57921641
buffer:
time = 2113000
data = 2132759067
buffer:
time = 2209000
data = -1742540521
buffer:
time = 2305000
data = 1657024301
buffer:
time = 2401000
data = -585080145
buffer:
time = 2497000
data = 427271397
buffer:
time = 2593000
data = -364201340
buffer:
time = 2689000
data = -627965287

View File

@ -0,0 +1,91 @@
config:
encoding = 536870912 (24 bit)
channel count = 2
sample rate = 48000
buffer:
time = 0
data = 225023649
buffer:
time = 96000
data = 455106306
buffer:
time = 192000
data = 2025727297
buffer:
time = 288000
data = 758514657
buffer:
time = 384000
data = 1044986473
buffer:
time = 480000
data = -2030029695
buffer:
time = 576000
data = 1907053281
buffer:
time = 672000
data = -1974954431
buffer:
time = 768000
data = -206248383
buffer:
time = 864000
data = 1484984417
buffer:
time = 960000
data = -1306117439
buffer:
time = 1056000
data = 692829792
buffer:
time = 1152000
data = 1070563058
buffer:
time = 1248000
data = -1444096479
buffer:
time = 1344000
data = 1753016419
buffer:
time = 1440000
data = 1947797953
buffer:
time = 1536000
data = 266121411
buffer:
time = 1632000
data = 1275494369
buffer:
time = 1728000
data = 372077825
buffer:
time = 1824000
data = -993079679
buffer:
time = 1920000
data = 177307937
buffer:
time = 2016000
data = 2037083009
buffer:
time = 2112000
data = -435776287
buffer:
time = 2208000
data = 1867447329
buffer:
time = 2304000
data = 1884495937
buffer:
time = 2400000
data = -804673375
buffer:
time = 2496000
data = -588531007
buffer:
time = 2592000
data = -1064642970
buffer:
time = 2688000
data = -1771406207

View File

@ -25,9 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
@ -37,7 +41,8 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class FlacPlaybackTest {
private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka";
private static final String BEAR_FLAC_16BIT = "bear-flac-16bit.mka";
private static final String BEAR_FLAC_24BIT = "bear-flac-24bit.mka";
@Before
public void setUp() {
@ -47,38 +52,56 @@ public class FlacPlaybackTest {
}
@Test
public void testBasicPlayback() throws Exception {
playUri(BEAR_FLAC_URI);
public void test16BitPlayback() throws Exception {
playAndAssertAudioSinkInput(BEAR_FLAC_16BIT);
}
private void playUri(String uri) throws Exception {
@Test
public void test24BitPlayback() throws Exception {
playAndAssertAudioSinkInput(BEAR_FLAC_24BIT);
}
private static void playAndAssertAudioSinkInput(String fileName) throws Exception {
CapturingAudioSink audioSink =
new CapturingAudioSink(
new DefaultAudioSink(/* audioCapabilities= */ null, new AudioProcessor[0]));
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
new TestPlaybackRunnable(
Uri.parse("asset:///" + fileName),
ApplicationProvider.getApplicationContext(),
audioSink);
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
if (testPlaybackRunnable.playbackException != null) {
throw testPlaybackRunnable.playbackException;
}
audioSink.assertOutput(
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
}
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
private final AudioSink audioSink;
private ExoPlayer player;
private ExoPlaybackException playbackException;
public TestPlaybackRunnable(Uri uri, Context context) {
public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
this.uri = uri;
this.context = context;
this.audioSink = audioSink;
}
@Override
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
LibflacAudioRenderer audioRenderer =
new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink);
player = new ExoPlayer.Builder(context, audioRenderer).build();
player.addListener(this);
MediaSource mediaSource =
@ -105,5 +128,4 @@ public class FlacPlaybackTest {
}
}
}
}

View File

@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
@ -55,6 +56,24 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
super(eventHandler, eventListener, audioProcessors);
}
/**
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioSink The sink to which audio will be output.
*/
public LibflacAudioRenderer(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink) {
super(
eventHandler,
eventListener,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
audioSink);
}
@Override
@FormatSupport
protected int supportsFormatInternal(
@ -68,8 +87,8 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (format.initializationData.isEmpty()) {
// The initialization data might not be set if the format was obtained from a manifest (e.g.
// for DASH playbacks) rather than directly from the media. In this case we assume
// ENCODING_PCM_16BIT. If the actual encoding is different, playback will still succeed as
// long as the AudioSink supports it (which will always be true when using DefaultAudioSink).
// ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as
// long as the AudioSink supports it, which will always be true when using DefaultAudioSink.
pcmEncoding = C.ENCODING_PCM_16BIT;
} else {
int streamMetadataOffset =

View File

@ -0,0 +1,151 @@
/*
* Copyright (C) 2020 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 com.google.android.exoplayer2.audio;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlaybackParameters;
import java.nio.ByteBuffer;
/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */
public class ForwardingAudioSink implements AudioSink {
private final AudioSink sink;
public ForwardingAudioSink(AudioSink sink) {
this.sink = sink;
}
@Override
public void setListener(Listener listener) {
sink.setListener(listener);
}
@Override
public boolean supportsOutput(int channelCount, int encoding) {
return sink.supportsOutput(channelCount, encoding);
}
@Override
public long getCurrentPositionUs(boolean sourceEnded) {
return sink.getCurrentPositionUs(sourceEnded);
}
@Override
public void configure(
int inputEncoding,
int inputChannelCount,
int inputSampleRate,
int specifiedBufferSize,
@Nullable int[] outputChannels,
int trimStartFrames,
int trimEndFrames)
throws ConfigurationException {
sink.configure(
inputEncoding,
inputChannelCount,
inputSampleRate,
specifiedBufferSize,
outputChannels,
trimStartFrames,
trimEndFrames);
}
@Override
public void play() {
sink.play();
}
@Override
public void handleDiscontinuity() {
sink.handleDiscontinuity();
}
@Override
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
throws InitializationException, WriteException {
return sink.handleBuffer(buffer, presentationTimeUs);
}
@Override
public void playToEndOfStream() throws WriteException {
sink.playToEndOfStream();
}
@Override
public boolean isEnded() {
return sink.isEnded();
}
@Override
public boolean hasPendingData() {
return sink.hasPendingData();
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
sink.setPlaybackParameters(playbackParameters);
}
@Override
public PlaybackParameters getPlaybackParameters() {
return sink.getPlaybackParameters();
}
@Override
public void setAudioAttributes(AudioAttributes audioAttributes) {
sink.setAudioAttributes(audioAttributes);
}
@Override
public void setAudioSessionId(int audioSessionId) {
sink.setAudioSessionId(audioSessionId);
}
@Override
public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
sink.setAuxEffectInfo(auxEffectInfo);
}
@Override
public void enableTunnelingV21(int tunnelingAudioSessionId) {
sink.enableTunnelingV21(tunnelingAudioSessionId);
}
@Override
public void disableTunneling() {
sink.disableTunneling();
}
@Override
public void setVolume(float volume) {
sink.setVolume(volume);
}
@Override
public void pause() {
sink.pause();
}
@Override
public void flush() {
sink.flush();
}
@Override
public void reset() {
sink.reset();
}
}

View File

@ -0,0 +1,198 @@
/*
* Copyright (C) 2020 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 com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertWithMessage;
import android.content.Context;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.ForwardingAudioSink;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** A {@link ForwardingAudioSink} that captures configuration, discontinuity and buffer events. */
public final class CapturingAudioSink extends ForwardingAudioSink implements Dumper.Dumpable {
/**
* If true, makes {@link #assertOutput(Context, String)} method write the output to a file, rather
* than validating that the output matches the dump file.
*
* <p>The output file is written to the test apk's external storage directory, which is typically:
* {@code /sdcard/Android/data/${package-under-test}.test/files/}.
*/
private static final boolean WRITE_DUMP = false;
private final List<Dumper.Dumpable> interceptedData;
@Nullable private ByteBuffer currentBuffer;
public CapturingAudioSink(AudioSink sink) {
super(sink);
interceptedData = new ArrayList<>();
}
@Override
public void configure(
int inputEncoding,
int inputChannelCount,
int inputSampleRate,
int specifiedBufferSize,
@Nullable int[] outputChannels,
int trimStartFrames,
int trimEndFrames)
throws ConfigurationException {
interceptedData.add(
new DumpableConfiguration(inputEncoding, inputChannelCount, inputSampleRate));
super.configure(
inputEncoding,
inputChannelCount,
inputSampleRate,
specifiedBufferSize,
outputChannels,
trimStartFrames,
trimEndFrames);
}
@Override
public void handleDiscontinuity() {
interceptedData.add(new DumpableDiscontinuity());
super.handleDiscontinuity();
}
@Override
@SuppressWarnings("ReferenceEquality")
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
throws InitializationException, WriteException {
// handleBuffer is called repeatedly with the same buffer until it's been fully consumed by the
// sink. We only want to dump each buffer once, and we need to do so before the sink being
// forwarded to has a chance to modify its position.
if (buffer != currentBuffer) {
interceptedData.add(new DumpableBuffer(buffer, presentationTimeUs));
currentBuffer = buffer;
}
boolean fullyConsumed = super.handleBuffer(buffer, presentationTimeUs);
if (fullyConsumed) {
currentBuffer = null;
}
return fullyConsumed;
}
@Override
public void flush() {
currentBuffer = null;
super.flush();
}
@Override
public void reset() {
currentBuffer = null;
super.reset();
}
/**
* Asserts that dump of this sink is equal to expected dump which is read from {@code dumpFile}.
*
* <p>If assertion fails because of an intended change in the output or a new dump file needs to
* be created, set {@link #WRITE_DUMP} flag to true and run the test again. Instead of assertion,
* actual dump will be written to {@code dumpFile}. This new dump file needs to be copied to the
* project, {@code library/src/androidTest/assets} folder manually.
*/
public void assertOutput(Context context, String dumpFile) throws IOException {
String actual = new Dumper().add(this).toString();
if (WRITE_DUMP) {
File directory = context.getExternalFilesDir(null);
File file = new File(directory, dumpFile);
file.getParentFile().mkdirs();
PrintWriter out = new PrintWriter(file);
out.print(actual);
out.close();
} else {
String expected = TestUtil.getString(context, dumpFile);
assertWithMessage(dumpFile).that(actual).isEqualTo(expected);
}
}
@Override
public void dump(Dumper dumper) {
for (int i = 0; i < interceptedData.size(); i++) {
interceptedData.get(i).dump(dumper);
}
}
private static final class DumpableConfiguration implements Dumper.Dumpable {
private final int inputEncoding;
private final int inputChannelCount;
private final int inputSampleRate;
public DumpableConfiguration(int inputEncoding, int inputChannelCount, int inputSampleRate) {
this.inputEncoding = inputEncoding;
this.inputChannelCount = inputChannelCount;
this.inputSampleRate = inputSampleRate;
}
@Override
public void dump(Dumper dumper) {
int bitDepth = (Util.getPcmFrameSize(inputEncoding, /* channelCount= */ 1) * 8);
dumper
.startBlock("config")
.add("encoding", inputEncoding + " (" + bitDepth + " bit)")
.add("channel count", inputChannelCount)
.add("sample rate", inputSampleRate)
.endBlock();
}
}
private static final class DumpableBuffer implements Dumper.Dumpable {
private final long presentationTimeUs;
private final int dataHashcode;
public DumpableBuffer(ByteBuffer buffer, long presentationTimeUs) {
this.presentationTimeUs = presentationTimeUs;
// Compute a hash of the buffer data without changing its position.
int initialPosition = buffer.position();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
buffer.position(initialPosition);
this.dataHashcode = Arrays.hashCode(data);
}
@Override
public void dump(Dumper dumper) {
dumper
.startBlock("buffer")
.add("time", presentationTimeUs)
.add("data", dataHashcode)
.endBlock();
}
}
private static final class DumpableDiscontinuity implements Dumper.Dumpable {
@Override
public void dump(Dumper dumper) {
dumper.startBlock("discontinuity").endBlock();
}
}
}