diff --git a/pop/.gitignore b/pop/.gitignore index 30dcc68bbe..7ddb223a9b 100644 --- a/pop/.gitignore +++ b/pop/.gitignore @@ -23,6 +23,11 @@ big-pictures/c/out/ big-pictures/c/amazing-dance big-pictures/c/amazinhym +# hopehop — compiled C binary + render outputs + freesound-derived +# autotune caches (sources .c/.mjs/.sh/.np/.illy.txt/kit.json ARE tracked) +hopehop/c/hopehop +hopehop/out/ + # pop/samples/ — third-party reference audio (downloaded from YouTube, # received via iMessage, etc.). Audio + chops + stems + render outputs # stay local; only the scripts and manifest are tracked. diff --git a/pop/hopehop/bin/fetch-kit.mjs b/pop/hopehop/bin/fetch-kit.mjs new file mode 100644 index 0000000000..5356895633 --- /dev/null +++ b/pop/hopehop/bin/fetch-kit.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +// fetch-kit.mjs — pull a trap drum/perc kit from Freesound for hopehop. +// +// Grabs a few candidates per role (kick / snare / clap / closed-hat / +// open-hat / shaker / rim / perc) and writes kit.json mapping each role +// to a chosen local WAV (preview decoded to 44.1k mono by the lib). Re-run +// with --role to refetch just one, or edit kit.json to pin a pick. +// +// node pop/hopehop/bin/fetch-kit.mjs # fetch all roles +// node pop/hopehop/bin/fetch-kit.mjs --list # show candidates per role +// node pop/hopehop/bin/fetch-kit.mjs --pick kick=3 # pin role to candidate idx + +import { searchSounds, downloadPreview } from "../../lib/freesound.mjs"; +import { writeFileSync, existsSync, readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const KIT_PATH = resolve(HERE, "..", "kit.json"); + +// role → freesound query + duration filter. Trap-leaning, punchy one-shots. +const ROLES = { + kick: { query: "trap kick punchy 808", filter: "duration:[0.1 TO 2]" }, + snare: { query: "trap snare", filter: "duration:[0.1 TO 2]" }, + clap: { query: "hand clap", filter: "duration:[0.05 TO 1.5]" }, + hat: { query: "closed hihat", filter: "duration:[0.02 TO 0.6]" }, + ohat: { query: "open hi hat", filter: "duration:[0.05 TO 1.2]" }, + shaker:{ query: "shaker percussion one shot", filter: "duration:[0.05 TO 1]" }, + rim: { query: "rim click percussion", filter: "duration:[0.02 TO 0.8]" }, + perc: { query: "percussion conga hit", filter: "duration:[0.05 TO 1.2]" }, + crash: { query: "crash cymbal", filter: "duration:[0.5 TO 4]" }, + panther:{ query: "panther", filter: "duration:[2 TO 6]" }, // #536331 = clean tonal roar + panther2:{ query: "jaguar growl roar", filter: "duration:[1 TO 5]" }, // alt big-cat voice + scratch:{ query: "turntable scratch vinyl", filter: "duration:[0.1 TO 1.5]" }, + scratch2:{ query: "dj scratch", filter: "duration:[0.1 TO 1.5]" }, +}; + +const argv = process.argv.slice(2); +const LIST = argv.includes("--list"); +const pickArg = argv.find((a) => a.startsWith("--pick")); +const pins = {}; +if (pickArg) { const v = argv[argv.indexOf(pickArg) + 1] || ""; const [r, i] = v.split("="); if (r) pins[r] = parseInt(i, 10) || 0; } + +const kit = existsSync(KIT_PATH) ? JSON.parse(readFileSync(KIT_PATH, "utf8")) : {}; + +for (const [role, spec] of Object.entries(ROLES)) { + try { + const data = await searchSounds({ query: spec.query, filter: spec.filter, pageSize: 5 }); + const results = data.results || []; + if (LIST) { + console.log(`\n${role} (${spec.query})`); + results.forEach((s, i) => console.log(` [${i}] ${s.name} · ${s.duration.toFixed(2)}s · ${s.username} · ${s.license.split("/").slice(-3, -1).join("/")}`)); + continue; + } + const idx = pins[role] ?? 0; + const chosen = results[idx] || results[0]; + if (!chosen) { console.warn(`! no result for ${role}`); continue; } + const mp3 = await downloadPreview(chosen); + const wav = mp3.replace(/\.mp3$/, ".wav"); + kit[role] = { + path: existsSync(wav) ? wav : mp3, + id: chosen.id, name: chosen.name, username: chosen.username, + license: chosen.license, duration: chosen.duration, + }; + console.log(`✓ ${role.padEnd(7)} ${chosen.name} (#${chosen.id} · ${chosen.username})`); + } catch (e) { + console.error(`✗ ${role}: ${e.message}`); + } +} + +if (!LIST) { + writeFileSync(KIT_PATH, JSON.stringify(kit, null, 2) + "\n"); + console.log(`\n✓ wrote ${KIT_PATH} (${Object.keys(kit).length} roles)`); +} diff --git a/pop/hopehop/bin/gen-illy.mjs b/pop/hopehop/bin/gen-illy.mjs new file mode 100644 index 0000000000..95e37d7b8c --- /dev/null +++ b/pop/hopehop/bin/gen-illy.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +// gen-illy.mjs — generate the hopehop album cover (text-to-image). +// +// Lean standalone generator (no jeffrey-platter identity refs — hopehop's +// cover is a panther/milkshake emblem, not a portrait). Calls gpt-image-2 +// /v1/images/generations with the prompt in hopehop.illy.txt. +// +// node pop/hopehop/bin/gen-illy.mjs # cached; skips if cover exists +// node pop/hopehop/bin/gen-illy.mjs --force # regenerate +// +// Output: pop/hopehop/out/hopehop-cover.png (1024x1024) + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const REPO = resolve(LANE, "..", ".."); +const FORCE = process.argv.includes("--force"); +const PROMPT_PATH = resolve(LANE, "hopehop.illy.txt"); +const OUT_PATH = resolve(LANE, "out", "hopehop-cover.png"); + +function loadKey() { + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + const vault = resolve(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(15).trim().replace(/^['"]|['"]$/g, ""); + } + } + throw new Error("OPENAI_API_KEY not set and not found in vault devcontainer.env"); +} + +if (existsSync(OUT_PATH) && !FORCE) { console.log(`✓ cover exists (use --force): ${OUT_PATH}`); process.exit(0); } + +const prompt = readFileSync(PROMPT_PATH, "utf8").trim(); +const apiKey = loadKey(); +mkdirSync(dirname(OUT_PATH), { recursive: true }); + +console.log("• generating hopehop cover (gpt-image-2, 1024x1024) …"); +const res = await fetch("https://api.openai.com/v1/images/generations", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ model: "gpt-image-2", prompt, size: "1024x1024", n: 1 }), +}); +if (!res.ok) { console.error(`✗ image API ${res.status}: ${await res.text()}`); process.exit(1); } +const json = await res.json(); +const b64 = json.data?.[0]?.b64_json; +if (!b64) { console.error("✗ no image in response"); process.exit(1); } +writeFileSync(OUT_PATH, Buffer.from(b64, "base64")); +console.log(`✓ ${OUT_PATH}`); diff --git a/pop/hopehop/bin/render-hopehop.mjs b/pop/hopehop/bin/render-hopehop.mjs new file mode 100644 index 0000000000..e490518bbc --- /dev/null +++ b/pop/hopehop/bin/render-hopehop.mjs @@ -0,0 +1,403 @@ +#!/usr/bin/env node +// render-hopehop.mjs — render hopehop: C synth BED + freesound TRAP KIT. +// +// Pipeline (mirrors amaythingra's "C render + JS bake" split): +// 1. build + run c/hopehop.c → melodic bed WAV (no drums) +// 2. load kit.json freesound samples → 48k mono one-shots +// 3. sequence a trap drum/perc layer → hard 808 kick, half-time +// snare+clap on the 3, ratchet hi-hat rolls, layered perc +// 4. sidechain-duck the bed under the kick (trap pump) +// 5. bright summer master → mp3, play +// +// Run: +// node pop/hopehop/bin/render-hopehop.mjs +// node pop/hopehop/bin/render-hopehop.mjs --seed 7 --swing 0.66 --no-open +// node pop/hopehop/bin/render-hopehop.mjs --out ~/hopehop.mp3 +// (refresh samples first with: node pop/hopehop/bin/fetch-kit.mjs) + +import { existsSync, statSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { homedir, tmpdir } from "node:os"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const C_DIR = resolve(HERE, "..", "c"); +const SRC = resolve(C_DIR, "hopehop.c"); +const BIN = resolve(C_DIR, "hopehop"); +const OUT_DIR = resolve(HERE, "..", "out"); +const KIT_PATH = resolve(HERE, "..", "kit.json"); +const expand = (p) => (p.startsWith("~") ? p.replace(/^~/, homedir()) : p); + +const argv = process.argv.slice(2); +function takeFlag(n) { const i = argv.indexOf(n); if (i < 0) return null; const v = argv[i + 1]; argv.splice(i, 2); return v; } +const hasFlag = (n) => { const i = argv.indexOf(n); if (i < 0) return false; argv.splice(i, 1); return true; }; + +const noOpen = hasFlag("--no-open"); +const outPath = (() => { const o = takeFlag("--out"); return o ? expand(o) : resolve(OUT_DIR, "hopehop.mp3"); })(); +const SWING = parseFloat(takeFlag("--swing") ?? "0.60"); +const SEED = parseInt(takeFlag("--seed") ?? "7", 10); +mkdirSync(OUT_DIR, { recursive: true }); + +// ── transport (must match c/hopehop.c) ─────────────────────────────── +const SR = 48000, BPM = 82, BEAT = 60 / BPM, BAR = BEAT * 4, S16 = BEAT / 4; +// arrangement arc (bar ranges) — mirrors the C bed's SECTIONS for FLOW. +const ARC = [["intro", 2], ["groove", 8], ["hook", 8], ["break", 2], ["drop", 8], ["outro", 2]]; +const SEC_OF = []; for (const [nm, bars] of ARC) for (let i = 0; i < bars; i++) SEC_OF.push(nm); +const t16 = (bar, s) => { const pb = Math.floor(s / 2) * 2; const off = (s & 1) ? 2 * SWING : 0; return bar * BAR + (pb + off) * S16; }; +// deterministic RNG (mulberry32) +let _s = (SEED ^ 0x9e3779b9) >>> 0; +const rng = () => { _s |= 0; _s = (_s + 0x6d2b79f5) | 0; let t = Math.imul(_s ^ (_s >>> 15), 1 | _s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; +const hr = (a) => (rng() * 2 - 1) * a; + +// ── 1 · build + render the bed ─────────────────────────────────────── +if (!existsSync(BIN) || statSync(BIN).mtimeMs < statSync(SRC).mtimeMs) { + console.log("• building c/hopehop.c …"); + if (spawnSync("bash", [resolve(C_DIR, "build-hopehop.sh")], { stdio: "inherit" }).status !== 0) process.exit(1); +} +console.log("• rendering bed …"); +const bedWav = resolve(OUT_DIR, "hopehop.bed.wav"); +if (spawnSync(BIN, ["--out", bedWav, "--swing", String(SWING), "--seed", String(SEED), ...argv], { stdio: "inherit" }).status !== 0) process.exit(1); + +// ── audio helpers ──────────────────────────────────────────────────── +function decodeF32(path, channels) { + const r = spawnSync("ffmpeg", ["-hide_banner", "-loglevel", "error", "-i", path, + "-f", "f32le", "-ar", String(SR), "-ac", String(channels), "-"], { maxBuffer: 1 << 30 }); + if (r.status !== 0) throw new Error(`decode failed: ${path}\n${r.stderr}`); + return new Float32Array(r.stdout.buffer, r.stdout.byteOffset, Math.floor(r.stdout.length / 4)); +} +function loadOneShot(path) { + let s = decodeF32(path, 1); + // trim leading silence (tight transient) + normalize peak + let start = 0; const TH = 0.004; + while (start < s.length && Math.abs(s[start]) < TH) start++; + s = s.subarray(Math.max(0, start - 4)); + let peak = 0; for (let i = 0; i < s.length; i++) peak = Math.max(peak, Math.abs(s[i])); + if (peak > 0) { const g = 0.95 / peak; for (let i = 0; i < s.length; i++) s[i] *= g; } + return s; +} + +// decode bed → L/R +const bedI = decodeF32(bedWav, 2); +const N = Math.floor(bedI.length / 2); +const L = new Float32Array(N), R = new Float32Array(N); +for (let i = 0; i < N; i++) { L[i] = bedI[i * 2]; R[i] = bedI[i * 2 + 1]; } +const TOTAL_BARS = Math.floor(N / (BAR * SR)); + +// load kit +const kit = JSON.parse(readFileSync(KIT_PATH, "utf8")); +const S = {}; +for (const role of Object.keys(kit)) { try { S[role] = loadOneShot(kit[role].path); } catch (e) { console.warn(`! ${role}: ${e.message}`); } } +console.log(`• kit: ${Object.keys(S).join(", ")}`); + +// ── PANTHER as a pitched instrument ────────────────────────────────── +// The panther roar becomes the voice: pitch-shifted (resampled) copies +// sing the bassline and the hook. We detect the roar's natural pitch so +// the shifts land in-key, then resample per note. "pitch around the +// panther." Tune with --panther-base / --panther-oct . +const PANTHER_BASE = (() => { const v = takeFlag("--panther-base"); return v ? parseFloat(v) : null; })(); +const PANTHER_OCT = parseInt(takeFlag("--panther-oct") ?? "0", 10); +function detectF0(s) { + let pk = 0, pidx = 0; for (let i = 0; i < s.length; i++) { const a = Math.abs(s[i]); if (a > pk) { pk = a; pidx = i; } } + const w = Math.min(8192, s.length), start = Math.max(0, Math.min(s.length - w, pidx - (w >> 1))); + const minLag = Math.floor(SR / 320), maxLag = Math.floor(SR / 55); + let best = -1, bestLag = 0; + for (let lag = minLag; lag <= maxLag; lag++) { + let sum = 0; for (let i = 0; i + lag < w; i++) sum += s[start + i] * s[start + i + lag]; + if (sum > best) { best = sum; bestLag = lag; } + } + return bestLag > 0 ? SR / bestLag : 0; +} +const panF0 = S.panther ? detectF0(S.panther) : 0; +const panBaseMidi = (PANTHER_BASE ?? (panF0 > 50 && panF0 < 320 ? 69 + 12 * Math.log2(panF0 / 440) : 48)); +console.log(`• panther: f0≈${panF0.toFixed(1)}Hz → base midi ${panBaseMidi.toFixed(1)}`); +// generic linear-resample pitch shift (semitones), optional max length +function resample(s, semis, maxDurSec = Infinity) { + const ratio = Math.pow(2, semis / 12); + const outLen = Math.min(Math.floor(s.length / ratio), maxDurSec === Infinity ? Infinity : Math.floor(maxDurSec * SR)); + const out = new Float32Array(outLen); + for (let i = 0; i < outLen; i++) { const sp = i * ratio, i0 = Math.floor(sp), fr = sp - i0, i1 = Math.min(s.length - 1, i0 + 1); out[i] = s[i0] * (1 - fr) + s[i1] * fr; } + const fn = Math.floor(0.012 * SR); for (let i = 0; i < fn && i < outLen; i++) { out[i] *= i / fn; out[outLen - 1 - i] *= i / fn; } + return out; +} +function trimFade(s, maxDurSec) { + const len = Math.min(s.length, Math.floor(maxDurSec * SR)); + const out = s.slice(0, len); + const an = Math.floor(0.006 * SR); for (let i = 0; i < an && i < len; i++) out[i] *= i / an; + const fn = Math.floor(0.02 * SR); for (let i = 0; i < fn && i < len; i++) out[len - 1 - i] *= i / fn; + return out; +} + +// ── autotuned panther via WORLD (worldsing.py) ─────────────────────── +// Locks the roar's pitch to an exact note (replaces f0 with a constant, +// scales down breath) so it SINGS in tune instead of wobbling. Disk- +// cached per note so reruns are instant. --no-autotune = raw resample. +const AUTOTUNE = !hasFlag("--no-autotune"); +const PY = resolve(HERE, "..", "..", ".venv", "bin", "python"); +const WORLDSING = resolve(HERE, "..", "..", "marimba", "bin", "worldsing.py"); +const TUNE_DIR = resolve(OUT_DIR, "panther-tuned"); +mkdirSync(TUNE_DIR, { recursive: true }); +const panSrc = kit.panther?.path, panId = kit.panther?.id; +function midiToNote(m) { const NM = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; m = Math.round(m); return NM[((m % 12) + 12) % 12] + (Math.floor(m / 12) - 1); } +const _tuned = new Map(); +function tunedPanther(midi) { + if (_tuned.has(midi)) return _tuned.get(midi); + const note = midiToNote(midi), out = resolve(TUNE_DIR, `${panId}-${note}.wav`); + if (!existsSync(out)) { + const r = spawnSync(PY, [WORLDSING, panSrc, out, "--note", note, "--ap-scale", "0.12", "--unvoiced-gain", "0.05"], { stdio: ["ignore", "ignore", "pipe"] }); + if (r.status !== 0) { console.warn(`! worldsing ${note} failed`); _tuned.set(midi, null); return null; } + } + let s = null; try { s = loadOneShot(out); } catch {} + _tuned.set(midi, s); return s; +} +const _panRaw = new Map(); +function placePanther(midi, startSec, maxDurSec, gain, pan) { + if (!S.panther) return; + let src = null; + if (AUTOTUNE) { const t = tunedPanther(midi); if (t) src = trimFade(t, maxDurSec); } + if (!src) { const k = `${midi}|${maxDurSec}`; src = _panRaw.get(k) || resample(S.panther, midi - panBaseMidi, maxDurSec); _panRaw.set(k, src); } + place(src, startSec, gain, pan); +} + +// ── Apple `say` saucy vocal adlibs ─────────────────────────────────── +// macOS TTS → per-word clips, scattered + pitched through the hook/drop +// as a saucy adlib track. Tune the line with --say "..." / voice with +// --say-voice . +const SAY_VOICE = takeFlag("--say-voice") ?? "Samantha"; +const SAY_LINE = takeFlag("--say") ?? "want my,latina,gimme,thicc,milkshake,milkshake,mmm yeah"; +const ADLIBS = SAY_LINE.split(",").map((w) => w.trim()).filter(Boolean); +const SAY_DIR = resolve(OUT_DIR, "say-tuned"); mkdirSync(SAY_DIR, { recursive: true }); +const slugw = (t) => t.replace(/\W+/g, "_"); +function loadSafe(p) { try { return loadOneShot(p); } catch { return null; } } +function sayWord(text) { + const aiff = resolve(tmpdir(), `hh-say-${slugw(text)}.aiff`); + if (spawnSync("say", ["-v", SAY_VOICE, "-o", aiff, text], { stdio: "ignore" }).status !== 0) return null; + const wav = aiff.replace(/\.aiff$/, ".wav"); + if (spawnSync("ffmpeg", ["-y", "-loglevel", "error", "-i", aiff, "-ar", "44100", "-ac", "1", wav], { stdio: "ignore" }).status !== 0) return null; + return wav; +} +const sayPaths = ADLIBS.map(sayWord); +const sayRaw = sayPaths.map((p) => (p ? loadSafe(p) : null)); +console.log(`• say-vocal: ${sayPaths.filter(Boolean).length}/${ADLIBS.length} adlibs (voice ${SAY_VOICE}, autotuned)`); +// autotune (WORLD pitch-lock) a say word to a NOTE — higher ap/unvoiced +// than the panther so the words stay intelligible while singing in tune. +const _sayTuned = new Map(); +function tunedSay(idx, midi) { + const key = `${idx}|${midi}`; if (_sayTuned.has(key)) return _sayTuned.get(key); + const src = sayPaths[idx]; if (!src) { _sayTuned.set(key, null); return null; } + const note = midiToNote(midi), out = resolve(SAY_DIR, `${slugw(ADLIBS[idx])}-${note}.wav`); + if (!existsSync(out)) { + const r = spawnSync(PY, [WORLDSING, src, out, "--note", note, "--ap-scale", "0.45", "--unvoiced-gain", "0.7"], { stdio: ["ignore", "ignore", "pipe"] }); + if (r.status !== 0) { _sayTuned.set(key, null); return null; } + } + const s = loadSafe(out); _sayTuned.set(key, s); return s; +} +// vocal HOOK melody — the words SING this line (NOT random pitches), +// autotuned per note. Loop-bar indexed [step, midi]. +const VOCAL_MEL = { + 0: [[4, 67], [10, 69]], 1: [[4, 65]], + 2: [[4, 64], [10, 67]], 3: [[2, 69]], + 4: [[4, 67], [10, 72]], 5: [[4, 69]], + 6: [[2, 71], [8, 67]], 7: [[4, 65]], +}; +// place a vocal with a hip-hop slap "throw" so it sits into the mix +function placeVox(s, startSec, gain, pan) { + place(s, startSec, gain, pan); + place(s, startSec + BEAT * 0.5, gain * 0.38, -pan * 0.8); // 1/8 throw + place(s, startSec + BEAT * 1.0, gain * 0.18, pan * 0.5); // 1/4 tail +} +let adlibIdx = 0; + +// panther voicing data (loop-bar indexed). bass = chord roots; the hook +// is the melodic line the lead will eventually follow. +const PAN_ROOT = [41, 45, 38, 43, 41, 40, 45, 43]; // F A D G F E A G +const PAN_HOOK = { // [step, midi, maxBeats] + 0: [[4, 53, 1.4], [10, 57, 1.0]], 1: [[4, 55, 1.6]], + 2: [[4, 53, 1.0], [10, 60, 1.2]], 3: [[2, 62, 1.4]], + 4: [[4, 53, 1.4], [10, 57, 1.0]], 5: [[4, 55, 1.6]], + 6: [[2, 60, 1.0], [8, 64, 1.2]], 7: [[4, 55, 1.8]], +}; + +// ── trap drum/perc sequencer ───────────────────────────────────────── +const duck = new Float32Array(N).fill(1); // sidechain envelope on the bed +function place(sample, startSec, gain, pan, isKick = false) { + if (!sample) return; + const i0 = Math.round(startSec * SR); + const a = (pan * 0.5 + 0.5) * (Math.PI / 2), gL = Math.cos(a) * gain, gR = Math.sin(a) * gain; + for (let j = 0; j < sample.length; j++) { const i = i0 + j; if (i < 0 || i >= N) break; L[i] += sample[j] * gL; R[i] += sample[j] * gR; } + if (isKick) { // carve a DEEP pump under the kick (sidechain "wiggle") + const dur = 0.22 * SR; + for (let j = 0; j < dur; j++) { const i = i0 + j; if (i < 0 || i >= N) break; const p = j / dur; const d = 0.42 + 0.58 * p * p; if (d < duck[i]) duck[i] = d; } + } +} + +// hi-hat with ratchet rolls: subdiv quick hits across one swung 16th cell +function hatCell(bar, s, baseVel, subdiv) { + const t0 = t16(bar, s), t1 = t16(bar, (s + 1) % 16) + ((s + 1) >= 16 ? BAR : 0); + const span = (t1 > t0 ? t1 - t0 : S16); + for (let k = 0; k < subdiv; k++) { + const v = baseVel * (subdiv > 1 ? 0.7 + 0.3 * (k / Math.max(1, subdiv - 1)) : 1) * (0.85 + 0.3 * rng()); + place(S.hat, t0 + (span * k) / subdiv + hr(0.003), 0.42 * v, hr(0.3)); + } +} + +// synth swell riser (cute noise crescendo that LANDS on endSec) +function riser(endSec, durSec, gain) { + const i1 = Math.round(endSec * SR), i0 = Math.round((endSec - durSec) * SR); + let lp = 0; + for (let i = Math.max(0, i0); i < Math.min(N, i1); i++) { + const p = (i - i0) / (i1 - i0); + const nz = rng() * 2 - 1; lp += (nz - lp) * (0.03 + 0.4 * p); + const env = p * p * gain; + const a = (hr(0.3) * 0.5 + 0.5) * (Math.PI / 2); + L[i] += lp * env * Math.cos(a); R[i] += lp * env * Math.sin(a); + } +} + +console.log("• sequencing trap layer (chill / flow-aware) …"); +for (let bar = 0; bar < TOTAL_BARS; bar++) { + const lb = bar % 8, phraseEnd = lb === 7; + const sec = SEC_OF[bar] || "drop"; + const isDrop = sec === "drop", isBreak = sec === "break", isIntro = sec === "intro", isOutro = sec === "outro"; + + // BREAK = breather: drums drop out, bed washes; riser swells into the drop + if (isBreak) { + for (const s of [2, 6, 10, 14]) place(S.shaker, t16(bar, s) + hr(0.004), 0.2 * (0.8 + 0.4 * rng()), hr(0.5)); + if (bar === SEC_OF.indexOf("break") + 1) riser(t16(bar + 1, 0), BAR * 1.6, 0.5); // land on the drop + continue; + } + + // intro builds; outro thins out; drop is fullest. Density scales it all. + const build = isIntro ? (lb >= 1 ? 0.7 : 0.4) : 1.0; + const energy = isDrop ? 1.0 : 0.85; + + // KICK — softer 808 (less "war war"), syncopated, lighter in intro/outro + const kicks = new Set([0, 6, 10]); + if (!isOutro && rng() < 0.4) kicks.add(13); + if (isDrop && rng() < 0.3) kicks.add(11); + if (isOutro) { kicks.clear(); kicks.add(0); kicks.add(10); } + for (const s of kicks) place(S.kick, t16(bar, s) + hr(0.003), 0.82 * build, 0, true); + + // BACKBEAT — snare + clap on beat 3 (half-time), gentler + if (!(isIntro && lb === 0)) { + place(S.snare, t16(bar, 8) + hr(0.003), 0.62 * build, 0.0); + place(S.clap, t16(bar, 8) + hr(0.004), 0.42 * build, 0.0); + } + if (phraseEnd && !isOutro) place(S.snare, t16(bar, 14) + hr(0.003), 0.34, 0.05); + + // HI-HATS — laid-back 16ths, FEWER rolls (less aggressive) + const rolls = new Map(); + if (!isOutro && rng() < 0.3) rolls.set(7, 2); + if (isDrop && rng() < 0.25) rolls.set(11, rng() < 0.5 ? 3 : 2); + if (phraseEnd && isDrop) { rolls.set(14, 2); rolls.set(15, 3); } + const hatGain = (isOutro ? 0.6 : 1.0) * energy; + for (let s = 0; s < 16; s++) { + if (isOutro && s % 2) continue; // thin to 8ths in the outro + const accent = (s % 4 === 2) ? 0.9 : (s % 2 === 0 ? 0.7 : 0.5); + hatCell(bar, s, accent * hatGain * build, rolls.get(s) || 1); + } + if ((lb === 1 || lb === 5) && !rolls.has(14)) place(S.ohat, t16(bar, 14) + hr(0.003), 0.24, 0.25); + + // PERC — shaker + rim + conga, panned (kept gentle) + for (const s of [2, 6, 10, 14]) place(S.shaker, t16(bar, s) + hr(0.004), 0.26 * (0.8 + 0.4 * rng()) * build, hr(0.5)); + for (const s of (rng() < 0.5 ? [3, 11] : [3, 11, 6])) place(S.rim, t16(bar, s) + hr(0.004), 0.3 * (0.8 + 0.4 * rng()) * build, hr(0.55)); + if (!isOutro && rng() < 0.6) place(S.perc, t16(bar, rng() < 0.5 ? 7 : 13) + hr(0.005), 0.28 * build, hr(0.45)); + // soft cymbal to mark the drop's downbeat (gentle, not a crash-out) + if (isDrop && lb === 0) place(S.crash, t16(bar, 0), 0.28, 0.0); + + // ── PANTHER VOICE — sings the bass + the hook ────────────────────── + const po = PANTHER_OCT * 12; + if (sec === "groove" || sec === "hook" || isDrop) { + // panther bass — low growl on the roots + placePanther(PAN_ROOT[lb] + po, t16(bar, 0) + hr(0.004), 1.7 * BEAT, 0.55, 0); + if (rng() < 0.6) placePanther(PAN_ROOT[lb] + po, t16(bar, 8) + hr(0.004), 1.0 * BEAT, 0.4, 0); + } + if (sec === "hook" || isDrop) { + // panther hook — the melodic lead (pitched up into a growl-sing) + for (const [s, midi, mb] of (PAN_HOOK[lb] || [])) + placePanther(midi + po, t16(bar, s) + hr(0.004), mb * BEAT, 0.5, hr(0.2)); + } + // dramatic full roar announcing the drop + if (isDrop && lb === 0) placePanther(PAN_ROOT[0] + po, t16(bar, 0) - 0.04, 3.0, 0.62, 0); + + // ── saucy say-vocal — autotuned, SINGS the vocal hook melody ─────── + if (sayPaths.some(Boolean) && (sec === "hook" || isDrop)) { + for (const [step, midi] of (VOCAL_MEL[lb] || [])) { + if (sec === "hook" && rng() < 0.45) continue; // sparser in the first hook + const idx = adlibIdx++ % ADLIBS.length; + let s = tunedSay(idx, midi); + if (!s && sayRaw[idx]) s = resample(sayRaw[idx], midi - 60); // fallback + if (s) placeVox(trimFade(s, 1.3), t16(bar, step) + hr(0.008), 0.52, hr(0.35)); + } + } + + // ── record scratches — phrase transitions + drop accents ─────────── + if (S.scratch && phraseEnd && !isOutro) place(S.scratch, t16(bar, 12) + hr(0.005), 0.45, hr(0.4)); + if (S.scratch2 && isDrop && rng() < 0.3) place(S.scratch2, t16(bar, [3, 7, 11][Math.floor(rng() * 3)]) + hr(0.005), 0.38, hr(0.5)); +} + +// ── sidechain pump (kicks punch through — deeper wiggle) ───────────── +for (let i = 0; i < N; i++) { const d = 0.4 + 0.6 * duck[i]; L[i] *= d; R[i] *= d; } + +// ── filter + volume automation (FLOW) ──────────────────────────────── +// One-pole LP whose cutoff opens over the intro and dips in the break, +// plus a volume swell in/out — gives the track an arc instead of a flat loop. +const introEnd = 2 * BAR, brkStart = 18 * BAR, brkEnd = 20 * BAR, outroStart = 28 * BAR, end = 30 * BAR; +const lerp = (a, b, t) => a + (b - a) * Math.max(0, Math.min(1, t)); +function cutoff(t) { + if (t < introEnd) return 700 * Math.pow(20000 / 700, t / introEnd); // open up + if (t >= brkStart && t < brkStart + BAR) return lerp(20000, 1400, (t - brkStart) / BAR); + if (t >= brkStart + BAR && t < brkEnd) return lerp(1400, 20000, (t - (brkStart + BAR)) / BAR); + return 20000; +} +function autoGain(t) { + if (t < introEnd) return lerp(0.55, 1.0, (t / introEnd) ** 0.5); + if (t >= brkStart && t < brkEnd) return lerp(1.0, 0.82, Math.sin(Math.PI * (t - brkStart) / (brkEnd - brkStart))); + if (t >= outroStart) return lerp(1.0, 0.0, (t - outroStart) / (end - outroStart)); // fade out + return 1.0; +} +let lpL = 0, lpR = 0; +for (let i = 0; i < N; i++) { + const t = i / SR; + const a = 1 - Math.exp(-2 * Math.PI * cutoff(t) / SR); + lpL += a * (L[i] - lpL); lpR += a * (R[i] - lpR); + const g = autoGain(t); + L[i] = lpL * g; R[i] = lpR * g; +} + +// ── write mixed f32 → warm (less-bright, less-loud) master → mp3 ────── +const mix = new Float32Array(N * 2); +for (let i = 0; i < N; i++) { mix[i * 2] = L[i]; mix[i * 2 + 1] = R[i]; } +const tmpRaw = resolve(tmpdir(), `hopehop-${SEED}.f32`); +writeFileSync(tmpRaw, Buffer.from(mix.buffer)); + +// hip-hop master: controlled sub, cleared mud, vocal presence + air, +// bus glue, then loud-but-clean ceiling. +const MASTER = [ + "highpass=f=30", + "equalizer=f=55:t=q:w=0.7:g=2.2", // 808 / sub weight + "equalizer=f=250:t=q:w=1.1:g=-1.8", // clear the mud so vocal + flute sit + "equalizer=f=1800:t=q:w=1.2:g=0.7", // body + "equalizer=f=4200:t=q:w=1.0:g=1.4", // vocal presence (say-vocal sits up) + "equalizer=f=10000:t=h:w=0.7:g=1.8", // air / sheen + "acompressor=threshold=-20dB:ratio=2.5:attack=10:release=150:makeup=2.5", // glue + "alimiter=limit=0.97:level=false", + "loudnorm=I=-9.5:TP=-1.0:LRA=9", // hip-hop loudness +].join(","); + +const ff = spawnSync("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-f", "f32le", "-ar", String(SR), "-ac", "2", "-i", tmpRaw, + "-af", MASTER, "-c:a", "libmp3lame", "-b:a", "256k", outPath], { stdio: "inherit" }); +try { unlinkSync(tmpRaw); } catch {} +try { unlinkSync(bedWav); } catch {} +if (ff.status !== 0) { console.error("✗ master failed"); process.exit(1); } +console.log(`✓ ${outPath} (${TOTAL_BARS} bars · trap kit + C bed · seed ${SEED})`); + +if (noOpen) process.exit(0); +spawnSync("osascript", ["-e", ` +tell application "QuickTime Player" + if running then close every document + open POSIX file ${JSON.stringify(outPath)} + play (front document) + activate +end tell`], { stdio: "ignore" }); diff --git a/pop/hopehop/c/build-hopehop.sh b/pop/hopehop/c/build-hopehop.sh new file mode 100755 index 0000000000..7b657049d8 --- /dev/null +++ b/pop/hopehop/c/build-hopehop.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# build-hopehop.sh — compile hopehop.c. No -ffast-math so additive +# summation stays stable across machines (matches amaythingra). +set -euo pipefail +HERE="$(cd -- "$(dirname -- "$0")" && pwd)" +cc -O3 -std=c11 -Wall -Wextra \ + -o "$HERE/hopehop" \ + "$HERE/hopehop.c" \ + -lm +echo "→ $HERE/hopehop" diff --git a/pop/hopehop/c/hopehop.c b/pop/hopehop/c/hopehop.c new file mode 100644 index 0000000000..d189ff76f7 --- /dev/null +++ b/pop/hopehop/c/hopehop.c @@ -0,0 +1,631 @@ +// hopehop.c — summer hip-hop / R&B beat, De La Soul / Native-Tongues +// flavor. 96 BPM 4/4 with a HARD 16th shuffle. Bright jazzy boom-bap: +// walking upright bass, drawbar-organ stabs on the offbeats, hand +// percussion (congas + tambourine), a sweet flute hook, and a formant- +// synthesized "ooh-ah" vocal chop. +// +// Architecture mirrors amaythingra.c: voices are *_render() functions +// that sum into global L/R buffers (+ WL/WR reverb sends); a Schroeder +// reverb, a chord SCORE, one render_track(), a WAV writer, and main(). +// +// Build: ./build-hopehop.sh +// Run: ./hopehop --out out/hopehop.wav + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif +#define TAU (2.0 * M_PI) + +// ── config ──────────────────────────────────────────────────────────── +static const int SR = 48000; +static double BPM = 82.0; // slower, chill — less "war war" +static double TOTAL_SEC = 90.0; // 30 bars @ 2.927s + tail +static double SWING = 0.60; // relaxed shuffle (0.5 = straight) +static const char *OUT_PATH = NULL; +static double DRIVE = 1.0; +static unsigned SEED_ARG = 0; +static int SYNTH_PERC = 0; // synth drums/perc OFF by default — + // the JS driver layers freesound + // trap samples instead (--synth-drums + // re-enables the built-in synth kit). + +static uint32_t RNG_STATE = 0x9e3779b9u; +static void rng_seed(uint32_t s) { RNG_STATE = s ? s : 0x9e3779b9u; } +static uint32_t rng_u32(void) { uint32_t x = RNG_STATE; x ^= x << 13; x ^= x >> 17; x ^= x << 5; RNG_STATE = x; return x; } +static double rng_d(void) { return (double)rng_u32() / 4294967296.0; } +static double jit(double a) { return (rng_d() * 2.0 - 1.0) * a; } + +// ── shared mix buffers ──────────────────────────────────────────────── +static long N = 0; +static float *L = NULL, *R = NULL, *WL = NULL, *WR = NULL; + +// ── timing / reporting ──────────────────────────────────────────────── +static double t0_wall = 0.0; +static double now_wall(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } +__attribute__((format(printf, 1, 2))) +static void report(const char *fmt, ...) { + fprintf(stderr, "[%6.2fs] ", now_wall() - t0_wall); + va_list a; va_start(a, fmt); vfprintf(stderr, fmt, a); va_end(a); fprintf(stderr, "\n"); +} +static inline double m2f(double m) { return 440.0 * pow(2.0, (m - 69.0) / 12.0); } + +// ── drawbar organ (the offbeat stabs + soft pad) ───────────────────── +// Three detuned unison voices (±7 cents, spread L–C–R) through tanh +// overdrive — the chorus/Leslie thickness + tube warmth that keeps it +// off the thin "additive sine" sound. +typedef struct { double atk, rel, pan, gain, wet_send, sub_amt, upper_amt, drive; } OrganOpts; +static const double ORGAN_PARTIALS[8][2] = { + {0.5, 0.55}, {1.0, 1.00}, {1.5, 0.42}, {2.0, 0.78}, + {3.0, 0.32}, {4.0, 0.55}, {5.0, 0.20}, {6.0, 0.18}, +}; +static void organ_render(double t0, double dur, double midi, OrganOpts opt) { + const double f = m2f(midi); + const double atk = opt.atk > 0 ? opt.atk : 0.006; + const double rel = opt.rel > 0 ? opt.rel : 0.05; + const double sub = opt.sub_amt >= 0 ? opt.sub_amt : 1.0; + const double upr = opt.upper_amt >= 0 ? opt.upper_amt : 1.0; + const double drive = opt.drive > 0 ? opt.drive : 1.7; + const double ndrive = tanh(drive); + static const double det[3] = { 0.9959, 1.0, 1.0041 }, vpan[3] = { -0.5, 0.0, 0.5 }; + double phs[3][8] = {{0}}; + long iS = (long)(t0 * SR), iE = (long)((t0 + dur) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR, tRem = dur - t; + double env = (t < atk) ? t / atk : (tRem < rel ? tRem / rel : 1.0); + if (env < 0) env = 0; + const double vib = 1.0 + 0.0009 * sin(TAU * 6.2 * t); // gentle Leslie + double oL = 0, oR = 0; + for (int u = 0; u < 3; u++) { + double smp = 0, ampsum = 0; + for (int p = 0; p < 8; p++) { + double amp = ORGAN_PARTIALS[p][1]; + if (p == 0) amp *= sub; else if (p >= 4) amp *= upr; + ampsum += amp; + phs[u][p] += (f * ORGAN_PARTIALS[p][0] * vib * det[u]) / (double)SR; if (phs[u][p] >= 1.0) phs[u][p] -= 1.0; + smp += amp * sin(TAU * phs[u][p]); + } + smp = tanh((smp / ampsum) * drive) / ndrive; // overdriven, normalized + const double up = opt.pan + vpan[u] * 0.35; + oL += smp * (1.0 - 0.5 * up); oR += smp * (1.0 + 0.5 * up); + } + oL *= env * opt.gain / 3.0; oR *= env * opt.gain / 3.0; + L[i] += oL; R[i] += oR; WL[i] += oL * opt.wet_send; WR[i] += oR * opt.wet_send; + } +} + +// ── walking upright bass (rich woody body + string-pluck transient) ── +// Two slightly-detuned voices × 6 harmonics (sawtooth-ish woody body) + +// a bright filtered-noise pluck, all driven through tanh for a fat, +// saturated double-bass — not a thin sine. +static const double UP_HAMP[6] = { 1.0, 0.62, 0.42, 0.30, 0.20, 0.13 }; +static void upright_render(double t0, double dur, double midi, double gain, double pan) { + const double f = m2f(midi); + long iS = (long)(t0 * SR), iE = (long)((t0 + dur + 0.12) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + static const double det[2] = { 0.997, 1.003 }; + double phs[2][6] = {{0}}, hpI = 0, hpP = 0; + uint32_t s = (uint32_t)(t0 * 100003.0) | 1; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + const double env = (t < 0.006) ? t / 0.006 : exp(-(t - 0.006) / (dur * 0.7 + 0.12)); + if (t > 0.01 && env < 4e-4) break; + double body = 0; + for (int u = 0; u < 2; u++) for (int h = 0; h < 6; h++) { + phs[u][h] += f * (h + 1) * det[u] / (double)SR; if (phs[u][h] >= 1) phs[u][h] -= 1; + body += UP_HAMP[h] * sin(TAU * phs[u][h]); + } + body /= (2.0 * 2.67); + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + const double nz = ((double)s / 4294967296.0) * 2 - 1; + const double hp = 0.9 * (hpP + nz - hpI); hpI = nz; hpP = hp; + const double pluck = hp * exp(-t / 0.014) * 0.9; + const double smp = tanh((body + pluck) * 1.9) * 0.72 * env * gain; + L[i] += smp * (1.0 - 0.5 * pan); R[i] += smp * (1.0 + 0.5 * pan); + WL[i] += smp * 0.05; WR[i] += smp * 0.05; + } +} + +// ── flute (the hook) ───────────────────────────────────────────────── +// Two chorus-detuned voices (3 harmonics each) + an attack "chiff" noise +// burst + audible breath — a breathy, present flute instead of a thin +// sine. The chiff is what reads as a real player articulating each note. +static void flute_render(double t0, double midi, double dur, double gain, double pan) { + const double f = m2f(midi); + long iS = (long)(t0 * SR), iE = (long)((t0 + dur + 0.3) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + static const double det[2] = { 0.996, 1.004 }; + double ph[2][3] = {{0}}, lp = 0; uint32_t s = (uint32_t)(t0 * 1000033.0) | 1; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + double env = (t < 0.04) ? t / 0.04 : (t > dur ? exp(-(t - dur) / 0.11) : 1.0); + if (t > dur && env < 4e-4) break; + const double vib = 1.0 + 0.009 * (t / 0.25 < 1 ? t / 0.25 : 1.0) * sin(TAU * 5.2 * t); + double tone = 0; + for (int u = 0; u < 2; u++) { + ph[u][0] += f * vib * det[u] / (double)SR; if (ph[u][0] >= 1) ph[u][0] -= 1; + ph[u][1] += 2 * f * vib * det[u] / (double)SR; if (ph[u][1] >= 1) ph[u][1] -= 1; + ph[u][2] += 3 * f * vib * det[u] / (double)SR; if (ph[u][2] >= 1) ph[u][2] -= 1; + tone += sin(TAU * ph[u][0]) + 0.25 * sin(TAU * ph[u][1]) + 0.10 * sin(TAU * ph[u][2]); + } + tone *= 0.5; + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + const double nz = ((double)s / 4294967296.0) * 2 - 1; lp += (nz - lp) * 0.10; + const double chiff = (t < 0.05) ? nz * exp(-t / 0.02) * 0.5 : 0.0; + const double smp = (tone + lp * 0.18 + chiff) * env * gain; + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * 0.16; WR[i] += sR * 0.16; + } +} + +// ── bell (FM glass sparkle) ────────────────────────────────────────── +static void bell_render(double t0, double midi, double dur, double gain, double pan) { + const double f = m2f(midi); + long iS = (long)(t0 * SR), iE = (long)((t0 + dur + 0.8) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double cph = 0, mph = 0; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + double env = exp(-t / (dur * 0.5 + 0.3)); if (t < 0.004) env *= t / 0.004; + if (env < 2e-4) break; + const double index = 2.2 * exp(-t / 0.12); + cph += f / (double)SR; if (cph >= 1) cph -= 1; + mph += f * 3.51 / (double)SR; if (mph >= 1) mph -= 1; + const double smp = sin(TAU * cph + index * sin(TAU * mph)) * env * gain; + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * 0.16; WR[i] += sR * 0.16; + } +} + +// ── music box (cute twinkly mallet — FM bell, bright + short) ──────── +// Brighter, shorter sibling of the bell: high partial ratio, fast decay, +// a tiny attack tick. The "cute instrumentation" sparkle. +static void musicbox_render(double t0, double midi, double gain, double pan) { + const double f = m2f(midi); + long iS = (long)(t0 * SR), iE = iS + (long)(0.9 * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double cph = 0, mph = 0; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + double env = exp(-t / 0.28); if (t < 0.002) env *= t / 0.002; + if (env < 2e-4) break; + const double index = 1.4 * exp(-t / 0.05); // quick metallic tick + cph += f / (double)SR; if (cph >= 1) cph -= 1; + mph += f * 5.0 / (double)SR; if (mph >= 1) mph -= 1; // glassy ratio + double smp = sin(TAU * cph + index * sin(TAU * mph)); + smp += 0.3 * sin(TAU * 2 * cph) * exp(-t / 0.12); // shimmer octave + smp *= env * gain; + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * 0.18; WR[i] += sR * 0.18; + } +} + +// ── sine bell (pure AC sinebell — soft attack, ringing decay) ──────── +static void sinebell_render(double t0, double midi, double gain, double pan) { + const double f = m2f(midi); + long iS = (long)(t0 * SR), iE = iS + (long)(1.4 * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double p1 = 0, p2 = 0; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + double env = exp(-t / 0.55); if (t < 0.004) env *= t / 0.004; + if (env < 2e-4) break; + p1 += f / (double)SR; if (p1 >= 1) p1 -= 1; + p2 += 2 * f / (double)SR; if (p2 >= 1) p2 -= 1; + const double smp = (sin(TAU * p1) + 0.22 * sin(TAU * p2) * exp(-t / 0.18)) * env * gain; + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * 0.24; WR[i] += sR * 0.24; + } +} + +// ── vocal chop (formant synthesis — the "ooh-ah" hook) ─────────────── +// Glottal sawtooth source → three resonant bandpass FORMANT filters (RBJ +// biquads) tuned to a vowel, morphing vowelA→vowelB across the note for +// the sung "ooh→ah" soul shape. Fully bottom-up: no sample. +enum { VOW_OO = 0, VOW_AH = 1, VOW_EH = 2 }; +static const double VF[3][3] = { {320, 870, 2250}, {730, 1090, 2440}, {530, 1840, 2480} }; +static const double VG[3][3] = { {1.0, 0.22, 0.10}, {1.0, 0.45, 0.18}, {1.0, 0.33, 0.16} }; +static const double VQ[3][3] = { {12, 13, 13}, {10, 11, 12}, {11, 12, 12} }; +static void vocal_chop_render(double t0, double midi, double dur, double gain, double pan, + int va, int vb, double glideSemis) { + const double fT = m2f(midi); + long iS = (long)(t0 * SR), iE = (long)((t0 + dur + 0.18) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double x1[3] = {0}, x2[3] = {0}, y1[3] = {0}, y2[3] = {0}; + double b0[3] = {0}, b2[3] = {0}, a1[3] = {0}, a2[3] = {0}; + double phase = 0, phase2 = 0, subph = 0; const double glideT = 0.07; uint32_t s = (uint32_t)(t0 * 1000003.0) | 1; + for (long i = iS; i < iE; i++) { + const long k = i - iS; const double t = k / (double)SR; + double env; + if (t < 0.012) env = t / 0.012; + else if (t > dur) env = exp(-(t - dur) / 0.07); + else env = 0.85 + 0.15 * exp(-(t - 0.012) / 0.2); + if (t > dur && env < 4e-4) break; + const double gfrac = 1.0 - t / glideT; const double glide = glideSemis * (gfrac > 0 ? gfrac : 0.0); + const double vib = 1.0 + 0.01 * (t / 0.12 < 1 ? t / 0.12 : 1.0) * sin(TAU * 5.6 * t); + const double f = fT * pow(2.0, glide / 12.0) * vib; + phase += f / (double)SR; if (phase >= 1.0) phase -= 1.0; + phase2 += f * 1.006 / (double)SR; if (phase2 >= 1.0) phase2 -= 1.0; // detuned twin + subph += f * 0.5 / (double)SR; if (subph >= 1.0) subph -= 1.0; // sub octave + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + // richer glottal source: two detuned saws + a sub sine + breath + double src = 0.55 * (2.0 * (phase - floor(phase + 0.5))) + + 0.45 * (2.0 * (phase2 - floor(phase2 + 0.5))) + + 0.30 * sin(TAU * subph); + src = src * 0.7 + (((double)s / 4294967296.0) * 2 - 1) * 0.05; + const double mr = t / (dur * 0.6 + 0.001); const double m = mr > 1 ? 1 : mr; + if ((k & 31) == 0) for (int j = 0; j < 3; j++) { + const double fc = VF[va][j] + (VF[vb][j] - VF[va][j]) * m, Q = VQ[va][j]; + const double w0 = TAU * fc / (double)SR, cs = cos(w0), sn = sin(w0), al = sn / (2 * Q), a0 = 1 + al; + b0[j] = al / a0; b2[j] = -al / a0; a1[j] = (-2 * cs) / a0; a2[j] = (1 - al) / a0; + } + double out = 0; + for (int j = 0; j < 3; j++) { + const double g = VG[va][j] + (VG[vb][j] - VG[va][j]) * m; + const double y = b0[j] * src + b2[j] * x2[j] - a1[j] * y1[j] - a2[j] * y2[j]; + x2[j] = x1[j]; x1[j] = src; y2[j] = y1[j]; y1[j] = y; + out += y * g; + } + const double smp = tanh(out * env * gain * 1.76); + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * 0.15; WR[i] += sR * 0.15; + } +} + +// ── kick (pitched sine sweep + click) ──────────────────────────────── +static void kick_render(double t0, double gain) { + long iS = (long)(t0 * SR), iE = iS + (long)(0.4 * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double phase = 0; uint32_t s = (uint32_t)(t0 * 13337.0) | 1; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + const double freq = (t < 0.028) ? 115.0 * pow(48.0 / 115.0, t / 0.028) : 48.0; + const double env = (t < 0.002) ? t / 0.002 : exp(-(t - 0.002) / 0.15); + if (t > 0.005 && env < 1e-5) break; + phase += freq / (double)SR; if (phase >= 1) phase -= 1; + double smp = sin(TAU * phase) * env; + if (t < 0.003) { s ^= s << 13; s ^= s >> 17; s ^= s << 5; smp += (((double)s / 4294967296.0) * 2 - 1) * (1 - t / 0.003) * 0.5; } + smp = tanh(smp * gain * 1.2); + L[i] += smp; R[i] += smp; + } +} + +// ── AC-native perc kit (sine/tri/sqr/noise hit) ────────────────────── +enum PercWave { PW_SINE, PW_TRI, PW_SQR, PW_NOISE }; +static void perc_hit(double t0, int wave, double tone, double dur, double vol, + double atk, double dec, double pan, double wet) { + long iS = (long)(t0 * SR), iE = (long)((t0 + dur) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double phase = 0; const double dP = tone / (double)SR; uint32_t s = (uint32_t)(t0 * 99877.0) | 1; + double lp = 0; const double lpc = exp(-TAU * tone / (double)SR); const double a = atk > 0 ? atk : 1e-6; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + const double env = (t < a) ? t / a : exp(-(t - a) / dec); + if (t > a + 0.0005 && env < 1e-5) break; + double osc; + if (wave == PW_NOISE) { s ^= s << 13; s ^= s >> 17; s ^= s << 5; const double w = ((double)s / 4294967296.0) * 2 - 1; lp = lp * lpc + (1 - lpc) * w; osc = lp * 2; } + else { phase += dP; if (phase >= 1) phase -= 1; + if (wave == PW_SINE) osc = sin(TAU * phase); + else if (wave == PW_TRI) osc = 2.0 * fabs(2.0 * (phase - floor(phase + 0.5))) - 1.0; + else osc = (phase < 0.5) ? 1.0 : -1.0; } + const double smp = osc * env * vol; + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * wet; WR[i] += sR * wet; + } +} +static void tambo(double t0, double v, double pan) { + perc_hit(t0, PW_NOISE, 7000, 0.080, 0.38 * v, 0.002, 0.075, pan, 0.18); + perc_hit(t0, PW_NOISE, 6500, 0.100, 0.25 * v, 0.030, 0.070, pan, 0.18); + perc_hit(t0, PW_SQR, 6000, 0.030, 0.14 * v, 0.001, 0.028, pan, 0.10); +} +static void snare808(double t0, double v, double pan) { + perc_hit(t0, PW_SINE, 200, 0.20, 0.55 * v, 0.0005, 0.16, pan, 0.12); + perc_hit(t0, PW_SINE, 330, 0.14, 0.30 * v, 0.0005, 0.10, pan, 0.12); + perc_hit(t0, PW_NOISE, 3800, 0.20, 0.70 * v, 0.0004, 0.16, pan, 0.16); +} + +// ── conga (pitched membrane + skin noise) ──────────────────────────── +static void conga_render(double t0, int slap, double gain, double pan) { + const double f0 = slap ? 380.0 : 210.0, dec = slap ? 0.09 : 0.16; + long iS = (long)(t0 * SR), iE = iS + (long)((dec + 0.02) * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + double ph = 0, hpI = 0, hpP = 0; uint32_t s = (uint32_t)(t0 * 99991.0) | 1; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + ph += (f0 + f0 * 1.4 * exp(-t / 0.012)) / (double)SR; if (ph >= 1) ph -= 1; + s ^= s << 13; s ^= s >> 17; s ^= s << 5; const double nz = ((double)s / 4294967296.0) * 2 - 1; + const double hp = 0.7 * (hpP + nz - hpI); hpI = nz; hpP = hp; + const double smp = (sin(TAU * ph) * 0.9 + hp * 0.25 * exp(-t / 0.02)) * exp(-t / dec) * gain; + const double sL = smp * (1.0 - 0.5 * pan), sR = smp * (1.0 + 0.5 * pan); + L[i] += sL; R[i] += sR; WL[i] += sL * 0.08; WR[i] += sR * 0.08; + } +} + +// ── hi-hat (doubled noise burst) ───────────────────────────────────── +static void hat_burst(double t0, double pan, double gain, double decay, double bright, uint32_t seed) { + long iS = (long)(t0 * SR), iE = iS + (long)(decay * 6.0 * SR); + if (iS < 0) iS = 0; if (iE > N) iE = N; + uint32_t s = seed | 1; double i1 = 0, o1 = 0, i2 = 0, o2 = 0; + for (long i = iS; i < iE; i++) { + const double t = (i - iS) / (double)SR; + const double env = (t < 0.001) ? t / 0.001 : exp(-(t - 0.001) / decay); + if (t > 0.005 && env < 1e-5) break; + s ^= s << 13; s ^= s >> 17; s ^= s << 5; const double nz = ((double)s / 4294967296.0) * 2 - 1; + const double h1 = bright * (o1 + nz - i1); i1 = nz; o1 = h1; + const double h2 = bright * (o2 + h1 - i2); i2 = h1; o2 = h2; + const double smp = h2 * env * gain; + L[i] += smp * (1.0 - 0.5 * pan); R[i] += smp * (1.0 + 0.5 * pan); + } +} +static void hihat(double t0, double pan, double gain, int open) { + const double dec = open ? 0.14 : 0.032; + hat_burst(t0, pan, gain, dec, 0.93, (uint32_t)(t0 * 7919.0 + 31)); + hat_burst(t0 + 0.004, pan, gain * 0.6, dec * 0.85, 0.9, (uint32_t)(t0 * 8087.0 + 17)); +} + +// ── Schroeder reverb ───────────────────────────────────────────────── +typedef struct { float *buf; long size; long idx; float feedback; } Comb; +typedef struct { float *buf; long size; long idx; } Allpass; +static void comb_init(Comb *c, long size, float fb) { c->buf = calloc(size, sizeof(float)); c->size = size; c->idx = 0; c->feedback = fb; } +static inline float comb_tick(Comb *c, float in) { float out = c->buf[c->idx]; c->buf[c->idx] = in + out * c->feedback; if (++c->idx >= c->size) c->idx = 0; return out; } +static void allpass_init(Allpass *a, long size) { a->buf = calloc(size, sizeof(float)); a->size = size; a->idx = 0; } +static inline float allpass_tick(Allpass *a, float in) { float bo = a->buf[a->idx]; float out = -in + bo; a->buf[a->idx] = in + bo * 0.5f; if (++a->idx >= a->size) a->idx = 0; return out; } +static void apply_reverb(double wet) { + static const int dl_L[4] = { 1116, 1188, 1277, 1356 }; + static const int dl_R[4] = { 1139, 1211, 1300, 1379 }; + const double sc = (double)SR / 44100.0; + Comb cL[4], cR[4]; for (int i = 0; i < 4; i++) { comb_init(&cL[i], (long)(dl_L[i] * sc), 0.82f); comb_init(&cR[i], (long)(dl_R[i] * sc), 0.82f); } + Allpass apL[2], apR[2]; + allpass_init(&apL[0], (long)(556 * sc)); allpass_init(&apL[1], (long)(441 * sc)); + allpass_init(&apR[0], (long)(589 * sc)); allpass_init(&apR[1], (long)(467 * sc)); + for (long i = 0; i < N; i++) { + float sumL = 0, sumR = 0; + for (int k = 0; k < 4; k++) { sumL += comb_tick(&cL[k], WL[i]); sumR += comb_tick(&cR[k], WR[i]); } + sumL = allpass_tick(&apL[0], sumL); sumL = allpass_tick(&apL[1], sumL); + sumR = allpass_tick(&apR[0], sumR); sumR = allpass_tick(&apR[1], sumR); + L[i] += sumL * wet; R[i] += sumR * wet; + } + for (int i = 0; i < 4; i++) { free(cL[i].buf); free(cR[i].buf); } + for (int i = 0; i < 2; i++) { free(apL[i].buf); free(apR[i].buf); } +} + +// ── global stereo chorus (modulated delay — width + analog motion) ─── +// Two LFO-modulated delay lines (one per channel, different rates) mixed +// in ~28% wet. This is the single biggest "un-MIDI" move: it smears the +// dry phase so the bed breathes and widens instead of sitting static. +static inline double dread(const float *buf, long size, long wi, double dly) { + double rp = (double)wi - dly; while (rp < 0) rp += size; + long i0 = (long)rp; double fr = rp - i0; long i1 = i0 + 1; if (i1 >= size) i1 -= size; + return buf[i0] * (1.0 - fr) + buf[i1] * fr; +} +static void apply_chorus(double wet) { + const long maxd = (long)(0.014 * SR) + 4; + float *dl = calloc(maxd, sizeof(float)), *dr = calloc(maxd, sizeof(float)); + long wi = 0; + for (long i = 0; i < N; i++) { + dl[wi] = L[i]; dr[wi] = R[i]; + const double t = (double)i / SR; + const double m1 = (0.007 + 0.0045 * sin(TAU * 0.5 * t)) * SR; + const double m2 = (0.007 + 0.0045 * sin(TAU * 0.34 * t + 1.7)) * SR; + const double rL = dread(dl, maxd, wi, m1), rR = dread(dr, maxd, wi, m2); + L[i] = (float)(L[i] * (1.0 - wet * 0.5) + rL * wet); + R[i] = (float)(R[i] * (1.0 - wet * 0.5) + rR * wet); + if (++wi >= maxd) wi = 0; + } + free(dl); free(dr); +} + +// ── SCORE: bright 8-bar loop (ii-V motion) ─────────────────────────── +typedef struct { int n[4]; const char *name; } Chord; +static const Chord PROG[8] = { + { {60, 64, 65, 69}, "Fmaj7" }, { {60, 64, 67, 69}, "Am7" }, + { {60, 62, 65, 69}, "Dm7" }, { {59, 62, 65, 67}, "G7" }, + { {60, 64, 65, 69}, "Fmaj7" }, { {59, 62, 64, 67}, "Em7" }, + { {60, 64, 67, 69}, "Am7" }, { {60, 62, 65, 67}, "Gm7C7" }, +}; +// walking double-bass: 4 quarters/bar, landing a step from the next root. +static const int WALK[8][4] = { + {41, 45, 48, 44}, {45, 48, 52, 39}, {38, 41, 45, 42}, {43, 46, 38, 40}, + {41, 45, 48, 41}, {40, 43, 47, 46}, {45, 48, 52, 44}, {43, 46, 48, 41}, +}; + +// hook melody (flute). loopBar, 16th-step, midi, dur in beats. +typedef struct { int lb, step, midi; double durB; } HookNote; +static const HookNote HOOK[] = { + {0, 4, 69, 0.5}, {0, 6, 72, 0.5}, {0, 10, 69, 1.0}, + {1, 4, 67, 0.5}, {1, 6, 69, 1.5}, + {2, 4, 65, 0.5}, {2, 6, 69, 0.5}, {2, 8, 72, 1.0}, + {3, 4, 74, 0.5}, {3, 6, 67, 0.5}, {3, 8, 71, 1.0}, + {4, 4, 69, 0.5}, {4, 6, 72, 0.5}, {4, 10, 69, 1.0}, + {5, 4, 67, 0.5}, {5, 6, 71, 1.5}, + {6, 2, 72, 0.5}, {6, 4, 76, 0.5}, {6, 8, 72, 0.5}, {6, 10, 69, 1.0}, + {7, 4, 70, 0.5}, {7, 8, 67, 0.5}, {7, 10, 65, 1.0}, +}; +static const int HOOK_N = (int)(sizeof(HOOK) / sizeof(HOOK[0])); + +// vocal chop hits: 16th-step, chord-tone index, vowelA, vowelB, dur(beats), glide(semis) +typedef struct { int step, idx, va, vb; double durB, glide; } VoxHit; +static const VoxHit VOX_BUSY[] = { + {0, 3, VOW_OO, VOW_AH, 0.5, 0}, {3, 2, VOW_AH, VOW_AH, 0.5, -2}, + {6, 3, VOW_OO, VOW_OO, 0.5, 0}, {8, 1, VOW_AH, VOW_AH, 0.5, 0}, + {11, 3, VOW_OO, VOW_AH, 0.75, 0}, {14, 2, VOW_EH, VOW_EH, 0.25, 0}, +}; +static const VoxHit VOX_SPARSE[] = { {2, 3, VOW_OO, VOW_AH, 0.5, 0}, {11, 2, VOW_OO, VOW_OO, 0.75, 0} }; + +// patterns +static const int KICK_S[3] = {0, 7, 10}; +static const int SNARE_S[2] = {4, 12}; +static const int GHOST_S[2] = {7, 15}; +static const int HAT8[8] = {0, 2, 4, 6, 8, 10, 12, 14}; +static const int TAMB_S[4] = {2, 6, 10, 14}; +static const int CONGA_S[5] = {3, 6, 8, 11, 14}; +static const int CONGA_SLAP[5] = {0, 1, 0, 0, 1}; +static const int ORGAN_S[5] = {2, 6, 9, 10, 14}; + +// section flags — arc for FLOW. drums/perc are sequenced in the JS layer +// (freesound), so those columns stay 0; the bed carries the musical arc. +// name bars soft organ bass flute vox music vpad +typedef struct { const char *name; int bars, soft, organ, bass, flute, vox, music, vpad; } Sec; +static const Sec SECTIONS[] = { + { "intro", 2, 1, 0, 1, 0, 1, 0, 1 }, // soft pad + light bass + dreamy vox pad + { "groove", 8, 0, 1, 1, 0, 2, 0, 0 }, // organ + walking bass + busy vocal chop + { "hook", 8, 0, 1, 1, 1, 1, 1, 0 }, // + flute hook + music-box twinkle + { "break", 2, 1, 0, 1, 1, 0, 0, 1 }, // breather: pad + flute + woo-woo pad + { "drop", 8, 0, 1, 1, 1, 1, 1, 0 }, // full hook energy + { "outro", 2, 1, 1, 0, 0, 0, 1, 1 }, // pad + sparse twinkle, fade +}; +static const int NSEC = (int)(sizeof(SECTIONS) / sizeof(SECTIONS[0])); + +// ── render the full track ──────────────────────────────────────────── +static double SPB, BAR, S16; +static double t16(int bar, int s) { const int pb = (s / 2) * 2; const double off = (s & 1) ? 2.0 * SWING : 0.0; return bar * BAR + (pb + off) * S16; } + +static void render_track(void) { + SPB = 60.0 / BPM; BAR = 4.0 * SPB; S16 = SPB / 4.0; + int total_bars = 0; for (int i = 0; i < NSEC; i++) total_bars += SECTIONS[i].bars; + report("hopehop: bpm=%.1f swing=%.2f bar=%.3fs · %d bars · %.1fs", BPM, SWING, BAR, total_bars, TOTAL_SEC); + + int bar = 0; + for (int si = 0; si < NSEC; si++) { + const Sec *sec = &SECTIONS[si]; + for (int b = 0; b < sec->bars; b++, bar++) { + const int lb = bar % 8; + const Chord *c = &PROG[lb]; + const double baseT = bar * BAR; + + // organ — offbeat stabs (the bounce) + if (sec->organ) { + for (int k = 0; k < 5; k++) { + const double t = t16(bar, ORGAN_S[k]) + jit(0.004); + for (int v = 0; v < 4; v++) + organ_render(t, S16 * 1.1, c->n[v], + (OrganOpts){ .atk = 0.006, .rel = 0.05, .pan = jit(0.22), + .gain = 0.14, .wet_send = 0.12, .sub_amt = 0.5, .upper_amt = 0.85 }); + } + } + // soft organ pad — intro / break / outro warmth + if (sec->soft) { + for (int v = 0; v < 4; v++) + organ_render(baseT + jit(0.004), 3.0 * SPB, c->n[v], + (OrganOpts){ .atk = 0.4, .rel = 0.6, .pan = jit(0.25), + .gain = 0.06, .wet_send = 0.3, .sub_amt = 0.8, .upper_amt = 0.4 }); + organ_render(baseT, 3.0 * SPB, c->n[0] - 12, + (OrganOpts){ .atk = 0.5, .rel = 0.6, .pan = 0, .gain = 0.05, .wet_send = 0.3, .sub_amt = 1.0, .upper_amt = 0.3 }); + } + // walking upright bass — straight quarters (pulled back so the + // pitched PANTHER can sing the low end in the JS layer) + if (sec->bass) for (int beat = 0; beat < 4; beat++) + upright_render(baseT + beat * SPB + jit(0.004), WALK[lb][beat], 0.92 * SPB, 0.32, 0.0); + + // optional built-in synth kit (--synth-drums); default OFF — + // the JS driver sequences freesound trap samples instead. + if (SYNTH_PERC) { + for (int k = 0; k < 3; k++) kick_render(t16(bar, KICK_S[k]) + jit(0.004), 0.9); + for (int k = 0; k < 2; k++) snare808(t16(bar, SNARE_S[k]) + jit(0.005), 0.6, 0.04); + for (int k = 0; k < 2; k++) snare808(t16(bar, GHOST_S[k]) + jit(0.005), 0.13, 0.04); + for (int k = 0; k < 8; k++) hihat(t16(bar, HAT8[k]) + jit(0.003), jit(0.35), ((HAT8[k] % 4 == 2) ? 0.32 : 0.2) + jit(0.04), 0); + for (int k = 0; k < 4; k++) tambo(t16(bar, TAMB_S[k]) + jit(0.004), 0.16, jit(0.45)); + for (int k = 0; k < 5; k++) conga_render(t16(bar, CONGA_S[k]) + jit(0.004), CONGA_SLAP[k], 0.26, jit(0.4)); + } + // flute hook (+ a soft sparkle bell on the climax bar) + if (sec->flute) { + for (int k = 0; k < HOOK_N; k++) if (HOOK[k].lb == lb) + flute_render(t16(bar, HOOK[k].step), HOOK[k].midi, SPB * HOOK[k].durB, 0.10, jit(0.18)); + if (lb == 6) bell_render(t16(bar, 12), 81, SPB * 1.0, 0.09, 0.3); + } + // music box — cute high twinkle (chord tones, two octaves up) + if (sec->music) { + static const int TW_STEP[4] = { 0, 5, 8, 13 }; + static const int TW_IDX[4] = { 3, 0, 2, 3 }; + for (int k = 0; k < 4; k++) + musicbox_render(t16(bar, TW_STEP[k]) + jit(0.004), c->n[TW_IDX[k]] + 24, 0.12, jit(0.4)); + // sine bells ring a sparse counter-line over the music box + sinebell_render(t16(bar, 6) + jit(0.004), c->n[1] + 12, 0.10, jit(0.35)); + if (lb % 2 == 1) sinebell_render(t16(bar, 12) + jit(0.004), c->n[3] + 12, 0.09, jit(0.4)); + } + // sine-bell twinkles drift through the soft sections (intro/break/outro) + if (sec->soft && lb % 2 == 0) + sinebell_render(t16(bar, 8) + jit(0.004), c->n[2] + 12, 0.09, jit(0.4)); + // vocal chop — the "woo woo", brought forward + if (sec->vox) { + const VoxHit *pat = sec->vox == 2 ? VOX_BUSY : VOX_SPARSE; + const int pn = sec->vox == 2 ? (int)(sizeof(VOX_BUSY) / sizeof(VOX_BUSY[0])) + : (int)(sizeof(VOX_SPARSE) / sizeof(VOX_SPARSE[0])); + for (int k = 0; k < pn; k++) { + const int m = c->n[pat[k].idx < 4 ? pat[k].idx : 3]; + vocal_chop_render(t16(bar, pat[k].step) + jit(0.004), m, SPB * pat[k].durB, + 0.26, jit(0.2), pat[k].va, pat[k].vb, pat[k].glide); + } + } + // dreamy sustained vocal pad — held "wooo" choir chord (woo woo) + if (sec->vpad) for (int v = 1; v < 4; v++) + vocal_chop_render(baseT, c->n[v], BAR * 0.95, 0.10, (v - 2) * 0.4, VOW_OO, VOW_AH, 0.0); + } + } + report(" scheduled %d bars", bar); + apply_chorus(0.14); + report(" chorus applied"); + apply_reverb(0.11); + report(" reverb applied"); +} + +// ── WAV writer (tanh drive → normalize to -3 dB) ───────────────────── +static void write_wav(const char *path) { + double peak = 0; for (long i = 0; i < N; i++) { double a = fabs(L[i]); if (a > peak) peak = a; double b = fabs(R[i]); if (b > peak) peak = b; } + // Pre-gain into tanh so the saturation amount is CONSISTENT regardless + // of arrangement density — pushes the sum into tanh's warm knee for + // analog glue (gentle, capped) instead of staying clean-thin or + // hard-clipping. This is tape-style drive, not a brick limiter. + double pre = peak > 0 ? 1.3 / peak : 1.0; if (pre > 3.0) pre = 3.0; + report(" peak before tanh: %.3f → pregain %.2f × drive %.2f", peak, pre, DRIVE); + double pp = 0; + for (long i = 0; i < N; i++) { L[i] = tanhf(L[i] * pre * DRIVE); R[i] = tanhf(R[i] * pre * DRIVE); double a = fabs(L[i]); if (a > pp) pp = a; double b = fabs(R[i]); if (b > pp) pp = b; } + const double target = pow(10.0, -3.0 / 20.0); const double g = pp > 0 ? target / pp : 1.0; + for (long i = 0; i < N; i++) { L[i] *= g; R[i] *= g; } + FILE *f = fopen(path, "wb"); if (!f) { fprintf(stderr, "hopehop: cannot open %s\n", path); exit(1); } + const long data_size = N * 2 * 2; uint8_t h[44]; + memcpy(h, "RIFF", 4); *(uint32_t*)(h + 4) = (uint32_t)(36 + data_size); memcpy(h + 8, "WAVE", 4); + memcpy(h + 12, "fmt ", 4); *(uint32_t*)(h + 16) = 16; *(uint16_t*)(h + 20) = 1; *(uint16_t*)(h + 22) = 2; + *(uint32_t*)(h + 24) = (uint32_t)SR; *(uint32_t*)(h + 28) = (uint32_t)(SR * 4); *(uint16_t*)(h + 32) = 4; *(uint16_t*)(h + 34) = 16; + memcpy(h + 36, "data", 4); *(uint32_t*)(h + 40) = (uint32_t)data_size; fwrite(h, 1, 44, f); + int16_t *out = malloc(N * 2 * sizeof(int16_t)); + for (long i = 0; i < N; i++) { + double sL = L[i], sR = R[i]; if (sL > 1) sL = 1; else if (sL < -1) sL = -1; if (sR > 1) sR = 1; else if (sR < -1) sR = -1; + out[2 * i] = (int16_t)lrint(sL * 32767.0); out[2 * i + 1] = (int16_t)lrint(sR * 32767.0); + } + fwrite(out, sizeof(int16_t), N * 2, f); free(out); fclose(f); + report("✓ wrote %s (%.2f s · %d Hz · stereo)", path, (double)N / SR, SR); +} + +int main(int argc, char **argv) { + t0_wall = now_wall(); + 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) BPM = atof(argv[++i]); + else if (!strcmp(argv[i], "--swing") && i + 1 < argc) SWING = atof(argv[++i]); + else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) TOTAL_SEC = atof(argv[++i]); + else if (!strcmp(argv[i], "--drive") && i + 1 < argc) DRIVE = atof(argv[++i]); + else if (!strcmp(argv[i], "--seed") && i + 1 < argc) SEED_ARG = (unsigned)strtoul(argv[++i], NULL, 10); + else if (!strcmp(argv[i], "--synth-drums")) SYNTH_PERC = 1; + else { fprintf(stderr, "hopehop: unknown arg %s\n", argv[i]); return 1; } + } + if (!OUT_PATH) { fprintf(stderr, "hopehop: --out required\n"); return 1; } + rng_seed(SEED_ARG ? SEED_ARG : (uint32_t)time(NULL)); + N = (long)(TOTAL_SEC * SR); + L = calloc(N, sizeof(float)); R = calloc(N, sizeof(float)); WL = calloc(N, sizeof(float)); WR = calloc(N, sizeof(float)); + if (!L || !R || !WL || !WR) { fprintf(stderr, "hopehop: alloc failed\n"); return 1; } + report("hopehop: rendering %.2f s @ %d Hz", TOTAL_SEC, SR); + render_track(); + write_wav(OUT_PATH); + free(L); free(R); free(WL); free(WR); + return 0; +} diff --git a/pop/hopehop/hopehop.illy.txt b/pop/hopehop/hopehop.illy.txt new file mode 100644 index 0000000000..febe53ecd7 --- /dev/null +++ b/pop/hopehop/hopehop.illy.txt @@ -0,0 +1 @@ +a cute black panther cub sitting upright, sipping a tall pink strawberry milkshake through a striped straw — eyes half-closed, blissed-out, mid-purr. lo-fi summer hip-hop mood. warm dusk light, soft risograph grain, limited palette of cream / chartreuse / dusty rose / deep teal. a few little sine-bell sparkles and music-note twinkles floating around the cub's head like it is singing. hand-lettered chunky bubble wordmark "hopehop" arched across the top, friendly and round. flat illustration, thick tapered ink outlines, Hockney-ish colored-pencil hatching for shading, gentle optical-mix texture. single centered emblem on a soft solid backdrop — NO real brand logos, NO text other than the wordmark, NO photographic realism. album-cover composition, square. diff --git a/pop/hopehop/hopehop.np b/pop/hopehop/hopehop.np new file mode 100644 index 0000000000..8773142085 --- /dev/null +++ b/pop/hopehop/hopehop.np @@ -0,0 +1,22 @@ +# hopehop — summer hip-hop / R&B beat. de la soul / native-tongues flavor: +# bright jazzy boom-bap, walking upright bass, organ stabs, hand perc. +# 96 BPM · 4/4 · heavy swung 16ths (~60% shuffle) · key of C +# +# This .np carries the topline flute "hook" (one syllable per note) so the +# melody is playable on notepat. The full arrangement (organ stabs, +# walking bass, congas/tambourine, flute, drums) lives in +# bin/render-hopehop.mjs — the .np grid is monophonic so it holds only the +# hook line, not the chord stack or the walk. +# +# 8-bar chord loop (one chord per bar, bright 7th voicings + ii-V motion): +# 1 Fmaj7 5 Fmaj7 +# 2 Am7 6 Em7 +# 3 Dm7 7 Am7 +# 4 G7 8 Gm7→C7 (ii-V back to Fmaj7) +# +# walking upright bass: four quarter notes/bar, landing a step from the +# next root each bar (see WALK[] in the renderer). + +hook +A4:hope C5:e- A4:nough G4:to A4:on F4:in +G4:the A4:sun D5:and A4:back C5:a- E5:gain A4:home diff --git a/pop/hopehop/kit.json b/pop/hopehop/kit.json new file mode 100644 index 0000000000..9d364201e3 --- /dev/null +++ b/pop/hopehop/kit.json @@ -0,0 +1,114 @@ +{ + "kick": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/437273-tr_808_kick_wav.wav", + "id": 437273, + "name": "TR-808 kick.wav", + "username": "derickgtwk", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 0.711542 + }, + "snare": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/278983-snare_trap_wav.wav", + "id": 278983, + "name": "snare trap.wav", + "username": "dflee4", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 2 + }, + "ohat": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/317096-hat_open2_wav.wav", + "id": 317096, + "name": "hat open2.wav", + "username": "shpira", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 1.00002 + }, + "shaker": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/375638-shaker_shake7_wav.wav", + "id": 375638, + "name": "Shaker (shake7.wav)", + "username": "sgossner", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 0.234694 + }, + "rim": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/662274-playtime_rim.wav", + "id": 662274, + "name": "Playtime Rim", + "username": "adh.dreaming", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 0.0607937 + }, + "perc": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/35918-conga_wav.wav", + "id": 35918, + "name": "conga.wav", + "username": "altemark", + "license": "https://creativecommons.org/licenses/by/4.0/", + "duration": 0.0679365 + }, + "clap": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/493711-handclap05_wav.wav", + "id": 493711, + "name": "HandClap05.wav", + "username": "bastianpusch", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 0.489365 + }, + "hat": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/509971-hi_hat_closed_9_mp3.wav", + "id": 509971, + "name": "Hi-Hat Closed 9.mp3", + "username": "blakengouda", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 0.144739 + }, + "crash": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/269905-02_crash_med_cymbals_snares_wav.wav", + "id": 269905, + "name": "02_Crash_Med_Cymbals_&_Snares.wav", + "username": "Theriavirra", + "license": "https://creativecommons.org/licenses/by/4.0/", + "duration": 1.728 + }, + "tiger": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/528034-robot_tiger_roar_mp3.wav", + "id": 528034, + "name": "Robot Tiger Roar.mp3", + "username": "Poppletron", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 2.41739 + }, + "panther": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/536331-panther_wav.wav", + "id": 536331, + "name": "panther.wav", + "username": "XHwZQ6GV", + "license": "http://creativecommons.org/licenses/by/3.0/", + "duration": 3.58168 + }, + "panther2": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/571287-jaguar_growl_roar_wav.wav", + "id": 571287, + "name": "jaguar_growl_roar.wav", + "username": "Lewis.B.M", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 2.73996 + }, + "scratch": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/610673-scratch_3x_b_wav.wav", + "id": 610673, + "name": "Scratch 3x B.wav", + "username": "LiftPizzas", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 0.835918 + }, + "scratch2": { + "path": "/Users/jas/aesthetic-computer-vault/personal/pop/freesound-cache/661380-bassscratch_c_mp3.wav", + "id": 661380, + "name": "BassScratch C.mp3", + "username": "SaschaRettberg", + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "duration": 1.24082 + } +}