mirror of
https://github.com/androidx/media.git
synced 2025-05-04 14:10:40 +08:00
Add tests to validate FLAC decoder output
PiperOrigin-RevId: 289091494
This commit is contained in:
parent
f4271f55bc
commit
ca11e56fe6
@ -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
|
BIN
extensions/flac/src/androidTest/assets/bear-flac-24bit.mka
Normal file
BIN
extensions/flac/src/androidTest/assets/bear-flac-24bit.mka
Normal file
Binary file not shown.
@ -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
|
@ -25,9 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Player;
|
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.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -37,7 +41,8 @@ import org.junit.runner.RunWith;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class FlacPlaybackTest {
|
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
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
@ -47,38 +52,56 @@ public class FlacPlaybackTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBasicPlayback() throws Exception {
|
public void test16BitPlayback() throws Exception {
|
||||||
playUri(BEAR_FLAC_URI);
|
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 =
|
TestPlaybackRunnable testPlaybackRunnable =
|
||||||
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
|
new TestPlaybackRunnable(
|
||||||
|
Uri.parse("asset:///" + fileName),
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
audioSink);
|
||||||
Thread thread = new Thread(testPlaybackRunnable);
|
Thread thread = new Thread(testPlaybackRunnable);
|
||||||
thread.start();
|
thread.start();
|
||||||
thread.join();
|
thread.join();
|
||||||
if (testPlaybackRunnable.playbackException != null) {
|
if (testPlaybackRunnable.playbackException != null) {
|
||||||
throw testPlaybackRunnable.playbackException;
|
throw testPlaybackRunnable.playbackException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audioSink.assertOutput(
|
||||||
|
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
private final AudioSink audioSink;
|
||||||
|
|
||||||
private ExoPlayer player;
|
private ExoPlayer player;
|
||||||
private ExoPlaybackException playbackException;
|
private ExoPlaybackException playbackException;
|
||||||
|
|
||||||
public TestPlaybackRunnable(Uri uri, Context context) {
|
public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.audioSink = audioSink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Looper.prepare();
|
Looper.prepare();
|
||||||
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
|
LibflacAudioRenderer audioRenderer =
|
||||||
|
new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink);
|
||||||
player = new ExoPlayer.Builder(context, audioRenderer).build();
|
player = new ExoPlayer.Builder(context, audioRenderer).build();
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
@ -105,5 +128,4 @@ public class FlacPlaybackTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C;
|
|||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
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.audio.SimpleDecoderAudioRenderer;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
@ -55,6 +56,24 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||||||
super(eventHandler, eventListener, audioProcessors);
|
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
|
@Override
|
||||||
@FormatSupport
|
@FormatSupport
|
||||||
protected int supportsFormatInternal(
|
protected int supportsFormatInternal(
|
||||||
@ -68,8 +87,8 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||||||
if (format.initializationData.isEmpty()) {
|
if (format.initializationData.isEmpty()) {
|
||||||
// The initialization data might not be set if the format was obtained from a manifest (e.g.
|
// 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
|
// 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
|
// 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).
|
// long as the AudioSink supports it, which will always be true when using DefaultAudioSink.
|
||||||
pcmEncoding = C.ENCODING_PCM_16BIT;
|
pcmEncoding = C.ENCODING_PCM_16BIT;
|
||||||
} else {
|
} else {
|
||||||
int streamMetadataOffset =
|
int streamMetadataOffset =
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user