Skip to content

Add Android Oboe backend and AMY wire socket transport - #1136

Closed
linuxificator wants to merge 5 commits into
shorepine:mainfrom
linuxificator:upstream/android-oboe
Closed

Add Android Oboe backend and AMY wire socket transport#1136
linuxificator wants to merge 5 commits into
shorepine:mainfrom
linuxificator:upstream/android-oboe

Conversation

@linuxificator

@linuxificator linuxificator commented Aug 22, 2026

Copy link
Copy Markdown

Summary

Adds a generic Android backend for AMY:

  • native Android AAR service in a separate, unexported :amy process;
  • Oboe/AAudio low-latency stereo signed-16-bit output at AMY's 48 kHz / 128-frame profile;
  • private AF_UNIX / SOCK_SEQPACKET transport at <filesDir>/amy.sock;
  • one unchanged AMY wire request per packet;
  • same-UID peer validation with SO_PEERCRED, socket mode 0600, and a bounded 64-packet queue;
  • minimal transport-only Java hello-world client that plays C4 through C5 entirely through AMY wire messages over the socket.

The client contract is framework-independent. The hello-world application does not import or call AmyService, does not start or stop AMY, does not load a JNI AMY client, and does not compile AMY source. It uses Android's public LocalSocket(SOCKET_SEQPACKET) API and sends raw AMY wire packets only. The AAR owns Android service startup through its private manifest ContentProvider, so the same client boundary can be used from Java/Kotlin, Godot, Qt, or another framework/language that can package the AAR and open the Unix-domain socket.

The AAR intentionally remains packaged into the same Android application/UID as the client because amy.sock is app-private (0600) and accepted peers are validated with SO_PEERCRED. That packaging requirement is separate from the client API: application/framework code has no AMY headers, source, JNI binding, or lifecycle call.

Realtime design

  • The socket receiver thread never calls AMY directly.
  • The audio owner drains already-queued packets at AMY block boundaries.
  • The Oboe callback adapts arbitrary Android callback numFrames values to AMY's 128-frame blocks without adding another full-block output ring.
  • amy.sock is published only after Oboe has started and its first realtime callback has executed, so a successful socket connection is also the engine-readiness boundary.
  • Test-only audio capture preallocates its buffers before audio starts; the realtime callback only copies into those buffers and never performs file I/O or takes the writer mutex.

Client boundary

application/framework code
        |
        | ordinary AMY wire packets only
        v
<filesDir>/amy.sock  (AF_UNIX / SOCK_SEQPACKET)
        |
        v
independent Android :amy process -> AMY -> Oboe/AAudio

The Java example performs no service control. On launch it retries the private socket until it exists, then sends v0w0V10.0Z followed by the C-major note-on/note-off wire packets. The previous JNI socket wrapper (libamy_hello_client.so) has been removed entirely.

Validation

The contribution includes focused transport tests and Android integration/audio-level CI:

  • bash tests/run_amy_unix_socket_test.sh validates packet round-trip, peer/ownership behavior, oversize handling, cleanup, and stale-path safeguards;
  • Android CI builds the AAR and hello-world APK for arm64-v8a and x86_64 service binaries;
  • CI explicitly verifies that the hello-world APK contains no libamy_hello_client.so and that the hello-world module has no native src/main/cpp client code;
  • an Android 35 emulator performs two independent uninstall/install/cold-launch cycles and requires AMY/Oboe startup, successful pure-Java connection to amy.sock, an output-route diagnostic, V10.0, all eight C-scale note-on packets (60, 62, 64, 65, 67, 69, 71, 72), successful completion, and no socket failure;
  • CI, rather than the example application, arms the optional test-only audio capture before launch, keeping the example strictly transport-only;
  • the integration test captures four seconds of the raw signed-16-bit AMY render stream and the exact signed-16-bit callback buffer handed to Oboe, measures peak/RMS dBFS, requires byte-for-byte AMY-to-Oboe identity, requires at least -6 dBFS peak, rejects full-scale clipping, and uploads the WAVs/statistics as a CI artifact;
  • validation run 32651283791 on corrected head commit a12c19bfdd9936dcb10ab8b8e39214041dd56cd9 completed successfully: peak -2.721 dBFS, RMS about -7.27 dBFS, zero clipped samples, and zero AMY-to-Oboe sample differences.

The audio-level regression previously caught a hello-world configuration error: AMY's V control is a 0..10 bus/master scale and the final mixer applies a 0.1 factor. The earlier example's V2.0 therefore selected only 20% linear master gain (about -14 dB relative to V10.0). The example and documentation use V10.0.

