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