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