From e82731e1f03eaa25772fd1ed38a98ffe9b7c3611 Mon Sep 17 00:00:00 2001 From: Stefano D'Angelo Date: Sun, 30 Jul 2023 09:48:56 +0200 Subject: [PATCH] android support done + fixes bw_{note_queue,voice_alloc} --- TODO | 2 + ...oidManifest.xml => AndroidManifest-fx.xml} | 2 +- .../common/android/AndroidManifest-synth.xml | 14 ++ ...MainActivity.java => MainActivity-fx.java} | 0 .../common/android/MainActivity-synth.java | 160 ++++++++++++++++++ examples/common/android/android.mk | 34 +++- examples/common/android/jni.cpp | 105 ++++++++++-- examples/synth_poly/android/Makefile | 7 + examples/synth_simple/android/Makefile | 1 + include/bw_note_queue.h | 6 +- include/bw_voice_alloc.h | 10 +- 11 files changed, 317 insertions(+), 24 deletions(-) rename examples/common/android/{AndroidManifest.xml => AndroidManifest-fx.xml} (97%) create mode 100644 examples/common/android/AndroidManifest-synth.xml rename examples/common/android/{MainActivity.java => MainActivity-fx.java} (100%) create mode 100644 examples/common/android/MainActivity-synth.java create mode 100644 examples/synth_poly/android/Makefile diff --git a/TODO b/TODO index 3374f2c..89a803c 100644 --- a/TODO +++ b/TODO @@ -86,6 +86,8 @@ code: * fix vst3 mapped values (visible in Ableton Live) and short names * polish examples (ranges, etc.) * dump data structures (see note queue) and indicate error precisely +* process (multi) no update +* examples cpu usage build system: * make makefiles handle paths with spaces etc diff --git a/examples/common/android/AndroidManifest.xml b/examples/common/android/AndroidManifest-fx.xml similarity index 97% rename from examples/common/android/AndroidManifest.xml rename to examples/common/android/AndroidManifest-fx.xml index 13d2075..54f2662 100644 --- a/examples/common/android/AndroidManifest.xml +++ b/examples/common/android/AndroidManifest-fx.xml @@ -1,7 +1,7 @@ - + diff --git a/examples/common/android/AndroidManifest-synth.xml b/examples/common/android/AndroidManifest-synth.xml new file mode 100644 index 0000000..fa72a82 --- /dev/null +++ b/examples/common/android/AndroidManifest-synth.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/examples/common/android/MainActivity.java b/examples/common/android/MainActivity-fx.java similarity index 100% rename from examples/common/android/MainActivity.java rename to examples/common/android/MainActivity-fx.java diff --git a/examples/common/android/MainActivity-synth.java b/examples/common/android/MainActivity-synth.java new file mode 100644 index 0000000..643103a --- /dev/null +++ b/examples/common/android/MainActivity-synth.java @@ -0,0 +1,160 @@ +package com.orastron.@NAME@; + +import android.app.Activity; +import android.os.Bundle; +import android.webkit.WebView; +import android.webkit.WebSettings; +import android.webkit.WebChromeClient; +import android.webkit.WebViewClient; +import android.webkit.JavascriptInterface; +import android.content.Context; +import android.content.pm.PackageManager; +import androidx.core.app.ActivityCompat; +import android.media.midi.MidiManager; +import android.media.midi.MidiManager.DeviceCallback; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceInfo.PortInfo; +import android.media.midi.MidiDevice; +import java.util.ArrayList; + +import android.util.Log; +public class MainActivity extends Activity { + static { + System.loadLibrary("@NAME@"); + } + + public native boolean nativeAudioStart(); + public native void nativeAudioStop(); + public native float nativeGetParameter(int i); + public native void nativeSetParameter(int i, float v); + public native void addMidiPort(MidiDevice d, int p); + public native void removeMidiPort(MidiDevice d, int p); + + private WebView webView; + + public class WebAppInterface { + private MidiManager midiManager; + private MidiDeviceCallback midiDeviceCallback; + public ArrayList midiDevices = new ArrayList(); + + public void addMidiDevices(MidiDeviceInfo[] devices) { + for (int i = 0; i < devices.length; i++) { + if (devices[i].getOutputPortCount() == 0) + continue; + midiManager.openDevice(devices[i], + new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + PortInfo[] ports = device.getInfo().getPorts(); + for (int i = 0; i < ports.length; i++) + if (ports[i].getType() == PortInfo.TYPE_OUTPUT) + addMidiPort(device, ports[i].getPortNumber()); + WebAppInterface.this.midiDevices.add(device); + } + }, null); + } + } + + public void removeMidiDevices(MidiDeviceInfo[] devices) { + for (int i = 0; i < midiDevices.size(); i++) { + MidiDevice device = midiDevices.get(i); + int id = device.getInfo().getId(); + int j = 0; + for (; j < devices.length; j++) + if (id == devices[j].getId()) + break; + if (j == devices.length) + continue; + PortInfo[] ports = device.getInfo().getPorts(); + for (j = 0; j < ports.length; j++) + if (ports[j].getType() == PortInfo.TYPE_OUTPUT) + removeMidiPort(device, ports[j].getPortNumber()); + midiDevices.remove(i); + } + } + + public void removeAllMidiDevices() { + for (int i = 0; i < midiDevices.size(); i++) { + MidiDevice device = midiDevices.get(i); + PortInfo[] ports = device.getInfo().getPorts(); + for (int j = 0; j < ports.length; j++) + if (ports[j].getType() == PortInfo.TYPE_OUTPUT) + removeMidiPort(device, ports[j].getPortNumber()); + } + midiDevices.clear(); + } + + public class MidiDeviceCallback extends MidiManager.DeviceCallback { + @Override + public void onDeviceAdded(MidiDeviceInfo device) { + WebAppInterface.this.addMidiDevices(new MidiDeviceInfo[]{device}); + } + + @Override + public void onDeviceRemoved(MidiDeviceInfo device) { + WebAppInterface.this.removeMidiDevices(new MidiDeviceInfo[]{device}); + } + } + + @JavascriptInterface + public boolean hasAudioPermission() { + return MainActivity.this.checkCallingOrSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED; + } + + @JavascriptInterface + public void requestAudioPermission() { + ActivityCompat.requestPermissions(MainActivity.this, new String[] { android.Manifest.permission.RECORD_AUDIO }, 0); + } + + @JavascriptInterface + public boolean audioStart() { + midiManager = (MidiManager)getSystemService(Context.MIDI_SERVICE); + + addMidiDevices(midiManager.getDevices()); + + midiDeviceCallback = new MidiDeviceCallback(); + midiManager.registerDeviceCallback(midiDeviceCallback, null); + + return nativeAudioStart(); + } + + @JavascriptInterface + public void audioStop() { + nativeAudioStop(); + + midiManager.unregisterDeviceCallback(midiDeviceCallback); + + removeAllMidiDevices(); + } + + @JavascriptInterface + public float getParameter(int i) { + return nativeGetParameter(i); + } + + @JavascriptInterface + public void setParameter(int i, float v) { + nativeSetParameter(i, v * 0.001f); + } + } + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + webView = new WebView(this); + setContentView(webView); + WebSettings webSettings = webView.getSettings(); + webSettings.setJavaScriptEnabled(true); + webView.setWebChromeClient(new WebChromeClient()); + webView.setWebViewClient(new WebViewClient()); + webSettings.setDomStorageEnabled(true); + webView.addJavascriptInterface(new WebAppInterface(), "Android"); + webView.loadUrl("file:///android_asset/index.html"); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (grantResults.length > 0) + webView.loadUrl("javascript:gotAudioPermission()"); + } +} diff --git a/examples/common/android/android.mk b/examples/common/android/android.mk index 5ca4b19..d37febd 100644 --- a/examples/common/android/android.mk +++ b/examples/common/android/android.mk @@ -17,7 +17,11 @@ KOTLINX_COROUTINES_CORE_JVM_FILE := ${KOTLIN_DIR}/kotlinx-coroutines-core-jvm-1. JAVAC := javac KEYTOOL := keytool -CXX := ${ANDROID_NDK_DIR}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi23-clang++ +ifdef SYNTH +CXX := ${ANDROID_NDK_DIR}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang++ +else +CXX := ${ANDROID_NDK_DIR}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi26-clang++ +endif ADB := ${HOME}/Android/Sdk/platform-tools/adb APKSIGNER := ${BUILD_TOOLS_DIR}/apksigner ZIPALIGN := ${BUILD_TOOLS_DIR}/zipalign @@ -43,6 +47,10 @@ LDFLAGS := \ -llog \ -landroid +ifdef SYNTH +LDFLAGS += -lamidi +endif + SOURCES_COMMON := \ build/gen/jni.cpp @@ -57,6 +65,14 @@ JARS := \ JNI_NAME := $(shell echo ${NAME} | sed 's:_:_1:g') +ifdef SYNTH +ANDROID_MANIFEST_SOURCE := ${COMMON_DIR}/AndroidManifest-synth.xml +MAIN_ACTIVITY_SOURCE := ${COMMON_DIR}/MainActivity-synth.java +else +ANDROID_MANIFEST_SOURCE := ${COMMON_DIR}/AndroidManifest-fx.xml +MAIN_ACTIVITY_SOURCE := ${COMMON_DIR}/MainActivity-fx.java +endif + all: build/${NAME}.apk build/${NAME}.apk: build/gen/${NAME}.aligned.apk build/apk/lib/armeabi-v7a/lib${NAME}.so build/gen/keystore.jks @@ -71,11 +87,19 @@ build/gen/${NAME}.aligned.apk: build/gen/${NAME}.unsigned.apk build/gen/${NAME}.unsigned.apk: build/apk/classes.dex build/gen/AndroidManifest.xml build/assets/index.html build/assets/config.js build/apk/lib/armeabi-v7a/lib${NAME}.so| build/gen ${AAPT} package -f -M build/gen/AndroidManifest.xml -A build/assets -I ${JAR_FILE} -I ${ANDROIDX_CORE_FILE} -I ${ANDROIDX_LIFECYCLE_COMMON_FILE} -I ${ANDROIDX_VERSIONEDPARCELABLE_FILE} -I ${KOTLIN_STDLIB_FILE} -I ${KOTLINX_COROUTINES_CORE_FILE} -I ${KOTLINX_COROUTINES_CORE_JVM_FILE} -F $@ build/apk +ifdef SYNTH build/apk/classes.dex: build/apk/my_classes.jar - cd build/apk && ${BUILD_TOOLS_DIR}/d8 --min-api 21 ../../$^ ${JARS} && cd ../.. + cd build/apk && ${D8} --min-api 29 ../../$^ ${JARS} && cd ../.. + +build/apk/my_classes.jar: build/obj/com/orastron/${NAME}/MainActivity.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$MidiDeviceCallback.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$1.class | build/apk + ${D8} build/obj/com/orastron/${NAME}/MainActivity.class 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class' 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$MidiDeviceCallback.class' 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$1.class' --min-api 29 --output $@ --no-desugaring +else +build/apk/classes.dex: build/apk/my_classes.jar + cd build/apk && ${D8} --min-api 26 ../../$^ ${JARS} && cd ../.. build/apk/my_classes.jar: build/obj/com/orastron/${NAME}/MainActivity.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class | build/apk - ${D8} build/obj/com/orastron/${NAME}/MainActivity.class 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class' --output $@ --no-desugaring + ${D8} build/obj/com/orastron/${NAME}/MainActivity.class 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class' --min-api 26 --output $@ --no-desugaring +endif build/apk/lib/armeabi-v7a/lib${NAME}.so: ${SOURCES} | build/apk/lib/armeabi-v7a ${CXX} $^ ${CXXFLAGS} ${LDFLAGS} -o $@ @@ -88,10 +112,10 @@ build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class: build/obj/co build/obj/com/orastron/${NAME}/MainActivity.class: build/gen/com/orastron/${NAME}/MainActivity.java | build/obj ${JAVAC} -classpath "${JAR_FILE}:${ANDROIDX_CORE_FILE}:${ANDROIDX_LIFECYCLE_COMMON_FILE}:${ANDROIDX_VERSIONEDPARCELABLE_FILE}:${KOTLIN_STDLIB_FILE}:${KOTLINX_COROUTINES_CORE_FILE}:${KOTLINX_COROUTINES_CORE_JVM_FILE}" -d build/obj build/gen/com/orastron/${NAME}/MainActivity.java -build/gen/com/orastron/${NAME}/MainActivity.java: ${COMMON_DIR}/MainActivity.java | build/gen/com/orastron/${NAME} +build/gen/com/orastron/${NAME}/MainActivity.java: ${MAIN_ACTIVITY_SOURCE} | build/gen/com/orastron/${NAME} cat $^ | sed s:@NAME@:${NAME}:g > $@ -build/gen/AndroidManifest.xml: ${COMMON_DIR}/AndroidManifest.xml | build/gen/com/orastron/${NAME} +build/gen/AndroidManifest.xml: ${ANDROID_MANIFEST_SOURCE} | build/gen/com/orastron/${NAME} cat $^ | sed s:@NAME@:${NAME}:g > $@ build/assets/index.html: ${COMMON_DIR}/index.html | build/assets diff --git a/examples/common/android/jni.cpp b/examples/common/android/jni.cpp index 3750d77..d987cde 100644 --- a/examples/common/android/jni.cpp +++ b/examples/common/android/jni.cpp @@ -1,8 +1,12 @@ #include #include +#include #include #define MINIAUDIO_IMPLEMENTATION +#define MA_ENABLE_ONLY_SPECIFIC_BACKENDS +#define MA_ENABLE_AAUDIO #include +#include #include "config.h" #define BLOCK_SIZE 32 @@ -20,16 +24,64 @@ std::mutex mutex; #ifdef P_MEM_REQ void *mem; #endif +#ifdef P_NOTE_ON +struct PortData { + AMidiDevice *device; + int portNumber; + AMidiOutputPort *port; +}; +std::vector midiPorts; +#define MIDI_BUFFER_SIZE 1024 +uint8_t midiBuffer[MIDI_BUFFER_SIZE]; +#endif static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { (void)pDevice; #if NUM_CHANNELS_IN == 0 (void)pInput; -#endif - +#else const float *x = reinterpret_cast(pInput); +#endif float *y = reinterpret_cast(pOutput); + if (mutex.try_lock()) { + for (int i = 0; i < NUM_PARAMETERS; i++) + if (config_parameters[i].out) + paramValues[i] = P_GET_PARAMETER(&instance, i); + else + P_SET_PARAMETER(&instance, i, paramValues[i]); +#ifdef P_NOTE_ON + for (std::vector::iterator it = midiPorts.begin(); it != midiPorts.end(); it++) { + int32_t opcode; + size_t numBytes; + while (AMidiOutputPort_receive(it->port, &opcode, midiBuffer, MIDI_BUFFER_SIZE, &numBytes, NULL) > 0) { + if (opcode != AMIDI_OPCODE_DATA) + continue; + switch (midiBuffer[0] & 0xf0) { + case 0x90: + P_NOTE_ON(&instance, midiBuffer[1], midiBuffer[2]); + break; + case 0x80: + P_NOTE_OFF(&instance, midiBuffer[1]); + break; +#ifdef P_PITCH_BEND + case 0xe0: + P_PITCH_BEND(&instance, midiBuffer[2] << 7 | midiBuffer[1]); + break; +#endif +#ifdef P_MOD_WHEEL + case 0xb0: + if (midiBuffer[1] == 1) + P_MOD_WHEEL(&instance, midiBuffer[2]); + break; +#endif + } + } + } +#endif + mutex.unlock(); + } + ma_uint32 i = 0; while (i < frameCount) { ma_uint32 n = std::min(frameCount - i, static_cast(BLOCK_SIZE)); @@ -42,7 +94,11 @@ static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, bufs[k][j] = x[l]; #endif +#if NUM_CHANNELS_IN != 0 P_PROCESS(&instance, inBufs, outBufs, n); +#else + P_PROCESS(&instance, NULL, outBufs, n); +#endif l = NUM_CHANNELS_OUT * i; for (ma_uint32 j = 0; j < n; j++) @@ -51,15 +107,6 @@ static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, i += n; } - - if (mutex.try_lock()) { - for (int i = 0; i < NUM_PARAMETERS; i++) - if (config_parameters[i].out) - paramValues[i] = P_GET_PARAMETER(&instance, i); - else - P_SET_PARAMETER(&instance, i, paramValues[i]); - mutex.unlock(); - } } extern "C" @@ -173,3 +220,39 @@ Java_com_orastron_@JNI_NAME@_MainActivity_nativeSetParameter(JNIEnv* env, jobjec paramValues[i] = v; mutex.unlock(); } + +#ifdef P_NOTE_ON +extern "C" +JNIEXPORT void JNICALL +Java_com_orastron_@JNI_NAME@_MainActivity_addMidiPort(JNIEnv* env, jobject thiz, jobject d, jint p) { + (void)thiz; + + PortData data; + AMidiDevice_fromJava(env, d, &data.device); + data.portNumber = p; + mutex.lock(); + if (AMidiOutputPort_open(data.device, p, &data.port) == AMEDIA_OK) + midiPorts.push_back(data); + mutex.unlock(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_orastron_@JNI_NAME@_MainActivity_removeMidiPort(JNIEnv* env, jobject thiz, jobject d, jint p) { + (void)thiz; + + AMidiDevice *device; + AMidiDevice_fromJava(env, d, &device); + mutex.lock(); + for (std::vector::iterator it = midiPorts.begin(); it != midiPorts.end(); ) { + PortData data = *it; + if (data.device != device || data.portNumber != p) { + it++; + continue; + } + AMidiOutputPort_close(data.port); + it = midiPorts.erase(it); + } + mutex.unlock(); +} +#endif diff --git a/examples/synth_poly/android/Makefile b/examples/synth_poly/android/Makefile new file mode 100644 index 0000000..c8098ad --- /dev/null +++ b/examples/synth_poly/android/Makefile @@ -0,0 +1,7 @@ +ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +NAME := bw_example_synth_poly +SOURCES = ${SOURCES_COMMON} ${ROOT_DIR}/../src/bw_example_synth_poly.c +SYNTH := yes + +include ${ROOT_DIR}/../../common/android/android.mk diff --git a/examples/synth_simple/android/Makefile b/examples/synth_simple/android/Makefile index 5810f0c..0f2ad87 100644 --- a/examples/synth_simple/android/Makefile +++ b/examples/synth_simple/android/Makefile @@ -2,5 +2,6 @@ ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) NAME := bw_example_synth_simple SOURCES = ${SOURCES_COMMON} ${ROOT_DIR}/../src/bw_example_synth_simple.c +SYNTH := yes include ${ROOT_DIR}/../../common/android/android.mk diff --git a/include/bw_note_queue.h b/include/bw_note_queue.h index 37f00a5..092165b 100644 --- a/include/bw_note_queue.h +++ b/include/bw_note_queue.h @@ -186,11 +186,11 @@ static inline char bw_note_queue_is_valid(const bw_note_queue *BW_RESTRICT queue return 0; for (int i = 0; i < (int)queue->n_events; i++) { - bw_note_queue_event *ev = queue->events + i; + const bw_note_queue_event *ev = queue->events + i; if (ev->note >= 128 || !bw_is_finite(ev->status.velocity) || ev->status.velocity > 1.f) return 0; for (int j = 0; j < i; j++) { - bw_note_queue_event *ev2 = queue->events + j; + const bw_note_queue_event *ev2 = queue->events + j; if (ev2->note == ev->note) return 0; } @@ -198,7 +198,7 @@ static inline char bw_note_queue_is_valid(const bw_note_queue *BW_RESTRICT queue int cnt = 0; for (int i = 0; i < 128; i++) { - bw_note_queue_status *st = queue->status + i; + const bw_note_queue_status *st = queue->status + i; if (st->pressed) cnt++; if (!bw_is_finite(st->velocity) || st->velocity > 1.f) diff --git a/include/bw_voice_alloc.h b/include/bw_voice_alloc.h index bb318c3..e19e235 100644 --- a/include/bw_voice_alloc.h +++ b/include/bw_voice_alloc.h @@ -29,6 +29,8 @@ *
    *
  • Version 0.6.0: *
      + *
    • Now using BW_SIZE_T to count voices in + * bw_voice_alloc(). *
    • Added debugging code.
    • *
    • Removed dependency on bw_config.
    • *
    @@ -116,7 +118,7 @@ void bw_voice_alloc(const bw_voice_alloc_opts *BW_RESTRICT opts, bw_note_queue * for (unsigned char i = 0; i < queue->n_events; i++) { bw_note_queue_event *ev = queue->events + i; - for (int j = 0; j < n_voices; j++) + for (BW_SIZE_T j = 0; j < n_voices; j++) if (!opts->is_free(voices[j]) && opts->get_note(voices[j]) == ev->note) { if (!ev->status.pressed || ev->went_off) opts->note_off(voices[j], ev->status.velocity); @@ -126,7 +128,7 @@ void bw_voice_alloc(const bw_voice_alloc_opts *BW_RESTRICT opts, bw_note_queue * } if (ev->status.pressed) { - for (int j = 0; j < n_voices; j++) + for (BW_SIZE_T j = 0; j < n_voices; j++) if (opts->is_free(voices[j])) { opts->note_on(voices[j], ev->note, ev->status.velocity); goto next_event; @@ -134,7 +136,7 @@ void bw_voice_alloc(const bw_voice_alloc_opts *BW_RESTRICT opts, bw_note_queue * int k = -1; int v = ev->note; - for (int j = 0; j < n_voices; j++) { + for (BW_SIZE_T j = 0; j < n_voices; j++) { int n = opts->get_note(voices[j]); if (!queue->status[n].pressed && (k < 0 || (opts->priority == bw_voice_alloc_priority_low ? n > v : n < v))) { v = n; @@ -146,7 +148,7 @@ void bw_voice_alloc(const bw_voice_alloc_opts *BW_RESTRICT opts, bw_note_queue * continue; } - for (int j = 0; j < n_voices; j++) { + for (BW_SIZE_T j = 0; j < n_voices; j++) { int n = opts->get_note(voices[j]); if (opts->priority == bw_voice_alloc_priority_low ? n > v : n < v) { v = n;