Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
0229cfc
Add private Unix socket transport API
linuxificator Aug 22, 2026
b095cd5
Implement private Unix socket transport
linuxificator Aug 22, 2026
bc811b2
Test private Unix socket transport
linuxificator Aug 22, 2026
813c51b
Add Unix socket transport test runner
linuxificator Aug 22, 2026
52d7351
Add Unix socket transport CI
linuxificator Aug 22, 2026
77abd3a
Document Android private amy.sock transport
linuxificator Aug 22, 2026
79e091a
Add Android AMY service Gradle settings
linuxificator Aug 22, 2026
a254f06
Configure Android library build
linuxificator Aug 22, 2026
05e5d97
Add AMY Android AAR module
linuxificator Aug 22, 2026
f696516
Declare private AMY audio service
linuxificator Aug 22, 2026
04ec529
Add Android AMY service JNI lifecycle
linuxificator Aug 22, 2026
5b16952
Add private Unix socket transport API
linuxificator Aug 22, 2026
da1c845
Implement private Unix socket command transport
linuxificator Aug 22, 2026
6980c90
Add host regression test for Unix socket transport
linuxificator Aug 22, 2026
4ade8cd
Add host socket transport test target
linuxificator Aug 22, 2026
3f1189b
Add native AMY Oboe engine and JNI lifecycle
linuxificator Aug 22, 2026
5454155
Build AMY with Oboe for Android
linuxificator Aug 22, 2026
b3a34e5
Add Android AMY build and socket CI
linuxificator Aug 22, 2026
0091870
Document AMY Android Oboe service
linuxificator Aug 22, 2026
7d3ffa9
Target Android ARM64 for AMY service
linuxificator Aug 22, 2026
a977d34
Reuse validated AMY Unix transport in Android engine
linuxificator Aug 22, 2026
344fec5
Link validated AMY Unix socket transport
linuxificator Aug 22, 2026
2cdeed2
Remove duplicate Android socket transport
linuxificator Aug 22, 2026
83746ec
Remove duplicate Android socket header
linuxificator Aug 22, 2026
5d12507
Remove duplicate Android socket regression test
linuxificator Aug 22, 2026
0869a44
Remove duplicate Android socket test Makefile
linuxificator Aug 22, 2026
e621f5d
Run validated socket test in Android CI
linuxificator Aug 22, 2026
8d49ed5
Document Android Oboe service using AMY socket transport
linuxificator Aug 22, 2026
98f70dc
Run Android build for AMY core changes
linuxificator Aug 22, 2026
d263ca4
Pin Android NDK used by AMY service
linuxificator Aug 22, 2026
9956a30
Publish Android AMY AAR artifact
linuxificator Aug 22, 2026
feb18cd
Make Android host own AMY MIDI layer
linuxificator Aug 22, 2026
2eb2235
Provide Android-owned AMY MIDI stubs
linuxificator Aug 22, 2026
0ab5e2d
Assert Android AMY audio profile
linuxificator Aug 22, 2026
a31c07a
Compile Android AMY profile assertions
linuxificator Aug 22, 2026
f985c6f
Android: declare Daisy allocation shim
linuxificator Aug 22, 2026
2e723ea
Android: implement Daisy allocation shim
linuxificator Aug 22, 2026
f7930db
Android: preserve Daisy profile with libc allocation shim
linuxificator Aug 22, 2026
5abe827
Run Android socket CI only for pull requests
linuxificator Aug 22, 2026
5774b4a
Run Android AAR CI only for pull requests
linuxificator Aug 22, 2026
69fb953
Fix Android AMY duplicate QSPI allocator symbols
linuxificator Aug 22, 2026
4804354
Make Android AMY documentation client-agnostic
linuxificator Aug 22, 2026
1047019
Make Android AMY service start idempotent
linuxificator Aug 22, 2026
963fb8b
Publish Android socket only after audio callback
linuxificator Aug 22, 2026
0727020
Document amy.sock audio readiness semantics
linuxificator Aug 22, 2026
871e902
Log actual Android Oboe output routing
linuxificator Aug 22, 2026
0c92595
Resolve Android output device type
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
21 changes: 21 additions & 0 deletions .github/workflows/android-unix-socket.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Android Unix socket transport

on:
pull_request:
paths:
- 'src/amy_unix_socket.c'
- 'src/amy_unix_socket.h'
- 'tests/test_amy_unix_socket.c'
- 'tests/run_amy_unix_socket_test.sh'
- '.github/workflows/android-unix-socket.yml'

permissions:
contents: read

jobs:
linux-socket-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Compile and run private Unix socket transport test
run: bash tests/run_amy_unix_socket_test.sh
57 changes: 57 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Android AMY

on:
pull_request:
paths:
- "android/**"
- "src/**"
- "tests/test_amy_unix_socket.c"
- "tests/run_amy_unix_socket_test.sh"
- ".github/workflows/android.yml"

