Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723#!/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.//// ── 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//// Mallet voices (unchanged from the original hop):// xylophone — the singer; bright daytime syllabic lead// 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// node pop/marimba/bin/render-flutterbap.mjs --out ~/flutterbap.mp3//// 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, renderHat } from "../synths/perc.mjs";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 _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;
// ── 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;// extended coda after the landing: cave (4) → squabble (2) → chord// progression + button (5) = 11 bars.const OUTRO_BARS = 11;const TOTAL_BARS = INTRO_BARS + BODY_BARS + OUTRO_BARS; // 47const totalSec = TOTAL_BARS * BAR + 2.8; // + tail for the final ringconst ns = Math.ceil(totalSec * SR);
// ── 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 appliedconst t = (bar, beat = 0) => bar * BAR + swingBeat(beat) * BEAT;// body bar (1-based) → absolute timeconst 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");// ── loop mode ──────────────────────────────────────────────────────────// --loop renders a seam-perfect loop: the file is trimmed to an exact whole// number of bars (so the downbeat lands on the loop point), the decay tail// that rings past the boundary is folded back onto the head (so the final// button + reverb continues into the next pass instead of being cut), and// the intro/outro fades are skipped (a fade would break the seam). Use the// .distrokid.wav for truly gapless playback — MP3 carries encoder padding.const LOOP = process.argv.includes("--loop");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 ≥0const vel = (g, amt) => (HUMANIZE ? Math.max(0, g * (1 + hr2() * amt)) : g);
// ── note table ────────────────────────────────────────────────────────const N = { 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, C6: 84, D6: 86, E6: 88, F6: 89, G6: 91,};
// ── output buffers — one mono bus per voice, panned at mixdown ────────const leadBuf = new Float32Array(ns); // xylophone — the singerconst bassBuf = new Float32Array(ns); // bass marimba — bouncing ballconst sparkBuf = new Float32Array(ns); // kalimba — off-beat twinklesconst bellBuf = new Float32Array(ns); // glockenspiel — high sparkleconst vibBuf = new Float32Array(ns); // vibraphone — sustained padconst riseBuf = new Float32Array(ns); // reverse sine-bell risers (breaks)const scrBuf = new Float32Array(ns); // turntable scratchesconst screamBuf = new Float32Array(ns); // scratchy kitten screamsconst kickBuf = new Float32Array(ns); // kick drumconst subBuf = new Float32Array(ns); // bass-perc — gritty pitched subconst snareBuf = new Float32Array(ns); // snareconst hatBuf = new Float32Array(ns); // hats (closed + open)const shakBuf = new Float32Array(ns); // shakerconst caveBuf = new Float32Array(ns); // CAVE section bus — gets a big Schroeder reverb at mixdown
// ── mallet voice routes (humanized: ±ms timing, ±% velocity) ──────────function lead(t0, midi, beats, gain = 1.0) { mixEventMarimba({ startSec: jit(t0, 13), midi, durSec: beats * BEAT, gain: vel(gain, 0.14), preset: "xylophone", decayMul: 1.15 }, leadBuf, { sampleRate: SR });}function bass(t0, midi, beats, gain = 0.85) { mixEventMarimba({ startSec: jit(t0, 9), midi, durSec: beats * BEAT, gain: vel(gain, 0.12), preset: "bass", decayMul: 0.55 }, bassBuf, { sampleRate: SR });}function spark(t0, midi, beats, gain = 0.55) { mixEventMarimba({ startSec: jit(t0, 14), midi, durSec: beats * BEAT, gain: vel(gain, 0.16), preset: "kalimba", decayMul: 0.95 }, sparkBuf, { sampleRate: SR });}function bell(t0, midi, beats, gain = 0.65) { mixEventMarimba({ startSec: jit(t0, 10), midi, durSec: beats * BEAT, gain: vel(gain, 0.12), preset: "glockenspiel", decayMul: 1.35 }, bellBuf, { sampleRate: SR });}// vibraphone pad — long soft sustain, motor off, for held chord bedsfunction 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 });}
// ── 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 = {}) { // deeper kick: lower sub fundamental (41 Hz vs 48) + longer body + a touch // more drive, so it lands with real chest weight. Per-call `o` still wins. mixKick({ startSec: jit(t(ab, beat), 4), gain: vel(g, 0.10), fEnd: 41, ampDecay: 0.46, pitchDecay: 0.062, drive: 1.75, ...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 });}// ── reverse HAT — an open hat played backwards so it SWELLS up into the// beat and snaps off on the downbeat. Lands at t(landAb, landBeat). ──function rhat(targetBuf, landAb, landBeat, g = 0.32) { const seg = renderHat({ gain: g, open: true }, { sampleRate: SR }); const startIdx = Math.floor(t(landAb, landBeat) * SR) - seg.length; for (let i = 0; i < seg.length; i++) { const d = startIdx + i; if (d >= 0 && d < targetBuf.length) targetBuf[d] += seg[seg.length - 1 - i]; // reversed }}// ── Schroeder reverb (4 parallel feedback combs → 2 series allpasses),// applied in place to a bus. Used to drench the CAVE section. ──function reverbInPlace(buf, { wet = 0.6, decay = 0.8 } = {}) { const combDelays = [0.0297, 0.0371, 0.0411, 0.0437], apDelays = [0.005, 0.0017]; const out = new Float32Array(buf.length); for (const cd of combDelays) { const d = Math.max(1, Math.floor(cd * SR)), ring = new Float32Array(d); for (let i = 0; i < buf.length; i++) { const y = ring[i % d]; out[i] += y; ring[i % d] = buf[i] + y * decay; } } for (let i = 0; i < out.length; i++) out[i] /= combDelays.length; for (const ad of apDelays) { const d = Math.max(1, Math.floor(ad * SR)), ring = new Float32Array(d), g = 0.7; for (let i = 0; i < out.length; i++) { const bd = ring[i % d], y = -g * out[i] + bd; ring[i % d] = out[i] + g * y; out[i] = y; } } for (let i = 0; i < buf.length; i++) buf[i] = buf[i] * (1 - wet) + out[i] * wet;}
// ── 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 offbeatsfunction 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: "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: "cave", bar0: 36, bars: 4 }, { name: "squabble", bar0: 40, bars: 2 }, { name: "progression", bar0: 42, bars: 5 }, // 42–45 + button at 46];
// ══════════════════════════════════════════════════════════════════════// 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);L(2, 0, N.C6, 1); L(2, 1, N.E6, 1); L(2, 2, N.C6, 2);L(3, 0, N.D5, 1); L(3, 1, N.G5, 1); L(3, 2, N.B5, 2);L(4, 0, N.C6, 2); L(4, 2, N.G5, 2);
// § palofmine (5–8) — "but-ter-FLY / pal-of-MINE / fly-with-me / BY-the-blue"L(5, 0, N.E5, 1); L(5, 1, N.G5, 1); L(5, 2, N.C6, 2);L(6, 0, N.A5, 1); L(6, 1, N.G5, 1); L(6, 2, N.F5, 2);L(7, 0, N.E5, 1); L(7, 1, N.F5, 1); L(7, 2, N.G5, 1); L(7, 3, N.A5, 1);L(8, 0, N.C6, 4);
// § mommywow (9–12) — held wonderL(9, 0, N.G5, 4, 0.9);L(10, 0, N.C6, 1); L(10, 1, N.E6, 1); L(10, 2, N.G6, 2);L(11, 0, N.G5, 4, 0.9);L(12, 0, N.G6, 1); L(12, 1, N.E6, 1); L(12, 2, N.C6, 1); L(12, 3, N.G6, 1);
// § slinky (13–20) — wobbly springingL(13, 0, N.C5, 1); L(13, 1, N.E5, 1); L(13, 2, N.G5, 2);L(14, 0, N.G5, 1); L(14, 1, N.F5, 1); L(14, 2, N.E5, 1); L(14, 3, N.D5, 1);L(15, 0, N.C5, 1); L(15, 1, N.E5, 1); L(15, 2, N.G5, 2);// bar 16 — ascending scale, eighth-notesL(16, 0, N.C5, 0.5); L(16, 0.5, N.D5, 0.5);L(16, 1, N.E5, 0.5); L(16, 1.5, N.F5, 0.5);L(16, 2, N.G5, 0.5); L(16, 2.5, N.A5, 0.5);L(16, 3, N.B5, 0.5); L(16, 3.5, N.C6, 0.5);// bar 17 — descending scale, eighth-notesL(17, 0, N.C6, 0.5); L(17, 0.5, N.B5, 0.5);L(17, 1, N.A5, 0.5); L(17, 1.5, N.G5, 0.5);L(17, 2, N.F5, 0.5); L(17, 2.5, N.E5, 0.5);L(17, 3, N.D5, 0.5); L(17, 3.5, N.C5, 0.5);L(18, 0, N.C5, 1); L(18, 1, N.E5, 1); L(18, 2, N.G5, 1); L(18, 3, N.C6, 1);L(19, 0, N.C6, 1); L(19, 1, N.A5, 1); L(19, 2, N.F5, 1); L(19, 3, N.D5, 1);L(20, 0, N.E5, 1); L(20, 1, N.G5, 1); L(20, 2, N.C5, 2);
// § fly (21–28) — "lately-when-I-FLY"L(21, 0, N.C5, 1); L(21, 1, N.D5, 1); L(21, 2, N.E5, 1); L(21, 3, N.G5, 1);L(22, 0, N.C6, 4);L(23, 0, N.E5, 1); L(23, 1, N.F5, 1); L(23, 2, N.G5, 1); L(23, 3, N.A5, 1);L(24, 0, N.D6, 4);L(25, 0, N.E6, 1); L(25, 1, N.D6, 1); L(25, 2, N.C6, 1); L(25, 3, N.G5, 1);L(26, 0, N.A5, 1); L(26, 1, N.C6, 1); L(26, 2, N.G6, 2);L(27, 0, N.G6, 1); L(27, 1, N.E6, 1); L(27, 2, N.C6, 1); L(27, 3, N.A5, 1);L(28, 0, N.G5, 1); L(28, 1, N.E5, 1); L(28, 2, N.C5, 2);
// § land (29–32) — settlingL(29, 0, N.C6, 1); L(29, 1, N.G5, 1); L(29, 2, N.E5, 1); L(29, 3, N.C5, 1);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 (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 floorfor (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); // skip grace}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 — 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);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);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); vib(tb(bar, b), midis[0] - 12, d, g * 0.72); // octave-down root — fuller range + warmth under the lead}V(5, 0, [N.C4, N.E4, N.G4], 4); // C under pal-of-mineV(6, 0, [N.F3, N.A3, N.C4], 2); // FV(7, 0, [N.G3, N.B3, N.D4], 2); // GV(8, 0, [N.C4, N.E4, N.G4], 4); // CV(9, 0, [N.C4, N.E4, N.G4], 8, 0.30); // long bed across mommy-wowV(21, 0, [N.C4, N.E4, N.G4], 4); // C, the fly soarV(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): start with PRESENCE, then build a clear arrival ──// (was a slow fade-in from near-silence — felt hidden. Now the groove is// already moving at bar 0 and crescendos into the hook at abs bar 4 ≈ 7.7s,// which is where the song "really starts".)// bar 0 — kick + bass-perc + shaker + a soft bell beacon set the key, movingKICK(0, 0, 0.82); KICK(0, 2, 0.64);BP(0, 0, N.C2, 1.7, 0.8, { drive: 1.9 }); BP(0, 2.5, N.G1, 1.0, 0.68);for (let e = 0; e < 4; e++) SHK(0, e + 0.5, 0.2);hats8(0, 0.16);bell(t(0, 0), N.C6, 3, 0.34); bell(t(0, 2), N.G5, 2, 0.3); // soft tonal beacon// bar 1 — fuller kick + hats, bass bouncesKICK(1, 0, 0.9); KICK(1, 2, 0.76);BP(1, 0, N.C2, 1.6, 0.8); BP(1, 2.5, N.G1, 1.0, 0.72);hats8(1, 0.22);spark(t(1, 3.5), N.E6, 0.5, 0.4); // kalimba twinkle// bar 2 — snare backbeat enters, bass-perc bouncesKICK(2, 0, 0.94); KICK(2, 2, 0.82);SNR(2, 2, 0.62);BP(2, 0, N.C2, 1.4, 0.82); BP(2, 1.5, N.C2, 0.5, 0.64); BP(2, 2.5, N.G1, 1.2, 0.74);hats8(2, 0.26);spark(t(2, 3.5), N.G5, 0.5, 0.45); // kalimba pickup// bar 3 — full preview + a crescendo fill spilling into the dropKICK(3, 0, 0.96); KICK(3, 2, 0.84);SNR(3, 1, 0.56); SNR(3, 3, 0.6);BP(3, 0, N.C2, 1.4, 0.82); BP(3, 2.5, N.G1, 0.8, 0.74);hats8(3, 0.28);for (const fb of [3, 3.25, 3.5, 3.75]) SNR(3, fb, 0.4 + (fb - 3) * 0.6); // crescendo fillspark(t(3, 3.5), N.G5, 0.5, 0.48); spark(t(3, 3.75), N.C6, 0.5, 0.55); // kalimba run-up
// ── 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 Cfor (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
// ══════════════════════════════════════════════════════════════════════// EXTENDED CODA — cave → squabble → chord progression → resolve (abs 36–46)// After the landing the track drifts into a reverby cave, erupts in a// squeak-scratch squabble, then a chord progression walks it home to a// final resolved button.// ══════════════════════════════════════════════════════════════════════
// ── CAVE (abs 36–39): sparse + drenched. Everything routes into caveBuf,// which gets a big Schroeder reverb at mixdown. Reverse swells (bell + hat// + kick) breathe in and out of the dark. ──{ const caveVib = (tt, m, beats, g) => mixEventMarimba({ startSec: tt, midi: m, durSec: beats * BEAT, gain: g, preset: "vibraphone_off", decayMul: 1.5 }, caveBuf, { sampleRate: SR }); const caveLead = (tt, m, beats, g) => mixEventMarimba({ startSec: tt, midi: m, durSec: beats * BEAT, gain: g, preset: "xylophone", decayMul: 1.4 }, caveBuf, { sampleRate: SR }); for (const m of [N.A3, N.C4, N.E4, N.A4]) caveVib(t(36, 0), m, 7, 0.30); // Am pad floor for (const m of [N.F3, N.A3, N.C4]) caveVib(t(38, 0), m, 7, 0.28); // F caveLead(t(36, 1), N.E5, 1, 0.50); caveLead(t(36, 2.5), N.A5, 1.5, 0.46); // lost butterfly motif caveLead(t(38, 1), N.C6, 1, 0.46); caveLead(t(38, 2.5), N.G5, 1.5, 0.42); mixReverseBell({ landSec: t(36, 0), dur: 1.4 * BAR, midis: [N.A5, N.C6, N.E6], gain: 0.40 }, caveBuf, { sampleRate: SR }); mixReverseBell({ landSec: t(38, 2), dur: 1.0 * BAR, midis: [N.E6, N.G6], gain: 0.32 }, caveBuf, { sampleRate: SR }); mixKick({ startSec: t(36, 0), gain: 0.45, fEnd: 38, ampDecay: 0.6 }, caveBuf, { sampleRate: SR }); // distant heartbeat mixKick({ startSec: t(37, 2), gain: 0.45, fEnd: 38, ampDecay: 0.6 }, caveBuf, { sampleRate: SR }); mixKick({ startSec: t(39, 0), gain: 0.5, fEnd: 38, ampDecay: 0.7 }, caveBuf, { sampleRate: SR }); mixReverseKick({ landSec: t(38, 0), dur: 1.0 * BAR, gain: 0.4 }, caveBuf, { sampleRate: SR }); rhat(caveBuf, 37, 0, 0.32); rhat(caveBuf, 39, 0, 0.34); // reverse hats swelling in mixHat({ startSec: t(36, 2), gain: 0.18, open: true }, caveBuf, { sampleRate: SR }); mixHat({ startSec: t(38, 2), gain: 0.18, open: true }, caveBuf, { sampleRate: SR });}
// ── SQUABBLE (abs 40–41): a light, playful squeak-and-scratch break.// Tamed from the old feral fight (dense, loud, dry shrieks): now just a// couple of soft kitten chirps trading with a scratch over a continuing// groove that cadences Am → G — vi → V leading cleanly into the C of the// progression. Musical and brief, not abrasive or abrupt. ──groove(40, 0, 0.8); groove(41, 1, 0.82); // groove keeps moving — no holeBP(40, 0, N.A1, 1.5, 0.7); BP(40, 2, N.A1, 1.0, 0.64); // AmBP(41, 0, N.G1, 1.5, 0.72); BP(41, 2, N.G1, 1.0, 0.66); // G (V into the progression's C)SCREAM(40, 1.5, 84, 0.30, { bend: 6, rasp: 0.45, dur: 0.34 }); SC(40, 2.5, "baby", 0.40, 52);SCREAM(41, 1.5, 88, 0.30, { bend: 7, rasp: 0.42, dur: 0.32 }); SC(41, 3, "chirp", 0.40, 50);for (const fb of [3, 3.5]) SNR(41, fb, 0.4 + (fb - 3) * 0.4); // soft fill into the progression
// ── PROGRESSION (abs 42–45): a real chord progression walks it home.// C → G → Am → F (I–V–vi–IV); the button then resolves to C. Vibraphone// triads + bass-perc roots + a butterfly-motif reprise riding the changes. ──const PROG = [ { root: N.C2, triad: [N.C4, N.E4, N.G4] }, // C { root: N.G1, triad: [N.G3, N.B3, N.D4] }, // G { root: N.A1, triad: [N.A3, N.C4, N.E4] }, // Am { root: N.F1, triad: [N.F3, N.A3, N.C4] }, // F];const PROG_TOP = [N.C6, N.B5, N.A5, N.A5];for (let i = 0; i < 4; i++) { const ab = 42 + i, ch = PROG[i]; groove(ab, 1, 0.86); BP(ab, 0, ch.root, 1.5, 0.76); BP(ab, 2, ch.root, 1.0, 0.70); BP(ab, 3, ch.root, 0.8, 0.60); for (const m of ch.triad) vib(t(ab, 0), m, 4, 0.34); vib(t(ab, 0), ch.triad[0] - 12, 4, 0.24); // octave-down warmth lead(t(ab, 0), PROG_TOP[i], 1, 0.90); // motif riding the changes lead(t(ab, 1), ch.triad[2], 1, 0.80); lead(t(ab, 2), ch.triad[1] + 12, 2, 0.82); spark(t(ab, 3.5), N.E6, 0.5, 0.40); bell(t(ab, 0), ch.triad[2] + 12, 2, 0.40);}rhat(hatBuf, 46, 0, 0.42); // reverse-hat swell into the button
// ── BUTTON (abs 46): final resolved C hit + ring ──KICK(46, 0, 1.0); BP(46, 0, N.C2, 3.2, 0.86, { decay: 1.4 });SNR(46, 0, 0.5);bell(t(46, 0), N.C6, 4, 0.5); bell(t(46, 0.06), N.E6, 4, 0.4); bell(t(46, 0.12), N.G6, 4, 0.35);vib(t(46, 0), N.C4, 4, 0.4); vib(t(46, 0), N.E4, 4, 0.35); vib(t(46, 0), N.G4, 4, 0.3);lead(t(46, 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(4, 1.5 * BAR, [N.G5, N.C6, N.E6, N.G6], 0.44); // intro → THE START: lands the hook at ~7.7srise(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// reverse kicks — the low-end whoosh under the risersriseKick(4, 1.0 * BAR, 0.55); // arrival whoosh into the hookriseKick(16, 1.4 * BAR, 0.7); // big slinky dropriseKick(24, 1.0 * BAR, 0.5); // fly soar// a bright bell chime right on the hook downbeat — the song clearly "starts"bell(t(4, 0), N.C6, 3, 0.55); bell(t(4, 0.05), N.E6, 3, 0.42); bell(t(4, 0.10), N.G6, 3, 0.34);
// ── 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 dropSC(16, 0, "transform", 0.72, 50); // land on the slinky dropfor (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 entrancefor (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
// ── 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 dropSCREAM(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 soarSCREAM(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 peakSCREAM(39, 0, 88, 0.38, { bend: 8, rasp: 0.55, dur: 0.5 }); // last soft cry out of the cave
// ═══════════════════════════════════════════════════════════════════// STEREO MIXDOWN// ═══════════════════════════════════════════════════════════════════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 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; }}// Wider, clearer image: lead + low end stay centred; the harmonic/sparkle// voices (vib, glock, kalimba) are pushed out AND lifted so they read as their// own parts instead of hiding under the lead.place(leadBuf, 0.00, 1.02); // the singer, dead centre + a touch more presentplace(bassBuf, 0.00, 0.72); // mallet bass eases back — the bass-perc owns the lowplace(vibBuf, -0.24, 0.86); // pad: wider + much more audible (was buried)place(riseBuf, 0.00, 0.85); // reverse-bell risers, centred + wideplace(scrBuf, 0.34, 0.80); // scratches, off to the DJ handplace(screamBuf,-0.50, 0.62); // kitten screams, wide-left (the squeaks!) — kept proudplace(sparkBuf, 0.44, 0.88); // kalimba twinkles, hard-ish right + brighterplace(bellBuf, -0.42, 0.86); // glockenspiel halo, left + brighterplace(kickBuf, 0.00, 1.02); // deeper kick, centredplace(subBuf, 0.00, 0.98); // bass-perc — the groove backbone, centredplace(snareBuf, 0.04, 0.80);place(hatBuf, 0.34, 0.58);place(shakBuf, -0.34, 0.48);// CAVE bus — drench in Schroeder reverb, then place centred + presentreverbInPlace(caveBuf, { wet: 0.62, decay: 0.82 });place(caveBuf, 0.00, 0.92);
// scrub non-finite + peak-normaliselet nan = 0;for (let i = 0; i < ns; i++) { if (!Number.isFinite(outL[i])) { outL[i] = 0; nan++; } if (!Number.isFinite(outR[i])) { outR[i] = 0; nan++; }}if (nan) console.warn(` ! scrubbed ${nan} non-finite samples`);
// ── loop fold ──────────────────────────────────────────────────────────// In --loop mode the file is an exact whole number of bars. Any tail that// rings past the loop boundary (final button + reverb) is folded back onto// the head so it continues seamlessly into the next pass. outN is the span// we keep + master + write.let outN = ns;if (LOOP) { const loopN = Math.round(TOTAL_BARS * BAR * SR); for (let i = loopN; i < ns; i++) { const d = i - loopN; if (d < loopN) { outL[d] += outL[i]; outR[d] += outR[i]; } } outN = loopN;}
let peak = 0;for (let i = 0; i < outN; 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.84 / peak; for (let i = 0; i < outN; i++) { outL[i] *= nrm; outR[i] *= nrm; } }
let trimN;if (LOOP) { // no trailing-silence trim, no fades — the seam must stay sample-accurate. trimN = outN;} else { // trim trailing silence + tiny fades let lastLoud = outN - 1; while (lastLoud > 0 && Math.abs(outL[lastLoud]) < 0.004 && Math.abs(outR[lastLoud]) < 0.004) lastLoud--; trimN = Math.min(outN, lastLoud + Math.floor(0.6 * SR)); const fadeIn = Math.floor(0.004 * SR); 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; }}
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"}${LOOP ? " · LOOP (seam-folded)" : ""}`);
// ── write out ─────────────────────────────────────────────────────────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", LOOP ? "flutterbap-loop.mp3" : "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); }writeFileSync(rawPath, b);
// 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=26", "equalizer=f=48:t=q:w=0.9:g=2.7", // deep sub weight under the deeper kick + bass-perc "equalizer=f=90:t=q:w=1.0:g=1.2", // kick/bass body punch "equalizer=f=240:t=q:w=1.1:g=-1.3", // de-mud "equalizer=f=3000:t=q:w=1.5:g=0.9", // mallet presence "treble=g=1.4: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, "-af", MASTER, "-c:a", "libmp3lame", "-q:a", "2", outPath], { stdio: "inherit" });if (ff.status !== 0) { try { unlinkSync(rawPath); } catch {} console.error("✗ ffmpeg failed"); process.exit(1); }console.log(`✓ ${outPath} (pop-mastered · ${(trimN / SR).toFixed(1)} s)`);
// ── DistroKid WAV master — same chain → loudnorm to -14 LUFS / -1.5 TP,// 44.1 kHz / 16-bit PCM. Gentle target keeps the squeaks + scratches alive.const wavOut = outPath.replace(/\.mp3$/i, "") + ".distrokid.wav";const ffw = spawnSync("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-f", "f32le", "-ar", String(SR), "-ac", "2", "-i", rawPath, "-af", `${MASTER},loudnorm=I=-14:TP=-1.5:LRA=11`, "-ar", "44100", "-ac", "2", "-c:a", "pcm_s16le", wavOut], { stdio: "inherit" });try { unlinkSync(rawPath); } catch {}if (ffw.status === 0) console.log(`✓ ${wavOut} (DistroKid master · 44.1 kHz / 16-bit · -14 LUFS)`);else console.error("✗ DistroKid WAV master failed");
// ── struct.json — section map for any future visualizer ───────────────{ const structTotalSec = trimN / SR; const struct = { _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) => ({ name: s.name, startSec: +t(s.bar0, 0).toFixed(6), endSec: +Math.min(structTotalSec, t(s.bar0 + s.bars, 0)).toFixed(6), })), }; writeFileSync(resolve(HERE, "..", "out", "flutterbap.struct.json"), JSON.stringify(struct, null, 2) + "\n");}