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