Implementar MIDI con Delphi en Android

Estoy buscando desde algún tiempo para una manera de jugar MIDI en Delphi XE5 con Android dirigido. Varias de mis preguntas antes estaban relacionadas con esta "búsqueda" :-). He presentado dos solicitudes a embarcadero: # 119422 para agregar soporte MIDI a TMediaPlayer y # 119423 para agregar un framework MIDI a Firemonkey, pero eso no ayudó. He logrado al fin. Como sé que hay algunas personas más que estaban buscando MIDI en Android publicar esta pregunta con respuesta para la documentación.

El sistema Android tiene un sintetizador MIDI interno. Puede acceder a ella a través de Android NDK. Lo he descrito en un artículo que contiene algunas descargas . Esta respuesta es una breve descripción de este artículo. Lo que verá aquí es una prueba de concepto. Se mostrará cómo reproducir notas MIDI en un sistema Android pero necesita mejoras. Sugerencias para la mejora son bienvenidos 🙂

Utilice Eclipse para interactuar con el proyecto Java. Supongo que tienes Delphi XE5 con el paquete Mobile, que te da dos cosas ya instaladas: el SDK de Android y NDK. No vuelva a instalarlos descargando el SDK de Android completo de Google. Descargue e instale el complemento de herramientas de desarrollo de Android (ADT) de Eclipse y siga las instrucciones de instalación. Esto le permite usar el entorno Android SDK / NDK ya instalado por Delphi XE5 (encontrará las rutas en Delphi, Opciones | Herramientas | Administrador de SDK). De esta manera Delphi y Eclipse compartirán el mismo SDK y NDK.

Utilicé la biblioteca MIDI desarrollada por Willam Farmer . También tiene la documentación completa SoniVox disponible que no pude conseguir en otro lugar. Su controlador viene con un programa de ejemplo completo (Java). He creado mi propio proyecto con y cambiado el nombre del paquete a org.drivers.midioutput, por lo que todas las funciones son prefijadas por Java_org_drivers_midioutput_MidiDriver_ (ver código abajo).

Cuando desee compilar el midi.c jus abra una ventana de comandos, y llame a ndk-build en el directorio del proyecto. Algunos mensajes de error están bien. Las librerías mips y x86 no se construyeron en mi caso.

Hay un punto que debe tener en cuenta: la ruta de acceso a la ndk no puede contener espacios. Al dejar que el instalador de Delphi instala Delphi, hay un espacio en él: el subdirectorio Rad Studio en ese terrible nombre de archivo largo donde Delphi instala el SDK y NDK. Para evitar este problema, cree un directorio vacío en la unidad C :, llámelo C: \ ndk. Utilice MKLINK para vincular este directorio al directorio ndk. Esto sólo se puede hacer desde un símbolo del sistema elevado y al hacerlo, perderá las conexiones de red. El vínculo es persistente, así que cierra el símbolo del sistema y abre otro, no aumentado, y todo debería funcionar ahora. Ahora puedes usar ndk-build.

Midi.c – la interfaz NDK con el SoniVox

