diff --git a/.gitignore b/.gitignore
index a6f8563..a97fe8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ ipch/
*.lnk
.DS_Store
Thumbs.db
+*.csproj
+MomoMidiJack.sln
+*.idea/
diff --git a/AndroidStudio/MidiAndroidPlugin/.gitignore b/AndroidStudio/MidiAndroidPlugin/.gitignore
new file mode 100644
index 0000000..39fb081
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/AndroidStudio/MidiAndroidPlugin/build.gradle b/AndroidStudio/MidiAndroidPlugin/build.gradle
new file mode 100644
index 0000000..b78a0b8
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/gradle.properties b/AndroidStudio/MidiAndroidPlugin/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/AndroidStudio/MidiAndroidPlugin/gradle/wrapper/gradle-wrapper.jar b/AndroidStudio/MidiAndroidPlugin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/AndroidStudio/MidiAndroidPlugin/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AndroidStudio/MidiAndroidPlugin/gradle/wrapper/gradle-wrapper.properties b/AndroidStudio/MidiAndroidPlugin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..8bfbcf3
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Apr 15 18:24:51 PDT 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
diff --git a/AndroidStudio/MidiAndroidPlugin/gradlew b/AndroidStudio/MidiAndroidPlugin/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/AndroidStudio/MidiAndroidPlugin/gradlew.bat b/AndroidStudio/MidiAndroidPlugin/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/.gitignore b/AndroidStudio/MidiAndroidPlugin/mididroid/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/build.gradle b/AndroidStudio/MidiAndroidPlugin/mididroid/build.gradle
new file mode 100644
index 0000000..baa8590
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/build.gradle
@@ -0,0 +1,56 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.2"
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 24
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ provided files('./libs/UnityPlayer.jar')
+}
+
+android.libraryVariants.all { variant ->
+
+ // Task names.
+ String variantName = "${variant.name.capitalize()}"; // Like 'Debug'
+ String deployTaskGroup = "plugin";
+ String deployTaskName = "deploy${variantName}PluginArchive"; // Like 'deployDebugPluginArchive'
+ String dependencyTaskName = "assemble${variantName}"; // Like 'assembleDebug'
+
+ // Source.
+ String sourceAARFolder = "${buildDir.getPath()}/outputs/aar/";
+ String sourceAARName = "${project.name}-${variant.name}.aar";
+
+ // Target.
+ String targetAssetFolder = "Assets/MidiJack/Plugins/Android";
+ String targetAARFolder = "${rootDir.getPath()}/../../${targetAssetFolder}"; // Navigate into 'Assets'
+ String targetAARName = "MidiJackPlugin.aar"; // The form you ship your plugin
+
+ // Create task.
+ task(deployTaskName, dependsOn: dependencyTaskName, type: Copy) {
+ logger.lifecycle("${variant.name.capitalize()} AAR Folder: ${sourceAARFolder}")
+ logger.lifecycle("${variant.name.capitalize()} Target AAR Folder: ${targetAARFolder}")
+ logger.lifecycle("${variant.name.capitalize()} Target AAR name: ${targetAARName}")
+
+ from(sourceAARFolder)
+ into(targetAARFolder)
+ include(sourceAARName)
+ rename(sourceAARName, targetAARName)
+ }.group = deployTaskGroup;
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/libs/UnityPlayer.jar b/AndroidStudio/MidiAndroidPlugin/mididroid/libs/UnityPlayer.jar
new file mode 100644
index 0000000..77dbcc8
Binary files /dev/null and b/AndroidStudio/MidiAndroidPlugin/mididroid/libs/UnityPlayer.jar differ
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/proguard-rules.pro b/AndroidStudio/MidiAndroidPlugin/mididroid/proguard-rules.pro
new file mode 100644
index 0000000..8c37a0a
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in E:\Android/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/AndroidManifest.xml b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..57c7513
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/EventScheduler.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/EventScheduler.java
new file mode 100644
index 0000000..37c0140
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/EventScheduler.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Store SchedulableEvents in a timestamped buffer.
+ * Events may be written in any order.
+ * Events will be read in sorted order.
+ * Events with the same timestamp will be read in the order they were added.
+ *
+ * Only one Thread can write into the buffer.
+ * And only one Thread can read from the buffer.
+ */
+public class EventScheduler {
+ private static final long NANOS_PER_MILLI = 1000000;
+
+ private final Object lock = new Object();
+ private SortedMap mEventBuffer;
+ // This does not have to be guarded. It is only set by the writing thread.
+ // If the reader sees a null right before being set then that is OK.
+ private FastEventQueue mEventPool = null;
+ private static final int MAX_POOL_SIZE = 200;
+
+ public EventScheduler() {
+ mEventBuffer = new TreeMap();
+ }
+
+ // If we keep at least one node in the list then it can be atomic
+ // and non-blocking.
+ private class FastEventQueue {
+ // One thread takes from the beginning of the list.
+ volatile SchedulableEvent mFirst;
+ // A second thread returns events to the end of the list.
+ volatile SchedulableEvent mLast;
+ volatile long mEventsAdded;
+ volatile long mEventsRemoved;
+
+ FastEventQueue(SchedulableEvent event) {
+ mFirst = event;
+ mLast = mFirst;
+ mEventsAdded = 1; // Always created with one event added. Never empty.
+ mEventsRemoved = 0; // None removed yet.
+ }
+
+ int size() {
+ return (int)(mEventsAdded - mEventsRemoved);
+ }
+
+ /**
+ * Do not call this unless there is more than one event
+ * in the list.
+ * @return first event in the list
+ */
+ public SchedulableEvent remove() {
+ // Take first event.
+ mEventsRemoved++;
+ SchedulableEvent event = mFirst;
+ mFirst = event.mNext;
+ return event;
+ }
+
+ /**
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ event.mNext = null;
+ mLast.mNext = event;
+ mLast = event;
+ mEventsAdded++;
+ }
+ }
+
+ /**
+ * Base class for events that can be stored in the EventScheduler.
+ */
+ public static class SchedulableEvent {
+ private long mTimestamp;
+ private SchedulableEvent mNext = null;
+
+ /**
+ * @param timestamp
+ */
+ public SchedulableEvent(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * @return timestamp
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * The timestamp should not be modified when the event is in the
+ * scheduling buffer.
+ */
+ public void setTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+ }
+
+ /**
+ * Get an event from the pool.
+ * Always leave at least one event in the pool.
+ * @return event or null
+ */
+ public SchedulableEvent removeEventfromPool() {
+ SchedulableEvent event = null;
+ if (mEventPool != null && (mEventPool.size() > 1)) {
+ event = mEventPool.remove();
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ public void addEventToPool(SchedulableEvent event) {
+ if (mEventPool == null) {
+ mEventPool = new FastEventQueue(event);
+ // If we already have enough items in the pool then just
+ // drop the event. This prevents unbounded memory leaks.
+ } else if (mEventPool.size() < MAX_POOL_SIZE) {
+ mEventPool.add(event);
+ }
+ }
+
+ /**
+ * Add an event to the scheduler. Events with the same time will be
+ * processed in order.
+ *
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ synchronized (lock) {
+ FastEventQueue list = mEventBuffer.get(event.getTimestamp());
+ if (list == null) {
+ long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
+ : mEventBuffer.firstKey();
+ list = new FastEventQueue(event);
+ mEventBuffer.put(event.getTimestamp(), list);
+ // If the event we added is earlier than the previous earliest
+ // event then notify any threads waiting for the next event.
+ if (event.getTimestamp() < lowestTime) {
+ lock.notify();
+ }
+ } else {
+ list.add(event);
+ }
+ }
+ }
+
+ // Caller must synchronize on lock before calling.
+ private SchedulableEvent removeNextEventLocked(long lowestTime) {
+ SchedulableEvent event;
+ FastEventQueue list = mEventBuffer.get(lowestTime);
+ // Remove list from tree if this is the last node.
+ if ((list.size() == 1)) {
+ mEventBuffer.remove(lowestTime);
+ }
+ event = list.remove();
+ return event;
+ }
+
+ /**
+ * Check to see if any scheduled events are ready to be processed.
+ *
+ * @param timestamp
+ * @return next event or null if none ready
+ */
+ public SchedulableEvent getNextEvent(long time) {
+ SchedulableEvent event = null;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for this list to be processed?
+ if (lowestTime <= time) {
+ event = removeNextEventLocked(lowestTime);
+ }
+ }
+ }
+ // Log.i(TAG, "getNextEvent: event = " + event);
+ return event;
+ }
+
+ /**
+ * Return the next available event or wait until there is an event ready to
+ * be processed. This method assumes that the timestamps are in nanoseconds
+ * and that the current time is System.nanoTime().
+ *
+ * @return event
+ * @throws InterruptedException
+ */
+ public SchedulableEvent waitNextEvent() throws InterruptedException {
+ SchedulableEvent event = null;
+ while (true) {
+ long millisToWait = Integer.MAX_VALUE;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long now = System.nanoTime();
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for the earliest list to be processed?
+ if (lowestTime <= now) {
+ event = removeNextEventLocked(lowestTime);
+ break;
+ } else {
+ // Figure out how long to sleep until next event.
+ long nanosToWait = lowestTime - now;
+ // Add 1 millisecond so we don't wake up before it is
+ // ready.
+ millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI);
+ // Clip 64-bit value to 32-bit max.
+ if (millisToWait > Integer.MAX_VALUE) {
+ millisToWait = Integer.MAX_VALUE;
+ }
+ }
+ }
+ lock.wait((int) millisToWait);
+ }
+ }
+ return event;
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiConstants.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiConstants.java
new file mode 100644
index 0000000..da427a2
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiConstants.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+/**
+ * MIDI related constants and static methods.
+ * These values are defined in the MIDI Standard 1.0
+ * available from the MIDI Manufacturers Association.
+ */
+public class MidiConstants {
+ public final static String TAG = "MidiTools";
+ public static final byte STATUS_COMMAND_MASK = (byte) 0xF0;
+ public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F;
+
+ // Channel voice messages.
+ public static final byte STATUS_NOTE_OFF = (byte) 0x80;
+ public static final byte STATUS_NOTE_ON = (byte) 0x90;
+ public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0;
+ public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0;
+ public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0;
+ public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0;
+ public static final byte STATUS_PITCH_BEND = (byte) 0xE0;
+
+ // System Common Messages.
+ public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0;
+ public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1;
+ public static final byte STATUS_SONG_POSITION = (byte) 0xF2;
+ public static final byte STATUS_SONG_SELECT = (byte) 0xF3;
+ public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6;
+ public static final byte STATUS_END_SYSEX = (byte) 0xF7;
+
+ // System Real-Time Messages
+ public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8;
+ public static final byte STATUS_START = (byte) 0xFA;
+ public static final byte STATUS_CONTINUE = (byte) 0xFB;
+ public static final byte STATUS_STOP = (byte) 0xFC;
+ public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE;
+ public static final byte STATUS_RESET = (byte) 0xFF;
+
+ /** Number of bytes in a message nc from 8c to Ec */
+ public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 };
+
+ /** Number of bytes in a message Fn from F0 to FF */
+ public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1 };
+
+ /**
+ * MIDI messages, except for SysEx, are 1,2 or 3 bytes long.
+ * You can tell how long a MIDI message is from the first status byte.
+ * Do not call this for SysEx, which has variable length.
+ * @param statusByte
+ * @return number of bytes in a complete message, zero if data byte passed
+ */
+ public static int getBytesPerMessage(byte statusByte) {
+ // Java bytes are signed so we need to mask off the high bits
+ // to get a value between 0 and 255.
+ int statusInt = statusByte & 0xFF;
+ if (statusInt >= 0xF0) {
+ // System messages use low nibble for size.
+ return SYSTEM_BYTE_LENGTHS[statusInt & 0x0F];
+ } else if(statusInt >= 0x80) {
+ // Channel voice messages use high nibble for size.
+ return CHANNEL_BYTE_LENGTHS[(statusInt >> 4) - 8];
+ } else {
+ return 0; // data byte
+ }
+ }
+
+ /**
+ * @param msg
+ * @param offset
+ * @param count
+ * @return true if the entire message is ActiveSensing commands
+ */
+ public static boolean isAllActiveSensing(byte[] msg, int offset,
+ int count) {
+ // Count bytes that are not active sensing.
+ int goodBytes = 0;
+ for (int i = 0; i < count; i++) {
+ byte b = msg[offset + i];
+ if (b != MidiConstants.STATUS_ACTIVE_SENSING) {
+ goodBytes++;
+ }
+ }
+ return (goodBytes == 0);
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiDispatcher.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiDispatcher.java
new file mode 100644
index 0000000..b7f1fe1
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiDispatcher.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiSender;
+
+import java.io.IOException;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
+ * This class subclasses {@link MidiReceiver} and dispatches any data it receives
+ * to its receiver list. Any receivers that throw an exception upon receiving data will
+ * be automatically removed from the receiver list, but no IOException will be returned
+ * from the dispatcher's {@link MidiReceiver#onReceive} in that case.
+ */
+public final class MidiDispatcher extends MidiReceiver {
+
+ private final CopyOnWriteArrayList mReceivers
+ = new CopyOnWriteArrayList();
+
+ private final MidiSender mSender = new MidiSender() {
+ /**
+ * Called to connect a {@link MidiReceiver} to the sender
+ *
+ * @param receiver the receiver to connect
+ */
+ @Override
+ public void onConnect(MidiReceiver receiver) {
+ mReceivers.add(receiver);
+ }
+
+ /**
+ * Called to disconnect a {@link MidiReceiver} from the sender
+ *
+ * @param receiver the receiver to disconnect
+ */
+ @Override
+ public void onDisconnect(MidiReceiver receiver) {
+ mReceivers.remove(receiver);
+ }
+ };
+
+ /**
+ * Returns the number of {@link MidiReceiver}s this dispatcher contains.
+ * @return the number of receivers
+ */
+ public int getReceiverCount() {
+ return mReceivers.size();
+ }
+
+ /**
+ * Returns a {@link MidiSender} which is used to add and remove
+ * {@link MidiReceiver}s
+ * to the dispatcher's receiver list.
+ * @return the dispatcher's MidiSender
+ */
+ public MidiSender getSender() {
+ return mSender;
+ }
+
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
+ for (MidiReceiver receiver : mReceivers) {
+ try {
+ receiver.send(msg, offset, count, timestamp);
+ } catch (IOException e) {
+ // if the receiver fails we remove the receiver but do not propagate the exception
+ mReceivers.remove(receiver);
+ }
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ for (MidiReceiver receiver : mReceivers) {
+ receiver.flush();
+ }
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiEventScheduler.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiEventScheduler.java
new file mode 100644
index 0000000..513d393
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiEventScheduler.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Add MIDI Events to an EventScheduler
+ */
+public class MidiEventScheduler extends EventScheduler {
+ private static final String TAG = "MidiEventScheduler";
+ // Maintain a pool of scheduled events to reduce memory allocation.
+ // This pool increases performance by about 14%.
+ private final static int POOL_EVENT_SIZE = 16;
+ private MidiReceiver mReceiver = new SchedulingReceiver();
+
+ private class SchedulingReceiver extends MidiReceiver
+ {
+ /**
+ * Store these bytes in the EventScheduler to be delivered at the specified
+ * time.
+ */
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp)
+ throws IOException {
+ MidiEvent event = createScheduledEvent(msg, offset, count, timestamp);
+ if (event != null) {
+ add(event);
+ }
+ }
+ }
+
+ public static class MidiEvent extends SchedulableEvent {
+ public int count = 0;
+ public byte[] data;
+
+ private MidiEvent(int count) {
+ super(0);
+ data = new byte[count];
+ }
+
+ private MidiEvent(byte[] msg, int offset, int count, long timestamp) {
+ super(timestamp);
+ data = new byte[count];
+ System.arraycopy(msg, offset, data, 0, count);
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ String text = "Event: ";
+ for (int i = 0; i < count; i++) {
+ text += data[i] + ", ";
+ }
+ return text;
+ }
+ }
+
+ /**
+ * Create an event that contains the message.
+ */
+ private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
+ long timestamp) {
+ MidiEvent event;
+ if (count > POOL_EVENT_SIZE) {
+ event = new MidiEvent(msg, offset, count, timestamp);
+ } else {
+ event = (MidiEvent) removeEventfromPool();
+ if (event == null) {
+ event = new MidiEvent(POOL_EVENT_SIZE);
+ }
+ System.arraycopy(msg, offset, event.data, 0, count);
+ event.count = count;
+ event.setTimestamp(timestamp);
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ @Override
+ public void addEventToPool(SchedulableEvent event) {
+ // Make sure the event is suitable for the pool.
+ if (event instanceof MidiEvent) {
+ MidiEvent midiEvent = (MidiEvent) event;
+ if (midiEvent.data.length == POOL_EVENT_SIZE) {
+ super.addEventToPool(event);
+ }
+ }
+ }
+
+ /**
+ * This MidiReceiver will write date to the scheduling buffer.
+ * @return the MidiReceiver
+ */
+ public MidiReceiver getReceiver() {
+ return mReceiver;
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiEventThread.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiEventThread.java
new file mode 100644
index 0000000..626e83c
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiEventThread.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiSender;
+import android.util.Log;
+
+import java.io.IOException;
+
+public class MidiEventThread extends MidiEventScheduler {
+ protected static final String TAG = "MidiEventThread";
+
+ private EventThread mEventThread;
+ MidiDispatcher mDispatcher = new MidiDispatcher();
+
+ class EventThread extends Thread {
+ private boolean go = true;
+
+ @Override
+ public void run() {
+ while (go) {
+ try {
+ MidiEvent event = (MidiEvent) waitNextEvent();
+ try {
+ Log.i(TAG, "Fire event " + event.data[0] + " at "
+ + event.getTimestamp());
+ mDispatcher.send(event.data, 0,
+ event.count, event.getTimestamp());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // Put event back in the pool for future use.
+ addEventToPool(event);
+ } catch (InterruptedException e) {
+ // OK, this is how we stop the thread.
+ }
+ }
+ }
+
+ /**
+ * Asynchronously tell the thread to stop.
+ */
+ public void requestStop() {
+ go = false;
+ interrupt();
+ }
+ }
+
+ public void start() {
+ stop();
+ mEventThread = new EventThread();
+ mEventThread.start();
+ }
+
+ /**
+ * Asks the thread to stop then waits for it to stop.
+ */
+ public void stop() {
+ if (mEventThread != null) {
+ mEventThread.requestStop();
+ try {
+ mEventThread.join(500);
+ } catch (InterruptedException e) {
+ Log.e(TAG,
+ "Interrupted while waiting for MIDI EventScheduler thread to stop.");
+ } finally {
+ mEventThread = null;
+ }
+ }
+ }
+
+ public MidiSender getSender() {
+ return mDispatcher.getSender();
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiFramer.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiFramer.java
new file mode 100644
index 0000000..c274925
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiFramer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Convert stream of arbitrary MIDI bytes into discrete messages.
+ *
+ * Parses the incoming bytes and then posts individual messages to the receiver
+ * specified in the constructor. Short messages of 1-3 bytes will be complete.
+ * System Exclusive messages may be posted in pieces.
+ *
+ * Resolves Running Status and interleaved System Real-Time messages.
+ */
+public class MidiFramer extends MidiReceiver {
+ private MidiReceiver mReceiver;
+ private byte[] mBuffer = new byte[3];
+ private int mCount;
+ private byte mRunningStatus;
+ private int mNeeded;
+ private boolean mInSysEx;
+
+ public MidiFramer(MidiReceiver receiver) {
+ mReceiver = receiver;
+ }
+
+ /*
+ * @see android.midi.MidiReceiver#onSend(byte[], int, int, long)
+ */
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ int sysExStartOffset = (mInSysEx ? offset : -1);
+
+ for (int i = 0; i < count; i++) {
+ final byte currentByte = data[offset];
+ final int currentInt = currentByte & 0xFF;
+ if (currentInt >= 0x80) { // status byte?
+ if (currentInt < 0xF0) { // channel message?
+ mRunningStatus = currentByte;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+ } else if (currentInt < 0xF8) { // system common?
+ if (currentInt == 0xF0 /* SysEx Start */) {
+ // Log.i(TAG, "SysEx Start");
+ mInSysEx = true;
+ sysExStartOffset = offset;
+ } else if (currentInt == 0xF7 /* SysEx End */) {
+ // Log.i(TAG, "SysEx End");
+ if (mInSysEx) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset + 1, timestamp);
+ mInSysEx = false;
+ sysExStartOffset = -1;
+ }
+ } else {
+ mBuffer[0] = currentByte;
+ mRunningStatus = 0;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+ }
+ } else { // real-time?
+ // Single byte message interleaved with other data.
+ if (mInSysEx) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset, timestamp);
+ sysExStartOffset = offset + 1;
+ }
+ mReceiver.send(data, offset, 1, timestamp);
+ }
+ } else { // data byte
+ if (!mInSysEx) {
+ mBuffer[mCount++] = currentByte;
+ if (--mNeeded == 0) {
+ if (mRunningStatus != 0) {
+ mBuffer[0] = mRunningStatus;
+ }
+ mReceiver.send(mBuffer, 0, mCount, timestamp);
+ mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1;
+ mCount = 1;
+ }
+ }
+ }
+ ++offset;
+ }
+
+ // send any accumulatedSysEx data
+ if (sysExStartOffset >= 0 && sysExStartOffset < offset) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset, timestamp);
+ }
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiInputPortSelector.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiInputPortSelector.java
new file mode 100644
index 0000000..7c665ba
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiInputPortSelector.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiReceiver;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages a Spinner for selecting a MidiInputPort.
+ */
+public class MidiInputPortSelector extends MidiPortSelector {
+
+ private MidiInputPort mInputPort;
+ private MidiDevice mOpenDevice;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId ID from the layout resource
+ */
+ public MidiInputPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId) {
+ super(midiManager, activity, spinnerId, MidiDeviceInfo.PortInfo.TYPE_INPUT);
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ close();
+ final MidiDeviceInfo info = wrapper.getDeviceInfo();
+ if (info != null) {
+ mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info);
+ } else {
+ mOpenDevice = device;
+ mInputPort = mOpenDevice.openInputPort(
+ wrapper.getPortIndex());
+ if (mInputPort == null) {
+ Log.e(MidiConstants.TAG, "could not open input port on " + info);
+ }
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openInputPort might take a while.
+ }
+ }
+
+ public MidiReceiver getReceiver() {
+ return mInputPort;
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mInputPort != null) {
+ Log.i(MidiConstants.TAG, "MidiInputPortSelector.onClose() - close port");
+ mInputPort.close();
+ }
+ mInputPort = null;
+ if (mOpenDevice != null) {
+ mOpenDevice.close();
+ }
+ mOpenDevice = null;
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "cleanup failed", e);
+ }
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiOutputPortConnectionSelector.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiOutputPortConnectionSelector.java
new file mode 100644
index 0000000..ca1ade4
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiOutputPortConnectionSelector.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Select an output port and connect it to a destination input port.
+ */
+public class MidiOutputPortConnectionSelector extends MidiPortSelector {
+
+ private MidiPortConnector mSynthConnector;
+ private MidiDeviceInfo mDestinationDeviceInfo;
+ private int mDestinationPortIndex;
+ private MidiPortConnector.OnPortsConnectedListener mConnectedListener;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId
+ * @param type
+ */
+ public MidiOutputPortConnectionSelector(MidiManager midiManager,
+ Activity activity, int spinnerId,
+ MidiDeviceInfo destinationDeviceInfo, int destinationPortIndex) {
+ super(midiManager, activity, spinnerId,
+ MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
+ mDestinationDeviceInfo = destinationDeviceInfo;
+ mDestinationPortIndex = destinationPortIndex;
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ Log.i(MidiConstants.TAG, "connectPortToSynth: " + wrapper);
+ onClose();
+ if (wrapper.getDeviceInfo() != null) {
+ mSynthConnector = new MidiPortConnector(mMidiManager);
+ mSynthConnector.connectToDevicePort(wrapper.getDeviceInfo(),
+ wrapper.getPortIndex(), mDestinationDeviceInfo,
+ mDestinationPortIndex,
+ // not safe on UI thread
+ mConnectedListener, null);
+ }
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mSynthConnector != null) {
+ mSynthConnector.close();
+ mSynthConnector = null;
+ }
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "Exception in closeSynthResources()", e);
+ }
+ }
+
+ /**
+ * @param myPortsConnectedListener
+ */
+ public void setConnectedListener(
+ MidiPortConnector.OnPortsConnectedListener connectedListener) {
+ mConnectedListener = connectedListener;
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiOutputPortSelector.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiOutputPortSelector.java
new file mode 100644
index 0000000..5aebf72
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiOutputPortSelector.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiOutputPort;
+import android.media.midi.MidiSender;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages a Spinner for selecting a MidiOutputPort.
+ */
+public class MidiOutputPortSelector extends MidiPortSelector {
+ private MidiOutputPort mOutputPort;
+ private MidiDispatcher mDispatcher = new MidiDispatcher();
+ private MidiDevice mOpenDevice;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId ID from the layout resource
+ */
+ public MidiOutputPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId) {
+ super(midiManager, activity, spinnerId, MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ Log.i(MidiConstants.TAG, "onPortSelected: " + wrapper);
+ close();
+
+ final MidiDeviceInfo info = wrapper.getDeviceInfo();
+ if (info != null) {
+ mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info);
+ } else {
+ mOpenDevice = device;
+ mOutputPort = device.openOutputPort(wrapper.getPortIndex());
+ if (mOutputPort == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open output port for " + info);
+ return;
+ }
+ mOutputPort.connect(mDispatcher);
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openOutputPort might take a while.
+ }
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mOutputPort != null) {
+ mOutputPort.disconnect(mDispatcher);
+ }
+ mOutputPort = null;
+ if (mOpenDevice != null) {
+ mOpenDevice.close();
+ }
+ mOpenDevice = null;
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "cleanup failed", e);
+ }
+ }
+
+ /**
+ * You can connect your MidiReceivers to this sender. The user will then select which output
+ * port will send messages through this MidiSender.
+ * @return a MidiSender that will send the messages from the selected port.
+ */
+ public MidiSender getSender() {
+ return mDispatcher.getSender();
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortConnector.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortConnector.java
new file mode 100644
index 0000000..457494d
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortConnector.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDevice.MidiConnection;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiManager;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Tool for connecting MIDI ports on two remote devices.
+ */
+public class MidiPortConnector {
+ private final MidiManager mMidiManager;
+ private MidiDevice mSourceDevice;
+ private MidiDevice mDestinationDevice;
+ private MidiConnection mConnection;
+
+ /**
+ * @param mMidiManager
+ */
+ public MidiPortConnector(MidiManager midiManager) {
+ mMidiManager = midiManager;
+ }
+
+ public void close() throws IOException {
+ if (mConnection != null) {
+ Log.i(MidiConstants.TAG,
+ "MidiPortConnector closing connection " + mConnection);
+ mConnection.close();
+ mConnection = null;
+ }
+ if (mSourceDevice != null) {
+ mSourceDevice.close();
+ mSourceDevice = null;
+ }
+ if (mDestinationDevice != null) {
+ mDestinationDevice.close();
+ mDestinationDevice = null;
+ }
+ }
+
+ private void safeClose() {
+ try {
+ close();
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "could not close resources", e);
+ }
+ }
+
+ /**
+ * Listener class used for receiving the results of
+ * {@link #connectToDevicePort}
+ */
+ public interface OnPortsConnectedListener {
+ /**
+ * Called to respond to a {@link #connectToDevicePort} request
+ *
+ * @param connection
+ * a {@link MidiConnection} that represents the connected
+ * ports, or null if connection failed
+ */
+ abstract public void onPortsConnected(MidiConnection connection);
+ }
+
+ /**
+ * Open two devices and connect their ports.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationDeviceInfo
+ * @param destinationPortIndex
+ */
+ public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiDeviceInfo destinationDeviceInfo,
+ final int destinationPortIndex) {
+ connectToDevicePort(sourceDeviceInfo, sourcePortIndex,
+ destinationDeviceInfo, destinationPortIndex, null, null);
+ }
+
+ /**
+ * Open two devices and connect their ports.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationDeviceInfo
+ * @param destinationPortIndex
+ */
+ public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiDeviceInfo destinationDeviceInfo,
+ final int destinationPortIndex,
+ final OnPortsConnectedListener listener, final Handler handler) {
+ safeClose();
+ mMidiManager.openDevice(destinationDeviceInfo,
+ new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice destinationDevice) {
+ if (destinationDevice == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open " + destinationDeviceInfo);
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ } else {
+ mDestinationDevice = destinationDevice;
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened "
+ + destinationDeviceInfo);
+ // Destination device was opened so go to next step.
+ MidiInputPort destinationInputPort = destinationDevice
+ .openInputPort(destinationPortIndex);
+ if (destinationInputPort != null) {
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened port on "
+ + destinationDeviceInfo);
+ connectToDevicePort(sourceDeviceInfo,
+ sourcePortIndex,
+ destinationInputPort,
+ listener, handler);
+ } else {
+ Log.e(MidiConstants.TAG,
+ "could not open port on "
+ + destinationDeviceInfo);
+ safeClose();
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ }
+ }
+ }
+ }, handler);
+ }
+
+
+ /**
+ * Open a source device and connect its output port to the
+ * destinationInputPort.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationInputPort
+ */
+ private void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiInputPort destinationInputPort,
+ final OnPortsConnectedListener listener, final Handler handler) {
+ mMidiManager.openDevice(sourceDeviceInfo,
+ new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open " + sourceDeviceInfo);
+ safeClose();
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ } else {
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened "
+ + sourceDeviceInfo);
+ // Device was opened so connect the ports.
+ mSourceDevice = device;
+ mConnection = device.connectPorts(
+ destinationInputPort, sourcePortIndex);
+ if (mConnection == null) {
+ Log.e(MidiConstants.TAG, "could not connect to "
+ + sourceDeviceInfo);
+ safeClose();
+ }
+ if (listener != null) {
+ listener.onPortsConnected(mConnection);
+ }
+ }
+ }
+ }, handler);
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortSelector.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortSelector.java
new file mode 100644
index 0000000..39f983e
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortSelector.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiManager.DeviceCallback;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import java.util.HashSet;
+
+/**
+ * Base class that uses a Spinner to select available MIDI ports.
+ */
+public abstract class MidiPortSelector extends DeviceCallback {
+ private int mType = MidiDeviceInfo.PortInfo.TYPE_INPUT;
+ protected ArrayAdapter mAdapter;
+ protected HashSet mBusyPorts = new HashSet();
+ private Spinner mSpinner;
+ protected MidiManager mMidiManager;
+ protected Activity mActivity;
+ private MidiPortWrapper mCurrentWrapper;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId
+ * ID from the layout resource
+ * @param type
+ * TYPE_INPUT or TYPE_OUTPUT
+ */
+ public MidiPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId, int type) {
+ mMidiManager = midiManager;
+ mActivity = activity;
+ mType = type;
+ mAdapter = new ArrayAdapter(activity,
+ android.R.layout.simple_spinner_item);
+ mAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mAdapter.add(new MidiPortWrapper(null, 0, 0));
+
+ mSpinner = (Spinner) activity.findViewById(spinnerId);
+ mSpinner.setOnItemSelectedListener(
+ new AdapterView.OnItemSelectedListener() {
+
+ public void onItemSelected(AdapterView> parent, View view,
+ int pos, long id) {
+ mCurrentWrapper = mAdapter.getItem(pos);
+ onPortSelected(mCurrentWrapper);
+ }
+
+ public void onNothingSelected(AdapterView> parent) {
+ onPortSelected(null);
+ mCurrentWrapper = null;
+ }
+ });
+ mSpinner.setAdapter(mAdapter);
+
+ mMidiManager.registerDeviceCallback(this,
+ new Handler(Looper.getMainLooper()));
+
+ MidiDeviceInfo[] infos = mMidiManager.getDevices();
+ for (MidiDeviceInfo info : infos) {
+ onDeviceAdded(info);
+ }
+ }
+
+ /**
+ * Set to no port selected.
+ */
+ public void clearSelection() {
+ mSpinner.setSelection(0);
+ }
+
+ private int getInfoPortCount(final MidiDeviceInfo info) {
+ int portCount = (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT)
+ ? info.getInputPortCount() : info.getOutputPortCount();
+ return portCount;
+ }
+
+ @Override
+ public void onDeviceAdded(final MidiDeviceInfo info) {
+ int portCount = getInfoPortCount(info);
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ mAdapter.add(wrapper);
+ Log.i(MidiConstants.TAG, wrapper + " was added");
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onDeviceRemoved(final MidiDeviceInfo info) {
+ int portCount = getInfoPortCount(info);
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ MidiPortWrapper currentWrapper = mCurrentWrapper;
+ mAdapter.remove(wrapper);
+ // If the currently selected port was removed then select no port.
+ if (wrapper.equals(currentWrapper)) {
+ clearSelection();
+ }
+ mAdapter.notifyDataSetChanged();
+ Log.i(MidiConstants.TAG, wrapper + " was removed");
+ }
+ }
+
+ @Override
+ public void onDeviceStatusChanged(final MidiDeviceStatus status) {
+ // If an input port becomes busy then remove it from the menu.
+ // If it becomes free then add it back to the menu.
+ if (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT) {
+ MidiDeviceInfo info = status.getDeviceInfo();
+ Log.i(MidiConstants.TAG, "MidiPortSelector.onDeviceStatusChanged status = " + status
+ + ", mType = " + mType
+ + ", activity = " + mActivity.getPackageName()
+ + ", info = " + info);
+ // Look for transitions from free to busy.
+ int portCount = info.getInputPortCount();
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ if (!wrapper.equals(mCurrentWrapper)) {
+ if (status.isInputPortOpen(i)) { // busy?
+ if (!mBusyPorts.contains(wrapper)) {
+ // was free, now busy
+ mBusyPorts.add(wrapper);
+ mAdapter.remove(wrapper);
+ mAdapter.notifyDataSetChanged();
+ }
+ } else {
+ if (mBusyPorts.remove(wrapper)) {
+ // was busy, now free
+ mAdapter.add(wrapper);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Implement this method to handle the user selecting a port on a device.
+ *
+ * @param wrapper
+ */
+ public abstract void onPortSelected(MidiPortWrapper wrapper);
+
+ /**
+ * Implement this method to clean up any open resources.
+ */
+ public abstract void onClose();
+
+ /**
+ *
+ */
+ public void close() {
+ onClose();
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortWrapper.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortWrapper.java
new file mode 100644
index 0000000..77aa734
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiPortWrapper.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceInfo.PortInfo;
+import android.util.Log;
+
+// Wrapper for a MIDI device and port description.
+public class MidiPortWrapper {
+ private MidiDeviceInfo mInfo;
+ private int mPortIndex;
+ private int mType;
+ private String mString;
+
+ /**
+ * Wrapper for a MIDI device and port description.
+ * @param info
+ * @param portType
+ * @param portIndex
+ */
+ public MidiPortWrapper(MidiDeviceInfo info, int portType, int portIndex) {
+ mInfo = info;
+ mType = portType;
+ mPortIndex = portIndex;
+ }
+
+ private void updateString() {
+ if (mInfo == null) {
+ mString = "- - - - - -";
+ } else {
+ StringBuilder sb = new StringBuilder();
+ String name = mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_NAME);
+ if (name == null) {
+ name = mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER) + ", "
+ + mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ }
+ sb.append("#" + mInfo.getId());
+ sb.append(", ").append(name);
+ PortInfo portInfo = findPortInfo();
+ sb.append("[" + mPortIndex + "]");
+ if (portInfo != null) {
+ sb.append(", ").append(portInfo.getName());
+ } else {
+ sb.append(", null");
+ }
+ mString = sb.toString();
+ }
+ }
+
+ /**
+ * @param info
+ * @param portIndex
+ * @return
+ */
+ private PortInfo findPortInfo() {
+ PortInfo[] ports = mInfo.getPorts();
+ for (PortInfo portInfo : ports) {
+ if (portInfo.getPortNumber() == mPortIndex
+ && portInfo.getType() == mType) {
+ return portInfo;
+ }
+ }
+ return null;
+ }
+
+ public int getPortIndex() {
+ return mPortIndex;
+ }
+
+ public MidiDeviceInfo getDeviceInfo() {
+ return mInfo;
+ }
+
+ @Override
+ public String toString() {
+ if (mString == null) {
+ updateString();
+ }
+ return mString;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof MidiPortWrapper))
+ return false;
+ MidiPortWrapper otherWrapper = (MidiPortWrapper) other;
+ if (mPortIndex != otherWrapper.mPortIndex)
+ return false;
+ if (mType != otherWrapper.mType)
+ return false;
+ if (mInfo == null)
+ return (otherWrapper.mInfo == null);
+ return mInfo.equals(otherWrapper.mInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ hashCode = 31 * hashCode + mPortIndex;
+ hashCode = 31 * hashCode + mType;
+ hashCode = 31 * hashCode + mInfo.hashCode();
+ return hashCode;
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiTools.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiTools.java
new file mode 100644
index 0000000..82e3de4
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/MidiTools.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 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.common.midi;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+
+/**
+ * Miscellaneous tools for Android MIDI.
+ */
+public class MidiTools {
+
+ /**
+ * @return a device that matches the manufacturer and product or null
+ */
+ public static MidiDeviceInfo findDevice(MidiManager midiManager,
+ String manufacturer, String product) {
+ for (MidiDeviceInfo info : midiManager.getDevices()) {
+ String deviceManufacturer = info.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER);
+ if ((manufacturer != null)
+ && manufacturer.equals(deviceManufacturer)) {
+ String deviceProduct = info.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ if ((product != null) && product.equals(deviceProduct)) {
+ return info;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/EnvelopeADSR.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/EnvelopeADSR.java
new file mode 100644
index 0000000..a29a193
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/EnvelopeADSR.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+/**
+ * Very simple Attack, Decay, Sustain, Release envelope with linear ramps.
+ *
+ * Times are in seconds.
+ */
+public class EnvelopeADSR extends SynthUnit {
+ private static final int IDLE = 0;
+ private static final int ATTACK = 1;
+ private static final int DECAY = 2;
+ private static final int SUSTAIN = 3;
+ private static final int RELEASE = 4;
+ private static final int FINISHED = 5;
+ private static final float MIN_TIME = 0.001f;
+
+ private float mAttackRate;
+ private float mRreleaseRate;
+ private float mSustainLevel;
+ private float mDecayRate;
+ private float mCurrent;
+ private int mSstate = IDLE;
+
+ public EnvelopeADSR() {
+ setAttackTime(0.003f);
+ setDecayTime(0.08f);
+ setSustainLevel(0.3f);
+ setReleaseTime(1.0f);
+ }
+
+ public void setAttackTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mAttackRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void setDecayTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mDecayRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void setSustainLevel(float level) {
+ if (level < 0.0f)
+ level = 0.0f;
+ mSustainLevel = level;
+ }
+
+ public void setReleaseTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mRreleaseRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void on() {
+ mSstate = ATTACK;
+ }
+
+ public void off() {
+ mSstate = RELEASE;
+ }
+
+ @Override
+ public float render() {
+ switch (mSstate) {
+ case ATTACK:
+ mCurrent += mAttackRate;
+ if (mCurrent > 1.0f) {
+ mCurrent = 1.0f;
+ mSstate = DECAY;
+ }
+ break;
+ case DECAY:
+ mCurrent -= mDecayRate;
+ if (mCurrent < mSustainLevel) {
+ mCurrent = mSustainLevel;
+ mSstate = SUSTAIN;
+ }
+ break;
+ case RELEASE:
+ mCurrent -= mRreleaseRate;
+ if (mCurrent < 0.0f) {
+ mCurrent = 0.0f;
+ mSstate = FINISHED;
+ }
+ break;
+ }
+ return mCurrent;
+ }
+
+ public boolean isDone() {
+ return mSstate == FINISHED;
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawOscillator.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawOscillator.java
new file mode 100644
index 0000000..c02a6a1
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawOscillator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+public class SawOscillator extends SynthUnit {
+ private float mPhase = 0.0f;
+ private float mPhaseIncrement = 0.01f;
+ private float mFrequency = 0.0f;
+ private float mFrequencyScaler = 1.0f;
+ private float mAmplitude = 1.0f;
+
+ public void setPitch(float pitch) {
+ float freq = (float) pitchToFrequency(pitch);
+ setFrequency(freq);
+ }
+
+ public void setFrequency(float frequency) {
+ mFrequency = frequency;
+ updatePhaseIncrement();
+ }
+
+ private void updatePhaseIncrement() {
+ mPhaseIncrement = 2.0f * mFrequency * mFrequencyScaler / 48000.0f;
+ }
+
+ public void setAmplitude(float amplitude) {
+ mAmplitude = amplitude;
+ }
+
+ public float getAmplitude() {
+ return mAmplitude;
+ }
+
+ public float getFrequencyScaler() {
+ return mFrequencyScaler;
+ }
+
+ public void setFrequencyScaler(float frequencyScaler) {
+ mFrequencyScaler = frequencyScaler;
+ updatePhaseIncrement();
+ }
+
+ float incrementWrapPhase() {
+ mPhase += mPhaseIncrement;
+ while (mPhase > 1.0) {
+ mPhase -= 2.0;
+ }
+ while (mPhase < -1.0) {
+ mPhase += 2.0;
+ }
+ return mPhase;
+ }
+
+ @Override
+ public float render() {
+ return incrementWrapPhase() * mAmplitude;
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawOscillatorDPW.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawOscillatorDPW.java
new file mode 100644
index 0000000..e5d661d
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawOscillatorDPW.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+/**
+ * Band limited sawtooth oscillator.
+ * This will have very little aliasing at high frequencies.
+ */
+public class SawOscillatorDPW extends SawOscillator {
+ private float mZ1 = 0.0f; // delayed values
+ private float mZ2 = 0.0f;
+ private float mScaler; // frequency dependent scaler
+ private final static float VERY_LOW_FREQ = 0.0000001f;
+
+ @Override
+ public void setFrequency(float freq) {
+ /* Calculate scaling based on frequency. */
+ freq = Math.abs(freq);
+ super.setFrequency(freq);
+ if (freq < VERY_LOW_FREQ) {
+ mScaler = (float) (0.125 * 44100 / VERY_LOW_FREQ);
+ } else {
+ mScaler = (float) (0.125 * 44100 / freq);
+ }
+ }
+
+ @Override
+ public float render() {
+ float phase = incrementWrapPhase();
+ /* Square the raw sawtooth. */
+ float squared = phase * phase;
+ float diffed = squared - mZ2;
+ mZ2 = mZ1;
+ mZ1 = squared;
+ return diffed * mScaler * getAmplitude();
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawVoice.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawVoice.java
new file mode 100644
index 0000000..3b3e543
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SawVoice.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+/**
+ * Sawtooth oscillator with an ADSR.
+ */
+public class SawVoice extends SynthVoice {
+ private SawOscillator mOscillator;
+ private EnvelopeADSR mEnvelope;
+
+ public SawVoice() {
+ mOscillator = createOscillator();
+ mEnvelope = new EnvelopeADSR();
+ }
+
+ protected SawOscillator createOscillator() {
+ return new SawOscillator();
+ }
+
+ @Override
+ public void noteOn(int noteIndex, int velocity) {
+ super.noteOn(noteIndex, velocity);
+ mOscillator.setPitch(noteIndex);
+ mOscillator.setAmplitude(getAmplitude());
+ mEnvelope.on();
+ }
+
+ @Override
+ public void noteOff() {
+ super.noteOff();
+ mEnvelope.off();
+ }
+
+ @Override
+ public void setFrequencyScaler(float scaler) {
+ mOscillator.setFrequencyScaler(scaler);
+ }
+
+ @Override
+ public float render() {
+ float output = mOscillator.render() * mEnvelope.render();
+ return output;
+ }
+
+ @Override
+ public boolean isDone() {
+ return mEnvelope.isDone();
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SimpleAudioOutput.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SimpleAudioOutput.java
new file mode 100644
index 0000000..04aa19c
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SimpleAudioOutput.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * Simple base class for implementing audio output for examples.
+ * This can be sub-classed for experimentation or to redirect audio output.
+ */
+public class SimpleAudioOutput {
+
+ private static final String TAG = "AudioOutputTrack";
+ public static final int SAMPLES_PER_FRAME = 2;
+ public static final int BYTES_PER_SAMPLE = 4; // float
+ public static final int BYTES_PER_FRAME = SAMPLES_PER_FRAME * BYTES_PER_SAMPLE;
+ private AudioTrack mAudioTrack;
+ private int mFrameRate;
+
+ /**
+ *
+ */
+ public SimpleAudioOutput() {
+ super();
+ }
+
+ /**
+ * Create an audio track then call play().
+ *
+ * @param frameRate
+ */
+ public void start(int frameRate) {
+ stop();
+ mFrameRate = frameRate;
+ mAudioTrack = createAudioTrack(frameRate);
+ // AudioTrack will wait until it has enough data before starting.
+ mAudioTrack.play();
+ }
+
+ public AudioTrack createAudioTrack(int frameRate) {
+ int minBufferSizeBytes = AudioTrack.getMinBufferSize(frameRate,
+ AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT);
+ Log.i(TAG, "AudioTrack.minBufferSize = " + minBufferSizeBytes
+ + " bytes = " + (minBufferSizeBytes / BYTES_PER_FRAME)
+ + " frames");
+ int bufferSize = 8 * minBufferSizeBytes / 8;
+ int outputBufferSizeFrames = bufferSize / BYTES_PER_FRAME;
+ Log.i(TAG, "actual bufferSize = " + bufferSize + " bytes = "
+ + outputBufferSizeFrames + " frames");
+
+ AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC,
+ mFrameRate, AudioFormat.CHANNEL_OUT_STEREO,
+ AudioFormat.ENCODING_PCM_FLOAT, bufferSize,
+ AudioTrack.MODE_STREAM);
+ Log.i(TAG, "created AudioTrack");
+ return player;
+ }
+
+ public int write(float[] buffer, int offset, int length) {
+ return mAudioTrack.write(buffer, offset, length,
+ AudioTrack.WRITE_BLOCKING);
+ }
+
+ public void stop() {
+ if (mAudioTrack != null) {
+ mAudioTrack.stop();
+ mAudioTrack = null;
+ }
+ }
+
+ public int getFrameRate() {
+ return mFrameRate;
+ }
+
+ public AudioTrack getAudioTrack() {
+ return mAudioTrack;
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SineOscillator.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SineOscillator.java
new file mode 100644
index 0000000..c638c34
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SineOscillator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+/**
+ * Sinewave oscillator.
+ */
+public class SineOscillator extends SawOscillator {
+ // Factorial constants.
+ private static final float IF3 = 1.0f / (2 * 3);
+ private static final float IF5 = IF3 / (4 * 5);
+ private static final float IF7 = IF5 / (6 * 7);
+ private static final float IF9 = IF7 / (8 * 9);
+ private static final float IF11 = IF9 / (10 * 11);
+
+ /**
+ * Calculate sine using Taylor expansion. Do not use values outside the range.
+ *
+ * @param currentPhase in the range of -1.0 to +1.0 for one cycle
+ */
+ public static float fastSin(float currentPhase) {
+
+ /* Wrap phase back into region where results are more accurate. */
+ float yp = (currentPhase > 0.5f) ? 1.0f - currentPhase
+ : ((currentPhase < (-0.5f)) ? (-1.0f) - currentPhase : currentPhase);
+
+ float x = (float) (yp * Math.PI);
+ float x2 = (x * x);
+ /* Taylor expansion out to x**11/11! factored into multiply-adds */
+ return x * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ }
+
+ @Override
+ public float render() {
+ // Convert raw sawtooth to sine.
+ float phase = incrementWrapPhase();
+ return fastSin(phase) * getAmplitude();
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SineVoice.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SineVoice.java
new file mode 100644
index 0000000..e80d2c7
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SineVoice.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+/**
+ * Replace sawtooth with a sine wave.
+ */
+public class SineVoice extends SawVoice {
+ @Override
+ protected SawOscillator createOscillator() {
+ return new SineOscillator();
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthEngine.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthEngine.java
new file mode 100644
index 0000000..6cd02a6
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthEngine.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import com.example.android.common.midi.MidiConstants;
+import com.example.android.common.midi.MidiEventScheduler;
+import com.example.android.common.midi.MidiEventScheduler.MidiEvent;
+import com.example.android.common.midi.MidiFramer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.Iterator;
+
+/**
+ * Very simple polyphonic, single channel synthesizer. It runs a background
+ * thread that processes MIDI events and synthesizes audio.
+ */
+public class SynthEngine extends MidiReceiver {
+
+ private static final String TAG = "SynthEngine";
+
+ public static final int FRAME_RATE = 48000;
+ private static final int FRAMES_PER_BUFFER = 240;
+ private static final int SAMPLES_PER_FRAME = 2;
+
+ private boolean go;
+ private Thread mThread;
+ private float[] mBuffer = new float[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ private float mFrequencyScaler = 1.0f;
+ private float mBendRange = 2.0f; // semitones
+ private int mProgram;
+
+ private ArrayList mFreeVoices = new ArrayList();
+ private Hashtable
+ mVoices = new Hashtable();
+ private MidiEventScheduler mEventScheduler;
+ private MidiFramer mFramer;
+ private MidiReceiver mReceiver = new MyReceiver();
+ private SimpleAudioOutput mAudioOutput;
+
+ public SynthEngine() {
+ this(new SimpleAudioOutput());
+ }
+
+ public SynthEngine(SimpleAudioOutput audioOutput) {
+ mReceiver = new MyReceiver();
+ mFramer = new MidiFramer(mReceiver);
+ mAudioOutput = audioOutput;
+ }
+
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ if (mEventScheduler != null) {
+ if (!MidiConstants.isAllActiveSensing(data, offset, count)) {
+ mEventScheduler.getReceiver().send(data, offset, count,
+ timestamp);
+ }
+ }
+ }
+
+ private class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ byte command = (byte) (data[0] & MidiConstants.STATUS_COMMAND_MASK);
+ int channel = (byte) (data[0] & MidiConstants.STATUS_CHANNEL_MASK);
+ switch (command) {
+ case MidiConstants.STATUS_NOTE_OFF:
+ noteOff(channel, data[1], data[2]);
+ break;
+ case MidiConstants.STATUS_NOTE_ON:
+ noteOn(channel, data[1], data[2]);
+ break;
+ case MidiConstants.STATUS_PITCH_BEND:
+ int bend = (data[2] << 7) + data[1];
+ pitchBend(channel, bend);
+ break;
+ case MidiConstants.STATUS_PROGRAM_CHANGE:
+ mProgram = data[1];
+ mFreeVoices.clear();
+ break;
+ default:
+ logMidiMessage(data, offset, count);
+ break;
+ }
+ }
+ }
+
+ class MyRunnable implements Runnable {
+ @Override
+ public void run() {
+ try {
+ mAudioOutput.start(FRAME_RATE);
+ onLoopStarted();
+ while (go) {
+ processMidiEvents();
+ generateBuffer();
+ mAudioOutput.write(mBuffer, 0, mBuffer.length);
+ onBufferCompleted(FRAMES_PER_BUFFER);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "SynthEngine background thread exception.", e);
+ } finally {
+ onLoopEnded();
+ mAudioOutput.stop();
+ }
+ }
+ }
+
+ /**
+ * This is called form the synthesis thread before it starts looping.
+ */
+ public void onLoopStarted() {
+ }
+
+ /**
+ * This is called once at the end of each synthesis loop.
+ *
+ * @param framesPerBuffer
+ */
+ public void onBufferCompleted(int framesPerBuffer) {
+ }
+
+ /**
+ * This is called form the synthesis thread when it stop looping.
+ */
+ public void onLoopEnded() {
+ }
+
+ /**
+ * Assume message has been aligned to the start of a MIDI message.
+ *
+ * @param data
+ * @param offset
+ * @param count
+ */
+ public void logMidiMessage(byte[] data, int offset, int count) {
+ String text = "Received: ";
+ for (int i = 0; i < count; i++) {
+ text += String.format("0x%02X, ", data[offset + i]);
+ }
+ Log.i(TAG, text);
+ }
+
+ /**
+ * @throws IOException
+ *
+ */
+ private void processMidiEvents() throws IOException {
+ long now = System.nanoTime(); // TODO use audio presentation time
+ MidiEvent event = (MidiEvent) mEventScheduler.getNextEvent(now);
+ while (event != null) {
+ mFramer.send(event.data, 0, event.count, event.getTimestamp());
+ mEventScheduler.addEventToPool(event);
+ event = (MidiEvent) mEventScheduler.getNextEvent(now);
+ }
+ }
+
+ /**
+ *
+ */
+ private void generateBuffer() {
+ for (int i = 0; i < mBuffer.length; i++) {
+ mBuffer[i] = 0.0f;
+ }
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ if (voice.isDone()) {
+ iterator.remove();
+ // mFreeVoices.add(voice);
+ } else {
+ voice.mix(mBuffer, SAMPLES_PER_FRAME, 0.25f);
+ }
+ }
+ }
+
+ public void noteOff(int channel, int noteIndex, int velocity) {
+ SynthVoice voice = mVoices.get(noteIndex);
+ if (voice != null) {
+ voice.noteOff();
+ }
+ }
+
+ public void allNotesOff() {
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ voice.noteOff();
+ }
+ }
+
+ /**
+ * Create a SynthVoice.
+ */
+ public SynthVoice createVoice(int program) {
+ // For every odd program number use a sine wave.
+ if ((program & 1) == 1) {
+ return new SineVoice();
+ } else {
+ return new SawVoice();
+ }
+ }
+
+ /**
+ *
+ * @param channel
+ * @param noteIndex
+ * @param velocity
+ */
+ public void noteOn(int channel, int noteIndex, int velocity) {
+ if (velocity == 0) {
+ noteOff(channel, noteIndex, velocity);
+ } else {
+ mVoices.remove(noteIndex);
+ SynthVoice voice;
+ if (mFreeVoices.size() > 0) {
+ voice = mFreeVoices.remove(mFreeVoices.size() - 1);
+ } else {
+ voice = createVoice(mProgram);
+ }
+ voice.setFrequencyScaler(mFrequencyScaler);
+ voice.noteOn(noteIndex, velocity);
+ mVoices.put(noteIndex, voice);
+ }
+ }
+
+ public void pitchBend(int channel, int bend) {
+ double semitones = (mBendRange * (bend - 0x2000)) / 0x2000;
+ mFrequencyScaler = (float) Math.pow(2.0, semitones / 12.0);
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ voice.setFrequencyScaler(mFrequencyScaler);
+ }
+ }
+
+ /**
+ * Start the synthesizer.
+ */
+ public void start() {
+ stop();
+ go = true;
+ mThread = new Thread(new MyRunnable());
+ mEventScheduler = new MidiEventScheduler();
+ mThread.start();
+ }
+
+ /**
+ * Stop the synthesizer.
+ */
+ public void stop() {
+ go = false;
+ if (mThread != null) {
+ try {
+ mThread.interrupt();
+ mThread.join(500);
+ } catch (InterruptedException e) {
+ // OK, just stopping safely.
+ }
+ mThread = null;
+ mEventScheduler = null;
+ }
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthUnit.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthUnit.java
new file mode 100644
index 0000000..90599e2
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthUnit.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+public abstract class SynthUnit {
+
+ private static final double CONCERT_A_PITCH = 69.0;
+ private static final double CONCERT_A_FREQUENCY = 440.0;
+
+ /**
+ * @param pitch
+ * MIDI pitch in semitones
+ * @return frequency
+ */
+ public static double pitchToFrequency(double pitch) {
+ double semitones = pitch - CONCERT_A_PITCH;
+ return CONCERT_A_FREQUENCY * Math.pow(2.0, semitones / 12.0);
+ }
+
+ public abstract float render();
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthVoice.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthVoice.java
new file mode 100644
index 0000000..78ba09a
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/common/midi/synth/SynthVoice.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 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.common.midi.synth;
+
+/**
+ * Base class for a polyphonic synthesizer voice.
+ */
+public abstract class SynthVoice {
+ private int mNoteIndex;
+ private float mAmplitude;
+ public static final int STATE_OFF = 0;
+ public static final int STATE_ON = 1;
+ private int mState = STATE_OFF;
+
+ public SynthVoice() {
+ mNoteIndex = -1;
+ }
+
+ public void noteOn(int noteIndex, int velocity) {
+ mState = STATE_ON;
+ this.mNoteIndex = noteIndex;
+ setAmplitude(velocity / 128.0f);
+ }
+
+ public void noteOff() {
+ mState = STATE_OFF;
+ }
+
+ /**
+ * Add the output of this voice to an output buffer.
+ *
+ * @param outputBuffer
+ * @param samplesPerFrame
+ * @param level
+ */
+ public void mix(float[] outputBuffer, int samplesPerFrame, float level) {
+ int numFrames = outputBuffer.length / samplesPerFrame;
+ for (int i = 0; i < numFrames; i++) {
+ float output = render();
+ int offset = i * samplesPerFrame;
+ for (int jf = 0; jf < samplesPerFrame; jf++) {
+ outputBuffer[offset + jf] += output * level;
+ }
+ }
+ }
+
+ public abstract float render();
+
+ public boolean isDone() {
+ return mState == STATE_OFF;
+ }
+
+ public int getNoteIndex() {
+ return mNoteIndex;
+ }
+
+ public float getAmplitude() {
+ return mAmplitude;
+ }
+
+ public void setAmplitude(float amplitude) {
+ this.mAmplitude = amplitude;
+ }
+
+ /**
+ * @param scaler
+ */
+ public void setFrequencyScaler(float scaler) {
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/LoggingReceiver.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/LoggingReceiver.java
new file mode 100644
index 0000000..23ce8f7
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/LoggingReceiver.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 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.midiscope;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Convert incoming MIDI messages to a string and write them to a ScopeLogger.
+ * Assume that messages have been aligned using a MidiFramer.
+ */
+public class LoggingReceiver extends MidiReceiver {
+ public static final String TAG = "MidiScope";
+ private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
+ private long mStartTime;
+ private ScopeLogger mLogger;
+
+ public LoggingReceiver(ScopeLogger logger) {
+ mStartTime = System.nanoTime();
+ mLogger = logger;
+ }
+
+ /*
+ * @see android.media.midi.MidiReceiver#onReceive(byte[], int, int, long)
+ */
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ StringBuilder sb = new StringBuilder();
+ if (timestamp == 0) {
+ sb.append(String.format("-----0----: "));
+ } else {
+ long monoTime = timestamp - mStartTime;
+ double seconds = (double) monoTime / NANOS_PER_SECOND;
+ sb.append(String.format("%10.3f: ", seconds));
+ }
+ sb.append(MidiPrinter.formatBytes(data, offset, count));
+ sb.append(": ");
+ sb.append(MidiPrinter.formatMessage(data, offset, count));
+ String text = sb.toString();
+ mLogger.log(text);
+ Log.i(TAG, text);
+ }
+
+}
\ No newline at end of file
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/MidiPrinter.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/MidiPrinter.java
new file mode 100644
index 0000000..9e97c04
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/MidiPrinter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 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.midiscope;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceInfo.PortInfo;
+import android.os.Bundle;
+
+import com.example.android.common.midi.MidiConstants;
+
+/**
+ * Format a MIDI message for printing.
+ */
+public class MidiPrinter {
+
+ public static final String[] CHANNEL_COMMAND_NAMES = { "NoteOff", "NoteOn",
+ "PolyTouch", "Control", "Program", "Pressure", "Bend" };
+ public static final String[] SYSTEM_COMMAND_NAMES = { "SysEx", // F0
+ "TimeCode", // F1
+ "SongPos", // F2
+ "SongSel", // F3
+ "F4", // F4
+ "F5", // F5
+ "TuneReq", // F6
+ "EndSysex", // F7
+ "TimingClock", // F8
+ "F9", // F9
+ "Start", // FA
+ "Continue", // FB
+ "Stop", // FC
+ "FD", // FD
+ "ActiveSensing", // FE
+ "Reset" // FF
+ };
+
+ public static String getName(int status) {
+ if (status >= 0xF0) {
+ int index = status & 0x0F;
+ return SYSTEM_COMMAND_NAMES[index];
+ } else if (status >= 0x80) {
+ int index = (status >> 4) & 0x07;
+ return CHANNEL_COMMAND_NAMES[index];
+ } else {
+ return "data";
+ }
+ }
+
+ public static String formatBytes(byte[] data, int offset, int count) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ sb.append(String.format(" %02X", data[offset + i]));
+ }
+ return sb.toString();
+ }
+
+ public static String formatMessage(byte[] data, int offset, int count) {
+ StringBuilder sb = new StringBuilder();
+ byte statusByte = data[offset++];
+ int status = statusByte & 0xFF;
+ sb.append(getName(status)).append("(");
+ int numData = MidiConstants.getBytesPerMessage(statusByte) - 1;
+ if ((status >= 0x80) && (status < 0xF0)) { // channel message
+ int channel = status & 0x0F;
+ // Add 1 for humans who think channels are numbered 1-16.
+ sb.append((channel + 1)).append(", ");
+ }
+ for (int i = 0; i < numData; i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(data[offset++]);
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ public static String formatDeviceInfo(MidiDeviceInfo info) {
+ StringBuilder sb = new StringBuilder();
+ if (info != null) {
+ Bundle properties = info.getProperties();
+ for (String key : properties.keySet()) {
+ Object value = properties.get(key);
+ sb.append(key).append(" = ").append(value).append('\n');
+ }
+ for (PortInfo port : info.getPorts()) {
+ sb.append((port.getType() == PortInfo.TYPE_INPUT) ? "input"
+ : "output");
+ sb.append("[").append(port.getPortNumber()).append("] = \"").append(port.getName()
+ + "\"\n");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/MidiScope.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/MidiScope.java
new file mode 100644
index 0000000..3965d83
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/MidiScope.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2015 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.midiscope;
+
+import android.media.midi.MidiDeviceService;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiReceiver;
+
+import com.example.android.common.midi.MidiFramer;
+
+import java.io.IOException;
+
+/**
+ * Virtual MIDI Device that logs messages to a ScopeLogger.
+ */
+
+public class MidiScope extends MidiDeviceService {
+
+ private static ScopeLogger mScopeLogger;
+ private MidiReceiver mInputReceiver = new MyReceiver();
+ private static MidiFramer mDeviceFramer;
+
+ @Override
+ public MidiReceiver[] onGetInputPortReceivers() {
+ return new MidiReceiver[] { mInputReceiver };
+ }
+
+ public static ScopeLogger getScopeLogger() {
+ return mScopeLogger;
+ }
+
+ public static void setScopeLogger(ScopeLogger logger) {
+ if (logger != null) {
+ // Receiver that prints the messages.
+ LoggingReceiver loggingReceiver = new LoggingReceiver(logger);
+ mDeviceFramer = new MidiFramer(loggingReceiver);
+ }
+ mScopeLogger = logger;
+ }
+
+ private static class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count,
+ long timestamp) throws IOException {
+ if (mScopeLogger != null) {
+ // Send raw data to be parsed into discrete messages.
+ mDeviceFramer.send(data, offset, count, timestamp);
+ }
+ }
+ }
+
+ /**
+ * This will get called when clients connect or disconnect.
+ * Log device information.
+ */
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ if (mScopeLogger != null) {
+ if (status.isInputPortOpen(0)) {
+ mScopeLogger.log("=== connected ===");
+ String text = MidiPrinter.formatDeviceInfo(
+ status.getDeviceInfo());
+ mScopeLogger.log(text);
+ } else {
+ mScopeLogger.log("--- disconnected ---");
+ }
+ }
+ }
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/ScopeLogger.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/ScopeLogger.java
new file mode 100644
index 0000000..dc52efd
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/com/example/android/midiscope/ScopeLogger.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 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.midiscope;
+
+public interface ScopeLogger {
+ /**
+ * Write the text string somewhere that the user can see it.
+ * @param text
+ */
+ void log(String text);
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/mmmlabs/com/mididroid/MidiCallback.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/mmmlabs/com/mididroid/MidiCallback.java
new file mode 100644
index 0000000..c2d5d3b
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/mmmlabs/com/mididroid/MidiCallback.java
@@ -0,0 +1,5 @@
+package mmmlabs.com.mididroid;
+
+public interface MidiCallback {
+ public void midiJackMessage(int device, byte status, byte data1, byte data2);
+}
\ No newline at end of file
diff --git a/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/mmmlabs/com/mididroid/MidiDroid.java b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/mmmlabs/com/mididroid/MidiDroid.java
new file mode 100644
index 0000000..170e347
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/mididroid/src/main/java/mmmlabs/com/mididroid/MidiDroid.java
@@ -0,0 +1,134 @@
+package mmmlabs.com.mididroid;
+
+// Features.
+import android.app.Fragment;
+import android.content.Context;
+import android.media.midi.MidiDeviceInfo;
+import android.os.Bundle;
+
+// Unity
+import com.example.android.common.midi.MidiConstants;
+import com.example.android.common.midi.MidiFramer;
+import com.unity3d.player.UnityPlayer;
+
+// Debug
+import android.util.Log;
+
+// MIDI
+import android.media.midi.MidiManager;
+import android.media.midi.MidiDevice;
+import android.media.midi.*;
+
+import java.io.IOException;
+
+public class MidiDroid extends Fragment {
+
+ // Constants.
+ public static final String TAG = "MidiDROID";
+
+ // Singleton instance.
+ public static MidiDroid instance;
+
+ boolean foundDevice = false;
+
+ int deviceIndex;
+
+ MidiCallback midiCallback;
+
+ MidiManager manager;
+
+ // Receiver that parses raw data into complete messages.
+ MidiFramer connectFramer = new MidiFramer(new MyReceiver());
+
+ public static void start()
+ {
+ // Instantiate and add to Unity Player Activity.
+ Log.i(TAG, "Starting MidiDROID");
+ instance = new MidiDroid();
+ UnityPlayer.currentActivity.getFragmentManager().beginTransaction().add(instance, MidiDroid.TAG).commit();
+ }
+
+ public void findADevice(){
+ foundDevice = false;
+ String[] devices = getDevices();
+ for (int i = 0; i < devices.length; i++){
+ openDeviceAtIndex(i);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true); // Retain between configuration changes (like device rotation)
+
+ manager = (MidiManager)UnityPlayer.currentActivity.getApplicationContext().getSystemService(Context.MIDI_SERVICE);
+ }
+
+ public String[] getDevices(){
+ MidiDeviceInfo[] infos = manager.getDevices();
+ String[] deviceResult = new String[infos.length];
+ for (int i = 0; i < infos.length; i++) {
+ Bundle properties = infos[i].getProperties();
+ String name = properties.getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ if(name != null){
+ Log.i(TAG, "Device Property Name is " + name);
+ deviceResult[i] = name;
+ }
+ }
+ return deviceResult;
+ }
+
+ private MidiOutputPort mOutputPort;
+
+ public void openDeviceAtIndex(int index){
+ if(foundDevice) return;
+
+ MidiDeviceInfo[] infos = manager.getDevices();
+ final MidiDeviceInfo info = infos[index];
+ final int thisIndex = index;
+
+ if (info != null) {
+ manager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if(foundDevice) return;
+
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME));
+ return;
+ } else {
+ mOutputPort = device.openOutputPort(0);
+ if (mOutputPort == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open output port for " + info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME));
+ return;
+ }
+ // mOutputPort.connect(new LogReceiver());
+ mOutputPort.connect(connectFramer);
+ Log.i(TAG, "Opened device " + info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME));
+ foundDevice = true;
+ deviceIndex = thisIndex;
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openOutputPort might take a while.
+ }
+ };
+
+ private class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ if(midiCallback != null){
+ midiCallback.midiJackMessage(deviceIndex, data[0], data[1], data[2]);
+ }
+ }
+ }
+
+ public void setMidiCallback(MidiCallback callback){
+ midiCallback = callback;
+ }
+
+}
diff --git a/AndroidStudio/MidiAndroidPlugin/settings.gradle b/AndroidStudio/MidiAndroidPlugin/settings.gradle
new file mode 100644
index 0000000..762a07c
--- /dev/null
+++ b/AndroidStudio/MidiAndroidPlugin/settings.gradle
@@ -0,0 +1 @@
+include ':mididroid'
diff --git a/Assets/MidiJack/Editor/MidiJackWindow.cs b/Assets/MidiJack/Editor/MidiJackWindow.cs
index 167dd27..c40c255 100644
--- a/Assets/MidiJack/Editor/MidiJackWindow.cs
+++ b/Assets/MidiJack/Editor/MidiJackWindow.cs
@@ -23,12 +23,18 @@
//
using UnityEngine;
using UnityEditor;
+using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace MidiJack
{
class MidiJackWindow : EditorWindow
{
+
+ List allDevices = new List();
+ Dictionary allDevicesBound = new Dictionary();
+ bool _showDeviceManagement = true;
+
#region Custom Editor Window Code
[MenuItem("Window/MIDI Jack")]
@@ -37,30 +43,132 @@ public static void ShowWindow()
EditorWindow.GetWindow("MIDI Jack");
}
+ private void OnEnable()
+ {
+ // Refresh();
+ }
+
+ void Refresh()
+ {
+ RefreshDevices();
+ GetDeviceNames();
+ RestoreDeviceValues();
+ }
+
+ void RestoreDeviceValues()
+ {
+ // Restore state from EditorPrefs
+ var keys = new List();
+ foreach (var item in allDevicesBound.Keys)
+ {
+ keys.Add(item);
+ }
+ foreach (var key in keys)
+ {
+ allDevicesBound[key] = EditorPrefs.GetBool(GetPrefsKeyFromName(key), false);
+ }
+ }
+
void OnGUI()
{
- var endpointCount = CountEndpoints();
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
- // Endpoints
- var temp = "Detected MIDI devices:";
- for (var i = 0; i < endpointCount; i++)
+ // Device Management
+ _showDeviceManagement = EditorGUILayout.Foldout(_showDeviceManagement, "Device Management", true);
+ if (_showDeviceManagement)
{
- var id = GetEndpointIdAtIndex(i);
- var name = GetEndpointName(id);
- temp += "\n" + id.ToString("X8") + ": " + name;
+ var endpointCount = CountEndpoints();
+
+ EditorGUILayout.Space();
+
+ if (GUILayout.Button("Refresh"))
+ {
+ Refresh();
+ }
+
+ // Device Buttons
+ for (uint i = 0; i < allDevices.Count; i++)
+ {
+ string name = allDevices[(int)i];
+ bool newValue = (GUILayout.Toggle(allDevicesBound[name], name));
+ if (newValue != allDevicesBound[name])
+ {
+ if (newValue)
+ {
+ CloseAllDevices();
+ OpenDevice(i);
+ }
+ else
+ {
+ CloseAllDevices();
+ }
+
+ SetDeviceState(name, newValue);
+ }
+ }
+
+ // Close All Button
+ var closeButtonStyle = new GUIStyle(GUI.skin.button);
+ closeButtonStyle.normal.textColor = Color.red;
+ if (GUILayout.Button("Close All Devices", closeButtonStyle))
+ {
+ CloseAllDevices();
+ Repaint();
+ }
+
}
- EditorGUILayout.HelpBox(temp, MessageType.None);
+
+ EditorGUILayout.Space();
+
// Message history
- temp = "Recent MIDI messages:";
+ var temp = "Recent MIDI messages:";
foreach (var message in MidiDriver.Instance.History)
temp += "\n" + message.ToString();
EditorGUILayout.HelpBox(temp, MessageType.None);
}
- #endregion
+ void SetDeviceState(string deviceName, bool value)
+ {
+ EditorPrefs.SetBool(GetPrefsKeyFromName(deviceName), value);
+ allDevicesBound[deviceName] = value;
+ }
+
+ string GetPrefsKeyFromName(string name)
+ {
+ return string.Format("MidiJackDeviceEnabled-{0}", name);
+ }
- #region Update And Repaint
+ void CloseAllDevices()
+ {
+ List keys = new List(allDevicesBound.Keys);
+ foreach (string key in keys)
+ {
+ SetDeviceState(key, false);
+ }
+ CloseDevices();
+ }
+
+ void GetDeviceNames()
+ {
+ allDevices = new List();
+ var endpointCount = CountEndpoints();
+ for (int i = 0; i < endpointCount; i++)
+ {
+ var name = GetEndpointName((uint)i);
+
+ allDevices.Add(name);
+ if (!allDevicesBound.ContainsKey(name))
+ {
+ allDevicesBound.Add(name, EditorPrefs.GetBool(GetPrefsKeyFromName(name), false));
+ }
+ }
+ }
+
+#endregion
+
+#region Update And Repaint
const int _updateInterval = 15;
int _countToUpdate;
@@ -79,9 +187,9 @@ void Update()
_countToUpdate = _updateInterval;
}
- #endregion
+#endregion
- #region Native Plugin Interface
+#region Native Plugin Interface
[DllImport("MidiJackPlugin", EntryPoint="MidiJackCountEndpoints")]
static extern int CountEndpoints();
@@ -96,6 +204,18 @@ static string GetEndpointName(uint id) {
return Marshal.PtrToStringAnsi(MidiJackGetEndpointName(id));
}
- #endregion
+ [DllImport("MidiJackPlugin", EntryPoint = "MidiJackCloseAllDevices")]
+ static extern void CloseDevices();
+
+ [DllImport("MidiJackPlugin", EntryPoint = "MidiJackCloseDevice")]
+ static extern void CloseDevice(uint index);
+
+ [DllImport("MidiJackPlugin", EntryPoint = "MidiJackOpenDevice")]
+ static extern void OpenDevice(uint index);
+
+ [DllImport("MidiJackPlugin", EntryPoint = "MidiJackRefreshDevices")]
+ static extern void RefreshDevices();
+
+#endregion
}
}
diff --git a/Assets/MidiJack/Midi.cs b/Assets/MidiJack/Midi.cs
index 46fde7e..495970a 100644
--- a/Assets/MidiJack/Midi.cs
+++ b/Assets/MidiJack/Midi.cs
@@ -61,6 +61,14 @@ public MidiMessage(ulong data)
data2 = (byte)((data >> 48) & 0xff);
}
+ public MidiMessage(uint source, byte status, byte data1, byte data2)
+ {
+ this.source = source;
+ this.status = status;
+ this.data1 = data1;
+ this.data2 = data2;
+ }
+
public override string ToString()
{
const string fmt = "s({0:X2}) d({1:X2},{2:X2}) from {3:X8}";
diff --git a/Assets/MidiJack/MidiDriver.cs b/Assets/MidiJack/MidiDriver.cs
index ffc408f..a16281b 100644
--- a/Assets/MidiJack/MidiDriver.cs
+++ b/Assets/MidiJack/MidiDriver.cs
@@ -202,6 +202,10 @@ void Update()
}
}
+#if UNITY_ANDROID && !UNITY_EDITOR
+ return;
+#endif
+
// Process the message queue.
while (true)
{
@@ -266,12 +270,61 @@ void Update()
#region Native Plugin Interface
- [DllImport("MidiJackPlugin", EntryPoint="MidiJackDequeueIncomingData")]
+ #if UNITY_ANDROID && !UNITY_EDITOR
+
+ private void HandleMidiMessage(object sender, MidiMessage message)
+ {
+ // Split the first byte.
+ var statusCode = message.status >> 4;
+ var channelNumber = message.status & 0xf;
+
+ // Note on message?
+ if (statusCode == 9)
+ {
+ Debug.LogFormat("Getting {0} On", message.data1);
+ var velocity = 1.0f / 127 * message.data2 + 1;
+ _channelArray[channelNumber]._noteArray[message.data1] = velocity;
+ _channelArray[(int)MidiChannel.All]._noteArray[message.data1] = velocity;
+ if (noteOnDelegate != null)
+ noteOnDelegate((MidiChannel)channelNumber, message.data1, velocity - 1);
+ }
+
+ // Note off message?
+ if (statusCode == 8 || (statusCode == 9 && message.data2 == 0))
+ {
+ Debug.LogFormat("Getting {0} Off", message.data1);
+ _channelArray[channelNumber]._noteArray[message.data1] = -1;
+ _channelArray[(int)MidiChannel.All]._noteArray[message.data1] = -1;
+ if (noteOffDelegate != null)
+ noteOffDelegate((MidiChannel)channelNumber, message.data1);
+ }
+
+ // CC message?
+ if (statusCode == 0xb)
+ {
+ // Normalize the value.
+ var level = 1.0f / 127 * message.data2;
+ // Update the channel if it already exists, or add a new channel.
+ _channelArray[channelNumber]._knobMap[message.data1] = level;
+ // Do again for All-ch.
+ _channelArray[(int)MidiChannel.All]._knobMap[message.data1] = level;
+ if (knobDelegate != null)
+ knobDelegate((MidiChannel)channelNumber, message.data1, level);
+ }
+ }
+
+ private MidiDroid midiDroid;
+ public ulong DequeueIncomingData(){
+ return 0;
+ }
+ #else
+ [DllImport("MidiJackPlugin", EntryPoint = "MidiJackDequeueIncomingData")]
public static extern ulong DequeueIncomingData();
+ #endif
- #endregion
+#endregion
- #region Singleton Class Instance
+#region Singleton Class Instance
static MidiDriver _instance;
@@ -280,8 +333,15 @@ public static MidiDriver Instance {
if (_instance == null) {
_instance = new MidiDriver();
if (Application.isPlaying)
+ {
MidiStateUpdater.CreateGameObject(
new MidiStateUpdater.Callback(_instance.Update));
+#if UNITY_ANDROID && !UNITY_EDITOR
+ _instance.midiDroid = new MidiDroid();
+ _instance.midiDroid.Start();
+ _instance.midiDroid.callback.DroidMidiEvent += _instance.HandleMidiMessage;
+#endif
+ }
}
return _instance;
}
diff --git a/Assets/MidiJack/MidiDroid.meta b/Assets/MidiJack/MidiDroid.meta
new file mode 100644
index 0000000..73928c5
--- /dev/null
+++ b/Assets/MidiJack/MidiDroid.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 114b94bf497c5ed4daa6fb83bcb20abd
+folderAsset: yes
+timeCreated: 1492888139
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/MidiJack/MidiDroid/MidiDroid.cs b/Assets/MidiJack/MidiDroid/MidiDroid.cs
new file mode 100644
index 0000000..4c274e5
--- /dev/null
+++ b/Assets/MidiJack/MidiDroid/MidiDroid.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace MidiJack
+{
+ public class MidiDroid
+ {
+
+ List deviceNames;
+
+ // Android Glue
+ AndroidJavaClass _class;
+ AndroidJavaObject mdPlugin { get { return _class.GetStatic("instance"); } }
+ public MidiDroidCallback callback;
+
+ int currentDevice = -1;
+
+ public void Start()
+
+ {
+#if UNITY_ANDROID && !UNITY_EDITOR
+ _class = new AndroidJavaClass("mmmlabs.com.mididroid.MidiDroid");
+ _class.CallStatic("start");
+#endif
+ callback = new MidiDroidCallback();
+ mdPlugin.Call("setMidiCallback", callback);
+ mdPlugin.Call("findADevice");
+ /*
+ string currentDeviceName = UnityEngine.PlayerPrefs.GetString("MidiDroidDevice", "");
+ if(currentDeviceName.Length > 0)
+ {
+ int index = IndexOfDeviceNamed(currentDeviceName);
+ if(index > -1)
+ {
+ TryOpenDeviceAt(index);
+ }
+ }
+ else
+ {
+ TryOpenNextDevice();
+ }
+ */
+ // TryOpenDeviceAt(2);
+ }
+
+ public void SetCallback(MidiDroidCallback callback)
+ {
+ mdPlugin.Call("setMidiCallback", callback);
+ }
+
+ #region MidiJack Methods
+ public MidiMessage GetNextMessage()
+ {
+ MidiMessage m = new MidiMessage(0);
+ AndroidJavaObject obj = mdPlugin.Call("getIncoming");
+ if (obj.GetRawObject().ToInt32() != 0)
+ {
+ // byte[] returned with some data!
+ byte[][] result = AndroidJNIHelper.ConvertFromJNIArray(obj.GetRawObject());
+ if(result.Length == 0)
+ {
+ /*
+ // return empty message
+ m.source = 999;
+ m.status = 0;
+ m.data1 = 0;
+ m.data2 = 0;
+ */
+ }
+ else
+ {
+ Debug.LogFormat("Unity Got {0} messages", result.Length);
+ for (int i = 0; i < result.Length; i++)
+ {
+ Debug.LogFormat("Messages {0} is {1} {2} {3}", i, result[i][0], result[i][1], result[i][2]);
+ }
+ /*
+ m.source = (uint)currentDevice;
+ m.status = result[0];
+ m.data1 = result[1];
+ m.data2 = result[2];
+ */
+ }
+ }
+ else
+ {
+ m.source = 999;
+ Debug.LogError("Couldn't parse returned Java Object");
+ }
+ return m;
+ }
+
+ public ulong DequeueIncomingData()
+ {
+
+ if(mdPlugin != null)
+ {
+ AndroidJavaObject obj = mdPlugin.Call("getIncoming");
+ if (obj.GetRawObject().ToInt64() != 0)
+ {
+ return (ulong)obj.GetRawObject().ToInt64();
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ return 0;
+ }
+
+ return 0;
+ }
+
+ public int MidiJackCountEndpoints()
+ {
+ return 0;
+ }
+
+ public uint GetEndpointIdAtIndex(int index)
+ {
+ return 0;
+ }
+
+ public string GetEndpointName(uint id)
+ {
+ return "";
+ }
+ #endregion
+
+ private int IndexOfDeviceNamed(string midiDroidDevice)
+ {
+ int result = -1;
+ getDeviceList();
+ for (int i = 0; i < deviceNames.Count; i++)
+ {
+ if(deviceNames[i] == midiDroidDevice)
+ {
+ result = i;
+ }
+ }
+ return result;
+ }
+
+ public void TryOpenDeviceAt(int deviceIndex)
+ {
+ mdPlugin.Call("openDeviceAtIndex", deviceIndex);
+ currentDevice = deviceIndex;
+ // UnityEngine.PlayerPrefs.SetString("MidiDroidDevice", deviceNames[deviceIndex]);
+ }
+
+ public void TryOpenNextDevice()
+ {
+ getDeviceList();
+
+ if (deviceNames.Count == 0)
+ {
+ throw new Exception("No Devices available to open");
+ }
+
+ int nextDevice = currentDevice + 1;
+ if(nextDevice >= deviceNames.Count)
+ {
+ nextDevice = 0;
+ }
+
+ TryOpenDeviceAt(nextDevice);
+ }
+
+ public void getDeviceList()
+ {
+ deviceNames = new List();
+
+ //some methods to set the object that you want to call the method on
+ AndroidJavaObject obj = mdPlugin.Call("getDevices");
+ if (obj.GetRawObject().ToInt32() != 0)
+ {
+ // String[] returned with some data!
+ System.String[] result = AndroidJNIHelper.ConvertFromJNIArray
+ (obj.GetRawObject());
+ foreach (System.String str in result)
+ {
+ // Do something with the strings
+ deviceNames.Add(str);
+ }
+ }
+ else
+ {
+ Debug.LogErrorFormat("Got null strings back from getDevices");
+ // null String[] returned
+ }
+ obj.Dispose();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Assets/MidiJack/MidiDroid/MidiDroid.cs.meta b/Assets/MidiJack/MidiDroid/MidiDroid.cs.meta
new file mode 100644
index 0000000..1bbabe7
--- /dev/null
+++ b/Assets/MidiJack/MidiDroid/MidiDroid.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 7ead4ca3d79c9c34eacfb5b354d02e37
+timeCreated: 1492307868
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/MidiJack/MidiDroid/MidiDroidCallback.cs b/Assets/MidiJack/MidiDroid/MidiDroidCallback.cs
new file mode 100644
index 0000000..81b610a
--- /dev/null
+++ b/Assets/MidiJack/MidiDroid/MidiDroidCallback.cs
@@ -0,0 +1,20 @@
+using UnityEngine;
+
+namespace MidiJack
+{
+ public class MidiDroidCallback: AndroidJavaProxy
+ {
+ public delegate void RawMidiDelegate(object sender, MidiMessage m);
+ public event RawMidiDelegate DroidMidiEvent;
+
+ public MidiDroidCallback() : base("mmmlabs.com.mididroid.MidiCallback") { }
+
+ public void midiJackMessage(int deviceIndex, byte status, byte data1, byte data2)
+ {
+ if(DroidMidiEvent != null)
+ {
+ DroidMidiEvent(this, new MidiMessage((uint)deviceIndex, status, data1, data2));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/MidiJack/MidiDroid/MidiDroidCallback.cs.meta b/Assets/MidiJack/MidiDroid/MidiDroidCallback.cs.meta
new file mode 100644
index 0000000..bb4b0fc
--- /dev/null
+++ b/Assets/MidiJack/MidiDroid/MidiDroidCallback.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 0dcf97471bc84f5419fb7f7f5d42c4b0
+timeCreated: 1492494905
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/MidiJack/Plugins/Android.meta b/Assets/MidiJack/Plugins/Android.meta
new file mode 100644
index 0000000..56c8e65
--- /dev/null
+++ b/Assets/MidiJack/Plugins/Android.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 7da7907ed041ceb4aa12c385cd04d68e
+folderAsset: yes
+timeCreated: 1492894266
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/MidiJack/Plugins/Android/MidiJackPlugin.aar b/Assets/MidiJack/Plugins/Android/MidiJackPlugin.aar
new file mode 100644
index 0000000..fd92cb6
Binary files /dev/null and b/Assets/MidiJack/Plugins/Android/MidiJackPlugin.aar differ
diff --git a/Assets/MidiJack/Plugins/Android/MidiJackPlugin.aar.meta b/Assets/MidiJack/Plugins/Android/MidiJackPlugin.aar.meta
new file mode 100644
index 0000000..4206d02
--- /dev/null
+++ b/Assets/MidiJack/Plugins/Android/MidiJackPlugin.aar.meta
@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 17c23874caba1a146a23f54dfd163b1a
+timeCreated: 1492894266
+licenseType: Free
+PluginImporter:
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ isPreloaded: 0
+ isOverridable: 0
+ platformData:
+ data:
+ first:
+ Android: Android
+ second:
+ enabled: 1
+ settings: {}
+ data:
+ first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ data:
+ first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle.meta
deleted file mode 100644
index 2b6fc35..0000000
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle.meta
+++ /dev/null
@@ -1,68 +0,0 @@
-fileFormatVersion: 2
-guid: 744350758f8fa49e69d70d2300c2133e
-folderAsset: yes
-timeCreated: 1434713393
-licenseType: Pro
-PluginImporter:
- serializedVersion: 1
- iconMap: {}
- executionOrder: {}
- isPreloaded: 0
- platformData:
- Android:
- enabled: 0
- settings:
- CPU: AnyCPU
- Any:
- enabled: 0
- settings: {}
- Editor:
- enabled: 1
- settings:
- CPU: AnyCPU
- DefaultValueInitialized: true
- OS: OSX
- Linux:
- enabled: 1
- settings:
- CPU: x86
- Linux64:
- enabled: 1
- settings:
- CPU: x86_64
- LinuxUniversal:
- enabled: 1
- settings:
- CPU: AnyCPU
- OSXIntel:
- enabled: 1
- settings:
- CPU: AnyCPU
- OSXIntel64:
- enabled: 1
- settings:
- CPU: AnyCPU
- OSXUniversal:
- enabled: 1
- settings:
- CPU: AnyCPU
- SamsungTV:
- enabled: 0
- settings:
- STV_MODEL: STANDARD_13
- Win:
- enabled: 1
- settings:
- CPU: AnyCPU
- Win64:
- enabled: 1
- settings:
- CPU: AnyCPU
- iOS:
- enabled: 0
- settings:
- CompileFlags:
- FrameworkDependencies:
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents.meta
index a167ee7..735a42a 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents.meta
@@ -1,7 +1,7 @@
fileFormatVersion: 2
-guid: ad530dc3d37874bd0bae73599d790f33
+guid: 3a19345d0af00441eb94f92912f15e30
folderAsset: yes
-timeCreated: 1434713393
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist
index 988349c..eb8fa7f 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist
@@ -3,7 +3,7 @@
BuildMachineOSBuild
- 14D136
+ 15G1217
CFBundleDevelopmentRegion
English
CFBundleExecutable
@@ -20,6 +20,10 @@
1.0
CFBundleSignature
????
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
CFBundleVersion
1
CFPlugInDynamicRegisterFunction
@@ -43,17 +47,17 @@
DTCompiler
com.apple.compilers.llvm.clang.1_0
DTPlatformBuild
- 6D2105
+ 8C1002
DTPlatformVersion
GM
DTSDKBuild
- 14D125
+ 16C58
DTSDKName
- macosx10.10
+ macosx10.12
DTXcode
- 0632
+ 0821
DTXcodeBuild
- 6D2105
+ 8C1002
NSHumanReadableCopyright
Copyright © 2013 Keijiro Takahashi. All rights reserved.
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist.meta
index c35ce63..3752999 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Info.plist.meta
@@ -1,6 +1,6 @@
fileFormatVersion: 2
-guid: b69f6e983d34f47bb88a95417b265f33
-timeCreated: 1434713393
+guid: f69a1655e0aa3439b8ddee2ba64e0d4c
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS.meta
index 5d4c16b..db90612 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS.meta
@@ -1,7 +1,7 @@
fileFormatVersion: 2
-guid: 4885c6a494d924beabe72827da0757b4
+guid: cd810e48e5df84a30ad04d38de30b84a
folderAsset: yes
-timeCreated: 1434713393
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin
index 83b54e9..62b9d73 100755
Binary files a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin and b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin differ
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin.meta
index bb36e38..05b8388 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/MacOS/MidiJackPlugin.meta
@@ -1,6 +1,6 @@
fileFormatVersion: 2
-guid: 68754cade329c4f3f8625841612cf8ad
-timeCreated: 1434713393
+guid: 3f8243ae7531d4ca09fc2f98931a771a
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources.meta
index 90d5224..61a34b7 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources.meta
@@ -1,7 +1,7 @@
fileFormatVersion: 2
-guid: 58f45b20f8d5a4538bfab63ee9eb4f44
+guid: e9fce332efdb14de3baa9e7e37d2a099
folderAsset: yes
-timeCreated: 1434713393
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj.meta
index 5b1e3de..54eb143 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj.meta
@@ -1,7 +1,7 @@
fileFormatVersion: 2
-guid: 0328ce934fb9e49338a62fd86a58b099
+guid: 231d44607140c4c1caf8f9d59e3628be
folderAsset: yes
-timeCreated: 1434713393
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.meta b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.meta
index 44c1cc5..4a5cfc2 100644
--- a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.meta
+++ b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.meta
@@ -1,6 +1,6 @@
fileFormatVersion: 2
-guid: b8697363c91774658969851a7121b732
-timeCreated: 1434713393
+guid: 20fad39d65c684df8addc1db14957912
+timeCreated: 1504040267
licenseType: Pro
DefaultImporter:
userData:
diff --git a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.txt
similarity index 67%
rename from Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings
rename to Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.txt
index 5e45963..42522ec 100644
Binary files a/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings and b/Assets/MidiJack/Plugins/MidiJackPlugin.bundle/Contents/Resources/en.lproj/InfoPlist.strings.txt differ
diff --git a/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll b/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll
index d006dbe..6f413cc 100644
Binary files a/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll and b/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll differ
diff --git a/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll.meta b/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll.meta
index 5f38d72..5bb2368 100644
--- a/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll.meta
+++ b/Assets/MidiJack/Plugins/x64/MidiJackPlugin.dll.meta
@@ -1,80 +1,140 @@
fileFormatVersion: 2
guid: 6a16b8fbf2730aa4089342d8f3df936e
-timeCreated: 1434808833
-licenseType: Pro
+timeCreated: 1495396271
+licenseType: Free
PluginImporter:
- serializedVersion: 1
+ serializedVersion: 2
iconMap: {}
executionOrder: {}
isPreloaded: 0
+ isOverridable: 0
platformData:
- Android:
- enabled: 0
- settings:
- CPU: AnyCPU
- Any:
- enabled: 0
- settings: {}
- Editor:
- enabled: 1
- settings:
- CPU: x86_64
- DefaultValueInitialized: true
- OS: Windows
- Linux:
- enabled: 1
- settings:
- CPU: x86
- Linux64:
- enabled: 1
- settings:
- CPU: x86_64
- LinuxUniversal:
- enabled: 1
- settings:
- CPU: AnyCPU
- OSXIntel:
- enabled: 1
- settings:
- CPU: AnyCPU
- OSXIntel64:
- enabled: 1
- settings:
- CPU: AnyCPU
- OSXUniversal:
- enabled: 1
- settings:
- CPU: AnyCPU
- SamsungTV:
- enabled: 0
- settings:
- STV_MODEL: STANDARD_13
- WP8:
- enabled: 0
- settings:
- CPU: AnyCPU
- DontProcess: False
- PlaceholderPath:
- Win:
- enabled: 0
- settings:
- CPU: None
- Win64:
- enabled: 1
- settings:
- CPU: AnyCPU
- WindowsStoreApps:
- enabled: 0
- settings:
- CPU: AnyCPU
- DontProcess: False
- PlaceholderPath:
- SDK: AnySDK
- iOS:
- enabled: 0
- settings:
- CompileFlags:
- FrameworkDependencies:
+ data:
+ first:
+ '': WP8
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ DontProcess: False
+ PlaceholderPath:
+ data:
+ first:
+ Android: Android
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ data:
+ first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ DefaultValueInitialized: true
+ OS: Windows
+ data:
+ first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ data:
+ first:
+ Facebook: Win64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Samsung TV: SamsungTV
+ second:
+ enabled: 0
+ settings:
+ STV_MODEL: STANDARD_13
+ data:
+ first:
+ Standalone: Linux
+ second:
+ enabled: 1
+ settings:
+ CPU: x86
+ data:
+ first:
+ Standalone: Linux64
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ data:
+ first:
+ Standalone: LinuxUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Standalone: OSXIntel
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Standalone: OSXIntel64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ data:
+ first:
+ Standalone: Win64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ data:
+ first:
+ Windows Store Apps: WindowsStoreApps
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ DontProcess: False
+ PlaceholderPath:
+ SDK: AnySDK
+ data:
+ first:
+ iPhone: iOS
+ second:
+ enabled: 0
+ settings:
+ CompileFlags:
+ FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
diff --git a/MidiJack.unitypackage b/MidiJack.unitypackage
index a3c88b4..35d28d6 100644
Binary files a/MidiJack.unitypackage and b/MidiJack.unitypackage differ
diff --git a/README.md b/README.md
index 745f3d2..339f91c 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ System Requirements
-------------------
- Unity 5
-- Windows or Mac OS X
+- Windows, Mac OS X or Android 6+
Installation
------------
@@ -74,16 +74,17 @@ The MIDI Monitor window is avilable from the menu Window -> MIDI Jack.
Current Limitations
-------------------
-- Currently MIDI Jack only supports Windows and OS X. No iOS support yet.
+- Currently MIDI Jack only supports Windows, OS X and Android. No iOS support yet.
- Only supports note and CC messages. No support for program changes nor
SysEx.
-- The MIDI Jack plugin always tries to capture all available MIDI devices.
- On Windows this behavior may conflict with other MIDI applications.
+- The MIDI Jack plugin always tries to capture all available MIDI devices on OS X.
+- On Windows you can use the MidiJack Window to choose which device to open.
+- On Android, it will cycle through available MIDI devices until it succeeds in opening one or runs out of devices.
License
-------
-Copyright (C) 2013-2015 Keijiro Takahashi
+Copyright (C) 2013-2017 Keijiro Takahashi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/VisualStudio/MidiJackPlugin/MidiJackPlugin.cpp b/VisualStudio/MidiJackPlugin/MidiJackPlugin.cpp
index 1106e54..34281b3 100644
--- a/VisualStudio/MidiJackPlugin/MidiJackPlugin.cpp
+++ b/VisualStudio/MidiJackPlugin/MidiJackPlugin.cpp
@@ -63,6 +63,7 @@ namespace
std::queue message_queue;
// Device handler lists
+ std::list all_devices;
std::list active_handles;
std::stack handles_to_close;
@@ -130,6 +131,11 @@ namespace
resource_lock.unlock();
}
+ void CloseDevice(unsigned int id) {
+ auto handle = DeviceIDToHandle(id);
+ CloseDevice(handle);
+ }
+
// Open the all devices.
void OpenAllDevices()
{
@@ -137,6 +143,20 @@ namespace
for (int i = 0; i < device_count; i++) OpenDevice(i);
}
+ void CacheDeviceNames() {
+ // Create list of device names
+ MIDIINCAPS caps;
+ int device_count = midiInGetNumDevs();
+ all_devices.clear();
+ for (int i = 0; i < device_count; i++) {
+ midiInGetDevCaps(i, &caps, sizeof(MIDIINCAPS));
+ std::wstring wname(caps.szPname);
+ std::string name = std::string(wname.begin(), wname.end());
+ all_devices.push_back(name);
+ }
+
+ }
+
// Refresh device handlers
void RefreshDevices()
{
@@ -148,8 +168,7 @@ namespace
handles_to_close.pop();
}
- // Try open all devices to detect newly connected ones.
- OpenAllDevices();
+ CacheDeviceNames();
resource_lock.unlock();
}
@@ -171,7 +190,7 @@ namespace
// Counts the number of endpoints.
EXPORT_API int MidiJackCountEndpoints()
{
- return static_cast(active_handles.size());
+ return static_cast(all_devices.size());
}
// Get the unique ID of an endpoint.
@@ -194,8 +213,6 @@ EXPORT_API const char* MidiJackGetEndpointName(uint32_t id)
// Retrieve and erase an MIDI message data from the message queue.
EXPORT_API uint64_t MidiJackDequeueIncomingData()
{
- RefreshDevices();
-
if (message_queue.empty()) return 0;
resource_lock.lock();
@@ -205,3 +222,22 @@ EXPORT_API uint64_t MidiJackDequeueIncomingData()
return msg.Encode64Bit();
}
+
+// Free all open devices
+EXPORT_API void MidiJackCloseAllDevices()
+{
+ CloseAllDevices();
+}
+
+// Open Specific devices
+EXPORT_API void MidiJackOpenDevice(unsigned int index) {
+ OpenDevice(index);
+}
+
+EXPORT_API void MidiJackCloseDevice(unsigned int index) {
+ CloseDevice(index);
+}
+
+EXPORT_API void MidiJackRefreshDevices() {
+ RefreshDevices();
+}
\ No newline at end of file
diff --git a/VisualStudio/MidiJackPlugin/stdafx.h b/VisualStudio/MidiJackPlugin/stdafx.h
index dd65f83..8ee05a0 100644
--- a/VisualStudio/MidiJackPlugin/stdafx.h
+++ b/VisualStudio/MidiJackPlugin/stdafx.h
@@ -13,4 +13,5 @@
#include
#include
#include
-#include
\ No newline at end of file
+#include
+#include
\ No newline at end of file
diff --git a/Xcode/MidiJackPlugin/PluginEntry.cpp b/Xcode/MidiJackPlugin/PluginEntry.cpp
index bb782f3..fa0f1f7 100644
--- a/Xcode/MidiJackPlugin/PluginEntry.cpp
+++ b/Xcode/MidiJackPlugin/PluginEntry.cpp
@@ -147,6 +147,51 @@ namespace
return buffer;
}
+
+ void OpenDevice(unsigned int i) {
+ MIDIEndpointRef source = MIDIGetSource(i);
+ if (source == 0) return;
+
+ // Retrieve the ID of the source.
+ SInt32 id;
+ if (MIDIObjectGetIntegerProperty(source, kMIDIPropertyUniqueID, &id) != noErr) return;
+ source_ids.at(i) = id;
+
+ // Connect the MIDI source to the input port.
+ if (MIDIPortConnectSource(midi_port, source, reinterpret_cast(id)) != noErr) return;
+ }
+
+ void CloseDevice(unsigned int id) {
+ if (midi_client != 0) {
+
+ MIDIEndpointRef source = MIDIGetSource(id);
+ if (source == 0) return;
+
+ MIDIPortDisconnectSource(midi_port, source);
+
+ }
+ }
+
+ // Close the all devices.
+ void CloseAllDevices()
+ {
+ if (midi_client != 0) {
+ ItemCount sourceCount = MIDIGetNumberOfSources();
+ source_ids.resize(sourceCount);
+
+ for (int i = 0; i < sourceCount; i++)
+ {
+ MIDIEndpointRef source = MIDIGetSource(i);
+ if (source == 0) return;
+
+ MIDIPortDisconnectSource(midi_port, source);
+ }
+ }
+ }
+
+ void RefreshDevices() {
+ // Stub for now
+ }
}
#pragma mark Exposed functions
@@ -167,8 +212,9 @@ extern "C" uint32_t MidiJackGetEndpointIDAtIndex(int index)
}
// Get the name of an endpoint.
-extern "C" const char* MidiJackGetEndpointName(uint32_t id)
+extern "C" const char* MidiJackGetEndpointName(int index)
{
+ uint32_t id = MidiJackGetEndpointIDAtIndex(index);
if (!ResetIfRequired()) return "(not ready)";
static std::string temp;
temp = GetSourceName(id);
@@ -187,3 +233,21 @@ extern "C" uint64_t MidiJackDequeueIncomingData()
return m.Encode64Bit();
}
+
+extern "C" void MidiJackCloseAllDevices()
+{
+ CloseAllDevices();
+}
+
+// Open Specific devices
+extern "C" void MidiJackOpenDevice(unsigned int index) {
+ OpenDevice(index);
+}
+
+extern "C" void MidiJackCloseDevice(unsigned int index) {
+ CloseDevice(index);
+}
+
+extern "C" void MidiJackRefreshDevices() {
+ RefreshDevices();
+}
\ No newline at end of file