Arnold Arnold - 2 months ago 20
Android Question

Implement MIDI with Delphi on Android

I am looking since some time for a way to play MIDI in Delphi XE5 with Android targeted. Several of my questions before were related to this "quest" :-). I have filed two requests to embarcadero: #119422 to add MIDI support to TMediaPlayer and #119423 to add a MIDI framework to Firemonkey, but that did not help. I have succeeded at last. As I know there are some more people who were looking for MIDI on Android I post this question with answer for documentation.

Answer

The Android system has an internal MIDI synthesizer. You can access it via the Android NDK. I have described this in an article containing some downloads. This answer is a short description of this article. What you'll see here is a Proof of Concept. It will show how to play MIDI notes on an Android system but needs improvement. Suggestions for improvement are welcome :-)

Use Eclipse to interface with the Java project. I presume you have Delphi XE5 with the Mobile pack, which gives you two things already installed: the Android SDK and NDK. Do not reinstall these by downloading the complete Android SDK from Google. Download and install the Eclipse Android Development Tools (ADT) plugin and follow the installation instructions. This allows you to use the Android SDK/NDK environment already installed by Delphi XE5 (you will find the paths in Delphi, Options | Tools | SDK Manager). In this way Delphi and Eclipse will share the same SDK and NDK.

I used the MIDI library developed by Willam Farmer. He also has the full SoniVox documentation available which I couldn't get elsewhere. His driver comes with a full example (Java) program. I created my own project with and changed the package name to org.drivers.midioutput, so all functions are prefixed by Java_org_drivers_midioutput_MidiDriver_ (see code below).

When you wish to compile the midi.c jus open a command window, and call ndk-build in the project directory. Some error messages are ok. The mips and x86 libraries were not built in my case.

There is one point though you should be aware of: the path to the ndk may not contain spaces. As you let the Delphi installer install Delphi there is bound to be a space in it: the subdirectory Rad Studio in that terrible long file name where Delphi installs the SDK and NDK. In order to work around this problem, create an empty directory on drive C:, call it C:\ndk. Use MKLINK to link this directory to the ndk directory. This can only be done from an elevated command prompt and as you do so you'll lose your network connections. The link is persistent so just close the command prompt and open another, unelevated one, and all should work now. Now you can really use ndk-build.

midi.c - the NDK interface with the 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;
}

When the library is built by ndk-build this will prefix the compiled library with lib and replace the extension by .so. So midi.c will compile to libmidi.so. Compiled libraries are added to the download, so you needn't compile midi.c.

MidiDriver.Java declares an interface, an audioTrack and a thread to handle all these. I haven't taken the trouble to find how exactly this works. Because I didn't know how to handle an interface and such in Delphi I created a Java wrapper for MidiDriver: class MIDI_Output. This class is used to interface with Delphi.

Class MidiDriver is the interface between Java and the C-functions that call SoniVox functions. Class MIDI_Output is the interface between Java and Delphi. MIDI_Output creates an instance of MidiDriver.

Class MidiDriver - the interface with the 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");
    }
}

Class MIDI_Output - providing a wrap for class 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 //

From MidiDriver and MIDI_Output an Eclipse Android project was created, a MainActivity added and run. After eliminating a lot of bugs I got it up and running. A useful tool is the android debugger (adb). Open a command windows and run adb -d logcat. I have added a lot of log.d ('midi", " * message') statements in the code in order to see where things went wrong. Remove them if you don't like them, but if you are unknown to Android (which I still am to a great extent) it is a useful way to see what happens in your application. Log works as wel in Delphi, see the Delphi Sources.

When the program compiles well you have a MIDI_Output.apk package in your project\bin directory. This package will be used by Delphi to run the Java methods.

Java can be accessed from Delphi by using JNI. A hands-on tutorial can be found on the site of RedTitan. The ideas of this tutorial were implemented in class TMIDI_Output_Device.

As you may see a constant string test_apk_fn is defined with the path to the MIDI_Output.apk Android package. This string provides JNI with the name where the Java library can be found. The string javaClassName provides the package name necessary to interface with Java. With these string the Delphi JNI is able to find the requested classes.

Class TMIDI_Output_Device - providing a Delphi wrap for Java class 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;    // N.B 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 now knows where to find the Java classes. In theory it should now be able to find libmidi.so because an Android package is a .zip file containing the necessary files to run the Java package. If you open MIDI_Output.apk with WinZip or WinRar then you see these files. In the archive you'll find a directory lib which contains libmidi.so for the ARM 5 and 7 platforms. When launching the program and having adb -d logcat running in a command window adb says so much as unpacking MIDI_Output.apk. Well, it might do so, but libmidi.so will not be found.

Libmidi.so should be added to the \usr\lib somewhere under the \platforms directory of the Android SDK. The complete link in my case is: C:\Users\Public\Documents\RAD Studio\12.0\PlatformSDKs\android-ndk-r8e\platforms\android-14\arch-arm\usr\lib. This should help as I found out some time ago.

Using the call chain as I have shown here one may call MIDI functions in Delphi generated Android code. There are some questions regarding this technique:

  • Wouldn't it be easier to call the NDK function directly? It is possible to call NDK functions directly from Delphi in the same way as DLL's. However, class MidiDriver adds a lot of functionality which I do not understand at this moment. This functionality must be programmed in C or Pascal when call the NDK functions directly.

  • In the code from Bill Farmer he uses the MediaPlayer to play MIDI files. Alas the MediaPlayer can only be accessed from an Activity and I do not know how to transfer the Delphi MainActivity to a JNI Java function. So this functionality does not work as of yet.

  • Native libraries are packed into the .apk but not unpacked in such a way that the JavaVM detects it. Now the libmidi.so has to be put manually into \usr\lib.

  • Even worse is that a hard link must be added to the .apk package. The package should be deployed automatically to the /data/app-lib of the application, else creating an app with JNI classes and installing it from the Play Store seems impossible.

Comments