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