واجهة برمجة تطبيقات MIDI الأصلية

تتوفّر واجهة برمجة التطبيقات AMidi في الإصدار r20b من Android NDK والإصدارات الأحدث. وتتيح لمطوّري التطبيقات إمكانية إرسال بيانات MIDI وتلقّيها باستخدام رمز C/C++.

تستخدم تطبيقات Android MIDI عادةً واجهة برمجة التطبيقات midi للتواصل مع خدمة Android MIDI. تعتمد تطبيقات MIDI بشكل أساسي على MidiManager لاكتشاف وفتح وإغلاق عنصر واحد أو أكثر من عناصر MidiDevice، وتمرير البيانات من وإلى كل جهاز عبر منافذ الإدخال والإخراج في MIDI:

عند استخدام AMidi، يمكنك تمرير عنوان MidiDevice إلى طبقة الرموز البرمجية الأصلية باستخدام طلب JNI. بعد ذلك، تنشئ AMidi مرجعًا إلى AMidiDevice يحتوي على معظم وظائف MidiDevice. تستخدم الرموز البرمجية الأصلية دوال AMidi التي تتصل مباشرةً بـ AMidiDevice يتصل AMidiDevice مباشرةً بخدمة MIDI:

باستخدام طلبات AMidi، يمكنك دمج منطق التحكّم/الصوت C/C++ في تطبيقك بشكل وثيق مع إرسال MIDI. تقل الحاجة إلى استدعاءات JNI أو عمليات الاستدعاء إلى جانب Java في تطبيقك. على سبيل المثال، يمكن أن يتلقّى جهاز المزج الرقمي الذي تم تنفيذه في رمز C أحداثًا رئيسية مباشرةً من AMidiDevice، بدلاً من انتظار استدعاء JNI لإرسال الأحداث من جانب Java. أو يمكن لعملية تأليف خوارزمية إرسال أداء MIDI مباشرةً إلى AMidiDevice بدون استدعاء احتياطي إلى جانب Java لنقل الأحداث الرئيسية.

على الرغم من أنّ AMidi تحسِّن الاتصال المباشر بأجهزة MIDI، لا يزال على التطبيقات استخدام MidiManager لاكتشاف عناصر MidiDevice وفتحها. ويمكن لـ AMidi تولّي الأمر من هناك.

قد تحتاج أحيانًا إلى تمرير المعلومات من طبقة واجهة المستخدم إلى الرموز البرمجية الأصلية. على سبيل المثال، عندما يتم إرسال أحداث MIDI استجابةً للأزرار الموجودة على الشاشة. لإجراء ذلك، أنشئ طلبات JNI مخصّصة لمنطقك الأصلي. إذا كنت بحاجة إلى إرسال البيانات مرة أخرى لتحديث واجهة المستخدم، يمكنك معاودة الاتصال من الطبقة الأصلية كالمعتاد.

يوضّح هذا المستند كيفية إعداد تطبيق رموز برمجية أصلية لـ AMidi، مع تقديم أمثلة على إرسال أوامر MIDI وتلقّيها. للاطّلاع على مثال كامل، يمكنك مراجعة نموذج تطبيق NativeMidi.

استخدام AMidi

تتّبع جميع التطبيقات التي تستخدم AMidi خطوات الإعداد والإغلاق نفسها، سواء كانت ترسل بيانات MIDI أو تستقبلها أو كليهما.

بدء استخدام AMidi

على جانب Java، يجب أن يكتشف التطبيق جهاز MIDI متصل، وإنشاء MidiDevice مطابق، وتمريره إلى الرموز البرمجية الأصلية.

  1. استكشِف أجهزة MIDI باستخدام فئة Java MidiManager.
  2. احصل على عنصر Java MidiDevice يتوافق مع جهاز MIDI.
  3. مرِّر Java MidiDevice إلى الرموز البرمجية الأصلية باستخدام JNI.

التعرّف على الأجهزة والمنافذ

لا تنتمي عناصر منفذ الإدخال والإخراج إلى التطبيق، بل تمثّل منافذ على جهاز MIDI. لإرسال بيانات MIDI إلى جهاز، يفتح التطبيق MIDIInputPort ثم يكتب البيانات فيه. وعلى العكس، لتلقّي البيانات، يفتح التطبيق MIDIOutputPort. لكي يعمل التطبيق بشكل صحيح، يجب أن يتأكد من أنّ المنافذ التي يفتحها من النوع الصحيح. يتم اكتشاف الأجهزة والمنافذ من خلال Java.

