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ı genellikle ile iletişim kurmak için midi API'yi Android MIDI hizmeti. 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. Ardından AMidi bir AMidiDevice referansı oluşturuyor Bu, MidiDevice ürününün çoğu işlevine sahiptir. 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ıyla doğrudan bağlantıyı iyileştirse de uygulamalar yine de MidiDevice nesneyi keşfetmek ve açmak için MidiManager öğesini kullanın. AMidi bundan sonra 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 kullan

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ı bulup 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 alın.
  3. JNI ile Java MidiDevice'ü 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 cihaza MIDI verileri göndermek için uygulama, MIDIInputPort kullanır ve ardından verileri bu dosyaya 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ı. Alma işlemi için çıkış bağlantı noktalarına sahip cihazların listesini döndürür veri veya veri göndermek için giriş bağlantı noktalarına sahip cihaz listesi içerir. MIDI cihazı, hem giriş hem de çıkış bağlantı noktaları vardır.

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 ve amidi kitaplığına bağlantı oluşturun. 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. Bu işlemin ardından yerel katman, şu adımları uygulayın:

  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 durdur

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ı kesildiğinden veya uygulamadan çıkıldığından emin olun.

MIDI kaynaklarını serbest bırakmak için kodunuzun şu görevleri gerçekleştirmesi gerekir:

  1. MIDI bağlantı noktalarında okuma ve/veya yazma işlemlerini durdurun. Ölçüm özelliğini kullansaydınız girdi için yoklama yapmak üzere ileti dizisi (aşağıdaki Yoklama döngüsü uygulama bölümüne bakın), ileti dizisini durdurabilirsiniz.
  2. Tüm açık AMidiInputPort ve/veya AMidiOutputPort nesneyi şununla kapatın: AMidiInputPort_close() ve/veya AMidiOutputPort_close() işlevleri.
  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 en iyisi MIDI sürekli olarak bir veya MIDI çıkış bağlantı noktasını yoklayan ayrı bir iş parçacığı içinde çalışır. 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. Java tarafı hangi cihazın ve bağlantı noktalarının kullanılacağını belirlemesi gerekir.

Bu snippet, Android'in MIDI hizmetinden MidiManager ve açıldı bulduğu ilk cihaz için bir MidiDevice. MidiDevice açıldığında MidiManager.OnDeviceOpenedListener() örneğine geri çağırma alınır. Bunun onDeviceOpened yöntemi işleyici çağrılır ve ardından 0 numaralı çıkış bağlantı noktasını açmak için startReadingMidi() çağrısı yapılır cihaz üzerinde. Bu AppMidiManager.cpp içinde tanımlanan bir JNI işlevidir. Bu işlev nasıl yapılacağını göstereceğim.

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.

Aşağıdaki gibi çağırarak AMidiDevice oluşturan JNI işlevi verilmiştir: AMidiDevice_fromJava() ve ardından açmak için AMidiOutputPort_open() numaralı telefonu arar cihazdaki bir çıkış bağlantı noktası:

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 anket yapabilirsiniz. (uygun uykularla) arka plan ileti dizisi.

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.

startReadingMidi() işlevinden çağrılan readThreadRoutine() fonksiyonu aşağıdaki gibi 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, bir 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

Bir uygulama, giden MIDI verilerini MIDI cihazının giriş bağlantı noktalarına yazıyor. 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. MidiManager oluşturur Google'ın MIDI hizmetinden yararlanabilir. 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. Fonksiyon sonraki snippet'i seçin.

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

Aşağıdaki gibi çağırarak AMidiDevice oluşturan JNI işlevi verilmiştir: AMidiDevice_fromJava() ve ardından açmak için AMidiInputPort_open() numaralı telefonu arar cihazdaki bir giriş bağlantı noktası:

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ı Google tarafından iyi anlaşıldığından ve kontrol edildiğinden uygulamanın kendisinde veri iletimi, MIDI uygulamasının ana iş parçacığında yapılabilir. Ancak, performans nedeniyle (bir sıralayıcıda olduğu gibi) oluşturma ve MIDI iletimi ayrı bir iş parçacığında yapılabilir.

Uygulamalar gerektiğinde MIDI verileri gönderebilir. AMidi'nin aşağıdaki durumlarda engellediğini unutmayın: verileri yazma konusunda konuşacağız.

Aşağıda, MIDI komutları içeren bir arabellek alan ve bunu 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 bir AMidi özelliği olmasa da yerel kodunuzun verileri iletmesi gerekebilir Java tarafına geri dönün (ö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ğırmayı çağırmak için gereken bilgileri depolayan bir JNI işlevi yazın.

Geri çağırma zamanı geldiğinde, yerel kodunuz

Java tarafı geri çağırma yöntemi olan 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ğı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