L'API AMidi è disponibile su Android NDK r20b e versioni successive. Offre all'app agli sviluppatori la possibilità di inviare e ricevere dati MIDI con codice C/C++.
Le app Android MIDI in genere utilizzano
API midi
per comunicare con
Servizio Android MIDI. Le app MIDI dipendono principalmente
MidiManager
per scoprire, aprire
e chiudine una o più
MidiDevice
oggetti e
trasferire i dati da e verso ogni dispositivo tramite la
Ingresso MIDI
e output:
Quando utilizzi AMidi, passi l'indirizzo di un MidiDevice
al 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
. Il AMidiDevice
si collega direttamente
Servizio MIDI:
Utilizzando le chiamate AMidi, puoi integrare da vicino la logica di controllo/audio C/C++ della tua app.
con trasmissione MIDI. Vi è meno necessità di effettuare chiamate JNI o di richiamare il
Il lato Java della tua app. Ad esempio, un sintetizzatore digitale implementato in codice C
Ricevere eventi chiave direttamente da un AMidiDevice
, anziché attendere una richiesta JNI
per inviare gli eventi dal lato Java. Oppure un algoritmo che compone
potrebbe inviare una performance MIDI direttamente a un AMidiDevice
senza chiamare
tornare sul lato Java per trasmettere gli eventi chiave.
Sebbene AMidi migliori la connessione diretta ai dispositivi MIDI, le app devono comunque
usa MidiManager
per trovare e aprire MidiDevice
oggetti. AMidi può
e iniziare a lavorare.
A volte potresti dover trasferire le informazioni dal livello dell'interfaccia utente il codice nativo. Ad esempio, quando gli eventi MIDI vengono inviati in risposta ai pulsanti sullo schermo. Per farlo, crea chiamate JNI personalizzate nella tua logica nativa. Se inviare dati per aggiornare la UI, puoi richiamare dal livello nativo come al solito.
Questo documento mostra come configurare un'app di codice nativa AMidi, con esempi dei comandi MIDI per l'invio e la ricezione. Per un esempio funzionante completo, controlla NativoMidi di esempio.
Utilizzare AMidi
Tutte le app che utilizzano AMidi hanno la stessa configurazione e gli stessi passaggi di chiusura, inviare e ricevere messaggi MIDI, o entrambi.
Avvia AMidi
Sul lato Java, l'app deve rilevare un
componente hardware MIDI collegato, creare un elemento MidiDevice
corrispondente,
e la passiamo al codice nativo.
- Scopri l'hardware MIDI con la classe Java
MidiManager
. - Ottieni un oggetto
MidiDevice
Java corrispondente all'hardware MIDI. - Passa il codice
MidiDevice
Java al codice nativo con JNI.
Scopri hardware e porte
Gli oggetti delle porte di input e output non appartengono all'app. Rappresentano le porte
sul dispositivo midi. Per inviare dati MIDI a un dispositivo, un'app apre una
MIDIInputPort
e vi scrive i dati. Al contrario, per ricevere dati, un'app
apre MIDIOutputPort
. Per funzionare correttamente, l'app deve essere sicura che le porte
sono del tipo corretto. Il rilevamento di dispositivi e porte viene eseguito sul lato Java.
Di seguito è riportato un metodo che rileva ogni dispositivo MIDI e ne esamina porte. Restituisce un elenco di dispositivi con porte di uscita per la ricezione o un elenco di dispositivi dotati di porte di input per l'invio di dati. Un dispositivo MIDI può dispongono di 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++, è necessario includere
AMidi/AMidi.h
e creare un link alla raccolta amidi
. Puoi trovare entrambe le opzioni
nell'NDK di Android.
Il lato Java deve passare uno o più oggetti MidiDevice
e numeri di porta a
lo strato nativo tramite una chiamata JNI. Il livello nativo dovrebbe quindi eseguire
seguenti passaggi:
- Per ogni
MidiDevice
Java, ottieni unAMidiDevice
utilizzandoAMidiDevice_fromJava()
. - Ottieni un
AMidiInputPort
e/o unAMidiOutputPort
dalAMidiDevice
conAMidiInputPort_open()
e/oAMidiOutputPort_open()
. - Utilizza le porte ottenute per inviare e/o ricevere dati MIDI.
Interrompi AMidi
L'app Java deve segnalare al livello nativo di rilasciare risorse quando più a lungo usando il dispositivo MIDI. Il motivo potrebbe essere che il dispositivo MIDI disconnesso o sta uscendo dall'app.
Per rilasciare risorse MIDI, il codice deve eseguire queste attività:
- Interrompi la lettura e/o la scrittura sulle porte MIDI. Se utilizzassi un sistema di lettura thread per eseguire il polling per l'input (vedi Implementare un loop di polling di seguito), interrompere il thread.
- Chiudi tutti gli oggetti
AMidiInputPort
e/oAMidiOutputPort
aperti conAMidiInputPort_close()
e/oAMidiOutputPort_close()
. - Rilascia
AMidiDevice
conAMidiDevice_release()
.
Ricevi dati MIDI
Un esempio tipico di app MIDI che riceve dati MIDI è un "sintetizzatore virtuale" che riceve dati sulle prestazioni MIDI per controllare la sintesi audio.
I dati MIDI in entrata vengono ricevuti in modo asincrono. Pertanto, è preferibile leggere la musica MIDI in un thread separato che esegue il polling continuo di una porta di uscita o delle porte MIDI. Questo potrebbe essere un thread di sottofondo o un thread audio. AMidi non blocca durante la lettura da una porta ed è quindi sicuro da utilizzare all'interno un callback audio.
Configurare un MidiDevice e le relative porte di output
Un'app legge i dati MIDI in arrivo dalle porte di uscita di un dispositivo. Il lato Java della tua app deve stabilire quale dispositivo e quali porte utilizzare.
Questo snippet crea
Si apre MidiManager
dal servizio MIDI di Android
un MidiDevice
per il primo dispositivo che trova. Dopo che MidiDevice
è stato
viene ricevuto un callback per un'istanza
MidiManager.OnDeviceOpenedListener()
. Il metodo onDeviceOpened
viene chiamato il listener, che quindi chiama startReadingMidi()
per aprire la porta di output 0
sul dispositivo. Questo
è una funzione JNI definita in AppMidiManager.cpp
. Questa funzione è
spiegato nel prossimo snippet.
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 sue porte in utilizzati dalle funzioni AMidi.
Ecco la funzione JNI che crea un AMidiDevice
chiamando
AMidiDevice_fromJava()
, poi chiama AMidiOutputPort_open()
per aprire
una porta di uscita 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 loop di polling
Le app che ricevono dati MIDI devono eseguire il polling della porta di uscita e rispondere quando
AMidiOutputPort_receive()
restituisce un numero maggiore di zero.
Per le app a bassa larghezza di banda, come un ambito MIDI, puoi eseguire il polling in una thread in background (con sonno adeguato).
Per le app che generano audio e hanno un rendimento in tempo reale più rigido
requisiti, puoi fare un sondaggio nel callback principale di generazione audio (il
BufferQueue
per OpenSL ES, il callback di dati AudioStream in AAudio).
Poiché AMidiOutputPort_receive()
non blocca, c'è molto poco
un impatto sulle prestazioni.
La funzione readThreadRoutine()
chiamata dalla funzione startReadingMidi()
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, AAudio) può aggiungere codice di ricezione MIDI al callback di generazione audio in questo 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:
Invia dati MIDI
Un esempio tipico di app di scrittura MIDI è un controller o sequencer MIDI.
Configurare un MidiDevice e le relative porte di input
Un'app scrive dati MIDI in uscita sulle porte di ingresso di un dispositivo MIDI. Il lato Java dell'app, devi stabilire quale dispositivo MIDI e porte utilizzare.
Il codice di configurazione riportato di seguito è una variante dell'esempio di ricezione riportato sopra. Crea MidiManager
dal servizio MIDI di Android. Poi apre il primoMidiDevice
trovato e
chiama startWritingMidi()
per aprire la prima porta di ingresso del dispositivo. Si tratta di un
Chiamata JNI definita in AppMidiManager.cpp
. La funzione è spiegata nel
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()
, poi chiama AMidiInputPort_open()
per aprire
una porta di ingresso 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;
}
Invia dati MIDI
Poiché la sincronizzazione dei dati MIDI in uscita è ben compresa e controllata dall'app stessa, la trasmissione dei dati può essere effettuata nel thread principale dell'app MIDI. Tuttavia, per motivi legati alle prestazioni (come in un sequenziatore), la generazione e la trasmissione MIDI può essere eseguita in un thread separato.
Le app possono inviare dati MIDI quando necessario. Tieni presente che AMidi blocca quando la scrittura dei dati.
Ecco un esempio di metodo JNI che riceve un buffer di comandi MIDI 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à di AMidi, il codice nativo potrebbe dover trasmettere dati tornare al lato Java (per aggiornare, ad esempio, l'interfaccia utente). Per farlo, devi scrivi il codice sul lato Java e nel livello nativo:
- Crea un metodo di callback sul lato Java.
- Scrivere una funzione JNI che memorizzi le informazioni necessarie per richiamare il callback.
Quando è il momento di richiamare, il codice nativo può creare
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); } }); }
Ecco il codice C per la funzione JNI che imposta il callback thea su
MainActivity.onNativeMessageReceive()
. Chiamate MainActivity
Java
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 il callback puntatori 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
- Guarda l'app MIDI nativa di esempio completa su GitHub.