Native MIDI API

AMidi API'si, Android NDK r20b ve sonraki sürümlerde kullanılabilir. Bu API, uygulama geliştiricilerine C/C++ koduyla MIDI verileri gönderme ve alma olanağı sunar.

Android MIDI uygulamaları, Android MIDI hizmetiyle iletişim kurmak için genellikle midi API'yi kullanır. MIDI uygulamaları, bir veya daha fazla MidiDevice nesnesini keşfetmek, açmak ve kapatmak ve cihazın MIDI giriş ile çıkış bağlantı noktaları üzerinden her cihazla veri aktarmak için öncelikle MidiManager'den yararlanır:

AMidi'yi kullandığınızda MidiDevice adresini JNI çağrısıyla yerel kod katmanına iletirsiniz. AMidi, buradan MidiDevice işlevinin çoğuna sahip bir AMidiDevice referansı oluşturur. Yerel kodunuz, doğrudan bir AMidiDevice ile iletişim kuran AMidi işlevlerini kullanır. AMidiDevice doğrudan MIDI hizmetine bağlanır:

AMidi çağrılarını kullanarak uygulamanızın C/C++ ses/kontrol mantığını MIDI iletimi ile yakın bir şekilde entegre edebilirsiniz. JNI çağrılarına veya uygulamanızın Java tarafına geri çağrılara daha az ihtiyaç duyulur. Örneğin, C kodunda uygulanan bir dijital sentezleyici, etkinlikleri Java tarafından gönderilmesini beklemek yerine önemli etkinlikleri doğrudan bir AMidiDevice'ten alabilir. Ya da algoritmik bir besteleme işlemi, önemli etkinlikleri iletmek için Java tarafına geri dönmeden MIDI performansını doğrudan bir AMidiDevice'e gönderebilir.

AMidi, MIDI cihazlarına doğrudan bağlantıyı iyileştirse de uygulamaların MidiDevice nesnelerini keşfetmek ve açmak için MidiManager öğesini kullanması gerekir. AMidi bu noktadan itibaren devam edebilir.

Bazen kullanıcı arayüzü katmanından yerel koda bilgi aktarmanız gerekebilir. Örneğin, ekrandaki düğmelere yanıt olarak MIDI etkinlikleri gönderildiğinde. Bunu yapmak için yerel mantığınıza özel JNI çağrıları oluşturun. Kullanıcı arayüzünü güncellemek için geri veri göndermeniz gerekiyorsa her zamanki gibi yerel katmandan geri çağırabilirsiniz.

Bu belgede, hem MIDI komutları gönderme hem de alma örnekleri verilerek AMidi yerel kod uygulamasının nasıl oluşturulacağı gösterilmektedir. Çalışan tam bir örnek için NativeMidi örnek uygulamasına göz atın.

AMidi kullanma

AMidi kullanan tüm uygulamalarda, MIDI gönderip alma veya her ikisini birden yapma işlemlerinde aynı kurulum ve kapatma adımları geçerlidir.

AMidi'yi başlatma

Java tarafında, uygulama bağlı bir MIDI donanım parçası keşfetmeli, buna karşılık gelen bir MidiDevice oluşturmalı ve bunu yerel koda iletmelidir.

  1. Java MidiManager sınıfını kullanarak MIDI donanımlarını keşfedin.
  2. MIDI donanımına karşılık gelen bir Java MidiDevice nesnesi edinin.
  3. Java MidiDevice'ü JNI ile yerel koda aktarın.

Donanımı ve bağlantı noktalarını keşfetme

Giriş ve çıkış bağlantı noktası nesneleri uygulamaya ait değildir. Midi cihazındaki bağlantı noktalarını temsil eder. Bir uygulama, MIDI verilerini bir cihaza göndermek için bir MIDIInputPort açar ve ardından bu MIDIInputPort'ye veri yazar. Buna karşılık, uygulama veri almak için MIDIOutputPort açar. Uygulamanın düzgün çalışması için açtığı bağlantı noktalarının doğru türde olduğundan emin olması gerekir. Cihaz ve bağlantı noktası keşfi Java tarafında yapılır.

