From 5e24087e0dda53cbf6902599f773b630db059ae7 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 15 Apr 2026 09:53:06 -0700 Subject: [PATCH] speaker: interactive PCM probe piece + system.audio API + onboard probing docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- SCORE.md | 66 +++++++ fedac/native/docker-build.sh | 2 +- fedac/native/src/js-bindings.c | 168 ++++++++++++++++++ .../aesthetic.computer/disks/speaker.mjs | 166 +++++++++++++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 system/public/aesthetic.computer/disks/speaker.mjs diff --git a/SCORE.md b/SCORE.md index 3b7b37d62b..3fd72d7cb3 100644 --- a/SCORE.md +++ b/SCORE.md @@ -361,3 +361,69 @@ Different agents perform from this score in different ways. - **AestheticAnts** — Automated AI colony that makes small, confident changes. See `ants/` for colony rules and implementation. - **Human contributors** — Welcome in `chat`. Read the score, pick a task, follow signal. - **@jeffrey (the queen)** — Writes and maintains this score. +- **Claude on ac-native** — A Claude Code CLI binary is bundled into the + initramfs at `/bin/claude`, pre-authenticated with @jeffrey's + credentials. When running inside ac-native it has the same repo view + as everywhere else, plus direct access to the real hardware. + +--- + +## Hardware Probing on ac-native + +When the onboard Claude is debugging a device (missing speakers, WiFi, +trackpad, etc.) the below surfaces are always available without needing +a separate diagnostic build. Probe from the prompt, a piece, or a shell +— whatever fits the task. + +### Boot-time logs (USB pulled out then inspected on a host) + +- `/mnt/pre-launch.log` — init script's full probe dump: GPU nodes, + block devices, net ifaces, rfkill state, PCI devices with bound + drivers, ACPI codecs, sound PCM names, GPIO chips + descriptor + consumers, ASoC debugfs, MAX98357A amp state, and a subset of the + running kernel config via `/proc/config.gz` (CONFIG_IKCONFIG=y). +- `/mnt/kmsg.log` — persistent `cat /dev/kmsg` for the entire boot + session. Grep for any driver probe, dev_dbg, or warning. +- `/mnt/ac-native-stderr.log` — ac-native's ALSA init trace + (device opened, mixer enumeration, volume sets, XRUN count). +- `/mnt/flash-last.log` — flash-thread telemetry from the last OTA. + +### Runtime probing (piece APIs) + +- `system.audio.listPcms()` → array of `{device, card, num, id, name}`. + Skips HDMI. `active: true` marks the PCM ac-native's main audio + thread opened. +- `system.audio.testPcm(device, freq_hz, duration_ms, volume)` — plays + a short sine wave on an arbitrary ALSA device in a detached thread. + Used by the `speaker` piece to find which PCM actually drives the + onboard speakers vs headphone jack vs HDMI. Piece source: + [speaker.mjs](system/public/aesthetic.computer/disks/speaker.mjs). +- `system.firmware.{available, board, biosVersion, install}` — + machines running MrChromebox coreboot can reflash from os.mjs's + firmware panel (gated behind `/dev/mtd0` + `bios_vendor=coreboot`). + +### Live sysfs hotspots + +- `/proc/asound/card0/pcm*p/info` — per-PCM id/name. On SOF: + pcm0p is usually "Speakers", pcm1p "Headset", pcm2-4p are HDMI. +- `/sys/bus/gpio/devices/` + `/sys/kernel/debug/gpio` — list every + GPIO chip + which consumer holds which pin. `gpio-* | sdmode` shows + the MAX98357A speaker-enable line. +- `/sys/bus/pci/devices//driver` — symlinks to each PCI device's + driver. `drv=NONE` means the driver isn't bound (either missing + config, missing firmware file, or missing ACPI device match). +- `/sys/bus/acpi/devices/:NN/physical_node/driver` — same but + for ACPI-enumerated devices (audio codecs, embedded controller, + pinctrl, etc). +- `/proc/config.gz` — `zcat /proc/config.gz | grep CONFIG_FOO=` tells + you definitively whether a config survived `make olddefconfig`. + +### Build-time canaries + +`docker-build.sh` verifies six critical driver symbols (`jsl_pinctrl_ +acpi_match`, `max98357a_sdmode_event`, `rt5682_i2c_probe`, +`i2c_dw_prepare_clk`, …) are linked into vmlinux via `nm`. When a new +config toggles on, a hash sentinel wipes the matching subsystem's +object files so kbuild actually rebuilds them. If your build fails at +"BUILD SANITY: critical driver symbols missing", wipe the persistent +docker volume: `docker volume rm ac-os-kbuild`. diff --git a/fedac/native/docker-build.sh b/fedac/native/docker-build.sh index 9a540ba6be..304f06355c 100644 --- a/fedac/native/docker-build.sh +++ b/fedac/native/docker-build.sh @@ -561,7 +561,7 @@ done # runtime. If you add a piece that uses browser-only APIs, the runtime # will throw — don't "port" the piece, expose the missing API in # js-bindings.c instead (so web and native stay in parity). -for webpiece in clock.mjs arena.mjs; do +for webpiece in clock.mjs arena.mjs speaker.mjs; do SRC_PIECE="$SRC/system/public/aesthetic.computer/disks/$webpiece" if [ -f "$SRC_PIECE" ]; then cp "$SRC_PIECE" "$IROOT/pieces/$webpiece" diff --git a/fedac/native/src/js-bindings.c b/fedac/native/src/js-bindings.c index 9dabea1286..2de9a4a445 100644 --- a/fedac/native/src/js-bindings.c +++ b/fedac/native/src/js-bindings.c @@ -4466,6 +4466,153 @@ static void *flash_thread_fn(void *arg) { return NULL; } +// ───────────────────────────────────────────────────────────────── +// Audio diagnostic — list PCMs + play a short test tone on an arbitrary +// device. Used by the speaker.mjs piece to figure out which ALSA PCM +// actually produces sound on a given machine (vs wiring-dependent guesses +// like hw:0,0). The tone plays in a detached thread so the JS caller +// returns immediately; the piece UI stays responsive. +// ───────────────────────────────────────────────────────────────── +#include + +static JSValue js_audio_list_pcms(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + (void)this_val; (void)argc; (void)argv; + JSValue arr = JS_NewArray(ctx); + int idx = 0; + for (int c = 0; c < 4; c++) { + for (int d = 0; d < 8; d++) { + char path[80]; + snprintf(path, sizeof(path), + "/proc/asound/card%d/pcm%dp/info", c, d); + FILE *fp = fopen(path, "r"); + if (!fp) continue; + char line[256], id_str[96] = "", name_str[96] = ""; + while (fgets(line, sizeof(line), fp)) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + if (!strncmp(line, "id: ", 4)) + snprintf(id_str, sizeof(id_str), "%s", line + 4); + else if (!strncmp(line, "name: ", 6)) + snprintf(name_str, sizeof(name_str), "%s", line + 6); + } + fclose(fp); + char dev[16]; + snprintf(dev, sizeof(dev), "hw:%d,%d", c, d); + JSValue obj = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, obj, "device", JS_NewString(ctx, dev)); + JS_SetPropertyStr(ctx, obj, "card", JS_NewInt32(ctx, c)); + JS_SetPropertyStr(ctx, obj, "num", JS_NewInt32(ctx, d)); + JS_SetPropertyStr(ctx, obj, "id", JS_NewString(ctx, id_str)); + JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, name_str)); + /* Tag the PCM that ac-native's main audio thread picked so the + * UI can highlight it. */ + if (current_rt && current_rt->audio && + strcmp(current_rt->audio->audio_device, dev) == 0) + JS_SetPropertyStr(ctx, obj, "active", JS_NewBool(ctx, 1)); + JS_SetPropertyUint32(ctx, arr, idx++, obj); + } + } + return arr; +} + +struct audio_probe_args { + char device[32]; + int freq_hz; + int duration_ms; + float volume; +}; + +static void *audio_probe_thread(void *arg) { + struct audio_probe_args *a = (struct audio_probe_args *)arg; + snd_pcm_t *pcm = NULL; + int err = snd_pcm_open(&pcm, a->device, SND_PCM_STREAM_PLAYBACK, 0); + if (err < 0) { + ac_log("[audio-probe] open %s failed: %s", + a->device, snd_strerror(err)); + free(a); + return NULL; + } + unsigned int rate = 48000; + snd_pcm_uframes_t period = 480, buffer = 1920; + err = snd_pcm_set_params(pcm, + SND_PCM_FORMAT_S16_LE, + SND_PCM_ACCESS_RW_INTERLEAVED, + 2, /* stereo */ + rate, + 1, /* soft_resample */ + 100 * 1000); /* 100ms latency */ + if (err < 0) { + ac_log("[audio-probe] set_params %s failed: %s", + a->device, snd_strerror(err)); + snd_pcm_close(pcm); + free(a); + return NULL; + } + /* Synthesize + play a sine wave of a->freq_hz at a->volume for + * a->duration_ms milliseconds. Stereo S16 LE. */ + int total_frames = (long long)rate * a->duration_ms / 1000; + int16_t *buf = (int16_t *)malloc(period * 2 * sizeof(int16_t)); + if (!buf) { snd_pcm_close(pcm); free(a); return NULL; } + double phase = 0.0; + double step = 2.0 * 3.14159265358979 * (double)a->freq_hz / (double)rate; + int32_t amp = (int32_t)(a->volume * 32760.0); + if (amp < 0) amp = 0; if (amp > 32760) amp = 32760; + int written = 0; + ac_log("[audio-probe] %s %dHz %dms vol=%.2f", a->device, a->freq_hz, + a->duration_ms, a->volume); + while (written < total_frames) { + int chunk = total_frames - written; + if (chunk > (int)period) chunk = (int)period; + for (int i = 0; i < chunk; i++) { + int16_t s = (int16_t)(sin(phase) * amp); + buf[i * 2] = s; + buf[i * 2 + 1] = s; + phase += step; + if (phase > 6.283185307179586) phase -= 6.283185307179586; + } + snd_pcm_sframes_t w = snd_pcm_writei(pcm, buf, chunk); + if (w < 0) w = snd_pcm_recover(pcm, w, 1); + if (w < 0) { ac_log("[audio-probe] writei failed: %s", + snd_strerror((int)w)); break; } + written += (int)w; + } + snd_pcm_drain(pcm); + snd_pcm_close(pcm); + free(buf); + ac_log("[audio-probe] %s done (%d frames)", a->device, written); + free(a); + return NULL; +} + +static JSValue js_audio_test_pcm(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + (void)this_val; + if (argc < 1) return JS_FALSE; + struct audio_probe_args *a = (struct audio_probe_args *)calloc(1, sizeof(*a)); + if (!a) return JS_FALSE; + const char *dev = JS_ToCString(ctx, argv[0]); + if (!dev) { free(a); return JS_FALSE; } + strncpy(a->device, dev, sizeof(a->device) - 1); + JS_FreeCString(ctx, dev); + a->freq_hz = 440; + a->duration_ms = 500; + a->volume = 0.3f; + if (argc >= 2) JS_ToInt32(ctx, &a->freq_hz, argv[1]); + if (argc >= 3) JS_ToInt32(ctx, &a->duration_ms, argv[2]); + if (argc >= 4) { + double vol; JS_ToFloat64(ctx, &vol, argv[3]); + a->volume = (float)vol; + } + pthread_t th; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + int pr = pthread_create(&th, &attr, audio_probe_thread, a); + pthread_attr_destroy(&attr); + if (pr != 0) { free(a); return JS_FALSE; } + return JS_TRUE; +} + // ───────────────────────────────────────────────────────────────── // Firmware update thread — runs /bin/ac-firmware-install (bundled copy of // system/public/install-firmware.sh) and streams its stdout into @@ -6179,6 +6326,27 @@ static JSValue build_system_obj(JSContext *ctx) { JS_SetPropertyStr(ctx, sys, "firmware", firmware); } + // Audio diagnostic API — speaker.mjs piece uses these to probe which + // ALSA PCM actually produces sound on this machine. listPcms scans + // /proc/asound; testPcm plays a short sine tone on the named device + // in a detached thread. Both are read-only — no state change to the + // main audio thread. + { + JSValue audio = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, audio, "listPcms", + JS_NewCFunction(ctx, js_audio_list_pcms, "listPcms", 0)); + JS_SetPropertyStr(ctx, audio, "testPcm", + JS_NewCFunction(ctx, js_audio_test_pcm, "testPcm", 4)); + /* Report which device ac-native's main audio thread is currently + * using — helpful context when the user is probing alternatives. */ + if (current_rt && current_rt->audio && + current_rt->audio->audio_device[0]) { + JS_SetPropertyStr(ctx, audio, "activeDevice", + JS_NewString(ctx, current_rt->audio->audio_device)); + } + JS_SetPropertyStr(ctx, sys, "audio", audio); + } + // User config (handle, piece — read from /mnt/config.json at boot) { JSValue config = JS_NewObject(ctx); diff --git a/system/public/aesthetic.computer/disks/speaker.mjs b/system/public/aesthetic.computer/disks/speaker.mjs new file mode 100644 index 0000000000..f5810cacd7 --- /dev/null +++ b/system/public/aesthetic.computer/disks/speaker.mjs @@ -0,0 +1,166 @@ +// speaker.mjs — interactive ALSA playback device probe. +// +// Each row of the list is one /proc/asound playback PCM the kernel +// advertised. Select with ↑/↓, press space or enter to play a 440 Hz tone +// on the selected device; if something comes out of a speaker or +// headphone, that's the PCM the rest of the system should be using. +// Especially useful on SOF/Chromebook topologies where pcm0 = Speakers vs +// pcm1 = Headset differs per board. +// +// Works on ac-native via `system.audio.listPcms()` / `system.audio.testPcm()`. +// On the web runtime those bindings don't exist — we fall back to a stub +// list so the piece still paints and the user sees it's a native-only +// diagnostic. + +let pcms = []; +let selected = 0; +let lastTone = { device: "", freq: 0, ts: 0 }; +let activeDevice = ""; +let freqs = [200, 440, 880, 1760]; +let freqIdx = 1; // 440 Hz by default +let volumes = [0.1, 0.3, 0.5, 0.8]; +let volIdx = 1; // 30% +let durationsMs = [200, 500, 1000, 2000]; +let durIdx = 1; // 500 ms + +function boot({ system }) { + refreshPcms(system); + activeDevice = system?.audio?.activeDevice || ""; + if (activeDevice) { + const match = pcms.findIndex(p => p.device === activeDevice); + if (match >= 0) selected = match; + } +} + +function refreshPcms(system) { + const raw = system?.audio?.listPcms?.(); + if (Array.isArray(raw)) { + pcms = raw; + } else { + // Web fallback: show a single stub so the piece paints something + // informative when the native API isn't present. + pcms = [{ + device: "(web stub)", + card: 0, num: 0, + id: "native-only", + name: "system.audio not available on web", + }]; + } +} + +function act({ event: e, system, sound }) { + if (!e.is("keyboard:down")) return; + + if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) { + system?.jump?.("prompt"); + return; + } + if (e.is("keyboard:down:arrowdown") || e.is("keyboard:down:j")) { + if (pcms.length) selected = (selected + 1) % pcms.length; + return; + } + if (e.is("keyboard:down:arrowup") || e.is("keyboard:down:k")) { + if (pcms.length) selected = (selected - 1 + pcms.length) % pcms.length; + return; + } + if (e.is("keyboard:down:r")) { + refreshPcms(system); + return; + } + + // Frequency cycle: f = next, F / shift+f = prev + if (e.is("keyboard:down:f")) { + freqIdx = (freqIdx + 1) % freqs.length; + return; + } + // Volume cycle: v = next + if (e.is("keyboard:down:v")) { + volIdx = (volIdx + 1) % volumes.length; + return; + } + // Duration cycle: d = next + if (e.is("keyboard:down:d")) { + durIdx = (durIdx + 1) % durationsMs.length; + return; + } + + if (e.is("keyboard:down:space") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) { + const p = pcms[selected]; + if (!p || !p.device || p.device === "(web stub)") return; + const freq = freqs[freqIdx]; + const dur = durationsMs[durIdx]; + const vol = volumes[volIdx]; + const ok = system?.audio?.testPcm?.(p.device, freq, dur, vol); + lastTone = { device: p.device, freq, ts: Date.now(), ok }; + // Gentle feedback chirp via the main audio path so the user knows the + // keypress registered even if this particular PCM is silent. + sound?.synth?.({ type: "sine", tone: 660, duration: 0.04, + volume: 0.08, attack: 0.002, decay: 0.03 }); + return; + } +} + +function paint({ wipe, ink, box, write, screen }) { + const w = screen.width, h = screen.height; + const pad = 10; + const font = "font_1"; + + wipe(10, 10, 20); + ink(255, 255, 180); + write("speaker probe", { x: pad, y: 10, size: 2, font: "matrix" }); + + // Top summary + ink(140, 160, 180); + write(`active: ${activeDevice || "none"}`, + { x: pad, y: 34, size: 1, font }); + + ink(200, 200, 100); + write(`tone: ${freqs[freqIdx]} Hz vol: ${Math.round(volumes[volIdx] * 100)}% dur: ${durationsMs[durIdx]} ms`, + { x: pad, y: 48, size: 1, font }); + + // PCM list + let y = 70; + ink(80, 180, 200); + write("playback PCMs:", { x: pad, y, size: 1, font }); + y += 14; + + const rowH = 18; + const maxRows = Math.min(pcms.length, Math.floor((h - y - 60) / rowH)); + for (let i = 0; i < maxRows; i++) { + const p = pcms[i]; + const isSel = i === selected; + const isActive = p.device === activeDevice; + if (isSel) { + ink(30, 50, 80); + box(pad - 2, y - 2, w - pad * 2 + 4, rowH - 2, true); + } + ink(isSel ? 255 : 180, isSel ? 255 : 180, isSel ? 255 : 180); + const marker = isActive ? "●" : " "; + write(`${marker} ${p.device}`, { x: pad, y, size: 1, font }); + ink(140, 180, 150); + const idShort = (p.id || "").slice(0, 32); + write(idShort, { x: pad + 78, y, size: 1, font }); + y += rowH; + } + + // Last tone status + if (lastTone.device) { + const sinceMs = Date.now() - lastTone.ts; + if (sinceMs < 3000) { + ink(100, 220, 100); + write(`▶ ${lastTone.device} @ ${lastTone.freq} Hz`, + { x: pad, y: h - 50, size: 1, font }); + } else { + ink(120, 120, 140); + write(`last: ${lastTone.device} @ ${lastTone.freq} Hz`, + { x: pad, y: h - 50, size: 1, font }); + } + } + + // Key hints + ink(120, 140, 120); + write("↑↓ select space play f freq v vol d dur r refresh esc back", + { x: pad, y: h - 12, size: 1, font }); +} + +export { boot, paint, act }; -- 2.51.2