| /* |
| * Copyright 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.mobileer.oboetester; |
| |
| import static com.mobileer.oboetester.StreamConfiguration.convertChannelMaskToText; |
| |
| import android.content.Context; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioManager; |
| import android.os.Bundle; |
| |
| import androidx.annotation.Nullable; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Locale; |
| |
| public class BaseAutoGlitchActivity extends GlitchActivity { |
| |
| private static final int SETUP_TIME_SECONDS = 4; // Time for the stream to settle. |
| protected static final int DEFAULT_DURATION_SECONDS = 8; // Run time for each test. |
| private static final int DEFAULT_GAP_MILLIS = 400; // Idle time between each test. |
| private static final String TEXT_SKIP = "SKIP"; |
| public static final String TEXT_PASS = "PASS"; |
| public static final String TEXT_FAIL = "FAIL !!!!"; |
| |
| protected int mDurationSeconds = DEFAULT_DURATION_SECONDS; |
| protected int mGapMillis = DEFAULT_GAP_MILLIS; |
| private String mTestName = ""; |
| |
| protected AudioManager mAudioManager; |
| |
| protected ArrayList<TestResult> mTestResults = new ArrayList<TestResult>(); |
| |
| public static boolean arrayContains(int[] haystack, int needle) { |
| for (int n: haystack) { |
| if (n == needle) return true; |
| } |
| return false; |
| } |
| |
| void logDeviceInfo() { |
| log("\n############################"); |
| log("\nDevice Info:"); |
| AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); |
| log(AudioQueryTools.getAudioManagerReport(audioManager)); |
| log(AudioQueryTools.getAudioFeatureReport(getPackageManager())); |
| log(AudioQueryTools.getAudioPropertyReport()); |
| log("\n############################"); |
| } |
| |
| void setTestName(String name) { |
| mTestName = name; |
| } |
| |
| @Override |
| public int getDeviceId() { |
| return super.getDeviceId(); |
| } |
| |
| private static class TestStreamOptions { |
| public final int channelUsed; |
| public final int channelCount; |
| public final int channelMask; |
| public final int deviceId; |
| public final int mmapUsed; |
| public final int performanceMode; |
| public final int sharingMode; |
| |
| public TestStreamOptions(StreamConfiguration configuration, int channelUsed) { |
| this.channelUsed = channelUsed; |
| channelCount = configuration.getChannelCount(); |
| channelMask = configuration.getChannelMask(); |
| deviceId = configuration.getDeviceId(); |
| mmapUsed = configuration.isMMap() ? 1 : 0; |
| performanceMode = configuration.getPerformanceMode(); |
| sharingMode = configuration.getSharingMode(); |
| } |
| |
| int countDifferences(TestStreamOptions other) { |
| int count = 0; |
| count += (channelUsed != other.channelUsed) ? 1 : 0; |
| count += (channelCount != other.channelCount) ? 1 : 0; |
| count += (channelMask != other.channelMask) ? 1 : 0; |
| count += (deviceId != other.deviceId) ? 1 : 0; |
| count += (mmapUsed != other.mmapUsed) ? 1 : 0; |
| count += (performanceMode != other.performanceMode) ? 1 : 0; |
| count += (sharingMode != other.sharingMode) ? 1 : 0; |
| return count; |
| } |
| |
| public String comparePassedDirection(String prefix, TestStreamOptions passed) { |
| StringBuffer text = new StringBuffer(); |
| text.append(TestDataPathsActivity.comparePassedField(prefix, this, passed, "channelUsed")); |
| text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "channelCount")); |
| text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "channelMask")); |
| text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "deviceId")); |
| text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "mmapUsed")); |
| text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "performanceMode")); |
| text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "sharingMode")); |
| return text.toString(); |
| } |
| @Override |
| public String toString() { |
| return "D=" + deviceId |
| + ", " + ((mmapUsed > 0) ? "MMAP" : "Lgcy") |
| + ", ch=" + channelText(channelUsed, channelCount) |
| + ", cm=" + convertChannelMaskToText(channelMask) |
| + "," + StreamConfiguration.convertPerformanceModeToText(performanceMode) |
| + "," + StreamConfiguration.convertSharingModeToText(sharingMode); |
| } |
| } |
| |
| protected static class TestResult { |
| final int testIndex; |
| final TestStreamOptions input; |
| final TestStreamOptions output; |
| public final int inputPreset; |
| public final int sampleRate; |
| final String testName; // name or purpose of test |
| |
| int result = TEST_RESULT_SKIPPED; // TEST_RESULT_FAILED, etc |
| private String mComments = ""; // additional info, ideas for why it failed |
| |
| public TestResult(int testIndex, |
| String testName, |
| StreamConfiguration inputConfiguration, |
| int inputChannel, |
| StreamConfiguration outputConfiguration, |
| int outputChannel) { |
| this.testIndex = testIndex; |
| this.testName = testName; |
| input = new TestStreamOptions(inputConfiguration, inputChannel); |
| output = new TestStreamOptions(outputConfiguration, outputChannel); |
| sampleRate = outputConfiguration.getSampleRate(); |
| this.inputPreset = inputConfiguration.getInputPreset(); |
| } |
| |
| int countDifferences(TestResult other) { |
| int count = 0; |
| count += input.countDifferences((other.input)); |
| count += output.countDifferences((other.output)); |
| count += (sampleRate != other.sampleRate) ? 1 : 0; |
| count += (inputPreset != other.inputPreset) ? 1 : 0; |
| return count; |
| } |
| |
| public boolean failed() { |
| return result == TEST_RESULT_FAILED; |
| } |
| |
| public boolean passed() { |
| return result == TEST_RESULT_PASSED; |
| } |
| |
| public String comparePassed(TestResult passed) { |
| StringBuffer text = new StringBuffer(); |
| text.append("Compare with passed test #" + passed.testIndex + "\n"); |
| text.append(input.comparePassedDirection("IN", passed.input)); |
| text.append(TestDataPathsActivity.comparePassedInputPreset("IN", this, passed)); |
| text.append(output.comparePassedDirection("OUT", passed.output)); |
| text.append(TestDataPathsActivity.comparePassedField("I/O",this, passed, "sampleRate")); |
| |
| return text.toString(); |
| } |
| |
| @Override |
| public String toString() { |
| return "IN: " + input + ", ip=" + inputPreset + "\n" |
| + "OUT: " + output + ", sr=" + sampleRate |
| + mComments; |
| } |
| |
| public void addComment(String comment) { |
| mComments += "\n"; |
| mComments += comment; |
| } |
| |
| public void setResult(int result) { |
| this.result = result; |
| } |
| public int getResult(int result) { |
| return result; |
| } |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); |
| |
| mAutomatedTestRunner = findViewById(R.id.auto_test_runner); |
| mAutomatedTestRunner.setActivity(this); |
| } |
| |
| protected void log(String text) { |
| mAutomatedTestRunner.log(text); |
| } |
| |
| protected void appendFailedSummary(String text) { |
| mAutomatedTestRunner.appendFailedSummary(text); |
| } |
| protected void appendSummary(String text) { |
| mAutomatedTestRunner.appendSummary(text); |
| } |
| |
| @Override |
| public void onStopTest() { |
| mAutomatedTestRunner.stopTest(); |
| } |
| |
| static String channelText(int index, int count) { |
| return index + "/" + count; |
| } |
| |
| protected String getConfigText(StreamConfiguration config) { |
| int channel = (config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) |
| ? getOutputChannel() : getInputChannel(); |
| return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "INP") |
| + (config.isMMap() ? "-M" : "-L") |
| + "-" + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) |
| + ", ID = " + String.format(Locale.getDefault(), "%2d", config.getDeviceId()) |
| + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( |
| config.getPerformanceMode()) |
| + ",\n ch = " + channelText(channel, config.getChannelCount()) |
| + ", cm = " + convertChannelMaskToText(config.getChannelMask()); |
| } |
| |
| protected String getStreamText(AudioStreamBase stream) { |
| return ("burst=" + stream.getFramesPerBurst() |
| + ", size=" + stream.getBufferSizeInFrames() |
| + ", cap=" + stream.getBufferCapacityInFrames() |
| ); |
| } |
| |
| public final static int TEST_RESULT_FAILED = -2; |
| public final static int TEST_RESULT_WARNING = -1; |
| public final static int TEST_RESULT_SKIPPED = 0; |
| public final static int TEST_RESULT_PASSED = 1; |
| |
| // Run one test based on the requested input/output configurations. |
| @Nullable |
| protected TestResult testCurrentConfigurations() throws InterruptedException { |
| mAutomatedTestRunner.incrementTestCount(); |
| if ((getSingleTestIndex() >= 0) && (getTestCount() != getSingleTestIndex())) { |
| return null; |
| } |
| |
| log("========================== #" + getTestCount()); |
| int result = 0; |
| StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; |
| StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; |
| |
| StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; |
| StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; |
| |
| log("Requested:"); |
| log(" SR = " + requestedOutConfig.getSampleRate()); |
| log(" " + getConfigText(requestedInConfig)); |
| log(" " + getConfigText(requestedOutConfig)); |
| |
| String reason = ""; |
| boolean openFailed = false; |
| try { |
| openAudio(); // this will fill in actualConfig |
| |
| log("Actual:"); |
| log(" SR = " + actualOutConfig.getSampleRate()); |
| // Set output size to a level that will avoid glitches. |
| AudioStreamBase outStream = mAudioOutTester.getCurrentAudioStream(); |
| int sizeFrames = outStream.getBufferCapacityInFrames() / 2; |
| sizeFrames = Math.max(sizeFrames, 2 * outStream.getFramesPerBurst()); |
| outStream.setBufferSizeInFrames(sizeFrames); |
| AudioStreamBase inStream = mAudioInputTester.getCurrentAudioStream(); |
| log(" " + getConfigText(actualInConfig)); |
| log(" " + getStreamText(inStream)); |
| log(" " + getConfigText(actualOutConfig)); |
| log(" " + getStreamText(outStream)); |
| } catch (Exception e) { |
| openFailed = true; |
| log(e.getMessage()); |
| reason = e.getMessage(); |
| } |
| |
| TestResult testResult = new TestResult( |
| getTestCount(), |
| mTestName, |
| mAudioInputTester.actualConfiguration, |
| getInputChannel(), |
| mAudioOutTester.actualConfiguration, |
| getOutputChannel() |
| ); |
| |
| // The test will only be worth running if we got the configuration we requested on input or output. |
| String skipReason = whyShouldTestBeSkipped(); |
| boolean skipped = skipReason.length() > 0; |
| boolean valid = !openFailed && !skipped; |
| boolean startFailed = false; |
| if (valid) { |
| try { |
| startAudioTest(); // Start running the test in the background. |
| } catch (IOException e) { |
| e.printStackTrace(); |
| valid = false; |
| startFailed = true; |
| log(e.getMessage()); |
| reason = e.getMessage(); |
| } |
| } |
| mAutomatedTestRunner.flushLog(); |
| |
| if (valid) { |
| // Check for early return until we reach full duration. |
| long now = System.currentTimeMillis(); |
| long startedAt = now; |
| long endTime = System.currentTimeMillis() + (mDurationSeconds * 1000); |
| boolean finishedEarly = false; |
| while (now < endTime && !finishedEarly) { |
| Thread.sleep(100); // Let test run. |
| now = System.currentTimeMillis(); |
| finishedEarly = isFinishedEarly(); |
| if (finishedEarly) { |
| log("Finished early after " + (now - startedAt) + " msec."); |
| } |
| } |
| } |
| int inXRuns = 0; |
| int outXRuns = 0; |
| |
| if (!openFailed) { |
| // get xRuns before closing the streams. |
| inXRuns = mAudioInputTester.getCurrentAudioStream().getXRunCount(); |
| outXRuns = mAudioOutTester.getCurrentAudioStream().getXRunCount(); |
| |
| super.stopAudioTest(); |
| } |
| |
| if (openFailed || startFailed) { |
| appendFailedSummary("------ #" + getTestCount() + "\n"); |
| appendFailedSummary(getConfigText(requestedInConfig) + "\n"); |
| appendFailedSummary(getConfigText(requestedOutConfig) + "\n"); |
| appendFailedSummary(reason + "\n"); |
| mAutomatedTestRunner.incrementFailCount(); |
| } else if (skipped) { |
| log(TEXT_SKIP + " - " + skipReason); |
| } else { |
| log("Result:"); |
| reason += didTestFail(); |
| boolean passed = reason.length() == 0; |
| |
| String resultText = getShortReport(); |
| resultText += ", xruns = " + inXRuns + "/" + outXRuns; |
| resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL); |
| resultText += reason; |
| log(" " + resultText); |
| if (!passed) { |
| appendFailedSummary("------ #" + getTestCount() + "\n"); |
| appendFailedSummary(" " + getConfigText(actualInConfig) + "\n"); |
| appendFailedSummary(" " + getConfigText(actualOutConfig) + "\n"); |
| appendFailedSummary(" " + resultText + "\n"); |
| mAutomatedTestRunner.incrementFailCount(); |
| result = TEST_RESULT_FAILED; |
| } else { |
| mAutomatedTestRunner.incrementPassCount(); |
| result = TEST_RESULT_PASSED; |
| } |
| |
| } |
| mAutomatedTestRunner.flushLog(); |
| |
| // Give hardware time to settle between tests. |
| Thread.sleep(mGapMillis); |
| |
| if (valid) { |
| testResult.setResult(result); |
| mTestResults.add(testResult); |
| } |
| return testResult; |
| } |
| |
| protected int getTestCount() { |
| return mAutomatedTestRunner.getTestCount(); |
| } |
| |
| protected AudioDeviceInfo getDeviceInfoById(int deviceId) { |
| AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL); |
| for (AudioDeviceInfo deviceInfo : devices) { |
| if (deviceInfo.getId() == deviceId) { |
| return deviceInfo; |
| } |
| } |
| return null; |
| } |
| |
| protected AudioDeviceInfo getDeviceInfoByType(int deviceType, int flags) { |
| AudioDeviceInfo[] devices = mAudioManager.getDevices(flags); |
| for (AudioDeviceInfo deviceInfo : devices) { |
| if (deviceInfo.getType() == deviceType) { |
| return deviceInfo; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Are outputs mixed in the air or by a loopback plug? |
| * @param type device type, eg AudioDeviceInfo.TYPE_BUILTIN_SPEAKER |
| * @return true if stereo output channels get mixed to mono input |
| */ |
| protected boolean isDeviceTypeMixedForLoopback(int type) { |
| switch(type) { |
| // Mixed in the air. |
| case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: |
| case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE: |
| // Mixed in the loopback fun-plug. |
| case AudioDeviceInfo.TYPE_WIRED_HEADSET: |
| case AudioDeviceInfo.TYPE_USB_HEADSET: |
| return true; |
| |
| case AudioDeviceInfo.TYPE_USB_DEVICE: |
| default: |
| return false; // channels are discrete |
| } |
| } |
| |
| protected ArrayList<Integer> getCompatibleDeviceTypes(int type) { |
| ArrayList<Integer> compatibleTypes = new ArrayList<Integer>(); |
| switch(type) { |
| case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: |
| case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE: |
| compatibleTypes.add(AudioDeviceInfo.TYPE_BUILTIN_MIC); |
| break; |
| case AudioDeviceInfo.TYPE_BUILTIN_MIC: |
| compatibleTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); |
| break; |
| case AudioDeviceInfo.TYPE_USB_DEVICE: |
| compatibleTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE); |
| // A USB Device is often mistaken for a headset. |
| compatibleTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET); |
| break; |
| default: |
| compatibleTypes.add(type); |
| break; |
| } |
| return compatibleTypes; |
| } |
| |
| /** |
| * Scan available device for one with a compatible device type for loopback testing. |
| * @return deviceId |
| */ |
| |
| protected AudioDeviceInfo findCompatibleInputDevice(int outputDeviceType) { |
| ArrayList<Integer> compatibleDeviceTypes = getCompatibleDeviceTypes(outputDeviceType); |
| AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); |
| for (AudioDeviceInfo candidate : devices) { |
| if (compatibleDeviceTypes.contains(candidate.getType())) { |
| return candidate; |
| } |
| } |
| return null; |
| } |
| |
| protected boolean isFinishedEarly() { |
| return false; |
| } |
| |
| /** |
| * Figure out if a test should be skipped and return the reason. |
| * |
| * @return reason for skipping or an empty string |
| */ |
| protected String whyShouldTestBeSkipped() { |
| String why = ""; |
| StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; |
| StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; |
| StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; |
| StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; |
| // No point running the test if we don't get any of the sharing modes we requested. |
| if (actualInConfig.getSharingMode() != requestedInConfig.getSharingMode() |
| && actualOutConfig.getSharingMode() != requestedOutConfig.getSharingMode()) { |
| log("Did not get requested sharing mode."); |
| why += "share,"; |
| } |
| if (actualInConfig.getPerformanceMode() != requestedInConfig.getPerformanceMode() |
| && actualOutConfig.getPerformanceMode() != requestedOutConfig.getPerformanceMode()) { |
| log("Did not get requested performance mode."); |
| why += "perf,"; |
| } |
| if (actualInConfig.isMMap() != requestedInConfig.isMMap() |
| && actualOutConfig.isMMap() != requestedOutConfig.isMMap()) { |
| log("Did not get requested MMAP data path."); |
| why += "mmap,"; |
| } |
| return why; |
| } |
| |
| public String didTestFail() { |
| String why = ""; |
| if (getMaxSecondsWithNoGlitch() <= (mDurationSeconds - SETUP_TIME_SECONDS)) { |
| why += ", glitch"; |
| } |
| return why; |
| } |
| |
| void logAnalysis(String text) { |
| appendFailedSummary(text + "\n"); |
| } |
| |
| private int countPassingTests() { |
| int numPassed = 0; |
| for (TestResult other : mTestResults) { |
| if (other.passed()) { |
| numPassed++; |
| } |
| } |
| return numPassed; |
| } |
| |
| protected void compareFailedTestsWithNearestPassingTest() { |
| logAnalysis("\n==== COMPARISON ANALYSIS ==========="); |
| if (countPassingTests() == 0) { |
| logAnalysis("Comparison skipped because NO tests passed."); |
| return; |
| } |
| logAnalysis("Compare failed tests with others that passed."); |
| // Analyze each failed test. |
| for (TestResult testResult : mTestResults) { |
| if (testResult.failed()) { |
| logAnalysis("-------------------- #" + testResult.testIndex + " FAILED"); |
| String name = testResult.testName; |
| if (name.length() > 0) { |
| logAnalysis(name); |
| } |
| TestResult[] closest = findClosestPassingTestResults(testResult); |
| for (TestResult other : closest) { |
| logAnalysis(testResult.comparePassed(other)); |
| } |
| logAnalysis(testResult.toString()); |
| } |
| } |
| } |
| |
| |
| @Nullable |
| private TestResult[] findClosestPassingTestResults(TestResult testResult) { |
| int minDifferences = Integer.MAX_VALUE; |
| for (TestResult other : mTestResults) { |
| if (other.passed()) { |
| int numDifferences = testResult.countDifferences(other); |
| if (numDifferences < minDifferences) { |
| minDifferences = numDifferences; |
| } |
| } |
| } |
| // Now find all the tests that are just as close as the closest. |
| ArrayList<TestResult> list = new ArrayList<TestResult>(); |
| for (TestResult other : mTestResults) { |
| if (other.passed()) { |
| int numDifferences = testResult.countDifferences(other); |
| if (numDifferences == minDifferences) { |
| list.add(other); |
| } |
| } |
| } |
| return list.toArray(new TestResult[0]); |
| } |
| |
| } |