permissions:
contents: read

jobs:
socket-transport:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Build and run private Unix socket test
run: bash tests/run_amy_unix_socket_test.sh

android-aar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

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

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

- name: Install Android SDK components
run: |
yes | sdkmanager --licenses >/dev/null
sdkmanager \
"platforms;android-36" \
"build-tools;35.0.0" \
"ndk;27.0.12077973" \
"cmake;3.22.1"

- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.13"

- name: Build AMY Android AAR
working-directory: android
run: gradle :amy-service: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
177 changes: 177 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# AMY Android Oboe service

This directory builds a generic Android AAR that hosts AMY in an unexported
`:amy` service process. The service owns Oboe/AAudio output and receives native
AMY wire messages through the private pathname Unix transport implemented by
`src/amy_unix_socket.[ch]`.

```text
Android client process
|
| AF_UNIX / SOCK_SEQPACKET
| <app filesDir>/amy.sock
| one AMY wire message per packet
v
Android :amy service process
|
+-- amy_unix_socket receiver thread
+-- fixed 64-packet SPSC queue
+-- AMY C engine
+-- Oboe low-latency callback
|
v
AAudio
```

The AAR is intended to be embedded by an Android application that wants to use
AMY as its local synth engine. The client can be written with the Android SDK,
Kotlin/Java, native code, Qt, another framework, or any other environment able
to start the service and use an Android Unix-domain `SOCK_SEQPACKET` socket.
AMY itself has no dependency on the client UI framework.

The service declaration uses `android:exported="false"` and
`android:process=":amy"`. Consequently the service runs in a separate process
from the client while remaining in the same Android application package and
under the same application UID.

The service only accepts the exact pathname `<Context.getFilesDir()>/amy.sock`.
The native transport creates that node mode `0600` and additionally verifies
accepted peers with `SO_PEERCRED` against the service effective UID. See
`docs/android_unix_socket.md` for the transport/security contract.

## Audio profile

The Android native build uses AMY's existing 48 kHz / 128-frame build profile
and defines `AMY_NO_MINIAUDIO`; Oboe is the sole audio backend.

Oboe requests:

- stereo signed 16-bit output
- 48 kHz
- `PerformanceMode::LowLatency`
- `SharingMode::Exclusive`
- callback-driven output

The callback size is not assumed to equal 128 frames. The native adapter keeps
only the unconsumed tail of the current AMY block and calls
`amy_simple_fill_buffer()` exactly when another AMY block is required. It does
not add an extra 128-frame output ring.

Before each new AMY block the callback drains up to 64 already-queued socket
packets and passes them to `amy_add_message()`. The socket thread itself never
calls AMY and never participates in audio rendering.

AMY is started with its internal platform audio disabled and with AMY rendering
owned by the Oboe callback thread. The current Android build configuration
reserves 16 Karplus-Strong oscillators.

## JNI boundary

JNI is lifecycle glue only. `AmyService` calls the native library to start and
stop AMY/Oboe with the validated socket pathname. Notes, patches, sequencer
commands and other musical control do not cross JNI; they use the unchanged AMY
wire protocol through `amy.sock`.

The client-facing architecture is therefore deliberately transport-oriented:

```text
client application -> amy.sock -> AMY/Oboe service
```

A client does not need AMY-specific JNI bindings. It only needs to start the
service and exchange AMY wire packets over the private socket.

## Socket client contract

Use `AF_UNIX` + `SOCK_SEQPACKET` and send one logical AMY request per packet.
For example the payload of three consecutive packets may be:

```text
K28i2Z
n60l1i2Z
n60l0i2Z
```

Do not add stream framing or depend on newline boundaries. Packet boundaries
are preserved by `SOCK_SEQPACKET`.

The pathname also serves as the engine readiness boundary. `amy.sock` is not
created until Oboe has started and the realtime audio callback has executed at
least once. A client may therefore retry `connect()` while the service starts;
once `connect()` succeeds it may begin sending AMY wire packets immediately.
No fixed Android-startup sleep is required.

The socket is bidirectional. The Android engine currently consumes ordinary AMY
wire commands; the existing `amy_unix_socket_send()` path is ready for compact
introspection/status replies when that functionality is integrated.

## Client integration

A client application needs to:

1. package the `amy-service` AAR/module in the Android application;
2. start `org.amy.audio.AmyService` while synthesis is required;
3. obtain the application's actual private files directory rather than
hard-code `/data/user/...`;
4. retry an `AF_UNIX` / `SOCK_SEQPACKET` connection to `<filesDir>/amy.sock`
until the service publishes its ready socket;
5. send one ordinary AMY wire message per packet;
6. optionally receive response packets over the same bidirectional socket;
7. stop and reconnect cleanly across Android application/audio lifecycle
events.