//////////////////////////////////////////////////////////////////////////////// // // MidiDriver - An Android Midi Driver. // // Copyright (C) 2013 Bill Farmer // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. // // Bill Farmer william j farmer [at] yahoo [dot] co [dot] uk. // /////////////////////////////////////////////////////////////////////////////// // Some slight modifications by Arnold Reinders. Added a test function and changed // the package to org.drivers.midioutput. The original copyright still applies #include // for EAS midi #include "eas.h" #include "eas_reverb.h" // determines how many EAS buffers to fill a host buffer #define NUM_BUFFERS 4 // EAS data static EAS_DATA_HANDLE pEASData; const S_EAS_LIB_CONFIG *pLibConfig; static EAS_PCM *buffer; static EAS_I32 bufferSize; static EAS_HANDLE midiHandle; // This function is added to test whether the functionality of this NDK code can be accesses // without needing to access the MIDI system. Added for testing purposes jint Java_org_drivers_midioutput_MidiDriver_version (JNIEnv *env, jobject clazz) { return 3; } // init EAS midi jint Java_org_drivers_midioutput_MidiDriver_init(JNIEnv *env, jobject clazz) { EAS_RESULT result; // get the library configuration pLibConfig = EAS_Config(); if (pLibConfig == NULL || pLibConfig->libVersion != LIB_VERSION) return 0; // calculate buffer size bufferSize = pLibConfig->mixBufferSize * pLibConfig->numChannels * NUM_BUFFERS; // init library if ((result = EAS_Init(&pEASData)) != EAS_SUCCESS) return 0; // select reverb preset and enable EAS_SetParameter(pEASData, EAS_MODULE_REVERB, EAS_PARAM_REVERB_PRESET, EAS_PARAM_REVERB_CHAMBER); EAS_SetParameter(pEASData, EAS_MODULE_REVERB, EAS_PARAM_REVERB_BYPASS, EAS_FALSE); // open midi stream if (result = EAS_OpenMIDIStream(pEASData, &midiHandle, NULL) != EAS_SUCCESS) { EAS_Shutdown(pEASData); return 0; } return bufferSize; } // midi config jintArray Java_org_drivers_midioutput_MidiDriver_config(JNIEnv *env, jobject clazz) { jboolean isCopy; if (pLibConfig == NULL) return NULL; jintArray configArray = (*env)->NewIntArray(env, 4); jint *config = (*env)->GetIntArrayElements(env, configArray, &isCopy); config[0] = pLibConfig->maxVoices; config[1] = pLibConfig->numChannels; config[2] = pLibConfig->sampleRate; config[3] = pLibConfig->mixBufferSize; (*env)->ReleaseIntArrayElements(env, configArray, config, 0); return configArray; } // midi render jint Java_org_drivers_midioutput_MidiDriver_render(JNIEnv *env, jobject clazz, jshortArray shortArray) { jboolean isCopy; EAS_RESULT result; EAS_I32 numGenerated; EAS_I32 count; jsize size; // jbyte* GetByteArrayElements(jbyteArray array, jboolean* isCopy) // void ReleaseByteArrayElements(jbyteArray array, jbyte* elems, // void* GetPrimitiveArrayCritical(JNIEnv*, jarray, jboolean*); // void ReleasePrimitiveArrayCritical(JNIEnv*, jarray, void*, jint); if (pEASData == NULL) return 0; buffer = (EAS_PCM *)(*env)->GetShortArrayElements(env, shortArray, &isCopy); size = (*env)->GetArrayLength(env, shortArray); count = 0; while (count < size) { result = EAS_Render(pEASData, buffer + count, pLibConfig->mixBufferSize, &numGenerated); if (result != EAS_SUCCESS) break; count += numGenerated * pLibConfig->numChannels; } (*env)->ReleaseShortArrayElements(env, shortArray, buffer, 0); return count; } // midi write jboolean Java_org_drivers_midioutput_MidiDriver_write(JNIEnv *env, jobject clazz, jbyteArray byteArray) { jboolean isCopy; EAS_RESULT result; jint length; EAS_U8 *buf; if (pEASData == NULL || midiHandle == NULL) return JNI_FALSE; buf = (EAS_U8 *)(*env)->GetByteArrayElements(env, byteArray, &isCopy); length = (*env)->GetArrayLength(env, byteArray); result = EAS_WriteMIDIStream(pEASData, midiHandle, buf, length); (*env)->ReleaseByteArrayElements(env, byteArray, buf, 0); if (result != EAS_SUCCESS) return JNI_FALSE; return JNI_TRUE; } // shutdown EAS midi jboolean Java_org_drivers_midioutput_MidiDriver_shutdown(JNIEnv *env, jobject clazz) { EAS_RESULT result; if (pEASData == NULL || midiHandle == NULL) return JNI_FALSE; if ((result = EAS_CloseMIDIStream(pEASData, midiHandle)) != EAS_SUCCESS) { EAS_Shutdown(pEASData); return JNI_FALSE; } if ((result = EAS_Shutdown(pEASData)) != EAS_SUCCESS) return JNI_FALSE; return JNI_TRUE; } 

