1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media.audio.cts; 18 19 import android.content.pm.PackageManager; 20 import android.media.AudioFormat; 21 import android.media.AudioManager; 22 import android.media.AudioTrack; 23 import android.media.AudioTrack.OnPlaybackPositionUpdateListener; 24 import android.media.cts.AudioHelper; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.platform.test.annotations.AppModeSdkSandbox; 29 30 import com.android.compatibility.common.util.CtsAndroidTestCase; 31 import com.android.compatibility.common.util.DeviceReportLog; 32 import com.android.compatibility.common.util.NonMainlineTest; 33 import com.android.compatibility.common.util.ResultType; 34 import com.android.compatibility.common.util.ResultUnit; 35 36 import java.util.ArrayList; 37 38 @NonMainlineTest 39 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 40 public class AudioTrack_ListenerTest extends CtsAndroidTestCase { 41 private final static String TAG = "AudioTrack_ListenerTest"; 42 private static final String REPORT_LOG_NAME = "CtsMediaAudioTestCases"; 43 private final static int TEST_SR = 11025; 44 private final static int TEST_CONF = AudioFormat.CHANNEL_OUT_MONO; 45 private final static int TEST_FORMAT = AudioFormat.ENCODING_PCM_8BIT; 46 private final static int TEST_STREAM_TYPE = AudioManager.STREAM_MUSIC; 47 private final static int TEST_LOOP_FACTOR = 2; // # loops (>= 1) for static tracks 48 // simulated for streaming. 49 private final static int TEST_BUFFER_FACTOR = 25; 50 private boolean mIsHandleMessageCalled; 51 private int mMarkerPeriodInFrames; 52 private int mMarkerPosition; 53 private int mFrameCount; 54 private Handler mHandler = new Handler(Looper.getMainLooper()) { 55 @Override 56 public void handleMessage(Message msg) { 57 mIsHandleMessageCalled = true; 58 super.handleMessage(msg); 59 } 60 }; 61 isLowLatencyDevice()62 private boolean isLowLatencyDevice() { 63 return getContext().getPackageManager() 64 .hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY); 65 } 66 testAudioTrackCallback()67 public void testAudioTrackCallback() throws Exception { 68 doTest("streaming_local_looper", true /*localTrack*/, false /*customHandler*/, 69 30 /*periodsPerSecond*/, 2 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STREAM); 70 } 71 testAudioTrackCallbackWithHandler()72 public void testAudioTrackCallbackWithHandler() throws Exception { 73 // with 100 periods per second, trigger back-to-back notifications. 74 doTest("streaming_private_handler", false /*localTrack*/, true /*customHandler*/, 75 100 /*periodsPerSecond*/, 10 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STREAM); 76 // verify mHandler is used only for accessing its associated Looper 77 assertFalse(mIsHandleMessageCalled); 78 } 79 testStaticAudioTrackCallback()80 public void testStaticAudioTrackCallback() throws Exception { 81 doTest("static", false /*localTrack*/, false /*customHandler*/, 82 100 /*periodsPerSecond*/, 10 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STATIC); 83 } 84 testStaticAudioTrackCallbackWithHandler()85 public void testStaticAudioTrackCallbackWithHandler() throws Exception { 86 String streamName = "test_static_audio_track_callback_handler"; 87 doTest("static_private_handler", false /*localTrack*/, true /*customHandler*/, 88 30 /*periodsPerSecond*/, 2 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STATIC); 89 // verify mHandler is used only for accessing its associated Looper 90 assertFalse(mIsHandleMessageCalled); 91 } 92 doTest(String reportName, boolean localTrack, boolean customHandler, int periodsPerSecond, int markerPeriodsPerSecond, final int mode)93 private void doTest(String reportName, boolean localTrack, boolean customHandler, 94 int periodsPerSecond, int markerPeriodsPerSecond, final int mode) throws Exception { 95 mIsHandleMessageCalled = false; 96 final int minBuffSize = AudioTrack.getMinBufferSize(TEST_SR, TEST_CONF, TEST_FORMAT); 97 final int bufferSizeInBytes; 98 if (mode == AudioTrack.MODE_STATIC && TEST_LOOP_FACTOR > 1) { 99 // use setLoopPoints for static mode 100 bufferSizeInBytes = minBuffSize * TEST_BUFFER_FACTOR; 101 mFrameCount = bufferSizeInBytes * TEST_LOOP_FACTOR; 102 } else { 103 bufferSizeInBytes = minBuffSize * TEST_BUFFER_FACTOR * TEST_LOOP_FACTOR; 104 mFrameCount = bufferSizeInBytes; 105 } 106 107 final AudioTrack track; 108 final AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioTrack> makeSomething; 109 if (localTrack) { 110 makeSomething = null; 111 track = new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, 112 TEST_FORMAT, bufferSizeInBytes, mode); 113 } else { 114 makeSomething = 115 new AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioTrack>( 116 new AudioHelper.MakesSomething<AudioTrack>() { 117 @Override 118 public AudioTrack makeSomething() { 119 return new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, 120 TEST_FORMAT, bufferSizeInBytes, mode); 121 } 122 } 123 ); 124 // create audiotrack on different thread's looper. 125 track = makeSomething.make(); 126 } 127 final MockOnPlaybackPositionUpdateListener listener; 128 if (customHandler) { 129 listener = new MockOnPlaybackPositionUpdateListener(track, mHandler); 130 } else { 131 listener = new MockOnPlaybackPositionUpdateListener(track); 132 } 133 134 byte[] vai = AudioHelper.createSoundDataInByteArray( 135 bufferSizeInBytes, TEST_SR, 1024 /* frequency */, 0 /* sweep */); 136 int markerPeriods = Math.max(3, mFrameCount * markerPeriodsPerSecond / TEST_SR); 137 mMarkerPeriodInFrames = mFrameCount / markerPeriods; 138 markerPeriods = mFrameCount / mMarkerPeriodInFrames; // recalculate due to round-down 139 mMarkerPosition = mMarkerPeriodInFrames; 140 141 // check that we can get and set notification marker position 142 assertEquals(0, track.getNotificationMarkerPosition()); 143 assertEquals(AudioTrack.SUCCESS, 144 track.setNotificationMarkerPosition(mMarkerPosition)); 145 assertEquals(mMarkerPosition, track.getNotificationMarkerPosition()); 146 147 int updatePeriods = Math.max(3, mFrameCount * periodsPerSecond / TEST_SR); 148 final int updatePeriodInFrames = mFrameCount / updatePeriods; 149 updatePeriods = mFrameCount / updatePeriodInFrames; // recalculate due to round-down 150 151 // we set the notification period before running for better period positional accuracy. 152 // check that we can get and set notification periods 153 assertEquals(0, track.getPositionNotificationPeriod()); 154 assertEquals(AudioTrack.SUCCESS, 155 track.setPositionNotificationPeriod(updatePeriodInFrames)); 156 assertEquals(updatePeriodInFrames, track.getPositionNotificationPeriod()); 157 158 if (mode == AudioTrack.MODE_STATIC && TEST_LOOP_FACTOR > 1) { 159 track.setLoopPoints(0, vai.length, TEST_LOOP_FACTOR - 1); 160 } 161 // write data with single blocking write, then play. 162 assertEquals(vai.length, track.write(vai, 0 /* offsetInBytes */, vai.length)); 163 track.play(); 164 165 // sleep until track completes playback - it must complete within 1 second 166 // of the expected length otherwise the periodic test should fail. 167 final int numChannels = AudioFormat.channelCountFromOutChannelMask(TEST_CONF); 168 final int bytesPerSample = AudioFormat.getBytesPerSample(TEST_FORMAT); 169 final int bytesPerFrame = numChannels * bytesPerSample; 170 final int trackLengthMs = (int)((double)mFrameCount * 1000 / TEST_SR / bytesPerFrame); 171 Thread.sleep(trackLengthMs + 1000); 172 173 // stop listening - we should be done. 174 listener.stop(); 175 176 // Beware: stop() resets the playback head position for both static and streaming 177 // audio tracks, so stop() cannot be called while we're still logging playback 178 // head positions. We could recycle the track after stop(), which isn't done here. 179 track.stop(); 180 181 // clean up 182 if (makeSomething != null) { 183 makeSomething.join(); 184 } 185 listener.release(); 186 track.release(); 187 188 // collect statistics 189 final ArrayList<Integer> markerList = listener.getMarkerList(); 190 final ArrayList<Integer> periodicList = listener.getPeriodicList(); 191 // verify count of markers and periodic notifications. 192 assertEquals(markerPeriods, markerList.size()); 193 assertEquals(updatePeriods, periodicList.size()); 194 // verify actual playback head positions returned. 195 // the max diff should really be around 24 ms (on h/w implementations), 196 // but system load and stability will affect this test; 197 // we use 80ms or 100ms limit here for failure. 198 final int toleranceMs = isLowLatencyDevice() ? 80 : 100; 199 final int toleranceInFrames = toleranceMs * TEST_SR / 1000; 200 201 AudioHelper.Statistics markerStat = new AudioHelper.Statistics(); 202 for (int i = 0; i < markerPeriods; ++i) { 203 final int expected = mMarkerPeriodInFrames * (i + 1); 204 final int actual = markerList.get(i); 205 // Log.d(TAG, "Marker: expected(" + expected + ") actual(" + actual 206 // + ") diff(" + (actual - expected) + ")"); 207 assertEquals(expected, actual, toleranceInFrames); 208 markerStat.add((double)(actual - expected) * 1000 / TEST_SR); 209 } 210 211 AudioHelper.Statistics periodicStat = new AudioHelper.Statistics(); 212 for (int i = 0; i < updatePeriods; ++i) { 213 final int expected = updatePeriodInFrames * (i + 1); 214 final int actual = periodicList.get(i); 215 // Log.d(TAG, "Update: expected(" + expected + ") actual(" + actual 216 // + ") diff(" + (actual - expected) + ")"); 217 assertEquals(expected, actual, toleranceInFrames); 218 periodicStat.add((double)(actual - expected) * 1000 / TEST_SR); 219 } 220 221 // report this 222 DeviceReportLog log = new DeviceReportLog(REPORT_LOG_NAME, reportName); 223 log.addValue("average_marker_diff", markerStat.getAvg(), ResultType.LOWER_BETTER, 224 ResultUnit.MS); 225 log.addValue("maximum_marker_abs_diff", markerStat.getMaxAbs(), ResultType.LOWER_BETTER, 226 ResultUnit.MS); 227 log.addValue("average_marker_abs_diff", markerStat.getAvgAbs(), ResultType.LOWER_BETTER, 228 ResultUnit.MS); 229 log.addValue("average_periodic_diff", periodicStat.getAvg(), ResultType.LOWER_BETTER, 230 ResultUnit.MS); 231 log.addValue("maximum_periodic_abs_diff", periodicStat.getMaxAbs(), ResultType.LOWER_BETTER, 232 ResultUnit.MS); 233 log.addValue("average_periodic_abs_diff", periodicStat.getAvgAbs(), ResultType.LOWER_BETTER, 234 ResultUnit.MS); 235 log.setSummary("unified_abs_diff", (periodicStat.getAvgAbs() + markerStat.getAvgAbs()) / 2, 236 ResultType.LOWER_BETTER, ResultUnit.MS); 237 log.submit(getInstrumentation()); 238 } 239 240 private class MockOnPlaybackPositionUpdateListener 241 implements OnPlaybackPositionUpdateListener { MockOnPlaybackPositionUpdateListener(AudioTrack track)242 public MockOnPlaybackPositionUpdateListener(AudioTrack track) { 243 mAudioTrack = track; 244 track.setPlaybackPositionUpdateListener(this); 245 } 246 MockOnPlaybackPositionUpdateListener(AudioTrack track, Handler handler)247 public MockOnPlaybackPositionUpdateListener(AudioTrack track, Handler handler) { 248 mAudioTrack = track; 249 track.setPlaybackPositionUpdateListener(this, handler); 250 } 251 onMarkerReached(AudioTrack track)252 public synchronized void onMarkerReached(AudioTrack track) { 253 if (mIsTestActive) { 254 int position = mAudioTrack.getPlaybackHeadPosition(); 255 mOnMarkerReachedCalled.add(position); 256 mMarkerPosition += mMarkerPeriodInFrames; 257 if (mMarkerPosition <= mFrameCount) { 258 assertEquals(AudioTrack.SUCCESS, 259 mAudioTrack.setNotificationMarkerPosition(mMarkerPosition)); 260 } 261 } else { 262 fail("onMarkerReached called when not active"); 263 } 264 } 265 onPeriodicNotification(AudioTrack track)266 public synchronized void onPeriodicNotification(AudioTrack track) { 267 if (mIsTestActive) { 268 mOnPeriodicNotificationCalled.add(mAudioTrack.getPlaybackHeadPosition()); 269 } else { 270 fail("onPeriodicNotification called when not active"); 271 } 272 } 273 stop()274 public synchronized void stop() { 275 mIsTestActive = false; 276 } 277 getMarkerList()278 public ArrayList<Integer> getMarkerList() { 279 return mOnMarkerReachedCalled; 280 } 281 getPeriodicList()282 public ArrayList<Integer> getPeriodicList() { 283 return mOnPeriodicNotificationCalled; 284 } 285 release()286 public synchronized void release() { 287 mAudioTrack.setPlaybackPositionUpdateListener(null); 288 mAudioTrack = null; 289 } 290 291 private boolean mIsTestActive = true; 292 private AudioTrack mAudioTrack; 293 private ArrayList<Integer> mOnMarkerReachedCalled = new ArrayList<Integer>(); 294 private ArrayList<Integer> mOnPeriodicNotificationCalled = new ArrayList<Integer>(); 295 } 296 } 297