1 /* 2 * Copyright 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.sample.oboe.manualtest; 18 19 import android.Manifest; 20 import android.content.pm.PackageManager; 21 import android.media.midi.MidiDevice; 22 import android.media.midi.MidiDeviceInfo; 23 import android.media.midi.MidiInputPort; 24 import android.media.midi.MidiManager; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.util.Log; 29 import android.view.Menu; 30 import android.view.MenuItem; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.widget.TextView; 34 import android.widget.Toast; 35 36 import com.mobileer.miditools.MidiOutputPortConnectionSelector; 37 import com.mobileer.miditools.MidiPortConnector; 38 import com.mobileer.miditools.MidiTools; 39 40 import java.io.IOException; 41 42 import static com.google.sample.oboe.manualtest.AudioMidiTester.TestListener; 43 import static com.google.sample.oboe.manualtest.AudioMidiTester.TestResult; 44 45 public class TapToToneActivity extends TestOutputActivityBase { 46 private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 1234; 47 private TextView mResultView; 48 private MidiManager mMidiManager; 49 private MidiInputPort mInputPort; 50 51 protected AudioMidiTester mAudioMidiTester; 52 53 private MidiOutputPortConnectionSelector mPortSelector; 54 private MyTestListener mTestListener = new MyTestListener(); 55 private WaveformView mWaveformView; 56 // Stats for latency 57 private int mMeasurementCount; 58 private int mLatencySumSamples; 59 private int mLatencyMin; 60 private int mLatencyMax; 61 62 @Override inflateActivity()63 protected void inflateActivity() { 64 setContentView(R.layout.activity_tap_to_tone); 65 } 66 67 @Override onCreate(Bundle savedInstanceState)68 protected void onCreate(Bundle savedInstanceState) { 69 super.onCreate(savedInstanceState); 70 71 mAudioOutTester = addAudioOutputTester(); 72 73 mResultView = (TextView) findViewById(R.id.resultView); 74 75 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI)) { 76 setupMidi(); 77 } else { 78 Toast.makeText(TapToToneActivity.this, 79 "MIDI not supported!", Toast.LENGTH_LONG) 80 .show(); 81 } 82 83 mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); 84 85 // Start a blip test when the waveform view is tapped. 86 mWaveformView.setOnTouchListener(new View.OnTouchListener() { 87 @Override 88 public boolean onTouch(View view, MotionEvent event) { 89 int action = event.getActionMasked(); 90 switch (action) { 91 case MotionEvent.ACTION_DOWN: 92 case MotionEvent.ACTION_POINTER_DOWN: 93 mAudioMidiTester.trigger(); 94 break; 95 case MotionEvent.ACTION_MOVE: 96 break; 97 case MotionEvent.ACTION_UP: 98 case MotionEvent.ACTION_POINTER_UP: 99 break; 100 } 101 // Must return true or we do not get the ACTION_MOVE and 102 // ACTION_UP events. 103 return true; 104 } 105 }); 106 107 updateEnabledWidgets(); 108 } 109 110 @Override getActivityType()111 int getActivityType() { 112 return ACTIVITY_TAP_TO_TONE; 113 } 114 115 @Override onDestroy()116 protected void onDestroy() { 117 mAudioMidiTester.removeTestListener(mTestListener); 118 closeMidiResources(); 119 super.onDestroy(); 120 } 121 setupMidi()122 private void setupMidi() { 123 // Setup MIDI 124 mMidiManager = (MidiManager) getSystemService(MIDI_SERVICE); 125 MidiDeviceInfo[] infos = mMidiManager.getDevices(); 126 127 // Open the port now so that the AudioMidiTester gets created. 128 for (MidiDeviceInfo info : infos) { 129 Bundle properties = info.getProperties(); 130 String product = properties 131 .getString(MidiDeviceInfo.PROPERTY_PRODUCT); 132 133 Log.i(TAG, "product = " + product); 134 if ("AudioLatencyTester".equals(product)) { 135 openPort(info); 136 break; 137 } 138 } 139 140 } 141 142 // These should only be set after mAudioMidiTester is set. setSpinnerListeners()143 private void setSpinnerListeners() { 144 MidiDeviceInfo synthInfo = MidiTools.findDevice(mMidiManager, "AndroidTest", 145 "AudioLatencyTester"); 146 Log.i(TAG, "found tester virtual device info: " + synthInfo); 147 int portIndex = 0; 148 mPortSelector = new MidiOutputPortConnectionSelector(mMidiManager, this, 149 R.id.spinner_synth_sender, synthInfo, portIndex); 150 mPortSelector.setConnectedListener(new MyPortsConnectedListener()); 151 152 } 153 154 private class MyTestListener implements TestListener { 155 @Override onTestFinished(final TestResult result)156 public void onTestFinished(final TestResult result) { 157 runOnUiThread(new Runnable() { 158 public void run() { 159 showTestResults(result); 160 } 161 }); 162 } 163 164 @Override onNoteOn(final int pitch)165 public void onNoteOn(final int pitch) { 166 runOnUiThread(new Runnable() { 167 public void run() { 168 mStreamContexts.get(0).configurationView.setStatusText("MIDI pitch = " + pitch); 169 } 170 }); 171 } 172 } 173 174 // Runs on UI thread. showTestResults(TestResult result)175 private void showTestResults(TestResult result) { 176 String text; 177 int previous = 0; 178 if (result == null) { 179 text = ""; 180 mWaveformView.clearSampleData(); 181 } else { 182 if (result.events.length < 2) { 183 text = "Not enough edges. Use fingernail.\n"; 184 mWaveformView.setCursorData(null); 185 } else if (result.events.length > 2) { 186 text = "Too many edges.\n"; 187 mWaveformView.setCursorData(null); 188 } else { 189 int[] cursors = new int[2]; 190 cursors[0] = result.events[0].sampleIndex; 191 cursors[1] = result.events[1].sampleIndex; 192 int latencySamples = cursors[1] - cursors[0]; 193 mLatencySumSamples += latencySamples; 194 mMeasurementCount++; 195 196 int latencyMillis = 1000 * latencySamples / result.frameRate; 197 if (mLatencyMin > latencyMillis) { 198 mLatencyMin = latencyMillis; 199 } 200 if (mLatencyMax < latencyMillis) { 201 mLatencyMax = latencyMillis; 202 } 203 204 text = String.format("latency = %3d msec\n", latencyMillis); 205 mWaveformView.setCursorData(cursors); 206 } 207 mWaveformView.setSampleData(result.filtered); 208 } 209 210 if (mMeasurementCount > 0) { 211 int averageLatencySamples = mLatencySumSamples / mMeasurementCount; 212 int averageLatencyMillis = 1000 * averageLatencySamples / result.frameRate; 213 final String plural = (mMeasurementCount == 1) ? "test" : "tests"; 214 text = text + String.format("min = %3d, avg = %3d, max = %3d, %d %s", 215 mLatencyMin, averageLatencyMillis, mLatencyMax, mMeasurementCount, plural); 216 } 217 final String postText = text; 218 mWaveformView.post(new Runnable() { 219 public void run() { 220 mResultView.setText(postText); 221 mWaveformView.postInvalidate(); 222 } 223 }); 224 } 225 openPort(final MidiDeviceInfo info)226 private void openPort(final MidiDeviceInfo info) { 227 mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { 228 @Override 229 public void onDeviceOpened(MidiDevice device) { 230 if (device == null) { 231 Log.e(TAG, "could not open device " + info); 232 } else { 233 mInputPort = device.openInputPort(0); 234 Log.i(TAG, "opened MIDI port = " + mInputPort + " on " + info); 235 mAudioMidiTester = AudioMidiTester.getInstance(); 236 237 Log.i(TAG, "openPort() mAudioMidiTester = " + mAudioMidiTester); 238 // Now that we have created the AudioMidiTester, close the port so we can 239 // open it later. 240 try { 241 mInputPort.close(); 242 } catch (IOException e) { 243 e.printStackTrace(); 244 } 245 mAudioMidiTester.addTestListener(mTestListener); 246 247 setSpinnerListeners(); 248 } 249 } 250 }, new Handler(Looper.getMainLooper()) 251 ); 252 } 253 254 // TODO Listen to the synth server 255 // for open/close events and then disable/enable the spinner. 256 private class MyPortsConnectedListener 257 implements MidiPortConnector.OnPortsConnectedListener { 258 @Override onPortsConnected(final MidiDevice.MidiConnection connection)259 public void onPortsConnected(final MidiDevice.MidiConnection connection) { 260 Log.i(TAG, "onPortsConnected, connection = " + connection); 261 runOnUiThread(new Runnable() { 262 @Override 263 public void run() { 264 if (connection == null) { 265 Toast.makeText(TapToToneActivity.this, 266 R.string.error_port_busy, Toast.LENGTH_LONG) 267 .show(); 268 mPortSelector.clearSelection(); 269 } else { 270 Toast.makeText(TapToToneActivity.this, 271 R.string.port_open_ok, Toast.LENGTH_LONG) 272 .show(); 273 } 274 } 275 }); 276 } 277 } 278 closeMidiResources()279 private void closeMidiResources() { 280 if (mPortSelector != null) { 281 mPortSelector.close(); 282 } 283 } 284 285 @Override onCreateOptionsMenu(Menu menu)286 public boolean onCreateOptionsMenu(Menu menu) { 287 // Inflate the menu; this adds items to the action bar if it is present. 288 getMenuInflater().inflate(R.menu.menu_main, menu); 289 return true; 290 } 291 292 @Override onOptionsItemSelected(MenuItem item)293 public boolean onOptionsItemSelected(MenuItem item) { 294 // Handle action bar item clicks here. The action bar will 295 // automatically handle clicks on the Home/Up button, so long 296 // as you specify a parent activity in AndroidManifest.xml. 297 int id = item.getItemId(); 298 299 //noinspection SimplifiableIfStatement 300 if (id == R.id.action_settings) { 301 return true; 302 } 303 304 return super.onOptionsItemSelected(item); 305 } 306 hasRecordAudioPermission()307 private boolean hasRecordAudioPermission(){ 308 boolean hasPermission = (checkSelfPermission( 309 Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED); 310 Log.i(TAG, "Has RECORD_AUDIO permission? " + hasPermission); 311 return hasPermission; 312 } 313 requestRecordAudioPermission()314 private void requestRecordAudioPermission(){ 315 316 String requiredPermission = Manifest.permission.RECORD_AUDIO; 317 318 // If the user previously denied this permission then show a message explaining why 319 // this permission is needed 320 if (shouldShowRequestPermissionRationale(requiredPermission)) { 321 showErrorToast("This app needs to record audio through the microphone...."); 322 } 323 324 // request the permission. 325 requestPermissions(new String[]{requiredPermission}, 326 MY_PERMISSIONS_REQUEST_RECORD_AUDIO); 327 } 328 @Override onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)329 public void onRequestPermissionsResult(int requestCode, 330 String permissions[], int[] grantResults) { 331 // TODO 332 } 333 334 @Override startAudio()335 public void startAudio() throws IOException { 336 if (hasRecordAudioPermission()) { 337 startAudioPermitted(); 338 } else { 339 requestRecordAudioPermission(); 340 } 341 } 342 startAudioPermitted()343 private void startAudioPermitted() throws IOException { 344 super.startAudio(); 345 resetLatency(); 346 mAudioMidiTester.start(); 347 } 348 349 @Override stopAudio()350 public void stopAudio() { 351 mAudioMidiTester.stop(); 352 super.stopAudio(); 353 } 354 355 @Override pauseAudio()356 public void pauseAudio() { 357 mAudioMidiTester.stop(); 358 super.pauseAudio(); 359 } 360 361 @Override closeAudio()362 public void closeAudio() { 363 mAudioMidiTester.stop(); 364 super.closeAudio(); 365 } 366 resetLatency()367 private void resetLatency() { 368 mMeasurementCount = 0; 369 mLatencySumSamples = 0; 370 mLatencyMin = Integer.MAX_VALUE; 371 mLatencyMax = 0; 372 showTestResults(null); 373 } 374 375 } 376