From 6dc1be8ed9e1030d36990d8cf989c7c71291477a Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 12 May 2026 11:32:02 -0400 Subject: [PATCH] =?UTF-8?q?native:=20babypat=20learns=20notepat=20?= =?UTF-8?q?=E2=80=94=20layout=20match=20+=20bg=20flash=20+=20AC=5FBOOT=5FP?= =?UTF-8?q?IECE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit babypat rewrite: tone-key layout now mirrors notepat.mjs NOTE_TO_KEY exactly, so muscle memory carries over once the baby graduates. Every letter key + ; ' ] plays a chromatic note in the piano voice across 4 octaves (sub-base / base / upper / highest). Numbers play drums, arrows swoosh, space kicks. Visual: every press flashes a new background color, with the settled bg drifting through hues as you play. Splash letters still pop and fade. Note colors mirror notepat's NOTE_COLORS palette so the same note shows the same color in both pieces. flash-mac.sh: AC_BOOT_PIECE env var overrides the hardcoded boot piece in config.json on both ACBOOT and ACEFI partitions. Default stays "notepat". Lets us flash a stick that boots straight into babypat (or any other piece) without editing the script. Co-Authored-By: Claude Opus 4.7 --- fedac/native/pieces/babypat.mjs | 270 +++++++++++++++++++----------- fedac/native/scripts/flash-mac.sh | 12 +- 2 files changed, 177 insertions(+), 105 deletions(-) diff --git a/fedac/native/pieces/babypat.mjs b/fedac/native/pieces/babypat.mjs index 47994f0fed..2bd51bb600 100644 --- a/fedac/native/pieces/babypat.mjs +++ b/fedac/native/pieces/babypat.mjs @@ -1,84 +1,99 @@ -// babypat.mjs — every key makes a sound, every press paints a splash. +// babypat.mjs — a baby's notepat. Every key plays a sound, every press +// changes the whole-screen background color, and the letter you pressed +// splashes onto the screen. No modes, no menus, no effects. // -// Designed for the smallest hands: no modes, no menus, no effects. Mash -// any key and you get a sound effect plus a colored letter on screen. -// Triple-press escape (within 2 s) to bail back to the prompt — keeps -// curious fingers from accidentally exiting after one tap. -// -// Sound bank: 26 letters + 10 number keys + space/arrows. Letters split -// across zoo samples, percussion kit, and a handful of synth chirps so -// every press feels distinct. +// The note layout is THE SAME as notepat.mjs so muscle-memory carries +// over once the baby graduates. Letter keys + ; ' ] are all musical +// notes (piano voice). Numbers play drums, arrows swoosh, space kicks. +// Triple-press escape within 2 s to bail back to the prompt. import { playPercussion } from "/lib/percussion.mjs"; -// ── key → sound bank ────────────────────────────────────────────── -// Each entry is { kind, arg, color }. -// kind="zoo" → sound.zoo.play(arg) (uses the baked samples) -// kind="perc" → playPercussion(sound, arg) (synth drum kit) -// kind="tone" → sound.synth({ type, tone }) (synth chirp at tone Hz) -const KEYS = { - // top row — numbers play a happy C-major run, low to high - "1": { kind: "tone", wave: "sine", tone: 262, color: [255, 100, 100] }, // C4 - "2": { kind: "tone", wave: "sine", tone: 294, color: [255, 160, 80] }, // D4 - "3": { kind: "tone", wave: "sine", tone: 330, color: [255, 220, 80] }, // E4 - "4": { kind: "tone", wave: "sine", tone: 349, color: [180, 255, 80] }, // F4 - "5": { kind: "tone", wave: "sine", tone: 392, color: [100, 255, 140] }, // G4 - "6": { kind: "tone", wave: "sine", tone: 440, color: [100, 220, 255] }, // A4 - "7": { kind: "tone", wave: "sine", tone: 494, color: [140, 160, 255] }, // B4 - "8": { kind: "tone", wave: "sine", tone: 523, color: [200, 120, 255] }, // C5 - "9": { kind: "tone", wave: "sine", tone: 587, color: [255, 140, 220] }, // D5 - "0": { kind: "tone", wave: "sine", tone: 659, color: [255, 200, 200] }, // E5 - - // qwerty row — zoo animals (10 of 12) - q: { kind: "zoo", arg: "dog", color: [200, 130, 80] }, - w: { kind: "zoo", arg: "cat", color: [230, 180, 220] }, - e: { kind: "zoo", arg: "cow", color: [220, 200, 170] }, - r: { kind: "zoo", arg: "sheep", color: [240, 240, 220] }, - t: { kind: "zoo", arg: "bird", color: [140, 220, 220] }, - y: { kind: "zoo", arg: "pig", color: [240, 180, 180] }, - u: { kind: "zoo", arg: "lion", color: [220, 170, 90] }, - i: { kind: "zoo", arg: "owl", color: [120, 120, 180] }, - o: { kind: "zoo", arg: "frog", color: [140, 220, 140] }, - p: { kind: "zoo", arg: "horse", color: [170, 120, 80] }, - - // home row — percussion kit - a: { kind: "perc", arg: "c", color: [255, 140, 100] }, // kick - s: { kind: "perc", arg: "d", color: [255, 200, 100] }, // snare - d: { kind: "perc", arg: "e", color: [255, 240, 140] }, // clap - f: { kind: "perc", arg: "f", color: [200, 255, 140] }, // snap - g: { kind: "perc", arg: "g", color: [140, 240, 200] }, // hat-c - h: { kind: "perc", arg: "a", color: [140, 220, 255] }, // hat-o - j: { kind: "perc", arg: "b", color: [180, 180, 255] }, // ride - k: { kind: "perc", arg: "c#", color: [220, 140, 255] }, // crash - l: { kind: "perc", arg: "d#", color: [255, 140, 220] }, // splash - - // bottom row — remaining zoo + extra perc + synth chirps - z: { kind: "zoo", arg: "snake", color: [170, 230, 130] }, - x: { kind: "zoo", arg: "whale", color: [100, 180, 230] }, - c: { kind: "perc", arg: "f#", color: [255, 220, 100] }, // cowbell - v: { kind: "perc", arg: "g#", color: [200, 200, 200] }, // block - b: { kind: "perc", arg: "a#", color: [255, 200, 240] }, // tambo - n: { kind: "tone", wave: "triangle", tone: 880, color: [180, 255, 255] }, - m: { kind: "tone", wave: "triangle", tone: 440, color: [255, 200, 100] }, - - // space — big satisfying kick - " ": { kind: "perc", arg: "c", color: [255, 255, 255] }, - // arrows — directional swooshes +// ── notepat note layout (mirrors NOTE_TO_KEY in notepat.mjs) ────── +// Each letter / punctuation key → note. Octave shift is parsed from the +// "+", "++", or "-" prefix, then folded into a real octave number so we +// can compute frequency. Base octave is 4 (c=C4, +c=C5, ++c=C6, -a#=A#3). +const KEY_NOTE = { + // base octave (4) + a: "a", b: "b", c: "c", d: "d", e: "e", f: "f", g: "g", + q: "a#", r: "g#", s: "d#", v: "c#", w: "f#", + // upper octave (+, octave 5) + h: "+c", i: "+d", j: "+e", k: "+f", l: "+g", + m: "+a", n: "+b", o: "+g#", p: "+a#", + t: "+c#", u: "+f#", y: "+d#", + // highest (++, octave 6) + ";": "++c", "'": "++c#", "]": "++d", + // sub-base (-, octave 3) + z: "-a#", x: "-b", +}; + +const CHROMATIC = ["c","c#","d","d#","e","f","f#","g","g#","a","a#","b"]; + +// Notepat's note-color palette — keeps the chromatic rainbow consistent +// across the two pieces so the baby sees the same color for the same +// note in notepat later. +const NOTE_COLORS = { + c: [255, 30, 30], "c#": [255, 80, 0], + d: [255, 150, 0], "d#": [200, 200, 0], + e: [230, 220, 0], + f: [ 30, 200, 30], "f#": [ 0, 200, 180], + g: [ 30, 100, 255], "g#": [ 80, 50, 255], + a: [140, 30, 220], "a#": [200, 30, 150], + b: [255, 60, 120], +}; + +// Non-note keys — drums, swooshes, kicks. These keep babypat's fun side +// while preserving the notepat-learning layout for the letters. +// kind="perc" → playPercussion(sound, arg) +// kind="tone" → sound.synth({ type, tone }) +const EXTRA = { + // number row — 10 drum hits for satisfying chaos + "1": { kind: "perc", arg: "c", color: [255, 140, 100] }, // kick + "2": { kind: "perc", arg: "d", color: [255, 200, 100] }, // snare + "3": { kind: "perc", arg: "e", color: [255, 240, 140] }, // clap + "4": { kind: "perc", arg: "f", color: [200, 255, 140] }, // snap + "5": { kind: "perc", arg: "g", color: [140, 240, 200] }, // hat-c + "6": { kind: "perc", arg: "a", color: [140, 220, 255] }, // hat-o + "7": { kind: "perc", arg: "b", color: [180, 180, 255] }, // ride + "8": { kind: "perc", arg: "c#", color: [220, 140, 255] }, // crash + "9": { kind: "perc", arg: "d#", color: [255, 140, 220] }, // splash + "0": { kind: "perc", arg: "f#", color: [255, 200, 80] }, // cowbell + // big satisfying kick on space + " ": { kind: "perc", arg: "c", color: [255, 255, 255] }, + // arrows — directional sawtooth swooshes arrowleft: { kind: "tone", wave: "sawtooth", tone: 220, color: [255, 100, 200] }, arrowright: { kind: "tone", wave: "sawtooth", tone: 660, color: [100, 255, 200] }, arrowup: { kind: "tone", wave: "sawtooth", tone: 880, color: [255, 200, 100] }, arrowdown: { kind: "tone", wave: "sawtooth", tone: 165, color: [200, 100, 255] }, }; -// ── splash state ────────────────────────────────────────────────── -// Each press spawns a splash that fades over ~30 frames. We keep them -// in a small ring so concurrent presses overlap naturally. +// Parse a note token like "c", "c#", "+c", "++c", "-a#" → [letter, octave] +function parseNote(token) { + let oct = 4; + let t = token; + if (t.startsWith("++")) { oct = 6; t = t.slice(2); } + else if (t.startsWith("+")) { oct = 5; t = t.slice(1); } + else if (t.startsWith("-")) { oct = 3; t = t.slice(1); } + return [t, oct]; +} + +// Equal-tempered A=440 Hz frequency for any chromatic-letter + octave. +function noteToFreq(letter, oct) { + const idx = CHROMATIC.indexOf(letter); + if (idx < 0) return 440; + return 440 * Math.pow(2, (oct - 4) + (idx - 9) / 12); +} + +// ── state ───────────────────────────────────────────────────────── const SPLASH_LIFE = 30; +const BG_FADE = 28; // frames it takes the bg flash to settle const splashes = []; let frame = 0; +let bgColor = [12, 12, 22]; // current settled background +let bgFlash = [12, 12, 22]; // most-recent press color (fades into bgColor) +let bgFlashFrame = -1000; -// Triple-escape exit. Three presses within 2 s (120 frames) jumps to -// the prompt; otherwise the counter resets so a stray tap won't bail. +// Triple-escape exit let escCount = 0; let lastEscFrame = -1000; @@ -94,15 +109,42 @@ function spawnSplash(label, color, screen) { y: rand(20, Math.max(40, screen.height - 40)), born: frame, }); - // Cap so memory doesn't grow if a key is held / repeated rapidly. if (splashes.length > 64) splashes.splice(0, splashes.length - 64); } -function triggerSound(sound, entry, pan = 0) { - if (!entry) return; - if (entry.kind === "zoo") { - sound?.zoo?.play?.(entry.arg, { volume: 1.0, pan, pitch: 1.0 }); - } else if (entry.kind === "perc") { +function setBg(color) { + bgFlash = color; + bgFlashFrame = frame; + // Soft the settled bg toward the press color so successive taps drift + // through hues instead of always snapping back to deep navy. + bgColor = [ + Math.floor(bgColor[0] * 0.5 + color[0] * 0.15), + Math.floor(bgColor[1] * 0.5 + color[1] * 0.15), + Math.floor(bgColor[2] * 0.5 + color[2] * 0.15), + ]; +} + +function playNote(sound, key, pan) { + const token = KEY_NOTE[key]; + if (!token) return null; + const [letter, oct] = parseNote(token); + const freq = noteToFreq(letter, oct); + // Piano voice — matches the bank baked into the OTA. Short percussive + // envelope so each press is a clean strike, not a sustained hold. + sound?.synth?.({ + type: "piano", + tone: freq, + duration: 0.6, + volume: 0.9, + attack: 0.001, + decay: 0.55, + pan, + }); + return { letter, oct, color: NOTE_COLORS[letter] || [200, 200, 200] }; +} + +function playExtra(sound, entry, pan) { + if (entry.kind === "perc") { playPercussion(sound, entry.arg, 1.2, pan); } else if (entry.kind === "tone") { sound?.synth?.({ @@ -121,10 +163,12 @@ function boot() { frame = 0; splashes.length = 0; escCount = 0; + bgColor = [12, 12, 22]; + bgFlash = bgColor; } function act({ event: e, sound, screen, system }) { - // Triple-escape → prompt. Reset if it's been more than 2 s. + // Triple-escape → prompt if (e.is("keyboard:down:escape")) { if (frame - lastEscFrame > 120) escCount = 0; escCount++; @@ -133,60 +177,84 @@ function act({ event: e, sound, screen, system }) { system?.jump?.("prompt"); return; } - // Audible feedback so the user knows the count is climbing. sound?.synth?.({ type: "triangle", tone: 440 + escCount * 120, duration: 0.08, volume: 0.25, attack: 0.002, decay: 0.07, pan: 0, }); spawnSplash(`esc ${escCount}/3`, [255, 80, 80], screen); + setBg([60, 0, 0]); return; } - // Any other key → sound + splash. Match the generic key-down event - // and pull the actual key name from e.key (lowercased — matches the - // KEYS table). - if (e.is("keyboard:down")) { - const key = String(e.key || "").toLowerCase(); - const entry = KEYS[key]; + if (!e.is("keyboard:down")) return; + const key = String(e.key || "").toLowerCase(); + + // Stereo pan: notes pan by chromatic position (low→left, high→right); + // non-note keys pan center. + let pan = 0; + let label, color; + + const noteToken = KEY_NOTE[key]; + if (noteToken) { + const [letter, oct] = parseNote(noteToken); + // Pan: c3→-0.9, c6→+0.9, scaled by 36 chromatic steps from C3 to C6. + const idx = CHROMATIC.indexOf(letter); + const semitone = (oct - 3) * 12 + idx; // 0..35 ish + pan = Math.max(-1, Math.min(1, (semitone - 18) / 18 * 0.9)); + const info = playNote(sound, key, pan); + if (!info) return; + color = info.color; + label = key === ";" ? "C" : key === "'" ? "C#" : key === "]" ? "D" + : key.toUpperCase(); + } else { + const entry = EXTRA[key]; if (!entry) return; - // Stereo pan based on letter position (a=left, z=right) for tactile - // spatial feel. - let pan = 0; - if (key.length === 1 && key >= "a" && key <= "z") { - pan = (key.charCodeAt(0) - 97) / 25 * 1.4 - 0.7; - } - triggerSound(sound, entry, pan); - const label = key === " " ? "␣" - : key.startsWith("arrow") ? { arrowleft:"◀", arrowright:"▶", arrowup:"▲", arrowdown:"▼" }[key] - : key.toUpperCase(); - spawnSplash(label, entry.color, screen); + playExtra(sound, entry, 0); + color = entry.color; + label = key === " " ? "␣" + : key.startsWith("arrow") + ? { arrowleft:"◀", arrowright:"▶", arrowup:"▲", arrowdown:"▼" }[key] + : key.toUpperCase(); } + + spawnSplash(label, color, screen); + setBg(color); } function paint({ wipe, ink, write }) { frame++; - // Soft fade so trails dissolve cleanly without staying as a blur. - wipe(8, 8, 14); + // Background flash: fade from the most-recent press color back toward + // the drifting settled color over BG_FADE frames. Each press paints + // the whole screen for a moment before settling. + const age = frame - bgFlashFrame; + let bg; + if (age < BG_FADE) { + const t = age / BG_FADE; + bg = [ + Math.floor(bgFlash[0] * (1 - t) + bgColor[0] * t), + Math.floor(bgFlash[1] * (1 - t) + bgColor[1] * t), + Math.floor(bgFlash[2] * (1 - t) + bgColor[2] * t), + ]; + } else { + bg = bgColor; + } + wipe(bg[0], bg[1], bg[2]); for (let i = 0; i < splashes.length; i++) { const s = splashes[i]; - const age = frame - s.born; - if (age > SPLASH_LIFE) continue; - const t = age / SPLASH_LIFE; // 0 → 1 + const a = frame - s.born; + if (a > SPLASH_LIFE) continue; + const t = a / SPLASH_LIFE; const alpha = Math.floor((1 - t) * 255); const [r, g, b] = s.color; ink(r, g, b, alpha); - // Big block letter. font_1 at size 4 ≈ 24×40 px per char. write(s.label, { x: s.x, y: s.y, size: 4, font: "font_1" }); } - // Drop expired splashes. while (splashes.length > 0 && frame - splashes[0].born > SPLASH_LIFE) { splashes.shift(); } - - // No HUD. The whole screen is sound-pressed letters. } function sim() {} diff --git a/fedac/native/scripts/flash-mac.sh b/fedac/native/scripts/flash-mac.sh index c7200a361c..47a73744d0 100755 --- a/fedac/native/scripts/flash-mac.sh +++ b/fedac/native/scripts/flash-mac.sh @@ -444,8 +444,12 @@ log "Writing ACBOOT (kernel-direct boot tree)…" mkdir -p "${M1}/EFI/BOOT" cp "${KERNEL}" "${M1}/EFI/BOOT/BOOTX64.EFI" cp "${INITRAMFS}" "${M1}/initramfs.cpio.gz" -printf '{"handle":"%s","piece":"notepat","sub":"%s","email":"%s","udpMidiBroadcast":true}\n' \ - "${USER_HANDLE}" "${USER_SUB}" "${USER_EMAIL}" | tee "${M1}/config.json" >/dev/null +# Boot piece: notepat by default. Override per-flash with AC_BOOT_PIECE +# (e.g. AC_BOOT_PIECE=babypat flash-mac.sh ...). Kernel resolves the +# name to /pieces/.mjs at boot — see ac-native.c:3853. +BOOT_PIECE="${AC_BOOT_PIECE:-notepat}" +printf '{"handle":"%s","piece":"%s","sub":"%s","email":"%s","udpMidiBroadcast":true}\n' \ + "${USER_HANDLE}" "${BOOT_PIECE}" "${USER_SUB}" "${USER_EMAIL}" | tee "${M1}/config.json" >/dev/null # Build merged wifi_creds.json (presets + preserved + optional override) # once, reuse for both partitions. @@ -499,8 +503,8 @@ linux /EFI/BOOT/KERNEL.EFI initrd /initramfs.cpio.gz options console=tty0 quiet loglevel=3 vt.global_cursor_default=0 init=/init nomodeset efi=noruntime EOF -printf '{"handle":"%s","piece":"notepat","sub":"%s","email":"%s"}\n' \ - "${USER_HANDLE}" "${USER_SUB}" "${USER_EMAIL}" | tee "${M2}/config.json" >/dev/null +printf '{"handle":"%s","piece":"%s","sub":"%s","email":"%s"}\n' \ + "${USER_HANDLE}" "${BOOT_PIECE}" "${USER_SUB}" "${USER_EMAIL}" | tee "${M2}/config.json" >/dev/null [ -f "${WIFI_MERGED}" ] && cp "${WIFI_MERGED}" "${M2}/wifi_creds.json" # --- verify (sha256 round-trip on every kernel + initramfs copy) --- -- 2.51.2