AMidi API'si, Android NDK r20b ve sonraki sürümlerde kullanılabilir. Bu API, uygulama geliştiricilerine C/C++ koduyla MIDI verileri gönderme ve alma olanağı sunar.
Android MIDI uygulamaları, Android MIDI hizmetiyle iletişim kurmak için genellikle midi
API'yi kullanır. MIDI uygulamaları, bir veya daha fazla MidiDevice
nesnesini keşfetmek, açmak ve kapatmak ve cihazın MIDI giriş ile çıkış bağlantı noktaları aracılığıyla her cihaza veri aktarmak için öncelikle MidiManager
'den yararlanır:
AMidi'yi kullandığınızda MidiDevice
adresini JNI çağrısıyla yerel kod katmanına iletirsiniz. AMidi, buradan MidiDevice
işlevinin çoğuna sahip bir AMidiDevice
referansı oluşturur. Yerel kodunuz, doğrudan bir AMidiDevice
ile iletişim kuran AMidi işlevlerini kullanır. AMidiDevice
doğrudan MIDI hizmetine bağlanır:
AMidi çağrılarını kullanarak uygulamanızın C/C++ ses/kontrol mantığını MIDI iletimi ile yakın bir şekilde entegre edebilirsiniz. JNI çağrılarına veya uygulamanızın Java tarafına geri çağrılara daha az ihtiyaç duyulur. Örneğin, C kodunda uygulanan bir dijital sentezleyici, etkinlikleri Java tarafından gönderilmesini beklemek yerine önemli etkinlikleri doğrudan bir AMidiDevice
'ten alabilir. Ya da algoritmik bir besteleme işlemi, önemli etkinlikleri iletmek için Java tarafına geri dönmeden MIDI performansını doğrudan bir AMidiDevice
'e gönderebilir.
AMidi, MIDI cihazlarına doğrudan bağlantıyı iyileştirse de uygulamaların MidiDevice
nesnelerini keşfetmek ve açmak için MidiManager
öğesini kullanması gerekir. AMidi bundan sonra devam edebilir.
Bazen kullanıcı arayüzü katmanından yerel koda bilgi aktarmanız gerekebilir. Örneğin, ekrandaki düğmelere yanıt olarak MIDI etkinlikleri gönderildiğinde. Bunu yapmak için yerel mantığınıza özel JNI çağrıları oluşturun. Kullanıcı arayüzünü güncellemek için geri veri göndermeniz gerekiyorsa her zamanki gibi yerel katmandan geri çağırabilirsiniz.
Bu belgede, hem MIDI komutları gönderme hem de alma örnekleri verilerek AMidi yerel kod uygulamasının nasıl oluşturulacağı gösterilmektedir. Çalışan tam bir örnek için NativeMidi örnek uygulamasına göz atın.
AMidi'yi kullanma
AMidi kullanan tüm uygulamalarda, MIDI gönderip alma veya her ikisini birden yapma işlemlerinde aynı kurulum ve kapatma adımları uygulanır.
AMidi'yi başlatma
Java tarafında, uygulama bağlı bir MIDI donanım parçası bulup buna karşılık gelen bir MidiDevice
oluşturmalı ve bunu yerel koda iletmelidir.
- Java
MidiManager
sınıfını kullanarak MIDI donanımlarını keşfedin. - MIDI donanımına karşılık gelen bir Java
MidiDevice
nesnesi edinin. - JNI ile Java
MidiDevice
'ü yerel koda aktarın.
Donanımı ve bağlantı noktalarını keşfetme
Giriş ve çıkış bağlantı noktası nesneleri uygulamaya ait değildir. Midi cihazındaki bağlantı noktalarını temsil eder. Bir uygulama, MIDI verilerini bir cihaza göndermek için bir MIDIInputPort
açar ve ardından bu MIDIInputPort
'ye veri yazar. Buna karşılık, uygulama veri almak için MIDIOutputPort
açar. Uygulamanın düzgün çalışması için açtığı bağlantı noktalarının doğru türde olduğundan emin olması gerekir. Cihaz ve bağlantı noktası keşfi Java tarafında yapılır.
Aşağıda, her MIDI cihazını keşfeden ve bağlantı noktalarına bakan bir yöntem verilmiştir. Veri almak için çıkış bağlantı noktalarına sahip cihazların veya veri göndermek için giriş bağlantı noktalarına sahip cihazların listesini döndürür. MIDI cihazlarında hem giriş hem de çıkış bağlantı noktaları olabilir.
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; }
C/C++ kodunuzda AMidi işlevlerini kullanmak için AMidi/AMidi.h
'ü eklemeniz ve amidi
kitaplığına bağlamanız gerekir. Bunları Android NDK'da da bulabilirsiniz.
Java tarafı, bir veya daha fazla MidiDevice
nesnesini ve bağlantı noktası numarasını JNI çağrısı aracılığıyla yerel katmana iletmelidir. Yerel katman daha sonra aşağıdaki adımları gerçekleştirmelidir:
- Her Java
MidiDevice
içinAMidiDevice_fromJava()
kullanarak birAMidiDevice
elde edin. AMidiInputPort_open()
ve/veyaAMidiOutputPort_open()
ileAMidiDevice
'denAMidiInputPort
ve/veyaAMidiOutputPort
elde edin.- MIDI verileri göndermek ve/veya almak için elde edilen bağlantı noktalarını kullanın.
AMidi'yi durdurma
Java uygulaması, MIDI cihazını kullanmayı bıraktığında yerel katmana kaynakları serbest bırakması için sinyal göndermelidir. Bunun nedeni MIDI cihazının bağlantısının kesilmesi veya uygulamanın kapanması olabilir.
MIDI kaynaklarını yayınlamak için kodunuz şu görevleri gerçekleştirmelidir:
- MIDI bağlantı noktalarında okuma ve/veya yazma işlemlerini durdurun. Giriş için anket yapmak üzere bir okuma mesaj dizisi kullanıyorsanız (aşağıdaki Anket döngüsü uygulama bölümüne bakın) mesaj dizisini durdurun.
AMidiInputPort_close()
ve/veyaAMidiOutputPort_close()
işlevleriyle açık olan tümAMidiInputPort
ve/veyaAMidiOutputPort
nesnelerini kapatın.AMidiDevice
öğesiniAMidiDevice_release()
ile yayınlayın.
MIDI verilerini alma
MIDI alan MIDI uygulamalarının tipik bir örneği, ses sentezleme işlemini kontrol etmek için MIDI performans verilerini alan "sanal sentezleyici"dir.
Gelen MIDI verileri eşzamansız olarak alınır. Bu nedenle, MIDI'yi bir veya daha fazla MIDI çıkış bağlantı noktasını sürekli olarak sorgulayan ayrı bir iş parçacığında okumak en iyisidir. Bu, arka plan veya ses mesajı olabilir. AMidi, bir bağlantı noktasından okuma yaparken engellemez ve bu nedenle ses geri çağırma işlevinde kullanılması güvenlidir.
MidiDevice ve çıkış bağlantı noktalarını ayarlama
Uygulama, cihazın çıkış bağlantı noktalarından gelen MIDI verilerini okur. Uygulamanızın Java tarafı, hangi cihaz ve bağlantı noktalarının kullanılacağını belirlemelidir.
Bu snippet, Android'in MIDI hizmetinden MidiManager
oluşturur ve bulduğu ilk cihaz için bir MidiDevice
açar. MidiDevice
açıldığında MidiManager.OnDeviceOpenedListener()
örneğine geri çağırma alınır. Bu dinleyicinin onDeviceOpened
yöntemi çağrılır ve ardından cihazdaki 0 numaralı çıkış bağlantı noktasını açmak için startReadingMidi()
çağrılır. Bu, AppMidiManager.cpp
içinde tanımlanmış bir JNI işlevidir. Bu işlev bir sonraki snippet'te açıklanmıştır.
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); } } }
Yerel kod, Java tarafındaki MIDI cihazını ve bağlantı noktalarını AMidi işlevleri tarafından kullanılan referanslara dönüştürür.
AMidiDevice_fromJava()
'u çağırarak bir AMidiDevice
oluşturan ve ardından cihazda bir çıkış bağlantı noktası açmak için AMidiOutputPort_open()
'yi çağıran JNI işlevi aşağıda verilmiştir:
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...
}
Anket döngüsü uygulama
MIDI verileri alan uygulamalar, çıkış bağlantı noktasını yoklamalı ve AMidiOutputPort_receive()
sıfırdan büyük bir sayı döndürdüğünde yanıt vermelidir.
MIDI kapsamı gibi düşük bant genişliğine sahip uygulamalarda, düşük öncelikli bir arka plan iş parçacığında (uygun uyku süreleriyle) anket yapabilirsiniz.
Ses üreten ve daha katı gerçek zamanlı performans koşullarına sahip uygulamalar için ana ses oluşturma geri çağırma işlevinde (OpenSL ES için BufferQueue
geri çağırma işlevi, AAudio'daki AudioStream veri geri çağırma işlevi) anket yapabilirsiniz.
AMidiOutputPort_receive()
engelleme olmadığı için performans üzerinde çok az etki vardır.
Yukarıdaki startReadingMidi()
işlevinden çağrılan readThreadRoutine()
işlevi şu şekilde görünebilir:
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;
}
}
}
Yerel bir ses API'si (OpenSL ES veya AAudio gibi) kullanan bir uygulama, ses oluşturma geri çağırma işlevine aşağıdaki gibi MIDI alma kodu ekleyebilir:
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…
// ...
}
Aşağıdaki şemada MIDI okuma uygulamasının akışı gösterilmektedir:
MIDI verilerini gönderme
MIDI yazma uygulamasına örnek olarak MIDI kontrol cihazı veya sıralayıcı verilebilir.
MidiDevice ve giriş bağlantı noktalarını ayarlama
Uygulama, giden MIDI verilerini MIDI cihazının giriş bağlantı noktalarına yazar. Uygulamanızın Java tarafı, hangi MIDI cihazının ve bağlantı noktalarının kullanılacağını belirlemelidir.
Aşağıdaki kurulum kodu, yukarıdaki alma örneğinin bir varyasyonudur. Android'in MIDI hizmetinden MidiManager
oluşturur. Ardından, bulduğu ilk MidiDevice
öğesini açar ve cihazdaki ilk giriş bağlantı noktasını açmak için startWritingMidi()
öğesini çağırır. Bu, AppMidiManager.cpp
içinde tanımlanmış bir JNI çağrısıdır. İşlev, sonraki snippet'te açıklanmaktadır.
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); } } }
AMidiDevice_fromJava()
'ü çağırarak bir AMidiDevice
oluşturan ve ardından cihazda bir giriş bağlantı noktası açmak için AMidiInputPort_open()
'yi çağıran JNI işlevi aşağıda verilmiştir:
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 verilerini gönderme
Giden MIDI verilerinin zamanlaması uygulama tarafından iyi anlaşılır ve kontrol edildiğinden veri aktarımı MIDI uygulamasının ana iş akışında yapılabilir. Ancak performans nedeniyle (sıralı düzenleyicide olduğu gibi) MIDI'nin oluşturulması ve iletilmesi ayrı bir iş parçacığında yapılabilir.
Uygulamalar, gerektiğinde MIDI verileri gönderebilir. AMidi'nin veri yazarken engellediğini unutmayın.
Aşağıda, MIDI komutları içeren bir arabellek alan ve bu arabelleği yazan örnek bir JNI yöntemi verilmiştir:
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);
}
Aşağıdaki şemada, MIDI yazma uygulamasının akışı gösterilmektedir:
Geri aramalar
Tam olarak AMidi özelliği olmasa da yerel kodunuzun, verileri Java tarafına geri aktarması gerekebilir (ör. kullanıcı arayüzünü güncellemek için). Bunun için Java tarafında ve yerel katmanda kod yazmanız gerekir:
- Java tarafında bir geri çağırma yöntemi oluşturun.
- Geri çağırma işlevini çağırmak için gereken bilgileri depolayan bir JNI işlevi yazın.
Geri çağırma zamanı geldiğinde doğal kodunuz
Java tarafındaki geri çağırma yöntemi 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); } }); }
MainActivity.onNativeMessageReceive()
için geri çağırma işlevi ayarlayan JNI işlevinin C kodu aşağıda verilmiştir. Java MainActivity
, başlangıçta initNative()
'ı çağırır:
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");
}
Verileri Java'ya geri gönderme zamanı geldiğinde yerel kod, geri çağırma işaretçilerini alır ve geri çağırmayı oluşturur:
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);
}
Ek kaynaklar
- AMidi referansı
- Native MIDI örnek uygulamasının tamamını GitHub'da bulabilirsiniz.