diff --git a/marketing/podcast/bin/feed.mjs b/marketing/podcast/bin/feed.mjs --- a/marketing/podcast/bin/feed.mjs +++ b/marketing/podcast/bin/feed.mjs @@ -60,6 +60,24 @@ .filter((e) => e && e.slug && e.audio); sidecars.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)); // newest first +// Local slug → hosted basename on the CDN (matches the papers siteName so the +// same mp3 backs both the papers "podcast" link and this feed). +const HOSTED = { + "may-26": "aesthetic-may-26-essay", + "june-26": "aesthetic-june-26-essay", +}; +const host = (e) => HOSTED[e.slug] || e.slug; + +// HOSTED is also the PUBLISH ALLOWLIST — only episodes listed here go in the +// public feed. This keeps local-only / confidential readings (e.g. the +// REGARDE-adjacent "named-markets") out of the feed no matter what's in out/. +for (let i = sidecars.length - 1; i >= 0; i--) { + if (!HOSTED[sidecars[i].slug]) { + console.log(` · excluding ${sidecars[i].slug} (not in publish allowlist)`); + sidecars.splice(i, 1); + } +} + if (!sidecars.length) { console.error("✗ no episodes found in out/ — run produce.mjs first."); process.exit(1); @@ -90,8 +108,8 @@ length: e.lengthText, durationSec: e.durationSec, bytes: e.bytes, words: e.wordCount, - audio: `${BASE}/${e.audio}`, - cover: `${BASE}/${e.cover}`, + audio: `${BASE}/${host(e)}.mp3`, + cover: `${BASE}/${host(e)}-cover.png`, description: e.description, pubDate: e.pubDate, })), @@ -101,16 +119,16 @@ // ── feed.xml (RSS 2.0 + iTunes) ──────────────────────────────────────── const items = sidecars.map((e) => ` ${xml(e.title)} - ${xml(CHANNEL.link)}/readings/${xml(e.slug)} + ${xml(`https://papers.aesthetic.computer/${host(e)}.pdf`)} ac-reading-${xml(e.slug)} ${xml(e.pubDate)} ${xml(e.description)} ${xml(e.description)} ${xml(CHANNEL.author)} ${hms(e.durationSec)} - + false - + `).join("\n"); const feed = ` diff --git a/marketing/podcast/bin/kits.mjs b/marketing/podcast/bin/kits.mjs new file mode 100644 --- /dev/null +++ b/marketing/podcast/bin/kits.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// kits.mjs — audition the drum kits. Renders the lo-fi bed with each kit +// (same pad+melody, different drums) back to back into one file so you can +// hear them and pick. Order is printed; pass the winner to produce with --kit. +// +// Usage: node bin/kits.mjs [--secs 9] [--play] + +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { renderBed, KITS } from "./jingle.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(HERE, ".."); +const argv = process.argv.slice(2); +const flag = (k, d) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : d; }; +const SECS = Number(flag("secs", 9)); + +const outDir = resolve(ROOT, "out"); +mkdirSync(outDir, { recursive: true }); +const build = resolve(outDir, ".kit-audition"); +rmSync(build, { recursive: true, force: true }); +mkdirSync(build, { recursive: true }); + +console.log(`\nAuditioning ${KITS.length} kits (${SECS}s each):\n`); +const segs = []; +KITS.forEach((kit, i) => { + const wav = resolve(build, `${String(i).padStart(2, "0")}-${kit}.wav`); + renderBed(SECS, wav, { kit }); + // normalize each to a common format + const norm = resolve(build, `n${String(i).padStart(2, "0")}.wav`); + execFileSync("ffmpeg", ["-y", "-i", wav, "-ar", "44100", "-ac", "2", "-af", "loudnorm=I=-18:TP=-1.5", "-c:a", "pcm_s16le", norm], { stdio: "ignore" }); + segs.push(norm); + const sil = resolve(build, `s${String(i).padStart(2, "0")}.wav`); + execFileSync("ffmpeg", ["-y", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=stereo", "-t", "0.8", "-c:a", "pcm_s16le", sil], { stdio: "ignore" }); + segs.push(sil); + console.log(` ${i + 1}. ${kit}`); +}); + +const list = resolve(build, "list.txt"); +writeFileSync(list, segs.map((f) => `file '${f}'`).join("\n") + "\n"); +const outMp3 = resolve(outDir, "kit-audition.mp3"); +execFileSync("ffmpeg", ["-y", "-f", "concat", "-safe", "0", "-i", list, "-c:a", "libmp3lame", "-b:a", "220k", outMp3], { stdio: "ignore" }); +rmSync(build, { recursive: true, force: true }); + +console.log(`\n✓ ${outMp3}`); +if (argv.includes("--play")) { + spawnSync("open", ["-a", "QuickTime Player", outMp3]); + spawnSync("osascript", ["-e", 'tell application "QuickTime Player" to play the front document'], { stdio: "ignore" }); +} diff --git a/marketing/podcast/bin/produce.mjs b/marketing/podcast/bin/produce.mjs --- a/marketing/podcast/bin/produce.mjs +++ b/marketing/podcast/bin/produce.mjs @@ -250,10 +250,11 @@ "-y", "-i", voiceWav, "-af", "loudnorm=I=-16:TP=-1.5:LRA=11", "-c:a", "libmp3lame", "-b:a", "256k", audioTmp, ], { stdio: "ignore" }); } else { - console.log("Scoring bed…"); + const kit = flags.kit || "felt"; + console.log(`Scoring bed… (kit: ${kit})`); const bedGain = flags.bedgain !== undefined ? Number(flags.bedgain) : 0.22; const bedWav = resolve(build, "bed.wav"); - renderBed(dur(voiceWav) + 1.0, bedWav); + renderBed(dur(voiceWav) + 1.0, bedWav, { kit }); // Voice chain (sharper + upfront): high-pass rumble, presence + air EQ, // gentle compression for a consistent forward level. const vox = "highpass=f=85," +