في ما يلي طريقة تكتشف كل جهاز MIDI وتفحص منافذه. وتعرض إما قائمة بالأجهزة التي تحتوي على منافذ إخراج لتلقّي البيانات، أو قائمة بالأجهزة التي تحتوي على منافذ إدخال لإرسال البيانات. يمكن أن يحتوي جهاز MIDI على منافذ إدخال ومنافذ إخراج.

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

لاستخدام وظائف AMidi في رمز C/C++‎، يجب تضمين AMidi/AMidi.h والربط بمكتبة amidi. ويمكن العثور على كليهما في Android NDK.

يجب أن يمرر جانب Java عنصرًا واحدًا أو أكثر من عناصر MidiDevice وأرقام المنافذ إلى الطبقة الأصلية عبر استدعاء JNI. يجب أن تنفّذ الطبقة الأصلية بعد ذلك الخطوات التالية:

  1. بالنسبة إلى كل Java MidiDevice، يمكنك الحصول على AMidiDevice باستخدام AMidiDevice_fromJava().
  2. الحصول على AMidiInputPort و/أو AMidiOutputPort من AMidiDevice باستخدام AMidiInputPort_open() و/أو AMidiOutputPort_open()
  3. استخدِم المنافذ التي تم الحصول عليها لإرسال بيانات MIDI و/أو تلقّيها.

إيقاف AMidi

يجب أن يرسل تطبيق Java إشارة إلى الطبقة الأصلية لتحرير الموارد عندما لا يعود يستخدم جهاز MIDI. قد يرجع ذلك إلى أنّ جهاز MIDI غير متصل أو أنّ التطبيق قيد الإغلاق.

لتحرير موارد MIDI، يجب أن ينفّذ الرمز المهام التالية:

  1. إيقاف القراءة و/أو الكتابة في منافذ MIDI إذا كنت تستخدم سلسلة قراءة للاستطلاع عن الإدخال (انظر تنفيذ حلقة استطلاع أدناه)، فأوقف السلسلة.
  2. أغلِق أي عناصر AMidiInputPort و/أو AMidiOutputPort مفتوحة باستخدام دوال AMidiInputPort_close() و/أو AMidiOutputPort_close().
  3. يمكنك إصدار AMidiDevice باستخدام AMidiDevice_release().

تلقّي بيانات MIDI

من الأمثلة النموذجية لتطبيق MIDI الذي يتلقّى بيانات MIDI هو "المُركِّب الافتراضي" الذي يتلقّى بيانات أداء MIDI للتحكّم في تركيب الصوت.

يتم تلقّي بيانات MIDI الواردة بشكل غير متزامن. لذلك، من الأفضل قراءة MIDI في سلسلة محادثات منفصلة تستقصي باستمرار منفذًا واحدًا أو منافذ إخراج MIDI. قد يكون هذا سلسلة محادثات في الخلفية أو سلسلة محادثات صوتية. لا تحظر واجهة برمجة التطبيقات AMidi القراءة من منفذ، وبالتالي يمكن استخدامها بأمان داخل عملية استدعاء صوتي.

إعداد MidiDevice ومنافذ الإخراج الخاصة به

يقرأ التطبيق بيانات MIDI الواردة من منافذ الإخراج في الجهاز. يجب أن يحدّد جانب Java في تطبيقك الجهاز والمنافذ التي يجب استخدامها.

ينشئ هذا المقتطف MidiManager من خدمة MIDI في Android ويفتح MidiDevice لأول جهاز يعثر عليه. عند فتح MidiDevice، يتم تلقّي رد اتصال لمثيل من MidiManager.OnDeviceOpenedListener(). يتم استدعاء طريقة onDeviceOpened لهذا المستمع، والتي تستدعي بعد ذلك startReadingMidi() لفتح منفذ الإخراج 0 على الجهاز. هذه دالة JNI محدّدة في AppMidiManager.cpp. يتم شرح هذه الدالة في المقتطف التالي.

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

تترجم الرموز البرمجية الأصلية جهاز MIDI من جهة Java ومنافذه إلى مراجع تستخدمها دوال AMidi.

في ما يلي دالة JNI التي تنشئ AMidiDevice من خلال استدعاء AMidiDevice_fromJava()، ثم تستدعي AMidiOutputPort_open() لفتح منفذ إخراج على الجهاز:

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

}

تنفيذ حلقة استطلاع

يجب أن تستطلع التطبيقات التي تتلقّى بيانات MIDI منفذ الإخراج وتستجيب عندما تعرض الدالة AMidiOutputPort_receive() رقمًا أكبر من الصفر.

