• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 com.android.cts.verifier.audio;
18 
19 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
20 import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix;
21 
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.media.AudioManager;
25 import android.media.AudioRecord;
26 import android.media.AudioTrack;
27 import android.media.MediaRecorder;
28 import android.media.audiofx.AcousticEchoCanceler;
29 import android.os.Bundle;
30 import android.util.Log;
31 import android.view.View;
32 import android.widget.Button;
33 import android.widget.LinearLayout;
34 import android.widget.ProgressBar;
35 import android.widget.TextView;
36 
37 import com.android.compatibility.common.util.ResultType;
38 import com.android.compatibility.common.util.ResultUnit;
39 import com.android.cts.verifier.CtsVerifierReportLog;
40 import com.android.cts.verifier.R;
41 import com.android.cts.verifier.audio.wavelib.DspBufferDouble;
42 import com.android.cts.verifier.audio.wavelib.DspBufferMath;
43 import com.android.cts.verifier.audio.wavelib.PipeShort;
44 
45 public class AudioAEC extends AudioFrequencyActivity implements View.OnClickListener {
46     private static final String TAG = "AudioAEC";
47 
48     private static final int TEST_NONE = -1;
49     private static final int TEST_AEC = 0;
50     private static final int TEST_COUNT = 1;
51     private static final float MAX_VAL = (float)(1 << 15);
52 
53     private static final int RESULT_CODE_OK = 0;
54     private static final int RESULT_CODE_FAILED = 1; // test ran but results were out of range
55     private static final int RESULT_CODE_NOT_RUN = 2;
56     private static final int RESULT_CODE_AEC_CREATION_EXCEPTION = 3;
57     private static final int RESULT_CODE_AEC_NULL = 4;
58     private static final int RESULT_CODE_AEC_DISABLED = 5; // not enabled by default
59 
60     private int mCurrentTest = TEST_NONE;
61     private LinearLayout mLinearLayout;
62     private Button mButtonTest;
63     private ProgressBar mProgress;
64     private TextView mResultText;
65     private SoundPlayerObject mSPlayer;
66     private SoundRecorderObject mSRecorder;
67     private AcousticEchoCanceler mAec;
68 
69     // These 4 values are written to the report log.
70     private boolean mDeviceHasAEC = AcousticEchoCanceler.isAvailable();
71     private int mResultCode = RESULT_CODE_OK;
72     private double mMaxAec;
73     private double mMaxNoAec;
74 
75     private final int mBlockSizeSamples = 4096;
76     private final int mSamplingRate = 48000;
77     private final int mSelectedRecordSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
78 
79     private final int TEST_DURATION_MS = 8000;
80     private final int SHOT_FREQUENCY_MS = 200;
81     private final int CORRELATION_DURATION_MS = TEST_DURATION_MS - 3000;
82     private final int SHOT_COUNT_CORRELATION = CORRELATION_DURATION_MS/SHOT_FREQUENCY_MS;
83     private final int SHOT_COUNT = TEST_DURATION_MS/SHOT_FREQUENCY_MS;
84     private final float MIN_RMS_DB = -60.0f; //dB
85     private final float MIN_RMS_VAL = (float)Math.pow(10,(MIN_RMS_DB/20));
86 
87     private final double TEST_THRESHOLD_AEC_ON = 0.5;
88     private final double TEST_THRESHOLD_AEC_OFF = 0.6;
89     private RmsHelper mRMSRecorder1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
90     private RmsHelper mRMSRecorder2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
91 
92     private RmsHelper mRMSPlayer1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
93     private RmsHelper mRMSPlayer2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
94 
95     private Thread mTestThread;
96 
97     //RMS helpers
98     public class RmsHelper {
99         private double mRmsCurrent;
100         public int mBlockSize;
101         private int mShoutCount;
102         public boolean mRunning = false;
103 
104         private short[] mAudioShortArray;
105 
106         private DspBufferDouble mRmsSnapshots;
107         private int mShotIndex;
108 
RmsHelper(int blockSize, int shotCount)109         public RmsHelper(int blockSize, int shotCount) {
110             mBlockSize = blockSize;
111             mShoutCount = shotCount;
112             reset();
113         }
114 
reset()115         public void reset() {
116             mAudioShortArray = new short[mBlockSize];
117             mRmsSnapshots = new DspBufferDouble(mShoutCount);
118             mShotIndex = 0;
119             mRmsCurrent = 0;
120             mRunning = false;
121         }
122 
captureShot()123         public void captureShot() {
124             if (mShotIndex >= 0 && mShotIndex < mRmsSnapshots.getSize()) {
125                 mRmsSnapshots.setValue(mShotIndex++, mRmsCurrent);
126             }
127         }
128 
setRunning(boolean running)129         public void setRunning(boolean running) {
130             mRunning = running;
131         }
132 
getRmsCurrent()133         public double getRmsCurrent() {
134             return mRmsCurrent;
135         }
136 
getRmsSnapshots()137         public DspBufferDouble getRmsSnapshots() {
138             return mRmsSnapshots;
139         }
140 
updateRms(PipeShort pipe, int channelCount, int channel)141         public boolean updateRms(PipeShort pipe, int channelCount, int channel) {
142             if (mRunning) {
143                 int samplesAvailable = pipe.availableToRead();
144                 while (samplesAvailable >= mBlockSize) {
145                     pipe.read(mAudioShortArray, 0, mBlockSize);
146 
147                     double rmsTempSum = 0;
148                     int count = 0;
149                     for (int i = channel; i < mBlockSize; i += channelCount) {
150                         float value = mAudioShortArray[i] / MAX_VAL;
151 
152                         rmsTempSum += value * value;
153                         count++;
154                     }
155                     float rms = count > 0 ? (float)Math.sqrt(rmsTempSum / count) : 0f;
156                     if (rms < MIN_RMS_VAL) {
157                         rms = MIN_RMS_VAL;
158                     }
159 
160                     double alpha = 0.9;
161                     double total_rms = rms * alpha + mRmsCurrent * (1.0f - alpha);
162                     mRmsCurrent = total_rms;
163 
164                     samplesAvailable = pipe.availableToRead();
165                 }
166                 return true;
167             }
168             return false;
169         }
170     }
171 
172     //compute Acoustic Coupling Factor
computeAcousticCouplingFactor(DspBufferDouble buffRmsPlayer, DspBufferDouble buffRmsRecorder, int firstShot, int lastShot)173     private double computeAcousticCouplingFactor(DspBufferDouble buffRmsPlayer,
174                                                  DspBufferDouble buffRmsRecorder,
175                                                  int firstShot, int lastShot) {
176         int len = Math.min(buffRmsPlayer.getSize(), buffRmsRecorder.getSize());
177 
178         firstShot = Math.min(firstShot, 0);
179         lastShot = Math.min(lastShot, len -1);
180 
181         int actualLen = lastShot - firstShot + 1;
182 
183         double maxValue = 0;
184         if (actualLen > 0) {
185             DspBufferDouble rmsPlayerdB = new DspBufferDouble(actualLen);
186             DspBufferDouble rmsRecorderdB = new DspBufferDouble(actualLen);
187             DspBufferDouble crossCorr = new DspBufferDouble(actualLen);
188 
189             for (int i = firstShot, index = 0; i <= lastShot; ++i, ++index) {
190                 double valPlayerdB = Math.max(20 * Math.log10(buffRmsPlayer.mData[i]), MIN_RMS_DB);
191                 rmsPlayerdB.setValue(index, valPlayerdB);
192                 double valRecorderdB = Math.max(20 * Math.log10(buffRmsRecorder.mData[i]),
193                         MIN_RMS_DB);
194                 rmsRecorderdB.setValue(index, valRecorderdB);
195             }
196 
197             //cross correlation...
198             if (DspBufferMath.crossCorrelation(crossCorr, rmsPlayerdB, rmsRecorderdB) !=
199                     DspBufferMath.MATH_RESULT_SUCCESS) {
200                 Log.v(TAG, "math error in cross correlation");
201             }
202 
203             for (int i = 0; i < len; i++) {
204                 if (Math.abs(crossCorr.mData[i]) > maxValue) {
205                     maxValue = Math.abs(crossCorr.mData[i]);
206                 }
207             }
208         }
209         return maxValue;
210     }
211 
212     @Override
onCreate(Bundle savedInstanceState)213     protected void onCreate(Bundle savedInstanceState) {
214         super.onCreate(savedInstanceState);
215         setContentView(R.layout.audio_aec_activity);
216 
217         mLinearLayout = (LinearLayout)findViewById(R.id.audio_aec_test_layout);
218         enableUILayout(mLinearLayout, false);
219 
220         // Test
221         mButtonTest = (Button) findViewById(R.id.audio_aec_button_test);
222         mButtonTest.setOnClickListener(this);
223         mProgress = (ProgressBar) findViewById(R.id.audio_aec_test_progress_bar);
224         mResultText = (TextView) findViewById(R.id.audio_aec_test_result);
225 
226         // Instructions
227         TextView instructionTx = (TextView) findViewById(R.id.audio_aec_instructions);
228         Resources resources = getResources();
229         if (mDeviceHasAEC) {
230             instructionTx.setText(resources.getString(R.string.audio_aec_instructions));
231         } else {
232             instructionTx.setText(resources.getString(R.string.audio_aec_no_aec_support));
233             mResultText.setText(resources.getString(R.string.audio_aec_no_aec_pass));
234         }
235 
236         showView(mProgress, false);
237 
238         mSPlayer = new SoundPlayerObject(false, mBlockSizeSamples) {
239 
240             @Override
241             public void periodicNotification(AudioTrack track) {
242                 int channelCount = getChannelCount();
243                 mRMSPlayer1.updateRms(mPipe, channelCount, 0); //Only updated if running
244                 mRMSPlayer2.updateRms(mPipe, channelCount, 0);
245             }
246         };
247 
248         mSRecorder = new SoundRecorderObject(mSamplingRate, mBlockSizeSamples,
249                 mSelectedRecordSource) {
250             @Override
251             public void periodicNotification(AudioRecord recorder) {
252                 mRMSRecorder1.updateRms(mPipe, 1, 0); //always 1 channel
253                 mRMSRecorder2.updateRms(mPipe, 1, 0);
254             }
255         };
256 
257         setPassFailButtonClickListeners();
258 
259         // If device doesn't support AEC, allow pass
260         enableUILayout(mLinearLayout, mDeviceHasAEC);
261         getPassButton().setEnabled(!mDeviceHasAEC);
262 
263         setInfoResources(R.string.audio_aec_test,
264                 R.string.audio_aec_info, -1);
265     }
266 
showView(View v, boolean show)267     private void showView(View v, boolean show) {
268         v.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
269     }
270 
271     @Override
onClick(View v)272     public void onClick(View v) {
273         int id = v.getId();
274         if (id == R.id.audio_aec_button_test) {
275             startTest();
276         }
277     }
278 
startTest()279     private void startTest() {
280 
281         if (mTestThread != null && mTestThread.isAlive()) {
282             Log.v(TAG,"test Thread already running.");
283             return;
284         }
285         mTestThread = new Thread(new AudioTestRunner(TAG, TEST_AEC, mMessageHandler) {
286             public void run() {
287                 super.run();
288 
289                 mResultCode = RESULT_CODE_NOT_RUN;
290                 mMaxAec = 0.0;
291                 mMaxNoAec = 0.0;
292 
293                 StringBuilder sb = new StringBuilder(); //test results strings
294                 sendMessage(AudioTestRunner.TEST_MESSAGE,
295                         "Testing Recording with AEC");
296 
297                  //Step 0. Prepare system
298                 AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
299                 int targetMode = AudioManager.MODE_IN_COMMUNICATION;
300                 int originalMode = am.getMode();
301                 am.setMode(targetMode);
302 
303                 if (am.getMode() != targetMode) {
304                     sendMessage(AudioTestRunner.TEST_ENDED_ERROR,
305                             "Couldn't set mode to MODE_IN_COMMUNICATION.");
306                     return;
307                 }
308 
309                 int playbackStreamType = AudioManager.STREAM_VOICE_CALL;
310                 int maxLevel = getMaxLevelForStream(playbackStreamType);
311                 int desiredLevel = maxLevel - 1;
312                 setLevelForStream(playbackStreamType, desiredLevel);
313 
314                 int currentLevel = getLevelForStream(playbackStreamType);
315                 if (am.isVolumeFixed()) {
316                     sendMessage(AudioTestRunner.TEST_MESSAGE,
317                         "configured for Fixed volume, bypassing volume level check");
318 
319                 } else if (currentLevel != desiredLevel) {
320                     am.setMode(originalMode);
321                     sendMessage(AudioTestRunner.TEST_ENDED_ERROR,
322                         "Couldn't set level for STREAM_VOICE_CALL. Expected " +
323                         desiredLevel +" got: " + currentLevel);
324                     return;
325                 }
326 
327                 boolean originalSpeakerPhone = am.isSpeakerphoneOn();
328                 am.setSpeakerphoneOn(true);
329 
330                 //Step 1. With AEC (on by Default when using VOICE_COMMUNICATION audio source).
331                 mSPlayer.setStreamType(playbackStreamType);
332                 mSPlayer.setSoundWithResId(getApplicationContext(), R.raw.speech);
333                 mSRecorder.startRecording();
334 
335                 //get AEC
336                 int audioSessionId = mSRecorder.getAudioSessionId();
337                 if (mAec != null) {
338                     mAec.release();
339                     mAec = null;
340                 }
341                 try {
342                     mAec = AcousticEchoCanceler.create(audioSessionId);
343                 } catch (Exception e) {
344                     mSRecorder.stopRecording();
345                     String msg = "Could not create AEC Effect. " + e.toString();
346                     mResultCode = RESULT_CODE_AEC_CREATION_EXCEPTION;
347                     am.setSpeakerphoneOn(originalSpeakerPhone);
348                     am.setMode(originalMode);
349                     sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
350                     return;
351                 }
352 
353                 if (mAec == null) {
354                     mSRecorder.stopRecording();
355                     String msg = "Could not create AEC Effect (AEC Null)";
356                     mResultCode = RESULT_CODE_AEC_NULL;
357                     am.setSpeakerphoneOn(originalSpeakerPhone);
358                     am.setMode(originalMode);
359                     sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
360                     return;
361                 }
362 
363                 if (!mAec.getEnabled()) {
364                     String msg = "AEC is not enabled by default.";
365                     mSRecorder.stopRecording();
366                     mResultCode = RESULT_CODE_AEC_DISABLED;
367                     am.setSpeakerphoneOn(originalSpeakerPhone);
368                     am.setMode(originalMode);
369                     sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
370                     return;
371                 }
372 
373                 mRMSPlayer1.reset();
374                 mRMSRecorder1.reset();
375                 mSPlayer.play(true);
376                 mRMSPlayer1.setRunning(true);
377                 mRMSRecorder1.setRunning(true);
378 
379                 for (int s = 0; s < SHOT_COUNT; s++) {
380                     sleep(SHOT_FREQUENCY_MS);
381                     mRMSRecorder1.captureShot();
382                     mRMSPlayer1.captureShot();
383 
384                     sendMessage(AudioTestRunner.TEST_MESSAGE,
385                             String.format("AEC ON. Rec: %.2f dB, Play: %.2f dB",
386                                     20 * Math.log10(mRMSRecorder1.getRmsCurrent()),
387                                     20 * Math.log10(mRMSPlayer1.getRmsCurrent())));
388                 }
389 
390                 mRMSPlayer1.setRunning(false);
391                 mRMSRecorder1.setRunning(false);
392                 mSPlayer.play(false);
393 
394                 int lastShot = SHOT_COUNT - 1;
395                 int firstShot = SHOT_COUNT - SHOT_COUNT_CORRELATION;
396 
397                 double maxAEC = computeAcousticCouplingFactor(mRMSPlayer1.getRmsSnapshots(),
398                         mRMSRecorder1.getRmsSnapshots(), firstShot, lastShot);
399                 sendMessage(AudioTestRunner.TEST_MESSAGE,
400                         String.format("AEC On: Acoustic Coupling: %.2f", maxAEC));
401 
402                 //Wait
403                 sleep(1000);
404                 sendMessage(AudioTestRunner.TEST_MESSAGE, "Testing Recording AEC OFF");
405 
406                 //Step 2. Turn off the AEC
407                 mSPlayer.setSoundWithResId(getApplicationContext(),
408                         R.raw.speech);
409                 mAec.setEnabled(false);
410 
411                 // mSRecorder.startRecording();
412                 mRMSPlayer2.reset();
413                 mRMSRecorder2.reset();
414                 mSPlayer.play(true);
415                 mRMSPlayer2.setRunning(true);
416                 mRMSRecorder2.setRunning(true);
417 
418                 for (int s = 0; s < SHOT_COUNT; s++) {
419                     sleep(SHOT_FREQUENCY_MS);
420                     mRMSRecorder2.captureShot();
421                     mRMSPlayer2.captureShot();
422 
423                     sendMessage(AudioTestRunner.TEST_MESSAGE,
424                             String.format("AEC OFF. Rec: %.2f dB, Play: %.2f dB",
425                                     20 * Math.log10(mRMSRecorder2.getRmsCurrent()),
426                                     20 * Math.log10(mRMSPlayer2.getRmsCurrent())));
427                 }
428 
429                 mRMSPlayer2.setRunning(false);
430                 mRMSRecorder2.setRunning(false);
431                 mSRecorder.stopRecording();
432                 mSPlayer.play(false);
433 
434                 am.setSpeakerphoneOn(originalSpeakerPhone);
435                 am.setMode(originalMode);
436 
437                 double maxNoAEC = computeAcousticCouplingFactor(mRMSPlayer2.getRmsSnapshots(),
438                         mRMSRecorder2.getRmsSnapshots(), firstShot, lastShot);
439                 sendMessage(AudioTestRunner.TEST_MESSAGE, String.format("AEC Off: Corr: %.2f",
440                         maxNoAEC));
441 
442                 //test decision
443                 boolean testPassed = true;
444 
445                 sb.append(String.format(" Acoustic Coupling AEC ON: %.2f <= %.2f : ", maxAEC,
446                         TEST_THRESHOLD_AEC_ON));
447                 if (maxAEC <= TEST_THRESHOLD_AEC_ON) {
448                     sb.append("SUCCESS\n");
449                 } else {
450                     sb.append("FAILED\n");
451                     testPassed = false;
452                 }
453 
454                 sb.append(String.format(" Acoustic Coupling AEC OFF: %.2f >= %.2f : ", maxNoAEC,
455                         TEST_THRESHOLD_AEC_OFF));
456                 if (maxNoAEC >= TEST_THRESHOLD_AEC_OFF) {
457                     sb.append("SUCCESS\n");
458                 } else {
459                     sb.append("FAILED\n");
460                     testPassed = false;
461                 }
462 
463                 mMaxAec = maxAEC;
464                 mMaxNoAec = maxNoAEC;
465 
466                 if (testPassed) {
467                     sb.append("All Tests Passed");
468                     mResultCode = RESULT_CODE_OK;
469                 } else {
470                     sb.append("Test failed. Please fix issues and try again");
471                     mResultCode = RESULT_CODE_FAILED;
472                 }
473 
474                 //compute results.
475                 sendMessage(AudioTestRunner.TEST_ENDED_OK, "\n" + sb.toString());
476             }
477         });
478         mTestThread.start();
479     }
480 
481     // These must match the definitions in the AECActivity protobuffer at:
482     // google3/wireless/android/partner/adl/proto/testmetrics/ctsv_audio_metrics.proto
483     private static final String SECTION_AEC = "aec_activity";
484     private static final String KEY_AEC_MANDATORY = "aec_mandatory"; // bool, 1
485     private static final String KEY_AEC_MAX_WITH = "max_with_aec"; // float, 2
486     private static final String KEY_AEC_MAX_WITHOUT = "max_without_aec"; // float, 3
487     private static final String KEY_AEC_RESULT = "result"; // bool, 4
488     private static final String KEY_AEC_SUPPORTED = "aec_supported"; // bool 5
489     private static final String KEY_AEC_RESULT_CODE = "result_code"; // int, 6
490 
491     //
492     // PassFailButtons
493     //
494     @Override
getReportSectionName()495     public final String getReportSectionName() {
496         return setTestNameSuffix(sCurrentDisplayMode, SECTION_AEC);
497     }
498 
499     @Override
recordTestResults()500     public void recordTestResults() {
501         CtsVerifierReportLog reportLog = getReportLog();
502 
503         reportLog.addValue(KEY_AEC_MANDATORY,
504                 mDeviceHasAEC, // AEC was never mandatory. But this can simplify our queries.
505                 ResultType.NEUTRAL,
506                 ResultUnit.NONE);
507 
508         reportLog.addValue(KEY_AEC_MAX_WITH,
509                 mMaxAec,
510                 ResultType.LOWER_BETTER,
511                 ResultUnit.SCORE);
512 
513         reportLog.addValue(KEY_AEC_MAX_WITHOUT,
514                 mMaxNoAec,
515                 ResultType.HIGHER_BETTER,
516                 ResultUnit.SCORE);
517 
518         reportLog.addValue(KEY_AEC_RESULT,
519                 (mResultCode == RESULT_CODE_OK), // true if passed
520                 ResultType.NEUTRAL,
521                 ResultUnit.NONE);
522 
523         reportLog.addValue(KEY_AEC_SUPPORTED,
524                 mDeviceHasAEC,
525                 ResultType.NEUTRAL,
526                 ResultUnit.NONE);
527 
528         reportLog.addValue(KEY_AEC_RESULT_CODE,
529                 mResultCode,
530                 ResultType.LOWER_BETTER,
531                 ResultUnit.NONE);
532 
533         getReportLog().submit();
534     }
535 
536     // TestMessageHandler
537     private AudioTestRunner.AudioTestRunnerMessageHandler mMessageHandler =
538             new AudioTestRunner.AudioTestRunnerMessageHandler() {
539 
540         @Override
541         public void testStarted(int testId, String str) {
542             super.testStarted(testId, str);
543             Log.v(TAG, "Test Started! " + testId + " str:"+str);
544             showView(mProgress, true);
545             getPassButton().setEnabled(false);
546             mResultText.setText("test in progress..");
547         }
548 
549         @Override
550         public void testMessage(int testId, String str) {
551             super.testMessage(testId, str);
552             Log.v(TAG, "Message TestId: " + testId + " str:"+str);
553             mResultText.setText("test in progress.. " + str);
554         }
555 
556         @Override
557         public void testEndedOk(int testId, String str) {
558             super.testEndedOk(testId, str);
559             Log.v(TAG, "Test EndedOk. " + testId + " str:" + str);
560             showView(mProgress, false);
561             mResultText.setText("test completed. " + str);
562             if (!isReportLogOkToPass()) {
563                 mResultText.setText(getResources().getString(R.string.audio_general_reportlogtest));
564             } else if (mResultCode == RESULT_CODE_OK) {
565                 getPassButton().setEnabled(true);
566             }
567         }
568 
569         @Override
570         public void testEndedError(int testId, String str) {
571             super.testEndedError(testId, str);
572             Log.v(TAG, "Test EndedError. " + testId + " str:"+str);
573             showView(mProgress, false);
574             mResultText.setText("test failed. " + str);
575         }
576     };
577 }
578