AMidi API זמין ב-Android NDK r20b ואילך. הוא מאפשר למפתחי אפליקציות לשלוח ולקבל נתוני MIDI באמצעות קוד C/C++.
אפליקציות MIDI ל-Android בדרך כלל משתמשות ב-API של midi כדי לתקשר עם שירות ה-MIDI של Android. אפליקציות MIDI מסתמכות בעיקר על MidiManager כדי לגלות, לפתוח ולסגור אובייקט אחד או יותר של MidiDevice, ולהעביר נתונים אל כל מכשיר וממנו דרך יציאות הקלט והפלט של MIDI במכשיר:
כשמשתמשים ב-AMidi, מעבירים את הכתובת של MidiDevice לשכבת קוד ה-Native באמצעות קריאת JNI. משם, AMidi יוצר הפניה אל AMidiDevice
שכולל את רוב הפונקציונליות של MidiDevice. קוד ה-Native משתמש בפונקציות AMidi שמתקשרות ישירות עם AMidiDevice. ה-AMidiDevice מתחבר ישירות לשירות ה-MIDI:
באמצעות קריאות ל-AMidi, אתם יכולים לשלב את לוגיקת האודיו/הבקרה של האפליקציה ב-C/C++ עם שידור MIDI. יש פחות צורך בקריאות JNI או בקריאות חוזרות (callbacks) לצד של Java באפליקציה. לדוגמה, סינתיסייזר דיגיטלי שמוטמע בקוד C יכול לקבל אירועי מפתח ישירות מ-AMidiDevice, במקום להמתין לקריאת JNI כדי לשלוח את האירועים מהצד של Java. או שתהליך הלחנה אלגוריתמי יכול לשלוח ביצוע MIDI ישירות אל AMidiDevice בלי לבצע קריאה חוזרת לצד של Java כדי להעביר את אירועי המקשים.
למרות ש-AMidi משפר את החיבור הישיר למכשירי MIDI, האפליקציות עדיין צריכות להשתמש ב-MidiManager כדי לגלות ולפתוח אובייקטים של MidiDevice. מכאן, AMidi יכול
להמשיך.
לפעמים צריך להעביר מידע משכבת ממשק המשתמש אל קוד ה-Native. לדוגמה, כשאירועי MIDI נשלחים בתגובה ללחיצה על כפתורים במסך. כדי לעשות את זה, צריך ליצור קריאות JNI בהתאמה אישית ללוגיקה המקורית. אם אתם צריכים לשלוח נתונים בחזרה כדי לעדכן את ממשק המשתמש, אתם יכולים להפעיל קריאה חוזרת (callback) מהשכבה המקורית כרגיל.
במסמך הזה מוסבר איך להגדיר אפליקציית קוד Native של AMidi, ומוצגות דוגמאות לשליחה ולקבלה של פקודות MIDI. אפשר לראות דוגמה מלאה שעובדת באפליקציה לדוגמה NativeMidi.
שימוש ב-AMidi
כל האפליקציות שמשתמשות ב-AMidi כוללות את אותם שלבי הגדרה וסגירה, בין אם הן שולחות או מקבלות MIDI, או גם וגם.
הפעלת AMidi
בצד של Java, האפליקציה צריכה לזהות חומרת MIDI מחוברת, ליצור MidiDevice תואם ולהעביר אותו לקוד ה-Native.
- מגלים חומרת MIDI באמצעות המחלקה
MidiManagerשל Java. - מקבלים אובייקט
MidiDeviceשל Java שתואם לחומרת ה-MIDI. - מעבירים את ה-
MidiDeviceשל Java לקוד Native באמצעות 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. זה יכול להיות שרשור ברקע או שרשור אודיו. AMidi לא חוסם את הקריאה מיציאה, ולכן אפשר להשתמש בו בבטחה בתוך קריאה חוזרת (callback) של אודיו.
הגדרת MidiDevice ויציאות הפלט שלו
אפליקציה קוראת נתוני MIDI נכנסים מיציאות הפלט של המכשיר. הצד של Java באפליקציה צריך לקבוע באילו מכשיר ויציאות להשתמש.
הקטע הזה יוצר את MidiManager משירות ה-MIDI של Android ופותח את MidiDevice למכשיר הראשון שהוא מוצא. כשהמודעה מסוג MidiDevice נפתחת, מתקבלת קריאה חוזרת (callback) למופע של MidiManager.OnDeviceOpenedListener(). מתבצעת קריאה ל-method 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); } } }
קוד ה-Native מתרגם את מכשיר ה-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, אפשר לבצע דגימה בשרשור רקע בעדיפות נמוכה (עם זמני השהיה מתאימים).
באפליקציות שיוצרות אודיו ויש להן דרישות מחמירות יותר לגבי ביצועים בזמן אמת, אפשר לבצע דגימה בקריאה החוזרת (callback) הראשית של יצירת האודיו (הקריאה החוזרת 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;
}
}
}
באפליקציה שמשתמשת ב-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), ×tamp);
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); 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, יכול להיות שקוד ה-Native יצטרך להעביר נתונים בחזרה לצד של Java (למשל, כדי לעדכן את ממשק המשתמש). כדי לעשות את זה, צריך לכתוב קוד בצד של Java ובשכבה המקורית:
- יוצרים שיטת קריאה חוזרת בצד של Java.
- כותבים פונקציית JNI שמאחסנת את המידע שנדרש כדי להפעיל את פונקציית ה-callback.
כשיגיע הזמן להפעיל את פונקציית ה-callback, קוד ה-Native יוכל ליצור
זו שיטת הקריאה החוזרת בצד של 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 שמגדירה את הקריאה החוזרת (callback) ל-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, קוד ה-Native מאחזר את המצביעים של הקריאה החוזרת ויוצר את הקריאה החוזרת:
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
- אפליקציה לדוגמה של Native MIDI ב-GitHub.