Native MIDI API

AMidi API tersedia di Android NDK r20b dan yang lebih baru. API ini memungkinkan developer aplikasi mengirim dan menerima data MIDI dengan kode C/C++.

Aplikasi MIDI Android biasanya menggunakan midi API untuk berkomunikasi dengan layanan MIDI Android. Aplikasi MIDI utamanya bergantung pada MidiManager untuk menemukan, membuka, dan menutup satu atau beberapa objek MidiDevice, serta meneruskan data ke dan dari setiap perangkat melalui port input dan output MIDI perangkat:

Saat menggunakan AMidi, Anda meneruskan alamat MidiDevice ke lapisan kode native dengan panggilan JNI. Dari sana, AMidi membuat referensi ke AMidiDevice yang memiliki sebagian besar fungsionalitas MidiDevice. Kode native Anda menggunakan fungsi AMidi yang berkomunikasi langsung dengan AMidiDevice. AMidiDevice terhubung langsung ke layanan MIDI:

Dengan panggilan AMidi, Anda dapat mengintegrasikan logika audio/kontrol C/C++ aplikasi Anda secara mendalam dengan transmisi MIDI. Panggilan JNI atau callback ke sistem Java aplikasi Anda tidak begitu diperlukan. Misalnya, synthesizer digital yang diimplementasikan dalam kode C dapat menerima peristiwa utama langsung dari AMidiDevice, daripada menunggu panggilan JNI untuk mengirimkan peristiwa dari sistem Java. Atau, proses penulisan algoritmik dapat mengirim performa MIDI langsung ke AMidiDevice tanpa melakukan callback ke sistem Java untuk mengirimkan peristiwa utama.

Meskipun AMidi meningkatkan sambungan langsung ke perangkat MIDI, aplikasi tetap harus menggunakan MidiManager untuk menemukan dan membuka objek MidiDevice. AMidi dapat melanjutkannya dari sana.

Terkadang, Anda mungkin perlu meneruskan informasi dari lapisan UI ke kode native. Misalnya, saat peristiwa MIDI dikirim sebagai respons terhadap tombol di layar. Untuk melakukannya, buat panggilan JNI kustom ke logika native. Jika perlu mengirim data kembali untuk mengupdate UI, Anda dapat melakukan callback dari lapisan native seperti biasa.

Dokumen ini menunjukkan cara menyiapkan aplikasi kode native AMidi, dengan memberikan contoh tentang cara mengirim dan menerima perintah MIDI. Untuk contoh yang berfungsi penuh, lihat aplikasi contoh NativeMidi.

Menggunakan AMidi

Semua aplikasi yang menggunakan AMidi memiliki langkah penyiapan dan penutupan yang sama, baik mengirim atau menerima MIDI, atau keduanya.

Memulai AMidi

Pada sistem Java, aplikasi harus menemukan bagian hardware MIDI yang terpasang, membuat MidiDevice terkait, dan meneruskannya ke kode native.

  1. Temukan hardware MIDI dengan class MidiManager Java.
  2. Dapatkan objek Java MidiDevice yang terkait dengan hardware MIDI.
  3. Teruskan MidiDevice Java ke kode native dengan JNI.

Menemukan hardware dan port

Objek port input dan output tidak termasuk dalam aplikasi. Objek tersebut mewakili port pada perangkat midi. Untuk mengirim data MIDI ke perangkat, aplikasi membuka MIDIInputPort, kemudian menulis data ke dalamnya. Sebaliknya, untuk menerima data, aplikasi membuka MIDIOutputPort. Agar berfungsi dengan baik, aplikasi harus memastikan port yang dibuka adalah jenis yang benar. Penemuan perangkat dan port dilakukan di sistem Java.

Berikut adalah metode yang menemukan setiap perangkat MIDI dan melihat port-nya. Metode tersebut menampilkan daftar perangkat dengan port output untuk menerima data, atau daftar perangkat dengan port input untuk mengirim data. Perangkat MIDI dapat memiliki port input dan juga port output.

