blob: 9de3bd26691f546b100f74cd87902d154eebaae0 [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.media.cts;
import android.annotation.RawRes;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTimestamp;
import android.media.AudioTrack;
import android.util.Log;
import com.android.compatibility.common.util.CtsAndroidTestCase;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Random;
// Test the Java AudioTrack surround sound and HDMI passthrough.
// Most tests involve creating a track with a given format and then playing
// a few seconds of audio. The playback is verified by measuring the output
// sample rate based on the AudioTimestamps.
public class AudioTrackSurroundTest extends CtsAndroidTestCase {
private static final String TAG = "AudioTrackSurroundTest";
private static final double MAX_RATE_TOLERANCE_FRACTION = 0.01;
private static final boolean LOG_TIMESTAMPS = false; // set true for debugging
// Set this true to prefer the device that supports the particular encoding.
// But note that as of 3/25/2016, a bug causes Direct tracks to fail.
// So only set true when debugging that problem.
private static final boolean USE_PREFERRED_DEVICE = false;
// Should we fail if there is no PCM16 device reported by device enumeration?
// This can happen if, for example, an ATV set top box does not have its HDMI cable plugged in.
private static final boolean REQUIRE_PCM_DEVICE = false;
private final static long NANOS_PER_MILLISECOND = 1000000L;
private final static int MILLIS_PER_SECOND = 1000;
private final static long NANOS_PER_SECOND = NANOS_PER_MILLISECOND * MILLIS_PER_SECOND;
private final static int RES_AC3_VOICE_48000 = R.raw.voice12_48k_128kbps_15s_ac3;
private static int mLastPlayedEncoding = AudioFormat.ENCODING_INVALID;
// Devices that support various encodings.
private static boolean mDeviceScanComplete = false;
private static AudioDeviceInfo mInfoPCM16 = null;
private static AudioDeviceInfo mInfoAC3 = null;
private static AudioDeviceInfo mInfoE_AC3 = null;
private static AudioDeviceInfo mInfoDTS = null;
private static AudioDeviceInfo mInfoDTS_HD = null;
private static AudioDeviceInfo mInfoIEC61937 = null;
private static void log(String testName, String message) {
Log.i(TAG, "[" + testName + "] " + message);
}
private static void logw(String testName, String message) {
Log.w(TAG, "[" + testName + "] " + message);
}
private static void loge(String testName, String message) {
Log.e(TAG, "[" + testName + "] " + message);
}
// This is a special method that is called automatically before each test.
@Override
protected void setUp() throws Exception {
// Note that I tried to only scan for encodings once but the static
// data did not persist properly. That may be a bug.
// For now, just scan before every test.
scanDevicesForEncodings();
}
private void scanDevicesForEncodings() throws Exception {
final String MTAG = "scanDevicesForEncodings";
// Scan devices to see which encodings are supported.
AudioManager audioManager = (AudioManager) getContext()
.getSystemService(Context.AUDIO_SERVICE);
AudioDeviceInfo[] infos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo info : infos) {
log(MTAG, "scanning devices, name = " + info.getProductName()
+ ", id = " + info.getId()
+ ", " + (info.isSink() ? "sink" : "source")
+ ", type = " + info.getType()
+ " ------");
String text = "{";
for (int encoding : info.getEncodings()) {
text += String.format("0x%08X, ", encoding);
}
text += "}";
log(MTAG, " encodings = " + text);
text = "{";
for (int rate : info.getSampleRates()) {
text += rate + ", ";
}
text += "}";
log(MTAG, " sample rates = " + text);
if (info.isSink()) {
for (int encoding : info.getEncodings()) {
switch (encoding) {
case AudioFormat.ENCODING_PCM_16BIT:
mInfoPCM16 = info;
log(MTAG, "mInfoPCM16 set to " + info);
break;
case AudioFormat.ENCODING_AC3:
mInfoAC3 = info;
log(MTAG, "mInfoAC3 set to " + info);
break;
case AudioFormat.ENCODING_E_AC3:
mInfoE_AC3 = info;
log(MTAG, "mInfoE_AC3 set to " + info);
break;
case AudioFormat.ENCODING_DTS:
mInfoDTS = info;
log(MTAG, "mInfoDTS set to " + info);
break;
case AudioFormat.ENCODING_DTS_HD:
mInfoDTS_HD = info;
log(MTAG, "mInfoDTS_HD set to " + info);
break;
case AudioFormat.ENCODING_IEC61937:
mInfoIEC61937 = info;
log(MTAG, "mInfoIEC61937 set to " + info);
break;
default:
// This is OK. It is just an encoding that we don't care about.
break;
}
}
}
}
}
// Load a resource into a byte[]
private byte[] loadRawResourceBytes(@RawRes int id) throws Exception {
AssetFileDescriptor masterFd = getContext().getResources().openRawResourceFd(id);
long masterLength = masterFd.getLength();
byte[] masterBuffer = new byte[(int) masterLength];
InputStream is = masterFd.createInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
int result = bis.read(masterBuffer);
bis.close();
masterFd.close();
return masterBuffer;
}
// Load a resource into a short[]
private short[] loadRawResourceShorts(@RawRes int id) throws Exception {
AssetFileDescriptor masterFd = getContext().getResources().openRawResourceFd(id);
long masterLength = masterFd.getLength();
short[] masterBuffer = new short[(int) (masterLength / 2)];
InputStream is = masterFd.createInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
for (int i = 0; i < masterBuffer.length; i++) {
int lo = bis.read(); // assume Little Endian
int hi = bis.read();
masterBuffer[i] = (short) (hi * 256 + lo);
}
bis.close();
masterFd.close();
return masterBuffer;
}
public void testLoadSineSweep() throws Exception {
final String TEST_NAME = "testLoadSineSweep";
short[] shortData = loadRawResourceShorts(R.raw.sinesweepraw);
assertTrue(TEST_NAME + ": load sinesweepraw as shorts", shortData.length > 100);
byte[] byteData = loadRawResourceBytes(R.raw.sinesweepraw);
assertTrue(TEST_NAME + ": load sinesweepraw as bytes", byteData.length > shortData.length);
}
private static AudioTrack createAudioTrack(int sampleRate, int encoding, int channelConfig) {
final String TEST_NAME = "createAudioTrack";
int minBufferSize = AudioTrack.getMinBufferSize(
sampleRate, channelConfig,
encoding);
assertTrue(TEST_NAME + ": getMinBufferSize", minBufferSize > 0);
int bufferSize = minBufferSize * 3; // plenty big
AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, channelConfig,
encoding, bufferSize,
AudioTrack.MODE_STREAM);
return track;
}
static class TimestampAnalyzer {
ArrayList<AudioTimestamp> mTimestamps = new ArrayList<AudioTimestamp>();
AudioTimestamp mPreviousTimestamp = null;
static String timestampToString(AudioTimestamp timestamp) {
if (timestamp == null)
return "null";
return "(pos = " + timestamp.framePosition + ", nanos = " + timestamp.nanoTime + ")";
}
// Add timestamp if unique and valid.
void addTimestamp(AudioTrack track) {
AudioTimestamp timestamp = new AudioTimestamp();
boolean gotTimestamp = track.getTimestamp(timestamp);
if (gotTimestamp) {
// Only save timestamps after the data is flowing.
if (mPreviousTimestamp != null
&& timestamp.framePosition > 0
&& (timestamp.nanoTime != mPreviousTimestamp.nanoTime
|| timestamp.framePosition != mPreviousTimestamp.framePosition)) {
mTimestamps.add(timestamp);
}
mPreviousTimestamp = timestamp;
}
}
void checkIndividualTimestamps(int sampleRate) {
AudioTimestamp previous = null;
double sumDeltaSquared = 0.0;
int populationSize = 0;
double maxDeltaMillis = 0.0;
// Make sure the timestamps are smooth and don't go retrograde.
for (AudioTimestamp timestamp : mTimestamps) {
if (previous != null) {
assertTrue("framePosition must be monotonic",
timestamp.framePosition >= previous.framePosition);
assertTrue("nanoTime must be monotonic",
timestamp.nanoTime >= previous.nanoTime);
if (timestamp.framePosition > previous.framePosition) {
// Measure timing jitter.
// Calculate predicted duration based on measured rate and compare
// it with actual duration.
final double TOLERANCE_MILLIS = 2.0;
long elapsedFrames = timestamp.framePosition - previous.framePosition;
long elapsedNanos = timestamp.nanoTime - previous.nanoTime;
double measuredMillis = elapsedNanos / (double) NANOS_PER_MILLISECOND;
double expectedMillis = elapsedFrames * (double) MILLIS_PER_SECOND
/ sampleRate;
double deltaMillis = measuredMillis - expectedMillis;
sumDeltaSquared += deltaMillis * deltaMillis;
populationSize++;
// We only issue a warning here because the CDD does not mandate a
// specific tolerance.
double absDeltaMillis = Math.abs(deltaMillis);
if (absDeltaMillis > TOLERANCE_MILLIS) {
Log.w(TAG, "measured time exceeds expected"
+ ", srate = " + sampleRate
+ ", frame = " + timestamp.framePosition
+ ", expected = " + expectedMillis
+ ", measured = " + measuredMillis + " (msec)"
);
}
if (absDeltaMillis > maxDeltaMillis) {
maxDeltaMillis = absDeltaMillis;
}
}
}
previous = timestamp;
}
Log.d(TAG, "max abs(delta) from expected duration = " + maxDeltaMillis + " msec");
if (populationSize > 0) {
double deviation = Math.sqrt(sumDeltaSquared / populationSize);
Log.d(TAG, "standard deviation from expected duration = " + deviation + " msec");
}
}
// Use collected timestamps to estimate a sample rate.
double estimateSampleRate() {
assertTrue("expect many timestamps, got " + mTimestamps.size(),
mTimestamps.size() > 10);
// Use first and last timestamp to get the most accurate rate.
AudioTimestamp first = mTimestamps.get(0);
AudioTimestamp last = mTimestamps.get(mTimestamps.size() - 1);
return calculateSampleRate(first, last);
}
/**
* @param timestamp1
* @param timestamp2
*/
private double calculateSampleRate(AudioTimestamp timestamp1, AudioTimestamp timestamp2) {
long elapsedFrames = timestamp2.framePosition - timestamp1.framePosition;
long elapsedNanos = timestamp2.nanoTime - timestamp1.nanoTime;
double measuredRate = elapsedFrames * (double) NANOS_PER_SECOND / elapsedNanos;
if (LOG_TIMESTAMPS) {
Log.i(TAG, "calculateSampleRate(), elapsedFrames =, " + elapsedFrames
+ ", measuredRate =, "
+ (int) measuredRate);
}
return measuredRate;
}
}
// Class for looping a recording for several seconds and measuring the sample rate.
// This is not static because it needs to call getContext().
abstract class SamplePlayerBase {
private final int mSampleRate;
private final int mEncoding;
private final int mChannelConfig;
private int mBlockSize = 512;
protected int mOffset = 0;
protected AudioTrack mTrack;
private final TimestampAnalyzer mTimestampAnalyzer = new TimestampAnalyzer();
SamplePlayerBase(int sampleRate, int encoding, int channelConfig) {
mSampleRate = sampleRate;
mEncoding = encoding;
mChannelConfig = channelConfig;
}
// Use abstract write to handle byte[] or short[] data.
protected abstract int writeBlock(int numSamples);
private int primeBuffer() {
// Will not block when track is stopped.
return writeBlock(Integer.MAX_VALUE);
}
// Add a warning to the assert message that might help folks figure out why their
// PCM test is failing.
private String getPcmWarning() {
return (mInfoPCM16 == null && AudioFormat.isEncodingLinearPcm(mEncoding))
? " (No PCM device!)" : "";
}
/**
* Use a device that we know supports the current encoding.
*/
private void usePreferredDevice() {
AudioDeviceInfo info = null;
switch (mEncoding) {
case AudioFormat.ENCODING_PCM_16BIT:
info = mInfoPCM16;
break;
case AudioFormat.ENCODING_AC3:
info = mInfoAC3;
break;
case AudioFormat.ENCODING_E_AC3:
info = mInfoE_AC3;
break;
case AudioFormat.ENCODING_DTS:
info = mInfoDTS;
break;
case AudioFormat.ENCODING_DTS_HD:
info = mInfoDTS_HD;
break;
case AudioFormat.ENCODING_IEC61937:
info = mInfoIEC61937;
break;
default:
break;
}
if (info != null) {
log(TAG, "track.setPreferredDevice(" + info + ")");
mTrack.setPreferredDevice(info);
}
}
public void playAndMeasureRate() throws Exception {
final String TEST_NAME = "playAndMeasureRate";
final long TEST_DURATION_MILLIS = 5000; // just long enough to measure the rate
if (mLastPlayedEncoding == AudioFormat.ENCODING_INVALID ||
!AudioFormat.isEncodingLinearPcm(mEncoding) ||
!AudioFormat.isEncodingLinearPcm(mLastPlayedEncoding)) {
Log.d(TAG, "switching from format: " + mLastPlayedEncoding
+ " to: " + mEncoding
+ " requires sleep");
// Switching between compressed formats may require
// some time for the HAL to adjust and give proper timing.
// One second should be ok, but we use 2 just in case.
Thread.sleep(2000 /* millis */);
}
mLastPlayedEncoding = mEncoding;
log(TEST_NAME, String.format("test using rate = %d, encoding = 0x%08x",
mSampleRate, mEncoding));
// Create a track and prime it.
mTrack = createAudioTrack(mSampleRate, mEncoding, mChannelConfig);
try {
assertEquals(TEST_NAME + ": track created" + getPcmWarning(),
AudioTrack.STATE_INITIALIZED,
mTrack.getState());
if (USE_PREFERRED_DEVICE) {
usePreferredDevice();
}
int bytesWritten = 0;
mOffset = primeBuffer(); // prime the buffer
assertTrue(TEST_NAME + ": priming offset = " + mOffset + getPcmWarning(),
mOffset > 0);
bytesWritten += mOffset;
// Play for a while.
mTrack.play();
log(TEST_NAME, "native rate = "
+ mTrack.getNativeOutputSampleRate(mTrack.getStreamType()));
long elapsedMillis = 0;
long startTime = System.currentTimeMillis();
while (elapsedMillis < TEST_DURATION_MILLIS) {
writeBlock(mBlockSize);
elapsedMillis = System.currentTimeMillis() - startTime;
mTimestampAnalyzer.addTimestamp(mTrack);
}
// Did we underrun? Allow 0 or 1 because there is sometimes
// an underrun on startup.
int underrunCount1 = mTrack.getUnderrunCount();
assertTrue(TEST_NAME + ": too many underruns, got underrunCount1" + getPcmWarning(),
underrunCount1 < 2);
// Estimate the sample rate and compare it with expected.
double estimatedRate = mTimestampAnalyzer.estimateSampleRate();
Log.d(TAG, "measured sample rate = " + estimatedRate);
assertEquals(TEST_NAME + ": measured sample rate" + getPcmWarning(),
mSampleRate, estimatedRate, mSampleRate * MAX_RATE_TOLERANCE_FRACTION);
// Check for jitter or retrograde motion in each timestamp.
mTimestampAnalyzer.checkIndividualTimestamps(mSampleRate);
} finally {
mTrack.release();
}
}
}
// Create player for short[]
class SamplePlayerShorts extends SamplePlayerBase {
private final short[] mData;
SamplePlayerShorts(int sampleRate, int encoding, int channelConfig) {
super(sampleRate, encoding, channelConfig);
mData = new short[64 * 1024];
// Fill with noise. We should not hear the noise for IEC61937.
int amplitude = 8000;
Random random = new Random();
for (int i = 0; i < mData.length; i++) {
mData[i] = (short)(random.nextInt(amplitude) - (amplitude / 2));
}
}
SamplePlayerShorts(int sampleRate, int encoding, int channelConfig, @RawRes int resourceId)
throws Exception {
super(sampleRate, encoding, channelConfig);
mData = loadRawResourceShorts(resourceId);
assertTrue("SamplePlayerShorts: load resource file as shorts", mData.length > 0);
}
@Override
protected int writeBlock(int numShorts) {
int result = 0;
int shortsToWrite = numShorts;
int shortsLeft = mData.length - mOffset;
if (shortsToWrite > shortsLeft) {
shortsToWrite = shortsLeft;
}
if (shortsToWrite > 0) {
result = mTrack.write(mData, mOffset, shortsToWrite);
mOffset += result;
} else {
mOffset = 0; // rewind
}
return result;
}
}
// Create player for byte[]
class SamplePlayerBytes extends SamplePlayerBase {
private final byte[] mData;
SamplePlayerBytes(int sampleRate, int encoding, int channelConfig) {
super(sampleRate, encoding, channelConfig);
mData = new byte[128 * 1024];
}
SamplePlayerBytes(int sampleRate, int encoding, int channelConfig, @RawRes int resourceId)
throws Exception {
super(sampleRate, encoding, channelConfig);
mData = loadRawResourceBytes(resourceId);
assertTrue("SamplePlayerBytes: load resource file as bytes", mData.length > 0);
}
@Override
protected int writeBlock(int numBytes) {
int result = 0;
int bytesToWrite = numBytes;
int bytesLeft = mData.length - mOffset;
if (bytesToWrite > bytesLeft) {
bytesToWrite = bytesLeft;
}
if (bytesToWrite > 0) {
result = mTrack.write(mData, mOffset, bytesToWrite);
mOffset += result;
} else {
mOffset = 0; // rewind
}
return result;
}
}
public void testPlayAC3Bytes() throws Exception {
if (mInfoAC3 != null) {
SamplePlayerBytes player = new SamplePlayerBytes(
48000, AudioFormat.ENCODING_AC3, AudioFormat.CHANNEL_OUT_STEREO,
RES_AC3_VOICE_48000);
player.playAndMeasureRate();
}
}
public void testPlayAC3Shorts() throws Exception {
if (mInfoAC3 != null) {
SamplePlayerShorts player = new SamplePlayerShorts(
48000, AudioFormat.ENCODING_AC3, AudioFormat.CHANNEL_OUT_STEREO,
RES_AC3_VOICE_48000);
player.playAndMeasureRate();
}
}
// Note that for testing IEC61937, the Audio framework does not look at the
// wrapped data. It just passes it through over HDMI. See we can just use
// zeros instead of real data.
public void testPlayIEC61937_32000() throws Exception {
if (mInfoIEC61937 != null) {
SamplePlayerShorts player = new SamplePlayerShorts(
32000, AudioFormat.ENCODING_IEC61937, AudioFormat.CHANNEL_OUT_STEREO);
player.playAndMeasureRate();
}
}
public void testPlayIEC61937_44100() throws Exception {
if (mInfoIEC61937 != null) {
SamplePlayerShorts player = new SamplePlayerShorts(
44100, AudioFormat.ENCODING_IEC61937, AudioFormat.CHANNEL_OUT_STEREO);
player.playAndMeasureRate();
}
}
public void testPlayIEC61937_48000() throws Exception {
if (mInfoIEC61937 != null) {
SamplePlayerShorts player = new SamplePlayerShorts(
48000, AudioFormat.ENCODING_IEC61937, AudioFormat.CHANNEL_OUT_STEREO);
player.playAndMeasureRate();
}
}
public void testIEC61937_Errors() throws Exception {
if (mInfoIEC61937 != null) {
final String TEST_NAME = "testIEC61937_Errors";
try {
AudioTrack track = createAudioTrack(48000, AudioFormat.ENCODING_IEC61937,
AudioFormat.CHANNEL_OUT_MONO);
assertTrue(TEST_NAME + ": IEC61937 track creation should fail for mono", false);
} catch (IllegalArgumentException e) {
// This is expected behavior.
}
try {
AudioTrack track = createAudioTrack(48000, AudioFormat.ENCODING_IEC61937,
AudioFormat.CHANNEL_OUT_5POINT1);
assertTrue(TEST_NAME + ": IEC61937 track creation should fail for 5.1", false);
} catch (IllegalArgumentException e) {
// This is expected behavior.
}
}
}
public void testPcmSupport() throws Exception {
if (REQUIRE_PCM_DEVICE) {
// There should always be a dummy PCM device available.
assertTrue("testPcmSupport: PCM should be supported."
+ " On ATV device please check HDMI connection.",
mInfoPCM16 != null);
}
}
private boolean isPcmTestingEnabled() {
return (mInfoPCM16 != null || !REQUIRE_PCM_DEVICE);
}
public void testPlaySineSweepShorts() throws Exception {
if (isPcmTestingEnabled()) {
SamplePlayerShorts player = new SamplePlayerShorts(
44100, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.CHANNEL_OUT_STEREO,
R.raw.sinesweepraw);
player.playAndMeasureRate();
}
}
public void testPlaySineSweepBytes() throws Exception {
if (isPcmTestingEnabled()) {
SamplePlayerBytes player = new SamplePlayerBytes(
44100, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.CHANNEL_OUT_STEREO,
R.raw.sinesweepraw);
player.playAndMeasureRate();
}
}
public void testPlaySineSweepBytes48000() throws Exception {
if (isPcmTestingEnabled()) {
SamplePlayerBytes player = new SamplePlayerBytes(
48000, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.CHANNEL_OUT_STEREO,
R.raw.sinesweepraw);
player.playAndMeasureRate();
}
}
public void testPlaySineSweepShortsMono() throws Exception {
if (isPcmTestingEnabled()) {
SamplePlayerShorts player = new SamplePlayerShorts(44100, AudioFormat.ENCODING_PCM_16BIT,
AudioFormat.CHANNEL_OUT_MONO,
R.raw.sinesweepraw);
player.playAndMeasureRate();
}
}
public void testPlaySineSweepBytesMono()
throws Exception {
if (isPcmTestingEnabled()) {
SamplePlayerBytes player = new SamplePlayerBytes(44100,
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.CHANNEL_OUT_MONO, R.raw.sinesweepraw);
player.playAndMeasureRate();
}
}
}