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