blob: 48795efc1425ed52bc2a80f3534d972c127b30b9 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.media.audiofx.AcousticEchoCanceler;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
import com.android.cts.verifier.CtsVerifierReportLog;
import com.android.cts.verifier.R;
import com.android.cts.verifier.audio.wavelib.DspBufferDouble;
import com.android.cts.verifier.audio.wavelib.DspBufferMath;
import com.android.cts.verifier.audio.wavelib.PipeShort;
public class AudioAEC extends AudioFrequencyActivity implements View.OnClickListener {
private static final String TAG = "AudioAEC";
private static final int TEST_NONE = -1;
private static final int TEST_AEC = 0;
private static final int TEST_COUNT = 1;
private static final float MAX_VAL = (float)(1 << 15);
private int mCurrentTest = TEST_NONE;
private LinearLayout mLinearLayout;
private Button mButtonTest;
private Button mButtonMandatoryYes;
private Button mButtonMandatoryNo;
private ProgressBar mProgress;
private TextView mResultTest;
private boolean mTestAECPassed;
private SoundPlayerObject mSPlayer;
private SoundRecorderObject mSRecorder;
private AcousticEchoCanceler mAec;
private boolean mMandatory = true;
private final int mBlockSizeSamples = 4096;
private final int mSamplingRate = 48000;
private final int mSelectedRecordSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
private final int TEST_DURATION_MS = 8000;
private final int SHOT_FREQUENCY_MS = 200;
private final int CORRELATION_DURATION_MS = TEST_DURATION_MS - 3000;
private final int SHOT_COUNT_CORRELATION = CORRELATION_DURATION_MS/SHOT_FREQUENCY_MS;
private final int SHOT_COUNT = TEST_DURATION_MS/SHOT_FREQUENCY_MS;
private final float MIN_RMS_DB = -60.0f; //dB
private final float MIN_RMS_VAL = (float)Math.pow(10,(MIN_RMS_DB/20));
private final double TEST_THRESHOLD_AEC_ON = 0.5;
private final double TEST_THRESHOLD_AEC_OFF = 0.6;
private RmsHelper mRMSRecorder1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
private RmsHelper mRMSRecorder2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
private RmsHelper mRMSPlayer1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
private RmsHelper mRMSPlayer2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
private Thread mTestThread;
//RMS helpers
public class RmsHelper {
private double mRmsCurrent;
public int mBlockSize;
private int mShoutCount;
public boolean mRunning = false;
private short[] mAudioShortArray;
private DspBufferDouble mRmsSnapshots;
private int mShotIndex;
public RmsHelper(int blockSize, int shotCount) {
mBlockSize = blockSize;
mShoutCount = shotCount;
reset();
}
public void reset() {
mAudioShortArray = new short[mBlockSize];
mRmsSnapshots = new DspBufferDouble(mShoutCount);
mShotIndex = 0;
mRmsCurrent = 0;
mRunning = false;
}
public void captureShot() {
if (mShotIndex >= 0 && mShotIndex < mRmsSnapshots.getSize()) {
mRmsSnapshots.setValue(mShotIndex++, mRmsCurrent);
}
}
public void setRunning(boolean running) {
mRunning = running;
}
public double getRmsCurrent() {
return mRmsCurrent;
}
public DspBufferDouble getRmsSnapshots() {
return mRmsSnapshots;
}
public boolean updateRms(PipeShort pipe, int channelCount, int channel) {
if (mRunning) {
int samplesAvailable = pipe.availableToRead();
while (samplesAvailable >= mBlockSize) {
pipe.read(mAudioShortArray, 0, mBlockSize);
double rmsTempSum = 0;
int count = 0;
for (int i = channel; i < mBlockSize; i += channelCount) {
float value = mAudioShortArray[i] / MAX_VAL;
rmsTempSum += value * value;
count++;
}
float rms = count > 0 ? (float)Math.sqrt(rmsTempSum / count) : 0f;
if (rms < MIN_RMS_VAL) {
rms = MIN_RMS_VAL;
}
double alpha = 0.9;
double total_rms = rms * alpha + mRmsCurrent * (1.0f - alpha);
mRmsCurrent = total_rms;
samplesAvailable = pipe.availableToRead();
}
return true;
}
return false;
}
}
//compute Acoustic Coupling Factor
private double computeAcousticCouplingFactor(DspBufferDouble buffRmsPlayer,
DspBufferDouble buffRmsRecorder,
int firstShot, int lastShot) {
int len = Math.min(buffRmsPlayer.getSize(), buffRmsRecorder.getSize());
firstShot = Math.min(firstShot, 0);
lastShot = Math.min(lastShot, len -1);
int actualLen = lastShot - firstShot + 1;
double maxValue = 0;
if (actualLen > 0) {
DspBufferDouble rmsPlayerdB = new DspBufferDouble(actualLen);
DspBufferDouble rmsRecorderdB = new DspBufferDouble(actualLen);
DspBufferDouble crossCorr = new DspBufferDouble(actualLen);
for (int i = firstShot, index = 0; i <= lastShot; ++i, ++index) {
double valPlayerdB = Math.max(20 * Math.log10(buffRmsPlayer.mData[i]), MIN_RMS_DB);
rmsPlayerdB.setValue(index, valPlayerdB);
double valRecorderdB = Math.max(20 * Math.log10(buffRmsRecorder.mData[i]),
MIN_RMS_DB);
rmsRecorderdB.setValue(index, valRecorderdB);
}
//cross correlation...
if (DspBufferMath.crossCorrelation(crossCorr, rmsPlayerdB, rmsRecorderdB) !=
DspBufferMath.MATH_RESULT_SUCCESS) {
Log.v(TAG, "math error in cross correlation");
}
for (int i = 0; i < len; i++) {
if (Math.abs(crossCorr.mData[i]) > maxValue) {
maxValue = Math.abs(crossCorr.mData[i]);
}
}
}
return maxValue;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.audio_aec_activity);
//
mLinearLayout = (LinearLayout)findViewById(R.id.audio_aec_test_layout);
mButtonMandatoryYes = (Button) findViewById(R.id.audio_aec_mandatory_yes);
mButtonMandatoryYes.setOnClickListener(this);
mButtonMandatoryNo = (Button) findViewById(R.id.audio_aec_mandatory_no);
mButtonMandatoryNo.setOnClickListener(this);
enableUILayout(mLinearLayout, false);
// Test
mButtonTest = (Button) findViewById(R.id.audio_aec_button_test);
mButtonTest.setOnClickListener(this);
mProgress = (ProgressBar) findViewById(R.id.audio_aec_test_progress_bar);
mResultTest = (TextView) findViewById(R.id.audio_aec_test_result);
showView(mProgress, false);
mSPlayer = new SoundPlayerObject(false, mBlockSizeSamples) {
@Override
public void periodicNotification(AudioTrack track) {
int channelCount = getChannelCount();
mRMSPlayer1.updateRms(mPipe, channelCount, 0); //Only updated if running
mRMSPlayer2.updateRms(mPipe, channelCount, 0);
}
};
mSRecorder = new SoundRecorderObject(mSamplingRate, mBlockSizeSamples,
mSelectedRecordSource) {
@Override
public void periodicNotification(AudioRecord recorder) {
mRMSRecorder1.updateRms(mPipe, 1, 0); //always 1 channel
mRMSRecorder2.updateRms(mPipe, 1, 0);
}
};
setPassFailButtonClickListeners();
getPassButton().setEnabled(false);
setInfoResources(R.string.audio_aec_test,
R.string.audio_aec_info, -1);
}
private void showView(View v, boolean show) {
v.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.audio_aec_button_test) {
startTest();
} else if (id == R.id.audio_aec_mandatory_no) {
enableUILayout(mLinearLayout, false);
getPassButton().setEnabled(true);
mButtonMandatoryNo.setEnabled(false);
mButtonMandatoryYes.setEnabled(false);
mMandatory = false;
Log.v(TAG, "AEC marked as NOT mandatory");
} else if (id == R.id.audio_aec_mandatory_yes) {
enableUILayout(mLinearLayout, true);
mButtonMandatoryNo.setEnabled(false);
mButtonMandatoryYes.setEnabled(false);
mMandatory = true;
Log.v(TAG, "AEC marked as mandatory");
}
}
private void startTest() {
if (mTestThread != null && mTestThread.isAlive()) {
Log.v(TAG,"test Thread already running.");
return;
}
mTestThread = new Thread(new AudioTestRunner(TAG, TEST_AEC, mMessageHandler) {
public void run() {
super.run();
StringBuilder sb = new StringBuilder(); //test results strings
mTestAECPassed = false;
sendMessage(AudioTestRunner.TEST_MESSAGE,
"Testing Recording with AEC");
//is AEC Available?
if (!AcousticEchoCanceler.isAvailable()) {
String msg;
if (mMandatory) {
msg = "Error. AEC not available";
sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
} else {
mTestAECPassed = true;
msg = "Warning. AEC not implemented.";
sendMessage(AudioTestRunner.TEST_ENDED_OK, msg);
}
storeTestResults(mMandatory, 0, 0, msg);
return;
}
//Step 0. Prepare system
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int targetMode = AudioManager.MODE_IN_COMMUNICATION;
int originalMode = am.getMode();
am.setMode(targetMode);
if (am.getMode() != targetMode) {
sendMessage(AudioTestRunner.TEST_ENDED_ERROR,
"Couldn't set mode to MODE_IN_COMMUNICATION.");
return;
}
int playbackStreamType = AudioManager.STREAM_VOICE_CALL;
int maxLevel = getMaxLevelForStream(playbackStreamType);
int desiredLevel = maxLevel - 1;
setLevelForStream(playbackStreamType, desiredLevel);
int currentLevel = getLevelForStream(playbackStreamType);
if (currentLevel != desiredLevel) {
am.setMode(originalMode);
sendMessage(AudioTestRunner.TEST_ENDED_ERROR,
"Couldn't set level for STREAM_VOICE_CALL. Expected " +
desiredLevel +" got: " + currentLevel);
return;
}
boolean originalSpeakerPhone = am.isSpeakerphoneOn();
am.setSpeakerphoneOn(true);
//Step 1. With AEC (on by Default when using VOICE_COMMUNICATION audio source).
mSPlayer.setStreamType(playbackStreamType);
mSPlayer.setSoundWithResId(getApplicationContext(), R.raw.speech);
mSRecorder.startRecording();
//get AEC
int audioSessionId = mSRecorder.getAudioSessionId();
if (mAec != null) {
mAec.release();
mAec = null;
}
try {
mAec = AcousticEchoCanceler.create(audioSessionId);
} catch (Exception e) {
mSRecorder.stopRecording();
String msg = "Could not create AEC Effect. " + e.toString();
storeTestResults(mMandatory, 0, 0, msg);
am.setSpeakerphoneOn(originalSpeakerPhone);
am.setMode(originalMode);
sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
return;
}
if (mAec == null) {
mSRecorder.stopRecording();
String msg = "Could not create AEC Effect (AEC Null)";
storeTestResults(mMandatory,0, 0, msg);
am.setSpeakerphoneOn(originalSpeakerPhone);
am.setMode(originalMode);
sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
return;
}
if (!mAec.getEnabled()) {
String msg = "AEC is not enabled by default.";
if (mMandatory) {
mSRecorder.stopRecording();
storeTestResults(mMandatory,0, 0, msg);
am.setSpeakerphoneOn(originalSpeakerPhone);
am.setMode(originalMode);
sendMessage(AudioTestRunner.TEST_ENDED_ERROR, msg);
return;
} else {
sb.append("Warning. " + msg + "\n");
}
}
mRMSPlayer1.reset();
mRMSRecorder1.reset();
mSPlayer.play(true);
mRMSPlayer1.setRunning(true);
mRMSRecorder1.setRunning(true);
for (int s = 0; s < SHOT_COUNT; s++) {
sleep(SHOT_FREQUENCY_MS);
mRMSRecorder1.captureShot();
mRMSPlayer1.captureShot();
sendMessage(AudioTestRunner.TEST_MESSAGE,
String.format("AEC ON. Rec: %.2f dB, Play: %.2f dB",
20 * Math.log10(mRMSRecorder1.getRmsCurrent()),
20 * Math.log10(mRMSPlayer1.getRmsCurrent())));
}
mRMSPlayer1.setRunning(false);
mRMSRecorder1.setRunning(false);
mSPlayer.play(false);
int lastShot = SHOT_COUNT - 1;
int firstShot = SHOT_COUNT - SHOT_COUNT_CORRELATION;
double maxAEC = computeAcousticCouplingFactor(mRMSPlayer1.getRmsSnapshots(),
mRMSRecorder1.getRmsSnapshots(), firstShot, lastShot);
sendMessage(AudioTestRunner.TEST_MESSAGE,
String.format("AEC On: Acoustic Coupling: %.2f", maxAEC));
//Wait
sleep(1000);
sendMessage(AudioTestRunner.TEST_MESSAGE, "Testing Recording AEC OFF");
//Step 2. Turn off the AEC
mSPlayer.setSoundWithResId(getApplicationContext(),
R.raw.speech);
mAec.setEnabled(false);
// mSRecorder.startRecording();
mRMSPlayer2.reset();
mRMSRecorder2.reset();
mSPlayer.play(true);
mRMSPlayer2.setRunning(true);
mRMSRecorder2.setRunning(true);
for (int s = 0; s < SHOT_COUNT; s++) {
sleep(SHOT_FREQUENCY_MS);
mRMSRecorder2.captureShot();
mRMSPlayer2.captureShot();
sendMessage(AudioTestRunner.TEST_MESSAGE,
String.format("AEC OFF. Rec: %.2f dB, Play: %.2f dB",
20 * Math.log10(mRMSRecorder2.getRmsCurrent()),
20 * Math.log10(mRMSPlayer2.getRmsCurrent())));
}
mRMSPlayer2.setRunning(false);
mRMSRecorder2.setRunning(false);
mSRecorder.stopRecording();
mSPlayer.play(false);
am.setSpeakerphoneOn(originalSpeakerPhone);
am.setMode(originalMode);
double maxNoAEC = computeAcousticCouplingFactor(mRMSPlayer2.getRmsSnapshots(),
mRMSRecorder2.getRmsSnapshots(), firstShot, lastShot);
sendMessage(AudioTestRunner.TEST_MESSAGE, String.format("AEC Off: Corr: %.2f",
maxNoAEC));
//test decision
boolean testPassed = true;
sb.append(String.format(" Acoustic Coupling AEC ON: %.2f <= %.2f : ", maxAEC,
TEST_THRESHOLD_AEC_ON));
if (maxAEC <= TEST_THRESHOLD_AEC_ON) {
sb.append("SUCCESS\n");
} else {
sb.append("FAILED\n");
testPassed = false;
}
sb.append(String.format(" Acoustic Coupling AEC OFF: %.2f >= %.2f : ", maxNoAEC,
TEST_THRESHOLD_AEC_OFF));
if (maxNoAEC >= TEST_THRESHOLD_AEC_OFF) {
sb.append("SUCCESS\n");
} else {
sb.append("FAILED\n");
testPassed = false;
}
mTestAECPassed = testPassed;
if (mTestAECPassed) {
sb.append("All Tests Passed");
} else {
if (mMandatory) {
sb.append("Test failed. Please fix issues and try again");
} else {
sb.append("Warning. Acoustic Coupling Levels did not pass criteria");
mTestAECPassed = true;
}
}
storeTestResults(mMandatory, maxAEC, maxNoAEC, sb.toString());
//compute results.
sendMessage(AudioTestRunner.TEST_ENDED_OK, "\n" + sb.toString());
}
});
mTestThread.start();
}
private void storeTestResults(boolean aecMandatory, double maxAEC, double maxNoAEC,
String msg) {
CtsVerifierReportLog reportLog = getReportLog();
reportLog.addValue("AEC_mandatory",
aecMandatory,
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue("max_with_AEC",
maxAEC,
ResultType.LOWER_BETTER,
ResultUnit.SCORE);
reportLog.addValue("max_without_AEC",
maxNoAEC,
ResultType.HIGHER_BETTER,
ResultUnit.SCORE);
reportLog.addValue("result_string",
msg,
ResultType.NEUTRAL,
ResultUnit.NONE);
}
@Override // PassFailButtons
public void recordTestResults() {
getReportLog().submit();
}
// TestMessageHandler
private AudioTestRunner.AudioTestRunnerMessageHandler mMessageHandler =
new AudioTestRunner.AudioTestRunnerMessageHandler() {
@Override
public void testStarted(int testId, String str) {
super.testStarted(testId, str);
Log.v(TAG, "Test Started! " + testId + " str:"+str);
showView(mProgress, true);
mTestAECPassed = false;
getPassButton().setEnabled(false);
mResultTest.setText("test in progress..");
}
@Override
public void testMessage(int testId, String str) {
super.testMessage(testId, str);
Log.v(TAG, "Message TestId: " + testId + " str:"+str);
mResultTest.setText("test in progress.. " + str);
}
@Override
public void testEndedOk(int testId, String str) {
super.testEndedOk(testId, str);
Log.v(TAG, "Test EndedOk. " + testId + " str:"+str);
showView(mProgress, false);
mResultTest.setText("test completed. " + str);
if (mTestAECPassed) {
getPassButton().setEnabled(true);;
}
}
@Override
public void testEndedError(int testId, String str) {
super.testEndedError(testId, str);
Log.v(TAG, "Test EndedError. " + testId + " str:"+str);
showView(mProgress, false);
mResultTest.setText("test failed. " + str);
}
};
}