ממשק API מקורי ל-MIDI

API של AMidi הוא זמין ב-Android NDK r20b ואילך. הוא מספק לאפליקציה מפתחים את היכולת לשלוח ולקבל נתוני MIDI באמצעות קוד C/C++.

בדרך כלל, אפליקציות ל-Android MIDI משתמשות ב midi API לתקשורת עם שירות Android MIDI. אפליקציות MIDI תלויות בעיקר MidiManager כדי לגלות, לפתוח, וסגירה של שדה אחד או יותר MidiDevice אובייקטים, וגם להעביר נתונים אל כל מכשיר וממנו דרך קלט MIDI ויציאות פלט:

כשמשתמשים ב-AMidi, מעבירים את הכתובת של MidiDevice לקוד הנייטיב באמצעות קריאה ל-JNI. משם, AMidi יוצר הפניה אל AMidiDevice עם רוב הפונקציונליות של MidiDevice. קוד ה-Native שלך משתמש פונקציות Aidi שמתקשרים ישירות באמצעות AMidiDevice ה-AMidiDevice מתחבר ישירות אל שירות MIDI:

באמצעות שיחות AMidi, אפשר לשלב בצורה הדוקה את לוגיקת האודיו/C/C++ של האפליקציה. באמצעות העברת MIDI. יש פחות צורך בשיחות JNI או בהתקשרות חוזרת הצד של Java באפליקציה. לדוגמה, סינתיסייזר דיגיטלי שהוטמע בקוד C יכול לקבל אירועים מרכזיים ישירות מ-AMidiDevice, במקום לחכות ל-JNI לשלוח את האירועים מהצד של Java. או כתיבה אלגוריתמית התהליך יכול לשלוח נתוני MIDI ישירות אל AMidiDevice בלי להתקשר לגבות לצד Java כדי לשדר את האירועים המרכזיים.

למרות ש-AMidi משפרת את החיבור הישיר למכשירי MIDI, אפליקציות עדיין צריכות להשתמש בMidiManager כדי לגלות ולפתוח אובייקטים של MidiDevice. AMidi יכול ולקחת את זה משם.

לפעמים תידרשו להעביר מידע משכבת ממשק המשתמש למטה את הקוד המקורי. לדוגמה, כשאירועי MIDI נשלחים בתגובה ללחצנים במסך. כדי לעשות את זה, יוצרים קריאות JNI מותאמות אישית ללוגיקה המקורית. אם צריך לשלוח נתונים בחזרה כדי לעדכן את ממשק המשתמש, אפשר להתקשר חזרה מהשכבה המקורית כרגיל.

במסמך הזה נסביר איך להגדיר אפליקציית קוד נייטיב של AMidi, כולל דוגמאות שליחה וקבלה של פקודות MIDI. כדי לקבל דוגמה מלאה, אפשר לעיין NativeMidi אפליקציה לדוגמה.

שימוש ב-AMidi

לכל האפליקציות שמשתמשות ב-AMidi יש אותם שלבי הגדרה ושלבי סגירה, גם אם לשלוח או לקבל MIDI, או את שניהם.

הפעלת AMidi

בצד Java, האפליקציה חייבת לגלות של חומרת MIDI שמצורפת, ליצור MidiDevice תואם, ומעבירים את הקלט לקוד ה-Native.

  1. מגלים את חומרת MIDI באמצעות מחלקת Java MidiManager.
  2. מקבלים אובייקט Java MidiDevice שתואם לחומרת ה-MIDI.
  3. מעבירים את MidiDevice של Java לקוד נייטיב באמצעות 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 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. לכל MidiDevice של Java צריך לקבל 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. הזה יכול להיות שרשור ברקע או שרשור אודיו. אמידי לא חוסם את האפשרות לקרוא מיציאה, ולכן הוא בטוח לשימוש מבפנים התקשרות חזרה באודיו.

הגדרת MidiDevice ויציאות הפלט שלו

אפליקציה קוראת נתוני MIDI נכנסים מיציאות הפלט של המכשיר. הצד של Java של האפליקציה חייבת לקבוע באיזה מכשיר ובאילו יציאות להשתמש.

קטע הקוד הזה יוצר MidiManager משירות MIDI ב-Android ופתיחה MidiDevice של המכשיר הראשון שהוא מוצא. אחרי שהMidiDevice נפתח קריאה חוזרת למופע של MidiManager.OnDeviceOpenedListener(). ה-method onDeviceOpened של זה נשלחת קריאה ל-listener ואז לקרוא ל-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);
    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 באמצעות הפקודה 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), &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 או סדרת רצף 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);
    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 באמצעות הפקודה 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 היוצאים מובן ומבוקר על ידי את האפליקציה עצמה, ניתן להעביר את הנתונים ב-thread הראשי של אפליקציית ה-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 ובשכבת ה-Native:

  • ליצור שיטת קריאה חוזרת בצד Java.
  • כותבים פונקציית JNI שמאחסנת את המידע שנדרש להפעלת הקריאה החוזרת.

כשמגיע הזמן לקרוא חזרה, קוד ה-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, הקוד המקורי מאחזר את הקריאה החוזרת (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);
}

מקורות מידע נוספים