Native MIDI API

AMidi API, Android NDK r20b ve sonraki sürümlerde kullanılabilir. Uygulama geliştiricilere, C/C++ koduyla MIDI verileri gönderme ve alma olanağı sağlar.

Android MIDI uygulamaları, Android MIDI hizmetiyle iletişim kurmak için genellikle midi API'sini kullanır. MIDI uygulamaları, bir veya daha fazla MidiDevice nesnesini keşfetmek, açmak ve kapatmak için öncelikle MidiManager nesnesini kullanır. Ayrıca, cihazın MIDI giriş ve çıkış bağlantı noktaları üzerinden her cihaza veri gönderir ve her cihazdan veri alır:

AMidi'yi kullandığınızda bir JNI çağrısıyla yerel kod katmanına MidiDevice adresini iletirsiniz. Buradan AMidi, MidiDevice'nin işlevlerinin ç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 iletimiyle yakından entegre edebilirsiniz. JNI çağrılarına veya uygulamanızın Java tarafına geri çağırmalara daha az ihtiyaç duyulur. Örneğin, C kodunda uygulanan bir dijital sentezleyici, etkinlikleri Java tarafına göndermek için JNI çağrısını beklemek yerine doğrudan AMidiDevice üzerinden tuş etkinliklerini alabilir. Ya da algoritmik bir oluşturma işlemi, tuş etkinliklerini iletmek için Java tarafına geri çağırma yapmadan bir MIDI performansını doğrudan AMidiDevice'e gönderebilir.

AMidi, MIDI cihazlarıyla doğrudan bağlantıyı iyileştirse de uygulamaların MidiDevice nesnelerini keşfetmek ve açmak için MidiManager kullanması gerekir. AMidi, bu noktadan sonra görevi devralabilir.

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. Bunun için yerel mantığınıza özel JNI çağrıları oluşturun. Kullanıcı arayüzünü güncellemek için verileri geri göndermeniz gerekiyorsa her zamanki gibi yerel katmandan geri arama yapabilirsiniz.

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

AMidi'yi kullanma

AMidi kullanan tüm uygulamalar, MIDI gönderme, alma veya her ikisini birden yapma durumuna bakılmaksızın aynı kurulum ve kapatma adımlarına sahiptir.

AMidi'yi başlatma

Java tarafında uygulama, bağlı bir MIDI donanım parçası bulmalı, ilgili MidiDevice öğesini oluşturmalı ve yerel koda iletmelidir.

  1. Java MidiManager sınıfıyla MIDI donanımını keşfedin.
  2. MIDI donanımına karşılık gelen bir Java MidiDevice nesnesi alın.
  3. JNI ile Java MidiDevice nesnesini yerel koda iletin.

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 ederler. Bir cihaza MIDI verisi göndermek için uygulama, MIDIInputPort açar ve ardından verileri buraya yazar. Veri almak için ise uygulama bir 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ı olan cihazların listesini veya veri göndermek için giriş bağlantı noktaları olan cihazların listesini döndürür. Bir MIDI cihazında hem giriş bağlantı noktaları hem de çıkış bağlantı noktaları bulunabilir.

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 öğesini eklemeniz ve amidi kitaplığına bağlamanız gerekir. Bunların ikisini de Android NDK'da bulabilirsiniz.

Java tarafı, bir JNI çağrısı aracılığıyla yerel katmana bir veya daha fazla MidiDevice nesnesi ve bağlantı noktası numarası iletmelidir. Ardından yerel katman aşağıdaki adımları uygulamalıdır:

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

AMidi'yi durdurma

Java uygulaması, MIDI cihazını artık kullanmadığı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ı serbest bırakmak için kodunuzun şu görevleri gerçekleştirmesi gerekir:

  1. MIDI bağlantı noktalarından okumayı ve/veya bu bağlantı noktalarına yazmayı durdurun. Giriş için yoklama yapmak üzere bir okuma iş parçacığı kullanıyorsanız (aşağıdaki Yoklama döngüsü uygulama bölümüne bakın) iş parçacığını durdurun.
  2. Açık olan AMidiInputPort ve/veya AMidiOutputPort nesnelerini AMidiInputPort_close() ve/veya AMidiOutputPort_close() işlevleriyle kapatın.
  3. AMidiDevice, AMidiDevice_release() ile yayınlansın.

MIDI verilerini alma

MIDI alan bir MIDI uygulamasının tipik bir örneği, ses sentezini kontrol etmek için MIDI performans verilerini alan bir "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, bir arka plan iş parçacığı veya ses iş parçacığı olabilir. AMidi, bir bağlantı noktasından okuma yaparken engelleme yapmaz ve bu nedenle ses geri çağırma işlevinde güvenle kullanılabilir.

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ın 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 MidiDevice açar. MidiDevice açıldığında MidiManager.OnDeviceOpenedListener() örneğine bir geri çağırma alınır. Bu dinleyicinin onDeviceOpened yöntemi çağrılır ve bu yöntem de cihazda 0 numaralı çıkış bağlantı noktasını açmak için startReadingMidi() yöntemini çağırır. Bu, AppMidiManager.cpp içinde tanımlanan 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() işlevini çağırarak AMidiDevice oluşturan ve ardından cihazda bir çıkış bağlantı noktası açmak için AMidiOutputPort_open() işlevini ç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 verisi alan uygulamalar, çıkış bağlantı noktasını sorgulamalı 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 uygulamalar için düşük öncelikli bir arka plan iş parçacığında (uygun uyku süreleriyle) yoklama yapabilirsiniz.

Ses üreten ve daha katı gerçek zamanlı performans gereksinimleri olan uygulamalar için ana ses oluşturma geri çağırma işlevinde (OpenSL ES için BufferQueue geri çağırma işlevi, AAudio'da AudioStream veri geri çağırma işlevi) sorgulama yapabilirsiniz. AMidiOutputPort_receive() engelleme yapmadığından performans üzerindeki etkisi çok azdı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 (ör. OpenSL ES veya AAudio) kullanan bir uygulama, ses oluşturma geri çağırma işlevine MIDI alma kodunu şu şekilde 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 diyagramda, MIDI okuma uygulamasının akışı gösterilmektedir:

MIDI verileri gönderme

MIDI yazma uygulamalarına örnek olarak MIDI denetleyiciler veya sıralayıcılar verilebilir.

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

Uygulamalar, giden MIDI verilerini bir 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ımlanan bir JNI çağrısıdır. İşlev, bir sonraki snippet'te açıklanmıştı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() işlevini çağırarak AMidiDevice oluşturan ve ardından cihazda bir giriş bağlantı noktası açmak için AMidiInputPort_open() işlevini ç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 verileri gönderme

Giden MIDI verilerinin zamanlaması uygulamanın kendisi tarafından iyi anlaşılıp kontrol edildiğinden, veri aktarımı MIDI uygulamasının ana iş parçacığında yapılabilir. Ancak performansla ilgili nedenlerden dolayı (sıralayıcıda 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ının arabelleğini alan ve yazan bir JNI yöntemi örneği 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 çağırma işlevleri

Tam olarak bir AMidi özelliği olmasa da yerel kodunuzun verileri Java tarafına geri iletmesi gerekebilir (örneğin, 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 yerel kodunuz,

Java tarafındaki geri çağırma yöntemi onNativeMessageReceive() şu şekildedir:

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ğırmayı ayarlayan JNI işlevinin C kodu aşağıda verilmiştir. Java, başlangıçta MainActivity 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