From a30eef65a7eaaf1597667eab4319bbb1f52f519c Mon Sep 17 00:00:00 2001 From: Yellow Labrador <132696731+YellowLabrador@users.noreply.github.com> Date: Tue, 16 May 2023 18:48:15 -0700 Subject: [PATCH 1/6] Added Android --- CMakeLists.txt | 26 ++- README.md | 2 +- RtMidi.cpp | 486 ++++++++++++++++++++++++++++++++++++++++++++++++- RtMidi.h | 1 + configure.ac | 7 + rtmidi_c.cpp | 1 + rtmidi_c.h | 1 + 7 files changed, 511 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f5e997..0d4e2ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ # CopyPolicy: RtMidi license. # Set minimum CMake required version for this project. -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) # Define a C++ project. project(RtMidi LANGUAGES CXX C) @@ -40,11 +40,10 @@ set(RTMIDI_TARGETNAME_UNINSTALL "uninstall" CACHE STRING "Name of 'uninstall' bu # API Options option(RTMIDI_API_JACK "Compile with JACK support." ${HAVE_JACK}) -if(UNIX AND NOT APPLE) - option(RTMIDI_API_ALSA "Compile with ALSA support." ON) -endif() option(RTMIDI_API_WINMM "Compile with WINMM support." ${WIN32}) option(RTMIDI_API_CORE "Compile with CoreMIDI support." ${APPLE}) +option(RTMIDI_API_ALSA "Compile with ALSA support." ${ALSA}) +option(RTMIDI_API_AMIDI "Compile with Android support." ${ANDROID}) # Add -Wall if possible if (CMAKE_COMPILER_IS_GNUCXX) @@ -119,12 +118,9 @@ if(RTMIDI_API_JACK) endif() # ALSA -if(RTMIDI_API_ALSA) +find_package(ALSA) +if(ALSA_FOUND OR ALSA) set(NEED_PTHREAD ON) - find_package(ALSA) - if (NOT ALSA_FOUND) - message(FATAL_ERROR "ALSA API requested but no ALSA dev libraries found") - endif() list(APPEND INCDIRS ${ALSA_INCLUDE_DIR}) list(APPEND LINKLIBS ALSA::ALSA) list(APPEND PKGCONFIG_REQUIRES "alsa") @@ -153,6 +149,18 @@ if(RTMIDI_API_CORE) list(APPEND LINKFLAGS "-Wl,-F/Library/Frameworks") endif() +# Android AMIDI +if(ANDROID_NDK) + set(NEED_PTHREAD ON) + set(JAVA_INCLUDE_PATH2 NotNeeded) + set(JAVA_AWT_INCLUDE_PATH NotNeeded) + find_package(JNI) +# find_library(ALOG_LIB log android) + list(APPEND API_DEFS "-D__AMIDI__") + list(APPEND API_LIST "amidi") + list(APPEND LINKLIBS log ${JNI_LIBRARIES} amidi) +endif() + # pthread if (NEED_PTHREAD) find_package(Threads REQUIRED diff --git a/README.md b/README.md index bdfab02..1067e30 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Build Status](https://github.com/thestk/rtmidi/actions/workflows/ci.yml/badge.svg) -A set of C++ classes that provide a common API for realtime MIDI input/output across Linux (ALSA & JACK), Macintosh OS X (CoreMIDI & JACK) and Windows (Multimedia). +A set of C++ classes that provide a common API for realtime MIDI input/output across Linux (ALSA & JACK), Macintosh OS X (CoreMIDI & JACK), Windows (Multimedia) and Android. By Gary P. Scavone, 2003-2021. diff --git a/RtMidi.cpp b/RtMidi.cpp index cfd4263..846faac 100644 --- a/RtMidi.cpp +++ b/RtMidi.cpp @@ -80,7 +80,7 @@ // // **************************************************************** // -#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) && !defined(TARGET_IPHONE_OS) && !defined(__WEB_MIDI_API__) +#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) && !defined(TARGET_IPHONE_OS) && !defined(__WEB_MIDI_API__) && !defined(__AMIDI__) #define __RTMIDI_DUMMY__ #endif @@ -308,6 +308,69 @@ class MidiOutWeb: public MidiOutApi #endif +#if defined(__AMIDI__) + +#define LOG_TAG "RtMidi" +#include +#include +#include +#include +#include +#include +#include +#include + +class MidiInAndroid : public MidiInApi +{ + public: + MidiInAndroid(const std::string &/*clientName*/, unsigned int queueSizeLimit ); + ~MidiInAndroid( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::ANDROID_AMIDI; }; + void openPort( unsigned int portNumber, const std::string &portName ); + void openVirtualPort( const std::string &portName ); + void closePort( void ); + void setClientName( const std::string &clientName ); + void setPortName( const std::string &portName ); + unsigned int getPortCount( void ); + std::string getPortName( unsigned int portNumber ); + + void onMidiMessage( uint8_t* data, double domHishResTimeStamp ); + + protected: + void initialize( const std::string& clientName ); + void connect(); + AMidiDevice* receiveDevice = NULL; + AMidiOutputPort* midiOutputPort = NULL; + pthread_t readThread; + std::atomic reading = ATOMIC_VAR_INIT(false); + static void* pollMidi(void* context); + int64_t lastTime; +}; + +class MidiOutAndroid: public MidiOutApi +{ + public: + MidiOutAndroid( const std::string &clientName ); + ~MidiOutAndroid( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::ANDROID_AMIDI; }; + void openPort( unsigned int portNumber, const std::string &portName ); + void openVirtualPort( const std::string &portName ); + void closePort( void ); + void setClientName( const std::string &clientName ); + void setPortName( const std::string &portName ); + unsigned int getPortCount( void ); + std::string getPortName( unsigned int portNumber ); + void sendMessage( const unsigned char *message, size_t size ); + + protected: + void initialize( const std::string& clientName ); + void connect(); + AMidiDevice* sendDevice = NULL; + AMidiInputPort* midiInputPort = NULL; +}; + +#endif + #if defined(__RTMIDI_DUMMY__) class MidiInDummy: public MidiInApi @@ -382,6 +445,7 @@ const char* rtmidi_api_names[][2] = { { "jack" , "Jack" }, { "winmm" , "Windows MultiMedia" }, { "web" , "Web MIDI API" }, + { "amidi" , "Android MIDI API" }, { "dummy" , "Dummy" }, }; const unsigned int rtmidi_num_api_names = @@ -405,8 +469,11 @@ extern "C" const RtMidi::Api rtmidi_compiled_apis[] = { #if defined(__WEB_MIDI_API__) RtMidi::WEB_MIDI_API, #endif -#if defined(__RTMIDI_DUMMY__) - RtMidi::RTMIDI_DUMMY, +#if defined(__WEB_MIDI_API__) + RtMidi::WEB_MIDI_API, +#endif +#if defined(__AMIDI__) + RtMidi::ANDROID_AMIDI, #endif RtMidi::UNSPECIFIED, }; @@ -491,6 +558,10 @@ void RtMidiIn :: openMidiApi( RtMidi::Api api, const std::string &clientName, un if ( api == WEB_MIDI_API ) rtapi_ = new MidiInWeb( clientName, queueSizeLimit ); #endif +#if defined(__AMIDI__) + if ( api == ANDROID_AMIDI ) + rtapi_ = new MidiInAndroid( clientName, queueSizeLimit ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiInDummy( clientName, queueSizeLimit ); @@ -563,6 +634,10 @@ void RtMidiOut :: openMidiApi( RtMidi::Api api, const std::string &clientName ) if ( api == WEB_MIDI_API ) rtapi_ = new MidiOutWeb( clientName ); #endif +#if defined(__AMIDI__) + if ( api == ANDROID_AMIDI ) + rtapi_ = new MidiOutAndroid( clientName ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiOutDummy( clientName ); @@ -3936,3 +4011,408 @@ void MidiOutWeb::initialize( const std::string& clientName ) } #endif // __WEB_MIDI_API__ + + +//*********************************************************************// +// API: ANDROID AMIDI +// +// Written by Yellow Labrador, May 2023. +// https://github.com/YellowLabrador/rtmidi +// *********************************************************************// + +#if defined(__AMIDI__) + +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) + +static std::string androidClientName; +static std::vector androidMidiDevices; + +//*********************************************************************// +// API: Android AMIDI +// Class Definitions: MidiInAndroid +//*********************************************************************// + +static JNIEnv* androidGetThreadEnv() { + // Every Android app has only one JVM. Calling JNI_GetCreatedJavaVMs + // will retrieve the JVM running the app. + jsize jvmsFound = 0; + JavaVM jvms[1]; + JavaVM* pjvms = jvms; + jint result = JNI_GetCreatedJavaVMs(&pjvms, 1, &jvmsFound); + + // Something went terribly wrong, no JVM was found + if (jvmsFound != 1 || result != JNI_OK) { + LOGE("No JVM found"); + return NULL; + } + + // Get the JNIEnv for the current thread + JNIEnv* env = NULL; + int rc = pjvms->GetEnv((void**)&env, JNI_VERSION_1_6); + + // The current thread was not attached to the JVM. Add it to the JVM + if (rc == JNI_EDETACHED) { + pjvms->AttachCurrentThreadAsDaemon(&env, NULL); + } + + // Neither way to retrieve the JNIEnv worked + if (env == NULL) { + LOGE("Unable to retrieve JNI environment"); + } + + return env; +} + +static jobject androidGetContext(JNIEnv *env) { + auto activityThread = env->FindClass("android/app/ActivityThread"); + auto currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;"); + auto at = env->CallStaticObjectMethod(activityThread, currentActivityThread); + if (at == NULL) { + LOGE("Unable to locate the global ActivityThread"); + return NULL; + } + + auto getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;"); + auto context = env->CallObjectMethod(at, getApplication); + if (context == NULL) { + LOGE("Application context was NULL"); + } + + return context; +} + +static void androidRefreshMidiDevices(JNIEnv *env, jobject context, bool isOutput) { + // Remove all midi devices + for (jobject jMidiDevice : androidMidiDevices) { + env->DeleteGlobalRef(jMidiDevice); + } + androidMidiDevices.clear(); + + // MidiManager midiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + auto contextClass = env->FindClass("android/content/Context"); + auto getServiceMethod = env->GetMethodID(contextClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + auto midiService = env->CallObjectMethod(context, getServiceMethod, env->NewStringUTF("midi")); + + // MidiDeviceInfo[] devInfos = mMidiManager.getDevices(); + auto midiMgrClass = env->FindClass("android/media/midi/MidiManager"); + auto getDevicesMethod = env->GetMethodID(midiMgrClass, "getDevices", "()[Landroid/media/midi/MidiDeviceInfo;"); + auto jDevices = (jobjectArray) env->CallObjectMethod(midiService, getDevicesMethod); + + auto deviceInfoClass = env->FindClass("android/media/midi/MidiDeviceInfo"); + auto getInputPortCountMethod = env->GetMethodID(deviceInfoClass, "getInputPortCount", "()I"); + auto getOutputPortCountMethod = env->GetMethodID(deviceInfoClass, "getOutputPortCount", "()I"); + + jsize len = env->GetArrayLength((jarray)jDevices); + for (int i=0; iGetObjectArrayElement(jDevices, i); + + int numPorts = env->CallIntMethod(jDeviceInfo, isOutput ? getOutputPortCountMethod : getInputPortCountMethod); + if (numPorts > 0) { + androidMidiDevices.push_back(env->NewGlobalRef(jDeviceInfo)); + } + } +} + +// Calls midiManager.openDevice() without a callback object. Unfortunately the callback +// object can't be defined using JNI so this implementation assumes opening a device +// always succeeds. Problem is there is no way to tell when the device is ready so subsequent +// commands need to be retried +static void androidOpenDevice(JNIEnv *env, jobject midiMgr, jobject deviceInfo) { + // openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener, Handler handler) + auto midiMgrClass = env->FindClass("android/media/midi/MidiManager"); + auto getDevicesMethod = env->GetMethodID(midiMgrClass, "openDevice", "(Landroid/media/midi/MidiDeviceInfo;Landroid/media/midi/MidiManager$OnDeviceOpenedListener;Landroid/os/Handler;)V"); + + auto handlerClass = env->FindClass("android/os/Handler"); + auto handlerCtor = env->GetMethodID(handlerClass, "", "()V"); + auto handler = env->NewObject(handlerClass, handlerCtor); + + env->CallVoidMethod(midiMgr, getDevicesMethod, deviceInfo, nullptr, handler); + env->DeleteLocalRef(handler); +} + +static std::string androidPortName(JNIEnv *env, unsigned int portNumber) { + if (portNumber >= androidMidiDevices.size()) { + LOGE("androidPortName: Invalid port number"); + return ""; + } + + // String deviceName = devInfo.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME); + auto deviceInfoClass = env->FindClass("android/media/midi/MidiDeviceInfo"); + auto getPropsMethod = env->GetMethodID(deviceInfoClass, "getProperties", "()Landroid/os/Bundle;"); + auto bundle = env->CallObjectMethod(androidMidiDevices[portNumber], getPropsMethod); + + auto bundleClass = env->FindClass("android/os/Bundle"); + auto getStringMethod = env->GetMethodID(bundleClass, "getString", "(Ljava/lang/String;)Ljava/lang/String;"); + auto jPortName = (jstring) env->CallObjectMethod(bundle, getStringMethod, env->NewStringUTF("name")); + + auto portNameChars = env->GetStringUTFChars(jPortName, NULL); + auto name = std::string(portNameChars); + env->ReleaseStringUTFChars(jPortName, portNameChars); + + return name; +} + +MidiInAndroid :: MidiInAndroid( const std::string &clientName, unsigned int queueSizeLimit ) + : MidiInApi( queueSizeLimit ) { + MidiInAndroid::initialize( clientName ); +} + +void MidiInAndroid :: initialize( const std::string& clientName ) { + androidClientName = clientName; + connect(); +} + +void MidiInAndroid :: connect() { + auto env = androidGetThreadEnv(); + auto context = androidGetContext(env); + androidRefreshMidiDevices(env, context, true); + + env->DeleteLocalRef(context); +} + +MidiInAndroid :: ~MidiInAndroid() { + auto env = androidGetThreadEnv(); + + // Remove all midi devices + for (jobject jMidiDevice : androidMidiDevices) { + env->DeleteGlobalRef(jMidiDevice); + } + androidMidiDevices.clear(); + + androidClientName = ""; +} + +void MidiInAndroid :: openPort(unsigned int portNumber, const std::string &portName) { + if (portNumber >= androidMidiDevices.size()) { + errorString_ = "MidiInAndroid::openPort: Invalid port number"; + error( RtMidiError::INVALID_PARAMETER, errorString_ ); + + return; + } + + if (reading) { + errorString_ = "MidiInAndroid::openPort: A port is already open"; + error( RtMidiError::INVALID_USE, errorString_ ); + + return; + } + + auto env = androidGetThreadEnv(); + + AMidiDevice_fromJava(env, androidMidiDevices[portNumber], &receiveDevice); + // int32_t deviceType = AMidiDevice_getType(receiveDevice); + // ssize_t numPorts = AMidiDevice_getNumOutputPorts(receiveDevice); + + AMidiOutputPort_open(receiveDevice, portNumber, &midiOutputPort); + + // Start read thread + // pthread_init(true); + pthread_create(&readThread, NULL, MidiInAndroid::pollMidi, this); +} + +void MidiInAndroid :: openVirtualPort(const std::string &portName) { + errorString_ = "MidiInAndroid::openVirtualPort: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +unsigned int MidiInAndroid :: getPortCount() { + connect(); + return androidMidiDevices.size(); +} + +std::string MidiInAndroid :: getPortName(unsigned int portNumber) { + auto env = androidGetThreadEnv(); + return androidPortName(env, portNumber); +} + +void MidiInAndroid :: closePort() { + reading = false; + pthread_join(readThread, NULL); + + AMidiDevice_release(receiveDevice); + receiveDevice = NULL; + midiOutputPort = NULL; +} + +void MidiInAndroid:: setClientName(const std::string& clientName) { + androidClientName = clientName; +} + +void MidiInAndroid :: setPortName(const std::string &portName) { + errorString_ = "MidiInAndroid::setPortName: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +void* MidiInAndroid :: pollMidi(void* context) { + auto self = (MidiInAndroid*) context; + self->reading = true; + + const size_t MAX_BYTES_TO_RECEIVE = 128; + uint8_t incomingMessage[MAX_BYTES_TO_RECEIVE]; + + while (self->reading) { + // AMidiOutputPort_receive is non-blocking, must poll with some sleep + usleep(2000); + + int32_t opcode; + size_t numBytesReceived; + int64_t timestamp; + ssize_t numMessagesReceived = AMidiOutputPort_receive( + self->midiOutputPort, &opcode, incomingMessage, MAX_BYTES_TO_RECEIVE, + &numBytesReceived, ×tamp); + + if (numMessagesReceived < 0) { + self->errorString_ = "MidiInAndroid::pollMidi: error receiving MIDI data"; + self->error( RtMidiError::SYSTEM_ERROR, self->errorString_ ); + self->reading = false; + } + + if (numMessagesReceived > 0 && numBytesReceived >= 0) { + auto message = self->inputData_.message; + auto ignoreFlags = self->inputData_.ignoreFlags; + + if (self->inputData_.firstMessage == true) { + message.timeStamp = 0.0; + self->inputData_.firstMessage = false; + } else { + message.timeStamp = (timestamp - self->lastTime) * 0.000001; + } + self->lastTime = timestamp; + + bool& continueSysex = self->inputData_.continueSysex; + if (!continueSysex) message.bytes.clear(); + + if ( !( ( continueSysex || incomingMessage[0] == 0xF0 ) && ( ignoreFlags & 0x01 ) ) ) { + // Unless this is a (possibly continued) SysEx message and we're ignoring SysEx, + // copy the event buffer into the MIDI message struct. + for (unsigned int i=0; iinputData_.usingCallback) { + auto callback = (RtMidiIn::RtMidiCallback) self->inputData_.userCallback; + callback(message.timeStamp, &message.bytes, self->inputData_.userData); + } else { + if (!self->inputData_.queue.push(message)) + std::cerr << "\nMidiInAndroid: message queue limit reached!!\n\n"; + } + } + } + } + + return NULL; +} + + +//*********************************************************************// +// API: Android AMIDI +// Class Definitions: MidiOutAndroid +//*********************************************************************// + + +MidiOutAndroid :: MidiOutAndroid( const std::string &clientName ) : MidiOutApi() { + MidiOutAndroid::initialize( clientName ); +} + +void MidiOutAndroid :: initialize( const std::string& clientName ) { + androidClientName = clientName; + connect(); +} + +void MidiOutAndroid :: connect() { + auto env = androidGetThreadEnv(); + auto context = androidGetContext(env); + androidRefreshMidiDevices(env, context, false); + + env->DeleteLocalRef(context); +} + +MidiOutAndroid :: ~MidiOutAndroid() { + auto env = androidGetThreadEnv(); + + // Remove all midi devices + for (jobject jMidiDevice : androidMidiDevices) { + env->DeleteGlobalRef(jMidiDevice); + } + androidMidiDevices.clear(); + + androidClientName = ""; +} + +void MidiOutAndroid :: openPort( unsigned int portNumber, const std::string &portName ) { + if (portNumber >= androidMidiDevices.size()) { + errorString_ = "MidiOutAndroid::openPort: Invalid port number"; + error( RtMidiError::INVALID_PARAMETER, errorString_ ); + + return; + } + + auto env = androidGetThreadEnv(); + + AMidiDevice_fromJava(env, androidMidiDevices[portNumber], &sendDevice); + AMidiInputPort_open(sendDevice, portNumber, &midiInputPort); +} + +void MidiOutAndroid :: openVirtualPort( const std::string &portName ) { + errorString_ = "MidiOutAndroid::openVirtualPort: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +unsigned int MidiOutAndroid :: getPortCount() { + connect(); + return androidMidiDevices.size(); +} + +std::string MidiOutAndroid :: getPortName( unsigned int portNumber ) { + auto env = androidGetThreadEnv(); + return androidPortName(env, portNumber); +} + +void MidiOutAndroid :: closePort() { + AMidiDevice_release(sendDevice); + sendDevice = NULL; + midiInputPort = NULL; +} + +void MidiOutAndroid:: setClientName( const std::string& name ) { + androidClientName = name; +} + +void MidiOutAndroid :: setPortName( const std::string &portName ) { + errorString_ = "MidiOutAndroid::setPortName: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +void MidiOutAndroid :: sendMessage( const unsigned char *message, size_t size ) { + AMidiInputPort_send(midiInputPort, (uint8_t*)message, size); +} + +#endif // __AMIDI__ diff --git a/RtMidi.h b/RtMidi.h index 6ca3a55..7534505 100644 --- a/RtMidi.h +++ b/RtMidi.h @@ -142,6 +142,7 @@ class RTMIDI_DLL_PUBLIC RtMidi LINUX_ALSA, /*!< The Advanced Linux Sound Architecture API. */ UNIX_JACK, /*!< The JACK Low-Latency MIDI Server API. */ WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ + ANDROID_AMIDI, /*!< Native Android MIDI API. */ RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ WEB_MIDI_API, /*!< W3C Web MIDI API. */ NUM_APIS /*!< Number of values in this enum. */ diff --git a/configure.ac b/configure.ac index 960af42..b8d5c3d 100644 --- a/configure.ac +++ b/configure.ac @@ -66,6 +66,7 @@ AC_ARG_WITH(core, [AS_HELP_STRING([--with-core], [ choose CoreMIDI API support ( AC_ARG_WITH(winmm, [AS_HELP_STRING([--with-winmm], [ choose Windows MultiMedia (MM) API support (win32 only)])]) AC_ARG_WITH(winks, [AS_HELP_STRING([--with-winks], [ choose kernel streaming support (win32 only)])]) AC_ARG_WITH(webmidi, [AS_HELP_STRING([--with-webmidi], [ choose Web MIDI support])]) +AC_ARG_WITH(android, [AS_HELP_STRING([--with-android], [ choose Android support])]) # Checks for programs. @@ -140,6 +141,7 @@ AS_IF([test "x$with_core" = "xyes"], [systems="$systems core"]) AS_IF([test "x$with_winmm" = "xyes"], [systems="$systems winmm"]) AS_IF([test "x$with_winks" = "xyes"], [systems="$systems winks"]) AS_IF([test "x$with_webmidi" = "xyes"], [systems="$systems webmidi"]) +AS_IF([test "x$with_android" = "xyes"], [systems="$systems android"]) AS_IF([test "x$with_dummy" = "xyes"], [systems="$systems dummy"]) required=" $systems " @@ -162,6 +164,7 @@ AS_IF([test "x$with_winmm" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep AS_IF([test "x$with_winks" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v winks`]) AS_IF([test "x$with_core" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v core`]) AS_IF([test "x$with_webmidi" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v webmidi`]) +AS_IF([test "x$with_android" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v android`]) AS_IF([test "x$with_dummy" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v dummy`]) systems=" `echo $systems|tr \\\\n ' '` " @@ -230,6 +233,10 @@ AS_CASE(["$systems"], [*" webmidi "*], [ found="$found Web MIDI"]) ]) +AS_CASE(["$systems"], [*" android "*], [ + AC_MSG_ERROR([Android NDK configuration not implemented, use CMAKE]) +]) + AS_IF([test -n "$need_ole32"], [LIBS="-lole32 $LIBS"]) AS_IF([test -n "$need_pthread"],[ diff --git a/rtmidi_c.cpp b/rtmidi_c.cpp index 5c93588..cad4e1a 100644 --- a/rtmidi_c.cpp +++ b/rtmidi_c.cpp @@ -16,6 +16,7 @@ class StaticEnumAssertions { StaticEnumAssertions() { ENUM_EQUAL( RTMIDI_API_LINUX_ALSA, RtMidi::LINUX_ALSA ); ENUM_EQUAL( RTMIDI_API_UNIX_JACK, RtMidi::UNIX_JACK ); ENUM_EQUAL( RTMIDI_API_WINDOWS_MM, RtMidi::WINDOWS_MM ); + ENUM_EQUAL( RTMIDI_API_ANDROID, RtMidi::ANDROID_AMIDI ); ENUM_EQUAL( RTMIDI_API_RTMIDI_DUMMY, RtMidi::RTMIDI_DUMMY ); ENUM_EQUAL( RTMIDI_ERROR_WARNING, RtMidiError::WARNING ); diff --git a/rtmidi_c.h b/rtmidi_c.h index 82db105..cecdbc6 100644 --- a/rtmidi_c.h +++ b/rtmidi_c.h @@ -64,6 +64,7 @@ enum RtMidiApi { RTMIDI_API_LINUX_ALSA, /*!< The Advanced Linux Sound Architecture API. */ RTMIDI_API_UNIX_JACK, /*!< The Jack Low-Latency MIDI Server API. */ RTMIDI_API_WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ + RTMIDI_API_ANDROID, /*!< The Android MIDI API. */ RTMIDI_API_RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ RTMIDI_API_NUM /*!< Number of values in this enum. */ }; From bb9eacd1c1b2b47902e06aab22e2cf3427ccdbfa Mon Sep 17 00:00:00 2001 From: Yellow Labrador <132696731+YellowLabrador@users.noreply.github.com> Date: Sun, 21 May 2023 16:53:08 -0700 Subject: [PATCH 2/6] Fixed Java implementation. See contrib/java --- RtMidi.cpp | 147 ++++++++++++--------- contrib/java/MidiDeviceOpenedListener.java | 21 +++ 2 files changed, 104 insertions(+), 64 deletions(-) create mode 100644 contrib/java/MidiDeviceOpenedListener.java diff --git a/RtMidi.cpp b/RtMidi.cpp index 846faac..49787bf 100644 --- a/RtMidi.cpp +++ b/RtMidi.cpp @@ -336,7 +336,6 @@ class MidiInAndroid : public MidiInApi void onMidiMessage( uint8_t* data, double domHishResTimeStamp ); - protected: void initialize( const std::string& clientName ); void connect(); AMidiDevice* receiveDevice = NULL; @@ -344,7 +343,7 @@ class MidiInAndroid : public MidiInApi pthread_t readThread; std::atomic reading = ATOMIC_VAR_INIT(false); static void* pollMidi(void* context); - int64_t lastTime; + double lastTime; }; class MidiOutAndroid: public MidiOutApi @@ -362,7 +361,6 @@ class MidiOutAndroid: public MidiOutApi std::string getPortName( unsigned int portNumber ); void sendMessage( const unsigned char *message, size_t size ); - protected: void initialize( const std::string& clientName ); void connect(); AMidiDevice* sendDevice = NULL; @@ -444,9 +442,9 @@ const char* rtmidi_api_names[][2] = { { "alsa" , "ALSA" }, { "jack" , "Jack" }, { "winmm" , "Windows MultiMedia" }, - { "web" , "Web MIDI API" }, { "amidi" , "Android MIDI API" }, { "dummy" , "Dummy" }, + { "web" , "Web MIDI API" }, }; const unsigned int rtmidi_num_api_names = sizeof(rtmidi_api_names)/sizeof(rtmidi_api_names[0]); @@ -4022,6 +4020,8 @@ void MidiOutWeb::initialize( const std::string& clientName ) #if defined(__AMIDI__) +#include + #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) @@ -4084,6 +4084,13 @@ static jobject androidGetContext(JNIEnv *env) { return context; } +static jobject androidGetMidiManager(JNIEnv *env, jobject context) { + // MidiManager midiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + auto contextClass = env->FindClass("android/content/Context"); + auto getServiceMethod = env->GetMethodID(contextClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + return env->CallObjectMethod(context, getServiceMethod, env->NewStringUTF("midi")); +} + static void androidRefreshMidiDevices(JNIEnv *env, jobject context, bool isOutput) { // Remove all midi devices for (jobject jMidiDevice : androidMidiDevices) { @@ -4091,10 +4098,7 @@ static void androidRefreshMidiDevices(JNIEnv *env, jobject context, bool isOutpu } androidMidiDevices.clear(); - // MidiManager midiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); - auto contextClass = env->FindClass("android/content/Context"); - auto getServiceMethod = env->GetMethodID(contextClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); - auto midiService = env->CallObjectMethod(context, getServiceMethod, env->NewStringUTF("midi")); + auto midiService = androidGetMidiManager(env, context); // MidiDeviceInfo[] devInfos = mMidiManager.getDevices(); auto midiMgrClass = env->FindClass("android/media/midi/MidiManager"); @@ -4116,21 +4120,48 @@ static void androidRefreshMidiDevices(JNIEnv *env, jobject context, bool isOutpu } } -// Calls midiManager.openDevice() without a callback object. Unfortunately the callback -// object can't be defined using JNI so this implementation assumes opening a device -// always succeeds. Problem is there is no way to tell when the device is ready so subsequent -// commands need to be retried -static void androidOpenDevice(JNIEnv *env, jobject midiMgr, jobject deviceInfo) { - // openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener, Handler handler) - auto midiMgrClass = env->FindClass("android/media/midi/MidiManager"); - auto getDevicesMethod = env->GetMethodID(midiMgrClass, "openDevice", "(Landroid/media/midi/MidiDeviceInfo;Landroid/media/midi/MidiManager$OnDeviceOpenedListener;Landroid/os/Handler;)V"); - auto handlerClass = env->FindClass("android/os/Handler"); - auto handlerCtor = env->GetMethodID(handlerClass, "", "()V"); - auto handler = env->NewObject(handlerClass, handlerCtor); +extern "C" +JNIEXPORT void JNICALL +Java_com_yellowlab_rtmidi_MidiDeviceOpenedListener_midiDeviceOpened(JNIEnv *env, jclass clazz, + jobject midi_device, jlong targetPtr, jboolean isOutput) { + if (isOutput) { + auto midiOut = reinterpret_cast(targetPtr); + AMidiDevice_fromJava(env, midi_device, &midiOut->sendDevice); + AMidiInputPort_open(midiOut->sendDevice, 0, &midiOut->midiInputPort); + } else { + auto midiIn = reinterpret_cast(targetPtr); + AMidiDevice_fromJava(env, midi_device, &midiIn->receiveDevice); + AMidiOutputPort_open(midiIn->receiveDevice, 0, &midiIn->midiOutputPort); + pthread_create(&midiIn->readThread, NULL, MidiInAndroid::pollMidi, midiIn); + } +} + +static void androidOpenDevice(jobject deviceInfo, void* target, bool isOutput) { + auto env = androidGetThreadEnv(); + auto context = androidGetContext(env); + auto midiMgr = androidGetMidiManager(env, context); + + // openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener, Handler handler) + auto midiMgrClass = env->GetObjectClass(midiMgr); + auto openDevicesMethod = env->GetMethodID(midiMgrClass, "openDevice", "(Landroid/media/midi/MidiDeviceInfo;Landroid/media/midi/MidiManager$OnDeviceOpenedListener;Landroid/os/Handler;)V"); - env->CallVoidMethod(midiMgr, getDevicesMethod, deviceInfo, nullptr, handler); - env->DeleteLocalRef(handler); + auto handlerClass = env->FindClass("android/os/Handler"); + auto handlerCtor = env->GetMethodID(handlerClass, "", "()V"); + auto handler = env->NewObject(handlerClass, handlerCtor); + + auto listenerClass = env->FindClass("com/yellowlab/rtmidi/MidiDeviceOpenedListener"); + if (!listenerClass) { + LOGE(LOG_TAG, "Midi listener class not found com.yellowlab.rtmidi.MidiDeviceOpenedListener. Did you forget to add it to your APK?"); + return; + } + + auto targetPtr = reinterpret_cast(target); + auto listenerCtor = env->GetMethodID(listenerClass, "", "(JZ)V"); + auto listener = env->NewObject(listenerClass, listenerCtor, targetPtr, isOutput); + + env->CallVoidMethod(midiMgr, openDevicesMethod, deviceInfo, listener, handler); + env->DeleteLocalRef(handler); } static std::string androidPortName(JNIEnv *env, unsigned int portNumber) { @@ -4200,17 +4231,7 @@ void MidiInAndroid :: openPort(unsigned int portNumber, const std::string &portN return; } - auto env = androidGetThreadEnv(); - - AMidiDevice_fromJava(env, androidMidiDevices[portNumber], &receiveDevice); - // int32_t deviceType = AMidiDevice_getType(receiveDevice); - // ssize_t numPorts = AMidiDevice_getNumOutputPorts(receiveDevice); - - AMidiOutputPort_open(receiveDevice, portNumber, &midiOutputPort); - - // Start read thread - // pthread_init(true); - pthread_create(&readThread, NULL, MidiInAndroid::pollMidi, this); + androidOpenDevice(androidMidiDevices[portNumber], this, false); } void MidiInAndroid :: openVirtualPort(const std::string &portName) { @@ -4256,6 +4277,8 @@ void* MidiInAndroid :: pollMidi(void* context) { while (self->reading) { // AMidiOutputPort_receive is non-blocking, must poll with some sleep usleep(2000); + auto ignoreFlags = self->inputData_.ignoreFlags; + bool& continueSysex = self->inputData_.continueSysex; int32_t opcode; size_t numBytesReceived; @@ -4268,21 +4291,44 @@ void* MidiInAndroid :: pollMidi(void* context) { self->errorString_ = "MidiInAndroid::pollMidi: error receiving MIDI data"; self->error( RtMidiError::SYSTEM_ERROR, self->errorString_ ); self->reading = false; + break; + } + + switch (incomingMessage[0]) { + case 0xF0: + // Start of a SysEx message + continueSysex = incomingMessage[numBytesReceived - 1] != 0xF7; + if (ignoreFlags & 0x01) continue; + break; + case 0xF1: + case 0xF8: + // MIDI Time Code or Timing Clock message + if (ignoreFlags & 0x02) continue; + break; + case 0xFE: + // Active Sensing message + if (ignoreFlags & 0x04) continue; + break; + default: + if (continueSysex) { + // Continuation of a SysEx message + continueSysex = incomingMessage[numBytesReceived - 1] != 0xF7; + if (ignoreFlags & 0x01) continue; + } + // All other MIDI messages } if (numMessagesReceived > 0 && numBytesReceived >= 0) { auto message = self->inputData_.message; - auto ignoreFlags = self->inputData_.ignoreFlags; if (self->inputData_.firstMessage == true) { message.timeStamp = 0.0; self->inputData_.firstMessage = false; } else { - message.timeStamp = (timestamp - self->lastTime) * 0.000001; + message.timeStamp = (timestamp * 0.000001) - self->lastTime; } - self->lastTime = timestamp; + self->lastTime = (timestamp * 0.000001); - bool& continueSysex = self->inputData_.continueSysex; if (!continueSysex) message.bytes.clear(); if ( !( ( continueSysex || incomingMessage[0] == 0xF0 ) && ( ignoreFlags & 0x01 ) ) ) { @@ -4292,30 +4338,6 @@ void* MidiInAndroid :: pollMidi(void* context) { message.bytes.push_back(incomingMessage[i]); } - switch (incomingMessage[0]) { - case 0xF0: - // Start of a SysEx message - continueSysex = incomingMessage[numBytesReceived - 1] != 0xF7; - if (ignoreFlags & 0x01) continue; - break; - case 0xF1: - case 0xF8: - // MIDI Time Code or Timing Clock message - if (ignoreFlags & 0x02) continue; - break; - case 0xFE: - // Active Sensing message - if (ignoreFlags & 0x04) continue; - break; - default: - if (continueSysex) { - // Continuation of a SysEx message - continueSysex = incomingMessage[numBytesReceived - 1] != 0xF7; - if (ignoreFlags & 0x01) continue; - } - // All other MIDI messages - } - if (!continueSysex) { if (self->inputData_.usingCallback) { auto callback = (RtMidiIn::RtMidiCallback) self->inputData_.userCallback; @@ -4375,10 +4397,7 @@ void MidiOutAndroid :: openPort( unsigned int portNumber, const std::string &por return; } - auto env = androidGetThreadEnv(); - - AMidiDevice_fromJava(env, androidMidiDevices[portNumber], &sendDevice); - AMidiInputPort_open(sendDevice, portNumber, &midiInputPort); + androidOpenDevice(androidMidiDevices[portNumber], this, true); } void MidiOutAndroid :: openVirtualPort( const std::string &portName ) { diff --git a/contrib/java/MidiDeviceOpenedListener.java b/contrib/java/MidiDeviceOpenedListener.java new file mode 100644 index 0000000..333b547 --- /dev/null +++ b/contrib/java/MidiDeviceOpenedListener.java @@ -0,0 +1,21 @@ +package com.yellowlab.rtmidi; + +import android.media.midi.MidiDevice; +import android.media.midi.MidiManager; + +public class MidiDeviceOpenedListener implements MidiManager.OnDeviceOpenedListener { + private long nativeId; + private boolean isOutput; + + public MidiDeviceOpenedListener(long id, boolean output) { + nativeId = id; + isOutput = output; + } + + @Override + public void onDeviceOpened(MidiDevice midiDevice) { + midiDeviceOpened(midiDevice, nativeId, isOutput); + } + + private native static void midiDeviceOpened(MidiDevice midiDevice, long id, boolean isOutput); +} From d691ff776dd3ccd2ef747f11091051ca59fc556d Mon Sep 17 00:00:00 2001 From: YellowLabrador <132696731+YellowLabrador@users.noreply.github.com> Date: Sat, 27 May 2023 15:33:09 -0700 Subject: [PATCH 3/6] Added comments to MidiDeviceOpenedListener.java --- contrib/java/MidiDeviceOpenedListener.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contrib/java/MidiDeviceOpenedListener.java b/contrib/java/MidiDeviceOpenedListener.java index 333b547..61d642e 100644 --- a/contrib/java/MidiDeviceOpenedListener.java +++ b/contrib/java/MidiDeviceOpenedListener.java @@ -3,6 +3,11 @@ import android.media.midi.MidiDevice; import android.media.midi.MidiManager; +/** + * This class must be included in the Android app using rtmidi. It is used by the + * C++ code in the Android library because a Java listener class is required by the + * Android Midi API. + */ public class MidiDeviceOpenedListener implements MidiManager.OnDeviceOpenedListener { private long nativeId; private boolean isOutput; From 1c7be947f5500413b6580245b60b1a0a0fa7af77 Mon Sep 17 00:00:00 2001 From: YellowLabrador <132696731+YellowLabrador@users.noreply.github.com> Date: Sat, 27 May 2023 15:40:58 -0700 Subject: [PATCH 4/6] Update CMakeLists.txt Using the correct Android flag --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d4e2ba..3a85a03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,7 +150,7 @@ if(RTMIDI_API_CORE) endif() # Android AMIDI -if(ANDROID_NDK) +if(ANDROID) set(NEED_PTHREAD ON) set(JAVA_INCLUDE_PATH2 NotNeeded) set(JAVA_AWT_INCLUDE_PATH NotNeeded) From c3fd496d848569ea994784b6e4af86d0e875c455 Mon Sep 17 00:00:00 2001 From: Yellow Labrador <132696731+YellowLabrador@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:30:59 -0700 Subject: [PATCH 5/6] Moved Android to the end of the enumeration --- RtMidi.cpp | 2 +- rtmidi_c.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RtMidi.cpp b/RtMidi.cpp index 6bd25f2..66a48fd 100644 --- a/RtMidi.cpp +++ b/RtMidi.cpp @@ -490,10 +490,10 @@ const char* rtmidi_api_names[][2] = { { "alsa" , "ALSA" }, { "jack" , "Jack" }, { "winmm" , "Windows MultiMedia" }, - { "amidi" , "Android MIDI API" }, { "dummy" , "Dummy" }, { "web" , "Web MIDI API" }, { "winuwp" , "Windows UWP" }, + { "amidi" , "Android MIDI API" }, }; const unsigned int rtmidi_num_api_names = sizeof(rtmidi_api_names)/sizeof(rtmidi_api_names[0]); diff --git a/rtmidi_c.h b/rtmidi_c.h index 2f6d2e5..e246411 100644 --- a/rtmidi_c.h +++ b/rtmidi_c.h @@ -64,10 +64,10 @@ enum RtMidiApi { RTMIDI_API_LINUX_ALSA, /*!< The Advanced Linux Sound Architecture API. */ RTMIDI_API_UNIX_JACK, /*!< The Jack Low-Latency MIDI Server API. */ RTMIDI_API_WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ - RTMIDI_API_ANDROID, /*!< The Android MIDI API. */ RTMIDI_API_RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ RTMIDI_API_WEB_MIDI_API, /*!< W3C Web MIDI API. */ RTMIDI_API_WINDOWS_UWP, /*!< The Microsoft Universal Windows Platform MIDI API. */ + RTMIDI_API_ANDROID, /*!< The Android MIDI API. */ RTMIDI_API_NUM /*!< Number of values in this enum. */ }; From cbb0297e6445f1b26a325719b232f2eb78081d58 Mon Sep 17 00:00:00 2001 From: Yellow Labrador <132696731+YellowLabrador@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:47:44 -0700 Subject: [PATCH 6/6] Aligned API target enumerations --- RtMidi.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RtMidi.h b/RtMidi.h index bc23156..7f7254a 100644 --- a/RtMidi.h +++ b/RtMidi.h @@ -159,10 +159,10 @@ class RTMIDI_DLL_PUBLIC RtMidi LINUX_ALSA, /*!< The Advanced Linux Sound Architecture API. */ UNIX_JACK, /*!< The JACK Low-Latency MIDI Server API. */ WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ - ANDROID_AMIDI, /*!< Native Android MIDI API. */ RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ WEB_MIDI_API, /*!< W3C Web MIDI API. */ WINDOWS_UWP, /*!< The Microsoft Universal Windows Platform MIDI API. */ + ANDROID_AMIDI, /*!< Native Android MIDI API. */ NUM_APIS /*!< Number of values in this enum. */ };