Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a11421c
Add private Unix socket transport for AMY wire protocol
linuxificator Aug 22, 2026
d2755c9
Add Android Oboe service and hello-world client
linuxificator Aug 22, 2026
9a3e4e1
Document Android AMY integration and socket contract
linuxificator Aug 22, 2026
bd45e9b
Validate Android audio level and correct hello-world gain
linuxificator Aug 23, 2026
a12c19b
Decouple Android hello-world from AMY service API
linuxificator Aug 23, 2026
1bdc0d8
Add private Unix socket transport for AMY wire protocol
linuxificator Aug 22, 2026
4104247
Harden Unix socket transport and tests
Aug 30, 2026
237ff60
Expose Godot backend lifecycle signals
Aug 30, 2026
d90917c
Document portable service integrations
Aug 30, 2026
218dbc2
Merge portable socket API offer into Android integration
Aug 30, 2026
8738ae2
Define the Android integration service profile
Aug 30, 2026
20f714a
Align Android service with PySide NDK r27c
Aug 30, 2026
8b7cd10
Extend Android framework audio capture
Aug 30, 2026
74bed04
Add reusable sequencer groups
Sep 3, 2026
066bc1f
Expand sequencer group behavior coverage
Sep 3, 2026
83ae7f1
Document sequencer group API
Sep 3, 2026
4cbd2bd
Merge Android and socket integration
Sep 3, 2026
fe4851d
Document downstream Android packaging lessons
Aug 30, 2026
08b25fb
Add selectable CPython PCM bank build
Aug 30, 2026
95b2926
Support deterministic offline live configuration
Aug 31, 2026
3472bb2
Build Gamma9001 Omnichord service profile
Aug 30, 2026
8262de2
Register Gamma9001 PCM in Android service
Aug 30, 2026
b6ff038
Configure hosted sequencer group capacity
Sep 3, 2026
788742f
Size Omnichord group execution pool
Sep 3, 2026
3d6ec07
Document hosted sequencer capacity
Sep 3, 2026
1098528
Cover sequencer group identity and rollover
Sep 3, 2026
3462b26
Merge sequencer group regression coverage
Sep 3, 2026
45fc871
Add sequencer group usage guides
Sep 3, 2026
890ec66
Preserve socket commands under receiver backpressure
Sep 3, 2026
b1e995f
Align sequencer group terminology and links
Sep 4, 2026
2b98a7e
Add sequencer group usage guides
Sep 3, 2026
d28af9d
Align sequencer group terminology and links
Sep 4, 2026
bb4f0f3
Align release sequencer group references
Sep 4, 2026
c3ebcae
Harden sequencer group validation and diagnostics
Sep 4, 2026
ac8ea86
Expand sequencer group edge-case coverage
Sep 4, 2026
6c9829c
Unify sequencer group tick processing
Sep 4, 2026
2c1c052
Cover sequencer group control boundaries
Sep 4, 2026
2aa4323
Clarify sequencer group contracts and errors
Sep 4, 2026
b791eb2
Cover sequencer group republish and tag edges
Sep 4, 2026
ffebbbe
Merge audited sequencer group implementation
Sep 4, 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
158 changes: 158 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Android AMY

on:
push:
branches:
- integration/amy_android
pull_request:
paths:
- "android/**"
- "src/**"
- "tests/test_amy_unix_socket.c"
- "tests/run_amy_unix_socket_test.sh"
- "tests/check_android_audio_capture.py"
- "tests/test_android_service_contract.py"
- ".github/workflows/android.yml"
workflow_dispatch:

permissions:
contents: read

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

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

- name: Verify Android service contract
run: python3 tests/test_android_service_contract.py

- 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.2.12479018" \
"cmake;3.22.1"

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

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

- name: Verify transport-only hello-world packaging
run: |
APK=android/hello-world/build/outputs/apk/debug/hello-world-debug.apk
test -s "$APK"
! unzip -l "$APK" | grep -q 'libamy_hello_client.so'
test ! -e android/hello-world/src/main/cpp

- 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 shell run-as org.amy.hello mkdir -p files
adb shell run-as org.amy.hello touch files/amy-audio-capture.enable
adb logcat -c
adb shell am start -W -n org.amy.hello/.MainActivity
sleep 10
adb logcat -d -s AmyAndroid:I AmyAudioCapture: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
grep -q 'connected to amy.sock' /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: v0w0V10.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
grep -q 'Audio capture complete:' /tmp/amy-first.log

adb uninstall org.amy.hello
adb install android/hello-world/build/outputs/apk/debug/hello-world-debug.apk
adb shell run-as org.amy.hello mkdir -p files
adb shell run-as org.amy.hello touch files/amy-audio-capture.enable
adb logcat -c
adb shell am start -W -n org.amy.hello/.MainActivity
sleep 10
adb logcat -d -s AmyAndroid:I AmyAudioCapture: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
grep -q 'connected to amy.sock' /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: v0w0V10.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
grep -q 'Audio capture complete:' /tmp/amy-second.log

