1 /* 2 * Copyright (C) 2016 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 18 package com.example.android.nativemididemo; 19 20 import android.app.Activity; 21 import android.content.Context; 22 import android.media.midi.MidiDevice; 23 import android.media.midi.MidiDeviceInfo; 24 import android.media.midi.MidiManager; 25 import android.media.midi.MidiOutputPort; 26 import android.media.midi.MidiReceiver; 27 import android.media.AudioManager; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.view.View; 31 import android.view.WindowManager; 32 import android.widget.RadioButton; 33 import android.widget.RadioGroup; 34 import android.widget.TextView; 35 36 import java.io.IOException; 37 38 public class NativeMidi extends Activity 39 { 40 private TextView mCallbackStatusTextView; 41 private TextView mJavaMidiStatusTextView; 42 private TextView mMessagesTextView; 43 private RadioGroup mMidiDevicesRadioGroup; 44 private Handler mTimerHandler = new Handler(); 45 private boolean mAudioWorks; 46 private final int mMinFramesPerBuffer = 32; // See {min|max}PlaySamples in nativemidi-jni.cpp 47 private final int mMaxFramesPerBuffer = 1000; 48 private int mFramesPerBuffer; 49 50 private TouchableScrollView mMessagesContainer; 51 private MidiManager mMidiManager; 52 private MidiOutputPortSelector mActivePortSelector; 53 54 private Runnable mTimerRunnable = new Runnable() { 55 private long mLastTime; 56 private long mLastPlaybackCounter; 57 private int mLastCallbackRate; 58 private long mLastUntouchedTime; 59 60 @Override 61 public void run() { 62 final long checkIntervalMs = 1000; 63 long currentTime = System.currentTimeMillis(); 64 long currentPlaybackCounter = getPlaybackCounter(); 65 if (currentTime - mLastTime >= checkIntervalMs) { 66 int callbackRate = Math.round( 67 (float)(currentPlaybackCounter - mLastPlaybackCounter) / 68 ((float)(currentTime - mLastTime) / (float)1000)); 69 if (mLastCallbackRate != callbackRate) { 70 mCallbackStatusTextView.setText( 71 "CB: " + callbackRate + " Hz"); 72 mLastCallbackRate = callbackRate; 73 } 74 mLastTime = currentTime; 75 mLastPlaybackCounter = currentPlaybackCounter; 76 } 77 78 String[] newMessages = getRecentMessages(); 79 if (newMessages != null) { 80 for (String message : newMessages) { 81 mMessagesTextView.append(message); 82 mMessagesTextView.append("\n"); 83 } 84 if (!mMessagesContainer.isTouched) { 85 if (mLastUntouchedTime == 0) mLastUntouchedTime = currentTime; 86 if (currentTime - mLastUntouchedTime > 3000) { 87 mMessagesContainer.fullScroll(View.FOCUS_DOWN); 88 } 89 } else { 90 mLastUntouchedTime = 0; 91 } 92 } 93 94 mTimerHandler.postDelayed(this, checkIntervalMs / 4); 95 } 96 }; 97 98 private void addMessage(final String message) { 99 mTimerHandler.postDelayed(new Runnable() { 100 @Override 101 public void run() { 102 mMessagesTextView.append(message); 103 } 104 }, 0); 105 } 106 107 private class MidiOutputPortSelector implements View.OnClickListener { 108 private final MidiDeviceInfo mDeviceInfo; 109 private final int mPortNumber; 110 private MidiDevice mDevice; 111 private MidiOutputPort mOutputPort; 112 113 MidiOutputPortSelector() { 114 mDeviceInfo = null; 115 mPortNumber = -1; 116 } 117 118 MidiOutputPortSelector(MidiDeviceInfo info, int portNumber) { 119 mDeviceInfo = info; 120 mPortNumber = portNumber; 121 } 122 123 MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; } 124 125 @Override 126 public void onClick(View v) { 127 if (mActivePortSelector != null) { 128 mActivePortSelector.close(); 129 mActivePortSelector = null; 130 } 131 if (mDeviceInfo == null) { 132 mActivePortSelector = this; 133 return; 134 } 135 mMidiManager.openDevice(mDeviceInfo, new MidiManager.OnDeviceOpenedListener() { 136 @Override 137 public void onDeviceOpened(MidiDevice device) { 138 if (device == null) { 139 addMessage("! Failed to open MIDI device !\n"); 140 } else { 141 mDevice = device; 142 try { 143 mDevice.mirrorToNative(); 144 startReadingMidi(mDevice.getInfo().getId(), mPortNumber); 145 } catch (IOException e) { 146 addMessage("! Failed to mirror to native !\n" + e.getMessage() + "\n"); 147 } 148 149 mActivePortSelector = MidiOutputPortSelector.this; 150 151 mOutputPort = device.openOutputPort(mPortNumber); 152 mOutputPort.connect(mMidiReceiver); 153 } 154 } 155 }, null); 156 } 157 158 void closePortOnly() { 159 stopReadingMidi(); 160 } 161 162 void close() { 163 closePortOnly(); 164 try { 165 if (mOutputPort != null) { 166 mOutputPort.close(); 167 } 168 } catch (IOException e) { 169 mMessagesTextView.append("! Port close error: " + e + "\n"); 170 } finally { 171 mOutputPort = null; 172 } 173 try { 174 if (mDevice != null) { 175 mDevice.close(); 176 } 177 } catch (IOException e) { 178 mMessagesTextView.append("! Device close error: " + e + "\n"); 179 } finally { 180 mDevice = null; 181 } 182 } 183 } 184 185 private MidiManager.DeviceCallback mMidiDeviceCallback = new MidiManager.DeviceCallback() { 186 @Override 187 public void onDeviceAdded(MidiDeviceInfo info) { 188 Bundle deviceProps = info.getProperties(); 189 String deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_NAME); 190 if (deviceName == null) { 191 deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); 192 } 193 194 for (MidiDeviceInfo.PortInfo port : info.getPorts()) { 195 if (port.getType() != MidiDeviceInfo.PortInfo.TYPE_OUTPUT) continue; 196 String portName = port.getName(); 197 int portNumber = port.getPortNumber(); 198 if (portName.length() == 0) portName = "[" + portNumber + "]"; 199 portName += "@" + deviceName; 200 RadioButton outputDevice = new RadioButton(NativeMidi.this); 201 outputDevice.setText(portName); 202 outputDevice.setTag(info); 203 outputDevice.setOnClickListener(new MidiOutputPortSelector(info, portNumber)); 204 mMidiDevicesRadioGroup.addView(outputDevice); 205 } 206 207 NativeMidi.this.updateKeepScreenOn(); 208 } 209 210 @Override 211 public void onDeviceRemoved(MidiDeviceInfo info) { 212 if (mActivePortSelector != null && info.equals(mActivePortSelector.getDeviceInfo())) { 213 mActivePortSelector.close(); 214 mActivePortSelector = null; 215 } 216 int removeButtonStart = -1, removeButtonCount = 0; 217 final int buttonCount = mMidiDevicesRadioGroup.getChildCount(); 218 boolean checked = false; 219 for (int i = 0; i < buttonCount; ++i) { 220 RadioButton button = (RadioButton) mMidiDevicesRadioGroup.getChildAt(i); 221 if (!info.equals(button.getTag())) continue; 222 if (removeButtonStart == -1) removeButtonStart = i; 223 ++removeButtonCount; 224 if (button.isChecked()) checked = true; 225 } 226 if (removeButtonStart != -1) { 227 mMidiDevicesRadioGroup.removeViews(removeButtonStart, removeButtonCount); 228 if (checked) { 229 mMidiDevicesRadioGroup.check(R.id.device_none); 230 } 231 } 232 233 NativeMidi.this.updateKeepScreenOn(); 234 } 235 }; 236 237 private class JavaMidiReceiver extends MidiReceiver implements Runnable { 238 @Override 239 public void onSend(byte[] data, int offset, 240 int count, long timestamp) throws IOException { 241 mTimerHandler.removeCallbacks(this); 242 mTimerHandler.postDelayed(new Runnable() { 243 @Override 244 public void run() { 245 mJavaMidiStatusTextView.setText("Java: MSG"); 246 } 247 }, 0); 248 mTimerHandler.postDelayed(this, 100); 249 } 250 251 @Override 252 public void run() { 253 mJavaMidiStatusTextView.setText("Java: ---"); 254 } 255 } 256 257 private JavaMidiReceiver mMidiReceiver = new JavaMidiReceiver(); 258 259 private void updateKeepScreenOn() { 260 if (mMidiDevicesRadioGroup.getChildCount() > 1) { 261 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 262 } else { 263 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 264 } 265 } 266 267 @Override 268 public void onCreate(Bundle savedInstanceState) { 269 super.onCreate(savedInstanceState); 270 setContentView(R.layout.main); 271 272 mCallbackStatusTextView = findViewById(R.id.callback_status); 273 mJavaMidiStatusTextView = findViewById(R.id.java_midi_status); 274 mMessagesTextView = findViewById(R.id.messages); 275 mMessagesContainer = findViewById(R.id.messages_scroll); 276 mMidiDevicesRadioGroup = findViewById(R.id.devices); 277 RadioButton deviceNone = findViewById(R.id.device_none); 278 deviceNone.setOnClickListener(new MidiOutputPortSelector()); 279 280 AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 281 String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); 282 if (sampleRate == null) sampleRate = "48000"; 283 String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); 284 if (framesPerBuffer == null) framesPerBuffer = Integer.toString(mMaxFramesPerBuffer); 285 mFramesPerBuffer = Integer.parseInt(framesPerBuffer); 286 String audioInitResult = initAudio(Integer.parseInt(sampleRate), mFramesPerBuffer); 287 mMessagesTextView.append("Open SL ES init: " + audioInitResult + "\n"); 288 289 if (audioInitResult.startsWith("Success")) { 290 mAudioWorks = true; 291 mTimerHandler.postDelayed(mTimerRunnable, 0); 292 mTimerHandler.postDelayed(mMidiReceiver, 0); 293 } 294 295 mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); 296 mMidiManager.registerDeviceCallback(mMidiDeviceCallback, new Handler()); 297 for (MidiDeviceInfo info : mMidiManager.getDevices()) { 298 mMidiDeviceCallback.onDeviceAdded(info); 299 } 300 } 301 302 @Override 303 public void onPause() { 304 super.onPause(); 305 if (mAudioWorks) { 306 mTimerHandler.removeCallbacks(mTimerRunnable); 307 pauseAudio(); 308 } 309 } 310 311 @Override 312 public void onResume() { 313 super.onResume(); 314 if (mAudioWorks) { 315 mTimerHandler.postDelayed(mTimerRunnable, 0); 316 resumeAudio(); 317 } 318 } 319 320 @Override 321 protected void onDestroy() { 322 if (mActivePortSelector != null) { 323 mActivePortSelector.close(); 324 mActivePortSelector = null; 325 } 326 shutdownAudio(); 327 super.onDestroy(); 328 } 329 330 public void onClearMessages(View v) { 331 mMessagesTextView.setText(""); 332 } 333 334 public void onClosePort(View v) { 335 if (mActivePortSelector != null) { 336 mActivePortSelector.closePortOnly(); 337 } 338 } 339 340 private native String initAudio(int sampleRate, int playSamples); 341 private native void pauseAudio(); 342 private native void resumeAudio(); 343 private native void shutdownAudio(); 344 345 private native long getPlaybackCounter(); 346 private native String[] getRecentMessages(); 347 348 private native void startReadingMidi(int deviceId, int portNumber); 349 private native void stopReadingMidi(); 350 351 static { 352 System.loadLibrary("nativemidi_jni"); 353 } 354 } 355