API MIDI แบบเนทีฟ

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

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

ฝั่ง Java ควรส่งออบเจ็กต์ MidiDevice อย่างน้อย 1 รายการและพอร์ตหมายเลขไปยัง เลเยอร์ดั้งเดิมผ่านการเรียก JNI จากนั้นเลเยอร์ดั้งเดิมควรดำเนินการ ขั้นตอนต่อไปนี้

  1. สําหรับ MidiDevice ของ Java แต่ละรายการ ให้รับ AMidiDevice โดยใช้ AMidiDevice_fromJava()
  2. รับ AMidiInputPort และ/หรือ AMidiOutputPort จาก AMidiDevice ด้วย AMidiInputPort_open() และ/หรือ AMidiOutputPort_open()
  3. ใช้พอร์ตที่ได้เพื่อส่งและ/หรือรับข้อมูล MIDI

Stop 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 ได้รับการ Callback ไปยังอินสแตนซ์ 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 คุณสามารถสำรวจในลำดับความสำคัญต่ำได้ ชุดข้อความเบื้องหลัง (ที่มีการนอนหลับที่เหมาะสม)

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

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

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

แม้ว่าจะไม่ใช่ฟีเจอร์ 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 โค้ดแบบเนทีฟจะเรียกคืน Callback Pointer และสร้าง 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);
}

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