Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

speaker: interactive PCM probe piece + system.audio API + onboard probing docs

When ac-native's main audio path is silent the fastest way to figure
out which ALSA device actually drives the speakers is to play a tone
on every playback PCM in turn and listen for which one comes out.
speaker.mjs makes that a two-key workflow:

↑↓ — navigate /proc/asound-enumerated PCMs (HDMI hidden)
space — play a 440Hz sine for 500ms on the highlighted device
f / v / d — cycle frequency / volume / duration
r — re-enumerate PCMs (e.g. after hot-plugging a USB DAC)
esc — back to prompt

Two new native APIs in js-bindings.c:

system.audio.listPcms()
Scans card0..card3, pcm0p..pcm7p, returns [{device,card,num,id,name}]
with `active: true` marking whichever PCM the main audio thread
currently owns (so the UI can show where the rest of the system is
pointed).

system.audio.testPcm(device, freq_hz, duration_ms, volume)
Opens the given hw:N,M, set_params to stereo S16LE @ 48kHz /
~100ms latency, writes a sine wave, drains + closes. Runs in a
detached pthread so the calling JS frame stays responsive. Default
args: 440Hz / 500ms / 0.3.

Both are read-only — they never touch the main audio thread's PCM
handle, so a user can probe while notepat is playing without
glitching.

SCORE.md: new "Hardware Probing on ac-native" section telling the
onboard Claude instance (the CLI binary bundled in the initramfs at
/bin/claude) where to look when debugging hardware issues — boot-time
logs, runtime piece APIs, sysfs hotspots, build-time canaries. The
same Claude that's been helping offline through this whole audio saga
now has the playbook baked in.

