diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9d2d0ba..0b85c27 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -36,7 +36,7 @@ jobs:
- { name: iOS (Mac Catalyst aarch64-apple-ios-macabi), os: macos-latest, target: aarch64-apple-ios-macabi, features: "", setup: ios-macabi }
- { name: Linux (aarch64-unknown-linux-gnu), os: ubuntu-latest, target: aarch64-unknown-linux-gnu, features: "", setup: linux-aarch64 }
- { name: Windows (ARM64 MSVC), os: windows-latest, target: aarch64-pc-windows-msvc, features: "", setup: windows-msvc }
- #- { name: Android (aarch64/armv7/x86_64/i686; API 29), os: ubuntu-latest, target: "", features: "", setup: android }
+ - { name: Android (aarch64/armv7/x86_64/i686; API 29), os: ubuntu-latest, target: "", features: "", setup: android }
steps:
- name: Checkout
diff --git a/Cargo.toml b/Cargo.toml
index 98f1601..115571d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,3 +75,13 @@ env_logger = "0.11" # only for examples
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.2"
+
+# Android parts.
+[target.'cfg(target_os = "android")'.dependencies]
+jni = { version = "0.21", default-features = false }
+ndk-context = "0.1"
+jni-min-helper = "0.3"
+
+# is already provided by the NDK. (will show a link error below API 29)
+[target.'cfg(target_os = "android")'.build-dependencies]
+cc = "1.0"
diff --git a/README.md b/README.md
index 4e66cf9..325a63b 100644
--- a/README.md
+++ b/README.md
@@ -7,15 +7,25 @@ Cross-platform, realtime MIDI processing in Rust.
* With the exception of message queues, but these can be implemented on top of callbacks using e.g. Rust's channels.
-**midir** currently supports the following platforms/backends:
+**midir** currently supports the following platforms/backends:
- [x] ALSA (Linux)
- [x] WinMM (Windows)
- [x] CoreMIDI (macOS, iOS)
- [x] WinRT (Windows 8+), enable the `winrt` feature
- [x] Jack (Linux, macOS), enable the `jack` feature
- [x] Web MIDI (Chrome, Opera, perhaps others browsers)
+- [x] Android (API 29+, NDK AMidi + JNI)
A higher-level API for parsing and assembling MIDI messages might be added in the future.
## Documentation & Example
API docs can be found at [docs.rs](https://docs.rs/crate/midir/). You can find some examples in the [`examples`](examples/) directory. Or simply run `cargo run --example test_play` after cloning this repository.
+
+### Android
+- Requires Android API 29+ and the Android NDK (r20b+).
+- Build (example, to remove before merging):
+ - Install: `cargo install cargo-ndk`
+ - Targets: `rustup target add aarch64-linux-android`
+ - Build: `cargo ndk -t arm64-v8a -o ./app/src/main/jniLibs build --release`
+- Permissions/features:
+ - Manifest should declare `` (not needed for USB/BLE MIDI).
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..3acaf25
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,4 @@
+fn main() {
+ #[cfg(target_os = "android")]
+ println!("cargo:rustc-link-lib=amidi");
+}
diff --git a/src/backend/android/mod.rs b/src/backend/android/mod.rs
new file mode 100644
index 0000000..32d8cb7
--- /dev/null
+++ b/src/backend/android/mod.rs
@@ -0,0 +1,695 @@
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ mpsc, Arc,
+};
+use std::thread::{self, JoinHandle};
+use std::time::Duration;
+
+use crate::errors::*;
+use crate::Ignore;
+
+use jni::errors::Error as JniError;
+use jni::objects::{GlobalRef, JObject, JObjectArray, JString, JValue};
+use jni::sys::{jint, jobject, JNIEnv as JNISys};
+use jni::JNIEnv;
+
+use jni_min_helper::{android_context, jni_with_env, JObjectGet, JniProxy};
+
+// AMidi (NDK) FFI
+
+#[allow(non_camel_case_types)]
+type media_status_t = i32;
+
+// Opaque types
+#[repr(C)]
+struct AMidiDevice;
+#[repr(C)]
+struct AMidiInputPort;
+#[repr(C)]
+struct AMidiOutputPort;
+
+// opcodes for AMidiOutputPort_receive
+const AMIDI_OPCODE_DATA: i32 = 1;
+const AMIDI_OPCODE_FLUSH: i32 = 2;
+
+#[cfg(target_os = "android")]
+#[link(name = "amidi")]
+extern "C" {
+ fn AMidiDevice_fromJava(
+ env: *mut JNISys,
+ midiDeviceObj: jobject,
+ outDevicePtrPtr: *mut *mut AMidiDevice,
+ ) -> media_status_t;
+ fn AMidiDevice_release(device: *mut AMidiDevice) -> media_status_t;
+
+ fn AMidiInputPort_open(
+ device: *const AMidiDevice,
+ port_number: i32,
+ out_input_port: *mut *mut AMidiInputPort,
+ ) -> media_status_t;
+ fn AMidiInputPort_close(input_port: *mut AMidiInputPort);
+
+ fn AMidiInputPort_send(
+ input_port: *mut AMidiInputPort,
+ buffer: *const u8,
+ num_bytes: usize,
+ ) -> isize;
+
+ fn AMidiOutputPort_open(
+ device: *const AMidiDevice,
+ port_number: i32,
+ out_output_port: *mut *mut AMidiOutputPort,
+ ) -> media_status_t;
+ fn AMidiOutputPort_close(output_port: *mut AMidiOutputPort);
+
+ fn AMidiOutputPort_receive(
+ output_port: *mut AMidiOutputPort,
+ opcode_ptr: *mut i32,
+ buffer: *mut u8,
+ max_bytes: usize,
+ num_bytes_received_ptr: *mut usize,
+ out_timestamp_ns_ptr: *mut i64,
+ ) -> isize;
+}
+
+// Helpers (JNI)
+
+fn get_midi_manager<'a>(env: &mut JNIEnv<'a>) -> Result, InitError> {
+ // Acquire a local ref for the app Context
+ let ctx_local = env
+ .new_local_ref(android_context())
+ .map_err(|_| InitError)?;
+
+ let class_ctx = env
+ .find_class("android/content/Context")
+ .map_err(|_| InitError)?;
+ let midi_service_field = env
+ .get_static_field(&class_ctx, "MIDI_SERVICE", "Ljava/lang/String;")
+ .map_err(|_| InitError)?
+ .l()
+ .map_err(|_| InitError)?;
+
+ let mgr = env
+ .call_method(
+ &ctx_local,
+ "getSystemService",
+ "(Ljava/lang/String;)Ljava/lang/Object;",
+ &[JValue::from(&midi_service_field)],
+ )
+ .map_err(|_| InitError)?
+ .l()
+ .map_err(|_| InitError)?;
+
+ Ok(mgr)
+}
+
+fn java_string(env: &mut JNIEnv<'_>, s: JString<'_>) -> String {
+ env.get_string(&s)
+ .map(|os| os.to_string_lossy().into_owned())
+ .unwrap_or_default()
+}
+
+fn get_devices<'a>(
+ env: &mut JNIEnv<'a>,
+ midi_manager: &JObject<'a>,
+) -> Result>, InitError> {
+ let devices_obj = env
+ .call_method(
+ midi_manager,
+ "getDevices",
+ "()[Landroid/media/midi/MidiDeviceInfo;",
+ &[],
+ )
+ .map_err(|_| InitError)?
+ .l()
+ .map_err(|_| InitError)?;
+
+ let arr: JObjectArray<'_> = devices_obj.into();
+ let len = env.get_array_length(&arr).map_err(|_| InitError)? as i32;
+ let mut out = Vec::with_capacity(len as usize);
+ for i in 0..len {
+ let obj = env
+ .get_object_array_element(&arr, i)
+ .map_err(|_| InitError)?;
+ out.push(obj);
+ }
+ Ok(out)
+}
+
+fn port_label<'a>(env: &mut JNIEnv<'a>, info: &JObject<'a>, port_info: &JObject<'a>) -> String {
+ let dev = (|| -> Result {
+ let info_cls = env.find_class("android/media/midi/MidiDeviceInfo")?;
+ let props = env
+ .call_method(info, "getProperties", "()Landroid/os/Bundle;", &[])?
+ .l()?;
+ let key = env
+ .get_static_field(&info_cls, "PROPERTY_NAME", "Ljava/lang/String;")?
+ .l()?;
+ let name_obj = env
+ .call_method(
+ &props,
+ "getString",
+ "(Ljava/lang/String;)Ljava/lang/String;",
+ &[JValue::from(&key)],
+ )?
+ .l()?;
+ let name: JString<'_> = JString::from(name_obj);
+ Ok(java_string(env, name))
+ })()
+ .unwrap_or_else(|_| "MIDI Device".to_owned());
+
+ let port_name = (|| -> Result {
+ let name_obj = env
+ .call_method(port_info, "getName", "()Ljava/lang/String;", &[])?
+ .l()?;
+ let s: JString<'_> = JString::from(name_obj);
+ Ok(java_string(env, s))
+ })()
+ .unwrap_or_else(|_| "Port".to_owned());
+
+ let port_number = env
+ .call_method(port_info, "getPortNumber", "()I", &[])
+ .and_then(|v| v.i())
+ .unwrap_or(0);
+
+ format!("{dev} – {port_name} (#{port_number})")
+}
+
+fn open_midi_device_global<'a>(
+ env: &mut JNIEnv<'a>,
+ info: &JObject<'a>,
+ midi_manager: &JObject<'a>,
+) -> Result {
+ // Prepare a oneshot channel to receive the device object.
+ let (tx, rx) = mpsc::channel::