AMidi API tersedia di Android NDK r20b dan yang lebih baru. API ini memungkinkan developer aplikasi mengirim dan menerima data MIDI dengan kode C/C++.
Aplikasi MIDI Android biasanya menggunakan
midi
API untuk berkomunikasi dengan
layanan MIDI Android. Aplikasi MIDI utamanya bergantung pada
MidiManager
untuk menemukan, membuka,
dan menutup satu atau beberapa
objek MidiDevice
, serta
meneruskan data ke dan dari setiap perangkat melalui port
input dan
output MIDI perangkat:
Saat menggunakan AMidi, Anda meneruskan alamat MidiDevice
ke lapisan kode native
dengan panggilan JNI. Dari sana, AMidi membuat referensi ke AMidiDevice
yang memiliki sebagian besar fungsionalitas MidiDevice
. Kode native Anda menggunakan fungsi AMidi yang berkomunikasi langsung dengan AMidiDevice
. AMidiDevice
terhubung langsung ke layanan MIDI:
Dengan panggilan AMidi, Anda dapat mengintegrasikan logika audio/kontrol C/C++ aplikasi Anda secara mendalam dengan transmisi MIDI. Panggilan JNI atau callback ke sistem Java aplikasi Anda tidak begitu diperlukan. Misalnya, synthesizer digital yang diimplementasikan dalam kode C dapat menerima peristiwa utama langsung dari AMidiDevice
, daripada menunggu panggilan JNI untuk mengirimkan peristiwa dari sistem Java. Atau, proses penulisan algoritmik dapat mengirim performa MIDI langsung ke AMidiDevice
tanpa melakukan callback ke sistem Java untuk mengirimkan peristiwa utama.
Meskipun AMidi meningkatkan sambungan langsung ke perangkat MIDI, aplikasi tetap harus menggunakan MidiManager
untuk menemukan dan membuka objek MidiDevice
. AMidi dapat melanjutkannya dari sana.
Terkadang, Anda mungkin perlu meneruskan informasi dari lapisan UI ke kode native. Misalnya, saat peristiwa MIDI dikirim sebagai respons terhadap tombol di layar. Untuk melakukannya, buat panggilan JNI kustom ke logika native. Jika perlu mengirim data kembali untuk mengupdate UI, Anda dapat melakukan callback dari lapisan native seperti biasa.
Dokumen ini menunjukkan cara menyiapkan aplikasi kode native AMidi, dengan memberikan contoh tentang cara mengirim dan menerima perintah MIDI. Untuk contoh yang berfungsi penuh, lihat aplikasi contoh NativeMidi.
Menggunakan AMidi
Semua aplikasi yang menggunakan AMidi memiliki langkah penyiapan dan penutupan yang sama, baik mengirim atau menerima MIDI, atau keduanya.
Memulai AMidi
Pada sistem Java, aplikasi harus menemukan
bagian hardware MIDI yang terpasang, membuat MidiDevice
terkait,
dan meneruskannya ke kode native.
- Temukan hardware MIDI dengan class
MidiManager
Java. - Dapatkan objek Java
MidiDevice
yang terkait dengan hardware MIDI. - Teruskan
MidiDevice
Java ke kode native dengan JNI.
Menemukan hardware dan port
Objek port input dan output tidak termasuk dalam aplikasi. Objek tersebut mewakili port
pada perangkat midi. Untuk mengirim data MIDI ke perangkat, aplikasi membuka MIDIInputPort
, kemudian menulis data ke dalamnya. Sebaliknya, untuk menerima data, aplikasi membuka MIDIOutputPort
. Agar berfungsi dengan baik, aplikasi harus memastikan port yang dibuka adalah jenis yang benar. Penemuan perangkat dan port dilakukan di sistem Java.
Berikut adalah metode yang menemukan setiap perangkat MIDI dan melihat port-nya. Metode tersebut menampilkan daftar perangkat dengan port output untuk menerima data, atau daftar perangkat dengan port input untuk mengirim data. Perangkat MIDI dapat memiliki port input dan juga port output.
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; }
Untuk menggunakan fungsi AMidi dalam kode C/C++, Anda harus menyertakan
AMidi/AMidi.h
dan menautkannya ke library amidi
. Ini dapat ditemukan di Android NDK.
Sistem Java harus meneruskan satu atau beberapa objek MidiDevice
dan nomor port ke lapisan native melalui panggilan JNI. Selanjutnya, lapisan native harus melakukan langkah-langkah berikut:
- Untuk setiap
MidiDevice
Java, dapatkanAMidiDevice
menggunakanAMidiDevice_fromJava()
. - Dapatkan
AMidiInputPort
dan/atauAMidiOutputPort
dariAMidiDevice
denganAMidiInputPort_open()
dan/atauAMidiOutputPort_open()
. - Gunakan port yang diperoleh untuk mengirim dan/atau menerima data MIDI.
Menghentikan AMidi
Aplikasi Java harus memberi sinyal ke lapisan native agar melepas resource saat tidak lagi menggunakan perangkat MIDI. Hal ini bisa jadi karena perangkat MIDI terputus atau karena aplikasi keluar.
Untuk melepaskan resource MIDI, kode harus menjalankan tugas berikut:
- Berhenti membaca dan/atau menulis ke port MIDI. Jika Anda menggunakan thread baca untuk melakukan polling input (lihat Mengimplementasikan loop polling di bawah), hentikan thread.
- Menutup semua objek
AMidiInputPort
dan/atauAMidiOutputPort
yang terbuka dengan fungsiAMidiInputPort_close()
dan/atauAMidiOutputPort_close()
. - Melepaskan
AMidiDevice
denganAMidiDevice_release()
.
Menerima data MIDI
Contoh umum aplikasi MIDI yang menerima MIDI adalah "synthesizer virtual" yang menerima data performa MIDI untuk mengontrol sintesis audio.
Data MIDI yang masuk diterima secara asinkron. Oleh karena itu, sebaiknya baca MIDI di thread terpisah yang secara terus-menerus memilih satu atau beberapa port output MIDI. Thread ini bisa berupa thread latar belakang, atau thread audio. AMidi tidak diblokir saat membaca dari port dan oleh karena itu aman untuk digunakan dalam callback audio.
Menyiapkan MidiDevice dan port output-nya
Aplikasi membaca data MIDI yang masuk dari port output perangkat. Sistem Java pada aplikasi Anda harus menentukan perangkat dan port mana yang akan digunakan.
Cuplikan ini membuat MidiManager
dari layanan MIDI Android dan membuka MidiDevice
untuk perangkat pertama yang ditemukannya. Setelah MidiDevice
dibuka, callback diterima ke instance MidiManager.OnDeviceOpenedListener()
. Metode onDeviceOpened
pemroses ini dipanggil, yang selanjutnya memanggil startReadingMidi()
untuk membuka port output 0 di perangkat. Tindakan ini adalah fungsi JNI yang ditentukan dalam AppMidiManager.cpp
. Fungsi kode ini dijelaskan dalam cuplikan berikutnya.
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); } } }
Kode native menerjemahkan perangkat MIDI lingkungan Java beserta portnya ke dalam referensi yang digunakan oleh fungsi AMidi.
Berikut adalah fungsi JNI yang membuat AMidiDevice
dengan memanggil AMidiDevice_fromJava()
, kemudian memanggil AMidiOutputPort_open()
untuk membuka port output di perangkat:
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...
}
Mengimplementasikan loop polling
Aplikasi yang menerima data MIDI harus melakukan polling port output dan merespons saat
AMidiOutputPort_receive()
menampilkan angka yang lebih besar daripada nol.
Untuk aplikasi bandwidth rendah, seperti cakupan MIDI, Anda dapat membuat kueri di thread latar belakang prioritas rendah (dengan fungsi sleep yang sesuai).
Untuk aplikasi yang menghasilkan audio dan memiliki persyaratan performa real-time yang lebih ketat, Anda dapat membuat kueri di callback pembuatan audio utama (callback BufferQueue
untuk OpenSL ES, callback data AudioStream di AAudio).
Karena AMidiOutputPort_receive()
tidak melakukan pemblokiran, dampaknya pada performa akan sangat kecil.
Fungsi readThreadRoutine()
yang dipanggil dari fungsi startReadingMidi()
di atas mungkin terlihat seperti ini:
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;
}
}
}
Aplikasi yang menggunakan API audio native (seperti OpenSL ES atau AAudio) dapat menambahkan kode penerimaan MIDI ke callback pembuatan audio seperti ini:
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…
// ...
}
Diagram berikut mengilustrasikan alur aplikasi pembacaan MIDI:
Mengirim data MIDI
Contoh umum aplikasi penulisan MIDI adalah pengontrol atau sequencer MIDI.
Menyiapkan MidiDevice dan port inputnya
Aplikasi menulis data MIDI keluar ke port input perangkat MIDI. Sistem Java pada aplikasi Anda harus menentukan perangkat dan port mana yang akan digunakan.
Kode penyiapan di bawah ini adalah variasi dari contoh penerimaan di atas. Kode ini membuat MidiManager
dari layanan MIDI Android. Selanjutnya, kode ini membuka MidiDevice
pertama yang ditemukannya dan memanggil startWritingMidi()
untuk membuka port input pertama di perangkat. Tindakan ini adalah panggilan JNI yang ditentukan dalam AppMidiManager.cpp
. Fungsi kode ini dijelaskan dalam cuplikan berikutnya.
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); } } }
Berikut adalah fungsi JNI yang membuat AMidiDevice
dengan memanggil AMidiDevice_fromJava()
, kemudian memanggil AMidiInputPort_open()
untuk membuka port input di perangkat:
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;
}
Mengirim data MIDI
Karena penentuan waktu data MIDI keluar dipahami dengan baik dan dikontrol oleh aplikasi itu sendiri, transmisi data dapat dilakukan di thread utama aplikasi MIDI. Namun, karena alasan performa (seperti pada sequencer), pembuatan dan transmisi MIDI dapat dilakukan di thread terpisah.
Aplikasi dapat mengirimkan data MIDI setiap kali diperlukan. Perhatikan bahwa AMidi melakukan pemblokiran saat sedang menulis data.
Berikut adalah contoh metode JNI yang menerima buffer perintah MIDI dan menuliskannya:
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);
}
Diagram berikut mengilustrasikan alur aplikasi penulisan MIDI:
Callback
Meskipun bukan sepenuhnya fitur AMidi, kode native Anda mungkin perlu meneruskan data kembali ke sistem Java (misalnya untuk mengupdate UI). Untuk melakukannya, Anda harus menulis kode di sistem Java dan lapisan native:
- Buat metode callback di sistem Java.
- Tulis fungsi JNI yang menyimpan informasi yang diperlukan untuk memanggil callback.
Jika waktu callback sudah tiba, kode native Anda dapat dibuat
Berikut adalah metode callback untuk lingkungan 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); } }); }
Berikut adalah kode C untuk fungsi JNI yang menyiapkan callback ke MainActivity.onNativeMessageReceive()
. MainActivity
Java memanggil initNative()
pada saat mulai:
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");
}
Jika waktu untuk mengirim data kembali ke Java sudah tiba, kode native akan mengambil pointer callback dan membuat 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);
}
Referensi tambahan
- Referensi AMidi
- Lihat Aplikasi contoh MIDI native selengkapnya di github.