docker-build.sh: bundle speaker.mjs alongside clock and arena.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+401 -1
+66
SCORE.md
··· 361 361 - **AestheticAnts** — Automated AI colony that makes small, confident changes. See `ants/` for colony rules and implementation. 362 362 - **Human contributors** — Welcome in `chat`. Read the score, pick a task, follow signal. 363 363 - **@jeffrey (the queen)** — Writes and maintains this score. 364 + - **Claude on ac-native** — A Claude Code CLI binary is bundled into the 365 + initramfs at `/bin/claude`, pre-authenticated with @jeffrey's 366 + credentials. When running inside ac-native it has the same repo view 367 + as everywhere else, plus direct access to the real hardware. 368 + 369 + --- 370 + 371 + ## Hardware Probing on ac-native 372 + 373 + When the onboard Claude is debugging a device (missing speakers, WiFi, 374 + trackpad, etc.) the below surfaces are always available without needing 375 + a separate diagnostic build. Probe from the prompt, a piece, or a shell 376 + — whatever fits the task. 377 + 378 + ### Boot-time logs (USB pulled out then inspected on a host) 379 + 380 + - `/mnt/pre-launch.log` — init script's full probe dump: GPU nodes, 381 + block devices, net ifaces, rfkill state, PCI devices with bound 382 + drivers, ACPI codecs, sound PCM names, GPIO chips + descriptor 383 + consumers, ASoC debugfs, MAX98357A amp state, and a subset of the 384 + running kernel config via `/proc/config.gz` (CONFIG_IKCONFIG=y). 385 + - `/mnt/kmsg.log` — persistent `cat /dev/kmsg` for the entire boot 386 + session. Grep for any driver probe, dev_dbg, or warning. 387 + - `/mnt/ac-native-stderr.log` — ac-native's ALSA init trace 388 + (device opened, mixer enumeration, volume sets, XRUN count). 389 + - `/mnt/flash-last.log` — flash-thread telemetry from the last OTA. 390 + 391 + ### Runtime probing (piece APIs) 392 + 393 + - `system.audio.listPcms()` → array of `{device, card, num, id, name}`. 394 + Skips HDMI. `active: true` marks the PCM ac-native's main audio 395 + thread opened. 396 + - `system.audio.testPcm(device, freq_hz, duration_ms, volume)` — plays 397 + a short sine wave on an arbitrary ALSA device in a detached thread. 398 + Used by the `speaker` piece to find which PCM actually drives the 399 + onboard speakers vs headphone jack vs HDMI. Piece source: 400 + [speaker.mjs](system/public/aesthetic.computer/disks/speaker.mjs). 401 + - `system.firmware.{available, board, biosVersion, install}` — 402 + machines running MrChromebox coreboot can reflash from os.mjs's 403 + firmware panel (gated behind `/dev/mtd0` + `bios_vendor=coreboot`). 404 + 405 + ### Live sysfs hotspots 406 + 407 + - `/proc/asound/card0/pcm*p/info` — per-PCM id/name. On SOF: 408 + pcm0p is usually "Speakers", pcm1p "Headset", pcm2-4p are HDMI. 409 + - `/sys/bus/gpio/devices/` + `/sys/kernel/debug/gpio` — list every 410 + GPIO chip + which consumer holds which pin. `gpio-* | sdmode` shows 411 + the MAX98357A speaker-enable line. 412 + - `/sys/bus/pci/devices/<BDF>/driver` — symlinks to each PCI device's 413 + driver. `drv=NONE` means the driver isn't bound (either missing 414 + config, missing firmware file, or missing ACPI device match). 415 + - `/sys/bus/acpi/devices/<HID>:NN/physical_node/driver` — same but 416 + for ACPI-enumerated devices (audio codecs, embedded controller, 417 + pinctrl, etc). 418 + - `/proc/config.gz` — `zcat /proc/config.gz | grep CONFIG_FOO=` tells 419 + you definitively whether a config survived `make olddefconfig`. 420 + 421 + ### Build-time canaries 422 + 423 + `docker-build.sh` verifies six critical driver symbols (`jsl_pinctrl_ 424 + acpi_match`, `max98357a_sdmode_event`, `rt5682_i2c_probe`, 425 + `i2c_dw_prepare_clk`, …) are linked into vmlinux via `nm`. When a new 426 + config toggles on, a hash sentinel wipes the matching subsystem's 427 + object files so kbuild actually rebuilds them. If your build fails at 428 + "BUILD SANITY: critical driver symbols missing", wipe the persistent 429 + docker volume: `docker volume rm ac-os-kbuild`.
+1 -1
fedac/native/docker-build.sh
··· 561 561 # runtime. If you add a piece that uses browser-only APIs, the runtime 562 562 # will throw — don't "port" the piece, expose the missing API in 563 563 # js-bindings.c instead (so web and native stay in parity). 564 - for webpiece in clock.mjs arena.mjs; do 564 + for webpiece in clock.mjs arena.mjs speaker.mjs; do 565 565 SRC_PIECE="$SRC/system/public/aesthetic.computer/disks/$webpiece" 566 566 if [ -f "$SRC_PIECE" ]; then 567 567 cp "$SRC_PIECE" "$IROOT/pieces/$webpiece"
+168
fedac/native/src/js-bindings.c
··· 4467 4467 } 4468 4468 4469 4469 // ───────────────────────────────────────────────────────────────── 4470 + // Audio diagnostic — list PCMs + play a short test tone on an arbitrary 4471 + // device. Used by the speaker.mjs piece to figure out which ALSA PCM 4472 + // actually produces sound on a given machine (vs wiring-dependent guesses 4473 + // like hw:0,0). The tone plays in a detached thread so the JS caller 4474 + // returns immediately; the piece UI stays responsive. 4475 + // ───────────────────────────────────────────────────────────────── 4476 + #include <alsa/asoundlib.h> 4477 + 4478 + static JSValue js_audio_list_pcms(JSContext *ctx, JSValueConst this_val, 4479 + int argc, JSValueConst *argv) { 4480 + (void)this_val; (void)argc; (void)argv; 4481 + JSValue arr = JS_NewArray(ctx); 4482 + int idx = 0; 4483 + for (int c = 0; c < 4; c++) { 4484 + for (int d = 0; d < 8; d++) { 4485 + char path[80]; 4486 + snprintf(path, sizeof(path), 4487 + "/proc/asound/card%d/pcm%dp/info", c, d); 4488 + FILE *fp = fopen(path, "r"); 4489 + if (!fp) continue; 4490 + char line[256], id_str[96] = "", name_str[96] = ""; 4491 + while (fgets(line, sizeof(line), fp)) { 4492 + char *nl = strchr(line, '\n'); if (nl) *nl = 0; 4493 + if (!strncmp(line, "id: ", 4)) 4494 + snprintf(id_str, sizeof(id_str), "%s", line + 4); 4495 + else if (!strncmp(line, "name: ", 6)) 4496 + snprintf(name_str, sizeof(name_str), "%s", line + 6); 4497 + } 4498 + fclose(fp); 4499 + char dev[16]; 4500 + snprintf(dev, sizeof(dev), "hw:%d,%d", c, d); 4501 + JSValue obj = JS_NewObject(ctx); 4502 + JS_SetPropertyStr(ctx, obj, "device", JS_NewString(ctx, dev)); 4503 + JS_SetPropertyStr(ctx, obj, "card", JS_NewInt32(ctx, c)); 4504 + JS_SetPropertyStr(ctx, obj, "num", JS_NewInt32(ctx, d)); 4505 + JS_SetPropertyStr(ctx, obj, "id", JS_NewString(ctx, id_str)); 4506 + JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, name_str)); 4507 + /* Tag the PCM that ac-native's main audio thread picked so the 4508 + * UI can highlight it. */ 4509 + if (current_rt && current_rt->audio && 4510 + strcmp(current_rt->audio->audio_device, dev) == 0) 4511 + JS_SetPropertyStr(ctx, obj, "active", JS_NewBool(ctx, 1)); 4512 + JS_SetPropertyUint32(ctx, arr, idx++, obj); 4513 + } 4514 + } 4515 + return arr; 4516 + } 4517 + 4518 + struct audio_probe_args { 4519 + char device[32]; 4520 + int freq_hz; 4521 + int duration_ms; 4522 + float volume; 4523 + }; 4524 + 4525 + static void *audio_probe_thread(void *arg) { 4526 + struct audio_probe_args *a = (struct audio_probe_args *)arg; 4527 + snd_pcm_t *pcm = NULL; 4528 + int err = snd_pcm_open(&pcm, a->device, SND_PCM_STREAM_PLAYBACK, 0); 4529 + if (err < 0) { 4530 + ac_log("[audio-probe] open %s failed: %s", 4531 + a->device, snd_strerror(err)); 4532 + free(a); 4533 + return NULL; 4534 + } 4535 + unsigned int rate = 48000; 4536 + snd_pcm_uframes_t period = 480, buffer = 1920; 4537 + err = snd_pcm_set_params(pcm, 4538 + SND_PCM_FORMAT_S16_LE, 4539 + SND_PCM_ACCESS_RW_INTERLEAVED, 4540 + 2, /* stereo */ 4541 + rate, 4542 + 1, /* soft_resample */ 4543 + 100 * 1000); /* 100ms latency */ 4544 + if (err < 0) { 4545 + ac_log("[audio-probe] set_params %s failed: %s", 4546 + a->device, snd_strerror(err)); 4547 + snd_pcm_close(pcm); 4548 + free(a); 4549 + return NULL; 4550 + } 4551 + /* Synthesize + play a sine wave of a->freq_hz at a->volume for 4552 + * a->duration_ms milliseconds. Stereo S16 LE. */ 4553 + int total_frames = (long long)rate * a->duration_ms / 1000; 4554 + int16_t *buf = (int16_t *)malloc(period * 2 * sizeof(int16_t)); 4555 + if (!buf) { snd_pcm_close(pcm); free(a); return NULL; } 4556 + double phase = 0.0; 4557 + double step = 2.0 * 3.14159265358979 * (double)a->freq_hz / (double)rate; 4558 + int32_t amp = (int32_t)(a->volume * 32760.0); 4559 + if (amp < 0) amp = 0; if (amp > 32760) amp = 32760; 4560 + int written = 0; 4561 + ac_log("[audio-probe] %s %dHz %dms vol=%.2f", a->device, a->freq_hz, 4562 + a->duration_ms, a->volume); 4563 + while (written < total_frames) { 4564 + int chunk = total_frames - written; 4565 + if (chunk > (int)period) chunk = (int)period; 4566 + for (int i = 0; i < chunk; i++) { 4567 + int16_t s = (int16_t)(sin(phase) * amp); 4568 + buf[i * 2] = s; 4569 + buf[i * 2 + 1] = s; 4570 + phase += step; 4571 + if (phase > 6.283185307179586) phase -= 6.283185307179586; 4572 + } 4573 + snd_pcm_sframes_t w = snd_pcm_writei(pcm, buf, chunk); 4574 + if (w < 0) w = snd_pcm_recover(pcm, w, 1); 4575 + if (w < 0) { ac_log("[audio-probe] writei failed: %s", 4576 + snd_strerror((int)w)); break; } 4577 + written += (int)w; 4578 + } 4579 + snd_pcm_drain(pcm); 4580 + snd_pcm_close(pcm); 4581 + free(buf); 4582 + ac_log("[audio-probe] %s done (%d frames)", a->device, written); 4583 + free(a); 4584 + return NULL; 4585 + } 4586 + 4587 + static JSValue js_audio_test_pcm(JSContext *ctx, JSValueConst this_val, 4588 + int argc, JSValueConst *argv) { 4589 + (void)this_val; 4590 + if (argc < 1) return JS_FALSE; 4591 + struct audio_probe_args *a = (struct audio_probe_args *)calloc(1, sizeof(*a)); 4592 + if (!a) return JS_FALSE; 4593 + const char *dev = JS_ToCString(ctx, argv[0]); 4594 + if (!dev) { free(a); return JS_FALSE; } 4595 + strncpy(a->device, dev, sizeof(a->device) - 1); 4596 + JS_FreeCString(ctx, dev); 4597 + a->freq_hz = 440; 4598 + a->duration_ms = 500; 4599 + a->volume = 0.3f; 4600 + if (argc >= 2) JS_ToInt32(ctx, &a->freq_hz, argv[1]); 4601 + if (argc >= 3) JS_ToInt32(ctx, &a->duration_ms, argv[2]); 4602 + if (argc >= 4) { 4603 + double vol; JS_ToFloat64(ctx, &vol, argv[3]); 4604 + a->volume = (float)vol; 4605 + } 4606 + pthread_t th; 4607 + pthread_attr_t attr; 4608 + pthread_attr_init(&attr); 4609 + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 4610 + int pr = pthread_create(&th, &attr, audio_probe_thread, a); 4611 + pthread_attr_destroy(&attr); 4612 + if (pr != 0) { free(a); return JS_FALSE; } 4613 + return JS_TRUE; 4614 + } 4615 + 4616 + // ───────────────────────────────────────────────────────────────── 4470 4617 // Firmware update thread — runs /bin/ac-firmware-install (bundled copy of 4471 4618 // system/public/install-firmware.sh) and streams its stdout into 4472 4619 // rt->fw_log. The script handles all the dangerous bits (backup via ··· 6177 6324 JS_SetPropertyStr(ctx, firmware, "install", 6178 6325 JS_NewCFunction(ctx, js_firmware_install, "install", 1)); 6179 6326 JS_SetPropertyStr(ctx, sys, "firmware", firmware); 6327 + } 6328 + 6329 + // Audio diagnostic API — speaker.mjs piece uses these to probe which 6330 + // ALSA PCM actually produces sound on this machine. listPcms scans 6331 + // /proc/asound; testPcm plays a short sine tone on the named device 6332 + // in a detached thread. Both are read-only — no state change to the 6333 + // main audio thread. 6334 + { 6335 + JSValue audio = JS_NewObject(ctx); 6336 + JS_SetPropertyStr(ctx, audio, "listPcms", 6337 + JS_NewCFunction(ctx, js_audio_list_pcms, "listPcms", 0)); 6338 + JS_SetPropertyStr(ctx, audio, "testPcm", 6339 + JS_NewCFunction(ctx, js_audio_test_pcm, "testPcm", 4)); 6340 + /* Report which device ac-native's main audio thread is currently 6341 + * using — helpful context when the user is probing alternatives. */ 6342 + if (current_rt && current_rt->audio && 6343 + current_rt->audio->audio_device[0]) { 6344 + JS_SetPropertyStr(ctx, audio, "activeDevice", 6345 + JS_NewString(ctx, current_rt->audio->audio_device)); 6346 + } 6347 + JS_SetPropertyStr(ctx, sys, "audio", audio); 6180 6348 } 6181 6349 6182 6350 // User config (handle, piece — read from /mnt/config.json at boot)
+166
system/public/aesthetic.computer/disks/speaker.mjs
··· 1 + // speaker.mjs — interactive ALSA playback device probe. 2 + // 3 + // Each row of the list is one /proc/asound playback PCM the kernel 4 + // advertised. Select with ↑/↓, press space or enter to play a 440 Hz tone 5 + // on the selected device; if something comes out of a speaker or 6 + // headphone, that's the PCM the rest of the system should be using. 7 + // Especially useful on SOF/Chromebook topologies where pcm0 = Speakers vs 8 + // pcm1 = Headset differs per board. 9 + // 10 + // Works on ac-native via `system.audio.listPcms()` / `system.audio.testPcm()`. 11 + // On the web runtime those bindings don't exist — we fall back to a stub 12 + // list so the piece still paints and the user sees it's a native-only 13 + // diagnostic. 14 + 15 + let pcms = []; 16 + let selected = 0; 17 + let lastTone = { device: "", freq: 0, ts: 0 }; 18 + let activeDevice = ""; 19 + let freqs = [200, 440, 880, 1760]; 20 + let freqIdx = 1; // 440 Hz by default 21 + let volumes = [0.1, 0.3, 0.5, 0.8]; 22 + let volIdx = 1; // 30% 23 + let durationsMs = [200, 500, 1000, 2000]; 24 + let durIdx = 1; // 500 ms 25 + 26 + function boot({ system }) { 27 + refreshPcms(system); 28 + activeDevice = system?.audio?.activeDevice || ""; 29 + if (activeDevice) { 30 + const match = pcms.findIndex(p => p.device === activeDevice); 31 + if (match >= 0) selected = match; 32 + } 33 + } 34 + 35 + function refreshPcms(system) { 36 + const raw = system?.audio?.listPcms?.(); 37 + if (Array.isArray(raw)) { 38 + pcms = raw; 39 + } else { 40 + // Web fallback: show a single stub so the piece paints something 41 + // informative when the native API isn't present. 42 + pcms = [{ 43 + device: "(web stub)", 44 + card: 0, num: 0, 45 + id: "native-only", 46 + name: "system.audio not available on web", 47 + }]; 48 + } 49 + } 50 + 51 + function act({ event: e, system, sound }) { 52 + if (!e.is("keyboard:down")) return; 53 + 54 + if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) { 55 + system?.jump?.("prompt"); 56 + return; 57 + } 58 + if (e.is("keyboard:down:arrowdown") || e.is("keyboard:down:j")) { 59 + if (pcms.length) selected = (selected + 1) % pcms.length; 60 + return; 61 + } 62 + if (e.is("keyboard:down:arrowup") || e.is("keyboard:down:k")) { 63 + if (pcms.length) selected = (selected - 1 + pcms.length) % pcms.length; 64 + return; 65 + } 66 + if (e.is("keyboard:down:r")) { 67 + refreshPcms(system); 68 + return; 69 + } 70 + 71 + // Frequency cycle: f = next, F / shift+f = prev 72 + if (e.is("keyboard:down:f")) { 73 + freqIdx = (freqIdx + 1) % freqs.length; 74 + return; 75 + } 76 + // Volume cycle: v = next 77 + if (e.is("keyboard:down:v")) { 78 + volIdx = (volIdx + 1) % volumes.length; 79 + return; 80 + } 81 + // Duration cycle: d = next 82 + if (e.is("keyboard:down:d")) { 83 + durIdx = (durIdx + 1) % durationsMs.length; 84 + return; 85 + } 86 + 87 + if (e.is("keyboard:down:space") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) { 88 + const p = pcms[selected]; 89 + if (!p || !p.device || p.device === "(web stub)") return; 90 + const freq = freqs[freqIdx]; 91 + const dur = durationsMs[durIdx]; 92 + const vol = volumes[volIdx]; 93 + const ok = system?.audio?.testPcm?.(p.device, freq, dur, vol); 94 + lastTone = { device: p.device, freq, ts: Date.now(), ok }; 95 + // Gentle feedback chirp via the main audio path so the user knows the 96 + // keypress registered even if this particular PCM is silent. 97 + sound?.synth?.({ type: "sine", tone: 660, duration: 0.04, 98 + volume: 0.08, attack: 0.002, decay: 0.03 }); 99 + return; 100 + } 101 + } 102 + 103 + function paint({ wipe, ink, box, write, screen }) { 104 + const w = screen.width, h = screen.height; 105 + const pad = 10; 106 + const font = "font_1"; 107 + 108 + wipe(10, 10, 20); 109 + ink(255, 255, 180); 110 + write("speaker probe", { x: pad, y: 10, size: 2, font: "matrix" }); 111 + 112 + // Top summary 113 + ink(140, 160, 180); 114 + write(`active: ${activeDevice || "none"}`, 115 + { x: pad, y: 34, size: 1, font }); 116 + 117 + ink(200, 200, 100); 118 + write(`tone: ${freqs[freqIdx]} Hz vol: ${Math.round(volumes[volIdx] * 100)}% dur: ${durationsMs[durIdx]} ms`, 119 + { x: pad, y: 48, size: 1, font }); 120 + 121 + // PCM list 122 + let y = 70; 123 + ink(80, 180, 200); 124 + write("playback PCMs:", { x: pad, y, size: 1, font }); 125 + y += 14; 126 + 127 + const rowH = 18; 128 + const maxRows = Math.min(pcms.length, Math.floor((h - y - 60) / rowH)); 129 + for (let i = 0; i < maxRows; i++) { 130 + const p = pcms[i]; 131 + const isSel = i === selected; 132 + const isActive = p.device === activeDevice; 133 + if (isSel) { 134 + ink(30, 50, 80); 135 + box(pad - 2, y - 2, w - pad * 2 + 4, rowH - 2, true); 136 + } 137 + ink(isSel ? 255 : 180, isSel ? 255 : 180, isSel ? 255 : 180); 138 + const marker = isActive ? "●" : " "; 139 + write(`${marker} ${p.device}`, { x: pad, y, size: 1, font }); 140 + ink(140, 180, 150); 141 + const idShort = (p.id || "").slice(0, 32); 142 + write(idShort, { x: pad + 78, y, size: 1, font }); 143 + y += rowH; 144 + } 145 + 146 + // Last tone status 147 + if (lastTone.device) { 148 + const sinceMs = Date.now() - lastTone.ts; 149 + if (sinceMs < 3000) { 150 + ink(100, 220, 100); 151 + write(`▶ ${lastTone.device} @ ${lastTone.freq} Hz`, 152 + { x: pad, y: h - 50, size: 1, font }); 153 + } else { 154 + ink(120, 120, 140); 155 + write(`last: ${lastTone.device} @ ${lastTone.freq} Hz`, 156 + { x: pad, y: h - 50, size: 1, font }); 157 + } 158 + } 159 + 160 + // Key hints 161 + ink(120, 140, 120); 162 + write("↑↓ select space play f freq v vol d dur r refresh esc back", 163 + { x: pad, y: h - 12, size: 1, font }); 164 + } 165 + 166 + export { boot, paint, act };