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.google.sample.oboe.manualtest;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.media.AudioDeviceInfo;
22 import android.media.AudioManager;
23 import android.os.Bundle;
24 
25 import com.google.sample.audio_device.AudioDeviceInfoConverter;
26 
27 /**
28  * Play a recognizable tone on each channel of each speaker device
29  * and listen for the result through a microphone.
30  * Also test each microphone channel and device.
31  * Try each InputPreset.
32  *
33  * The analysis is based on a cosine transform of a single
34  * frequency. The magnitude indicates the level.
35  * The variations in phase, "jitter" indicate how noisy the
36  * signal is or whether it is corrupted. A noisy room may have
37  * energy at the target frequency but the phase will be random.
38  *
39  * This test requires a quiet room but no other hardware.
40  */
41 public class TestDataPathsActivity  extends BaseAutoGlitchActivity {
42 
43     public static final int DURATION_SECONDS = 3;
44     private final static double MIN_REQUIRED_MAGNITUDE = 0.001;
45     private final static double MAX_SINE_FREQUENCY = 1000.0;
46     private final static int TYPICAL_SAMPLE_RATE = 48000;
47     private final static double FRAMES_PER_CYCLE = TYPICAL_SAMPLE_RATE / MAX_SINE_FREQUENCY;
48     private final static double PHASE_PER_BIN = 2.0 * Math.PI / FRAMES_PER_CYCLE;
49     private final static double MAX_ALLOWED_JITTER = 0.5 * PHASE_PER_BIN;
50     // Start by failing then let good results drive us into a pass value.
51     private final static double INITIAL_JITTER = 2.0 * MAX_ALLOWED_JITTER;
52     // A coefficient of 0.0 is no filtering. 0.9999 is extreme low pass.
53     private final static double JITTER_FILTER_COEFFICIENT = 0.8;
54     private final static String MAGNITUDE_FORMAT = "%7.5f";
55 
56     final int TYPE_BUILTIN_SPEAKER_SAFE = 0x18; // API 30
57 
58     private double mMagnitude;
59     private double mMaxMagnitude;
60     private int    mPhaseCount;
61     private double mPhase;
62     private double mPhaseJitter;
63 
64     AudioManager   mAudioManager;
65 
66     private static final int[] INPUT_PRESETS = {
67             // VOICE_RECOGNITION gets tested in testInputs()
68             // StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION,
69             StreamConfiguration.INPUT_PRESET_GENERIC,
70             StreamConfiguration.INPUT_PRESET_CAMCORDER,
71             // TODO Resolve issue with echo cancellation killing the signal.
72             // TODO StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
73             StreamConfiguration.INPUT_PRESET_UNPROCESSED,
74             StreamConfiguration.INPUT_PRESET_VOICE_PERFORMANCE,
75     };
76 
77     // Periodically query for magnitude and phase from the native detector.
78     protected class DataPathSniffer extends NativeSniffer {
79 
DataPathSniffer(Activity activity)80         public DataPathSniffer(Activity activity) {
81             super(activity);
82         }
83 
84         @Override
startSniffer()85         public void startSniffer() {
86             mMagnitude = -1.0;
87             mMaxMagnitude = -1.0;
88             mPhaseCount = 0;
89             mPhase = 0.0;
90             mPhaseJitter = INITIAL_JITTER;
91             super.startSniffer();
92         }
93 
94         @Override
run()95         public void run() {
96             mMagnitude = getMagnitude();
97             mMaxMagnitude = getMaxMagnitude();
98             // Only look at the phase if we have a signal.
99             if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) {
100                 double phase = getPhase();
101                 if (mPhaseCount > 3) {
102                     double diff = Math.abs(phase - mPhase);
103                     // low pass filter
104                     mPhaseJitter = (mPhaseJitter * JITTER_FILTER_COEFFICIENT)
105                             + ((diff * (1.0 - JITTER_FILTER_COEFFICIENT)));
106                 }
107                 mPhase = phase;
108                 mPhaseCount++;
109             }
110             reschedule();
111         }
112 
getCurrentStatusReport()113         public String getCurrentStatusReport() {
114             StringBuffer message = new StringBuffer();
115             message.append(
116                     "magnitude = " + getMagnitudeText(mMagnitude)
117                     + ", max = " + getMagnitudeText(mMaxMagnitude)
118                     + "\nphase = " + getMagnitudeText(mPhase)
119                     + ", jitter = " + getMagnitudeText(mPhaseJitter)
120                     + "\n");
121             return message.toString();
122         }
123 
124         @Override
getShortReport()125         public String getShortReport() {
126             return "maxMag = " + getMagnitudeText(mMaxMagnitude)
127                     + ", jitter = " + getMagnitudeText(mPhaseJitter);
128         }
129 
130         @Override
updateStatusText()131         public void updateStatusText() {
132             mLastGlitchReport = getCurrentStatusReport();
133             setAnalyzerText(mLastGlitchReport);
134         }
135 
136     }
137 
138     @Override
createNativeSniffer()139     NativeSniffer createNativeSniffer() {
140         return new TestDataPathsActivity.DataPathSniffer(this);
141     }
142 
getMagnitude()143     native double getMagnitude();
getMaxMagnitude()144     native double getMaxMagnitude();
getPhase()145     native double getPhase();
146 
147     @Override
inflateActivity()148     protected void inflateActivity() {
149         setContentView(R.layout.activity_data_paths);
150     }
151 
152     @Override
onCreate(Bundle savedInstanceState)153     protected void onCreate(Bundle savedInstanceState) {
154         super.onCreate(savedInstanceState);
155         mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
156     }
157 
158     @Override
getTestName()159     public String getTestName() {
160         return "DataPaths";
161     }
162 
163     @Override
getActivityType()164     int getActivityType() {
165         return ACTIVITY_DATA_PATHS;
166     }
167 
getMagnitudeText(double value)168     String getMagnitudeText(double value) {
169         return String.format(MAGNITUDE_FORMAT, value);
170     }
171 
getConfigText(StreamConfiguration config)172     protected String getConfigText(StreamConfiguration config) {
173         String text = super.getConfigText(config);
174         if (config.getDirection() == StreamConfiguration.DIRECTION_INPUT) {
175             text += ", inPre = " + StreamConfiguration.convertInputPresetToText(config.getInputPreset());
176         }
177         return text;
178     }
179 
180     @Override
shouldTestBeSkipped()181     protected String shouldTestBeSkipped() {
182         String why = "";
183         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
184         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
185         StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
186         StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
187         // No point running the test if we don't get the sharing mode we requested.
188         if (actualInConfig.isMMap() != requestedInConfig.isMMap()
189                 || actualOutConfig.isMMap() != requestedOutConfig.isMMap()) {
190             log("Did not get requested MMap stream");
191             why += "mmap";
192         }        // Did we request a device and not get that device?
193         if (requestedInConfig.getDeviceId() != 0
194                 && (requestedInConfig.getDeviceId() != actualInConfig.getDeviceId())) {
195             why += ", inDev(" + requestedInConfig.getDeviceId()
196                     + "!=" + actualInConfig.getDeviceId() + ")";
197         }
198         if (requestedOutConfig.getDeviceId() != 0
199                 && (requestedOutConfig.getDeviceId() != actualOutConfig.getDeviceId())) {
200             why += ", outDev(" + requestedOutConfig.getDeviceId()
201                     + "!=" + actualOutConfig.getDeviceId() + ")";
202         }
203         if ((requestedInConfig.getInputPreset() != actualInConfig.getInputPreset())) {
204             why += ", inPre(" + requestedInConfig.getInputPreset()
205                     + "!=" + actualInConfig.getInputPreset() + ")";
206         }
207         return why;
208     }
209 
210     @Override
isFinishedEarly()211     protected boolean isFinishedEarly() {
212         return (mMaxMagnitude > MIN_REQUIRED_MAGNITUDE) && (mPhaseJitter < MAX_ALLOWED_JITTER);
213     }
214 
215     // @return reasons for failure of empty string
216     @Override
didTestFail()217     public String didTestFail() {
218         String why = "";
219         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
220         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
221         StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
222         StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
223         boolean passed = true;
224         if (mMaxMagnitude <= MIN_REQUIRED_MAGNITUDE) {
225             why += ", mag";
226         }
227         if (mPhaseJitter > MAX_ALLOWED_JITTER) {
228             why += ", jitter";
229         }
230         return why;
231     }
232 
getOneLineSummary()233     String getOneLineSummary() {
234         StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
235         StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
236         return "#" + mAutomatedTestRunner.getTestCount()
237                 + ", IN" + (actualInConfig.isMMap() ? "-M" : "-L")
238                 + " D=" + actualInConfig.getDeviceId()
239                 + ", ch=" + actualInConfig.getChannelCount() + "[" + getInputChannel() + "]"
240                 + ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L")
241                 + " D=" + (actualOutConfig.isMMap() ? "-M" : "-L")
242                 + ", ch=" + actualOutConfig.getChannelCount() + "[" + getOutputChannel() + "]"
243                 + ", mag = " + getMagnitudeText(mMaxMagnitude);
244     }
245 
setupDeviceCombo(int numInputChannels, int inputChannel, int numOutputChannels, int outputChannel)246     void setupDeviceCombo(int numInputChannels,
247                           int inputChannel,
248                           int numOutputChannels,
249                           int outputChannel) throws InterruptedException {
250         // Configure settings
251         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
252         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
253 
254         requestedInConfig.reset();
255         requestedOutConfig.reset();
256 
257         requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
258         requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
259 
260         requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
261         requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
262 
263         requestedInConfig.setChannelCount(numInputChannels);
264         requestedOutConfig.setChannelCount(numOutputChannels);
265 
266         requestedInConfig.setMMap(false);
267         requestedOutConfig.setMMap(false);
268 
269         setInputChannel(inputChannel);
270         setOutputChannel(outputChannel);
271     }
272 
testPresetCombo(int inputPreset, int numInputChannels, int inputChannel, int numOutputChannels, int outputChannel, boolean mmapEnabled )273     void testPresetCombo(int inputPreset,
274                          int numInputChannels,
275                          int inputChannel,
276                          int numOutputChannels,
277                          int outputChannel,
278                          boolean mmapEnabled
279                    ) throws InterruptedException {
280 
281         setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, outputChannel);
282 
283         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
284         requestedInConfig.setInputPreset(inputPreset);
285         requestedInConfig.setMMap(mmapEnabled);
286 
287         mMagnitude = -1.0;
288         int result = testConfigurations();
289         if (result != TEST_RESULT_SKIPPED) {
290             String summary = getOneLineSummary()
291                     + ", inPre = "
292                     + StreamConfiguration.convertInputPresetToText(inputPreset)
293                     + "\n";
294             appendSummary(summary);
295             if (result == TEST_RESULT_FAILED) {
296                 if (inputPreset == StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION) {
297                     logFailed("Maybe sine wave blocked by Echo Cancellation!");
298                 }
299             }
300         }
301     }
302 
testPresetCombo(int inputPreset, int numInputChannels, int inputChannel, int numOutputChannels, int outputChannel )303     void testPresetCombo(int inputPreset,
304                          int numInputChannels,
305                          int inputChannel,
306                          int numOutputChannels,
307                          int outputChannel
308     ) throws InterruptedException {
309         if (NativeEngine.isMMapSupported()) {
310             testPresetCombo(inputPreset, numInputChannels, inputChannel,
311                     numOutputChannels, outputChannel, true);
312         }
313         testPresetCombo(inputPreset, numInputChannels, inputChannel,
314                 numOutputChannels, outputChannel, false);
315     }
316 
testPresetCombo(int inputPreset)317     void testPresetCombo(int inputPreset) throws InterruptedException {
318         testPresetCombo(inputPreset, 1, 0, 1, 0);
319     }
320 
testInputPresets()321     private void testInputPresets() throws InterruptedException {
322         logBoth("\nTest InputPreset -------");
323 
324         for (int inputPreset : INPUT_PRESETS) {
325             testPresetCombo(inputPreset);
326         }
327 // TODO Resolve issue with echo cancellation killing the signal.
328 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
329 //                1, 0, 2, 0);
330 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
331 //                1, 0, 2, 1);
332 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
333 //                2, 0, 2, 0);
334 //        testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
335 //                2, 0, 2, 1);
336     }
337 
testInputDeviceCombo(int deviceId, int numInputChannels, int inputChannel, boolean mmapEnabled)338     void testInputDeviceCombo(int deviceId,
339                               int numInputChannels,
340                               int inputChannel,
341                               boolean mmapEnabled) throws InterruptedException {
342         final int numOutputChannels = 2;
343         setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, 0);
344 
345         StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
346         requestedInConfig.setInputPreset(StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION);
347         requestedInConfig.setDeviceId(deviceId);
348         requestedInConfig.setMMap(mmapEnabled);
349 
350         mMagnitude = -1.0;
351         int result = testConfigurations();
352         if (result != TEST_RESULT_SKIPPED) {
353             appendSummary(getOneLineSummary() + "\n");
354         }
355     }
356 
testInputDeviceCombo(int deviceId, int numInputChannels, int inputChannel)357     void testInputDeviceCombo(int deviceId,
358                               int numInputChannels,
359                               int inputChannel) throws InterruptedException {
360         if (NativeEngine.isMMapSupported()) {
361             testInputDeviceCombo(deviceId, numInputChannels, inputChannel, true);
362         }
363         testInputDeviceCombo(deviceId, numInputChannels, inputChannel, false);
364     }
365 
testInputDevices()366     void testInputDevices() throws InterruptedException {
367         logBoth("\nTest Input Devices -------");
368 
369         AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
370         int numTested = 0;
371         for (AudioDeviceInfo deviceInfo : devices) {
372             log("----\n"
373                     + AudioDeviceInfoConverter.toString(deviceInfo) + "\n");
374             if (!deviceInfo.isSource()) continue; // FIXME log as error?!
375             if (deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
376                 int id = deviceInfo.getId();
377                 int[] channelCounts = deviceInfo.getChannelCounts();
378                 numTested++;
379                 // Always test mono and stereo.
380                 testInputDeviceCombo(id, 1, 0);
381                 testInputDeviceCombo(id, 2, 0);
382                 testInputDeviceCombo(id, 2, 1);
383                 if (channelCounts.length > 0) {
384                     for (int numChannels : channelCounts) {
385                         // Test higher channel counts.
386                         if (numChannels > 2) {
387                             log("numChannels = " + numChannels + "\n");
388                             for (int channel = 0; channel < numChannels; channel++) {
389                                 testInputDeviceCombo(id, numChannels, channel);
390                             }
391                         }
392                     }
393                 }
394             } else {
395                 log("Device skipped for type.");
396             }
397         }
398 
399         if (numTested == 0) {
400             log("NO INPUT DEVICE FOUND!\n");
401         }
402     }
403 
testOutputDeviceCombo(int deviceId, int deviceType, int numOutputChannels, int outputChannel, boolean mmapEnabled)404     void testOutputDeviceCombo(int deviceId,
405                                int deviceType,
406                                int numOutputChannels,
407                                int outputChannel,
408                                boolean mmapEnabled) throws InterruptedException {
409         final int numInputChannels = 2; // TODO review, done because of mono problems on some devices
410         setupDeviceCombo(numInputChannels, 0, numOutputChannels, outputChannel);
411 
412         StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
413         requestedOutConfig.setDeviceId(deviceId);
414         requestedOutConfig.setMMap(mmapEnabled);
415 
416         mMagnitude = -1.0;
417         int result = testConfigurations();
418         if (result != TEST_RESULT_SKIPPED) {
419             appendSummary(getOneLineSummary() + "\n");
420             if (result == TEST_RESULT_FAILED) {
421                 if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
422                         && numOutputChannels == 2
423                         && outputChannel == 1) {
424                     logFailed("Maybe EARPIECE does not mix stereo to mono!");
425                 }
426                 if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE
427                         && numOutputChannels == 2
428                         && outputChannel == 0) {
429                     logFailed("Maybe SPEAKER_SAFE blocked channel 0!");
430                 }
431             }
432         }
433     }
434 
testOutputDeviceCombo(int deviceId, int deviceType, int numOutputChannels, int outputChannel)435     void testOutputDeviceCombo(int deviceId,
436                                int deviceType,
437                                int numOutputChannels,
438                                int outputChannel) throws InterruptedException {
439         if (NativeEngine.isMMapSupported()) {
440             testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, true);
441         }
442         testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, false);
443     }
444 
logBoth(String text)445     void logBoth(String text) {
446         log(text);
447         appendSummary(text + "\n");
448     }
logFailed(String text)449     void logFailed(String text) {
450         log(text);
451         appendFailedSummary(text + "\n");
452     }
453 
testOutputDevices()454     void testOutputDevices() throws InterruptedException {
455         logBoth("\nTest Output Devices -------");
456 
457         AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
458         int numTested = 0;
459         for (AudioDeviceInfo deviceInfo : devices) {
460             log("----\n"
461                     + AudioDeviceInfoConverter.toString(deviceInfo) + "\n");
462             if (!deviceInfo.isSink()) continue;
463             int deviceType = deviceInfo.getType();
464             if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
465                 || deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
466                 || deviceType == TYPE_BUILTIN_SPEAKER_SAFE) {
467                 int id = deviceInfo.getId();
468                 int[] channelCounts = deviceInfo.getChannelCounts();
469                 numTested++;
470                 // Always test mono and stereo.
471                 testOutputDeviceCombo(id, deviceType, 1, 0);
472                 testOutputDeviceCombo(id, deviceType, 2, 0);
473                 testOutputDeviceCombo(id, deviceType, 2, 1);
474                 if (channelCounts.length > 0) {
475                     for (int numChannels : channelCounts) {
476                         // Test higher channel counts.
477                         if (numChannels > 2) {
478                             log("numChannels = " + numChannels + "\n");
479                             for (int channel = 0; channel < numChannels; channel++) {
480                                 testOutputDeviceCombo(id, deviceType, numChannels, channel);
481                             }
482                         }
483                     }
484                 }
485             } else {
486                 log("Device skipped for type.");
487             }
488         }
489         if (numTested == 0) {
490             log("NO OUTPUT DEVICE FOUND!\n");
491         }
492     }
493 
494     @Override
runTest()495     public void runTest() {
496         try {
497             mDurationSeconds = DURATION_SECONDS;
498 
499             log("min.required.magnitude = " + MIN_REQUIRED_MAGNITUDE);
500             log("max.allowed.jitter = " + MAX_ALLOWED_JITTER);
501             log("test.gap.msec = " + mGapMillis);
502 
503             testInputPresets();
504             testInputDevices();
505             testOutputDevices();
506         } catch (Exception e) {
507             log(e.getMessage());
508             showErrorToast(e.getMessage());
509         }
510     }
511 
512 }
513