diff --git a/pop/big-pictures/bin/bake.mjs b/pop/big-pictures/bin/bake.mjs index 91ae9fd8e2..972b9cad9f 100644 --- a/pop/big-pictures/bin/bake.mjs +++ b/pop/big-pictures/bin/bake.mjs @@ -46,9 +46,12 @@ const BED_GAIN = flag("bed", "0.9"); const REVERB = parseFloat(flag("reverb", "0.22")); const VOX_FADEIN = parseFloat(flag("vox-fadein", "15")); // slow swell into the track const VOX_DELAY = parseFloat(flag("vox-delay", "1.5")); // true silence before the swell +// @jeffrey: the jeffrey ahh/ooh vocals should ONLY appear in the last N +// seconds — an outro vocal swell, not a full-length wash. >0 windows the +// TAIL of the vowel stem and places it so it rides out the final N s. +const VOX_LAST = parseFloat(flag("vox-last", "60")); // 0 = full-length (old behavior) const DRIVE = flag("drive", "0.30"); // engine pre-tanh soft-clip (kills maxing) const MONO_KICK = !argv.includes("--haas-kick"); // mono-solid low end (no HAAS phase smear) -const PEAK_DB = parseFloat(flag("peak", "-1.0")); // master true-peak ceiling const MONO_BASS_HZ = parseFloat(flag("mono-bass", "120")); // fold bed lows below this to mono (0 = off) const BED_ONLY = argv.includes("--bed-only"); // master just the bed, no vocals // Opening rebalance (@jeffrey 2026-06-02): the bed's crunchy kicks slam @@ -57,8 +60,15 @@ const BED_ONLY = argv.includes("--bed-only"); // master just the bed // swells up instead of punching in. The chainsaw/tornado FX were "wayyy // too loud" — knocked right down (now flag-tunable). const BED_FADEIN = parseFloat(flag("bed-fadein", "6")); // bed swell-in seconds (0 = off) +// @jeffrey: more drama at the FIRST DROP (the 0:32 WOOP). Pull the build +// DOWN over the seconds leading in (anticipation), then snap back to full +// ON the drop so it hits harder by contrast. The dip ramps from 1.0 → +// DROP_DIP across DROP_DIP_LEN s ending at DROP_AT, then jumps to full. +const DROP_AT = parseFloat(flag("drop-at", "32")); // 0:32 WOOP downbeat +const DROP_DIP = parseFloat(flag("drop-dip", "0.58")); // build floor (1 = no dip) +const DROP_DIP_LEN = parseFloat(flag("drop-dip-len", "6"));// seconds of dip before the drop const SAW_GAIN = parseFloat(flag("saw", "0")); // chainsaw sample REMOVED (@jeffrey) -const WIND_GAIN = parseFloat(flag("wind", "0.06")); // tornado in break (was 0.20) +const WIND_GAIN = parseFloat(flag("wind", "0")); // tornado REMOVED — too loud/annoying (@jeffrey) // @jeffrey 2026-06-02: an "aesthetic dot computer" voice stamp before the // drop, but the "com" is replaced by a single crow CAW (crow ≈ caw ≈ com) // landing at 2:09 ON THE BEAT (129.0 s = beat 258 on the 0.5 s grid). @@ -75,11 +85,11 @@ const HUM_VERB = parseFloat(flag("hum-verb", "0.30")); // reverb wash so the // keep the bed + boot chime + car-horn intro + FX. The vowel stem stays // on disk; --no-vox just leaves it out of this mix. const NO_VOX = argv.includes("--no-vox"); -const CHIME_GAIN = parseFloat(flag("chime", "0")); // boot chime REMOVED (@jeffrey) +const CHIME_GAIN = parseFloat(flag("chime", "0.5")); // boot chime — flourish at the end of the drop (@jeffrey) // @jeffrey 2026-06-02: the boot chime sits WAY quiet + deep in reverb, // landing at ~32 s — right after the cowbell (24 s), as part of the build // ramp into the WOOP. ON THE BEAT (120 BPM → 32.0 s = bar 16 downbeat). -const CHIME_DELAY = parseFloat(flag("chime-delay", "26.0")); +const CHIME_DELAY = parseFloat(flag("chime-delay", "162.0")); // ~2:42, end of drop, on bar 81 downbeat const ENGINE = `${C}/amaythingra`; const ENGINE_C = `${C}/amaythingra.c`; @@ -236,13 +246,41 @@ if (!BED_ONLY && USE_HUM) { // Assemble the mix label list: bed always, vowels only when included, // then the intro layers. const mixLabels = [`[bedlvl]`]; -const stages = [bedCond, `[bedmain]volume=${BED_GAIN}${bedFade}[bedlvl]`]; +// Build-dip automation: 1.0 until (DROP_AT - DROP_DIP_LEN), ramp down to +// DROP_DIP right at DROP_AT, then snap back to 1.0 — the anticipation dip +// + drop impact. Disabled when DROP_DIP >= 1. +const dipStart = (DROP_AT - DROP_DIP_LEN).toFixed(3); +const dropAtS = DROP_AT.toFixed(3); +const dropAuto = DROP_DIP < 1 + ? `,volume='if(lt(t,${dipStart}),1,if(lt(t,${dropAtS}),` + + `1-${(1 - DROP_DIP).toFixed(3)}*(t-${dipStart})/${DROP_DIP_LEN.toFixed(3)},1))':eval=frame` + : ""; +const stages = [bedCond, `[bedmain]volume=${BED_GAIN}${bedFade}${dropAuto}[bedlvl]`]; if (keySplitStage) stages.push(keySplitStage); if (includeVox) { - stages.push( - `[1:a]aresample=48000,aformat=channel_layouts=stereo,${reverbStage}` + + let voxChain; + if (VOX_LAST > 0) { + // Outro-only vocals: take the TAIL of the vowel stem and place it so it + // occupies just the final VOX_LAST seconds of the track, swelling in. + // The master's 7 s tail fade rides it out at the very end. + const winLen = Math.min(VOX_LAST, voxDur); + const voxStart = Math.max(0, voxDur - winLen); + const placeMs = Math.round(Math.max(0, total - winLen) * 1000); + const fadeIn = Math.min(VOX_FADEIN, winLen * 0.4); + voxChain = + `[1:a]aresample=48000,aformat=channel_layouts=stereo,` + + `atrim=start=${voxStart.toFixed(3)}:end=${voxDur.toFixed(3)},asetpts=PTS-STARTPTS,` + + `${reverbStage}volume=${VOX_GAIN},` + + `afade=t=in:st=0:d=${fadeIn.toFixed(2)}:curve=exp,` + + `adelay=${placeMs}|${placeMs}[voxwet]`; + } else { + voxChain = + `[1:a]aresample=48000,aformat=channel_layouts=stereo,${reverbStage}` + `volume=${VOX_GAIN},afade=t=in:st=0:d=${VOX_FADEIN.toFixed(2)}:curve=exp,` + - `adelay=${voxDelayMs}|${voxDelayMs}[voxwet]`, + `adelay=${voxDelayMs}|${voxDelayMs}[voxwet]`; + } + stages.push( + voxChain, `[voxwet]${keyVox}sidechaincompress=threshold=0.04:ratio=${DUCK_RATIO}:` + `attack=5:release=240:makeup=2:level_sc=0.8[voxducked]`); mixLabels.push(`[voxducked]`); @@ -423,4 +461,30 @@ console.log(` sample peak: ${peakDb} dBFS · clipped samples: ${clipped}` + console.log(`\n✓ ${FINAL_WAV.replace(homedir(), "~")} (${dur(FINAL_WAV).toFixed(1)}s · 48k/24-bit master)`); console.log(`✓ ${DK_WAV.replace(homedir(), "~")} ← DistroKid upload (44.1k/16-bit, ${LUFS} LUFS)`); console.log(`✓ ${FINAL_MP3.replace(homedir(), "~")}`); + +// ── DISTROKID PACKAGE (--package): refresh the tagged WAV + cover-embedded +// mp3 on the Desktop so every bake keeps the release deliverable current. +if (argv.includes("--package")) { + const PKG = `${homedir()}/Desktop/amaythingra-DistroKid`; + mkdirSync(PKG, { recursive: true }); + const META = `-metadata title="Amaythingra" -metadata artist="Aesthetic Dot Computer" ` + + `-metadata album_artist="Aesthetic Dot Computer" -metadata album="Pixsies" ` + + `-metadata genre="Dance" -metadata date="2026"`; + const deskWav = `${homedir()}/Desktop/amaythingra.wav`; + const deskMp3 = `${homedir()}/Desktop/amaythingra.mp3`; + const cover3000 = `${PKG}/cover-3000.png`; + execSync(`ffmpeg -y -loglevel error -i "${DK_WAV}" -c:a copy ${META} "${deskWav}"`); + execSync(`cp "${deskWav}" "${PKG}/amaythingra.wav"`); + if (existsSync(cover3000)) { + const embed = "/tmp/amaythingra-cover-embed.jpg"; + execSync(`magick "${cover3000}" -resize 1500x1500 -quality 90 "${embed}"`); + execSync(`ffmpeg -y -loglevel error -i "${FINAL_MP3}" -i "${embed}" -map 0:a -map 1:v ` + + `-c:a copy -c:v copy -id3v2_version 3 ${META} -metadata:s:v title="Album cover" ` + + `-disposition:v:0 attached_pic "${deskMp3}"`); + } else { + execSync(`ffmpeg -y -loglevel error -i "${FINAL_MP3}" -c:a copy ${META} "${deskMp3}"`); + } + execSync(`cp "${deskMp3}" "${PKG}/amaythingra-reference.mp3"`); + console.log(`✓ packaged → ${PKG.replace(homedir(), "~")} (tagged WAV + cover-embedded mp3, Title-Case)`); +} console.log(`\n open -a "QuickTime Player" "${FINAL_MP3}"`); diff --git a/pop/big-pictures/bin/extract-vowels.mjs b/pop/big-pictures/bin/extract-vowels.mjs index bd9884f930..3dec83821e 100644 --- a/pop/big-pictures/bin/extract-vowels.mjs +++ b/pop/big-pictures/bin/extract-vowels.mjs @@ -65,9 +65,20 @@ const CRISP = flag("crisp", "2"); // rubberband const HARMONY = flag("harmony", "0,-12").split(",").map((s) => parseInt(s, 10)); const DRONE_GAIN = parseFloat(flag("drone-gain", "0.18")); // continuous floor level (0 = off) const NO_CACHE = argv.includes("--no-cache"); -const CORE_START = parseFloat(flag("core-start", "0.30")); -const CORE_END = parseFloat(flag("core-end", "0.68")); +// Tighter default core window than before: pull the grain to the pure +// vowel nucleus so consonant onsets/codas can't leak in (@jeffrey: "no +// consonants in the jeffrey takes"). The min-length guard below still +// widens it for short takes. +const CORE_START = parseFloat(flag("core-start", "0.36")); +const CORE_END = parseFloat(flag("core-end", "0.62")); const STRETCH_CAP = 5.0; // max rubberband ratio (loop first to stay under) +// Vowel gate (@jeffrey: "only ashhhs and oooh"). Keep the open + rounded +// vowels — ah (the "ashh") and aw/uh (the rounded "oooh") — and drop the +// bright/closed syllables (zing=ih, grace/wretch=eh, sweet/me/see=ee, +// saved=ay) whose hard consonant attacks carry the words. Timing still +// advances across skipped syllables, so the kept vowels stay on their +// original onsets with the dropped ones simply left as breathing room. +const KEEP_VOWELS = flag("vowels", "ah,aw,uh").split(",").map((s) => s.trim()); // ── tooling ─────────────────────────────────────────────────────────── const WORLD_PY = resolve(POP, ".venv/bin/python"); @@ -171,26 +182,31 @@ for (const sample of spec.samples) { if (!sample.score) continue; for (let i = 0; i < sample.score.notes.length; i++) { const n = sample.score.notes[i]; + const vowel = (VOWELS[sample.name] || [])[i] || "ah"; + // Advance the timeline for EVERY syllable so kept vowels keep their + // original onsets; only push the ones that pass the vowel gate. + const onsetSec = cumBeats * STRIDE_PER_BEAT; + cumBeats += n.beats; + if (!KEEP_VOWELS.includes(vowel)) continue; // drop bright/closed vowels const takePath = `${TAKES_DIR}/${sample.name}-note-${i}.wav`; notes.push({ idx: notes.length, sampleName: sample.name, noteIdx: i, - onsetSec: cumBeats * STRIDE_PER_BEAT, + onsetSec, holdSec: n.beats * HOLD_PER_BEAT, targetMidi: sample.score.rootMel + n.off, - vowel: (VOWELS[sample.name] || [])[i] || "ah", + vowel, takePath: existsSync(takePath) ? takePath : null, }); - cumBeats += n.beats; } } const TOTAL = notes.length ? Math.max(...notes.map((n) => n.onsetSec + n.holdSec)) + 1 : 0; console.log( - `vowel extraction: ${notes.length} syllables · onset span ` + - `${(cumBeats * STRIDE_PER_BEAT).toFixed(1)}s · total ${TOTAL.toFixed(1)}s\n` + + `vowel extraction: ${notes.length} syllables kept [${KEEP_VOWELS.join("/")}] · ` + + `onset span ${(cumBeats * STRIDE_PER_BEAT).toFixed(1)}s · total ${TOTAL.toFixed(1)}s\n` + ` hold/beat ${HOLD_PER_BEAT}s · stride/beat ${STRIDE_PER_BEAT}s · ` + `harmony [${HARMONY.join(", ")}] · drone ${DRONE_GAIN}\n` ); diff --git a/pop/big-pictures/bin/gen-struct.mjs b/pop/big-pictures/bin/gen-struct.mjs new file mode 100644 index 0000000000..8a30677583 --- /dev/null +++ b/pop/big-pictures/bin/gen-struct.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +// gen-struct.mjs — author the visualizer telemetry (struct.json) for the +// amaythingra release video. amaythingra is rendered by a bespoke C engine +// (c/amaythingra.c), which (unlike the JS pop tracks) does NOT emit a +// struct.json. This script reconstructs an equivalent from the engine's +// known bar map so the marimbaba/trancepenta-style score-panel + section +// narrative can animate against it. +// +// Track facts (from c/amaythingra.c): 120 BPM, 4/4, G major, 273 s (4:33). +// bars 0-15 (0-32s) INTRO — sine pads + church bells, kick from ~bar 3 +// bars 16-31 (32-64s) BUILD — WOOP at 32s; kick+shaker, powersaws+bells +// bars 32-47 (64-96s) DROP 1 — full kit + chord stab +// bars 48-55 (96-112s) BREAK — pads + resolved chord-tone bell melody +// bars 56-63 (112-128s) VORTEX — scratch-glissando bell storm into drop 2 +// bars 64-95 (128-192s) DROP 2 — full kit + glock; kick arpeggio @164-178s +// bars 96-123 (192-248s) OUTRO — 3:15 mix-switch, perc steps back; reverse +// kick swooshes ~3:00; vowel vocal in @213s +// bars 124-136 (248-273s) RESOLVE — wind-down, final tolls, descending outro +// melody, vowel finale, 7 s tail fade +// +// Writes: pop/big-pictures/out/amaythingra.struct.json (and a copy next to +// the mp3 under .assets/struct.json, where the builders look). +// +// Events are REPRESENTATIVE (the engine's per-render chord progression is +// random, so pitches use a fixed G-pentatonic stand-in) — enough to drive a +// rhythmically/melodically honest score panel. + +import { writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO = resolve(HERE, "../../.."); +const POP = resolve(REPO, "pop"); +const OUT = resolve(POP, "big-pictures/out/amaythingra.struct.json"); +const TRACK_ASSETS = `${homedir()}/Documents/Shelf/amaythingra/amaythingra.mp3.assets`; + +const BPM = 120, SPB = 60 / BPM, BAR = 4 * SPB; // 0.5 s beat, 2.0 s bar +const TOTAL = 273; + +// ── sections (8) — musical arc + narrative beats for the illustrations ── +const sections = [ + { name: "intro", startSec: 0, endSec: 32 }, + { name: "build", startSec: 32, endSec: 64 }, + { name: "drop1", startSec: 64, endSec: 96 }, + { name: "break", startSec: 96, endSec: 112 }, + { name: "vortex", startSec: 112, endSec: 128 }, + { name: "drop2", startSec: 128, endSec: 192 }, + { name: "outro", startSec: 192, endSec: 248 }, + { name: "resolve", startSec: 248, endSec: 273 }, +]; + +// ── helpers ───────────────────────────────────────────────────────── +const barT = (bar) => bar * BAR; +const inBreak = (bar) => bar >= 48 && bar < 64; // kit drops here +// G major pentatonic stand-in pitches (the engine's real prog is random) +const PENT = [55, 57, 59, 62, 64, 67, 69, 71, 74, 76]; // G3..D5 +const ROOTS = [43, 48, 43, 50]; // G C G D (per 4-bar block) + +const events = { kick: [], sub: [], glock: [], pad: [] }; + +// percMult mirror (for gain shaping in the back third, matches the engine) +function ramp01(t, a, b) { return t <= a ? 0 : t >= b ? 1 : (t - a) / (b - a); } +function percGain(t) { + let g = 1 - 0.55 * ramp01(t, 195, 225); + g *= 1 - 0.80 * ramp01(t, 214, 230); + g *= 1 - ramp01(t, 255, 268); + return g; +} + +for (let bar = 0; bar < Math.floor(TOTAL / BAR); bar++) { + const t0 = barT(bar); + const kickOn = bar >= 3 && !inBreak(bar) && bar < 134; // groove window + if (kickOn) { + const pg = percGain(t0); + for (let beat = 0; beat < 4; beat++) { + // 4-on-floor + offbeat 8th "heartbeat" kick + events.kick.push({ t: +(t0 + beat * SPB).toFixed(3), dur: 0.18, midi: 36, gain: +(0.9 * pg).toFixed(3) }); + events.kick.push({ t: +(t0 + (beat + 0.5) * SPB).toFixed(3), dur: 0.14, midi: 36, gain: +(0.45 * pg).toFixed(3) }); + } + // sub-bass follows the downbeats + events.sub.push({ t: +t0.toFixed(3), dur: BAR, midi: 31, gain: +(0.5 * pg).toFixed(3) }); + } + // pad chord every 4 bars (bars 4..end), sustained + if (bar >= 4 && bar % 4 === 0) { + const root = ROOTS[(bar / 4) % 4]; + events.pad.push({ t: +t0.toFixed(3), dur: 4 * BAR, midi: root, gain: 0.5 }); + events.pad.push({ t: +t0.toFixed(3), dur: 4 * BAR, midi: root + 7, gain: 0.4 }); + } +} + +// glock: break resolved melody (bars 48-55, 2/bar), vortex storm (56-63, +// dense), drop2 arpeggio (64-95, on beats), + kick-arp marker (82-89). +for (let bar = 48; bar < 56; bar++) { + events.glock.push({ t: +(barT(bar)).toFixed(3), dur: 1.6, midi: PENT[5], gain: 0.5 }); + events.glock.push({ t: +(barT(bar) + 2 * SPB).toFixed(3), dur: 1.6, midi: PENT[3], gain: 0.45 }); +} +for (let bar = 56; bar < 64; bar++) { // vortex: fast flicks + const subdiv = bar < 60 ? 8 : 16; + for (let s = 0; s < subdiv; s++) { + events.glock.push({ t: +(barT(bar) + s * (BAR / subdiv)).toFixed(3), dur: 0.14, + midi: PENT[(bar * 3 + s) % PENT.length], gain: 0.45 }); + } +} +for (let bar = 64; bar < 96; bar++) { // drop2 arpeggio + for (let beat = 0; beat < 4; beat++) { + events.glock.push({ t: +(barT(bar) + beat * SPB).toFixed(3), dur: 0.45, + midi: PENT[(bar + beat) % PENT.length], gain: 0.45 }); + } +} +// outro descending resolve (256-262s) +const line = [76, 74, 71, 67, 64, 59]; +line.forEach((m, i) => events.glock.push({ t: +(256 + i).toFixed(3), dur: 6 + i, midi: m, gain: 0.55 })); + +// sort every lane by time +for (const k of Object.keys(events)) events.events?.length, events[k].sort((a, b) => a.t - b.t); + +const struct = { + _comment: "amaythingra visualizer telemetry — reconstructed from c/amaythingra.c (representative events; engine prog is random)", + meter: 4, bpm: BPM, scale: "major", rootMidi: 67, totalSec: TOTAL, prerollSec: 0, + sections, events, +}; + +mkdirSync(dirname(OUT), { recursive: true }); +writeFileSync(OUT, JSON.stringify(struct, null, 2)); +let copied = ""; +if (existsSync(dirname(TRACK_ASSETS)) || true) { + mkdirSync(TRACK_ASSETS, { recursive: true }); + writeFileSync(`${TRACK_ASSETS}/struct.json`, JSON.stringify(struct, null, 2)); + copied = ` + ${TRACK_ASSETS}/struct.json`; +} +const counts = Object.entries(events).map(([k, v]) => `${k}:${v.length}`).join(" "); +console.log(`✓ ${OUT.replace(POP + "/", "pop/")}${copied}`); +console.log(` ${sections.length} sections · events { ${counts} } · ${TOTAL}s @ ${BPM} BPM`); diff --git a/pop/big-pictures/bin/make-boot-chime.mjs b/pop/big-pictures/bin/make-boot-chime.mjs index ecf7db71f8..21f59e0acb 100644 --- a/pop/big-pictures/bin/make-boot-chime.mjs +++ b/pop/big-pictures/bin/make-boot-chime.mjs @@ -37,9 +37,14 @@ const NOTES = [ { tone: 659.25, dur: 0.15, vol: 0.70 }, // E5 { tone: 783.99, dur: 0.20, vol: 0.80 }, // G5 ]; -const GAP = 0.060; +// @jeffrey: space the chimes out — ONE PER KICK, still on the beat. Each +// note onset lands exactly one beat apart (0.5 s @ 120 BPM) so the three +// chimes ring out on three consecutive kicks instead of a fast arpeggio. +const BPM = parseFloat(flag("bpm", "120")); +const STEP = parseFloat(flag("step", (60 / BPM).toFixed(4))); // one beat / kick -const totalSec = NOTES.reduce((s, n) => s + n.dur + GAP, 0) + 0.10; +const lastNote = NOTES[NOTES.length - 1]; +const totalSec = (NOTES.length - 1) * STEP + lastNote.dur + 0.30; const N = Math.ceil(totalSec * SR); const buf = new Float32Array(N); @@ -49,9 +54,9 @@ function triangle(phase) { return (2 / Math.PI) * Math.asin(Math.sin(2 * Math.PI * phase)); } -let cursor = 0; // seconds +let step = 0; // note index → onset = step * STEP (one per kick, on the beat) for (const n of NOTES) { - const start = Math.round(cursor * SR); + const start = Math.round(step * STEP * SR); const len = Math.round(n.dur * SR); const atk = Math.round(0.003 * SR); const dec = n.dur * 0.6; @@ -65,7 +70,7 @@ for (const n of NOTES) { const idx = start + i; if (idx < N) buf[idx] += s; } - cursor += n.dur + GAP; + step++; } // write a temp raw f32 → let ffmpeg wrap to 16-bit pcm wav (mono 48k) diff --git a/pop/big-pictures/c/amaythingra.c b/pop/big-pictures/c/amaythingra.c index 233f6bbd80..087f573699 100644 --- a/pop/big-pictures/c/amaythingra.c +++ b/pop/big-pictures/c/amaythingra.c @@ -42,7 +42,7 @@ // ── config ──────────────────────────────────────────────────────────── static const int SR = 48000; static double BPM = 120.0; -static double TOTAL_SEC = 274.0; // ~4:34 — winds down + resolves (was a hard 5:00 cut, @jeffrey) +static double TOTAL_SEC = 273.0; // 4:33 exactly (@jeffrey) — winds down + resolves static const char *OUT_PATH = NULL; // --drive : pre-tanh scale. 1.0 (default) keeps the original hard // tanh "limiter" used by the released cover. The vowel bake passes a @@ -646,6 +646,33 @@ static void snare_render(double t0, SnareOpts opt) { } } +// ── reverse snare (the snare's noise burst, played backwards) ───────── +// A high-passed noise wash whose envelope SWELLS from near-silent up to a +// hard cut at t0+dur, brightening as it rises so it "opens up" and sucks +// INTO a downbeat. Pair with a forward snare808 on the landing beat for a +// "reverse-snare → snare" transition fill (@jeffrey, 3:30). +static void snare_reverse_render(double t0, double dur, double gain, double pan) { + long iStart = (long)(t0 * SR); + long iEnd = iStart + (long)(dur * SR); + if (iStart < 0) iStart = 0; + if (iEnd > N) iEnd = N; + uint32_t s = (uint32_t)(t0 * 1000003.0) | 1; + double hp_in_prev = 0.0, hp_out_prev = 0.0; + for (long i = iStart; i < iEnd; i++) { + const double t = (i - iStart) / (double)SR; + const double u = t / dur; // 0 → 1 across the swell + const double env = u * u; // quadratic crescendo, rushing up late + const double bright = 0.5 + 0.45 * u; // opens up toward the hit + s ^= s << 13; s ^= s >> 17; s ^= s << 5; + const double noise = ((double)s / 4294967296.0) * 2.0 - 1.0; + const double hp_out = bright * (hp_out_prev + noise - hp_in_prev); + hp_in_prev = noise; hp_out_prev = hp_out; + const double sample = hp_out * env * gain; + L[i] += sample * (1.0 - 0.5 * pan); + R[i] += sample * (1.0 + 0.5 * pan); + } +} + // ── ocean crash voice (pink-noise wash with slow attack + long tail) ─ // Replaces the clap backbeat per @jeffrey 2026-05-29. Wide-band pink // noise (cascade of LP filters approximating 1/f spectrum) with a @@ -1461,22 +1488,30 @@ static void render_track(void) { .pitch_hi = kphi * opf * 0.97, .pitch_lo = kplo * opf, .pitch_dur = kpdur, .decay_s = kdec * 0.8 }); } - // REVERSE KICK — SPRINKLED, never earlier than ~30 s - // (@jeffrey 2026-05-29). Picks 6 strategic bar boundaries - // through the rest of the track instead of every 4 bars. - // Each is a 2-beat rising sweep building into the next bar. - const int rev_bars[] = { 23, 79, 95 }; // dropped 39 & 47 (1:18/1:34 "big crashes" — too dramatic, @jeffrey) - const int n_rev = sizeof(rev_bars) / sizeof(rev_bars[0]); + // REVERSE KICK "woops" — SPRINKLED, never earlier than ~30 s + // (@jeffrey 2026-05-29). Each entry tunes its OWN length + pitch + // range + pan so the late-track woops at ~3:00 and ~3:12 read as + // DISTINCT, longer swooshes rather than three identical sweeps + // (@jeffrey "those woops can be different longer"). + // bar 88 ≈ 2:56 · bar 90 ≈ 3:00 · bar 95 ≈ 3:10-3:12 + typedef struct { int bar; double offBeats, gain, plo, phi, durBeats; } RevKick; + static const RevKick rev_kicks[] = { + { 23, 2, 1.6, 60, 220, 2 }, // mid-track sprinkle (unchanged) + { 79, 0, 2.0, 50, 260, 4 }, // major DROP-2 transition (big) + { 88, 0, 1.7, 40, 190, 6 }, // ~2:56 — long, low, slow "booooowub" rise + { 90, 1, 1.7, 64, 320, 5 }, // ~3:01 — later onset, sweeps higher/brighter + { 95, 0, 1.9, 72, 380, 8 }, // ~3:10 — longest, highest "wooooop" lifting into the outro + }; + const int n_rev = sizeof(rev_kicks) / sizeof(rev_kicks[0]); for (int q = 0; q < n_rev; q++) { - if (bar == rev_bars[q]) { - // Bigger sweeps on the major transition bars - const int big = (bar == 15 || bar == 47 || bar == 79); - reverse_kick_render(t0 + (big ? 0 : 2 * SPB), + if (bar == rev_kicks[q].bar) { + const RevKick rk = rev_kicks[q]; + reverse_kick_render(t0 + rk.offBeats * SPB, (ReverseKickOpts){ .pan = 0.0, - .gain = big ? 2.0 : 1.6, - .pitch_lo = big ? 50 : 60, - .pitch_hi = big ? 260 : 220, - .dur = big ? 4 * SPB : 2 * SPB }); + .gain = rk.gain, + .pitch_lo = rk.plo, + .pitch_hi = rk.phi, + .dur = rk.durBeats * SPB }); break; } } @@ -1609,13 +1644,28 @@ static void render_track(void) { // drives the second half (skips the break + final outro) (@jeffrey). { const double SPB = 60.0 / BPM; + // Snares sit UNDER the vocals now they're back in the mix: backbeat + // velocity pulled down from 0.50 so the 808 doesn't fight the + // ashh/ooh drone (@jeffrey "bring those snares down when the jeffrey + // vocals come in"). The 3:30 transition hit below stays loud. + const double SNARE_VEL = 0.36; for (int bar = 42; bar < TOTAL_BARS - 2; bar++) { if (bar >= 48 && bar < 64) continue; // break const double t0 = bar * BAR; - snare808(t0 + 1.0 * SPB + humanize(t0 + 1, 15, 5), 0.50, -0.05); // beat 2, laid back - snare808(t0 + 3.0 * SPB + humanize(t0 + 3, 15, 5), 0.50, +0.05); // beat 4, laid back + snare808(t0 + 1.0 * SPB + humanize(t0 + 1, 15, 5), SNARE_VEL, -0.05); // beat 2, laid back + snare808(t0 + 3.0 * SPB + humanize(t0 + 3, 15, 5), SNARE_VEL, +0.05); // beat 4, laid back } - report(" 808 snare on backbeats from ~1:25"); + report(" 808 snare on backbeats from ~1:25 (ducked under vocals)"); + + // ── reverse-snare → snare transition at 3:30 (bar 105) ─────────── + // A 1-bar reverse snare swells UP into the 3:30 downbeat and lands + // on a louder forward 808 hit — the "snare-reverse-snare" gesture + // (@jeffrey). Sits right where the 2:30-3:30 play-more bell section + // ends and the groove drives on into the outro. + const double land = 105.0 * BAR; // 210.0 s = 3:30 downbeat + snare_reverse_render(land - BAR, BAR, 0.42, 0.0); // 2 s crescendo swell + snare808(land, 0.85, 0.0); // the landing hit + report(" reverse-snare -> snare transition @3:30"); } // OPENING SINE BELL REMOVED — the long G5/G6 sine tails read as @@ -1675,13 +1725,17 @@ static void render_track(void) { const double csDet = cs * 3.0; // faint buzz only const double csGain = 1.0 + cs * 0.05; // no real swell const double csAtk = 1.0 - cs * 0.1; - powersaw_render(t0, dur, c->root - 12, // sub saw — more low + // Lift the hymnal saws an octave through the late outro approach so + // they climb HIGHER by ~3:45 (@jeffrey). Sub stays low for weight; + // the mid + top voices rise. Ramps in from bar 104 (~3:28). + const int hi = (bar >= 104) ? 12 : 0; + powersaw_render(t0, dur, c->root - 12, // sub saw — stays low for weight (PowersawOpts){ .atk = 0.06 * csAtk, .rel = 0.30, .pan = -0.16, .gain = 0.13 * csGain, .wet_send = 0.35, .detune_cents = 12.0 + csDet }); - powersaw_render(t0, dur, c->root, // low-mid root + powersaw_render(t0, dur, c->root + hi, // low-mid root (lifts late) (PowersawOpts){ .atk = 0.08 * csAtk, .rel = 0.30, .pan = +0.12, .gain = 0.10 * csGain, .wet_send = 0.40, .detune_cents = 16.0 + csDet }); - powersaw_render(t0, dur, top, // subtle upper voice + powersaw_render(t0, dur, top + hi, // subtle upper voice (lifts late) (PowersawOpts){ .atk = 0.10 * csAtk, .rel = 0.30, .pan = 0.0, .gain = 0.07 * csGain, .wet_send = 0.40, .detune_cents = 18.0 + csDet }); }