Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980#!/usr/bin/env node// beat.mjs — synthesize a low-key kick track from the waltz timing,// one kick on every bar's beat 1. Mixed under the narration in// compose.fish for a touch of musical pulse.//// Reads: recap/out/waltz-events.json (for bpm + totalSec)// Writes: recap/out/beat.mp3//// Kick: 60Hz sine, 0.18s, exponential amplitude decay. Quiet enough// to sit under the narration without competing.//// Usage: node bin/beat.mjs
import { readFileSync, writeFileSync, existsSync } from "node:fs";import { resolve, dirname } from "node:path";import { fileURLToPath } from "node:url";import { spawnSync } from "node:child_process";
const HERE = dirname(fileURLToPath(import.meta.url));const ROOT = resolve(HERE, "..");const eventsPath = `${ROOT}/out/waltz-events.json`;const rawPath = `${ROOT}/out/beat.f32.raw`;const outPath = `${ROOT}/out/beat.mp3`;
if (!existsSync(eventsPath)) { console.error(`✗ missing ${eventsPath}`); process.exit(1); }const { bpm, totalSec } = JSON.parse(readFileSync(eventsPath, "utf8"));const SR = 48000;const beatSec = 60 / bpm;const barSec = beatSec * 3;const totalSamples = Math.ceil((totalSec + 1) * SR);const out = new Float32Array(totalSamples);
// Kick voice — 60 Hz sine, exponential pitch+amp decay (drum-like).const kickDur = 0.18;const kickGain = 0.35;function placeKick(startSec) { const start = Math.floor(startSec * SR); const len = Math.floor(kickDur * SR); for (let i = 0; i < len; i++) { const dst = start + i; if (dst < 0 || dst >= out.length) continue; const t = i / SR; // pitch sweep: 90Hz → 50Hz across the kick (helps it feel "round") const f = 90 - 40 * (t / kickDur); const env = Math.exp(-t / 0.06); // exponential amplitude decay out[dst] += Math.sin(2 * Math.PI * f * t) * env * kickGain; }}
// Place a kick on every bar's beat 1.const nBars = Math.floor(totalSec / barSec);for (let b = 0; b < nBars; b++) placeKick(b * barSec);
// Soft clip / normalize to stay under -1.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.95) { const k = 0.95 / peak; for (let i = 0; i < out.length; i++) out[i] *= k;}
// Write float32 raw → ffmpeg → mp3const buf = Buffer.from(out.buffer);writeFileSync(rawPath, buf);const ff = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg";const result = spawnSync(ff, [ "-hide_banner", "-loglevel", "error", "-y", "-f", "f32le", "-ar", String(SR), "-ac", "1", "-i", rawPath, "-c:a", "libmp3lame", "-q:a", "4", outPath,], { stdio: "inherit" });if (result.status !== 0) { console.error(`✗ ffmpeg encode failed`); process.exit(result.status || 1);}console.log(`✓ ${outPath} · ${nBars} kicks · ${bpm} bpm`);