Yerel MIDI API'sı

AMidi API, Android NDK r20b ve sonraki sürümlerde kullanılabilir. Bu özellik, uygulama geliştiricilerine C/C++ kodu ile MIDI verilerini gönderip alma olanağı tanır.

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ş ve çıkış bağlantı noktaları aracılığıyla her bir cihaza veri iletmek için öncelikle MidiManager'i kullanır:

AMidi'yi kullandığınızda bir MidiDevice adresini, bir JNI çağrısıyla yerel kod katmanına geçirirsiniz. Burada AMidi, MidiDevice işlevlerinin çoğuna sahip bir AMidiDevice referansı oluşturur. Yerel kodunuz, doğrudan 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ın şekilde entegre edebilirsiniz. JNI çağrılarına veya uygulamanızın Java tarafındaki geri çağırmalara daha az ihtiyaç vardır. Örneğin, C kodunda uygulanan bir dijital sentezleyici, bir JNI çağrısının etkinlikleri Java tarafından göndermesini beklemek yerine, önemli etkinlikleri doğrudan bir AMidiDevice öğesinden alabilir. Veya algoritmik oluşturma işlemi, önemli olayları iletmek için Java tarafında geri çağrılmadan bir MIDI performansını doğrudan AMidiDevice öğesine 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 yine de MidiManager'yi kullanması gerekir. AMidi oradan alabilir.

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 verileri geri göndermeniz gerekirse yerel katmandan her zamanki gibi geri çağırabilirsiniz.

Bu dokümanda, AMidi yerel kod uygulamasının nasıl oluşturulacağı gösterilmektedir. Ayrıca, MIDI komutları gönderme ve alma örnekleri verilmiştir. Eksiksiz bir çalışan örnek için NativeMidi örnek uygulamasına göz atın.

AMidi'yi kullan

AMidi kullanan tüm uygulamalar, MIDI gönderme, alma veya her ikisi için de aynı kurulum ve kapatma adımlarına sahiptir.

AMidi'yi başlat

Java tarafında, uygulamanın ekli bir MIDI donanımı parçasını keşfetmesi, karşılık gelen bir MidiDevice oluşturması ve bunu yerel koda aktarması gerekir.

  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 edinin.
  3. Java MidiDevice kodunu JNI ile yerel koda geçirin.

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

Giriş ve çıkış bağlantı noktası nesneleri uygulamaya ait değildir. MIDi cihazındaki bağlantı noktalarını temsil ederler. Uygulama, MIDI verilerini bir cihaza göndermek için bir MIDIInputPort açıp veri yazıyor. Öte yandan, uygulama, verileri almak için bir MIDIOutputPort açar. Düzgün çalışması için uygulamanın, 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.

Burada, her bir MIDI cihazını keşfedip bağlantı noktalarına bakan bir yöntem gösterilmektedir. Veri almak için çıkış bağlantı noktaları olan cihazların listesini veya veri göndermek için giriş bağlantı noktalarına sahip cihazların listesini döndürür. MIDI cihazlarının 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 özelliğini eklemeniz ve amidi kitaplığına bağlantı oluşturmanız gerekir. Bunların her ikisi de Android NDK'da bulunabilir.

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() ile bir AMidiDevice elde edin.
  2. AMidiInputPort_open() ve/veya AMidiOutputPort_open() ile AMidiDevice üzerinden AMidiInputPort ve/veya AMidiOutputPort edinin.
  3. MIDI verilerini göndermek ve/veya almak için elde edilen bağlantı noktalarını kullanın.

AMidi'yi durdur

Java uygulaması, yerel katmanı, MIDI cihazını artık kullanmadığında kaynakları serbest bırakması için işaret etmelidir. Bunun nedeni MIDI cihazının bağlantısının kesilmiş olması veya uygulamanın kapatılıyor olması olabilir.

Kodunuz, MIDI kaynaklarını serbest bırakmak için şu görevleri gerçekleştirmelidir:

  1. MIDI bağlantı noktalarına okuma ve/veya yazma işlemini durdurun. Giriş için anket yapmak üzere bir okuma iş parçacığı kullandıysanız (aşağıdaki Yoklama döngüsü uygulama bölümüne bakın) ileti dizisini durdurun.
  2. AMidiInputPort_close() ve/veya AMidiOutputPort_close() işlevlerine sahip açık AMidiInputPort ve/veya AMidiOutputPort nesnelerini kapatın.
  3. AMidiDevice uygulamasını AMidiDevice_release() ile serbest bırakın.

MIDI verilerini al

MIDI alan bir MIDI uygulamasının tipik bir örneği, ses sentezini kontrol etmek için MIDI performans verilerini alan bir "sanal sentezleyicidir".

