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