Aşağıda, her MIDI cihazını keşfeden ve bağlantı noktalarına bakan bir yöntem verilmiştir. Veri almak için çıkış bağlantı noktalarına sahip cihazların veya veri göndermek için giriş bağlantı noktalarına sahip cihazların listesini döndürür. MIDI cihazlarında hem giriş hem de çıkış bağlantı noktaları olabilir.

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

C/C++ kodunuzda AMidi işlevlerini kullanmak için AMidi/AMidi.h'ü eklemeniz ve amidi kitaplığına bağlamanız gerekir. Bunları Android NDK'da da bulabilirsiniz.

Java tarafı, bir veya daha fazla MidiDevice nesnesini ve bağlantı noktası numarasını JNI çağrısı aracılığıyla yerel katmana iletmelidir. Yerel katman daha sonra aşağıdaki adımları gerçekleştirmelidir:

  1. Her Java MidiDevice için AMidiDevice_fromJava() kullanarak bir AMidiDevice elde edin.
  2. AMidiInputPort_open() ve/veya AMidiOutputPort_open() ile AMidiDevice'den AMidiInputPort ve/veya AMidiOutputPort elde edin.
  3. MIDI verileri göndermek ve/veya almak için elde edilen bağlantı noktalarını kullanın.

AMidi'yi durdurma

Java uygulaması, MIDI cihazını kullanmayı bıraktığında yerel katmana kaynakları serbest bırakması için sinyal göndermelidir. Bunun nedeni MIDI cihazının bağlantısının kesilmesi veya uygulamanın kapanması olabilir.

MIDI kaynaklarını yayınlamak için kodunuzun şu görevleri gerçekleştirmesi gerekir:

  1. MIDI bağlantı noktalarında okuma ve/veya yazma işlemlerini durdurun. Giriş için anket yapmak üzere bir okuma mesaj dizisi kullanıyorsanız (aşağıdaki Anket döngüsü uygulama bölümüne bakın) mesaj dizisini durdurun.
  2. AMidiInputPort_close() ve/veya AMidiOutputPort_close() işlevleriyle açık olan tüm AMidiInputPort ve/veya AMidiOutputPort nesnelerini kapatın.
  3. AMidiDevice öğesini AMidiDevice_release() ile yayınlayın.

MIDI verilerini alma

MIDI alan MIDI uygulamalarının tipik bir örneği, ses sentezleme işlemini kontrol etmek için MIDI performans verilerini alan "sanal sentezleyici"dir.

Gelen MIDI verileri eşzamansız olarak alınır. Bu nedenle, MIDI'yi bir veya daha fazla MIDI çıkış bağlantı noktasını sürekli olarak sorgulayan ayrı bir iş parçacığında okumak en iyisidir. Bu, arka plan veya ses mesajı olabilir. AMidi, bir bağlantı noktasından okuma yaparken engellemez ve bu nedenle ses geri çağırma işlevinde kullanılması güvenlidir.

MidiDevice ve çıkış bağlantı noktalarını ayarlama

Uygulama, cihazın çıkış bağlantı noktalarından gelen MIDI verilerini okur. Uygulamanızın Java tarafı, hangi cihaz ve bağlantı noktalarının kullanılacağını belirlemelidir.

Bu snippet, Android'in MIDI hizmetinden MidiManager oluşturur ve bulduğu ilk cihaz için bir MidiDevice açar. MidiDevice açıldığında MidiManager.OnDeviceOpenedListener() örneğine geri çağırma alınır. Bu dinleyicinin onDeviceOpened yöntemi çağrılır ve ardından cihazdaki 0 numaralı çıkış bağlantı noktasını açmak için startReadingMidi() çağrılır. Bu, AppMidiManager.cpp içinde tanımlanmış bir JNI işlevidir. Bu işlev bir sonraki snippet'te açıklanmıştır.

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

Yerel kod, Java tarafındaki MIDI cihazını ve bağlantı noktalarını AMidi işlevleri tarafından kullanılan referanslara dönüştürür.

AMidiDevice_fromJava()'u çağırarak bir AMidiDevice oluşturan ve ardından cihazda bir çıkış bağlantı noktası açmak için AMidiOutputPort_open()'yi çağıran JNI işlevi aşağıda verilmiştir:

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

}

Anket döngüsü uygulama

