From 4eba4d99d6bc37466a011b6d95d56718c8cf3a3a Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Tue, 14 Jul 2026 16:26:31 +0000 Subject: [PATCH] kidlisp: give it the pad voices — pluck, bell, sub, flute, hat, voice KidLisp already had the hard half of being an instrument: `melody` is UTC-locked to the same clock authority pads.mjs uses (two people anywhere play in phase), and `1s` / `2s...` blocks ARE a beat grid. What it didn't have was a body — `overtone` is a bare square wave at a fixed volume, so there was no way to say "play that as a bell." Now there is. These are the SAME physical models the JS pads use, imported from lib/pads.mjs rather than reimplemented — two definitions of what a bell sounds like is one too many, and they would drift. (pluck c4) (bell e5 0.35) (sub a1) (flute g4) (hat) (voice sine c4 0.4 0 1.2) ; type, note, volume, pan, beats Notes are what you'd say out loud — c4, e5, a1 — because a language you can't say out loud is one nobody writes music in. Why this matters beyond sound: a pad that is KidLisp needs NO DEPLOY. KidLisp pieces already live in the database as $codes. The whole "how do we ship machine- made pads" problem dissolves if the machine writes KidLisp instead of JS — and the artifact gets safer to generate and mutable as a tree instead of as 300 lines of JS. Also: serve-local returned index.html (200) for a MISSING .mjs, so AC believed `foo.mjs` existed, imported HTML as JavaScript, and never tried `foo.lisp`. No KidLisp piece could ever be tested locally. A missing asset 404s now; only a bare route falls back to the SPA index. kidlisp specs: 149, 6 failures — the same 6 that fail on a clean tree (Tezos contract tests, unrelated). Co-Authored-By: Claude Opus 4.8 (1M context) --- marketing/av-reels/bin/serve-local.mjs | 8 ++++++++ system/public/aesthetic.computer/disks/klpad.lisp | 33 +++++++++++++++++++++++++++++++++ system/public/aesthetic.computer/lib/kidlisp.mjs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 file(s) changed, 97 insertion(s)(+), 0 deletion(s)(-) diff --git a/marketing/av-reels/bin/serve-local.mjs b/marketing/av-reels/bin/serve-local.mjs --- a/marketing/av-reels/bin/serve-local.mjs +++ b/marketing/av-reels/bin/serve-local.mjs @@ -36,6 +36,14 @@ if (s && s.isFile()) { res.writeHead(200, { ...hdr, "Content-Type": MIME[extname(fp).toLowerCase()] || "application/octet-stream" }); return res.end(await readFile(fp)); } + // A missing ASSET must 404 — only a bare route falls back to the SPA index. + // Serving index.html for a missing `disks/foo.mjs` makes AC believe the .mjs + // exists, import HTML as JavaScript, and never try `foo.lisp` — which is why + // KidLisp pieces could not be tested here at all. + if (extname(p)) { + res.writeHead(404, { ...hdr, "Content-Type": "text/plain" }); + return res.end("not found"); + } res.writeHead(200, { ...hdr, "Content-Type": "text/html; charset=utf-8" }); // SPA fallback → offline index res.end(await readFile(INDEX)); } catch (e) { res.writeHead(500); res.end(String(e)); } diff --git a/system/public/aesthetic.computer/disks/klpad.lisp b/system/public/aesthetic.computer/disks/klpad.lisp new file mode 100644 --- /dev/null +++ b/system/public/aesthetic.computer/disks/klpad.lisp @@ -0,0 +1,33 @@ +; klpad — proof that KidLisp can be a pad. +; A beat grid, real voices, and a visual that IS the score. No JS, no deploy. + +(wipe 4 4 12) + +; The pulse. `1s` is the beat — UTC-locked, so two people anywhere play in phase. +1s (bell c4 0.35) + (ink 120 220 255 90) + (circle (/ width 2) (/ height 2) 40) + +; The bass, half-time. This is the one you feel. +2s... (sub a1 0.5) + (ink 90 60 200 70) + (circle (/ width 2) (/ height 2) 90) + +; A plucked line on the off-beat, panned across. +1.5s... (pluck e4 0.4 -0.4) + (ink 255 200 120 120) + (box 20 (/ height 2) 30 4) + +; Breath on top. +3s... (flute g4 0.25) + (ink 200 255 220 60) + (circle (/ width 2) (/ height 2) 130) + +; The tick. +0.5s... (hat 0.1) + (ink 255 255 255 40) + (box 0 (- height 4) width 2) + +; Audio-reactive: the field answers the sound it is making. +(ink rainbow) +(circle (/ width 2) (/ height 2) (+ 6 (* amplitude 200))) diff --git a/system/public/aesthetic.computer/lib/kidlisp.mjs b/system/public/aesthetic.computer/lib/kidlisp.mjs --- a/system/public/aesthetic.computer/lib/kidlisp.mjs +++ b/system/public/aesthetic.computer/lib/kidlisp.mjs @@ -23,6 +23,11 @@ const MELODY_BASE_MS = 500; import { qrcode as qr } from "../dep/@akamfoad/qr/qr.mjs"; import { cssColors, rainbow, zebra, resetZebraCache, resetRainbowCache, staticColorMap } from "./num.mjs"; import { setFadeAlpha, clearFadeAlpha } from "./fade-state.mjs"; +// The pad voices — Karplus-Strong pluck, layered bell, waveguide flute, warm sub, +// filtered-noise hat. They live in pads.mjs because that's where they were grown; +// we borrow them rather than growing a second set here, because two definitions of +// "what a bell sounds like" is one too many and they WILL drift. +import { voices } from "./pads.mjs"; import { checkPackMode, getPackMode } from "./pack-mode.mjs"; import { log } from "./logs.mjs"; import { captureFrame, formatTimestamp, generateFilename } from "./frame-capture.mjs"; @@ -8222,6 +8227,57 @@ "..": (api, args = [], env) => { // Delegate to the three-dot version return this.getGlobalEnv()["..."](api, args, env); }, + // 🔈 Voices — the same physical models the JS pads use (lib/pads.mjs), said + // in Lisp. `overtone` below is a bare square wave; these have bodies. + // + // (pluck c4) a struck string + // (bell e5 0.3) shimmering, long tail — second arg is volume + // (sub a1) warm low end + // (flute g4) breathy waveguide lead + // (hat) a closed hi-hat tick + // (voice harp c4 0.4 -0.5) any type, volume, pan + // + // A note is what you'd say out loud — c4, e5, a1 — because a language you + // can't say out loud is a language nobody writes music in. + pluck: (api, args = []) => + voices.pluck(api.sound?.synth, args[0] ?? "c4", { + volume: args[1] ?? 0.5, + pan: args[2] ?? 0, + }), + bell: (api, args = []) => + voices.bell(api.sound?.synth, args[0] ?? "e5", { + volume: args[1] ?? 0.4, + pan: args[2] ?? 0, + }), + sub: (api, args = []) => + voices.sub(api.sound?.synth, args[0] ?? "a1", { + volume: args[1] ?? 0.5, + pan: args[2] ?? 0, + }), + flute: (api, args = []) => + voices.flute(api.sound?.synth, args[0] ?? "g4", { + volume: args[1] ?? 0.3, + pan: args[2] ?? 0, + }), + hat: (api, args = []) => + voices.hat(api.sound?.synth, { volume: args[0] ?? 0.14, pan: args[1] ?? 0 }), + + // The whole synth, for when a named voice isn't the one you hear. + // (voice sine c4 0.4 0 1.2) → type, note, volume, pan, beats + voice: (api, args = []) => { + const synth = api.sound?.synth; + if (!synth) return; + return synth({ + type: unquoteString(String(args[0] ?? "sine")), + tone: args[1] ?? "c4", + volume: args[2] ?? 0.4, + pan: args[3] ?? 0, + beats: args[4] ?? 0.6, + attack: args[5] ?? 0.01, + decay: args[6] ?? 0.6, + }); + }, + // 🔈 Sound overtone: (api, args = []) => { // console.log("Synth at:", args); -- tangled.sh