API AMidi พร้อมใช้งานใน 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 - ส่งผ่าน Java
MidiDevice
ไปยังโค้ดเนทีฟด้วย 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 จะไม่บล็อกเมื่ออ่านจากพอร์ต จึงปลอดภัยที่จะใช้ภายใน การเรียกกลับของเสียง
ตั้งค่า 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); 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 คุณสามารถสำรวจในเธรดพื้นหลังที่มีลำดับความสำคัญต่ำ (โดยมีการหยุดชั่วคราวที่เหมาะสม)
สำหรับแอปที่สร้างเสียงและมีข้อกำหนดด้านประสิทธิภาพแบบเรียลไทม์ที่เข้มงวดกว่า
คุณสามารถสำรวจในโค้ดเรียกกลับการสร้างเสียงหลัก (โค้ดเรียกกลับ 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), ×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 ลงในโค้ดเรียกกลับการสร้างเสียงได้ดังนี้
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
การติดต่อกลับ
แม้จะไม่ใช่ฟีเจอร์ 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
calls
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);
}
แหล่งข้อมูลเพิ่มเติม
- เอกสารอ้างอิง AMidi
- ดูแอปตัวอย่าง MIDI ดั้งเดิมฉบับสมบูรณ์ได้ที่ GitHub