Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
77fc89f
Add Android AMY hello-world app module
linuxificator Aug 22, 2026
a972a20
Add hello-world activity manifest
linuxificator Aug 22, 2026
236d80c
Add AMY hello-world activity
linuxificator Aug 22, 2026
114e59a
Add native socket client build
linuxificator Aug 22, 2026
f74466c
Implement AMY wire C-scale socket client
linuxificator Aug 22, 2026
f72a697
Enable Android application plugin for hello-world app
linuxificator Aug 22, 2026
669f301
Include Android hello-world app
linuxificator Aug 22, 2026
2906ca9
Add x86_64 ABI for Android emulator smoke test
linuxificator Aug 22, 2026
41c513c
Fix hello-world native socket client build
linuxificator Aug 22, 2026
b94a24c
Build and smoke-test Android AMY hello world
linuxificator Aug 22, 2026
cc632be
Pin hello-world Android NDK
linuxificator Aug 22, 2026
5d7349f
Document Android AMY hello-world demo
linuxificator Aug 22, 2026
6bc5736
Fix hello-world emulator APK path and enable audio
linuxificator Aug 22, 2026
baf8c85
Keep AMY service alive across repeated start requests
linuxificator Aug 22, 2026
ba98036
Keep hello-world scale stable across activity recreation
linuxificator Aug 22, 2026
2dc8aa5
Make Android hello-world smoke test detect duplicate failures
linuxificator Aug 22, 2026
45e5845
Use audio-ready amy.sock on hello-world branch
linuxificator Aug 22, 2026
f7911f3
Sync generic Android service docs
linuxificator Aug 22, 2026
de094d8
Make first-install scale deterministic and louder
linuxificator Aug 22, 2026
b89e699
Test two fresh installs of Android hello world
linuxificator Aug 22, 2026
7c8eaa4
Document louder deterministic Android hello world
linuxificator Aug 22, 2026
b05db1b
Fix repeated fresh-install emulator test
linuxificator Aug 22, 2026
f85aa7b
Raise Android hello-world AMY output gain
linuxificator Aug 22, 2026
3596ff0
Log Android Oboe output routing
linuxificator Aug 22, 2026
b790001
Resolve and log Android audio output device
linuxificator Aug 22, 2026
877da74
Validate Android output route and V2 gain
linuxificator Aug 22, 2026
ed338bf
Document Android hello-world gain and route diagnostics
linuxificator Aug 22, 2026
d57e342
Merge latest Android AMY service into hello-world
linuxificator Aug 22, 2026
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
58 changes: 55 additions & 3 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Build and run private Unix socket test
run: bash tests/run_amy_unix_socket_test.sh

android-aar:
android-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -45,13 +45,65 @@ jobs:
with:
gradle-version: "8.13"

- name: Build AMY Android AAR
- name: Build AMY Android AAR and hello-world APK
working-directory: android
run: gradle :amy-service:assembleDebug --stacktrace
run: gradle :amy-service:assembleDebug :hello-world:assembleDebug --stacktrace

- name: Upload AMY Android AAR
uses: actions/upload-artifact@v4
with:
name: amy-service-debug-aar
path: android/amy-service/build/outputs/aar/amy-service-debug.aar
if-no-files-found: error

- name: Upload AMY hello-world APK
uses: actions/upload-artifact@v4
with:
name: amy-hello-world-debug-apk
path: android/hello-world/build/outputs/apk/debug/hello-world-debug.apk
if-no-files-found: error

