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