L'API AMidi è disponibile in Android NDK r20b e versioni successive. Consente agli sviluppatori di app di inviare e ricevere dati MIDI con codice C/C++.
Le app MIDI per Android utilizzano in genere l'API
midi
per comunicare con il servizio
MIDI per Android. Le app MIDI dipendono principalmente dall'MidiManager
per rilevare, aprire e chiudere uno o più oggetti MidiDevice
e trasferire dati da e verso ogni dispositivo tramite le porte di input e output MIDI del dispositivo:
Quando utilizzi AMidi, passi l'indirizzo di un MidiDevice
al livello di codice nativo
con una chiamata JNI. Da qui, AMidi crea un riferimento a un AMidiDevice
che ha la maggior parte delle funzionalità di un MidiDevice
. Il codice nativo utilizza
funzioni AMidi che comunicano
direttamente con un AMidiDevice
. AMidiDevice
si connette direttamente al
servizio MIDI:
Utilizzando le chiamate AMidi, puoi integrare strettamente la logica di controllo/audio C/C++ della tua app
con la trasmissione MIDI. C'è meno bisogno di chiamate JNI o callback al
lato Java dell'app. Ad esempio, un sintetizzatore digitale implementato nel codice C potrebbe
ricevere eventi chiave direttamente da un AMidiDevice
, anziché attendere una chiamata JNI
per inviare gli eventi dal lato Java. In alternativa, un processo di composizione algoritmica
potrebbe inviare una performance MIDI direttamente a un AMidiDevice
senza richiamare
il lato Java per trasmettere gli eventi chiave.
Sebbene AMidi migliori la connessione diretta ai dispositivi MIDI, le app devono comunque
utilizzare MidiManager
per rilevare e aprire gli oggetti MidiDevice
. AMidi può
proseguire da lì.
A volte potrebbe essere necessario passare informazioni dal livello UI al codice nativo. Ad esempio, quando gli eventi MIDI vengono inviati in risposta ai pulsanti sullo schermo. Per farlo, crea chiamate JNI personalizzate alla tua logica nativa. Se devi inviare dati per aggiornare la UI, puoi richiamare dal livello nativo come di consueto.
Questo documento mostra come configurare un'app con codice nativo AMidi, fornendo esempi di invio e ricezione di comandi MIDI. Per un esempio funzionante completo, consulta l'app di esempio NativeMidi.
Utilizzare AMidi
Tutte le app che utilizzano AMidi hanno gli stessi passaggi di configurazione e chiusura, indipendentemente dal fatto che inviano o ricevano MIDI o entrambi.
Avvia AMidi
Sul lato Java, l'app deve rilevare un
dispositivo MIDI collegato, creare un MidiDevice
corrispondente
e passarlo al codice nativo.
- Scopri l'hardware MIDI con la classe Java
MidiManager
. - Ottieni un oggetto
MidiDevice
Java corrispondente all'hardware MIDI. - Passa il Java
MidiDevice
al codice nativo con JNI.
Scopri l'hardware e le porte
Gli oggetti porta di input e output non appartengono all'app. Rappresentano le porte
sul dispositivo MIDI. Per inviare dati MIDI a un dispositivo, un'app apre un
MIDIInputPort
e poi scrive i dati. Al contrario, per ricevere dati, un'app
apre un MIDIOutputPort
. Per funzionare correttamente, l'app deve assicurarsi che le porte che apre siano del tipo corretto. Il rilevamento di dispositivi e porte viene eseguito lato Java.
Ecco un metodo che rileva ogni dispositivo MIDI e ne esamina le porte. Restituisce un elenco di dispositivi con porte di output per la ricezione dei dati o un elenco di dispositivi con porte di input per l'invio dei dati. Un dispositivo MIDI può avere porte di input e di 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; }
Per utilizzare le funzioni AMidi nel codice C/C++, devi includere
AMidi/AMidi.h
e collegare la libreria amidi
. Entrambi sono disponibili
nell'NDK Android.
Il lato Java deve passare uno o più oggetti MidiDevice
e numeri di porta al
livello nativo tramite una chiamata JNI. Il livello nativo deve quindi eseguire i seguenti passaggi:
- Per ogni
MidiDevice
Java, ottieni unAMidiDevice
utilizzandoAMidiDevice_fromJava()
. - Ottieni un
AMidiInputPort
e/o unAMidiOutputPort
daAMidiDevice
conAMidiInputPort_open()
e/oAMidiOutputPort_open()
. - Utilizza le porte ottenute per inviare e/o ricevere dati MIDI.
Stop AMidi
L'app Java deve segnalare al livello nativo di rilasciare le risorse quando non utilizza più il dispositivo MIDI. Il problema potrebbe essere dovuto al fatto che il dispositivo MIDI è stato disconnesso o che l'app è in chiusura.
Per rilasciare le risorse MIDI, il tuo codice deve eseguire queste attività:
- Interrompi la lettura e/o la scrittura sulle porte MIDI. Se utilizzavi un thread di lettura per eseguire il polling per l'input (vedi Implementare un ciclo di polling di seguito), interrompi il thread.
- Chiudi tutti gli oggetti
AMidiInputPort
e/oAMidiOutputPort
aperti con le funzioniAMidiInputPort_close()
e/oAMidiOutputPort_close()
. - Rilascia
AMidiDevice
conAMidiDevice_release()
.
Ricevere dati MIDI
Un esempio tipico di app MIDI che riceve MIDI è un "sintetizzatore virtuale" che riceve dati di esecuzione MIDI per controllare la sintesi audio.
I dati MIDI in entrata vengono ricevuti in modo asincrono. Pertanto, è meglio leggere MIDI in un thread separato che esegue il polling continuo di una o più porte di output MIDI. Potrebbe essere un thread in background o un thread audio. AMidi non si blocca durante la lettura da una porta ed è quindi sicuro da usare all'interno di un callback audio.
Configurare un MidiDevice e le relative porte di output
Un'app legge i dati MIDI in entrata dalle porte di output di un dispositivo. La parte Java della tua app deve determinare quali dispositivi e porte utilizzare.
Questo snippet crea
MidiManager
dal servizio MIDI di Android e apre
un MidiDevice
per il primo dispositivo trovato. Quando MidiDevice
è stato
aperto, viene ricevuto un callback a un'istanza di
MidiManager.OnDeviceOpenedListener()
. Viene chiamato il metodo onDeviceOpened
di questo listener, che a sua volta chiama startReadingMidi()
per aprire la porta di output 0 sul dispositivo. Si tratta di una funzione JNI definita in AppMidiManager.cpp
. Questa funzione è
spiegata nello snippet successivo.
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); } } }
Il codice nativo traduce il dispositivo MIDI lato Java e le relative porte in riferimenti utilizzati dalle funzioni AMidi.
Ecco la funzione JNI che crea un AMidiDevice
chiamando
AMidiDevice_fromJava()
, quindi chiama AMidiOutputPort_open()
per aprire
una porta di output sul dispositivo:
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...
}
Implementare un ciclo di polling
Le app che ricevono dati MIDI devono eseguire il polling della porta di output e rispondere quando
AMidiOutputPort_receive()
restituisce un numero maggiore di zero.
Per le app a bassa larghezza di banda, come un oscilloscopio MIDI, puoi eseguire il polling in un thread in background a bassa priorità (con sospensioni appropriate).
Per le app che generano audio e hanno requisiti di prestazioni in tempo reale più rigorosi, puoi eseguire il polling nel callback principale di generazione audio (il callback BufferQueue
per OpenSL ES, il callback dei dati AudioStream in AAudio).
Poiché AMidiOutputPort_receive()
non è bloccante, l'impatto sulle prestazioni è minimo.
La funzione readThreadRoutine()
chiamata dalla funzione startReadingMidi()
sopra potrebbe avere il seguente aspetto:
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;
}
}
}
Un'app che utilizza un'API audio nativa (come OpenSL ES o AAudio) può aggiungere il codice di ricezione MIDI al callback di generazione audio nel seguente modo:
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…
// ...
}
Il seguente diagramma illustra il flusso di un'app di lettura MIDI:
Inviare dati MIDI
Un tipico esempio di app di scrittura MIDI è un controller o un sequencer MIDI.
Configurare un MidiDevice e le relative porte di ingresso
Un'app scrive i dati MIDI in uscita nelle porte di input di un dispositivo MIDI. La parte Java della tua app deve determinare quali porte e dispositivo MIDI utilizzare.
Il codice di configurazione riportato di seguito è una variante dell'esempio di ricezione riportato sopra. Crea MidiManager
dal servizio MIDI di Android. Quindi apre il primoMidiDevice
che trova e
chiama startWritingMidi()
per aprire la prima porta di input sul dispositivo. Si tratta di una
chiamata JNI definita in AppMidiManager.cpp
. La funzione è spiegata nello snippet successivo.
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); } } }
Ecco la funzione JNI che crea un AMidiDevice
chiamando
AMidiDevice_fromJava()
, quindi chiama AMidiInputPort_open()
per aprire
una porta di input sul dispositivo:
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;
}
Inviare dati MIDI
Poiché la tempistica dei dati MIDI in uscita è ben compresa e controllata dall'app stessa, la trasmissione dei dati può essere eseguita nel thread principale dell'app MIDI. Tuttavia, per motivi di prestazioni (come in un sequencer), la generazione e la trasmissione di MIDI possono essere eseguite in un thread separato.
Le app possono inviare dati MIDI ogni volta che è necessario. Tieni presente che AMidi blocca la scrittura dei dati.
Ecco un esempio di metodo JNI che riceve un buffer di comandi MIDI e lo scrive:
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);
}
Il seguente diagramma illustra il flusso di un'app di scrittura MIDI:
Callback
Sebbene non sia strettamente una funzionalità AMidi, il codice nativo potrebbe dover passare i dati al lato Java (per aggiornare l'interfaccia utente, ad esempio). Per farlo, devi scrivere codice nel lato Java e nel livello nativo:
- Crea un metodo di callback sul lato Java.
- Scrivi una funzione JNI che memorizzi le informazioni necessarie per richiamare il callback.
Quando è il momento di richiamare, il codice nativo può costruire
Ecco il metodo di callback lato 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); } }); }
Di seguito è riportato il codice C per la funzione JNI che configura un callback per MainActivity.onNativeMessageReceive()
. Chiamate Java MainActivity
initNative()
all'avvio:
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");
}
Quando è il momento di inviare i dati a Java, il codice nativo recupera i puntatori di callback e costruisce il 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);
}
Risorse aggiuntive
- Riferimento AMidi
- Consulta l'app di esempio Native MIDI completa su GitHub.