From f245e44a11e37059b9c5119b570e4fa9bf93b2d8 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Mon, 22 Jun 2026 03:02:09 +0000 Subject: [PATCH] working tree: pop updates (americomputadora, boombaboom, marimba/flutterbap, maytrax/pianotrax, nullabye, emotrib) + events --- events/fia-birthday-edendale/build-text.mjs | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ events/fia-birthday-edendale/gen-fia-bunny.mjs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ events/fia-birthday-edendale/gen-fia-felt.mjs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ events/fia-birthday-edendale/gen-fia-human.mjs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ events/fia-birthday-edendale/gen-invite.mjs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ events/fia-birthday-edendale/out/.card-base.png | 0 events/fia-birthday-edendale/out/.card-preview.png | 0 events/fia-birthday-edendale/out/.chk-B.png | 0 events/fia-birthday-edendale/out/.chk-anim.png | 0 events/fia-birthday-edendale/out/.chk-settled.png | 0 events/fia-birthday-edendale/out/.frame-mid.png | 0 events/fia-birthday-edendale/out/.scrim.png | 0 events/fia-birthday-edendale/out/.text/.ch.txt | 1 + events/fia-birthday-edendale/out/.text/.fg.png | 0 events/fia-birthday-edendale/out/.text/.sh.png | 0 events/fia-birthday-edendale/out/.text/compose.sh | 4 ++++ events/fia-birthday-edendale/out/.text/g000.png | 0 events/fia-birthday-edendale/out/.text/g001.png | 0 events/fia-birthday-edendale/out/.text/g002.png | 0 events/fia-birthday-edendale/out/.text/g003.png | 0 events/fia-birthday-edendale/out/.text/g004.png | 0 events/fia-birthday-edendale/out/.text/g005.png | 0 events/fia-birthday-edendale/out/.text/g006.png | 0 events/fia-birthday-edendale/out/.text/g007.png | 0 events/fia-birthday-edendale/out/.text/g008.png | 0 events/fia-birthday-edendale/out/.text/g009.png | 0 events/fia-birthday-edendale/out/.text/g010.png | 0 events/fia-birthday-edendale/out/.text/g011.png | 0 events/fia-birthday-edendale/out/.text/g012.png | 0 events/fia-birthday-edendale/out/.text/g013.png | 0 events/fia-birthday-edendale/out/.text/g014.png | 0 events/fia-birthday-edendale/out/.text/g015.png | 0 events/fia-birthday-edendale/out/.text/g016.png | 0 events/fia-birthday-edendale/out/.text/g017.png | 0 events/fia-birthday-edendale/out/.text/g018.png | 0 events/fia-birthday-edendale/out/.text/g019.png | 0 events/fia-birthday-edendale/out/.text/g020.png | 0 events/fia-birthday-edendale/out/.text/g021.png | 0 events/fia-birthday-edendale/out/.text/g022.png | 0 events/fia-birthday-edendale/out/.text/g023.png | 0 events/fia-birthday-edendale/out/.text/g024.png | 0 events/fia-birthday-edendale/out/.text/g025.png | 0 events/fia-birthday-edendale/out/.text/g026.png | 0 events/fia-birthday-edendale/out/.text/g027.png | 0 events/fia-birthday-edendale/out/.text/g028.png | 0 events/fia-birthday-edendale/out/.text/g029.png | 0 events/fia-birthday-edendale/out/.text/g030.png | 0 events/fia-birthday-edendale/out/.text/g031.png | 0 events/fia-birthday-edendale/out/.text/g032.png | 0 events/fia-birthday-edendale/out/.text/g033.png | 0 events/fia-birthday-edendale/out/.text/g034.png | 0 events/fia-birthday-edendale/out/.text/g035.png | 0 events/fia-birthday-edendale/out/.text/g036.png | 0 events/fia-birthday-edendale/out/.text/g037.png | 0 events/fia-birthday-edendale/out/.text/g038.png | 0 events/fia-birthday-edendale/out/.text/g039.png | 0 events/fia-birthday-edendale/out/.text/g040.png | 0 events/fia-birthday-edendale/out/.text/g041.png | 0 events/fia-birthday-edendale/out/.text/g042.png | 0 events/fia-birthday-edendale/out/.text/g043.png | 0 events/fia-birthday-edendale/out/.text/g044.png | 0 events/fia-birthday-edendale/out/.text/g045.png | 0 events/fia-birthday-edendale/out/.text/g046.png | 0 events/fia-birthday-edendale/out/.text/g047.png | 0 events/fia-birthday-edendale/out/.text/g048.png | 0 events/fia-birthday-edendale/out/.text/g049.png | 0 events/fia-birthday-edendale/out/.text/layout-preview.png | 0 events/fia-birthday-edendale/out/.text/scrim.png | 0 events/fia-birthday-edendale/out/.v-1.0.png | 0 events/fia-birthday-edendale/out/.v-3.0.png | 0 events/fia-birthday-edendale/out/.v-8.5.png | 0 events/fia-birthday-edendale/out/clip-A.mp4 | 0 events/fia-birthday-edendale/out/clip-B.mp4 | 0 events/fia-birthday-edendale/out/fia-birthday-invite.mp4 | 0 events/fia-birthday-edendale/out/fia-bunny-a.png | 0 events/fia-birthday-edendale/out/fia-bunny-b.motion.mp4 | 0 events/fia-birthday-edendale/out/fia-bunny-b.png | 0 events/fia-birthday-edendale/out/fia-felt-A-v1.png | 0 events/fia-birthday-edendale/out/fia-felt-A-v2.png | 0 events/fia-birthday-edendale/out/fia-felt-B-v1.png | 0 events/fia-birthday-edendale/out/fia-felt-B-v2.png | 0 events/fia-birthday-edendale/out/fia-felt-combined.mp4 | 0 events/fia-birthday-edendale/out/fia-human-a.motion.mp4 | 0 events/fia-birthday-edendale/out/fia-human-a.png | 0 events/fia-birthday-edendale/out/fia-human-b.png | 0 events/fia-birthday-edendale/out/invite-bunny-a.png | 0 events/fia-birthday-edendale/out/invite-bunny-b.png | 0 events/fia-birthday-edendale/out/invite-v1.png | 0 events/fia-birthday-edendale/out/textcard.png | 0 events/fia-birthday-edendale/refs/68409d23.jpg | 0 events/fia-birthday-edendale/refs/9addf5d8.jpg | 0 events/fia-birthday-edendale/refs/b308548b.jpg | 0 events/fia-birthday-edendale/refs/d7ba1a64.jpg | 0 pop/RELEASES.md | 23 ++++++++++++++++++++++- pop/americomputadora/c/americomputadora.c | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------- pop/americomputadora/c/render-c.mjs | 31 ++++++++++++++++++++++--------- pop/americomputadora/c/score.h | 10 +++++----- pop/americomputadora/c/syl-cache/do.mp3 | 0 pop/americomputadora/c/syllables.mjs | 12 ++++++++---- pop/americomputadora/melody.json | 30 +++++++++++++++--------------- pop/boombaboom/c/boombaboom.c | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------- pop/boombaboom/c/render.sh | 10 +++++++++- pop/boombaboom/sources/highway.CREDITS.txt | 19 +++++++++++++++++++ pop/boombaboom/sources/triangle.CREDITS.txt | 20 ++++++++++++++++++++ pop/emotrib/c/build.sh | 9 +++++++++ pop/emotrib/c/emotrib | 0 pop/emotrib/c/emotrib.c | 470 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pop/emotrib/c/render.sh | 43 +++++++++++++++++++++++++++++++++++++++++++ pop/marimba/bin/render-flutterbap.mjs | 519 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------- pop/marimba/flutterbap.np | 36 ++++++++++++++++++++++++++++++++---- pop/marimba/synths/perc.mjs | 530 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pop/maytrax/c/pianotrax | 0 pop/maytrax/c/pianotrax.c | 534 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pop/maytrax/c/render-c.mjs | 20 ++++++++++++++++---- pop/nullabye/README.md | 37 +++++++++++++++++++++++++++++++++++++ pop/nullabye/bin/render-nullabata.mjs | 659 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pop/out/pop-library.json | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------ 117 file(s) changed, 3722 insertion(s)(+), 377 deletion(s)(-) diff --git a/events/fia-birthday-edendale/build-text.mjs b/events/fia-birthday-edendale/build-text.mjs new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/build-text.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +// build-text.mjs — per-letter animated invite text. +// each glyph: chunky Rockwell slab, its own party color, a sharp (un-blurred) +// drop shadow. letters bounce in one-by-one (damped cosine), then hold. +// emits out/.text/compose.sh — an ffmpeg command that overlays the animated +// letters onto the motion clip and muxes the flutterbap soundtrack. + +import { execSync } from "node:child_process"; +import { writeFileSync, mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const REPO = join(__dir, "..", ".."); +const OUT = join(__dir, "out"); +const TXT = join(OUT, ".text"); +mkdirSync(TXT, { recursive: true }); + +const FONT = "/System/Library/Fonts/Supplemental/Rockwell.ttc"; +const SHADOW = "#2a1020"; // deep plum, sharp +const DX = 6, DY = 7; // sharp drop-shadow offset +const W = 720, H = 1280; + +// fun party palette, cycled per glyph +const PAL = ["#FF3B6B", "#FF8A1E", "#FFD23F", "#36D399", "#34C3FF", "#9B6BFF", "#FF5FA2", "#7BE04A"]; + +// lines: text, pointsize, extra tracking (px between glyphs), top-y, entrance fx +// fx ∈ type | bounce | slideL | slideR | rise — one distinct effect per line +const LINES = [ + { t: "YOU'RE INVITED TO", size: 34, track: 10, y: 792, fx: "type" }, + { t: "FIA'S", size: 118, track: 6, y: 826, fx: "bounce" }, + { t: "BIRTHDAY", size: 92, track: 4, y: 952, fx: "slideL" }, + { t: "EDENDALE", size: 50, track: 12, y: 1060, fx: "slideR" }, + { t: "JULY 3RD · 8–11PM", size: 40, track: 8, y: 1126, fx: "rise" }, +]; + +const sh = (c) => execSync(c, { encoding: "utf8" }).trim(); + +// render one glyph (color fill + sharp shadow), return {png, w, h} +let gid = 0; +function glyph(ch, size, color) { + const fg = join(TXT, `.fg.png`); + const shp = join(TXT, `.sh.png`); + const chFile = join(TXT, `.ch.txt`); + writeFileSync(chFile, ch); // feed glyph via file → no shell-quoting hazards + const common = `-background none -font "${FONT}" -pointsize ${size}`; + sh(`magick ${common} -fill "${color}" label:@"${chFile}" -trim +repage "${fg}"`); + sh(`magick ${common} -fill "${SHADOW}" label:@"${chFile}" -trim +repage "${shp}"`); + const [w, h] = sh(`magick identify -format "%w %h" "${fg}"`).split(" ").map(Number); + const cw = w + DX, chh = h + DY; + const out = join(TXT, `g${String(gid).padStart(3, "0")}.png`); + sh(`magick -size ${cw}x${chh} xc:none "${shp}" -geometry +${DX}+${DY} -composite "${fg}" -geometry +0+0 -composite "${out}"`); + gid++; + return { png: out, w, h }; // advance by fg width w +} + +// scrim — gentle dark gradient at the bottom for legibility +const scrim = join(TXT, "scrim.png"); +sh(`magick -size 720x650 gradient:none-black -channel A -evaluate multiply 0.66 +channel "${scrim}"`); +sh(`magick -size ${W}x${H} xc:none "${scrim}" -gravity South -composite "${scrim}"`); + +// layout + timing +const placements = []; +let colorIdx = 0; +const T0 = 0.45; // first letter start +const STEP = 0.045; // stagger between letters +let order = 0; +for (const line of LINES) { + // measure advances first + const chars = [...line.t]; + const rendered = chars.map((ch) => { + if (ch === " ") return { space: true, adv: Math.round(line.size * 0.34) }; + const color = PAL[colorIdx++ % PAL.length]; + const g = glyph(ch, line.size, color); + return { ...g, adv: g.w + line.track }; + }); + const lineW = rendered.reduce((s, r) => s + r.adv, 0) - line.track; + let x = Math.round((W - lineW) / 2); + for (const r of rendered) { + if (!r.space) { + placements.push({ png: r.png, x, y: line.y, t: (T0 + order * STEP).toFixed(3), fx: line.fx }); + order++; + } + x += r.adv; + } +} + +console.log(`rendered ${gid} glyphs, ${placements.length} placed`); + +// static layout preview over the still (landed state) for quick QA +{ + const still = join(OUT, "fia-felt-A-v2.png"); + const prev = join(TXT, "layout-preview.png"); + let c = `magick "${still}" -resize ${W}x${H}^ -gravity center -extent ${W}x${H} -gravity NorthWest "${scrim}" -geometry +0+0 -composite`; + for (const p of placements) c += ` "${p.png}" -geometry +${p.x}+${p.y} -composite`; + c += ` "${prev}"`; + sh(c); + console.log("preview →", prev); +} + +// build ffmpeg compose.sh +const DUR = 14; // two 7s scenes +const MOTION = join(OUT, "fia-felt-combined.mp4"); // clip-A + clip-B concatenated +const AUDIO = join(REPO, "pop/marimba/out/flutterbap.mp3"); +const FINAL = join(OUT, "fia-birthday-invite.mp4"); + +// per-line entrance fx → overlay x/y expressions (dt = t - start). no commas +// allowed in ffmpeg expressions, so these use only exp/cos. +function fxExpr(p) { + const dt = `(t-${p.t})`; + switch (p.fx) { + case "type": return { x: `${p.x}`, y: `${p.y}`, fade: 0.10 }; + case "slideL": return { x: `${p.x} - 360*exp(-11*${dt})`, y: `${p.y}`, fade: 0.12 }; + case "slideR": return { x: `${p.x} + 360*exp(-11*${dt})`, y: `${p.y}`, fade: 0.12 }; + case "rise": return { x: `${p.x}`, y: `${p.y} + 80*exp(-11*${dt})`, fade: 0.14 }; + case "bounce": + default: return { x: `${p.x}`, y: `${p.y} - 55*exp(-9*${dt})*cos(14*${dt})`, fade: 0.16 }; + } +} + +const inputs = [`-i "${MOTION}"`, `-loop 1 -i "${scrim}"`]; +placements.forEach((p) => inputs.push(`-loop 1 -i "${p.png}"`)); +inputs.push(`-ss 6 -t ${DUR} -i "${AUDIO}"`); + +// filter graph +const fc = []; +// scrim is input 1, static full-frame, hold all 12s +fc.push(`[0:v][1:v]overlay=0:0:eof_action=repeat[b0]`); +placements.forEach((p, i) => { + const inIdx = i + 2; // glyph inputs start at 2 + const prev = `b${i}`; + const next = `b${i + 1}`; + const fade = `f${i}`; + const e = fxExpr(p); + fc.push(`[${inIdx}:v]format=rgba,fade=t=in:st=${p.t}:d=${e.fade}:alpha=1[${fade}]`); + fc.push(`[${prev}][${fade}]overlay=x=${e.x}:y=${e.y}:eof_action=repeat[${next}]`); +}); +const vlabel = `b${placements.length}`; +const aIdx = placements.length + 2; +fc.push(`[${aIdx}:a]afade=t=in:st=0:d=1.2,afade=t=out:st=${DUR - 1.4}:d=1.4,volume=0.95[a]`); + +const cmd = [ + `ffmpeg -y -v error`, + inputs.join(" "), + `-filter_complex "${fc.join(";")}"`, + `-map "[${vlabel}]" -map "[a]" -t ${DUR}`, + `-c:v libx264 -pix_fmt yuv420p -crf 19 -preset medium -movflags +faststart`, + `-c:a aac -b:a 192k`, + `"${FINAL}"`, +].join(" "); + +const composeSh = join(TXT, "compose.sh"); +writeFileSync(composeSh, "#!/bin/bash\nset -e\n" + cmd + "\necho \"✓ " + FINAL + "\"\n"); +console.log("wrote", composeSh); diff --git a/events/fia-birthday-edendale/gen-fia-bunny.mjs b/events/fia-birthday-edendale/gen-fia-bunny.mjs new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/gen-fia-bunny.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +// gen-fia-bunny.mjs — fia's birthday invite, take 2: a pixar-style 3D bunny +// whose face + spirit are based on real photos of fia (from the @whistlegraph / +// solafiya IG platter), celebrating at the edendale vintage bar. very bright. +// vertical 1024x1536, calm top/bottom safe zones for an event-text overlay. + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const REPO = join(__dir, "..", ".."); + +function loadOpenAIKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = join(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"); +} + +const JUNGLE_REFS = join(REPO, "pop/jungle/out/.refs"); +const REFS = [ + // fia identity (real photos, normalized copies from the solafiya pipeline) + join(JUNGLE_REFS, "_jas_Documents_Working_Desktop_gens_IMG_0933_jpg.png"), // clean frontal + join(JUNGLE_REFS, "ments_Working_Desktop_gens__fia_platter_sec4_png.png"), // smiling 3/4 + join(JUNGLE_REFS, "ments_Working_Desktop_gens__fia_platter_sec2_png.png"), // smiling frontal + // edendale room + join(__dir, "refs/9addf5d8.jpg"), // starlet-portrait wall + red damask + banquette + join(__dir, "refs/b308548b.jpg"), // pressed-tin ceiling + maroon wainscot +]; + +const PROMPT = `a charming 3D animated character in the polished style of a modern pixar feature film — glossy cinematic CG render, soft global illumination, big expressive sparkly eyes, fluffy fur with gentle subsurface scattering, rounded appealing shapes, warm and adorable. + +the character is a cute fluffy bunny whose face and spirit are based on the woman in the first three reference photos: keep HER warm dark-brown eyes, HER long dark wavy/curly hair framing the face (rendered as soft flowing hair around and between the bunny's ears), HER genuine friendly smile, HER warm olive skin tone in the little muzzle and cheeks. she is recognizably the same person, lovingly reimagined as a bunny. keep the real warm human-like eyes and gentle happy expression — that is what makes her HER. + +she wears a little cone-shaped birthday party hat and a tiny delicate star necklace. she sits at a small round marble bistro table inside a vintage 1920s silver lake bar — the room from the other reference photos: wine-red damask wallpaper, a gallery wall of small gold-framed vintage portraits, a tufted red leather banquette, a pressed-tin ceiling. she raises a pretty pink cocktail in a coupe glass in a joyful birthday toast. one or two soft pastel balloons drift nearby. + +the scene is VERY BRIGHTLY LIT — airy, sunny, light-filled, fresh and cheerful daylight glow. NOT dim, NOT moody, NOT candlelit. + +vertical portrait composition, the bunny centered in the bright middle band. keep the very top of the frame quiet — ceiling and balloon tops — and the very bottom quiet — table edge and floor — so there is calm empty space at top and bottom for text. + +only the ONE bunny — no other animals, no crowd. no human people in frame. no text, no lettering, no words anywhere. no harsh edges, no motion blur.`; + +async function main() { + const apiKey = loadOpenAIKey(); + const outDir = join(__dir, "out"); + mkdirSync(outDir, { recursive: true }); + + for (const ref of REFS) { + if (!existsSync(ref)) throw new Error(`ref missing: ${ref}`); + } + + const fd = new FormData(); + fd.append("model", "gpt-image-2"); + fd.append("prompt", PROMPT); + fd.append("size", "1024x1536"); + fd.append("quality", "high"); + fd.append("n", "1"); + for (const ref of REFS) { + const buf = readFileSync(ref); + const ext = ref.toLowerCase().endsWith(".png") ? "png" : "jpeg"; + fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), ref.split("/").pop()); + } + + console.log("→ gpt-image-2 edit, 1024x1536, 3 fia + 2 edendale refs…"); + const res = await fetch("https://api.openai.com/v1/images/edits", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}` }, + body: fd, + }); + const json = await res.json(); + if (!res.ok || !json.data?.[0]?.b64_json) { + console.error("FAILED:", JSON.stringify(json, null, 2)); + process.exit(1); + } + const stamp = process.argv[2] || "v1"; + const out = join(outDir, `fia-bunny-${stamp}.png`); + writeFileSync(out, Buffer.from(json.data[0].b64_json, "base64")); + console.log("✓ wrote", out); +} + +main(); diff --git a/events/fia-birthday-edendale/gen-fia-felt.mjs b/events/fia-birthday-edendale/gen-fia-felt.mjs new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/gen-fia-felt.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node +// gen-fia-felt.mjs — fia's birthday invite, felt edition. everything is a +// handmade needle-felted wool diorama, lightly colored (pale pastels). +// fia recognizable (from real IG-platter photos), bunnies, edendale patio. +// two scenes: A = patio toast w/ bunnies, B = cozy close-up w/ bunny + cake. +// usage: node gen-fia-felt.mjs [stamp] + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const REPO = join(__dir, "..", ".."); + +function loadOpenAIKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = join(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"); +} + +const JUNGLE_REFS = join(REPO, "pop/jungle/out/.refs"); +const REFS = [ + join(JUNGLE_REFS, "_jas_Documents_Working_Desktop_gens_IMG_0933_jpg.png"), // fia frontal (primary) + join(JUNGLE_REFS, "ments_Working_Desktop_gens__fia_platter_sec4_png.png"), + join(JUNGLE_REFS, "ments_Working_Desktop_gens__fia_platter_sec2_png.png"), + join(__dir, "refs/d7ba1a64.jpg"), // edendale string-lit patio +]; + +const FELT = `everything in the image is a handmade NEEDLE-FELTED WOOL DIORAMA — every surface is soft fuzzy felted wool with visible wool fibers, fuzzy soft edges, little imperfections, a cozy handmade craft look, as if photographed up close on a tabletop. LIGHTLY COLORED — a soft pale pastel wool palette (cream, dusty rose, pale sage, soft lavender, butter yellow), gentle and airy. soft warm diffuse light, shallow depth of field. NOT plastic, NOT glossy, NOT cgi-smooth — soft matte wool throughout.`; + +const FIA = `a fully needle-felted WOOL figure of a young woman — her ENTIRE figure including her FACE, SKIN, and HAIR is made of soft felted wool: fuzzy matte wool skin with visible fibers, hair felted from dark-brown wool yarn in soft curls, warm dark-brown eyes as tiny round glass beads, a small stitched felt smile. she still clearly resembles the woman in the first three reference photos — same hair, same warm friendly face shape and expression — but rendered entirely as a handmade felt doll. she wears a soft grey felted button-up shirt and a tiny star-bead necklace. NO real human skin anywhere — she is all wool.`; + +const SCENES = { + A: `${FELT} + +${FIA} she wears a little felt cone party hat and sits at a tiny round felt bistro table on a felted version of the edendale garden patio from the reference photo — felted bougainvillea climbing the walls, tiny felt string-light bulbs strung overhead, little felt archways. she raises a tiny felt cocktail coupe (pale pink wool) in a joyful birthday toast, beaming. + +gathered lovingly around her are several adorable fluffy felted bunnies in pale wool (cream, grey, soft brown) — little four-legged felt rabbits, NOT human-like, NOT clothed. tiny felt confetti bits are scattered on the table and tossed in the air. a soft felt balloon or two. a small glowing bead 'mood ring' on her felt hand. + +vertical portrait composition, the felt fia centered. keep the very top quiet (felt string lights, balloon tops) and the very bottom quiet (table edge) for a text overlay. no text, no lettering. no other people — just felt fia and her felt bunnies.`, + + B: `${FELT} + +a cozy close-up: ${FIA} she leans in cheek-to-cheek with one big fluffy felted bunny (soft cream wool), both beaming with joy. she wears her little felt party hat. in front of them sits a tiny felt birthday cake with a single soft felt candle-flame. behind them, soft felt balloons and a few warm felt string-lights of the edendale patio, felted bougainvillea. tiny felt confetti drifting. + +vertical portrait composition, the two faces in the upper-middle band, warm and intimate. keep the very bottom quiet (the little cake and table) for a text overlay. no text, no lettering. just felt fia and her felt bunny.`, +}; + +async function main() { + const scene = (process.argv[2] || "A").toUpperCase(); + if (!SCENES[scene]) throw new Error(`scene must be A or B (got ${scene})`); + const stamp = process.argv[3] || "v1"; + const apiKey = loadOpenAIKey(); + const outDir = join(__dir, "out"); + mkdirSync(outDir, { recursive: true }); + for (const ref of REFS) if (!existsSync(ref)) throw new Error(`ref missing: ${ref}`); + + const fd = new FormData(); + fd.append("model", "gpt-image-2"); + fd.append("prompt", SCENES[scene]); + fd.append("size", "1024x1536"); + fd.append("quality", "high"); + fd.append("n", "1"); + for (const ref of REFS) { + const buf = readFileSync(ref); + const ext = ref.toLowerCase().endsWith(".png") ? "png" : "jpeg"; + fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), ref.split("/").pop()); + } + + console.log(`→ gpt-image-2 felt scene ${scene}, 1024x1536…`); + const res = await fetch("https://api.openai.com/v1/images/edits", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}` }, + body: fd, + }); + const json = await res.json(); + if (!res.ok || !json.data?.[0]?.b64_json) { + console.error("FAILED:", JSON.stringify(json, null, 2)); + process.exit(1); + } + const out = join(outDir, `fia-felt-${scene}-${stamp}.png`); + writeFileSync(out, Buffer.from(json.data[0].b64_json, "base64")); + console.log("✓ wrote", out); +} + +main(); diff --git a/events/fia-birthday-edendale/gen-fia-human.mjs b/events/fia-birthday-edendale/gen-fia-human.mjs new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/gen-fia-human.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node +// gen-fia-human.mjs — fia's birthday invite: a pixar-style 3D animated version +// of fia HERSELF (human), based on real photos from the @whistlegraph/solafiya +// IG platter, celebrating at the edendale vintage bar. very bright. +// vertical 1024x1536, calm top/bottom safe zones for an event-text overlay. + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const REPO = join(__dir, "..", ".."); + +function loadOpenAIKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = join(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"); +} + +const JUNGLE_REFS = join(REPO, "pop/jungle/out/.refs"); +const REFS = [ + // fia identity (real photos, normalized copies from the solafiya pipeline) + join(JUNGLE_REFS, "_jas_Documents_Working_Desktop_gens_IMG_0933_jpg.png"), // clean frontal + join(JUNGLE_REFS, "ments_Working_Desktop_gens__fia_platter_sec4_png.png"), // smiling 3/4 + join(JUNGLE_REFS, "ments_Working_Desktop_gens__fia_platter_sec2_png.png"), // smiling frontal + // edendale room — the magical back patio (bougainvillea walls, arches, string lights) + join(__dir, "refs/d7ba1a64.jpg"), +]; + +const PROMPT = `a charming 3D animated character in the polished style of a modern pixar feature film — glossy cinematic CG render, soft global illumination, big warm expressive eyes, soft skin with gentle subsurface scattering, appealing stylized proportions, warm and adorable. + +the character is a young woman based closely on the woman in the first three reference photos — recognizably HER: keep her warm dark-brown eyes, her long dark wavy/curly hair, her genuine friendly smile, her warm olive skin tone and her real facial features and bone structure. she wears a soft grey button-up shirt and a tiny delicate star necklace, just like in the photos. lovingly stylized as a pixar character but unmistakably the same person. + +the setting is fantastical and magical: edendale's enchanted back garden patio from the reference photo — tall old walls covered in climbing bougainvillea and vines, romantic stone archways, and crisscrossing strings of warm glowing fairy lights overhead. but dialed up into a dreamy storybook fantasy — soft floating sparkles and bokeh in the air, a gentle luminous glow, magical and whimsical. + +she is celebrating her birthday: a little cone-shaped party hat perched on her curls, sitting at a small round bistro table. she raises a pretty pink cocktail in a coupe glass in a joyful birthday toast, beaming. her other hand rests on the very edge of the table in the foreground, wearing a glowing MOOD RING — a chunky ring with a domed gemstone that radiates shifting jewel colors (teal, violet, amber), casting a soft magical glow. soft pastel balloons drift nearby. + +gathered lovingly around her are several adorable, real, fluffy pet bunnies — soft rabbits of different colors (cream, grey, brown, white) perched on the bench, on the table, and nestled beside her, all cute and happy, joining the party. they are ordinary cute bunnies (four-legged little rabbits), NOT human-like, NOT wearing clothes. + +bright, colorful confetti is tossed through the air and scattered across the table, falling festively around her. + +the light is LUMINOUS and bright-magical — glowing fairy lights, a warm radiant dreamy glow, fresh and enchanting. NOT dim, NOT dark, NOT gloomy. + +vertical portrait composition, she sits centered in the glowing middle band. keep the very top of the frame quiet — fairy lights and balloon tops — and the very bottom quiet — table edge and her mood-ring hand — so there is calm space at top and bottom for text. + +no other PEOPLE besides her — just fia and her cute bunnies. no text, no lettering, no words anywhere. no harsh edges, no motion blur.`; + +async function main() { + const apiKey = loadOpenAIKey(); + const outDir = join(__dir, "out"); + mkdirSync(outDir, { recursive: true }); + + for (const ref of REFS) { + if (!existsSync(ref)) throw new Error(`ref missing: ${ref}`); + } + + const fd = new FormData(); + fd.append("model", "gpt-image-2"); + fd.append("prompt", PROMPT); + fd.append("size", "1024x1536"); + fd.append("quality", "high"); + fd.append("n", "1"); + for (const ref of REFS) { + const buf = readFileSync(ref); + const ext = ref.toLowerCase().endsWith(".png") ? "png" : "jpeg"; + fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), ref.split("/").pop()); + } + + console.log("→ gpt-image-2 edit, 1024x1536, 3 fia + 2 edendale refs (human)…"); + const res = await fetch("https://api.openai.com/v1/images/edits", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}` }, + body: fd, + }); + const json = await res.json(); + if (!res.ok || !json.data?.[0]?.b64_json) { + console.error("FAILED:", JSON.stringify(json, null, 2)); + process.exit(1); + } + const stamp = process.argv[2] || "v1"; + const out = join(outDir, `fia-human-${stamp}.png`); + writeFileSync(out, Buffer.from(json.data[0].b64_json, "base64")); + console.log("✓ wrote", out); +} + +main(); diff --git a/events/fia-birthday-edendale/gen-invite.mjs b/events/fia-birthday-edendale/gen-invite.mjs new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/gen-invite.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// gen-invite.mjs — fia's birthday invite: edendale vintage bar reimagined as a +// soft pastel-surreal party of fancy fluffy animals. gpt-image-2 image-edit, +// conditioned on real edendale interior reference photos. vertical 1024x1536, +// calm top/bottom safe zones reserved for an event-text overlay added in post. + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const REPO = join(__dir, "..", ".."); + +function loadOpenAIKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = join(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"); +} + +const REFS = [ + join(__dir, "refs/9addf5d8.jpg"), // starlet-portrait wall + red damask + banquette + join(__dir, "refs/b308548b.jpg"), // pressed-tin ceiling + maroon wainscot tile +]; + +const PROMPT = `a soft, dreamy, painterly illustration — a single adorable fluffy bunny celebrating a birthday inside a vintage 1920s silver lake bar, exactly the room in the reference photos: wine-red damask wallpaper, a gallery wall of small gold-framed vintage portraits, a tufted red leather banquette, a pressed-tin ceiling. + +but the scene is VERY BRIGHTLY LIT — airy, sunny, light-filled, fresh and cheerful, bright clean daylight glow. NOT dim, NOT moody, NOT candlelit. + +just one bunny — plush, huggable, big soft eyes, wearing a little cone-shaped birthday party hat, sitting at a small table holding a pretty cocktail in a coupe glass, raising it in a happy toast. cute, tender, slightly mischievous. only the ONE bunny — no other animals, no crowd, no guests. one or two soft pastel balloons nearby. + +soft pastel-surreal storybook treatment: gentle bright pastel palette, dreamy painterly brushwork, a touch of sparkle. joyful and fresh. + +vertical portrait composition, the bunny centered in the bright middle band. keep the very top quiet — wallpaper, ceiling, balloon tops — and keep the very bottom quiet — table edge and floor — so there is calm empty space at top and bottom for text. + +no text, no lettering, no signage, no words anywhere in the image. no human people. no harsh edges, no motion blur.`; + +async function main() { + const apiKey = loadOpenAIKey(); + const outDir = join(__dir, "out"); + mkdirSync(outDir, { recursive: true }); + + const fd = new FormData(); + fd.append("model", "gpt-image-2"); + fd.append("prompt", PROMPT); + fd.append("size", "1024x1536"); + fd.append("quality", "high"); + fd.append("n", "1"); + for (const ref of REFS) { + const buf = readFileSync(ref); + const ext = ref.toLowerCase().endsWith(".png") ? "png" : "jpeg"; + fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), ref.split("/").pop()); + } + + console.log("→ gpt-image-2 edit, 1024x1536, 2 edendale refs…"); + const res = await fetch("https://api.openai.com/v1/images/edits", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}` }, + body: fd, + }); + const json = await res.json(); + if (!res.ok || !json.data?.[0]?.b64_json) { + console.error("FAILED:", JSON.stringify(json, null, 2)); + process.exit(1); + } + const stamp = process.argv[2] || "v1"; + const out = join(outDir, `invite-${stamp}.png`); + writeFileSync(out, Buffer.from(json.data[0].b64_json, "base64")); + console.log("✓ wrote", out); +} + +main(); diff --git a/events/fia-birthday-edendale/out/.card-base.png b/events/fia-birthday-edendale/out/.card-base.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.card-base.png diff --git a/events/fia-birthday-edendale/out/.card-preview.png b/events/fia-birthday-edendale/out/.card-preview.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.card-preview.png diff --git a/events/fia-birthday-edendale/out/.chk-B.png b/events/fia-birthday-edendale/out/.chk-B.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.chk-B.png diff --git a/events/fia-birthday-edendale/out/.chk-anim.png b/events/fia-birthday-edendale/out/.chk-anim.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.chk-anim.png diff --git a/events/fia-birthday-edendale/out/.chk-settled.png b/events/fia-birthday-edendale/out/.chk-settled.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.chk-settled.png diff --git a/events/fia-birthday-edendale/out/.frame-mid.png b/events/fia-birthday-edendale/out/.frame-mid.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.frame-mid.png diff --git a/events/fia-birthday-edendale/out/.scrim.png b/events/fia-birthday-edendale/out/.scrim.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.scrim.png diff --git a/events/fia-birthday-edendale/out/.text/.ch.txt b/events/fia-birthday-edendale/out/.text/.ch.txt new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/.ch.txt @@ -0,0 +1,1 @@ +M \ No newline at end of file diff --git a/events/fia-birthday-edendale/out/.text/.fg.png b/events/fia-birthday-edendale/out/.text/.fg.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/.fg.png diff --git a/events/fia-birthday-edendale/out/.text/.sh.png b/events/fia-birthday-edendale/out/.text/.sh.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/.sh.png diff --git a/events/fia-birthday-edendale/out/.text/compose.sh b/events/fia-birthday-edendale/out/.text/compose.sh new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/compose.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +ffmpeg -y -v error -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/fia-felt-combined.mp4" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/scrim.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g000.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g001.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g002.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g003.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g004.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g005.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g006.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g007.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g008.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g009.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g010.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g011.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g012.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g013.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g014.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g015.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g016.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g017.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g018.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g019.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g020.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g021.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g022.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g023.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g024.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g025.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g026.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g027.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g028.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g029.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g030.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g031.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g032.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g033.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g034.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g035.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g036.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g037.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g038.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g039.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g040.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g041.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g042.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g043.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g044.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g045.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g046.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g047.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g048.png" -loop 1 -i "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/.text/g049.png" -ss 6 -t 14 -i "/Users/jas/aesthetic-computer/pop/marimba/out/flutterbap.mp3" -filter_complex "[0:v][1:v]overlay=0:0:eof_action=repeat[b0];[2:v]format=rgba,fade=t=in:st=0.450:d=0.1:alpha=1[f0];[b0][f0]overlay=x=119:y=792:eof_action=repeat[b1];[3:v]format=rgba,fade=t=in:st=0.495:d=0.1:alpha=1[f1];[b1][f1]overlay=x=155:y=792:eof_action=repeat[b2];[4:v]format=rgba,fade=t=in:st=0.540:d=0.1:alpha=1[f2];[b2][f2]overlay=x=192:y=792:eof_action=repeat[b3];[5:v]format=rgba,fade=t=in:st=0.585:d=0.1:alpha=1[f3];[b3][f3]overlay=x=227:y=792:eof_action=repeat[b4];[6:v]format=rgba,fade=t=in:st=0.630:d=0.1:alpha=1[f4];[b4][f4]overlay=x=241:y=792:eof_action=repeat[b5];[7:v]format=rgba,fade=t=in:st=0.675:d=0.1:alpha=1[f5];[b5][f5]overlay=x=274:y=792:eof_action=repeat[b6];[8:v]format=rgba,fade=t=in:st=0.720:d=0.1:alpha=1[f6];[b6][f6]overlay=x=318:y=792:eof_action=repeat[b7];[9:v]format=rgba,fade=t=in:st=0.765:d=0.1:alpha=1[f7];[b7][f7]overlay=x=338:y=792:eof_action=repeat[b8];[10:v]format=rgba,fade=t=in:st=0.810:d=0.1:alpha=1[f8];[b8][f8]overlay=x=375:y=792:eof_action=repeat[b9];[11:v]format=rgba,fade=t=in:st=0.855:d=0.1:alpha=1[f9];[b9][f9]overlay=x=412:y=792:eof_action=repeat[b10];[12:v]format=rgba,fade=t=in:st=0.900:d=0.1:alpha=1[f10];[b10][f10]overlay=x=432:y=792:eof_action=repeat[b11];[13:v]format=rgba,fade=t=in:st=0.945:d=0.1:alpha=1[f11];[b11][f11]overlay=x=463:y=792:eof_action=repeat[b12];[14:v]format=rgba,fade=t=in:st=0.990:d=0.1:alpha=1[f12];[b12][f12]overlay=x=495:y=792:eof_action=repeat[b13];[15:v]format=rgba,fade=t=in:st=1.035:d=0.1:alpha=1[f13];[b13][f13]overlay=x=543:y=792:eof_action=repeat[b14];[16:v]format=rgba,fade=t=in:st=1.080:d=0.1:alpha=1[f14];[b14][f14]overlay=x=574:y=792:eof_action=repeat[b15];[17:v]format=rgba,fade=t=in:st=1.125:d=0.16:alpha=1[f15];[b15][f15]overlay=x=215:y=826 - 55*exp(-9*(t-1.125))*cos(14*(t-1.125)):eof_action=repeat[b16];[18:v]format=rgba,fade=t=in:st=1.170:d=0.16:alpha=1[f16];[b16][f16]overlay=x=289:y=826 - 55*exp(-9*(t-1.170))*cos(14*(t-1.170)):eof_action=repeat[b17];[19:v]format=rgba,fade=t=in:st=1.215:d=0.16:alpha=1[f17];[b17][f17]overlay=x=328:y=826 - 55*exp(-9*(t-1.215))*cos(14*(t-1.215)):eof_action=repeat[b18];[20:v]format=rgba,fade=t=in:st=1.260:d=0.16:alpha=1[f18];[b18][f18]overlay=x=427:y=826 - 55*exp(-9*(t-1.260))*cos(14*(t-1.260)):eof_action=repeat[b19];[21:v]format=rgba,fade=t=in:st=1.305:d=0.16:alpha=1[f19];[b19][f19]overlay=x=446:y=826 - 55*exp(-9*(t-1.305))*cos(14*(t-1.305)):eof_action=repeat[b20];[22:v]format=rgba,fade=t=in:st=1.350:d=0.12:alpha=1[f20];[b20][f20]overlay=x=113 - 360*exp(-11*(t-1.350)):y=952:eof_action=repeat[b21];[23:v]format=rgba,fade=t=in:st=1.395:d=0.12:alpha=1[f21];[b21][f21]overlay=x=170 - 360*exp(-11*(t-1.395)):y=952:eof_action=repeat[b22];[24:v]format=rgba,fade=t=in:st=1.440:d=0.12:alpha=1[f22];[b22][f22]overlay=x=200 - 360*exp(-11*(t-1.440)):y=952:eof_action=repeat[b23];[25:v]format=rgba,fade=t=in:st=1.485:d=0.12:alpha=1[f23];[b23][f23]overlay=x=264 - 360*exp(-11*(t-1.485)):y=952:eof_action=repeat[b24];[26:v]format=rgba,fade=t=in:st=1.530:d=0.12:alpha=1[f24];[b24][f24]overlay=x=323 - 360*exp(-11*(t-1.530)):y=952:eof_action=repeat[b25];[27:v]format=rgba,fade=t=in:st=1.575:d=0.12:alpha=1[f25];[b25][f25]overlay=x=393 - 360*exp(-11*(t-1.575)):y=952:eof_action=repeat[b26];[28:v]format=rgba,fade=t=in:st=1.620:d=0.12:alpha=1[f26];[b26][f26]overlay=x=465 - 360*exp(-11*(t-1.620)):y=952:eof_action=repeat[b27];[29:v]format=rgba,fade=t=in:st=1.665:d=0.12:alpha=1[f27];[b27][f27]overlay=x=541 - 360*exp(-11*(t-1.665)):y=952:eof_action=repeat[b28];[30:v]format=rgba,fade=t=in:st=1.710:d=0.12:alpha=1[f28];[b28][f28]overlay=x=179 + 360*exp(-11*(t-1.710)):y=1060:eof_action=repeat[b29];[31:v]format=rgba,fade=t=in:st=1.755:d=0.12:alpha=1[f29];[b29][f29]overlay=x=223 + 360*exp(-11*(t-1.755)):y=1060:eof_action=repeat[b30];[32:v]format=rgba,fade=t=in:st=1.800:d=0.12:alpha=1[f30];[b30][f30]overlay=x=273 + 360*exp(-11*(t-1.800)):y=1060:eof_action=repeat[b31];[33:v]format=rgba,fade=t=in:st=1.845:d=0.12:alpha=1[f31];[b31][f31]overlay=x=317 + 360*exp(-11*(t-1.845)):y=1060:eof_action=repeat[b32];[34:v]format=rgba,fade=t=in:st=1.890:d=0.12:alpha=1[f32];[b32][f32]overlay=x=368 + 360*exp(-11*(t-1.890)):y=1060:eof_action=repeat[b33];[35:v]format=rgba,fade=t=in:st=1.935:d=0.12:alpha=1[f33];[b33][f33]overlay=x=418 + 360*exp(-11*(t-1.935)):y=1060:eof_action=repeat[b34];[36:v]format=rgba,fade=t=in:st=1.980:d=0.12:alpha=1[f34];[b34][f34]overlay=x=470 + 360*exp(-11*(t-1.980)):y=1060:eof_action=repeat[b35];[37:v]format=rgba,fade=t=in:st=2.025:d=0.12:alpha=1[f35];[b35][f35]overlay=x=510 + 360*exp(-11*(t-2.025)):y=1060:eof_action=repeat[b36];[38:v]format=rgba,fade=t=in:st=2.070:d=0.14:alpha=1[f36];[b36][f36]overlay=x=130:y=1126 + 80*exp(-11*(t-2.070)):eof_action=repeat[b37];[39:v]format=rgba,fade=t=in:st=2.115:d=0.14:alpha=1[f37];[b37][f37]overlay=x=154:y=1126 + 80*exp(-11*(t-2.115)):eof_action=repeat[b38];[40:v]format=rgba,fade=t=in:st=2.160:d=0.14:alpha=1[f38];[b38][f38]overlay=x=192:y=1126 + 80*exp(-11*(t-2.160)):eof_action=repeat[b39];[41:v]format=rgba,fade=t=in:st=2.205:d=0.14:alpha=1[f39];[b39][f39]overlay=x=223:y=1126 + 80*exp(-11*(t-2.205)):eof_action=repeat[b40];[42:v]format=rgba,fade=t=in:st=2.250:d=0.14:alpha=1[f40];[b40][f40]overlay=x=275:y=1126 + 80*exp(-11*(t-2.250)):eof_action=repeat[b41];[43:v]format=rgba,fade=t=in:st=2.295:d=0.14:alpha=1[f41];[b41][f41]overlay=x=304:y=1126 + 80*exp(-11*(t-2.295)):eof_action=repeat[b42];[44:v]format=rgba,fade=t=in:st=2.340:d=0.14:alpha=1[f42];[b42][f42]overlay=x=339:y=1126 + 80*exp(-11*(t-2.340)):eof_action=repeat[b43];[45:v]format=rgba,fade=t=in:st=2.385:d=0.14:alpha=1[f43];[b43][f43]overlay=x=391:y=1126 + 80*exp(-11*(t-2.385)):eof_action=repeat[b44];[46:v]format=rgba,fade=t=in:st=2.430:d=0.14:alpha=1[f44];[b44][f44]overlay=x=419:y=1126 + 80*exp(-11*(t-2.430)):eof_action=repeat[b45];[47:v]format=rgba,fade=t=in:st=2.475:d=0.14:alpha=1[f45];[b45][f45]overlay=x=449:y=1126 + 80*exp(-11*(t-2.475)):eof_action=repeat[b46];[48:v]format=rgba,fade=t=in:st=2.520:d=0.14:alpha=1[f46];[b46][f46]overlay=x=480:y=1126 + 80*exp(-11*(t-2.520)):eof_action=repeat[b47];[49:v]format=rgba,fade=t=in:st=2.565:d=0.14:alpha=1[f47];[b47][f47]overlay=x=501:y=1126 + 80*exp(-11*(t-2.565)):eof_action=repeat[b48];[50:v]format=rgba,fade=t=in:st=2.610:d=0.14:alpha=1[f48];[b48][f48]overlay=x=522:y=1126 + 80*exp(-11*(t-2.610)):eof_action=repeat[b49];[51:v]format=rgba,fade=t=in:st=2.655:d=0.14:alpha=1[f49];[b49][f49]overlay=x=554:y=1126 + 80*exp(-11*(t-2.655)):eof_action=repeat[b50];[52:a]afade=t=in:st=0:d=1.2,afade=t=out:st=12.6:d=1.4,volume=0.95[a]" -map "[b50]" -map "[a]" -t 14 -c:v libx264 -pix_fmt yuv420p -crf 19 -preset medium -movflags +faststart -c:a aac -b:a 192k "/Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/fia-birthday-invite.mp4" +echo "✓ /Users/jas/aesthetic-computer/events/fia-birthday-edendale/out/fia-birthday-invite.mp4" diff --git a/events/fia-birthday-edendale/out/.text/g000.png b/events/fia-birthday-edendale/out/.text/g000.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g000.png diff --git a/events/fia-birthday-edendale/out/.text/g001.png b/events/fia-birthday-edendale/out/.text/g001.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g001.png diff --git a/events/fia-birthday-edendale/out/.text/g002.png b/events/fia-birthday-edendale/out/.text/g002.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g002.png diff --git a/events/fia-birthday-edendale/out/.text/g003.png b/events/fia-birthday-edendale/out/.text/g003.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g003.png diff --git a/events/fia-birthday-edendale/out/.text/g004.png b/events/fia-birthday-edendale/out/.text/g004.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g004.png diff --git a/events/fia-birthday-edendale/out/.text/g005.png b/events/fia-birthday-edendale/out/.text/g005.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g005.png diff --git a/events/fia-birthday-edendale/out/.text/g006.png b/events/fia-birthday-edendale/out/.text/g006.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g006.png diff --git a/events/fia-birthday-edendale/out/.text/g007.png b/events/fia-birthday-edendale/out/.text/g007.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g007.png diff --git a/events/fia-birthday-edendale/out/.text/g008.png b/events/fia-birthday-edendale/out/.text/g008.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g008.png diff --git a/events/fia-birthday-edendale/out/.text/g009.png b/events/fia-birthday-edendale/out/.text/g009.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g009.png diff --git a/events/fia-birthday-edendale/out/.text/g010.png b/events/fia-birthday-edendale/out/.text/g010.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g010.png diff --git a/events/fia-birthday-edendale/out/.text/g011.png b/events/fia-birthday-edendale/out/.text/g011.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g011.png diff --git a/events/fia-birthday-edendale/out/.text/g012.png b/events/fia-birthday-edendale/out/.text/g012.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g012.png diff --git a/events/fia-birthday-edendale/out/.text/g013.png b/events/fia-birthday-edendale/out/.text/g013.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g013.png diff --git a/events/fia-birthday-edendale/out/.text/g014.png b/events/fia-birthday-edendale/out/.text/g014.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g014.png diff --git a/events/fia-birthday-edendale/out/.text/g015.png b/events/fia-birthday-edendale/out/.text/g015.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g015.png diff --git a/events/fia-birthday-edendale/out/.text/g016.png b/events/fia-birthday-edendale/out/.text/g016.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g016.png diff --git a/events/fia-birthday-edendale/out/.text/g017.png b/events/fia-birthday-edendale/out/.text/g017.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g017.png diff --git a/events/fia-birthday-edendale/out/.text/g018.png b/events/fia-birthday-edendale/out/.text/g018.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g018.png diff --git a/events/fia-birthday-edendale/out/.text/g019.png b/events/fia-birthday-edendale/out/.text/g019.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g019.png diff --git a/events/fia-birthday-edendale/out/.text/g020.png b/events/fia-birthday-edendale/out/.text/g020.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g020.png diff --git a/events/fia-birthday-edendale/out/.text/g021.png b/events/fia-birthday-edendale/out/.text/g021.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g021.png diff --git a/events/fia-birthday-edendale/out/.text/g022.png b/events/fia-birthday-edendale/out/.text/g022.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g022.png diff --git a/events/fia-birthday-edendale/out/.text/g023.png b/events/fia-birthday-edendale/out/.text/g023.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g023.png diff --git a/events/fia-birthday-edendale/out/.text/g024.png b/events/fia-birthday-edendale/out/.text/g024.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g024.png diff --git a/events/fia-birthday-edendale/out/.text/g025.png b/events/fia-birthday-edendale/out/.text/g025.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g025.png diff --git a/events/fia-birthday-edendale/out/.text/g026.png b/events/fia-birthday-edendale/out/.text/g026.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g026.png diff --git a/events/fia-birthday-edendale/out/.text/g027.png b/events/fia-birthday-edendale/out/.text/g027.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g027.png diff --git a/events/fia-birthday-edendale/out/.text/g028.png b/events/fia-birthday-edendale/out/.text/g028.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g028.png diff --git a/events/fia-birthday-edendale/out/.text/g029.png b/events/fia-birthday-edendale/out/.text/g029.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g029.png diff --git a/events/fia-birthday-edendale/out/.text/g030.png b/events/fia-birthday-edendale/out/.text/g030.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g030.png diff --git a/events/fia-birthday-edendale/out/.text/g031.png b/events/fia-birthday-edendale/out/.text/g031.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g031.png diff --git a/events/fia-birthday-edendale/out/.text/g032.png b/events/fia-birthday-edendale/out/.text/g032.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g032.png diff --git a/events/fia-birthday-edendale/out/.text/g033.png b/events/fia-birthday-edendale/out/.text/g033.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g033.png diff --git a/events/fia-birthday-edendale/out/.text/g034.png b/events/fia-birthday-edendale/out/.text/g034.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g034.png diff --git a/events/fia-birthday-edendale/out/.text/g035.png b/events/fia-birthday-edendale/out/.text/g035.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g035.png diff --git a/events/fia-birthday-edendale/out/.text/g036.png b/events/fia-birthday-edendale/out/.text/g036.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g036.png diff --git a/events/fia-birthday-edendale/out/.text/g037.png b/events/fia-birthday-edendale/out/.text/g037.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g037.png diff --git a/events/fia-birthday-edendale/out/.text/g038.png b/events/fia-birthday-edendale/out/.text/g038.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g038.png diff --git a/events/fia-birthday-edendale/out/.text/g039.png b/events/fia-birthday-edendale/out/.text/g039.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g039.png diff --git a/events/fia-birthday-edendale/out/.text/g040.png b/events/fia-birthday-edendale/out/.text/g040.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g040.png diff --git a/events/fia-birthday-edendale/out/.text/g041.png b/events/fia-birthday-edendale/out/.text/g041.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g041.png diff --git a/events/fia-birthday-edendale/out/.text/g042.png b/events/fia-birthday-edendale/out/.text/g042.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g042.png diff --git a/events/fia-birthday-edendale/out/.text/g043.png b/events/fia-birthday-edendale/out/.text/g043.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g043.png diff --git a/events/fia-birthday-edendale/out/.text/g044.png b/events/fia-birthday-edendale/out/.text/g044.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g044.png diff --git a/events/fia-birthday-edendale/out/.text/g045.png b/events/fia-birthday-edendale/out/.text/g045.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g045.png diff --git a/events/fia-birthday-edendale/out/.text/g046.png b/events/fia-birthday-edendale/out/.text/g046.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g046.png diff --git a/events/fia-birthday-edendale/out/.text/g047.png b/events/fia-birthday-edendale/out/.text/g047.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g047.png diff --git a/events/fia-birthday-edendale/out/.text/g048.png b/events/fia-birthday-edendale/out/.text/g048.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g048.png diff --git a/events/fia-birthday-edendale/out/.text/g049.png b/events/fia-birthday-edendale/out/.text/g049.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/g049.png diff --git a/events/fia-birthday-edendale/out/.text/layout-preview.png b/events/fia-birthday-edendale/out/.text/layout-preview.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/layout-preview.png diff --git a/events/fia-birthday-edendale/out/.text/scrim.png b/events/fia-birthday-edendale/out/.text/scrim.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.text/scrim.png diff --git a/events/fia-birthday-edendale/out/.v-1.0.png b/events/fia-birthday-edendale/out/.v-1.0.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.v-1.0.png diff --git a/events/fia-birthday-edendale/out/.v-3.0.png b/events/fia-birthday-edendale/out/.v-3.0.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.v-3.0.png diff --git a/events/fia-birthday-edendale/out/.v-8.5.png b/events/fia-birthday-edendale/out/.v-8.5.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/.v-8.5.png diff --git a/events/fia-birthday-edendale/out/clip-A.mp4 b/events/fia-birthday-edendale/out/clip-A.mp4 new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/clip-A.mp4 diff --git a/events/fia-birthday-edendale/out/clip-B.mp4 b/events/fia-birthday-edendale/out/clip-B.mp4 new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/clip-B.mp4 diff --git a/events/fia-birthday-edendale/out/fia-birthday-invite.mp4 b/events/fia-birthday-edendale/out/fia-birthday-invite.mp4 new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-birthday-invite.mp4 diff --git a/events/fia-birthday-edendale/out/fia-bunny-a.png b/events/fia-birthday-edendale/out/fia-bunny-a.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-bunny-a.png diff --git a/events/fia-birthday-edendale/out/fia-bunny-b.motion.mp4 b/events/fia-birthday-edendale/out/fia-bunny-b.motion.mp4 new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-bunny-b.motion.mp4 diff --git a/events/fia-birthday-edendale/out/fia-bunny-b.png b/events/fia-birthday-edendale/out/fia-bunny-b.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-bunny-b.png diff --git a/events/fia-birthday-edendale/out/fia-felt-A-v1.png b/events/fia-birthday-edendale/out/fia-felt-A-v1.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-felt-A-v1.png diff --git a/events/fia-birthday-edendale/out/fia-felt-A-v2.png b/events/fia-birthday-edendale/out/fia-felt-A-v2.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-felt-A-v2.png diff --git a/events/fia-birthday-edendale/out/fia-felt-B-v1.png b/events/fia-birthday-edendale/out/fia-felt-B-v1.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-felt-B-v1.png diff --git a/events/fia-birthday-edendale/out/fia-felt-B-v2.png b/events/fia-birthday-edendale/out/fia-felt-B-v2.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-felt-B-v2.png diff --git a/events/fia-birthday-edendale/out/fia-felt-combined.mp4 b/events/fia-birthday-edendale/out/fia-felt-combined.mp4 new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-felt-combined.mp4 diff --git a/events/fia-birthday-edendale/out/fia-human-a.motion.mp4 b/events/fia-birthday-edendale/out/fia-human-a.motion.mp4 new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-human-a.motion.mp4 diff --git a/events/fia-birthday-edendale/out/fia-human-a.png b/events/fia-birthday-edendale/out/fia-human-a.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-human-a.png diff --git a/events/fia-birthday-edendale/out/fia-human-b.png b/events/fia-birthday-edendale/out/fia-human-b.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/fia-human-b.png diff --git a/events/fia-birthday-edendale/out/invite-bunny-a.png b/events/fia-birthday-edendale/out/invite-bunny-a.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/invite-bunny-a.png diff --git a/events/fia-birthday-edendale/out/invite-bunny-b.png b/events/fia-birthday-edendale/out/invite-bunny-b.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/invite-bunny-b.png diff --git a/events/fia-birthday-edendale/out/invite-v1.png b/events/fia-birthday-edendale/out/invite-v1.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/invite-v1.png diff --git a/events/fia-birthday-edendale/out/textcard.png b/events/fia-birthday-edendale/out/textcard.png new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/out/textcard.png diff --git a/events/fia-birthday-edendale/refs/68409d23.jpg b/events/fia-birthday-edendale/refs/68409d23.jpg new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/refs/68409d23.jpg diff --git a/events/fia-birthday-edendale/refs/9addf5d8.jpg b/events/fia-birthday-edendale/refs/9addf5d8.jpg new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/refs/9addf5d8.jpg diff --git a/events/fia-birthday-edendale/refs/b308548b.jpg b/events/fia-birthday-edendale/refs/b308548b.jpg new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/refs/b308548b.jpg diff --git a/events/fia-birthday-edendale/refs/d7ba1a64.jpg b/events/fia-birthday-edendale/refs/d7ba1a64.jpg new file mode 100644 --- /dev/null +++ b/events/fia-birthday-edendale/refs/d7ba1a64.jpg diff --git a/pop/RELEASES.md b/pop/RELEASES.md --- a/pop/RELEASES.md +++ b/pop/RELEASES.md @@ -909,7 +909,28 @@ C→Eb→F→C; per-bar loudness ride). CRITICAL DSP LESSON: the EQ chain is SERIAL, so two bells at the same frequency ADD their dB — detuned pad doubles (+19+13+13=+45) and a root-partial choir both blew up the mix this way; layers must own disjoint frequencies. -- **Next:** @jeffrey ear-check both cuts → decide if one grows into a +- **Cut 3: nullabata** (`bin/render-nullabata.mjs` → `out/nullabata.mp3`, + ~4:21, −19.1 LUFS) — the **sonata cut** (*nullaby + sonata*): four + carved-noise movements running **attacca (no silence between)**, bound by + one lullaby motif (semitone offsets from a per-movement voice tonic) + transformed per movement: **I. Andante** C maj 72 BPM (theme + warm pads + + heartbeat kick) · **II. Scherzo** A min 132 BPM (theme shattered into a + fast arp + twinkle cloud) · **III. Adagio** F maj 56 BPM (theme in long + tones over sustained pads + drones + spice-partial choir, no perc) · + **IV. Finale** C maj 84 BPM (theme recapitulated over a compact 47-point + accumulation arch: 2→bloom→2, nuellaby's Boléro logic gathering the suite + home). **CLARITY = Q:** first pass sounded like radio static — every + bell-on-noise voice is breathy, and a wide bell passes a wide band of + hiss. Fix was to run the engine at its TONAL extreme: high Q on all + pitched voices (lead 110 / pads 55 / drones 45 / choir+halo 50+), **kill + the wide veil bed entirely** (Q 0.55 = the worst static source), + narrow/strip the noise perc (no shaker/open-hat; hats → narrow pitched + ticks), and master with a 15 kHz lowpass + no treble boost — energy + >12 kHz now sits 36 dB below full band. New engine piece: a **per-second, + movement-aware loudness ride** (`rideTargetAt(t)`) so four very different + tempos/densities land evenly. 47 lanes, `--proof` passes flat; + `--bake` → `c/run-c.mjs` renders the same score. +- **Next:** @jeffrey ear-check the three cuts → decide if one grows into a full single (more sections, maybe formant-vowel bells for a "choir" à la [[feedback_chillwave_formant_voice]]) or they stay studies. diff --git a/pop/americomputadora/c/americomputadora.c b/pop/americomputadora/c/americomputadora.c --- a/pop/americomputadora/c/americomputadora.c +++ b/pop/americomputadora/c/americomputadora.c @@ -404,6 +404,40 @@ static int n_kicks = 0; static void push_kick(double t, int serious) { if (n_kicks < 4096) { kick_times[n_kicks].t = t; kick_times[n_kicks].serious = serious; n_kicks++; } } +// dora onsets (last word of each hook phrase) — collected during the render +// so the FX pass can throw a record scratch on some of them. +static double dora_times[256]; +static int n_doras = 0; +// light shuffle: nudge the off-8th (odd half-beat) later so the hats swing. +static inline double swing8(double t, int i) { return (i & 1) ? t + 0.085 * BEAT : t; } + +// place a clip with a time-varying pitch ratio (linear-interp resample), for +// whistley swoops + theremin waver. ratio>1 = higher & shorter; <1 = stretched +// & lower. slide bends the pitch across the note (portamento); vib adds a sine +// waver. af_s/rf_s are attack/release fades in seconds. +static void place_pitched(float *buf, const float *clip, long cn, double t, + double ratio, double slide, double vibHz, double vibDepth, + double g, double af_s, double rf_s) { + if (!clip || cn < 2) return; + long s0 = (long)(t * SR); + long af = (long)(af_s * SR), rf = (long)(rf_s * SR); + long outEst = (long)(cn / (ratio + 0.5 * slide > 0.25 ? ratio + 0.5 * slide : 0.25)); + if (outEst < 1) outEst = 1; + double pos = 0; + for (long i = 0; pos < cn - 1; i++) { + double frac = (double)i / outEst; + double r = ratio + slide * frac + vibDepth * sin(2.0 * M_PI * vibHz * i / SR); + if (r < 0.25) r = 0.25; + long j = (long)pos; double fr = pos - j; + double s = clip[j] * (1 - fr) + clip[j + 1] * fr; + double env = 1; + if (i < af) env = (double)i / af; + else if (i > outEst - rf) env = (double)(outEst - i) / (rf > 0 ? rf : 1); + if (env < 0) env = 0; + addb(buf, s0 + i, s * g * env); + pos += r; + } +} static const ChordDef *chord_at_bar(int bar_idx) { int b = bar_idx % PROG_BARS; @@ -457,23 +491,33 @@ double bt = (bar_cursor + b) * BAR; const ChordDef *ch = chord_at_bar(bar_cursor + b); int root = ch->root, third = root + (ch->minor ? 3 : 4), fifth = root + 7; - // very faint harmony bed — every bar, no matter what - choir(cho, bt, root, third, fifth, 0.022); + // very faint harmony bed — every bar EXCEPT the first 3 bars of + // the opener: those are bone-dry, just the vocal, no pads. + if (!(!strcmp(sec->section, "intro") && b < 3)) + choir(cho, bt, root, third, fifth, 0.022); if (strcmp(sec->feel, "claps-only") && strcmp(sec->feel, "fade")) { double sg = (!strcmp(sec->feel, "thin") || !strcmp(sec->feel, "sparse")) ? 0.22 : 0.35; - if (!strcmp(sec->section, "intro")) sg = 0.10; // hushed under the hats - sub_bass(sb, bt, root, BAR * 0.95, sg); + if (!strcmp(sec->section, "intro")) { + // opener is just the vocal — sub holds out, then eases + // in over the last 4 bars toward the drop. + sg = 0.10 * fmax(0.0, fmin(1.0, (b - 5) / 4.0)); + } + if (sg > 0) sub_bass(sb, bt, root, BAR * 0.95, sg); } if (!strcmp(sec->section, "stop") || !strcmp(sec->feel, "claps-only")) { clap(drm, bt + BEAT, 1.05); clap(drm, bt + BEAT * 3, 1.05); } else if (!strcmp(sec->section, "intro")) { - // the opener: soft hats + the toy-piano melody (the same - // every-other-beat figure from verse 2) + a very quiet, - // smooth jeffrey syllable chant. spare and pretty. - for (int i = 0; i < 8; i++) tamb(drm, bt + i * (BEAT / 2), 0.08); + // the opener: for the first 6 bars it's JUST jeffrey's + // vocal (whistley + stretchy). the instrumental — soft hats + // + toy-piano melody — holds out and only enters over the + // last 4 bars, building into the drop. + // hats enter from the 2nd bar; the toy piano (and the rest) + // hold out until bar 6. + if (b >= 2) for (int i = 0; i < 8; i++) tamb(drm, bt + i * (BEAT / 2), 0.08); + if (b >= 6) { // toy piano — fuller now: a soft chord on 1 and 3 (triad // + inversion) under a stepping melody line on every beat. toy_piano(toy, bt, root, BEAT * 1.9, 0.06); @@ -487,6 +531,7 @@ toy_piano(toy, bt, third + 12, BEAT * 0.85, 0.07); toy_piano(toy, bt + BEAT, fifth + 12, BEAT * 0.85, 0.06); toy_piano(toy, bt + BEAT * 2, root + 24, BEAT * 0.85, 0.06); toy_piano(toy, bt + BEAT * 3, fifth + 12, BEAT * 0.85, 0.06); + } #if N_SYL > 0 // OPENER — human + machine in unison: jeffrey (ElevenLabs) // and the computer voice chant the word TOGETHER from the @@ -513,17 +558,43 @@ int ti = -1; for (int q = 0; q < N_SYLT; q++) if (SYL_NOTES[q] == deg) { ti = q; break; } if (ti < 0) continue; double lineG = ln == 0 ? 1.0 : harmG; + // OPENER ARC: word 1 is the whistley swoop (kept — + // it's the hook of the intro). From word 2 on + // jeffrey EAGERLY becomes musical: pitching IN to + // tune (slide → 0), lengthening into sustained sung + // notes, the waver settling to a gentle musical + // vibrato — blooming into the drop. // both voices in unison: jeffrey (0) + computer (1) for (int vx = 0; vx < 2; vx++) { Clip *sc = get_syl(vx, si, ti); if (!sc) continue; - long lim = sc->n < slot ? sc->n : slot; - double g = (vx == 0 ? 0.50 : 0.52) * lineG; - for (long i = 0; i < lim; i++) { - double env = 1; - if (i < af) env = (double)i / af; - else if (i > lim - rf) env = (double)(lim - i) / rf; - addb(voc, s0 + i, sc->s[i] * g * env); + double g = (vx == 0 ? 0.60 : 0.07) * lineG; // jeffrey carries it; computer way back in the background + if (vx == 0 && intro_beat < N_SYL) { + // word 1 — whistley swoop + double slide = (intro_beat & 1) ? 0.20 : -0.16; + place_pitched(voc, sc->s, sc->n, bt + k * BEAT, + 0.90, slide, 5.5, 0.05, g, 0.05, 0.34); + } else if (vx == 0) { + // words 2+ — eagerly turn musical over ~2 words: + // stay IN TUNE (ratio 1.0 — no pitching lower + // and lower), the portamento settles onto the + // note, the waver becomes a musical vibrato, + // and the legato tail lengthens. + double p = fmin(1.0, (double)(intro_beat - N_SYL) / (2.0 * N_SYL)); + double slide = (1.0 - p) * ((intro_beat & 1) ? 0.12 : -0.10); // pitch in + double vib = 0.045 - 0.026 * p; // waver → musical vibrato + double rel = 0.34 + 0.18 * p; // legato tail grows + place_pitched(voc, sc->s, sc->n, bt + k * BEAT, + 1.0, slide, 5.2, vib, g * (1.0 + 0.12 * p), 0.05, rel); + } else { + // the bg computer voice — straight + long lim = sc->n < slot ? sc->n : slot; + for (long i = 0; i < lim; i++) { + double env = 1; + if (i < af) env = (double)i / af; + else if (i > lim - rf) env = (double)(lim - i) / rf; + addb(voc, s0 + i, sc->s[i] * g * env); + } } } } @@ -559,7 +630,7 @@ long slot = (long)(BEAT * 0.98 * SR); for (int vx = 0; vx < 2; vx++) { Clip *sc = get_syl(vx, si, ti); // jeffrey (0) + computer (1) if (!sc) continue; - double g = vx == 0 ? 0.48 : 0.50; + double g = vx == 0 ? 0.52 : 0.07; // jeffrey carries it; computer way back in the background long lim = sc->n < slot ? sc->n : slot; for (long i = 0; i < lim; i++) { double env = i > lim - 480 ? (double)(lim - i) / 480 : 1.0; @@ -596,7 +667,7 @@ if (!(b & 1)) { kick_serious(kik, bt, 0.95); push_kick(bt, 1); } kick_serious(kik, bt + BEAT * 2, 0.95); push_kick(bt + BEAT * 2, 1); snare_hit(snr, bt + BEAT, 0.8); snare_hit(snr, bt + BEAT * 3, 0.8); - for (int i = 0; i < 8; i++) tamb(drm, bt + i * (BEAT / 2), 0.04); + for (int i = 0; i < 8; i++) tamb(drm, swing8(bt + i * (BEAT / 2), i), 0.04); } else { int floor4 = tins > 0.95; for (int i = 0; i < 4; i++) { @@ -608,7 +679,7 @@ } } snare_hit(snr, bt + BEAT, 0.95); snare_hit(snr, bt + BEAT * 3, 0.95); - for (int i = 0; i < 8; i++) tamb(drm, bt + i * (BEAT / 2), 0.055); + for (int i = 0; i < 8; i++) tamb(drm, swing8(bt + i * (BEAT / 2), i), 0.055); } bell(bel, bt, root + 12, BAR * 0.95, 0.10 + 0.05 * tins); bach_arp(arp, bt, root, ch->minor, 16, 12, 0.07 + 0.04 * tins); @@ -677,9 +748,16 @@ Clip *c = get_clip(tier, w, phrase_idx % ROSTER_N[w], vi); if (c) { Break breaks[2]; int nb; double attack, gain; + // first Whitney hit at the drop lands LOUD — the + // opening "america" of the very first hook gets a + // big lift, the rest of that phrase a smaller one. + double firstHookG = (is_hook && hook_count == 0 && p == 0) + ? (w == 0 ? 1.40 : 1.12) : 1.0; if (last) { breaks[0] = (Break){phrase_len - onsets[w] + 0.06, 0, 0.22}; - nb = 1; attack = 0.008; gain = 1.22 * sec_gain; /* dora! */ + nb = 1; attack = 0.008; gain = 1.22 * sec_gain * firstHookG; /* dora! */ + if (is_hook && n_doras < 256) + dora_times[n_doras++] = phrase_start + onsets[w]; // for the scratch } else { double duck_at = onsets[w + 1] - onsets[w] + lead; double choke_at = (w + 2 < N_WORDS ? onsets[w + 2] : phrase_len) - onsets[w] + lead; @@ -695,9 +773,13 @@ breaks[1] = (Break){choke_at - 0.06, 0, 0.22}; } nb = 2; attack = w == 0 ? 0 : BLEND; - gain = 0.95 * sec_gain; + gain = 0.95 * sec_gain * firstHookG; } - paint_shaped(voc, c->s, c->n, phrase_start + onsets[w] - lead, + // soften "computer": skip the hard "co" transient so + // the long crossfade swells in as "...uhmmmputer". + long coSkip = (w == 1) ? (long)(0.085 * SR) : 0; + paint_shaped(voc, c->s + coSkip, c->n - coSkip, + phrase_start + onsets[w] - lead, gain, breaks, nb, attack); } if (is_hook) @@ -782,32 +864,38 @@ // ── FX one-shots: gong + record scratches (freesound, c/fx/) ────── // scratches sprinkle the first 30s and cluster around ~2:15; the gong // marks the top of the finale. mixed into the drum bus. { - long gn = 0, s1n = 0, s2n = 0; + long gn = 0, s1n = 0, s2n = 0, shn = 0; char fp[512]; snprintf(fp, sizeof fp, "%sfx/gong.wav", voc_base); float *gong = load_wav_mono(fp, &gn); snprintf(fp, sizeof fp, "%sfx/scratch1.wav", voc_base); float *sc1 = load_wav_mono(fp, &s1n); snprintf(fp, sizeof fp, "%sfx/scratch2.wav", voc_base); float *sc2 = load_wav_mono(fp, &s2n); + snprintf(fp, sizeof fp, "%sfx/shock-rifle.wav", voc_base); float *shk = load_wav_mono(fp, &shn); // place a clip into drm at time t with gain #define PLACE(CLIP, CN, T, G) do { \ if (CLIP) { long _s0 = (long)((T) * SR); \ for (long _i = 0; _i < (CN); _i++) addb(drm, _s0 + _i, (CLIP)[_i] * (G)); } } while (0) - // scratches arrive WITH the main vocal (first chorus, after the - // quiet intro) — not over the opener. + // TWO deliberate moments only (computed from the structure so they + // stay aligned when the arrangement changes) — kept sparse on purpose: + // 1) the DROP — first hook after the quiet opener + // 2) the FINALE LAUNCH — the stop/pad-dropout into the last hook double choir_in = (!strcmp(STRUCTURE[0].section, "intro") ? STRUCTURE[0].bars * STRUCTURE[0].reps : 0) * BAR; - PLACE(sc1, s1n, choir_in + 0.0, 0.5); - PLACE(sc2, s2n, choir_in + 4 * BAR, 0.45); - PLACE(sc1, s1n, choir_in + 8 * BAR + 2.5 * BEAT, 0.5); - // ~2:15 cluster - PLACE(sc2, s2n, 133.0, 0.6); - PLACE(sc1, s1n, 135.0, 0.55); - PLACE(sc2, s2n, 136.5, 0.5); - // gong marks the lift OUT of the intro into the first chorus, and - // the finale push (~2:18). a soft swell, not a hit at bar 0. - PLACE(gong, gn, choir_in - 0.4, 0.55); - PLACE(gong, gn, 138.0, 0.7); + int lastHook = -1; + for (int s = 0; s < N_SECS; s++) if (!strcmp(STRUCTURE[s].section, "hook")) lastHook = s; + double finale = 0; + { int bb = 0; for (int s = 0; s < lastHook; s++) bb += STRUCTURE[s].bars * STRUCTURE[s].reps; + finale = bb * BAR; } + // 1) drop + PLACE(gong, gn, choir_in - 0.40, 0.55); // soft swell into the drop + PLACE(shk, shn, choir_in - 0.18, 0.80); // shock rifle fires the first Whitney hit in + PLACE(sc1, s1n, choir_in + 0.00, 0.90); // one loud scratch announces it + // 2) finale launch — shock rifle + scratch + gong all land together as + // the pads drop out and the last hook hits. + PLACE(gong, gn, finale - 0.45, 0.65); + PLACE(shk, shn, finale - 0.18, 0.85); + PLACE(sc1, s1n, finale + 0.00, 0.75); #undef PLACE - free(gong); free(sc1); free(sc2); + free(gong); free(sc1); free(sc2); free(shk); } if (stem_voc) { @@ -818,9 +906,20 @@ return 0; } // ── reverb send → schroeder ─────────────────────────────────────── + // the main vocal starts DRY — its reverb send fades in across the first + // ~75s, so the opener and the drop punch dry and the space only opens up + // as the song develops. (instruments keep their constant send.) + // the main vocal is bone-DRY (zero reverb) through the whole opener, then + // its send fades in over ~60s after the drop — the space opens up only as + // the song develops. (instruments keep their constant send.) + double introEnd = STRUCTURE[0].bars * STRUCTURE[0].reps * BAR; + double rvStart = introEnd * SR, rvLen = 60.0 * SR; float *send = malloc(N * 4); - for (long i = 0; i < N; i++) - send[i] = voc[i] * 0.85f + snr[i] * 0.65f + bel[i] * 0.55f + sqr[i] * 0.5f + arp[i] * 0.3f; + for (long i = 0; i < N; i++) { + double vfade = i < rvStart ? 0.0 : fmin(1.0, (i - rvStart) / rvLen); + send[i] = (float)(voc[i] * 0.85 * vfade) + + snr[i] * 0.65f + bel[i] * 0.55f + sqr[i] * 0.5f + arp[i] * 0.3f; + } float *room = calloc(N, 4); { static const double CDEL[4] = {0.0297, 0.0371, 0.0411, 0.0437}; @@ -857,6 +956,22 @@ float *echo = calloc(N, 4); for (long i = 0; i < N; i++) echo[i] = voc[i] + (i >= dsamp ? echo[i - dsamp] * 0.40f : 0); + // ── vocal-keyed sidechain: the backing DIPS while the lead vocal sounds + // so the voice sits forward and the track flows (envelope follower on the + // dry vocal — fast attack, slow release). ──────────────────────────── + float *vSide = malloc(N * 4); + { + double e = 0; + double atk = exp(-1.0 / (0.004 * SR)); // 4 ms attack + double rel = exp(-1.0 / (0.18 * SR)); // 180 ms release + for (long i = 0; i < N; i++) { + double x = fabs(voc[i]); + double c = x > e ? atk : rel; + e = x + c * (e - x); + vSide[i] = (float)fmin(1.0, e * 2.2); // normalize → ~0..1 presence + } + } + // ── master ──────────────────────────────────────────────────────── float *mix = malloc(N * 4); double peak = 0; @@ -873,8 +988,11 @@ } else { // mix balance: drums punchy, the dense melodic bus pulled back a // touch to clear room for the voice, vocals brought forward and // sitting on a tighter (less washy) reverb. - double drums = (drm[i] + kik[i] + snr[i]) * 0.96; - double melodic = (bel[i] + sqr[i] + toy[i] + arp[i] + pds[i] + cho[i]) * 0.86 * dk; + // vocal sidechain: melodic bus ducks hardest, drums a touch, so the + // lead vocal carves its own space (flow). + double vd = vSide[i]; + double drums = (drm[i] + kik[i] + snr[i]) * 0.96 * (1.0 - 0.16 * vd); + double melodic = (bel[i] + sqr[i] + toy[i] + arp[i] + pds[i] + cho[i]) * 0.86 * dk * (1.0 - 0.45 * vd); double subb = sb[i] * (0.55 + 0.45 * dk); double tail = (echo[i] - voc[i]) * 0.28; double vocal = voc[i] * (0.78 + 0.22 * dk) + tail * dk; diff --git a/pop/americomputadora/c/render-c.mjs b/pop/americomputadora/c/render-c.mjs --- a/pop/americomputadora/c/render-c.mjs +++ b/pop/americomputadora/c/render-c.mjs @@ -61,17 +61,30 @@ console.warn("! acdsp not built (pop/dsp/c) — skipping mastering stage"); } const src = existsSync(mastered) ? mastered : rawWav; -// crisp normalization over the whole thing — one even, present level -// (EBU R128, -11 LUFS / -1 dBTP) so it lands consistent end to end. -run("encode mp3 (ffmpeg + loudnorm)", "ffmpeg", [ - "-hide_banner", "-y", "-loglevel", "error", - "-i", src, - "-af", "loudnorm=I=-11:TP=-1.0:LRA=11", - "-ar", "48000", - "-c:a", "libmp3lame", "-q:a", "2", +// DistroKid-ready master: EBU R128 normalization to -11 LUFS / -1 dBTP, one +// even present level end to end. We render TWO deliverables from the same +// loudnorm pass — a 320 kbps CBR mp3, and a 16-bit/44.1 kHz WAV (the format +// DistroKid prefers for upload). +const LOUDNORM = "loudnorm=I=-11:TP=-1.0:LRA=11"; +const META = [ "-metadata", "title=americomputadora", "-metadata", "artist=jeffrey", "-metadata", "album=pixsies", - outMp3, +]; +run("encode mp3 (320k CBR + loudnorm)", "ffmpeg", [ + "-hide_banner", "-y", "-loglevel", "error", + "-i", src, + "-af", LOUDNORM, "-ar", "48000", + "-c:a", "libmp3lame", "-b:a", "320k", + ...META, outMp3, ]); console.log(`✓ ${outMp3}`); + +const outWav = join(ROOT, "out", "americomputadora-master.wav"); +run("master wav (16-bit / 44.1k, DistroKid)", "ffmpeg", [ + "-hide_banner", "-y", "-loglevel", "error", + "-i", src, + "-af", LOUDNORM, "-ar", "44100", "-sample_fmt", "s16", + ...META, outWav, +]); +console.log(`✓ ${outWav}`); diff --git a/pop/americomputadora/c/score.h b/pop/americomputadora/c/score.h --- a/pop/americomputadora/c/score.h +++ b/pop/americomputadora/c/score.h @@ -18,13 +18,13 @@ typedef struct { const char *section; int bars; int reps; const char *feel; } Sec; static const Sec STRUCTURE[] = { {"intro", 10, 1, "soft"}, - {"hook", 4, 4, "full"}, - {"verse", 8, 1, "thin"}, - {"hook", 4, 4, "full"}, - {"verse", 8, 1, "sparse"}, + {"hook", 4, 3, "full"}, + {"verse", 6, 1, "thin"}, + {"hook", 4, 3, "full"}, + {"verse", 6, 1, "sparse"}, {"bridge", 4, 1, "breakdown"}, {"stop", 1, 1, "claps-only"}, - {"hook", 4, 4, "full"}, + {"hook", 4, 3, "full"}, {"outro", 8, 1, "fade"}, }; #define N_SECS 9 diff --git a/pop/americomputadora/c/syl-cache/do.mp3 b/pop/americomputadora/c/syl-cache/do.mp3 --- a/pop/americomputadora/c/syl-cache/do.mp3 +++ b/pop/americomputadora/c/syl-cache/do.mp3 diff --git a/pop/americomputadora/c/syllables.mjs b/pop/americomputadora/c/syllables.mjs --- a/pop/americomputadora/c/syllables.mjs +++ b/pop/americomputadora/c/syllables.mjs @@ -28,13 +28,17 @@ // 8 against a 4-beat bar so the word restarts on the downbeat every 2 bars // instead of precessing (the old 7-syllable pun drifted out of phase). const SYLS = [ ["a", "ah"], - ["me", "meh"], - ["ri", "ree"], + ["me", "may"], + ["ri", "rick"], // ah-may-rick → "America" ["com", "comb"], - ["pu", "poo"], + ["pu", "pew"], ["ta", "tah"], - ["do", "doe"], + ["do", "door"], // door-ah → "dora" ["ra", "rah"], + // extra takes for jeffrey's 1:45 re-entrance phrase + // ("ahh may rico puter door ahh!") — not part of the 8-syllable word cycle. + ["rico", "rico"], + ["puter", "pewter"], ]; // a second voice — a macOS "computer" — chants the SAME syllables in diff --git a/pop/americomputadora/melody.json b/pop/americomputadora/melody.json --- a/pop/americomputadora/melody.json +++ b/pop/americomputadora/melody.json @@ -1,5 +1,5 @@ { - "_comment": "americomputadora — hook melody. D major, 112 BPM. The three vocal samples (whole-word clips, not per-syllable) get autotuned to these target MIDI notes at render time. The hook phrase spans TWO bars (8 beats) so the words can actually speak — 'americaaaaa cawwwmputer dora!': america sings a full bar (4 beats) BEFORE computer enters, computer 2 beats, dora 2 beats as the exclamation point with ring-out.", + "_comment": "americomputadora \u2014 hook melody. D major, 112 BPM. The three vocal samples (whole-word clips, not per-syllable) get autotuned to these target MIDI notes at render time. The hook phrase spans TWO bars (8 beats) so the words can actually speak \u2014 'americaaaaa cawwwmputer dora!': america sings a full bar (4 beats) BEFORE computer enters, computer 2 beats, dora 2 beats as the exclamation point with ring-out.", "bpm": 112, "key": "D", "mode": "major", @@ -24,7 +24,7 @@ 4, 2, 2 ], - "_variant_note": "ballad melody set (2026-06-09) — variants rotate per hook rep; ordered so the gentle contours open the song and the climax contour arrives as the percussion ramp peaks. bach contours kept from the bachiamatrixian pass.", + "_variant_note": "ballad melody set (2026-06-09) \u2014 variants rotate per hook rep; ordered so the gentle contours open the song and the climax contour arrives as the percussion ramp peaks. bach contours kept from the bachiamatrixian pass.", "variants": [ { "label": "rise-rest", @@ -33,7 +33,7 @@ 62, 69, 66 ], - "tone": "ballad opener — D4→A4, resting on the third" + "tone": "ballad opener \u2014 D4\u2192A4, resting on the third" }, { "label": "step-home", @@ -42,7 +42,7 @@ 64, 69, 62 ], - "tone": "E4→A4→D4 — supertonic pull home" + "tone": "E4\u2192A4\u2192D4 \u2014 supertonic pull home" }, { "label": "lift", @@ -51,7 +51,7 @@ 66, 71, 69 ], - "tone": "F#4→B4→A4 — the lift" + "tone": "F#4\u2192B4\u2192A4 \u2014 the lift" }, { "label": "bach-circle", @@ -60,7 +60,7 @@ 71, 69, 66 ], - "tone": "bachiamatrixian — B4→A4→F#4, lament step-down onto the third" + "tone": "bachiamatrixian \u2014 B4\u2192A4\u2192F#4, lament step-down onto the third" }, { "label": "bach-arc", @@ -69,7 +69,7 @@ 66, 69, 74 ], - "tone": "bachiamatrixian — F#4→A4→D5, rising tonic arpeggio, fanfare resolution" + "tone": "bachiamatrixian \u2014 F#4\u2192A4\u2192D5, rising tonic arpeggio, fanfare resolution" }, { "label": "bach-fall", @@ -78,7 +78,7 @@ 74, 71, 62 ], - "tone": "bachiamatrixian — D5→B4→D4, falling sixth through the vi, lands tonic" + "tone": "bachiamatrixian \u2014 D5\u2192B4\u2192D4, falling sixth through the vi, lands tonic" }, { "label": "climax", @@ -87,7 +87,7 @@ 69, 74, 71 ], - "tone": "A4→D5→B4 — top of the ramp" + "tone": "A4\u2192D5\u2192B4 \u2014 top of the ramp" } ] }, @@ -114,7 +114,7 @@ "root_midi": 67, "quality": "maj" } ], - "_progression_note": "I — V — vi — IV. canonical bubblegum loop (Sugar Sugar / Baby One More Time pre-chorus / countless others). 8 bars total, repeats." + "_progression_note": "I \u2014 V \u2014 vi \u2014 IV. canonical bubblegum loop (Sugar Sugar / Baby One More Time pre-chorus / countless others). 8 bars total, repeats." }, "structure": [ { @@ -126,22 +126,22 @@ { "section": "hook", "bars": 4, "feel": "full", - "reps": 4 + "reps": 3 }, { "section": "verse", - "bars": 8, + "bars": 6, "feel": "thin" }, { "section": "hook", "bars": 4, "feel": "full", - "reps": 4 + "reps": 3 }, { "section": "verse", - "bars": 8, + "bars": 6, "feel": "sparse" }, { @@ -158,7 +158,7 @@ { "section": "hook", "bars": 4, "feel": "full", - "reps": 4 + "reps": 3 }, { "section": "outro", diff --git a/pop/boombaboom/c/boombaboom.c b/pop/boombaboom/c/boombaboom.c --- a/pop/boombaboom/c/boombaboom.c +++ b/pop/boombaboom/c/boombaboom.c @@ -121,7 +121,7 @@ ph += TAU * pf / SR; phs += TAU * (floorf * 0.66) / SR; // a pure DEEP sub layer under it double amp = exp(-tt * 5.6); // longer body = bassier double sub = sin(phs) * exp(-tt * 3.6) * 0.55; // sustained sub weight - double click = sin(TAU * 1700.0 * tt) * exp(-tt * 300.0) * 0.32; // small click + double click = sin(TAU * 1900.0 * tt) * exp(-tt * 280.0) * 0.48; // punchier attack double v = tanh((sin(ph) + sub + click) * 2.5) * amp * g; // harder tanh = CRUNCH addD(s0 + i, v, v); } @@ -199,6 +199,41 @@ addM(s0 + i, out * lg, out * rg); addR(s0 + i, out * 0.3 * lg, out * 0.3 * rg); } } +// ding — play a sampled triangle resampled UP by `semis` (high shimmer), +// letting its own metallic ring decay. Overlays the music bus. +static void ding(float *buf, long bn, double t, double semis, double g, double pan) { + if (!buf || bn < 2) return; + long s0 = (long)(t * SR); double rate = pow(2.0, semis / 12.0); long n = (long)(bn / rate); + double lg = 1 - pan * 0.5, rg = 1 + pan * 0.5; + for (long i = 0; i < n; i++) { + double pos = i * rate; if (pos > bn - 2) break; + long i0 = (long)pos; double fr = pos - i0; + double s = (buf[i0] * (1 - fr) + buf[i0 + 1] * fr) * g; + long j = s0 + i; if (j >= 0 && j < N) { musL[j] += (float)(s * lg); musR[j] += (float)(s * rg); addR(j, s * 0.3 * lg, s * 0.3 * rg); } + } +} + +// grand piano — a struck-string tone (still all sine): a stack of 6 partials +// with slight inharmonicity + a fast hammer attack + a piano-ish decay. For the +// high classical frills on top. +static void piano(double note, double t, double dur, double g, double pan) { + double f = midi_hz(note); long s0 = (long)(t * SR), n = (long)(dur * SR); + double lg = 1 - pan * 0.5, rg = 1 + pan * 0.5; + double ph[6] = {0}; + const double amp[6] = { 1.0, 0.52, 0.32, 0.18, 0.10, 0.06 }; + const double inh[6] = { 1.0, 2.001, 3.003, 4.006, 5.01, 6.02 }; // slight inharmonicity + long att = (long)(0.0015 * SR) + 1; + for (long i = 0; i < n; i++) { + double tt = (double)i / SR, s = 0; + for (int k = 0; k < 6; k++) { ph[k] += TAU * f * inh[k] / SR; s += amp[k] * sin(ph[k]); } + s /= 2.2; + double env = exp(-tt * 1.5); // longer sustain (rings out) + if (i < att) env *= (double)i / att; // hammer attack + double v = s * env * g; + addM(s0 + i, v * lg, v * rg); addR(s0 + i, v * 0.28 * lg, v * 0.28 * rg); + } +} + // whistle — the vocal taken ALL THE WAY to a whistle: a near-pure sine with a // gentle vibrato + a breath of noise, slow attack / long release, drenched in // reverb. Featured for a few ethereal bars in the breakdown. @@ -264,6 +299,21 @@ } // snare — a BANGING backbeat: a punchy tonal body (~185 Hz) + a bright // bandpassed noise crack, tanh-saturated for weight. Sends to the verb. +// donk — the classic early-2000s UK "donk": a pitchy, bouncy resonant blip with +// a quick downward pitch snap + tanh saturation, plopped on the offbeats. +static void donk(double note, double t, double g, double pan) { + double f = midi_hz(note); long s0 = (long)(t * SR), n = (long)(0.16 * SR); + double ph = 0, lg = 1 - pan * 0.5, rg = 1 + pan * 0.5; + for (long i = 0; i < n; i++) { + double tt = (double)i / SR; + double pf = f * (1.0 + 1.4 * exp(-tt * 48.0)); // quick "doonk" pitch drop + ph += TAU * pf / SR; + double env = exp(-tt * 24.0); // short + bouncy + double v = tanh(sin(ph) * 2.2) * env * g; // saturated = resonant donk + addD(s0 + i, v * lg, v * rg); addR(s0 + i, v * 0.15, v * 0.15); + } +} + // snare — a tight "pffp" HIP-HOP snare: noise-FORWARD bright bandpassed crack // (the "pff") with a quick snappy decay and only a touch of tonal body. Drier. static void snare(double t, double g, double pan) { @@ -368,19 +418,15 @@ } // hat — a soft closed hat / shaker. LOW-PITCHED: the noise is lowpassed to a // dark "shh" (not a bright "tss") so it sits down out of the way. +// hat — now a low THUD (not a tss/tick): a low sine thump with a tiny pitch +// drop + tanh weight, on the offbeats. Works in both cuts (it's a sine). static void hat(double t, double g, double pan) { - long s0 = (long)(t * SR), n = (long)(0.05 * SR); double low = 0, ph = 0, lg = 1 - pan * 0.5, rg = 1 + pan * 0.5; + long s0 = (long)(t * SR), n = (long)(0.08 * SR); double ph = 0, lg = 1 - pan * 0.5, rg = 1 + pan * 0.5; for (long i = 0; i < n; i++) { double tt = (double)i / SR; - double v; - if (SINES) { // pure-sine "tick" approximating a hat - ph += TAU * 4200.0 / SR; - v = sin(ph) * exp(-tt * 140.0) * g * 0.22; - } else { - double nz = rnd2(); - low += 0.32 * (nz - low); // lowpass → darker, lower-pitched - v = low * exp(-tt * 120.0) * g * 0.30; - } + double pf = 92.0 + 42.0 * exp(-tt * 60.0); // low thud, small pitch drop + ph += TAU * pf / SR; + double v = tanh(sin(ph) * 1.5) * exp(-tt * 40.0) * g * 0.5; addD(s0 + i, v * lg, v * rg); } } @@ -511,6 +557,8 @@ float *maS = load_wav_mono("../sources/boombaboom-ma.wav", &maN); long boompN = 0, mapN = 0; float *boomP = load_wav_mono("../sources/boombaboom-boomp.wav", &boompN); // short percussive boom float *maP = load_wav_mono("../sources/boombaboom-map.wav", &mapN); // short percussive ma + long triN = 0; + float *tri = load_wav_mono("../sources/triangle.wav", &triN); // sampled triangle (overlay) fprintf(stderr, "# samples: oscu=%s oscd=%s boom=%s ma=%s\n", oscu?"ok":"MISS", oscd?"ok":"MISS", boomS?"ok":"MISS", maS?"ok":"MISS"); double voxSec = (double)voxN / SR; @@ -524,7 +572,7 @@ // starts soon. Each pass gets 20 bars so the take floats freely (only the // booms anchor to the kick). // PRELUDE = lingering "lost vocal" ambient (no beat) · INTRO = build/sweep · // then the drop on the first BOOM. Start lingers; kick hits strong at PB0. - const int PRELUDE = 4, INTRO = 4, PASS = 30, BR = 8, OUTRO = 10; // less intro (faster to the drop), long passes fit the stretched vocal + const int PRELUDE = 4, INTRO = 10, PASS = 30, BR = 8, OUTRO = 4; // longer build (beat ~24s), short outro (shorter track) const int NP = 3; const int DROP = PRELUDE + INTRO; // first BOOM / strong kick / sweep ends int passBar[3] = { DROP, DROP + PASS + BR, DROP + PASS + BR + PASS + BR }; @@ -543,23 +591,17 @@ // spread L↔R. On the LAST pass the stack drops to the octave BELOW (the // grounded "true ending") instead of the bright octave above. // pass0 pass1 pass2(end) // fuller, more POWERFUL choral arrangement — harmonies pushed up. - const double G_OUP[3] = { 0.95, 1.20, 0.45 }; // octave UP (pulled back at the end) + const double G_OUP[3] = { 0.00, 1.10, 0.45 }; // octave UP — OFF on the 1st pass (one strong vocal at the drop) const double G_FIF[3] = { 0.75, 1.10, 1.10 }; const double G_THR[3] = { 0.40, 0.85, 1.05 }; - for (int pass = 0; pass < NP; pass++) { - int sb = passBar[pass]; - // FEMALE lead (his take in its own octave = the higher voice) — leads, - // present every pass, soft + deep + drenched. - place_layer(vox, voxN, vox0, sb, 1.75, 0.00, 0.46); // more powerful lead - // MALE counterpart (octave-DOWN = the deeper second voice). It enters - // AFTER the female intro (pass ≥ 1) and answers her in COUNTERPOINT — - // the same line a half-bar behind = a deep call-and-response round. - if (pass >= 1) - place_at(hd, hdN, vox0, sb * BAR + 2 * BEAT, (pass == 1 ? 1.05 : 1.25), 0.00, 0.42); - // MINIMAL harmony — lead + deep male + a single soft octave-up only. - // (No fifth/third/doublings — keep it spare so the voice is clear.) - place_layer(ho, hoN, vox0, sb, G_OUP[pass] * 0.55, 0.25, 0.46); // octave up, soft - } + // SINGULAR vocal that MOVES THROUGH OCTAVES across the track — one voice at + // a time, but a different octave each pass: pass1 up (cuts), pass2 down to + // his own octave, pass3 back up. (Still no harmonies/layers.) + float *voct[3] = { vox, ho, hd }; // a JOURNEY: original → up → down (not always up) + long voctN[3] = { voxN, hoN, hdN }; + double voctG[3] = { 1.95, 1.7, 1.9 }; + for (int pass = 0; pass < NP; pass++) + place_layer(voct[pass], voctN[pass], vox0, passBar[pass], voctG[pass], 0.00, 0.44); // LOST-VOCAL PRELUDE — a SINGLE lost-vocal phrase, sung at three octaves // AT THE SAME TIME (no confusing time-offsets): the wobble + its octave-up // and octave-down, stacked in sync and drenched in reverb. One coherent @@ -569,37 +611,19 @@ { // SOUND FROM THE START — the VOCAL itself opens the track (no startup // chime): the phrase + its deep octave-down, CENTRED, soft + very wet, // landing almost immediately and restated once. - // ONE voice at the start — just the single phrase, centred + drenched. - // No octave stacking (the opening reads as a lone lost voice). - double at1 = 0.12 * BAR, at2 = 2.5 * BAR; - place_at(osc, oscN, 0, at1, 0.46, 0.00, 0.95); - place_at(osc, oscN, 0, at2, 0.32, 0.00, 0.95); + // ONE voice — the lone lost phrase, restated across the whole longer + // build (drop ~24s) so the vocal carries the intro until the beat lands. + double at[4] = { 0.12, 4.0, 8.0, 11.0 }; // bars + double g[4] = { 0.44, 0.34, 0.40, 0.48 }; // builds toward the drop + float *pre[4] = { osc, oscu, oscd, oscu }; // moves through octaves + long preN[4] = { oscN, oscuN, oscdN, oscuN }; + for (int k = 0; k < 4; k++) place_at(pre[k], preN[k], 0, at[k] * BAR, g[k], 0.00, 0.95); } - // SQUARE-WAVE STARTUP TRIO — a digital boot-chime that lands WITH THE KICK - // at the drop (not at t=0), a LOWER octave (D3·A3·D4) so it sits under the - // voice: three ascending pulse notes across the first beat of the drop. - { - double d0 = passBar[0] * BAR; - sqbell(50, d0, 0.50, 0.55, -0.25); // D3 - sqbell(57, d0 + BEAT * 0.5, 0.50, 0.55, 0.05); // A3 - sqbell(62, d0 + BEAT * 1.0, 1.00, 0.60, 0.25); // D4 - } - // OSC FEATURE — the "ooowwoowwoo" wobble as a hook in each bridge breakdown. - place_layer(osc, oscN, 0, passBar[0] + PASS, 1.5, -0.2, 0.4); // bridge 1 - place_layer(osc, oscN, 0, passBar[1] + PASS, 1.5, 0.2, 0.4); // bridge 2 - - // ANGELS — a high, airy octave-up vocal swelling in on the back half to lift - // the track, very wet + panned wide (alongside the deep throat below). - place_layer(ho, hoN, vox0, passBar[1], 0.45, -0.55, 0.60); - place_layer(ho, hoN, vox0, passBar[2], 0.58, 0.55, 0.62); - - // BOOM BOOM BOOM BOOM — four single-beat stretched booms as a fill leading - // into the final drop (the bar before pass 3). - { - double b0 = (passBar[2] - 1) * BAR; - for (int k = 0; k < 4; k++) - place_at(boomS, boomN, 0, b0 + k * BEAT, 0.7, (k & 1) ? 0.2 : -0.2, 0.30); - } + // OSC FEATURE — the "ooowwoowwoo" wobble (pitched up = the single voice) as + // the ONE vocal hook in each bridge breakdown. (No angels, no boom-fill — + // singular vocal, simpler track.) + place_at(oscu, oscuN, 0, (passBar[0] + PASS) * BAR, 1.4, 0.0, 0.4); // bridge 1 — up + place_at(oscd, oscdN, 0, (passBar[1] + PASS) * BAR, 1.4, 0.0, 0.4); // bridge 2 — down free(vox); free(h5); free(ho); free(h3); free(hd); free(osc); free(oscu); free(oscd); // ── HEALING SINE SWARM — his voice re-cast as a swarm of sustained sines ─ @@ -717,6 +741,24 @@ free(rain); } } + // ── DEEP HIGHWAY DRIVING (freesound) — the low rushing roar the vocal cuts + // across. Looped, deep + present (more than the rain), from the build on. + if (!SINES) { + long hwN = 0; float *hw = load_wav_mono("../sources/highway.wav", &hwN); + if (hw && hwN > SR) { + double pos = 0; + for (long i = 0; i < N; i++) { + pos += 1.0; if (pos >= hwN - 1) pos -= (hwN - 1); + long i0 = (long)pos; double fr = pos - i0; + double s = hw[i0] * (1 - fr) + hw[(i0 + 1 < hwN) ? i0 + 1 : i0] * fr; + double pg = fmin(1.0, fmax(0.0, ((double)i / SR - PRELUDE * BAR) / (3 * BAR))); + double v = s * 0.16 * pg; // deep + present + musL[i] += (float)v; musR[i] += (float)v; + } + free(hw); + } + } + // ── LA HELICOPTER flyby (freesound) — a close chopper passes overhead, // panning L→R, at a couple of dramatic moments (into bridge 1 + into the // final drop). Skipped if the file is missing. ────────────────────────── @@ -779,23 +821,36 @@ if (kickOn) for (int beat = 0; beat < 4; beat++) { if (bar % 8 == 6 && beat == 3) continue; // SKIP a kick for a stutter double floorf = (beat & 1) ? 46.0 : 31.0; // deeper A (tick) / D (tock) - kick(bar * BAR + beat * BEAT + hum, 0.74 * fmin(1.18, intens), floorf); - // a syncopated ghost-kick "skip" pushes some bars forward - if (bar % 4 == 2 && beat == 1) kick(bar * BAR + beat * BEAT + BEAT * 0.5 + hum, 0.40, 40.0); + kick(bar * BAR + beat * BEAT + hum, 0.98 * fmin(1.18, intens), floorf); } + // classic 2000s DONK — used PLAYFULLY: a pattern of which offbeats get a + // donk, flipping each bar (not a mechanical donk on every offbeat). + if (inPass) { + const char *DK[4] = { "..x.", "x..x", ".x..", "x.x." }; + const char *dk = DK[bar % 4]; + for (int beat = 0; beat < 4; beat++) + if (dk[beat] == 'x') + donk(ROOT[phr] + 24, bar * BAR + beat * BEAT + BEAT * 0.5 + hum, 0.55 * fmin(1.15, intens), (beat & 1) ? 0.3 : -0.3); + } + + // reverse-kick used PLAYFULLY — a whoosh into the big section starts, and + // the odd lift into a phrase. NOT on every kick. + if (bar == passBar[0] || bar == passBar[1] || bar == passBar[2]) revkick(bar * BAR, BEAT * 2.0, 0.55); + else if (inPass && bar % 8 == 7) revkick((bar + 1) * BAR, BEAT * 1.0, 0.34); + // TIGHT closed hat — centred, on the offbeat 8ths, no swing wobble, no // panning. Simple and locked. - double hatg = (inPass ? 1.0 : 0.0) * (0.15 + 0.04 * (pass > 0 ? pass : 0)); // pulled way down + double hatg = (inPass ? 1.0 : 0.0) * (0.09 + 0.03 * (pass > 0 ? pass : 0)); // quieter if (hatg > 0) for (int st = 1; st < 8; st += 2) hat(bar * BAR + st * (BEAT / 2.0) + hum, hatg, 0.0); // VOCALS-ONLY OPENING — sub-bell + pad are SILENT in the prelude. - if (!inPrelude) subbell(ROOT[phr], bar * BAR + hum, BAR * (phr == 0 ? 1.4 : 1.0), 0.26); + if (!inPrelude) subbell(ROOT[phr], bar * BAR + hum, BAR * (phr == 0 ? 1.4 : 1.0), 0.20); if (!inPrelude) chordpad(CHORD[phr], 3, bar * BAR, BAR * 1.02, - (inBridge ? 0.20 : 0.13), (phr & 1) ? 0.22 : -0.22); + (inBridge ? 0.14 : 0.095), (phr & 1) ? 0.22 : -0.22); // bell arpeggio — from the INTRO build onward (no bells in the prelude). // Gentle pluck on the 8ths; busier + brighter on later passes, dropped @@ -811,7 +866,7 @@ for (int st = 0; st < 8; st++) { if (pat[st] != 'x') continue; int note = arp[ai % 4] + boct - 12; // an octave LOWER (less high) ai++; - double bg = ((inBridge ? 0.22 : 0.18) + (pass >= 1 ? 0.04 : 0.0)) * fmin(1.2, intens); + double bg = ((inBridge ? 0.15 : 0.12) + (pass >= 1 ? 0.03 : 0.0)) * fmin(1.2, intens); // sines lower double bpan = panLFO * 0.45 + ((st & 2) ? 0.25 : -0.25); // movement bell(note, swung(st) + hum, BEAT * 4.0, bg, bpan); // SWUNG + humanized + long ring } @@ -835,7 +890,7 @@ if (pp[s] != 'x') continue; double t = bar * BAR + s * (BEAT / 4.0) - 0.014 + hum; // eager (ahead) + human double dur = (s % 8 == 4) ? BEAT * 1.3 : BEAT * 0.6; // "dash" long notes vs dribbles int note = SQMEL[phr][(bar + li) % 4]; li++; - sqlead(note, t, dur, 0.085, 380, openness, 3.0, (s & 2) ? 0.25 : -0.25); + sqlead(note, t, dur, 0.060, 380, openness, 3.0, (s & 2) ? 0.25 : -0.25); } } } @@ -846,22 +901,19 @@ for (int b2 = 1; b2 < 4; b2 += 2) snare(bar * BAR + b2 * BEAT + hum, 0.62 * intens, (rnd() - 0.5) * 0.3); // (reverse-swell risers removed — the "shheeep" every ~4 bars was too repetitive) - // SUPERSAMPLING SCRATCH fill — a turntable scratch on the boom sample at - // phrase ends in the passes (busier as the track pushes harder). - if (inPass && phr == 3 && boomS && !SINES) - scratch(boomS, boomN, bar * BAR + 3 * BEAT + hum, BEAT * 0.95, 0.45 * intens, (rnd() - 0.5) * 0.4); + // (scratch + bird/elf arp removed — simpler track, no regulated arps) - // VOCAL ADLIB ARP — little "bird/elf" chirps: a fast 8th-note STAIRCASE - // up AND down the D-minor scale, pitched HIGH, pad-ish with SHARPER - // starts on alternate steps. An ornament every other phrase. - if (inPass && (bar % 8 == 7) && maS) { - static const int stair[8] = { 12, 15, 17, 19, 17, 15, 14, 12 }; // up then down (staircase), high - for (int s = 0; s < 8; s++) { - double t = bar * BAR + s * (BEAT / 2.0) + hum; - double atk = (s & 1) ? 20.0 : 1.5; // sharper starts on some - vocnote(maS, maN, t, BEAT * 1.2, stair[s], 0.28, atk, (s & 1) ? 0.45 : -0.45); - } + // GRAND PIANO — occasional single sustained high note (NOT a run/arp). + // Once every 8 bars, a lone ringing piano tone up high. + if (inPass && (bar % 8 == 0)) { + static const int hi[4] = { 81, 77, 84, 79 }; // A5 / F5 / C6 / G5 (D minor) + piano(hi[(bar / 8) % 4], bar * BAR + hum, BEAT * 5.0, 0.18, 0.2 * panLFO); } + + // TRIANGLE — just ONCE IN A WHILE (not a pattern): a lone high shimmer + // roughly every 16 bars, on an irregular beat. (sample → off in --sines.) + if (inPass && tri && !SINES && (bar % 16 == 9)) + ding(tri, triN, bar * BAR + 2.5 * BEAT + hum, 12.0, 0.34, 0.3); // WHISTLE FEATURE — the breakdown before the final drop goes FULL // ethereal: a pure whistle climbs the chord, drenched, taking the @@ -875,30 +927,8 @@ // (filter-sweep risers removed — they were too repetitive into every pass) } - // ── scattered single-beat BOOM / MA vocal hits — rhythmic accents placed - // in time across the passes (the boom-ba-boom syllables, sprinkled). ────── - { - for (int bar = passBar[0]; bar < passBar[NP - 1] + PASS; bar++) { - int inP = 0; for (int p = 0; p < NP; p++) if (bar >= passBar[p] && bar < passBar[p] + PASS) inP = 1; - if (!inP) continue; - if (bar % 4 == 2 && boomS) place_at(boomS, boomN, 0, bar * BAR + 1 * BEAT, 0.5, -0.3, 0.40); - if (bar % 4 == 3 && maS) place_at(maS, maN, 0, bar * BAR + 3 * BEAT, 0.45, 0.3, 0.45); - if (bar % 8 == 5 && boomS) place_at(boomS, boomN, 0, bar * BAR + 2 * BEAT + 0.25 * BEAT, 0.42, 0.2, 0.40); - // SHORT PERCUSSIVE boom/ma hits — tight transients scattered like - // little drum elements on a syncopated 16th pattern (boom=B, ma=m). - if (boomP && maP && !SINES) { - const char *PB = "B..m..B..m.B..m."; // booms + mas as percussion - for (int s = 0; s < 16; s++) { - double t = bar * BAR + s * (BEAT / 4.0) + ((s & 1) ? 0.18 * (BEAT / 4.0) : 0.0); - double pan = ((s * 5) % 7) / 7.0 - 0.5; - // DRUM bus (dry, punchy, not ducked) + boosted so they CUT. - if (PB[s] == 'B') place_drum(boomP, boompN, t, 0.85, pan, 0.12); - else if (PB[s] == 'm') place_drum(maP, mapN, t, 0.72, pan, 0.14); - } - } - } - } - free(boomS); free(maS); free(boomP); free(maP); + // (boom/ma percussion scatter removed — simpler track, singular vocal) + free(boomS); free(maS); free(boomP); free(maP); free(tri); // ── sidechain: the kick ducks the music bed (bells/chord/swarm/etc.) ────── { double depth = 0.45, rel = exp(-1.0 / (0.13 * SR)), env = 0; @@ -962,9 +992,12 @@ } free(vocL); free(vocR); double peak = 0; for (long i = 0; i < N; i++) { double a = fmax(fabs(busL[i]), fabs(busR[i])); if (a > peak) peak = a; } if (peak > 0) { double g = 0.92 / peak; for (long i = 0; i < N; i++) { busL[i] *= (float)g; busR[i] *= (float)g; } } - long fin = (long)(0.4 * SR), fout = (long)(2.2 * SR); + long fin = (long)(0.4 * SR), fout = (long)(1.0 * SR); // short safety taper; the real for (long i = 0; i < fin && i < N; i++) { double g = (double)i / fin; busL[i] *= (float)g; busR[i] *= (float)g; } - for (long i = 0; i < fout && i < N; i++) { double g = (double)(fout - i) / fout; long idx = N - 1 - i; busL[idx] *= (float)g; busR[idx] *= (float)g; } + for (long i = 0; i < fout && i < N; i++) { // long graceful fade is applied + double g = (double)(fout - i) / fout; // AFTER loudnorm in render.sh so + long idx = N - 1 - i; busL[idx] *= (float)g; busR[idx] *= (float)g; // it isn't re-normalized + } if (!write_wav_f32_stereo(out_path, busL, busR, N)) { fprintf(stderr, "✗ write failed\n"); return 1; } fprintf(stderr, "✓ %s · %.1fs\n", out_path, (double)N / SR); diff --git a/pop/boombaboom/c/render.sh b/pop/boombaboom/c/render.sh --- a/pop/boombaboom/c/render.sh +++ b/pop/boombaboom/c/render.sh @@ -151,8 +151,16 @@ "$PY" "$ROOT/bin/accel.py" "$MASTER" "$ROOT/sources/.master-accel.wav" 0.18 # re-normalize after the accel (rubberband overshoots past the limiter) with a # true-peak ceiling so there is NO clipping. ffmpeg -hide_banner -loglevel error -y -i "$ROOT/sources/.master-accel.wav" \ - -af "loudnorm=I=-13:TP=-1.5:LRA=12,aformat=s16" -ar 44100 "$MASTER" + -af "loudnorm=I=-13:TP=-1.5:LRA=12,aformat=s16" -ar 44100 "$ROOT/sources/.master-pre.wav" rm -f "$ROOT/sources/.master-accel.wav" + +# FINAL fade-out AFTER loudnorm (so the normalizer can't pump the tail back up — +# that was the "stops then starts again" ending). Clean 9s fade. +DUR=$("$PY" -c "import soundfile as sf; print(sf.info('$ROOT/sources/.master-pre.wav').duration)") +FST=$("$PY" -c "print(max(0.0, $DUR - 9.0))") +ffmpeg -hide_banner -loglevel error -y -i "$ROOT/sources/.master-pre.wav" \ + -af "afade=t=out:st=${FST}:d=9,aformat=s16" -ar 44100 "$MASTER" +rm -f "$ROOT/sources/.master-pre.wav" ffmpeg -hide_banner -loglevel error -y -i "$MASTER" -c:a libmp3lame -b:a 320k "$MP3" echo "✓ $MP3" diff --git a/pop/boombaboom/sources/highway.CREDITS.txt b/pop/boombaboom/sources/highway.CREDITS.txt new file mode 100644 --- /dev/null +++ b/pop/boombaboom/sources/highway.CREDITS.txt @@ -0,0 +1,19 @@ +highway.wav — deep continuous highway/freeway roar (inside a car at speed: tire roar + wind + distant traffic whoosh; no honks/sirens/stop-and-go) + +Freesound attribution + sound id: 581022 + title: S_Amb_InsideACarWhileDriving_Mono_01.wav + author: yake01 + license: CC0 1.0 Public Domain Dedication (http://creativecommons.org/publicdomain/zero/1.0/) + url: https://freesound.org/people/yake01/sounds/581022/ + +Attribution line: + "S_Amb_InsideACarWhileDriving_Mono_01.wav" by yake01 — Freesound #581022 — CC0 1.0 — https://freesound.org/people/yake01/sounds/581022/ + +Processing + Source: Freesound HQ preview (44.1 kHz mono-source MP3, ~403.5 s) fetched via pop/lib/freesound.mjs. + Region: 35 s extracted from 60 s — the steadiest stretch (per-10s mean held -17.7 to -17.8 dB, no transient spikes/horns/bumps). + Output: mono, 48000 Hz, 16-bit PCM WAV; loudnorm I=-20 TP=-3 LRA=7; 0.2 s fade in/out. + Character: deep, even roar — low band (<250 Hz) within ~0.6 dB of full-band; highs (>2 kHz) ~29 dB down. Continuous rushing wall, not city stop-and-go. + +Note: CC0 requires no attribution, but credit retained per pop convention. diff --git a/pop/boombaboom/sources/triangle.CREDITS.txt b/pop/boombaboom/sources/triangle.CREDITS.txt new file mode 100644 --- /dev/null +++ b/pop/boombaboom/sources/triangle.CREDITS.txt @@ -0,0 +1,20 @@ +triangle.wav — orchestral percussion triangle: a single bright "ting" with a long shimmering metallic ring-out + +Freesound attribution + sound id: 349502 + title: Triangle_Open_03 + author: cabled_mess + license: CC0 1.0 Public Domain Dedication (http://creativecommons.org/publicdomain/zero/1.0/) + url: https://freesound.org/people/cabled_mess/sounds/349502/ + +Attribution line: + "Triangle_Open_03" by cabled_mess — Freesound #349502 — CC0 1.0 — https://freesound.org/people/cabled_mess/sounds/349502/ + +Processing + Source: Freesound HQ preview (48 kHz mono MP3, 5.21 s) fetched via pop/lib/freesound.mjs. + Region: one clean open-triangle strike. Attack placed at sample 0 (silenceremove of leading sub-threshold pre-roll), kept the full ring decay out to 2.4 s. + Output: mono, 48000 Hz, 16-bit PCM WAV; 5 ms fade-out on the tail. + Character: single bright "ting" attack (peak -1.79 dB at sample 0), long shimmering metallic ring (~-32 dB at 1.0 s, gently decaying to ~-34.5 dB by 2.0 s). Not a roll, not a loop. + +Note: CC0 requires no attribution, but credit retained per pop convention. + Considered alternates: #349503 (cabled_mess, CC0, shorter ring), #375581 (sgossner VSCO, CC0, 44.1k stereo, brief gap after hit), #360327 (InspectorJ, CC-BY, requires attribution). diff --git a/pop/emotrib/c/build.sh b/pop/emotrib/c/build.sh new file mode 100644 --- /dev/null +++ b/pop/emotrib/c/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# build.sh — compile emotrib.c (the emo-trap engine). +set -euo pipefail +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +cc -O3 -std=c11 -Wall -Wextra -Wno-unused-parameter \ + -o "$HERE/emotrib" \ + "$HERE/emotrib.c" \ + -lm +echo "→ $HERE/emotrib" diff --git a/pop/emotrib/c/emotrib b/pop/emotrib/c/emotrib new file mode 100644 --- /dev/null +++ b/pop/emotrib/c/emotrib diff --git a/pop/emotrib/c/emotrib.c b/pop/emotrib/c/emotrib.c new file mode 100644 --- /dev/null +++ b/pop/emotrib/c/emotrib.c @@ -0,0 +1,470 @@ +// emotrib.c — "emo trap." An emotional trap instrumental, rendered ENTIRELY +// in C (no JS, no samples): deep gliding 808 sub, a layered click kick, a +// halftime snare/clap, triplet hi-hat rolls, and a sad F-minor bell/piano +// loop over a soft pad, with a thread of vinyl crackle for the feels. +// +// House style (per pop/boombaboom/c/boombaboom.c + pop/hellsine/c): tonal +// voices are sums of sin() phases advanced by freq/SR, rounded with a +// memoryless tanh for grit; percussion (hats, snare noise, vinyl) is the one +// licensed exception — xorshift noise shaped by one-pole filters + envelopes, +// the same lane nullnoise.c lives in. Nothing here reads a WAV. +// +// Form (140 BPM, halftime feel — snare on beat 3): an 8-bar melody-only +// intro, then three full-beat sections (A1/A2/A3) of rising hat density and +// melody octaves, split by a stripped pad+melody break and a bridge, closing +// on a melody-fade outro. +// +// Build: bash emotrib/c/build.sh +// Run: ./emotrib --out out/emotrib-raw.wav [--bpm 140] + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif +#define TAU (2.0 * M_PI) + +static const int SR = 48000; +static double BPMV = 140, BEAT, BAR; +static long N; // total samples + +// ── tiny rng (xorshift) — for noise perc + vinyl + humanized timing ──────── +static uint32_t rng_s = 0x656d6f74; // "emot" +static inline double rnd(void) { + rng_s ^= rng_s << 13; rng_s ^= rng_s >> 17; rng_s ^= rng_s << 5; + return (double)rng_s / 4294967296.0; +} +static inline double rnd2(void) { return rnd() * 2.0 - 1.0; } +static inline double midi_hz(double m) { return 440.0 * pow(2.0, (m - 69.0) / 12.0); } +static inline double clampd(double x, double a, double b) { return x < a ? a : x > b ? b : x; } + +// ── buses ────────────────────────────────────────────────────────────────── +// bass : 808 + kick (low end, gets a gentle kick-keyed pump) +// mus : bell/piano + pad melodic content (also pumps with the kick) +// drum : snare + hats (dry, punchy) +// rev : reverb send (bell + snare tail) — convolved-ish at mixdown +static float *bassL, *bassR, *musL, *musR, *drumL, *drumR, *revL, *revR; +static float *duck; // kick-keyed sidechain envelope (1 = open, dips on hits) + +static inline void addB(long i, double l, double r) { if (i >= 0 && i < N) { bassL[i] += (float)l; bassR[i] += (float)r; } } +static inline void addM(long i, double l, double r) { if (i >= 0 && i < N) { musL[i] += (float)l; musR[i] += (float)r; } } +static inline void addD(long i, double l, double r) { if (i >= 0 && i < N) { drumL[i] += (float)l; drumR[i] += (float)r; } } +static inline void addR(long i, double l, double r) { if (i >= 0 && i < N) { revL[i] += (float)l; revR[i] += (float)r; } } + +static int write_wav_f32_stereo(const char *path, const float *L, const float *R, long n) { + FILE *f = fopen(path, "wb"); if (!f) return 0; + uint32_t dsz = (uint32_t)(n * 8), riff = 36 + dsz, sr = SR, br = SR * 8, fsz = 16; + uint16_t fmt = 3, ch = 2, ba = 8, bits = 32; + fwrite("RIFF", 1, 4, f); fwrite(&riff, 4, 1, f); fwrite("WAVE", 1, 4, f); + fwrite("fmt ", 1, 4, f); fwrite(&fsz, 4, 1, f); + fwrite(&fmt, 2, 1, f); fwrite(&ch, 2, 1, f); fwrite(&sr, 4, 1, f); + fwrite(&br, 4, 1, f); fwrite(&ba, 2, 1, f); fwrite(&bits, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&dsz, 4, 1, f); + for (long i = 0; i < n; i++) { fwrite(&L[i], 4, 1, f); fwrite(&R[i], 4, 1, f); } + fclose(f); return 1; +} + +// ════════════════════════════════════════════════════════════════════════ +// VOICES +// ════════════════════════════════════════════════════════════════════════ + +// 808 SUB — sine carrier with a fast pitch "click" at the attack, long +// exponential body, tanh saturation, and optional portamento glide IN from a +// previous pitch (the signature emo-trap slide). Writes to the bass bus and +// stamps the sidechain duck so everything breathes on the sub hits too. +static void e808(double t, double midi, double durBeats, double fromMidi, double gain) { + long s0 = (long)(t * SR); + double dur = durBeats * BEAT; + long len = (long)((dur + 0.30) * SR); + double f0 = midi_hz(midi); + double fprev = fromMidi >= 0 ? midi_hz(fromMidi) : f0; + double phase = 0, drift = rnd2() * 0.5; // tiny per-hit detune for life + for (long i = 0; i < len; i++) { + double tt = (double)i / SR; + double gl = fromMidi >= 0 ? clampd(tt / 0.07, 0, 1) : 1.0; // 70ms glide + double f = fprev * pow(f0 / fprev, gl) + drift; + double click = 1.0 + 5.0 * exp(-tt / 0.010); // attack chirp + double amp = (1.0 - exp(-tt / 0.003)) * exp(-tt / (dur * 0.55 + 0.05)); + phase += f * click / SR; + double x = sin(TAU * phase); + x = tanh(x * 1.7) * 0.78; // warm sat + double s = x * amp * gain; + addB(s0 + i, s, s); + } +} + +// KICK — short sine thump (pitch drop) layered under 808 attacks for punch. +// Also stamps the sidechain duck (the groove pump). +static void kick(double t, double gain) { + long s0 = (long)(t * SR); + long len = (long)(0.18 * SR); + double phase = 0; + for (long i = 0; i < len; i++) { + double tt = (double)i / SR; + double f = 52.0 + 150.0 * exp(-tt / 0.016); // 200→52 Hz drop + double amp = exp(-tt / 0.07); + phase += f / SR; + double x = tanh(sin(TAU * phase) * 1.5) * amp * gain; + addB(s0 + i, x, x); + } + // stamp duck: a deep 1→0.30 dip recovering over ~260ms keyed at this onset — + // the audible kick-keyed pump that makes the pad/melody breathe in groove. + long dl = (long)(0.30 * SR); + for (long i = 0; i < dl; i++) { + long j = s0 + i; if (j < 0 || j >= N) continue; + double tt = (double)i / SR; + double d = 0.30 + 0.70 * (1.0 - exp(-tt / 0.11)); // deep dip, smooth recover + if (d < duck[j]) duck[j] = (float)d; + } +} + +// HI-HAT — crisp metallic tick. White noise through TWO cascaded one-pole +// highpasses (cut the gushy body), a fast-decay env, and a snappy attack so +// it reads as a tick rather than a wash. Panned slightly per hit. +static void hat(double t, double vel, int open) { + long s0 = (long)(t * SR); + double dec = open ? 0.045 : 0.013; // tight: was 0.12 / 0.028 + long len = (long)((dec * 3.5) * SR); + double h1 = 0, h2 = 0, p1 = 0, p2 = 0, pan = rnd2() * 0.25; + for (long i = 0; i < len; i++) { + double tt = (double)i / SR; + double w = rnd2(); + // two HP stages with a low coefficient → high cutoff, very little body left + h1 = 0.55 * (h1 + w - p1); p1 = w; + h2 = 0.55 * (h2 + h1 - p2); p2 = h1; + double amp = exp(-tt / dec) * vel * 0.42; + double s = h2 * amp; + addD(s0 + i, s * (0.82 - 0.4 * pan), s * (0.82 + 0.4 * pan)); + } +} + +// SNARE / CLAP — a noise body (bandpass-ish via HP+LP one-poles) plus a short +// tonal "ping" around 190 Hz, with a 3-tap clap stutter on the attack. Sends +// a tail to reverb. Lands on beat 3 (halftime). +static void snare(double t, double gain) { + long s0 = (long)(t * SR); + long len = (long)(0.34 * SR); + double hp = 0, prevN = 0, lp = 0, pphase = 0; + // clap stutter offsets (seconds) + relative levels + double taps[4] = { 0.0, 0.011, 0.021, 0.030 }; + for (long i = 0; i < len; i++) { + double tt = (double)i / SR; + // layered noise from the clap taps + double body = 0; + for (int k = 0; k < 4; k++) { + double dt = tt - taps[k]; + if (dt < 0) continue; + body += rnd2() * exp(-dt / (k == 0 ? 0.13 : 0.05)); + } + hp = 0.80 * (hp + body - prevN); prevN = body; // brighten + lp = lp + 0.55 * (hp - lp); // tame the very top + double tone = sin(TAU * pphase); pphase += 190.0 / SR; + double tamp = exp(-tt / 0.05); + double s = (lp * 0.9 + tone * tamp * 0.35) * gain; + addD(s0 + i, s, s); + addR(s0 + i, s * 0.30, s * 0.30); // reverb send + } +} + +// BELL / PIANO — the melodic voice. A struck tone: fundamental + a couple of +// inharmonic-ish partials, pluck envelope (fast attack, medium decay), light +// detune chorus across L/R. Sends to reverb for the emotional wash. +static void bell(double t, double midi, double durBeats, double vel) { + long s0 = (long)(t * SR); + double dur = durBeats * BEAT; + long len = (long)((dur + 0.6) * SR); + double f = midi_hz(midi); + double pa = 0, pb = 0, pc = 0, pd = 0; + double det = f * 0.0016; + for (long i = 0; i < len; i++) { + double tt = (double)i / SR; + double amp = (1.0 - exp(-tt / 0.004)) * exp(-tt / (dur * 0.7 + 0.18)) * vel; + pa += (f - det) / SR; pb += (f + det) / SR; // detuned pair + pc += (f * 2.01) / SR; pd += (f * 3.0) / SR; // shimmer partials + double l = (sin(TAU * pa) + 0.5 * sin(TAU * pc) + 0.22 * sin(TAU * pd)) * amp; + double r = (sin(TAU * pb) + 0.5 * sin(TAU * pc) + 0.22 * sin(TAU * pd)) * amp; + l = tanh(l * 1.1) * 0.32; r = tanh(r * 1.1) * 0.32; + addM(s0 + i, l, r); + addR(s0 + i, l * 0.45, r * 0.45); + } +} + +// PAD — soft sustained chord (stack of sine voices) under the melody. Slow +// attack/release, sits low in the mix on the music bus. +static void pad(double t, const int *midis, int nv, double durBeats, double gain) { + long s0 = (long)(t * SR); + double dur = durBeats * BEAT; + long len = (long)((dur + 0.4) * SR); + double atk = 0.25, rel = 0.5; + double ph[8] = {0}, ph2[8] = {0}; + if (nv > 8) nv = 8; + for (long i = 0; i < len; i++) { + double tt = (double)i / SR; + double env; + if (tt < atk) env = tt / atk; + else if (tt < dur) env = 1.0; + else env = clampd(1.0 - (tt - dur) / rel, 0, 1); + env *= gain; + double l = 0, r = 0; + for (int v = 0; v < nv; v++) { + double f = midi_hz(midis[v]); + ph[v] += f / SR; ph2[v] += (f * 1.004) / SR; // slight detune width + l += sin(TAU * ph[v]); + r += sin(TAU * ph2[v]); + } + double scale = env * 0.10 / (nv > 0 ? nv : 1); + addM(s0 + i, l * scale, r * scale); + } +} + +// VINYL CRACKLE — sparse low-level pops + a noise floor, for emotional texture. +static void vinyl(double t0, double dur, double gain) { + long s0 = (long)(t0 * SR), len = (long)(dur * SR); + double lp = 0; + for (long i = 0; i < len; i++) { + double w = rnd2(); + lp = lp + 0.04 * (w - lp); // dark hiss floor + double s = lp * 0.5; + if (rnd() > 0.99955) s += rnd2() * 0.9; // occasional pop + s *= gain; + addM(s0 + i, s, s); + } +} + +// ════════════════════════════════════════════════════════════════════════ +// COMPOSITION — F minor. i–VI–III–VII (Fm – Db – Ab – Eb), one bar each. +// ════════════════════════════════════════════════════════════════════════ + +// MIDI helpers (F minor scale tones used below) +enum { F2=41, Ab1=32, Db2=37, Eb2=39, // 808 roots + G3=55, Ab3=56, Bb3=58, C4=60, Db4=61, Eb4=63, F4=65, G4=67, Ab4=68 }; + +// 808 root per chord-bar (low octave) + a glide-target into the next bar. +static const int ROOTS[4] = { F2, Db2, Ab1, Eb2 }; +// melody — eighth-note grid (8 per bar; -1 = rest), one row per chord. FOUR +// variations so the line evolves across the form instead of looping forever: +// 0 = sad descending motif (intro / A1) +// 1 = more active, higher answer (A2) +// 2 = top-octave climax lead, denser (A3) +// 3 = sparse, breathing (break / outro) +static const int MEL[4][4][8] = { + { // 0 — the core motif + /* Fm */ { C4, -1, Ab3, -1, F4, -1, Eb4, C4 }, + /* Db */ { Db4,-1, Ab3, -1, F4, -1, Db4, -1 }, + /* Ab */ { C4, -1, Ab3, -1, Eb4,-1, C4, -1 }, + /* Eb */ { Bb3,-1, G3, -1, Eb4,-1, Bb3, G3 }, + }, + { // 1 — active answer + /* Fm */ { Ab3,C4, Eb4,-1, F4, Eb4,C4, Ab3 }, + /* Db */ { F4, -1, Ab3,Db4, -1, F4, Eb4,Db4 }, + /* Ab */ { Eb4,C4, Ab3,C4, Eb4,-1, G4, Eb4 }, + /* Eb */ { Bb3,Eb4,G4, -1, F4, Eb4,Bb3,G3 }, + }, + { // 2 — top-octave climax + /* Fm */ { C4, Eb4,F4, Ab4, G4, F4, Eb4,C4 }, + /* Db */ { Db4,F4, Ab4,-1, F4, Ab3,F4, Db4 }, + /* Ab */ { C4, Eb4,Ab4,-1, G4, Eb4,C4, Eb4 }, + /* Eb */ { Bb3,Eb4,G4, Bb3, Eb4,G4, F4, Eb4 }, + }, + { // 3 — sparse / breathing + /* Fm */ { -1,-1, C4, -1, -1,-1, Ab3,-1 }, + /* Db */ { -1,-1, Ab3,-1, -1,-1, F4, -1 }, + /* Ab */ { -1,-1, Eb4,-1, -1,-1, C4, -1 }, + /* Eb */ { -1,-1, Bb3,-1, -1,-1, G3, -1 }, + }, +}; +// pad chord voicings (3-note triads, mid octave) +static const int PAD[4][3] = { + /* Fm */ { 53, 56, 60 }, // F3 Ab3 C4 + /* Db */ { 49, 53, 56 }, // Db3 F3 Ab3 + /* Ab */ { 56, 60, 63 }, // Ab3 C4 Eb4 + /* Eb */ { 51, 55, 58 }, // Eb3 G3 Bb3 +}; + +// swing/humanize: nudge a beat time by a few ms +static double hum(double t) { return t + rnd2() * 0.006; } + +// place the 808 for one bar (root on the 1, a syncopated push on the "+3", +// gliding in from the previous bar's root on the downbeat). +static void bar808(double bt, int root, int prevRoot, double gain) { + e808(bt + 0.0 * BEAT, root, 2.3, prevRoot, gain); // glide-in downbeat + e808(bt + 2.5 * BEAT, root, 1.3, -1, gain * 0.92); // syncopated push +} + +// hats for one bar. density 0 = none, 1 = eighths, 2 = sixteenths + a triplet +// roll on beat 4, 3 = busy (sixteenths + two rolls). +static void barHats(double bt, int density) { + if (density <= 0) return; + int steps = density >= 2 ? 16 : 8; + for (int s = 0; s < steps; s++) { + double t = bt + (double)s / steps * 4.0 * BEAT; + double vel = 0.6 + 0.25 * ((s % 2) == 0); // accent on-beats + if (density >= 2 && (s % 4) == 0) vel += 0.12; + hat(hum(t), vel, (s % 8) == 6); // an open-ish hat now and then + } + // triplet rolls: subdivide the last beat (and beat 2 when busy) + if (density >= 2) { + double rb = bt + 3.0 * 4.0 / 4.0 * BEAT; // start of beat 4 + int n = 6; + for (int k = 0; k < n; k++) + hat(rb + (double)k / n * BEAT, 0.45 + 0.5 * k / n, 0); + } + if (density >= 3) { + double rb = bt + 1.0 * BEAT; // a roll on beat 2 as well + int n = 4; + for (int k = 0; k < n; k++) + hat(rb + (double)k / n * BEAT, 0.4 + 0.5 * k / n, 0); + } +} + +// one full bar of the beat. flags pick which layers play. melVar = which of +// the four MEL variations this section's melody uses. +typedef struct { int drums, hats, oct808, melody, melOct, pad, snareTriplet, melVar; } Bar; + +static void renderBar(int barIdx, double bt, int chord, int prevRootMidi, Bar cfg) { + int root = ROOTS[chord]; + + // 808 + kick + if (cfg.drums) { + int r = cfg.oct808 ? root + 12 : root; + int pr = prevRootMidi >= 0 ? (cfg.oct808 ? prevRootMidi + 12 : prevRootMidi) : -1; + bar808(bt, r, pr, 0.95); + kick(hum(bt + 0.0), 0.9); + kick(hum(bt + 2.5 * BEAT), 0.62); // ghost kick with the 808 push + // halftime snare on beat 3 + snare(bt + 2.0 * BEAT, 0.9); + if (cfg.snareTriplet) { // a triplet snare fill feel + snare(bt + 3.66 * BEAT, 0.5); + snare(bt + 3.83 * BEAT, 0.62); + } + } + barHats(bt, cfg.hats); + + // pad + if (cfg.pad) pad(bt, PAD[chord], 3, 4.0, 1.0); + + // melody + if (cfg.melody) { + for (int s = 0; s < 8; s++) { + int m = MEL[cfg.melVar][chord][s]; + if (m < 0) continue; + double t = bt + (double)s / 8.0 * 4.0 * BEAT; + double dur = 0.9; // eighth-ish, let it ring a touch + double vel = 0.85 + 0.1 * rnd(); + bell(hum(t), m + (cfg.melOct ? 12 : 0), dur, vel); + if (cfg.melOct) bell(hum(t), m, dur, vel * 0.5); // double low for body + } + } +} + +// the arrangement: a list of (bars, chordStartOffset, Bar cfg). +int main(int argc, char **argv) { + const char *outPath = "out/emotrib-raw.wav"; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--out") && i + 1 < argc) outPath = argv[++i]; + else if (!strcmp(argv[i], "--bpm") && i + 1 < argc) BPMV = atof(argv[++i]); + } + BEAT = 60.0 / BPMV; BAR = BEAT * 4.0; + + // section table — bar counts + per-section Bar config + struct { int bars; Bar cfg; } sections[] = { + // drums hats oct melody melOct pad snTrip melVar + { 8, (Bar){ 0, 0, 0, 1, 0, 1, 0, 0 } }, // intro: pad + melody (motif) + atmos + { 16, (Bar){ 1, 1, 0, 1, 0, 1, 0, 0 } }, // A1: beat in, eighth hats, motif + { 8, (Bar){ 0, 0, 0, 1, 0, 1, 0, 3 } }, // break: strip drums, sparse melody + { 16, (Bar){ 1, 2, 0, 1, 0, 1, 1, 1 } }, // A2: 16th hats + rolls, active melody + { 8, (Bar){ 1, 1, 0, 0, 0, 1, 0, 0 } }, // bridge: drums + pad, melody rest + { 16, (Bar){ 1, 3, 1, 1, 1, 1, 1, 2 } }, // A3: climax — busy, octave 808+mel + { 8, (Bar){ 0, 0, 0, 1, 0, 1, 0, 3 } }, // outro: sparse melody fade + }; + int nSec = (int)(sizeof(sections) / sizeof(sections[0])); + + int totalBars = 0; + for (int s = 0; s < nSec; s++) totalBars += sections[s].bars; + double dur = totalBars * BAR + 1.0; // +1s tail for ring-out + N = (long)(dur * SR); + + bassL = calloc(N, sizeof(float)); bassR = calloc(N, sizeof(float)); + musL = calloc(N, sizeof(float)); musR = calloc(N, sizeof(float)); + drumL = calloc(N, sizeof(float)); drumR = calloc(N, sizeof(float)); + revL = calloc(N, sizeof(float)); revR = calloc(N, sizeof(float)); + duck = malloc(N * sizeof(float)); + for (long i = 0; i < N; i++) duck[i] = 1.0f; + + // atmospheric vinyl under the whole thing + vinyl(0.0, dur, 0.5); + + // walk the arrangement + int barIdx = 0; double bt = 0; int prevRoot = -1; + for (int s = 0; s < nSec; s++) { + for (int b = 0; b < sections[s].bars; b++) { + int chord = barIdx % 4; + int root = ROOTS[chord]; + renderBar(barIdx, bt, chord, prevRoot, sections[s].cfg); + prevRoot = root; + bt += BAR; barIdx++; + } + } + + // ── simple stereo reverb on the rev bus (3 combs + 1 allpass) ──────────── + { + int combLen[3] = { 1789, 1999, 2237 }, apLen = 557; + float *cb[3]; for (int k = 0; k < 3; k++) cb[k] = calloc(combLen[k], sizeof(float)); + float *ap = calloc(apLen, sizeof(float)); + int ci[3] = {0,0,0}, ai = 0; + for (long i = 0; i < N; i++) { + double in = (revL[i] + revR[i]) * 0.5; + double acc = 0; + for (int k = 0; k < 3; k++) { + double y = cb[k][ci[k]]; + cb[k][ci[k]] = (float)(in + y * 0.78); + ci[k] = (ci[k] + 1) % combLen[k]; + acc += y; + } + acc /= 3.0; + double yo = ap[ai]; + double xo = acc + yo * 0.5; + ap[ai] = (float)xo; ai = (ai + 1) % apLen; + double wet = yo - acc * 0.5; + revL[i] = (float)(wet * 0.9); + revR[i] = (float)(wet * 1.0); + } + for (int k = 0; k < 3; k++) free(cb[k]); free(ap); + } + + // ── mixdown: apply the kick-keyed pump to bass + music, sum buses ──────── + float *L = calloc(N, sizeof(float)), *R = calloc(N, sizeof(float)); + for (long i = 0; i < N; i++) { + double d = duck[i]; + double l = bassL[i] * (0.6 + 0.4 * d) // 808/kick breathe a little + + musL[i] * (0.18 + 0.82 * d) // melody/pad pump hard + + drumL[i] * 0.9 + + revL[i] * (0.35 + 0.65 * d); // wash pumps too + double r = bassR[i] * (0.6 + 0.4 * d) + + musR[i] * (0.18 + 0.82 * d) + + drumR[i] * 0.9 + + revR[i] * (0.35 + 0.65 * d); + L[i] = (float)l; R[i] = (float)r; + } + + // normalize to -1.5 dBFS-ish peak (final loudness is the ffmpeg master's job) + double peak = 0; + for (long i = 0; i < N; i++) { + if (fabs(L[i]) > peak) peak = fabs(L[i]); + if (fabs(R[i]) > peak) peak = fabs(R[i]); + } + double g = peak > 0 ? 0.84 / peak : 1.0; + long fi = (long)(0.004 * SR), fo = (long)(0.8 * SR); + for (long i = 0; i < N; i++) { L[i] *= (float)g; R[i] *= (float)g; } + for (long i = 0; i < fi && i < N; i++) { L[i] *= (float)i / fi; R[i] *= (float)i / fi; } + for (long i = 0; i < fo && i < N; i++) { L[N-1-i] *= (float)i / fo; R[N-1-i] *= (float)i / fo; } + + if (!write_wav_f32_stereo(outPath, L, R, N)) { fprintf(stderr, "emotrib: cannot write %s\n", outPath); return 1; } + fprintf(stderr, "emotrib: %d bars, %.1f BPM, %.1f s -> %s\n", totalBars, BPMV, (double)N / SR, outPath); + return 0; +} diff --git a/pop/emotrib/c/render.sh b/pop/emotrib/c/render.sh new file mode 100644 --- /dev/null +++ b/pop/emotrib/c/render.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# render.sh — full emotrib render, all native (C engine → ffmpeg trap master), +# no JS. Builds + runs the engine, then a punchy, bass-forward club master with +# loud-but-controlled lows → mp3 + wav in ~/Documents/Shelf/emotrib. +set -euo pipefail +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +ROOT="$(cd -- "$HERE/.." && pwd)" # pop/emotrib +OUT="${1:-$HOME/Documents/Shelf/emotrib}" +mkdir -p "$OUT" "$HERE/out" +BPM="${BPM:-140}" + +RAW="$HERE/out/emotrib-raw.wav" +MASTER="$OUT/emotrib-MASTER.wav" +MP3="$OUT/emotrib.mp3" + +# 1 · build + render the C engine +bash "$HERE/build.sh" +( cd "$HERE" && ./emotrib --out "$RAW" --bpm "$BPM" ) + +# 2 · TRAP MASTER — protect the sub, keep it tight + loud. Highpass the rumble, +# firm the 808 fundamental, scoop a little low-mid mud, lift presence on the +# snare/hats + air on the bells, glue compress, soft-clip for density, +# loudnorm + true-peak ceiling. +MASTER_AF="highpass=f=24,\ +equalizer=f=50:t=q:w=0.8:g=2.2,\ +equalizer=f=120:t=q:w=1.0:g=-1.0,\ +equalizer=f=350:t=q:w=1.1:g=-2.0,\ +equalizer=f=3500:t=q:w=1.1:g=1.0,\ +equalizer=f=8000:t=q:w=1.0:g=1.2,\ +highshelf=f=12000:g=2.4,\ +lowpass=f=19000,\ +acompressor=threshold=-18dB:ratio=2.0:attack=20:release=240:makeup=1.4:knee=8,\ +asoftclip=type=tanh:threshold=0.98,\ +loudnorm=I=-11:TP=-1.2:LRA=11,\ +alimiter=limit=0.96:attack=4:release=80" + +echo "# master → $MASTER" +ffmpeg -hide_banner -loglevel error -y -i "$RAW" -af "$MASTER_AF" \ + -ar 44100 -sample_fmt s16 "$MASTER" + +ffmpeg -hide_banner -loglevel error -y -i "$MASTER" -c:a libmp3lame -b:a 320k "$MP3" +echo "✓ $MP3" +echo "✓ $MASTER" diff --git a/pop/marimba/bin/render-flutterbap.mjs b/pop/marimba/bin/render-flutterbap.mjs --- a/pop/marimba/bin/render-flutterbap.mjs +++ b/pop/marimba/bin/render-flutterbap.mjs @@ -2,14 +2,33 @@ #!/usr/bin/env node // render-flutterbap.mjs — render the flutterbap daytime hop to mp3. // // Score notation lives in pop/marimba/flutterbap.np (the human-readable -// form). This script holds the same music as scheduled per-voice -// events. Inline because there's no .np parser in the marimba lane yet. +// form). This script holds the same music as scheduled per-voice events. +// Inline because there's no .np parser in the marimba lane yet. +// +// ── the build-out (next stage of /pop) ───────────────────────────────── +// flutterbap started as four mallet voices with no low end. This stage +// adds a synthesized rhythm section (../synths/perc.mjs — sample-free, +// same posture as the marimba modal synth) and complexifies the form: +// +// • a 4-bar drum-led INTRO that builds the groove before the hook +// • a bass-perc bassline (the "sick" gritty 808-flavoured low voice) +// that bounces under the whole track and carries the groove +// • a kick / snare / hat / shaker kit with per-section dynamics +// (light under the hook, pulled back for the "mommy-wow" hush, +// driving through slinky + fly, resolving under the landing) +// • a vibraphone counter-pad sustaining the harmony under the lead +// • a 4-bar OUTRO that reprises the butterfly hook over the groove +// and buttons the song with a final resolved hit // -// Voices: +// Mallet voices (unchanged from the original hop): // xylophone — the singer; bright daytime syllabic lead -// bass — the bouncing ball (low C / G alternating on 1 & 3) +// bass(marimba) — the bouncing-ball mallet bass on 1 & 3 // kalimba — twinkles on phrase tails (off-beat sparkle) // glockenspiel — high steel sparkle on mommy-wow + the fly climaxes +// New voices (this stage): +// vibraphone — soft sustained chord pad under the lead +// bass-perc — gritty pitched sub; the groove's backbone +// kick/snare/hat/shaker — the synthesized kit // // Run: // node pop/marimba/bin/render-flutterbap.mjs @@ -18,6 +37,7 @@ // // All timing in seconds. 124 BPM 4/4 → beat = 60/124 ≈ 0.484, bar ≈ 1.935. import { mixEventMarimba } from "../synths/marimba.mjs"; +import { mixKick, mixBassPerc, mixSnare, mixHat, mixShaker, mixReverseBell, mixReverseKick, mixScratch, mixScream } from "../synths/perc.mjs"; import { writeFileSync, mkdirSync, unlinkSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -31,16 +51,66 @@ const _argi = (k) => { const i = process.argv.indexOf(k); return i >= 0 ? process.argv[i + 1] : null; }; const BPM = Number(_argi("--bpm")) || 124; const BEAT = 60 / BPM; const BAR = 4 * BEAT; -const TOTAL_BARS = 32; -const totalSec = TOTAL_BARS * BAR + 2.5; // + tail for the final ring + +// ── form ──────────────────────────────────────────────────────────────── +// The original hook/verse content is 32 bars. We bookend it with a drum +// intro and an outro reprise. Everything mallet-related is written against +// the ORIGINAL 1-based body bar numbers and shifted by INTRO_BARS at the +// helper, so the hook score never had to be renumbered. +const INTRO_BARS = 4; +const BODY_BARS = 32; +const OUTRO_BARS = 4; +const TOTAL_BARS = INTRO_BARS + BODY_BARS + OUTRO_BARS; // 40 +const totalSec = TOTAL_BARS * BAR + 2.8; // + tail for the final ring const ns = Math.ceil(totalSec * SR); -// time helper — t(barIndexZeroBased, beatOffsetWithinBar) -const t = (bar, beat = 0) => bar * BAR + beat * BEAT; +// ── swing ──────────────────────────────────────────────────────────── +// A straight grid marches; swing makes it skip. SWING is where the "&" +// sits inside each beat — 0.5 = dead straight, 0.667 = full triplet swing. +// We default to a relaxed 0.60 hop and expose --swing. swingBeat() maps a +// musical beat offset onto the swung grid: the first 8th of each beat is +// squeezed earlier, the & is pushed late, and 16ths stretch proportionally +// inside each half so fast runs swing too. Integer/half-beat downbeats are +// untouched (so section starts, risers and the kick anchor stay locked). +const SWING = Math.min(0.72, Math.max(0.5, Number(_argi("--swing")) || 0.60)); +function swingBeat(x) { + const q = Math.floor(x + 1e-9); // which quarter + const r = x - q; // position within the quarter (0..1) + let rp; + if (r < 0.5) rp = r * 2 * SWING; // first 8th → [0, SWING) + else rp = SWING + (r - 0.5) * 2 * (1 - SWING); // the & → [SWING, 1) + return q + rp; +} +// time helper — t(absBarZeroBased, beatOffsetWithinBar), swing applied +const t = (bar, beat = 0) => bar * BAR + swingBeat(beat) * BEAT; +// body bar (1-based) → absolute time +const tb = (bar1, beat = 0) => t(bar1 - 1 + INTRO_BARS, beat); + +// ── humanize ─────────────────────────────────────────────────────────── +// A quantised grid sounds like a machine. Real players push and drag a few +// ms off the beat and never hit two notes at the same velocity. We add a +// deterministic (reproducible) per-note micro-timing jitter and a velocity +// (gain) wobble. Chord notes jitter independently, so block chords get a +// natural micro-strum. Pass --robotic to disable. mulberry32 RNG keeps the +// sequence identical run-to-run (the scheduling order is fixed). +const HUMANIZE = !process.argv.includes("--robotic"); +let _hs = 0x1a2b3c4d >>> 0; +function hrand() { + _hs = (_hs + 0x6d2b79f5) >>> 0; + let x = Math.imul(_hs ^ (_hs >>> 15), 1 | _hs); + x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x; + return ((x ^ (x >>> 14)) >>> 0) / 4294967296; +} +const hr2 = () => hrand() * 2 - 1; // −1..1 +// jitter a start time by ±ms; never let it go negative (clip at song start) +const jit = (t0, ms) => (HUMANIZE ? Math.max(0, t0 + hr2() * ms / 1000) : t0); +// wobble a gain by ±amt (fraction); clamp ≥0 +const vel = (g, amt) => (HUMANIZE ? Math.max(0, g * (1 + hr2() * amt)) : g); // ── note table ──────────────────────────────────────────────────────── const N = { - C2: 36, G2: 43, + C1: 24, E1: 28, F1: 29, G1: 31, A1: 33, + C2: 36, D2: 38, E2: 40, F2: 41, G2: 43, A2: 45, C3: 48, E3: 52, F3: 53, G3: 55, A3: 57, B3: 59, C4: 60, D4: 62, E4: 64, F4: 65, G4: 67, A4: 69, B4: 71, C5: 72, D5: 74, E5: 76, F5: 77, G5: 79, A5: 81, B5: 83, @@ -52,48 +122,108 @@ const leadBuf = new Float32Array(ns); // xylophone — the singer const bassBuf = new Float32Array(ns); // bass marimba — bouncing ball const sparkBuf = new Float32Array(ns); // kalimba — off-beat twinkles const bellBuf = new Float32Array(ns); // glockenspiel — high sparkle +const vibBuf = new Float32Array(ns); // vibraphone — sustained pad +const riseBuf = new Float32Array(ns); // reverse sine-bell risers (breaks) +const scrBuf = new Float32Array(ns); // turntable scratches +const screamBuf = new Float32Array(ns); // scratchy kitten screams +const kickBuf = new Float32Array(ns); // kick drum +const subBuf = new Float32Array(ns); // bass-perc — gritty pitched sub +const snareBuf = new Float32Array(ns); // snare +const hatBuf = new Float32Array(ns); // hats (closed + open) +const shakBuf = new Float32Array(ns); // shaker -// ── voice routes ────────────────────────────────────────────────────── -// xylophone is naturally short — extend slightly so held notes still -// carry across a full bar without sounding choked. +// ── mallet voice routes (humanized: ±ms timing, ±% velocity) ────────── function lead(t0, midi, beats, gain = 1.0) { - mixEventMarimba({ startSec: t0, midi, durSec: beats * BEAT, - gain, preset: "xylophone", decayMul: 1.15 }, - leadBuf, { sampleRate: SR }); + mixEventMarimba({ startSec: jit(t0, 13), midi, durSec: beats * BEAT, + gain: vel(gain, 0.14), preset: "xylophone", decayMul: 1.15 }, leadBuf, { sampleRate: SR }); } -// bass: tight bouncing-ball — not a droning sub. Shorten the natural -// bass-marimba ring so each bounce is distinct. function bass(t0, midi, beats, gain = 0.85) { - mixEventMarimba({ startSec: t0, midi, durSec: beats * BEAT, - gain, preset: "bass", decayMul: 0.55 }, - bassBuf, { sampleRate: SR }); + mixEventMarimba({ startSec: jit(t0, 9), midi, durSec: beats * BEAT, + gain: vel(gain, 0.12), preset: "bass", decayMul: 0.55 }, bassBuf, { sampleRate: SR }); } -// kalimba: bright tine for off-beat twinkles. Default ring works. function spark(t0, midi, beats, gain = 0.55) { - mixEventMarimba({ startSec: t0, midi, durSec: beats * BEAT, - gain, preset: "kalimba", decayMul: 0.95 }, - sparkBuf, { sampleRate: SR }); + mixEventMarimba({ startSec: jit(t0, 14), midi, durSec: beats * BEAT, + gain: vel(gain, 0.16), preset: "kalimba", decayMul: 0.95 }, sparkBuf, { sampleRate: SR }); } -// glockenspiel: high steel, let it shimmer. function bell(t0, midi, beats, gain = 0.65) { - mixEventMarimba({ startSec: t0, midi, durSec: beats * BEAT, - gain, preset: "glockenspiel", decayMul: 1.35 }, - bellBuf, { sampleRate: SR }); + mixEventMarimba({ startSec: jit(t0, 10), midi, durSec: beats * BEAT, + gain: vel(gain, 0.12), preset: "glockenspiel", decayMul: 1.35 }, bellBuf, { sampleRate: SR }); +} +// vibraphone pad — long soft sustain, motor off, for held chord beds +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 }); } -// ── sections (matches flutterbap.np) ────────────────────────────────── +// ── kit routes (absolute 0-based bar + beat; humanized) ────────────── +// Drums keep tighter timing than the mallets but vary their velocity a lot +// (especially the hats — that's where a programmed beat gives itself away). +function KICK(ab, beat, g = 0.95, o = {}) { + mixKick({ startSec: jit(t(ab, beat), 4), gain: vel(g, 0.10), ...o }, kickBuf, { sampleRate: SR }); +} +function SNR(ab, beat, g = 0.7, o = {}) { + mixSnare({ startSec: jit(t(ab, beat), 5), gain: vel(g, 0.14), ...o }, snareBuf, { sampleRate: SR }); +} +function HAT(ab, beat, g = 0.28, o = {}) { + mixHat({ startSec: jit(t(ab, beat), 6), gain: vel(g, 0.24), ...o }, hatBuf, { sampleRate: SR }); +} +function SHK(ab, beat, g = 0.2) { + mixShaker({ startSec: jit(t(ab, beat), 7), gain: vel(g, 0.22) }, shakBuf, { sampleRate: SR }); +} +// bass-perc — the groove backbone. drive ↑ = grittier. +function BP(ab, beat, midi, durBeats, g = 0.78, o = {}) { + mixBassPerc({ startSec: jit(t(ab, beat), 5), midi, durSec: durBeats * BEAT, gain: vel(g, 0.12), ...o }, + subBuf, { sampleRate: SR }); +} +// ── reverse sine-bell riser — a glassy swell that lands on a downbeat ── +function rise(landBar, dur, midis, g) { + mixReverseBell({ landSec: t(landBar, 0), dur, midis, gain: g }, riseBuf, { sampleRate: SR }); +} +// ── reverse kick — the low-end whoosh under the bell riser ──────────── +function riseKick(landBar, dur, g) { + mixReverseKick({ landSec: t(landBar, 0), dur, gain: g }, kickBuf, { sampleRate: SR }); +} + +// ── turntable scratches — four flavours ─────────────────────────────── +const SCR = { + baby: { rate: 5.5, depth: 2.0, tone: 0.5, dur: 0.30 }, // steady forward-back + scribble: { rate: 14, depth: 1.6, tone: 0.6, dur: 0.40 }, // fast tremolo scratch + transform: { rate: 5, depth: 2.2, gate: 12, gateDuty: 0.5, tone: 0.55, dur: 0.42 }, // chopped + chirp: { rate: 7, depth: 3.0, gate: 7, gateDuty: 0.55, tone: 0.6, dur: 0.26 }, // pitch-cut +}; +function SC(ab, beat, kind, g = 0.6, midi = 50) { + mixScratch({ startSec: jit(t(ab, beat), 8), midi, gain: vel(g, 0.15), ...SCR[kind] }, + scrBuf, { sampleRate: SR }); +} +// ── scratchy kitten scream ──────────────────────────────────────────── +function SCREAM(ab, beat, midi, g = 0.5, o = {}) { + mixScream({ startSec: jit(t(ab, beat), 10), midi, gain: vel(g, 0.16), ...o }, + screamBuf, { sampleRate: SR }); +} +// a closed-hat pattern across a bar — straight 8ths, accented offbeats +function hats8(ab, g = 0.26, openLast = false) { + for (let e = 0; e < 8; e++) { + const off = e % 2 === 1; + HAT(ab, e * 0.5, off ? g : g * 0.55, { open: openLast && e === 7 }); + } +} + +// ── sections (absolute bar map; body sections offset by INTRO_BARS) ──── const SECTIONS = [ - { name: "butterfly", bar0: 0, bars: 4 }, - { name: "palofmine", bar0: 4, bars: 4 }, - { name: "mommywow", bar0: 8, bars: 4 }, - { name: "slinky", bar0: 12, bars: 8 }, - { name: "fly", bar0: 20, bars: 8 }, - { name: "land", bar0: 28, bars: 4 }, + { name: "intro", bar0: 0, bars: INTRO_BARS }, + { name: "butterfly", bar0: 4, bars: 4 }, + { name: "palofmine", bar0: 8, bars: 4 }, + { name: "mommywow", bar0: 12, bars: 4 }, + { name: "slinky", bar0: 16, bars: 8 }, + { name: "fly", bar0: 24, bars: 8 }, + { name: "land", bar0: 32, bars: 4 }, + { name: "outro", bar0: 36, bars: OUTRO_BARS }, ]; -// ── LEAD line (xylophone) — bar-by-bar from flutterbap.np ───────────── -// L(bar1based, beatOffset, midi, durBeats[, gain]) -function L(bar, b, midi, d, g) { lead(t(bar - 1, b), midi, d, g ?? 1.0); } +// ══════════════════════════════════════════════════════════════════════ +// LEAD line (xylophone) — bar-by-bar from flutterbap.np (body bars 1–32) +// ══════════════════════════════════════════════════════════════════════ +function L(bar, b, midi, d, g) { lead(tb(bar, b), midi, d, g ?? 1.0); } // § butterfly (1–4) — "but-ter-FLY / cos-PLAY-er / but-ter-FLY / FLY-by" L(1, 0, N.E5, 1); L(1, 1, N.G5, 1); L(1, 2, N.C6, 2); @@ -147,70 +277,204 @@ L(30, 0, N.D5, 1); L(30, 1, N.C5, 3); L(31, 0, N.E5, 1); L(31, 1, N.D5, 1); L(31, 2, N.C5, 2); L(32, 0, N.C4, 4, 0.9); -// ── BASS — bouncing-ball pulse on 1 & 3 ─────────────────────────────── -// Quiet during mommywow (held wonder) and dropped on the soaring fly bars. -function B(bar, b, midi, d, g) { bass(t(bar - 1, b), midi, d, g ?? 0.85); } -// butterfly + palofmine: classic bouncing-ball, low/high octave alternation -for (let bar = 1; bar <= 8; bar++) { - B(bar, 0, N.C3, 1.6); - B(bar, 2, N.G3, 1.6); -} -// mommywow: one held low C per bar at lower volume — the floor of the wow -for (let bar = 9; bar <= 12; bar++) B(bar, 0, N.C3, 4, 0.55); -// slinky: bouncing returns, with a tiny C4 grace note on the &-of-4 for skip +// ── BASS (mallet bouncing-ball on 1 & 3) — body bars ────────────────── +function B(bar, b, midi, d, g) { bass(tb(bar, b), midi, d, g ?? 0.85); } +for (let bar = 1; bar <= 8; bar++) { B(bar, 0, N.C3, 1.6); B(bar, 2, N.G3, 1.6); } +for (let bar = 9; bar <= 12; bar++) B(bar, 0, N.C3, 4, 0.55); // held wonder floor for (let bar = 13; bar <= 20; bar++) { - B(bar, 0, N.C3, 1.6); - B(bar, 2, N.G3, 1.4); - if (bar % 2 === 0) B(bar, 3.5, N.C4, 0.4, 0.6); + B(bar, 0, N.C3, 1.6); B(bar, 2, N.G3, 1.4); + if (bar % 2 === 0) B(bar, 3.5, N.C4, 0.4, 0.6); // skip grace } -// fly: bass drops out on the soaring bars (22, 24, 26, 28); walking on the others -B(21, 0, N.C3, 1.6); B(21, 2, N.G3, 1.6); -B(23, 0, N.C3, 1.6); B(23, 2, N.G3, 1.6); -B(25, 0, N.C3, 1.6); B(25, 2, N.G3, 1.6); -B(27, 0, N.C3, 1.6); B(27, 2, N.G3, 1.6); -// land: a final bounce, then sustain into the low C4 landing -B(29, 0, N.C3, 1.6); B(29, 2, N.G3, 1.6); -B(30, 0, N.C3, 3); -B(31, 0, N.C3, 2); -B(32, 0, N.C2, 4, 0.75); // deep landing tone under the C4 lead +B(21, 0, N.C3, 1.6); B(21, 2, N.G3, 1.6); +B(23, 0, N.C3, 1.6); B(23, 2, N.G3, 1.6); +B(25, 0, N.C3, 1.6); B(25, 2, N.G3, 1.6); +B(27, 0, N.C3, 1.6); B(27, 2, N.G3, 1.6); +B(29, 0, N.C3, 1.6); B(29, 2, N.G3, 1.6); +B(30, 0, N.C3, 3); B(31, 0, N.C3, 2); +B(32, 0, N.C2, 4, 0.75); -// ── KALIMBA — twinkles on phrase tails + slinky off-beats ───────────── -function K(bar, b, midi, d, g) { spark(t(bar - 1, b), midi, d, g ?? 0.55); } -// palofmine: little three-note response in the tail of each bar -K(5, 3.5, N.C6, 0.5); -K(6, 3.5, N.A5, 0.5); -K(7, 3.5, N.G5, 0.5); +// ── KALIMBA twinkles — body bars ────────────────────────────────────── +function K(bar, b, midi, d, g) { spark(tb(bar, b), midi, d, g ?? 0.55); } +K(5, 3.5, N.C6, 0.5); K(6, 3.5, N.A5, 0.5); K(7, 3.5, N.G5, 0.5); K(8, 1, N.E6, 0.5); K(8, 2, N.G6, 0.5); K(8, 3, N.E6, 0.5); -// slinky: offbeat 8ths riding the wobble — only on the bounce bars (not the scales) -for (const bar of [13, 14, 15, 18, 19, 20]) { - K(bar, 0.5, N.E6, 0.5, 0.45); - K(bar, 2.5, N.C6, 0.5, 0.45); -} -// fly: sparkle haloes above the held high notes +for (const bar of [13, 14, 15, 18, 19, 20]) { K(bar, 0.5, N.E6, 0.5, 0.45); K(bar, 2.5, N.C6, 0.5, 0.45); } K(22, 0, N.E6, 0.5); K(22, 1, N.G6, 0.5); K(22, 2, N.E6, 0.5); K(22, 3, N.C6, 0.5); K(24, 0, N.F6, 0.5); K(24, 1, N.A5, 0.5); K(24, 2, N.D6, 0.5); K(24, 3, N.F6, 0.5); K(26, 3, N.E6, 0.5); K(28, 2.5, N.C6, 0.5); K(28, 3.5, N.E6, 0.5); -// land: a tiny goodnight twinkle on bar 30 and 31 -K(30, 3.5, N.E5, 0.5); -K(31, 3.5, N.G5, 0.5); -K(32, 1.5, N.C6, 0.5, 0.4); // one last drifting sparkle over the low C +K(30, 3.5, N.E5, 0.5); K(31, 3.5, N.G5, 0.5); +K(32, 1.5, N.C6, 0.5, 0.4); + +// ── GLOCKENSPIEL halo — body bars ───────────────────────────────────── +function G(bar, b, midi, d, g) { bell(tb(bar, b), midi, d, g ?? 0.65); } +G(9, 0, N.E6, 4, 0.4); G(10, 2, N.G6, 2, 0.85); +G(11, 0, N.E6, 4, 0.4); G(12, 0, N.G6, 1, 0.85); G(12, 3, N.C6, 1, 0.75); +G(22, 0, N.G6, 4, 0.6); G(24, 0, N.G6, 4, 0.6); +G(26, 2, N.G6, 2, 0.6); G(28, 0, N.G5, 2, 0.45); +G(32, 0, N.C6, 4, 0.5); G(32, 1, N.E6, 3, 0.4); + +// ── VIBRAPHONE counter-pad — sustained chord beds under the lead ────── +// Soft, low in the mix; gives the bright mallets a harmonic floor that the +// hook never had. Triads follow the implied harmony of each section. +function V(bar, b, midis, d, g = 0.34) { for (const m of midis) vib(tb(bar, b), m, d, g); } +V(5, 0, [N.C4, N.E4, N.G4], 4); // C under pal-of-mine +V(6, 0, [N.F3, N.A3, N.C4], 2); // F +V(7, 0, [N.G3, N.B3, N.D4], 2); // G +V(8, 0, [N.C4, N.E4, N.G4], 4); // C +V(9, 0, [N.C4, N.E4, N.G4], 8, 0.30); // long bed across mommy-wow +V(21, 0, [N.C4, N.E4, N.G4], 4); // C, the fly soar +V(23, 0, [N.F3, N.A3, N.C4], 4); // F lift on "I fly" +V(25, 0, [N.C4, N.E4, N.G4], 2); V(26, 0, [N.A3, N.C4, N.E4], 2); +V(27, 0, [N.G3, N.B3, N.D4], 2); V(28, 0, [N.C4, N.E4, N.G4], 2); +V(29, 0, [N.C4, N.E4, N.G4], 4, 0.30); // settle under the landing + +// ══════════════════════════════════════════════════════════════════════ +// RHYTHM SECTION — bass-perc + kit, with per-section dynamics +// ══════════════════════════════════════════════════════════════════════ +// Bass-perc lives low (C2/G1) and bounces; the kit fills in around it. The +// groove builds in the intro, eases for the hush, drives through the body, +// and resolves at the landing. + +// ── INTRO (abs 0–3): build the groove from nothing ── +// bar 0 — bass-perc breath + shaker only +BP(0, 0, N.C2, 3.5, 0.7, { drive: 1.8 }); +for (let e = 0; e < 4; e++) SHK(0, e + 0.5, 0.16); +// bar 1 — kick joins on 1 & 3, hats start +KICK(1, 0, 0.85); KICK(1, 2, 0.7); +BP(1, 0, N.C2, 1.6, 0.74); BP(1, 2.5, N.G1, 1.0, 0.66); +hats8(1, 0.18); +// bar 2 — snare backbeat enters, bass-perc bounces +KICK(2, 0, 0.9); KICK(2, 2, 0.75); +SNR(2, 2, 0.55); +BP(2, 0, N.C2, 1.4, 0.76); BP(2, 1.5, N.C2, 0.5, 0.6); BP(2, 2.5, N.G1, 1.2, 0.7); +hats8(2, 0.22); +// bar 3 — full preview + a snare fill spilling into the drop +KICK(3, 0, 0.92); KICK(3, 2, 0.78); +SNR(3, 1, 0.5); SNR(3, 3, 0.55); +BP(3, 0, N.C2, 1.4, 0.78); BP(3, 2.5, N.G1, 0.8, 0.7); +hats8(3, 0.24); +for (const fb of [3, 3.25, 3.5, 3.75]) SNR(3, fb, 0.35 + (fb - 3) * 0.5); // crescendo fill +spark(t(3, 3.5), N.G5, 0.5, 0.4); spark(t(3, 3.75), N.C6, 0.5, 0.45); // kalimba pickup + +// ── helper: a standard groove bar (abs bar) at an intensity level ── +// level 0 = light (kick 1&3, soft hats), 1 = full (snare 2&4, busy hats), +// 2 = driving (adds kick &-of-3 + open hat). +function groove(ab, level, kg = 0.9) { + KICK(ab, 0, kg); KICK(ab, 2, kg * 0.82); + if (level >= 2) KICK(ab, 3.5, kg * 0.6); + if (level >= 1) { SNR(ab, 1, 0.62); SNR(ab, 3, 0.66); } + hats8(ab, level >= 1 ? 0.26 : 0.16, level >= 2); + if (level >= 1) for (let e = 0; e < 4; e++) SHK(ab, e + 0.5, 0.14); +} + +// ── BUTTERFLY (abs 4–7): light, bouncy groove under the hook ── +for (let ab = 4; ab <= 7; ab++) { + groove(ab, 0); + BP(ab, 0, N.C2, 1.5, 0.76); BP(ab, 2, N.G1, 1.0, 0.7); BP(ab, 3, N.C2, 0.8, 0.62); +} + +// ── PALOFMINE (abs 8–11): full backbeat groove ── +const PAL_ROOT = [N.C2, N.F1, N.G1, N.C2]; // C F G C +for (let i = 0; i < 4; i++) { + const ab = 8 + i, r = PAL_ROOT[i]; + groove(ab, 1); + BP(ab, 0, r, 1.4, 0.78); BP(ab, 1.5, r, 0.5, 0.6); BP(ab, 2.5, r === N.C2 ? N.G1 : r, 1.2, 0.72); +} + +// ── MOMMYWOW (abs 12–15): HUSH — pull the kit back for the held wonder ── +for (let ab = 12; ab <= 15; ab++) { + KICK(ab, 0, 0.6); // soft heartbeat + BP(ab, 0, N.C2, 3.6, 0.6, { drive: 1.6, decay: 0.9 }); // long sustained sub + HAT(ab, 2, 0.18, { open: true }); // a single open-hat swell mid-bar + for (let e = 0; e < 4; e++) SHK(ab, e + 0.5, 0.1); +} +SNR(15, 3, 0.4); SNR(15, 3.5, 0.5); SNR(15, 3.75, 0.6); // fill back into slinky + +// ── SLINKY (abs 16–23): the bass-perc really drives — springy bounce ── +for (let i = 0; i < 8; i++) { + const ab = 16 + i; + groove(ab, i >= 4 ? 2 : 1); + // a wobbling, syncopated bass-perc line — the "slinky" spring in the low end + BP(ab, 0, N.C2, 0.9, 0.8); BP(ab, 1.5, N.C2, 0.5, 0.62); + BP(ab, 2, N.G1, 0.9, 0.74); BP(ab, 3.5, N.C2, 0.5, 0.6); +} + +// ── FLY (abs 24–31): biggest energy — driving four-on-floor-ish ── +const FLY_ROOT = [N.C2, N.C2, N.F1, N.F1, N.C2, N.A1, N.G1, N.C2]; +for (let i = 0; i < 8; i++) { + const ab = 24 + i, r = FLY_ROOT[i]; + groove(ab, 2, 0.96); + KICK(ab, 1, 0.55); // four-on-the-floor lift + BP(ab, 0, r, 1.4, 0.82); BP(ab, 2, r, 0.9, 0.74); BP(ab, 3, r, 0.8, 0.66); + if (i === 7) for (const fb of [3, 3.25, 3.5, 3.75]) SNR(ab, fb, 0.35 + (fb - 3) * 0.55); // crescendo fill into land +} + +// ── LAND (abs 32–35): wind down, resolve to C ── +for (let i = 0; i < 4; i++) { + const ab = 32 + i; + KICK(ab, 0, 0.85 - i * 0.12); + if (i < 2) { SNR(ab, 1, 0.5); SNR(ab, 3, 0.5); hats8(ab, 0.2); } + BP(ab, 0, N.C2, 2.2 - i * 0.4, 0.74 - i * 0.1); +} +BP(35, 0, N.C2, 3.0, 0.7, { decay: 1.1 }); // final low resolve + +// ── OUTRO (abs 36–39): reprise the butterfly hook over the groove ── +// One last cute statement of "but-ter-FLY", then a buttoned final hit. +function O(ab, beat, midi, d, g) { lead(t(ab, beat), midi, d, g ?? 1.0); } +for (let i = 0; i < 3; i++) { // 3 bars of groove + const ab = 36 + i; + groove(ab, i < 2 ? 1 : 2, 0.9); + BP(ab, 0, N.C2, 1.4, 0.78); BP(ab, 2, N.G1, 1.0, 0.7); BP(ab, 3, N.C2, 0.8, 0.62); +} +// the hook reprise (butterfly motif), bars 36–37 +O(36, 0, N.E5, 1); O(36, 1, N.G5, 1); O(36, 2, N.C6, 2); +O(37, 0, N.C6, 1); O(37, 1, N.E6, 1); O(37, 2, N.G6, 2); +spark(t(37, 3.5), N.C6, 0.5, 0.45); +// bar 38 — a rising mallet flourish + open hat into the button +for (let s = 0; s < 6; s++) lead(t(38, s * 0.5), [N.C5, N.E5, N.G5, N.C6, N.E6, N.G6][s], 0.5, 0.9); +HAT(38, 3.5, 0.4, { open: true }); +// bar 39 — the button: big resolved hit, then ring out +KICK(39, 0, 1.0); BP(39, 0, N.C2, 3.0, 0.85, { decay: 1.3 }); +SNR(39, 0, 0.5); +bell(t(39, 0), N.C6, 4, 0.5); bell(t(39, 0.06), N.E6, 4, 0.4); bell(t(39, 0.12), N.G6, 4, 0.35); +vib(t(39, 0), N.C4, 4, 0.4); vib(t(39, 0), N.E4, 4, 0.35); vib(t(39, 0), N.G4, 4, 0.3); +lead(t(39, 0), N.C6, 4, 0.85); + +// ── REVERSE SINE-BELL RISERS — at the breaks ────────────────────────── +// The signature transition: a glassy sine swell rising out of the texture +// and landing a bright bell on the downbeat of the next section. +// • the big one: through the mommy-wow hush → DROP into slinky (abs 16). +// 2 bars of swell starting ≈0:27, landing on the slinky downbeat. +// • a softer lift into the fly soar (abs 24). +// • a wash into the outro hook reprise (abs 36). +rise(16, 2 * BAR, [N.E5, N.G5, N.C6, N.E6], 0.52); // mommy-wow → slinky (the 0:27 break) +rise(24, 1.5 * BAR, [N.G5, N.C6, N.E6, N.G6], 0.40); // → fly soar +rise(36, 1.5 * BAR, [N.C6, N.E6, N.G6], 0.34); // → outro reprise +// reverse kicks — the low-end whoosh under the two big risers +riseKick(16, 1.4 * BAR, 0.7); // big slinky drop +riseKick(24, 1.0 * BAR, 0.5); // fly soar + +// ── SUPER SCRATCHES — DJ flavour through the wild sections ──────────── +SC(3, 2, "scribble", 0.5, 52); SC(3, 3, "chirp", 0.55, 50); // intro build into the drop +SC(16, 0, "transform", 0.72, 50); // land on the slinky drop +for (let ab = 16; ab <= 23; ab++) { // slinky: rhythmic scratch stabs + SC(ab, 3, ab % 2 ? "baby" : "scribble", 0.5, 48 + (ab % 3) * 4); + if (ab % 2 === 0) SC(ab, 1.5, "chirp", 0.44, 55); +} +SC(24, 0, "chirp", 0.6, 52); // fly entrance +for (let ab = 24; ab <= 31; ab++) // fly: transform/chirp accents + SC(ab, 2.5, ab % 2 ? "transform" : "chirp", 0.5, 47 + (ab % 4) * 3); +SC(31, 3, "scribble", 0.62, 50); // fill into the landing +SC(38, 0, "scribble", 0.55, 53); SC(38, 2, "chirp", 0.5, 50); // outro flourish -// ── GLOCKENSPIEL — high steel halo on mommywow + fly ────────────────── -function G(bar, b, midi, d, g) { bell(t(bar - 1, b), midi, d, g ?? 0.65); } -// mommywow: ring out the wow chord above the held G5 -G(9, 0, N.E6, 4, 0.4); -G(10, 2, N.G6, 2, 0.85); -G(11, 0, N.E6, 4, 0.4); -G(12, 0, N.G6, 1, 0.85); G(12, 3, N.C6, 1, 0.75); -// fly: shimmer trails above the soaring notes -G(22, 0, N.G6, 4, 0.6); -G(24, 0, N.G6, 4, 0.6); -G(26, 2, N.G6, 2, 0.6); -G(28, 0, N.G5, 2, 0.45); -// land: a small final chime ringing the home C -G(32, 0, N.C6, 4, 0.5); -G(32, 1, N.E6, 3, 0.4); +// ── SCRATCHY KITTEN SCREAMS — feral little shrieks at the wild bits ─── +SCREAM(16, 0, 88, 0.5, { bend: 8, rasp: 0.7, dur: 0.55 }); // ride the slinky drop +SCREAM(19, 3.5, 91, 0.42, { bend: 6, rasp: 0.65, dur: 0.4 }); +SCREAM(22, 2, 84, 0.4, { bend: 9, rasp: 0.6, dur: 0.45 }); +SCREAM(24, 0, 90, 0.5, { bend: 7, rasp: 0.72, dur: 0.5 }); // answer the fly soar +SCREAM(27, 3.5, 86, 0.44, { bend: 10, rasp: 0.7, dur: 0.42 }); +SCREAM(30, 2.5, 93, 0.46, { bend: 6, rasp: 0.68, dur: 0.38 }); // high screech near the peak +SCREAM(39, 0, 88, 0.5, { bend: 8, rasp: 0.75, dur: 0.6 }); // last cry on the button // ═══════════════════════════════════════════════════════════════════ // STEREO MIXDOWN @@ -218,24 +482,26 @@ // ═══════════════════════════════════════════════════════════════════ const outL = new Float32Array(ns); const outR = new Float32Array(ns); -function pan(p) { - const a = (p + 1) * Math.PI / 4; - return [Math.cos(a), Math.sin(a)]; -} +function pan(p) { const a = (p + 1) * Math.PI / 4; return [Math.cos(a), Math.sin(a)]; } function place(buf, p, gain) { const [lg, rg] = pan(p); - for (let i = 0; i < ns; i++) { - const s = buf[i] * gain; - outL[i] += s * lg; - outR[i] += s * rg; - } + for (let i = 0; i < ns; i++) { const s = buf[i] * gain; outL[i] += s * lg; outR[i] += s * rg; } } place(leadBuf, 0.00, 1.00); -place(bassBuf, 0.00, 0.92); -place(sparkBuf, 0.38, 0.85); -place(bellBuf, -0.32, 0.80); +place(bassBuf, 0.00, 0.80); // mallet bass eases back — the bass-perc now owns the low +place(vibBuf, -0.18, 0.70); +place(riseBuf, 0.00, 0.85); // reverse-bell risers, centred + wide +place(scrBuf, 0.22, 0.80); // scratches, slightly off-centre (the DJ hand) +place(screamBuf,-0.40, 0.62); // kitten screams, panned wide-left so they don't mask the lead +place(sparkBuf, 0.38, 0.82); +place(bellBuf, -0.32, 0.78); +place(kickBuf, 0.00, 1.00); +place(subBuf, 0.00, 0.98); // bass-perc — the groove backbone, centred +place(snareBuf, 0.05, 0.78); +place(hatBuf, 0.30, 0.55); +place(shakBuf, -0.28, 0.45); -// scrub non-finite + peak-normalise to -1.3 dBFS +// scrub non-finite + peak-normalise let nan = 0; for (let i = 0; i < ns; i++) { if (!Number.isFinite(outL[i])) { outL[i] = 0; nan++; } @@ -247,26 +513,20 @@ for (let i = 0; i < ns; i++) { const a = Math.abs(outL[i]); if (a > peak) peak = a; const b = Math.abs(outR[i]); if (b > peak) peak = b; } -if (peak > 0) { const nrm = 0.86 / peak; - for (let i = 0; i < ns; i++) { outL[i] *= nrm; outR[i] *= nrm; } } +if (peak > 0) { const nrm = 0.84 / peak; for (let i = 0; i < ns; i++) { outL[i] *= nrm; outR[i] *= nrm; } } // trim trailing silence + tiny fades let lastLoud = ns - 1; -while (lastLoud > 0 && - Math.abs(outL[lastLoud]) < 0.004 && - Math.abs(outR[lastLoud]) < 0.004) lastLoud--; +while (lastLoud > 0 && Math.abs(outL[lastLoud]) < 0.004 && Math.abs(outR[lastLoud]) < 0.004) lastLoud--; const trimN = Math.min(ns, lastLoud + Math.floor(0.6 * SR)); const fadeIn = Math.floor(0.004 * SR); -const fadeOut = Math.floor(1.2 * SR); -for (let i = 0; i < fadeIn && i < trimN; i++) { - const g = i / fadeIn; outL[i] *= g; outR[i] *= g; -} +const fadeOut = Math.floor(1.4 * SR); +for (let i = 0; i < fadeIn && i < trimN; i++) { const g = i / fadeIn; 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; + const idx = trimN - 1 - i, g = i / fadeOut; outL[idx] *= g; outR[idx] *= g; } -console.log(`→ flutterbap · ${TOTAL_BARS} bars · C major 4/4 @ ${BPM} BPM · ${(trimN / SR).toFixed(1)} s`); +console.log(`→ flutterbap · ${TOTAL_BARS} bars · C major 4/4 @ ${BPM} BPM · ${(trimN / SR).toFixed(1)} s · bass-perc kit · risers · scratches · kitten screams · swing=${SWING.toFixed(2)} · humanize=${HUMANIZE ? "on" : "off"}`); // ── write out ───────────────────────────────────────────────────────── function expandHome(p) { @@ -279,19 +539,20 @@ const outPath = expandHome(_argi("--out")) || resolve(HERE, "..", "out", "flutterbap.mp3"); mkdirSync(dirname(outPath), { recursive: true }); const rawPath = `${outPath}.f32.raw`; const b = Buffer.alloc(trimN * 2 * 4); -for (let i = 0; i < trimN; i++) { - b.writeFloatLE(outL[i], i * 8); - b.writeFloatLE(outR[i], i * 8 + 4); -} +for (let i = 0; i < trimN; i++) { b.writeFloatLE(outL[i], i * 8); b.writeFloatLE(outR[i], i * 8 + 4); } writeFileSync(rawPath, b); -// pop master chain — bright, light, daytime. No low-shelf lift (no kick -// to flatter), gentle glue comp, a touch of air on top, brickwall. +// pop master chain — now with real low end (kick + bass-perc): tighten and +// weight the sub, de-mud, keep the mallet presence + daytime air, glue + +// brickwall. const MASTER = [ - "highpass=f=35", - "acompressor=threshold=-20dB:ratio=2.6:attack=18:release=180:makeup=2.2:knee=6", - "treble=g=1.4:f=7500", - "alimiter=limit=0.96:attack=4:release=70", + "highpass=f=28", + "equalizer=f=60:t=q:w=0.9:g=2.2", // sub weight under the bass-perc + kick + "equalizer=f=240:t=q:w=1.1:g=-1.3", // de-mud + "equalizer=f=3000:t=q:w=1.5:g=0.8", // mallet presence + "treble=g=1.3:f=8000", // daytime air + "acompressor=threshold=-18dB:ratio=2.8:attack=14:release=170:makeup=2.4:knee=6", + "alimiter=limit=0.95:attack=4:release=70", ].join(","); const ff = spawnSync("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-f", "f32le", "-ar", String(SR), "-ac", "2", "-i", rawPath, @@ -304,7 +565,7 @@ // ── struct.json — section map for any future visualizer ─────────────── { const structTotalSec = trimN / SR; const struct = { - _comment: "Section map for flutterbap (the daytime butterfly hop). 4/4, 124 BPM → bar ≈ 1.935 s. C major. Derived from render-flutterbap.mjs SECTIONS.", + _comment: "Section map for flutterbap (the daytime butterfly hop, +bass-perc kit). 4/4, 124 BPM → bar ≈ 1.935 s. C major. Derived from render-flutterbap.mjs SECTIONS (absolute bar map; body offset by INTRO_BARS).", meter: 4, bpm: BPM, scale: "major", rootMidi: 60, totalSec: +structTotalSec.toFixed(6), prerollSec: 0, sections: SECTIONS.map((s) => ({ diff --git a/pop/marimba/flutterbap.np b/pop/marimba/flutterbap.np --- a/pop/marimba/flutterbap.np +++ b/pop/marimba/flutterbap.np @@ -9,17 +9,31 @@ # layered, not arranged as a single line. # # This is marimbaba's daytime sibling. Same lane, same whistlegraph # DNA, opposite vibe: bright C major, brisk 4/4 at 124 BPM, no haze. -# A 62-second pop romp instead of a nap. +# +# ── next stage of /pop (this build) ─────────────────────────────────── +# The original hop had no low end. This stage adds a sample-free +# synthesized rhythm section (../synths/perc.mjs) and complexifies the +# form: a 4-bar drum INTRO that builds the groove, a gritty bass-perc +# bassline that bounces under the whole track, a kick/snare/hat/shaker +# kit with per-section dynamics, a soft vibraphone counter-pad under the +# lead, and a 4-bar OUTRO that reprises the butterfly hook and buttons +# the song. ~62 s → ~78 s. # # Key: C major. 4/4, ~124 BPM (1 beat ≈ 0.484 s, 1 bar ≈ 1.94 s). -# Form: 32 bars / ~62 s. The score lives at flutterbap.np; the actual -# event scheduling + mix lives in bin/render-flutterbap.mjs. +# Form: 4 (intro) + 32 (hook/verse) + 4 (outro) = 40 bars / ~78 s. The +# score lives here; event scheduling + mix lives in +# bin/render-flutterbap.mjs. Mallet voices are written against the +# original 1-based body bar numbers (1–32 below) and shifted past the +# intro at render time. # # Voices: # xylophone — the singer; bright daytime syllabic lead -# bass — the bouncing ball (low C / G alternating, on 1 & 3) +# bass(marimba) — the bouncing ball (low C / G alternating, on 1 & 3) # kalimba — twinkles on phrase tails (cute, off-beat) # glockenspiel — high steel sparkle on the climaxes (mommy-wow, fly) +# vibraphone — soft sustained chord pad under the lead (new) +# bass-perc — gritty pitched sub; the groove's backbone (new) +# kick/snare/hat/shaker — synthesized kit, dynamic per section (new) # # ── unspoken lyric, syllable-by-syllable ────────────────────────────── # @@ -66,6 +80,20 @@ # bar 29: LAND (C6 G5 E5 C5) # bar 30: and LAND (D5 ─ C5) # bar 31: land-ing HERE (E5 D5 ─ C5) # bar 32: (here) (C4 held, low landing) +# +# ── rhythm section (the build-out) ──────────────────────────────────── +# Bass-perc lives low (C2 ≈ root, G1 ≈ fifth) and bounces; the kit fills +# in around it. Groove intensity by section: +# +# intro build: bar1 sub+shaker → +kick → +snare → fill into drop +# butterfly light: kick 1&3, soft hats, bass-perc bounce (no backbeat) +# palofmine full: + snare on 2&4, busy hats; bass walks C–F–G–C +# mommywow HUSH: kit pulled back — soft kick on 1, long sub, open-hat +# slinky drive: springy syncopated bass-perc, snare 2&4, busy hats +# fly peak: four-on-the-floor lift, driving bass-perc on roots +# land resolve: kit thins, bass-perc settles to a held low C +# outro reprise: groove + butterfly hook again, rising flourish, +# then a buttoned final hit (kick + sub + bell + vibe chord) # # Below: section markers for the renderer (sections by name). diff --git a/pop/marimba/synths/perc.mjs b/pop/marimba/synths/perc.mjs new file mode 100644 --- /dev/null +++ b/pop/marimba/synths/perc.mjs @@ -0,0 +1,530 @@ +#!/usr/bin/env node +// perc.mjs — a synthesized percussion + bass-perc kit for the marimba lane. +// +// The marimba lane is sample-free on purpose (see ../../SCORE.md and the +// long note at the top of marimba.mjs): we model from physics / first +// principles rather than reaching for a drum sample. A mallet ensemble +// like flutterbap has no low-end groove of its own, so this module adds +// the missing rhythm section as pure synthesis — a punchy kick, a gritty +// pitched "bass-perc" (the 808-flavoured low voice that is half bassline, +// half drum), a snappy snare, hats (closed + open) and a shaker. +// +// ── why these models ─────────────────────────────────────────────────── +// Each voice is the cheapest model that still reads as the real thing: +// +// • kick — a sine body whose pitch drops fast from a "click" pitch to +// a sub fundamental (the classic 808/909 pitch-envelope), softened +// through a tanh so it punches, plus a tiny broadband transient for +// the beater click. (Pitch-drop kick: every drum machine since the +// TR-808; see Gordon Reid, "Synth Secrets" pt.35, Sound on Sound.) +// • bass — a pitched sub voice with a short downward pitch glide and +// optional tanh drive. One knob (`drive`) walks it from a clean sine +// sub to a saturated, harmonically-rich bass that cuts through a mix +// on small speakers. It IS the bassline and the low percussion at +// once — hence "bass-perc". +// • snare — two low sines (the drum's tuned shell modes ~180/330 Hz) +// plus a high-passed noise burst (the snares). Classic additive snare. +// • hat — high-passed white noise with a fast (closed) or slow (open) +// exponential decay. Cheap but convincing for a bright daytime track. +// • shaker — band-passed noise with a soft (non-transient) attack — the +// "tss" of beads rather than a struck membrane. +// +// All DSP is pure float with deterministic per-event noise — no JS-only +// constructs — so every voice ports straight into C (fedac/native audio.c +// or a maytrax-style engine) with no math changes, exactly like +// marimba.mjs. +// +// ── library ────────────────────────────────────────────────────────── +// import { mixKick, mixBassPerc, mixSnare, mixHat, mixShaker } from "..."; +// mixKick({ startSec, gain }, out, { sampleRate }) +// mixBassPerc({ startSec, midi, durSec, gain, drive }, out, { sampleRate }) +// renderKick(ev, opts) -> Float32Array // bare DSP +// +// ── CLI demo ─────────────────────────────────────────────────────────── +// node pop/marimba/synths/perc.mjs # one bar of the kit +// node pop/marimba/synths/perc.mjs --out ~/kit.mp3 + +import { writeFileSync, mkdirSync, unlinkSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { homedir } from "node:os"; + +const DEFAULT_SAMPLE_RATE = 48_000; +const LN1000 = Math.log(1000); // T60 helper: alpha = ln(1000) / T60 + +function midiToFreq(midi) { return 440 * Math.pow(2, (midi - 69) / 12); } + +// Deterministic LCG noise, seeded per event so a given hit always sounds +// identical (reproducible renders). Pure float → ports to C verbatim. +function makeNoise(seed) { + let s = (seed >>> 0) || 1; + return () => { + s = (Math.imul(s, 1664525) + 1013904223) >>> 0; + return (s / 0xffffffff) * 2 - 1; + }; +} + +// ── KICK ───────────────────────────────────────────────────────────── +// A pitch-enveloped sine drum. The instantaneous frequency starts high +// (the beater "click" pitch) and decays exponentially to a sub +// fundamental; the body amplitude rings down over its own T60. A short +// noise transient adds the beater attack. tanh keeps the low end punchy +// rather than letting big sine excursions go flabby. +export function renderKick(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 1.0; + const fStart = ev.fStart ?? 150; // click pitch (Hz) + const fEnd = ev.fEnd ?? 48; // sub fundamental (Hz) + const pitchT = ev.pitchDecay ?? 0.055; // how fast the pitch falls (s) + const ampT60 = ev.ampDecay ?? 0.34; // body T60 (s) + const clickAmt = ev.click ?? 0.55; + const drive = ev.drive ?? 1.6; // tanh punch + if (gain === 0) return new Float32Array(0); + + const tail = ampT60 * 1.3 + 0.02; + const ns = Math.ceil(tail * SR); + const out = new Float32Array(ns); + const ampA = LN1000 / ampT60; + const noise = makeNoise(Math.floor((ev.startSec ?? 0) * 9173) | 1); + let phase = 0; + const dt = 1 / SR; + for (let i = 0; i < ns; i++) { + const t = i * dt; + const f = fEnd + (fStart - fEnd) * Math.exp(-t / pitchT); + phase += f * dt; + const body = Math.tanh(Math.sin(2 * Math.PI * phase) * drive) / Math.tanh(drive); + const ampEnv = Math.exp(-ampA * t); + let s = body * ampEnv; + // beater click — broadband, gone in ~4 ms + if (t < 0.006) s += noise() * clickAmt * Math.exp(-t / 0.0012); + out[i] = s * gain; + } + return out; +} + +// ── BASS-PERC ────────────────────────────────────────────────────────── +// The signature low voice: a pitched sub with a quick downward pitch glide +// at the onset (the "dwop") and a tanh drive that adds bite. Decays over +// `decay` (T60) so a sparse line rings like an 808, or set a short decay + +// fast notes for a plucky percussive bass. A tiny transient click gives it +// the percussive snap that ties it to the drums. This is what carries the +// groove under the mallets. +export function renderBassPerc(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + if (!Number.isFinite(ev.midi)) return new Float32Array(0); + const gain = Number.isFinite(ev.gain) ? ev.gain : 1.0; + if (gain === 0) return new Float32Array(0); + const f0 = midiToFreq(ev.midi); + const pitchUp = ev.pitchUp ?? 5; // semitones the onset glides down from + const pitchT = ev.pitchDecay ?? 0.022; + const decay = ev.decay ?? 0.5; // body T60 (s) + const drive = ev.drive ?? 2.2; // grit / saturation + const sub = ev.sub ?? 0.35; // amount of -1 octave reinforcement + const clickAmt = ev.click ?? 0.18; + const durSec = ev.durSec ?? decay; + + // ring until the body has decayed; release-fade if the note is gated short + const tail = Math.max(durSec, decay) * 1.25 + 0.03; + const ns = Math.ceil(tail * SR); + const out = new Float32Array(ns); + const ampA = LN1000 / decay; + const fHi = f0 * Math.pow(2, pitchUp / 12); + const noise = makeNoise((Math.floor((ev.startSec ?? 0) * 7919) + ev.midi * 53) | 1); + const relS = Math.floor(0.03 * SR); + const gateS = Math.floor(durSec * SR); + let phase = 0, phaseSub = 0; + const dt = 1 / SR; + const norm = Math.tanh(drive); + for (let i = 0; i < ns; i++) { + const t = i * dt; + const f = f0 + (fHi - f0) * Math.exp(-t / pitchT); + phase += f * dt; + phaseSub += (f * 0.5) * dt; + let body = Math.sin(2 * Math.PI * phase) + sub * Math.sin(2 * Math.PI * phaseSub); + body = Math.tanh(body * drive) / norm; + let env = Math.exp(-ampA * t); + // gate release so a short note doesn't ring into the next + if (i > gateS) { const r = (i - gateS) / relS; env *= r >= 1 ? 0 : 1 - r; } + let s = body * env; + if (t < 0.005) s += noise() * clickAmt * Math.exp(-t / 0.0015); + out[i] = s * gain; + if (env <= 0 && i > gateS) break; + } + return out; +} + +// ── SNARE ──────────────────────────────────────────────────────────── +// Tuned shell modes (two low sines) + a high-passed noise burst for the +// snares. toneAmt / noiseAmt balance the body against the rasp. +export function renderSnare(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 1.0; + if (gain === 0) return new Float32Array(0); + const toneT60 = ev.toneDecay ?? 0.10; + const noiseT60 = ev.noiseDecay ?? 0.16; + const toneAmt = ev.tone ?? 0.55; + const noiseAmt = ev.noise ?? 0.9; + const f1 = ev.f1 ?? 185, f2 = ev.f2 ?? 330; + const tail = Math.max(toneT60, noiseT60) * 1.4 + 0.01; + const ns = Math.ceil(tail * SR); + const out = new Float32Array(ns); + const aT = LN1000 / toneT60, aN = LN1000 / noiseT60; + const noise = makeNoise(Math.floor((ev.startSec ?? 0) * 6271) | 1); + let hp = 0, prevN = 0; + const dt = 1 / SR; + for (let i = 0; i < ns; i++) { + const t = i * dt; + const tone = (Math.sin(2 * Math.PI * f1 * t) * 0.6 + Math.sin(2 * Math.PI * f2 * t) * 0.4) + * Math.exp(-aT * t) * toneAmt; + const nRaw = noise(); + hp = 0.85 * (hp + nRaw - prevN); prevN = nRaw; // one-pole high-pass → bright snares + const nz = hp * Math.exp(-aN * t) * noiseAmt; + out[i] = (tone + nz) * gain; + } + return out; +} + +// ── HAT ────────────────────────────────────────────────────────────── +// High-passed white noise; fast decay for a closed hat, long for an open +// one (pass ev.open = true). Bright, simple, daytime. +export function renderHat(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 1.0; + if (gain === 0) return new Float32Array(0); + const t60 = ev.decay ?? (ev.open ? 0.26 : 0.045); + const tail = t60 * 1.5 + 0.005; + const ns = Math.ceil(tail * SR); + const out = new Float32Array(ns); + const a = LN1000 / t60; + const noise = makeNoise(Math.floor((ev.startSec ?? 0) * 5147) | 1); + let hp = 0, prevN = 0, hp2 = 0, prevH = 0; + const dt = 1 / SR; + for (let i = 0; i < ns; i++) { + const t = i * dt; + const nRaw = noise(); + hp = 0.92 * (hp + nRaw - prevN); prevN = nRaw; // two cascaded high-passes → + hp2 = 0.92 * (hp2 + hp - prevH); prevH = hp; // very bright, metallic-ish + out[i] = hp2 * Math.exp(-a * t) * gain; + } + return out; +} + +// ── SHAKER ────────────────────────────────────────────────────────── +// Band-passed noise with a soft raised-cosine attack — beads sliding, not +// a struck head. Sits on the off-beats to keep the groove moving. +export function renderShaker(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 1.0; + if (gain === 0) return new Float32Array(0); + const t60 = ev.decay ?? 0.06; + const attS = Math.max(1, Math.floor((ev.attack ?? 0.008) * SR)); + const tail = t60 * 1.6 + 0.01; + const ns = Math.ceil(tail * SR); + const out = new Float32Array(ns); + const a = LN1000 / t60; + const noise = makeNoise(Math.floor((ev.startSec ?? 0) * 4019) | 1); + // band-pass ≈ one-pole HP then one-pole LP, centred ~6 kHz + let hp = 0, prevN = 0, lp = 0; + const lpC = Math.min(1, (2 * Math.PI * 6500) / SR); + const dt = 1 / SR; + for (let i = 0; i < ns; i++) { + const t = i * dt; + const nRaw = noise(); + hp = 0.7 * (hp + nRaw - prevN); prevN = nRaw; + lp += lpC * (hp - lp); + let env = Math.exp(-a * t); + if (i < attS) env *= i / attS; + out[i] = lp * env * gain; + } + return out; +} + +// ── REVERSE SINE BELL (transition riser) ─────────────────────────────── +// Not percussion, but it ships with the kit because risers live with the +// drums in production. A glassy FM sine bell rendered forward (sharp attack +// + exponential decay) and then PLAYED BACKWARDS: the decay tail becomes a +// swell that rises out of nothing, and the bell's bright attack transient +// lands exactly on the downbeat. The modulator index is brightest at the +// original attack, so reversed the swell gets glassier as it rises — the +// classic reverse-cymbal riser, but tonal and pure sine (same family as +// the glasstrax bells). Pass a chord via `midis` (or a single `midi`), and +// `landSec` = the downbeat the transient should hit. +export function renderReverseBell(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 0.5; + const dur = ev.dur ?? 2.0; // swell length (s) + const decay = ev.decay ?? 2.6; // forward decay rate (↑ = quicker swell) + const ratio = ev.ratio ?? 3.5; // FM ratio — glassy bell timbre + const midis = ev.midis ?? (Number.isFinite(ev.midi) ? [ev.midi] : [84]); + const ns = Math.ceil(dur * SR); + const fwd = new Float32Array(ns); + const dt = 1 / SR; + // render the bell(s) forward — same FM voice as glasstrax's bell() + for (const m of midis) { + const fc = midiToFreq(m), fm = fc * ratio; + let phc = 0, phm = 0; + for (let i = 0; i < ns; i++) { + const tt = i * dt; + const env = Math.exp(-tt * decay); + const mi = 6.0 * Math.exp(-tt * 11.0); // bright attack transient + phm += 2 * Math.PI * fm * dt; + phc += 2 * Math.PI * fc * dt + Math.sin(phm) * mi * dt; + fwd[i] += Math.sin(phc) * env; + } + } + // reverse → swell, normalise by chord size + const out = new Float32Array(ns); + const norm = gain / Math.max(1, Math.sqrt(midis.length)); + for (let i = 0; i < ns; i++) out[i] = fwd[ns - 1 - i] * norm; + return out; +} + +// ── REVERSE KICK (low-end riser) ─────────────────────────────────────── +// The reverse bell's low-end companion: a kick rendered forward with a long +// boomy tail, then played BACKWARDS. Reversed, the pitch envelope sweeps +// UP (low → high) and the amplitude swells, so it whooshes upward and the +// beater transient lands on the downbeat (`landSec`). Sits under a reverse +// bell at a break for a big two-band (sub + glass) lift into the drop. +export function renderReverseKick(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 0.7; + const dur = ev.dur ?? 1.2; // swell length (s) + const fStart = ev.fStart ?? 190; // (forward) click pitch + const fEnd = ev.fEnd ?? 48; // (forward) sub fundamental + const pitchT = ev.pitchDecay ?? 0.13; + const ampT60 = ev.ampDecay ?? dur * 0.55; // boomy long tail → smooth swell + const drive = ev.drive ?? 1.6; + const ns = Math.ceil(dur * SR); + const fwd = new Float32Array(ns); + const ampA = LN1000 / ampT60; + const noise = makeNoise(Math.floor((ev.landSec ?? 0) * 8311) | 1); + const norm = Math.tanh(drive); + let phase = 0; + const dt = 1 / SR; + for (let i = 0; i < ns; i++) { + const t = i * dt; + const f = fEnd + (fStart - fEnd) * Math.exp(-t / pitchT); + phase += f * dt; + const body = Math.tanh(Math.sin(2 * Math.PI * phase) * drive) / norm; + let s = body * Math.exp(-ampA * t); + if (t < 0.006) s += noise() * 0.5 * Math.exp(-t / 0.0012); + fwd[i] = s; + } + const out = new Float32Array(ns); + for (let i = 0; i < ns; i++) out[i] = fwd[ns - 1 - i] * gain; + return out; +} +export function mixReverseKick(ev, out, opts = {}) { + if (!(out instanceof Float32Array)) return; + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const seg = renderReverseKick(ev, opts); + const land = ev.landSec ?? ((ev.startSec ?? 0) + (ev.dur ?? 1.2)); + const startIdx = Math.floor(land * SR) - seg.length; + for (let i = 0; i < seg.length; i++) { const d = startIdx + i; if (d >= 0 && d < out.length) out[d] += seg[i]; } +} + +// ── TURNTABLE SCRATCH ────────────────────────────────────────────────── +// A scratch built from scratch — no vinyl sampled, the record is synthetic. +// Step 1: synthesize a buzzy vocal-ish source (saw harmonics + surface grit, +// shaped by a resonant formant ≈ an "aah"). Step 2: "scrub" it — read the +// source back at a position that oscillates with the DJ's hand. The read +// VELOCITY is depth·sin(2π·rate·t): on the forward swing the tone pitches up, +// on the back swing it plays in REVERSE — that bending back-and-forth is the +// scratch. A crossfader gate chops it on/off for transform / chirp styles. +// rate — hand back-and-forth speed (Hz): ~5 baby, ~14 scribble +// depth — how hard the hand throws it (read-speed swing → pitch range) +// gate — crossfader chop rate (Hz); 0 = open fader (baby scratch) +export function renderScratch(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 0.6; + const dur = ev.dur ?? 0.32; + const midi = ev.midi ?? 50; + const rate = ev.rate ?? 7; + const depth = ev.depth ?? 2.6; + const gateHz = ev.gate ?? 0; + const gateDuty = ev.gateDuty ?? 0.5; + const tone = ev.tone ?? 0.55; // source brightness / buzz + const f0 = midiToFreq(midi); + const dt = 1 / SR; + const noise = makeNoise((Math.floor((ev.startSec ?? 0) * 3301) + midi * 17) | 1); + + // ── source: a rich buzzy "aah", long enough to scrub within ── + const srcLen = Math.ceil(0.4 * SR); + const src = new Float32Array(srcLen); + const nh = Math.max(4, Math.floor(8 + tone * 22)); // # harmonics (saw-ish) + let bp = 0, lp = 0; + const fc = 900, k = 2 * Math.sin(Math.PI * fc / SR), damp = 0.22; // formant ≈ vowel + for (let i = 0; i < srcLen; i++) { + const tt = i * dt; + let s = 0; + for (let h = 1; h <= nh; h++) s += Math.sin(2 * Math.PI * f0 * h * tt) / h; + s = s * 0.5 + noise() * 0.18 * tone; + const hi = s - lp - damp * bp; bp += k * hi; lp += k * bp; // resonant formant + src[i] = s * 0.5 + bp * 0.9; + } + let sp = 0; for (let i = 0; i < srcLen; i++) { const a = Math.abs(src[i]); if (a > sp) sp = a; } + if (sp > 0) { const n = 0.9 / sp; for (let i = 0; i < srcLen; i++) src[i] *= n; } + + // ── scrub: read src at the oscillating hand position ── + const ns = Math.ceil(dur * SR); + const out = new Float32Array(ns); + const center = srcLen * 0.5; + const amp = depth * SR / (2 * Math.PI * rate); // position swing (samples) + const attS = Math.floor(0.004 * SR), relS = Math.floor(0.02 * SR); + let gSmooth = 1; + for (let i = 0; i < ns; i++) { + const tt = i * dt; + // pos = center − amp·cos → velocity = depth·sin (starts at rest, like a hand) + let pos = center - amp * Math.cos(2 * Math.PI * rate * tt); + if (pos < 0) pos = 0; else if (pos >= srcLen - 1) pos = srcLen - 2; + const i0 = Math.floor(pos), fr = pos - i0; + let smp = src[i0] * (1 - fr) + src[i0 + 1] * fr; + if (gateHz > 0) { // crossfader chop (smoothed ~1ms) + const ph = (tt * gateHz) % 1; + gSmooth += ((ph < gateDuty ? 1 : 0) - gSmooth) * 0.02; + smp *= gSmooth; + } + let env = 1; + if (i < attS) env = i / attS; + else if (i > ns - relS) env = Math.max(0, (ns - i) / relS); + out[i] = smp * env * gain; + } + return out; +} +export function mixScratch(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderScratch(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} + +// ── KITTEN SCREAM ────────────────────────────────────────────────────── +// A scratchy little shriek. A high glottal saw source sweeps up then falls +// (the "myaaa!" cry contour) with fast vibrato; two vocal-tract formants +// give it a vowel; and the SCREAM comes from going nonlinear like a real +// strained throat — a subharmonic an octave down flutters in, pitch +// roughness jitters the fundamental, and a tanh stage drives the whole +// thing into rasp. `rasp` (0..1) sets how feral. Pitch it high (midi ~81–91) +// for a kitten; lower for a bigger, angrier cat. +export function renderScream(ev, opts = {}) { + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const gain = Number.isFinite(ev.gain) ? ev.gain : 0.5; + const dur = ev.dur ?? 0.5; + const midi = ev.midi ?? 86; // base cry pitch (high) + const bend = ev.bend ?? 7; // semitone rise of the cry + const rasp = Math.max(0, Math.min(1, ev.rasp ?? 0.65)); + const f0 = midiToFreq(midi); + const ns = Math.ceil(dur * SR); + const out = new Float32Array(ns); + const dt = 1 / SR; + const noise = makeNoise((Math.floor((ev.startSec ?? 0) * 2719) + midi * 29) | 1); + // two vocal-tract formants + let bp1 = 0, lp1 = 0, bp2 = 0, lp2 = 0; + const k1 = 2 * Math.sin(Math.PI * 950 / SR), k2 = 2 * Math.sin(Math.PI * 2500 / SR), dmp = 0.16; + let ph = 0, phSub = 0, roughLP = 0; + const drive = 1 + rasp * 3; + const norm = Math.tanh(drive); + for (let i = 0; i < ns; i++) { + const tt = i * dt, u = tt / dur; + // cry contour: pitch rises fast over the first third, then sags + const rise = Math.pow(2, (bend / 12) * Math.sin(Math.PI * Math.min(1, u * 1.35))); + const vibrato = 1 + 0.035 * Math.sin(2 * Math.PI * 17 * tt); + roughLP += (noise() - roughLP) * 0.4; // smoothed pitch roughness + const f = f0 * rise * vibrato * (1 + rasp * 0.05 * roughLP); + ph += f * dt; phSub += f * 0.5 * dt; + // glottal saw source (band-limited to Nyquist) + subharmonic strain + let s = 0; const nh = Math.min(24, Math.floor(SR / 2 / f)); + for (let h = 1; h <= nh; h++) s += Math.sin(2 * Math.PI * ph * h) / h; + s = s * 0.5 + rasp * 0.45 * Math.sin(2 * Math.PI * phSub) * Math.min(1, u * 4); // strain fades in + s += noise() * rasp * 0.22; // breath / hiss + // formants + const hi1 = s - lp1 - dmp * bp1; bp1 += k1 * hi1; lp1 += k1 * bp1; + const hi2 = s - lp2 - dmp * bp2; bp2 += k2 * hi2; lp2 += k2 * bp2; + let v = s * 0.35 + bp1 * 0.8 + bp2 * 0.5; + v = Math.tanh(v * drive) / norm; // scream into nonlinearity + // amplitude cry envelope: fast in, swell at the rise, fall away + let env = Math.min(1, u * 12) * (0.55 + 0.45 * Math.sin(Math.PI * Math.min(1, u * 1.1))); + if (u > 0.85) env *= Math.max(0, (1 - u) / 0.15); + out[i] = v * env * gain; + } + return out; +} +export function mixScream(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderScream(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} + +// ── node-side buffer mixers ──────────────────────────────────────────── +function mixInto(seg, ev, out, SR) { + const startIdx = Math.floor((ev.startSec ?? 0) * SR); + for (let i = 0; i < seg.length; i++) { + const dst = startIdx + i; + if (dst < 0 || dst >= out.length) continue; + out[dst] += seg[i]; + } +} +export function mixKick(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderKick(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} +export function mixBassPerc(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderBassPerc(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} +export function mixSnare(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderSnare(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} +export function mixHat(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderHat(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} +export function mixShaker(ev, out, opts = {}) { + if (out instanceof Float32Array) mixInto(renderShaker(ev, opts), ev, out, opts.sampleRate ?? DEFAULT_SAMPLE_RATE); +} +// reverse bell lands its transient on ev.landSec — the swell occupies the +// `dur` seconds BEFORE that downbeat. +export function mixReverseBell(ev, out, opts = {}) { + if (!(out instanceof Float32Array)) return; + const SR = opts.sampleRate ?? DEFAULT_SAMPLE_RATE; + const seg = renderReverseBell(ev, opts); + const land = ev.landSec ?? ((ev.startSec ?? 0) + (ev.dur ?? 2.0)); + const startIdx = Math.floor(land * SR) - seg.length; + for (let i = 0; i < seg.length; i++) { const d = startIdx + i; if (d >= 0 && d < out.length) out[d] += seg[i]; } +} + +// ── CLI demo — one bar of the full kit at 120 BPM ────────────────────── +const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]); +if (isMain) { + const argv = process.argv.slice(2); + const flags = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--")) { const k = a.slice(2), n = argv[i + 1]; if (n && !n.startsWith("--")) { flags[k] = n; i++; } else flags[k] = true; } + } + const expand = (p) => (p && p.startsWith("~/") ? resolve(homedir(), p.slice(2)) : p); + const HERE = dirname(fileURLToPath(import.meta.url)); + const SR = 48_000; + const BPM = 120, BEAT = 60 / BPM; + const bars = 4, total = bars * 4 * BEAT + 1.0; + const out = new Float32Array(Math.ceil(total * SR)); + for (let bar = 0; bar < bars; bar++) { + const b0 = bar * 4 * BEAT; + // four-on-the-floor-ish groove with the bass-perc walking C–G + mixKick({ startSec: b0 + 0 * BEAT, gain: 0.95 }, out, { sampleRate: SR }); + mixKick({ startSec: b0 + 2 * BEAT, gain: 0.85 }, out, { sampleRate: SR }); + mixSnare({ startSec: b0 + 1 * BEAT, gain: 0.7 }, out, { sampleRate: SR }); + mixSnare({ startSec: b0 + 3 * BEAT, gain: 0.7 }, out, { sampleRate: SR }); + for (let e = 0; e < 8; e++) + mixHat({ startSec: b0 + e * 0.5 * BEAT, gain: e % 2 ? 0.32 : 0.18, open: e === 7 }, out, { sampleRate: SR }); + mixBassPerc({ startSec: b0 + 0 * BEAT, midi: 36, durSec: BEAT * 1.5, gain: 0.8 }, out, { sampleRate: SR }); + mixBassPerc({ startSec: b0 + 2.5 * BEAT, midi: 43, durSec: BEAT * 1.0, gain: 0.7 }, out, { sampleRate: SR }); + } + let peak = 0; for (let i = 0; i < out.length; i++) { const a = Math.abs(out[i]); if (a > peak) peak = a; } + if (peak > 0) { const n = 0.9 / peak; for (let i = 0; i < out.length; i++) out[i] *= n; } + const outPath = expand(flags.out) || resolve(HERE, "..", "out", "perc-demo.mp3"); + mkdirSync(dirname(outPath), { recursive: true }); + const raw = `${outPath}.f32.raw`; + const buf = Buffer.alloc(out.length * 4); + for (let i = 0; i < out.length; i++) buf.writeFloatLE(out[i], i * 4); + writeFileSync(raw, buf); + const ff = spawnSync("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-f", "f32le", "-ar", String(SR), "-ac", "1", "-i", raw, + "-c:a", "libmp3lame", "-q:a", "3", outPath], { stdio: "inherit" }); + try { unlinkSync(raw); } catch {} + if (ff.status !== 0) { console.error("✗ ffmpeg failed"); process.exit(1); } + console.log(`✓ ${outPath}`); +} diff --git a/pop/maytrax/c/pianotrax b/pop/maytrax/c/pianotrax new file mode 100644 --- /dev/null +++ b/pop/maytrax/c/pianotrax diff --git a/pop/maytrax/c/pianotrax.c b/pop/maytrax/c/pianotrax.c new file mode 100644 --- /dev/null +++ b/pop/maytrax/c/pianotrax.c @@ -0,0 +1,534 @@ +// pianotrax.c — glasstrax, arranged for a REAL PIANO. Same song (A major, 124 +// BPM, the airy I–IV–vi–V loop, the same rising-then-cresting LEAD motif, the +// same 8-section form) but two big changes: TIMBRE and PLAYABILITY. +// +// TIMBRE: a REAL SAMPLED GRAND — the exact same piano AC OS plays. The audio +// engine in fedac/native uses the Salamander Grand Piano V3 (CC0 by Alexander +// Holm), decimated to 26 anchor samples every 3 semitones (raw f32 mono 48 kHz, +// 3 s each) in fedac/native/samples/piano/.raw. We load that same bank and +// voice every note the same way audio.c does: pick the nearest anchor, pitch- +// shift it by resampling (step = 2^((note−anchor)/12)) with linear interpolation, +// and apply an amp + damper-release envelope. So pianotrax sounds like the OS. +// +// PLAYABILITY: glasstrax's unplayable layers (random droplet mist, 8-note +// 3-octave shimmer sweeps, the busy independent 16th arp bed) are gone. In +// their place: a left-hand bass + broken-chord/Alberti accompaniment and a +// right-hand melody (the LEAD) with tasteful pentatonic eighths, rolled chords +// and grace notes. ≤ ~10 notes sounding at once, reaches ≤ an octave per hand. +// A natural room reverb (drier than glasstrax's crystalline hall). +// +// Samples are read from fedac/native/samples/piano (override with --piano-dir +// or $AC_PIANO_DIR). render-c.mjs --engine pianotrax passes the path and +// masters on the sine-family chain. +// +// Build: cc -O3 -std=c11 -o pianotrax pianotrax.c -lm +// Run: ./pianotrax --out out/pianotrax-raw.wav --piano-dir fedac/native/samples/piano + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif +#define TAU (2.0 * M_PI) + +static const int SR = 48000; +static double BPMV = 124, BEAT, BAR, SX; +static double PIANO_GAIN = 1.35; // overall press level (extra volume, vs the beat) +static double PIANO_RELEASE = 0.42; // damper release in seconds (sustain / pedal bloom) + +// ── swing / sway ──────────────────────────────────────────────────────────── +// A gentle pianist's swing (0.56) — the "&"s lean just slightly, the way a +// player phrases broken-chord eighths. 16ths interpolate inside the swung 8th. +static double SWING = 0.56; +static double swing_beats(double beats) { + double e8 = beats * 2.0; // position in 8ths + long idx = (long)floor(e8 + 1e-9); + double frac = e8 - idx; + double pos = (idx / 2) * BEAT + ((idx & 1) ? SWING * BEAT : 0.0); + return pos + frac * (BEAT / 2.0); // interpolate 16ths inside the 8th +} + +static long N; +static float *busL, *busR, *revL, *revR; +static inline void adds(float *L, float *R, long i, double l, double r) { if (i >= 0 && i < N) { L[i] += (float)l; R[i] += (float)r; } } + +static int write_wav_f32_stereo(const char *path, const float *L, const float *R, long n) { + FILE *f = fopen(path, "wb"); if (!f) return 0; + uint32_t dsz = (uint32_t)(n * 8), riff = 36 + dsz, sr = SR, br = SR * 8, fsz = 16; + uint16_t fmt = 3, ch = 2, ba = 8, bits = 32; + fwrite("RIFF", 1, 4, f); fwrite(&riff, 4, 1, f); fwrite("WAVE", 1, 4, f); + fwrite("fmt ", 1, 4, f); fwrite(&fsz, 4, 1, f); + fwrite(&fmt, 2, 1, f); fwrite(&ch, 2, 1, f); fwrite(&sr, 4, 1, f); + fwrite(&br, 4, 1, f); fwrite(&ba, 2, 1, f); fwrite(&bits, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&dsz, 4, 1, f); + for (long i = 0; i < n; i++) { fwrite(&L[i], 4, 1, f); fwrite(&R[i], 4, 1, f); } + fclose(f); return 1; +} + +// ── Salamander Grand Piano sample bank (the AC OS piano) ────────────────────── +// Anchor samples are raw f32 mono 48 kHz, named ".raw" (MIDI 21..96 every +// 3 semitones). Same on-disk format + filename convention as fedac/native +// /samples/piano, loaded the same way audio.c's load_piano_bank() does. +#define PIANO_ANCHOR_MAX 64 +static float *pn_data[PIANO_ANCHOR_MAX]; +static long pn_len[PIANO_ANCHOR_MAX]; +static int pn_midi[PIANO_ANCHOR_MAX]; +static int pn_count = 0; +static const char *PIANO_DIR = NULL; // set from --piano-dir / $AC_PIANO_DIR + +// Probe candidate dirs (CLI/env first, then paths relative to repo root and to +// pop/maytrax/c) by looking for the middle-C anchor 60.raw. Returns NULL if none. +static const char *find_piano_dir(void) { + const char *cands[] = { + PIANO_DIR, getenv("AC_PIANO_DIR"), + "fedac/native/samples/piano", + "../../../fedac/native/samples/piano", + "../../fedac/native/samples/piano", + }; + for (int i = 0; i < (int)(sizeof cands / sizeof *cands); i++) { + if (!cands[i]) continue; + char probe[1024]; snprintf(probe, sizeof probe, "%s/60.raw", cands[i]); + FILE *f = fopen(probe, "rb"); + if (f) { fclose(f); return cands[i]; } + } + return NULL; +} + +// Load the 26 anchors (MIDI 21,24,…,96) from `dir` into the bank. +static int load_piano_bank(const char *dir) { + pn_count = 0; + for (int m = 21; m <= 96 && pn_count < PIANO_ANCHOR_MAX; m += 3) { + char path[1024]; snprintf(path, sizeof path, "%s/%d.raw", dir, m); + FILE *f = fopen(path, "rb"); if (!f) continue; + fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f); + if (sz <= 0 || sz % (long)sizeof(float) != 0) { fclose(f); continue; } + long cnt = sz / (long)sizeof(float); + float *b = malloc((size_t)sz); + if (!b) { fclose(f); continue; } + if (fread(b, 1, (size_t)sz, f) != (size_t)sz) { free(b); fclose(f); continue; } + fclose(f); + pn_data[pn_count] = b; pn_len[pn_count] = cnt; pn_midi[pn_count] = m; pn_count++; + } + return pn_count; +} + +// Nearest anchor to a target MIDI note (same as audio.c's pick_piano_anchor). +static int pick_anchor(double note) { + int best = 0; double bd = 1e18; + for (int i = 0; i < pn_count; i++) { double d = fabs(note - pn_midi[i]); if (d < bd) { bd = d; best = i; } } + return best; +} + +// ── the piano voice — sampled grand ─────────────────────────────────────────── +// Pick the nearest Salamander anchor, pitch-shift it by reading at a fractional +// step (2^((note−anchor)/12)) with linear interpolation, and shape it with a +// tiny click-suppressing attack + a damper RELEASE after `dur` (so staccato +// notes damp and held/pedalled notes ring out their natural 3 s decay). The +// keyboard pan (low→left, high→right) is blended with the caller's pan, exactly +// as the synth voice did. +// +// note MIDI pitch dur sounding length (sec) before the damper falls +// g velocity/level pan stereo placement vel mild loudness tilt +static void piano(float *L, float *R, double note, double t, double dur, double g, double pan, double vel) { + if (pn_count == 0 || dur <= 0) return; + int a = pick_anchor(note); + const float *src = pn_data[a]; long slen = pn_len[a]; + double step = pow(2.0, (note - (double)pn_midi[a]) / 12.0); // resample ratio + + double kbpan = (note - 60.0) / 40.0; if (kbpan > 1) kbpan = 1; if (kbpan < -1) kbpan = -1; + double P = pan * 0.5 + kbpan * 0.35; + double lg = 1 - P * 0.5, rg = 1 + P * 0.5; + // wider velocity → loudness map: soft notes sit back, accents punch — good + // dynamic range, and a hotter overall level so the presses carry. + double amp = g * (0.70 + 0.85 * vel) * PIANO_GAIN; + + long s0 = (long)(t * SR); + long att = (long)(0.0015 * SR); if (att < 1) att = 1; // click-suppress onset + long noteN = (long)(dur * SR); // key held this long + // SUSTAIN: a long damper release so presses ring and bloom into each other + // (the pedal-down, hazy wash the track wants) instead of clipping dry at dur. + long relN = (long)(PIANO_RELEASE * SR); if (relN < 1) relN = 1; + + double pos = 0.0; + for (long i = 0; ; i++) { + long idx = (long)pos; + if (idx >= slen - 1) break; // sample exhausted (natural end) + double frac = pos - (double)idx; + double s = (1.0 - frac) * (double)src[idx] + frac * (double)src[idx + 1]; + double env = 1.0; + if (i < att) env = (double)i / att; + if (i >= noteN) { long r = i - noteN; if (r >= relN) break; env *= 1.0 - (double)r / relN; } + double v = s * amp * env; + adds(L, R, s0 + i, v * lg, v * rg); + pos += step; + } +} + +// ── humanize — a hazy, lazy hand ────────────────────────────────────────────── +// A real pianist drags a hair behind the beat and never strikes two keys at the +// same velocity. LAZY pushes every press slightly LATE (behind the dead-on-grid +// kick → the laid-back feel); HUM_MS jitters the timing; HUM_VEL wobbles the +// loudness. Chord notes humanize independently → a natural micro-roll/spread. +// Deterministic xorshift so renders stay identical run-to-run. +static double LAZY = 0.016; // seconds behind the beat (the drag) +static double HUM_MS = 12.0; // ± timing jitter (ms) +static double HUM_VEL = 0.17; // ± velocity wobble (fraction) +static uint32_t hrs = 0x515A4953; // humanize rng state +static inline double hr2(void) { hrs ^= hrs << 13; hrs ^= hrs >> 17; hrs ^= hrs << 5; return ((double)hrs / 4294967296.0) * 2.0 - 1.0; } + +// piano note that also feeds a little into the room reverb (for sustained / lead +// notes we want a touch of tail). `wet` is the reverb-send level. The hazy-lazy +// humanize is applied HERE so every press inherits it (the kick/scratch grid +// stays tight, so the piano drags against it). +static void pkey(double note, double t, double dur, double g, double pan, double vel, double wet) { + double tt = t + LAZY + hr2() * HUM_MS / 1000.0; if (tt < 0) tt = 0; + double gg = g * (1.0 + hr2() * HUM_VEL); if (gg < 0) gg = 0; + piano(busL, busR, note, tt, dur, gg, pan, vel); + if (wet > 0) piano(revL, revR, note, tt, dur, gg * wet, 0, vel); +} + +// a rolled (arpeggiated) chord — notes struck in quick succession bottom-up, +// the way a pianist rolls a wide voicing the hand can't grab flat. ~18 ms apart. +static void roll(const int *midis, int nm, double t, double g, double vel, double wet) { + for (int k = 0; k < nm; k++) pkey(midis[k], t + 0.018 * k, BEAT * 3.0, g * (0.92 + 0.08 * k / nm), 0.0, vel, wet); +} + +// a block chord — struck together (a small hand-grab, ≤ an octave span). +static void block(const int *midis, int nm, double t, double dur, double g, double vel, double wet) { + for (int k = 0; k < nm; k++) pkey(midis[k], t + 0.002 * k, dur, g, 0.0, vel, wet); +} + +// ── harmony — A major, the airy I–IV–vi–V loop (same as glasstrax) ──────────── +// per-bar chords A, D, F#m, E — the same loop glasstrax walks (its A_ROOTS were +// A D F# E); the left-hand chord-tone sets below carry those roots in the bass. +// Left-hand chord tones (root-position triads, low register). The LH plays +// broken-chord / Alberti patterns over these — root, 5th, octave, 3rd — all +// within an octave so one hand reaches them comfortably. +// root 5th oct 3rd +static const int LH_A[4] = { 45, 52, 57, 49 }; // A2 E3 A3 C#3 +static const int LH_D[4] = { 38, 45, 50, 42 }; // D2 A2 D3 F#2 +static const int LH_FM[4] = { 42, 49, 54, 45 }; // F#2 C#3 F#3 A2 +static const int LH_E[4] = { 40, 47, 52, 44 }; // E2 B2 E3 G#2 +static const int *LH[4] = { LH_A, LH_D, LH_FM, LH_E }; + +// Right-hand block voicings (close, ≤ an octave, mid register) for sustained +// chords / hush passages — the kind a relaxed right hand holds. +static const int RH_A[3] = { 61, 64, 69 }; // C#4 E4 A4 +static const int RH_D[3] = { 57, 62, 66 }; // A3 D4 F#4 +static const int RH_FM[3] = { 57, 61, 66 }; // A3 C#4 F#4 +static const int RH_E[3] = { 56, 59, 64 }; // G#3 B3 E4 +static const int *RH[4] = { RH_A, RH_D, RH_FM, RH_E }; + +// A major pentatonic (A B C# E F#) for right-hand runs/ornaments — pianistic, +// the hand stays in one position. +static const int PENT[5] = { 69, 71, 73, 76, 78 }; // A4 B4 C#5 E5 F#5 + +// section map — IDENTICAL to glasstrax so the form/length match exactly. At 124 +// BPM a bar is ~1.935s, so this 56-bar form lands ~114s. +static const char *ORDER[8] = { "intro", "bloom", "waveA", "hushA", "waveB", "hushB", "waveC", "outro" }; +static const int SECBARS[8] = { 4, 4, 8, 4, 8, 4, 12, 12 }; +static int START[8]; +static int sec_index(const char *k) { for (int i = 0; i < 8; i++) if (!strcmp(ORDER[i], k)) return i; return 0; } + +// ── left-hand patterns ──────────────────────────────────────────────────────── +// Alberti-ish broken chord across one bar: low–high–mid–high in eighths, the +// classic supporting figure a left hand runs without thinking. Uses the 4 LH +// chord tones; pattern indices keep the bass on the down-beats. +static void lh_broken(double t, const int *ch, double g, double vel) { + // eighths over a 4/4 bar (8 hits): root, 5th, oct, 5th | 3rd, 5th, oct, 5th + static const int pat[8] = { 0, 1, 2, 1, 3, 1, 2, 1 }; + for (int e = 0; e < 8; e++) { + double tt = t + swing_beats(e * 0.5); + double gg = (e % 2 == 0) ? g : g * 0.7; // on-beats fuller + pkey(ch[pat[e]], tt, BEAT * 0.85, gg, -0.10, vel * (e % 2 == 0 ? 1.0 : 0.8), 0.12); + } +} + +// simpler half-time LH for spacious sections: a low root on 1, an open 5th/oct +// on beat 3 — lets the bar breathe. +static void lh_half(double t, const int *ch, double g, double vel) { + pkey(ch[0], t, BEAT * 2.0, g, -0.12, vel, 0.18); + int two[2] = { ch[1], ch[2] }; + block(two, 2, t + BEAT * 2, BEAT * 2.0, g * 0.85, vel * 0.85, 0.18); +} + +// ── turntable scratch (synthesized; ported from pop/marimba/synths/perc.mjs) ── +// No vinyl sampled — the record is synthetic. Build a buzzy vowel source (saw +// harmonics + grit through a resonant formant), then "scrub" it: read it back at +// a position that oscillates with the DJ's hand. The read velocity is depth·sin +// (2π·rate·t) — on the back-swing the tone plays in REVERSE; that bend is the +// scratch. A crossfader gate (gateHz>0) chops it for transform/chirp styles. +// rate ~5 baby · ~14 scribble depth = throw → pitch range gateHz = chop +static void scratch(double t, double dur, double smidi, double rate, double depth, + double gateHz, double gateDuty, double tone, double g, double pan, double wet) { + long s0 = (long)(t * SR), ns = (long)(dur * SR); + if (ns <= 0) return; + long srcLen = (long)(0.4 * SR); + float *src = malloc((size_t)srcLen * sizeof(float)); + if (!src) return; + double f0 = 440.0 * pow(2.0, (smidi - 69.0) / 12.0); + int nh = (int)(8 + tone * 22); if (nh < 4) nh = 4; + double bp = 0, lp = 0, fc = 900.0, k = 2.0 * sin(M_PI * fc / SR), damp = 0.22; + uint32_t r = (uint32_t)(s0 * 2654435761u) | 1u; // per-event noise + for (long i = 0; i < srcLen; i++) { + double tt = (double)i / SR, s = 0; + for (int h = 1; h <= nh; h++) s += sin(TAU * f0 * h * tt) / h; + r ^= r << 13; r ^= r >> 17; r ^= r << 5; + s = s * 0.5 + (((double)r / 4294967296.0) * 2.0 - 1.0) * 0.18 * tone; + double hi = s - lp - damp * bp; bp += k * hi; lp += k * bp; // resonant formant + src[i] = (float)(s * 0.5 + bp * 0.9); + } + double sp = 0; for (long i = 0; i < srcLen; i++) { double a = fabs(src[i]); if (a > sp) sp = a; } + if (sp > 0) { double n = 0.9 / sp; for (long i = 0; i < srcLen; i++) src[i] *= (float)n; } + + double center = srcLen * 0.5, amp = depth * SR / (TAU * rate); + long att = (long)(0.004 * SR), rel = (long)(0.02 * SR); + double lgp = 1 - pan * 0.5, rgp = 1 + pan * 0.5, gSmooth = 1.0; + for (long i = 0; i < ns; i++) { + double tt = (double)i / SR; + double pos = center - amp * cos(TAU * rate * tt); + if (pos < 0) pos = 0; else if (pos >= srcLen - 1) pos = srcLen - 2; + long i0 = (long)pos; double fr = pos - i0; + double smp = (1.0 - fr) * src[i0] + fr * src[i0 + 1]; + if (gateHz > 0) { // crossfader chop (~1 ms smoothed) + double ph = fmod(tt * gateHz, 1.0); + gSmooth += ((ph < gateDuty ? 1.0 : 0.0) - gSmooth) * 0.02; + smp *= gSmooth; + } + double env = 1.0; + if (i < att) env = (double)i / att; + else if (i > ns - rel) env = fmax(0.0, (double)(ns - i) / rel); + double v = smp * env * g; + adds(busL, busR, s0 + i, v * lgp, v * rgp); + if (wet > 0) adds(revL, revR, s0 + i, v * wet, v * wet); + } + free(src); +} + +// ── kick drum (synthesized; ported from pop/marimba/synths/perc.mjs) ────────── +// Pitch-enveloped sine body (click pitch → sub fundamental) driven through a +// tanh for punch, plus a short broadband beater click. Centered + dry. Sits +// dead on the swung grid so the hazy-lazy piano drags behind it. +static void kick(double t, double g) { + long s0 = (long)(t * SR); + double fStart = 150, fEnd = 48, pitchT = 0.055, ampT60 = 0.34, drive = 1.6, click = 0.55; + double ampA = log(1000.0) / ampT60, nrm = tanh(drive); + long tail = (long)((ampT60 * 1.3 + 0.02) * SR); + uint32_t r = (uint32_t)(s0 * 2246822519u) | 1u; + double phase = 0; + for (long i = 0; i < tail; i++) { + double tt = (double)i / SR; + double f = fEnd + (fStart - fEnd) * exp(-tt / pitchT); + phase += f / SR; + double body = tanh(sin(TAU * phase) * drive) / nrm; + double s = body * exp(-ampA * tt); + if (tt < 0.006) { r ^= r << 13; r ^= r >> 17; r ^= r << 5; s += (((double)r / 4294967296.0) * 2.0 - 1.0) * click * exp(-tt / 0.0012); } + adds(busL, busR, s0 + i, s * g, s * g); + } +} + +int main(int argc, char **argv) { + const char *out_path = "out/pianotrax-raw.wav"; + for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--out") && i + 1 < argc) out_path = argv[++i]; else if (!strcmp(argv[i], "--bpm") && i + 1 < argc) BPMV = atof(argv[++i]); else if (!strcmp(argv[i], "--swing") && i + 1 < argc) SWING = atof(argv[++i]); else if (!strcmp(argv[i], "--piano-dir") && i + 1 < argc) PIANO_DIR = argv[++i]; } + BEAT = 60.0 / BPMV; BAR = BEAT * 4; SX = BEAT / 4; + + // load the Salamander grand bank — the same samples AC OS plays. + const char *pdir = find_piano_dir(); + if (!pdir || load_piano_bank(pdir) == 0) { + fprintf(stderr, "✗ no piano samples found (looked for /60.raw; set --piano-dir or $AC_PIANO_DIR). " + "Expected fedac/native/samples/piano\n"); + return 1; + } + fprintf(stderr, "# piano bank · %d Salamander anchors from %s\n", pn_count, pdir); + { int c = 0; for (int i = 0; i < 8; i++) { START[i] = c; c += SECBARS[i]; } } + int TB = 0; for (int i = 0; i < 8; i++) TB += SECBARS[i]; + double totalSec = TB * BAR + 6.0; N = (long)(totalSec * SR); + busL = calloc(N, 4); busR = calloc(N, 4); revL = calloc(N, 4); revR = calloc(N, 4); + fprintf(stderr, "# pianotrax.c · %g BPM · %d bars · %.1fs · A major solo piano (glasstrax arranged)\n", BPMV, TB, totalSec); + + // the lead, as {phrase-bar, beat, note} — the SAME bright rising-then- + // cresting glasstrax motif, dropped an octave into a comfortable right-hand + // range (originals were 76..90 = E5..F#6; here ~ A4..F#5, where the hand + // lives and the melody sings rather than tinkles). + double LEAD[8][3] = { {0,0,69},{0,2.5,73},{1,0,76},{1,1.5,78},{2,0,81},{2,1.5,78},{3,0,76},{3,2,83} }; + + // ── INTRO — a single line: sparse RH chord glints over a held LH root ────── + { int idx = sec_index("intro"); int c = START[idx], nb = SECBARS[idx], h = nb / 2; double t0 = c * BAR; + // held low roots, hand resting, pedal down — long natural ring. + pkey(45, t0, BAR * h - 0.05, 0.20, -0.15, 0.35, 0.22); // A2 + pkey(50, t0 + BAR * h, BAR * (nb - h) - 0.05, 0.20, -0.15, 0.35, 0.22); // D2→hand to IV + // a few descending RH glints (a player feeling out the melody), from bar 2. + int mel[4] = { 81, 78, 76, 73 }; // A5 F#5 E5 C#5 + for (int b = 0; b < 4 && 2 + b < nb; b++) pkey(mel[b], t0 + (2 + b) * BAR, BAR * 0.9, 0.16, 0.20, 0.45, 0.28); + // one soft rolled A chord at the top of the back half (a breath in). + roll(RH_A, 3, t0 + BAR * h + BEAT * 2, 0.12, 0.40, 0.25); + } + + // ── BLOOM — LH broken chords arrive; RH answers with a gentle pentatonic run ── + { int idx = sec_index("bloom"); int c = START[idx], nb = SECBARS[idx]; double t0 = c * BAR; + // chord per bar follows vi, vi, V, V (the glasstrax bloom climb). + const int *prog[4] = { LH_FM, LH_FM, LH_E, LH_E }; + for (int b = 0; b < nb; b++) { + lh_broken(t0 + b * BAR, prog[b], 0.16, 0.42); + // RH: a 4-note ascending pentatonic eighth-figure in the back half of + // each bar — a hand-shaped flourish, not a sweep. + if (b >= 1) for (int s = 0; s < 4; s++) { + double tt = t0 + b * BAR + swing_beats(2.0 + s * 0.5); + pkey(PENT[(s + b) % 5], tt, BEAT * 0.6, 0.10 + 0.012 * b, 0.25, 0.5, 0.28); + } + } + // a rising 3-note grace pickup into wave A (RH). + for (int s = 0; s < 3; s++) pkey(74 + s * 2, t0 + BAR * nb - BEAT * 0.9 + 0.07 * s, BEAT * 0.5, 0.10, 0.3, 0.5, 0.3); + } + + // ── the three waves — the body. LH broken-chord accompaniment + RH melody. ── + const char *waves[3] = { "waveA", "waveB", "waveC" }; + for (int d = 0; d < 3; d++) { + int wi = sec_index(waves[d]); int c = START[wi], nbw = SECBARS[wi]; int big = (d == 2); + for (int b = 0; b < nbw; b++) { + double t0 = (c + b) * BAR; int ci = b % 4; int phrase = (ci == 0); + const int *lhc = LH[ci]; + + // LEFT HAND — broken-chord accompaniment running the bar. In the big + // wave it's fuller; elsewhere a touch lighter. + lh_broken(t0, lhc, big ? 0.165 : 0.14, big ? 0.46 : 0.40); + + // a sustained RH chord pad on each phrase downbeat (held ~2 bars), + // rolled so it sounds played, not punched. + if (phrase) roll(RH[ci], 3, t0, big ? 0.085 : 0.07, 0.42, 0.30); + + // RIGHT HAND — the LEAD melody (4-bar phrase), singing, with a single + // grace-note before high notes (idiomatic, one finger) instead of the + // glasstrax frill sweep. + for (int z = 0; z < 8; z++) if ((int)LEAD[z][0] == ci) { + double note = LEAD[z][2], tt = t0 + swing_beats(LEAD[z][1]); + if (note >= 78) pkey(note - 2, tt - 0.05, BEAT * 0.25, big ? 0.07 : 0.055, 0.25, 0.55, 0.2); // grace + pkey(note, tt, BEAT * 1.3, big ? 0.135 : 0.11, 0.22, big ? 0.7 : 0.6, 0.34); + } + + // a light RH connecting figure between melody notes — two pentatonic + // eighths mid-bar (the "ornaments" energy that replaces the 16th bed), + // only when the hand is free (not on a phrase-busy big-wave crest). + if (!(big && ci == 3)) for (int s = 0; s < 2; s++) { + double tt = t0 + swing_beats(2.5 + s * 0.5); + int p = PENT[(s + b + 2) % 5]; + pkey(p, tt, BEAT * 0.45, 0.06 + 0.01 * (s), 0.28, 0.45, 0.3); + } + + // crest of each big-wave phrase end: a rolled high A-add chord (both + // hands meet) — the recognizable glasstrax "crest", made playable. + if (big && ci == 3) { int crest[3] = { 76, 81, 85 }; roll(crest, 3, t0 + BEAT * 2, 0.09, 0.7, 0.34); } + } + } + + // ── HUSHES — spacious: half-time LH + a held RH chord + a lone melody line ── + const char *hush[2] = { "hushA", "hushB" }; + for (int z2 = 0; z2 < 2; z2++) { + int hi = sec_index(hush[z2]); int c = START[hi], nb = SECBARS[hi], h = nb / 2; double t0 = c * BAR; + // IV then V, the glasstrax hush harmony. + for (int b = 0; b < nb; b++) { + const int *lhc = (b < h) ? LH_D : LH_E; + lh_half(t0 + b * BAR, lhc, 0.15, 0.36); + // a sparse high RH note on alternating bars — the melody catching its breath. + if (b % 2 == 0) { + int rhn = (b < h) ? RH_D[2] : RH_E[2]; + pkey(rhn, t0 + b * BAR + BEAT * 1, BAR * 0.8, 0.10, 0.25, 0.45, 0.34); + } + } + // a held RH chord across the back half (pedal ringing). + roll((z2 == 0) ? RH_D : RH_E, 3, t0 + BAR * h, 0.075, 0.4, 0.36); + // a short rising pentatonic pickup into the next wave (RH), z2==1 stronger. + if (z2 == 1) for (int s = 0; s < 4; s++) pkey(PENT[s % 5] + (s / 5) * 12, t0 + BAR * (nb - 1) + swing_beats(s * 0.5), BEAT * 0.5, 0.085, 0.3, 0.5, 0.32); + } + + // ── OUTRO — thinning LH, a last statement of the melody, a final rolled chord ── + { int idx = sec_index("outro"); int c = START[idx], nb = SECBARS[idx], h = nb / 2; double t0 = c * BAR; + for (int b = 0; b < nb; b++) { + double e = fmax(0.12, 1.0 - (double)b / nb); // gentle decrescendo + const int *lhc = (b < h) ? LH_A : LH_E; + if (b < nb - 3) lh_half(t0 + b * BAR, lhc, 0.14 * e, 0.34); // LH fades out + // a soft RH chord glint every other bar, also fading. + if (b % 2 == 0 && b < nb - 3) roll((b < h) ? RH_A : RH_E, 3, t0 + b * BAR + BEAT * 2, 0.065 * e, 0.38, 0.36); + } + // a final, long, rolled A-major chord (both hands) ringing into the fade — + // low A, A, C#, E, A: the home chord, pedal held. + int fin[5] = { 33, 45, 61, 64, 69 }; // A1 A2 C#4 E4 A4 + for (int k = 0; k < 5; k++) pkey(fin[k], t0 + BAR * (nb - 3) + 0.03 * k, 5.5, 0.16 * (k == 0 ? 0.8 : 1.0), 0, 0.5, 0.4); + } + + // ── BEAT LAYER — kick + rhythmic scratches, through the three waves only ── + // The groove drops in for the waves and pulls out for intro / bloom / hushes + // / outro — that contrast IS the dynamic range. Kick + scratches sit dead on + // the swung grid; the humanized piano drags lazily behind them. Scratches are + // rhythmic and timely (a stab on the &-of-4 every bar, chirps on the &-of-2), + // not just one-shots at the seams. + { + const char *wv[3] = { "waveA", "waveB", "waveC" }; + for (int d = 0; d < 3; d++) { + int wi = sec_index(wv[d]); int c = START[wi], nb = SECBARS[wi]; int big = (d == 2); + scratch(c * BAR, 0.44, 50, big ? 7 : 5, big ? 3.0 : 2.2, big ? 7 : 12, 0.55, 0.58, big ? 0.14 : 0.12, 0.05, 0.18); // drop-in + for (int b = 0; b < nb; b++) { + double t0 = (c + b) * BAR; + // kick — laid-back boom-bap: strong 1, syncopated &-of-2 (+ beat 3 in the big wave) + kick(t0 + swing_beats(0), 0.18); + kick(t0 + swing_beats(2.5), 0.13); + if (big && b % 2 == 0) kick(t0 + swing_beats(3), 0.10); + // rhythmic scratch on the &-of-4 every bar (baby ↔ scribble), chirp on &-of-2 + double sm = 47 + (b % 4) * 3; + scratch(t0 + swing_beats(3.5), 0.28, sm, (b % 2) ? 14 : 5.5, (b % 2) ? 1.6 : 2.0, 0, 0, 0.5, big ? 0.12 : 0.10, 0.22, 0.10); + if (b % 2 == 1) scratch(t0 + swing_beats(1.5), 0.24, 52, 7, 3.0, 7, 0.55, 0.55, 0.10, -0.18, 0.10); + if (big) scratch(t0 + swing_beats(0.5), 0.22, 50, 14, 1.5, 0, 0, 0.6, 0.09, 0.26, 0.08); // extra scribble + } + } + } + + // ── REVERB (Schroeder) — a NATURAL ROOM, drier & warmer than glasstrax's hall. + // Lower wet, more damping (darker tail) — a piano in a room, not a glass cave. + { + double decay = 0.80, wet = 0.30, damp = 0.45; + int CD[6]; double cds[6] = { 0.0297, 0.0371, 0.0411, 0.0437, 0.0497, 0.0581 }; + for (int k = 0; k < 6; k++) CD[k] = (int)(cds[k] * SR); + int AD[2] = { (int)(0.005 * SR), (int)(0.0017 * SR) }; double apFb = 0.5; + float *cbL[6], *cbR[6]; int ciL[6] = {0}, ciR[6] = {0}; double cLPL[6] = {0}, cLPR[6] = {0}; + for (int k = 0; k < 6; k++) { cbL[k] = calloc(CD[k], 4); cbR[k] = calloc(CD[k], 4); } + float *abL[2], *abR[2]; int aiL[2] = {0}, aiR[2] = {0}; + for (int k = 0; k < 2; k++) { abL[k] = calloc(AD[k], 4); abR[k] = calloc(AD[k], 4); } + for (long i = 0; i < N; i++) { + double inL = revL[i], inR = revR[i], cL = 0, cR = 0; + for (int k = 0; k < 6; k++) { + double dL = cbL[k][ciL[k]], dR = cbR[k][ciR[k]]; cL += dL; cR += dR; + cLPL[k] = dL * (1 - damp) + cLPL[k] * damp; cLPR[k] = dR * (1 - damp) + cLPR[k] * damp; + cbL[k][ciL[k]] = (float)(inL + cLPL[k] * decay); cbR[k][ciR[k]] = (float)(inR + cLPR[k] * decay); + ciL[k] = (ciL[k] + 1) % CD[k]; ciR[k] = (ciR[k] + 1) % CD[k]; + } + cL /= 6; cR /= 6; + for (int k = 0; k < 2; k++) { + double dL = abL[k][aiL[k]], dR = abR[k][aiR[k]]; + double oL = -apFb * cL + dL, oR = -apFb * cR + dR; + abL[k][aiL[k]] = (float)(cL + apFb * oL); abR[k][aiR[k]] = (float)(cR + apFb * oR); + aiL[k] = (aiL[k] + 1) % AD[k]; aiR[k] = (aiR[k] + 1) % AD[k]; + cL = oL; cR = oR; + } + busL[i] += (float)(cL * wet); busR[i] += (float)(cR * wet); + } + for (int k = 0; k < 6; k++) { free(cbL[k]); free(cbR[k]); } + for (int k = 0; k < 2; k++) { free(abL[k]); free(abR[k]); } + } + + // ── normalize + gentle in/out fades ── + double peak = 0; for (long i = 0; i < N; i++) { double a = fmax(fabs(busL[i]), fabs(busR[i])); if (a > peak) peak = a; } + fprintf(stderr, "# pre-norm peak %.3f\n", peak); + if (peak > 0) { double g = 0.85 / peak; for (long i = 0; i < N; i++) { busL[i] *= (float)g; busR[i] *= (float)g; } } + long fin = (long)(1.5 * SR), fout = (long)(4.5 * SR); + for (long i = 0; i < fin && i < N; i++) { double g = 0.5 - 0.5 * cos(M_PI * (double)i / fin); busL[i] *= (float)g; busR[i] *= (float)g; } + for (long i = 0; i < fout && i < N; i++) { double g = 0.5 - 0.5 * cos(M_PI * (double)i / fout); long idx = N - 1 - i; busL[idx] *= (float)g; busR[idx] *= (float)g; } + + if (!write_wav_f32_stereo(out_path, busL, busR, N)) { fprintf(stderr, "✗ write failed\n"); return 1; } + fprintf(stderr, "✓ %s · %.1fs\n", out_path, (double)N / SR); + return 0; +} diff --git a/pop/maytrax/c/render-c.mjs b/pop/maytrax/c/render-c.mjs --- a/pop/maytrax/c/render-c.mjs +++ b/pop/maytrax/c/render-c.mjs @@ -44,7 +44,13 @@ if (!SINEFAM && !flags["no-bake"]) run("bake (score.h)", "node", [join(HERE, "bake.mjs")]); run("cc", "cc", ["-O3", "-std=c11", "-Wall", "-Wextra", "-Wno-unused-parameter", "-o", join(HERE, ENGINE), join(HERE, `${ENGINE}.c`), "-lm"]); const rawWav = join(HERE, "out", `${ENGINE}-raw.wav`); -run("render (C engine)", join(HERE, ENGINE), ["--out", rawWav]); +// pianotrax voices the real Salamander grand from fedac/native/samples/piano — +// pass an absolute path so it loads regardless of the cwd it's spawned from. +const engineArgs = ["--out", rawWav]; +if (ENGINE === "pianotrax") { + engineArgs.push("--piano-dir", resolve(HERE, "..", "..", "..", "fedac", "native", "samples", "piano")); +} +run("render (C engine)", join(HERE, ENGINE), engineArgs); // ── pop master chain (ported from bin/maytrax.mjs) ──────────────────────── // chiller, warmer, more dynamic master than the old hot pop chain: lower @@ -68,9 +74,15 @@ // the build, so the opening sounds stay soft far longer than a linear ramp. const INTRO_FADE = "afade=t=in:curve=cub:st=0:d=13"; // sinetrax is gentle bells (and fades itself) — master it softer + more // dynamic with no extra fade; maytrax gets the slow cubic intro fade. -const MASTER_MP3 = SINEFAM - ? `${MASTER_CORE},loudnorm=I=-13:TP=-1.5:LRA=13,alimiter=limit=0.95:attack=8:release=120` - : `${MASTER_CORE},loudnorm=I=-11:TP=-1.5:LRA=11,alimiter=limit=0.93:attack=5:release=90,${INTRO_FADE}`; +// pianotrax wants extra volume but a WIDE dynamic range (the beat drops in for +// the waves, the hushes breathe) — louder loudnorm target, high LRA, a gentle +// limiter ceiling so the key-press transients still punch. +const MASTER_MP3 = + ENGINE === "pianotrax" + ? `${MASTER_CORE},loudnorm=I=-11.5:TP=-1.3:LRA=13,alimiter=limit=0.97:attack=6:release=110` + : SINEFAM + ? `${MASTER_CORE},loudnorm=I=-13:TP=-1.5:LRA=13,alimiter=limit=0.95:attack=8:release=120` + : `${MASTER_CORE},loudnorm=I=-11:TP=-1.5:LRA=11,alimiter=limit=0.93:attack=5:release=90,${INTRO_FADE}`; run("master (ffmpeg) → mp3", "ffmpeg", [ "-hide_banner", "-y", "-loglevel", "error", diff --git a/pop/nullabye/README.md b/pop/nullabye/README.md --- a/pop/nullabye/README.md +++ b/pop/nullabye/README.md @@ -76,3 +76,40 @@ ticks, Q 70 high sparkles on offbeats. - **veil-lifts** (20–23) — one wide bell sweeps 250 Hz → 6 kHz letting the raw noise wash through, a last lone whistle, then the EQ goes flat and the song ends in literal silence. + +## nullabata — the sonata cut + +Third cut in the lane (`bin/render-nullabata.mjs` → `out/nullabata.mp3`, +~4:21): *nullaby + sonata*. **Four movements run attacca — no silence +between them** — bound by one lullaby motif (semitone offsets from a +per-movement voice tonic) transformed in each: + +``` +I. Andante C maj 72 BPM 18 bars the theme, lyrical whistle + warm pads +II. Scherzo A min 132 BPM 32 bars theme shattered into a fast arp/twinkle cloud +III. Adagio F maj 56 BPM 13 bars theme sung slow over sustained pads + drones +IV. Finale C maj 84 BPM 30 bars theme recapitulated: 2→bloom→2 accumulation +``` + +**Clarity is a Q decision.** In this engine every voice is a peaking-EQ +bell on noise: a *narrow* (high-Q) bell rings like a breathy tone, a *wide* +(low-Q) bell passes a wide band of raw noise (hiss). nullabata runs the +engine at its **tonal extreme** — high Q on every pitched voice (lead 110, +pads 55, drones 45, choir/halo 50+), **no wide "veil" wash** (the single +biggest static source in the lane), and percussion kept narrow (no shaker / +open-hat; hats are narrow pitched ticks). Master drops the treble boost and +adds a 15 kHz lowpass; energy above 12 kHz sits 36 dB below the full band. + +Otherwise reuses the engine wholesale — lanes, the serial-dB +disjoint-frequency discipline, sorted mono events. The loudness ride is +**per-second, movement-aware** (each movement targets its own dBFS curve) +so the four very different tempos and densities land evenly; integrated +**−19.1 LUFS**. The Finale runs a compact accumulation arch (47-point +roster, 2 → bloom → 2). `--proof` passes with all 47 lanes flat (bit-exact +silence); the `--bake` → `c/run-c.mjs` C path renders the same score. + +```bash +node pop/nullabye/bin/render-nullabata.mjs # → out/nullabata.mp3 +node pop/nullabye/bin/render-nullabata.mjs --proof # flat ⇒ bit-exact silence +node pop/nullabye/bin/render-nullabata.mjs --bake out/nullabata.score.txt +``` diff --git a/pop/nullabye/bin/render-nullabata.mjs b/pop/nullabye/bin/render-nullabata.mjs new file mode 100644 --- /dev/null +++ b/pop/nullabye/bin/render-nullabata.mjs @@ -0,0 +1,659 @@ +#!/usr/bin/env node +// render-nullabata.mjs — the sonata cut: nullaby + sonata. +// +// Four movements run ATTACCA (no silence between them), bound by one +// lullaby motif transformed in each: +// +// I. Andante C maj 92 BPM the theme, lyrical whistle + warm pads +// II. Scherzo A min 152 BPM theme shattered into a fast arp/twinkle cloud +// III. Adagio F maj 72 BPM theme sung slow over sustained pads + drones +// IV. Finale C maj 100 BPM theme recapitulated: 2→bloom→2 accumulation +// +// Same trick as render-nullabye.mjs / render-nuellaby.mjs (after Andy +// Brewer's "this song has no instruments in it", +// https://youtu.be/_Rk-hmIMv6I): two copies of pink noise, one +// phase-inverted — perfect silence — and every note is a peaking EQ bell +// breaking the cancellation. No oscillator anywhere in this file. +// +// CLARITY IS A Q DECISION: a narrow (high-Q) bell on noise rings like a +// breathy tone; a wide (low-Q) bell passes a wide band of raw noise (hiss). +// This cut runs the engine at its TONAL extreme — high Q on every pitched +// voice, no wide "veil" wash, percussion kept narrow — so it reads as a +// pitched choir, not radio static. +// +// LANES (from nuellaby): a lane is one persistent EQ point; consecutive +// notes on a lane reuse it monophonically (the bell drags to the new +// frequency, clipping the old ring). The EQ chain is SERIAL — two bells +// at the same frequency ADD their dB — so every layer here owns disjoint +// frequencies. The spectrum is carved into the usual zones: +// SUB 40–90 (kick, bass) · BASS 90–250 (drones) · LOW-MID 250–500 (pads) +// MID 500–1k (lead, harmony, choir) · UPPER-MID 1–2.5k (octave lead, arps) +// PRESENCE 2.5–5k (sparkles) · AIR 5–10k (narrow tone ticks) +// +// Run: +// node pop/nullabye/bin/render-nullabata.mjs # → out/nullabata.mp3 +// node pop/nullabye/bin/render-nullabata.mjs --proof # flat ⇒ bit-exact silence +// node pop/nullabye/bin/render-nullabata.mjs --bake out.txt # score for the C engine + +import { writeFileSync, mkdirSync, unlinkSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; +import { spawnSync } from "node:child_process"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SR = 48_000; +const BLOCK = 64; + +const _argi = (k) => { const i = process.argv.indexOf(k); return i >= 0 ? process.argv[i + 1] : null; }; +const PROOF = process.argv.includes("--proof"); + +// ── the four movements ──────────────────────────────────────────────── +// each has its own tempo and bar count; a movement's local clock is +// mt(mov, bar, beat). movements run attacca — no silence between them. +const GAP = 0; +const MOVES = [ + { id: "andante", title: "I. Andante", bpm: 92, bars: 12 }, // ~31 s + { id: "scherzo", title: "II. Scherzo", bpm: 152, bars: 28 }, // ~44 s + { id: "adagio", title: "III. Adagio", bpm: 72, bars: 10 }, // ~33 s + { id: "finale", title: "IV. Finale", bpm: 100, bars: 24 }, // ~58 s +]; +{ + let cursor = 0; + for (const m of MOVES) { + m.beat = 60 / m.bpm; + m.bar = 4 * m.beat; + m.dur = m.bars * m.bar; + m.startSec = cursor; + m.endSec = cursor + m.dur; + cursor = m.endSec + GAP; + } +} +const TAIL = 1.6; // let the finale's fade breathe +const totalSec = PROOF ? 6 : MOVES[MOVES.length - 1].endSec + TAIL; +const ns = Math.ceil(totalSec * SR); +const MOV = Object.fromEntries(MOVES.map((m) => [m.id, m])); + +// absolute seconds for a (movement, bar, beat) +const mt = (m, bar, beat = 0) => m.startSec + bar * m.bar + beat * m.beat; + +// ── note table (Hz) ─────────────────────────────────────────────────── +const HZ = { + A1: 55.0, Bb1: 58.27, C2: 65.41, D2: 73.42, F2: 87.31, G2: 98.0, + A2: 110.0, Bb2: 116.54, C3: 130.81, D3: 146.83, F3: 174.61, G3: 196.0, + A3: 220.0, Bb3: 233.08, B3: 246.94, + C4: 261.63, D4: 293.66, E4: 329.63, F4: 349.23, G4: 392.0, A4: 440.0, Bb4: 466.16, B4: 493.88, + C5: 523.25, D5: 587.33, E5: 659.26, F5: 698.46, G5: 783.99, A5: 880.0, B5: 987.77, + C6: 1046.5, D6: 1174.66, E6: 1318.51, F6: 1396.91, G6: 1567.98, A6: 1760.0, + C7: 2093.0, D7: 2349.32, E7: 2637.02, +}; + +// ── the lullaby motif — the sonata's binding idea ───────────────────── +// semitone offsets from a movement's "voice tonic" (the register the lead +// whistle sings in), laid over a 2-bar (8-beat) phrase. Every movement +// states a transformation of this same head: lyrical, shattered, stretched, +// recapitulated. transposing it is one multiply (tonic · 2^(semi/12)). +const THEME = [ + { semi: 4, beat: 0, dur: 1.5 }, // 3rd + { semi: 7, beat: 1.5, dur: 0.5 }, // 5th + { semi: 9, beat: 2, dur: 2 }, // 6th — the reach + { semi: 7, beat: 4, dur: 1 }, // 5th + { semi: 4, beat: 5, dur: 1 }, // 3rd + { semi: 2, beat: 6, dur: 2 }, // 2nd — settle +]; +const semi = (tonic, s) => tonic * Math.pow(2, s / 12); +// diatonic third below, per motif semitone (C-major degrees) — keyed on the +// semitone offset, not the Hz value (computed Hz won't byte-match a table) +const THIRD3 = { 2: -1, 4: 0, 7: 4, 9: 5 }; + +// ── lanes: persistent monophonic EQ points ──────────────────────────── +const lanes = new Map(); // name → { events: [] } + +function note(lane, t0, { freq, freqEnd = null, peakDb, q, atk, hold, rel }) { + if (PROOF) peakDb = 0; + let L = lanes.get(lane); + if (!L) { L = { events: [] }; lanes.set(lane, L); } + const prev = L.events[L.events.length - 1]; + if (prev && prev.end > t0) { prev.cut = t0; prev.end = t0; } // mono: clip the old ring + const end = t0 + atk + hold + rel; + L.events.push({ t0, freq, freqEnd, peakDb, q, atk, hold, rel, end, cut: null }); +} + +function envOf(e, time) { + const dt = time - e.t0; + if (dt < 0 || dt >= e.atk + e.hold + e.rel) return 0; + let v; + if (dt < e.atk) v = dt / e.atk; + else if (dt < e.atk + e.hold) v = 1; + else v = 1 - (dt - e.atk - e.hold) / e.rel; + if (e.cut !== null) v *= Math.min(1, Math.max(0, (e.cut - time) / 0.03)); + return v; +} + +// shared articulation helpers. KEY TIMBRE FACT: one bell on noise is always +// a bit breathy (it IS filtered noise), and LOW Q widens the band → MORE +// air, not body. So we keep Q HIGH (narrow = tonal, minimal air) and build +// body from HARMONIC STACKS instead — a fundamental plus partials reads as a +// pitched instrument, full but not airy. +const lead = (m, lane, bar, beat, freq, beats, db = 26, q = 130) => + note(lane, mt(m, bar, beat), { freq, peakDb: db, q, atk: 0.06, hold: Math.max(0.04, beats * m.beat - 0.06), rel: 0.45 }); +const pad = (m, lane, bar, freq, db = 19, q = 90) => { + note(lane, mt(m, bar), { freq, peakDb: db, q, atk: 0.8, hold: m.bar - 2.0, rel: 1.2 }); + // octave partial = body without air + note(`${lane}o`, mt(m, bar), { freq: freq * 2, peakDb: db - 9, q: q * 1.4, atk: 0.85, hold: m.bar - 2.1, rel: 1.2 }); +}; + +// a VOICE = a harmonic stack of high-Q bells (fundamental + 3 partials with +// a natural rolloff). this is where the melody's body comes from. +const VOICE_PARTS = [ + { mult: 1, dbRel: 0, q: 140 }, + { mult: 2, dbRel: -5, q: 165 }, + { mult: 3, dbRel: -10, q: 190 }, + { mult: 4, dbRel: -15, q: 215 }, +]; +function leadVoice(m, lane, bar, beat, freq, beats, db = 26) { + for (const pt of VOICE_PARTS) + if (freq * pt.mult < 15000) + lead(m, `${lane}_p${pt.mult}`, bar, beat, freq * pt.mult, beats, db + pt.dbRel, pt.q); +} + +// state the motif on a lane at (movement, barBase, voiceTonic) +function statTheme(m, lane, barBase, voiceTonic, { db = 26, artic = 1, oct = 1 } = {}) { + for (const n of THEME) { + const f = semi(voiceTonic, n.semi) * oct; + leadVoice(m, lane, barBase + Math.floor(n.beat / 4), n.beat % 4, f, n.dur * artic, db); + } +} + +// chord = root frequency (mid register) + interval set; bass an octave or two below +const chord = (rootMid, ivals, bassHz, ninth) => ({ + tones: ivals.map((s) => semi(rootMid, s)), + ninth: ninth ?? semi(rootMid, 14), + root: bassHz, +}); +const MAJ = [0, 4, 7], MIN = [0, 3, 7]; + +// tiny deterministic LCG for the twinkle clouds (no Math.random — resume-safe) +const lcg = (seed) => { let s = seed >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; }; + +// ======================================================================= +// I. ANDANTE — C major. the theme stated plainly: whistle + warm pads, +// a heartbeat kick that wakes halfway, the gentlest hat ticks at the end. +// ======================================================================= +function andante() { + const m = MOV.andante; + const PROG = [ + chord(HZ.C4, MAJ, HZ.C2), chord(HZ.A3, MIN, HZ.A1), + chord(HZ.F3, MAJ, HZ.F2), chord(HZ.G3, MAJ, HZ.G2), + ]; + for (let bar = 0; bar < m.bars; bar++) { + const ch = PROG[bar % 4]; + // pads — low-mid, fade in over the first phrase + const padDb = bar < 2 ? 15 : 20; + pad(m, "padR", bar, ch.tones[0], padDb); pad(m, "pad5", bar, ch.tones[2], padDb); + if (bar >= 4) pad(m, "pad3", bar, ch.tones[1], 18); + if (bar >= 8) pad(m, "pad9", bar, ch.ninth, 16); + // bass root, half notes once the texture settles + if (bar < 4) note("bass", mt(m, bar), { freq: ch.root, peakDb: 24, q: 13, atk: 0.04, hold: m.bar - 0.6, rel: 0.4 }); + else [[0, 1], [2, 1.5]].forEach(([beat, mul]) => + note("bass", mt(m, bar, beat), { freq: ch.root * mul, peakDb: 25, q: 13, atk: 0.03, hold: 2 * m.beat - 0.2, rel: 0.3 })); + // heartbeat kick from bar 6 — soft, in the noise sub-zone + if (bar >= 6) for (const beat of [0, 2]) + note("kick", mt(m, bar, beat), { freq: 100, freqEnd: 48, peakDb: beat ? 24 : 27, q: 2.6, atk: 0.003, hold: 0.012, rel: 0.18 }); + // the gentlest tick in the last phrase — narrow + pitched, not a hiss + if (bar >= 12) for (const beat of [1, 3]) + note("hat", mt(m, bar, beat), { freq: 6800, peakDb: 12, q: 9, atk: 0.003, hold: 0.01, rel: 0.06 }); + } + // theme on its own 2-bar phrase grid, voice tonic C5 + for (let p = 0; p < m.bars / 2; p++) + statTheme(m, "lead", p * 2, HZ.C5, { db: p === 0 ? 22 : 26 }); + // a quiet third-below harmony once the theme is established + for (let p = 2; p < m.bars / 2; p++) + for (const n of THEME) + lead(m, "harm", p * 2 + Math.floor(n.beat / 4), n.beat % 4, semi(HZ.C5, THIRD3[n.semi]), n.dur, 18); +} + +// ======================================================================= +// II. SCHERZO — A minor, fast. the theme is shattered into staccato arp +// fragments and a twinkle cloud; a light kick on 1, driving eighth hats. +// playful, bittersweet (relative minor), highest register of the suite. +// ======================================================================= +function scherzo() { + const m = MOV.scherzo; + const PROG = [ + chord(HZ.A3, MIN, HZ.A1), chord(HZ.F3, MAJ, HZ.F2), + chord(HZ.C4, MAJ, HZ.C2), chord(HZ.G3, MAJ, HZ.G2), + ]; + const arpOf = (ch) => [ch.tones[0], ch.tones[1], ch.tones[2], ch.tones[1]].map((f) => f * 2); + const PENT = [HZ.A5, HZ.C6, HZ.D6, HZ.E6, HZ.G6]; + for (let bar = 0; bar < m.bars; bar++) { + const ch = PROG[bar % 4]; + // pads, short and light (scherzo bars are tiny — fast attack/release); + // octave partial for tone/body + for (const [lane, f] of [["padR", ch.tones[0]], ["pad5", ch.tones[2]]]) { + note(lane, mt(m, bar), { freq: f, peakDb: 17, q: 90, atk: 0.25, hold: m.bar - 0.7, rel: 0.45 }); + note(`${lane}o`, mt(m, bar), { freq: f * 2, peakDb: 9, q: 130, atk: 0.28, hold: m.bar - 0.75, rel: 0.45 }); + } + // walking bass, two notes a bar + [[0, 1], [2, 1.5]].forEach(([beat, mul]) => + note("bass", mt(m, bar, beat), { freq: ch.root * mul, peakDb: 25, q: 13, atk: 0.02, hold: 1.6 * m.beat, rel: 0.2 })); + // light kick on 1 (and 3 once it's running) + const kbeats = bar >= 4 ? [0, 2] : [0]; + for (const beat of kbeats) + note("kick", mt(m, bar, beat), { freq: 105, freqEnd: 50, peakDb: beat ? 24 : 28, q: 3, atk: 0.003, hold: 0.01, rel: 0.13 }); + // light offbeat ticks — narrow + pitched, not a wash of hiss + const hatBeats = bar >= 2 ? [0.5, 1.5, 2.5, 3.5] : [2]; + for (const beat of hatBeats) + note("hat", mt(m, bar, beat), { freq: 7200, peakDb: 14, q: 9, atk: 0.002, hold: 0.008, rel: 0.05 }); + // the theme SHATTERED: every motif note becomes a fast up-down arp run + const arp = arpOf(ch); + for (const n of THEME) { + const base = semi(HZ.A4, n.semi) * 2; // voice tonic A5 region + const steps = [base, arp[1], arp[2], arp[3]]; + for (let k = 0; k < 4; k++) { + const beat = (n.beat + k * 0.25) % 4; + const barOff = bar + Math.floor((n.beat + k * 0.25) / 4); + if (barOff >= m.bars) continue; + note(`arp${k}`, mt(m, barOff, beat), { freq: steps[k], peakDb: 16, q: 150, atk: 0.012, hold: 0.07, rel: 0.18 }); + // octave + twelfth partials → tone, not a thin sine + note(`arp${k}o`, mt(m, barOff, beat), { freq: steps[k] * 2, peakDb: 10, q: 180, atk: 0.012, hold: 0.06, rel: 0.16 }); + } + } + // twinkle cloud — accretes, presence/air register, A-minor pentatonic + const cloudN = Math.min(10, 2 + Math.floor(bar / 2)); + for (let j = 0; j < cloudN; j++) { + const rnd = lcg(j * 7919 + bar * 104729); + const f = PENT[(j + bar) % PENT.length]; + const offs = [Math.floor(rnd() * 8) * 0.5, Math.floor(rnd() * 8) * 0.5].sort((a, z) => a - z); + for (const beat of offs) + note(`cloud${j}`, mt(m, bar, beat), { freq: f, peakDb: 11, q: 150, atk: 0.01, hold: 0.06, rel: 0.18 }); + } + } +} + +// ======================================================================= +// III. ADAGIO — F major, very slow. the theme sung in long tones over a +// veil-bed drone and low choir partials. no percussion — pure breath. +// the warm, still heart of the sonata. +// ======================================================================= +function adagio() { + const m = MOV.adagio; + const PROG = [ + chord(HZ.F3, MAJ, HZ.F2), chord(HZ.D3, MIN, HZ.D2), + chord(HZ.Bb2, MAJ, HZ.Bb1), chord(HZ.C3, MAJ, HZ.C2), + ]; + for (let bar = 0; bar < m.bars; bar++) { + const ch = PROG[bar % 4]; + // sustained pads, very slow attack/release — the bars are long here. + // higher Q than before: this movement used to live on a wide veil wash + // (pure static); now the pads + drones carry it as tones + // high-Q (tonal) fundamentals + octave partials = tone + body, no air + for (const [lane, f, db] of [["padR", ch.tones[0], 21], ["pad5", ch.tones[2], 20], ["pad3", ch.tones[1], 18]]) { + note(lane, mt(m, bar), { freq: f, peakDb: db, q: 90, atk: 1.4, hold: m.bar - 3.0, rel: 1.6 }); + note(`${lane}o`, mt(m, bar), { freq: f * 2, peakDb: db - 9, q: 130, atk: 1.5, hold: m.bar - 3.1, rel: 1.6 }); + } + // low drone roots — root and fifth, sustained + note("droneR", mt(m, bar), { freq: ch.root * 2, peakDb: 17, q: 90, atk: 1.0, hold: m.bar - 2.4, rel: 1.4 }); + note("droneF", mt(m, bar), { freq: ch.root * 3, peakDb: 14, q: 90, atk: 1.0, hold: m.bar - 2.4, rel: 1.4 }); + // choir — spice partials only (7,11,13): everything else is owned below + [7, 11, 13].forEach((p, i) => + note(`choir${i}`, mt(m, bar), { freq: ch.root * p, peakDb: 8 - i * 0.5, q: 90, atk: 1.2, hold: m.bar - 3.0, rel: 1.6 })); + } + // theme in long tones, voice tonic F4 — each note stretched to ~a bar + for (let p = 0; p < Math.floor(m.bars / 2); p++) + for (const n of THEME) + leadVoice(m, "lead", p * 2 + Math.floor(n.beat / 4), n.beat % 4, + semi(HZ.F4, n.semi), n.dur * 1.4, p === 0 ? 22 : 25); +} + +// ======================================================================= +// IV. FINALE — C major. the theme recapitulated over a compact accumulation +// arch: the rack grows 2 → full → 2, blooms, then thins to the lone theme +// — nuellaby's Boléro logic, gathering the whole sonata home. +// ======================================================================= +const FIN_ROSTER = [ + "lead", "bass", "padR", "pad5", "kick", "hat", "pad3", "pad9", + "harm", "leadOct", "droneR", "droneF", "spark0", "spark1", + "arp0", "arp1", "arp2", +]; +{ // bloom groups (own disjoint frequencies; serial dB-add discipline) + const groups = [ + Array.from({ length: 12 }, (_, j) => `halo${j}`), + Array.from({ length: 10 }, (_, j) => `cloud${j}`), + Array.from({ length: 4 }, (_, j) => `choir${j}`), + Array.from({ length: 3 }, (_, j) => `airOct${j}`), + ]; + let k = 0; + while (groups.some((g) => g.length)) { + if (groups[k].length) FIN_ROSTER.push(groups[k].shift()); + k = (k + 1) % groups.length; + } +} +const FIN_NMAX = FIN_ROSTER.length; +const FIN_RANK = new Map(FIN_ROSTER.map((n, i) => [n, i])); + +function finale() { + const m = MOV.finale; + const B = m.bars; + // count curve, knees as fractions of B so the arch completes at any length: + // hold 2 · accumulate to NMAX · bloom · shed back to 2 · lone theme to end + const k1 = Math.max(1, B * 0.08), k2 = B * 0.55, k3 = B * 0.72, k4 = B * 0.93; + const pointsAt = (bar) => { + if (bar < k1) return 2; + if (bar < k2) return 2 + ((bar - k1) / (k2 - k1)) * (FIN_NMAX - 2); + if (bar < k3) return FIN_NMAX; + if (bar < k4) return FIN_NMAX - ((bar - k3) / (k4 - k3)) * (FIN_NMAX - 2); + return 2; + }; + const on = (lane, bar) => { // roster gate: is this lane on the rack yet? + const r = FIN_RANK.get(lane); + return r === undefined || r < pointsAt(bar); + }; + m._pointsAt = pointsAt; m._nmax = FIN_NMAX; // for the ride + log + + const PROG = [ + chord(HZ.C4, MAJ, HZ.C2), chord(HZ.A3, MIN, HZ.A1), + chord(HZ.F3, MAJ, HZ.F2), chord(HZ.G3, MAJ, HZ.G2), + ]; + const ARP = (ch) => [ch.tones[0] * 2, ch.tones[1] * 2, ch.tones[2] * 2]; + const HALO = []; + for (const oct of [1, 2, 4]) for (const f of [HZ.C4, HZ.E4, HZ.G4, HZ.A4]) if (f * oct <= HZ.E7) HALO.push(f * oct); + const PENT_HI = [HZ.C6, HZ.D6, HZ.E6, HZ.G6, HZ.A6, HZ.C7]; + + for (let bar = 0; bar < B; bar++) { + const ch = PROG[bar % 4]; + const n0 = pointsAt(bar); + const frac = n0 / FIN_NMAX; + // bass — root while sparse, walk once awake + if (n0 < 8) note("bass", mt(m, bar), { freq: ch.root, peakDb: 26, q: 13, atk: 0.03, hold: m.bar - 0.6, rel: 0.4 }); + else [[0, 1], [1, 1.5], [2, 1], [3, 2]].forEach(([beat, mul]) => + note("bass", mt(m, bar, beat), { freq: ch.root * mul, peakDb: mul > 1.6 ? 20 : 26, q: 13, atk: 0.02, hold: 0.9 * m.beat, rel: 0.25 })); + // pads + if (on("padR", bar)) pad(m, "padR", bar, ch.tones[0]); + if (on("pad5", bar)) pad(m, "pad5", bar, ch.tones[2]); + if (on("pad3", bar)) pad(m, "pad3", bar, ch.tones[1]); + if (on("pad9", bar)) pad(m, "pad9", bar, ch.ninth, 17); + // kick + if (on("kick", bar)) for (const beat of [0, 2]) + note("kick", mt(m, bar, beat), { freq: 110, freqEnd: 42, peakDb: beat ? 29 : 31, q: 2.2, atk: 0.003, hold: 0.012, rel: 0.18 }); + // ticks — narrow + pitched, not a wash of hiss + if (on("hat", bar)) { + const hatBeats = n0 >= FIN_NMAX * 0.4 ? [0.5, 1.5, 2.5, 3.5] : [1, 3]; + for (const beat of hatBeats) + note("hat", mt(m, bar, beat), { freq: 7000, peakDb: 14, q: 9, atk: 0.002, hold: 0.008, rel: 0.05 }); + } + // drones + if (on("droneR", bar)) note("droneR", mt(m, bar), { freq: ch.root * 2, peakDb: 16, q: 90, atk: 0.5, hold: m.bar - 1.3, rel: 0.8 }); + if (on("droneF", bar)) note("droneF", mt(m, bar), { freq: ch.root * 3, peakDb: 13, q: 90, atk: 0.5, hold: m.bar - 1.3, rel: 0.8 }); + // glints + const glint = [HZ.E6, HZ.G6, HZ.D6, HZ.E6][bar % 4]; + if (on(`spark${bar % 2}`, bar)) + note(`spark${bar % 2}`, mt(m, bar, bar % 2 ? 3.5 : 1.5), { freq: glint, peakDb: 18, q: 90, atk: 0.01, hold: 0.06, rel: 0.18 }); + // arpeggio — assembles note by note as its lanes join + const arp = ARP(ch), steps = [0, 1, 2, 1, 0, 1, 2, 1]; + for (let k = 0; k < 8; k++) + if (on(`arp${k % 3}`, bar)) { + note(`arp${k % 3}`, mt(m, bar, k * 0.5), { freq: arp[steps[k]], peakDb: 15, q: 150, atk: 0.015, hold: 0.12, rel: 0.2 }); + note(`arp${k % 3}o`, mt(m, bar, k * 0.5), { freq: arp[steps[k]] * 2, peakDb: 9, q: 180, atk: 0.015, hold: 0.11, rel: 0.18 }); + } + // air octave — chord tones two octaves up + [...ch.tones.map((f) => f * 4)].forEach((f, i) => { + if (on(`airOct${i}`, bar)) pad(m, `airOct${i}`, bar, f, 10, 30); + }); + // choir — spice partials only + [7, 11, 13, 14].forEach((p, i) => { + if (on(`choir${i}`, bar)) note(`choir${i}`, mt(m, bar), { freq: ch.root * p, peakDb: 9 - i * 0.5, q: 90, atk: 0.7, hold: m.bar - 1.6, rel: 0.9 }); + }); + // halo — diatonic wash + HALO.forEach((f, i) => { if (on(`halo${i}`, bar)) note(`halo${i}`, mt(m, bar), { freq: f, peakDb: 7.5, q: 70, atk: 0.9, hold: m.bar - 2.1, rel: 1.2 }); }); + // twinkle cloud + for (let j = 0; j < 10; j++) { + if (!on(`cloud${j}`, bar)) continue; + const rnd = lcg(j * 7919 + bar * 104729); + const f = PENT_HI[(j + bar) % PENT_HI.length]; + const offs = [Math.floor(rnd() * 8) * 0.5, Math.floor(rnd() * 8) * 0.5].sort((a, z) => a - z); + for (const beat of offs) + note(`cloud${j}`, mt(m, bar, beat), { freq: f, peakDb: 12, q: 90, atk: 0.012, hold: 0.08, rel: 0.22 }); + } + } + // the recapitulated theme — runs throughout, swelling at the bloom; an + // octave double joins once leadOct is on the rack + for (let p = 0; p < B / 2; p++) { + const bloom = pointsAt(p * 2) > FIN_NMAX * 0.85; + statTheme(m, "lead", p * 2, HZ.C5, { db: bloom ? 29 : 26 }); + if (on("harm", p * 2)) + for (const n of THEME) + lead(m, "harm", p * 2 + Math.floor(n.beat / 4), n.beat % 4, semi(HZ.C5, THIRD3[n.semi]), n.dur, 20); + } +} + +// ── compose all four ─────────────────────────────────────────────────── +andante(); +scherzo(); +adagio(); +finale(); + +// ── the loudness-ride target: a per-second dBFS curve, movement-aware ─── +// (each movement sits at its own level; quiet movements get mid/high +// content so K-weighting doesn't bury them. gaps target a low floor — +// they're literal zero anyway.) used by BOTH the JS ride and the C bake. +function rideTargetAt(timeSec) { + for (const m of MOVES) { + if (timeSec < m.startSec || timeSec >= m.endSec) continue; + const f = (timeSec - m.startSec) / m.dur; // 0..1 within the movement + switch (m.id) { + case "andante": return -25.5 + 3.0 * Math.min(1, f * 2); // swell in + case "scherzo": return -21.0 - 1.5 * Math.cos(f * Math.PI); // energetic, dips at edges + case "adagio": return -25.0 + 2.0 * Math.sin(f * Math.PI); // quiet, arched + case "finale": { + const frac = (m._pointsAt ? m._pointsAt(f * m.bars) : 2) / (m._nmax || 1); + return -26.0 + 8.0 * Math.sqrt(frac); // ride the count + } + } + } + return -52; // a gap: pull down (audio is silence here regardless) +} + +// ── log the shape ─────────────────────────────────────────────────────── +{ + console.log(`→ nullabata · sonata in four movements · ${(totalSec).toFixed(1)} s · ${lanes.size} lanes`); + for (const m of MOVES) + console.log(` ${m.title.padEnd(12)} ${String(m.bpm).padStart(3)} BPM · ${m.bars} bars · ${m.startSec.toFixed(1)}–${m.endSec.toFixed(1)} s`); +} + +// ── collect bands (time-sort per lane for the renderer) ────────────────── +const bands = [...lanes.values()]; +for (const b of bands) b.events.sort((x, y) => x.t0 - y.t0); + +// ── --bake: write the score for the C engine and exit ──────────────────── +{ + const bakeIdx = process.argv.indexOf("--bake"); + if (bakeIdx >= 0) { + const bakePath = process.argv[bakeIdx + 1] || resolve(HERE, "..", "out", "nullabata.score.txt"); + const L = []; + L.push(`sr ${SR}`, `dur ${totalSec}`, `detune 1.0012 0.9988`, `seed ${0xBEEF} ${0xC0FFEE}`); + L.push(`normpeak 0.9`, `fadein 0.004`, `fadeout ${TAIL}`); + const RIDE_WIN = 1.0, RIDE_N = Math.ceil(totalSec); + L.push(`ridewin ${RIDE_WIN} ${RIDE_N}`); + for (let w = 0; w < RIDE_N; w++) L.push(String(rideTargetAt((w + 0.5) * RIDE_WIN).toFixed(3))); + for (const band of bands) { + L.push(`band ${band.events.length}`); + for (const e of band.events) + L.push([e.t0, e.freq, e.freqEnd ?? 0, e.peakDb, e.q, e.atk, e.hold, e.rel, e.cut ?? -1].join(" ")); + } + mkdirSync(dirname(bakePath), { recursive: true }); + writeFileSync(bakePath, L.join("\n") + "\n"); + console.log(`✓ baked ${bands.length} bands → ${bakePath}`); + process.exit(0); + } +} + +// ── the dual-noise null engine (identical to nuellaby) ─────────────────── +function makePink(seed) { + let s = seed >>> 0; + const rnd = () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296) * 2 - 1; + let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; + return () => { + const w = rnd(); + b0 = 0.99886 * b0 + w * 0.0555179; + b1 = 0.99332 * b1 + w * 0.0750759; + b2 = 0.969 * b2 + w * 0.153852; + b3 = 0.8665 * b3 + w * 0.3104856; + b4 = 0.55 * b4 + w * 0.5329522; + b5 = -0.7616 * b5 - w * 0.016898; + const out = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362) * 0.11; + b6 = w * 0.115926; + return out; + }; +} + +const out = [new Float32Array(ns), new Float32Array(ns)]; +const DETUNE = [1.0012, 0.9988]; + +for (let ch = 0; ch < 2; ch++) { + const pink = makePink(ch ? 0xC0FFEE : 0xBEEF); + const dry = new Float32Array(BLOCK); + const wet = new Float32Array(BLOCK); + const st = bands.map(() => ({ z1: 0, z2: 0, cursor: 0, b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 })); + + for (let blockStart = 0; blockStart < ns; blockStart += BLOCK) { + const n = Math.min(BLOCK, ns - blockStart); + const time = blockStart / SR; + for (let i = 0; i < n; i++) wet[i] = dry[i] = pink(); + + for (let bi = 0; bi < bands.length; bi++) { + const band = bands[bi], s = st[bi]; + while (s.cursor < band.events.length && time > band.events[s.cursor].end + 0.4) { + s.cursor++; s.z1 = 0; s.z2 = 0; + } + const e = band.events[s.cursor]; + if (!e || time < e.t0 - 0.05) continue; + + const env = envOf(e, time); + const db = e.peakDb * env; + const ringing = Math.abs(s.z1) + Math.abs(s.z2) > 1e-9; + if (db <= 0.01 && !ringing) continue; + + let freq = e.freq; + if (e.freqEnd) { + const p = Math.min(1, Math.max(0, (time - e.t0) / (e.end - e.t0))); + freq = e.freq * Math.pow(e.freqEnd / e.freq, p); + } + const A = Math.pow(10, db / 40); + const w0 = (2 * Math.PI * Math.min(freq * DETUNE[ch], SR * 0.45)) / SR; + const alpha = Math.sin(w0) / (2 * e.q); + const cosw = Math.cos(w0); + const a0 = 1 + alpha / A; + s.b0 = (1 + alpha * A) / a0; + s.b1 = (-2 * cosw) / a0; + s.b2 = (1 - alpha * A) / a0; + s.a1 = (-2 * cosw) / a0; + s.a2 = (1 - alpha / A) / a0; + let { z1, z2 } = s; + for (let i = 0; i < n; i++) { + const x = wet[i]; + const y = s.b0 * x + z1; + z1 = s.b1 * x - s.a1 * y + z2; + z2 = s.b2 * x - s.a2 * y; + wet[i] = y; + } + s.z1 = z1; s.z2 = z2; + } + + const o = out[ch]; + for (let i = 0; i < n; i++) o[blockStart + i] = wet[i] - dry[i]; + } +} + +if (PROOF) { + let peak = 0; + for (const ch of out) for (let i = 0; i < ns; i++) peak = Math.max(peak, Math.abs(ch[i])); + console.log(peak === 0 + ? `✓ proof: all ${bands.length} bands flat → output is bit-exact silence (peak = 0)` + : `✗ proof failed: peak = ${peak}`); + process.exit(peak === 0 ? 0 : 1); +} + +// ── loudness ride: per-second window RMS onto the movement-aware target ── +{ + const WIN = 1.0; + const nWin = Math.ceil(totalSec / WIN); + const gains = []; + for (let w = 0; w < nWin; w++) { + const a = Math.floor(w * WIN * SR), z = Math.min(ns, Math.floor((w + 1) * WIN * SR)); + let sum = 0; + for (const chB of out) for (let j = a; j < z; j++) sum += chB[j] * chB[j]; + const rms = 10 * Math.log10(sum / Math.max(1, (z - a) * 2) + 1e-20); + gains.push(Math.max(-60, Math.min(30, rideTargetAt((w + 0.5) * WIN) - rms))); + } + const sm = gains.map((_, i) => { + const lo = Math.max(0, i - 1), hi = Math.min(gains.length - 1, i + 1); + let s = 0; for (let k = lo; k <= hi; k++) s += gains[k]; + return s / (hi - lo + 1); + }); + for (let i = 0; i < ns; i++) { + const pos = i / SR / WIN - 0.5; + const b0 = Math.max(0, Math.min(sm.length - 1, Math.floor(pos))); + const b1 = Math.min(sm.length - 1, b0 + 1); + const f = Math.max(0, Math.min(1, pos - b0)); + const lin = Math.pow(10, (sm[b0] * (1 - f) + sm[b1] * f) / 20); + out[0][i] *= lin; out[1][i] *= lin; + } +} + +// ── normalize + edge fades ────────────────────────────────────────────── +let peak = 0; +for (const ch of out) for (let i = 0; i < ns; i++) peak = Math.max(peak, Math.abs(ch[i])); +const g = peak > 0 ? 0.9 / peak : 1; +const fadeIn = Math.floor(0.004 * SR), fadeOut = Math.floor(TAIL * SR); +for (const ch of out) { + for (let i = 0; i < ns; i++) ch[i] *= g; + for (let i = 0; i < fadeIn; i++) ch[i] *= i / fadeIn; + for (let i = 0; i < fadeOut; i++) ch[ns - 1 - i] *= i / fadeOut; +} + +// ── write out (lullaby master, mirrors run-c.mjs) ──────────────────────── +function expandHome(p) { + if (!p) return p; + if (p === "~") return homedir(); + if (p.startsWith("~/")) return resolve(homedir(), p.slice(2)); + return p; +} +const outPath = expandHome(_argi("--out")) || resolve(HERE, "..", "out", "nullabata.mp3"); +mkdirSync(dirname(outPath), { recursive: true }); +const rawPath = `${outPath}.f32.raw`; +const buf = Buffer.alloc(ns * 2 * 4); +for (let i = 0; i < ns; i++) { + buf.writeFloatLE(out[0][i], i * 8); + buf.writeFloatLE(out[1][i], i * 8 + 4); +} +writeFileSync(rawPath, buf); + +const MASTER = [ + "highpass=f=28", + "equalizer=f=220:t=q:w=1.1:g=2.8", // low-mid shelf = body/warmth in the notes + "acompressor=threshold=-22dB:ratio=2.2:attack=25:release=220:makeup=2.0:knee=8", + "lowpass=f=15000", // tame ultrasonic hiss (no treble boost — it lifted noise) + "alimiter=limit=0.96:attack=5:release=80", +].join(","); +const ff = spawnSync("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-f", "f32le", "-ar", String(SR), "-ac", "2", "-i", rawPath, + "-af", MASTER, "-c:a", "libmp3lame", "-q:a", "2", outPath], { stdio: "inherit" }); +if (!process.argv.includes("--keep-raw")) { try { unlinkSync(rawPath); } catch {} } +if (ff.status !== 0) { console.error("✗ ffmpeg failed"); process.exit(1); } +console.log(`✓ ${outPath} (pop-mastered · ${(ns / SR).toFixed(1)} s)`); + +// ── struct.json ────────────────────────────────────────────────────────── +{ + const struct = { + _comment: "Section map for nullabata (the sonata cut): four carved-noise movements run attacca (no silence between). One lullaby motif, transformed per movement; high-Q bells so voices read as tones, not static.", + meter: 4, bpm: MOV.finale.bpm, scale: "major", rootMidi: 60, + totalSec: +(ns / SR).toFixed(6), prerollSec: 0, + sections: MOVES.map((m) => ({ + name: m.id, title: m.title, bpm: m.bpm, + startSec: +m.startSec.toFixed(6), endSec: +m.endSec.toFixed(6), + })), + }; + writeFileSync(resolve(HERE, "..", "out", "nullabata.struct.json"), + JSON.stringify(struct, null, 2) + "\n"); +} diff --git a/pop/out/pop-library.json b/pop/out/pop-library.json --- a/pop/out/pop-library.json +++ b/pop/out/pop-library.json @@ -1,6 +1,6 @@ { - "generated": "2026-06-15T17:11:43.457Z", - "count": 91, + "generated": "2026-06-21T21:51:59.102Z", + "count": 92, "tracks": [ { "path": "/Users/jas/aesthetic-computer/pop/momboba/out/momabobasheep.mp3", @@ -9,7 +9,7 @@ "lane": "momboba", "backend": "C engine", "status": "RELEASED", "updated": "2026-06-12T03:15:25.590Z", - "revisions": 4, + "revisions": 7, "bytes": 25933009, "durationSec": 600, "bpm": null, @@ -18,7 +18,7 @@ "releaseDate": "2026-06-12", "links": { "spotify": null, "apple": null, - "youtube": null, + "youtube": "https://youtu.be/GCOhg5J-brw>", "distrokid": "https://distrokid.com/dashboard/album/?albumuuid=2456D650-9AEC-4C1A-831BCE628047AF6C>." } }, @@ -49,7 +49,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "RELEASED", "updated": "2026-05-23T03:50:50.812Z", - "revisions": 9, + "revisions": 10, "bytes": 1946733, "durationSec": 83.6, "bpm": 56, @@ -129,7 +129,7 @@ "lane": "teknull", "backend": "JS DSP", "status": "WIP", "updated": "2026-06-11T23:43:42.582Z", - "revisions": 0, + "revisions": 1, "bytes": 4447749, "durationSec": 120, "bpm": 140, @@ -149,7 +149,7 @@ "lane": "nullabye", "backend": "C engine", "status": "WIP", "updated": "2026-06-11T23:03:46.120Z", - "revisions": 0, + "revisions": 1, "bytes": 2666805, "durationSec": 120, "bpm": 76, @@ -169,7 +169,7 @@ "lane": "nullabye", "backend": "C engine", "status": "WIP", "updated": "2026-06-11T21:23:24.127Z", - "revisions": 0, + "revisions": 1, "bytes": 1561845, "durationSec": 78.8, "bpm": 76, @@ -183,13 +183,43 @@ "distrokid": null } }, { + "path": "/Users/jas/aesthetic-computer/pop/americomputadora/out/americomputadora.mp3", + "title": "americomputadora", + "lane": "americomputadora", + "backend": "C engine", + "status": "UNRELEASED", + "updated": "2026-06-21T02:18:15.307Z", + "revisions": 4, + "bytes": 9196946, + "durationSec": 186.9, + "bpm": null, + "key": null, + "releaseDate": null, + "links": null + }, + { + "path": "/Users/jas/aesthetic-computer/pop/menuband/out/vo.mp3", + "title": "vo", + "lane": "menuband", + "backend": "JS DSP", + "status": "UNRELEASED", + "updated": "2026-06-17T05:58:08.146Z", + "revisions": 5, + "bytes": 229985, + "durationSec": 34.4, + "bpm": null, + "key": null, + "releaseDate": null, + "links": null + }, + { "path": "/Users/jas/aesthetic-computer/pop/cornerfive/out/cornerfive-footwork.mp3", "title": "cornerfive-footwork", "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", "updated": "2026-06-15T04:43:55.552Z", - "revisions": 1, + "revisions": 2, "bytes": 5704056, "durationSec": 84, "bpm": null, @@ -204,7 +234,7 @@ "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", "updated": "2026-06-15T04:43:48.172Z", - "revisions": 1, + "revisions": 2, "bytes": 5364136, "durationSec": 84, "bpm": null, @@ -219,7 +249,7 @@ "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", "updated": "2026-06-15T04:43:42.670Z", - "revisions": 1, + "revisions": 2, "bytes": 5301856, "durationSec": 84, "bpm": null, @@ -234,7 +264,7 @@ "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", "updated": "2026-06-15T04:34:38.882Z", - "revisions": 1, + "revisions": 2, "bytes": 5549371, "durationSec": 84, "bpm": null, @@ -774,7 +804,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:50:35.604Z", - "revisions": 4, + "revisions": 5, "bytes": 4876903, "durationSec": 121.9, "bpm": null, @@ -789,7 +819,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:50:33.302Z", - "revisions": 4, + "revisions": 5, "bytes": 5122663, "durationSec": 128, "bpm": null, @@ -804,7 +834,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:50:21.338Z", - "revisions": 4, + "revisions": 5, "bytes": 4935463, "durationSec": 123.3, "bpm": null, @@ -819,7 +849,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:50:09.907Z", - "revisions": 4, + "revisions": 5, "bytes": 4791464, "durationSec": 119.7, "bpm": null, @@ -834,7 +864,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:48:00.764Z", - "revisions": 4, + "revisions": 5, "bytes": 4578344, "durationSec": 114.4, "bpm": null, @@ -849,7 +879,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:47:52.284Z", - "revisions": 4, + "revisions": 5, "bytes": 4894183, "durationSec": 122.3, "bpm": null, @@ -864,7 +894,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:47:50.566Z", - "revisions": 4, + "revisions": 5, "bytes": 4904744, "durationSec": 122.6, "bpm": null, @@ -879,7 +909,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:47:43.717Z", - "revisions": 4, + "revisions": 5, "bytes": 4890343, "durationSec": 122.2, "bpm": null, @@ -894,7 +924,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-14T00:44:27.780Z", - "revisions": 4, + "revisions": 5, "bytes": 5042022, "durationSec": 126, "bpm": null, @@ -909,7 +939,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-13T06:28:06.994Z", - "revisions": 4, + "revisions": 5, "bytes": 5522023, "durationSec": 138, "bpm": null, @@ -924,7 +954,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-13T06:00:22.146Z", - "revisions": 4, + "revisions": 5, "bytes": 5718822, "durationSec": 142.9, "bpm": null, @@ -939,7 +969,7 @@ "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", "updated": "2026-06-12T06:10:38.819Z", - "revisions": 4, + "revisions": 5, "bytes": 5718765, "durationSec": 142.9, "bpm": null, @@ -948,21 +978,6 @@ "releaseDate": null, "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/americomputadora/out/americomputadora.mp3", - "title": "americomputadora", - "lane": "americomputadora", - "backend": "C engine", - "status": "UNRELEASED", - "updated": "2026-06-10T16:38:54.484Z", - "revisions": 1, - "bytes": 7479489, - "durationSec": 186.9, - "bpm": null, - "key": null, - "releaseDate": null, - "links": null - }, - { "path": "/Users/jas/aesthetic-computer/pop/hopehop/out/hopehop.mp3", "title": "hopehop", "lane": "hopehop", @@ -984,7 +999,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T02:30:24.224Z", - "revisions": 9, + "revisions": 10, "bytes": 789309, "durationSec": 71.8, "bpm": null, @@ -999,7 +1014,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T02:29:27.446Z", - "revisions": 9, + "revisions": 10, "bytes": 499677, "durationSec": 61.2, "bpm": null, @@ -1014,7 +1029,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:43:08.116Z", - "revisions": 9, + "revisions": 10, "bytes": 557493, "durationSec": 72.9, "bpm": null, @@ -1029,7 +1044,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:42:38.072Z", - "revisions": 9, + "revisions": 10, "bytes": 407685, "durationSec": 66.7, "bpm": null, @@ -1044,7 +1059,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:42:03.916Z", - "revisions": 9, + "revisions": 10, "bytes": 474597, "durationSec": 47.8, "bpm": null, @@ -1059,7 +1074,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:41:23.776Z", - "revisions": 9, + "revisions": 10, "bytes": 709773, "durationSec": 74.2, "bpm": null, @@ -1074,7 +1089,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:41:14.509Z", - "revisions": 9, + "revisions": 10, "bytes": 376653, "durationSec": 40, "bpm": null, @@ -1089,7 +1104,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:40:53.052Z", - "revisions": 9, + "revisions": 10, "bytes": 486861, "durationSec": 60.2, "bpm": null, @@ -1104,7 +1119,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:40:06.621Z", - "revisions": 9, + "revisions": 10, "bytes": 464373, "durationSec": 62.6, "bpm": null, @@ -1119,7 +1134,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:40:00.561Z", - "revisions": 9, + "revisions": 10, "bytes": 409149, "durationSec": 44.9, "bpm": null, @@ -1134,7 +1149,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:39:24.405Z", - "revisions": 9, + "revisions": 10, "bytes": 599853, "durationSec": 50.7, "bpm": null, @@ -1149,7 +1164,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:39:23.661Z", - "revisions": 9, + "revisions": 10, "bytes": 584829, "durationSec": 44.7, "bpm": null, @@ -1164,7 +1179,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:38:59.662Z", - "revisions": 9, + "revisions": 10, "bytes": 438381, "durationSec": 31.8, "bpm": null, @@ -1179,7 +1194,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:38:41.062Z", - "revisions": 9, + "revisions": 10, "bytes": 574173, "durationSec": 62.1, "bpm": null, @@ -1194,7 +1209,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:37:55.193Z", - "revisions": 9, + "revisions": 10, "bytes": 525765, "durationSec": 53.2, "bpm": null, @@ -1209,7 +1224,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:37:40.850Z", - "revisions": 9, + "revisions": 10, "bytes": 481725, "durationSec": 41.9, "bpm": null, @@ -1224,7 +1239,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-23T01:24:08.226Z", - "revisions": 9, + "revisions": 10, "bytes": 587685, "durationSec": 62, "bpm": null, @@ -1239,7 +1254,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-22T22:09:18.722Z", - "revisions": 9, + "revisions": 10, "bytes": 6693981, "durationSec": 369.7, "bpm": null, @@ -1269,7 +1284,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-21T16:00:37.912Z", - "revisions": 9, + "revisions": 10, "bytes": 3205215, "durationSec": 60, "bpm": null, @@ -1284,7 +1299,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-21T15:59:47.418Z", - "revisions": 9, + "revisions": 10, "bytes": 3440990, "durationSec": 78, "bpm": null, @@ -1299,7 +1314,7 @@ "lane": "marimba", "backend": "marimba synth (JS)", "status": "UNRELEASED", "updated": "2026-05-21T15:59:35.032Z", - "revisions": 9, + "revisions": 10, "bytes": 3763402, "durationSec": 106.4, "bpm": null, -- tangled.sh