/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.example.android.nativemididemo; import android.app.Activity; import android.content.Context; import android.media.midi.MidiDevice; import android.media.midi.MidiDeviceInfo; import android.media.midi.MidiManager; import android.media.midi.MidiOutputPort; import android.media.midi.MidiReceiver; import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.WindowManager; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; import java.io.IOException; public class NativeMidi extends Activity { private TextView mCallbackStatusTextView; private TextView mJavaMidiStatusTextView; private TextView mMessagesTextView; private RadioGroup mMidiDevicesRadioGroup; private Handler mTimerHandler = new Handler(); private boolean mAudioWorks; private final int mMinFramesPerBuffer = 32; // See {min|max}PlaySamples in nativemidi-jni.cpp private final int mMaxFramesPerBuffer = 1000; private int mFramesPerBuffer; private TouchableScrollView mMessagesContainer; private MidiManager mMidiManager; private MidiOutputPortSelector mActivePortSelector; private Runnable mTimerRunnable = new Runnable() { private long mLastTime; private long mLastPlaybackCounter; private int mLastCallbackRate; private long mLastUntouchedTime; @Override public void run() { final long checkIntervalMs = 1000; long currentTime = System.currentTimeMillis(); long currentPlaybackCounter = getPlaybackCounter(); if (currentTime - mLastTime >= checkIntervalMs) { int callbackRate = Math.round( (float)(currentPlaybackCounter - mLastPlaybackCounter) / ((float)(currentTime - mLastTime) / (float)1000)); if (mLastCallbackRate != callbackRate) { mCallbackStatusTextView.setText( "CB: " + callbackRate + " Hz"); mLastCallbackRate = callbackRate; } mLastTime = currentTime; mLastPlaybackCounter = currentPlaybackCounter; } String[] newMessages = getRecentMessages(); if (newMessages != null) { for (String message : newMessages) { mMessagesTextView.append(message); mMessagesTextView.append("\n"); } if (!mMessagesContainer.isTouched) { if (mLastUntouchedTime == 0) mLastUntouchedTime = currentTime; if (currentTime - mLastUntouchedTime > 3000) { mMessagesContainer.fullScroll(View.FOCUS_DOWN); } } else { mLastUntouchedTime = 0; } } mTimerHandler.postDelayed(this, checkIntervalMs / 4); } }; private void addMessage(final String message) { mTimerHandler.postDelayed(new Runnable() { @Override public void run() { mMessagesTextView.append(message); } }, 0); } private class MidiOutputPortSelector implements View.OnClickListener { private final MidiDeviceInfo mDeviceInfo; private final int mPortNumber; private MidiDevice mDevice; private MidiOutputPort mOutputPort; MidiOutputPortSelector() { mDeviceInfo = null; mPortNumber = -1; } MidiOutputPortSelector(MidiDeviceInfo info, int portNumber) { mDeviceInfo = info; mPortNumber = portNumber; } MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; } @Override public void onClick(View v) { if (mActivePortSelector != null) { mActivePortSelector.close(); mActivePortSelector = null; } if (mDeviceInfo == null) { mActivePortSelector = this; return; } mMidiManager.openDevice(mDeviceInfo, new MidiManager.OnDeviceOpenedListener() { @Override public void onDeviceOpened(MidiDevice device) { if (device == null) { addMessage("! Failed to open MIDI device !\n"); } else { mDevice = device; try { mDevice.mirrorToNative(); startReadingMidi(mDevice.getInfo().getId(), mPortNumber); } catch (IOException e) { addMessage("! Failed to mirror to native !\n" + e.getMessage() + "\n"); } mActivePortSelector = MidiOutputPortSelector.this; mOutputPort = device.openOutputPort(mPortNumber); mOutputPort.connect(mMidiReceiver); } } }, null); } void closePortOnly() { stopReadingMidi(); } void close() { closePortOnly(); try { if (mOutputPort != null) { mOutputPort.close(); } } catch (IOException e) { mMessagesTextView.append("! Port close error: " + e + "\n"); } finally { mOutputPort = null; } try { if (mDevice != null) { mDevice.close(); } } catch (IOException e) { mMessagesTextView.append("! Device close error: " + e + "\n"); } finally { mDevice = null; } } } private MidiManager.DeviceCallback mMidiDeviceCallback = new MidiManager.DeviceCallback() { @Override public void onDeviceAdded(MidiDeviceInfo info) { Bundle deviceProps = info.getProperties(); String deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_NAME); if (deviceName == null) { deviceName = deviceProps.getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); } for (MidiDeviceInfo.PortInfo port : info.getPorts()) { if (port.getType() != MidiDeviceInfo.PortInfo.TYPE_OUTPUT) continue; String portName = port.getName(); int portNumber = port.getPortNumber(); if (portName.length() == 0) portName = "[" + portNumber + "]"; portName += "@" + deviceName; RadioButton outputDevice = new RadioButton(NativeMidi.this); outputDevice.setText(portName); outputDevice.setTag(info); outputDevice.setOnClickListener(new MidiOutputPortSelector(info, portNumber)); mMidiDevicesRadioGroup.addView(outputDevice); } NativeMidi.this.updateKeepScreenOn(); } @Override public void onDeviceRemoved(MidiDeviceInfo info) { if (mActivePortSelector != null && info.equals(mActivePortSelector.getDeviceInfo())) { mActivePortSelector.close(); mActivePortSelector = null; } int removeButtonStart = -1, removeButtonCount = 0; final int buttonCount = mMidiDevicesRadioGroup.getChildCount(); boolean checked = false; for (int i = 0; i < buttonCount; ++i) { RadioButton button = (RadioButton) mMidiDevicesRadioGroup.getChildAt(i); if (!info.equals(button.getTag())) continue; if (removeButtonStart == -1) removeButtonStart = i; ++removeButtonCount; if (button.isChecked()) checked = true; } if (removeButtonStart != -1) { mMidiDevicesRadioGroup.removeViews(removeButtonStart, removeButtonCount); if (checked) { mMidiDevicesRadioGroup.check(R.id.device_none); } } NativeMidi.this.updateKeepScreenOn(); } }; private class JavaMidiReceiver extends MidiReceiver implements Runnable { @Override public void onSend(byte[] data, int offset, int count, long timestamp) throws IOException { mTimerHandler.removeCallbacks(this); mTimerHandler.postDelayed(new Runnable() { @Override public void run() { mJavaMidiStatusTextView.setText("Java: MSG"); } }, 0); mTimerHandler.postDelayed(this, 100); } @Override public void run() { mJavaMidiStatusTextView.setText("Java: ---"); } } private JavaMidiReceiver mMidiReceiver = new JavaMidiReceiver(); private void updateKeepScreenOn() { if (mMidiDevicesRadioGroup.getChildCount() > 1) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mCallbackStatusTextView = findViewById(R.id.callback_status); mJavaMidiStatusTextView = findViewById(R.id.java_midi_status); mMessagesTextView = findViewById(R.id.messages); mMessagesContainer = findViewById(R.id.messages_scroll); mMidiDevicesRadioGroup = findViewById(R.id.devices); RadioButton deviceNone = findViewById(R.id.device_none); deviceNone.setOnClickListener(new MidiOutputPortSelector()); AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); if (sampleRate == null) sampleRate = "48000"; String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); if (framesPerBuffer == null) framesPerBuffer = Integer.toString(mMaxFramesPerBuffer); mFramesPerBuffer = Integer.parseInt(framesPerBuffer); String audioInitResult = initAudio(Integer.parseInt(sampleRate), mFramesPerBuffer); mMessagesTextView.append("Open SL ES init: " + audioInitResult + "\n"); if (audioInitResult.startsWith("Success")) { mAudioWorks = true; mTimerHandler.postDelayed(mTimerRunnable, 0); mTimerHandler.postDelayed(mMidiReceiver, 0); } mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); mMidiManager.registerDeviceCallback(mMidiDeviceCallback, new Handler()); for (MidiDeviceInfo info : mMidiManager.getDevices()) { mMidiDeviceCallback.onDeviceAdded(info); } } @Override public void onPause() { super.onPause(); if (mAudioWorks) { mTimerHandler.removeCallbacks(mTimerRunnable); pauseAudio(); } } @Override public void onResume() { super.onResume(); if (mAudioWorks) { mTimerHandler.postDelayed(mTimerRunnable, 0); resumeAudio(); } } @Override protected void onDestroy() { if (mActivePortSelector != null) { mActivePortSelector.close(); mActivePortSelector = null; } shutdownAudio(); super.onDestroy(); } public void onClearMessages(View v) { mMessagesTextView.setText(""); } public void onClosePort(View v) { if (mActivePortSelector != null) { mActivePortSelector.closePortOnly(); } } private native String initAudio(int sampleRate, int playSamples); private native void pauseAudio(); private native void resumeAudio(); private native void shutdownAudio(); private native long getPlaybackCounter(); private native String[] getRecentMessages(); private native void startReadingMidi(int deviceId, int portNumber); private native void stopReadingMidi(); static { System.loadLibrary("nativemidi_jni"); } }