1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.oboe.samples.hellooboe; 18 19 import android.app.Activity; 20 import android.media.AudioManager; 21 import android.os.Build; 22 import android.os.Bundle; 23 import androidx.core.view.MotionEventCompat; 24 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.widget.AdapterView; 28 import android.widget.ArrayAdapter; 29 import android.widget.SimpleAdapter; 30 import android.widget.Spinner; 31 import android.widget.TextView; 32 33 import com.google.oboe.samples.audio_device.AudioDeviceListEntry; 34 import com.google.oboe.samples.audio_device.AudioDeviceSpinner; 35 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.List; 39 import java.util.Locale; 40 import java.util.Timer; 41 import java.util.TimerTask; 42 43 public class MainActivity extends Activity { 44 45 private static final String TAG = MainActivity.class.getName(); 46 private static final long UPDATE_LATENCY_EVERY_MILLIS = 1000; 47 private static final Integer[] CHANNEL_COUNT_OPTIONS = {1, 2, 3, 4, 5, 6, 7, 8}; 48 // Default to Stereo (OPTIONS is zero-based array so index 1 = 2 channels) 49 private static final int CHANNEL_COUNT_DEFAULT_OPTION_INDEX = 1; 50 private static final int[] BUFFER_SIZE_OPTIONS = {0, 1, 2, 4, 8}; 51 private static final String[] AUDIO_API_OPTIONS = {"Unspecified", "OpenSL ES", "AAudio"}; 52 // Default all other spinners to the first option on the list 53 private static final int SPINNER_DEFAULT_OPTION_INDEX = 0; 54 55 private Spinner mAudioApiSpinner; 56 private AudioDeviceSpinner mPlaybackDeviceSpinner; 57 private Spinner mChannelCountSpinner; 58 private Spinner mBufferSizeSpinner; 59 private TextView mLatencyText; 60 private Timer mLatencyUpdater; 61 62 /* 63 * Hook to user control to start / stop audio playback: 64 * touch-down: start, and keeps on playing 65 * touch-up: stop. 66 * simply pass the events to native side. 67 */ 68 @Override onTouchEvent(MotionEvent event)69 public boolean onTouchEvent(MotionEvent event) { 70 int action = MotionEventCompat.getActionMasked(event); 71 switch (action) { 72 case (MotionEvent.ACTION_DOWN): 73 PlaybackEngine.setToneOn(true); 74 break; 75 case (MotionEvent.ACTION_UP): 76 PlaybackEngine.setToneOn(false); 77 break; 78 } 79 return super.onTouchEvent(event); 80 } 81 82 @Override onCreate(Bundle savedInstanceState)83 protected void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 setContentView(R.layout.activity_main); 86 mLatencyText = findViewById(R.id.latencyText); 87 setupAudioApiSpinner(); 88 setupPlaybackDeviceSpinner(); 89 setupChannelCountSpinner(); 90 setupBufferSizeSpinner(); 91 92 } 93 /* 94 * Creating engine in onResume() and destroying in onPause() so the stream retains exclusive 95 * mode only while in focus. This allows other apps to reclaim exclusive stream mode. 96 */ 97 @Override onResume()98 protected void onResume() { 99 super.onResume(); 100 PlaybackEngine.create(this); 101 setupLatencyUpdater(); 102 // Return the spinner states to their default value 103 mChannelCountSpinner.setSelection(CHANNEL_COUNT_DEFAULT_OPTION_INDEX); 104 mPlaybackDeviceSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 105 mBufferSizeSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 106 mAudioApiSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 107 } 108 109 @Override onPause()110 protected void onPause() { 111 if (mLatencyUpdater != null) mLatencyUpdater.cancel(); 112 PlaybackEngine.delete(); 113 super.onPause(); 114 } 115 setupChannelCountSpinner()116 private void setupChannelCountSpinner() { 117 mChannelCountSpinner = findViewById(R.id.channelCountSpinner); 118 119 ArrayAdapter<Integer> channelCountAdapter = new ArrayAdapter<Integer>(this, R.layout.channel_counts_spinner, CHANNEL_COUNT_OPTIONS); 120 mChannelCountSpinner.setAdapter(channelCountAdapter); 121 mChannelCountSpinner.setSelection(CHANNEL_COUNT_DEFAULT_OPTION_INDEX); 122 123 mChannelCountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 124 @Override 125 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 126 PlaybackEngine.setChannelCount(CHANNEL_COUNT_OPTIONS[mChannelCountSpinner.getSelectedItemPosition()]); 127 } 128 129 @Override 130 public void onNothingSelected(AdapterView<?> adapterView) { 131 132 } 133 }); 134 } 135 setupBufferSizeSpinner()136 private void setupBufferSizeSpinner() { 137 mBufferSizeSpinner = findViewById(R.id.bufferSizeSpinner); 138 mBufferSizeSpinner.setAdapter(new SimpleAdapter( 139 this, 140 createBufferSizeOptionsList(), // list of buffer size options 141 R.layout.buffer_sizes_spinner, // the xml layout 142 new String[]{getString(R.string.buffer_size_description_key)}, // field to display 143 new int[]{R.id.bufferSizeOption} // View to show field in 144 )); 145 146 mBufferSizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 147 @Override 148 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 149 PlaybackEngine.setBufferSizeInBursts(getBufferSizeInBursts()); 150 } 151 152 @Override 153 public void onNothingSelected(AdapterView<?> adapterView) { 154 155 } 156 }); 157 } 158 setupPlaybackDeviceSpinner()159 private void setupPlaybackDeviceSpinner() { 160 mPlaybackDeviceSpinner = findViewById(R.id.playbackDevicesSpinner); 161 162 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 163 mPlaybackDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS); 164 mPlaybackDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 165 @Override 166 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 167 PlaybackEngine.setAudioDeviceId(getPlaybackDeviceId()); 168 } 169 170 @Override 171 public void onNothingSelected(AdapterView<?> adapterView) { 172 173 } 174 }); 175 } 176 } 177 setupAudioApiSpinner()178 private void setupAudioApiSpinner() { 179 mAudioApiSpinner = findViewById(R.id.audioApiSpinner); 180 mAudioApiSpinner.setAdapter(new SimpleAdapter( 181 this, 182 createAudioApisOptionsList(), 183 R.layout.audio_apis_spinner, // the xml layout 184 new String[]{getString(R.string.audio_api_description_key)}, // field to display 185 new int[]{R.id.audioApiOption} // View to show field in 186 )); 187 188 mAudioApiSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 189 @Override 190 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 191 PlaybackEngine.setAudioApi(i); 192 } 193 194 @Override 195 public void onNothingSelected(AdapterView<?> adapterView) { 196 197 } 198 }); 199 } 200 getPlaybackDeviceId()201 private int getPlaybackDeviceId() { 202 return ((AudioDeviceListEntry) mPlaybackDeviceSpinner.getSelectedItem()).getId(); 203 } 204 getBufferSizeInBursts()205 private int getBufferSizeInBursts() { 206 @SuppressWarnings("unchecked") 207 HashMap<String, String> selectedOption = (HashMap<String, String>) 208 mBufferSizeSpinner.getSelectedItem(); 209 210 String valueKey = getString(R.string.buffer_size_value_key); 211 212 // parseInt will throw a NumberFormatException if the string doesn't contain a valid integer 213 // representation. We don't need to worry about this because the values are derived from 214 // the BUFFER_SIZE_OPTIONS int array. 215 return Integer.parseInt(selectedOption.get(valueKey)); 216 } 217 setupLatencyUpdater()218 private void setupLatencyUpdater() { 219 //Update the latency every 1s 220 TimerTask latencyUpdateTask = new TimerTask() { 221 @Override 222 public void run() { 223 final String latencyStr; 224 if (PlaybackEngine.isLatencyDetectionSupported()) { 225 double latency = PlaybackEngine.getCurrentOutputLatencyMillis(); 226 if (latency >= 0) { 227 latencyStr = String.format(Locale.getDefault(), "%.2fms", latency); 228 } else { 229 latencyStr = "Unknown"; 230 } 231 } else { 232 latencyStr = getString(R.string.only_supported_on_api_26); 233 } 234 235 runOnUiThread(new Runnable() { 236 @Override 237 public void run() { 238 mLatencyText.setText(getString(R.string.latency, latencyStr)); 239 } 240 }); 241 } 242 }; 243 mLatencyUpdater = new Timer(); 244 mLatencyUpdater.schedule(latencyUpdateTask, 0, UPDATE_LATENCY_EVERY_MILLIS); 245 } 246 247 /** 248 * Creates a list of buffer size options which can be used to populate a SimpleAdapter. 249 * Each option has a description and a value. The description is always equal to the value, 250 * except when the value is zero as this indicates that the buffer size should be set 251 * automatically by the audio engine 252 * 253 * @return list of buffer size options 254 */ createBufferSizeOptionsList()255 private List<HashMap<String, String>> createBufferSizeOptionsList() { 256 257 ArrayList<HashMap<String, String>> bufferSizeOptions = new ArrayList<>(); 258 259 for (int i : BUFFER_SIZE_OPTIONS) { 260 HashMap<String, String> option = new HashMap<>(); 261 String strValue = String.valueOf(i); 262 String description = (i == 0) ? getString(R.string.automatic) : strValue; 263 option.put(getString(R.string.buffer_size_description_key), description); 264 option.put(getString(R.string.buffer_size_value_key), strValue); 265 266 bufferSizeOptions.add(option); 267 } 268 269 return bufferSizeOptions; 270 } 271 createAudioApisOptionsList()272 private List<HashMap<String, String>> createAudioApisOptionsList() { 273 274 ArrayList<HashMap<String, String>> audioApiOptions = new ArrayList<>(); 275 276 for (int i = 0; i < AUDIO_API_OPTIONS.length; i++) { 277 HashMap<String, String> option = new HashMap<>(); 278 option.put(getString(R.string.buffer_size_description_key), AUDIO_API_OPTIONS[i]); 279 option.put(getString(R.string.buffer_size_value_key), String.valueOf(i)); 280 audioApiOptions.add(option); 281 } 282 return audioApiOptions; 283 } 284 } 285