- name: Enable KVM for Android emulator
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Emulator end-to-end smoke test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
profile: pixel_2
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -no-boot-anim
script: |
adb uninstall org.amy.hello >/dev/null 2>&1 || true
adb install android/hello-world/build/outputs/apk/debug/hello-world-debug.apk
adb logcat -c
adb shell am start -W -n org.amy.hello/.MainActivity
sleep 10
adb logcat -d -s AmyAndroid:I AmyService:I AmyHelloWorld:I '*:S' > /tmp/amy-first.log
test "$(grep -c 'AMY/Oboe started' /tmp/amy-first.log)" -eq 1
grep -q 'AMY output route: deviceId=' /tmp/amy-first.log
test "$(grep -c 'C scale complete' /tmp/amy-first.log)" -eq 1
! grep -q 'C scale failed' /tmp/amy-first.log
grep -q 'wire: v0w0V2.0Z' /tmp/amy-first.log
test "$(grep -Ec 'wire: v0n(60|62|64|65|67|69|71|72)l1Z' /tmp/amy-first.log)" -eq 8
grep -q 'wire: v0n60l1Z' /tmp/amy-first.log
grep -q 'wire: v0n72l1Z' /tmp/amy-first.log
adb uninstall org.amy.hello
adb install android/hello-world/build/outputs/apk/debug/hello-world-debug.apk
adb logcat -c
adb shell am start -W -n org.amy.hello/.MainActivity
sleep 10
adb logcat -d -s AmyAndroid:I AmyService:I AmyHelloWorld:I '*:S' > /tmp/amy-second.log
test "$(grep -c 'AMY/Oboe started' /tmp/amy-second.log)" -eq 1
grep -q 'AMY output route: deviceId=' /tmp/amy-second.log
test "$(grep -c 'C scale complete' /tmp/amy-second.log)" -eq 1
! grep -q 'C scale failed' /tmp/amy-second.log
grep -q 'wire: v0w0V2.0Z' /tmp/amy-second.log
test "$(grep -Ec 'wire: v0n(60|62|64|65|67|69|71|72)l1Z' /tmp/amy-second.log)" -eq 8
grep -q 'wire: v0n60l1Z' /tmp/amy-second.log
grep -q 'wire: v0n72l1Z' /tmp/amy-second.log
5 changes: 4 additions & 1 deletion android/amy-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ android {
defaultConfig {
minSdk = 26

// arm64-v8a is the production target. x86_64 is included on this
// hello-world branch so CI can run the same AMY/Oboe service in the
// hardware-accelerated Android emulator.
ndk {
abiFilters += listOf("arm64-v8a")
abiFilters += listOf("arm64-v8a", "x86_64")
}

externalNativeBuild {
Expand Down
1 change: 1 addition & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
plugins {
id("com.android.application") version "8.13.2" apply false
id("com.android.library") version "8.13.2" apply false
}
51 changes: 51 additions & 0 deletions android/hello-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# AMY Android Hello World

Minimal Android application proving the generic AMY Android service end to end.

On launch it:

1. starts `org.amy.audio.AmyService` from the `amy-service` AAR/module;
2. retries a connection to the app-private `<filesDir>/amy.sock` Unix-domain `SOCK_SEQPACKET` socket until the AMY/Oboe service publishes its ready socket;
3. configures raw oscillator 0 as a sine wave and sets AMY global output gain to `V2.0`;
4. waits 30 ms so that setup is committed on a fresh AMY instance before the first note-on;
5. sends AMY wire commands for C4, D4, E4, F4, G4, A4, B4, C5;
6. shows `C scale complete` when all packets have been sent.

The note path does not call AMY through JNI. JNI is used only for the Android client-side Unix socket syscalls because the Java `LocalSocket` API is stream-oriented. The synth process receives ordinary AMY wire packets exactly as another AMY wire transport would.

The generic AMY Android service also logs Oboe's actual output device ID and resolves it through `AudioDeviceInfo`, so device logs identify routes such as `BUILTIN_SPEAKER`, `BUILTIN_EARPIECE`, Bluetooth, wired headphones, or USB where Android exposes a matching device.

## Wire sequence

Setup:

```text
v0w0V2.0Z
```

`V` is AMY's global output gain. It is intentionally set above unity in this audible hello-world test; it is not an oscillator-local amplitude control.

Notes use MIDI note numbers and velocity, e.g. middle C:

```text
v0n60l1Z
v0l0Z
```

The complete scale is MIDI notes `60, 62, 64, 65, 67, 69, 71, 72`.

## Build

From `android/`:

```bash
gradle :hello-world:assembleDebug
```

APK:

```text
hello-world/build/outputs/apk/debug/hello-world-debug.apk
```

The CI Android emulator smoke test builds the AAR/APK and performs two clean install/launch cycles. Each cycle must show exactly one AMY/Oboe startup, an output-route diagnostic, exactly one completed C scale, all eight note-on packets, and no socket failure.
38 changes: 38 additions & 0 deletions android/hello-world/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
id("com.android.application")
}

android {
namespace = "org.amy.hello"
compileSdk = 36
ndkVersion = "27.0.12077973"

defaultConfig {
applicationId = "org.amy.hello"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"

ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}

externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
}
}
}

externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}

dependencies {
implementation(project(":amy-service"))
}
16 changes: 16 additions & 0 deletions android/hello-world/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:label="AMY Hello World"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<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>
8 changes: 8 additions & 0 deletions android/hello-world/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.22.1)
project(amy_hello_client LANGUAGES CXX)

add_library(amy_hello_client SHARED amy_hello_client.cpp)

target_compile_features(amy_hello_client PRIVATE cxx_std_17)
target_compile_options(amy_hello_client PRIVATE -Wall -Wextra -Werror)
target_link_libraries(amy_hello_client PRIVATE log)
101 changes: 101 additions & 0 deletions android/hello-world/src/main/cpp/amy_hello_client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <jni.h>
#include <android/log.h>

#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define LOG_TAG "AmyHelloWorld"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

namespace {

int connect_with_retry(const char *path) {
if (path == nullptr || path[0] == '\0') return -EINVAL;

sockaddr_un addr{};
if (std::strlen(path) >= sizeof(addr.sun_path)) return -ENAMETOOLONG;
addr.sun_family = AF_UNIX;
std::strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);

for (int attempt = 0; attempt < 100; ++attempt) {
int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
if (fd < 0) return -errno;

if (connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) == 0) {
return fd;
}

int saved = errno;
close(fd);
if (saved != ENOENT && saved != ECONNREFUSED) return -saved;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return -ETIMEDOUT;
}

int send_wire(int fd, const char *wire) {
size_t len = std::strlen(wire);
ssize_t sent = send(fd, wire, len, MSG_NOSIGNAL);
if (sent < 0) return -errno;
if (static_cast<size_t>(sent) != len) return -EIO;
LOGI("wire: %s", wire);
return 0;
}

int play_c_scale(const char *path) {
int fd = connect_with_retry(path);
if (fd < 0) return fd;

// Raw oscillator 0, sine wave. V is AMY's global output gain, not an
// oscillator-local amplitude; use 2.0 here so the hello-world is easy to hear.
// Every packet is an ordinary AMY wire command sent through amy.sock.
int rc = send_wire(fd, "v0w0V2.0Z");
if (rc < 0) {
close(fd);
return rc;
}

// On a completely fresh AMY instance, commit oscillator setup before the
// first note-on instead of allowing both commands into the same first drain.
std::this_thread::sleep_for(std::chrono::milliseconds(30));

static constexpr int notes[] = {60, 62, 64, 65, 67, 69, 71, 72};
char wire[64];

for (int note : notes) {
std::snprintf(wire, sizeof(wire), "v0n%dl1Z", note);
rc = send_wire(fd, wire);
if (rc < 0) break;

std::this_thread::sleep_for(std::chrono::milliseconds(350));

rc = send_wire(fd, "v0l0Z");
if (rc < 0) break;
std::this_thread::sleep_for(std::chrono::milliseconds(80));
}

close(fd);
if (rc == 0) LOGI("C scale complete");
return rc;
}

} // namespace

extern "C" JNIEXPORT jint JNICALL
Java_org_amy_hello_MainActivity_nativePlayCScale(JNIEnv *env, jclass, jstring socketPath) {
if (socketPath == nullptr) return -EINVAL;
const char *path = env->GetStringUTFChars(socketPath, nullptr);
if (path == nullptr) return -ENOMEM;
int rc = play_c_scale(path);
env->ReleaseStringUTFChars(socketPath, path);
if (rc < 0) LOGE("C scale failed: %d", rc);
return rc;
}
Loading
Loading