Kotlin

private fun getMidiDevices(isOutput: Boolean) : List {
    if (isOutput) {
        return mMidiManager.devices.filter { it.outputPortCount > 0 }
    } else {
        return mMidiManager.devices.filter { it.inputPortCount > 0 }
    }
}

Java

private List getMidiDevices(boolean isOutput){
  ArrayList filteredMidiDevices = new ArrayList<>();

  for (MidiDeviceInfo midiDevice : mMidiManager.getDevices()){
    if (isOutput){
      if (midiDevice.getOutputPortCount() > 0) filteredMidiDevices.add(midiDevice);
    } else {
      if (midiDevice.getInputPortCount() > 0) filteredMidiDevices.add(midiDevice);
    }
  }
  return filteredMidiDevices;
}

Untuk menggunakan fungsi AMidi dalam kode C/C++, Anda harus menyertakan AMidi/AMidi.h dan menautkannya ke library amidi. Ini dapat ditemukan di Android NDK.

Sistem Java harus meneruskan satu atau beberapa objek MidiDevice dan nomor port ke lapisan native melalui panggilan JNI. Selanjutnya, lapisan native harus melakukan langkah-langkah berikut:

  1. Untuk setiap MidiDevice Java, dapatkan AMidiDevice menggunakan AMidiDevice_fromJava().
  2. Dapatkan AMidiInputPort dan/atau AMidiOutputPort dari AMidiDevice dengan AMidiInputPort_open() dan/atau AMidiOutputPort_open().
  3. Gunakan port yang diperoleh untuk mengirim dan/atau menerima data MIDI.

Menghentikan AMidi

Aplikasi Java harus memberi sinyal ke lapisan native agar melepas resource saat tidak lagi menggunakan perangkat MIDI. Hal ini bisa jadi karena perangkat MIDI terputus atau karena aplikasi keluar.

Untuk melepaskan resource MIDI, kode harus menjalankan tugas berikut:

  1. Berhenti membaca dan/atau menulis ke port MIDI. Jika Anda menggunakan thread baca untuk melakukan polling input (lihat Mengimplementasikan loop polling di bawah), hentikan thread.
  2. Menutup semua objek AMidiInputPort dan/atau AMidiOutputPort yang terbuka dengan fungsi AMidiInputPort_close() dan/atau AMidiOutputPort_close().
  3. Melepaskan AMidiDevice dengan AMidiDevice_release().

Menerima data MIDI

Contoh umum aplikasi MIDI yang menerima MIDI adalah "synthesizer virtual" yang menerima data performa MIDI untuk mengontrol sintesis audio.

Data MIDI yang masuk diterima secara asinkron. Oleh karena itu, sebaiknya baca MIDI di thread terpisah yang secara terus-menerus memilih satu atau beberapa port output MIDI. Thread ini bisa berupa thread latar belakang, atau thread audio. AMidi tidak diblokir saat membaca dari port dan oleh karena itu aman untuk digunakan dalam callback audio.

Menyiapkan MidiDevice dan port output-nya

Aplikasi membaca data MIDI yang masuk dari port output perangkat. Sistem Java pada aplikasi Anda harus menentukan perangkat dan port mana yang akan digunakan.

Cuplikan ini membuat MidiManager dari layanan MIDI Android dan membuka MidiDevice untuk perangkat pertama yang ditemukannya. Setelah MidiDevice dibuka, callback diterima ke instance MidiManager.OnDeviceOpenedListener(). Metode onDeviceOpened pemroses ini dipanggil, yang selanjutnya memanggil startReadingMidi() untuk membuka port output 0 di perangkat. Tindakan ini adalah fungsi JNI yang ditentukan dalam AppMidiManager.cpp. Fungsi kode ini dijelaskan dalam cuplikan berikutnya.

Kotlin

