Accessing MIDI in Delphi for Android

In this article I describe how to use MIDI sequencing with Delphi XE5 on the Android platform. This article is written for Delphi developers who, like me, are experienced with Delphi but less so with Eclipse, Android and Java.

Lots of my applications use MIDI to generate music. MIDI has never been well understood by system developers and they treat it somewhat casually. Audio and especially graphics and video get much more attention. This is a drawback for musicians and composers. They think in musical structures, something not feasible with just audio (and still not easy with MIDI).


Accessing the MIDI player in Android is a real chore. The Java SoundApi is a sufficient implementation of the MIDI standard but has not been incorporated into the Android SDK for one reason or another. This is a pity because there is a MIDI synthesizer available in Android. The MIDI capabilities are delivered by the SoniVox MIDI synthesizer but there is no Java interface for it. In 2010 there has been a request for a realtime API for the SoniVox MIDI synthesizer (Issue 8201) without results as yet.

Now there is Delphi XE5 which lets you generate applications for Windows, OSX, Android and iOS from one code base. I find this amazing. Together with a friend I started a little demo program on SourceForge and was pleasantly surprised how easy code could be ported. But: Delphi has no MIDI interface. For Windows the MIDI components of David Churcher are widely used and for Android… as you might have guessed for Android is nothing.

During the Delphi code rage from September 2013 I learned about JNI (Java Native Interface) with which Java classes can be accessed in Delphi XE5. Also I learned by reading more about issue 8201, that people had developed a SoniVox interface to Java via the NDK (Native Development Kit). I therefore decided that if I could piece all these bits together I might have some MIDI interface for Delphi apps on Android.

The good news is: I succeeded. The not so good news is: it ain’t easy. You hardly can avoid learning Java, Eclipse and the Android SDK and NDK, I did so in any case. I am still not sure whether I really had to root my Nexus 7, but it helped me too.

Summary of steps to be undertaken

Below I will summarize the steps which are ncessary to have MIDI in Delphi XE5 for Android. The text and downloads should get you started. If information lacks, just let me know.

  1. Setup your environment
  2.  Learn to access the SoniVox API functions via C
  3.  To access the API functions write a MIDI driver in C using the NDK
  4.  Access the NDK function via Java classes
  5.  Access the Java classes via JNI with Delphi
Call chain from the SoniVox to Delphi

Call chain from the SoniVox to Delphi

1. Setup your environment

Download and install the latest version for Eclipse for Java which in my case was Kepler with Service Pack 1. This should get you Java. 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 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).


You will need to use the ndk-build.cmd tool to build the libraries, You’ll find it in the NDK directory. In my case: C:\Users\Public\Documents\RAD Studio\12.0\PlatformSDKs\android-ndk-r8e. You’ll want to set a path to it.

2. Access the sonivox library

I made extensive use of the MIDI library developed by Willam Farmer. His library worked immediately without flaws on my system. He also has the full SoniVox documentation available which I couldn’t get elsewhere. His driver comes with a full example (Java) program.


For this Delphi implementation of MIDI download the MIDI_Output package. It contains both the sources for Java and Delphi. The Java package is essentially the same as Bill’s, only I changed the package name (see paragraph below for consequences). See the download page for more details. Unpack the library and import in Eclipse.

3. Write a MIDI driver in C

The MIDI library mentioned above already contains a MIDI driver in C. As you might know Java organizes its code in packages which typically have names like org.billthefarmer.mididriver or com.musoft.realgoodprogram etc. The function names in the NDK library (in this case in project\jni\midi.c) should reflect this. So a function which you would like to be known as version should be prefixed with Java_org_billthefarmer_mididriver. This prefix should be followed by the name of the class using this function. If that is MidiDriver, then the full function name would be: Java_org_billthefarmer_mididriver_MidiDriver_version.

I created my own project with package name org.drivers.midioutput, so all functions are prefixed by Java_org_drivers_midioutput_MidiDriver_ (see code below).  Of course all this is case sensitive.

When you wish to compile the midi.c jus open a command window, and call ndk-build in the project directory! Not in the jni directory. Some error messages are ok. The mips and x86 libraries will not always be built.

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 mentioned in paragraph 1. 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 prompot 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.

4. Acces the NDK via Java classes


I created all java stuff in the package org.drivers.midioutput. The consequences for midi.c have been described in the former paragraph.


MidiDriver.Java comes 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 the eliminating a lot of some bugs I got it up and running. A useful tool is the android debugger (adb). It can be found in C:\Users\Public\Documents\RAD Studio\12.0\PlatformSDKs\adt-bundle-windows-x86-20130522\sdk\platform-tools.


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. A valuable tool for your tool-chest.


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.

5. Access the Java classes via JNI with Delphi


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. Why is that?


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 liob 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 add 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.

Issues concerning the JNI/NDK interface


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.
  • 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 emplyed 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.

 

About Arnold

I compose music and love to write software so I write software to compose music. Somewhere in that proces I got attracted to robots. Probably this all makes few sense but it is fun.

Category(s): Music, Software
Tags: , , , , ,

Leave a Reply