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.
+