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