Native MIDI API

رابط برنامه‌نویسی کاربردی AMidi در اندروید NDK نسخه r20b و بالاتر موجود است و به توسعه‌دهندگان اپلیکیشن امکان ارسال و دریافت داده‌های MIDI را با کد C/C++ می‌دهد.

برنامه‌های MIDI اندروید معمولاً از رابط برنامه‌نویسی کاربردی midi برای ارتباط با سرویس MIDI اندروید استفاده می‌کنند. برنامه‌های MIDI در درجه اول به MidiManager برای کشف، باز کردن و بستن یک یا چند شیء MidiDevice و انتقال داده‌ها به و از هر دستگاه از طریق پورت‌های ورودی و خروجی MIDI دستگاه وابسته هستند:

وقتی از AMidi استفاده می‌کنید، آدرس یک MidiDevice را با یک فراخوانی JNI به لایه کد native ارسال می‌کنید. از آنجا، AMidi یک ارجاع به یک AMidiDevice ایجاد می‌کند که بیشتر قابلیت‌های یک MidiDevice را دارد. کد native شما از توابع AMidi استفاده می‌کند که مستقیماً با یک AMidiDevice ارتباط برقرار می‌کنند. AMidiDevice مستقیماً به سرویس MIDI متصل می‌شود:

با استفاده از فراخوانی‌های AMidi، می‌توانید منطق صوتی/کنترلی C/C++ برنامه خود را با انتقال MIDI از نزدیک ادغام کنید. نیاز کمتری به فراخوانی‌های JNI یا فراخوانی‌های برگشتی به سمت جاوای برنامه شما وجود دارد. به عنوان مثال، یک سینتی‌سایزر دیجیتال که با کد C پیاده‌سازی شده است، می‌تواند رویدادهای کلیدی را مستقیماً از یک AMidiDevice دریافت کند، به جای اینکه منتظر یک فراخوانی JNI برای ارسال رویدادها از سمت جاوا باشد. یا یک فرآیند آهنگسازی الگوریتمی می‌تواند یک اجرای MIDI را مستقیماً به یک AMidiDevice ارسال کند بدون اینکه دوباره به سمت جاوا برای انتقال رویدادهای کلیدی فراخوانی شود.

اگرچه AMidi اتصال مستقیم به دستگاه‌های MIDI را بهبود می‌بخشد، اما برنامه‌ها هنوز باید از MidiManager برای کشف و باز کردن اشیاء MidiDevice استفاده کنند. AMidi می‌تواند این کار را از آنجا انجام دهد.

گاهی اوقات ممکن است لازم باشد اطلاعات را از لایه رابط کاربری به کد بومی منتقل کنید. برای مثال، وقتی رویدادهای MIDI در پاسخ به دکمه‌های روی صفحه نمایش ارسال می‌شوند. برای انجام این کار، فراخوانی‌های JNI سفارشی را به منطق بومی خود ایجاد کنید. اگر نیاز به ارسال داده‌ها برای به‌روزرسانی رابط کاربری دارید، می‌توانید طبق معمول از لایه بومی فراخوانی کنید.

این سند نحوه راه‌اندازی یک برنامه کد بومی AMidi را نشان می‌دهد و نمونه‌هایی از ارسال و دریافت دستورات MIDI را ارائه می‌دهد. برای یک مثال کامل، برنامه نمونه NativeMidi را بررسی کنید.

از AMidi استفاده کنید

تمام برنامه‌هایی که از AMidi استفاده می‌کنند، چه MIDI ارسال کنند و چه دریافت کنند و چه هر دو، مراحل راه‌اندازی و بستن یکسانی دارند.

شروع AMidi

در سمت جاوا، برنامه باید یک قطعه سخت‌افزار MIDI متصل را کشف کند، یک MidiDevice مربوطه ایجاد کند و آن را به کد بومی منتقل کند.

  1. سخت‌افزار MIDI را با کلاس MidiManager جاوا کشف کنید.
  2. یک شیء Java MidiDevice مربوط به سخت‌افزار MIDI را دریافت کنید.
  3. با استفاده از JNI، Java MidiDevice به کد اصلی منتقل کنید.

سخت‌افزار و پورت‌ها را کشف کنید

اشیاء پورت ورودی و خروجی متعلق به برنامه نیستند. آنها پورت‌های روی دستگاه MIDI را نشان می‌دهند. برای ارسال داده‌های MIDI به یک دستگاه، یک برنامه یک MIDIInputPort را باز می‌کند و سپس داده‌ها را روی آن می‌نویسد. برعکس، برای دریافت داده‌ها، یک برنامه یک MIDIOutputPort را باز می‌کند. برای عملکرد صحیح، برنامه باید مطمئن شود که پورت‌هایی که باز می‌کند از نوع صحیح هستند. کشف دستگاه و پورت در سمت جاوا انجام می‌شود.

در اینجا روشی ارائه شده است که هر دستگاه 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 پیدا کنید.

طرف جاوا باید یک یا چند شیء MidiDevice و شماره پورت را از طریق فراخوانی JNI به لایه بومی ارسال کند. سپس لایه بومی باید مراحل زیر را انجام دهد:

  1. برای هر MidiDevice جاوا، یک AMidiDevice با استفاده از AMidiDevice_fromJava() بدست آورید.
  2. با استفاده از AMidiInputPort_open() و/یا AMidiOutputPort_open() یک AMidiInputPort و/یا AMidiOutputPort از AMidiDevice دریافت کنید.
  3. از پورت‌های به دست آمده برای ارسال و/یا دریافت داده‌های MIDI استفاده کنید.

توقف AMidi

برنامه جاوا باید به لایه بومی سیگنال دهد تا وقتی دیگر از دستگاه 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 ورودی را از پورت‌های خروجی دستگاه می‌خواند. بخش جاوای برنامه شما باید مشخص کند که از کدام دستگاه و پورت‌ها استفاده کند.

این قطعه کد MidiManager از سرویس MIDI اندروید ایجاد می‌کند و یک MidiDevice برای اولین دستگاهی که پیدا می‌کند، باز می‌کند. هنگامی که MidiDevice باز می‌شود، یک فراخوانی به نمونه‌ای از MidiManager.OnDeviceOpenedListener() دریافت می‌شود. متد onDeviceOpened از این شنونده فراخوانی می‌شود که سپس startReadingMidi() را برای باز کردن پورت خروجی 0 روی دستگاه فراخوانی می‌کند. این یک تابع 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 سمت جاوا و پورت‌های آن را به مراجع مورد استفاده توابع 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، می‌توانید در یک رشته پس‌زمینه با اولویت پایین (با زمان‌های خواب مناسب) نظرسنجی کنید.

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

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 اندروید ایجاد می‌کند. سپس اولین 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 نیست، اما کد native شما ممکن است نیاز داشته باشد داده‌ها را به سمت جاوا ارسال کند (مثلاً برای به‌روزرسانی رابط کاربری). برای انجام این کار، باید کد را در سمت جاوا و لایه native بنویسید:

  • یک متد فراخوانی (callback) در سمت جاوا ایجاد کنید.
  • یک تابع JNI بنویسید که اطلاعات مورد نیاز برای فراخوانی تابع فراخوانی برگشتی را ذخیره کند.

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

در اینجا متد فراخوانی سمت جاوا، 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() را در هنگام راه‌اندازی فراخوانی می‌کند:

فایل اصلی فعالیت.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");
}

وقتی زمان ارسال داده‌ها به جاوا فرا می‌رسد، کد بومی اشاره‌گرهای callback را بازیابی کرده و callback را می‌سازد:

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

منابع اضافی