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

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

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

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

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

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

قد تحتاج أحيانًا إلى تمرير معلومات من طبقة واجهة المستخدم إلى الرمز الأصلي. على سبيل المثال، عندما يتم إرسال أحداث 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. لكل 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 باستمرار. هذا النمط يمكن أن تكون سلسلة محادثات في الخلفية أو سلسلة صوتية. أميدي لا تمنع القراءة من المنفذ وبالتالي يمكن استخدامها بأمان في الداخل ومكالمة صوتية.

إعداد جهاز 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() مكالمات 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);
}

مصادر إضافية