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