diff --git a/pop/RELEASES.md b/pop/RELEASES.md --- a/pop/RELEASES.md +++ b/pop/RELEASES.md @@ -58,6 +58,65 @@ `boombaboom.mp3` to the lane. Master in `~/Documents/Shelf/boombaboom/`. --- +## fluttabap360 — READY (releasing 2026-06-24) + +- **Lane:** `pop/marimba/` · the fat, fast, six-minute butterfly-park banger · + **exactly 6:00.000** (185 bars, 4/4 @ 123.3 BPM, back-solved + hard-clamped + to the boundary so a player reads "6:00", never "6:01") · a buttery, + poppin' cousin of the flutterbap material (butterfly / pal-of-mine / + mommy-wow / slinky / fly / land hooks) re-cast over four escalating passes. + Rendered ENTIRELY in JS via the shared marimba + perc synths. Part of the + *pixsies* body. +- **Key-change journey:** modulates **C → D → E → (home C) → F** — Pass A in C, + Pass B lifts to D, Pass C is the brightest in E, the late cave breakdown + drops home to C for a breath, then a euphoric jump up to F for the final + chorus (resolves home through progression → land → button). Every + modulation blooms a **held tonic-triad pad swell**, with a long held chord + under each `mommy-wow` hush. +- **Squeak-rides:** short melodic scratch+squeak duets that **swell in and out** + of the fight (hump envelope) — the scratch SINGS a pitched call (C→E→G), the + squeak ANSWERS up top (G→C→E), both transposed with the key and blooming in + a shared space-reverb send so they sit BACK in the mix. Fights halved from + 8 → 4 bars (the old ~2:00 grind is now ~7.8 s, melodic). +- **Space:** a shared room-reverb SEND deepens the whole field (melodic + squeak + + pad voices); kick + sub stay DRY so it's still fat and punchy. +- **Artist:** Aesthetic Dot Computer +- **Status:** READY — rendered + ballet cover embedded; mastered WAV in hand, + not yet submitted to DistroKid. Awaiting @jeffrey ear-check + the + distribution step. +- **Cover:** `pop/marimba/out/fluttabap360-cover.png` (1024², embedded in the + mp3) — a colored-pencil + gouache drawing: the butterfly cosplayer (jeffrey + identity refs) **dancing ballet** — relevé + low arabesque + port de bras — + with four or five monarchs circling him as dance partners. His real clothes + (pale-yellow button-down + jeans), hand-made monarch wings, antenna + headband; NO tutu. Generated by `gen-illy.mjs` from + `pop/marimba/fluttabap360.illy.txt`. For DistroKid, upscale to 3000². +- **Outputs:** `pop/marimba/out/fluttabap360.mp3` (butter-mastered, cover + embedded) · `fluttabap360.distrokid.wav` (44.1 kHz / 16-bit, −14 LUFS / + −1.5 dBTP, **360.000 s**) · `fluttabap360.struct.json` (section map + + per-section `keyOffset`). +- **Reconstructable** ($0, deterministic): `node pop/marimba/bin/render-fluttabap360.mjs` + (`--out ` to redirect; `--loop` for the seam-perfect loop). Re-embed + the cover with `node pop/marimba/bin/gen-illy.mjs --embed-only --cover + out/fluttabap360-cover.png --mp3 out/fluttabap360.mp3`. +- **DistroKid submission fields (draft):** + - Track title: **fluttabap360** + - Artist: **Aesthetic Dot Computer** + - Release: single · release date **2026-06-24** · album field **pixsies** + - Genre: **Electronic** (primary) / **Pop** (secondary) + - Instrumental: **Yes** (no sung lyrics — the "vocal" is the melodic + kitten-squeak ride) + - Explicit: **No** · Language: Instrumental + - Songwriter / publishing: @jeffrey (legal name on the DistroKid form) · + Studio Zollo + - ISRC: auto (DistroKid) · Cover: 3000² upscale of `fluttabap360-cover.png` +- **TODO before submit:** durable master + cover to + `~/Documents/Shelf/fluttabap360-DISTROKID/` (MASTER.wav, cover-3000.jpg, + README with the fields above), publish assets to the AC CDN, optional + YouTube/visualizer cut. + +--- + ## marimbaba — RELEASED - **Lane:** `pop/marimba/` · lullaby for synthesized marimba, 1:24 · diff --git a/pop/marimba/bin/gen-sections-fluttabap360.mjs b/pop/marimba/bin/gen-sections-fluttabap360.mjs new file mode 100644 --- /dev/null +++ b/pop/marimba/bin/gen-sections-fluttabap360.mjs @@ -0,0 +1,290 @@ +#!/usr/bin/env node +// marimba/bin/gen-sections-fluttabap360.mjs — generate the fluttabap360 +// STORYLINE panel set: a portrait (9:16) cover + 11 per-beat +// colored-pencil + gouache illustrations tracing "the monarch keeper" — +// the butterfly cosplayer (jeffrey identity-ref character) learning to +// DANCE with living-data monarchs in a near-future AC nature-park. +// +// Concept + story: pop/marimba/fluttabap360.story.txt (read it first). +// Medium + character inherited verbatim from the locked album cover +// (pop/marimba/fluttabap360.illy.txt): colored-pencil + gouache on warm +// cream paper, the butterfly cosplayer — pale-yellow button-down, +// hand-made monarch paper-mache wings on wooden dowels, antenna headband. +// +// THE 11 UNIQUE BEATS (one illustration per unique section NAME): +// intro · butterfly · palofmine · mommywow · slinky · fly · ride · +// cave · progression · land · button +// The struct (out/fluttabap360.struct.json) repeats these names across +// four key passes (C→D→E→home C→F). preview-score.mjs resolves a panel +// per section INDEX as -p-sec--.png, so after generating +// the 11 unique-by-NAME panels we COPY each into every struct index that +// shares its name — 34 index files, but only 11 + cover gpt-image-2 calls +// (the visualizer's per-section tint + key-brightening makes each pass +// read brighter/higher). Cheap to re-run; per-file cached. +// +// Output: pop/marimba/out/fluttabap360-p-cover.png (portrait hero) +// pop/marimba/out/fluttabap360-p-sec--.png (34 index files) +// pop/marimba/out/_fluttabap360-beat-.png (11 unique masters, cached) +// +// Usage: +// node pop/marimba/bin/gen-sections-fluttabap360.mjs # cached +// node pop/marimba/bin/gen-sections-fluttabap360.mjs --force # regen all +// node pop/marimba/bin/gen-sections-fluttabap360.mjs --only fly # one beat +// node pop/marimba/bin/gen-sections-fluttabap360.mjs --proof # proof set only +// (cover + intro + fly + button — validate look/continuity cheap) +// node pop/marimba/bin/gen-sections-fluttabap360.mjs --no-fan-out # skip index copies + +import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import * as progress from "../../lib/render-progress.mjs"; +import { COLORED_PENCIL_TOOTH } from "../../lib/mediums.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const REPO = resolve(LANE, "..", ".."); +const SLUG = "fluttabap360"; + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]; + if (!a.startsWith("--")) continue; + const next = process.argv[i + 1]; + if (next === undefined || next.startsWith("--")) flags[a.slice(2)] = true; + else { flags[a.slice(2)] = next; i++; } +} +const FORCE = flags.force === true; +const PROOF = flags.proof === true; // cover + intro + fly + button only +const NO_FAN = flags["no-fan-out"] === true; +const SIZE = "1024x1536"; // portrait 9:16-ish +const TAG = "-p"; +mkdirSync(`${LANE}/out`, { recursive: true }); + +// ── identity refs — the butterfly-cosplayer character ──────────────── +// jeffrey portrait refs (same set as gen-illy.mjs) carry the FACE; the +// locked cover carries the COSTUME + medium (wings / antenna headband / +// pale-yellow button-down / the monarchs / the colored-pencil look) so +// every panel holds character + medium continuity. +const SHOOT_DIR = `${REPO}/portraits/jeffrey/corpus/shoot-2k`; +const ARCHIVE_DIR = `${REPO}/portraits/jeffrey/ig-archive/whistlegraph`; +const COVER_REF = `${LANE}/out/${SLUG}-cover.png`; +const REFS = [ + COVER_REF, // costume + medium anchor (first) + `${SHOOT_DIR}/jeffery-av--07.jpg`, + `${SHOOT_DIR}/jeffery-av--01.jpg`, + `${SHOOT_DIR}/jeffery-av--04.jpg`, + `${ARCHIVE_DIR}/2018-12-02_Bq4ckGFFNtW.jpg`, + `${ARCHIVE_DIR}/2020-09-02_CEpxlO2FOvD.jpg`, + `${ARCHIVE_DIR}/2021-07-10_CRI095Vl7AO_1.jpg`, + `${ARCHIVE_DIR}/2025-01-25_DFQ2lHPzN_W.jpg`, +].filter((p) => { + if (existsSync(p)) return true; + console.warn(` ⚠ ref missing, dropping: ${p}`); + return false; +}); + +function loadOpenAIKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = `${REPO}/aesthetic-computer-vault/.devcontainer/envs/devcontainer.env`; + if (existsSync(vault)) { + for (const line of readFileSync(vault, "utf8").split("\n")) { + if (line.startsWith("OPENAI_API_KEY=")) { + return line.slice("OPENAI_API_KEY=".length).trim().replace(/^['"]|['"]$/g, ""); + } + } + } + throw new Error("OPENAI_API_KEY not set and not found in vault devcontainer.env"); +} + +// ── PORTRAIT recomposition — tall vertical 9:16 staging ────────────── +const PORTRAIT_NOTE = +`PORTRAIT OVERRIDE — compose for a TALL vertical 9:16 frame. a roomy full-figure shot: the keeper stands comfortably WITHIN the park, head near the upper third, the lawn falling away below, sky above — never cropped tight. keep the colored-pencil + gouache medium on warm cream paper, the confident hatching + striping, diegetic park sunlight only.`; + +// ── shared scene law (distilled from fluttabap360.illy.txt cover) ──── +const MEDIUM = +`${COLORED_PENCIL_TOOTH} + +confident hatching and striping build tone — tapered pencil edges, visible paper grain, optical mixing in the knit textures. diegetic light only — clean park sunlight, NO overlay shine, NO glow filter, NO neon. NOT a photograph, NOT cinematic. this is a hand-drawn drawing. the keeper NEVER looks at the camera — his eyes always follow the butterflies.`; + +const KEEPER = +`THE KEEPER — the butterfly cosplayer, recognizable from the jeffrey reference photographs AND the album-cover ref: a gently-grinning adult about 30, soft brown curly hair, light beard. he wears his REAL clothes — a pale-yellow short-sleeve button-down with a small enamel butterfly pin on the collar, and blue jeans; NO tutu, NO leotard, NO costume beyond the wings. strapped to his back are HAND-MADE monarch wings — orange + black markings on paper-mache panels, edges visibly hand-cut, two slim wooden dowels along the bone of each wing; the wings tip + open with his motion. a thin black antenna headband arcs above his hair, springs tipped with two tiny felt balls. he is delighted, easy, alive — the dance is happening TO him.`; + +const MONARCHS = +`THE MONARCHS — real monarch butterflies (orange + black, painted-paper edged, drawn at different scales for depth) that are ALSO the living data of this near-future AESTHETIC COMPUTER nature-park. they drift on their OWN little arcs, each sharing the same turning motion — a current of grace, NEVER a swarm. the park's quiet computation reads ONLY as: a faint hand-ruled ground-grid in the lawn (almost invisible, pencil) and soft hand-drawn DOTTED signal-arcs trailing the butterflies' paths. NO screens, NO monitors, NO glowing UI, NO neon — the park is hand-drawn and warm; the "data" is just painted-paper butterflies + faint pencil grids/arcs.`; + +const PALETTE = +`PALETTE — warm cream paper ground; pale-sky-blue overhead; sap-green park grass; pale-yellow button-down; soft brown hair + warm grey; orange + black on the monarch wings (cosplay + real). hand-drawn, hatched, print-tech-aware. clean park sunlight; no haze, no neon, no glow filters.`; + +const AVOID = +`AVOID — any photographic / cinematic / neon look (this is a colored-pencil + gouache drawing); ANY screen, monitor, laptop, phone, tablet or glowing UI; any swarm of butterflies (a few partners on arcs, never a cloud); any motion-blur / speed-line / glow-filter language; any readable text / wordmark / logo; modern flat tech-illustration style; the keeper looking at or acknowledging the camera; a tutu or ballet costume (his real clothes only).`; + +// ── COVER prompt — the locked album-cover composition, widened ─────── +const COVER_VARIANT = +`COVER COMPOSITION — re-stage the locked album cover for a TALL portrait crop: the keeper caught mid-ballet, rising onto the ball of one foot (relevé) with the other leg extended back into a low arabesque, both arms lifted + curved overhead in a soft port de bras, fingertips reaching. four or five monarchs circle his lifted hands + arched back like dance partners, one resting a beat on his outstretched fingertips. weight floating up, chin lifted, lost in the dance. a soft ground-shadow anchors his standing foot to the green lawn. clean late-morning sun raking from camera-left.`; + +// ── the 11 unique story beats — "the monarch keeper" ───────────────── +// One illustration per UNIQUE name (preview-score reuses by index). +const BEAT_NAMES = [ + "intro", "butterfly", "palofmine", "mommywow", "slinky", + "fly", "ride", "cave", "progression", "land", "button", +]; +const PROOF_NAMES = new Set(["intro", "fly", "button"]); // + cover + +const BEATS = { + intro: +`BEAT — INTRO (dawn, the water-drop) · MOOD: stillness, pre-sun hush. a WIDE low vertical shot of the empty park lawn at first light, dew on the grass. ONE monarch rests folded on a single tall grass blade; a fat dew-drop hangs from the blade tip, caught the instant before it falls. the keeper is FAR OFF and small at the edge of the frame, just arriving, barely awake. cool pale-blue pre-sun sky, long soft shadows, a faint hand-ruled ground-grid in the lawn almost invisible. the world holding its breath before the first note.`, + butterfly: +`BEAT — BUTTERFLY (the hook wakes) · MOOD: recognition, first lift. the keeper, mid-frame now, lifts one open hand and the resting monarch UNFOLDS + lifts off toward his fingertips — the very first flutter. his face lighting up with recognition, weight just beginning to shift onto the ball of one foot. a faint hand-drawn dotted signal-arc trails the butterfly's rising path. morning sun warming the lawn behind him.`, + palofmine: +`BEAT — PALOFMINE (partner found) · MOOD: warm, companionable. a tighter three-quarter shot: the monarch settles for a beat on his outstretched fingertip, the two of them regarding each other — "pal of mine." his curly head tilted, a quiet grin, antenna headband springs tipped with felt balls. one or two more monarchs drift in on their own little arcs at the frame edges. the data has chosen to land on him.`, + mommywow: +`BEAT — MOMMYWOW (the held hush) · MOOD: suspended awe (this lands under a long held chord). the keeper goes very still, both hands cupped softly open at chest height, eyes half-lowered, a monarch hovering a hand-span above his open palms — neither touching. the lawn behind blurs into warm cream paper. time suspended. "mommy — wow."`, + slinky: +`BEAT — SLINKY (the learning spiral) · MOOD: playful, gathering momentum. the keeper begins to MOVE — a loose spiralling step, body uncoiling, arms drawing a long S-curve through the air; three or four monarchs trace the SAME spiralling path beside him on their arcs, learning his shape as he learns theirs. faint hand-drawn dotted signal-arcs describe the shared spiral. the wings on his back tip open with the motion.`, + fly: +`BEAT — FLY (full flight — the big lift) · MOOD: open joy, the brightest beat. the keeper at FULL ballet lift — relevé + low arabesque + port de bras, both arms lifted + curved overhead — four or five monarchs circling his lifted hands + arched back like dance partners, all sharing the same upward turning motion. chin lifted, lost in it, a current of grace. clean late-morning sun. a full-body vertical action shot, camera pulled back + up.`, + ride: +`BEAT — RIDE (the squeak-ride duet) · MOOD: mischievous, fast, a little wild. the keeper mid-turn, ONE monarch swooping in LOW and another answering UP HIGH — a call-and-response chase, his head whipping to follow the high one, a delighted open-mouthed laugh. two crossing hand-drawn dotted signal-arcs (one low, one high) describe the duet. light + quick.`, + cave: +`BEAT — CAVE (dub breakdown, the data rests) · MOOD: somber, breath-held — the ONE dark panel. the park dims: the keeper has stepped into the cool mouth of a computational cave / under a deep arching shade, the lawn falling away to dusk-blue behind him. he stands very still, head bowed, ONE monarch perched on his lowered hand, its wings half-folded + dim — the data resting, the system breathing. the faint hand-ruled ground-grid is a touch more visible in the low light (still hand-drawn pencil, NEVER neon). holding his breath.`, + progression: +`BEAT — PROGRESSION (the recommitment, climbing into bright light) · MOOD: rising, brighter than any pass before. the keeper steps OUT of the cave-shade back into bright light, the monarchs streaming back to him from the lawn — a rising procession of three, four, five butterflies converging on his lifting hands as he RE-ENTERS the dance, higher + brighter. the brightest light yet, sun fully up.`, + land: +`BEAT — LAND (the soft landing) · MOOD: warm, satisfied, spent. the keeper coming gently DOWN out of the lift — descending from relevé back to flat-footed, arms lowering in a soft port de bras — the monarchs settling around him: one returning to rest on his fingertip, others alighting on the grass + on his shoulders. weight returning to the earth. evening-gold light.`, + button: +`BEAT — BUTTON (the last frame) · MOOD: a quiet held breath out. the final image: the keeper at rest on the lawn, the dance done, the monarchs settled, ONE last monarch lifting off his fingertip into the warm gold sky — a quiet goodbye. balanced, light, gently grinning, eyes following the last butterfly up. a soft ground-shadow anchors his foot to the lawn. the end.`, +}; + +function safeName(n) { + return n.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); +} +function build(beatText, isCover) { + const orient = isCover ? "" : `\n\n${PORTRAIT_NOTE}`; + return [MEDIUM, KEEPER, MONARCHS, beatText, PALETTE, AVOID].join("\n\n") + orient + "\n"; +} + +const apiKey = loadOpenAIKey(); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +function buildForm(promptText) { + const fd = new FormData(); + fd.append("model", "gpt-image-2"); + fd.append("prompt", promptText); + fd.append("size", SIZE); + fd.append("quality", "high"); + fd.append("n", "1"); + for (const ref of REFS) { + const buf = readFileSync(ref); + const lower = ref.toLowerCase(); + const ext = lower.endsWith(".png") ? "png" : lower.endsWith(".webp") ? "webp" : "jpeg"; + fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), ref.split("/").pop()); + } + return fd; +} + +// One gpt-image-2 edit call → write outPath. Per-file cached; --force +// regens. Transient 429/5xx + network blips retried; never throws. +async function generate(promptText, outPath, label) { + const rel = outPath.replace(REPO + "/", ""); + if (existsSync(outPath) && !FORCE) { console.log(`✓ cached → ${rel}`); return true; } + console.log(`▸ ${label} · ${SIZE} · ${REFS.length} refs`); + const MAX_TRIES = 4; + for (let attempt = 1; attempt <= MAX_TRIES; attempt++) { + const t0 = Date.now(); + try { + const res = await fetch("https://api.openai.com/v1/images/edits", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}` }, + body: buildForm(promptText), + }); + if (!res.ok) { + const err = await res.text(); + const transient = res.status === 429 || res.status >= 500; + if (transient && attempt < MAX_TRIES) { + const wait = 4000 * attempt; + console.warn(` ⚠ OpenAI ${res.status} (${label}) — retry ${attempt}/${MAX_TRIES - 1} in ${wait / 1000}s`); + await sleep(wait); continue; + } + console.error(`✗ OpenAI ${res.status} (${label}): ${err.slice(0, 600)}`); + return false; + } + const json = await res.json(); + const b64 = json.data?.[0]?.b64_json; + if (!b64) { console.error(`✗ no image (${label}): ${JSON.stringify(json).slice(0, 280)}`); return false; } + writeFileSync(outPath, Buffer.from(b64, "base64")); + const elapsed = ((Date.now() - t0) / 1000).toFixed(1); + const u = json.usage || {}; + const tok = u.input_tokens ? ` · tok in=${u.input_tokens} out=${u.output_tokens}` : ""; + console.log(`✓ ${elapsed}s${tok} → ${rel}`); + return true; + } catch (e) { + const cause = e?.cause?.code || e?.cause?.message || e?.message || "unknown"; + if (attempt < MAX_TRIES) { + const wait = 4000 * attempt; + console.warn(` ⚠ network fail (${label}: ${cause}) — retry ${attempt}/${MAX_TRIES - 1} in ${wait / 1000}s`); + await sleep(wait); continue; + } + console.error(`✗ network fail (${label}): ${cause} — gave up after ${MAX_TRIES} tries`); + return false; + } + } +} + +// --only fly,intro,… regenerates just those beats. +const onlySet = typeof flags.only === "string" + ? new Set(flags.only.split(",").map((x) => x.trim().toLowerCase())) + : null; +const wantBeat = (name) => { + if (PROOF) return PROOF_NAMES.has(name); + if (onlySet) return onlySet.has(name); + return true; +}; +const wantCover = !onlySet || onlySet.has("cover"); + +// Master path for a unique beat illustration (one per NAME, reused +// across struct indices). Underscore prefix so it sorts away from the +// -p-sec-- index files the visualizer actually loads. +const masterPath = (name) => `${LANE}/out/_${SLUG}-beat-${safeName(name)}.png`; + +// ── 1) generate the cover + the 11 unique beat masters ─────────────── +const jobs = []; +if (wantCover) + jobs.push({ prompt: build(COVER_VARIANT, true), out: `${LANE}/out/${SLUG}${TAG}-cover.png`, label: `${SLUG}${TAG} cover` }); +for (const name of BEAT_NAMES) { + if (!wantBeat(name)) continue; + jobs.push({ prompt: build(BEATS[name], false), out: masterPath(name), label: `${SLUG} beat · ${name}` }); +} + +progress.begin({ type: "illy", label: `${SLUG}${TAG} · ${jobs.length} panels${PROOF ? " (PROOF)" : ""}` }); +let done = 0; +for (const job of jobs) { + await generate(job.prompt, job.out, job.label); + progress.update((++done / jobs.length) * 100, { done, total: jobs.length }); +} +progress.end(); + +// ── 2) fan out the unique masters → one file per struct INDEX ──────── +// preview-score.mjs loads -p-sec--.png by section index; +// names repeat across passes, so copy each master into every index that +// shares its name. Skipped in --proof / --no-fan-out. +if (!PROOF && !NO_FAN) { + const structPath = `${LANE}/out/${SLUG}.struct.json`; + if (existsSync(structPath)) { + const struct = JSON.parse(readFileSync(structPath, "utf8")); + const sections = struct.sections.slice().sort((a, b) => a.startSec - b.startSec); + let fanned = 0, missing = 0; + for (let i = 0; i < sections.length; i++) { + const name = sections[i].name; + const src = masterPath(name); + const dst = `${LANE}/out/${SLUG}${TAG}-sec-${i}-${safeName(name)}.png`; + if (!existsSync(src)) { console.warn(` ⚠ no master for "${name}" (§${i}) — run without --only/--proof`); missing++; continue; } + copyFileSync(src, dst); + fanned++; + } + console.log(` fan-out: ${fanned} index panels written${missing ? `, ${missing} missing` : ""}`); + } else { + console.warn(` ⚠ struct missing (${structPath.replace(REPO + "/", "")}) — skip fan-out; run render-fluttabap360.mjs --no-open first`); + } +} + +console.log(`\n✓ ${SLUG} storyline panel set${PROOF ? " (PROOF: cover + intro + fly + button)" : ""} — ${jobs.length} gpt-image-2 job(s) · "the monarch keeper"`); diff --git a/pop/marimba/bin/preview-score-fluttabap360.mjs b/pop/marimba/bin/preview-score-fluttabap360.mjs new file mode 100644 --- /dev/null +++ b/pop/marimba/bin/preview-score-fluttabap360.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env node +// marimba/bin/preview-score-fluttabap360.mjs — thin driver for the +// fluttabap360 storyline visualizer (1080×1920 vertical insta-story). +// +// fluttabap360 reuses the shared marimbaba storyline engine verbatim — +// preview-score.mjs is already --slug-parameterized and its SECTION_TINTS +// map carries the fluttabap360 beat keys. This wrapper just invokes it +// with the right flags so the pipeline reads as one command, and so the +// fluttabap360 cut can grow its own knobs later without touching +// marimbaba's driver. +// +// Pipeline (run in order): +// 1. node pop/marimba/bin/render-fluttabap360.mjs --no-open +// → out/fluttabap360.struct.json (section map) +// ⚠ NOTE: the fluttabap360 engine does NOT yet emit per-note +// `events`, so the visualizer's note-block LANES will be empty +// (the string + panels + backlight + chrome still render). To +// light the lanes, the engine needs to push events like +// render-marimbaba.mjs does (see RELEASES / open questions). +// 2. node pop/marimba/bin/gen-sections-fluttabap360.mjs +// → out/fluttabap360-p-cover.png + 34 -p-sec--.png panels +// (11 unique gpt-image-2 masters fanned out across the passes) +// 3. node pop/marimba/bin/preview-score-fluttabap360.mjs ← this +// → out/fluttabap360-preview-score-portrait-insta-story.mp4 +// +// All flags pass straight through to preview-score.mjs +// (--reel, --start T --frames N for a fast test render, --out, etc). +// +// Usage: +// node pop/marimba/bin/preview-score-fluttabap360.mjs +// node pop/marimba/bin/preview-score-fluttabap360.mjs --reel +// node pop/marimba/bin/preview-score-fluttabap360.mjs --start 154 --frames 30 + +import { spawnSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SCORE = resolve(HERE, "preview-score.mjs"); + +const passthrough = process.argv.slice(2); +const args = [ + SCORE, + "--slug", "fluttabap360", + "--title", "fluttabap360", + ...passthrough, +]; + +const r = spawnSync("node", args, { stdio: "inherit" }); +process.exit(r.status ?? 1); diff --git a/pop/marimba/bin/preview-score.mjs b/pop/marimba/bin/preview-score.mjs --- a/pop/marimba/bin/preview-score.mjs +++ b/pop/marimba/bin/preview-score.mjs @@ -172,6 +172,20 @@ baba1: "rgba(196,176,150,1)", // tense taupe — anxious effort baba2: "rgba(176,196,168,1)", // sage — teamwork sleep1: "rgba(236,196,160,1)", // warm glow — triumph sleep2: "rgba(214,180,180,1)", // soft rose — drowsy close + // fluttabap360 — "the monarch keeper" beats (warm park palette, + // climbing brighter with the C→D→E→C→F key journey). Distinct keys + // from marimbaba's, so this map serves both slugs from one driver. + intro: "rgba(150,170,205,1)", // cool pale dawn-blue — pre-sun still + butterfly: "rgba(214,200,165,1)", // warming oat — the hook wakes + palofmine: "rgba(238,228,205,1)", // warm cream — partner found + mommywow: "rgba(244,224,160,1)", // soft gold-cream — held hush + slinky: "rgba(196,210,150,1)", // sap-green + amber — the spiral + fly: "rgba(248,206,120,1)", // bright amber — full flight + ride: "rgba(214,224,120,1)", // bright lime-gold — mischief + cave: "rgba(110,130,180,1)", // cool dusk-blue (DARK) — breakdown + progression: "rgba(250,212,118,1)", // bright F-gold — recommitment + land: "rgba(238,196,128,1)", // evening-gold — soft landing + button: "rgba(232,188,168,1)", // warm rose-gold — last frame }; const TITLE_PALETTE = ["#f0e6cd", "#a6bcd6", "#b0c4a8", "#eec48c", "#d6b4b4"]; const BACKLIGHT_RGB = "255,196,116"; // deeper warm desk-lamp amber (was too pale) diff --git a/pop/marimba/bin/render-fluttabap360.mjs b/pop/marimba/bin/render-fluttabap360.mjs --- a/pop/marimba/bin/render-fluttabap360.mjs +++ b/pop/marimba/bin/render-fluttabap360.mjs @@ -1,16 +1,20 @@ #!/usr/bin/env node -// render-fattabop360.mjs — FATTABOP360: a fatter, butterier, six-minute +// render-fluttabap360.mjs — FLUTTABAP360: a fatter, butterier, six-minute // cousin of flutterbap. Same butterfly-park DNA + synthesized rhythm // section (../synths/perc.mjs + ../synths/marimba.mjs), but: // -// • exactly 360 s (the arrangement is summed in bars, then the BPM is -// back-solved so the whole form lands on the six-minute mark) +// • exactly 360 s — the form is hard-clamped to 6:00.000 so a player reads +// "6:00", never "6:01" (the final ring fades out right on the boundary) +// • a KEY-CHANGE JOURNEY — the form modulates C → D → E → (home) → F across +// the four passes so the song keeps lifting (see ARRANGEMENT `key:`) // • FATTER low end — buttery: a clean sine sub layered under a grittier, // sub-reinforced bass-perc, warmer master tilt // • more POPPIN + GROOVIN — busier kits, 16th "ride" hats, four-on-floor // lifts, escalating intensity across four passes of the hook -// • more SQUEEEEKIN + RIDIN — dedicated squeak-ride breaks where the -// kitten-screams ride a rhythmic, musical riff over the fat groove +// • SQUEAK-RIDES that SING — short melodic scratch+squeak duets that swell +// IN and OUT of the fight (a hump envelope), each voice on its own +// pitched riff, blooming in a shared space-reverb send +// • HELD pad swells at every modulation + a deeper overall room // // The flutterbap melodic material (butterfly / pal-of-mine / mommy-wow / // slinky / fly / land) is transcribed into reusable BLOCKS and arranged @@ -18,9 +22,9 @@ // over four escalating passes with ride breaks, cave breakdowns, a chord // progression and a buttoned finish. // // Run: -// node pop/marimba/bin/render-fattabop360.mjs -// node pop/marimba/bin/render-fattabop360.mjs --loop # seam-perfect -// node pop/marimba/bin/render-fattabop360.mjs --out ~/x.mp3 +// node pop/marimba/bin/render-fluttabap360.mjs +// node pop/marimba/bin/render-fluttabap360.mjs --loop # seam-perfect +// node pop/marimba/bin/render-fluttabap360.mjs --out ~/x.mp3 // // 4/4. bar = 4·60/BPM. BPM is solved so TOTAL_BARS bars == 360 s. @@ -47,32 +51,49 @@ C6: 84, D6: 86, E6: 88, F6: 89, G6: 91, }; const mf = (m) => 440 * Math.pow(2, (m - 69) / 12); +// ── KEY — global transpose in semitones. The dispatch sets this per section +// so the whole form MODULATES across the six minutes (C → D → E → home → F). +// Every PITCHED route adds gKey; the unpitched kit (kick/snare/hat/shaker) +// ignores it so the groove stays put while the melody climbs. ────────────── +let gKey = 0; +const TR = (m) => m + gKey; + +// ── note-event telemetry for the storyline visualizer. struct.events is +// keyed by the visualizer's voice lanes; each event is { t, midi, dur } in +// seconds, recording the SOUNDING pitch (post-transpose). ────────────────── +const NOTE_EVENTS = { rosewood: [], vibraphone: [], kalimba: [], bass: [], bubbles: [] }; +function EV(lane, t0, midi, beats) { + NOTE_EVENTS[lane].push({ t: +t0.toFixed(4), midi: TR(midi), dur: +(beats * BEAT).toFixed(4) }); +} + // ══════════════════════════════════════════════════════════════════════ // ARRANGEMENT — summed in bars first; the BPM is back-solved from this // so the whole form is exactly TARGET_SEC. opts: { lvl, ride, gMul }. // lvl = groove intensity (0 light → 3 peak). ride = 16th ride-hats. // ══════════════════════════════════════════════════════════════════════ +// `key` (semitones) modulates from here on; sections without it inherit it. const ARRANGEMENT = [ - ["intro", 8], - // ── PASS A — establish the hop (light → driving) ── + ["intro", 10, { key: 0 }], // ── home: C — slow water-drop dawn → build + // ── PASS A — establish the hop (light → driving), in C ── ["butterfly", 4, { lvl: 0 }], ["palofmine", 4, { lvl: 1 }], ["mommywow", 4], ["slinky", 8, { lvl: 1 }], ["fly", 8, { lvl: 2 }], ["ride", 4, { lvl: 2 }], - // ── PASS B — fatter, busier ── - ["butterfly", 4, { lvl: 1, ride: true }], ["palofmine", 4, { lvl: 1, ride: true }], ["mommywow", 4], + // ── PASS B — fatter, busier — LIFT to D (+2) ── + ["butterfly", 4, { lvl: 1, ride: true, key: 2 }], ["palofmine", 4, { lvl: 1, ride: true }], ["mommywow", 4], ["slinky", 8, { lvl: 2 }], ["fly", 8, { lvl: 3 }], - ["ride", 8, { lvl: 3 }], + ["ride", 4, { lvl: 3 }], // (was 8 — the long 2:00 fight, halved + melodic) ["cave", 8], - // ── PASS C — biggest ── - ["butterfly", 4, { lvl: 2, ride: true }], ["palofmine", 4, { lvl: 2, ride: true }], ["mommywow", 4], + // ── PASS C — biggest, brightest — LIFT to E (+4) ── + ["butterfly", 4, { lvl: 2, ride: true, key: 4 }], ["palofmine", 4, { lvl: 2, ride: true }], ["mommywow", 4], ["slinky", 8, { lvl: 2 }], ["fly", 8, { lvl: 3 }], - ["ride", 8, { lvl: 3 }], - // ── PASS D — second wind, then ride it home ── + ["ride", 4, { lvl: 3 }], // (was 8 — halved) + // ── PASS D — second wind in E, drop HOME for a breath, then FINAL lift ── ["butterfly", 4, { lvl: 2, ride: true }], ["palofmine", 4, { lvl: 2, ride: true }], ["slinky", 8, { lvl: 2 }], ["fly", 8, { lvl: 3 }], ["ride", 4, { lvl: 3 }], - ["cave", 8], - ["butterfly", 4, { lvl: 2, ride: true }], ["palofmine", 4, { lvl: 2, ride: true }], ["mommywow", 4], + ["cave", 8, { key: 0 }], // home breakdown — the calm before the lift + // FINAL CHORUS — euphoric jump up to F (+5), and ride it out + ["butterfly", 4, { lvl: 2, ride: true, key: 5 }], ["palofmine", 4, { lvl: 2, ride: true }], ["mommywow", 4], ["slinky", 8, { lvl: 2 }], ["fly", 8, { lvl: 3 }], ["progression", 4], ["land", 4], ["button", 1], ]; @@ -126,28 +147,43 @@ const snareBuf = new Float32Array(ns); const hatBuf = new Float32Array(ns); const shakBuf = new Float32Array(ns); const caveBuf = new Float32Array(ns); +const padBuf = new Float32Array(ns); // long held swells — the sustained "held notes" +const dropBuf = new Float32Array(ns); // water-drop intro — echo + big reverb at mixdown // ── mallet voice routes ──────────────────────────────────────────────── +// MAIN MARIMBA LEAD — softened: a longer, rounder attack, a longer mallet +// contact (low-passes the bright transient), the stick-click noise removed, +// and the glassy upper partials pulled back. Softer touch, less harsh, with +// a touch more ring. (Per-call opts so only the lead is softened.) function lead(t0, midi, beats, gain = 1.0) { - mixEventMarimba({ startSec: jit(t0, 13), midi, durSec: beats * BEAT, gain: vel(gain, 0.14), preset: "xylophone", decayMul: 1.15 }, leadBuf, { sampleRate: SR }); + EV("rosewood", t0, midi, beats); + mixEventMarimba( + { startSec: jit(t0, 13), midi: TR(midi), durSec: beats * BEAT, gain: vel(gain, 0.14), preset: "xylophone", decayMul: 1.3 }, + leadBuf, + { sampleRate: SR, attack: 0.0032, params: { mallet: 0.0032, noise: 0.0, amps: [1.0, 0.34, 0.13, 0.04], resGain: 0.42 } }, + ); } function bass(t0, midi, beats, gain = 0.85) { - mixEventMarimba({ startSec: jit(t0, 9), midi, durSec: beats * BEAT, gain: vel(gain, 0.12), preset: "bass", decayMul: 0.55 }, bassBuf, { sampleRate: SR }); + EV("bass", t0, midi, beats); + mixEventMarimba({ startSec: jit(t0, 9), midi: TR(midi), durSec: beats * BEAT, gain: vel(gain, 0.12), preset: "bass", decayMul: 0.55 }, bassBuf, { sampleRate: SR }); } function spark(t0, midi, beats, gain = 0.55) { - mixEventMarimba({ startSec: jit(t0, 14), midi, durSec: beats * BEAT, gain: vel(gain, 0.16), preset: "kalimba", decayMul: 0.95 }, sparkBuf, { sampleRate: SR }); + EV("kalimba", t0, midi, beats); + mixEventMarimba({ startSec: jit(t0, 14), midi: TR(midi), durSec: beats * BEAT, gain: vel(gain, 0.16), preset: "kalimba", decayMul: 0.95 }, sparkBuf, { sampleRate: SR }); } function bell(t0, midi, beats, gain = 0.65) { - mixEventMarimba({ startSec: jit(t0, 10), midi, durSec: beats * BEAT, gain: vel(gain, 0.12), preset: "glockenspiel", decayMul: 1.35 }, bellBuf, { sampleRate: SR }); + EV("bubbles", t0, midi, beats); + mixEventMarimba({ startSec: jit(t0, 10), midi: TR(midi), durSec: beats * BEAT, gain: vel(gain, 0.12), preset: "glockenspiel", decayMul: 1.35 }, bellBuf, { sampleRate: SR }); } function vib(t0, midi, beats, gain = 0.4) { - mixEventMarimba({ startSec: jit(t0, 18), midi, durSec: beats * BEAT, gain: vel(gain, 0.10), preset: "vibraphone_off", decayMul: 1.0 }, vibBuf, { sampleRate: SR }); + EV("vibraphone", t0, midi, beats); + mixEventMarimba({ startSec: jit(t0, 18), midi: TR(midi), durSec: beats * BEAT, gain: vel(gain, 0.10), preset: "vibraphone_off", decayMul: 1.0 }, vibBuf, { sampleRate: SR }); } // ── BUTTER SUB — a clean enveloped sine an octave anchor under the root. // This is the "like butter" weight: smooth, round, felt more than heard. ─ function SUBSINE(ab, beat, midi, beats, g = 0.5) { const start = jit(t(ab, beat), 4); - const f = mf(midi), dur = beats * BEAT; + const f = mf(TR(midi)), dur = beats * BEAT; const n0 = Math.floor(start * SR); const len = Math.ceil((dur * 1.2 + 0.05) * SR); const attS = Math.floor(0.008 * SR), relA = Math.log(1000) / (dur * 0.9); @@ -161,6 +197,59 @@ deepBuf[d] += Math.sin(2 * Math.PI * phase) * env * g; } } +// ── HELD PAD — a slow-attack sustained chord that BLOOMS across `bars`. +// These are the held notes that open each new key and float under the hush: +// a soft choir of detuned partials (root + octave + fifth-ish) routed to +// padBuf, which is fed into the space reverb at mixdown so it sits deep. ── +function PAD(ab, midis, bars, g = 0.4) { + const start = t(ab, 0), dur = bars * BAR; + const n0 = Math.floor(start * SR); + const len = Math.ceil((dur + 0.5) * SR); + const att = Math.max(1, Math.floor(dur * 0.42 * SR)); // slow swell IN + const rel = Math.floor(0.7 * SR); + const dt = 1 / SR; + for (const m of midis) { + const f = mf(TR(m)); let phase = 0; + for (let i = 0; i < len; i++) { + const d = n0 + i; if (d < 0 || d >= ns) continue; + phase += f * dt; + let env = 1; + if (i < att) env = i / att; else if (i > len - rel) env = Math.max(0, (len - i) / rel); + const s = Math.sin(2 * Math.PI * phase) + + 0.5 * Math.sin(2 * Math.PI * phase * 1.5) // a fifth of shimmer + + 0.35 * Math.sin(2 * Math.PI * phase * 0.5); // sub octave warmth + padBuf[d] += s * env * g * 0.45; + } + } +} + +// ── WATER DROP — a short pitched "plink": a sine that chirps UP fast then +// decays away, with a tiny click attack. Pitched high + placed irregularly = +// rain falling on a still pond. Routed to dropBuf, which gets a tempo-synced +// echo + a big reverb at mixdown (the "lots of echoes" of the dawn). ── +function drop(t0, f0, g = 0.45, dur = 0.24) { + const n0 = Math.floor(t0 * SR), ln = Math.ceil(dur * SR), dt = 1 / SR; + let ph = 0; + for (let i = 0; i < ln; i++) { + const d = n0 + i; if (d < 0 || d >= ns) continue; + const u = i / ln; + const f = f0 * (1 + 1.7 * Math.min(1, u * 3.4)); // fast upward chirp = the plink + ph += f * dt; + let env = Math.exp(-u * 6.2); + if (i < 20) env *= i / 20; // tiny click attack + const s = Math.sin(2 * Math.PI * ph) + 0.16 * Math.sin(4 * Math.PI * ph); + dropBuf[d] += s * env * g * 0.6; + } + // telemetry: water drops light the bubbles lane (pitch from the chirp f0) + NOTE_EVENTS.bubbles.push({ t: +t0.toFixed(4), midi: Math.round(69 + 12 * Math.log2(f0 / 440)), dur: +dur.toFixed(4) }); +} +// ── PLINK — a long-decay glassy tone into dropBuf (rides the same echo + +// reverb): the slow intro's sparse melody with the "long decaaaays". ── +function plink(t0, midi, beats, g = 0.5) { + EV("bubbles", t0, midi, beats); + mixEventMarimba({ startSec: t0, midi: TR(midi), durSec: beats * BEAT, gain: g, preset: "glockenspiel", decayMul: 2.8 }, dropBuf, { sampleRate: SR }); +} + // ── kit routes ────────────────────────────────────────────────────────── function KICK(ab, beat, g = 0.95, o = {}) { mixKick({ startSec: jit(t(ab, beat), 4), gain: vel(g, 0.10), fEnd: 40, ampDecay: 0.48, pitchDecay: 0.064, drive: 1.85, ...o }, kickBuf, { sampleRate: SR }); @@ -176,10 +265,10 @@ mixShaker({ startSec: jit(t(ab, beat), 7), gain: vel(g, 0.22) }, shakBuf, { sampleRate: SR }); } // FAT bass-perc — extra sub reinforcement + grit by default. function BP(ab, beat, midi, durBeats, g = 0.8, o = {}) { - mixBassPerc({ startSec: jit(t(ab, beat), 5), midi, durSec: durBeats * BEAT, gain: vel(g, 0.12), sub: 0.58, drive: 2.0, ...o }, subBuf, { sampleRate: SR }); + mixBassPerc({ startSec: jit(t(ab, beat), 5), midi: TR(midi), durSec: durBeats * BEAT, gain: vel(g, 0.12), sub: 0.58, drive: 2.0, ...o }, subBuf, { sampleRate: SR }); } function rise(landBar, dur, midis, g) { - mixReverseBell({ landSec: t(landBar, 0), dur, midis, gain: g }, riseBuf, { sampleRate: SR }); + mixReverseBell({ landSec: t(landBar, 0), dur, midis: midis.map(TR), gain: g }, riseBuf, { sampleRate: SR }); } function riseKick(landBar, dur, g) { mixReverseKick({ landSec: t(landBar, 0), dur, gain: g }, kickBuf, { sampleRate: SR }); @@ -206,6 +295,18 @@ for (let i = 0; i < out.length; i++) { const bd = ring[i % d], y = -g * out[i] + bd; ring[i % d] = out[i] + g * y; out[i] = y; } } for (let i = 0; i < buf.length; i++) buf[i] = buf[i] * (1 - wet) + out[i] * wet; } +// feedback delay (echo). `time` in seconds; `fb` = feedback amount (→ how many +// repeats); `wet` = dry/echo mix. Used to drench the water-drop intro. +function delayInPlace(buf, { time, fb = 0.45, wet = 0.5 } = {}) { + const d = Math.max(1, Math.floor(time * SR)); + const out = new Float32Array(buf.length); + for (let i = 0; i < buf.length; i++) { + let y = buf[i]; + if (i >= d) y += out[i - d] * fb; // feed the delayed output back in + out[i] = y; + } + for (let i = 0; i < buf.length; i++) buf[i] = buf[i] * (1 - wet) + out[i] * wet; +} const SCR = { baby: { rate: 5.5, depth: 2.0, tone: 0.5, dur: 0.30 }, scribble: { rate: 14, depth: 1.6, tone: 0.6, dur: 0.40 }, @@ -213,10 +314,10 @@ transform: { rate: 5, depth: 2.2, gate: 12, gateDuty: 0.5, tone: 0.55, dur: 0.42 }, chirp: { rate: 7, depth: 3.0, gate: 7, gateDuty: 0.55, tone: 0.6, dur: 0.26 }, }; function SC(ab, beat, kind, g = 0.5, midi = 50) { - mixScratch({ startSec: jit(t(ab, beat), 8), midi, gain: vel(g, 0.15), ...SCR[kind] }, scrBuf, { sampleRate: SR }); + mixScratch({ startSec: jit(t(ab, beat), 8), midi: TR(midi), gain: vel(g, 0.15), ...SCR[kind] }, scrBuf, { sampleRate: SR }); } function SCREAM(ab, beat, midi, g = 0.4, o = {}) { - mixScream({ startSec: jit(t(ab, beat), 10), midi, gain: vel(g, 0.16), ...o }, screamBuf, { sampleRate: SR }); + mixScream({ startSec: jit(t(ab, beat), 10), midi: TR(midi), gain: vel(g, 0.16), ...o }, screamBuf, { sampleRate: SR }); } function hats8(ab, g = 0.26, openLast = false) { for (let e = 0; e < 8; e++) HAT(ab, e * 0.5, e % 2 === 1 ? g : g * 0.55, { open: openLast && e === 7 }); @@ -310,18 +411,48 @@ // BLOCKS — each schedules everything for its span starting at abs bar ab // ══════════════════════════════════════════════════════════════════════ const BLOCKS = { intro(ab, bars) { - // fat build: groove already moving at bar 0, crescendos into the hook. + // SLOW WATER-DROP DAWN → gather into the hook. The first stretch is + // BEATLESS: a warm held pad, a soft sub drone, and irregular water-drop + // plinks with long decays — all drenched in echo + reverb (dropBuf is + // delayed + reverbed at mixdown). A sparse, wandering, non-repeating + // melody drifts upward over it. Only in the last few bars does the groove + // creep in and build into the first chorus. Slow to start, lots of space. + const amb = Math.max(3, bars - 5); // beatless ambient bars + PAD(ab, [N.C3, N.G3, N.C4, N.E4], bars + 1, 0.26); // warm held bed + SUBSINE(ab, 0, N.C2, amb * 4, 0.18); // soft sub drone under the dawn (eased in at mixdown) + + // ── water drops — sparse + IRREGULAR (off the grid, never cyclical) ── + const HZ = [1760, 2090, 1320, 1900, 1500, 2350, 1150, 1980, 1640, 2210]; + let di = 0; for (let i = 0; i < bars; i++) { const b = ab + i; - KICK(b, 0, 0.84 + i * 0.02); KICK(b, 2, 0.7 + i * 0.02); - BP(b, 0, N.C2, 1.6, 0.8); BP(b, 2.5, N.G1, 1.0, 0.7); - SUBSINE(b, 0, N.C2, 2.4, 0.5); - for (let e = 0; e < 4; e++) SHK(b, e + 0.5, 0.18); - if (i >= 2) hats8(b, 0.18 + i * 0.02); - if (i >= 4) { SNR(b, 1, 0.5); SNR(b, 3, 0.55); } - if (i >= 6) rideHats(b, 0.16); + const nd = i < amb ? (i % 2 ? 3 : 2) : 1; // a few per bar early, thinning as the groove enters + for (let k = 0; k < nd; k++) { + const beat = (k * 1.45 + (i % 3) * 0.7 + hrand() * 0.6) % 4; // irregular placement + drop(t(b, beat), HZ[di++ % HZ.length] * (1 + hr2() * 0.05), Math.max(0.18, 0.46 - i * 0.02)); + } } - bell(t(ab, 0), N.C6, 3, 0.34); bell(t(ab, 2), N.G5, 2, 0.3); + // ── a slow, sparse, NON-repeating melody (long decays) that gathers ── + const MEL = [[0, 1, "G4", 3], [1, 3, "C5", 4], [3, 0.5, "E5", 4], [4, 2, "D5", 3], + [amb - 1, 1, "G5", 3], [amb, 0, "C5", 3], [amb + 1, 2, "E5", 3], + [amb + 2, 0, "G5", 2], [amb + 2, 2.5, "A5", 2]]; + for (const [bb, beat, note, dur] of MEL) if (bb < bars) plink(t(ab + bb, beat), N[note], dur, Math.max(0.34, 0.5 - bb * 0.012)); + + // ── groove creeps in for the last (bars-amb) bars and builds ── + for (let i = amb; i < bars; i++) { + const b = ab + i, j = i - amb; + KICK(b, 0, 0.55 + j * 0.07); if (j >= 1) KICK(b, 2, 0.5 + j * 0.06); + BP(b, 0, N.C2, 1.6, 0.55 + j * 0.06); BP(b, 2.5, N.G1, 1.0, 0.5 + j * 0.05); + SUBSINE(b, 0, N.C2, 2.4, 0.42); + for (let e = 0; e < 4; e++) SHK(b, e + 0.5, 0.10 + j * 0.02); + if (j >= 1) hats8(b, 0.14 + j * 0.025); + if (j >= 2) { SNR(b, 1, 0.42); SNR(b, 3, 0.48); } + if (j >= 3) rideHats(b, 0.16); + } + // ── foreshadow the butterfly hook on the (now softer) lead, late ── + const TEASE = [[bars - 3, 0, "E5", 1], [bars - 3, 1, "G5", 1], [bars - 3, 2, "C6", 2], + [bars - 2, 0, "G5", 1], [bars - 2, 2, "E5", 2]]; + for (const [bb, beat, note, dur] of TEASE) lead(t(ab + bb, beat), N[note], dur, 0.5); for (const fb of [0, 0.25, 0.5, 0.75]) SNR(ab + bars - 1, 3 + fb, 0.4 + fb * 0.8); // fill into the hook spark(t(ab + bars - 1, 3.5), N.C6, 0.5, 0.5); rise(ab + bars, 1.6 * BAR, [N.G5, N.C6, N.E6, N.G6], 0.46); // → THE START @@ -349,6 +480,7 @@ // HUSH — pull the kit back for the held wonder, keep the butter sub long playLead(ab, "mommywow"); for (let i = 0; i < bars; i++) bass(t(ab + i, 0), N.C3, 4, 0.5); vibTriad(ab, 0, TRIAD.C, 4 * bars, 0.28); + PAD(ab, [N.C4, N.E4, N.G4, N.C5], bars, 0.42); // a long held chord blooms under the wonder for (let i = 0; i < bars; i++) { KICK(ab + i, 0, 0.6); BP(ab + i, 0, N.C2, 3.6, 0.6, { drive: 1.6, decay: 0.9 }); @@ -398,32 +530,41 @@ SCREAM(ab + 3, 3.5, 86, 0.38, { bend: 10, rasp: 0.6, dur: 0.42 }); if (o.lvl >= 3) for (const fb of [0, 0.25, 0.5, 0.75]) SNR(ab + bars - 1, 3 + fb, 0.35 + fb * 0.7); }, ride(ab, bars, o = {}) { - // SQUEEEK RIDIN — fat groove + 16th ride hats + a rhythmic, musical - // kitten-scream riff that rides the beat (low rasp = it sings, not shrieks). - const lvl = o.lvl ?? 2; + // SQUEAK-RIDE — a SHORT, melodic scratch+squeak duet that SWELLS in and + // out of the fight instead of grinding flat the whole time. The scratch + // SINGS a little climbing call (a pitched, buzzy "aah"), the squeak + // ANSWERS it up high; both bloom in the space reverb (scrBuf/screamBuf → + // space send), so the fight sits BACK in the mix and breathes. The fat + // groove + 16th ride hats hold the floor underneath. + const lvl = o.lvl ?? 3; layFatGroove(ab, bars, { lvl, subRoots: [N.C2, N.C2, N.G1, N.A1], ride: true, kg: 0.96 }); - const riff = [[0, 88], [1, 84], [1.5, 91], [2, 86], [3, 93], [3.5, 88]]; // beat, midi + // a real two-bar phrase: the scratch calls, the squeak answers, on pitches. + const CALL = [[0.0, N.C5, "scribble"], [0.75, N.E5, "chirp"], [1.5, N.G5, "scribble"]]; + const ANSWER = [[2.0, N.G5], [2.75, N.C6], [3.5, N.E6]]; // squeak tops the call for (let i = 0; i < bars; i++) { - for (const [beat, midi] of riff) { - const m = midi + (i % 2 ? -2 : 0) + (i % 4 === 3 ? 3 : 0); - SCREAM(ab + i, beat, m, 0.34, { bend: 5, rasp: 0.4, dur: 0.28 }); - } - SC(ab + i, 0.5, i % 2 ? "chirp" : "scribble", 0.42, 50 + (i % 3) * 3); - SC(ab + i, 2.5, "transform", 0.4, 48); - spark(t(ab + i, 3.75), N.E6, 0.5, 0.4); + // a hump envelope across the span — quiet at the edges, loud in the + // middle — so the duet DROPS IN, peaks, and DROPS OUT of the groove. + const swell = Math.sin(((i + 0.5) / bars) * Math.PI); // 0 → 1 → 0 + const g = 0.18 + 0.40 * swell; + const motif = (i % 2 ? 2 : 0) + (i === bars - 1 ? 4 : 0); // a touch of internal motion + for (const [beat, midi, kind] of CALL) + SC(ab + i, beat, kind, g, midi + motif); + for (const [beat, midi] of ANSWER) + SCREAM(ab + i, beat, midi + motif, g * 0.85, { bend: 4, rasp: 0.32, dur: 0.30 }); + spark(t(ab + i, 3.75), N.E6, 0.5, 0.36 * swell); } rise(ab, 1.2 * BAR, [N.C6, N.E6, N.G6], 0.4); }, cave(ab, bars) { // fat dub breakdown — sparse + drenched (routed into caveBuf → reverb). - const caveVib = (tt, m, beats, g) => mixEventMarimba({ startSec: tt, midi: m, durSec: beats * BEAT, gain: g, preset: "vibraphone_off", decayMul: 1.5 }, caveBuf, { sampleRate: SR }); - const caveLead = (tt, m, beats, g) => mixEventMarimba({ startSec: tt, midi: m, durSec: beats * BEAT, gain: g, preset: "xylophone", decayMul: 1.4 }, caveBuf, { sampleRate: SR }); + const caveVib = (tt, m, beats, g) => mixEventMarimba({ startSec: tt, midi: TR(m), durSec: beats * BEAT, gain: g, preset: "vibraphone_off", decayMul: 1.5 }, caveBuf, { sampleRate: SR }); + const caveLead = (tt, m, beats, g) => mixEventMarimba({ startSec: tt, midi: TR(m), durSec: beats * BEAT, gain: g, preset: "xylophone", decayMul: 1.4 }, caveBuf, { sampleRate: SR }); for (let half = 0; half < bars; half += 4) { for (const m of TRIAD.Am) caveVib(t(ab + half, 0), m, 7, 0.30); for (const m of TRIAD.F) caveVib(t(ab + half + 2, 0), m, 7, 0.28); caveLead(t(ab + half, 1), N.E5, 1, 0.48); caveLead(t(ab + half, 2.5), N.A5, 1.5, 0.44); caveLead(t(ab + half + 2, 1), N.C6, 1, 0.44); caveLead(t(ab + half + 2, 2.5), N.G5, 1.5, 0.4); - mixReverseBell({ landSec: t(ab + half, 0), dur: 1.4 * BAR, midis: [N.A5, N.C6, N.E6], gain: 0.38 }, caveBuf, { sampleRate: SR }); + mixReverseBell({ landSec: t(ab + half, 0), dur: 1.4 * BAR, midis: [N.A5, N.C6, N.E6].map(TR), gain: 0.38 }, caveBuf, { sampleRate: SR }); mixKick({ startSec: t(ab + half, 0), gain: 0.45, fEnd: 38, ampDecay: 0.6 }, caveBuf, { sampleRate: SR }); mixKick({ startSec: t(ab + half + 1, 2), gain: 0.45, fEnd: 38, ampDecay: 0.6 }, caveBuf, { sampleRate: SR }); rhat(caveBuf, ab + half + 1, 0, 0.32); @@ -478,9 +619,19 @@ // ── DISPATCH — lay every block at its cumulative bar offset ───────────── let cursor = 0; const placed = []; +let firstKeySet = false; for (const [name, bars, opts] of ARRANGEMENT) { - BLOCKS[name](cursor, bars, opts ?? {}); - placed.push({ name, bar0: cursor, bars }); + const o = opts ?? {}; + if (o.key !== undefined) { + const changed = o.key !== gKey; + gKey = o.key; + // on a real modulation (not the opening key set), open the new key with a + // held tonic-triad swell so the change lands as a bloom of held notes. + if (changed && firstKeySet) PAD(cursor, [N.C4, N.E4, N.G4], Math.min(bars, 4), 0.5); + firstKeySet = true; + } + BLOCKS[name](cursor, bars, o); + placed.push({ name, bar0: cursor, bars, key: gKey }); cursor += bars; } @@ -499,8 +650,8 @@ place(bassBuf, 0.00, 0.66); place(vibBuf, -0.24, 0.88); place(deepBuf, 0.00, 0.92); // butter sub, dead centre place(riseBuf, 0.00, 0.85); -place(scrBuf, 0.34, 0.78); -place(screamBuf,-0.48, 0.58); // the squeaks, wide-left, proud-but-musical +place(scrBuf, 0.34, 0.60); // a touch drier here — the space send does the rest +place(screamBuf,-0.48, 0.48); // the squeaks, wide-left, proud-but-musical, sitting back place(sparkBuf, 0.44, 0.86); place(bellBuf, -0.42, 0.84); place(kickBuf, 0.00, 1.02); @@ -511,6 +662,31 @@ place(shakBuf, -0.34, 0.48); reverbInPlace(caveBuf, { wet: 0.62, decay: 0.82 }); place(caveBuf, 0.00, 0.92); +// ── HELD PAD — place the sustained swells, centred and warm ── +place(padBuf, 0.00, 0.60); + +// ── WATER-DROP INTRO — tempo-synced (dotted-8th) echoes + a long reverb tail, +// so the drops + sparse plinks ring out and overlap like rain on a pond. ── +delayInPlace(dropBuf, { time: BEAT * 0.75, fb: 0.5, wet: 0.6 }); +reverbInPlace(dropBuf, { wet: 0.7, decay: 0.9 }); +place(dropBuf, 0.00, 0.92); + +// ── SPACE — a shared room-reverb SEND to deepen the whole field. The kit + +// sub stay DRY (fat, punchy, up front); the melodic + squeak voices bloom +// into a big room so the scratches sit BACK and everything gets depth. ── +const spaceSend = new Float32Array(ns); +const addSend = (buf, g) => { for (let i = 0; i < ns; i++) spaceSend[i] += buf[i] * g; }; +addSend(leadBuf, 0.16); +addSend(bellBuf, 0.30); +addSend(sparkBuf, 0.26); +addSend(vibBuf, 0.24); +addSend(padBuf, 0.46); +addSend(scrBuf, 0.46); // scratches bloom in the room +addSend(screamBuf, 0.52); // squeaks sit back with reverb +addSend(riseBuf, 0.20); +reverbInPlace(spaceSend, { wet: 1.0, decay: 0.88 }); // 100%-wet return +place(spaceSend, 0.00, 0.52); + // scrub non-finite let nan = 0; for (let i = 0; i < ns; i++) { @@ -538,12 +714,17 @@ } else { let lastLoud = outN - 1; while (lastLoud > 0 && Math.abs(outL[lastLoud]) < 0.004 && Math.abs(outR[lastLoud]) < 0.004) lastLoud--; trimN = Math.min(outN, lastLoud + Math.floor(0.6 * SR)); - const fadeIn = Math.floor(0.004 * SR), fadeOut = Math.floor(1.6 * SR); - for (let i = 0; i < fadeIn && i < trimN; i++) { const g = i / fadeIn; outL[i] *= g; outR[i] *= g; } + // HARD-CLAMP to exactly TARGET_SEC so a player reads "6:00.000", never "6:01" + // (the final ring just fades out right on the six-minute boundary). + trimN = Math.min(trimN, Math.round(TARGET_SEC * SR)); + // a long EASED intro swell so the water-drop dawn emerges from silence and + // BUILDS (instead of opening at full sub-drone level). pow > 1 = slow start. + const fadeIn = Math.floor(6.0 * SR), fadeOut = Math.floor(1.6 * SR); + for (let i = 0; i < fadeIn && i < trimN; i++) { const g = Math.pow(i / fadeIn, 1.7); outL[i] *= g; outR[i] *= g; } for (let i = 0; i < fadeOut && i < trimN; i++) { const idx = trimN - 1 - i, g = i / fadeOut; outL[idx] *= g; outR[idx] *= g; } } -console.log(`→ fattabop360 · ${TOTAL_BARS} bars · C major 4/4 @ ${BPM.toFixed(1)} BPM · ${(trimN / SR).toFixed(1)} s · FAT bass-perc + butter sub · ride hats · squeak-ride · swing=${SWING.toFixed(2)}${LOOP ? " · LOOP" : ""}`); +console.log(`→ fluttabap360 · ${TOTAL_BARS} bars · 4/4 @ ${BPM.toFixed(1)} BPM · ${(trimN / SR).toFixed(3)} s · modulates C→D→E→(home)→F · FAT bass-perc + butter sub · ride hats · melodic squeak-rides + space reverb · swing=${SWING.toFixed(2)}${LOOP ? " · LOOP" : ""}`); // ── write out ──────────────────────────────────────────────────────── function expandHome(p) { @@ -552,7 +733,7 @@ if (p === "~") return homedir(); if (p.startsWith("~/")) return resolve(homedir(), p.slice(2)); return p; } -const outPath = expandHome(_argi("--out")) || resolve(HERE, "..", "out", LOOP ? "fattabop360-loop.mp3" : "fattabop360.mp3"); +const outPath = expandHome(_argi("--out")) || resolve(HERE, "..", "out", LOOP ? "fluttabap360-loop.mp3" : "fluttabap360.mp3"); mkdirSync(dirname(outPath), { recursive: true }); const rawPath = `${outPath}.f32.raw`; const b = Buffer.alloc(trimN * 2 * 4); @@ -592,10 +773,14 @@ // struct.json — section map { const structTotalSec = trimN / SR; const struct = { - _comment: "Section map for fattabop360 (the fat six-minute butterfly hop). 4/4. BPM back-solved so the form == 360 s. C major.", + _comment: "Section map for fluttabap360 (the fat six-minute butterfly hop). 4/4. BPM back-solved + hard-clamped so the form == 360 s exactly. Modulates C→D→E→(home C)→F; keyOffset is semitones above the C home key.", meter: 4, bpm: +BPM.toFixed(3), scale: "major", rootMidi: 60, totalSec: +structTotalSec.toFixed(6), prerollSec: 0, - sections: placed.map((s) => ({ name: s.name, startSec: +t(s.bar0, 0).toFixed(6), endSec: +Math.min(structTotalSec, t(s.bar0 + s.bars, 0)).toFixed(6) })), + sections: placed.map((s) => ({ name: s.name, keyOffset: s.key, startSec: +t(s.bar0, 0).toFixed(6), endSec: +Math.min(structTotalSec, t(s.bar0 + s.bars, 0)).toFixed(6) })), + // per-note events for the storyline visualizer (lanes ride the playhead + // string). Keyed by voice lane; each { t, midi, dur } in seconds. + events: Object.fromEntries(Object.entries(NOTE_EVENTS).map( + ([lane, evs]) => [lane, evs.filter((e) => e.t < structTotalSec).sort((a, b) => a.t - b.t)])), }; - writeFileSync(resolve(HERE, "..", "out", "fattabop360.struct.json"), JSON.stringify(struct, null, 2) + "\n"); + writeFileSync(resolve(HERE, "..", "out", "fluttabap360.struct.json"), JSON.stringify(struct, null, 2) + "\n"); } diff --git a/pop/marimba/fluttabap360-forms.json b/pop/marimba/fluttabap360-forms.json new file mode 100644 --- /dev/null +++ b/pop/marimba/fluttabap360-forms.json @@ -0,0 +1,18 @@ +{ + "_comment": "Figure + face bounding boxes per unique story beat, in panel-image coords (1024x1536, the portrait monarch-keeper set). Keyed by section NAME (preview-score.mjs looks up FORMS by SECTIONS[i].name; names repeat across the C/D/E/F passes, so ONE entry per unique name covers every pass). Single figure (the keeper) per beat -> one box under `jeffrey`. `face` drives the in/out face zoom + the warm sun-rim halo; `figure` drives the per-figure backlight. RE-EYEBALLED 2026-06-24 against the real generated masters (out/_fluttabap360-beat-.png). intro keeper is tiny + far (mid-right, at the horizon line). fly/cover/progression are full-body lift poses (head high, arms overhead). butterfly/palofmine/slinky/ride/land/button are standing/striding full-body shots with the head in the upper third.", + "imgW": 1024, + "imgH": 1536, + "sections": { + "intro": { "jeffrey": { "figure": [278, 295, 78, 130], "face": [296, 298, 46, 52] } }, + "butterfly": { "jeffrey": { "figure": [90, 120, 280, 470], "face": [150, 128, 100, 110] } }, + "palofmine": { "jeffrey": { "figure": [70, 80, 290, 490], "face": [176, 88, 100, 108] } }, + "mommywow": { "jeffrey": { "figure": [120, 68, 200, 500], "face": [164, 70, 98, 110] } }, + "slinky": { "jeffrey": { "figure": [120, 98, 230, 450], "face": [184, 100, 92, 108] } }, + "fly": { "jeffrey": { "figure": [90, 128, 250, 392], "face": [128, 128, 92, 108] } }, + "ride": { "jeffrey": { "figure": [120, 128, 210, 410], "face": [174, 130, 94, 108] } }, + "cave": { "jeffrey": { "figure": [115, 74, 200, 482], "face": [164, 76, 96, 108] } }, + "progression":{ "jeffrey": { "figure": [110, 128, 222, 418], "face": [164, 130, 94, 108] } }, + "land": { "jeffrey": { "figure": [115, 108, 218, 434], "face": [164, 110, 94, 108] } }, + "button": { "jeffrey": { "figure": [108, 98, 234, 464], "face": [138, 98, 94, 108] } } + } +} diff --git a/pop/marimba/fluttabap360.illy.txt b/pop/marimba/fluttabap360.illy.txt --- a/pop/marimba/fluttabap360.illy.txt +++ b/pop/marimba/fluttabap360.illy.txt @@ -1,4 +1,4 @@ -fattabop360 — album cover for a fat, fast, six-minute butterfly-park pop track. +fluttabap360 — album cover: the butterfly cosplayer, dancing ballet with the butterflies. a colored-pencil + gouache drawing on warm cream paper, square. confident hatching and striping build tone — tapered pencil edges, visible paper @@ -6,38 +6,37 @@ grain, optical mixing in the knit textures. diegetic light only — clean late-morning sun raking in from camera-left, no overlay shine, no glow filter. NOT a photograph, NOT cinematic. -the whole image reads SPED-UP — caught mid-rush. a single butterfly -cosplayer is in joyful FORWARD MOTION across the frame, leaning into a -groove, hair and collar streaming back. their monarch wings are mid-beat, -tilted on a dynamic diagonal as if just launched. hand-drawn SPEED LINES -and soft directional motion-blur streak off the wing edges, the antenna -springs, and the trailing arm — tapered pencil strokes that smear the -warm air into the direction of travel. a couple of light repeated -"ghost" echoes of the wing edge trail behind, like fast frames of a flip -book. the figure is a little off-center, pulled toward camera-right by -the motion, NOT a static centered hero. +the same butterfly cosplayer from before — a gently-grinning adult about +30, soft brown curly hair, wide awake and lit up with the joy of it. +caught mid-ballet: a light, graceful pose — rising onto the ball of one +foot (relevé), the other leg extended back into a low arabesque, both +arms lifted and curved overhead in a soft port de bras, fingertips +reaching. weight floating up, chin lifted, spine long. not athletic-tense +— easy and delighted, like the dance is happening TO him. + +he wears hand-made monarch wings strapped to his back — orange + black +markings on paper-mache panels, edges visibly hand-cut, two slim wooden +dowels along the bone of each wing; the wings tip and open with the lift +of the pose. a pale-yellow short-sleeve button-down, a small enamel +butterfly pin on the collar. a thin black antenna headband arcs above his +hair, springs tipped with two tiny felt balls. NO tutu, NO leotard, NO +ballet costume — his real clothes; only the pose is ballet. -the figure is the butterfly cosplayer from before, a gently-grinning -adult about 30, wide awake and mid-laugh with the speed of it. they wear -hand-made monarch wings strapped to their back — orange + black markings -on paper-mache panels, edges visibly hand-cut, two slim wooden dowels -along the bone of each wing. a pale-yellow short-sleeve button-down with -a small enamel butterfly pin on the collar, fluttering open in the wind -of their own dash. a thin black antenna headband arcs above their hair, -springs tipped with two tiny felt balls, bent back by speed. +real monarch butterflies dance WITH him — four or five (orange + black, +painted-paper edged, drawn at different scales for depth) circling his +lifted hands and arched back like dance partners, each on its own little +arc, sharing the same upward turning motion. one rests for a beat on his +outstretched fingertips. a current of grace, not a swarm. -streaming past them on a steep diagonal: a fast PROCESSION of six or -seven real monarch butterflies (orange + black, painted-paper edged, -drawn at different scales for depth), each pulling its own little -motion-blur tail, the whole flock rushing the same direction as the -figure — a current of butterflies, not a swarm. the green park behind -smears very slightly into horizontal speed-streaks near the edges. +a soft hand-drawn ground-shadow anchors his standing foot to the green +park lawn so he reads as truly balanced on it — light, poised, not +floating away. palette: warm cream paper ground; bright pale-sky-blue overhead; sap -green grass smearing behind; pale-yellow button-down; soft brown hair and -warm grey; orange + black on the monarch wings (cosplay + real). -hand-drawn, hatched, print-tech-aware. clean late-morning sunlight, no -haze, no neon, no glow filters. +green grass behind; pale-yellow button-down; soft brown hair and warm +grey; orange + black on the monarch wings (cosplay + real). hand-drawn, +hatched, print-tech-aware. clean late-morning sunlight, no haze, no neon, +no glow filters. -square album cover. no text, no wordmarks, no readable logos. the figure -does not look at the camera — eyes ahead, chasing the flock. +square album cover. no text, no wordmarks, no readable logos. he does not +look at the camera — eyes following the butterflies, lost in the dance. diff --git a/pop/marimba/fluttabap360.story.txt b/pop/marimba/fluttabap360.story.txt new file mode 100644 --- /dev/null +++ b/pop/marimba/fluttabap360.story.txt @@ -0,0 +1,163 @@ +fluttabap360 — storyline visualizer (1080×1920 vertical insta-story / reel) + +THE STORY — "the monarch keeper" +================================ + +near-future, an AESTHETIC COMPUTER nature-park: a green public lawn +threaded with quiet computational infrastructure. the monarch +butterflies here are LIVING DATA — real-painted-paper monarchs that are +also little creatures of the park's system, drifting on their own arcs, +carrying colour. the butterfly cosplayer (the jeffrey identity-ref +character from the cover — about 30, soft brown curly hair, pale-yellow +short-sleeve button-down, hand-made monarch paper-mache wings on wooden +dowels, antenna headband) is the KEEPER who learns to DANCE with them: +the dance is how you talk to the data. not a swarm — a current of grace. + +the track climbs C → D → E → (home C cave breath) → F. the story climbs +WITH it: a slow water-drop dawn, the butterfly hook waking, three +brightening ballet passes as he and the monarchs learn each other, a +dub-cave breakdown where the data goes dark and he holds his breath, a +euphoric final lift into F, then a soft landing and a held last frame on +the cover pose. + +medium = hand-drawn colored-pencil + gouache on warm cream paper, exactly +as the cover. diegetic light only — clean morning/park sun, NO glow +filters, NO neon, NO motion-blur language. NOT a photograph, NOT +cinematic. no text, no wordmarks, no readable logos. the character never +looks at the camera — eyes always following the butterflies. + +the AC sci-fi register is QUIET and HAND-DRAWN, like amaythingra's +office→metaverse arc but warm instead of grey: the "computation" reads as +gentle painted-paper data-monarchs, faint hand-ruled ground-grids in the +lawn, soft signal-arcs trailing the butterflies — never glowing UI, never +screens. think a public park that happens to think. + + +ELEVEN STORY BEATS (one illustration per unique section name) +============================================================= +the struct repeats these names across four passes (C / D / E / F) — the +SAME illustration is reused per name; the visualizer's per-section tint + +key-brightening makes each pass read brighter and higher. so we draw 11 +panels + 1 cover, not 34. + +1. intro — DAWN, water-drop + wide low vertical shot of the empty park lawn at first light, dew + still on the grass. ONE monarch (painted-paper, orange + black) rests + folded on a single tall grass blade, a fat dew-drop hanging from the + blade tip about to fall — caught the instant before it drops. the + keeper is far off and small, just arriving at the edge of the frame, + barely awake. cool pale-blue pre-sun sky, long soft shadows, a faint + hand-ruled ground-grid in the lawn (the park's quiet computation), + almost invisible. stillness. the whole world holding its breath + before the first note. + +2. butterfly — THE HOOK WAKES + the keeper, mid-frame now, lifts one open hand and the resting + monarch UNFOLDS and lifts off toward his fingertips — the very first + flutter, the hook of the song. his face lighting up with recognition, + not surprise. a faint soft signal-arc trails the butterfly's path + (hand-drawn dotted curve, pencil). morning sun warming the lawn behind + him. he is just beginning the dance — weight shifting onto the ball of + one foot. + +3. palofmine — PARTNER FOUND + tighter three-quarter shot: the monarch settles for a beat on his + outstretched fingertip, the two of them regarding each other — "pal of + mine." his curly head tilted, a quiet grin, antenna headband springs + tipped with felt balls. one or two more monarchs drift in on their own + little arcs at frame edges. warm and companionable. the data has + chosen to land on him. + +4. mommywow — THE HELD HUSH + a held, breath-held beat (this lands under the long tonic-pad swell). + the keeper goes still, both hands cupped softly open at chest height, + eyes closed or half-lowered, a monarch hovering a hand-span above his + open palms — neither touching. soft awe. the lawn behind blurs into + warm cream paper. time suspended on the chord. "mommy — wow." + +5. slinky — THE LEARNING SPIRAL + the keeper begins to MOVE — a slinky, loose, spiralling step, body + uncoiling, arms drawing a long S-curve through the air, and three or + four monarchs trace the SAME spiralling path beside him on their arcs, + learning his shape / he learning theirs. faint dotted signal-arcs + describe the shared spiral (hand-drawn). playful, gathering momentum. + the wings on his back tipping open with the motion. + +6. fly — FULL FLIGHT (the big lift of the pass) + the brightest, widest beat of each pass: the keeper at full ballet + lift — relevé + low arabesque + port de bras, exactly the COVER pose + — both arms lifted and curved overhead, four or five monarchs circling + his lifted hands and arched back like dance partners, all sharing the + same upward turning motion. open joy, chin lifted, lost in it. clean + late-morning sun. this is the cover pose re-staged as a full-body + vertical action shot (camera pulled back + up). a current of grace. + +7. ride — THE SQUEAK-RIDE DUET + a quick, mischievous beat (the melodic scratch+squeak duet): the + keeper mid-turn, ONE monarch swooping in low and another answering up + high — a call-and-response chase, his head whipping to follow the + high one, a delighted open-mouthed laugh. light, fast, a little wild. + two crossing signal-arcs (one low, one high) describe the duet. + +8. cave — DUB BREAKDOWN (the data goes dark) + the cool, somber breakdown beat — the only DARK panel. the park dims: + the keeper has stepped into the mouth of a cool computational cave / + under a deep arching shade, the lawn falling away to dusk-blue behind + him. he stands very still, head bowed, ONE monarch perched on his + lowered hand, its wings half-folded and dim — the data resting, the + system breathing. the faint ground-grid glows a touch more visible in + the low light (still hand-drawn, never neon). holding his breath. home + key C — a return to the start's stillness, but knowing now. + +9. progression — THE RECOMMITMENT (climbing out into F) + he steps back OUT of the cave-shade into bright F-major light, the + monarchs streaming back to him from the lawn, gathering — a rising + procession of three, four, five butterflies converging on his lifting + hands as he re-enters the dance, brighter and higher than any pass + before. the recommitment. the brightest light yet, sun fully up. + +10. land — THE SOFT LANDING + the keeper coming gently down out of the lift, descending from + relevé back to flat-footed, arms lowering in a soft port de bras, the + monarchs settling around him — one returning to rest on his + fingertip, others alighting on the grass and on his shoulders. the + motion resolving, weight returning to the earth. warm, satisfied, + spent. evening-gold light now. + +11. button — THE LAST FRAME (lands on the cover) + the final held image: the keeper at rest on the lawn, the dance done, + the monarchs settled, ONE last monarch lifting off his fingertip into + the warm gold sky — a quiet goodbye, a held breath out. composes + toward the locked album-cover feeling: balanced, light, grinning, + eyes following the last butterfly up. NOT looking at camera. the soft + ground-shadow anchors his foot to the lawn. the end. + + +PER-SECTION VISUALIZER TINT (warm park palette, climbing brighter) +================================================================== +intro cool pale dawn-blue (still, pre-sun) +butterfly warming oat (the hook wakes) +palofmine warm cream (partner found) +mommywow soft gold-cream (held hush) +slinky sap-green + amber (the spiral) +fly bright amber (full flight — brightest) +ride bright lime-gold (mischief) +cave cool dusk-blue (DARK) (breakdown / breath) +progression bright F-gold (recommitment, brightest yet) +land evening-gold (soft landing) +button warm rose-gold (last frame) + + +NOTES FOR @JEFFREY +================== +- improved the seed concept: the monarchs as LIVING DATA the keeper talks + to by DANCING keeps the AC sci-fi register but stays warm + hand-drawn + (no screens, no neon) — the "computation" is faint ground-grids + + dotted signal-arcs only. +- the cave beat is the one dark panel (matches the dub breakdown + the + home-C "breath"); everything else climbs in warmth + height with the + key. +- the `fly` beat is the cover pose re-staged full-body, so the + visualizer's most-repeated/biggest beat IS the cover. +- per-section forms.json face/figure boxes drive the zoom + backlight, + hand-placed per panel (single figure, so one box per section).