API MIDI แบบเนทีฟ

AMidi API พร้อมใช้งานใน Android NDK r20b ขึ้นไป โดยจะช่วยให้นักพัฒนาแอปสามารถส่งและรับข้อมูล MIDI ด้วยโค้ด C/C++

โดยปกติแล้วแอป MIDI ของ Android จะใช้ midi API เพื่อสื่อสารกับ บริการ MIDI ของ Android แอป MIDI จะขึ้นอยู่กับ MidiManager เป็นหลักในการค้นหา เปิด และปิดออบเจ็กต์ MidiDevice อย่างน้อย 1 รายการ รวมถึง ส่งข้อมูลไปยังและจากอุปกรณ์แต่ละเครื่องผ่าน พอร์ต MIDI อินพุต และ เอาต์พุต ของอุปกรณ์

เมื่อใช้ AMidi คุณจะต้องส่งที่อยู่ของ MidiDevice ไปยังเลเยอร์โค้ดแบบเนทีฟด้วยการเรียก JNI จากนั้น AMidi จะสร้างการอ้างอิงไปยัง AMidiDevice ซึ่งมีฟังก์ชันการทำงานส่วนใหญ่ของ MidiDevice โค้ดแบบเนทีฟจะใช้ ฟังก์ชัน AMidi ที่สื่อสาร กับ AMidiDevice โดยตรง ส่วน AMidiDevice จะเชื่อมต่อกับ บริการ MIDI โดยตรง

การใช้การเรียก AMidi จะช่วยให้คุณผสานรวมตรรกะการควบคุม/เสียง C/C++ ของแอปเข้ากับการส่ง MIDI ได้อย่างใกล้ชิด และลดความจำเป็นในการเรียก JNI หรือ Callback ไปยังฝั่ง Java ของแอป ตัวอย่างเช่น เครื่องสังเคราะห์เสียงดิจิทัลที่ใช้โค้ด C อาจได้รับเหตุการณ์คีย์จาก AMidiDevice โดยตรง แทนที่จะรอการเรียก JNI เพื่อส่งเหตุการณ์จากฝั่ง Java หรือกระบวนการแต่งเพลงแบบอัลกอริทึมอาจส่งการแสดง MIDI ไปยัง AMidiDevice โดยตรงโดยไม่ต้องเรียกกลับไปยังฝั่ง Java เพื่อส่งเหตุการณ์คีย์

แม้ว่า AMidi จะปรับปรุงการเชื่อมต่อโดยตรงกับอุปกรณ์ MIDI แต่แอปก็ยังต้องใช้ MidiManager เพื่อค้นหาและเปิดออบเจ็กต์ MidiDevice จากนั้น AMidi จะจัดการต่อ

บางครั้งคุณอาจต้องส่งข้อมูลจากเลเยอร์ UI ลงไปยังโค้ดแบบเนทีฟ เช่น เมื่อมีการส่งเหตุการณ์ MIDI เพื่อตอบสนองต่อปุ่มบนหน้าจอ หากต้องการทำเช่นนี้ ให้สร้างการเรียก JNI ที่กำหนดเองไปยังตรรกะเนทีฟ หากต้องการส่งข้อมูลกลับเพื่ออัปเดต UI คุณสามารถเรียกกลับจากเลเยอร์เนทีฟได้ตามปกติ

เอกสารนี้แสดงวิธีตั้งค่าแอปโค้ดแบบเนทีฟ AMidi พร้อมตัวอย่างการส่งและรับคำสั่ง MIDI หากต้องการดูตัวอย่างการทำงานที่สมบูรณ์ โปรดดูแอปตัวอย่าง NativeMidi

ใช้ AMidi

แอปทั้งหมดที่ใช้ AMidi จะมีขั้นตอนการตั้งค่าและการปิดเหมือนกัน ไม่ว่าจะส่งหรือรับ MIDI หรือทั้งสองอย่าง

เริ่มใช้ AMidi

ในฝั่ง Java แอปต้องค้นหาฮาร์ดแวร์ MIDI ที่แนบมา สร้าง MidiDevice ที่เกี่ยวข้อง แล้วส่งไปยังโค้ดแบบเนทีฟ

  1. ค้นหาฮาร์ดแวร์ MIDI ด้วยคลาส MidiManager ของ Java
  2. รับออบเจ็กต์ MidiDevice ของ Java ที่สอดคล้องกับฮาร์ดแวร์ 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 ซึ่งทั้ง 2 อย่างนี้จะอยู่ใน Android NDK

ฝั่ง Java ควรส่งออบเจ็กต์ MidiDevice อย่างน้อย 1 รายการและหมายเลขพอร์ตไปยังเลเยอร์เนทีฟผ่านการเรียก 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 อย่างต่อเนื่อง ซึ่งอาจเป็นเธรดเบื้องหลังหรือเธรดเสียง AMidi จะไม่บล็อกเมื่ออ่านจากพอร์ต จึงใช้ภายใน Callback เสียงได้อย่างปลอดภัย

ตั้งค่า MidiDevice และพอร์ตเอาต์พุต

แอปจะอ่านข้อมูล MIDI ขาเข้าจากพอร์ตเอาต์พุตของอุปกรณ์ ฝั่ง Java ของแอปต้องกำหนดอุปกรณ์และพอร์ตที่จะใช้

ข้อมูลโค้ดนี้จะสร้าง MidiManager จากบริการ MIDI ของ Android และเปิด MidiDevice สำหรับอุปกรณ์แรกที่พบ เมื่อเปิด MidiDevice แล้ว ระบบจะได้รับ Callback ไปยังอินสแตนซ์ของ MidiManager.OnDeviceOpenedListener() ระบบจะเรียกใช้เมธอด 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() แสดงผลเป็นตัวเลขที่มากกว่า 0

สำหรับแอปที่มีแบนด์วิดท์ต่ำ เช่น ขอบเขต MIDI คุณสามารถสำรวจในเธรดเบื้องหลังที่มีลำดับความสำคัญต่ำ (โดยมีการหยุดชั่วคราวที่เหมาะสม)

สำหรับแอปที่สร้างเสียงและมีข้อกำหนดด้านประสิทธิภาพแบบเรียลไทม์ที่เข้มงวดกว่า คุณสามารถสำรวจใน Callback การสร้างเสียงหลัก (Callback BufferQueue สำหรับ OpenSL ES, Callback ข้อมูล 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;
        }
  }
}

แอปที่ใช้ Native Audio 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

ตั้งค่า 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 ขาออกได้ดี จึงสามารถส่งข้อมูลในเธรดหลักของแอป 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

Callback

แม้จะไม่ใช่ฟีเจอร์ AMidi โดยตรง แต่โค้ดแบบเนทีฟอาจต้องส่งข้อมูลกลับไปยังฝั่ง Java (เช่น เพื่ออัปเดต UI) หากต้องการทำเช่นนั้น คุณต้องเขียนโค้ดในฝั่ง Java และเลเยอร์เนทีฟดังนี้

  • สร้างเมธอด Callback ในฝั่ง Java
  • เขียนฟังก์ชัน JNI ที่จัดเก็บข้อมูลที่จำเป็นในการเรียกใช้ Callback

เมื่อถึงเวลา Callback โค้ดแบบเนทีฟจะสร้าง

นี่คือเมธอด Callback ฝั่ง 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() MainActivity ของ Java จะเรียกใช้ 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);
}

แหล่งข้อมูลเพิ่มเติม