Cuando la biblioteca es construida por ndk-build esto prefijará la biblioteca compilada con lib y reemplazará la extensión por .so. Así midi.c se compilará a libmidi.so. Las bibliotecas compiladas se añaden a la descarga, por lo que no es necesario compilar midi.c.

MidiDriver.Java declara una interfaz, un audioTrack y un hilo para manejar todo esto. No me he tomado la molestia de saber cómo funciona exactamente esto. Debido a que no sabía cómo manejar una interfaz y tal en Delphi he creado un contenedor de Java para MidiDriver: class MIDI_Output. Esta clase se utiliza para la interfaz con Delphi.

Class MidiDriver es la interfaz entre Java y las funciones C que llaman a las funciones de SoniVox. La clase MIDI_Output es la interfaz entre Java y Delphi. MIDI_Output crea una instancia de MidiDriver.

Class MidiDriver – la interfaz con el NDK

 //////////////////////////////////////////////////////////////////////////////// // // MidiDriver - An Android Midi Driver. // // Copyright (C) 2013 Bill Farmer // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. // // Bill Farmer william j farmer [at] yahoo [dot] co [dot] uk. // /////////////////////////////////////////////////////////////////////////////// package org.drivers.midioutput; import java.io.File; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.util.Log; // MidiDriver public class MidiDriver implements Runnable { private static final int SAMPLE_RATE = 22050; private static final int BUFFER_SIZE = 4096; private Thread thread; private AudioTrack audioTrack; private OnMidiStartListener listener; private short buffer[]; // Constructor public MidiDriver () { Log.d ("midi", " *** MidiDriver started"); } public void start () { // Start the thread thread = new Thread (this, "MidiDriver"); thread.start (); } // start // @Override public void run () { processMidi (); } // run // public void stop () { Thread t = thread; thread = null; // Wait for the thread to exit while (t != null && t.isAlive ()) Thread.yield (); } // stop // // Process MidiDriver private void processMidi () { int status = 0; int size = 0; // Init midi Log.d ("midi", " *** processMIDI"); if ((size = init()) == 0) return; buffer = new short [size]; // Create audio track audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, BUFFER_SIZE, AudioTrack.MODE_STREAM); if (audioTrack == null) { shutdown (); return; } // if // Call listener if (listener != null) listener.onMidiStart(); // Play track audioTrack.play(); // Keep running until stopped while (thread != null) { // Render the audio if (render (buffer) == 0) break; // Write audio to audiotrack status = audioTrack.write (buffer, 0, buffer.length); if (status < 0) break; } // while // Render and write the last bit of audio if (status > 0) if (render(buffer) > 0) audioTrack.write(buffer, 0, buffer.length); // Shut down audio shutdown(); audioTrack.release(); } // processMidi // public void setOnMidiStartListener (OnMidiStartListener l) { listener = l; } // setOnMidiStartListener // public static void load_lib (String libName) { File file = new File (libName); if (file.exists ()) { System.load (libName); } else { System.loadLibrary (libName); } } // Listener interface public interface OnMidiStartListener { public abstract void onMidiStart (); } // OnMidiStartListener // // Native midi methods public native int version (); private native int init (); public native int [] config (); private native int render (short a []); public native boolean write (byte a []); private native boolean shutdown (); // Load midi library static { System.loadLibrary ("midi"); } } 

