diff options
Diffstat (limited to 'apps/OboeTester/app/src/main/java/com')
50 files changed, 3379 insertions, 873 deletions
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java index 6e22da99..6444b2a7 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java @@ -16,13 +16,13 @@ package com.mobileer.audio_device; */ import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.mobileer.oboetester.R; diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java index d0228fdb..14c635d2 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java @@ -15,7 +15,13 @@ package com.mobileer.audio_device; * limitations under the License. */ +import android.media.AudioDescriptor; import android.media.AudioDeviceInfo; +import android.media.AudioProfile; +import android.os.Build; + +import java.util.List; +import java.util.Locale; public class AudioDeviceInfoConverter { @@ -49,11 +55,11 @@ public class AudioDeviceInfoConverter { sb.append("\nChannel masks: "); int[] channelMasks = adi.getChannelMasks(); - sb.append(intArrayToString(channelMasks)); + sb.append(intArrayToStringHex(channelMasks)); sb.append("\nChannel index masks: "); int[] channelIndexMasks = adi.getChannelIndexMasks(); - sb.append(intArrayToString(channelIndexMasks)); + sb.append(intArrayToStringHex(channelIndexMasks)); sb.append("\nEncodings: "); int[] encodings = adi.getEncodings(); @@ -62,6 +68,36 @@ public class AudioDeviceInfoConverter { sb.append("\nSample Rates: "); int[] sampleRates = adi.getSampleRates(); sb.append(intArrayToString(sampleRates)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + sb.append("\nAddress: "); + sb.append(adi.getAddress()); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + sb.append("\nEncapsulation Metadata Types: "); + int[] encapsulationMetadataTypes = adi.getEncapsulationMetadataTypes(); + sb.append(intArrayToString(encapsulationMetadataTypes)); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + sb.append("\nEncapsulation Modes: "); + int[] encapsulationModes = adi.getEncapsulationModes(); + sb.append(intArrayToString(encapsulationModes)); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + sb.append("\nAudio Descriptors: "); + List<AudioDescriptor> audioDescriptors = adi.getAudioDescriptors(); + sb.append(audioDescriptors); + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + sb.append("\nAudio Profiles: "); + List<AudioProfile> audioProfiles = adi.getAudioProfiles(); + sb.append(audioProfiles); + } + sb.append("\n"); return sb.toString(); } @@ -76,7 +112,22 @@ public class AudioDeviceInfoConverter { StringBuilder sb = new StringBuilder(); for (int i = 0; i < integerArray.length; i++){ sb.append(integerArray[i]); - if (i != integerArray.length -1) sb.append(" "); + if (i != integerArray.length - 1) sb.append(" "); + } + return sb.toString(); + } + + /** + * Converts an integer array into a hexadecimal string where each int is separated by a space + * + * @param integerArray the integer array to convert to a string + * @return string containing all the integer values separated by spaces + */ + private static String intArrayToStringHex(int[] integerArray){ + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < integerArray.length; i++){ + sb.append(String.format(Locale.getDefault(), "0x%02X", integerArray[i])); + if (i != integerArray.length - 1) sb.append(" "); } return sb.toString(); } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java index a0ea1837..baceb475 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java @@ -32,9 +32,16 @@ public class AudioDeviceListEntry { private int mId; private String mName; - public AudioDeviceListEntry(int deviceId, String deviceName){ + private AudioDeviceInfo mDeviceInfo; + + public AudioDeviceListEntry(int deviceId, String deviceName) { + this(deviceId, deviceName, null); + } + + public AudioDeviceListEntry(int deviceId, String deviceName, AudioDeviceInfo deviceInfo) { mId = deviceId; mName = deviceName; + mDeviceInfo = deviceInfo; } public int getId() { @@ -45,6 +52,8 @@ public class AudioDeviceListEntry { return mName; } + public AudioDeviceInfo getDeviceInfo() { return mDeviceInfo; } + public String toString(){ return getName(); } @@ -87,7 +96,8 @@ public class AudioDeviceListEntry { listEntries.add(new AudioDeviceListEntry(info.getId(), info.getId() + ": " + info.getProductName() + " " + - AudioDeviceInfoConverter.typeToString(info.getType()))); + AudioDeviceInfoConverter.typeToString(info.getType()), + info)); } } return listEntries; diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java index fcdcf412..46a924f5 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java @@ -29,8 +29,10 @@ import com.mobileer.oboetester.R; import java.util.List; public class CommunicationDeviceSpinner extends Spinner { - private static final int CLEAR_DEVICE_ID = 0; private static final String TAG = CommunicationDeviceSpinner.class.getName(); + // menu positions + public static final int POS_CLEAR = 0; + public static final int POS_DEVICES = 1; // base position for device list private AudioDeviceAdapter mDeviceAdapter; private AudioManager mAudioManager; private Context mContext; @@ -85,10 +87,9 @@ public class CommunicationDeviceSpinner extends Spinner { mDeviceAdapter = new AudioDeviceAdapter(context); setAdapter(mDeviceAdapter); - // Add a default entry to the list and select it - mDeviceAdapter.add(new AudioDeviceListEntry(CLEAR_DEVICE_ID, - mContext.getString(R.string.auto_select))); - setSelection(0); + // Add default entries to the list and select one. + addDefaultDevicesOptions(); + setSelection(POS_CLEAR); setupCommunicationDeviceListener(); } @@ -108,9 +109,8 @@ public class CommunicationDeviceSpinner extends Spinner { private void updateDeviceList() { mDeviceAdapter.clear(); - mDeviceAdapter.add(new AudioDeviceListEntry(CLEAR_DEVICE_ID, - mContext.getString(R.string.clear))); - setSelection(0); + addDefaultDevicesOptions(); + setSelection(POS_CLEAR); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { List<AudioDeviceInfo> commDeviceList = mAudioManager.getAvailableCommunicationDevices(); mCommDeviceArray = commDeviceList.toArray(new AudioDeviceInfo[0]); @@ -123,4 +123,9 @@ public class CommunicationDeviceSpinner extends Spinner { } }, null); } + + private void addDefaultDevicesOptions() { + mDeviceAdapter.add(new AudioDeviceListEntry(POS_CLEAR, + mContext.getString(R.string.clear_comm))); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java b/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java index 1e1c3a8b..da5d7512 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; +import java.util.Locale; /** * Very simple polyphonic, single channel synthesizer. It runs a background @@ -178,7 +179,7 @@ public class SynthEngine extends MidiReceiver { public void logMidiMessage(byte[] data, int offset, int count) { String text = "Received: "; for (int i = 0; i < count; i++) { - text += String.format("0x%02X, ", data[offset + i]); + text += String.format(Locale.getDefault(), "0x%02X, ", data[offset + i]); } Log.i(TAG, text); } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java index 368d5bc8..5e22c0bc 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java @@ -21,16 +21,17 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.util.Locale; /** * Activity to measure latency on a full duplex stream. @@ -78,8 +79,8 @@ public class AnalyzerActivity extends TestInputActivity { report.append("build.fingerprint = " + Build.FINGERPRINT + "\n"); try { PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0); - report.append(String.format("test.version = %s\n", pinfo.versionName)); - report.append(String.format("test.version.code = %d\n", pinfo.versionCode)); + report.append(String.format(Locale.getDefault(), "test.version = %s\n", pinfo.versionName)); + report.append(String.format(Locale.getDefault(), "test.version.code = %d\n", pinfo.versionCode)); } catch (PackageManager.NameNotFoundException e) { } report.append("time.millis = " + System.currentTimeMillis() + "\n"); @@ -87,18 +88,18 @@ public class AnalyzerActivity extends TestInputActivity { // INPUT report.append(mAudioInputTester.actualConfiguration.dump()); AudioStreamBase inStream = mAudioInputTester.getCurrentAudioStream(); - report.append(String.format("in.burst.frames = %d\n", inStream.getFramesPerBurst())); - report.append(String.format("in.xruns = %d\n", inStream.getXRunCount())); + report.append(String.format(Locale.getDefault(), "in.burst.frames = %d\n", inStream.getFramesPerBurst())); + report.append(String.format(Locale.getDefault(), "in.xruns = %d\n", inStream.getXRunCount())); // OUTPUT report.append(mAudioOutTester.actualConfiguration.dump()); AudioStreamBase outStream = mAudioOutTester.getCurrentAudioStream(); - report.append(String.format("out.burst.frames = %d\n", outStream.getFramesPerBurst())); + report.append(String.format(Locale.getDefault(), "out.burst.frames = %d\n", outStream.getFramesPerBurst())); int bufferSize = outStream.getBufferSizeInFrames(); - report.append(String.format("out.buffer.size.frames = %d\n", bufferSize)); + report.append(String.format(Locale.getDefault(), "out.buffer.size.frames = %d\n", bufferSize)); int bufferCapacity = outStream.getBufferCapacityInFrames(); - report.append(String.format("out.buffer.capacity.frames = %d\n", bufferCapacity)); - report.append(String.format("out.xruns = %d\n", outStream.getXRunCount())); + report.append(String.format(Locale.getDefault(), "out.buffer.capacity.frames = %d\n", bufferCapacity)); + report.append(String.format(Locale.getDefault(), "out.xruns = %d\n", outStream.getXRunCount())); return report.toString(); } @@ -160,11 +161,4 @@ public class AnalyzerActivity extends TestInputActivity { } } - private void writeTestInBackground(final String resultString) { - new Thread() { - public void run() { - writeTestResult(resultString); - } - }.start(); - } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java index 2c305fb5..04c3f2e3 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java @@ -51,6 +51,10 @@ public class AudioOutputTester extends AudioStreamTester { mOboeAudioOutputStream.setSignalType(type); } + public void setAmplitude(float amplitude) { + mOboeAudioOutputStream.setAmplitude(amplitude); + } + public int getLastErrorCallbackResult() {return mOboeAudioOutputStream.getLastErrorCallbackResult();}; public long getFramesRead() {return mOboeAudioOutputStream.getFramesRead();}; diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java index 421d098b..a4dd6fc4 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java @@ -23,6 +23,7 @@ import android.os.Build; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.util.Locale; public class AudioQueryTools { private static String GETPROP_EXECUTABLE_PATH = "/system/bin/getprop"; @@ -58,6 +59,10 @@ public class AudioQueryTools { + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)); report.append("\nLowLatency Feature : " + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)); + report.append("\nAudio Output Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)); + report.append("\nMicrophone Feature : " + + packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)); report.append("\nMIDI Feature : " + packageManager.hasSystemFeature(PackageManager.FEATURE_MIDI)); report.append("\nUSB Host Feature : " @@ -69,14 +74,24 @@ public class AudioQueryTools { public static String getAudioManagerReport(AudioManager audioManager) { StringBuffer report = new StringBuffer(); - String unprocessedSupport = audioManager.getParameters(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED); - report.append("\nSUPPORT_UNPROCESSED : " + ((unprocessedSupport == null) ? "null" : "yes")); + String unprocessedSupport = audioManager.getProperty( + AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED); + report.append("\nSUPPORT_AUDIO_SOURCE_UNPROCESSED : " + ((unprocessedSupport == null) ? + "null" : unprocessedSupport)); + String outputFramesPerBuffer = audioManager.getProperty( + AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + report.append("\nOUTPUT_FRAMES_PER_BUFFER : " + ((outputFramesPerBuffer == null) ? + "null" : outputFramesPerBuffer)); + String outputSampleRate = audioManager.getProperty( + AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + report.append("\nOUTPUT_SAMPLE_RATE : " + ((outputSampleRate == null) ? "null" : + outputSampleRate)); return report.toString(); } private static String formatKeyValueLine(String key, String value) { int numSpaces = Math.max(1, 21 - key.length()); - String spaces = String.format("%0" + numSpaces + "d", 0).replace("0", " "); + String spaces = String.format(Locale.getDefault(), "%0" + numSpaces + "d", 0).replace("0", " "); return "\n" + key + spaces + ": " + value; } @@ -112,6 +127,9 @@ public class AudioQueryTools { } public static String getMediaPerformanceClass() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) { + return formatKeyValueLine("Media Perf Class", "not supported"); + } int mpc = Build.VERSION.MEDIA_PERFORMANCE_CLASS; String text = (mpc == 0) ? "not declared" : convertSdkToShortName(mpc); return formatKeyValueLine("Media Perf Class", diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java index 7b472237..bc254552 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java @@ -17,6 +17,7 @@ package com.mobileer.oboetester; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; @@ -40,6 +41,8 @@ class AudioRecordThread implements Runnable { private int mTaskCountdown; private boolean mCaptureEnabled = true; + private AudioDeviceInfo mDeviceInfo; + public AudioRecordThread(int frameRate, int channelCount, int maxFrames) { mSampleRate = frameRate; mChannelCount = channelCount; @@ -59,6 +62,7 @@ class AudioRecordThread implements Runnable { channelConfig, audioFormat, 2 * minRecordBuffSizeInBytes); + mRecorder.setPreferredDevice(mDeviceInfo); if (mRecorder.getState() == AudioRecord.STATE_UNINITIALIZED) { throw new RuntimeException("Could not make the AudioRecord - UNINITIALIZED"); } @@ -159,4 +163,7 @@ class AudioRecordThread implements Runnable { return mCaptureBuffer.readMostRecent(buffer); } + public void setInputDevice(AudioDeviceInfo deviceInfo) { + mDeviceInfo = deviceInfo; + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java index b6eb6ff1..14eb163e 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java @@ -17,6 +17,7 @@ package com.mobileer.oboetester; import java.io.IOException; +import java.util.Locale; /** * Base class for any audio input or output. @@ -25,10 +26,43 @@ public abstract class AudioStreamBase { private StreamConfiguration mRequestedStreamConfiguration; private StreamConfiguration mActualStreamConfiguration; - AudioStreamBase.DoubleStatistics mLatencyStatistics; - + private AudioStreamBase.DoubleStatistics mLatencyStatistics; + private SampleRateMonitor mSampleRateMonitor = new SampleRateMonitor(); private int mBufferSizeInFrames; + private class SampleRateMonitor { + private static final int SIZE = 16; // power of 2 + private static final long MASK = SIZE - 1L; + private long[] times = new long[SIZE]; + private long[] frames = new long[SIZE]; + private long cursor; + + void add(long numFrames) { + int index = (int) (cursor & MASK); + frames[index] = numFrames; + times[index] = System.currentTimeMillis(); + cursor++; + } + + int getRate() { + if (cursor < 2) return 0; + long numValid = Math.min((long)SIZE, cursor); + int oldestIndex = (int)((cursor - numValid) & MASK); + int newestIndex = (int)((cursor - 1) & MASK); + long deltaTime = times[newestIndex] - times[oldestIndex]; + long deltaFrames = frames[newestIndex] - frames[oldestIndex]; + if (deltaTime <= 0) { + return -1; + } + long sampleRate = (deltaFrames * 1000) / deltaTime; + return (int) sampleRate; + } + + void reset() { + cursor = 0; + } + } + public StreamStatus getStreamStatus() { StreamStatus status = new StreamStatus(); status.bufferSize = getBufferSizeInFrames(); @@ -41,6 +75,8 @@ public abstract class AudioStreamBase { status.callbackTimeStr = getCallbackTimeStr(); status.cpuLoad = getCpuLoad(); status.state = getState(); + mSampleRateMonitor.add(status.framesRead); + status.measuredRate = mSampleRateMonitor.getRate(); return status; } @@ -48,6 +84,11 @@ public abstract class AudioStreamBase { return mLatencyStatistics; } + public void setPerformanceHintEnabled(boolean checked) { + } + public void setHearWorkload(boolean checked) { + } + public static class DoubleStatistics { private double sum; private int count; @@ -68,7 +109,7 @@ public abstract class AudioStreamBase { public String dump() { if (count == 0) return "?"; - return String.format("%3.1f/%3.1f/%3.1f ms", minimum, getAverage(), maximum); + return String.format(Locale.getDefault(), "%3.1f/%3.1f/%3.1f ms", minimum, getAverage(), maximum); } } @@ -84,8 +125,9 @@ public abstract class AudioStreamBase { public int state; public long callbackCount; public int framesPerCallback; - public double cpuLoad; + public float cpuLoad; public String callbackTimeStr; + public int measuredRate; // These are constantly changing. String dump(int framesPerBurst) { @@ -96,21 +138,22 @@ public abstract class AudioStreamBase { buffer.append("time between callbacks = " + callbackTimeStr + "\n"); - buffer.append("written " - + String.format("0x%08X", framesWritten) - + " - read " + String.format("0x%08X", framesRead) - + " = " + (framesWritten - framesRead) + " frames\n"); + buffer.append("wr " + + String.format(Locale.getDefault(), "%Xh", framesWritten) + + " - rd " + String.format(Locale.getDefault(), "%Xh", framesRead) + + " = " + (framesWritten - framesRead) + " fr" + + ", SR = " + ((measuredRate <= 0) ? "?" : measuredRate) + "\n"); - String cpuLoadText = String.format("%2d%c", (int)(cpuLoad * 100), '%'); + String cpuLoadText = String.format(Locale.getDefault(), "%2d%c", (int)(cpuLoad * 100), '%'); buffer.append( convertStateToString(state) + ", #cb=" + callbackCount - + ", f/cb=" + String.format("%3d", framesPerCallback) - + ", " + cpuLoadText + " cpu" + + ", f/cb=" + String.format(Locale.getDefault(), "%3d", framesPerCallback) + + ", " + cpuLoadText + " CPU" + "\n"); buffer.append("buffer size = "); - if (bufferSize < 0) { + if (bufferSize <= 0 || framesPerBurst <= 0) { buffer.append("?"); } else { int numBuffers = bufferSize / framesPerBurst; @@ -151,14 +194,19 @@ public abstract class AudioStreamBase { mLatencyStatistics = new AudioStreamBase.DoubleStatistics(); } + public void onStart() { + mSampleRateMonitor.reset(); + } + public void onStop() { + mSampleRateMonitor.reset(); + } + public abstract boolean isInput(); public void startPlayback() throws IOException {} public void stopPlayback() throws IOException {} - public abstract int write(float[] buffer, int offset, int length); - public abstract void close(); public int getChannelCount() { @@ -195,17 +243,15 @@ public abstract class AudioStreamBase { public double getLatency() { return -1.0; } - public double getCpuLoad() { return 0.0; } + public float getCpuLoad() { return 0.0f; } + public float getAndResetMaxCpuLoad() { return 0.0f; } + public int getAndResetCpuMask() { return 0; } public String getCallbackTimeStr() { return "?"; }; public int getState() { return -1; } - public boolean isThresholdSupported() { - return false; - } - - public void setWorkload(double workload) {} + public void setWorkload(int workload) {} public abstract int getXRunCount(); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java index 32bc2ae6..be740b3c 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java @@ -33,7 +33,7 @@ class AudioStreamTester { } public void reset() { - setWorkload(0.0); + setWorkload(0); requestedConfiguration.reset(); // TODO consider making new ones actualConfiguration.reset(); } @@ -46,7 +46,7 @@ class AudioStreamTester { mCurrentAudioStream.startPlayback(); } - public void setWorkload(double workload) { + public void setWorkload(int workload) { mCurrentAudioStream.setWorkload(workload); } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java index e8befcde..ae53a7f6 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java @@ -1,3 +1,19 @@ +/* + * 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 android.os.Bundle; @@ -5,6 +21,12 @@ import android.view.View; import android.widget.AdapterView; import android.widget.Spinner; +/** + * Look for glitches with various configurations. + * A sine wave is played and continuously recorded using loopback. + * An analyzer locks to the phase and magnitude of the detected sine wave. + * It then compares the incoming signal with a predicted sine wave. + */ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity { private Spinner mDurationSpinner; @@ -43,6 +65,8 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity { mDurationSpinner = (Spinner) findViewById(R.id.spinner_glitch_duration); mDurationSpinner.setOnItemSelectedListener(new DurationSpinnerListener()); + + setAnalyzerText(getString(R.string.auto_glitch_instructions)); } @Override @@ -75,9 +99,7 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity { requestedInConfig.setChannelCount(inChannels); requestedOutConfig.setChannelCount(outChannels); - setTolerance(0.3f); // FIXME remove - - testConfigurations(); + testCurrentConfigurations(); } private void testConfiguration(int performanceMode, @@ -98,10 +120,17 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity { mTestResults.clear(); + // Test with STEREO on both input and output. + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_EXCLUSIVE, + UNSPECIFIED, STEREO, STEREO); + + // Test EXCLUSIVE mode with a configuration most likely to work. testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, StreamConfiguration.SHARING_MODE_EXCLUSIVE, UNSPECIFIED); + // Test various combinations. for (int perfMode : PERFORMANCE_MODES) { for (int sampleRate : SAMPLE_RATES) { testConfiguration(perfMode, @@ -110,10 +139,10 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity { } } - analyzeTestResults(); + compareFailedTestsWithNearestPassingTest(); } catch (InterruptedException e) { - analyzeTestResults(); + compareFailedTestsWithNearestPassingTest(); } catch (Exception e) { log(e.getMessage()); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java index 83417809..27fc0456 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java @@ -7,6 +7,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.WindowManager; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ScrollView; @@ -184,8 +185,9 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable { private void stopAutoThread() { try { if (mAutoThread != null) { - log("Disable background test thread."); - new RuntimeException("Disable background test thread.").printStackTrace(); + Log.d(TestAudioActivity.TAG, + "Who called stopAutoThread()?", + new RuntimeException("Just for debugging.")); mThreadEnabled = false; mAutoThread.interrupt(); mAutoThread.join(100); @@ -231,7 +233,13 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable { } // Only call from UI thread. + public void onTestStarted() { + mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // Only call from UI thread. public void onTestFinished() { + mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); updateStartStopButtons(false); mShareButton.setEnabled(true); } @@ -262,6 +270,12 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable { @Override public void run() { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + onTestStarted(); + } + }); logClear(); log("=== STARTED at " + new Date()); log(mActivity.getTestName()); @@ -274,7 +288,7 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable { mFailCount = 0; try { mActivity.runTest(); - log("Tests finished without exception."); + log("Tests finished."); } catch(Exception e) { log("EXCEPTION: " + e.getMessage()); } finally { diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java index 5bc16fb5..847dc563 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java @@ -16,13 +16,18 @@ 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 android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; +import java.util.Locale; public class BaseAutoGlitchActivity extends GlitchActivity { @@ -37,8 +42,17 @@ public class BaseAutoGlitchActivity extends GlitchActivity { 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:"); @@ -53,27 +67,35 @@ public class BaseAutoGlitchActivity extends GlitchActivity { mTestName = name; } - private static class TestDirection { + @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 TestDirection(StreamConfiguration configuration, int channelUsed) { + 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(TestDirection other) { + 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; @@ -81,10 +103,11 @@ public class BaseAutoGlitchActivity extends GlitchActivity { return count; } - public String comparePassedDirection(String prefix, TestDirection passed) { + 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")); @@ -96,6 +119,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { return "D=" + deviceId + ", " + ((mmapUsed > 0) ? "MMAP" : "Lgcy") + ", ch=" + channelText(channelUsed, channelCount) + + ", cm=" + convertChannelMaskToText(channelMask) + "," + StreamConfiguration.convertPerformanceModeToText(performanceMode) + "," + StreamConfiguration.convertSharingModeToText(sharingMode); } @@ -103,8 +127,8 @@ public class BaseAutoGlitchActivity extends GlitchActivity { protected static class TestResult { final int testIndex; - final TestDirection input; - final TestDirection output; + final TestStreamOptions input; + final TestStreamOptions output; public final int inputPreset; public final int sampleRate; final String testName; // name or purpose of test @@ -120,8 +144,8 @@ public class BaseAutoGlitchActivity extends GlitchActivity { int outputChannel) { this.testIndex = testIndex; this.testName = testName; - input = new TestDirection(inputConfiguration, inputChannel); - output = new TestDirection(outputConfiguration, outputChannel); + input = new TestStreamOptions(inputConfiguration, inputChannel); + output = new TestStreamOptions(outputConfiguration, outputChannel); sampleRate = outputConfiguration.getSampleRate(); this.inputPreset = inputConfiguration.getInputPreset(); } @@ -147,7 +171,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { StringBuffer text = new StringBuffer(); text.append("Compare with passed test #" + passed.testIndex + "\n"); text.append(input.comparePassedDirection("IN", passed.input)); - text.append(TestDataPathsActivity.comparePassedField("IN", this, passed, "inputPreset")); + text.append(TestDataPathsActivity.comparePassedInputPreset("IN", this, passed)); text.append(output.comparePassedDirection("OUT", passed.output)); text.append(TestDataPathsActivity.comparePassedField("I/O",this, passed, "sampleRate")); @@ -177,6 +201,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { @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); @@ -208,12 +233,12 @@ public class BaseAutoGlitchActivity extends GlitchActivity { ? getOutputChannel() : getInputChannel(); return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "INP") + (config.isMMap() ? "-M" : "-L") - + ", ID = " + String.format("%2d", config.getDeviceId()) - + ", SR = " + String.format("%5d", config.getSampleRate()) + + "-" + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) + + ", ID = " + String.format(Locale.getDefault(), "%2d", config.getDeviceId()) + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( - config.getPerformanceMode()) - + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) - + ", ch = " + channelText(channel, config.getChannelCount()); + config.getPerformanceMode()) + + ",\n ch = " + channelText(channel, config.getChannelCount()) + + ", cm = " + convertChannelMaskToText(config.getChannelMask()); } protected String getStreamText(AudioStreamBase stream) { @@ -230,15 +255,14 @@ public class BaseAutoGlitchActivity extends GlitchActivity { // Run one test based on the requested input/output configurations. @Nullable - protected TestResult testConfigurations() throws InterruptedException { - int result = TEST_RESULT_SKIPPED; + protected TestResult testCurrentConfigurations() throws InterruptedException { mAutomatedTestRunner.incrementTestCount(); - if ((getSingleTestIndex() >= 0) && (mAutomatedTestRunner.getTestCount() != getSingleTestIndex())) { + if ((getSingleTestIndex() >= 0) && (getTestCount() != getSingleTestIndex())) { return null; } - log("========================== #" + mAutomatedTestRunner.getTestCount()); - + log("========================== #" + getTestCount()); + int result = 0; StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; @@ -246,6 +270,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; log("Requested:"); + log(" SR = " + requestedOutConfig.getSampleRate()); log(" " + getConfigText(requestedInConfig)); log(" " + getConfigText(requestedOutConfig)); @@ -253,7 +278,9 @@ public class BaseAutoGlitchActivity extends GlitchActivity { 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; @@ -271,7 +298,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { } TestResult testResult = new TestResult( - mAutomatedTestRunner.getTestCount(), + getTestCount(), mTestName, mAudioInputTester.actualConfiguration, getInputChannel(), @@ -279,14 +306,14 @@ public class BaseAutoGlitchActivity extends GlitchActivity { getOutputChannel() ); - // The test would only be worth running if we got the configuration we requested on input or output. - String skipReason = shouldTestBeSkipped(); + // 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(); + startAudioTest(); // Start running the test in the background. } catch (IOException e) { e.printStackTrace(); valid = false; @@ -324,7 +351,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { } if (openFailed || startFailed) { - appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n"); + appendFailedSummary("------ #" + getTestCount() + "\n"); appendFailedSummary(getConfigText(requestedInConfig) + "\n"); appendFailedSummary(getConfigText(requestedOutConfig) + "\n"); appendFailedSummary(reason + "\n"); @@ -342,7 +369,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { resultText += reason; log(" " + resultText); if (!passed) { - appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n"); + appendFailedSummary("------ #" + getTestCount() + "\n"); appendFailedSummary(" " + getConfigText(actualInConfig) + "\n"); appendFailedSummary(" " + getConfigText(actualOutConfig) + "\n"); appendFailedSummary(" " + resultText + "\n"); @@ -352,6 +379,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { mAutomatedTestRunner.incrementPassCount(); result = TEST_RESULT_PASSED; } + } mAutomatedTestRunner.flushLog(); @@ -362,28 +390,123 @@ public class BaseAutoGlitchActivity extends GlitchActivity { 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; } - protected String shouldTestBeSkipped() { + /** + * 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 the sharing mode we requested. + // 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()) { + && actualOutConfig.getSharingMode() != requestedOutConfig.getSharingMode()) { log("Did not get requested sharing mode."); - why += "share"; + 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,"; } - // We don't skip based on performance mode because if you request LOW_LATENCY you might - // get a smaller burst than if you request NONE. return why; } @@ -399,9 +522,23 @@ public class BaseAutoGlitchActivity extends GlitchActivity { appendFailedSummary(text + "\n"); } - protected void analyzeTestResults() { - logAnalysis("\n==== ANALYSIS ==========="); - logAnalysis("Compare failed configuration with closest one that passed."); + 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()) { @@ -419,6 +556,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity { } } + @Nullable private TestResult[] findClosestPassingTestResults(TestResult testResult) { int minDifferences = Integer.MAX_VALUE; diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java index 860b8145..4bf57496 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java @@ -20,9 +20,9 @@ import android.Manifest; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; /** * Support requesting RECORD_AUDIO permission. diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java index ce3b7a44..432e0ff3 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java @@ -23,6 +23,7 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.TextView; import android.widget.LinearLayout; @@ -36,6 +37,7 @@ public class BufferSizeView extends LinearLayout { private TextView mTextLabel; private SeekBar mFader; private ExponentialTaper mTaper; + private RadioGroup mBufferSizeGroup; private RadioButton mBufferSizeRadio1; private RadioButton mBufferSizeRadio2; private RadioButton mBufferSizeRadio3; @@ -97,6 +99,8 @@ public class BufferSizeView extends LinearLayout { mTaper = new ExponentialTaper(0.0, 1.0, 10.0); mFader.setProgress(0); + mBufferSizeGroup = (RadioGroup) findViewById(R.id.bufferSizeGroup); + mBufferSizeRadio1 = (RadioButton) findViewById(R.id.bufferSize1); mBufferSizeRadio1.setOnClickListener(new View.OnClickListener() { @Override @@ -118,13 +122,18 @@ public class BufferSizeView extends LinearLayout { onSizeRadioButtonClicked(view, 3); } }); + mNumBursts = DEFAULT_NUM_BURSTS; updateRadioButtons(); updateBufferSize(); } public void updateRadioButtons() { - if (mBufferSizeRadio3 != null) { + if (mNumBursts == USE_FADER && mBufferSizeGroup != null) { + // Clear all the radio buttons using the group. + // If you clear a checked button directly then it stops working. + mBufferSizeGroup.clearCheck(); + } else if (mBufferSizeRadio3 != null) { mBufferSizeRadio1.setChecked(mNumBursts == 1); mBufferSizeRadio2.setChecked(mNumBursts == 2); mBufferSizeRadio3.setChecked(mNumBursts == 3); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java index 4e8df135..3e4c96df 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java @@ -16,7 +16,11 @@ package com.mobileer.oboetester; +import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.Build; @@ -31,11 +35,27 @@ import android.widget.TextView; import com.mobileer.audio_device.CommunicationDeviceSpinner; +import java.util.Locale; + public class CommunicationDeviceView extends LinearLayout { - private CheckBox mSpeakerphoneCheckbox; - private TextView mIsSpeakerphoneText; + private AudioManager mAudioManager; + private CheckBox mSpeakerphoneCheckbox; + private CheckBox mScoCheckbox; + private TextView mSpeakerStatusView; + private TextView mScoStatusView; + private BroadcastReceiver mScoStateReceiver; + private boolean mScoStateReceiverRegistered = false; private CommunicationDeviceSpinner mDeviceSpinner; + private int mScoState; + private CommDeviceSniffer mCommDeviceSniffer = new CommDeviceSniffer();; + + protected class CommDeviceSniffer extends NativeSniffer { + @Override + public void updateStatusText() { + showCommDeviceStatus(); + } + } public CommunicationDeviceView(Context context) { super(context); @@ -66,6 +86,29 @@ public class CommunicationDeviceView extends LinearLayout { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mSpeakerphoneCheckbox = (CheckBox) findViewById(R.id.setSpeakerphoneOn); + mSpeakerphoneCheckbox.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onSetSpeakerphoneOn(view); + } + }); + mSpeakerStatusView = (TextView) findViewById(R.id.spkr_status_view); + + mScoCheckbox = (CheckBox) findViewById(R.id.setBluetoothScoOn); + mScoCheckbox.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onStartStopBluetoothSco(view); + } + }); + mScoStatusView = (TextView) findViewById(R.id.sco_status_view); + mScoStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mScoState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); + showCommDeviceStatus(); + } + }; mDeviceSpinner = (CommunicationDeviceSpinner) findViewById(R.id.comm_devices_spinner); mDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -74,15 +117,15 @@ public class CommunicationDeviceView extends LinearLayout { AudioDeviceInfo[] commDeviceArray = mDeviceSpinner.getCommunicationsDevices(); if (commDeviceArray != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (position == 0) { + if (position == CommunicationDeviceSpinner.POS_CLEAR) { mAudioManager.clearCommunicationDevice(); } else { - AudioDeviceInfo selectedDevice = commDeviceArray[position - 1]; // skip "Clear" + AudioDeviceInfo selectedDevice = commDeviceArray[position - CommunicationDeviceSpinner.POS_DEVICES]; // skip "Clear" mAudioManager.setCommunicationDevice(selectedDevice); } - showCommDeviceStatus(); } } + showCommDeviceStatus(); } public void onNothingSelected(AdapterView<?> parent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -92,19 +135,22 @@ public class CommunicationDeviceView extends LinearLayout { } }); - mSpeakerphoneCheckbox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onSetSpeakerphoneOn(view); - } - }); - mIsSpeakerphoneText = (TextView) findViewById(R.id.isSpeakerphoneOn); showCommDeviceStatus(); } - public void cleanup() { + public void onStart() { + registerScoStateReceiver(); + mCommDeviceSniffer.startSniffer(); + } + + + public void onStop() { + mCommDeviceSniffer.stopSniffer(); mSpeakerphoneCheckbox.setChecked(false); setSpeakerPhoneOn(false); + mScoCheckbox.setChecked(false); + mAudioManager.stopBluetoothSco(); + unregisterScoStateReceiver(); } public void onSetSpeakerphoneOn(View view) { @@ -122,14 +168,47 @@ public class CommunicationDeviceView extends LinearLayout { private void showCommDeviceStatus() { boolean enabled = mAudioManager.isSpeakerphoneOn(); - String text = (enabled ? "ON" : "OFF"); + String text = ":" + (enabled ? "ON" : "OFF"); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { AudioDeviceInfo commDeviceInfo = mAudioManager.getCommunicationDevice(); if (commDeviceInfo != null) { - text += ", CommDev=" + commDeviceInfo.getId(); + text += ", #" + commDeviceInfo.getId(); } } - mIsSpeakerphoneText.setText(" => " + text); + mSpeakerStatusView.setText(text + ","); + + if (mScoState == AudioManager.SCO_AUDIO_STATE_CONNECTING) { + text = ":WAIT"; + } else if (mScoState == AudioManager.SCO_AUDIO_STATE_CONNECTED) { + text = ":CON"; + } else if (mScoState == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) { + text = ":DISCON"; + } + mScoStatusView.setText(text); + } + + public void onStartStopBluetoothSco(View view) { + CheckBox checkBox = (CheckBox) view; + if (checkBox.isChecked()) { + mAudioManager.startBluetoothSco(); + } else { + mAudioManager.stopBluetoothSco(); + } + } + + private synchronized void registerScoStateReceiver() { + if (!mScoStateReceiverRegistered) { + getContext().registerReceiver(mScoStateReceiver, + new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)); + mScoStateReceiverRegistered = true; + } + } + + private synchronized void unregisterScoStateReceiver() { + if (mScoStateReceiverRegistered) { + getContext().unregisterReceiver(mScoStateReceiver); + mScoStateReceiverRegistered = false; + } } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java index a9e8e606..31146236 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java @@ -19,13 +19,22 @@ package com.mobileer.oboetester; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; +import android.content.Intent; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.MicrophoneInfo; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; import android.os.Build; import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.widget.TextView; +import android.widget.Toast; import com.mobileer.audio_device.AudioDeviceInfoConverter; @@ -33,10 +42,10 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Set; /** - * Guide the user through a series of tests plugging in and unplugging a headset. - * Print a summary at the end of any failures. + * Print a report of all the available audio devices. */ public class DeviceReportActivity extends Activity { @@ -63,6 +72,8 @@ public class DeviceReportActivity extends Activity { MyAudioDeviceCallback mDeviceCallback = new MyAudioDeviceCallback(); private TextView mAutoTextView; private AudioManager mAudioManager; + private UsbManager mUsbManager; + private MidiManager mMidiManager; @Override protected void onCreate(Bundle savedInstanceState) { @@ -70,6 +81,26 @@ public class DeviceReportActivity extends Activity { setContentView(R.layout.activity_device_report); mAutoTextView = (TextView) findViewById(R.id.text_log_device_report); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + MenuItem settings = menu.findItem(R.id.action_share); + settings.setOnMenuItemClickListener(item -> { + if(mAutoTextView !=null) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, mAutoTextView.getText().toString()); + sendIntent.setType("text/plain"); + Intent shareIntent = Intent.createChooser(sendIntent, null); + startActivity(shareIntent); + } + return false; + }); + return true; } @Override @@ -112,9 +143,78 @@ public class DeviceReportActivity extends Activity { report.append(item); } report.append(reportAllMicrophones()); + report.append(reportUsbDevices()); + report.append(reportMidiDevices()); log(report.toString()); } + public String reportUsbDevices() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nUsb Device Report:\n"); + try { + HashMap<String, UsbDevice> usbDeviceList = mUsbManager.getDeviceList(); + for (UsbDevice usbDevice : usbDeviceList.values()) { + report.append("\n==== USB Device ========= " + usbDevice.getDeviceId()); + report.append("\nProduct Name : " + usbDevice.getProductName()); + report.append("\nProduct ID : 0x" + Integer.toHexString(usbDevice.getProductId())); + report.append("\nManufacturer Name : " + usbDevice.getManufacturerName()); + report.append("\nVendor ID : 0x" + Integer.toHexString(usbDevice.getVendorId())); + report.append("\nDevice Name : " + usbDevice.getDeviceName()); + report.append("\nDevice Protocol : " + usbDevice.getDeviceProtocol()); + report.append("\nDevice Class : " + usbDevice.getDeviceClass()); + report.append("\nDevice Subclass : " + usbDevice.getDeviceSubclass()); + report.append("\nVersion : " + usbDevice.getVersion()); + report.append("\n" + usbDevice); + report.append("\n"); + } + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + return report.toString(); + } + + public String reportMidiDevices() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nMidi Device Report:\n"); + try { + MidiDeviceInfo[] midiDeviceInfos = mMidiManager.getDevices(); + for (MidiDeviceInfo midiDeviceInfo : midiDeviceInfos) { + report.append("\n==== MIDI Device ========= " + midiDeviceInfo.getId()); + addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Set<MidiDeviceInfo> umpDeviceInfos = + mMidiManager.getDevicesForTransport(MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS); + for (MidiDeviceInfo midiDeviceInfo : umpDeviceInfos) { + report.append("\n==== UMP Device ========= " + midiDeviceInfo.getId()); + addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report); + } + } + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + return report.toString(); + } + + private void addMidiDeviceInfoToDeviceReport(MidiDeviceInfo midiDeviceInfo, + StringBuffer report){ + report.append("\nInput Count : " + midiDeviceInfo.getInputPortCount()); + report.append("\nOutput Count : " + midiDeviceInfo.getOutputPortCount()); + report.append("\nType : " + midiDeviceInfo.getType()); + report.append("\nIs Private : " + midiDeviceInfo.isPrivate()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + report.append("\nDefault Protocol : " + midiDeviceInfo.getDefaultProtocol()); + } + report.append("\n" + midiDeviceInfo); + report.append("\n"); + } + public String reportAllMicrophones() { StringBuffer report = new StringBuffer(); report.append("\n############################"); @@ -127,8 +227,12 @@ public class DeviceReportActivity extends Activity { report.append(micItem); } } catch (IOException e) { - e.printStackTrace(); + Log.e(TestAudioActivity.TAG, "Caught ", e); return e.getMessage(); + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); } } else { report.append("\nMicrophoneInfo not available on V" + android.os.Build.VERSION.SDK_INT); @@ -169,4 +273,20 @@ public class DeviceReportActivity extends Activity { }); } + protected void showErrorToast(String message) { + String text = "Error: " + message; + Log.e(TestAudioActivity.TAG, text); + showToast(text); + } + + protected void showToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(DeviceReportActivity.this, + message, + Toast.LENGTH_SHORT).show(); + } + }); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java new file mode 100644 index 00000000..8c94a3b7 --- /dev/null +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java @@ -0,0 +1,388 @@ +/* + * Copyright 2015 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 android.graphics.Color; +import android.graphics.Typeface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Demonstrate the behavior of a changing CPU load on underruns. + * Display the workload and the callback duration in a chart. + * Enable or disable PerformanceHints (ADPF) using a checkbox. + * This might boost the CPU frequency when Oboe is taking too long to compute the next buffer. + * ADPF docs at: https://developer.android.com/reference/android/os/PerformanceHintManager + */ +public class DynamicWorkloadActivity extends TestOutputActivityBase { + private static final int WORKLOAD_HIGH_MIN = 30; + private static final int WORKLOAD_HIGH_MAX = 150; + // When the CPU is completely saturated then the load will be above 1.0. + public static final double LOAD_RECOVERY_HIGH = 1.0; + // Use a slightly lower value for going low so that the comparator has hysteresis. + public static final double LOAD_RECOVERY_LOW = 0.95; + + private static final float MARGIN_ABOVE_WORKLOAD_FOR_CPU = 1.2f; + + // By default, set high workload to 70 voices, which is reasonable for most devices. + public static final double WORKLOAD_PROGRESS_FOR_70_VOICES = 0.53; + + private Button mStopButton; + private Button mStartButton; + private TextView mResultView; + private LinearLayout mAffinityLayout; + private ArrayList<CheckBox> mAffinityBoxes = new ArrayList<CheckBox>(); + private WorkloadUpdateThread mUpdateThread; + + private MultiLineChart mMultiLineChart; + private MultiLineChart.Trace mMaxCpuLoadTrace; + private MultiLineChart.Trace mWorkloadTrace; + private CheckBox mUseAltAdpfBox; + private CheckBox mPerfHintBox; + private boolean mDrawChartAlways = true; + private CheckBox mDrawAlwaysBox; + private int mCpuCount; + + private static final int WORKLOAD_LOW = 1; + private int mWorkloadHigh; // this will get set later + private WorkloadView mDynamicWorkloadView; + + // Periodically query the status of the streams. + protected class WorkloadUpdateThread { + public static final int SNIFFER_UPDATE_PERIOD_MSEC = 40; + public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; + public static final int SNIFFER_TOGGLE_PERIOD_MSEC = 3000; + private static final int STATE_IDLE = 0; + private static final int STATE_RUN_LOW = 1; + private static final int STATE_RUN_HIGH = 2; + + private Handler mHandler; + + private int mWorkloadCurrent = 1; + + private int mState = STATE_IDLE; + private long mLastToggleTime = 0; + private long mRecoveryTimeBegin; + private long mRecoveryTimeEnd; + private long mStartTimeNanos; + + String stateToString(int state) { + switch(state) { + case STATE_IDLE: + return "Idle"; + case STATE_RUN_LOW: + return "low"; + case STATE_RUN_HIGH: + return "HIGH"; + default: + return "Unrecognized"; + } + } + + // Display status info for the stream. + private Runnable runnableCode = new Runnable() { + @Override + public void run() { + int nextWorkload = mWorkloadCurrent; + AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); + float cpuLoad = stream.getCpuLoad(); + float maxCpuLoad = stream.getAndResetMaxCpuLoad(); + int cpuMask = stream.getAndResetCpuMask(); + long now = System.currentTimeMillis(); + boolean drawChartOnce = false; + + switch (mState) { + case STATE_IDLE: + drawChartOnce = true; // clear old chart + mState = STATE_RUN_LOW; + mLastToggleTime = now; + break; + case STATE_RUN_LOW: + nextWorkload = WORKLOAD_LOW; + if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) { + mLastToggleTime = now; + mState = STATE_RUN_HIGH; + mRecoveryTimeBegin = 0; + mRecoveryTimeEnd = 0; + } + break; + case STATE_RUN_HIGH: + nextWorkload = mWorkloadHigh; + if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) { + mLastToggleTime = now; + mState = STATE_RUN_LOW; + // Draw now when a CPU spike will not affect the result. + drawChartOnce = true; + } + + if (mRecoveryTimeBegin == 0) { + if (maxCpuLoad > LOAD_RECOVERY_HIGH) { + mRecoveryTimeBegin = now; + } + } else if (mRecoveryTimeEnd == 0) { + if (maxCpuLoad < LOAD_RECOVERY_LOW) { + mRecoveryTimeEnd = now; + } + } else if (maxCpuLoad > LOAD_RECOVERY_LOW) { + mRecoveryTimeEnd = now; + } + break; + } + stream.setWorkload((int) nextWorkload); + mWorkloadCurrent = nextWorkload; + // Update chart + float nowMicros = (System.nanoTime() - mStartTimeNanos) * 0.001f; + mMultiLineChart.addX(nowMicros); + mMaxCpuLoadTrace.add((float) maxCpuLoad); + mWorkloadTrace.add((float) mWorkloadCurrent); + if (drawChartOnce || mDrawChartAlways){ + mMultiLineChart.update(); + } + + // Display numbers + String recoveryTimeString = (mRecoveryTimeEnd <= mRecoveryTimeBegin) ? + "---" : ((mRecoveryTimeEnd - mRecoveryTimeBegin) + " msec"); + String message = + "#Voices = " + (int) nextWorkload + + "\nWorkState = " + stateToString(mState) + + "\nCPU = " + String.format(Locale.getDefault(), "%6.3f%c", cpuLoad * 100, '%') + + "\ncores = " + cpuMaskToString(cpuMask, mCpuCount) + + "\nRecovery = " + recoveryTimeString; + postResult(message); + + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); + } + }; + + private void start() { + stop(); + mStartTimeNanos = System.nanoTime(); + mMultiLineChart.reset(); + mState = STATE_IDLE; + mHandler = new Handler(Looper.getMainLooper()); + // Start the initial runnable task by posting through the handler + mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); + } + + private void stop() { + if (mHandler != null) { + mHandler.removeCallbacks(runnableCode); + } + } + + } + + private void setWorkloadHigh(int workloadHigh) { + mWorkloadHigh = workloadHigh; + } + + + /** + * This text will look best in a monospace font. + * @param cpuMask CPU core bit mask + * @return a text display of the selected cores like "--2-45-7" + */ + // TODO move this to some utility class + private String cpuMaskToString(int cpuMask, int cpuCount) { + String text = ""; + long longMask = ((long) cpuMask) & 0x0FFFFFFFFL; + int index = 0; + while (longMask != 0 || index < cpuCount) { + text += ((longMask & 1) != 0) ? hexDigit(index) : "-"; + longMask = longMask >> 1; + index++; + } + return text; + } + + private char hexDigit(int n) { + byte x = (byte)(n & 0x0F); + if (x < 10) return (char)('0' + x); + else return (char)('A' + x); + } + + @Override + protected void inflateActivity() { + setContentView(R.layout.activity_dynamic_workload); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAudioOutTester = addAudioOutputTester(); + + mResultView = (TextView) findViewById(R.id.resultView); + mResultView.setTypeface(Typeface.MONOSPACE); + mStartButton = (Button) findViewById(R.id.button_start); + mStopButton = (Button) findViewById(R.id.button_stop); + + mDynamicWorkloadView = (WorkloadView) findViewById(R.id.dynamic_workload_view); + mWorkloadView.setVisibility(View.GONE); + + // Add a row of checkboxes for setting CPU affinity. + mCpuCount = NativeEngine.getCpuCount(); + final int defaultCpuAffinityMask = 0; + View.OnClickListener checkBoxListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + // Create a mack from all the checkboxes. + int mask = 0; + for (int cpuIndex = 0; cpuIndex < mCpuCount; cpuIndex++) { + CheckBox checkBox = mAffinityBoxes.get(cpuIndex); + if (checkBox.isChecked()) { + mask |= (1 << cpuIndex); + } + } + NativeEngine.setCpuAffinityMask(mask); + } + }; + mAffinityLayout = (LinearLayout) findViewById(R.id.affinityLayout); + for (int cpuIndex = 0; cpuIndex < mCpuCount; cpuIndex++) { + CheckBox checkBox = new CheckBox(DynamicWorkloadActivity.this); + mAffinityLayout.addView(checkBox); + mAffinityBoxes.add(checkBox); + checkBox.setText(cpuIndex + ""); + checkBox.setOnClickListener(checkBoxListener); + if (((1 << cpuIndex) & defaultCpuAffinityMask) != 0) { + checkBox.setChecked(true); + } + } + NativeEngine.setCpuAffinityMask(defaultCpuAffinityMask); + + mMultiLineChart = (MultiLineChart) findViewById(R.id.multiline_chart); + mMaxCpuLoadTrace = mMultiLineChart.createTrace("CPU", Color.RED, + 0.0f, 2.0f); + mWorkloadTrace = mMultiLineChart.createTrace("Work", Color.BLUE, + 0.0f, (MARGIN_ABOVE_WORKLOAD_FOR_CPU * WORKLOAD_HIGH_MAX)); + + mPerfHintBox = (CheckBox) findViewById(R.id.enable_perf_hint); + + // TODO remove when finished with ADPF experiments. + mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf); + mUseAltAdpfBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + setUseAlternativeAdpf(checkBox.isChecked()); + mPerfHintBox.setEnabled(!checkBox.isChecked()); + }); + mUseAltAdpfBox.setVisibility(View.GONE); + + mPerfHintBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + setPerformanceHintEnabled(checkBox.isChecked()); + mUseAltAdpfBox.setEnabled(!checkBox.isChecked()); + }); + + CheckBox hearWorkloadBox = (CheckBox) findViewById(R.id.hear_workload); + hearWorkloadBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + setHearWorkload(checkBox.isChecked()); + }); + + mDrawAlwaysBox = (CheckBox) findViewById(R.id.draw_always); + mDrawAlwaysBox.setOnClickListener(buttonView -> { + CheckBox checkBox = (CheckBox) buttonView; + mDrawChartAlways = checkBox.isChecked(); + }); + + if (mDynamicWorkloadView != null) { + mDynamicWorkloadView.setWorkloadReceiver((w) -> { + setWorkloadHigh(w); + }); + + mDynamicWorkloadView.setLabel("High Workload"); + mDynamicWorkloadView.setRange(WORKLOAD_HIGH_MIN, WORKLOAD_HIGH_MAX); + mDynamicWorkloadView.setFaderNormalizedProgress(WORKLOAD_PROGRESS_FOR_70_VOICES); + } + + updateButtons(false); + updateEnabledWidgets(); + hideSettingsViews(); // make more room + } + + private void setHearWorkload(boolean checked) { + mAudioOutTester.getCurrentAudioStream().setHearWorkload(checked); + } + + private void setPerformanceHintEnabled(boolean checked) { + mAudioOutTester.getCurrentAudioStream().setPerformanceHintEnabled(checked); + } + + private void updateButtons(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mPerfHintBox.setEnabled(running); + } + + private void postResult(final String text) { + runOnUiThread(new Runnable() { + public void run() { + mResultView.setText(text); + } + }); + } + + @Override + int getActivityType() { + return ACTIVITY_DYNAMIC_WORKLOAD; + } + + public void startTest(View view) { + try { + openAudio(); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Open audio failed!"); + return; + } + try { + super.startAudio(); + updateButtons(true); + postResult("Running test"); + mUpdateThread = new WorkloadUpdateThread(); + mUpdateThread.start(); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast("Start audio failed! " + e.getMessage()); + return; + } + } + + public void stopTest(View view) { + onStopTest(); + } + + @Override + public void onStopTest() { + WorkloadUpdateThread updateThread = mUpdateThread; + if (updateThread != null) { + updateThread.stop(); + } + updateButtons(false); + super.onStopTest(); + } +} diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java index 1d8e4dcc..9ce258a5 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java @@ -42,9 +42,8 @@ public class EchoActivity extends TestInputActivity { private Button mStartButton; private Button mStopButton; private TextView mStatusTextView; - private CommunicationDeviceView mCommunicationDeviceView; - private ColdStartSniffer mNativeSniffer = new ColdStartSniffer(this); + private ColdStartSniffer mNativeSniffer = new ColdStartSniffer(); protected static final int MAX_DELAY_TIME_PROGRESS = 1000; @@ -71,10 +70,6 @@ public class EchoActivity extends TestInputActivity { private int mInputLatency; private int mOutputLatency; - public ColdStartSniffer(Activity activity) { - super(activity); - } - @Override public void startSniffer() { stableCallCount = 0; @@ -83,16 +78,10 @@ public class EchoActivity extends TestInputActivity { super.startSniffer(); } - public void run() { + @Override + public boolean isComplete() { mInputLatency = getColdStartInputMillis(); mOutputLatency = getColdStartOutputMillis(); - updateStatusText(); - if (!isComplete()) { - reschedule(); - } - } - - private boolean isComplete() { if (mInputLatency > 0 && mOutputLatency > 0) { stableCallCount++; } @@ -116,11 +105,6 @@ public class EchoActivity extends TestInputActivity { } @Override - public String getShortReport() { - return getCurrentStatusReport(); - } - - @Override public void updateStatusText() { String message = getCurrentStatusReport(); mStatusTextView.setText(message); @@ -163,14 +147,6 @@ public class EchoActivity extends TestInputActivity { hideSettingsViews(); } - @Override - protected void onStop() { - if (mCommunicationDeviceView != null) { - mCommunicationDeviceView.cleanup(); - } - super.onStop(); - } - private void setDelayTimeByPosition(int progress) { mDelayTime = mTaperDelayTime.linearToExponential( ((double)progress)/MAX_DELAY_TIME_PROGRESS); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java new file mode 100644 index 00000000..759690e4 --- /dev/null +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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 android.content.Context; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +public class ExternalFileWriter { + private static final String TAG = "OboeTester"; + private Context mContext; + + public ExternalFileWriter(Context context) { + mContext = context; + } + + public File writeStringToExternalFile(String result, String fileName) throws IOException { + File dir = mContext.getExternalFilesDir(null); + File resultFile = new File(dir, fileName); + Log.d(TAG, "EXTFILE = " + resultFile.getAbsolutePath()); + Writer writer = null; + try { + writer = new OutputStreamWriter(new FileOutputStream(resultFile)); + writer.write(result); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return resultFile; + } +} diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java index 06cf26d2..c978da37 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java @@ -6,6 +6,7 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.view.WindowManager; import android.widget.Button; import android.widget.Toast; @@ -58,6 +59,7 @@ public class ExternalTapToToneActivity extends Activity { mTapToToneTester.resetLatency(); mTapToToneTester.start(); updateButtons(true); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } catch (IOException e) { e.printStackTrace(); showErrorToast("Start audio failed! " + e.getMessage()); @@ -68,11 +70,13 @@ public class ExternalTapToToneActivity extends Activity { public void stopTest(View view) { mTapToToneTester.stop(); updateButtons(false); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @Override public void onStop() { mTapToToneTester.stop(); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onStop(); } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java index 557ceb41..c744329b 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java @@ -29,4 +29,15 @@ public class ExtraTestsActivity extends BaseOboeTesterActivity { launchTestActivity(TestErrorCallbackActivity.class); } + public void onLaunchRouteDuringCallbackTest(View view) { + launchTestThatDoesRecording(TestRouteDuringCallbackActivity.class); + } + + public void onLaunchDynamicWorkloadTest(View view) { + launchTestActivity(DynamicWorkloadActivity.class); + } + + public void onLaunchColdStartLatencyTest(View view) { + launchTestActivity(TestColdStartLatencyActivity.class); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java index ce0182c1..63abe5cf 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java @@ -23,6 +23,7 @@ import android.widget.Button; import android.widget.TextView; import java.io.IOException; +import java.util.Locale; /** * Activity to measure the number of glitches. @@ -46,22 +47,23 @@ public class GlitchActivity extends AnalyzerActivity { native int getStateFrameCount(int state); native int getGlitchCount(); + + // Number of frames in last glitch. + native int getGlitchLength(); + native double getPhase(); native double getSignalToNoiseDB(); native double getPeakAmplitude(); native double getSineAmplitude(); + native int getSinePeriod(); - private GlitchSniffer mGlitchSniffer; - private NativeSniffer mNativeSniffer = createNativeSniffer(); + protected NativeSniffer mNativeSniffer = createNativeSniffer(); synchronized NativeSniffer createNativeSniffer() { - if (mGlitchSniffer == null) { - mGlitchSniffer = new GlitchSniffer(this); - } - return mGlitchSniffer; + return new GlitchSniffer(); } // Note that these strings must match the enum result_code in LatencyAnalyzer.h - String stateToString(int resultCode) { + static String stateToString(int resultCode) { switch (resultCode) { case STATE_IDLE: return "IDLE"; @@ -80,6 +82,10 @@ public class GlitchActivity extends AnalyzerActivity { } } + static String magnitudeToString(double magnitude) { + return String.format(Locale.US, "%6.4f", magnitude); + } + // Periodically query for glitches from the native detector. protected class GlitchSniffer extends NativeSniffer { @@ -100,10 +106,6 @@ public class GlitchActivity extends AnalyzerActivity { private double mPeakAmplitude; private double mSineAmplitude; - public GlitchSniffer(Activity activity) { - super(activity); - } - @Override public void startSniffer() { long now = System.currentTimeMillis(); @@ -119,7 +121,7 @@ public class GlitchActivity extends AnalyzerActivity { super.startSniffer(); } - public void run() { + private void gatherData() { int state = getAnalyzerState(); mSignalToNoiseDB = getSignalToNoiseDB(); mPeakAmplitude = getPeakAmplitude(); @@ -163,8 +165,6 @@ public class GlitchActivity extends AnalyzerActivity { mLastGlitchFrames = glitchFrames; mLastLockedFrames = lockedFrames; mLastResetCount = resetCount; - - reschedule(); } private String getCurrentStatusReport() { @@ -173,39 +173,46 @@ public class GlitchActivity extends AnalyzerActivity { StringBuffer message = new StringBuffer(); message.append("state = " + stateToString(mPreviousState) + "\n"); - message.append(String.format("unlocked.frames = %d\n", mLastUnlockedFrames)); - message.append(String.format("locked.frames = %d\n", mLastLockedFrames)); - message.append(String.format("glitch.frames = %d\n", mLastGlitchFrames)); - message.append(String.format("reset.count = %d\n", mLastResetCount - mStartResetCount)); - message.append(String.format("peak.amplitude = %8.6f\n", mPeakAmplitude)); - message.append(String.format("sine.amplitude = %8.6f\n", mSineAmplitude)); + message.append(String.format(Locale.getDefault(), "unlocked.frames = %d\n", mLastUnlockedFrames)); + message.append(String.format(Locale.getDefault(), "locked.frames = %d\n", mLastLockedFrames)); + message.append(String.format(Locale.getDefault(), "glitch.frames = %d\n", mLastGlitchFrames)); + message.append(String.format(Locale.getDefault(), "reset.count = %d\n", mLastResetCount - mStartResetCount)); + message.append(String.format(Locale.getDefault(), "peak.amplitude = %8.6f\n", mPeakAmplitude)); + message.append(String.format(Locale.getDefault(), "sine.amplitude = %8.6f\n", mSineAmplitude)); if (mLastLockedFrames > 0) { - message.append(String.format("signal.noise.ratio.db = %5.1f\n", mSignalToNoiseDB)); + message.append(String.format(Locale.getDefault(), "signal.noise.ratio.db = %5.1f\n", mSignalToNoiseDB)); } - message.append(String.format("time.total = %4.2f seconds\n", totalSeconds)); + message.append(String.format(Locale.getDefault(), "time.total = %4.2f seconds\n", totalSeconds)); if (mLastLockedFrames > 0) { - message.append(String.format("time.no.glitches = %4.2f\n", mSecondsWithoutGlitches)); - message.append(String.format("max.time.no.glitches = %4.2f\n", + message.append(String.format(Locale.getDefault(), "time.no.glitches = %4.2f\n", mSecondsWithoutGlitches)); + message.append(String.format(Locale.getDefault(), "max.time.no.glitches = %4.2f\n", mMaxSecondsWithoutGlitches)); - message.append(String.format("glitch.count = %d\n", mLastGlitchCount)); + message.append(String.format(Locale.getDefault(), "glitch.length = %d\n", getGlitchLength())); + message.append(String.format(Locale.getDefault(), "glitch.count = %d\n", mLastGlitchCount)); } return message.toString(); } - @Override public String getShortReport() { - String resultText = "#glitches = " + getLastGlitchCount() + String resultText = "amplitude: peak = " + magnitudeToString(mPeakAmplitude) + + ", sine = " + magnitudeToString(mSineAmplitude) + "\n"; + if (mPeakAmplitude < 0.01) { + resultText += "WARNING: volume is very low!\n"; + } + resultText += "#glitches = " + getLastGlitchCount() + ", #resets = " + getLastResetCount() + ", max no glitch = " + getMaxSecondsWithNoGlitch() + " secs\n"; - resultText += String.format("SNR = %5.1f db", mSignalToNoiseDB); + resultText += String.format(Locale.getDefault(), "SNR = %5.1f db", mSignalToNoiseDB); resultText += ", #locked = " + mLastLockedFrames; return resultText; } @Override public void updateStatusText() { + gatherData(); mLastGlitchReport = getCurrentStatusReport(); setAnalyzerText(mLastGlitchReport); + maybeDisplayWaveform(); } public double getMaxSecondsWithNoGlitch() { @@ -227,13 +234,16 @@ public class GlitchActivity extends AnalyzerActivity { protected void onGlitchDetected() { } + protected void maybeDisplayWaveform() {} + protected void setAnalyzerText(String s) { mAnalyzerTextView.setText(s); } /** * Set tolerance to deviations from expected value. - * The normalized value will be converted in the native code. + * The normalized value will be scaled by the measured magnitude + * of the sine wave.. * @param tolerance normalized between 0.0 and 1.0 */ public native void setTolerance(float tolerance); @@ -256,6 +266,12 @@ public class GlitchActivity extends AnalyzerActivity { return mOutputChannel; } + /** + * Set the duration of a periodic forced glitch. + * @param frames or zero for no glitch + */ + public native void setForcedGlitchDuration(int frames); + public native void setInputChannelNative(int channel); public native void setOutputChannelNative(int channel); @@ -332,18 +348,10 @@ public class GlitchActivity extends AnalyzerActivity { onTestBegan(); } - public void onCancel(View view) { - stopAudioTest(); - onTestFinished(); - } - // Called on UI thread public void onStopAudioTest(View view) { stopAudioTest(); onTestFinished(); - mStartButton.setEnabled(true); - mStopButton.setEnabled(false); - mShareButton.setEnabled(false); keepScreenOn(false); } @@ -365,6 +373,7 @@ public class GlitchActivity extends AnalyzerActivity { } public void stopTest() { + mNativeSniffer.stopSniffer(); stopAudio(); } @@ -374,11 +383,11 @@ public class GlitchActivity extends AnalyzerActivity { } public double getMaxSecondsWithNoGlitch() { - return mGlitchSniffer.getMaxSecondsWithNoGlitch(); + return ((GlitchSniffer)mNativeSniffer).getMaxSecondsWithNoGlitch(); } public String getShortReport() { - return mNativeSniffer.getShortReport(); + return ((GlitchSniffer)mNativeSniffer).getShortReport(); } @Override diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java index 594e7c41..4746ec72 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java @@ -39,13 +39,20 @@ public class IntentBasedTestSupport { public static final String KEY_IN_USE_MMAP = "in_use_mmap"; public static final String KEY_OUT_USE_MMAP = "out_use_mmap"; - public static final boolean VALUE_DEFAULT_USE_MMAP = true; + public static final boolean VALUE_DEFAULT_USE_MMAP = NativeEngine.isMMapSupported(); public static final String KEY_IN_PRESET = "in_preset"; public static final String KEY_SAMPLE_RATE = "sample_rate"; public static final int VALUE_DEFAULT_SAMPLE_RATE = 48000; public static final String VALUE_UNSPECIFIED = "unspecified"; + public static final String KEY_OUT_USAGE = "out_usage"; + public static final String VALUE_USAGE_MEDIA = "media"; + public static final String VALUE_USAGE_VOICE_COMMUNICATION = "voice_communication"; + public static final String VALUE_USAGE_ALARM = "alarm"; + public static final String VALUE_USAGE_NOTIFICATION = "notification"; + public static final String VALUE_USAGE_GAME = "game"; + public static final String KEY_IN_API = "in_api"; public static final String KEY_OUT_API = "out_api"; public static final String VALUE_API_AAUDIO = "aaudio"; @@ -138,6 +145,21 @@ public class IntentBasedTestSupport { return StreamConfiguration.SHARING_MODE_EXCLUSIVE; } } + public static int getUsageFromText(String text) { + if (VALUE_USAGE_GAME.equals(text)) { + return StreamConfiguration.USAGE_GAME; + } else if (VALUE_USAGE_VOICE_COMMUNICATION.equals(text)) { + return StreamConfiguration.USAGE_VOICE_COMMUNICATION; + } else if (VALUE_USAGE_MEDIA.equals(text)) { + return StreamConfiguration.USAGE_MEDIA; + } else if (VALUE_USAGE_ALARM.equals(text)) { + return StreamConfiguration.USAGE_ALARM; + } else if (VALUE_USAGE_NOTIFICATION.equals(text)) { + return StreamConfiguration.USAGE_NOTIFICATION; + } else { + return StreamConfiguration.UNSPECIFIED; + } + } public static void configureStreamsFromBundle(Bundle bundle, StreamConfiguration requestedInConfig, @@ -278,6 +300,11 @@ public class IntentBasedTestSupport { int sharingMode = getSharingFromText(text); requestedOutConfig.setSharingMode(sharingMode); + text = bundle.getString(KEY_OUT_USAGE, VALUE_USAGE_MEDIA); + int usage = getUsageFromText(text); + requestedOutConfig.setUsage(usage); + + } public static void configureInputStreamFromBundle(Bundle bundle, diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java index 70eca907..145dfbc0 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java @@ -57,9 +57,7 @@ public class MainActivity extends BaseOboeTesterActivity { protected TextView mDeviceView; private TextView mVersionTextView; private TextView mBuildTextView; - private TextView mBluetoothScoStatusView; private Bundle mBundleFromIntent; - private BroadcastReceiver mScoStateReceiver; private CheckBox mWorkaroundsCheckBox; private CheckBox mBackgroundCheckBox; private static String mVersionText; @@ -117,21 +115,6 @@ public class MainActivity extends BaseOboeTesterActivity { mBuildTextView = (TextView) findViewById(R.id.text_build_info); mBuildTextView.setText(Build.DISPLAY); - mBluetoothScoStatusView = (TextView) findViewById(R.id.textBluetoothScoStatus); - mScoStateReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); - if (state == AudioManager.SCO_AUDIO_STATE_CONNECTING) { - mBluetoothScoStatusView.setText("CONNECTING"); - } else if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { - mBluetoothScoStatusView.setText("CONNECTED"); - } else if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) { - mBluetoothScoStatusView.setText("DISCONNECTED"); - } - } - }; - saveIntentBundleForLaterProcessing(getIntent()); } @@ -139,15 +122,6 @@ public class MainActivity extends BaseOboeTesterActivity { return mVersionText; } - private void registerScoStateReceiver() { - registerReceiver(mScoStateReceiver, - new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)); - } - - private void unregisterScoStateReceiver() { - unregisterReceiver(mScoStateReceiver); - } - private void logScreenSize() { Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -213,16 +187,9 @@ public class MainActivity extends BaseOboeTesterActivity { public void onResume(){ super.onResume(); mWorkaroundsCheckBox.setChecked(NativeEngine.areWorkaroundsEnabled()); - registerScoStateReceiver(); processBundleFromIntent(); } - @Override - public void onPause(){ - unregisterScoStateReceiver(); - super.onPause(); - } - private void updateNativeAudioUI() { AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String audioManagerSampleRate = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); @@ -312,13 +279,4 @@ public class MainActivity extends BaseOboeTesterActivity { OboeAudioStream.setCallbackSize(callbackSize); } - public void onStartStopBluetoothSco(View view) { - CheckBox checkBox = (CheckBox) view; - AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - if (checkBox.isChecked()) { - myAudioMgr.startBluetoothSco(); - } else { - myAudioMgr.stopBluetoothSco(); - } - } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java index 699344df..9281cd59 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java @@ -16,13 +16,21 @@ package com.mobileer.oboetester; +import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.TextView; import java.io.IOException; +import java.util.Locale; public class ManualGlitchActivity extends GlitchActivity { @@ -30,15 +38,24 @@ public class ManualGlitchActivity extends GlitchActivity { public static final int VALUE_DEFAULT_BUFFER_BURSTS = 2; public static final String KEY_TOLERANCE = "tolerance"; - private static final float DEFAULT_TOLERANCE = 0.1f; + private static final float DEFAULT_TOLERANCE = 0.10f; private static final long MIN_DISPLAY_PERIOD_MILLIS = 500; + private static final int WAVEFORM_SIZE = 400; private TextView mTextTolerance; private SeekBar mFaderTolerance; protected ExponentialTaper mTaperTolerance; + + private CheckBox mForceGlitchesBox; + private CheckBox mAutoScopeBox; private WaveformView mWaveformView; - private float[] mWaveform = new float[256]; + private LinearLayout mLayoutGlitch; + + + private NumberedRadioButtons mInputChannelBoxes; + private NumberedRadioButtons mOutputChannelBoxes; + private float[] mWaveform = new float[WAVEFORM_SIZE]; private long mLastDisplayTime; private float mTolerance = DEFAULT_TOLERANCE; @@ -62,7 +79,56 @@ public class ManualGlitchActivity extends GlitchActivity { float tolerance = (float) mTaperTolerance.linearToExponential( ((double)progress) / FADER_PROGRESS_MAX); setTolerance(tolerance); - mTextTolerance.setText("Tolerance = " + String.format("%5.3f", tolerance)); + mTextTolerance.setText("Tolerance = " + String.format(Locale.getDefault(), "%5.3f", tolerance)); + } + + static class NumberedRadioButtons { + LinearLayout mRow; + RadioButton[] mRadioButtons; + + public interface SelectionListener { + void onSelected(int index); + } + + NumberedRadioButtons(Context context, int numBoxes, SelectionListener listener, String prompt) { + mRow = new LinearLayout(context); + mRow.setOrientation(LinearLayout.HORIZONTAL); + TextView textView = new TextView(context); + textView.setText(prompt); + mRow.addView(textView); + RadioGroup rg = new RadioGroup(context); + rg.setOrientation(LinearLayout.HORIZONTAL); + mRadioButtons = new RadioButton[numBoxes]; + for (int i = 0; i < numBoxes; i++) { + mRadioButtons[i] = new RadioButton(context); + mRadioButtons[i].setText("" + i); + mRadioButtons[i].setId(i); + rg.addView(mRadioButtons[i]); + } + mRow.addView(rg); + + //set listener to radio button group + rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + listener.onSelected(checkedId); + } + }); + + mRadioButtons[0].setChecked(true); + } + + public View getView() { + return mRow; + } + + public void setMaxEnabled(int max) { + max = Math.min(max, mRadioButtons.length); + for (int i = 0; i < mRadioButtons.length; i++) { + mRadioButtons[i].setEnabled(i < max); + } + mRadioButtons[0].setChecked(true); + } } @Override @@ -75,7 +141,17 @@ public class ManualGlitchActivity extends GlitchActivity { mFaderTolerance.setOnSeekBarChangeListener(mToleranceListener); setToleranceFader(DEFAULT_TOLERANCE); + mForceGlitchesBox = (CheckBox) findViewById(R.id.boxForceGlitch); + mAutoScopeBox = (CheckBox) findViewById(R.id.boxAutoScope); mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); + + mLayoutGlitch = (LinearLayout) findViewById(R.id.layoutGlitch); + mInputChannelBoxes = new NumberedRadioButtons(this, 8, + (int index) -> setInputChannel(index), "IN:"); + mLayoutGlitch.addView(mInputChannelBoxes.getView()); + mOutputChannelBoxes = new NumberedRadioButtons(this, 8, + (int index) -> setOutputChannel(index), "OUT:"); + mLayoutGlitch.addView(mOutputChannelBoxes.getView()); } private void setToleranceFader(float tolerance) { @@ -112,7 +188,12 @@ public class ManualGlitchActivity extends GlitchActivity { public void startAudioTest() throws IOException { super.startAudioTest(); + setToleranceProgress(mFaderTolerance.getProgress()); + int inChannels = mAudioInputTester.getCurrentAudioStream().getChannelCount(); + mInputChannelBoxes.setMaxEnabled(inChannels); + int outChannels = mAudioOutTester.getCurrentAudioStream().getChannelCount(); + mOutputChannelBoxes.setMaxEnabled(outChannels); } @Override @@ -147,7 +228,7 @@ public class ManualGlitchActivity extends GlitchActivity { void stopAutomaticTest() { String report = getCommonTestReport() - + String.format("tolerance = %5.3f\n", mTolerance) + + String.format(Locale.getDefault(), "tolerance = %5.3f\n", mTolerance) + mLastGlitchReport; onStopAudioTest(null); maybeWriteTestResult(report); @@ -156,13 +237,8 @@ public class ManualGlitchActivity extends GlitchActivity { // Only call from UI thread. @Override - public void onTestFinished() { - super.onTestFinished(); - } - - // Only call from UI thread. - @Override public void onTestBegan() { + mAutoScopeBox.setChecked(true); mWaveformView.clearSampleData(); mWaveformView.postInvalidate(); super.onTestBegan(); @@ -171,19 +247,45 @@ public class ManualGlitchActivity extends GlitchActivity { // Called on UI thread @Override protected void onGlitchDetected() { + if (mAutoScopeBox.isChecked()) { + mAutoScopeBox.setChecked(false); // stop auto drawing of waveform + mLastDisplayTime = 0; // force draw first glitch + } long now = System.currentTimeMillis(); + Log.i(TAG,"onGlitchDetected: glitch"); if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) { mLastDisplayTime = now; int numSamples = getGlitch(mWaveform); mWaveformView.setSampleData(mWaveform, 0, numSamples); + int glitchLength = getGlitchLength(); + int[] cursors = new int[glitchLength > 0 ? 2 : 1]; + int startOfGlitch = getSinePeriod(); + cursors[0] = startOfGlitch; + if (glitchLength > 0) { + cursors[1] = startOfGlitch + getGlitchLength(); + } + mWaveformView.setCursorData(cursors); + Log.i(TAG,"onGlitchDetected: glitch, numSamples = " + numSamples); mWaveformView.postInvalidate(); } } - - private float[] getGlitchWaveform() { - return mWaveform; + @Override + protected void maybeDisplayWaveform() { + if (!mAutoScopeBox.isChecked()) return; + long now = System.currentTimeMillis(); + if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) { + mLastDisplayTime = now; + int numSamples = getRecentSamples(mWaveform); + mWaveformView.setSampleData(mWaveform, 0, numSamples); + mWaveformView.setCursorData(null); + mWaveformView.postInvalidate(); + } } private native int getGlitch(float[] mWaveform); + private native int getRecentSamples(float[] mWaveform); + public void onForceGlitchClicked(View view) { + setForcedGlitchDuration(mForceGlitchesBox.isChecked() ? 100 : 0); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java index 8dbe502b..e96b4cb3 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java @@ -4,6 +4,7 @@ import android.media.MicrophoneInfo; import android.util.Pair; import java.util.List; +import java.util.Locale; public class MicrophoneInfoConverter { @@ -39,7 +40,7 @@ public class MicrophoneInfoConverter { static String convertCoordinates(MicrophoneInfo.Coordinate3F coordinates) { if (coordinates == MicrophoneInfo.POSITION_UNKNOWN) return "Unknown"; - return String.format("{ %6.4g, %5.3g, %5.3g }", + return String.format(Locale.getDefault(), "{ %6.4g, %5.3g, %5.3g }", coordinates.x, coordinates.y, coordinates.z); } @@ -60,6 +61,9 @@ public class MicrophoneInfoConverter { sb.append("\nType : " + micInfo.getType()); List<Pair<Integer, Integer>> mapping = micInfo.getChannelMapping(); + if (mapping == null) { + throw new RuntimeException("MicrophoneInfo. getChannelMapping() returned null!"); + } sb.append("\nChannelMapping: {"); for (Pair<Integer, Integer> pair : mapping) { sb.append("[" + pair.first + "," + pair.second + "], "); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java new file mode 100644 index 00000000..9b21e985 --- /dev/null +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java @@ -0,0 +1,254 @@ +package com.mobileer.oboetester; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.ArrayList; + +/** + * Draw a chart with multiple traces + */ +public class MultiLineChart extends View { + public static final int NUM_DATA_VALUES = 512; + private Paint mWavePaint; + private Paint mCursorPaint; + private int mBackgroundColor = 0xFFF0F0F0; + private int mLineColor = Color.RED; + private Paint mBackgroundPaint; + float[] mVertices = new float[4]; + + CircularFloatArray mXData = new CircularFloatArray(NUM_DATA_VALUES); + private ArrayList<Trace> mTraceList = new ArrayList<>(); + + public MultiLineChart(Context context) { + super(context); + init(null, 0); + } + + public MultiLineChart(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public MultiLineChart(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + private void init(AttributeSet attrs, int defStyle) { + // Load attributes + final TypedArray a = getContext().obtainStyledAttributes( + attrs, R.styleable.MultiLineChart, defStyle, 0); + + mBackgroundColor = a.getColor( + R.styleable.MultiLineChart_backgroundColor, + mBackgroundColor); + mLineColor = a.getColor( + R.styleable.MultiLineChart_backgroundColor, + mLineColor); + + a.recycle(); + + mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mWavePaint.setColor(mLineColor); + + mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mCursorPaint.setColor(Color.RED); + mCursorPaint.setStrokeWidth(3.0f); + + mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBackgroundPaint.setColor(mBackgroundColor); + mBackgroundPaint.setStyle(Paint.Style.FILL); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawRect(0.0f, 0.0f, getWidth(), + getHeight(), mBackgroundPaint); + + for (Trace trace : mTraceList) { + drawTrace(canvas, trace); + } + } + + void drawTrace(Canvas canvas, Trace trace) { + // Determine bounds and XY conversion. + int numPoints = mXData.size(); + if (numPoints < 2) return; + // Allocate array for polyline. + int arraySize = (numPoints - 1) * 4; + if (arraySize > mVertices.length) { + mVertices = new float[arraySize]; + } + // Setup scaling. + float previousX = 0.0f; + float previousY = 0.0f; + float xMax = getXData(1); + float xRange = xMax - getXData(numPoints); + float yMin = trace.getMin(); + float yRange = trace.getMax() - yMin; + float width = getWidth(); + float height = getHeight(); + float xScaler = width / xRange; + float yScaler = height / yRange; + // Iterate through the available data. + int vertexIndex = 0; + for (int i = 1; i < numPoints; i++) { + float xData = getXData(i); + float yData = trace.get(i); + float xPos = width - ((xMax - xData) * xScaler); + float yPos = height - ((yData - yMin) * yScaler); + if (i > 1) { + // Each line segment requires 4 values! + mVertices[vertexIndex++] = previousX; + mVertices[vertexIndex++] = previousY; + mVertices[vertexIndex++] = xPos; + mVertices[vertexIndex++] = yPos; + } + previousX = xPos; + previousY = yPos; + } + canvas.drawLines(mVertices, 0, vertexIndex, trace.paint); + } + + public float getXData(int i) { + return mXData.get(i); + } + + public MultiLineChart.Trace createTrace(String name, int color, float min, float max) { + Trace trace = new Trace(name, color, NUM_DATA_VALUES, min, max); + mTraceList.add(trace); + return trace; + } + + public void update() { + post(new Runnable() { + public void run() { + postInvalidate(); + } + }); + } + + public void addX(float value) { + mXData.add(value); + } + + public void reset() { + mXData.clear(); + for (Trace trace : mTraceList) { + trace.reset(); + } + } + + public static class Trace { + private final String mName; + public Paint paint; + protected float mMin; + protected float mMax; + protected CircularFloatArray mData; + + private Trace(String name, int color, int numValues, float min, float max) { + mName = name; + mMin = min; + mMax = max; + mData = new CircularFloatArray(numValues); + + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(color); + paint.setStrokeWidth(3.0f); + } + + public void reset() { + mData.clear(); + } + + public void add(float value) { + mData.add(value); + // Take the hit here instead of when drawing. + mData.add(Math.min(mMax, Math.max(mMin, value))); + } + + public int size() { + return mData.size(); + } + + /** + * Fetch a previous value. A delayIndex of 1 will return the most recently written value. + * A delayIndex of 2 will return the previously written value; + * @param delayIndex positive index of previously written data + * @return old value + */ + public float get(int delayIndex) { + return mData.get(delayIndex); + } + public float getMax() { + return mMax; + } + public float getMin() { + return mMin; + } + + public void setMin(float min) { + mMin = min; + } + public void setMax(float max) { + mMax = max; + } + } + + // Use explicit type for performance reasons. + private static class CircularFloatArray { + private float[] mData; + private int mIndexMask; + private int mCursor; // next location to be written + + public CircularFloatArray(int numValuesPowerOf2) { + if ((numValuesPowerOf2 & (numValuesPowerOf2 - 1)) != 0) { + throw new IllegalArgumentException("numValuesPowerOf2 not 2^N, was " + numValuesPowerOf2); + } + mData = new float[numValuesPowerOf2]; + mIndexMask = numValuesPowerOf2 - 1; + } + + /** + * Add one value to the array. + * This may overwrite the oldest data. + * @param value + */ + public void add(float value) { + int index = mCursor & mIndexMask; + mData[index] = value; + mCursor++; + } + + /** + * Number of valid entries. + * @return + */ + public int size() { + return Math.min(mCursor, mData.length); + } + + /** + * Fetch a previous value. A delayIndex of 1 will return the most recently written value. + * A delayIndex of 2 will return the previously written value; + * @param delayIndex positive index of previously written data + * @return old value + */ + public float get(int delayIndex) { + int index = (mCursor - delayIndex) & mIndexMask; + return mData[index]; + } + + public void clear() { + mCursor = 0; + } + } +}
\ No newline at end of file diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java index 6978bdca..985797b1 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java @@ -9,4 +9,8 @@ public class NativeEngine { static native void setWorkaroundsEnabled(boolean enabled); static native boolean areWorkaroundsEnabled(); + + static native int getCpuCount(); + + static native void setCpuAffinityMask(int mask); } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java index 1bc1232c..222173d7 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java @@ -16,23 +16,24 @@ package com.mobileer.oboetester; -import android.app.Activity; import android.os.Handler; import android.os.Looper; abstract class NativeSniffer implements Runnable { public static final int SNIFFER_UPDATE_PERIOD_MSEC = 100; public static final int SNIFFER_UPDATE_DELAY_MSEC = 200; - private final Activity activity; protected Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread protected volatile boolean mEnabled = true; - public NativeSniffer(Activity activity) { - this.activity = activity; + @Override + public void run() { + if (mEnabled && !isComplete()) { + updateStatusText(); + mHandler.postDelayed(this, SNIFFER_UPDATE_PERIOD_MSEC); + } } public void startSniffer() { - long now = System.currentTimeMillis(); // Start the initial runnable task by posting through the handler mEnabled = true; mHandler.postDelayed(this, SNIFFER_UPDATE_DELAY_MSEC); @@ -42,28 +43,19 @@ abstract class NativeSniffer implements Runnable { mEnabled = false; if (mHandler != null) { mHandler.removeCallbacks(this); + // Final update of the text. + mHandler.post(this); } - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - updateStatusText(); - } - }); } - public void reschedule() { - updateStatusText(); - // Reschedule so this task repeats - if (mEnabled) { - mHandler.postDelayed(this, SNIFFER_UPDATE_PERIOD_MSEC); - } + /** + * You can override this is if you want to control when sniffing is finished. + * @return true if finished + */ + public boolean isComplete() { + return false; } public abstract void updateStatusText(); - public String getShortReport() { - return "no-report"; - } - } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java index 92cde485..7a727db4 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java @@ -31,4 +31,6 @@ public class OboeAudioOutputStream extends OboeAudioStream { public native void setChannelEnabled(int channelIndex, boolean enabled); public native void setSignalType(int type); + + public native void setAmplitude(float amplitude); } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java index 5ff2dd12..5c8b2109 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java @@ -29,7 +29,7 @@ abstract class OboeAudioStream extends AudioStreamBase { @Override public void stopPlayback() throws IOException { int result = stopPlaybackNative(); - if (result < 0) { + if (result != 0) { throw new IOException("Stop Playback failed! result = " + result); } } @@ -39,19 +39,13 @@ abstract class OboeAudioStream extends AudioStreamBase { @Override public void startPlayback() throws IOException { int result = startPlaybackNative(); - if (result < 0) { + if (result != 0) { throw new IOException("Start Playback failed! result = " + result); } } public native int startPlaybackNative(); - // Write disabled because the synth is in native code. - @Override - public int write(float[] buffer, int offset, int length) { - return 0; - } - @Override public void open(StreamConfiguration requestedConfiguration, StreamConfiguration actualConfiguration, int bufferSizeInFrames) throws IOException { @@ -66,6 +60,7 @@ abstract class OboeAudioStream extends AudioStreamBase { requestedConfiguration.getInputPreset(), requestedConfiguration.getUsage(), requestedConfiguration.getContentType(), + requestedConfiguration.getBufferCapacityInFrames(), requestedConfiguration.getDeviceId(), requestedConfiguration.getSessionId(), requestedConfiguration.getChannelConversionAllowed(), @@ -76,7 +71,11 @@ abstract class OboeAudioStream extends AudioStreamBase { ); if (result < 0) { streamIndex = INVALID_STREAM_INDEX; - throw new IOException("Open failed! result = " + result); + String message = "Open " + + (isInput() ? "Input" : "Output") + + " failed! result = " + result + ", " + + StreamConfiguration.convertErrorToText(result); + throw new IOException(message); } else { streamIndex = result; } @@ -98,6 +97,9 @@ abstract class OboeAudioStream extends AudioStreamBase { actualConfiguration.setDirection(isInput() ? StreamConfiguration.DIRECTION_INPUT : StreamConfiguration.DIRECTION_OUTPUT); + actualConfiguration.setHardwareChannelCount(getHardwareChannelCount()); + actualConfiguration.setHardwareSampleRate(getHardwareSampleRate()); + actualConfiguration.setHardwareFormat(getHardwareFormat()); } private native int openNative( @@ -111,6 +113,7 @@ abstract class OboeAudioStream extends AudioStreamBase { int inputPreset, int usage, int contentType, + int bufferCapacityInFrames, int deviceId, int sessionId, boolean channelConversionAllowed, @@ -141,16 +144,23 @@ abstract class OboeAudioStream extends AudioStreamBase { private native int getBufferSizeInFrames(int streamIndex); @Override - public boolean isThresholdSupported() { - return true; - } - - @Override public int setBufferSizeInFrames(int thresholdFrames) { return setBufferSizeInFrames(streamIndex, thresholdFrames); } private native int setBufferSizeInFrames(int streamIndex, int thresholdFrames); + @Override + public void setPerformanceHintEnabled(boolean checked) { + setPerformanceHintEnabled(streamIndex, checked); + } + private native void setPerformanceHintEnabled(int streamIndex, boolean checked); + + @Override + public void setHearWorkload(boolean checked) { + setHearWorkload(streamIndex, checked); + } + private native void setHearWorkload(int streamIndex, boolean checked); + public int getNativeApi() { return getNativeApi(streamIndex); } @@ -207,6 +217,21 @@ abstract class OboeAudioStream extends AudioStreamBase { } private native int getChannelMask(int streamIndex); + public int getHardwareChannelCount() { + return getHardwareChannelCount(streamIndex); + } + private native int getHardwareChannelCount(int streamIndex); + + public int getHardwareSampleRate() { + return getHardwareSampleRate(streamIndex); + } + private native int getHardwareSampleRate(int streamIndex); + + public int getHardwareFormat() { + return getHardwareFormat(streamIndex); + } + private native int getHardwareFormat(int streamIndex); + public int getDeviceId() { return getDeviceId(streamIndex); } @@ -217,6 +242,7 @@ abstract class OboeAudioStream extends AudioStreamBase { } private native int getSessionId(int streamIndex); + public boolean isMMap() { return isMMap(streamIndex); } @@ -256,10 +282,22 @@ abstract class OboeAudioStream extends AudioStreamBase { private native double getTimestampLatency(int streamIndex); @Override - public double getCpuLoad() { + public float getCpuLoad() { return getCpuLoad(streamIndex); } - private native double getCpuLoad(int streamIndex); + private native float getCpuLoad(int streamIndex); + + @Override + public float getAndResetMaxCpuLoad() { + return getAndResetMaxCpuLoad(streamIndex); + } + private native float getAndResetMaxCpuLoad(int streamIndex); + + @Override + public int getAndResetCpuMask() { + return getAndResetCpuMask(streamIndex); + } + private native int getAndResetCpuMask(int streamIndex); @Override public String getCallbackTimeStr() { @@ -268,7 +306,7 @@ abstract class OboeAudioStream extends AudioStreamBase { public native String getCallbackTimeString(); @Override - public native void setWorkload(double workload); + public native void setWorkload(int workload); @Override public int getState() { @@ -278,6 +316,8 @@ abstract class OboeAudioStream extends AudioStreamBase { public static native void setCallbackReturnStop(boolean b); + public static native void setHangTimeMillis(int hangTimeMillis); + public static native void setUseCallback(boolean checked); public static native void setCallbackSize(int callbackSize); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java index 69207688..8f81f558 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java @@ -21,12 +21,15 @@ import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFro import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.annotation.NonNull; +import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; +import java.io.File; import java.io.IOException; +import java.util.Locale; /** * Activity to measure latency on a full duplex stream. @@ -132,18 +135,18 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { mTimestampLatencies.calculateMeanAbsoluteDeviation(timestampLatencyMean); } message = "average.latency.msec = " - + String.format(LATENCY_FORMAT, meanLatency) + "\n" + + String.format(Locale.getDefault(), LATENCY_FORMAT, meanLatency) + "\n" + "mean.absolute.deviation = " - + String.format(LATENCY_FORMAT, meanAbsoluteDeviation) + "\n" + + String.format(Locale.getDefault(), LATENCY_FORMAT, meanAbsoluteDeviation) + "\n" + "average.confidence = " - + String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n" - + "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMin()) + "\n" - + "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMax()) + "\n" + + String.format(Locale.getDefault(), CONFIDENCE_FORMAT, mAverageConfidence) + "\n" + + "min.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMin()) + "\n" + + "max.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMax()) + "\n" + "num.iterations = " + mLatencies.count() + "\n" + "timestamp.latency.msec = " - + String.format(LATENCY_FORMAT, timestampLatencyMean) + "\n" + + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMean) + "\n" + "timestamp.latency.mad = " - + String.format(LATENCY_FORMAT, timestampLatencyMAD) + "\n"; + + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMAD) + "\n"; } message += "num.failed = " + mBadCount + "\n"; message += "\n"; // mark end of average report @@ -208,7 +211,10 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { } else { message = getResultString(); } - onAnalyzerDone(); + File resultFile = onAnalyzerDone(); + if (resultFile != null) { + message = "result.file = " + resultFile.getAbsolutePath() + "\n" + message; + } } else { message = getProgressText(); message += "please wait... " + counter + "\n"; @@ -248,21 +254,23 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { int progress = getAnalyzerProgress(); int state = getAnalyzerState(); int resetCount = getResetCount(); - String message = String.format("progress = %d\nstate = %d\n#resets = %d\n", + String message = String.format(Locale.getDefault(), "progress = %d\nstate = %d\n#resets = %d\n", progress, state, resetCount); message += mAverageLatencyTestRunner.getLastReport(); return message; } - private void onAnalyzerDone() { + private File onAnalyzerDone() { + File resultFile = null; if (mTestRunningByIntent) { String report = getCommonTestReport(); report += getResultString(); - maybeWriteTestResult(report); + resultFile = maybeWriteTestResult(report); } mTestRunningByIntent = false; mHasRecording = true; stopAudioTest(); + return resultFile; } @NonNull @@ -272,8 +280,8 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { double confidence = getMeasuredConfidence(); String message = ""; - message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence); - message += String.format("result.text = %s\n", resultCodeToString(result)); + message += String.format(Locale.getDefault(), "confidence = " + CONFIDENCE_FORMAT + "\n", confidence); + message += String.format(Locale.getDefault(), "result.text = %s\n", resultCodeToString(result)); // Only report valid latencies. if (result == 0) { @@ -282,26 +290,26 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames(); int latencyEmptyFrames = latencyFrames - bufferSize; double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate(); - message += String.format("latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis); - message += String.format("latency.frames = %d\n", latencyFrames); - message += String.format("latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis); - message += String.format("latency.empty.frames = %d\n", latencyEmptyFrames); + message += String.format(Locale.getDefault(), "latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis); + message += String.format(Locale.getDefault(), "latency.frames = %d\n", latencyFrames); + message += String.format(Locale.getDefault(), "latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis); + message += String.format(Locale.getDefault(), "latency.empty.frames = %d\n", latencyEmptyFrames); } - message += String.format("rms.signal = %7.5f\n", getSignalRMS()); - message += String.format("rms.noise = %7.5f\n", getBackgroundRMS()); - message += String.format("correlation = " + CONFIDENCE_FORMAT + "\n", + message += String.format(Locale.getDefault(), "rms.signal = %7.5f\n", getSignalRMS()); + message += String.format(Locale.getDefault(), "rms.noise = %7.5f\n", getBackgroundRMS()); + message += String.format(Locale.getDefault(), "correlation = " + CONFIDENCE_FORMAT + "\n", getMeasuredCorrelation()); double timestampLatency = getTimestampLatencyMillis(); - message += String.format("timestamp.latency.msec = " + LATENCY_FORMAT + "\n", + message += String.format(Locale.getDefault(), "timestamp.latency.msec = " + LATENCY_FORMAT + "\n", timestampLatency); if (mTimestampLatencyStats.count() > 0) { - message += String.format("timestamp.latency.mad = " + LATENCY_FORMAT + "\n", + message += String.format(Locale.getDefault(), "timestamp.latency.mad = " + LATENCY_FORMAT + "\n", mTimestampLatencyStats.calculateMeanAbsoluteDeviation(timestampLatency)); } message += "timestamp.latency.count = " + mTimestampLatencyStats.count() + "\n"; - message += String.format("reset.count = %d\n", resetCount); - message += String.format("result = %d\n", result); + message += String.format(Locale.getDefault(), "reset.count = %d\n", resetCount); + message += String.format(Locale.getDefault(), "result = %d\n", result); return message; } @@ -348,6 +356,9 @@ public class RoundTripLatencyActivity extends AnalyzerActivity { hideSettingsViews(); mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency + + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); + } @Override diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java index ea9a9e8c..5c1f32b7 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; /** * Container for the properties of a Stream. @@ -44,6 +45,7 @@ public class StreamConfiguration { public static final int AUDIO_FORMAT_PCM_FLOAT = 2; // must match AAUDIO public static final int AUDIO_FORMAT_PCM_24 = 3; // must match AAUDIO public static final int AUDIO_FORMAT_PCM_32 = 4; // must match AAUDIO + public static final int AUDIO_FORMAT_IEC61937 = 5; // must match AAUDIO public static final int DIRECTION_OUTPUT = 0; // must match AAUDIO public static final int DIRECTION_INPUT = 1; // must match AAUDIO @@ -72,7 +74,25 @@ public class StreamConfiguration { public static final int INPUT_PRESET_UNPROCESSED = 9; // must match Oboe public static final int INPUT_PRESET_VOICE_PERFORMANCE = 10; // must match Oboe + public static final int ERROR_BASE = -900; // must match Oboe public static final int ERROR_DISCONNECTED = -899; // must match Oboe + public static final int ERROR_ILLEGAL_ARGUMENT = -898; // must match Oboe + public static final int ERROR_INTERNAL = -896; // must match Oboe + public static final int ERROR_INVALID_STATE = -895; // must match Oboe + public static final int ERROR_INVALID_HANDLE = -892; // must match Oboe + public static final int ERROR_UNIMPLEMENTED = -890; // must match Oboe + public static final int ERROR_UNAVAILABLE = -889; // must match Oboe + public static final int ERROR_NO_FREE_HANDLES = -888; // must match Oboe + public static final int ERROR_NO_MEMORY = -887; // must match Oboe + public static final int ERROR_NULL = -886; // must match Oboe + public static final int ERROR_TIMEOUT = -885; // must match Oboe + public static final int ERROR_WOULD_BLOCK = -884; // must match Oboe + public static final int ERROR_INVALID_FORMAT = -883; // must match Oboe + public static final int ERROR_OUT_OF_RANGE = -882; // must match Oboe + public static final int ERROR_NO_SERVICE = -881; // must match Oboe + public static final int ERROR_INVALID_RATE = -880; // must match Oboe + public static final int ERROR_CLOSED = -869; // must match Oboe + public static final int ERROR_OK = 0; // must match Oboe public static final int USAGE_MEDIA = 1; public static final int USAGE_VOICE_COMMUNICATION = 2; @@ -277,6 +297,9 @@ public class StreamConfiguration { private int mFramesPerBurst; private boolean mMMap; private int mChannelMask; + private int mHardwareChannelCount; + private int mHardwareSampleRate; + private int mHardwareFormat; public StreamConfiguration() { reset(); @@ -327,6 +350,9 @@ public class StreamConfiguration { mChannelConversionAllowed = false; mRateConversionQuality = RATE_CONVERSION_QUALITY_NONE; mMMap = NativeEngine.isMMapSupported(); + mHardwareChannelCount = UNSPECIFIED; + mHardwareSampleRate = UNSPECIFIED; + mHardwareFormat = UNSPECIFIED; } public int getFramesPerBurst() { @@ -486,6 +512,8 @@ public class StreamConfiguration { return "I32"; case AUDIO_FORMAT_PCM_FLOAT: return "Float"; + case AUDIO_FORMAT_IEC61937: + return "IEC61937"; default: return "Invalid"; } @@ -563,6 +591,25 @@ public class StreamConfiguration { } } + static String convertRateConversionQualityToText(int quality) { + switch(quality) { + case RATE_CONVERSION_QUALITY_NONE: + return "None"; + case RATE_CONVERSION_QUALITY_FASTEST: + return "Fastest"; + case RATE_CONVERSION_QUALITY_LOW: + return "Low"; + case RATE_CONVERSION_QUALITY_MEDIUM: + return "Medium"; + case RATE_CONVERSION_QUALITY_HIGH: + return "High"; + case RATE_CONVERSION_QUALITY_BEST: + return "Best"; + default: + return "?=" + quality; + } + } + public static int convertTextToChannelMask(String text) { return mChannelMaskStringToIntegerMap.get(text); } @@ -571,26 +618,30 @@ public class StreamConfiguration { public String dump() { String prefix = (getDirection() == DIRECTION_INPUT) ? "in" : "out"; StringBuffer message = new StringBuffer(); - message.append(String.format("%s.channels = %d\n", prefix, mChannelCount)); - message.append(String.format("%s.perf = %s\n", prefix, - convertPerformanceModeToText(mPerformanceMode).toLowerCase())); + message.append(String.format(Locale.getDefault(), "%s.channels = %d\n", prefix, mChannelCount)); + message.append(String.format(Locale.getDefault(), "%s.perf = %s\n", prefix, + convertPerformanceModeToText(mPerformanceMode).toLowerCase(Locale.getDefault()))); if (getDirection() == DIRECTION_INPUT) { - message.append(String.format("%s.preset = %s\n", prefix, - convertInputPresetToText(mInputPreset).toLowerCase())); + message.append(String.format(Locale.getDefault(), "%s.preset = %s\n", prefix, + convertInputPresetToText(mInputPreset).toLowerCase(Locale.getDefault()))); } else { - message.append(String.format("%s.preset = %s\n", prefix, - convertUsageToText(mUsage).toLowerCase())); - message.append(String.format("%s.contentType = %s\n", prefix, - convertContentTypeToText(mContentType).toLowerCase())); + message.append(String.format(Locale.getDefault(), "%s.usage = %s\n", prefix, + convertUsageToText(mUsage).toLowerCase(Locale.getDefault()))); + message.append(String.format(Locale.getDefault(), "%s.contentType = %s\n", prefix, + convertContentTypeToText(mContentType).toLowerCase(Locale.getDefault()))); } - message.append(String.format("%s.sharing = %s\n", prefix, - convertSharingModeToText(mSharingMode).toLowerCase())); - message.append(String.format("%s.api = %s\n", prefix, - convertNativeApiToText(getNativeApi()).toLowerCase())); - message.append(String.format("%s.rate = %d\n", prefix, mSampleRate)); - message.append(String.format("%s.device = %d\n", prefix, mDeviceId)); - message.append(String.format("%s.mmap = %s\n", prefix, isMMap() ? "yes" : "no")); - message.append(String.format("%s.rate.conversion.quality = %d\n", prefix, mRateConversionQuality)); + message.append(String.format(Locale.getDefault(), "%s.sharing = %s\n", prefix, + convertSharingModeToText(mSharingMode).toLowerCase(Locale.getDefault()))); + message.append(String.format(Locale.getDefault(), "%s.api = %s\n", prefix, + convertNativeApiToText(getNativeApi()).toLowerCase(Locale.getDefault()))); + message.append(String.format(Locale.getDefault(), "%s.rate = %d\n", prefix, mSampleRate)); + message.append(String.format(Locale.getDefault(), "%s.device = %d\n", prefix, mDeviceId)); + message.append(String.format(Locale.getDefault(), "%s.mmap = %s\n", prefix, isMMap() ? "yes" : "no")); + message.append(String.format(Locale.getDefault(), "%s.rate.conversion.quality = %d\n", prefix, mRateConversionQuality)); + message.append(String.format(Locale.getDefault(), "%s.hardware.channels = %d\n", prefix, mHardwareChannelCount)); + message.append(String.format(Locale.getDefault(), "%s.hardware.sampleRate = %d\n", prefix, mHardwareSampleRate)); + message.append(String.format(Locale.getDefault(), "%s.hardware.format = %s\n", prefix, + convertFormatToText(mHardwareFormat).toLowerCase(Locale.getDefault()))); return message.toString(); } @@ -622,7 +673,7 @@ public class StreamConfiguration { } private static boolean matchInputPreset(String text, int preset) { - return convertInputPresetToText(preset).toLowerCase().equals(text); + return convertInputPresetToText(preset).toLowerCase(Locale.getDefault()).equals(text); } /** @@ -631,7 +682,7 @@ public class StreamConfiguration { * @return inputPreset, eg. INPUT_PRESET_CAMCORDER */ public static int convertTextToInputPreset(String text) { - text = text.toLowerCase(); + text = text.toLowerCase(Locale.getDefault()); if (matchInputPreset(text, INPUT_PRESET_GENERIC)) { return INPUT_PRESET_GENERIC; } else if (matchInputPreset(text, INPUT_PRESET_CAMCORDER)) { @@ -726,4 +777,72 @@ public class StreamConfiguration { return mChannelMaskStrings; } + public int getHardwareChannelCount() { + return mHardwareChannelCount; + } + + public void setHardwareChannelCount(int hardwareChannelCount) { + this.mHardwareChannelCount = hardwareChannelCount; + } + + public int getHardwareSampleRate() { + return mHardwareSampleRate; + } + + public void setHardwareSampleRate(int hardwareSampleRate) { + this.mHardwareSampleRate = hardwareSampleRate; + } + + public int getHardwareFormat() { + return mHardwareFormat; + } + + public void setHardwareFormat(int hardwareFormat) { + this.mHardwareFormat = hardwareFormat; + } + + static String convertErrorToText(int error) { + switch (error) { + case ERROR_BASE: + return "ErrorBase"; + case ERROR_DISCONNECTED: + return "ErrorDisconnected"; + case ERROR_ILLEGAL_ARGUMENT: + return "ErrorIllegalArgument"; + case ERROR_INTERNAL: + return "ErrorInternal"; + case ERROR_INVALID_STATE: + return "ErrorInvalidState"; + case ERROR_INVALID_HANDLE: + return "ErrorInvalidHandle"; + case ERROR_UNIMPLEMENTED: + return "ErrorUnimplemented"; + case ERROR_UNAVAILABLE: + return "ErrorUnavailable"; + case ERROR_NO_FREE_HANDLES: + return "ErrorNoFreeHandles"; + case ERROR_NO_MEMORY: + return "ErrorNoMemory"; + case ERROR_NULL: + return "ErrorNull"; + case ERROR_TIMEOUT: + return "ErrorTimeout"; + case ERROR_WOULD_BLOCK: + return "ErrorWouldBlock"; + case ERROR_INVALID_FORMAT: + return "ErrorInvalidFormat"; + case ERROR_OUT_OF_RANGE: + return "ErrorOutOfRange"; + case ERROR_NO_SERVICE: + return "ErrorNoService"; + case ERROR_INVALID_RATE: + return "ErrorInvalidRate"; + case ERROR_CLOSED: + return "ErrorClosed"; + case ERROR_OK: + return "ErrorOk"; + default: + return "?=" + error; + } + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java index eeb962f0..418d08e5 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java @@ -39,6 +39,8 @@ import android.util.Log; import com.mobileer.audio_device.AudioDeviceListEntry; import com.mobileer.audio_device.AudioDeviceSpinner; +import java.util.Locale; + /** * View for Editing a requested StreamConfiguration * and displaying the actual StreamConfiguration. @@ -63,7 +65,8 @@ public class StreamConfigurationView extends LinearLayout { private Spinner mChannelMaskSpinner; private TextView mActualChannelMaskView; private TextView mActualFormatView; - + private Spinner mCapacitySpinner; + private TextView mActualCapacityView; private TableRow mInputPresetTableRow; private Spinner mInputPresetSpinner; private TextView mActualInputPresetView; @@ -288,6 +291,10 @@ public class StreamConfigurationView extends LinearLayout { }); mActualFormatView = (TextView) findViewById(R.id.actualAudioFormat); mFormatSpinner = (Spinner) findViewById(R.id.spinnerFormat); + + mActualCapacityView = (TextView) findViewById(R.id.actualCapacity); + mCapacitySpinner = (Spinner) findViewById(R.id.spinnerCapacity); + mRateConversionQualitySpinner = (Spinner) findViewById(R.id.spinnerSRCQuality); mActualPerformanceView = (TextView) findViewById(R.id.actualPerformanceMode); @@ -388,13 +395,17 @@ public class StreamConfigurationView extends LinearLayout { int channelMask = StreamConfiguration.convertTextToChannelMask(text); config.setChannelMask(channelMask); config.setChannelCount(0); - Log.d(TAG, String.format("Set channel mask as %s(%#x)", text, channelMask)); + Log.d(TAG, String.format(Locale.getDefault(), "Set channel mask as %s(%#x)", text, channelMask)); } else { config.setChannelCount(mChannelCountSpinner.getSelectedItemPosition()); config.setChannelMask(StreamConfiguration.UNSPECIFIED); Log.d(TAG, "Set channel count as " + mChannelCountSpinner.getSelectedItemPosition()); } + text = mCapacitySpinner.getSelectedItem().toString(); + int bufferCapacity = Integer.parseInt(text); + config.setBufferCapacityInFrames(bufferCapacity); + config.setMMap(mRequestedMMapView.isChecked()); config.setChannelConversionAllowed(mChannelConversionBox.isChecked()); config.setFormatConversionAllowed(mFormatConversionBox.isChecked()); @@ -418,6 +429,7 @@ public class StreamConfigurationView extends LinearLayout { mFormatConversionBox.setEnabled(enabled); mChannelCountSpinner.setEnabled(enabled); mChannelMaskSpinner.setEnabled(enabled); + mCapacitySpinner.setEnabled(enabled); mInputPresetSpinner.setEnabled(enabled); mUsageSpinner.setEnabled(enabled); mContentTypeSpinner.setEnabled(enabled); @@ -465,14 +477,27 @@ public class StreamConfigurationView extends LinearLayout { mActualSessionIdView.setText("S#: " + actualConfiguration.getSessionId()); value = actualConfiguration.getChannelMask(); mActualChannelMaskView.setText(StreamConfiguration.convertChannelMaskToText(value)); + mActualCapacityView.setText(actualConfiguration.getBufferCapacityInFrames() + ""); boolean isMMap = actualConfiguration.isMMap(); - mStreamInfoView.setText("burst = " + actualConfiguration.getFramesPerBurst() - + ", capacity = " + actualConfiguration.getBufferCapacityInFrames() - + ", devID = " + actualConfiguration.getDeviceId() - + ", " + (actualConfiguration.isMMap() ? "MMAP" : "Legacy") - + (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : "") - ); + + String msg = ""; + msg += "burst = " + actualConfiguration.getFramesPerBurst(); + msg += ", devID = " + actualConfiguration.getDeviceId(); + msg += ", " + (actualConfiguration.isMMap() ? "MMAP" : "Legacy"); + msg += (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : ""); + + int hardwareChannelCount = actualConfiguration.getHardwareChannelCount(); + int hardwareSampleRate = actualConfiguration.getHardwareSampleRate(); + int hardwareFormat = actualConfiguration.getHardwareFormat(); + msg += "\nHW: #ch=" + (hardwareChannelCount == + StreamConfiguration.UNSPECIFIED ? "?" : hardwareChannelCount); + msg += ", SR=" + (hardwareSampleRate == + StreamConfiguration.UNSPECIFIED ? "?" : hardwareSampleRate); + msg += ", format=" + (hardwareFormat == StreamConfiguration.UNSPECIFIED ? + "?" : StreamConfiguration.convertFormatToText(hardwareFormat)); + + mStreamInfoView.setText(msg); mHideableView.requestLayout(); } @@ -531,7 +556,7 @@ public class StreamConfigurationView extends LinearLayout { if (mAcousticEchoCanceler != null) { mAcousticEchoCanceler.setEnabled(mAcousticEchoCancelerCheckBox.isChecked()); } else { - Log.e(TAG, String.format("Could not create AcousticEchoCanceler")); + Log.e(TAG, String.format(Locale.getDefault(), "Could not create AcousticEchoCanceler")); } } // If AGC is not available, the checkbox will be disabled in initializeViews(). @@ -540,7 +565,7 @@ public class StreamConfigurationView extends LinearLayout { if (mAutomaticGainControl != null) { mAutomaticGainControl.setEnabled(mAutomaticGainControlCheckBox.isChecked()); } else { - Log.e(TAG, String.format("Could not create AutomaticGainControl")); + Log.e(TAG, String.format(Locale.getDefault(), "Could not create AutomaticGainControl")); } } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java index 2da6bf5f..ad2c3c97 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java @@ -18,6 +18,8 @@ package com.mobileer.oboetester; import android.Manifest; import android.content.pm.PackageManager; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; import android.media.midi.MidiDevice; import android.media.midi.MidiDeviceInfo; import android.media.midi.MidiInputPort; @@ -30,9 +32,13 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; import android.widget.Button; import android.widget.Toast; +import com.mobileer.audio_device.AudioDeviceListEntry; +import com.mobileer.audio_device.AudioDeviceSpinner; import com.mobileer.miditools.MidiOutputPortConnectionSelector; import com.mobileer.miditools.MidiPortConnector; import com.mobileer.miditools.MidiTools; @@ -59,6 +65,8 @@ public class TapToToneActivity extends TestOutputActivityBase { private MidiOutputPortConnectionSelector mPortSelector; private final MyNoteListener mTestListener = new MyNoteListener(); + private AudioDeviceSpinner mInputDeviceSpinner; + @Override protected void inflateActivity() { setContentView(R.layout.activity_tap_to_tone); @@ -103,11 +111,16 @@ public class TapToToneActivity extends TestOutputActivityBase { return true; }); + mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); + mStartButton = (Button) findViewById(R.id.button_start); mStopButton = (Button) findViewById(R.id.button_stop); updateButtons(false); updateEnabledWidgets(); + + mInputDeviceSpinner = (AudioDeviceSpinner) findViewById(R.id.input_devices_spinner); + mInputDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS); } private void updateButtons(boolean running) { @@ -252,28 +265,6 @@ public class TapToToneActivity extends TestOutputActivityBase { } } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - - return super.onOptionsItemSelected(item); - } - public void startTest(View view) { try { openAudio(); @@ -284,8 +275,7 @@ public class TapToToneActivity extends TestOutputActivityBase { } try { super.startAudio(); - mTapToToneTester.resetLatency(); - mTapToToneTester.start(); + startTapToToneTester(); updateButtons(true); } catch (IOException e) { e.printStackTrace(); @@ -295,9 +285,25 @@ public class TapToToneActivity extends TestOutputActivityBase { } public void stopTest(View view) { - mTapToToneTester.stop(); + stopTapToToneTester(); stopAudio(); closeAudio(); updateButtons(false); } + + private void startTapToToneTester() throws IOException { + AudioDeviceInfo deviceInfo = + ((AudioDeviceListEntry) mInputDeviceSpinner.getSelectedItem()).getDeviceInfo(); + mTapToToneTester.setInputDevice(deviceInfo); + mInputDeviceSpinner.setEnabled(false); + mTapToToneTester.resetLatency(); + mTapToToneTester.start(); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void stopTapToToneTester() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mInputDeviceSpinner.setEnabled(true); + mTapToToneTester.stop(); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java index bd931534..ce92a25a 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java @@ -1,9 +1,11 @@ package com.mobileer.oboetester; import android.app.Activity; +import android.media.AudioDeviceInfo; import android.widget.TextView; import java.io.IOException; +import java.util.Locale; /** * Measure tap-to-tone latency by and update the waveform display. @@ -176,7 +178,7 @@ public class TapToToneTester { mLatencyMax = latencyMillis; } - text = String.format("tap-to-tone latency = %3d msec\n", latencyMillis); + text = String.format(Locale.getDefault(), "tap-to-tone latency = %3d msec\n", latencyMillis); } mWaveformView.setSampleData(result.filtered); } @@ -185,7 +187,7 @@ public class TapToToneTester { int averageLatencySamples = mLatencySumSamples / mMeasurementCount; int averageLatencyMillis = 1000 * averageLatencySamples / result.frameRate; final String plural = (mMeasurementCount == 1) ? "test" : "tests"; - text = text + String.format("min = %3d, avg = %3d, max = %3d, %d %s", + text = text + String.format(Locale.getDefault(), "min = %3d, avg = %3d, max = %3d, %d %s", mLatencyMin, averageLatencyMillis, mLatencyMax, mMeasurementCount, plural); } final String postText = text; @@ -198,4 +200,8 @@ public class TapToToneTester { mArmed = true; } + + void setInputDevice(AudioDeviceInfo deviceInfo) { + mRecorder.setInputDevice(deviceInfo); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java index f61873fa..9043f28d 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java @@ -16,34 +16,32 @@ package com.mobileer.oboetester; -import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.media.AudioAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.View; import android.view.WindowManager; +import android.widget.AdapterView; import android.widget.Button; import android.widget.CheckBox; +import android.widget.Spinner; import android.widget.Toast; +import androidx.annotation.NonNull; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; import java.util.ArrayList; +import java.util.Locale; /** * Base class for other Activities. @@ -57,9 +55,11 @@ abstract class TestAudioActivity extends Activity { public static final int AUDIO_STATE_OPEN = 0; public static final int AUDIO_STATE_STARTED = 1; public static final int AUDIO_STATE_PAUSED = 2; - public static final int AUDIO_STATE_STOPPED = 3; - public static final int AUDIO_STATE_CLOSING = 4; - public static final int AUDIO_STATE_CLOSED = 5; + public static final int AUDIO_STATE_FLUSHED = 3; + public static final int AUDIO_STATE_STOPPED = 4; + public static final int AUDIO_STATE_RELEASED = 5; + public static final int AUDIO_STATE_CLOSING = 6; + public static final int AUDIO_STATE_CLOSED = 7; public static final int COLOR_ACTIVE = 0xFFD0D0A0; public static final int COLOR_IDLE = 0xFFD0D0D0; @@ -75,8 +75,7 @@ abstract class TestAudioActivity extends Activity { public static final int ACTIVITY_GLITCHES = 6; public static final int ACTIVITY_TEST_DISCONNECT = 7; public static final int ACTIVITY_DATA_PATHS = 8; - - private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 1001; + public static final int ACTIVITY_DYNAMIC_WORKLOAD = 9; private int mAudioState = AUDIO_STATE_CLOSED; @@ -84,10 +83,16 @@ abstract class TestAudioActivity extends Activity { private Button mOpenButton; private Button mStartButton; private Button mPauseButton; + private Button mFlushButton; private Button mStopButton; + private Button mReleaseButton; private Button mCloseButton; private MyStreamSniffer mStreamSniffer; private CheckBox mCallbackReturnStopBox; + private Spinner mHangTimeSpinner; + + // Only set in some activities + protected CommunicationDeviceView mCommunicationDeviceView; private int mSampleRate; private int mSingleTestIndex = -1; private static boolean mBackgroundEnabled; @@ -96,6 +101,7 @@ abstract class TestAudioActivity extends Activity { protected boolean mTestRunningByIntent; protected String mResultFileName; private String mTestResults; + private ExternalFileWriter mExternalFileWriter = new ExternalFileWriter(this); public String getTestName() { return "TestAudio"; @@ -225,6 +231,10 @@ abstract class TestAudioActivity extends Activity { super.onStart(); resetConfiguration(); setActivityType(getActivityType()); + // TODO Use LifeCycleObserver instead of this. + if (mCommunicationDeviceView != null) { + mCommunicationDeviceView.onStart(); + } } protected void resetConfiguration() { @@ -289,6 +299,9 @@ abstract class TestAudioActivity extends Activity { Log.i(TAG, "onStop() called so stop the test ========================="); onStopTest(); } + if (mCommunicationDeviceView != null) { + mCommunicationDeviceView.onStop(); + } super.onStop(); } @@ -307,7 +320,9 @@ abstract class TestAudioActivity extends Activity { mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE); mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE); mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE); + mFlushButton.setBackgroundColor(mAudioState == AUDIO_STATE_FLUSHED ? COLOR_ACTIVE : COLOR_IDLE); mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE); + mReleaseButton.setBackgroundColor(mAudioState == AUDIO_STATE_RELEASED ? COLOR_ACTIVE : COLOR_IDLE); mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE); } setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED); @@ -368,7 +383,6 @@ abstract class TestAudioActivity extends Activity { if (streamContext.configurationView != null) { streamContext.configurationView.setOutput(false); } - streamContext.tester = AudioInputTester.getInstance(); mStreamContexts.add(streamContext); return streamContext; } @@ -407,7 +421,9 @@ abstract class TestAudioActivity extends Activity { if (mOpenButton != null) { mStartButton = (Button) findViewById(R.id.button_start); mPauseButton = (Button) findViewById(R.id.button_pause); + mFlushButton = (Button) findViewById(R.id.button_flush); mStopButton = (Button) findViewById(R.id.button_stop); + mReleaseButton = (Button) findViewById(R.id.button_release); mCloseButton = (Button) findViewById(R.id.button_close); } mStreamContexts = new ArrayList<StreamContext>(); @@ -423,6 +439,24 @@ abstract class TestAudioActivity extends Activity { } OboeAudioStream.setCallbackReturnStop(false); + mHangTimeSpinner = (Spinner) findViewById(R.id.spinner_hang_time); + if (mHangTimeSpinner != null) { + mHangTimeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + String hangTimeText = (String) mHangTimeSpinner.getAdapter().getItem(position); + int hangTimeMillis = Integer.parseInt(hangTimeText); + Log.d(TAG, "Hang Time = " + hangTimeMillis + " msec"); + + OboeAudioStream.setHangTimeMillis(hangTimeMillis); + } + + public void onNothingSelected(AdapterView<?> parent) { + OboeAudioStream.setHangTimeMillis(0); + } + }); + } + OboeAudioStream.setHangTimeMillis(0); + mStreamSniffer = new MyStreamSniffer(); } @@ -438,8 +472,8 @@ abstract class TestAudioActivity extends Activity { } protected void showErrorToast(String message) { + Log.e(TAG, "showErrorToast(\"" + message + "\")"); String text = "Error: " + message; - Log.e(TAG, text); showToast(text); } @@ -454,20 +488,39 @@ abstract class TestAudioActivity extends Activity { }); } + private void onStartAllContexts() { + for (StreamContext streamContext : mStreamContexts) { + streamContext.tester.getCurrentAudioStream().onStart(); + } + } + private void onStopAllContexts() { + for (StreamContext streamContext : mStreamContexts) { + streamContext.tester.getCurrentAudioStream().onStop(); + } + } + public void openAudio(View view) { try { openAudio(); } catch (Exception e) { - showErrorToast(e.getMessage()); + showErrorToast("openAudio() caught " + e.getMessage()); + } + } + + void clearHangTime() { + OboeAudioStream.setHangTimeMillis(0); + if (mHangTimeSpinner != null) { + mHangTimeSpinner.setSelection(0); } } public void startAudio(View view) { Log.i(TAG, "startAudio() called ======================================="); + clearHangTime(); // start running try { startAudio(); } catch (Exception e) { - showErrorToast(e.getMessage()); + showErrorToast("startAudio() caught " + e.getMessage()); } keepScreenOn(true); } @@ -490,10 +543,18 @@ abstract class TestAudioActivity extends Activity { keepScreenOn(false); } + public void flushAudio(View view) { + flushAudio(); + } + public void closeAudio(View view) { closeAudio(); } + public void releaseAudio(View view) { + releaseAudio(); + } + public int getSampleRate() { return mSampleRate; } @@ -507,18 +568,25 @@ abstract class TestAudioActivity extends Activity { applyConfigurationViewsToModels(); } - int sampleRate = 0; + int sampleRate = 0; // Use the OUTPUT sample rate for INPUT // Open output streams then open input streams. // This is so that the capacity of input stream can be expanded to // match the burst size of the output for full duplex. for (StreamContext streamContext : mStreamContexts) { - if (!streamContext.isInput()) { + if (!streamContext.isInput()) { // OUTPUT? openStreamContext(streamContext); int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate(); if (sampleRate == 0) { sampleRate = streamSampleRate; } + + if (shouldSetStreamControlByAttributes()) { + // Associate volume keys with this output stream. + int actualUsage = streamContext.tester.actualConfiguration.getUsage(); + int actualContentType = streamContext.tester.actualConfiguration.getContentType(); + setStreamControlByAttributes(actualUsage, actualContentType); + } } } for (StreamContext streamContext : mStreamContexts) { @@ -530,9 +598,29 @@ abstract class TestAudioActivity extends Activity { } } updateEnabledWidgets(); + onStartAllContexts(); mStreamSniffer.startStreamSniffer(); } + protected boolean shouldSetStreamControlByAttributes() { + return true; + } + + /** + * Associate the volume keys with the stream we are playing. + * @param usage usage for the stream + * @param contentType tupe of the stream + */ + private void setStreamControlByAttributes(int usage, int contentType) { + AudioAttributes attributes = new AudioAttributes.Builder().setUsage(usage) + .setContentType(contentType).build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int volumeControlStream = attributes.getVolumeControlStream(); + Log.i(TAG, "setVolumeControlStream(" + volumeControlStream + ")"); + setVolumeControlStream(volumeControlStream); + } + } + /** * @param deviceId * @return true if the device is TYPE_BLUETOOTH_SCO @@ -564,7 +652,7 @@ abstract class TestAudioActivity extends Activity { try { streamContext.configurationView.setupEffects(sessionId); } catch (Exception e) { - showErrorToast(e.getMessage()); + showErrorToast("openStreamContext() caught " + e.getMessage()); } } streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration); @@ -576,21 +664,28 @@ abstract class TestAudioActivity extends Activity { private native int pauseNative(); + private native int flushNative(); + private native int stopNative(); + private native int releaseNative(); + protected native void setActivityType(int activityType); private native int getFramesPerCallback(); + public native void setUseAlternativeAdpf(boolean enabled); + private static native void setDefaultAudioValues(int audioManagerSampleRate, int audioManagerFramesPerBurst); public void startAudio() throws IOException { Log.i(TAG, "startAudio() called ========================="); int result = startNative(); - if (result < 0) { - showErrorToast("Start failed with " + result); - throw new IOException("startNative returned " + result); + if (result != 0) { + showErrorToast("Start failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + throw new IOException("startNative returned " + result + ", " + StreamConfiguration.convertErrorToText(result)); } else { + onStartAllContexts(); for (StreamContext streamContext : mStreamContexts) { StreamConfigurationView configView = streamContext.configurationView; if (configView != null) { @@ -603,26 +698,49 @@ abstract class TestAudioActivity extends Activity { } protected void toastPauseError(int result) { - showErrorToast("Pause failed with " + result); + showErrorToast("Pause failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); } public void pauseAudio() { int result = pauseNative(); - if (result < 0) { + if (result != 0) { toastPauseError(result); } else { mAudioState = AUDIO_STATE_PAUSED; updateEnabledWidgets(); + onStopAllContexts(); + } + } + + public void flushAudio() { + int result = flushNative(); + if (result != 0) { + showErrorToast("Flush failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } else { + mAudioState = AUDIO_STATE_FLUSHED; + updateEnabledWidgets(); } } public void stopAudio() { int result = stopNative(); - if (result < 0) { - showErrorToast("Stop failed with " + result); + if (result != 0) { + showErrorToast("Stop failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); } else { mAudioState = AUDIO_STATE_STOPPED; updateEnabledWidgets(); + onStopAllContexts(); + } + } + + public void releaseAudio() { + int result = releaseNative(); + if (result != 0) { + showErrorToast("Release failed with " + result + ", " + StreamConfiguration.convertErrorToText(result)); + } else { + mAudioState = AUDIO_STATE_RELEASED; + updateEnabledWidgets(); + onStopAllContexts(); } } @@ -684,24 +802,6 @@ abstract class TestAudioActivity extends Activity { myAudioMgr.stopBluetoothSco(); } - @Override - public void onRequestPermissionsResult(int requestCode, - String[] permissions, - int[] grantResults) { - - if (MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE != requestCode) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - return; - } - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - writeTestResult(mTestResults); - } else { - showToast("Writing external storage needed for test results."); - } - } - @NonNull protected String getCommonTestReport() { StringBuffer report = new StringBuffer(); @@ -709,8 +809,8 @@ abstract class TestAudioActivity extends Activity { report.append("build.fingerprint = " + Build.FINGERPRINT + "\n"); try { PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0); - report.append(String.format("test.version = %s\n", pinfo.versionName)); - report.append(String.format("test.version.code = %d\n", pinfo.versionCode)); + report.append(String.format(Locale.getDefault(), "test.version = %s\n", pinfo.versionName)); + report.append(String.format(Locale.getDefault(), "test.version.code = %d\n", pinfo.versionCode)); } catch (PackageManager.NameNotFoundException e) { } report.append("time.millis = " + System.currentTimeMillis() + "\n"); @@ -733,48 +833,17 @@ abstract class TestAudioActivity extends Activity { return report.toString(); } - void writeTestResultIfPermitted(String resultString) { - // Here, thisActivity is the current activity - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - mTestResults = resultString; - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE); - } else { - // Permission has already been granted - writeTestResult(resultString); - } - } - - void maybeWriteTestResult(String resultString) { + File maybeWriteTestResult(String resultString) { + File fileWritten = null; if (mResultFileName != null) { - writeTestResultIfPermitted(resultString); - }; - } - - // Run this in a background thread. - void writeTestResult(String resultString) { - File resultFile = new File(mResultFileName); - Writer writer = null; - try { - writer = new OutputStreamWriter(new FileOutputStream(resultFile)); - writer.write(resultString); - } catch ( - IOException e) { - e.printStackTrace(); - showErrorToast(" writing result file. " + e.getMessage()); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } + try { + fileWritten = mExternalFileWriter.writeStringToExternalFile(resultString, mResultFileName); + } catch (IOException e) { + e.printStackTrace(); + showErrorToast(" writing result file. " + e.getMessage()); } + mResultFileName = null; } - - mResultFileName = null; + return fileWritten; } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java new file mode 100644 index 00000000..b88b611c --- /dev/null +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java @@ -0,0 +1,210 @@ +/* + * Copyright 2023 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.TestAudioActivity.TAG; + +import android.app.Activity; +import android.content.Context; +import android.media.AudioManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.Random; + +/** + * Test for getting the cold start latency + */ +public class TestColdStartLatencyActivity extends Activity { + + private TextView mStatusView; + private MyStreamSniffer mStreamSniffer; + private AudioManager mAudioManager; + private RadioButton mOutputButton; + private RadioButton mInputButton; + private CheckBox mLowLatencyCheckBox; + private CheckBox mMmapCheckBox; + private CheckBox mExclusiveCheckBox; + private Spinner mStartStabilizeDelaySpinner; + private Spinner mCloseOpenDelaySpinner; + private Spinner mOpenStartDelaySpinner; + private Button mStartButton; + private Button mStopButton; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_cold_start_latency); + mStatusView = (TextView) findViewById(R.id.text_status); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mStartButton = (Button) findViewById(R.id.button_start_test); + mStopButton = (Button) findViewById(R.id.button_stop_test); + mOutputButton = (RadioButton) findViewById(R.id.direction_output); + mInputButton = (RadioButton) findViewById(R.id.direction_input); + mMmapCheckBox = (CheckBox) findViewById(R.id.checkbox_mmap); + mExclusiveCheckBox = (CheckBox) findViewById(R.id.checkbox_exclusive); + mLowLatencyCheckBox = (CheckBox) findViewById(R.id.checkbox_low_latency); + mStartStabilizeDelaySpinner = (Spinner) findViewById(R.id.spinner_start_stabilize_time); + mStartStabilizeDelaySpinner.setSelection(7); // Set to 1000 ms by default + mCloseOpenDelaySpinner = (Spinner) findViewById(R.id.spinner_close_open_time); + mOpenStartDelaySpinner = (Spinner) findViewById(R.id.spinner_open_start_time); + + setButtonsEnabled(false); + } + + public void onStartColdStartLatencyTest(View view) { + keepScreenOn(true); + stopSniffer(); + mStreamSniffer = new MyStreamSniffer(); + mStreamSniffer.start(); + setButtonsEnabled(true); + } + + public void onStopColdStartLatencyTest(View view) { + keepScreenOn(false); + stopSniffer(); + setButtonsEnabled(false); + } + + private void setButtonsEnabled(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mOutputButton.setEnabled(!running); + mInputButton.setEnabled(!running); + mLowLatencyCheckBox.setEnabled(!running); + mMmapCheckBox.setEnabled(!running); + mExclusiveCheckBox.setEnabled(!running); + mStartStabilizeDelaySpinner.setEnabled(!running); + mCloseOpenDelaySpinner.setEnabled(!running); + mOpenStartDelaySpinner.setEnabled(!running); + } + + protected class MyStreamSniffer extends Thread { + boolean enabled = true; + StringBuffer statusBuffer = new StringBuffer(); + int loopCount; + + @Override + public void run() { + boolean useInput = mInputButton.isChecked(); + boolean useLowLatency = mLowLatencyCheckBox.isChecked(); + boolean useMmap = mMmapCheckBox.isChecked(); + boolean useExclusive = mExclusiveCheckBox.isChecked(); + Log.d(TAG,(useInput ? "IN" : "OUT") + + ", " + (useLowLatency ? "LOW_LATENCY" : "NOT LOW_LATENCY") + + ", " + (useMmap ? "MMAP" : "NOT MMAP") + + ", " + (useExclusive ? "EXCLUSIVE" : "SHARED")); + String closeSleepTimeText = + (String) mCloseOpenDelaySpinner.getAdapter().getItem( + mCloseOpenDelaySpinner.getSelectedItemPosition()); + int closedSleepTimeMillis = Integer.parseInt(closeSleepTimeText); + Log.d(TAG, "Sleep before open time = " + closedSleepTimeMillis + " msec"); + String openSleepTimeText = (String) mOpenStartDelaySpinner.getAdapter().getItem( + mOpenStartDelaySpinner.getSelectedItemPosition()); + int openSleepTimeMillis = Integer.parseInt(openSleepTimeText); + Log.d(TAG, "Sleep after open Time = " + openSleepTimeMillis + " msec"); + String startStabilizeTimeText = (String) mStartStabilizeDelaySpinner.getAdapter().getItem( + mStartStabilizeDelaySpinner.getSelectedItemPosition()); + int startSleepTimeMillis = Integer.parseInt(startStabilizeTimeText); + Log.d(TAG, "Sleep after start Time = " + startSleepTimeMillis + " msec"); + while (enabled) { + loopCount++; + try { + sleep(closedSleepTimeMillis); + openStream(useInput, useLowLatency, useMmap, useExclusive); + log("-------#" + loopCount + " Device Id: " + getAudioDeviceId()); + log("open() Latency: " + getOpenTimeMicros() / 1000 + " msec"); + sleep(openSleepTimeMillis); + startStream(); + log("requestStart() Latency: " + getStartTimeMicros() / 1000 + " msec"); + sleep(startSleepTimeMillis); + log("Cold Start Latency: " + getColdStartTimeMicros() / 1000 + " msec"); + closeStream(); + } catch (InterruptedException e) { + enabled = false; + } finally { + closeStream(); + } + } + } + + // Log to screen and logcat. + private void log(String text) { + statusBuffer.append(text + "\n"); + showStatus(statusBuffer.toString()); + } + + // Stop the test thread. + void finish() { + enabled = false; + interrupt(); + try { + join(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void showStatus(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mStatusView.setText(message); + } + }); + } + + private native int openStream(boolean useInput, boolean useLowLatency, boolean useMmap, + boolean useExclusive); + private native int startStream(); + private native int closeStream(); + private native int getOpenTimeMicros(); + private native int getStartTimeMicros(); + private native int getColdStartTimeMicros(); + private native int getAudioDeviceId(); + + @Override + public void onPause() { + super.onPause(); + stopSniffer(); + } + + private void stopSniffer() { + if (mStreamSniffer != null) { + mStreamSniffer.finish(); + mStreamSniffer = null; + } + } + + protected void keepScreenOn(boolean on) { + if (on) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } +} diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java index b0aea932..74879f14 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java @@ -17,49 +17,74 @@ package com.mobileer.oboetester; import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle; +import static com.mobileer.oboetester.StreamConfiguration.convertChannelMaskToText; -import android.app.Activity; -import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioManager; +import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; import android.util.Log; import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; import com.mobileer.audio_device.AudioDeviceInfoConverter; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Locale; /** - * Play a recognizable tone on each channel of each speaker device - * and listen for the result through a microphone. - * Also test each microphone channel and device. - * Try each InputPreset. + * Play a recognizable tone on each channel of an output device + * and listen for the result through an input. + * Test various channels, InputPresets, ChannelMasks and SampleRates. + * + * Select device types based on priority of attached peripherals. + * Print devices types being tested. * * The analysis is based on a cosine transform of a single * frequency. The magnitude indicates the level. * The variations in phase, "jitter" indicate how noisy the - * signal is or whether it is corrupted. A noisy room may have - * energy at the target frequency but the phase will be random. + * signal is or whether it is corrupted. A very noisy room may have + * lots of energy at the target frequency but the phase will be random. * - * This test requires a quiet room but no other hardware. + * This test requires a quiet room if you are testing speaker/mic pairs. + * It can also be used to test using analog loopback adapters + * or USB devices configured in loopback mode. */ public class TestDataPathsActivity extends BaseAutoGlitchActivity { public static final String KEY_USE_INPUT_PRESETS = "use_input_presets"; public static final boolean VALUE_DEFAULT_USE_INPUT_PRESETS = true; - public static final String KEY_USE_INPUT_DEVICES = "use_input_devices"; - public static final boolean VALUE_DEFAULT_USE_INPUT_DEVICES = true; + public static final String KEY_USE_ALL_SAMPLE_RATES = "use_all_sample_rates"; + public static final boolean VALUE_DEFAULT_USE_ALL_SAMPLE_RATES = false; + + public static final String KEY_SINGLE_TEST_INDEX = "single_test_index"; + public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1; + public static final String KEY_USE_ALL_CHANNEL_COUNTS = "use_all_channel_counts"; + public static final boolean VALUE_DEFAULT_USE_ALL_CHANNEL_COUNTS = true; + public static final String KEY_USE_INPUT_CHANNEL_MASKS = "use_input_channel_masks"; + public static final boolean VALUE_DEFAULT_USE_INPUT_CHANNEL_MASKS = false; + public static final String KEY_OUTPUT_CHANNEL_MASKS_LEVEL = "output_channel_masks_level"; + + // The following KEYs are for old deprecated commands. + public static final String KEY_USE_ALL_OUTPUT_CHANNEL_MASKS = "use_all_output_channel_masks"; + public static final boolean VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS = false; + + // How many tests should be run in a specific category, eg. channel masks? + private static final int COVERAGE_LEVEL_NONE = 0; + private static final int COVERAGE_LEVEL_SOME = 1; + private static final int COVERAGE_LEVEL_ALL = 2; + public static final String KEY_USE_INPUT_DEVICES = "use_input_devices"; + public static final boolean VALUE_DEFAULT_USE_INPUT_DEVICES = false; public static final String KEY_USE_OUTPUT_DEVICES = "use_output_devices"; public static final boolean VALUE_DEFAULT_USE_OUTPUT_DEVICES = true; - public static final String KEY_SINGLE_TEST_INDEX = "single_test_index"; - public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1; public static final int DURATION_SECONDS = 3; private final static double MIN_REQUIRED_MAGNITUDE = 0.001; @@ -70,6 +95,42 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { private final static double MAX_ALLOWED_JITTER = 2.0 * PHASE_PER_BIN; private final static String MAGNITUDE_FORMAT = "%7.5f"; + // These define the values returned by the Java API deviceInfo.getChannelMasks(). + public static final int JAVA_CHANNEL_IN_LEFT = 1 << 2; // AudioFormat.CHANNEL_IN_LEFT + public static final int JAVA_CHANNEL_IN_RIGHT = 1 << 3; // AudioFormat.CHANNEL_IN_RIGHT + public static final int JAVA_CHANNEL_IN_FRONT = 1 << 4; // AudioFormat.CHANNEL_IN_FRONT + public static final int JAVA_CHANNEL_IN_BACK = 1 << 5; // AudioFormat.CHANNEL_IN_BACK + + // These do not have corresponding Java definitions. + // They match definitions in system/media/audio/include/system/audio-hal-enums.h + public static final int JAVA_CHANNEL_IN_BACK_LEFT = 1 << 16; + public static final int JAVA_CHANNEL_IN_BACK_RIGHT = 1 << 17; + public static final int JAVA_CHANNEL_IN_CENTER = 1 << 18; + public static final int JAVA_CHANNEL_IN_LOW_FREQUENCY = 1 << 20; + public static final int JAVA_CHANNEL_IN_TOP_LEFT = 1 << 21; + public static final int JAVA_CHANNEL_IN_TOP_RIGHT = 1 << 22; + + public static final int JAVA_CHANNEL_IN_MONO = JAVA_CHANNEL_IN_FRONT; + public static final int JAVA_CHANNEL_IN_STEREO = JAVA_CHANNEL_IN_LEFT | JAVA_CHANNEL_IN_RIGHT; + public static final int JAVA_CHANNEL_IN_FRONT_BACK = JAVA_CHANNEL_IN_FRONT | JAVA_CHANNEL_IN_BACK; + public static final int JAVA_CHANNEL_IN_2POINT0POINT2 = JAVA_CHANNEL_IN_LEFT | + JAVA_CHANNEL_IN_RIGHT | + JAVA_CHANNEL_IN_TOP_LEFT | + JAVA_CHANNEL_IN_TOP_RIGHT; + public static final int JAVA_CHANNEL_IN_2POINT1POINT2 = + JAVA_CHANNEL_IN_2POINT0POINT2 | JAVA_CHANNEL_IN_LOW_FREQUENCY; + public static final int JAVA_CHANNEL_IN_3POINT0POINT2 = + JAVA_CHANNEL_IN_2POINT0POINT2 | JAVA_CHANNEL_IN_CENTER; + public static final int JAVA_CHANNEL_IN_3POINT1POINT2 = + JAVA_CHANNEL_IN_3POINT0POINT2 | JAVA_CHANNEL_IN_LOW_FREQUENCY; + public static final int JAVA_CHANNEL_IN_5POINT1 = JAVA_CHANNEL_IN_LEFT | + JAVA_CHANNEL_IN_CENTER | + JAVA_CHANNEL_IN_RIGHT | + JAVA_CHANNEL_IN_BACK_LEFT | + JAVA_CHANNEL_IN_BACK_RIGHT | + JAVA_CHANNEL_IN_LOW_FREQUENCY; + public static final int JAVA_CHANNEL_UNDEFINED = -1; + final int TYPE_BUILTIN_SPEAKER_SAFE = 0x18; // API 30 private double mMagnitude; @@ -79,22 +140,73 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { private double mPhaseErrorSum; private double mPhaseErrorCount; - AudioManager mAudioManager; private CheckBox mCheckBoxInputPresets; - private CheckBox mCheckBoxInputDevices; - private CheckBox mCheckBoxOutputDevices; + private CheckBox mCheckBoxAllChannels; + private CheckBox mCheckBoxInputChannelMasks; + private RadioGroup mRadioGroupOutputChannelMasks; + private RadioButton mRadioOutputChannelMasksNone; + private RadioButton mRadioOutputChannelMasksSome; + private RadioButton mRadioOutputChannelMasksAll; + private CheckBox mCheckBoxAllSampleRates; + private TextView mInstructionsTextView; private static final int[] INPUT_PRESETS = { - // VOICE_RECOGNITION gets tested in testInputs() - // StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION, StreamConfiguration.INPUT_PRESET_GENERIC, StreamConfiguration.INPUT_PRESET_CAMCORDER, - // TODO Resolve issue with echo cancellation killing the signal. - StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION, StreamConfiguration.INPUT_PRESET_UNPROCESSED, + // Do not use INPUT_PRESET_VOICE_COMMUNICATION because AEC kills the signal. + StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION, StreamConfiguration.INPUT_PRESET_VOICE_PERFORMANCE, }; + private static final int[] SHORT_OUTPUT_CHANNEL_MASKS = { + StreamConfiguration.CHANNEL_MONO, + StreamConfiguration.CHANNEL_STEREO, + StreamConfiguration.CHANNEL_2POINT1, // Smallest mask with more than two channels. + StreamConfiguration.CHANNEL_5POINT1, // This mask is very common. + StreamConfiguration.CHANNEL_7POINT1POINT4, // More than 8 channels might break. + }; + + private static final int[] ALL_OUTPUT_CHANNEL_MASKS = { + StreamConfiguration.CHANNEL_MONO, + StreamConfiguration.CHANNEL_STEREO, + StreamConfiguration.CHANNEL_2POINT1, + StreamConfiguration.CHANNEL_TRI, + StreamConfiguration.CHANNEL_TRI_BACK, + StreamConfiguration.CHANNEL_3POINT1, + StreamConfiguration.CHANNEL_2POINT0POINT2, + StreamConfiguration.CHANNEL_2POINT1POINT2, + StreamConfiguration.CHANNEL_3POINT0POINT2, + StreamConfiguration.CHANNEL_3POINT1POINT2, + StreamConfiguration.CHANNEL_QUAD, + StreamConfiguration.CHANNEL_QUAD_SIDE, + StreamConfiguration.CHANNEL_SURROUND, + StreamConfiguration.CHANNEL_PENTA, + StreamConfiguration.CHANNEL_5POINT1, + StreamConfiguration.CHANNEL_5POINT1_SIDE, + StreamConfiguration.CHANNEL_6POINT1, + StreamConfiguration.CHANNEL_7POINT1, + StreamConfiguration.CHANNEL_5POINT1POINT2, + StreamConfiguration.CHANNEL_5POINT1POINT4, + StreamConfiguration.CHANNEL_7POINT1POINT2, + StreamConfiguration.CHANNEL_7POINT1POINT4, + }; + + private static final int[] SAMPLE_RATES = { + 8000, + 11025, + 12000, + 16000, + 22050, + 24000, + 32000, + 44100, + 48000, + 64000, + 88200, + 96000, + }; + @NonNull public static String comparePassedField(String prefix, Object failed, Object passed, String name) { try { @@ -109,16 +221,25 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { return "ERROR - cannot access " + name; } } + public static String comparePassedInputPreset(String prefix, TestResult failed, TestResult passed) { + int failedValue = failed.inputPreset; + int passedValue = passed.inputPreset; + return (failedValue == passedValue) ? "" + : (prefix + " inPreset: passed = " + + StreamConfiguration.convertInputPresetToText(passedValue) + + ", failed = " + + StreamConfiguration.convertInputPresetToText(failedValue) + + "\n"); + } public static double calculatePhaseError(double p1, double p2) { - double diff = Math.abs(p1 - p2); + double diff = p1 - p2; // Wrap around the circle. - while (diff > (2 * Math.PI)) { - diff -= (2 * Math.PI); + while (diff > Math.PI) { + diff -= 2 * Math.PI; } - // A phase error close to 2*PI is actually a small phase error. - if (diff > Math.PI) { - diff = (2 * Math.PI) - diff; + while (diff < -Math.PI) { + diff += 2 * Math.PI; } return diff; } @@ -126,10 +247,6 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { // Periodically query for magnitude and phase from the native detector. protected class DataPathSniffer extends NativeSniffer { - public DataPathSniffer(Activity activity) { - super(activity); - } - @Override public void startSniffer() { mMagnitude = -1.0; @@ -141,30 +258,28 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { super.startSniffer(); } - @Override - public void run() { + private void gatherData() { mMagnitude = getMagnitude(); mMaxMagnitude = getMaxMagnitude(); - Log.d(TAG, String.format("magnitude = %7.4f, maxMagnitude = %7.4f", + Log.d(TAG, String.format(Locale.getDefault(), "magnitude = %7.4f, maxMagnitude = %7.4f", mMagnitude, mMaxMagnitude)); // Only look at the phase if we have a signal. if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) { - double phase = getPhase(); + double phase = getPhaseDataPaths(); // Wait for the analyzer to get a lock on the signal. // Arbitrary number of phase measurements before we start measuring jitter. final int kMinPhaseMeasurementsRequired = 4; if (mPhaseCount >= kMinPhaseMeasurementsRequired) { - double phaseError = calculatePhaseError(phase, mPhase); - // low pass filter + double phaseError = Math.abs(calculatePhaseError(phase, mPhase)); + // collect average error mPhaseErrorSum += phaseError; mPhaseErrorCount++; - Log.d(TAG, String.format("phase = %7.4f, diff = %7.4f, jitter = %7.4f", - phase, phaseError, getAveragePhaseError())); + Log.d(TAG, String.format(Locale.getDefault(), "phase = %7.4f, mPhase = %7.4f, phaseError = %7.4f, jitter = %7.4f", + phase, mPhase, phaseError, getAveragePhaseError())); } mPhase = phase; mPhaseCount++; } - reschedule(); } public String getCurrentStatusReport() { @@ -179,7 +294,6 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { return message.toString(); } - @Override public String getShortReport() { return "maxMag = " + getMagnitudeText(mMaxMagnitude) + ", jitter = " + getJitterText(); @@ -187,6 +301,7 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { @Override public void updateStatusText() { + gatherData(); mLastGlitchReport = getCurrentStatusReport(); runOnUiThread(() -> { setAnalyzerText(mLastGlitchReport); @@ -194,18 +309,34 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { } } + // Write to status and command view + private void setInstructionsText(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mInstructionsTextView.setText(text); + } + }); + } + private String getJitterText() { return isPhaseJitterValid() ? getMagnitudeText(getAveragePhaseError()) : "?"; } @Override NativeSniffer createNativeSniffer() { - return new TestDataPathsActivity.DataPathSniffer(this); + return new TestDataPathsActivity.DataPathSniffer(); + } + + @Override + public String getShortReport() { + return ((DataPathSniffer) mNativeSniffer).getShortReport(); } native double getMagnitude(); native double getMaxMagnitude(); - native double getPhase(); + + native double getPhaseDataPaths(); @Override protected void inflateActivity() { @@ -215,10 +346,18 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mCheckBoxInputPresets = (CheckBox)findViewById(R.id.checkbox_paths_input_presets); - mCheckBoxInputDevices = (CheckBox)findViewById(R.id.checkbox_paths_input_devices); - mCheckBoxOutputDevices = (CheckBox)findViewById(R.id.checkbox_paths_output_devices); + mCheckBoxAllChannels = (CheckBox)findViewById(R.id.checkbox_paths_all_channels); + mCheckBoxInputChannelMasks = (CheckBox)findViewById(R.id.checkbox_paths_in_channel_masks); + mCheckBoxAllSampleRates = + (CheckBox)findViewById(R.id.checkbox_paths_all_sample_rates); + + mInstructionsTextView = (TextView) findViewById(R.id.text_instructions); + + mRadioGroupOutputChannelMasks = (RadioGroup) findViewById(R.id.group_ch_mask_options); + mRadioOutputChannelMasksNone = (RadioButton) findViewById(R.id.radio_out_ch_masks_none); + mRadioOutputChannelMasksSome = (RadioButton) findViewById(R.id.radio_out_ch_masks_some); + mRadioOutputChannelMasksAll = (RadioButton) findViewById(R.id.radio_out_ch_masks_all); } @Override @@ -232,7 +371,7 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { } static String getMagnitudeText(double value) { - return String.format(MAGNITUDE_FORMAT, value); + return String.format(Locale.getDefault(), MAGNITUDE_FORMAT, value); } protected String getConfigText(StreamConfiguration config) { @@ -244,35 +383,27 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { } @Override - protected String shouldTestBeSkipped() { - String why = ""; + protected String whyShouldTestBeSkipped() { + String why = super.whyShouldTestBeSkipped(); 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 the data path we requested. - if (actualInConfig.isMMap() != requestedInConfig.isMMap()) { - log("Did not get requested MMap input stream"); - why += "mmap"; - } - if (actualOutConfig.isMMap() != requestedOutConfig.isMMap()) { - log("Did not get requested MMap output stream"); - why += "mmap"; - } + // Did we request a device and not get that device? if (requestedInConfig.getDeviceId() != 0 && (requestedInConfig.getDeviceId() != actualInConfig.getDeviceId())) { - why += ", inDev(" + requestedInConfig.getDeviceId() - + "!=" + actualInConfig.getDeviceId() + ")"; + why += "inDev(" + requestedInConfig.getDeviceId() + + "!=" + actualInConfig.getDeviceId() + "),"; } if (requestedOutConfig.getDeviceId() != 0 && (requestedOutConfig.getDeviceId() != actualOutConfig.getDeviceId())) { why += ", outDev(" + requestedOutConfig.getDeviceId() - + "!=" + actualOutConfig.getDeviceId() + ")"; + + "!=" + actualOutConfig.getDeviceId() + "),"; } if ((requestedInConfig.getInputPreset() != actualInConfig.getInputPreset())) { why += ", inPre(" + requestedInConfig.getInputPreset() - + "!=" + actualInConfig.getInputPreset() + ")"; + + "!=" + actualInConfig.getInputPreset() + "),"; } return why; } @@ -288,10 +419,6 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { @Override public String didTestFail() { String why = ""; - StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; - StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; - StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration; - StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration; if (mMaxMagnitude <= MIN_REQUIRED_MAGNITUDE) { why += ", mag"; } @@ -322,280 +449,387 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { + ", IN" + (actualInConfig.isMMap() ? "-M" : "-L") + " D=" + actualInConfig.getDeviceId() + ", ch=" + channelText(getInputChannel(), actualInConfig.getChannelCount()) + + ", SR=" + actualInConfig.getSampleRate() + ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L") + " D=" + actualOutConfig.getDeviceId() + ", ch=" + channelText(getOutputChannel(), actualOutConfig.getChannelCount()) + + ", SR=" + actualOutConfig.getSampleRate() + ", mag = " + getMagnitudeText(mMaxMagnitude); } - void setupDeviceCombo(int numInputChannels, - int inputChannel, - int numOutputChannels, - int outputChannel) throws InterruptedException { - // Configure settings - StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; - StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; - - requestedInConfig.reset(); - requestedOutConfig.reset(); - - requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); - requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); - - requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); - requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); - - requestedInConfig.setChannelCount(numInputChannels); - requestedOutConfig.setChannelCount(numOutputChannels); - - requestedInConfig.setMMap(false); - requestedOutConfig.setMMap(false); - - setInputChannel(inputChannel); - setOutputChannel(outputChannel); - } - - private TestResult testConfigurationsAddMagJitter() throws InterruptedException { - TestResult testResult = testConfigurations(); + @Override + protected TestResult testCurrentConfigurations() throws InterruptedException { + TestResult testResult = super.testCurrentConfigurations(); if (testResult != null) { testResult.addComment("mag = " + TestDataPathsActivity.getMagnitudeText(mMagnitude) + ", jitter = " + getJitterText()); - } - return testResult; - } - - void testPresetCombo(int inputPreset, - int numInputChannels, - int inputChannel, - int numOutputChannels, - int outputChannel, - boolean mmapEnabled - ) throws InterruptedException { - setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, outputChannel); - StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; - requestedInConfig.setInputPreset(inputPreset); - requestedInConfig.setMMap(mmapEnabled); - - mMagnitude = -1.0; - TestResult testResult = testConfigurationsAddMagJitter(); - if (testResult != null) { + logOneLineSummary(testResult); int result = testResult.result; - String summary = getOneLineSummary() - + ", inPre = " - + StreamConfiguration.convertInputPresetToText(inputPreset) - + "\n"; - appendSummary(summary); if (result == TEST_RESULT_FAILED) { - if (getMagnitude() < 0.000001) { - testResult.addComment("The input is completely SILENT!"); - } else if (inputPreset == StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION) { - testResult.addComment("Maybe sine wave blocked by Echo Cancellation!"); + int id = mAudioOutTester.actualConfiguration.getDeviceId(); + int deviceType = getDeviceInfoById(id).getType(); + int channelCount = mAudioOutTester.actualConfiguration.getChannelCount(); + if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + && channelCount == 2 + && getOutputChannel() == 1) { + testResult.addComment("Maybe EARPIECE does not mix stereo to mono!"); + } + if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE + && channelCount == 2 + && getOutputChannel() == 0) { + testResult.addComment("Maybe SPEAKER_SAFE dropped channel zero!"); } } } + return testResult; } - void testPresetCombo(int inputPreset, - int numInputChannels, - int inputChannel, - int numOutputChannels, - int outputChannel - ) throws InterruptedException { - if (NativeEngine.isMMapSupported()) { - testPresetCombo(inputPreset, numInputChannels, inputChannel, - numOutputChannels, outputChannel, true); - } - testPresetCombo(inputPreset, numInputChannels, inputChannel, - numOutputChannels, outputChannel, false); - } - - void testPresetCombo(int inputPreset) throws InterruptedException { - setTestName("Test InPreset = " + StreamConfiguration.convertInputPresetToText(inputPreset)); - testPresetCombo(inputPreset, 1, 0, 1, 0); + private void logSection(String name) { + logBoth("\n#" + (getTestCount() + 1) + " ########### " + name + "\n"); } private void testInputPresets() throws InterruptedException { - logBoth("\nTest InputPreset -------"); - + logSection("InputPreset"); + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + int originalPreset = requestedInConfig.getInputPreset(); for (int inputPreset : INPUT_PRESETS) { - testPresetCombo(inputPreset); + requestedInConfig.setInputPreset(inputPreset); + testPerformancePaths(); + } + requestedInConfig.setInputPreset(originalPreset); + } + + // The native out channel mask is its channel mask shifted right by 2 bits. + // See AudioFormat.convertChannelOutMaskToNativeMask() + int convertJavaOutChannelMaskToNativeChannelMask(int javaChannelMask) { + return javaChannelMask >> 2; + } + + // The native channel mask in AAudio and Oboe is different than the Java IN channel mask. + // See AAudioConvert_aaudioToAndroidChannelLayoutMask() + int convertJavaInChannelMaskToNativeChannelMask(int javaChannelMask) { + switch (javaChannelMask) { + case JAVA_CHANNEL_IN_MONO: + return StreamConfiguration.CHANNEL_MONO; + case JAVA_CHANNEL_IN_STEREO: + return StreamConfiguration.CHANNEL_STEREO; + case JAVA_CHANNEL_IN_FRONT_BACK: + return StreamConfiguration.CHANNEL_FRONT_BACK; + case JAVA_CHANNEL_IN_2POINT0POINT2: + return StreamConfiguration.CHANNEL_2POINT0POINT2; + case JAVA_CHANNEL_IN_2POINT1POINT2: + return StreamConfiguration.CHANNEL_2POINT1POINT2; + case JAVA_CHANNEL_IN_3POINT0POINT2: + return StreamConfiguration.CHANNEL_3POINT0POINT2; + case JAVA_CHANNEL_IN_3POINT1POINT2: + return StreamConfiguration.CHANNEL_3POINT1POINT2; + case JAVA_CHANNEL_IN_5POINT1: + return StreamConfiguration.CHANNEL_5POINT1; + default: + log("Unimplemented java channel mask: " + javaChannelMask + "\n"); + return JAVA_CHANNEL_UNDEFINED; } -// TODO Resolve issue with echo cancellation killing the signal. -// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION, -// 1, 0, 2, 0); -// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION, -// 1, 0, 2, 1); -// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION, -// 2, 0, 2, 0); -// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION, -// 2, 0, 2, 1); - } - - void testInputDeviceCombo(int deviceId, - int numInputChannels, - int inputChannel, - boolean mmapEnabled) throws InterruptedException { - final int numOutputChannels = 2; - setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, 0); + } - StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; - requestedInConfig.setInputPreset(StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION); - requestedInConfig.setDeviceId(deviceId); - requestedInConfig.setMMap(mmapEnabled); + void logOneLineSummary(TestResult testResult) { + logOneLineSummary(testResult, ""); + } - mMagnitude = -1.0; - TestResult testResult = testConfigurationsAddMagJitter(); - if (testResult != null) { - appendSummary(getOneLineSummary() + "\n"); + void logOneLineSummary(TestResult testResult, String extra) { + int result = testResult.result; + String oneLineSummary; + if (result == TEST_RESULT_SKIPPED) { + oneLineSummary = "#" + mAutomatedTestRunner.getTestCount() + extra + ", SKIP"; + } else if (result == TEST_RESULT_FAILED) { + oneLineSummary = getOneLineSummary() + extra + ", FAIL"; + } else { + oneLineSummary = getOneLineSummary() + extra; } + appendSummary(oneLineSummary + "\n"); } - void testInputDeviceCombo(int deviceId, - int deviceType, - int numInputChannels, - int inputChannel) throws InterruptedException { + void logBoth(String text) { + log(text); + appendSummary(text + "\n"); + } - String typeString = AudioDeviceInfoConverter.typeToString(deviceType); - setTestName("Test InDev: #" + deviceId + " " + typeString - + "_" + inputChannel + "/" + numInputChannels); - if (NativeEngine.isMMapSupported()) { - testInputDeviceCombo(deviceId, numInputChannels, inputChannel, true); - } - testInputDeviceCombo(deviceId, numInputChannels, inputChannel, false); - } - - void testInputDevices() throws InterruptedException { - logBoth("\nTest Input Devices -------"); - - AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); - int numTested = 0; - for (AudioDeviceInfo deviceInfo : devices) { - log("----\n" - + AudioDeviceInfoConverter.toString(deviceInfo) + "\n"); - if (!deviceInfo.isSource()) continue; // FIXME log as error?! - int deviceType = deviceInfo.getType(); - if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_MIC) { - int id = deviceInfo.getId(); - int[] channelCounts = deviceInfo.getChannelCounts(); - numTested++; - // Always test mono and stereo. - testInputDeviceCombo(id, deviceType, 1, 0); - testInputDeviceCombo(id, deviceType, 2, 0); - testInputDeviceCombo(id, deviceType, 2, 1); - if (channelCounts.length > 0) { - for (int numChannels : channelCounts) { - // Test higher channel counts. - if (numChannels > 2) { - log("numChannels = " + numChannels + "\n"); - for (int channel = 0; channel < numChannels; channel++) { - testInputDeviceCombo(id, deviceType, numChannels, channel); - } - } - } - } - } else { - log("Device skipped for type."); - } + void logFailed(String text) { + log(text); + logAnalysis(text + "\n"); + } + + private void testDeviceOutputInfo(AudioDeviceInfo outputDeviceInfo) throws InterruptedException { + AudioDeviceInfo inputDeviceInfo = findCompatibleInputDevice(outputDeviceInfo.getType()); + showDeviceInfo(outputDeviceInfo, inputDeviceInfo); + if (inputDeviceInfo == null) { + return; + } + + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + requestedInConfig.reset(); + requestedOutConfig.reset(); + requestedInConfig.setDeviceId(inputDeviceInfo.getId()); + requestedOutConfig.setDeviceId(outputDeviceInfo.getId()); + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + + if (mCheckBoxAllChannels.isChecked()) { + runOnUiThread(() -> mCheckBoxAllChannels.setEnabled(false)); + testOutputChannelCounts(inputDeviceInfo, outputDeviceInfo); + } + + if (mCheckBoxInputPresets.isChecked()) { + runOnUiThread(() -> mCheckBoxInputPresets.setEnabled(false)); + testInputPresets(); + } + + if (mCheckBoxAllSampleRates.isChecked()) { + logSection("Sample Rates"); + runOnUiThread(() -> mCheckBoxAllSampleRates.setEnabled(false)); + for (int sampleRate : SAMPLE_RATES) { + requestedInConfig.setSampleRate(sampleRate); + requestedOutConfig.setSampleRate(sampleRate); + testPerformancePaths(); + } + } + requestedInConfig.setSampleRate(0); + requestedOutConfig.setSampleRate(0); + + // Channel Masks added to AAudio API in S_V2 + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 + && isDeviceTypeMixedForLoopback(outputDeviceInfo.getType())) { + + if (mCheckBoxInputChannelMasks.isChecked()) { // INPUT? + logSection("Input Channel Masks"); + runOnUiThread(() -> mCheckBoxInputChannelMasks.setEnabled(false)); + + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + requestedInConfig.setChannelCount(0); + // Test the reported channel masks. + int[] channelMasks = inputDeviceInfo.getChannelMasks(); + if (channelMasks.length > 0) { + for (int channelMask : channelMasks) { + int nativeChannelMask = + convertJavaInChannelMaskToNativeChannelMask(channelMask); + if (nativeChannelMask == JAVA_CHANNEL_UNDEFINED) { + log("channelMask: " + channelMask + " not supported. Skipping.\n"); + continue; + } + log("\n#### nativeChannelMask = " + + convertChannelMaskToText(nativeChannelMask) + "\n"); + int channelCount = Integer.bitCount(nativeChannelMask); + requestedInConfig.setChannelMask(nativeChannelMask); + for (int channel = 0; channel < channelCount; channel++) { + setInputChannel(channel); + testPerformancePaths(); + } + } + } + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + } + + runOnUiThread(() -> mRadioGroupOutputChannelMasks.setEnabled(false)); + if (!mRadioOutputChannelMasksNone.isChecked()) { // OUTPUT? + logSection("Output Channel Masks"); + requestedInConfig.setChannelCount(1); + for (int channelMask : mRadioOutputChannelMasksAll.isChecked() ? + ALL_OUTPUT_CHANNEL_MASKS : SHORT_OUTPUT_CHANNEL_MASKS) { + int channelCount = Integer.bitCount(channelMask); + requestedOutConfig.setChannelMask(channelMask); + for (int channel = 0; channel < channelCount; channel++) { + setOutputChannel(channel); + testPerformancePaths(); + } + } + resetChannelConfigurations(requestedInConfig, requestedOutConfig); + } + } + } + + private void resetChannelConfigurations(StreamConfiguration requestedInConfig, StreamConfiguration requestedOutConfig) { + requestedInConfig.setChannelMask(0); + requestedOutConfig.setChannelMask(0); + requestedInConfig.setChannelCount(1); + requestedOutConfig.setChannelCount(1); + setInputChannel(0); + setOutputChannel(0); + } + + private void showDeviceInfo(AudioDeviceInfo outputDeviceInfo, AudioDeviceInfo inputDeviceInfo) { + String deviceText = "OUT: type = " + + AudioDeviceInfoConverter.typeToString(outputDeviceInfo.getType()) + + ", #ch = " + findLargestChannelCount(outputDeviceInfo.getChannelCounts()); + + setInstructionsText(deviceText); + + if (inputDeviceInfo == null) { + deviceText += "ERROR - cannot find compatible device type for input!"; + } else { + deviceText = "IN: type = " + + AudioDeviceInfoConverter.typeToString(inputDeviceInfo.getType()) + + ", #ch = " + findLargestChannelCount(inputDeviceInfo.getChannelCounts()) + + "\n" + deviceText; } + setInstructionsText(deviceText); + } - if (numTested == 0) { - log("NO INPUT DEVICE FOUND!\n"); + public static int findLargestChannelCount(int[] arr) { + if (arr == null || arr.length == 0) { + return 2; } + return findLargestInt(arr); } - void testOutputDeviceCombo(int deviceId, - int deviceType, - int numOutputChannels, - int outputChannel, - boolean mmapEnabled) throws InterruptedException { - final int numInputChannels = 2; // TODO review, done because of mono problems on some devices - setupDeviceCombo(numInputChannels, 0, numOutputChannels, outputChannel); + public static int findLargestInt(int[] arr) { + if (arr == null || arr.length == 0) { + throw new IllegalArgumentException("Array cannot be empty"); + } + + int max = arr[0]; + for (int i = 1; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + } + } + return max; + } + private void testOutputChannelCounts(AudioDeviceInfo inputDeviceInfo, AudioDeviceInfo outputDeviceInfo) throws InterruptedException { + logSection("Output Channel Counts"); + ArrayList<Integer> channelCountsTested =new ArrayList<Integer>(); + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; - requestedOutConfig.setDeviceId(deviceId); - requestedOutConfig.setMMap(mmapEnabled); - mMagnitude = -1.0; - TestResult testResult = testConfigurationsAddMagJitter(); - if (testResult != null) { - int result = testResult.result; - appendSummary(getOneLineSummary() + "\n"); - if (result == TEST_RESULT_FAILED) { - if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE - && numOutputChannels == 2 - && outputChannel == 1) { - testResult.addComment("Maybe EARPIECE does not mix stereo to mono!"); + int[] outputChannelCounts = outputDeviceInfo.getChannelCounts(); + if (isDeviceTypeMixedForLoopback(outputDeviceInfo.getType())) { + requestedInConfig.setChannelCount(1); + setInputChannel(0); + // test mono + requestedOutConfig.setChannelCount(1); + channelCountsTested.add(1); + setOutputChannel(0); + testPerformancePaths(); + // test stereo + requestedOutConfig.setChannelCount(2); + channelCountsTested.add(2); + setOutputChannel(0); + testPerformancePaths(); + setOutputChannel(1); + testPerformancePaths(); + // Test channels for each channelCount above 2 + for (int numChannels : outputChannelCounts) { + log("numChannels = " + numChannels); + if (numChannels > 4) { + log("numChannels forced to 4!"); } - if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE - && numOutputChannels == 2 - && outputChannel == 0) { - testResult.addComment("Maybe SPEAKER_SAFE dropped channel zero!"); + if (!channelCountsTested.contains(numChannels)) { + requestedOutConfig.setChannelCount(numChannels); + channelCountsTested.add(numChannels); + for (int channel = 0; channel < numChannels; channel++) { + setOutputChannel(channel); + testPerformancePaths(); + } + } + } + } else { + // test mono + testMatchingChannels(1); + channelCountsTested.add(1); + // Test two matching stereo channels. + testMatchingChannels(2); + channelCountsTested.add(2); + // Test matching channels for each channelCount above 2 + for (int numChannels : outputChannelCounts) { + log("numChannels = " + numChannels); + if (numChannels > 4) { + log("numChannels forced to 4!"); + numChannels = 4; + } + if (!channelCountsTested.contains(numChannels)) { + testMatchingChannels(numChannels); + channelCountsTested.add(numChannels); } } } + // Restore defaults. + requestedInConfig.setChannelCount(1); + setInputChannel(0); + requestedOutConfig.setChannelCount(1); + setOutputChannel(0); + } + + private void testMatchingChannels(int numChannels) throws InterruptedException { + mAudioInputTester.requestedConfiguration.setChannelCount(numChannels); + mAudioOutTester.requestedConfiguration.setChannelCount(numChannels); + for (int channel = 0; channel < numChannels; channel++) { + setInputChannel(channel); + setOutputChannel(channel); + testPerformancePaths(); + } } - void testOutputDeviceCombo(int deviceId, - int deviceType, - int numOutputChannels, - int outputChannel) throws InterruptedException { - String typeString = AudioDeviceInfoConverter.typeToString(deviceType); - setTestName("Test OutDev: #" + deviceId + " " + typeString - + "_" + outputChannel + "/" + numOutputChannels); + private void testPerformancePaths() throws InterruptedException { + StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; + StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; + + requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); + requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); + + // Legacy NONE + requestedInConfig.setMMap(false); + requestedOutConfig.setMMap(false); + requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE); + requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE); + testCurrentConfigurations(); + + // Legacy LOW_LATENCY + requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); + requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY); + testCurrentConfigurations(); + + // MMAP LowLatency if (NativeEngine.isMMapSupported()) { - testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, true); + requestedInConfig.setMMap(true); + requestedOutConfig.setMMap(true); + testCurrentConfigurations(); } - testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, false); - } - - void logBoth(String text) { - log(text); - appendSummary(text + "\n"); - } + requestedInConfig.setMMap(false); + requestedOutConfig.setMMap(false); - void logFailed(String text) { - log(text); - logAnalysis(text + "\n"); } - void testOutputDevices() throws InterruptedException { - logBoth("\nTest Output Devices -------"); - - AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); - int numTested = 0; - for (AudioDeviceInfo deviceInfo : devices) { - log("----\n" - + AudioDeviceInfoConverter.toString(deviceInfo) + "\n"); - if (!deviceInfo.isSink()) continue; - int deviceType = deviceInfo.getType(); - if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER - || deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE - || deviceType == TYPE_BUILTIN_SPEAKER_SAFE) { - int id = deviceInfo.getId(); - int[] channelCounts = deviceInfo.getChannelCounts(); - numTested++; - // Always test mono and stereo. - testOutputDeviceCombo(id, deviceType, 1, 0); - testOutputDeviceCombo(id, deviceType, 2, 0); - testOutputDeviceCombo(id, deviceType, 2, 1); - if (channelCounts.length > 0) { - for (int numChannels : channelCounts) { - // Test higher channel counts. - if (numChannels > 2) { - log("numChannels = " + numChannels + "\n"); - for (int channel = 0; channel < numChannels; channel++) { - testOutputDeviceCombo(id, deviceType, numChannels, channel); - } - } - } - } - } else { - log("Device skipped for type."); - } + private void testOutputDeviceTypes() throws InterruptedException { + // Determine which output device type to test based on priorities. + AudioDeviceInfo info = getDeviceInfoByType(AudioDeviceInfo.TYPE_USB_DEVICE, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + return; + } + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_USB_HEADSET, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + return; } - if (numTested == 0) { - log("NO OUTPUT DEVICE FOUND!\n"); + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + return; + } + // Test both SPEAKER and SPEAKER_SAFE + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); + // Continue on to SPEAKER_SAFE + } + info = getDeviceInfoByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE, + AudioManager.GET_DEVICES_OUTPUTS); + if (info != null) { + testDeviceOutputInfo(info); } } @@ -610,31 +844,25 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { mTestResults.clear(); mDurationSeconds = DURATION_SECONDS; - if (mCheckBoxInputPresets.isChecked()) { - runOnUiThread(() -> mCheckBoxInputPresets.setEnabled(false)); - testInputPresets(); - } - if (mCheckBoxInputDevices.isChecked()) { - runOnUiThread(() -> mCheckBoxInputDevices.setEnabled(false)); - testInputDevices(); - } - if (mCheckBoxOutputDevices.isChecked()) { - runOnUiThread(() -> mCheckBoxOutputDevices.setEnabled(false)); - testOutputDevices(); - } + runOnUiThread(() -> keepScreenOn(true)); - analyzeTestResults(); + testOutputDeviceTypes(); + + compareFailedTestsWithNearestPassingTest(); } catch (InterruptedException e) { - analyzeTestResults(); + compareFailedTestsWithNearestPassingTest(); } catch (Exception e) { log(e.getMessage()); showErrorToast(e.getMessage()); } finally { runOnUiThread(() -> { mCheckBoxInputPresets.setEnabled(true); - mCheckBoxInputDevices.setEnabled(true); - mCheckBoxOutputDevices.setEnabled(true); + mCheckBoxAllChannels.setEnabled(true); + mCheckBoxInputChannelMasks.setEnabled(true); + mRadioGroupOutputChannelMasks.setEnabled(true); + mCheckBoxAllSampleRates.setEnabled(true); + keepScreenOn(false); }); } } @@ -645,22 +873,59 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig); - boolean shouldUseInputPresets = mBundleFromIntent.getBoolean(KEY_USE_INPUT_PRESETS, + // These are the current supported options. + final boolean shouldUseInputPresets = mBundleFromIntent.getBoolean(KEY_USE_INPUT_PRESETS, VALUE_DEFAULT_USE_INPUT_PRESETS); - boolean shouldUseInputDevices = mBundleFromIntent.getBoolean(KEY_USE_INPUT_DEVICES, - VALUE_DEFAULT_USE_INPUT_DEVICES); - boolean shouldUseOutputDevices = mBundleFromIntent.getBoolean(KEY_USE_OUTPUT_DEVICES, - VALUE_DEFAULT_USE_OUTPUT_DEVICES); - int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX, + final boolean shouldUseAllSampleRates = + mBundleFromIntent.getBoolean(KEY_USE_ALL_SAMPLE_RATES, + VALUE_DEFAULT_USE_ALL_SAMPLE_RATES); + + final int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX, VALUE_DEFAULT_SINGLE_TEST_INDEX); + // The old deprecated commands will get mapped to the closest new options. + final boolean shouldUseInputDevices = mBundleFromIntent.getBoolean(KEY_USE_INPUT_DEVICES, + VALUE_DEFAULT_USE_INPUT_CHANNEL_MASKS); + final boolean shouldUseInputChannelMasks = + mBundleFromIntent.getBoolean(KEY_USE_INPUT_CHANNEL_MASKS, + shouldUseInputDevices); + + final boolean shouldUseOutputDevices = mBundleFromIntent.getBoolean(KEY_USE_OUTPUT_DEVICES, + VALUE_DEFAULT_USE_ALL_CHANNEL_COUNTS); + final boolean shouldUseAllChannelCounts = + mBundleFromIntent.getBoolean(KEY_USE_ALL_CHANNEL_COUNTS, + shouldUseOutputDevices); + + final boolean shouldUseAllOutputChannelMasks = + mBundleFromIntent.getBoolean(KEY_USE_ALL_OUTPUT_CHANNEL_MASKS, + VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS); + final int defaultOutputChannelMasksLevel = shouldUseAllOutputChannelMasks + ? COVERAGE_LEVEL_ALL : COVERAGE_LEVEL_SOME; + final int outputChannelMasksLevel = mBundleFromIntent.getInt(KEY_OUTPUT_CHANNEL_MASKS_LEVEL, + defaultOutputChannelMasksLevel); + runOnUiThread(() -> { mCheckBoxInputPresets.setChecked(shouldUseInputPresets); - mCheckBoxInputDevices.setChecked(shouldUseInputDevices); - mCheckBoxOutputDevices.setChecked(shouldUseOutputDevices); + mCheckBoxAllSampleRates.setChecked(shouldUseAllSampleRates); mAutomatedTestRunner.setTestIndexText(singleTestIndex); + mCheckBoxAllChannels.setChecked(shouldUseAllChannelCounts); + mCheckBoxInputChannelMasks.setChecked(shouldUseInputChannelMasks); + switch(outputChannelMasksLevel) { + case COVERAGE_LEVEL_ALL: + mRadioOutputChannelMasksAll.setChecked(true); + break; + case COVERAGE_LEVEL_SOME: + mRadioOutputChannelMasksSome.setChecked(true); + break; + case COVERAGE_LEVEL_NONE: + default: + mRadioOutputChannelMasksNone.setChecked(true); + break; + } }); + // This will sync with the above checkbox code because it will log on the UI + // thread before running any tests. mAutomatedTestRunner.startTest(); } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java index 2f3c78c5..b74c3b10 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java @@ -20,9 +20,16 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.Button; +import android.widget.CheckBox; import android.widget.TextView; import java.io.IOException; @@ -47,9 +54,14 @@ public class TestDisconnectActivity extends TestAudioActivity { private volatile boolean mTestFailed; private volatile boolean mSkipTest; private volatile int mPlugCount; + private volatile int mUsbDeviceAttachedCount; + private volatile int mPlugState; + private volatile int mPlugMicrophone; private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver(); private Button mFailButton; private Button mSkipButton; + private CheckBox mCheckBoxInputs; + private CheckBox mCheckBoxOutputs; protected AutomatedTestRunner mAutomatedTestRunner; @@ -58,15 +70,68 @@ public class TestDisconnectActivity extends TestAudioActivity { public class PluginBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - mPlugCount++; + switch (intent.getAction()) { + case Intent.ACTION_HEADSET_PLUG: { + mPlugMicrophone = intent.getIntExtra("microphone", -1); + mPlugState = intent.getIntExtra("state", -1); + mPlugCount++; + } break; + case UsbManager.ACTION_USB_DEVICE_ATTACHED: + case UsbManager.ACTION_USB_DEVICE_DETACHED: { + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + final boolean hasAudioPlayback = + containsAudioStreamingInterface(device, UsbConstants.USB_DIR_OUT); + final boolean hasAudioCapture = + containsAudioStreamingInterface(device, UsbConstants.USB_DIR_IN); + if (hasAudioPlayback || hasAudioCapture) { + mPlugState = + intent.getAction() == UsbManager.ACTION_USB_DEVICE_ATTACHED ? 1 : 0; + mUsbDeviceAttachedCount++; + mPlugMicrophone = hasAudioCapture ? 1 : 0; + } + } break; + default: + break; + } runOnUiThread(new Runnable() { @Override public void run() { - String message = "Intent.HEADSET_PLUG #" + mPlugCount; + String message = "HEADSET_PLUG #" + mPlugCount + + ", USB_DEVICE_DE/ATTACHED #" + mUsbDeviceAttachedCount + + ", mic = " + mPlugMicrophone + + ", state = " + mPlugState; mPlugTextView.setText(message); + log(message); } }); } + + private static final int AUDIO_STREAMING_SUB_CLASS = 2; + + /** + * Figure out if an UsbDevice contains audio input/output streaming interface or not. + * + * @param device the given UsbDevice + * @param direction the direction of the audio streaming interface + * @return true if the UsbDevice contains the audio input/output streaming interface. + */ + private boolean containsAudioStreamingInterface(UsbDevice device, int direction) { + final int interfaceCount = device.getInterfaceCount(); + for (int i = 0; i < interfaceCount; ++i) { + UsbInterface usbInterface = device.getInterface(i); + if (usbInterface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO + && usbInterface.getInterfaceSubclass() != AUDIO_STREAMING_SUB_CLASS) { + continue; + } + final int endpointCount = usbInterface.getEndpointCount(); + for (int j = 0; j < endpointCount; ++j) { + if (usbInterface.getEndpoint(j).getDirection() == direction) { + return true; + } + } + } + return false; + } } @Override @@ -85,6 +150,9 @@ public class TestDisconnectActivity extends TestAudioActivity { mStatusTextView = (TextView) findViewById(R.id.text_status); mPlugTextView = (TextView) findViewById(R.id.text_plug_events); + mCheckBoxInputs = (CheckBox)findViewById(R.id.checkbox_disco_inputs); + mCheckBoxOutputs = (CheckBox)findViewById(R.id.checkbox_disco_outputs); + mFailButton = (Button) findViewById(R.id.button_fail); mSkipButton = (Button) findViewById(R.id.button_skip); updateFailSkipButton(false); @@ -138,6 +206,8 @@ public class TestDisconnectActivity extends TestAudioActivity { public void onResume() { super.onResume(); IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); this.registerReceiver(mPluginReceiver, filter); } @@ -185,15 +255,25 @@ public class TestDisconnectActivity extends TestAudioActivity { private String getConfigText(StreamConfiguration config) { return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN") + ", Perf = " + StreamConfiguration.convertPerformanceModeToText( - config.getPerformanceMode()) + config.getPerformanceMode()) + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode()) - + ", " + config.getSampleRate(); + + ", " + config.getSampleRate() + + ", SRC = " + StreamConfiguration.convertRateConversionQualityToText(config.getRateConversionQuality()); + } + + private void log(Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + mAutomatedTestRunner.log("Caught " + e); } private void log(String text) { mAutomatedTestRunner.log(text); } + private void flushLog() { + mAutomatedTestRunner.flushLog(); + } + private void appendFailedSummary(String text) { mAutomatedTestRunner.appendFailedSummary(text); } @@ -202,14 +282,36 @@ public class TestDisconnectActivity extends TestAudioActivity { int perfMode, int sharingMode, int sampleRate, + int sampleRateConversionQuality, boolean requestPlugin) throws InterruptedException { if ((getSingleTestIndex() >= 0) && (mAutomatedTestRunner.getTestCount() != getSingleTestIndex())) { mAutomatedTestRunner.incrementTestCount(); return; } + if (!isInput && !mCheckBoxOutputs.isChecked()) { + return; + } + if (isInput && !mCheckBoxInputs.isChecked()) { + return; + } + + updateFailSkipButton(true); + String actualConfigText = "none"; mSkipTest = false; + mTestFailed = false; + + // Try to synchronize with the current headset state, IN or OUT. + while (mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && !mTestFailed) { + if (requestPlugin != (mPlugState == 0)) { + String message = "SYNC: " + (requestPlugin ? "UNplug" : "Plug IN") + " headset now!"; + setInstructionsText(message); + Thread.sleep(POLL_DURATION_MILLIS); + } else { + break; + } + } AudioInputTester mAudioInTester = null; AudioOutputTester mAudioOutTester = null; @@ -234,8 +336,9 @@ public class TestDisconnectActivity extends TestAudioActivity { requestedConfig.setPerformanceMode(perfMode); requestedConfig.setSharingMode(sharingMode); requestedConfig.setSampleRate(sampleRate); + if (sampleRate != 0) { - requestedConfig.setRateConversionQuality(StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM); + requestedConfig.setRateConversionQuality(sampleRateConversionQuality); } log("========================== #" + mAutomatedTestRunner.getTestCount()); @@ -246,20 +349,24 @@ public class TestDisconnectActivity extends TestAudioActivity { Thread.sleep(SETTLING_TIME_MILLIS); if (!mAutomatedTestRunner.isThreadEnabled()) return; boolean openFailed = false; + boolean hasMicFailed = false; AudioStreamBase stream = null; try { openAudio(); log("Actual:"); actualConfigText = getConfigText(actualConfig) - + ", " + (actualConfig.isMMap() ? "MMAP" : "Legacy"); + + ", " + ((actualConfig.isMMap() ? "MMAP" : "Legacy") + + ", Dev = " + actualConfig.getDeviceId() + ); log(actualConfigText); + flushLog(); stream = (isInput) ? mAudioInTester.getCurrentAudioStream() : mAudioOutTester.getCurrentAudioStream(); } catch (IOException e) { openFailed = true; - log(e.getMessage()); + log(e); } // The test is only worth running if we got the configuration we requested. @@ -285,14 +392,13 @@ public class TestDisconnectActivity extends TestAudioActivity { } catch (IOException e) { e.printStackTrace(); valid = false; - log(e.getMessage()); + log(e); } } int oldPlugCount = mPlugCount; if (!openFailed && valid) { mTestFailed = false; - updateFailSkipButton(true); // poll until stream started while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) { @@ -305,15 +411,18 @@ public class TestDisconnectActivity extends TestAudioActivity { // Wait for Java plug count to change or stream to disconnect. while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) { + flushLog(); Thread.sleep(POLL_DURATION_MILLIS); if (mPlugCount > oldPlugCount) { timeoutCount = TIME_TO_FAILURE_MILLIS / POLL_DURATION_MILLIS; break; } } + // Wait for timeout or stream to disconnect. while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && (timeoutCount > 0) && stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) { + flushLog(); Thread.sleep(POLL_DURATION_MILLIS); timeoutCount--; if (timeoutCount == 0) { @@ -322,15 +431,26 @@ public class TestDisconnectActivity extends TestAudioActivity { setStatusText("Plug detected by Java.\nCounting down to Oboe failure: " + timeoutCount); } } - if (!mTestFailed) { - int error = stream.getLastErrorCallbackResult(); - if (error != StreamConfiguration.ERROR_DISCONNECTED) { - log("onEerrorCallback error = " + error - + ", expected " + StreamConfiguration.ERROR_DISCONNECTED); - mTestFailed = true; + + if (mSkipTest) { + setStatusText("Skipped"); + } else { + if (mTestFailed) { + // Check whether the peripheral has a microphone. + // Sometimes the microphones does not appear on the first HEADSET_PLUG event. + if (isInput && (mPlugMicrophone == 0)) { + hasMicFailed = true; + } + } else { + int error = stream.getLastErrorCallbackResult(); + if (error != StreamConfiguration.ERROR_DISCONNECTED) { + log("onErrorCallback error = " + error + + ", expected " + StreamConfiguration.ERROR_DISCONNECTED); + mTestFailed = true; + } } + setStatusText(mTestFailed ? "Failed" : "Passed - detected"); } - setStatusText(mTestFailed ? "Failed" : "Passed - detected"); } updateFailSkipButton(false); setInstructionsText("Wait..."); @@ -352,6 +472,9 @@ public class TestDisconnectActivity extends TestAudioActivity { boolean passed = !mTestFailed; String resultText = requestPlugin ? "plugIN" : "UNplug"; resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL); + if (hasMicFailed) { + resultText += ", Headset has no mic!"; + } log(resultText); if (!passed) { appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n"); @@ -365,17 +488,27 @@ public class TestDisconnectActivity extends TestAudioActivity { } else { log(TEXT_SKIP); } + flushLog(); // Give hardware time to settle between tests. Thread.sleep(1000); mAutomatedTestRunner.incrementTestCount(); } private void testConfiguration(boolean isInput, int performanceMode, - int sharingMode, int sampleRate) throws InterruptedException { + int sharingMode, int sampleRate, + int sampleRateConversionQuality) throws InterruptedException { boolean requestPlugin = true; // plug IN - testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin); + testConfiguration(isInput, performanceMode, sharingMode, sampleRate, + sampleRateConversionQuality, requestPlugin); requestPlugin = false; // UNplug - testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin); + testConfiguration(isInput, performanceMode, sharingMode, sampleRate, + sampleRateConversionQuality, requestPlugin); + } + + private void testConfiguration(boolean isInput, int performanceMode, + int sharingMode, int sampleRate) throws InterruptedException { + testConfiguration(isInput, performanceMode, sharingMode, sampleRate, + StreamConfiguration.RATE_CONVERSION_QUALITY_NONE); } private void testConfiguration(boolean isInput, int performanceMode, @@ -390,25 +523,45 @@ public class TestDisconnectActivity extends TestAudioActivity { testConfiguration(true, performanceMode, sharingMode); } + private void testConfiguration(int performanceMode, + int sharingMode, int sampleRate, + int sampleRateConversionQuality) throws InterruptedException { + testConfiguration(false, performanceMode, sharingMode, sampleRate, sampleRateConversionQuality); + testConfiguration(true, performanceMode, sharingMode, sampleRate, sampleRateConversionQuality); + } + @Override public void runTest() { + + runOnUiThread(() -> keepScreenOn(true)); + mPlugCount = 0; + // Try several different configurations. try { - testConfiguration(false, StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_EXCLUSIVE, 44100); - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_EXCLUSIVE); - testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, - StreamConfiguration.SHARING_MODE_SHARED); testConfiguration(StreamConfiguration.PERFORMANCE_MODE_NONE, StreamConfiguration.SHARING_MODE_SHARED); + if (NativeEngine.isMMapExclusiveSupported()){ + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_EXCLUSIVE); + } + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_SHARED); + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_SHARED, 44100, + StreamConfiguration.RATE_CONVERSION_QUALITY_NONE); + testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY, + StreamConfiguration.SHARING_MODE_SHARED, 44100, + StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM); } catch (InterruptedException e) { - log(e.getMessage()); - showErrorToast(e.getMessage()); + log("Test CANCELLED - INVALID!"); + } catch (Exception e) { + log(e); + showErrorToast("Caught " + e); } finally { - setInstructionsText("Test completed."); + setInstructionsText("Test finished."); updateFailSkipButton(false); + runOnUiThread(() -> keepScreenOn(false)); } } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java index 7f33d803..a6c9bbd6 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java @@ -17,17 +17,15 @@ package com.mobileer.oboetester; import android.content.Intent; -import android.media.audiofx.AcousticEchoCanceler; -import android.media.audiofx.AutomaticGainControl; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.v4.content.FileProvider; import android.view.View; import android.widget.RadioButton; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; import java.io.File; import java.io.IOException; @@ -39,12 +37,12 @@ import java.io.IOException; public class TestInputActivity extends TestAudioActivity { protected AudioInputTester mAudioInputTester; - private static final int NUM_VOLUME_BARS = 4; + // Note that this must match the number of volume bars defined in the layout file. + private static final int NUM_VOLUME_BARS = 8; private VolumeBarView[] mVolumeBars = new VolumeBarView[NUM_VOLUME_BARS]; private InputMarginView mInputMarginView; private int mInputMarginBursts = 0; private WorkloadView mWorkloadView; - private CommunicationDeviceView mCommunicationDeviceView; public native void setMinimumFramesBeforeRead(int frames); public native int saveWaveFile(String absolutePath); @@ -67,6 +65,10 @@ public class TestInputActivity extends TestAudioActivity { mVolumeBars[1] = (VolumeBarView) findViewById(R.id.volumeBar1); mVolumeBars[2] = (VolumeBarView) findViewById(R.id.volumeBar2); mVolumeBars[3] = (VolumeBarView) findViewById(R.id.volumeBar3); + mVolumeBars[4] = (VolumeBarView) findViewById(R.id.volumeBar4); + mVolumeBars[5] = (VolumeBarView) findViewById(R.id.volumeBar5); + mVolumeBars[6] = (VolumeBarView) findViewById(R.id.volumeBar6); + mVolumeBars[7] = (VolumeBarView) findViewById(R.id.volumeBar7); mInputMarginView = (InputMarginView) findViewById(R.id.input_margin_view); @@ -76,21 +78,13 @@ public class TestInputActivity extends TestAudioActivity { mWorkloadView = (WorkloadView) findViewById(R.id.workload_view); if (mWorkloadView != null) { - mWorkloadView.setAudioStreamTester(mAudioInputTester); + mWorkloadView.setWorkloadReceiver((w) -> mAudioInputTester.setWorkload(w)); } mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); } @Override - protected void onStop() { - if (mCommunicationDeviceView != null) { - mCommunicationDeviceView.cleanup(); - } - super.onStop(); - } - - @Override int getActivityType() { return ACTIVITY_TEST_INPUT; } @@ -150,11 +144,6 @@ public class TestInputActivity extends TestAudioActivity { resetVolumeBars(); } - @Override - protected void toastPauseError(int result) { - showToast("Pause not implemented. Returned " + result); - } - protected int saveWaveFile(File file) { // Pass filename to native to write WAV file int result = saveWaveFile(file.getAbsolutePath()); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java index 0d5d0ce3..f2ac5ed5 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java @@ -16,19 +16,18 @@ package com.mobileer.oboetester; -import android.content.Context; -import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.CheckBox; +import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TextView; import java.io.IOException; +import java.util.Locale; /** * Test basic output. @@ -38,7 +37,9 @@ public final class TestOutputActivity extends TestOutputActivityBase { public static final int MAX_CHANNEL_BOXES = 16; private CheckBox[] mChannelBoxes; private Spinner mOutputSignalSpinner; - protected CommunicationDeviceView mCommunicationDeviceView; + private TextView mVolumeTextView; + private SeekBar mVolumeSeekBar; + private CheckBox mShouldSetStreamControlByAttributes; private class OutputSignalSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener { @Override @@ -52,6 +53,21 @@ public final class TestOutputActivity extends TestOutputActivityBase { } } + private SeekBar.OnSeekBarChangeListener mVolumeChangeListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + setVolume(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }; + @Override protected void inflateActivity() { setContentView(R.layout.activity_test_output); @@ -90,6 +106,12 @@ public final class TestOutputActivity extends TestOutputActivityBase { mOutputSignalSpinner.setSelection(StreamConfiguration.NATIVE_API_UNSPECIFIED); mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); + + mVolumeTextView = (TextView) findViewById(R.id.textVolumeSlider); + mVolumeSeekBar = (SeekBar) findViewById(R.id.faderVolumeSlider); + mVolumeSeekBar.setOnSeekBarChangeListener(mVolumeChangeListener); + + mShouldSetStreamControlByAttributes = (CheckBox) findViewById(R.id.enableSetStreamControlByAttributes); } @Override @@ -97,16 +119,9 @@ public final class TestOutputActivity extends TestOutputActivityBase { return ACTIVITY_TEST_OUTPUT; } - @Override - protected void onStop() { - if (mCommunicationDeviceView != null) { - mCommunicationDeviceView.cleanup(); - } - super.onStop(); - } - public void openAudio() throws IOException { super.openAudio(); + mShouldSetStreamControlByAttributes.setEnabled(false); } private void configureChannelBoxes(int channelCount) { @@ -116,6 +131,20 @@ public final class TestOutputActivity extends TestOutputActivityBase { } } + private void setVolume(int progress) { + // Convert from (0, 500) range to (-50, 0). + double decibels = (progress - 500) / 10.0f; + double amplitude = Math.pow(10.0, decibels / 20.0); + // When the slider is all way to the left, set a zero amplitude. + if (progress == 0) { + amplitude = 0; + } + mVolumeTextView.setText("Volume(dB): " + String.format(Locale.getDefault(), "%.1f", + decibels)); + mAudioOutTester.setAmplitude((float) amplitude); + } + + public void stopAudio() { configureChannelBoxes(0); mOutputSignalSpinner.setEnabled(true); @@ -128,9 +157,16 @@ public final class TestOutputActivity extends TestOutputActivityBase { super.pauseAudio(); } + public void releaseAudio() { + configureChannelBoxes(0); + mOutputSignalSpinner.setEnabled(true); + super.releaseAudio(); + } + public void closeAudio() { configureChannelBoxes(0); mOutputSignalSpinner.setEnabled(true); + mShouldSetStreamControlByAttributes.setEnabled(true); super.closeAudio(); } @@ -149,6 +185,11 @@ public final class TestOutputActivity extends TestOutputActivityBase { } @Override + protected boolean shouldSetStreamControlByAttributes() { + return mShouldSetStreamControlByAttributes.isChecked(); + } + + @Override public void startTestUsingBundle() { try { StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java index 5c17e928..731351b9 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java @@ -26,7 +26,7 @@ abstract class TestOutputActivityBase extends TestAudioActivity { AudioOutputTester mAudioOutTester; private BufferSizeView mBufferSizeView; - private WorkloadView mWorkloadView; + protected WorkloadView mWorkloadView; @Override boolean isOutput() { return true; } @@ -40,13 +40,14 @@ abstract class TestOutputActivityBase extends TestAudioActivity { super.findAudioCommon(); mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view); mWorkloadView = (WorkloadView) findViewById(R.id.workload_view); + if (mWorkloadView != null) { + mWorkloadView.setWorkloadReceiver((w) -> mAudioOutTester.setWorkload(w)); + } } @Override public AudioOutputTester addAudioOutputTester() { - AudioOutputTester audioOutTester = super.addAudioOutputTester(); - mWorkloadView.setAudioStreamTester(audioOutTester); - return audioOutTester; + return super.addAudioOutputTester(); } @Override diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java index 3110447b..b55fa127 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java @@ -40,6 +40,7 @@ import java.util.HashMap; public class TestPlugLatencyActivity extends TestAudioActivity { public static final int POLL_DURATION_MILLIS = 1; + public static final int TIMEOUT_MILLIS = 1000; private TextView mInstructionsTextView; private TextView mPlugTextView; @@ -48,6 +49,7 @@ public class TestPlugLatencyActivity extends TestAudioActivity { private AudioManager mAudioManager; private volatile int mPlugCount = 0; + private long mTimeoutAtMillis; private AudioOutputTester mAudioOutTester; @@ -58,12 +60,17 @@ public class TestPlugLatencyActivity extends TestAudioActivity { @Override public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { boolean isBootingUp = mDevices.isEmpty(); + AudioDeviceInfo outputDeviceInfo = null; for (AudioDeviceInfo info : addedDevices) { mDevices.put(info.getId(), info); if (!isBootingUp) { - log("Device Added"); + log("====== Device Added ======="); log(adiToString(info)); + // Only process OUTPUT devices because that is what we are testing. + if (info.isSink()) { + outputDeviceInfo = info; + } } } @@ -71,17 +78,26 @@ public class TestPlugLatencyActivity extends TestAudioActivity { if (isBootingUp) { log("Starting stream with existing audio devices"); } - updateLatency(false /* wasDeviceRemoved */); + if (outputDeviceInfo != null) { + updateLatency(false /* wasDeviceRemoved */); + } } public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + AudioDeviceInfo outputDeviceInfo = null; for (AudioDeviceInfo info : removedDevices) { mDevices.remove(info.getId()); - log("Device Removed"); + log("====== Device Removed ======="); log(adiToString(info)); + // Only process OUTPUT devices because that is what we are testing. + if (info.isSink()) { + outputDeviceInfo = info; + } } - updateLatency(true /* wasDeviceRemoved */); + if (outputDeviceInfo != null) { + updateLatency(true /* wasDeviceRemoved */); + } } } @@ -153,41 +169,79 @@ public class TestPlugLatencyActivity extends TestAudioActivity { startAudio(); } - private long calculateLatencyMs(boolean wasDeviceRemoved) { + private void setupTimeout() { + mTimeoutAtMillis = System.currentTimeMillis() + TIMEOUT_MILLIS; + } - long startMillis = System.currentTimeMillis(); + private void sleepOrTimeout(String message) throws InterruptedException { + Thread.sleep(POLL_DURATION_MILLIS); + if (System.currentTimeMillis() >= mTimeoutAtMillis) { + throw new InterruptedException(message); + } + } + private long calculateLatencyMs(boolean wasDeviceRemoved) { + long testStartMillis = System.currentTimeMillis(); + long frameReadMillis = -1; + final int TIMEOUT_MAX = 100; + int timeout; try { + long callbackMillis = -1; if (wasDeviceRemoved && (mAudioOutTester != null)) { + log("Wait for error callback != 0"); // Keep querying as long as error is ok + setupTimeout(); while (mAudioOutTester.getLastErrorCallbackResult() == 0) { - Thread.sleep(POLL_DURATION_MILLIS); + sleepOrTimeout("timed out waiting while error==0"); } - log("Error callback at " + (System.currentTimeMillis() - startMillis) + " ms"); + callbackMillis = System.currentTimeMillis(); + log("Error callback at " + (callbackMillis - testStartMillis) + " ms. " + + "WAIT -> CALLBACK = took " + (callbackMillis - testStartMillis) + " ms"); } closeAudio(); - log("Audio closed at " + (System.currentTimeMillis() - startMillis) + " ms"); + long closedMillis = System.currentTimeMillis(); + if (callbackMillis == -1) { + log("Audio closed at " + (closedMillis - testStartMillis) + " ms"); + } else { + log("Audio closed at " + (closedMillis - testStartMillis) + " ms. " + + "CALLBACK -> CLOSED took " + (closedMillis - callbackMillis) + " ms"); + } + clearStreamContexts(); mAudioOutTester = addAudioOutputTester(); openAudio(); - log("Audio opened at " + (System.currentTimeMillis() - startMillis) + " ms"); + long openedMillis = System.currentTimeMillis(); + log("Audio opened at " + (openedMillis - testStartMillis) + " ms. " + + "CLOSED -> OPENED took " + (openedMillis - closedMillis) + " ms"); AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); startAudioTest(); - log("Audio starting at " + (System.currentTimeMillis() - startMillis) + " ms"); + long startingMillis = System.currentTimeMillis(); + log("Audio starting at " + (startingMillis - testStartMillis) + " ms. " + + "OPENED -> STARTING took " + (startingMillis - openedMillis) + " ms"); + + setupTimeout(); while (stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) { - Thread.sleep(POLL_DURATION_MILLIS); + sleepOrTimeout("timed out waiting while STATE_STARTING"); } - log("Audio started at " + (System.currentTimeMillis() - startMillis) + " ms"); + long startedMillis = System.currentTimeMillis(); + log("Audio started at " + (startedMillis - testStartMillis) + " ms. " + + "STARTING -> STARTED took " + (startedMillis - startingMillis) + " ms"); + + setupTimeout(); while (mAudioOutTester.getFramesRead() == 0) { - Thread.sleep(POLL_DURATION_MILLIS); + sleepOrTimeout("timed out waiting while framesRead()==0"); } - log("First frame read at " + (System.currentTimeMillis() - startMillis) + " ms"); + frameReadMillis = System.currentTimeMillis(); + log("First frame read at " + (frameReadMillis - testStartMillis) + " ms. " + + "STARTED -> READ took " + (frameReadMillis - startedMillis) + " ms"); } catch (IOException | InterruptedException e) { + log("EXCEPTION: " + e); e.printStackTrace(); + closeAudio(); return -1; } - return System.currentTimeMillis() - startMillis; + return frameReadMillis - testStartMillis; } public static String adiToString(AudioDeviceInfo adi) { diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java new file mode 100644 index 00000000..4dfce073 --- /dev/null +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java @@ -0,0 +1,177 @@ +/* + * Copyright 2023 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.TestAudioActivity.TAG; + +import android.app.Activity; +import android.content.Context; +import android.media.AudioManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.RadioButton; +import android.widget.TextView; + +import java.util.Random; + +/** + * Try to crash in the native AAudio code by causing a routing change + * while playing audio. The buffer may get deleted while we are writing to it! + * See b/274815060 + */ +public class TestRouteDuringCallbackActivity extends Activity { + + private TextView mStatusView; + private MyStreamSniffer mStreamSniffer; + private AudioManager mAudioManager; + private RadioButton mOutputButton; + private RadioButton mInputButton; + private Button mStartButton; + private Button mStopButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_routing_crash); + mStatusView = (TextView) findViewById(R.id.text_callback_status); + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + mStartButton = (Button) findViewById(R.id.button_start_test); + mStopButton = (Button) findViewById(R.id.button_stop_test); + mOutputButton = (RadioButton) findViewById(R.id.direction_output); + mInputButton = (RadioButton) findViewById(R.id.direction_input); + setButtonsEnabled(false); + } + + public void onStartRoutingTest(View view) { + startRoutingTest(); + } + + public void onStopRoutingTest(View view) { + stopRoutingTest(); + } + + private void setButtonsEnabled(boolean running) { + mStartButton.setEnabled(!running); + mStopButton.setEnabled(running); + mOutputButton.setEnabled(!running); + mInputButton.setEnabled(!running); + } + + // Change routing while the stream is playing. + // Keep trying until we crash. + protected class MyStreamSniffer extends Thread { + boolean enabled = true; + int routingOption = 0; + StringBuffer statusBuffer = new StringBuffer(); + int loopCount; + + @Override + public void run() { + routingOption = 0; + changeRoute(routingOption); + int result; + Random random = new Random(); + while (enabled) { + loopCount++; + if (routingOption == 0) { + statusBuffer = new StringBuffer(); + } + try { + sleep(100); + boolean useInput = mInputButton.isChecked(); + result = startStream(useInput); + sleep(100); + log("-------#" + loopCount + ", " + (useInput ? "IN" : "OUT") + + "\nstartStream() returned " + result); + int sleepTimeMillis = 500 + random.nextInt(500); + sleep(sleepTimeMillis); + routingOption = (routingOption == 0) ? 1 : 0; + log( "changeRoute " + routingOption); + changeRoute(routingOption); + sleep(50); + } catch (InterruptedException e) { + } finally { + result = stopStream(); + log("stopStream() returned " + result); + } + } + changeRoute(0); + } + + // Log to screen and logcat. + private void log(String text) { + Log.d(TAG, "RoutingCrash: " + text); + statusBuffer.append(text + ", sleep " + getSleepTimeMicros() + " us\n"); + showStatus(statusBuffer.toString()); + } + + // Stop the test thread. + void finish() { + enabled = false; + interrupt(); + try { + join(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void showStatus(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mStatusView.setText(message); + } + }); + } + + private void changeRoute(int option) { + mAudioManager.setSpeakerphoneOn(option > 0); + } + + private native int startStream(boolean useInput); + private native int getSleepTimeMicros(); + private native int stopStream(); + + @Override + public void onPause() { + super.onPause(); + stopRoutingTest(); + } + + private void startRoutingTest() { + stopRoutingTest(); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setButtonsEnabled(true); + mStreamSniffer = new MyStreamSniffer(); + mStreamSniffer.start(); + } + + private void stopRoutingTest() { + if (mStreamSniffer != null) { + mStreamSniffer.finish(); + mStreamSniffer = null; + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setButtonsEnabled(false); + } + } +} diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java index c6a2dc24..0dc6f4df 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java @@ -24,6 +24,8 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; +import java.util.Locale; + /** * Display volume in dB as a red bar. * Display the amplitude, 0.0 to 1.0, as text in the bar. @@ -81,7 +83,7 @@ public class VolumeBarView extends View { float volumeWidth = ((MIN_VOLUME_DB - mVolume) / MIN_VOLUME_DB) * mCurrentWidth; canvas.drawRect(0.0f, 0.0f, volumeWidth, mCurrentHeight, mBarPaint); - String text = String.format(FORMAT_AMPLITUDE, mClippedAmplitude); + String text = String.format(Locale.getDefault(), FORMAT_AMPLITUDE, mClippedAmplitude); mTextPaint.getTextBounds(text, 0, text.length(), mTextBounds); canvas.drawText(text, 20.0f, diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java index fb50914b..4f4705c0 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java @@ -27,6 +27,7 @@ import android.view.View; /** * Display an audio waveform in a custom View. + * Data is assumed to have a constant x-axis increment, ie. isochronous. */ public class WaveformView extends View { private static final float MESSAGE_TEXT_SIZE = 80; @@ -105,7 +106,18 @@ public class WaveformView extends View { if (localData == null || mSampleCount == 0) { return; } + float xScale = ((float) mCurrentWidth) / (mSampleCount - 1); + + // Draw cursors. + if (mCursors != null) { + for (int i = 0; i < mCursors.length; i++) { + float x = mCursors[i] * xScale; + canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint); + } + } + + // Draw waveform. float x0 = 0.0f; if (xScale < 1.0) { // Draw a vertical bar for multiple samples. @@ -135,16 +147,11 @@ public class WaveformView extends View { y0 = y1; } } - if (mCursors != null) { - for (int i = 0; i < mCursors.length; i++) { - float x = mCursors[i] * xScale; - canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint); - } - } } /** - * Copy data into internal buffer then repaint. + * Copy data into internal buffer. + * Caller should then postInvalidate(). */ public void setSampleData(float[] samples) { setSampleData(samples, 0, samples.length); @@ -170,7 +177,8 @@ public class WaveformView extends View { } /** - * Copy cursor positions into internal buffer then repaint. + * Copy cursor positions into internal buffer. + * Caller should then postInvalidate(). */ public void setCursorData(int[] cursors) { if (cursors == null) { diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java index 78d78df7..74027c2c 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java @@ -18,20 +18,29 @@ package com.mobileer.oboetester; import android.content.Context; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; -public class WorkloadView extends LinearLayout { +import java.util.Locale; - private AudioStreamTester mAudioStreamTester; +public class WorkloadView extends LinearLayout { protected static final int FADER_PROGRESS_MAX = 1000; // must match layout protected TextView mTextView; protected SeekBar mSeekBar; + + private String mLabel = "Workload"; protected ExponentialTaper mExponentialTaper; + public interface WorkloadReceiver { + void setWorkload(int workload); + } + + WorkloadReceiver mWorkloadReceiver; + private SeekBar.OnSeekBarChangeListener mChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @@ -64,12 +73,8 @@ public class WorkloadView extends LinearLayout { initializeViews(context); } - public AudioStreamTester getAudioStreamTester() { - return mAudioStreamTester; - } - - public void setAudioStreamTester(AudioStreamTester audioStreamTester) { - mAudioStreamTester = audioStreamTester; + public void setWorkloadReceiver(WorkloadReceiver workloadReceiver) { + mWorkloadReceiver = workloadReceiver; } void setFaderNormalizedProgress(double fraction) { @@ -90,15 +95,21 @@ public class WorkloadView extends LinearLayout { mTextView = (TextView) findViewById(R.id.textWorkload); mSeekBar = (SeekBar) findViewById(R.id.faderWorkload); mSeekBar.setOnSeekBarChangeListener(mChangeListener); - mExponentialTaper = new ExponentialTaper(0.0, 100.0, 10.0); + setRange(0.0, 100.0); //mSeekBar.setProgress(0); } + void setRange(double dMin, double dMax) { + mExponentialTaper = new ExponentialTaper(dMin, dMax, 10.0); + } + private void setValueByPosition(int progress) { - double workload = mExponentialTaper.linearToExponential( + int workload = (int) mExponentialTaper.linearToExponential( ((double)progress) / FADER_PROGRESS_MAX); - mAudioStreamTester.setWorkload(workload); - mTextView.setText("Workload = " + String.format("%6.2f", workload)); + if (mWorkloadReceiver != null) { + mWorkloadReceiver.setWorkload(workload); + } + mTextView.setText(getLabel() + " = " + String.format(Locale.getDefault(), "%3d", workload)); } @Override @@ -106,4 +117,12 @@ public class WorkloadView extends LinearLayout { super.setEnabled(enabled); mSeekBar.setEnabled(enabled); } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + this.mLabel = label; + } } |