Home | History | Annotate | Download | only in nativemididemo
      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