Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Android Build

on:
push:
paths-ignore:
- 'docs/**'
- '**.md'
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- uses: android-actions/setup-android@v3

- run: yes | sdkmanager --licenses || true

- run: |
sdkmanager --install "platforms;android-34"
sdkmanager --install "build-tools;34.0.0"
sdkmanager --install "ndk;25.2.9519653"
sdkmanager --install "cmake;3.22.1"

- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- run: chmod +x examples/android/gradlew

- run: |
cd examples/android
./gradlew assembleDebug --no-daemon

1 change: 1 addition & 0 deletions .github/workflows/build_cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths-ignore:
- '**.md'
- 'docs/**'

jobs:
build:
name: ${{ matrix.config.name }}
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/mingw.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
name: MSYS2 build

on: push
on:
push:
paths-ignore:
- 'docs/**'
- '**.md'
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'

jobs:
mingw:
Expand Down
83 changes: 83 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,86 @@ build

.vscode

### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/15d684578f37580886f29e4b8462f3a27f722cb5/Android.gitignore

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Log/OS Files
*.log

# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.aab
*.apk
output-metadata.json

# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml

# Keystore files
*.jks
*.keystore

# Google Services (e.g. APIs or Firebase)
google-services.json

# Android Profiling
*.hprof
### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/15d684578f37580886f29e4b8462f3a27f722cb5/Gradle.gitignore

.gradle
**/build/
!**/src/**/build/

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties

# Cache of project
.gradletasknamecache

# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Generated by gibo (https://github.com/simonwhitaker/gibo)
### https://raw.github.com/github/gitignore/15d684578f37580886f29e4b8462f3a27f722cb5/CMake.gitignore

CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
CMakeUserPresets.json

# CLion
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#cmake-build-*
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ option(LIBREMIDI_NO_JACK "Disable JACK back-end" OFF)
option(LIBREMIDI_NO_PIPEWIRE "Disable PipeWire back-end" OFF)
option(LIBREMIDI_NO_NETWORK "Disable Network back-end" OFF)
option(LIBREMIDI_NO_KEYBOARD "Disable Computer keyboard back-end" OFF)
cmake_dependent_option(LIBREMIDI_NO_ANDROID "Disable Android AMidi back-end" OFF "ANDROID" OFF)

option(LIBREMIDI_NO_EXPORTS "Disable dynamic symbol exporting" OFF)
option(LIBREMIDI_NO_BOOST "Do not use Boost if available" OFF)
Expand Down Expand Up @@ -70,6 +71,7 @@ include(libremidi.jack)
include(libremidi.pipewire)
include(libremidi.keyboard)
include(libremidi.net)
include(libremidi.android)

### Install ###
include(libremidi.install)
Expand Down
18 changes: 18 additions & 0 deletions cmake/libremidi.android.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### Android ###
if(ANDROID AND NOT LIBREMIDI_NO_ANDROID)
# Check for AMidi API availability (API level 29+)
# Also we need API 31+ for JNI_GetCreatedJavaVMs
if(ANDROID_NATIVE_API_LEVEL AND ANDROID_NATIVE_API_LEVEL GREATER_EQUAL 31)
message(STATUS "libremidi: Using Android AMidi API")
message(STATUS " !!! Remember to include MidiDeviceCallback.java in your Android app")
message(STATUS " !!! See the example in examples/android/java/dev/celtera/libremidi")

target_compile_definitions(libremidi ${_public} LIBREMIDI_ANDROID=1)
target_link_libraries(libremidi ${_public} amidi log nativehelper)
target_sources(libremidi ${_public}
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/libremidi/backends/android/helpers.cpp>"
)
else()
message(FATAL_ERROR "libremidi: Android AMidi API requires API level 31+ (current: ${ANDROID_NATIVE_API_LEVEL})")
endif()
endif()
51 changes: 51 additions & 0 deletions examples/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apply plugin: 'com.android.application'