The transport deliberately does not prescribe a programming language or UI
framework. A minimal example client is provided separately by the Android
hello-world application.

## Building the AAR

Requirements used by CI:

- JDK 17
- Android SDK platform 36
- Android NDK 27.0.12077973
- CMake 3.22.1
- Gradle 8.13
- Android Gradle Plugin 8.13.2
- Oboe 1.10.0 (Prefab dependency)

From the repository root:

```bash
cd android
gradle :amy-service:assembleDebug
```

The production Android service build targets `arm64-v8a`. Output is below:

```text
android/amy-service/build/outputs/aar/
```

## Tests

The private socket regression test is:

```bash
bash tests/run_amy_unix_socket_test.sh
```

It validates packet round-trip, mode/ownership, `EMSGSIZE` behavior,
oversized-packet rejection, cleanup, and protection against deleting an
existing non-socket path.

`.github/workflows/android.yml` runs that regression plus a complete Android
AAR/NDK/Oboe build. The earlier `.github/workflows/android-unix-socket.yml`
continues to isolate the transport regression itself.

## Hardware-test items

The first device tests should measure:

1. command-to-audio latency;
2. negotiated Oboe callback/device buffer sizes;
3. xruns during patch changes and heavy reverb/delay loads;
4. suspend/resume and audio-device changes;
5. whether executing rare heavy AMY commands at a block boundary needs further
separation from the realtime callback.
45 changes: 45 additions & 0 deletions android/amy-service/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
plugins {
id("com.android.library")
}

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

defaultConfig {
minSdk = 26

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

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

buildFeatures {
prefab = true
}

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

packaging {
jniLibs {
useLegacyPackaging = false
}
}
}

dependencies {
implementation("com.google.oboe:oboe:1.10.0")
}
10 changes: 10 additions & 0 deletions android/amy-service/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name="org.amy.audio.AmyService"
android:exported="false"
android:process=":amy"
android:stopWithTask="true" />
</application>
</manifest>
70 changes: 70 additions & 0 deletions android/amy-service/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
cmake_minimum_required(VERSION 3.22.1)
project(amy_android LANGUAGES C CXX)

find_package(oboe REQUIRED CONFIG)

set(AMY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../..")
set(AMY_SRC "${AMY_ROOT}/src")

set(AMY_SOURCES
${AMY_SRC}/algorithms.c
${AMY_SRC}/amy.c
${AMY_SRC}/amy_unix_socket.c
${AMY_SRC}/delay.c
${AMY_SRC}/envelope.c
${AMY_SRC}/filters.c
${AMY_SRC}/parse.c
${AMY_SRC}/sequencer.c
${AMY_SRC}/transfer.c
${AMY_SRC}/midi_mappings.c
${AMY_SRC}/custom.c
${AMY_SRC}/patches.c
${AMY_SRC}/oscillators.c
${AMY_SRC}/interp_partials.c
${AMY_SRC}/pcm.c
${AMY_SRC}/log2_exp2.c
${AMY_SRC}/instrument.c
${AMY_SRC}/amy_midi.c
${AMY_SRC}/api.c
${AMY_SRC}/cv_trigger.c
)

add_library(amy_android SHARED
amy_android.cpp
amy_android_profile.cpp
${AMY_SOURCES}
)

target_include_directories(amy_android PRIVATE
${AMY_SRC}
${CMAKE_CURRENT_SOURCE_DIR}
)

# AMY_DAISY selects AMY's existing 48 kHz / 128-frame compile profile. Android
# owns both the audio and MIDI device layers, so no Daisy device implementation
# is linked: AMY_NO_MINIAUDIO leaves Oboe as the sole audio backend and
# AMY_HOST_MIDI leaves run_midi/stop_midi/midi_out to amy_android.cpp.
# delay.c already provides qspi_malloc/qspi_free under AMY_DAISY. pcm.c needs
# declarations for those helpers, so force only the compatibility declarations
# into C translation units; do not link a second allocator implementation.
target_compile_definitions(amy_android PRIVATE
AMY_ANDROID=1
AMY_DAISY=1
AMY_HOST_MIDI=1
AMY_NO_MINIAUDIO=1
AMY_WAVETABLE=1
)

target_compile_options(amy_android PRIVATE
$<$<COMPILE_LANGUAGE:C>:-include;${CMAKE_CURRENT_SOURCE_DIR}/amy_android_daisy_alloc.h;-O3;-Wall;-Wextra;-Wno-unused-parameter;-Wno-float-conversion>
$<$<COMPILE_LANGUAGE:CXX>:-O3;-Wall;-Wextra;-Wno-unused-parameter>
)

target_compile_features(amy_android PRIVATE c_std_11 cxx_std_17)

target_link_libraries(amy_android PRIVATE
oboe::oboe
android
log
m
)
Loading
Loading