mkdir -p android/audio-capture
adb exec-out run-as org.amy.hello cat files/amy-render.wav > android/audio-capture/amy-render.wav
adb exec-out run-as org.amy.hello cat files/amy-oboe.wav > android/audio-capture/amy-oboe.wav
adb exec-out run-as org.amy.hello cat files/amy-audio-levels.txt > android/audio-capture/amy-audio-levels.txt
test -s android/audio-capture/amy-render.wav
test -s android/audio-capture/amy-oboe.wav
test -s android/audio-capture/amy-audio-levels.txt
cat android/audio-capture/amy-audio-levels.txt

- name: Analyze captured AMY and Oboe audio levels
run: |
python3 tests/check_android_audio_capture.py \
android/audio-capture/amy-render.wav \
android/audio-capture/amy-oboe.wav

- name: Upload Android audio captures
uses: actions/upload-artifact@v4
with:
name: amy-android-audio-capture
path: android/audio-capture/
if-no-files-found: error
3 changes: 3 additions & 0 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ jobs:
- name: Validate GDScript parses
run: gdparse godot/amy.gd

- name: Test Godot backend lifecycle signals
run: python tests/test_godot_backend_signals.py

- name: Check godot/amy.gd is in sync with amy/__init__.py
run: |
if ! git diff --quiet -- godot/amy.gd; then
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/unix-socket.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: 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/unix-socket.yml'

permissions:
contents: read

jobs:
linux-socket-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Compile and run Unix socket transport test
run: bash tests/run_amy_unix_socket_test.sh
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ amy-message: $(OBJECTS) src/amy-message.o
# Plain C tests for things the audio-rendering suite can't reach -- e.g. clock
# rollovers 50 days out, which you can only hit by fast-forwarding the counters.
CTESTS = tests/test_clock_wrap tests/test_sequencer_active tests/test_sequencer_bounds \
tests/test_sequence_groups \
tests/test_bus_config tests/test_patch_slots \
tests/test_synth_readout tests/test_log2_lut tests/test_clone_on_grow \
tests/test_timebase_reset tests/test_osc_free_on_release \
Expand All @@ -145,6 +146,7 @@ amy-module: amy-example

test: amy-module
${PYTHON} -m amy.test
${PYTHON} tests/test_python_offline_live.py

qtest: amy-module
${PYTHON} -m amy.test quiet
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ AMY was built by [DAn Ellis](https://research.google/people/DanEllis/) and [Bria
* [**Interactive AMY tutorial**](https://shorepine.github.io/amy/tutorial.html)
* [**AMY API**](docs/api.md)
* [**AMY Synthesizer Details**](docs/synth.md)
* [**AMY Sequencer Groups**](docs/sequencer-groups.md)
* [**Distortion in AMY**](docs/distortions.md)
* [**AMY's MIDI specification**](docs/midi.md)
* [**AMY in Arduino Getting Started**](docs/arduino.md)
* [**Porting AMY and local-service transports**](docs/porting.md)
* [**Other AMY web demos**](https://shorepine.github.io/amy/)

AMY supports
Expand Down Expand Up @@ -111,6 +113,11 @@ In Python:
>>> # play MIDI notes using system MIDI
```

`amy.live(audio=False, ...)` applies the same runtime configuration without
starting a system-audio callback. This is intended for deterministic offline
rendering with `c_amy.render_to_list()`; omitting `audio` retains the existing
live-audio behavior.

In C:

```c
Expand Down Expand Up @@ -171,10 +178,12 @@ It's good to understand what wire messages are but you don't need to construct t
* [**Interactive AMY tutorial**](https://shorepine.github.io/amy/tutorial.html)
* [**AMY API**](docs/api.md)
* [**AMY Synthesizer Details**](docs/synth.md)
* [**AMY Sequencer Groups**](docs/sequencer-groups.md)
* [**Distortion in AMY**](docs/distortions.md)
* [**AMY's MIDI specification**](docs/midi.md)
* [**AMY in Arduino Getting Started**](docs/arduino.md)
* [**AMY in Godot**](docs/godot.md)
* [**Porting AMY and local-service transports**](docs/porting.md)
* [**AMY on Windows**](windows/README.md)
* [**Other AMY web demos**](https://shorepine.github.io/amy/)

Expand Down
1 change: 1 addition & 0 deletions amy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def str_of_int(arg):
('algo_source', 'OL'), ('load_sample', 'zL'), ('transfer_file', 'zTL'), ('disk_sample', 'zFL'),
('algorithm', 'oI'), ('chorus', 'kL'), ('reverb', 'hL'), ('echo', 'ML'), ('patch', 'KI'),
('external_channel', 'WI'), ('portamento', 'mI'), ('tempo', 'jF'), ('sequencer_run', 'zYI'),
('sequence_control', 'zQL'),
('external_midi_sync', 'zCI'),
('synth', 'iI'), ('pedal', 'ipI'), ('synth_flags', 'ifI'), ('num_voices', 'ivI'), ('oscs_per_voice', 'inI'),
('synth_level', 'iVF'),
Expand Down
6 changes: 6 additions & 0 deletions amy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@
TICKS_TICK=0
TICKS_PERIOD=1
TICKS_TAG=2
TICKS_GROUP=3
SEQUENCE_CONTROL_STOP=0
SEQUENCE_CONTROL_START=1
SEQUENCE_CONTROL_GATE=2
SEQUENCE_CONTROL_PUBLISH=3
SEQUENCE_CONTROL_CLEAR=4
RESET_SEQUENCER=4096
RESET_ALL_OSCS=8192
RESET_TIMEBASE=16384
Expand Down
56 changes: 44 additions & 12 deletions amy/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,40 @@ def _write_int16_carray(p, data, column=15):
p.write(" %s,\n" % (",".join([("%d" % (d)).ljust(8) for d in data[-rem:]])))


def generate_gamma9001_blob_c(c_path, sounds_dir='sounds/gamma9001',
pcm_AMY_SAMPLE_RATE=22050):
"""Write only the linkable Gamma9001 PCM blob for native host builds.

Unlike ``generate_gamma9001_headers()``, this entry point does not rewrite
tracked headers. A CMake target can therefore generate one private source
file per ABI/build directory without two concurrent Android builds racing
over files in the source checkout.
"""
import json
manifest = json.load(open(os.path.join(sounds_dir, 'manifest.json')))
bin_entries = [m for m in manifest if m['bank'] != GAMMA9001_ROM_BANK]
entries = [
(m, _read_wav_mono16(
os.path.join(sounds_dir, m['file']), pcm_AMY_SAMPLE_RATE))
for m in bin_entries
]
frames = sum(len(data) for _, data in entries)
parent = os.path.dirname(c_path)
if parent:
os.makedirs(parent, exist_ok=True)
with open(c_path, 'w') as p:
p.write("// Automatically generated by amy.headers.generate_gamma9001_blob_c()\n")
p.write("// The Gamma9001 drums.bin blob as C data; see src/pcm_gamma9001.h for the map.\n")
p.write("#include <stdint.h>\n")
p.write("const int16_t gamma9001_pcm_data[%d] = {\n" % frames)
for m, data in entries:
p.write(" // %s: %s\n" % (m['bank'], m['name']))
_write_int16_carray(p, data)
p.write("};\n")
print("gamma9001: %d samples (%.2f MB) -> %s" % (
len(entries), frames * 2 / 1e6, c_path))


def generate_gamma9001_headers(sounds_dir='sounds/gamma9001', bin_path='build/drums.bin',
pcm_AMY_SAMPLE_RATE=22050):
import json
Expand Down Expand Up @@ -338,19 +372,10 @@ def generate_gamma9001_headers(sounds_dir='sounds/gamma9001', bin_path='build/dr
p.close()

# drums.bin as a linkable C array, for targets that bake the banks into the
# binary (the wasm build). ESP32-S3 flashes drums.bin as a partition instead.
# binary (web, CPython and native host builds). ESP32-S3 flashes drums.bin
# as a partition instead.
c_path = os.path.join(os.path.dirname(bin_path), 'drums_bin.c')
p = open(c_path, 'w')
p.write("// Automatically generated by amy.headers.generate_gamma9001_headers()\n")
p.write("// The Gamma9001 drums.bin blob as C data; see src/pcm_gamma9001.h for the map.\n")
p.write("#include <stdint.h>\n")
p.write("const int16_t gamma9001_pcm_data[%d] = {\n" % offset)
for m in bin_entries:
data = _read_wav_mono16(os.path.join(sounds_dir, m['file']), pcm_AMY_SAMPLE_RATE)
p.write(" // %s: %s\n" % (m['bank'], m['name']))
_write_int16_carray(p, data)
p.write("};\n")
p.close()
generate_gamma9001_blob_c(c_path, sounds_dir, pcm_AMY_SAMPLE_RATE)

print("gamma9001: %d ROM samples -> pcm_gamma808.h, %d samples (%.2f MB) -> %s + pcm_gamma9001.h + %s"
% (len(rom), len(bin_entries), offset * 2 / 1e6, bin_path, c_path))
Expand Down Expand Up @@ -1222,6 +1247,13 @@ def generate_all():


def main():
if 'gamma9001-blob-c' in sys.argv:
index = sys.argv.index('gamma9001-blob-c')
if len(sys.argv) != index + 2:
raise SystemExit(
'usage: python -m amy.headers gamma9001-blob-c OUTPUT.c')
generate_gamma9001_blob_c(sys.argv[index + 1])
return
if 'gamma9001' in sys.argv:
generate_gamma9001_headers()
return
Expand Down
Loading
Loading