Native MIDI API

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

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

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

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

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

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

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

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

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

آمیدی را راه اندازی کنید

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

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

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

اشیاء پورت ورودی و خروجی به برنامه تعلق ندارند. آنها نشان دهنده پورت های دستگاه 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 و/یا AMidiOutputPort را از AMidiDevice با AMidiInputPort_open() و/یا AMidiOutputPort_open() دریافت کنید.
  3. از پورت های به دست آمده برای ارسال و/یا دریافت داده های MIDI استفاده کنید.

آمیدی را بس کن

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

  • یک متد پاسخ به تماس در سمت جاوا ایجاد کنید.
  • یک تابع 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() تنظیم می کند. 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");
}

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

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

منابع اضافی