//AppMidiManager.kt
class AppMidiManager(context : Context) {
  private external fun startReadingMidi(midiDevice: MidiDevice,
  portNumber: Int)
  val mMidiManager : MidiManager = context.getSystemService(Context.MIDI_SERVICE) as MidiManager

  init {
    val midiDevices = getMidiDevices(true) // method defined in snippet above
    if (midiDevices.isNotEmpty()){
      midiManager.openDevice(midiDevices[0], {
        startReadingMidi(it, 0)
      }, null)
    }
  }
}

Java

//AppMidiManager.java
public class AppMidiManager {
  private native void startReadingMidi(MidiDevice device, int portNumber);
  private MidiManager mMidiManager;
  AppMidiManager(Context context){
    mMidiManager = (MidiManager)
      context.getSystemService(Context.MIDI_SERVICE);
    List midiDevices = getMidiDevices(true); // method defined in snippet above
    if (midiDevices.size() > 0){
      mMidiManager.openDevice(midiDevices.get(0),
        new MidiManager.OnDeviceOpenedListener() {
        @Override
        public void onDeviceOpened(MidiDevice device) {
          startReadingMidi(device, 0);
        }
      },null);
    }
  }
}

Kode native menerjemahkan perangkat MIDI lingkungan Java beserta portnya ke dalam referensi yang digunakan oleh fungsi AMidi.

Berikut adalah fungsi JNI yang membuat AMidiDevice dengan memanggil AMidiDevice_fromJava(), kemudian memanggil AMidiOutputPort_open() untuk membuka port output di perangkat:

AppMidiManager.cpp

AMidiDevice midiDevice;
static pthread_t readThread;

static const AMidiDevice* midiDevice = AMIDI_INVALID_HANDLE;
static std::atomic<AMidiOutputPort*> midiOutputPort(AMIDI_INVALID_HANDLE);

void Java_com_nativemidiapp_AppMidiManager_startReadingMidi(
        JNIEnv* env, jobject, jobject deviceObj, jint portNumber) {
    AMidiDevice_fromJava(j_env, deviceObj, &midiDevice);

    AMidiOutputPort* outputPort;
    int32_t result =
      AMidiOutputPort_open(midiDevice, portNumber, &outputPort);
    // check for errors...

    // Start read thread
    int pthread_result =
      pthread_create(&readThread, NULL, readThreadRoutine, NULL);
    // check for errors...

}

Mengimplementasikan loop polling

Aplikasi yang menerima data MIDI harus melakukan polling port output dan merespons saat AMidiOutputPort_receive() menampilkan angka yang lebih besar daripada nol.

Untuk aplikasi bandwidth rendah, seperti cakupan MIDI, Anda dapat membuat kueri di thread latar belakang prioritas rendah (dengan fungsi sleep yang sesuai).

Untuk aplikasi yang menghasilkan audio dan memiliki persyaratan performa real-time yang lebih ketat, Anda dapat membuat kueri di callback pembuatan audio utama (callback BufferQueue untuk OpenSL ES, callback data AudioStream di AAudio). Karena AMidiOutputPort_receive() tidak melakukan pemblokiran, dampaknya pada performa akan sangat kecil.

Fungsi readThreadRoutine() yang dipanggil dari fungsi startReadingMidi() di atas mungkin terlihat seperti ini:

void* readThreadRoutine(void * /*context*/) {
    uint8_t inDataBuffer[SIZE_DATABUFFER];
    int32_t numMessages;
    uint32_t opCode;
    uint64_t timestamp;
    reading = true;
    while (reading) {
        AMidiOutputPort* outputPort = midiOutputPort.load();
        numMessages =
              AMidiOutputPort_receive(outputPort, &opCode, inDataBuffer,
                                sizeof(inDataBuffer), &timestamp);
        if (numMessages >= 0) {
            if (opCode == AMIDI_OPCODE_DATA) {
                // Dispatch the MIDI data.
            }
        } else {
            // some error occurred, the negative numMessages is the error code
            int32_t errorCode = numMessages;
        }
  }
}

