• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.Context;
21 import android.media.AudioDeviceInfo;
22 import android.media.AudioManager;
23 import android.os.Build;
24 import android.os.Bundle;
25 
26 import androidx.annotation.RequiresApi;
27 
28 import android.os.Message;
29 import android.util.Log;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.widget.AdapterView;
33 import android.widget.ArrayAdapter;
34 import android.widget.SimpleAdapter;
35 import android.widget.Spinner;
36 import android.widget.TextView;
37 import android.widget.Toast;
38 
39 import com.google.oboe.samples.audio_device.AudioDeviceListEntry;
40 import com.google.oboe.samples.audio_device.AudioDeviceSpinner;
41 
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Objects;
47 import java.util.Timer;
48 import java.util.TimerTask;
49 
50 public class MainActivity extends Activity {
51     private static final String TAG = "HelloOboe";
52     private static final long UPDATE_LATENCY_EVERY_MILLIS = 1000;
53     private static final Integer[] CHANNEL_COUNT_OPTIONS = {1, 2, 3, 4, 5, 6, 7, 8};
54     // Default to Stereo (OPTIONS is zero-based array so index 1 = 2 channels)
55     private static final int CHANNEL_COUNT_DEFAULT_OPTION_INDEX = 1;
56     private static final int[] BUFFER_SIZE_OPTIONS = {0, 1, 2, 4, 8};
57     private static final String[] AUDIO_API_OPTIONS = {"Unspecified", "OpenSL ES", "AAudio"};
58     private static final int OBOE_API_OPENSL_ES = 1;
59     // Default all other spinners to the first option on the list
60     private static final int SPINNER_DEFAULT_OPTION_INDEX = 0;
61 
62     private Spinner mAudioApiSpinner;
63     private AudioDeviceSpinner mPlaybackDeviceSpinner;
64     private Spinner mChannelCountSpinner;
65     private Spinner mBufferSizeSpinner;
66     private TextView mLatencyText;
67     private Timer mLatencyUpdater;
68     private boolean mScoStarted = false;
69 
70     /** Commands for background thread. */
71     private static final int WHAT_START = 100;
72     private static final int WHAT_STOP = 101;
73     private static final int WHAT_SET_DEVICE_ID = 102;
74     private static final int WHAT_SET_AUDIO_API = 103;
75     private static final int WHAT_SET_CHANNEL_COUNT = 104;
76     private BackgroundRunner mRunner = new MyBackgroundRunner();
77 
78     private class MyBackgroundRunner extends BackgroundRunner {
79         // These are initialized to zero by Java.
80         // Zero matches the oboe::Unspecified value.
81         int audioApi;
82         int deviceId;
83         int channelCount;
84 
85         @Override
86             /* Execute this in a background thread to avoid ANRs. */
handleMessageInBackground(Message message)87         void handleMessageInBackground(Message message) {
88             int what = message.what;
89             int arg1 = message.arg1;
90             Log.i(MainActivity.TAG, "got background message, what = " + what + ", arg1 = " + arg1);
91             int result = 0;
92             boolean restart = false;
93             switch (what) {
94                 case WHAT_START:
95                     result = PlaybackEngine.startEngine(audioApi, deviceId, channelCount);
96                     break;
97                 case WHAT_STOP:
98                     result = PlaybackEngine.stopEngine();
99                     break;
100                 case WHAT_SET_AUDIO_API:
101                     if (audioApi != arg1) {
102                         audioApi = arg1;
103                         restart = true;
104                     }
105                     break;
106                 case WHAT_SET_DEVICE_ID:
107                     if (deviceId != arg1) {
108                         deviceId = arg1;
109                         restart = true;
110                     }
111                     break;
112                 case WHAT_SET_CHANNEL_COUNT:
113                     if (channelCount != arg1) {
114                         channelCount = arg1;
115                         restart = true;
116                     }
117                     break;
118             }
119             if (restart) {
120                 int result1 = PlaybackEngine.stopEngine();
121                 int result2 = PlaybackEngine.startEngine(audioApi, deviceId, channelCount);
122                 result = (result2 != 0) ? result2 : result1;
123             }
124             if (result != 0) {
125                 Log.e(TAG, "audio error " + result);
126                 showToast("Error in audio =" + result);
127             }
128         }
129     }
130 
131     /*
132      * Hook to user control to start / stop audio playback:
133      *    touch-down: start, and keeps on playing
134      *    touch-up: stop.
135      * simply pass the events to native side.
136      */
137     @Override
onTouchEvent(MotionEvent event)138     public boolean onTouchEvent(MotionEvent event) {
139         switch (event.getAction()) {
140             case (MotionEvent.ACTION_DOWN):
141                 PlaybackEngine.setToneOn(true);
142                 break;
143             case (MotionEvent.ACTION_UP):
144                 PlaybackEngine.setToneOn(false);
145                 break;
146         }
147         return super.onTouchEvent(event);
148     }
149 
150     @Override
onCreate(Bundle savedInstanceState)151     protected void onCreate(Bundle savedInstanceState) {
152         super.onCreate(savedInstanceState);
153         setContentView(R.layout.activity_main);
154         mLatencyText = findViewById(R.id.latencyText);
155         setupAudioApiSpinner();
156         setupPlaybackDeviceSpinner();
157         setupChannelCountSpinner();
158         setupBufferSizeSpinner();
159     }
160 
161     /*
162     * Creating engine in onResume() and destroying in onPause() so the stream retains exclusive
163     * mode only while in focus. This allows other apps to reclaim exclusive stream mode.
164     */
165     @Override
onResume()166     protected void onResume() {
167         super.onResume();
168         PlaybackEngine.setDefaultStreamValues(this);
169         setupLatencyUpdater();
170 
171         // Return the spinner states to their default value
172         mChannelCountSpinner.setSelection(CHANNEL_COUNT_DEFAULT_OPTION_INDEX);
173         mPlaybackDeviceSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX);
174         mBufferSizeSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX);
175         if (PlaybackEngine.isAAudioRecommended()) {
176             mAudioApiSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX);
177         } else {
178             mAudioApiSpinner.setSelection(OBOE_API_OPENSL_ES);
179             mAudioApiSpinner.setEnabled(false);
180         }
181 
182         startAudioAsync();
183     }
184 
185     @Override
onPause()186     protected void onPause() {
187         if (mLatencyUpdater != null) mLatencyUpdater.cancel();
188         stopAudioAsync();
189 
190         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
191             clearCommunicationDevice();
192         } else {
193             if (mScoStarted) {
194                 stopBluetoothSco();
195                 mScoStarted = false;
196             }
197         }
198         super.onPause();
199     }
200 
setupChannelCountSpinner()201     private void setupChannelCountSpinner() {
202         mChannelCountSpinner = findViewById(R.id.channelCountSpinner);
203 
204         ArrayAdapter<Integer> channelCountAdapter = new ArrayAdapter<>(this, R.layout.channel_counts_spinner, CHANNEL_COUNT_OPTIONS);
205         mChannelCountSpinner.setAdapter(channelCountAdapter);
206         mChannelCountSpinner.setSelection(CHANNEL_COUNT_DEFAULT_OPTION_INDEX);
207 
208         mChannelCountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
209             @Override
210             public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
211                 setChannelCountAsync(CHANNEL_COUNT_OPTIONS[mChannelCountSpinner.getSelectedItemPosition()]);
212             }
213 
214             @Override
215             public void onNothingSelected(AdapterView<?> adapterView) {
216             }
217         });
218     }
219 
setupBufferSizeSpinner()220     private void setupBufferSizeSpinner() {
221         mBufferSizeSpinner = findViewById(R.id.bufferSizeSpinner);
222         mBufferSizeSpinner.setAdapter(new SimpleAdapter(
223                 this,
224                 createBufferSizeOptionsList(), // list of buffer size options
225                 R.layout.buffer_sizes_spinner, // the xml layout
226                 new String[]{getString(R.string.buffer_size_description_key)}, // field to display
227                 new int[]{R.id.bufferSizeOption} // View to show field in
228         ));
229 
230         mBufferSizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
231             @Override
232             public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
233                 PlaybackEngine.setBufferSizeInBursts(getBufferSizeInBursts());
234             }
235 
236             @Override
237             public void onNothingSelected(AdapterView<?> adapterView) {
238             }
239         });
240     }
241 
setupPlaybackDeviceSpinner()242     private void setupPlaybackDeviceSpinner() {
243         mPlaybackDeviceSpinner = findViewById(R.id.playbackDevicesSpinner);
244 
245         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
246             mPlaybackDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS);
247             mPlaybackDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
248                 @Override
249                 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
250                     // To use Bluetooth SCO, setCommunicationDevice() or startBluetoothSco() must
251                     // be called. The AudioManager.startBluetoothSco() is deprecated in Android T.
252                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
253                         if (isScoDevice(getPlaybackDeviceId())){
254                             setCommunicationDevice(getPlaybackDeviceId());
255                         } else {
256                             clearCommunicationDevice();
257                         }
258                     } else {
259                         // Start Bluetooth SCO if needed.
260                         if (isScoDevice(getPlaybackDeviceId()) && !mScoStarted) {
261                             startBluetoothSco();
262                             mScoStarted = true;
263                         } else if (!isScoDevice(getPlaybackDeviceId()) && mScoStarted) {
264                             stopBluetoothSco();
265                             mScoStarted = false;
266                         }
267                     }
268                     setAudioDeviceIdAsync(getPlaybackDeviceId());
269                 }
270 
271                 @Override
272                 public void onNothingSelected(AdapterView<?> adapterView) {
273                 }
274             });
275         } else {
276             mPlaybackDeviceSpinner.setEnabled(false);
277         }
278     }
279 
setupAudioApiSpinner()280     private void setupAudioApiSpinner() {
281         mAudioApiSpinner = findViewById(R.id.audioApiSpinner);
282         mAudioApiSpinner.setAdapter(new SimpleAdapter(
283                 this,
284                 createAudioApisOptionsList(),
285                 R.layout.audio_apis_spinner, // the xml layout
286                 new String[]{getString(R.string.audio_api_description_key)}, // field to display
287                 new int[]{R.id.audioApiOption} // View to show field in
288         ));
289 
290         mAudioApiSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
291             @Override
292             public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
293                 setAudioApiAsync(i);
294                 if (i == OBOE_API_OPENSL_ES) {
295                     mPlaybackDeviceSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX);
296                     mPlaybackDeviceSpinner.setEnabled(false);
297                 } else {
298                     mPlaybackDeviceSpinner.setEnabled(true);
299                 }
300             }
301 
302             @Override
303             public void onNothingSelected(AdapterView<?> adapterView) {
304             }
305         });
306     }
307 
getPlaybackDeviceId()308     private int getPlaybackDeviceId() {
309         return ((AudioDeviceListEntry) mPlaybackDeviceSpinner.getSelectedItem()).getId();
310     }
311 
getBufferSizeInBursts()312     private int getBufferSizeInBursts() {
313         @SuppressWarnings("unchecked")
314         HashMap<String, String> selectedOption = (HashMap<String, String>)
315                 mBufferSizeSpinner.getSelectedItem();
316 
317         String valueKey = getString(R.string.buffer_size_value_key);
318 
319         // parseInt will throw a NumberFormatException if the string doesn't contain a valid integer
320         // representation. We don't need to worry about this because the values are derived from
321         // the BUFFER_SIZE_OPTIONS int array.
322         return Integer.parseInt(Objects.requireNonNull(selectedOption.get(valueKey)));
323     }
324 
setupLatencyUpdater()325     private void setupLatencyUpdater() {
326         //Update the latency every 1s
327         TimerTask latencyUpdateTask = new TimerTask() {
328             @Override
329             public void run() {
330                 final String latencyStr;
331                 if (PlaybackEngine.isLatencyDetectionSupported()) {
332                     double latency = PlaybackEngine.getCurrentOutputLatencyMillis();
333                     if (latency >= 0) {
334                         latencyStr = String.format(Locale.getDefault(), "%.2fms", latency);
335                     } else {
336                         latencyStr = "Unknown";
337                     }
338                 } else {
339                     latencyStr = getString(R.string.only_supported_on_api_26);
340                 }
341 
342                 runOnUiThread(() -> mLatencyText.setText(getString(R.string.latency, latencyStr)));
343             }
344         };
345         mLatencyUpdater = new Timer();
346         mLatencyUpdater.schedule(latencyUpdateTask, 0, UPDATE_LATENCY_EVERY_MILLIS);
347     }
348 
349     /**
350      * Creates a list of buffer size options which can be used to populate a SimpleAdapter.
351      * Each option has a description and a value. The description is always equal to the value,
352      * except when the value is zero as this indicates that the buffer size should be set
353      * automatically by the audio engine
354      *
355      * @return list of buffer size options
356      */
createBufferSizeOptionsList()357     private List<HashMap<String, String>> createBufferSizeOptionsList() {
358 
359         ArrayList<HashMap<String, String>> bufferSizeOptions = new ArrayList<>();
360 
361         for (int i : BUFFER_SIZE_OPTIONS) {
362             HashMap<String, String> option = new HashMap<>();
363             String strValue = String.valueOf(i);
364             String description = (i == 0) ? getString(R.string.automatic) : strValue;
365             option.put(getString(R.string.buffer_size_description_key), description);
366             option.put(getString(R.string.buffer_size_value_key), strValue);
367 
368             bufferSizeOptions.add(option);
369         }
370 
371         return bufferSizeOptions;
372     }
373 
createAudioApisOptionsList()374     private List<HashMap<String, String>> createAudioApisOptionsList() {
375 
376         ArrayList<HashMap<String, String>> audioApiOptions = new ArrayList<>();
377 
378         for (int i = 0; i < AUDIO_API_OPTIONS.length; i++) {
379             HashMap<String, String> option = new HashMap<>();
380             option.put(getString(R.string.buffer_size_description_key), AUDIO_API_OPTIONS[i]);
381             option.put(getString(R.string.buffer_size_value_key), String.valueOf(i));
382             audioApiOptions.add(option);
383         }
384         return audioApiOptions;
385     }
386 
showToast(final String message)387     protected void showToast(final String message) {
388         runOnUiThread(() -> Toast.makeText(MainActivity.this,
389                 message,
390                 Toast.LENGTH_SHORT).show());
391     }
392 
393     @RequiresApi(api = Build.VERSION_CODES.S)
setCommunicationDevice(int deviceId)394     private void setCommunicationDevice(int deviceId) {
395         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
396         final AudioDeviceInfo[] devices;
397         devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
398         for (AudioDeviceInfo device : devices) {
399             if (device.getId() == deviceId) {
400                 audioManager.setCommunicationDevice(device);
401                 return;
402             }
403         }
404     }
405 
406     @RequiresApi(api = Build.VERSION_CODES.S)
clearCommunicationDevice()407     private void clearCommunicationDevice() {
408         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
409         audioManager.clearCommunicationDevice();
410     }
411 
412     @RequiresApi(api = Build.VERSION_CODES.M)
isScoDevice(int deviceId)413     private boolean isScoDevice(int deviceId) {
414         if (deviceId == 0) return false; // Unspecified
415         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
416         final AudioDeviceInfo[] devices;
417         devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
418         for (AudioDeviceInfo device : devices) {
419             if (device.getId() == deviceId) {
420                 return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
421             }
422         }
423         return false;
424     }
425 
startBluetoothSco()426     private void startBluetoothSco() {
427         AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
428         myAudioMgr.startBluetoothSco();
429     }
430 
stopBluetoothSco()431     private void stopBluetoothSco() {
432         AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
433         myAudioMgr.stopBluetoothSco();
434     }
435 
436 
startAudioAsync()437     void startAudioAsync() {
438         mRunner.sendMessage(WHAT_START);
439     }
440 
stopAudioAsync()441     void stopAudioAsync() {
442         mRunner.sendMessage(WHAT_STOP);
443     }
444 
setAudioApiAsync(int audioApi)445     void setAudioApiAsync(int audioApi){
446         mRunner.sendMessage(WHAT_SET_AUDIO_API, audioApi);
447     }
448 
setAudioDeviceIdAsync(int deviceId)449     void setAudioDeviceIdAsync(int deviceId){
450         mRunner.sendMessage(WHAT_SET_DEVICE_ID, deviceId);
451     }
452 
setChannelCountAsync(int channelCount)453     void setChannelCountAsync(int channelCount) {
454         mRunner.sendMessage(WHAT_SET_CHANNEL_COUNT, channelCount);
455     }
456 }
457