blob: b0ca0bb74548d7499c9de6ca29dbfb8f6932f1af [file] [log] [blame]
/*
* 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");
}
}