بالنسبة إلى التطبيقات ذات معدل نقل البيانات المنخفض، مثل نطاق MIDI، يمكنك إجراء الاستطلاع في سلسلة محادثات في الخلفية ذات أولوية منخفضة (مع فترات توقّف مناسبة).

بالنسبة إلى التطبيقات التي تنشئ الصوت وتتطلب أداءً أكثر صرامة في الوقت الفعلي، يمكنك إجراء استطلاع في معاودة الاتصال الرئيسية لإنشاء الصوت (معاودة الاتصال BufferQueue لـ OpenSL ES، ومعاودة الاتصال ببيانات AudioStream في AAudio). بما أنّ AMidiOutputPort_receive() لا يحظر المحتوى، يكون تأثيره في الأداء ضئيلاً جدًا.

قد تبدو الدالة readThreadRoutine() التي تم استدعاؤها من الدالة startReadingMidi() أعلاه على النحو التالي:

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

يمكن للتطبيق الذي يستخدم واجهة برمجة تطبيقات صوتية أصلية (مثل OpenSL ES أو AAudio) إضافة رمز استلام MIDI إلى دالة الاستدعاء لإنشاء الصوت على النحو التالي:

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

يوضّح المخطّط البياني التالي مسار تطبيق قراءة MIDI:

إرسال بيانات MIDI

من الأمثلة النموذجية على تطبيقات الكتابة عبر MIDI وحدة التحكّم في MIDI أو جهاز التسلسل.

إعداد MidiDevice ومنافذ الإدخال الخاصة به

يكتب التطبيق بيانات MIDI الصادرة إلى منافذ الإدخال في جهاز MIDI. يجب أن يحدّد جانب Java في تطبيقك أجهزة ومنافذ MIDI التي سيتم استخدامها.

رمز الإعداد أدناه هو صيغة مختلفة من مثال الاستلام أعلاه. ويتم إنشاء MidiManager من خدمة MIDI في Android. بعد ذلك، يفتح أولMidiDevice يعثر عليه ويستدعي startWritingMidi() لفتح أول منفذ إدخال على الجهاز. هذا استدعاء JNI محدّد في AppMidiManager.cpp. يتم شرح الدالة في المقتطف التالي.

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

في ما يلي دالة JNI التي تنشئ AMidiDevice من خلال استدعاء AMidiDevice_fromJava()، ثم تستدعي AMidiInputPort_open() لفتح منفذ إدخال على الجهاز:

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

بما أنّ توقيت بيانات MIDI الصادرة مفهوم جيدًا ويتم التحكّم فيه من خلال التطبيق نفسه، يمكن نقل البيانات في سلسلة التعليمات الرئيسية لتطبيق MIDI. ومع ذلك، لأسباب تتعلق بالأداء (كما هو الحال في جهاز التسلسل)، يمكن إنشاء MIDI وإرساله في سلسلة محادثات منفصلة.

يمكن للتطبيقات إرسال بيانات MIDI متى لزم الأمر. يُرجى العِلم أنّ AMidi يحظر عند كتابة البيانات.

في ما يلي مثال على طريقة JNI التي تتلقّى مخزنًا مؤقتًا لأوامر MIDI وتكتبه:

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

يوضّح المخطّط البياني التالي مسار تطبيق كتابة MIDI:

عمليات الاسترداد

على الرغم من أنّ هذه الميزة ليست من ميزات AMidi، قد تحتاج الرموز البرمجية الأصلية إلى إعادة البيانات إلى Java (لتعديل واجهة المستخدم على سبيل المثال). لإجراء ذلك، يجب كتابة رمز برمجي في جانب Java والطبقة الأصلية:

  • أنشئ طريقة رد اتصال في Java.
  • اكتب دالة JNI تخزّن المعلومات اللازمة لاستدعاء دالة رد الاتصال.

عندما يحين وقت معاودة الاتصال، يمكن للرمز البرمجي الأصلي إنشاء

في ما يلي طريقة رد الاتصال من جهة 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);
            }
        });
}

في ما يلي رمز C لدالة JNI التي تضبط ردّ الاتصال على MainActivity.onNativeMessageReceive(). يستدعي Java MainActivity السمة initNative() عند بدء التشغيل:

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

عندما يحين وقت إرسال البيانات إلى Java، يسترد الرمز البرمجي الأصلي مؤشرات رد الاتصال وينشئ رد الاتصال:

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

مراجع إضافية