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 หรือการเรียกกลับไปยังฝั่ง Java ของแอปมากนัก เช่น ซินธิไซเซอร์ดิจิทัลที่ใช้โค้ด C อาจรับเหตุการณ์สำคัญจาก AMidiDevice ได้โดยตรง แทนที่จะรอการเรียก JNI เพื่อส่งเหตุการณ์จากฝั่ง Java หรือกระบวนการแต่งเพลงตามอัลกอริทึมอาจส่งประสิทธิภาพ MIDI ไปยัง AMidiDevice โดยตรงโดยไม่ต้องเรียกใช้ฝั่ง Java เพื่อส่งเหตุการณ์สำคัญ

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

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

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

ใช้ AMidi

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

เริ่ม AMidi

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

  1. สำรวจฮาร์ดแวร์ MIDI ด้วยคลาส Java MidiManager
  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 อย่างน้อย 1 พอร์ตอย่างต่อเนื่อง ซึ่งอาจเป็นเธรดเบื้องหลังหรือเธรดเสียง AMidi ไม่บล็อกเมื่ออ่านจากพอร์ต จึงใช้ภายในการเรียกกลับเสียงได้อย่างปลอดภัย

ตั้งค่า 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);
    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 คุณสามารถโพลในเธรดเบื้องหลังที่มีลําดับความสําคัญต่ำ (โดยมีการหยุดชั่วคราวตามความเหมาะสม)

สําหรับแอปที่สร้างเสียงและมีข้อกําหนดด้านประสิทธิภาพแบบเรียลไทม์ที่เข้มงวดมากขึ้น คุณสามารถโพลในคอลแบ็กการสร้างเสียงหลัก (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 ฝั่ง 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

การติดต่อกลับ

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

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

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

เมธอด 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 ที่ตั้งค่าการเรียกกลับไปยัง 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);
}

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