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

تتوفّر واجهة برمجة التطبيقات AMidi في الإصدار r20b من حزمة تطوير البرامج (NDK) لنظام التشغيل Android والإصدارات الأحدث. ويتيح لمطوّري التطبيقات إمكانية إرسال بيانات 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 أو تستقبلها أو كليهما.

Start AMidi

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

  1. استكشاف أجهزة MIDI باستخدام فئة Java MidiManager
  2. احصل على كائن Java MidiDevice يتوافق مع أجهزة MIDI.
  3. مرِّر MidiDevice Java إلى الرمز البرمجي الأصلي باستخدام 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. لكل MidiDevice في Java، احصل على AMidiDevice باستخدام AMidiDevice_fromJava().
  2. الحصول على AMidiInputPort و/أو AMidiOutputPort من AMidiDevice باستخدام AMidiInputPort_open() و/أو AMidiOutputPort_open()
  3. استخدِم المنافذ التي تم الحصول عليها لإرسال و/أو تلقّي بيانات MIDI.

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

مراجع إضافية