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