| /* |
| * Copyright (C) 2009 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 java.util.ArrayList; |
| |
| import android.cts.util.CtsAndroidTestCase; |
| import android.media.AudioFormat; |
| import android.media.AudioManager; |
| import android.media.AudioTrack; |
| import android.media.AudioTrack.OnPlaybackPositionUpdateListener; |
| import android.media.cts.AudioHelper; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Log; |
| import com.android.cts.util.ReportLog; |
| import com.android.cts.util.ResultType; |
| import com.android.cts.util.ResultUnit; |
| |
| public class AudioTrack_ListenerTest extends CtsAndroidTestCase { |
| private final static String TAG = "AudioTrack_ListenerTest"; |
| private final static int TEST_SR = 11025; |
| private final static int TEST_CONF = AudioFormat.CHANNEL_OUT_MONO; |
| private final static int TEST_FORMAT = AudioFormat.ENCODING_PCM_8BIT; |
| private final static int TEST_STREAM_TYPE = AudioManager.STREAM_MUSIC; |
| private final static int TEST_LOOP_FACTOR = 2; // # loops (>= 1) for static tracks |
| // simulated for streaming. |
| private final static int TEST_BUFFER_FACTOR = 25; |
| private boolean mIsHandleMessageCalled; |
| private int mMarkerPeriodInFrames; |
| private int mMarkerPosition; |
| private int mFrameCount; |
| private Handler mHandler = new Handler(Looper.getMainLooper()) { |
| @Override |
| public void handleMessage(Message msg) { |
| mIsHandleMessageCalled = true; |
| super.handleMessage(msg); |
| } |
| }; |
| |
| public void testAudioTrackCallback() throws Exception { |
| doTest("Streaming Local Looper", true /*localTrack*/, false /*customHandler*/, |
| 30 /*periodsPerSecond*/, 2 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STREAM); |
| } |
| |
| public void testAudioTrackCallbackWithHandler() throws Exception { |
| // with 100 periods per second, trigger back-to-back notifications. |
| doTest("Streaming Private Handler", false /*localTrack*/, true /*customHandler*/, |
| 100 /*periodsPerSecond*/, 10 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STREAM); |
| // verify mHandler is used only for accessing its associated Looper |
| assertFalse(mIsHandleMessageCalled); |
| } |
| |
| public void testStaticAudioTrackCallback() throws Exception { |
| doTest("Static", false /*localTrack*/, false /*customHandler*/, |
| 100 /*periodsPerSecond*/, 10 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STATIC); |
| } |
| |
| public void testStaticAudioTrackCallbackWithHandler() throws Exception { |
| doTest("Static Private Handler", false /*localTrack*/, true /*customHandler*/, |
| 30 /*periodsPerSecond*/, 2 /*markerPeriodsPerSecond*/, AudioTrack.MODE_STATIC); |
| // verify mHandler is used only for accessing its associated Looper |
| assertFalse(mIsHandleMessageCalled); |
| } |
| |
| private void doTest(String reportName, boolean localTrack, boolean customHandler, |
| int periodsPerSecond, int markerPeriodsPerSecond, final int mode) throws Exception { |
| mIsHandleMessageCalled = false; |
| final int minBuffSize = AudioTrack.getMinBufferSize(TEST_SR, TEST_CONF, TEST_FORMAT); |
| final int bufferSizeInBytes; |
| if (mode == AudioTrack.MODE_STATIC && TEST_LOOP_FACTOR > 1) { |
| // use setLoopPoints for static mode |
| bufferSizeInBytes = minBuffSize * TEST_BUFFER_FACTOR; |
| mFrameCount = bufferSizeInBytes * TEST_LOOP_FACTOR; |
| } else { |
| bufferSizeInBytes = minBuffSize * TEST_BUFFER_FACTOR * TEST_LOOP_FACTOR; |
| mFrameCount = bufferSizeInBytes; |
| } |
| |
| final AudioTrack track; |
| final AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioTrack> makeSomething; |
| if (localTrack) { |
| makeSomething = null; |
| track = new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, |
| TEST_FORMAT, bufferSizeInBytes, mode); |
| } else { |
| makeSomething = |
| new AudioHelper.MakeSomethingAsynchronouslyAndLoop<AudioTrack>( |
| new AudioHelper.MakesSomething<AudioTrack>() { |
| @Override |
| public AudioTrack makeSomething() { |
| return new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, |
| TEST_FORMAT, bufferSizeInBytes, mode); |
| } |
| } |
| ); |
| // create audiotrack on different thread's looper. |
| track = makeSomething.make(); |
| } |
| final MockOnPlaybackPositionUpdateListener listener; |
| if (customHandler) { |
| listener = new MockOnPlaybackPositionUpdateListener(track, mHandler); |
| } else { |
| listener = new MockOnPlaybackPositionUpdateListener(track); |
| } |
| |
| byte[] vai = AudioHelper.createSoundDataInByteArray( |
| bufferSizeInBytes, TEST_SR, 1024 /* frequency */, 0 /* sweep */); |
| int markerPeriods = Math.max(3, mFrameCount * markerPeriodsPerSecond / TEST_SR); |
| mMarkerPeriodInFrames = mFrameCount / markerPeriods; |
| markerPeriods = mFrameCount / mMarkerPeriodInFrames; // recalculate due to round-down |
| mMarkerPosition = mMarkerPeriodInFrames; |
| |
| // check that we can get and set notification marker position |
| assertEquals(0, track.getNotificationMarkerPosition()); |
| assertEquals(AudioTrack.SUCCESS, |
| track.setNotificationMarkerPosition(mMarkerPosition)); |
| assertEquals(mMarkerPosition, track.getNotificationMarkerPosition()); |
| |
| int updatePeriods = Math.max(3, mFrameCount * periodsPerSecond / TEST_SR); |
| final int updatePeriodInFrames = mFrameCount / updatePeriods; |
| updatePeriods = mFrameCount / updatePeriodInFrames; // recalculate due to round-down |
| |
| // we set the notification period before running for better period positional accuracy. |
| // check that we can get and set notification periods |
| assertEquals(0, track.getPositionNotificationPeriod()); |
| assertEquals(AudioTrack.SUCCESS, |
| track.setPositionNotificationPeriod(updatePeriodInFrames)); |
| assertEquals(updatePeriodInFrames, track.getPositionNotificationPeriod()); |
| |
| if (mode == AudioTrack.MODE_STATIC && TEST_LOOP_FACTOR > 1) { |
| track.setLoopPoints(0, vai.length, TEST_LOOP_FACTOR - 1); |
| } |
| // write data with single blocking write, then play. |
| assertEquals(vai.length, track.write(vai, 0 /* offsetInBytes */, vai.length)); |
| track.play(); |
| |
| // sleep until track completes playback - it must complete within 1 second |
| // of the expected length otherwise the periodic test should fail. |
| final int numChannels = AudioFormat.channelCountFromOutChannelMask(TEST_CONF); |
| final int bytesPerSample = AudioFormat.getBytesPerSample(TEST_FORMAT); |
| final int bytesPerFrame = numChannels * bytesPerSample; |
| final int trackLengthMs = (int)((double)mFrameCount * 1000 / TEST_SR / bytesPerFrame); |
| Thread.sleep(trackLengthMs + 1000); |
| |
| // stop listening - we should be done. |
| listener.stop(); |
| |
| // Beware: stop() resets the playback head position for both static and streaming |
| // audio tracks, so stop() cannot be called while we're still logging playback |
| // head positions. We could recycle the track after stop(), which isn't done here. |
| track.stop(); |
| |
| // clean up |
| if (makeSomething != null) { |
| makeSomething.join(); |
| } |
| listener.release(); |
| track.release(); |
| |
| // collect statistics |
| final ArrayList<Integer> markerList = listener.getMarkerList(); |
| final ArrayList<Integer> periodicList = listener.getPeriodicList(); |
| // verify count of markers and periodic notifications. |
| assertEquals(markerPeriods, markerList.size()); |
| assertEquals(updatePeriods, periodicList.size()); |
| // verify actual playback head positions returned. |
| // the max diff should really be around 24 ms, |
| // but system load and stability will affect this test; |
| // we use 80ms limit here for failure. |
| final int tolerance80MsInFrames = TEST_SR * 80 / 1000; |
| |
| AudioHelper.Statistics markerStat = new AudioHelper.Statistics(); |
| for (int i = 0; i < markerPeriods; ++i) { |
| final int expected = mMarkerPeriodInFrames * (i + 1); |
| final int actual = markerList.get(i); |
| // Log.d(TAG, "Marker: expected(" + expected + ") actual(" + actual |
| // + ") diff(" + (actual - expected) + ")"); |
| assertEquals(expected, actual, tolerance80MsInFrames); |
| markerStat.add((double)(actual - expected) * 1000 / TEST_SR); |
| } |
| |
| AudioHelper.Statistics periodicStat = new AudioHelper.Statistics(); |
| for (int i = 0; i < updatePeriods; ++i) { |
| final int expected = updatePeriodInFrames * (i + 1); |
| final int actual = periodicList.get(i); |
| // Log.d(TAG, "Update: expected(" + expected + ") actual(" + actual |
| // + ") diff(" + (actual - expected) + ")"); |
| assertEquals(expected, actual, tolerance80MsInFrames); |
| periodicStat.add((double)(actual - expected) * 1000 / TEST_SR); |
| } |
| |
| // report this |
| ReportLog log = getReportLog(); |
| log.printValue(reportName + ": Average Marker diff", markerStat.getAvg(), |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| log.printValue(reportName + ": Maximum Marker abs diff", markerStat.getMaxAbs(), |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| log.printValue(reportName + ": Average Marker abs diff", markerStat.getAvgAbs(), |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| log.printValue(reportName + ": Average Periodic diff", periodicStat.getAvg(), |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| log.printValue(reportName + ": Maximum Periodic abs diff", periodicStat.getMaxAbs(), |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| log.printValue(reportName + ": Average Periodic abs diff", periodicStat.getAvgAbs(), |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| log.printSummary(reportName + ": Unified abs diff", |
| (periodicStat.getAvgAbs() + markerStat.getAvgAbs()) / 2, |
| ResultType.LOWER_BETTER, ResultUnit.MS); |
| } |
| |
| private class MockOnPlaybackPositionUpdateListener |
| implements OnPlaybackPositionUpdateListener { |
| public MockOnPlaybackPositionUpdateListener(AudioTrack track) { |
| mAudioTrack = track; |
| track.setPlaybackPositionUpdateListener(this); |
| } |
| |
| public MockOnPlaybackPositionUpdateListener(AudioTrack track, Handler handler) { |
| mAudioTrack = track; |
| track.setPlaybackPositionUpdateListener(this, handler); |
| } |
| |
| public synchronized void onMarkerReached(AudioTrack track) { |
| if (mIsTestActive) { |
| int position = mAudioTrack.getPlaybackHeadPosition(); |
| mOnMarkerReachedCalled.add(position); |
| mMarkerPosition += mMarkerPeriodInFrames; |
| if (mMarkerPosition <= mFrameCount) { |
| assertEquals(AudioTrack.SUCCESS, |
| mAudioTrack.setNotificationMarkerPosition(mMarkerPosition)); |
| } |
| } else { |
| fail("onMarkerReached called when not active"); |
| } |
| } |
| |
| public synchronized void onPeriodicNotification(AudioTrack track) { |
| if (mIsTestActive) { |
| mOnPeriodicNotificationCalled.add(mAudioTrack.getPlaybackHeadPosition()); |
| } else { |
| fail("onPeriodicNotification called when not active"); |
| } |
| } |
| |
| public synchronized void stop() { |
| mIsTestActive = false; |
| } |
| |
| public ArrayList<Integer> getMarkerList() { |
| return mOnMarkerReachedCalled; |
| } |
| |
| public ArrayList<Integer> getPeriodicList() { |
| return mOnPeriodicNotificationCalled; |
| } |
| |
| public synchronized void release() { |
| mAudioTrack.setPlaybackPositionUpdateListener(null); |
| mAudioTrack = null; |
| } |
| |
| private boolean mIsTestActive = true; |
| private AudioTrack mAudioTrack; |
| private ArrayList<Integer> mOnMarkerReachedCalled = new ArrayList<Integer>(); |
| private ArrayList<Integer> mOnPeriodicNotificationCalled = new ArrayList<Integer>(); |
| } |
| } |