Gelen MIDI verileri eşzamansız olarak alınır. Bu nedenle, MIDI'nin bir veya MIDI çıkış bağlantı noktasını sürekli olarak yoklayan ayrı bir iş parçacığında okumak en iyisidir. Bu bir arka plan ileti dizisi veya bir ses ileti dizisi olabilir. AMidi, bir bağlantı noktasından okuma sırasında engelleme yapmaz. Bu nedenle, sesli geri arama içinde güvenle kullanılabilir.

Bir MidiDevice ve çıkış bağlantı noktalarını kurun

Bir uygulama, cihazın çıkış bağlantı noktalarından gelen MIDI verilerini okuyor. Hangi cihazın ve bağlantı noktalarının kullanılacağını uygulamanızın Java tarafı 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 arama alınır. Bu işleyicinin onDeviceOpened yöntemi çağrılır. Daha sonra bu yöntem, cihazda 0. çıkış bağlantı noktasını açmak için startReadingMidi() yöntemini çağırı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ı MIDI cihazını ve bu cihazın bağlantı noktalarını AMidi işlevleri tarafından kullanılan referanslara çevirir.

AMidiDevice_fromJava() yöntemini çağırarak bir AMidiDevice oluşturan ve ardından, cihazda bir çıkış bağlantı noktası açmak için AMidiOutputPort_open() çağrısı yapan JNI işlevini burada görebilirsiniz:

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

}

Yoklama döngüsü uygulama

MIDI verilerini 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 uygulamalarda düşük öncelikli arka plan iş parçacığını (uygun uyku işlemleriyle) yoklayabilirsiniz.

Ses üreten ve daha katı gerçek zamanlı performans gereksinimleri olan uygulamalar için ana ses oluşturma geri çağırmasını (AAudio'daki OpenSL ES için BufferQueue geri çağırma, AudioStream veri geri çağırması) anket yapabilirsiniz. AMidiOutputPort_receive() engelleyici olmadığından performans üzerindeki etkisi çok azdır.

Yukarıdaki startReadingMidi() işlevinden çağrılan readThreadRoutine() işlevi 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 ses API'sı (OpenSL ES veya AAudio gibi) kullanan bir uygulama, ses oluşturma geri çağırmaya MIDI alma kodunu aşağıdaki gibi 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, bir MIDI okuma uygulamasının akışı gösterilmektedir:

MIDI verilerini gönder

MIDI yazma uygulamasına tipik bir örnek olarak MIDI denetleyicisi veya sıralayıcı verilebilir.

Bir MidiDevice ve giriş bağlantı noktalarını kurun

Bir uygulama, giden MIDI verilerini bir MIDI cihazının giriş bağlantı noktalarına yazıyor. Hangi MIDI cihazının ve bağlantı noktalarının kullanılacağını uygulamanızın Java tarafı belirlemelidir.

Aşağıdaki kurulum kodu, yukarıdaki alma örneğinde verilen bir varyasyondur. MidiManager öğesini Android'in MIDI hizmetinden oluşturur. Ardından, bulduğu ilkMidiDevice bağlantıyı açar ve cihazdaki ilk giriş bağlantı noktasını açmak için startWritingMidi() yöntemini çağırır. Bu, AppMidiManager.cpp içinde tanımlanmış 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() yöntemini çağırarak AMidiDevice oluşturan ve ardından cihazda bir giriş bağlantı noktası açmak için AMidiInputPort_open() çağrısı yapan JNI işlevini burada görebilirsiniz:

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önder

Giden MIDI verilerinin zamanlaması uygulamanın kendisi tarafından iyi anlaşıldığı ve kontrol edildiği için veri iletimi MIDI uygulamasının ana iş parçacığından yapılabilir. Bununla birlikte, performans nedeniyle (sıralayıcıda olduğu gibi) MIDI oluşturma ve iletme işlemleri ayrı bir iş parçacığında gerçekleştirilebilir.

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

MIDI komutlarının bir arabelleğini alan ve bunu yazan bir JNI yöntemi örneği aşağıda 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, bir MIDI yazma uygulamasının akışı gösterilmektedir:

Geri çağırma işlevleri

Tam anlamıyla bir AMidi özelliği olmasa da, yerel kodunuzun verileri Java tarafına geri aktarması 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ğırmayı çağırmak için gerekli bilgileri depolayan bir JNI işlevi yazın.

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

Burada, Java tarafı geri çağırma yöntemi (onNativeMessageReceive()) gösterilmektedir:

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

Aşağıda, MainActivity.onNativeMessageReceive() öğesine geri çağırmayı ayarlayan JNI işlevinin C kodunu bulabilirsiniz. Java MainActivity başlangıçta initNative() çağrısı yapar:

MainActivity.cpp (MainActivity.cpp dosyası)

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

Java'ya geri veri 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