diff --git a/CMakeLists.txt b/CMakeLists.txt index f5431dab..3de8282b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ check_include_file(immintrin.h HAVE_IMMINTRIN_H) find_package(PkgConfig) -if(LIBSAMPLERATE_EXAMPLES OR BUILD_TESTING) +if((LIBSAMPLERATE_EXAMPLES OR BUILD_TESTING) AND (NOT ANDROID)) if((NOT VCPKG_TOOLCHAIN) AND PKG_CONFIG_FOUND AND (NOT CMAKE_VERSION VERSION_LESS 3.6)) pkg_check_modules(SndFile sndfile IMPORTED_TARGET) if(SndFile_FOUND) diff --git a/README.md b/README.md index 0515418b..0f3951c9 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ There are detailed instructions for building libsamplerate on Win32 in the file Building on macOS should be the same as building it on any other Unix platform. +## Android + +To build for Android using the Android NDK, see the instructions in the file [`docs/android.md`] + ## Other Platforms To compile libsamplerate on platforms which have a Bourne compatible shell, an ANSI C compiler and a make utility should require no more that the following three commands: diff --git a/android/.gitattributes b/android/.gitattributes new file mode 100644 index 00000000..f91f6460 --- /dev/null +++ b/android/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..bba1a46f --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,9 @@ +local.properties +gradle.properties +build/ +.gradle/ +.cxx/ +.idea/ +gradle/wrapper/ +gradlew +gradlew.bat diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 00000000..e69de29b diff --git a/android/aar-template/AndroidManifest.xml b/android/aar-template/AndroidManifest.xml new file mode 100644 index 00000000..a308dd88 --- /dev/null +++ b/android/aar-template/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/android/aar-template/prefab/modules/samplerate/libs/android.arm64-v8a/abi.json b/android/aar-template/prefab/modules/samplerate/libs/android.arm64-v8a/abi.json new file mode 100644 index 00000000..e049905e --- /dev/null +++ b/android/aar-template/prefab/modules/samplerate/libs/android.arm64-v8a/abi.json @@ -0,0 +1,7 @@ +{ + "abi": "arm64-v8a", + "api": 21, + "ndk": 27, + "stl": "none", + "static": false +} diff --git a/android/aar-template/prefab/modules/samplerate/libs/android.armeabi-v7a/abi.json b/android/aar-template/prefab/modules/samplerate/libs/android.armeabi-v7a/abi.json new file mode 100644 index 00000000..bb9b7c92 --- /dev/null +++ b/android/aar-template/prefab/modules/samplerate/libs/android.armeabi-v7a/abi.json @@ -0,0 +1,7 @@ +{ + "abi": "armeabi-v7a", + "api": 21, + "ndk": 27, + "stl": "none", + "static": false +} diff --git a/android/aar-template/prefab/modules/samplerate/libs/android.x86/abi.json b/android/aar-template/prefab/modules/samplerate/libs/android.x86/abi.json new file mode 100644 index 00000000..17a6bc05 --- /dev/null +++ b/android/aar-template/prefab/modules/samplerate/libs/android.x86/abi.json @@ -0,0 +1,7 @@ +{ + "abi": "x86", + "api": 21, + "ndk": 27, + "stl": "none", + "static": false +} diff --git a/android/aar-template/prefab/modules/samplerate/libs/android.x86_64/abi.json b/android/aar-template/prefab/modules/samplerate/libs/android.x86_64/abi.json new file mode 100644 index 00000000..1fc9415d --- /dev/null +++ b/android/aar-template/prefab/modules/samplerate/libs/android.x86_64/abi.json @@ -0,0 +1,7 @@ +{ + "abi": "x86_64", + "api": 21, + "ndk": 27, + "stl": "none", + "static": false +} diff --git a/android/aar-template/prefab/modules/samplerate/module.json b/android/aar-template/prefab/modules/samplerate/module.json new file mode 100644 index 00000000..5d239944 --- /dev/null +++ b/android/aar-template/prefab/modules/samplerate/module.json @@ -0,0 +1,4 @@ +{ + "export_libraries": [], + "android": {} +} diff --git a/android/aar-template/prefab/prefab.json b/android/aar-template/prefab/prefab.json new file mode 100644 index 00000000..e78c0fc6 --- /dev/null +++ b/android/aar-template/prefab/prefab.json @@ -0,0 +1,6 @@ +{ + "schema_version": 2, + "name": "Samplerate", + "version": "0.2.2", + "dependencies": [] +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 00000000..1e79d354 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,139 @@ +@file:Suppress("UnstableApiUsage") + +val requiredGradleVersion = "8.9" + +require(gradle.gradleVersion == requiredGradleVersion) { + "Gradle version $requiredGradleVersion required (current version: ${gradle.gradleVersion})" +} + +plugins { + alias(libs.plugins.library) + id("maven-publish") +} + +// project.name ("samplerate") defined in settings.gradle.kts +project.group = "com.meganerd" +project.version = "0.2.2-android-r1" + +val abis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + +android { + namespace = "${project.group}.${project.name}" + compileSdk = libs.versions.compilesdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minsdk.get().toInt() + + buildToolsVersion = libs.versions.buildtools.get() + ndkVersion = libs.versions.ndk.get() + ndk { + abiFilters += abis + } + externalNativeBuild { + // build static libs and testing binaries only when running :ndkTest + val buildSharedLibs = if (isTestBuild()) "OFF" else "ON" + val buildTesting = if (isTestBuild()) "ON" else "OFF" + + cmake { + arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + + arguments += "-DBUILD_SHARED_LIBS=$buildSharedLibs" + arguments += "-DBUILD_TESTING=$buildTesting" + arguments += "-DLIBSAMPLERATE_INSTALL=OFF" + arguments += "-DLIBSAMPLERATE_EXAMPLES=OFF" + } + } + } + + externalNativeBuild { + cmake { + path = file("${project.projectDir.parentFile}/CMakeLists.txt") + version = libs.versions.cmake.get() + } + } +} + +tasks.register("prefabAar") { + archiveFileName = "${project.name}-release.aar" + destinationDirectory = file("build/outputs/prefab-aar") + + from("aar-template") + from("${projectDir.parentFile}/include") { + include("**/*.h") + into("prefab/modules/${project.name}/include") + } + abis.forEach { abi -> + from("build/intermediates/cmake/release/obj/$abi") { + include("lib${project.name}.so") + into("prefab/modules/${project.name}/libs/android.$abi") + } + } +} + +tasks.register(getTestTaskName()) { + commandLine("./ndk-test.sh") +} + +tasks.named("clean") { + delete.add(".cxx") +} + +afterEvaluate { + tasks.named("preBuild") { + mustRunAfter("clean") + } + + tasks.named("prefabAar") { + dependsOn("externalNativeBuildRelease") + } + + tasks.named("generatePomFileFor${project.name.cap()}Publication") { + mustRunAfter("prefabAar") + } + + tasks.named("publish") { + dependsOn("clean", "prefabAar") + } + + tasks.named(getTestTaskName()) { + dependsOn("clean", "externalNativeBuildRelease") + } +} + +publishing { + val githubPackagesUrl = "https://maven.pkg.github.com/jg-hot/libsamplerate-android" + + repositories { + maven { + url = uri(githubPackagesUrl) + credentials { + username = properties["gpr.user"]?.toString() + password = properties["gpr.key"]?.toString() + } + } + } + + publications { + create(project.name) { + artifact("build/outputs/prefab-aar/${project.name}-release.aar") + artifactId = "${project.name}-android" + + pom { + distributionManagement { + downloadUrl = githubPackagesUrl + } + } + } + } +} + +tasks.named("wrapper") { + gradleVersion = requiredGradleVersion +} + +fun getTestTaskName(): String = "ndkTest" + +fun isTestBuild(): Boolean = gradle.startParameter.taskNames.contains(getTestTaskName()) + +// capitalize the first letter to make task names matched when written in camel case +fun String.cap(): String = this.replaceFirstChar { it.uppercase() } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml new file mode 100644 index 00000000..6ba3bc6b --- /dev/null +++ b/android/gradle/libs.versions.toml @@ -0,0 +1,10 @@ +[versions] +agp = "8.7.1" +minsdk = "21" +compilesdk = "35" +buildtools = "35.0.0" +ndk = "27.2.12479018" +cmake = "3.30.5" + +[plugins] +library = { id = "com.android.library", version.ref = "agp" } diff --git a/android/ndk-test.sh b/android/ndk-test.sh new file mode 100755 index 00000000..a2cab4cd --- /dev/null +++ b/android/ndk-test.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +SCRIPT_DIR=$(dirname $0) + +LIB_NAME="samplerate" +TEST_DIR="/data/local/tmp/lib${LIB_NAME}/test" + +# remove existing test files +adb $@ shell "rm -r $TEST_DIR" > /dev/null +adb $@ shell "mkdir -p $TEST_DIR" > /dev/null + +ABIS=`adb $@ shell getprop ro.product.cpu.abilist` + +print_message() { + echo "[==========================================================]" + echo "| [lib${LIB_NAME}]: $1" + echo "[==========================================================]" +} + +for ABI in $(echo $ABIS | tr "," "\n"); do + if [ $ABI == "armeabi" ]; then + print_message "skipping deprecated ABI: [$ABI]"; echo + continue + fi + print_message "testing ABI [$ABI]" + + # create test abi directory + TEST_ABI_DIR="$TEST_DIR/$ABI" + adb $@ shell mkdir -p $TEST_ABI_DIR > /dev/null + + # push test files to device + pushd "$SCRIPT_DIR/build/intermediates/cmake/release/obj/$ABI" > /dev/null + adb $@ push * $TEST_ABI_DIR > /dev/null + popd > /dev/null + + # run tests + adb $@ shell -t "cd $TEST_ABI_DIR && export LD_LIBRARY_PATH=. && find . -type f -not -name '*.so' -exec {} \;" + echo +done + +print_message "tests finished for ABIS: [$ABIS]"; echo +echo "NOTE: make sure to verify the test results manually. This task will not fail if tests fail" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 00000000..b52cfa92 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +@file:Suppress("UnstableApiUsage") + +rootProject.name = "samplerate" + +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + diff --git a/docs/android.md b/docs/android.md new file mode 100644 index 00000000..0e06ceea --- /dev/null +++ b/docs/android.md @@ -0,0 +1,85 @@ +layout: default +--- + +# Building for Android + +An Android `gradle` project is located in the `android/` directory. The project +uses the standard [NDK CMake](https://developer.android.com/ndk/guides/cmake) +build system to generate a [prefab](https://google.github.io/prefab/) NDK package. + +## Building the prefab package / .aar +The following commands will build `libsamplerate` as a prefab NDK package and place +it into an [.aar](https://developer.android.com/studio/projects/android-library) library. + +You will need `gradle` version 8.7+ installed in in your path. +``` +cd android/ +gradle assembleRelease +``` + +The resulting `.aar` will be located at: +`android/build/outputs/aar/samplerate-release.aar` + +If you need to specify additional arguments to the `cmake` build, change the +NDK version used for the build, etc, you can do so by editing the `gradle` build +script located at: + +`android/build.gradle.kts` + +## Using as a dependency +After building the `.aar`, do one of the following: +1. `gradle publishToMavenLocal` is already supported in the build script +2. `gradle publishToMavenRepository` is not setup, but you can edit `android/build.gradle.kts` + to add your own maven repository to publish to +3. Copy the `.aar` directly to the `libs/` directory of your project (not recommended) + +Then, add the library to your project's dependencies in your `build.gradle.kts`: +``` +dependencies { + implementation("com.meganerd:samplerate:0.2.2-android-rc1") +} +``` + +Enable `prefab` support in your `build.gradle.kts`: +``` +android { + buildFeatures { + prefab = true + } +} +``` + +Update your `CMakeLists.txt` to find and link the prefab package, which will be +extracted from the `aar` by the build system: + +``` +find_package(samplerate REQUIRED CONFIG) + +target_link_libraries(${CMAKE_PROJECT_NAME} samplerate::samplerate) +``` + +That's it! You can now `#include ` in your NDK source code. + +## Testing on a device +To run the tests, follow these steps: +1. Ensure `adb` is in your path. +2. Have a single device (or emulator) connected and in debug mode. The testing task +only supports a single device. If you have more than one connected (or none) it will +notify you with an error. +3. You will also need `bash` to run the test script + +Run the following commands: +``` +cd android/ +gradle ndkTest +``` + +The test task `:ndkTest` will run `gradle clean assembleRelease` with the following +options set for testing: +* `-DBUILD_SHARED_LIBS=OFF` +* `-DBUILD_TESTING=ON` + +Then it runs `android/ndk-test.sh`, which pushes the binaries located at +`android/build/intermediates/cmake/release/obj/$ABI` to `/data/local/tmp/libsamplerate/test` +on the device, and uses `adb` to execute them. The results will be printed to the console. +