Clase MIDI_Output – proporciona un ajuste para la clase MidiDriver

 package org.drivers.midioutput; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; import org.drivers.midioutput.MidiDriver.OnMidiStartListener; import android.content.res.AssetFileDescriptor; import android.media.MediaPlayer; import android.os.Environment; import android.util.Log; public class MIDI_Output implements OnMidiStartListener { protected MidiDriver midi_driver; protected MediaPlayer media_player; public MIDI_Output () { // Create midi driver midi_driver = new MidiDriver(); Log.d ("midi", " *** midi_driver opened with version " + String.valueOf (midi_driver.version ())); // Set onmidistart listener to this class if (midi_driver != null) midi_driver.setOnMidiStartListener (this); } // MIDI_Output () // public int test_int (int n) { int sq = n * n; // Log.d ("midi", " *** test_int computes " + String.valueOf (sq)); return n * n; } public void start () { if (midi_driver != null) { midi_driver.start (); Log.d ("midi", " *** midi_driver.start ()"); } } // start // public void stop () { if (midi_driver != null) { midi_driver.stop (); Log.d ("midi", " *** midi_driver.stop ()"); } stopSong (); } // stop // // Listener for sending initial midi messages when the Sonivox // synthesizer has been started, such as program change. Runs on // the MidiDriver thread, so should only be used for sending midi // messages. @Override public void onMidiStart() { Log.d ("midi", " *** onSMidiStart"); // TODO } // Sends a midi message protected void putShort (int m, int n, int v) { if (midi_driver != null) { byte msg [] = new byte [3]; msg [0] = (byte) m; msg [1] = (byte) n; msg [2] = (byte) v; Log.d ("midi", " *** putShort (" + String.valueOf (m) + ", " + String.valueOf (n) + ", " + String.valueOf (v) + ")"); midi_driver.write (msg); } // if } // putShort // public boolean isPlayingSong () { return media_player != null; } // isPlayingSong // public void playSong (String audioFilename) { String audioPath; try { FileDescriptor fd = null; audioFilename = "/Data/d/song.mid"; File baseDir = Environment.getExternalStorageDirectory (); audioPath = baseDir.getAbsolutePath () + audioFilename; Log.d ("midi", " *** Look for file: " + audioPath); FileInputStream fis = new FileInputStream (audioPath); fd = fis.getFD (); if (fd != null) { Log.d ("midi", " *** Found file, trying to play: " + audioPath); MediaPlayer mediaPlayer = new MediaPlayer (); mediaPlayer.setDataSource (fd); mediaPlayer.prepare (); mediaPlayer.start (); } } catch (Exception e) { Log.d ("midi", " *** Exception while trying to play file: " + e.getMessage ()); } } public void stopSong () { if (media_player != null) { media_player.stop (); media_player.release (); media_player = null; } // if } // stopSong // } // Class: MIDI_Output // 

Desde MidiDriver y MIDI_Output se creó un proyecto Eclipse Android, se agregó una MainActivity y se ejecutó. Después de eliminar un montón de bugs lo conseguí y funcionando. Una herramienta útil es el depurador android (adb). Abra una ventana de comandos y ejecute adb -d logcat. He añadido muchas declaraciones de log.d ('midi "," mensaje *** ") en el código para ver donde las cosas salieron mal. Eliminarlos si no te gustan, pero si eres desconocido de Android (que todavía estoy en gran medida) es una manera útil de ver lo que sucede en su aplicación. El registro trabaja tan bien en Delphi, vea las fuentes de Delphi.

Cuando el programa compila bien, tiene un paquete MIDI_Output.apk en su directorio de proyecto \ bin. Este paquete será utilizado por Delphi para ejecutar los métodos Java.

Se puede acceder a Java desde Delphi usando JNI. Un tutorial práctico se puede encontrar en el sitio de RedTitan . Las ideas de este tutorial se implementaron en la clase TMIDI_Output_Device.

Como puede ver una cadena constante test_apk_fn se define con la ruta al paquete MIDI_Output.apk de Android. Esta cadena proporciona a JNI el nombre donde se puede encontrar la biblioteca de Java. La cadena javaClassName proporciona el nombre del paquete necesario para interactuar con Java. Con estas cadenas el JNI de Delphi es capaz de encontrar las clases solicitadas.

Clase TMIDI_Output_Device – proporcionando un reinicio de Delphi para la clase Java MIDI_Output

 unit MIDI_Output_Device; interface uses System.SysUtils, FMX.Types, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Jni, Androidapi.JNI.Dalvik, Androidapi.JNI.GraphicsContentViewText; const test_apk_fn = '/storage/sdcard0/Data/d/MIDI_Output.apk'; type TMIDI_Output_Device = class (TObject) private JavaEnv: PJNIEnv; context: JContext; CL: JDexClassLoader; JavaObject: JObject; JavaObjectID: JNIObject; jTempClass: Jlang_Class; jTemp: JObject; oTemp: TObject; jLocalInterface: ILocalObject; optimizedpath_jfile: JFile; dexpath_jstring, optimizedpath_jstring: JString; fun_version: JNIMethodID; fun_start: JNIMethodID; fun_put_short: JNIMethodID; fun_play_song: JNIMethodID; public constructor Create; procedure setup_midi_output (class_name: string); procedure put_short (status, data_1, data_2: integer); procedure play_song (file_name: string); end; // Class: MIDI_Output_Device // implementation uses FMX.Helpers.Android; constructor TMIDI_Output_Device.Create; begin setup_midi_output ('MIDI_Output'); end; // Create // procedure TMIDI_Output_Device.setup_midi_output (class_name: string); var javaClassName: string; ji: JNIInt; jiStatus, jiData_1, jiData_2: JNIValue; begin javaClassName := Format ('org.drivers.midioutput/%s', [class_name]); context := SharedActivityContext; JavaEnv := TJNIResolver.GetJNIEnv; Log.d ('Loading external library from "' + test_apk_fn + '"'); dexpath_jstring := StringToJString (test_apk_fn); // locate/create a directory where our dex files can be put optimizedpath_jfile := context.getDir (StringToJString ('outdex'), TJContext.javaclass.mode_private); optimizedpath_jstring := optimizedpath_jfile.getAbsolutePath; Log.d ('Path for DEX files = ' + JStringToString (optimizedpath_jstring)); Log.d ('APK containing target class = ' + JStringToString (dexpath_jstring)); CL := TJDexClassLoader.JavaClass.init (dexpath_jstring, optimizedpath_jstring, nil, TJDexClassLoader.JavaClass.getSystemClassLoader); // Test whether the Dex class is loaded, if not, exit if not assigned (CL) then begin Log.d ('?Failed to get DEXClassLoader'); exit; end; // if // Load the Java class jTempClass := CL.loadClass (StringToJString (javaClassName)); if assigned (jTempClass) then begin jTemp := jTempClass; // NB You could now import the entire class if jTemp.QueryInterface (ILocalObject,jLocalInterface) = S_OK then begin // supports ilocalobject JavaObject := jTempClass.newInstance; oTemp := JavaObject as TObject; JavaObjectID := tjavaimport (otemp).GetObjectID; Log.d (oTemp.ClassName); // try to access the version function from the midi_output class fun_version := TJNIResolver.GetJavaMethodID ((jTempClass as ILocalObject).GetObjectID, 'version', '()I'); if not assigned (fun_version) then begin Log.d ('?fun_version not supported'); end else begin ji := JavaEnv^.CallIntMethodA (JavaEnv, JavaObjectID, fun_version, nil); Log.d ('version returns ' + inttostr (ji)); end; // if // try to access the start function from the midi_output class fun_start := TJNIResolver.GetJavaMethodID ((jTempClass as ILocalObject).GetObjectID, 'start', '()V'); if not assigned (fun_start) then begin Log.d ('?fun_start not supported'); end else begin JavaEnv^.CallVoidMethodA (JavaEnv, JavaObjectID, fun_start, nil); Log.d ('fun_start found'); end; // if // try to access the putShort function from the midi_output class fun_put_short := TJNIResolver.GetJavaMethodID ((jTempClass as ILocalObject).GetObjectID, 'putShort','(III)V'); if not assigned (fun_put_short) then begin Log.d ('?putShort not supported'); end else begin Log.d (Format (' @@@ putShort (%d, %d, %d)', [jiStatus.i, jiData_1.i, jiData_2.i])); put_short ($90, 60, 127); end; // if // try to access the playSong function from the midi_output class fun_play_song := TJNIResolver.GetJavaMethodID ((jTempClass as ILocalObject).GetObjectID, 'playSong', '(Ljava/lang/String)V'); if not assigned (fun_play_song) then begin Log.d ('?playSong not supported'); end else begin Log.d (' @@@ playSong found'); end; // if end else begin Log.d ('?Could not derive ILOCALOBJECT'); end; end else Log.d ('?'+javaClassname+' not found') end; // setup_midi_output // procedure TMIDI_Output_Device.put_short (status, data_1, data_2: integer); var jiStatus, jiData_1, jiData_2: JNIValue; x: array of JNIOBJECT; begin jiStatus.i := status; jiData_1.i := data_1; jiData_2.i := data_2; setLength (x, 3); x [0] := jiStatus.l; x [1] := jiData_1.l; x [2] := jiData_2.l; Log.d (Format ('putShort (%d, %d, %d)', [jiStatus.i, jiData_1.i, jiData_2.i])); JavaEnv^.CallVoidMethodV (JavaEnv, JavaObjectID, fun_put_short, x); end; // put_short // procedure TMIDI_Output_Device.play_song (file_name: string); var x: array of JNIObject; begin SetLength (x, 1); x [0] := StringToJNIString (JavaEnv, file_name); Log.d ('playSong (' + file_name + ')'); JavaEnv^.CallVoidMethodV (JavaEnv, JavaObjectID, fun_play_song, x); end; // playSong // end. // Unit: MIDI_Output_Device // 

Delphi ahora sabe dónde encontrar las clases Java. En teoría ahora debería ser capaz de encontrar libmidi.so porque un paquete de Android es un archivo .zip que contiene los archivos necesarios para ejecutar el paquete de Java. Si abre MIDI_Output.apk con WinZip o WinRar, verá estos archivos. En el archivo encontrará una librería de directorios que contiene libmidi.so para las plataformas ARM 5 y 7. Al iniciar el programa y tener adb -d logcat ejecutándose en una ventana de comandos adb dice tanto desempaquetar MIDI_Output.apk. Bueno, podría hacerlo, pero libmidi.so no será encontrado.

Libmidi.so debe agregarse a \ usr \ lib en algún lugar bajo el directorio \ platformas del SDK de Android. El enlace completo en mi caso es: C: \ Users \ Public \ Documents \ RAD Studio \ 12.0 \ PlatformSDKs \ android-ndk-r8e \ plataformas \ android-14 \ arch-arm \ usr \ lib. Esto debería ayudar como lo descubrí hace algún tiempo .

Utilizando la cadena de llamadas como he mostrado aquí se puede llamar a funciones MIDI en Delphi generado Android código. Hay algunas preguntas con respecto a esta técnica:

  • ¿No sería más fácil llamar a la función NDK directamente? Es posible llamar a las funciones NDK directamente desde Delphi de la misma manera que las DLL. Sin embargo, la clase MidiDriver añade mucha funcionalidad que no entiendo en este momento. Esta funcionalidad debe ser programada en C o Pascal cuando se llama directamente a las funciones NDK.

  • En el código de Bill Farmer que utiliza el MediaPlayer para reproducir archivos MIDI. Por desgracia, sólo se puede acceder a MediaPlayer desde una actividad y no sé cómo transferir la MainActivity de Delphi a una función JNI Java. Así que esta funcionalidad no funciona todavía.

  • Las bibliotecas nativas se empaquetan en el archivo .apk pero no se desempaquetan de tal manera que el JavaVM lo detecta. Ahora el libmidi.so tiene que ser puesto manualmente en \ usr \ lib.

  • Aún peor es que un enlace duro se debe agregar al paquete .apk. El paquete se debe implementar automáticamente en / data / app-lib de la aplicación, de lo contrario, es imposible crear una aplicación con clases JNI e instalarla desde Play Store.

Otra forma habría sido usar una versión nativa de Android de BASS además del plugin BASSMIDI. Hay buen código de ejemplo que viene con él. Y el apoyo de Ian es excelente. Puedes encontrar los dos aquí:

BASS lib para Android: http://www.un4seen.com/forum/?topic=13225

Plugin BASSMIDI (d / l): http://www.un4seen.com/download.php?bassmidi24-linux

También hay una versión .NET en su sitio y un proyecto de terceros en Sourceforge que expone la API a Java. No se me permite publicar más de dos enlaces (todavía) pero se puede encontrar con una búsqueda rápida de nativebass

Respuesta bastante tardía, pero todavía podría ayudar a alguien más en busca de una forma más rápida o uno que funciona en Delphi solo.

  • ¿Es baja latencia de OSC / Midi (sin audio) posible en Android?
  • Android: Cargando archivos MIDI personalizados de DLS en Sonivox EAS
  • Cómo jugar notas individuales de archivo midi en LibGDX?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.