Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Native MIDI API

AMidi adalah Android NDK API yang memberi developer aplikasi kemampuan untuk mengirim dan menerima data MIDI dengan kode C/C++.

Aplikasi Android MIDI biasanya menggunakan midi API untuk berkomunikasi dengan layanan Android MIDI. 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 fungsi MidiDevice. Kode native Anda menggunakan fungsi AMidi yang berkomunikasi langsung dengan AMidiDevice. AMidiDevice terhubung langsung ke layanan MIDI:

Dengan panggilan AMidi, Anda dapat terus mengintegrasikan logika audio/kontrol C/C++ aplikasi dengan transmisi MIDI. Kebutuhan untuk panggilan JNI, atau callback ke sisi 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 menurunkan peristiwa dari sisi Java. Atau proses penulisan algoritmik dapat mengirim performa MIDI langsung ke AMidiDevice tanpa melakukan callback ke sisi 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 mengambilnya dari sana.

Terkadang, Anda mungkin perlu meneruskan informasi dari lapisan UI ke kode native. Misalnya, ketika peristiwa MIDI dikirim sebagai respons terhadap tombol di layar. Untuk melakukannya, buat panggilan JNI kustom ke logika native. Jika Anda 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, disertai contoh cara mengirim dan menerima perintah MIDI. Untuk contoh kerja lengkap, lihat aplikasi sampel NativeMidi.

Menggunakan AMidi

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

Memulai AMidi

Di sisi Java, aplikasi harus menemukan bagian hardware MIDI terpasang, membuat MidiDevice yang sesuai, dan meneruskannya ke kode native.

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

Menemukan hardware dan port

Objek port input dan output bukan milik 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, mendapatkan AMidiDevice menggunakan AMidiDevice_fromJava().
  2. Mendapatkan AMidiInputPort dan/atau AMidiOutputPort dari AMidiDevice dengan AMidiInputPort_open() dan/atau AMidiOutputPort_open().
  3. Menggunakan 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. Ini bisa jadi karena perangkat MIDI terputus atau karena aplikasi keluar.

Untuk melepaskan resource MIDI, kode Anda harus menjalankan tugas berikut:

  1. Berhenti membaca dan/atau menulis ke port MIDI. Jika Anda menggunakan thread pembacaan untuk melakukan pemilihan input (lihat Mengimplementasikan loop penggabungan di bawah), hentikan thread.
  2. Menutup semua objek AMidiInputPort dan/atau AMidiOutputPort yang terbuka dengan fungsi AMidiInputPort_close() dan/atau AMidiOutputPort_close().
  3. Melepas 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. Ini bisa berupa thread latar belakang, atau thread audio. AMidi tidak diblokir saat membaca dari port dan oleh karena itu aman untuk digunakan di callback audio.

Menyiapkan MidiDevice dan port outputnya

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. Saat MidiDevice dibuka, callback diterima ke instance MidiManager.OnDeviceOpenedListener(). Metode onDeviceOpened pemroses ini dipanggil, yang selanjutnya memanggil startReadingMidi() untuk membuka port output 0 pada perangkat. Ini adalah fungsi JNI yang ditetapkan dalam AppMidiManager.c. Fungsinya 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 sistem Java dan 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 pada perangkat:

AppMidiManager.c

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

    }
    

Menerapkan loop polling

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

Untuk aplikasi bandwidth rendah, seperti cakupan MIDI, Anda dapat melakukan polling di thread latar belakang prioritas rendah (dengan fungsi tidur yang sesuai).

Untuk aplikasi yang menghasilkan audio dan memiliki persyaratan performa real-time yang lebih ketat, Anda dapat melakukan polling di callback generasi audio utama (callback BufferQueue untuk OpenSL ES, callback data AudioStream di AAudio). Karena AMidiOutputPort_receive() adalah non-pemblokiran, akan ada sedikit dampak pada perfoma.

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;

        // Generate Audio…
        // ...

        // 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
            // ...
        }
    }
    

Diagram berikut mengilustrasikan alur aplikasi pembacaan MIDI:

Mengirim data MIDI

Contoh umum dari 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 pada perangkat. Ini adalah panggilan JNI yang ditetapkan di AppMidiManager.c. Fungsinya 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 pada perangkat:

AppMidiManager.c

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 waktu data MIDI keluar dipahami dengan baik dan dikendalikan 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 dalam 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 sisi Java (misalnya untuk mengupdate UI) Untuk melakukannya, Anda harus menulis kode di sisi Java dan lapisan native:

  • Buat metode callback di sisi 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 sisi 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(). Java MainActivity memanggil initNative() saat memulai:

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

Contoh aplikasi

Lihat contoh aplikasi Native MIDI lengkap di github.