From 348d6cd009baa2e40f712cfc42db0a0bc3e55a40 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 21 Apr 2026 23:59:57 -0700 Subject: [PATCH] macos: demo.sh \u2014 boot \u2192 prompt \u2192 notepat waltz, single mkv (Phase D) End-to-end pipeline tying together the pieces from phases A\u2013C: - AC_WIN_W / AC_WIN_H env overrides for SDL_CreateWindow so the demo renders natively at 1280x800 instead of the 640x480 dev default. Falls back to INITIAL_WIN_* when unset (notepat.app still launches at its original size). - scripts/demo.sh ties everything together: 1. Generates a "good afternoon . enjoy ." greeting via /usr/bin/say (Samantha @ 165 wpm). 2. Runs ac-native with AC_BOOT_ANIM + AC_INJECT_SEQUENCE typing 'notepat', then an oom-pah-pah 3/4 waltz across four bars in C minor (Cm \u2192 Fm \u2192 Gm \u2192 Cm) using notepat's z (low bass), c/e/f/a/g/b (mid chord) keys. 12 notes, 500 ms/beat. 3. Frame-dumps during boot anim + main loop so the video is one contiguous 60 fps h264 stream. 4. Mixes the synth WAV tap + TTS AIFF with ffmpeg amix (TTS delayed 167 ms to line up with the boot-anim f==10 cue that drives the greeting on Linux), then encodes mkv. - Boot-animation prelude now also dumps frames when AC_FRAME_DUMP_DIR is set; the piece loop seeds its frame index from there so both halves of the run form one sequence. Output: ~/Desktop/ac-boot-shots/demo.mkv \u2014 ~14 s, 1280x800, 60 fps h264 + 48 kHz stereo aac. Override HANDLE / CITY / HOUR / OUT for other greetings; KEEP_STAGES=1 preserves /tmp/ac-demo-final for debugging. Co-Authored-By: Claude Opus 4.7 (1M context) --- fedac/native/macos/main.c | 35 ++++++++- fedac/native/macos/scripts/demo.sh | 109 +++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100755 fedac/native/macos/scripts/demo.sh diff --git a/fedac/native/macos/main.c b/fedac/native/macos/main.c index 6024b3b8b..c9d5505e6 100644 --- a/fedac/native/macos/main.c +++ b/fedac/native/macos/main.c @@ -558,8 +558,16 @@ int main(int argc, char **argv) { | SDL_WINDOW_ALWAYS_ON_TOP; } + // AC_WIN_W / AC_WIN_H override the compile-time default so the demo + // pipeline can record at 1280x800 without recompiling. FB size then + // falls out of WIN/DENSITY so the piece still sees chunky-pixel + // coordinates. + int init_w = getenv("AC_WIN_W") ? atoi(getenv("AC_WIN_W")) : INITIAL_WIN_W; + int init_h = getenv("AC_WIN_H") ? atoi(getenv("AC_WIN_H")) : INITIAL_WIN_H; + if (init_w < 128) init_w = INITIAL_WIN_W; + if (init_h < 128) init_h = INITIAL_WIN_H; SDL_Window *win = SDL_CreateWindow("Notepat", - INITIAL_WIN_W, INITIAL_WIN_H, + init_w, init_h, win_flags); if (!win) { fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); SDL_Quit(); return 1; } if (!fullscreen) SDL_SetWindowHitTest(win, hit_test_cmd_drag, NULL); @@ -619,6 +627,12 @@ int main(int argc, char **argv) { }; if (!fb.pixels) { fprintf(stderr, "fb alloc failed\n"); return 1; } + // Frame-dump dir declared early so the boot-animation prelude below + // can also contribute frames to the same sequence. Read once here; + // the piece-loop block below re-reads it in the same variable. + const char *early_frame_dir = getenv("AC_FRAME_DUMP_DIR"); + int early_frame_idx = 0; + // Boot animation prelude — 2 s of the shared boot_anim renderer before // the piece loads. Gated on AC_BOOT_ANIM (default off so existing // notepat launches stay snappy; the demo pipeline exports it so every @@ -660,6 +674,21 @@ int main(int argc, char **argv) { SDL_RenderClear(ren); SDL_RenderTexture(ren, tex, NULL, NULL); SDL_RenderPresent(ren); + if (early_frame_dir) { + int d = density < 1 ? 1 : density; + const uint32_t *src = fb.pixels; + int out_w = fb.width, out_h = fb.height; + uint32_t *scaled = NULL; + if (d > 1) { + scaled = upscale_nn(fb.pixels, fb.width, fb.height, d); + if (scaled) { src = scaled; out_w *= d; out_h *= d; } + } + char path[1200]; + snprintf(path, sizeof path, "%s/frame_%05d.png", + early_frame_dir, early_frame_idx++); + png_write_argb(path, src, out_w, out_h, out_w); + free(scaled); + } // Drain events so the OS doesn't mark the window unresponsive. SDL_Event ev; while (SDL_PollEvent(&ev)) { if (ev.type == SDL_EVENT_QUIT) goto boot_anim_done; @@ -856,7 +885,9 @@ boot_anim_done: ; // into an mkv. Density-2 upscaling applied so the saved PNGs match // what you see on screen (chunky retro pixels stay crisp). const char *frame_dump_dir = getenv("AC_FRAME_DUMP_DIR"); - int frame_dump_idx = 0; + // Continue from the boot-animation prelude's index so the whole run + // forms one contiguous frame_00000…frame_NNNNN sequence. + int frame_dump_idx = early_frame_idx; // AC_WAV_OUT= — tap audio callback output into a float32 stereo // @ 48 kHz WAVE file. Paired with frame-dump, ffmpeg can mux the two diff --git a/fedac/native/macos/scripts/demo.sh b/fedac/native/macos/scripts/demo.sh new file mode 100755 index 000000000..677178046 --- /dev/null +++ b/fedac/native/macos/scripts/demo.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# demo.sh — record the full ac-native first-demo: +# boot animation → prompt → type "notepat" → waltz in notepat +# into a single mkv (1280x800, 60 fps, h264 + aac) with synced audio. +# +# Audio is a stereo mix of: +# - the host's CoreAudio WAV tap (synth voice playback for every +# keystroke + notepat's waltz notes) +# - a macOS TTS greeting rendered by `say` and delayed to match the +# boot animation's f==10 cue (the Linux kernel does the same) +# +# Env overrides (optional): +# HANDLE=jeffrey # the greeting handle (passed as AC_SHOT_HANDLE) +# CITY="Los Angeles" +# HOUR=13 +# OUT=~/Desktop/ac-boot-shots/demo.mkv +# KEEP_STAGES=1 # keep /tmp/ac-demo-final after success +# +# Requires: ffmpeg (brew install ffmpeg), /usr/bin/say, and the +# fedac/native/macos/build/ac-native-macos-core binary. + +set -euo pipefail + +HANDLE="${HANDLE:-jeffrey}" +CITY="${CITY:-Los Angeles}" +HOUR="${HOUR:-13}" +OUT="${OUT:-$HOME/Desktop/ac-boot-shots/demo.mkv}" +STAGE="${STAGE:-/tmp/ac-demo-final}" + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd) +BIN="$REPO_ROOT/fedac/native/macos/build/ac-native-macos-core" +PROMPT="$REPO_ROOT/fedac/native/pieces/prompt.mjs" + +[[ -x "$BIN" ]] || { echo "error: binary missing — run make in fedac/native/macos" >&2; exit 1; } +[[ -f "$PROMPT" ]] || { echo "error: prompt.mjs not found at $PROMPT" >&2; exit 1; } + +rm -rf "$STAGE" +mkdir -p "$STAGE/frames" + +# Typing timeline — all delays are cumulative offsets from prompt load. +# Boot animation runs for ~2s before this, so the earliest keystroke is +# effectively ~2.5s into the final video. +# +# Layout: +# prompt typing: n o t e p a t (quick, ~1s) +# ~1.4s pause while notepat loads + settles +# waltz: 4 bars × 3 beats at 500ms/beat = 6s +# z c e (Cm bar) +# z f a (Fm bar) +# z g b (Gm bar) +# z c e (Cm bar back home) +SEQ=( + "n,500" "o,120" "t,120" "e,120" "p,120" "a,120" "t,120" "enter,400" + "z,1380" "c,500" "e,500" + "z,500" "f,500" "a,500" + "z,500" "g,500" "b,500" + "z,500" "c,500" "e,500" +) +# Join with '|' separators (AC_INJECT_SEQUENCE syntax). +IFS='|' SEQ_STR="${SEQ[*]}"; unset IFS + +# Generate the TTS greeting. -r 165 slightly slower than default so it +# reads less urgent; -v Samantha is the long-time macOS US voice. +say -v Samantha -r 165 \ + -o "$STAGE/tts.aiff" \ + "good afternoon $HANDLE. enjoy $CITY." + +echo "→ running ac-native for ~12 s …" +AC_HEADLESS_MS=12000 \ + AC_BOOT_ANIM=1 \ + AC_WIN_W=1280 AC_WIN_H=800 \ + AC_SHOT_HANDLE="$HANDLE" \ + AC_SHOT_CITY="$CITY" \ + AC_SHOT_HOUR="$HOUR" \ + AC_FRAME_DUMP_DIR="$STAGE/frames" \ + AC_WAV_OUT="$STAGE/synth.wav" \ + AC_INJECT_SEQUENCE="$SEQ_STR" \ + "$BIN" "$PROMPT" 2>&1 | sed 's/^/ [ac] /' + +N_FRAMES=$(ls "$STAGE/frames" | wc -l | tr -d ' ') +echo "→ captured $N_FRAMES frames + $(du -h "$STAGE/synth.wav" | cut -f1) synth WAV" + +# Mix synth + TTS into one track. TTS starts at 167ms to match f==10 of +# the boot animation (where Linux kicks the greeting off). `-filter_complex` +# delays TTS and mixes equal-loudness; `duration=longest` keeps whichever +# track runs longest as the final length. +echo "→ mixing synth + TTS …" +ffmpeg -y \ + -i "$STAGE/synth.wav" \ + -i "$STAGE/tts.aiff" \ + -filter_complex "[1]adelay=167|167,volume=1.3[tts]; [0:a][tts]amix=inputs=2:duration=longest:dropout_transition=0[out]" \ + -map "[out]" "$STAGE/mix.wav" 2>&1 | tail -2 + +echo "→ encoding mkv …" +mkdir -p "$(dirname "$OUT")" +ffmpeg -y \ + -framerate 60 -i "$STAGE/frames/frame_%05d.png" \ + -i "$STAGE/mix.wav" \ + -c:v libx264 -pix_fmt yuv420p -crf 18 -preset slow \ + -c:a aac -b:a 192k \ + "$OUT" 2>&1 | tail -2 + +echo +echo "demo ready: $OUT ($(du -h "$OUT" | cut -f1))" +ffprobe -v error -show_entries format=duration,bit_rate "$OUT" 2>&1 | sed 's/^/ /' + +if [[ -z "${KEEP_STAGES:-}" ]]; then + rm -rf "$STAGE" +fi -- 2.51.2