Aplikasi yang menggunakan API audio native (seperti OpenSL ES atau AAudio) dapat menambahkan kode penerimaan MIDI ke callback pembuatan audio seperti ini:

void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void */*context*/)
{
    uint8_t inDataBuffer[SIZE_DATABUFFER];
    int32_t numMessages;
    uint32_t opCode;
    uint64_t timestamp;

    // Read MIDI Data
    numMessages = AMidiOutputPort_receive(outputPort, &opCode, inDataBuffer,
        sizeof(inDataBuffer), &timestamp);
    if (numMessages >= 0 && opCode == AMIDI_OPCODE_DATA) {
        // Parse and respond to MIDI data
        // ...
    }

    // Generate Audio
    // ...
}

Diagram berikut mengilustrasikan alur aplikasi pembacaan MIDI:

Mengirim data MIDI

Contoh umum aplikasi penulisan MIDI adalah pengontrol atau sequencer MIDI.

Menyiapkan MidiDevice dan port inputnya

Aplikasi menulis data MIDI keluar ke port input perangkat MIDI. Sistem Java pada aplikasi Anda harus menentukan perangkat dan port mana yang akan digunakan.

Kode penyiapan di bawah ini adalah variasi dari contoh penerimaan di atas. Kode ini membuat MidiManager dari layanan MIDI Android. Selanjutnya, kode ini membuka MidiDevice pertama yang ditemukannya dan memanggil startWritingMidi() untuk membuka port input pertama di perangkat. Tindakan ini adalah panggilan JNI yang ditentukan dalam AppMidiManager.cpp. Fungsi kode ini dijelaskan dalam cuplikan berikutnya.

Kotlin

//AppMidiManager.kt
class AppMidiManager(context : Context) {
  private external fun startWritingMidi(midiDevice: MidiDevice,
  portNumber: Int)
  val mMidiManager : MidiManager = context.getSystemService(Context.MIDI_SERVICE) as MidiManager

  init {
    val midiDevices = getMidiDevices(false) // method defined in snippet above
    if (midiDevices.isNotEmpty()){
      midiManager.openDevice(midiDevices[0], {
        startWritingMidi(it, 0)
      }, null)
    }
  }
}

Java

//AppMidiManager.java
public class AppMidiManager {
  private native void startWritingMidi(MidiDevice device, int portNumber);
  private MidiManager mMidiManager;

  AppMidiManager(Context context){
    mMidiManager = (MidiManager)
      context.getSystemService(Context.MIDI_SERVICE);
    List midiDevices = getMidiDevices(false); // method defined in snippet above
    if (midiDevices.size() > 0){
      mMidiManager.openDevice(midiDevices.get(0),
        new MidiManager.OnDeviceOpenedListener() {
        @Override
        public void onDeviceOpened(MidiDevice device) {
          startWritingMidi(device, 0);
        }
      },null);
    }
  }
}

Berikut adalah fungsi JNI yang membuat AMidiDevice dengan memanggil AMidiDevice_fromJava(), kemudian memanggil AMidiInputPort_open() untuk membuka port input di perangkat:

AppMidiManager.cpp

void Java_com_nativemidiapp_AppMidiManager_startWritingMidi(
       JNIEnv* env, jobject, jobject midiDeviceObj, jint portNumber) {
   media_status_t status;
   status = AMidiDevice_fromJava(
     env, midiDeviceObj, &sNativeSendDevice);
   AMidiInputPort *inputPort;
   status = AMidiInputPort_open(
     sNativeSendDevice, portNumber, &inputPort);

   // store it in a global
   sMidiInputPort = inputPort;
}

Mengirim data MIDI