Scope

This PR remains deliberately self-contained and does not modify existing AMY core synthesis source files. The Android service contains the AMY implementation; application/framework code communicates with it only through the wire socket.

The development history is five reviewable commits, with the final commit specifically decoupling the Java hello-world from the AMY service API/JNI client and moving Android startup ownership into the AAR.

Add a fixed-size AF_UNIX/SOCK_SEQPACKET transport for local AMY wire messages, including peer credential checks, bounded queueing, cleanup safeguards, standalone regression tests, and focused CI.
Add a generic Android AAR service that renders AMY through Oboe/AAudio and accepts ordinary AMY wire packets over the private Unix transport. Include the minimal C-scale hello-world app plus Android build and emulator integration CI.
Document the generic Android AAR, Oboe backend, private SOCK_SEQPACKET client contract, readiness semantics, build requirements, and hello-world example.
Measure the raw AMY render stream and exact signed-16-bit buffer handed to Oboe during the Android hello-world integration test. Retain both WAVs and level statistics in CI, require byte-for-byte AMY-to-Oboe identity, healthy peak level, and no full-scale clipping.

Use AMY V10.0 for the audible hello-world. AMY's V control is a 0..10 bus/master scale and the final mixer applies a 0.1 factor, so the previous V2.0 setting was only 20% linear gain (about -14 dB relative to V10.0).

Validated at -2.721 dBFS peak with zero clipping and zero AMY-to-Oboe sample differences in Android AMY run 32630088165.
Make the Java hello-world a transport-only client. MainActivity no longer imports or starts AmyService and no longer loads a JNI/native client library. It uses Android LocalSocket SOCK_SEQPACKET directly and sends only ordinary AMY wire packets to filesDir/amy.sock.

Move service startup to an AAR-owned ContentProvider lifecycle hook, remove the hello-world C++/CMake client wrapper, and move test-only audio capture arming into CI so the example remains free of service/test control logic.
@bwhitman

Copy link
Copy Markdown
Collaborator

This is great, thank you. Our sort of ad-hoc "policy" for platform/framework support (we went through the same thing with iOS) is that we won't take large framework-specific trees into the main repo. (Especially android, which me and dan can't easily test.)

The android/ Gradle project, the Oboe service, the hello-world apps, the Godot Android example, and the emulator CI workflows all fall into that category — none of us develop on Android day to day, so that code would rot here. The right home for it is a separate repo (yours, or an amy-android repo we'd happily link prominently from our docs).

That said, there are pieces of this work that are genuinely platform-independent and that we would like in the tree, because they make this port — and any future one — much easier:

  • src/amy_unix_socket.c/.h and its test. It's POSIX-generic, realtime-aware, small, and its test runs on plain Linux CI with no Android toolchain. This is the piece that makes "AMY as a local service any language can talk to" possible, and it's useful well beyond Android.
  • The backend_ready/backend_error signals in godot/amy.gd. That's a platform-independent API improvement.
  • A short porting-notes doc page capturing what you proved: AMY core compiles for Android NDK completely unmodified with AMY_DAISY=1 + AMY_HOST_MIDI=1 + AMY_NO_MINIAUDIO=1 and the small alloc-declaration header (The V10.0 vs V2.0 master-volume gotcha your CI caught is worth a doc note too.)

So: would you be up for slimming this into a PR with just those pieces? We'd merge that, link your Android repo from the docs, and future Android/Godot/Qt users get the full working recipe without us carrying a platform we can't maintain.

@linuxificator

Copy link
Copy Markdown
Author

Hi, yes I can understand that maintaining it would not work without androidians. On the other hand, for me this is also just a bit of hobby in a weekend now and then, so I'm afraid the code is going to suffer the same fate as my garden and the dishes. I wonder if there are any people who need this portability from a more professional situation.. That's the thing a bit, if it is available and people would use it, it could create an incentive to maintain it.
Anyway, next weekend I will try to make the PR you requested. For me it's just a spin-off of a birthday present I'm making: https://hackaday.io/project/206368-lb-omnichord and I though it would be nice to be able to run it on a tablet, and then I saw the godot request in the amy discussion section and fell into that rabbit hole.

@linuxificator

Copy link
Copy Markdown
Author

Superseded by #1147, the reduced portable-scope replacement requested in the review here. The full Android implementation remains available on the fork's upstream/android-oboe branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants