Skip to content

Commit 5de2f3b

Browse files
authored
Merge pull request #953 from shorepine/committed-js-codegen
2 parents 85bfc77 + 0f5cfd2 commit 5de2f3b

8 files changed

Lines changed: 1466 additions & 15 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The live Python REPL at `docs/tutorial.html` runs MicroPython in the browser wit
3030
3. Copy the output to amy's docs: `cp build-standard/tulip/obj/micropython.mjs build-standard/tulip/obj/micropython.wasm <amy-repo>/docs/`
3131
4. The browser AMY build (`docs/amy.js`, `docs/amy.wasm`) is now rebuilt and committed automatically on every release by `.github/workflows/release.yml` (it runs `make deploy-web` and folds the output into the version-bump commit it tags), so you don't need `make deploy-web` by hand for a release. Run `cd <amy-repo> && make web && make deploy-web` locally only to preview those changes before merging. This automation covers only `docs/amy.*` — the `docs/micropython.*` REPL files above are still the separate tulipcc build.
3232

33-
The `docs/amy.js` file is a concatenation of the Emscripten build (`build/amy.js`), `src/amy_connector.js`, and `build/amy_api.generated.js`. The JS API is auto-generated from `amy/__init__.py` by `scripts/gen_amy_js_api.py`.
33+
The `docs/amy.js` file is a concatenation of the Emscripten build (`build/amy.js`), `src/amy_connector.js`, and `src/amy_api.generated.js`. The JS API file — like `src/patches.generated.js` and `src/pcm_presets.generated.js`is a committed generated output: regenerate with `make c-api` after changing `amy/__init__.py`, `src/patches.h`, or `src/pcm_tiny.h`; CI's `make check-c-api` fails any PR where they drift.
3434

3535
## GitHub Auth
3636

Makefile

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,15 @@ all: default
5454
# web/godot .inc + .js + exports fragment + the amy/__init__.py block).
5555
c-api:
5656
$(PYTHON) scripts/gen_amy_c_api.py
57+
$(PYTHON) scripts/gen_amy_js_api.py
58+
$(PYTHON) scripts/gen_patches_js.py
59+
$(PYTHON) scripts/gen_pcm_presets_js.py
5760

5861
check-c-api:
5962
$(PYTHON) scripts/gen_amy_c_api.py --check
63+
$(PYTHON) scripts/gen_amy_js_api.py --check
64+
$(PYTHON) scripts/gen_patches_js.py --check
65+
$(PYTHON) scripts/gen_pcm_presets_js.py --check
6066

6167
SOURCES += src/algorithms.c src/amy.c src/envelope.c src/examples.c src/parse.c \
6268
src/filters.c src/oscillators.c src/pcm.c src/interp_partials.c src/custom.c \
@@ -133,20 +139,15 @@ build/amy.js: $(TARGET) build/drums_bin.c
133139
mkdir -p build
134140
emcc $(SOURCES) build/drums_bin.c $(CFLAGS) -DGAMMA9001 $(EMSCRIPTEN_OPTIONS) -O3 -o $@
135141

136-
build/amy_api.generated.js: scripts/gen_amy_js_api.py amy/__init__.py src/patches.h
137-
mkdir -p build
138-
$(PYTHON) scripts/gen_amy_js_api.py
139-
140-
build/patches.generated.js: scripts/gen_patches_js.py src/patches.h
141-
mkdir -p build
142-
$(PYTHON) scripts/gen_patches_js.py
143-
144-
web: build/amy.js build/amy_api.generated.js build/patches.generated.js
142+
# The JS API / patches / pcm-preset files are committed outputs in src/,
143+
# regenerated by `make c-api` and verified fresh in CI by `make check-c-api`
144+
# (they used to be built into build/ here, which let downstream copies drift).
145+
web: build/amy.js
145146

146147
# emscripten (>=4.x) inlines the AudioWorklet/WasmWorker glue into amy.js, so it
147148
# no longer emits separate amy.aw.js / amy.ww.js files (older builds did).
148149
deploy-web: web
149-
cat build/amy.js src/amy_connector.js build/amy_api.generated.js src/amy_c_api.generated.js > docs/amy.js
150+
cat build/amy.js src/amy_connector.js src/amy_api.generated.js src/amy_c_api.generated.js > docs/amy.js
150151
cp build/amy.wasm docs/
151152

152153
# Regenerate the Godot GDScript send() kwarg map from amy/__init__.py (the

scripts/gen_amy_js_api.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
plus all constants from amy/constants.py, and generates a self-contained
66
JS module with amy_message(), amy_send(), and an AMY constants object.
77
"""
8+
import argparse
89
import ast
910
import re
11+
import sys
1012
import textwrap
1113
from pathlib import Path
1214

1315
ROOT = Path(__file__).resolve().parents[1]
1416
AMY_INIT = ROOT / "amy" / "__init__.py"
1517
AMY_CONSTANTS = ROOT / "amy" / "constants.py"
16-
OUTPUT = ROOT / "build" / "amy_api.generated.js"
18+
OUTPUT = ROOT / "src" / "amy_api.generated.js"
1719

1820

1921
def extract_kw_map_list(source: str):
@@ -251,11 +253,22 @@ def generate_js(kw_map_list, coef_fields, constants=None):
251253

252254

253255
def main():
256+
ap = argparse.ArgumentParser()
257+
ap.add_argument('--check', action='store_true',
258+
help='verify the committed output is up to date')
259+
opts = ap.parse_args()
260+
254261
source = AMY_INIT.read_text()
255262
kw_map_list = extract_kw_map_list(source)
256263
coef_fields = extract_coef_fields(source)
257264
constants = extract_constants(AMY_CONSTANTS.read_text())
258265
js = generate_js(kw_map_list, coef_fields, constants)
266+
if opts.check:
267+
if not OUTPUT.exists() or OUTPUT.read_text() != js:
268+
print(f"stale {OUTPUT.relative_to(ROOT)} (run: python3 scripts/gen_amy_js_api.py)")
269+
sys.exit(1)
270+
print(f"{OUTPUT.relative_to(ROOT)} is up to date")
271+
return
259272
OUTPUT.write_text(js)
260273
print(f"Generated {OUTPUT.relative_to(ROOT)} ({len(kw_map_list)} parameters, {len(coef_fields)} coef fields, {len(constants)} constants)")
261274

scripts/gen_patches_js.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
Parses patch names and patch command strings from the C header and emits
55
a self-contained JS module that exposes them to the AMYboard web editor.
66
"""
7+
import argparse
78
import re
9+
import sys
810
from pathlib import Path
911

1012
ROOT = Path(__file__).resolve().parents[1]
1113
PATCHES_H = ROOT / "src" / "patches.h"
12-
OUTPUT = ROOT / "build" / "patches.generated.js"
14+
OUTPUT = ROOT / "src" / "patches.generated.js"
1315

1416
# Regex to match each entry in patch_commands[]:
1517
# /* 0: Juno A11 Brass Set 1 */ "v1w4a1,...Z",
@@ -105,10 +107,21 @@ def generate_js(patches):
105107

106108

107109
def main():
110+
ap = argparse.ArgumentParser()
111+
ap.add_argument('--check', action='store_true',
112+
help='verify the committed output is up to date')
113+
opts = ap.parse_args()
114+
108115
source = PATCHES_H.read_text()
109116
patches = parse_patches(source)
110-
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
111-
OUTPUT.write_text(generate_js(patches))
117+
js = generate_js(patches)
118+
if opts.check:
119+
if not OUTPUT.exists() or OUTPUT.read_text() != js:
120+
print(f"stale {OUTPUT.relative_to(ROOT)} (run: python3 scripts/gen_patches_js.py)")
121+
sys.exit(1)
122+
print(f"{OUTPUT.relative_to(ROOT)} is up to date")
123+
return
124+
OUTPUT.write_text(js)
112125
print(f"Generated {OUTPUT.relative_to(ROOT)} ({len(patches)} patches)")
113126

114127

scripts/gen_pcm_presets_js.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
"""Generate pcm_presets.generated.js from src/pcm_tiny.h.
3+
4+
Parses the pcm_map[] table (names from the trailing comments, wavetable
5+
entries marked "WT") and emits window.AMY_WAVE_PRESETS, keyed by wave type
6+
(PCM / WAVETABLE from amy/constants.py), for the AMYboard web editor's
7+
preset knob UI.
8+
"""
9+
import argparse
10+
import json
11+
import re
12+
import sys
13+
from pathlib import Path
14+
15+
ROOT = Path(__file__).resolve().parents[1]
16+
PCM_TINY_H = ROOT / "src" / "pcm_tiny.h"
17+
AMY_CONSTANTS = ROOT / "amy" / "constants.py"
18+
OUTPUT = ROOT / "src" / "pcm_presets.generated.js"
19+
20+
# One pcm_map[] row: /* [11] WT */ {51053, 16384, 0, 16384, 69}, /* 111.WAV */
21+
ENTRY_RE = re.compile(
22+
r'/\*\s*\[(\d+)\]\s*(\S+)\s*\*/\s*\{[^}]*\}\s*,\s*/\*\s*(.+?)\s*\*/'
23+
)
24+
25+
26+
def wave_constant(name: str) -> int:
27+
m = re.search(r'^%s\s*=\s*(\d+)' % name, AMY_CONSTANTS.read_text(), re.M)
28+
if not m:
29+
raise RuntimeError(f"Could not find {name} in amy/constants.py")
30+
return int(m.group(1))
31+
32+
33+
def parse_presets(source: str):
34+
"""Return {wave_number: [{name, value, wave}, ...]} from pcm_tiny.h."""
35+
pcm_wave = wave_constant("PCM")
36+
wavetable_wave = wave_constant("WAVETABLE")
37+
presets = {}
38+
for m in ENTRY_RE.finditer(source):
39+
index, marker, name = int(m.group(1)), m.group(2), m.group(3)
40+
wave = wavetable_wave if marker == "WT" else pcm_wave
41+
presets.setdefault(wave, []).append(
42+
{"name": name, "value": index, "wave": wave})
43+
if not presets:
44+
raise RuntimeError(f"No pcm_map entries found in {PCM_TINY_H}")
45+
return {str(k): v for k, v in presets.items()}
46+
47+
48+
def generate_js(presets):
49+
body = json.dumps(presets, indent=2, sort_keys=True)
50+
return (
51+
"// AUTO-GENERATED by scripts/gen_pcm_presets_js.py — do not edit by hand.\n"
52+
"// Source: src/pcm_tiny.h\n"
53+
f"window.AMY_WAVE_PRESETS = {body};\n"
54+
)
55+
56+
57+
def main():
58+
ap = argparse.ArgumentParser()
59+
ap.add_argument('--check', action='store_true',
60+
help='verify the committed output is up to date')
61+
opts = ap.parse_args()
62+
63+
presets = parse_presets(PCM_TINY_H.read_text())
64+
js = generate_js(presets)
65+
if opts.check:
66+
if not OUTPUT.exists() or OUTPUT.read_text() != js:
67+
print(f"stale {OUTPUT.relative_to(ROOT)} (run: python3 scripts/gen_pcm_presets_js.py)")
68+
sys.exit(1)
69+
print(f"{OUTPUT.relative_to(ROOT)} is up to date")
70+
return
71+
OUTPUT.write_text(js)
72+
n = sum(len(v) for v in presets.values())
73+
print(f"Generated {OUTPUT.relative_to(ROOT)} ({n} presets, {len(presets)} wave types)")
74+
75+
76+
if __name__ == "__main__":
77+
main()

0 commit comments

Comments
 (0)