Deploy MIDI with Delphi on Android

I have been looking for some time to play MIDI in Delphi XE5 with an Android landmark. Some of my questions were related to this "search" :-). I submitted two requests to embarcadero: # 119422 to add MIDI support for TMediaPlayer and # 119423 to add a MIDI framework to Firemonkey, but that didn't help. I finally succeeded. Since I know there are a few more people who were looking for MIDI on Android, I am posting this question with an answer to the documentation.

+4
source share
2 answers

Android has a built-in MIDI synthesizer. You can access it through the Android NDK. I described this in an article containing several downloads . This answer is a brief description of this article. What you see here is proof of concept. It will show how to play MIDI notes on Android, but needs to be improved. Suggestions for improvement are welcome :-)

Eclipse Java. , Delphi XE5 Mobile pack, , : Android SDK NDK. , Android SDK Google. Eclipse Android Development Tools (ADT) . Android SDK/NDK, Delphi XE5 ( Delphi, Options | Tools | SDK Manager). , Delphi Eclipse SDK NDK.

MIDI-, Willam Farmer. SoniVox, . (Java). org.drivers.midioutput, Java_org_drivers_midioutput_MidiDriver_ (. ).

midi.c jus, ndk-build . . mips x86 .

, : ndk . Delphi Delphi, : Rad Studio , Delphi SDK NDK. , C:, C:\ndk. MKLINK, ndk. , , , . , , , . ndk-build.

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

ndk-build, lib .so. , midi.c libmidi.so. , midi.c.

MidiDriver.Java , audioTrack . , . , , Delphi Java MidiDriver: class MIDI_Output. Delphi.

MidiDriver - Java C-, SoniVox. MIDI_Output - Java Delphi. MIDI_Output MidiDriver.

MidiDriver - 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");
    }
}

MIDI_Output - 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 //

MidiDriver MIDI_Output Eclipse Android, MainActivity. . (adb). adb -d logcat. log.d( "midi", "*** message" ) , , . , , Android ( ), , . Delphi, . Delphi.

, MIDI_Output.apk \bin. Delphi Java.

Java Delphi JNI. RedTitan. TMIDI_Output_Device.

, test_apk_fn MIDI_Output.apk Android. JNI , Java. javaClassName , Java. Delphi JNI .

TMIDI_Output_Device - Delphi 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;    // 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 , Java. libmidi.so, Android - .zip , Java. MIDI_Output.apk WinZip WinRar, . lib, libmidi.so ARM 5 7. adb -d logcat adb , MIDI_Output.apk. , , libmidi.so .

Libmidi.so \usr\lib - \platform Android SDK. : C:\Users\Public\Documents\RAD Studio\12.0\PlatformSDKs\android-ndk-r8e\platform\android-14\arch-arm\usr\lib. , .

, , MIDI Delphi Android-. :

  • NDK ? NDK Delphi DLL. MidiDriver . C Pascal, NDK.

  • MediaPlayer MIDI . , MediaPlayer Activity , Delphi MainActivity JNI Java . , .

  • .apk, JavaVM . libmidi.so \usr\lib.

  • , ​​ .apk. /data/app -lib , JNI Play Store .

+9

Android BASS plus BASSMIDI. . . :

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

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

.NET, Sourceforge, API Java. (), nativebass

, -, , Delphi.

+2

All Articles