android {
compileSdk 34
ndkVersion "25.2.9519653"

defaultConfig {
applicationId "com.libremidi.example"
minSdk 31
targetSdk 34
versionCode 1
versionName "1.0"

externalNativeBuild {
cmake {
cppFlags "-std=c++20 -frtti -fexceptions"
arguments "-DANDROID_STL=c++_shared"
}
}
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

namespace 'com.libremidi.example'
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
24 changes: 24 additions & 0 deletions examples/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-feature android:name="android.software.midi" android:required="true" />
<uses-permission android:name="android.permission.BIND_MIDI_DEVICE_SERVICE" />

<application
android:allowBackup="true"
android:icon="@android:drawable/ic_menu_manage"
android:label="LibreMIDI Example"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>
28 changes: 28 additions & 0 deletions examples/android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.22.1)
project(libremidi-example)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Find libremidi in the parent directory
set(LIBREMIDI_NO_ANDROID 0)
set(BUILD_SHARED_LIBS 1)
set(LIBREMIDI_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../..")
add_subdirectory(${LIBREMIDI_ROOT} libremidi-build)

# Create shared library
add_library(libremidi-example SHARED
native-lib.cpp
)

# Link libremidi
target_link_libraries(libremidi-example
libremidi
android
log
)

# Include directories
target_include_directories(libremidi-example PRIVATE
${LIBREMIDI_ROOT}/include
)
73 changes: 73 additions & 0 deletions examples/android/app/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include <jni.h>
#include <string>
#include <vector>
#include <sstream>
#include <libremidi/libremidi.hpp>

#include <android/log.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_libremidi_example_MainActivity_getMidiDevices(JNIEnv* env, jobject /* this */) {
std::stringstream result;
__android_log_print(ANDROID_LOG_WARN, "libremidi", "Java_com_libremidi_example_MainActivity_getMidiDevices!!");

try {
auto api = libremidi::API::ANDROID_AMIDI;
libremidi::observer obs{{.track_any = true}, libremidi::observer_configuration_for(api)};

result << "=== MIDI Input Devices ===\n";
auto inputs = obs.get_input_ports();
if (inputs.empty()) {
result << "No input devices found\n";
} else {
for (const auto& port : inputs) {
result << "Port " << port.port << ": " << port.port_name;
if (!port.device_name.empty() && port.device_name != port.port_name) {
result << " (Device: " << port.device_name << ")";
}
if (!port.display_name.empty() && port.display_name != port.port_name) {
result << " [" << port.display_name << "]";
}
result << "\n";

__android_log_print(ANDROID_LOG_WARN, "libremidi", "opening: %s", port.port_name.c_str());
auto midi_in = new libremidi::midi_in{{.on_message{[](const libremidi::message& m) {
__android_log_print(ANDROID_LOG_WARN, "libremidi", "ayy!!");
}}}, libremidi::midi_in_configuration_for(api)};

auto p = midi_in->get_current_api();

__android_log_print(ANDROID_LOG_WARN, "libremidi", ">> ??! %s", libremidi::get_api_name(p).data());
__android_log_print(ANDROID_LOG_WARN, "libremidi", ">> open port?!");
midi_in->open_port(port);
}
}

result << "\n=== MIDI Output Devices ===\n";
auto outputs = obs.get_output_ports();
if (outputs.empty()) {
result << "No output devices found\n";
} else {
for (const auto& port : outputs) {
result << "Port " << port.port << ": " << port.port_name;
if (!port.device_name.empty() && port.device_name != port.port_name) {
result << " (Device: " << port.device_name << ")";
}
if (!port.display_name.empty() && port.display_name != port.port_name) {
result << " [" << port.display_name << "]";
}
result << "\n";
}
}

} catch (const std::exception& e) {
result << "Error: " << e.what() << "\n";
}

__android_log_print(ANDROID_LOG_WARN, "libremidi", "FINITO!!");
return env->NewStringUTF(result.str().c_str());
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_libremidi_example_MainActivity_getLibremidiVersion(JNIEnv* env, jobject /* this */) {
return env->NewStringUTF("libremidi Android example");
}
Loading
Loading