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

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

تستخدم تطبيقات MIDI على Android عادةً واجهة برمجة التطبيقات 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 من خلال فئة MidiManager من Java.
  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.

إيقاف 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. يجب أن يحدد جانب جافا من تطبيقك جهاز MIDI والمنافذ التي يجب استخدامها.

رمز الإعداد هذا أدناه هو شكل مختلف عن المثال المستلم أعلاه. ينشئ التطبيق MidiManager من خدمة MIDI على Android. بعد ذلك، يفتح أولMidiDevice يعثر عليه ويطلب startWritingMidi() لفتح منفذ الإدخال الأول على الجهاز. تم تحديد هذا الطلب في 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(). استدعاءات MainActivity في Java 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);
}

مراجع إضافية