From 8161328e3e8bed781f2b2b4cc367556548a30519 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 11 Jun 2026 23:05:23 -0700 Subject: [PATCH] =?UTF-8?q?momabobasheep:=20MoMA=20+=20boba=20+=20sheep=20?= =?UTF-8?q?=E2=80=94=20the=20full=20release=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renamed from mombobasleep (concept: jeffrey goes to the MoMA, drinks a boba, falls asleep counting sheep); PRNG seed deliberately stays 'mombobasleep' — it keys this exact composed walk - jeffrey vocal choir: wave-wizard vowel takes (oo/oh/ah/eh/mm) → WORLD-locked to B2 (tune-voice.mjs, +flat set for low voices) → loop-crossfaded 16s sustains → staged choral engine (tenor/twin/bass/ upper debut chapter by chapter, octave stretches at the climax, melody shadow gliding the whistle line, sidechain duck to the walking bass with 12ms slew, dedicated vocal hall) - composition: zero bar-to-bar chord repeats incl. cycle wraps; pivot seams (max common tones, circle-of-fifths tie-break) kill the uncanny cross-key jumps; alto/sparkle fade in per chapter; smooth level curve replaces stepped chapter gains; seam chapters lifted 0.30→0.38 - bass: softest mallet (80ms swell, 1.6x decay), bed pump removed - no filter sweeps / no noise texture anywhere (phaser, felting fizz, LFO wub, whistle breath all removed); drone+sub floored above the speaker-rattle band; tape-clamp + duck-slew pop fixes (click-scan.mjs verifies clean per-channel) - master: 2004-CD toasty — mono lows below 120, warm 240Hz body, -13.5 LUFS target by linear gain only (lands -14.3 at the -1 dBTP ceiling), dynamic arc intact - C engine (pop/momboba/c/, house pattern): JS composes, C renders 610s in ~20s, matches the JS reference to ≤0.01 dB on every compare metric - felt cover (needle-felt MoMA diorama) + 8-beat felt reel illys for the 30s IG FMV (gen-reel.mjs) --- pop/RELEASES.md | 68 +- pop/momboba/.gitignore | 3 + pop/momboba/README.md | 64 +- pop/momboba/bin/analyze-voice.mjs | 62 ++ pop/momboba/bin/analyze-whistlegraph.mjs | 2 +- pop/momboba/bin/build-score.mjs | 282 +++++++ pop/momboba/bin/click-scan.mjs | 40 + pop/momboba/bin/gen-illy.mjs | 30 +- pop/momboba/bin/gen-reel.mjs | 90 +++ ...bobasleep.mjs => render-momabobasheep.mjs} | 686 ++++++++++++------ pop/momboba/bin/score-extract.mjs | 351 +++++++++ pop/momboba/bin/scorodeon-data.mjs | 50 ++ pop/momboba/bin/tune-voice.mjs | 60 ++ pop/momboba/c/.gitignore | 9 + pop/momboba/c/bake.mjs | 362 +++++++++ pop/momboba/c/build.sh | 11 + pop/momboba/c/compare.mjs | 153 ++++ pop/momboba/c/momabobasheep.c | 596 +++++++++++++++ pop/momboba/c/render-c.mjs | 103 +++ pop/momboba/momabobasheep.illy.txt | 50 ++ pop/momboba/mombobasleep.illy.txt | 45 -- pop/momboba/reel/01-arrival.txt | 7 + pop/momboba/reel/02-boba-sip.txt | 6 + pop/momboba/reel/03-galleries.txt | 6 + pop/momboba/reel/04-waterlily-awe.txt | 6 + pop/momboba/reel/05-heavy-eyes.txt | 6 + pop/momboba/reel/06-first-sheep.txt | 6 + pop/momboba/reel/07-asleep-flock.txt | 8 + pop/momboba/reel/08-dream.txt | 6 + pop/momboba/reel/PREAMBLE.txt | 17 + wave-wizard/samples/momabobasheep/spec.json | 34 + .../takes/momabobasheep-vowel-note-0.wav | Bin 0 -> 368194 bytes .../takes/momabobasheep-vowel-note-1.wav | Bin 0 -> 396332 bytes .../takes/momabobasheep-vowel-note-2.wav | Bin 0 -> 353324 bytes .../takes/momabobasheep-vowel-note-3.wav | Bin 0 -> 531500 bytes .../takes/momabobasheep-vowel-note-4.wav | Bin 0 -> 576044 bytes 36 files changed, 2884 insertions(+), 335 deletions(-) create mode 100644 pop/momboba/bin/analyze-voice.mjs create mode 100644 pop/momboba/bin/build-score.mjs create mode 100644 pop/momboba/bin/click-scan.mjs create mode 100644 pop/momboba/bin/gen-reel.mjs rename pop/momboba/bin/{render-mombobasleep.mjs => render-momabobasheep.mjs} (55%) create mode 100644 pop/momboba/bin/score-extract.mjs create mode 100644 pop/momboba/bin/scorodeon-data.mjs create mode 100644 pop/momboba/bin/tune-voice.mjs create mode 100644 pop/momboba/c/.gitignore create mode 100644 pop/momboba/c/bake.mjs create mode 100755 pop/momboba/c/build.sh create mode 100644 pop/momboba/c/compare.mjs create mode 100644 pop/momboba/c/momabobasheep.c create mode 100644 pop/momboba/c/render-c.mjs create mode 100644 pop/momboba/momabobasheep.illy.txt delete mode 100644 pop/momboba/mombobasleep.illy.txt create mode 100644 pop/momboba/reel/01-arrival.txt create mode 100644 pop/momboba/reel/02-boba-sip.txt create mode 100644 pop/momboba/reel/03-galleries.txt create mode 100644 pop/momboba/reel/04-waterlily-awe.txt create mode 100644 pop/momboba/reel/05-heavy-eyes.txt create mode 100644 pop/momboba/reel/06-first-sheep.txt create mode 100644 pop/momboba/reel/07-asleep-flock.txt create mode 100644 pop/momboba/reel/08-dream.txt create mode 100644 pop/momboba/reel/PREAMBLE.txt create mode 100644 wave-wizard/samples/momabobasheep/spec.json create mode 100644 wave-wizard/samples/momabobasheep/takes/momabobasheep-vowel-note-0.wav create mode 100644 wave-wizard/samples/momabobasheep/takes/momabobasheep-vowel-note-1.wav create mode 100644 wave-wizard/samples/momabobasheep/takes/momabobasheep-vowel-note-2.wav create mode 100644 wave-wizard/samples/momabobasheep/takes/momabobasheep-vowel-note-3.wav create mode 100644 wave-wizard/samples/momabobasheep/takes/momabobasheep-vowel-note-4.wav diff --git a/pop/RELEASES.md b/pop/RELEASES.md index d65d0046ff..7d729fc423 100644 --- a/pop/RELEASES.md +++ b/pop/RELEASES.md @@ -677,7 +677,7 @@ DistroKid has a "request Spotify for Artists" shortcut for new artists. --- -## mombobasleep — WIP (10-minute Bachian remix of marimbaba) +## momabobasheep — WIP (10-minute Bachian remix of marimbaba) - **Lane:** `pop/momboba/` · a 10:00 **remix of marimbaba**, the twin of marimbaba the way `pop/sleephellsine/` is the twin of `pop/hellsine/` — @@ -685,17 +685,32 @@ DistroKid has a "request Spotify for Artists" shortcut for new artists. which is what makes it a marimbaba *remix* and not a sleephellsine clone. Began as a calm sleep drone; evolved (2026-06-08) into a slow, **Bachian, rolling, ever-transforming** piece that travels through keys over its 10 - minutes. "Mom" = the lullaby rocked dozy + dumb-slow (renamed from - "moronbobasleep" 2026-06-10). Part of the *pixsies* body. + minutes. **THE CONCEPT (jas, 2026-06-11): MoMA (the art museum) + boba + tea + sheep — "jeffrey goes to the MoMA", drinks a boba, and falls + asleep counting sheep in the galleries.** Name history: "moronbobasleep" + → "mombobasleep" (2026-06-10) → **momabobasheep** (2026-06-11). NOTE: + the render seed stays `"mombobasleep"` — it keys this exact composed + walk. Part of the *pixsies* body. - **Artist:** Aesthetic Dot Computer (intended) -- **Status:** DistroKid-READY (2026-06-11) — sweep-free master rendered + - clip-tested (−16.0 LUFS / −2.6 dBTP), mp3 tagged (`title=mombobasleep · +- **Status:** DistroKid-READY (2026-06-11, final) — master rendered + + clip-tested (−16.0 LUFS integrated / −2.8 dBTP, no clipping). MASTERING + CALL: −16 LUFS is deliberate for a sleep mix — louder would sacrifice + the baked dynamic arc, and every platform normalizes playback anyway; + no brightening pass (warm/dark is the genre, unlike the trance lane's + note below). mp3 tagged (`title=momabobasheep · artist=Aesthetic Dot Computer · album=pixsies · date=2026`) with the new cover embedded; upload assets staged in `~/Documents/Shelf/momboba/` - (MASTER.wav + mombobasleep.mp3 + mombobasleep-cover-3000.png). Awaiting + (MASTER.wav + momabobasheep.mp3 + momabobasheep-cover-3000.png). Awaiting @jeffrey's ear-check + manual DistroKid upload. Not yet on the CDN. -- **Engine:** `pop/momboba/bin/render-mombobasleep.mjs` — a **generative** - renderer (deterministic per `--seed`, default `mombobasleep`). 100% +- **Engine:** `pop/momboba/bin/render-momabobasheep.mjs` — a **generative** + renderer (deterministic per `--seed`; the default seed string remains + `mombobasleep`, the PRNG key for this exact walk). **C ENGINE + (2026-06-11, house pattern like hellsine/americomputadora):** + `pop/momboba/c/` — composition stays in JS (score-extract + bake.mjs + fan-out with an rng parity gate), `momabobasheep.c` executes the DSP + ~30× faster (610 s in ~20 s) and matches the JS reference to ≤0.01 dB + on every compare metric (`c/compare.mjs` verdict: all PASS). Run: + `node pop/momboba/c/render-c.mjs`. 100% synthesized — the CC0 rain sample + brownian/air hiss beds were REMOVED (2026-06-10: too repetitive as a loop, environmental noise out of the track completely); it opens on the tonic pad + drone alone, then builds a @@ -741,23 +756,32 @@ DistroKid has a "request Spotify for Artists" shortcut for new artists. swells now, it never thumps); - **SOFT FELT MALLETS** throughout (long mallet contact + slow attack). Every render self-runs a **clip test** (sample/true peak + LUFS). -- **Master:** `~/Documents/Shelf/momboba/mombobasleep-MASTER.wav` - (44.1 kHz / 24-bit) + `mombobasleep.mp3` (320k). **Sleep** chain (kept +- **Master:** `~/Documents/Shelf/momboba/momabobasheep-MASTER.wav` + (44.1 kHz / 24-bit) + `momabobasheep.mp3` (320k). **Sleep** chain (kept gentle so the dynamic arc lives): highpass 22 → +3 dB low shelf @ 85 → −3.5 dB shelf @ 3.2 k → ONE soft slow glue comp (2:1, no leveller) → 2 s fade-in / 5 s fade-out guarding the loop seam → **measured LINEAR gain to −16 LUFS / −1 dBTP** (loudnorm as meter only), truncate to 10:00. (Durable home is `~/Documents/Shelf/` — Desktop auto-cleans, [[feedback_desktop_autocleaned]].) -- **Cover:** `pop/momboba/out/mombobasleep-cover.png` (1024², - gpt-image-2 via `bin/gen-illy.mjs` + `mombobasleep.illy.txt`) — replaced - the felted-doll naptime (2026-06-11, per @jeffrey): now a dim warm - phone-snapshot of the REAL jeffrey deep asleep on the couch under a - blanket, actual outfit from the platter refs, red glasses folded beside - his head, his CLOSED Citrus MacBook Neo tucked against him like a pet - (white-scrap whistlegraph butterfly on the lid; no screens, no text, no - wordmarks — diegetic amber lamp light only). Embedded in the mp3; - DistroKid upscale at `~/Documents/Shelf/momboba/mombobasleep-cover-3000.png`. -- **Graphic score:** `pop/momboba/out/mombobasleep-score.png` (3200×1400, +- **Cover:** `pop/momboba/out/momabobasheep-cover.png` (1024², + gpt-image-2 via `bin/gen-illy.mjs` + `momabobasheep.illy.txt`) — FINAL + (2026-06-11, soft-felt return): **the whole concept as a NEEDLE-FELTED + WOOL diorama** — felt jeffrey-doll (real outfit in felt, red felt + glasses folded by his hand) asleep on a felt gallery bench, the + water-lily room as a monumental FELTED TAPESTRY filling the wall, a + tiny felted boba with wool pearls on the bench, and a flock of + needle-felt sheep grazing the felt floor (wool playing itself). No + text/wordmarks/screens. (Concept lineage: felted couch-doll → + photographic MoMA scene → this felt MoMA scene.) Embedded in BOTH + engine mp3s; DistroKid upscale at + `~/Documents/Shelf/momboba/momabobasheep-cover-3000.png` + Desktop + mirrors of cover + all four masters. +- **Reel (FMV-ready):** `pop/momboba/reel/` — 8 vertical (1024×1536) + felt story beats via `bin/gen-reel.mjs` (PREAMBLE + per-beat prompts): + arrival-with-boba → the sip → galleries → pond-room awe → heavy eyes → + first sheep → asleep among the flock → the dream on the lily pad. + Outputs `reel/out/NN-*.png` + Desktop mirrors, for a ~30 s IG reel cut. +- **Graphic score:** `pop/momboba/out/momabobasheep-score.png` (3200×1400, also on ~/Desktop) — the whole 10:00 visualized: 9 Fibonacci movement bands, the 186-chord walk, per-voice lanes (whistle/bells/sparkle/arp/ alto/pad/drone+sub ribbons), the baked dynamic arc, golden-section climax @@ -772,9 +796,9 @@ DistroKid has a "request Spotify for Artists" shortcut for new artists. whole-piece minimap with the dynamic arc. Any `--size` aspect (default 1080×1920 story-vertical), `--zoom` = seconds visible per screen. Generic JSON contract; lane adapter `bin/scorodeon-data.mjs` → - `out/mombobasleep.scorodeon.json` → `out/mombobasleep-scorodeon.mp4`. + `out/momabobasheep.scorodeon.json` → `out/momabobasheep-scorodeon.mp4`. - **Next:** ear-check the master → CDN - (`assets.aesthetic.computer/pop/mombobasleep.jpg` / `.mp3`) → + (`assets.aesthetic.computer/pop/momabobasheep.jpg` / `.mp3`) → DistroKid → promote this entry to **RELEASED**. --- diff --git a/pop/momboba/.gitignore b/pop/momboba/.gitignore index 47efb6bc67..0b5ea04c76 100644 --- a/pop/momboba/.gitignore +++ b/pop/momboba/.gitignore @@ -9,3 +9,6 @@ assets/*.p-*.wav # recorded voice takes — RFA captures live outside the repo voice-takes/*.wav + +# derived vocal assets — regenerate via bin/tune-voice.mjs +voice/ diff --git a/pop/momboba/README.md b/pop/momboba/README.md index 96c08e93c9..7851dee04c 100644 --- a/pop/momboba/README.md +++ b/pop/momboba/README.md @@ -1,14 +1,14 @@ -# moronboba +# momboba -**Moronbobasleep** — our first 10-minute extended **sleep mix**, a remix of +**Mombobasleep** — our first 10-minute extended **sleep mix**, a remix of [`pop/marimba/marimbaba`](../marimba/). The calm nocturne twin of the lullaby, the way [`pop/sleephellsine`](../sleephellsine/) is the calm twin of [`pop/hellsine`](../hellsine/). The difference: sleephellsine *abandoned* hellsine's instruments for pure sine. -Moronbobasleep keeps the **actual marimba synth** (`../marimba/synths/marimba.mjs`) +Mombobasleep keeps the **actual marimba synth** (`../marimba/synths/marimba.mjs`) — that's what makes it a marimbaba *remix* and not a sleephellsine clone. -"Moron" = the lullaby gone dozy, dumb-happy, and very, very slow. +"Mom" = the lullaby rocked dozy, dumb-happy, and very, very slow. ## The formula @@ -29,44 +29,56 @@ on a deterministic random walk (seeded, so re-renders are bit-identical): `bass` (the rocking chair, deep + soft), `vibraphone_off` (the dream-haze pad, held thirds, 14 ms haas-widened), `rosewood` (the singer), `kalimba` (twinkles on phrase tails). -- A soft **brownian bed** underneath — two independent stereo random walks, - slow 90 s sneak-in, dry — the room tone under the marimba. +- A felted 5-voice **drone bed** underneath (stereo chorus + a fixed-filter + sub) — every layer is pitched and musical: **no field recordings, no noise + beds, no filter sweeps, no texture layers**. The old rain sample + + brownian/air hiss went first (too repetitive, too noisy for sleep); the + phaser sweep, the sub's LFO-swept "wub", and the felting noise-fizz + followed (2026-06-11). Movement comes only from the chorus and each drone + voice's own woozy vibrato/drift. ## Audio target -- **Length:** 10:00 final master (engine renders 10:10; the master fades out - over 14 s and truncates to 600 s). -- **Master:** same sleep chain as sleephellsine — highpass 28 Hz, −1.5 dB shelf - above 4 kHz, `loudnorm` to **−28 LUFS** (TP −6, LRA 14), soft limiter at 0.35, - **60 s fade-in** from silence, 14 s fade-out. +- **Length:** 10:00 final master (engine renders 10:10 and truncates to 600 s). +- **Master:** gentle sleep chain — highpass 22 Hz, +3 dB low shelf @ 85 Hz, + −3.5 dB shelf above 3.2 kHz, one soft 2:1 glue compressor (no leveller), a + measured LINEAR gain to **−16 LUFS / −1 dBTP**, with a 2 s fade-in and 5 s + fade-out guarding the loop seam — nothing ever cuts abruptly. ## Files -- `bin/render-moronbobasleep.mjs` — the generative renderer + inline sleep +- `bin/render-momabobasheep.mjs` — the generative renderer + inline sleep master. Reuses `mixEventMarimba` from the marimba lane. -- `bin/gen-illy.mjs` — cover generator. Inherits marimbaba's cover identity - verbatim (jeffrey + Bill Gates at the Model M), recomposed as a sleepy - nocturne — the two have drowsed off mid-task. -- `moronbobasleep.illy.txt` — cover prompt (lowercase fragments, papers voice). -- `out/moronbobasleep.struct.json` — the chord walk (for any future visualizer). +- `bin/gen-illy.mjs` — cover generator. A felted naptime: one needle-felted + wool doll of jeffrey alone, asleep in a nest of felt bedding, his little + felt laptop dozing beside him. +- `momabobasheep.illy.txt` — cover prompt (lowercase fragments, papers voice). +- `out/momabobasheep.struct.json` — the chord walk (for any future visualizer). ## Run ```bash -# render + sleep-master in one go → out/moronbobasleep.mp3 -node pop/moronboba/bin/render-moronbobasleep.mjs +# render + sleep-master in one go → out/momabobasheep.mp3 +node pop/momboba/bin/render-momabobasheep.mjs # write the master elsewhere -node pop/moronboba/bin/render-moronbobasleep.mjs --out ~/moronbobasleep.mp3 +node pop/momboba/bin/render-momabobasheep.mjs --out ~/momabobasheep.mp3 # a different overnight — same parts, new random walk -node pop/moronboba/bin/render-moronbobasleep.mjs --seed nightfall +node pop/momboba/bin/render-momabobasheep.mjs --seed nightfall -# cover (cached unless --force); embeds into out/moronbobasleep.mp3 -node pop/moronboba/bin/gen-illy.mjs -node pop/moronboba/bin/gen-illy.mjs --force -node pop/moronboba/bin/gen-illy.mjs --embed-only +# cover (cached unless --force); embeds into out/momabobasheep.mp3 +node pop/momboba/bin/gen-illy.mjs +node pop/momboba/bin/gen-illy.mjs --force +node pop/momboba/bin/gen-illy.mjs --embed-only +``` + +```bash +# scorodeon — watch the score fly by a fixed center line (vertical, story-ready) +node pop/momboba/bin/scorodeon-data.mjs # events.json → scorodeon.json +node pop/bin/scorodeon.mjs pop/momboba/out/momabobasheep.scorodeon.json \ + pop/momboba/out/momabobasheep.mp3 --desktop # --size WxH --zoom sec ``` Listen with QuickTime, not Apple Music: -`open -a "QuickTime Player" pop/moronboba/out/moronbobasleep.mp3` +`open -a "QuickTime Player" pop/momboba/out/momabobasheep.mp3` diff --git a/pop/momboba/bin/analyze-voice.mjs b/pop/momboba/bin/analyze-voice.mjs new file mode 100644 index 0000000000..74bd2af451 --- /dev/null +++ b/pop/momboba/bin/analyze-voice.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +// analyze-voice.mjs — pitch-analyze jeffrey's wave-wizard vowel takes so the +// renderer can use them as a sampled harmony voice. +// +// Reads: wave-wizard/samples/momabobasheep/takes/*.wav (48 kHz mono PCM) +// Writes: pop/momboba/voice/manifest.json +// [{ file, vowel, midi, hz, clarity, durSec }] +// +// Pitch via pop/lib/analysis.mjs pitchTrack (normalized autocorrelation): +// the take's pitch is the CLARITY-WEIGHTED MEDIAN of its voiced frames — +// robust against the onset scoop and the breathy tail. +// +// Run: node pop/momboba/bin/analyze-voice.mjs + +import { writeFileSync, readdirSync, mkdirSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { pitchTrack } from "../../lib/analysis.mjs"; +import { readWavMono } from "../../lib/wav.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO = resolve(HERE, "../../.."); +const TAKES = resolve(REPO, "wave-wizard/samples/momabobasheep/takes"); +const OUT_DIR = resolve(HERE, "../voice"); +mkdirSync(OUT_DIR, { recursive: true }); + +const VOWELS = ["oo", "oh", "ah", "eh", "mm"]; // spec.json take order + +if (!existsSync(TAKES)) { + console.error(`✗ no takes yet — record first: wave-wizard samples/momabobasheep/spec.json`); + process.exit(1); +} +const files = readdirSync(TAKES).filter((f) => f.endsWith(".wav")).sort(); +if (!files.length) { console.error(`✗ ${TAKES} is empty`); process.exit(1); } + +const rows = []; +for (const f of files) { + const path = resolve(TAKES, f); + const { samples, sampleRate } = readWavMono(path); + const frames = pitchTrack(samples, { sampleRate, fmin: 60, fmax: 500 }); + const voiced = frames.filter((r) => r.hz != null && r.clarity > 0.6); + if (voiced.length < 5) { console.warn(` ⚠ ${f}: too little voiced material, skipping`); continue; } + // clarity-weighted median of midi values + const sorted = voiced.map((r) => ({ m: r.midi, w: r.clarity })).sort((a, b) => a.m - b.m); + const totW = sorted.reduce((s, r) => s + r.w, 0); + let acc = 0, midi = sorted[0].m; + for (const r of sorted) { acc += r.w; if (acc >= totW / 2) { midi = r.m; break; } } + const idx = files.indexOf(f); + rows.push({ + file: path.replace(REPO + "/", ""), + vowel: VOWELS[idx] ?? `take${idx}`, + midi: Math.round(midi * 100) / 100, + hz: Math.round(440 * Math.pow(2, (midi - 69) / 12) * 10) / 10, + clarity: Math.round((voiced.reduce((s, r) => s + r.clarity, 0) / voiced.length) * 100) / 100, + durSec: Math.round((samples.length / sampleRate) * 100) / 100, + }); + console.log(` ${VOWELS[idx] ?? f} · ${rows.at(-1).hz} Hz (midi ${rows.at(-1).midi}) · clarity ${rows.at(-1).clarity} · ${rows.at(-1).durSec}s`); +} + +const out = resolve(OUT_DIR, "manifest.json"); +writeFileSync(out, JSON.stringify(rows, null, 2) + "\n"); +console.log(`✓ ${rows.length} takes → ${out.replace(REPO + "/", "")}`); diff --git a/pop/momboba/bin/analyze-whistlegraph.mjs b/pop/momboba/bin/analyze-whistlegraph.mjs index c7b473c34d..4c00f2d27b 100644 --- a/pop/momboba/bin/analyze-whistlegraph.mjs +++ b/pop/momboba/bin/analyze-whistlegraph.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // analyze-whistlegraph.mjs — pull the sung melody out of a whistlegraph // performance video's audio: a dependency-free YIN f0 tracker → segmented -// notes → a structural + dynamic summary used to shape moronbobasleep's arc. +// notes → a structural + dynamic summary used to shape mombobasleep's arc. // // ffmpeg -i -web.mp4 -ac 1 -ar 16000 out.wav (caller does this) // node analyze-whistlegraph.mjs out.wav diff --git a/pop/momboba/bin/build-score.mjs b/pop/momboba/bin/build-score.mjs new file mode 100644 index 0000000000..17148e29f1 --- /dev/null +++ b/pop/momboba/bin/build-score.mjs @@ -0,0 +1,282 @@ +#!/usr/bin/env node +// build-score.mjs — render the momabobasheep graphic score as a single wide +// SVG inside an HTML page, ready to screenshot with chrome-shot.mjs. +// +// Reads: pop/momboba/out/momabobasheep.events.json (from score-extract.mjs) +// Writes: pop/momboba/out/momabobasheep-score.html +// +// Then: node toolchain/macos/chrome-shot.mjs file://…/momabobasheep-score.html \ +// pop/momboba/out/momabobasheep-score.png --size 3200x1400 + +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const OUT = resolve(HERE, "..", "out"); +const D = JSON.parse(readFileSync(resolve(OUT, "momabobasheep.events.json"), "utf8")); + +// ── canvas ──────────────────────────────────────────────────────────────── +const W = 3200, H = 1400; +const ML = 190, MR = 56; // left gutter (lane labels) / right margin +const PW = W - ML - MR; // plot width +const T = D.masterSec; // 600 s +const x = (t) => ML + (t / T) * PW; + +const INK = "#1E1E1E", CREAM = "#FBFAFF"; +const FAINT = "rgba(30,30,30,0.14)", FAINTER = "rgba(30,30,30,0.07)"; + +// restrained accents, one per lane +const C = { + whistle: "#2A6F77", // deep teal + bells: "#6E5A8E", // dusty violet + sparkle: "#C08A2D", // amber + arp: "#A65A3A", // terracotta + alto: "#7A7B45", // olive + pad: "#5B7FA6", // soft slate blue + bass: "#6B4A36", // warm brown + drone: "#8A8378", // warm grey + sub: "#57514A", // deeper warm grey + golden: "#B3402E", // climax marker + band: "#3E4470", // movement band indigo +}; + +// chord-root pitch-class hues (subtle pastel fills) +const PC_HUE = { C: 205, Db: 230, D: 255, Eb: 280, E: 305, F: 35, Gb: 330, G: 80, Ab: 105, A: 130, Bb: 160, B: 185 }; +const chordFill = (name) => { + const pc = name.replace(/m$/, ""); + const minor = /m$/.test(name); + return `hsla(${PC_HUE[pc] ?? 0}, ${minor ? 30 : 45}%, ${minor ? 72 : 66}%, 0.55)`; +}; + +const esc = (s) => String(s).replace(/&/g, "&").replace(/`); + +// ── title ───────────────────────────────────────────────────────────────── +svg.push(`momabobasheep — graphic score`); +svg.push(`10:00 generative sleep mix · one night in nine fibonacci chapters · F major home · seed “momabobasheep” · ${D.totalBars} bars of ${D.bar}s`); + +// ── movement bands ──────────────────────────────────────────────────────── +for (let i = 0; i < D.movements.length; i++) { + const m = D.movements[i]; + const x0 = x(m.startSec), x1 = x(i === D.movements.length - 1 ? T : m.endSec); + const a = 0.10 + 0.42 * m.level; + svg.push(``); + // movement separators through the whole plot + if (i > 0) svg.push(``); + const cx = (x0 + x1) / 2, wpx = x1 - x0; + const big = wpx > 120; + svg.push(`${esc(m.name)}`); + if (big) { + svg.push(`${esc(m.sub)} · ${m.bars} bars`); + svg.push(`${esc(m.prog.join(" "))}`); + } else { + svg.push(`${m.bars}`); + } +} + +// ── chord lane (merged runs of the same chord) ──────────────────────────── +svg.push(`chord walk`); +{ + const runs = []; + for (const c of D.chords) { + const last = runs[runs.length - 1]; + if (last && last.name === c.name && Math.abs(last.t + last.dur - c.t) < 1e-6) last.dur += c.dur; + else runs.push({ name: c.name, t: c.t, dur: c.dur }); + } + for (const r of runs) { + const x0 = x(r.t), wpx = x(r.t + r.dur) - x0; + svg.push(``); + if (wpx >= 13.5) svg.push(`${esc(r.name)}`); + } + // pad-only tail to the seam (F home) + const tail0 = x(D.chords[D.chords.length - 1].t + D.chords[D.chords.length - 1].dur); + svg.push(``); + svg.push(`F…`); +} + +// ── lane scaffolding ────────────────────────────────────────────────────── +for (const l of LANES) { + svg.push(`${esc(l.label)}`); + svg.push(``); +} + +const pitchY = (lane, midi, lo, hi, pad = 6) => + lane.y + lane.h - pad - ((midi - lo) / Math.max(1, hi - lo)) * (lane.h - pad * 2); +const minmax = (evs) => { + let lo = 1e9, hi = -1e9; + for (const e of evs) { if (e.midi < lo) lo = e.midi; if (e.midi > hi) hi = e.midi; } + return [lo, hi]; +}; + +// whistle — line segments, pitch-placed, length = dur, weight = gain +{ + const L = LANES[0], [lo, hi] = minmax(D.whistle); + for (const e of D.whistle) { + const y = pitchY(L, e.midi, lo, hi); + svg.push(``); + } +} + +// reverso bells — swell wedges rising to the chord-arrival peak, then melting +{ + const L = LANES[1]; + for (const b of D.bells) { + const x0 = x(b.t), xp = x(b.peakT), x1 = x(b.t + b.dur); + const hgt = (b.gain / 0.18) * (L.h - 14); + const yb = L.y + L.h - 4; + svg.push(``); + svg.push(``); + } +} + +// kalimba sparkle — small diamonds on the euclidean onsets +{ + const L = LANES[2], [lo, hi] = minmax(D.sparkle); + for (const e of D.sparkle) { + const cx = x(e.t), cy = pitchY(L, e.midi, lo, hi, 10), r = 3 + e.g * 40; + svg.push(``); + } +} + +// arpeggio — dense pitch-placed ticks, height/opacity by gain +{ + const L = LANES[3], [lo, hi] = minmax(D.arp); + for (const e of D.arp) { + const y = pitchY(L, e.midi, lo, hi, 4); + const hgt = 5 + e.g * 30; + svg.push(``); + } +} + +// alto — short held dashes (dream chapters only) +{ + const L = LANES[4], [lo, hi] = minmax(D.alto); + for (const e of D.alto) { + const y = pitchY(L, e.midi, lo, hi, 8); + svg.push(``); + } +} + +// pad — translucent held blooms (always on) +{ + const L = LANES[5], [lo, hi] = minmax(D.pad); + for (const e of D.pad) { + const y = pitchY(L, e.midi, lo, hi, 7); + svg.push(``); + } +} + +// walking bass — root/approach ticks from the lane floor +{ + const L = LANES[6], [lo, hi] = minmax(D.bass); + for (const e of D.bass) { + const y = pitchY(L, e.midi, lo, hi, 6); + const hgt = 8 + e.g * 60; + svg.push(``); + } +} + +// drone bed + sub — continuous ribbons, thickness = envelope × dynamic arc +{ + const L = LANES[7]; + const mid = L.y + L.h * 0.42, midSub = L.y + L.h * 0.80; + const top = [], bot = [], stop = [], sbot = []; + for (let t = 0; t <= T; t += 2) { + const e = D.droneEnv[Math.min(t, D.droneEnv.length - 1)]; + const a = D.arc[Math.min(t, D.arc.length - 1)]; + const th = (3 + 30 * e * (0.45 + 0.55 * a)) / 2; + const ts = (1.5 + 12 * e * (0.45 + 0.55 * a)) / 2; + top.push(`${x(t).toFixed(1)},${(mid - th).toFixed(1)}`); + bot.push(`${x(t).toFixed(1)},${(mid + th).toFixed(1)}`); + stop.push(`${x(t).toFixed(1)},${(midSub - ts).toFixed(1)}`); + sbot.push(`${x(t).toFixed(1)},${(midSub + ts).toFixed(1)}`); + } + svg.push(``); + svg.push(``); + svg.push(`sub`); +} + +// ── dynamic arc ─────────────────────────────────────────────────────────── +{ + svg.push(`dynamic arc`); + const y0 = ARC_Y + ARC_H - 6; + const pts = []; + for (let t = 0; t <= T; t += 2) { + const a = D.arc[Math.min(t, D.arc.length - 1)]; + pts.push(`${x(t).toFixed(1)},${(y0 - a * (ARC_H - 18)).toFixed(1)}`); + } + svg.push(``); + svg.push(``); + svg.push(``); + // movement level dots + for (const m of D.movements) { + const cx = x((m.startSec + m.endSec) / 2), cy = y0 - m.level * (ARC_H - 18); + svg.push(``); + svg.push(`${m.level.toFixed(2)}`); + } +} + +// ── golden-section climax marker (full height) ──────────────────────────── +{ + const gx = x(D.goldenSec); + svg.push(``); + const mm = Math.floor(D.goldenSec / 60), ss = Math.round(D.goldenSec % 60); + svg.push(`golden section · ${mm}:${String(ss).padStart(2, "0")} · the dream crests`); +} + +// ── loop-seam note ──────────────────────────────────────────────────────── +svg.push(`10:00 meets 0:00 — the same F haze (loop seam)`); + +// ── time axis ───────────────────────────────────────────────────────────── +{ + svg.push(``); + for (let s = 0; s <= T; s += 30) { + const tx = x(s), minor = s % 60 !== 0; + svg.push(``); + if (!minor) svg.push(`${s / 60}:00`); + } +} + +// ── html shell ──────────────────────────────────────────────────────────── +const ywftPath = resolve(homedir(), "Library", "Fonts", "ywft-processing-bold.ttf"); +const ywftFace = existsSync(ywftPath) + ? `@font-face { font-family: "YWFT"; src: url(data:font/ttf;base64,${readFileSync(ywftPath).toString("base64")}) format("truetype"); }` + : ""; +const html = ` + +${svg.join("\n")} + +`; + +const outPath = resolve(OUT, "momabobasheep-score.html"); +writeFileSync(outPath, html); +console.log(`✓ score page → ${outPath} (${(html.length / 1024).toFixed(0)} KB)`); diff --git a/pop/momboba/bin/click-scan.mjs b/pop/momboba/bin/click-scan.mjs new file mode 100644 index 0000000000..ee91a7b752 --- /dev/null +++ b/pop/momboba/bin/click-scan.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// click-scan.mjs — objective pop/click detector for rendered masters. +// Decodes any audio file to mono f32 (ffmpeg), then flags samples where +// the instantaneous jump |x[i]-x[i-1]| is both absolutely large and far +// out of proportion to the local RMS — i.e. a discontinuity, not music. +// +// Run: node pop/momboba/bin/click-scan.mjs