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