Karena penentuan waktu data MIDI keluar dipahami dengan baik dan dikontrol oleh aplikasi itu sendiri, transmisi data dapat dilakukan di thread utama aplikasi MIDI. Namun, karena alasan performa (seperti pada sequencer), pembuatan dan transmisi MIDI dapat dilakukan di thread terpisah.

Aplikasi dapat mengirimkan data MIDI setiap kali diperlukan. Perhatikan bahwa AMidi melakukan pemblokiran saat sedang menulis data.

Berikut adalah contoh metode JNI yang menerima buffer perintah MIDI dan menuliskannya:

void Java_com_nativemidiapp_TBMidiManager_writeMidi(
JNIEnv* env, jobject, jbyteArray data, jint numBytes) {
   jbyte* bufferPtr = env->GetByteArrayElements(data, NULL);
   AMidiInputPort_send(sMidiInputPort, (uint8_t*)bufferPtr, numBytes);
   env->ReleaseByteArrayElements(data, bufferPtr, JNI_ABORT);
}

Diagram berikut mengilustrasikan alur aplikasi penulisan MIDI:

Callback

Meskipun bukan sepenuhnya fitur AMidi, kode native Anda mungkin perlu meneruskan data kembali ke sistem Java (misalnya untuk mengupdate UI). Untuk melakukannya, Anda harus menulis kode di sistem Java dan lapisan native:

  • Buat metode callback di sistem Java.
  • Tulis fungsi JNI yang menyimpan informasi yang diperlukan untuk memanggil callback.

Jika waktu callback sudah tiba, kode native Anda dapat dibuat

Berikut adalah metode callback untuk lingkungan Java, onNativeMessageReceive():

Kotlin

//MainActivity.kt
private fun onNativeMessageReceive(message: ByteArray) {
  // Messages are received on some other thread, so switch to the UI thread
  // before attempting to access the UI
  runOnUiThread { showReceivedMessage(message) }
}

Java

//MainActivity.java
private void onNativeMessageReceive(final byte[] message) {
        // Messages are received on some other thread, so switch to the UI thread
        // before attempting to access the UI
        runOnUiThread(new Runnable() {
            public void run() {
                showReceivedMessage(message);
            }
        });
}

Berikut adalah kode C untuk fungsi JNI yang menyiapkan callback ke MainActivity.onNativeMessageReceive(). MainActivity Java memanggil initNative() pada saat mulai:

MainActivity.cpp

/**
 * Initializes JNI interface stuff, specifically the info needed to call back into the Java
 * layer when MIDI data is received.
 */
JNICALL void Java_com_example_nativemidi_MainActivity_initNative(JNIEnv * env, jobject instance) {
    env->GetJavaVM(&theJvm);

    // Setup the receive data callback (into Java)
    jclass clsMainActivity = env->FindClass("com/example/nativemidi/MainActivity");
    dataCallbackObj = env->NewGlobalRef(instance);
    midDataCallback = env->GetMethodID(clsMainActivity, "onNativeMessageReceive", "([B)V");
}

Jika waktu untuk mengirim data kembali ke Java sudah tiba, kode native akan mengambil pointer callback dan membuat callback:

AppMidiManager.cpp

// The Data Callback
extern JavaVM* theJvm;              // Need this for allocating data buffer for...
extern jobject dataCallbackObj;     // This is the (Java) object that implements...
extern jmethodID midDataCallback;   // ...this callback routine

static void SendTheReceivedData(uint8_t* data, int numBytes) {
    JNIEnv* env;
    theJvm->AttachCurrentThread(&env, NULL);
    if (env == NULL) {
        LOGE("Error retrieving JNI Env");
    }

    // Allocate the Java array and fill with received data
    jbyteArray ret = env->NewByteArray(numBytes);
    env->SetByteArrayRegion (ret, 0, numBytes, (jbyte*)data);

    // send it to the (Java) callback
    env->CallVoidMethod(dataCallbackObj, midDataCallback, ret);
}

Referensi tambahan