MIDI verileri alan uygulamalar, çıkış bağlantı noktasını yoklamalı ve AMidiOutputPort_receive() sıfırdan büyük bir sayı döndürdüğünde yanıt vermelidir.

MIDI kapsamı gibi düşük bant genişliğine sahip uygulamalarda, düşük öncelikli bir arka plan iş parçacığında (uygun uyku süreleriyle) anket yapabilirsiniz.

Ses üreten ve daha katı gerçek zamanlı performans koşullarına sahip uygulamalar için ana ses oluşturma geri çağırma işlevinde (OpenSL ES için BufferQueue geri çağırma işlevi, AAudio'daki AudioStream veri geri çağırma işlevi) anket yapabilirsiniz. AMidiOutputPort_receive() engelleme olmadığı için performans üzerinde çok az etki vardır.

Yukarıdaki startReadingMidi() işlevinden çağrılan readThreadRoutine() işlevi şu şekilde görünebilir:

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

Yerel bir ses API'si (OpenSL ES veya AAudio gibi) kullanan bir uygulama, ses oluşturma geri çağırma işlevine aşağıdaki gibi MIDI alma kodu ekleyebilir:

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

Aşağıdaki şemada MIDI okuma uygulamasının akışı gösterilmektedir:

MIDI verilerini gönderme

MIDI yazma uygulamasına örnek olarak MIDI kontrol cihazı veya sıralayıcı verilebilir.

MidiDevice ve giriş bağlantı noktalarını ayarlama

Uygulama, giden MIDI verilerini MIDI cihazının giriş bağlantı noktalarına yazar. Uygulamanızın Java tarafı, hangi MIDI cihazının ve bağlantı noktalarının kullanılacağını belirlemelidir.

Aşağıdaki kurulum kodu, yukarıdaki alma örneğinin bir varyasyonudur. Android'in MIDI hizmetinden MidiManager oluşturur. Ardından, bulduğu ilk MidiDevice öğesini açar ve cihazdaki ilk giriş bağlantı noktasını açmak için startWritingMidi() öğesini çağırır. Bu, AppMidiManager.cpp içinde tanımlanmış bir JNI çağrısıdır. İşlev, sonraki snippet'te açıklanmaktadır.

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

AMidiDevice_fromJava()'ü çağırarak bir AMidiDevice oluşturan ve ardından cihazda bir giriş bağlantı noktası açmak için AMidiInputPort_open()'yi çağıran JNI işlevi aşağıda verilmiştir:

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

MIDI verilerini gönderme

Giden MIDI verilerinin zamanlaması uygulama tarafından iyi anlaşılır ve kontrol edildiğinden veri aktarımı MIDI uygulamasının ana iş akışında yapılabilir. Ancak performans nedeniyle (sıralı düzenleyicide olduğu gibi) MIDI'nin oluşturulması ve iletilmesi ayrı bir iş parçacığında yapılabilir.

Uygulamalar gerektiğinde MIDI verileri gönderebilir. AMidi'nin veri yazarken engellediğini unutmayın.

Aşağıda, MIDI komutları içeren bir arabellek alan ve bu arabelleği yazan örnek bir JNI yöntemi verilmiştir:

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

Aşağıdaki şemada, MIDI yazma uygulamasının akışı gösterilmektedir:

Geri aramalar

Tam olarak AMidi özelliği olmasa da yerel kodunuzun, verileri Java tarafına geri aktarması gerekebilir (ör. kullanıcı arayüzünü güncellemek için). Bunun için Java tarafında ve yerel katmanda kod yazmanız gerekir:

  • Java tarafında bir geri çağırma yöntemi oluşturun.
  • Geri çağırma işlevini çağırmak için gereken bilgileri depolayan bir JNI işlevi yazın.

Geri çağırma zamanı geldiğinde doğal kodunuz şunları oluşturabilir:

Java tarafındaki geri çağırma yöntemi 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);
            }
        });
}

MainActivity.onNativeMessageReceive() için geri çağırma işlevi ayarlayan JNI işlevinin C kodu aşağıda verilmiştir. Java MainActivity, başlangıçta initNative()'ı çağırır:

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

Verileri Java'ya geri gönderme zamanı geldiğinde yerel kod, geri çağırma işaretçilerini alır ve geri çağırmayı oluşturur:

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

Ek kaynaklar