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
ที่เกี่ยวข้อง และส่งไปยังโค้ดเนทีฟ
- สำรวจฮาร์ดแวร์ MIDI ด้วยคลาส Java
MidiManager
- รับออบเจ็กต์
MidiDevice
ของ Java ที่สอดคล้องกับฮาร์ดแวร์ 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 จากนั้นเลเยอร์เนทีฟควรทำตามขั้นตอนต่อไปนี้
- สําหรับ
MidiDevice
ของ Java แต่ละรายการ ให้รับ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 อย่างน้อย 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); 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
จะเรียกใช้ 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