• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 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.mobileer.oboetester;
18 
19 import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle;
20 
21 import android.app.Activity;
22 import android.content.Context;
23 import android.media.AudioDeviceInfo;
24 import android.media.AudioManager;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.support.annotation.NonNull;
29 import android.util.Log;
30 import android.widget.CheckBox;
31 
32 import com.mobileer.audio_device.AudioDeviceInfoConverter;
33 
34 import java.lang.reflect.Field;
35 
36 /**
37  * Play a recognizable tone on each channel of each speaker device
38  * and listen for the result through a microphone.
39  * Also test each microphone channel and device.
40  * Try each InputPreset.
41  *
42  * The analysis is based on a cosine transform of a single
43  * frequency. The magnitude indicates the level.
44  * The variations in phase, "jitter" indicate how noisy the
45  * signal is or whether it is corrupted. A noisy room may have
46  * energy at the target frequency but the phase will be random.
47  *
48  * This test requires a quiet room but no other hardware.
49  */
50 public class TestDataPathsActivity  extends BaseAutoGlitchActivity {
51 
52     public static final String KEY_USE_INPUT_PRESETS = "use_input_presets";
53     public static final boolean VALUE_DEFAULT_USE_INPUT_PRESETS = true;
54 
55     public static final String KEY_USE_INPUT_DEVICES = "use_input_devices";
56     public static final boolean VALUE_DEFAULT_USE_INPUT_DEVICES = true;
57 
58     public static final String KEY_USE_OUTPUT_DEVICES = "use_output_devices";
59     public static final boolean VALUE_DEFAULT_USE_OUTPUT_DEVICES = true;
60 
61     public static final String KEY_SINGLE_TEST_INDEX = "single_test_index";
62     public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1;
63 
64     public static final int DURATION_SECONDS = 3;
65     private final static double MIN_REQUIRED_MAGNITUDE = 0.001;
66     private final static double MAX_SINE_FREQUENCY = 1000.0;
67     private final static int TYPICAL_SAMPLE_RATE = 48000;
68     private final static double FRAMES_PER_CYCLE = TYPICAL_SAMPLE_RATE / MAX_SINE_FREQUENCY;
69     private final static double PHASE_PER_BIN = 2.0 * Math.PI / FRAMES_PER_CYCLE;
70     private final static double MAX_ALLOWED_JITTER = 2.0 * PHASE_PER_BIN;
71     private final static String MAGNITUDE_FORMAT = "%7.5f";
72 
73     final int TYPE_BUILTIN_SPEAKER_SAFE = 0x18; // API 30
74 
75     private double mMagnitude;
76     private double mMaxMagnitude;
77     private int    mPhaseCount;
78     private double mPhase;
79     private double mPhaseErrorSum;
80     private double mPhaseErrorCount;
81 
82     AudioManager   mAudioManager;
83     private CheckBox mCheckBoxInputPresets;
84     private CheckBox mCheckBoxInputDevices;
85     private CheckBox mCheckBoxOutputDevices;
86 
87     private static final int[] INPUT_PRESETS = {
88             // VOICE_RECOGNITION gets tested in testInputs()
89             // StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION,
90             StreamConfiguration.INPUT_PRESET_GENERIC,
91             StreamConfiguration.INPUT_PRESET_CAMCORDER,
92             // TODO Resolve issue with echo cancellation killing the signal.
93             StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
94             StreamConfiguration.INPUT_PRESET_UNPROCESSED,
95             StreamConfiguration.INPUT_PRESET_VOICE_PERFORMANCE,
96     };
97 
98     @NonNull
comparePassedField(String prefix, Object failed, Object passed, String name)99     public static String comparePassedField(String prefix, Object failed, Object passed, String name) {
100         try {
101             Field field = failed.getClass().getField(name);
102             int failedValue = field.getInt(failed);
103             int passedValue = field.getInt(passed);
104             return (failedValue == passedValue) ? ""
105                 :  (prefix + " " + name + ": passed = " + passedValue + ", failed = " + failedValue + "\n");
106         } catch (NoSuchFieldException e) {
107             return "ERROR - no such field  " + name;
108         } catch (IllegalAccessException e) {
109             return "ERROR - cannot access  " + name;
110         }
111     }
112 
calculatePhaseError(double p1, double p2)113     public static double calculatePhaseError(double p1, double p2) {
114         double diff = Math.abs(p1 - p2);
115         // Wrap around the circle.
116         while (diff > (2 * Math.PI)) {
117             diff -= (2 * Math.PI);
118         }
119         // A phase error close to 2*PI is actually a small phase error.
120         if (diff > Math.PI) {
121             diff = (2 * Math.PI) - diff;
122         }
123         return diff;
124     }
125 
126     // Periodically query for magnitude and phase from the native detector.
127     protected class DataPathSniffer extends NativeSniffer {
128 
DataPathSniffer(Activity activity)129         public DataPathSniffer(Activity activity) {
130             super(activity);
131         }
132 
133         @Override
startSniffer()134         public void startSniffer() {
135             mMagnitude = -1.0;
136             mMaxMagnitude = -1.0;
137             mPhaseCount = 0;
138             mPhase = 0.0;
139             mPhaseErrorSum = 0.0;
140             mPhaseErrorCount = 0;
141             super.startSniffer();
142         }
143 
144         @Override
run()145         public void run() {
146             mMagnitude = getMagnitude();
147             mMaxMagnitude = getMaxMagnitude();
148             Log.d(TAG, String.format("magnitude = %7.4f, maxMagnitude = %7.4f",
149                     mMagnitude, mMaxMagnitude));
150             // Only look at the phase if we have a signal.
151             if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) {
152                 double phase = getPhase();
153                 // Wait for the analyzer to get a lock on the signal.
154                 // Arbitrary number of phase measurements before we start measuring jitter.
155                 final int kMinPhaseMeasurementsRequired = 4;
156                 if (mPhaseCount >= kMinPhaseMeasurementsRequired) {
157                     double phaseError = calculatePhaseError(phase, mPhase);
158                     // low pass filter
159                     mPhaseErrorSum += phaseError;
160                     mPhaseErrorCount++;
161                     Log.d(TAG, String.format("phase = %7.4f, diff = %7.4f, jitter = %7.4f",
162                             phase, phaseError, getAveragePhaseError()));
163                 }
164                 mPhase = phase;
165                 mPhaseCount++;
166             }
167             reschedule();
168         }
169 
getCurrentStatusReport()170         public String getCurrentStatusReport() {
171             StringBuffer message = new StringBuffer();
172             message.append(
173                     "magnitude = " + getMagnitudeText(mMagnitude)
174                     + ", max = " + getMagnitudeText(mMaxMagnitude)
175                     + "\nphase = " + getMagnitudeText(mPhase)
176                     + ", jitter = " + getJitterText()
177                     + ", #" + mPhaseCount
178                     + "\n");
179             return message.toString();
180         }
181 
182         @Override
getShortReport()183         public String getShortReport() {
184             return "maxMag = " + getMagnitudeText(mMaxMagnitude)
185                     + ", jitter = " + getJitterText();
186         }
187 
188         @Override
updateStatusText()189         public void updateStatusText() {
190             mLastGlitchReport = getCurrentStatusReport();
191             runOnUiThread(() -> {
192                 setAnalyzerText(mLastGlitchReport);
193             });
194         }
195     }
196 
getJitterText()197     private String getJitterText() {
198         return isPhaseJitterValid() ? getMagnitudeText(getAveragePhaseError()) : "?";
199     }
200 
201     @Override
createNativeSniffer()202     NativeSniffer createNativeSniffer() {
203         return new TestDataPathsActivity.DataPathSniffer(this);
204     }
205 
getMagnitude()206     native double getMagnitude();
getMaxMagnitude()207     native double getMaxMagnitude();
getPhase()208     native double getPhase();
209 
210     @Override
inflateActivity()211     protected void inflateActivity() {
212         setContentView(R.layout.activity_data_paths);
213     }
214 
215     @Override
onCreate(Bundle savedInstanceState)216     protected void onCreate(Bundle savedInstanceState) {
217         super.onCreate(savedInstanceState);
218         mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
219         mCheckBoxInputPresets = (CheckBox)findViewById(R.id.checkbox_paths_input_presets);
220         mCheckBoxInputDevices = (CheckBox)findViewById(R.id.checkbox_paths_input_devices);
221         mCheckBoxOutputDevices = (CheckBox)findViewById(R.id.checkbox_paths_output_devices);
222     }
223 
224     @Override
getTestName()225     public String getTestName() {
226         return "DataPaths";
227     }
228 
229     @Override
getActivityType()230     int getActivityType() {
231         return ACTIVITY_DATA_PATHS;
232     }
233 
getMagnitudeText(double value)234     static String getMagnitudeText(double value) {
235         return String.format(MAGNITUDE_FORMAT, value);
236     }
237 
getConfigText(StreamConfiguration config)238     protected String getConfigText(StreamConfiguration config) {
239         String text = super.getConfigText(config);
240         if (config.getDirection() == StreamConfiguration.DIRECTION_INPUT) {
241             text += ", inPre = " + StreamConfiguration.convertInputPresetToText(config.getInputPreset());
242         }
243         return text;
244     }
245 
246     @Override
shouldTestBeSkipped()247     protected String shouldTestBeSkipped() {
248         String why = "";
249         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
250         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
251         StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
252         StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
253         // No point running the test if we don't get the data path we requested.
254         if (actualInConfig.isMMap() != requestedInConfig.isMMap()) {
255             log("Did not get requested MMap input stream");
256             why += "mmap";
257         }
258         if (actualOutConfig.isMMap() != requestedOutConfig.isMMap()) {
259             log("Did not get requested MMap output stream");
260             why += "mmap";
261         }
262         // Did we request a device and not get that device?
263         if (requestedInConfig.getDeviceId() != 0
264                 && (requestedInConfig.getDeviceId() != actualInConfig.getDeviceId())) {
265             why += ", inDev(" + requestedInConfig.getDeviceId()
266                     + "!=" + actualInConfig.getDeviceId() + ")";
267         }
268         if (requestedOutConfig.getDeviceId() != 0
269                 && (requestedOutConfig.getDeviceId() != actualOutConfig.getDeviceId())) {
270             why += ", outDev(" + requestedOutConfig.getDeviceId()
271                     + "!=" + actualOutConfig.getDeviceId() + ")";
272         }
273         if ((requestedInConfig.getInputPreset() != actualInConfig.getInputPreset())) {
274             why += ", inPre(" + requestedInConfig.getInputPreset()
275                     + "!=" + actualInConfig.getInputPreset() + ")";
276         }
277         return why;
278     }
279 
280     @Override
isFinishedEarly()281     protected boolean isFinishedEarly() {
282         return (mMaxMagnitude > MIN_REQUIRED_MAGNITUDE)
283                 && (getAveragePhaseError() < MAX_ALLOWED_JITTER)
284                 && isPhaseJitterValid();
285     }
286 
287     // @return reasons for failure of empty string
288     @Override
didTestFail()289     public String didTestFail() {
290         String why = "";
291         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
292         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
293         StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
294         StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
295         if (mMaxMagnitude <= MIN_REQUIRED_MAGNITUDE) {
296             why += ", mag";
297         }
298         if (!isPhaseJitterValid()) {
299             why += ", jitterUnknown";
300         } else if (getAveragePhaseError() > MAX_ALLOWED_JITTER) {
301             why += ", jitterHigh";
302         }
303         return why;
304     }
305 
getAveragePhaseError()306     private double getAveragePhaseError() {
307         // If we have no measurements then return maximum possible phase jitter
308         // to avoid dividing by zero.
309         return (mPhaseErrorCount > 0) ? (mPhaseErrorSum / mPhaseErrorCount) : Math.PI;
310     }
311 
isPhaseJitterValid()312     private boolean isPhaseJitterValid() {
313         // Arbitrary number of measurements to be considered valid.
314         final int kMinPhaseErrorCount = 5;
315         return mPhaseErrorCount >= kMinPhaseErrorCount;
316     }
317 
getOneLineSummary()318     String getOneLineSummary() {
319         StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
320         StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
321         return "#" + mAutomatedTestRunner.getTestCount()
322                 + ", IN" + (actualInConfig.isMMap() ? "-M" : "-L")
323                 + " D=" + actualInConfig.getDeviceId()
324                 + ", ch=" + channelText(getInputChannel(), actualInConfig.getChannelCount())
325                 + ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L")
326                 + " D=" + actualOutConfig.getDeviceId()
327                 + ", ch=" + channelText(getOutputChannel(), actualOutConfig.getChannelCount())
328                 + ", mag = " + getMagnitudeText(mMaxMagnitude);
329     }
330 
setupDeviceCombo(int numInputChannels, int inputChannel, int numOutputChannels, int outputChannel)331     void setupDeviceCombo(int numInputChannels,
332                           int inputChannel,
333                           int numOutputChannels,
334                           int outputChannel) throws InterruptedException {
335         // Configure settings
336         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
337         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
338 
339         requestedInConfig.reset();
340         requestedOutConfig.reset();
341 
342         requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
343         requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
344 
345         requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
346         requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
347 
348         requestedInConfig.setChannelCount(numInputChannels);
349         requestedOutConfig.setChannelCount(numOutputChannels);
350 
351         requestedInConfig.setMMap(false);
352         requestedOutConfig.setMMap(false);
353 
354         setInputChannel(inputChannel);
355         setOutputChannel(outputChannel);
356     }
357 
testConfigurationsAddMagJitter()358     private TestResult testConfigurationsAddMagJitter() throws InterruptedException {
359         TestResult testResult = testConfigurations();
360         if (testResult != null) {
361             testResult.addComment("mag = " + TestDataPathsActivity.getMagnitudeText(mMagnitude)
362                     + ", jitter = " + getJitterText());
363         }
364         return testResult;
365     }
366 
testPresetCombo(int inputPreset, int numInputChannels, int inputChannel, int numOutputChannels, int outputChannel, boolean mmapEnabled )367     void testPresetCombo(int inputPreset,
368                          int numInputChannels,
369                          int inputChannel,
370                          int numOutputChannels,
371                          int outputChannel,
372                          boolean mmapEnabled
373                    ) throws InterruptedException {
374         setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, outputChannel);
375 
376         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
377         requestedInConfig.setInputPreset(inputPreset);
378         requestedInConfig.setMMap(mmapEnabled);
379 
380         mMagnitude = -1.0;
381         TestResult testResult = testConfigurationsAddMagJitter();
382         if (testResult != null) {
383             int result = testResult.result;
384             String summary = getOneLineSummary()
385                     + ", inPre = "
386                     + StreamConfiguration.convertInputPresetToText(inputPreset)
387                     + "\n";
388             appendSummary(summary);
389             if (result == TEST_RESULT_FAILED) {
390                 if (getMagnitude() < 0.000001) {
391                     testResult.addComment("The input is completely SILENT!");
392                 } else if (inputPreset == StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION) {
393                     testResult.addComment("Maybe sine wave blocked by Echo Cancellation!");
394                 }
395             }
396         }
397     }
398 
testPresetCombo(int inputPreset, int numInputChannels, int inputChannel, int numOutputChannels, int outputChannel )399     void testPresetCombo(int inputPreset,
400                          int numInputChannels,
401                          int inputChannel,
402                          int numOutputChannels,
403                          int outputChannel
404     ) throws InterruptedException {
405         if (NativeEngine.isMMapSupported()) {
406             testPresetCombo(inputPreset, numInputChannels, inputChannel,
407                     numOutputChannels, outputChannel, true);
408         }
409         testPresetCombo(inputPreset, numInputChannels, inputChannel,
410                 numOutputChannels, outputChannel, false);
411     }
412 
testPresetCombo(int inputPreset)413     void testPresetCombo(int inputPreset) throws InterruptedException {
414         setTestName("Test InPreset = " + StreamConfiguration.convertInputPresetToText(inputPreset));
415         testPresetCombo(inputPreset, 1, 0, 1, 0);
416     }
417 
testInputPresets()418     private void testInputPresets() throws InterruptedException {
419         logBoth("\nTest InputPreset -------");
420 
421         for (int inputPreset : INPUT_PRESETS) {
422             testPresetCombo(inputPreset);
423         }
424 // TODO Resolve issue with echo cancellation killing the signal.
425 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
426 //                1, 0, 2, 0);
427 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
428 //                1, 0, 2, 1);
429 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
430 //                2, 0, 2, 0);
431 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
432 //                2, 0, 2, 1);
433     }
434 
testInputDeviceCombo(int deviceId, int numInputChannels, int inputChannel, boolean mmapEnabled)435     void testInputDeviceCombo(int deviceId,
436                               int numInputChannels,
437                               int inputChannel,
438                               boolean mmapEnabled) throws InterruptedException {
439         final int numOutputChannels = 2;
440         setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, 0);
441 
442         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
443         requestedInConfig.setInputPreset(StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION);
444         requestedInConfig.setDeviceId(deviceId);
445         requestedInConfig.setMMap(mmapEnabled);
446 
447         mMagnitude = -1.0;
448         TestResult testResult = testConfigurationsAddMagJitter();
449         if (testResult != null) {
450             appendSummary(getOneLineSummary() + "\n");
451         }
452     }
453 
testInputDeviceCombo(int deviceId, int deviceType, int numInputChannels, int inputChannel)454     void testInputDeviceCombo(int deviceId,
455                               int deviceType,
456                               int numInputChannels,
457                               int inputChannel) throws InterruptedException {
458 
459         String typeString = AudioDeviceInfoConverter.typeToString(deviceType);
460         setTestName("Test InDev: #" + deviceId + " " + typeString
461                 + "_" + inputChannel + "/" + numInputChannels);
462         if (NativeEngine.isMMapSupported()) {
463             testInputDeviceCombo(deviceId, numInputChannels, inputChannel, true);
464         }
465         testInputDeviceCombo(deviceId, numInputChannels, inputChannel, false);
466     }
467 
testInputDevices()468     void testInputDevices() throws InterruptedException {
469         logBoth("\nTest Input Devices -------");
470 
471         AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
472         int numTested = 0;
473         for (AudioDeviceInfo deviceInfo : devices) {
474             log("----\n"
475                     + AudioDeviceInfoConverter.toString(deviceInfo) + "\n");
476             if (!deviceInfo.isSource()) continue; // FIXME log as error?!
477             int deviceType = deviceInfo.getType();
478             if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
479                 int id = deviceInfo.getId();
480                 int[] channelCounts = deviceInfo.getChannelCounts();
481                 numTested++;
482                 // Always test mono and stereo.
483                 testInputDeviceCombo(id, deviceType, 1, 0);
484                 testInputDeviceCombo(id, deviceType, 2, 0);
485                 testInputDeviceCombo(id, deviceType, 2, 1);
486                 if (channelCounts.length > 0) {
487                     for (int numChannels : channelCounts) {
488                         // Test higher channel counts.
489                         if (numChannels > 2) {
490                             log("numChannels = " + numChannels + "\n");
491                             for (int channel = 0; channel < numChannels; channel++) {
492                                 testInputDeviceCombo(id, deviceType, numChannels, channel);
493                             }
494                         }
495                     }
496                 }
497             } else {
498                 log("Device skipped for type.");
499             }
500         }
501 
502         if (numTested == 0) {
503             log("NO INPUT DEVICE FOUND!\n");
504         }
505     }
506 
testOutputDeviceCombo(int deviceId, int deviceType, int numOutputChannels, int outputChannel, boolean mmapEnabled)507     void testOutputDeviceCombo(int deviceId,
508                                int deviceType,
509                                int numOutputChannels,
510                                int outputChannel,
511                                boolean mmapEnabled) throws InterruptedException {
512         final int numInputChannels = 2; // TODO review, done because of mono problems on some devices
513         setupDeviceCombo(numInputChannels, 0, numOutputChannels, outputChannel);
514 
515         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
516         requestedOutConfig.setDeviceId(deviceId);
517         requestedOutConfig.setMMap(mmapEnabled);
518 
519         mMagnitude = -1.0;
520         TestResult testResult = testConfigurationsAddMagJitter();
521         if (testResult != null) {
522             int result = testResult.result;
523             appendSummary(getOneLineSummary() + "\n");
524             if (result == TEST_RESULT_FAILED) {
525                 if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
526                         && numOutputChannels == 2
527                         && outputChannel == 1) {
528                     testResult.addComment("Maybe EARPIECE does not mix stereo to mono!");
529                 }
530                 if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE
531                         && numOutputChannels == 2
532                         && outputChannel == 0) {
533                     testResult.addComment("Maybe SPEAKER_SAFE dropped channel zero!");
534                 }
535             }
536         }
537     }
538 
testOutputDeviceCombo(int deviceId, int deviceType, int numOutputChannels, int outputChannel)539     void testOutputDeviceCombo(int deviceId,
540                                int deviceType,
541                                int numOutputChannels,
542                                int outputChannel) throws InterruptedException {
543         String typeString = AudioDeviceInfoConverter.typeToString(deviceType);
544         setTestName("Test OutDev: #" + deviceId + " " + typeString
545                 + "_" + outputChannel + "/" + numOutputChannels);
546         if (NativeEngine.isMMapSupported()) {
547             testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, true);
548         }
549         testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, false);
550     }
551 
logBoth(String text)552     void logBoth(String text) {
553         log(text);
554         appendSummary(text + "\n");
555     }
556 
logFailed(String text)557     void logFailed(String text) {
558         log(text);
559         logAnalysis(text + "\n");
560     }
561 
testOutputDevices()562     void testOutputDevices() throws InterruptedException {
563         logBoth("\nTest Output Devices -------");
564 
565         AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
566         int numTested = 0;
567         for (AudioDeviceInfo deviceInfo : devices) {
568             log("----\n"
569                     + AudioDeviceInfoConverter.toString(deviceInfo) + "\n");
570             if (!deviceInfo.isSink()) continue;
571             int deviceType = deviceInfo.getType();
572             if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
573                 || deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
574                 || deviceType == TYPE_BUILTIN_SPEAKER_SAFE) {
575                 int id = deviceInfo.getId();
576                 int[] channelCounts = deviceInfo.getChannelCounts();
577                 numTested++;
578                 // Always test mono and stereo.
579                 testOutputDeviceCombo(id, deviceType, 1, 0);
580                 testOutputDeviceCombo(id, deviceType, 2, 0);
581                 testOutputDeviceCombo(id, deviceType, 2, 1);
582                 if (channelCounts.length > 0) {
583                     for (int numChannels : channelCounts) {
584                         // Test higher channel counts.
585                         if (numChannels > 2) {
586                             log("numChannels = " + numChannels + "\n");
587                             for (int channel = 0; channel < numChannels; channel++) {
588                                 testOutputDeviceCombo(id, deviceType, numChannels, channel);
589                             }
590                         }
591                     }
592                 }
593             } else {
594                 log("Device skipped for type.");
595             }
596         }
597         if (numTested == 0) {
598             log("NO OUTPUT DEVICE FOUND!\n");
599         }
600     }
601 
602     @Override
runTest()603     public void runTest() {
604         try {
605             logDeviceInfo();
606             log("min.required.magnitude = " + MIN_REQUIRED_MAGNITUDE);
607             log("max.allowed.jitter = " + MAX_ALLOWED_JITTER);
608             log("test.gap.msec = " + mGapMillis);
609 
610             mTestResults.clear();
611             mDurationSeconds = DURATION_SECONDS;
612 
613             if (mCheckBoxInputPresets.isChecked()) {
614                 runOnUiThread(() -> mCheckBoxInputPresets.setEnabled(false));
615                 testInputPresets();
616             }
617             if (mCheckBoxInputDevices.isChecked()) {
618                 runOnUiThread(() -> mCheckBoxInputDevices.setEnabled(false));
619                 testInputDevices();
620             }
621             if (mCheckBoxOutputDevices.isChecked()) {
622                 runOnUiThread(() -> mCheckBoxOutputDevices.setEnabled(false));
623                 testOutputDevices();
624             }
625 
626             analyzeTestResults();
627 
628         } catch (InterruptedException e) {
629             analyzeTestResults();
630         } catch (Exception e) {
631             log(e.getMessage());
632             showErrorToast(e.getMessage());
633         } finally {
634             runOnUiThread(() -> {
635                 mCheckBoxInputPresets.setEnabled(true);
636                 mCheckBoxInputDevices.setEnabled(true);
637                 mCheckBoxOutputDevices.setEnabled(true);
638             });
639         }
640     }
641 
642     @Override
startTestUsingBundle()643     public void startTestUsingBundle() {
644         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
645         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
646         configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig);
647 
648         boolean shouldUseInputPresets = mBundleFromIntent.getBoolean(KEY_USE_INPUT_PRESETS,
649                 VALUE_DEFAULT_USE_INPUT_PRESETS);
650         boolean shouldUseInputDevices = mBundleFromIntent.getBoolean(KEY_USE_INPUT_DEVICES,
651                 VALUE_DEFAULT_USE_INPUT_DEVICES);
652         boolean shouldUseOutputDevices = mBundleFromIntent.getBoolean(KEY_USE_OUTPUT_DEVICES,
653                 VALUE_DEFAULT_USE_OUTPUT_DEVICES);
654         int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX,
655                 VALUE_DEFAULT_SINGLE_TEST_INDEX);
656 
657         runOnUiThread(() -> {
658             mCheckBoxInputPresets.setChecked(shouldUseInputPresets);
659             mCheckBoxInputDevices.setChecked(shouldUseInputDevices);
660             mCheckBoxOutputDevices.setChecked(shouldUseOutputDevices);
661             mAutomatedTestRunner.setTestIndexText(singleTestIndex);
662         });
663 
664         mAutomatedTestRunner.startTest();
665     }
666 }
667