AMidi API พร้อมใช้งานใน Android NDK r20b ขึ้นไป ซึ่งช่วยให้นักพัฒนาแอปสามารถส่งและรับข้อมูล MIDI ด้วยโค้ด C/C++ ได้
โดยปกติแล้วแอป MIDI ของ Android จะใช้ API midi เพื่อสื่อสารกับบริการ 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 ที่เกี่ยวข้อง และส่งไปยังโค้ดแบบเนทีฟ
- ค้นหาฮาร์ดแวร์ MIDI ด้วยคลาส
MidiManagerของ Java - รับออบเจ็กต์ Java
MidiDeviceที่สอดคล้องกับฮาร์ดแวร์ MIDI - ส่ง
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 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 ซึ่งทั้ง 2 อย่างนี้อยู่ใน Android NDK
ฝั่ง Java ควรส่งออบเจ็กต์ MidiDevice และหมายเลขพอร์ตอย่างน้อย 1 รายการไปยังเลเยอร์เนทีฟผ่านการเรียก JNI จากนั้นเลเยอร์เนทีฟควรทำตามขั้นตอนต่อไปนี้
- สำหรับ Java
MidiDeviceแต่ละรายการ ให้รับ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() ระบบจะเรียกใช้เมธอด 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); 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); } } }
โค้ดแบบเนทีฟจะแปลอุปกรณ์ 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), ×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

Callback
แม้ว่าจะไม่ใช่ฟีเจอร์ AMidi โดยตรง แต่โค้ดแบบเนทีฟอาจต้องส่งข้อมูลกลับไปยังฝั่ง Java (เช่น เพื่ออัปเดต UI) โดยคุณต้องเขียนโค้ดในฝั่ง Java และเลเยอร์เนทีฟ ดังนี้
- สร้างเมธอด Callback ในฝั่ง Java
- เขียนฟังก์ชัน JNI ที่จัดเก็บข้อมูลที่จำเป็นในการเรียกใช้ 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() มีดังนี้ 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);
}
แหล่งข้อมูลเพิ่มเติม
- ข้อมูลอ้างอิง AMidi
- ดูแอปตัวอย่าง MIDI แบบเนทีฟที่สมบูรณ์ใน GitHub