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