Native MIDI API

میانای برنامه‌سازی کاربردی AMidi در Android NDK r20b و نسخه‌های جدیدتر دردسترس است. این API به توسعه‌دهندگان نرم‌افزار امکان می‌دهد داده‌های MIDI را با کد C/C++‎ ارسال و دریافت کنند.

برنامه‌های MIDI در Android معمولاً از midi API برای برقراری ارتباط با سرویس MIDI در Android استفاده می‌کنند. برنامه‌های 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 می‌تواند از آنجا ادامه دهد.

گاهی اوقات ممکن است لازم باشد اطلاعات را از لایه UI به کد بومی منتقل کنید. برای مثال، وقتی رویدادهای 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 می‌تواند هم درگاه ورودی و هم درگاه خروجی داشته باشد.

کاتلین

private fun getMidiDevices(isOutput: Boolean) : List {
    if (isOutput) {
        return mMidiManager.devices.filter { it.outputPortCount > 0 }
    } else {
        return mMidiManager.devices.filter { it.inputPortCount > 0 }
    }
}

جاوا

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() را برای باز کردن درگاه خروجی ۰ در دستگاه فراخوانی می‌کند. این یک تابع JNI است که در AppMidiManager.cpp تعریف شده است. این عملکرد در قطعه کد بعدی توضیح داده شده است.

کاتلین

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

جاوا

//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_fromJava()، یک AMidiDevice ایجاد می‌کند و سپس 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 scope، می‌توانید در یک رشته پس‌زمینه با اولویت پایین (با وقفه‌های مناسب) نظرسنجی کنید.

برای برنامه‌هایی که صدا تولید می‌کنند و الزامات عملکرد هم‌زمان سخت‌گیرانه‌تری دارند، می‌توانید در فراخوانی برگشت تولید صدای اصلی (فراخوانی برگشت 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;
        }
  }
}

برنامه‌ای که از API صوتی بومی (مانند OpenSL ES یا AAudio) استفاده می‌کند، می‌تواند کد دریافت MIDI را به callback تولید صدا اضافه کند، مانند این:

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 تعریف شده است. این تابع در قطعه کد بعدی توضیح داده شده است.

کاتلین

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

جاوا

//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_fromJava()، یک AMidiDevice ایجاد می‌کند و سپس 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 بنویسید که اطلاعات لازم برای فراخوانی callback را ذخیره کند.

وقتی زمان فراخوانی مجدد است، کد بومی شما می‌تواند بسازد

در اینجا روش فراخوانی سمت Java، onNativeMessageReceive()، آورده شده است:

کاتلین

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

جاوا

//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 تماس می‌گیرد 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);
}

منابع بیشتر