تتوفّر واجهة برمجة التطبيقات AMidi في الإصدار r20b من Android NDK والإصدارات الأحدث. يمنح التطبيق إمكانية إرسال بيانات MIDI واستلامها باستخدام كود C/C++.
تستخدم تطبيقات MIDI على Android عادةً واجهة برمجة التطبيقات
midi
للتواصل مع
خدمة MIDI على Android. تعتمد تطبيقات MIDI بشكل أساسي على برمجة
MidiManager
لاكتشاف عنصر أو أكثر من
MidiDevice
وفتحه
وإغلاقه، ونقْل البيانات من كل جهاز وإليه من خلال منفذَي إدخال
إخراج MIDI للجهاز:
عند استخدام AMidi، يتم تمرير عنوان MidiDevice
إلى رمز برمجي أصلي
الطبقة باستخدام طلب JNI. بعد ذلك، تنشئ AMidi إشارة إلى AMidiDevice
.
التي تضم معظم وظائف MidiDevice
. يستخدم الرمز البرمجي الأصلي
دوال AMid التي تتواصل
مباشرةً باستخدام AMidiDevice
، يتصل AMidiDevice
مباشرةً
خدمة MIDI:
باستخدام طلبات AMidi، يمكنك دمج منطق الصوت/التحكّم في C/C++ الخاص بتطبيقك بشكل وثيق
مع نقل MIDI. لا حاجة إلى استدعاءات JNI أو عمليات استدعاء للوظائف من جانب Java في تطبيقك. على سبيل المثال، يمكن لمُركّب رقمي تم تنفيذه في رمز C تلقّي الأحداث الرئيسية مباشرةً من AMidiDevice
، بدلاً من انتظار استدعاء JNI لإرسال الأحداث من جانب Java. أو يمكن أن تُرسِل عملية الإنشاء algorithmic
أداءً MIDI مباشرةً إلى AMidiDevice
بدون الرجوع مجددًا إلى جانب Java لنقل الأحداث الرئيسية.
على الرغم من أنّ AMidi يحسّن الاتصال المباشر بأجهزة MIDI، يجب أن تستخدم التطبيقات
MidiManager
لاكتشاف عناصر MidiDevice
وفتحها. أميدي يمكن
لأخذها من هناك.
قد تحتاج أحيانًا إلى تمرير معلومات من طبقة واجهة المستخدم إلى الرمز الأصلي. على سبيل المثال، عند إرسال أحداث MIDI استجابةً للأزرار على الشاشة. ولإجراء ذلك، أنشئ اتصالات JNI مخصصة لمنطقك الأصلي. إذا كنت بحاجة إلى إعادة إرسال البيانات لتعديل واجهة المستخدم، يمكنك معاودة الاتصال من الطبقة الأصلية كالعادة.
يوضّح هذا المستند كيفية إعداد تطبيق رمز AMidi الأصلي، مع تقديم أمثلة على إرسال أوامر MIDI وتلقّيها. للحصول على مثال عمل كامل، اطلع على NativeMidi نموذج تطبيق.
استخدام AMidi
جميع التطبيقات التي تستخدم AMidi لها خطوات الإعداد والإغلاق نفسها، سواء كانت إرسال أو استلام ملفات MIDI أو كليهما
بدء AMidi
من جانب Java، يجب أن يكتشف التطبيق
جهاز MIDI المرفق، أو إنشاء MidiDevice
مطابق،
وتمريره إلى الرمز الأصلي.
- استكشِف أجهزة MIDI من خلال فئة Java
MidiManager
. - الحصول على كائن Java
MidiDevice
يتوافق مع أجهزة MIDI - مرِّر 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 ListgetMidiDevices(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. بعد ذلك، من المفترض أن تُجري الطبقة الأصلية الخطوات التالية:
- لكل
MidiDevice
من Java، يمكنك الحصول علىAMidiDevice
باستخدامAMidiDevice_fromJava()
. - الحصول على
AMidiInputPort
و/أوAMidiOutputPort
منAMidiDevice
معAMidiInputPort_open()
و/أوAMidiOutputPort_open()
. - استخدِم المنافذ التي تم الحصول عليها لإرسال و/أو تلقّي بيانات MIDI.
إيقاف AMidi
يجب أن يُرسل تطبيق Java إشارة إلى الطبقة الأصلية لتحرير الموارد عندما يتوقف عن استخدام جهاز MIDI. قد يرجع ذلك إلى أنّه تم انقطاع الاتصال بجهاز MIDI أو أنّ التطبيق قيد الخروج.
لإصدار موارد MIDI، يجب أن تنفذ التعليمة البرمجية المهام التالية:
- إيقاف القراءة و/أو الكتابة في منافذ MIDI إذا كنت تستخدم قراءة سلسلة محادثات إلى استطلاع للإدخال (يمكنك الاطّلاع على تنفيذ حلقة استطلاع رأي أدناه) أو إيقاف سلسلة المحادثات.
- إغلاق أي عناصر
AMidiInputPort
و/أوAMidiOutputPort
مفتوحة تحتوي على دالاتAMidiInputPort_close()
و/أوAMidiOutputPort_close()
. - ارفع إصبعك عن "
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); ListmidiDevices = 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), ×tamp);
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), ×tamp);
if (numMessages >= 0 && opCode == AMIDI_OPCODE_DATA) {
// Parse and respond to MIDI data
// ...
}
// Generate Audio…
// ...
}
يوضّح المخطّط التالي مسار تطبيق قراءة MIDI:
إرسال بيانات 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); ListmidiDevices = 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()
تُجري 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");
}
عندما يحين وقت إرسال البيانات مرة أخرى إلى 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);
}
مصادر إضافية
- مرجع AMidi
- يمكنك الاطّلاع على نموذج التطبيق الكامل لتنسيق MIDI الأصلي على GitHub.