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