diff --git a/pop/cult/.gitignore b/pop/cult/.gitignore index 8cdf25978b..ef3b06f026 100644 --- a/pop/cult/.gitignore +++ b/pop/cult/.gitignore @@ -39,3 +39,7 @@ source/ reference/ref reference/*.f32 reference/*.wav + +# phone/ — real telephone gestures decoded from the vault freesound cache +# (all CC0; bin/phones.sh rebuilds them; the manifest IS tracked) +phone/*.wav diff --git a/pop/cult/README.md b/pop/cult/README.md index a228b16bfc..629a89a73f 100644 --- a/pop/cult/README.md +++ b/pop/cult/README.md @@ -771,3 +771,40 @@ version failed at least once): The lead sits ~4 dB proud of the bed, which is the whole point of the measurement. + +## v10.1 — the words withheld, the dots everywhere, the phone made real + +Three asks from @jeffrey, one revision (`bin/render10.mjs`, gate: +`wordsIn()`): **"run real fast"** and **"i wanna hide away"** are removed +from every chorus statement and hook until act VII — before bar 76 the +steady loop runs morse-only, with dot figures standing exactly where the +worded lines will land. The shipped opening (bars 8–24) is now a **dot +collage from all nine harvested posts** — each bar one video speaks a +morse S into the dial's echo (raw `alt/samples` takes), answered by the +aesthetivoxed `sung/` dots in chord tones, both riding the signal layer's +pans and delay so human dots and phone tones read as one transmission. + +And the signal layer grew a **real telephone**: `phone/` holds CC0 +freesound gestures (receiver pickups, hang-ups, rotary dials, busy +signals — ids + credits in `phone/manifest.json`, tracked). Rebuild after +a fresh checkout with `bash bin/phones.sh` (decodes from the vault +freesound cache; re-fetch via `pop/bin/freesound-fetch.mjs` if the cache +is cold). The record now opens with a handset lifted under the first +kick, act IV's "receiver picked up" click is an actual receiver, the +act-VI switchboard has a rotary dial in it, and the last sound is a real +hang-up with a distant US busy signal after it. + +Second pass, same day, all from @jeffrey listening back: the opening +collage was "too chaotic … wayyyy less of them … all extended a lot … +more in the background" — so it became **one dot at a time**, each of the +nine posts appearing exactly once, its longest take granular-stretched +~4.5× into a dark two-second sigh deep in the dial's echo. The arriving +line lost its pickup ("no need for 'i wanna' before hide away") and got +"sung much longerrrrr": `sung/hideaway-hi` stretched 1.9× across 3.8 s +with an octave-down ghost and the tube "away"s moved late. And **THE +HUMANS act is cut** ("remove the ending section at 3:00 where it's no +longer sung … i don't like when vocals skip aesthetivox and feel too +raw") — 120 bars → 112, the peel-off walks straight into CARRIER OFF, +and no vocal on the record skips the aesthetivox anymore. + +Shipped master: −14.0 LUFS integrated target, ~3:29. diff --git a/pop/cult/bin/cut-v10.sh b/pop/cult/bin/cut-v10.sh index 0238fe5bee..15f04f7e44 100644 --- a/pop/cult/bin/cut-v10.sh +++ b/pop/cult/bin/cut-v10.sh @@ -40,6 +40,6 @@ fi ffmpeg -y -v error -i "$SRC" -c:a libmp3lame -b:a 320k \ -metadata title="whistlegraph cult --- remix (v10, kicks first)" \ -metadata artist="Whistlegraph" -metadata album="pop / cult" \ - -metadata comment="v9's steady loop, re-entered: opens on the kick, no voice until 'run real fast' at bar 24, the morse follows." \ + -metadata comment="v9's steady loop with the words withheld: opens on the kick and a crowd of dots from every video, the loop runs morse-only, and 'run real fast / i wanna hide away' arrive at the whole message. Real phone: pickup, rotary, hang-up, busy." \ "$OUT/cult-remix-v10.mp3" echo "✓ $OUT/cult-remix-v10.mp3" diff --git a/pop/cult/bin/longdots.sh b/pop/cult/bin/longdots.sh new file mode 100755 index 0000000000..ac4ccd678b --- /dev/null +++ b/pop/cult/bin/longdots.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# longdots.sh — aesthetivox the opening dots. +# +# v10.1: the opening's slow dots were granular-stretched raw takes, and +# @jeffrey heard the grains: "more smoooooth … lessss choppy in their +# slowness". The smooth way to make a spoken syllable long is the way this +# lane already makes everything long — the WORLD chain (bin/sing.py), whose +# duration control sustains the vowel and leaves the consonant alone. So: +# each of the nine posts' chosen dot take, held ~1.9 s at its own measured +# pitch snapped to the nearest B-natural-minor tone (median f0 from +# alt/harvest.json), gentle vibrato, into sung/altdot--long.wav. +# The render prefers these and only falls back to the granular stretch if +# they have not been built. +# +# Needs: alt/samples (see README's derived-bank recipe) and pop/.venv. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LANE="$(dirname "$HERE")" +PY="$LANE/../.venv/bin/python3" +[ -x "$PY" ] || PY=python3 + +# take → note (measured median f0, snapped into B natural minor) +render() { + local take="$1" note="$2" id + id=$(echo "$take" | sed 's/alt-\([0-9]*\)-.*/\1/') + local src="$LANE/alt/samples/$take.wav" out="$LANE/sung/altdot-$id-long.wav" + if [ ! -f "$src" ]; then echo "! $take missing (harvest alt/ first)" >&2; return; fi + if [ -f "$out" ] && [ ! "$src" -nt "$out" ]; then return; fi + "$PY" "$HERE/sing.py" "$src" "$out" --notes "$note:1.9" \ + --vibrato-cents 22 --vibrato-onset-ms 500 --formant-db 1.6 --release-ms 220 + echo "✓ altdot-$id-long ← $take @ $note" +} + +# …and the aesthetivox side of the drift: the three performers' own dot +# takes held the same way, two chord-safe tones each, so dotDriftVox can +# answer in tune without a stretcher (the 0.2 s staccato sung/dot-* bank +# chops when slowed — these don't). +rendervox() { + local src="$1" name="$2" note="$3" + local in="$LANE/samples/$src.wav" out="$LANE/sung/voxdot-$name.wav" + if [ ! -f "$in" ]; then echo "! $src missing (run bin/slice.mjs first)" >&2; return; fi + if [ -f "$out" ] && [ ! "$in" -nt "$out" ]; then return; fi + "$PY" "$HERE/sing.py" "$in" "$out" --notes "$note:1.7" \ + --vibrato-cents 20 --vibrato-onset-ms 450 --formant-db 1.6 --release-ms 200 + echo "✓ voxdot-$name ← $src @ $note" +} + +rendervox dot-jeffrey j-b2 B2 +rendervox dot-jeffrey j-fs3 F#3 +rendervox dot-alex a-a3 A3 +rendervox dot-alex a-d4 D4 +rendervox dot-camille c-b3 B3 +rendervox dot-camille c-fs4 F#4 + +# The "i wanna" pickups: @jeffrey — "could be slowed a bit more, like +# faded / a bit slowed, its sorta rushed". Same takes, re-sung ~1.5x +# longer with a softer attack; the render fades them a shade lower too. +renderiwanna() { + local src="$1" name="$2" notes="$3" + local in="$LANE/samples/$src.wav" out="$LANE/sung/$name.wav" + if [ ! -f "$in" ]; then echo "! $src missing (run bin/slice.mjs first)" >&2; return; fi + if [ -f "$out" ] && [ ! "$in" -nt "$out" ]; then return; fi + "$PY" "$HERE/sing.py" "$in" "$out" --notes "$notes" \ + --vibrato-cents 24 --attack-ms 40 --release-ms 160 + echo "✓ $name ← $src @ $notes" +} +renderiwanna i-wanna-a iwannaslow-a "D4:0.30,E4:0.48" +renderiwanna i-wanna-b iwannaslow-b "B3:0.30,C#4:0.48" +renderiwanna iwanna-c iwannaslow-c "B3:0.33,C#4:0.45" + +# The harmonic "cult" chords: the 4 s sung/cult-* takes end just as the +# drums come back, and @jeffrey heard the cut-off ("much lpnger decay … +# so they dont cut off when the perc and kicks come back"). So every choir +# pitch also gets a five-second hold with a 900 ms release — the choir +# rings past the act boundary and decays under the returning kick instead +# of vanishing at it. +rendercult() { + local note="$1" tag="$2" + local in="$LANE/samples/cult.wav" out="$LANE/sung/cultlong-$tag.wav" + [ -f "$in" ] || { echo "! samples/cult.wav missing (run bin/slice.mjs)" >&2; return; } + if [ -f "$out" ] && [ ! "$in" -nt "$out" ]; then return; fi + "$PY" "$HERE/sing.py" "$in" "$out" --notes "$note:5.0" \ + --vibrato-cents 18 --vibrato-onset-ms 900 --formant-db 2.0 --release-ms 900 + echo "✓ cultlong-$tag @ $note" +} +rendercult B2 b2; rendercult D3 d3; rendercult F#3 fs3; rendercult G3 g3 +rendercult A3 a3; rendercult B3 b3; rendercult C#4 cs4; rendercult D4 d4 +rendercult E4 e4; rendercult F#4 fs4; rendercult G4 g4 + +render alt-70551-dot2 B3 +render alt-70555-dot1 B2 +render alt-71018-dot3 G3 +render alt-71195-dot1 B2 +render alt-71244-dot2 C#4 +render alt-71437-dot1 B3 +render alt-71441-dot1 D4 +render alt-71448-dot F#4 +render alt-71560-dot2 C#4 diff --git a/pop/cult/bin/modemize.py b/pop/cult/bin/modemize.py new file mode 100644 index 0000000000..827dd13ef2 --- /dev/null +++ b/pop/cult/bin/modemize.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +modemize.py — the Matrix throat: a voice that turns into a modem. + +@jeffrey: "how did they do the matrix movie throat scene where the voice +turns into the modem? can we add a feature like that to aesthetivox?" +The technique (Dane Davis's Matrix sound design, and the Kyma-era spectral +morph in general) is cross-synthesis: keep the VOICE's formant envelope — +the part that makes it a mouth — and swap the EXCITATION underneath it +from vocal cords to data, interpolating between the two over time. The ear +tracks the formants, so it hears one continuous thing becoming another. + +The aesthetivox chain already speaks WORLD, and WORLD hands us exactly the +split we need: + + in.wav → harvest/stonemask (f0) · cheaptrick (spectral envelope = the + mouth) · d4c (aperiodicity) + → y_voice: normal WORLD synthesis (the human end of the morph) + → y_data: a modem carrier — V.21-flavour FSK (980/1180 Hz, 300 baud, + random bits), answer-tone bursts at 2100 Hz with the real 15 Hz + phase reversals, scraps of DTMF — pushed through the voice's own + spectral envelope frame by frame (STFT × envelope), so the data + is being said BY THE SAME MOUTH + → morph: equal-power crossfade voice→data along a cosine ramp + (--morph start:end, seconds; f0 also quantizes to semitone + steps as the morph proceeds — pitch becomes protocol) + → raised-cosine top and tail. + + modemize.py in.wav out.wav [--morph 0.4:2.2] [--baud 300] [--mix 1.0] + modemize.py in.wav out.wav --morph 0:0 # fully modem, still mouthed + +Runs on pop/.venv (pyworld/numpy/soundfile), same as sing.py. +""" +import argparse + +import numpy as np +import soundfile as sf +import pyworld as pw + +FRAME_MS = 5.0 + + +def data_carrier(n, fs, baud, seed): + """FSK + answer-tone + DTMF scraps, mono, unit-ish level.""" + rng = np.random.default_rng(seed) + t = np.arange(n) / fs + # 300-baud FSK: random bits, 980/1180 Hz (V.21 originate space/mark) + bits = rng.integers(0, 2, int(np.ceil(n / fs * baud)) + 1) + bit_at = (t * baud).astype(int) + f_inst = np.where(bits[bit_at] > 0, 1180.0, 980.0) + phase = 2 * np.pi * np.cumsum(f_inst) / fs + y = 0.55 * np.sin(phase) + # answer tone: 2100 Hz bursts with 15 Hz phase reversals (the classic + # "aaaannnn" under a handshake), gated on for ~0.45 s every ~1.4 s + gate = ((t % 1.4) < 0.45).astype(float) + rev = np.sign(np.sin(2 * np.pi * 15 * t / 2)) # flips phase at 15 Hz + y += 0.30 * gate * rev * np.sin(2 * np.pi * 2100 * t) + # DTMF scraps: a fresh digit every ~0.23 s, quiet, behind the FSK + ROWS, COLS = [697, 770, 852, 941], [1209, 1336, 1477, 1633] + seg = (t / 0.23).astype(int) + r = rng.integers(0, 4, seg.max() + 1) + c = rng.integers(0, 4, seg.max() + 1) + y += 0.16 * (np.sin(2 * np.pi * np.take(np.array(ROWS)[r], seg) * t) + + np.sin(2 * np.pi * np.take(np.array(COLS)[c], seg) * t)) * 0.5 + return y + + +def mouth_filter(carrier, sp, fs, hop): + """Impose the voice's spectral envelope (WORLD sp frames) on the carrier + via STFT multiply — the data spoken by the same mouth.""" + nfft = 2 * (sp.shape[1] - 1) + win = np.hanning(nfft) + out = np.zeros(len(carrier) + nfft) + norm = np.zeros_like(out) + n_frames = sp.shape[0] + env = np.sqrt(np.maximum(sp, 1e-12)) + env = env / (np.max(env) + 1e-9) + for i in range(0, len(carrier) - nfft, hop): + fr = min(n_frames - 1, int(round(i / fs * 1000.0 / FRAME_MS))) + seg = carrier[i:i + nfft] * win + spec = np.fft.rfft(seg) * env[fr] + rec = np.fft.irfft(spec) * win + out[i:i + nfft] += rec + norm[i:i + nfft] += win * win + out = out[: len(carrier)] / np.maximum(norm[: len(carrier)], 1e-6) + return out + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("in_wav") + p.add_argument("out_wav") + p.add_argument("--morph", default="0.3:1.8", + help="seconds start:end of the voice→modem ramp; 0:0 = all modem") + p.add_argument("--baud", type=float, default=300.0) + p.add_argument("--mix", type=float, default=1.0, help="depth of the modem end (0..1)") + p.add_argument("--f0-floor", type=float, default=90.0) + p.add_argument("--f0-ceil", type=float, default=700.0) + p.add_argument("--gain", type=float, default=0.92) + p.add_argument("--seed", type=int, default=20220120) + a = p.parse_args() + + x, fs = sf.read(a.in_wav) + if x.ndim > 1: + x = x.mean(axis=1) + x = np.ascontiguousarray(x, dtype=np.float64) + + f0, tpos = pw.harvest(x, fs, f0_floor=a.f0_floor, f0_ceil=a.f0_ceil, + frame_period=FRAME_MS) + f0 = pw.stonemask(x, f0, tpos, fs) + sp = pw.cheaptrick(x, f0, tpos, fs) + ap = pw.d4c(x, f0, tpos, fs) + + m0, m1 = (float(v) for v in a.morph.split(":")) + n = len(x) + t = np.arange(n) / fs + if m1 <= m0: + morph = np.ones(n) + else: + u = np.clip((t - m0) / (m1 - m0), 0, 1) + morph = 0.5 - 0.5 * np.cos(np.pi * u) + morph *= a.mix + + # the human end — with f0 quantizing to semitones as the morph comes in + # (measured against the frame-rate morph curve): pitch becomes protocol + frame_morph = morph[np.minimum((tpos * fs).astype(int), n - 1)] + voiced = f0 > 0 + midi = np.zeros_like(f0) + midi[voiced] = 69 + 12 * np.log2(f0[voiced] / 440.0) + quant = np.round(midi) + f0q = f0.copy() + f0q[voiced] = 440.0 * 2 ** (((1 - frame_morph[voiced]) * midi[voiced] + + frame_morph[voiced] * quant[voiced] - 69) / 12) + y_voice = pw.synthesize(f0q, sp, ap, fs, frame_period=FRAME_MS)[:n] + + # the machine end — data through the same mouth + carrier = data_carrier(n, fs, a.baud, a.seed) + y_data = mouth_filter(carrier, sp, fs, hop=(2 * (sp.shape[1] - 1)) // 4)[:n] + y_data *= 0.9 / (np.max(np.abs(y_data)) + 1e-9) + + y = np.sqrt(1 - morph) * y_voice[: len(morph)] + np.sqrt(morph) * y_data[: len(morph)] + + # raised-cosine top and tail + edge = int(0.012 * fs) + ramp = 0.5 - 0.5 * np.cos(np.pi * np.arange(edge) / edge) + y[:edge] *= ramp + y[-edge:] *= ramp[::-1] + y *= a.gain / (np.max(np.abs(y)) + 1e-9) + + sf.write(a.out_wav, y, fs) + dur = len(y) / fs + print(f" modemize · {dur:.2f}s · morph {m0:.2f}→{m1:.2f}s · {a.baud:.0f} baud") + + +if __name__ == "__main__": + main() diff --git a/pop/cult/bin/phones.sh b/pop/cult/bin/phones.sh new file mode 100755 index 0000000000..4ee89dcdd4 --- /dev/null +++ b/pop/cult/bin/phones.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# phones.sh — rebuild phone/*.wav from the vault freesound cache. +# +# The manifest (phone/manifest.json, tracked) names each sample and the +# freesound id it came from; the cached previews live in the vault at +# ~/aesthetic-computer-vault/personal/pop/freesound-cache as -.mp3. +# If one is missing, re-pull it with pop/bin/freesound-fetch.mjs (the ids +# are all CC0 — search the title from the manifest). This script only +# decodes: 48k mono WAV, which is what the render's loader expects. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LANE="$(dirname "$HERE")" +CACHE="$HOME/aesthetic-computer-vault/personal/pop/freesound-cache" + +python3 - "$LANE/phone/manifest.json" <<'PY' | while read -r name id; do +import json, sys +m = json.load(open(sys.argv[1])) +for name, e in m["samples"].items(): + print(name, e["id"]) +PY + src=$(ls "$CACHE/$id-"*.mp3 2>/dev/null | head -1) + if [ -z "$src" ]; then + echo "! $name: freesound $id not in vault cache — re-fetch it (see manifest title)" >&2 + continue + fi + out="$LANE/phone/$name.wav" + if [ ! -f "$out" ] || [ "$src" -nt "$out" ]; then + ffmpeg -y -v error -i "$src" -ac 1 -ar 48000 "$out" + echo "✓ $name ← $(basename "$src")" + fi +done diff --git a/pop/cult/bin/render10.mjs b/pop/cult/bin/render10.mjs index 0027e93591..127926d9f8 100644 --- a/pop/cult/bin/render10.mjs +++ b/pop/cult/bin/render10.mjs @@ -11,6 +11,24 @@ // AROUND a message that never stops looping — and the one place the loop // breaks (act IV, the turn) is now the loudest thing in the record. // +// v10.1, @jeffrey again, four asks in one breath: "remove 'i wanna hide +// away' and 'i wanna run real fast' until the end" · "in the opening we +// should have lots of the 'dot dot dot'" · "from all the videos — samples +// and aesthetivoxed" · "to blend in with the phone tones — and any way we +// can find hang up sounds, phone hang up click and other standard sounds, +// as samples?" So: the loop still never stops, but its two WORDED lines +// are withheld until act VII — before that every statement is morse-only, +// dots standing where the words will land (wordsIn() is the gate). The +// shipped opening carries the dots from all nine posts — after a second +// pass ("too chaotic … wayyyy less … all extended a lot … more in the +// background"), as ONE granular-stretched dot at a time drifting behind +// the dial, each video appearing exactly once. And the signal layer grows a REAL +// phone alongside its synthesized one — CC0 freesound gestures in phone/ +// (bin/phones.sh rebuilds; manifest tracked): a receiver picked up under +// the first kick and again at the turn, a rotary dial in the switchboard, +// and the record now ends on an actual handset meeting an actual cradle, +// with a distant busy signal after it. +// // Everything below this paragraph that talks about v5 still applies to the // bed, the busses and the acts; only the vocal plan changed. // @@ -37,8 +55,8 @@ // A SIGNAL GOES OUT, AND IT IS ANSWERED, AND THE JOKE TURNS OUT // TO BE TRUE. // -// Ten acts, 120 bars, 4:00. Nothing in the track exists that does not -// belong to one of them. +// Nine acts (v10.1 — THE HUMANS is cut), 112 bars, 3:44 full / 3:28 +// shipped. Nothing in the track exists that does not belong to one of them. // // I CARRIER 0:00 A channel opens before anything is said. DTMF // tones dial C-U-L-T (keypad 2-8-5-8) into empty @@ -70,12 +88,12 @@ // — over the home progression, which the harmony // also arrives at here for the first time. // VIII RECOGNITION 3:12 Elements peel off, one per bar. -// IX THE HUMANS 3:28 Drums out. The ORIGINAL recording returns, -// unprocessed: three friends, one take, laughing. -// Everything you just heard was made of this. -// X CARRIER OFF 3:44 The drone walks out. The beeps outlast it, and +// IX CARRIER OFF 3:28 The drone walks out. The beeps outlast it, and // the last sound on the record is a hang-up click, -// after the music has already stopped. +// after the music has already stopped. (v10.1: +// THE HUMANS — the unprocessed original take — +// is cut; no vocal on the record skips the +// aesthetivox anymore.) // // ══ THE FOUR SOUND-DESIGN ASKS ═════════════════════════════════════════ // @@ -136,7 +154,7 @@ const SR = 48_000; const BPM = 120; const BEAT = 60 / BPM; // 0.5 s const BAR = 4 * BEAT; // 2.0 s -const BARS = 120; // 240 s = 4:00 +const BARS = 112; // 224 s = 3:44 full · 3:28 shipped const TAU = Math.PI * 2; const N = Math.round((BARS * BAR + 3.2) * SR); @@ -190,8 +208,11 @@ function classify(name) { return { voice: "dash", who: m[1], midi: pitchMidi(m[2]) }; if ((m = /^dashlong-(camille|jeffrey)-([a-g]s?\d)$/.exec(name))) return { voice: "dash", who: m[1], midi: pitchMidi(m[2]) }; - if ((m = /^cult-([a-g]s?\d)$/.exec(name))) + if ((m = /^cult(?:long)?-([a-g]s?\d)$/.exec(name))) return { voice: "cult", who: "camille", midi: pitchMidi(m[1]) }; + if ((m = /^voxdot-(c|a|j)-([a-g]s?\d)$/.exec(name))) + return { voice: "dot", who: { c: "camille", a: "alex", j: "jeffrey" }[m[1]], midi: pitchMidi(m[2]) }; + if (/^altdot-\d+-long$/.test(name)) return { voice: "dot", who: null, midi: null }; if ((m = /^sos-dash-([a-g]s?\d)$/.exec(name))) return { voice: "sosdash", who: "camille", midi: pitchMidi(m[1]) }; if ((m = /^bassdash-([a-g]s?\d)$/.exec(name))) @@ -207,6 +228,7 @@ function classify(name) { if (name === "sweep") return { voice: "sweep" }; if (/^(chant-full|tagline|hook-spoken|three-of-us)$/.test(name)) return { voice: "source" }; if (name.startsWith("alt-")) return { voice: "alt" }; + if (name.startsWith("phone-")) return { voice: "phone" }; return { voice: "sample" }; } @@ -244,8 +266,10 @@ function load(name, path) { // samples/ = v1's slices; sung/ = the speech-to-singing renders; alt/samples // = anything harvested from the OTHER cult-tagged whistlegraph posts (see -// alt/harvest.json). alt/ is optional — the score checks before it plays. -for (const dir of ["samples", "sung", "alt/samples"]) { +// alt/harvest.json); phone/ = real telephone gestures, CC0 from freesound +// (bin/phones.sh rebuilds them from the manifest). alt/ and phone/ are +// optional — the score checks before it plays. +for (const dir of ["samples", "sung", "alt/samples", "phone"]) { const d = resolve(LANE, dir); if (!existsSync(d)) continue; for (const f of readdirSync(d)) if (f.endsWith(".wav")) load(f.replace(/\.wav$/, ""), join(d, f)); @@ -356,6 +380,77 @@ function kick(t, gain = 1, { weight = 1 } = {}) { // A quiet snare/rim on 3 — mostly there so the tubular pump has a second // trigger, which is what makes the pump read as rhythm rather than as the // kick's shadow. +// v10.1: @jeffrey — "bring in kick reverse wub wub kicks too, like we +// have on other /pop tracks at times". Two re-entry voices: revKick is +// the thick kick's sweep run BACKWARDS — pitch and amplitude rising into +// the downbeat it announces — and wub is the root's sub octave pumped by +// a 4 Hz amplitude-and-lowpass LFO, the kick's tail learning to talk. +function revKick(t, dur = 0.9, gain = 0.5) { + if (!allow("kick")) return; + EVENTS.push({ t: +(t - dur).toFixed(4), voice: "revkick", bus: "drums", + dur: +dur.toFixed(3), gain: +gain.toFixed(3), pan: 0 }); + const n = Math.round(dur * SR), i0 = Math.round((t - dur) * SR); + let p = 0; + for (let i = 0; i < n; i++) { + const u = i / n; + const f = 47 + (200 - 47) * u * u; + p += (TAU * f) / SR; + const env = Math.pow(u, 2.4); + const v = Math.tanh(Math.sin(p) * (1 + 1.5 * u)) * 0.5; + emit("drums", i0 + i, v * env * gain * tailFade(i, n), 0, null, 0.2); + } +} +function wub(t, midi, bars = 1, gain = 0.30, rate = 4) { + if (!allow("bass")) return; + EVENTS.push({ t: +t.toFixed(4), voice: "wub", bus: "music", midi, + dur: +(bars * BAR).toFixed(3), gain: +gain.toFixed(3), pan: 0 }); + const n = Math.round(bars * BAR * SR), i0 = Math.round(t * SR); + const f = hz(midi); + let p = 0, lp = 0; + for (let i = 0; i < n; i++) { + const u = i / SR; + p += (TAU * f) / SR; + const lfo = 0.5 - 0.5 * Math.cos(TAU * rate * u); + const raw = Math.tanh(Math.sin(p) * (1.2 + 2.2 * lfo)); + const k = 0.12 + 0.55 * lfo; + lp += k * (raw - lp); + const env = Math.min(1, u / 0.02); + emit("music", i0 + i, lp * env * 0.5 * gain * tailFade(i, n), 0, null, 0.2); + } +} + +// v10.1: @jeffrey — "more bass in the 1:10 chants, like heavier bass — +// maybe we can even bring in some electric guitar long decay there". So +// act IV grows the record's only stringed thing: a Karplus-Strong pluck +// through a soft tanh drive, feedback high enough to ring for seconds +// under the flanged chant. Synthesized like everything else — the guitar +// is a wavetable of noise the string forgets slowly. +function guitar(t, midi, dur, gain = 0.5, pan = 0, fb = 0.9965) { + if (!allow("guitar")) return; + EVENTS.push({ t: +t.toFixed(4), voice: "guitar", bus: "music", midi, + dur: +dur.toFixed(3), gain: +gain.toFixed(3), pan: +pan.toFixed(2) }); + const period = Math.max(2, Math.round(SR / hz(midi))); + const n = Math.round(dur * SR), i0 = Math.round(t * SR); + const line = new Float32Array(period); + for (let k = 0; k < period; k++) line[k] = nrnd(); + const sp = spatial(pan * 1.2); + let idx = 0, prev = 0; + let lp = 0; + for (let i = 0; i < n; i++) { + const v = line[idx]; + line[idx] = fb * 0.5 * (v + prev); + prev = v; + idx = (idx + 1) % period; + // two-stage asymmetric drive (real distortion, even harmonics), then + // a dark one-pole so the fuzz reads as weight rather than edge + const d1 = Math.tanh(v * 6.5 + 0.35) - Math.tanh(0.35); + const d2 = Math.tanh(d1 * 1.8) * 0.5; + lp += 0.16 * (d2 - lp); + const env = Math.min(1, i / (0.002 * SR)); + emit("music", i0 + i, lp * env * gain * tailFade(i, n), pan, sp, 0.4, 0.20); + } +} + function snare(t, gain = 1) { snares.push(t); // perc-block is a near-pure 2502 Hz sine (tonal prominence x14523). 126 of @@ -771,11 +866,27 @@ function beepSOS(t, { gain = 1, pan = 0.5, dly = 0.4, f = 0 } = {}) { // the note, and `wigIn` how long the depth takes to arrive. wigIn is the // whole trick: with the wiggle ramped in, the CONSONANT lands in tune and // only the held vowel shimmers, so intelligibility survives the effect. +// v10.1: @jeffrey — "the vocal amplitude should slowly rise … it gets a +// bit too loud sometimes … like it needs to stay deep, thick in the +// sounds". A macro arc on the whole vox bus: every sung thing starts at +// ~0.72 of its written gain with a darkness floor, and earns its full +// level and light only as the record approaches the whole message at 76. +// Applied inside shot() so no vocal can escape it; the events receipt +// records the post-arc gain the ear actually gets. +function voxArc(t) { + const u = smooth(clamp((t / BAR - 8) / (S.whole[0] - 8), 0, 1)); + return { g: 0.72 + 0.28 * u, dark: 0.26 * (1 - u) }; +} function shot(name, t, { gain = 1, pan = 0, semis = 0, bus = "drums", side = 0.35, dark = 0, - dur = null, dly = 0, off = 0, evVoice = null, + dur = null, dly = 0, off = 0, evVoice = null, atk = 0.0015, wig = 0, wigHz = 5.0, wigPhase = 0, wigDrift = 0, wigIn = 0.45, } = {}) { + if (bus === "vox") { + const arc = voxArc(t); + gain *= arc.g; + dark = Math.max(dark, arc.dark); + } const s = BANK[name]; if (!s) { missing.add(name); return; } if (!allow(evVoice ?? classify(name).voice)) return; @@ -807,7 +918,7 @@ function shot(name, t, { const f = pos - q; let v = s[q] + (s[q + 1] - s[q]) * f; if (dark > 0) { lp += (1 - dark) * (v - lp); v = lp; } - const env = Math.min(1, i / (0.0015 * SR)); + const env = smooth(Math.min(1, i / (atk * SR))); emit(bus, i0 + i, v * env * gain * tailFade(i, n), pan, sp, side, dly); if (wig || wigDrift) { const u = i / SR; @@ -941,8 +1052,11 @@ const S = { spread: [64, 76], // VI it travels; the unison comes apart whole: [76, 96], // VII the complete four-line chorus, at home recognise: [96, 104], // VIII peel off - humans: [104, 112], // IX drums out, the original recording returns - carrieroff:[112, 120], // X the drone leaves; the beeps outlast it + carrieroff:[104, 112], // IX the drone leaves; the beeps outlast it + // (v10.1: THE HUMANS is gone — @jeffrey: "remove the ending section at + // 3:00 where it's no longer sung … i don't like when vocals skip + // aesthetivox and feel too raw". The reveal act was the one place the + // record played the untreated recording; the record no longer does.) }; const ACTS = { carrier: "I · CARRIER — a channel opens before anything is said", @@ -953,8 +1067,7 @@ const ACTS = { spread: "VI · IT SPREADS — the unison comes apart; the wiggle is at its widest", whole: "VII · THE WHOLE MESSAGE — the complete chorus, and the harmony comes home", recognise: "VIII · RECOGNITION — elements peel off one at a time", - humans: "IX · THE HUMANS — the original recording: this was all made of eight seconds", - carrieroff: "X · CARRIER OFF — the drone leaves, the beeps outlast it, then a hang-up", + carrieroff: "IX · CARRIER OFF — the drone leaves, the beeps outlast it, then a hang-up", }; const inS = (bar, k) => bar >= S[k][0] && bar < S[k][1]; const sectionAt = (bar) => Object.keys(S).find((k) => inS(bar, k)) ?? "carrieroff"; @@ -963,7 +1076,7 @@ const sectionAt = (bar) => Object.keys(S).find((k) => inS(bar, k)) ?? "carrierof const introBar = (b) => b >= 8 && b < 16; const sosBar = (b) => b >= 16 && b < 24; -const kickOn = (b) => !(inS(b, "carrier") || inS(b, "secret") || inS(b, "humans") || inS(b, "carrieroff")); +const kickOn = (b) => !(inS(b, "carrier") || inS(b, "secret") || inS(b, "carrieroff")); const hatOn = (b) => kickOn(b) || inS(b, "secret"); const dense = (b) => inS(b, "reply") || inS(b, "whole"); const hookSection = (b) => inS(b, "message") || inS(b, "reply") || inS(b, "recognise"); @@ -984,7 +1097,7 @@ function degAt(bar) { // narrative rather than just extra material: act VI is called IT SPREADS, // and what spreads is literally this — the same word, said by different // people in different months across 2022, arriving from the edges of the -// stereo field. Act IX then gives the sung one to the humans. +// stereo field. // // Verified present before use; if alt/ has not been harvested the score // simply skips these lines. @@ -993,8 +1106,126 @@ const ALT_CULTS = [ ["alt-71244-cult", 0.41, 229], ["alt-70551-cult", 0.43, 136], ["alt-71441-cult", 0.52, 207], ["alt-71195-cult", 0.54, 121], ]; -const ALT_SUNG = "alt-71448-cult"; // 0.80 s, 298 Hz, actually sung -const ALT_THREE = "alt-71441-threeofus"; // 0.95 s, 221 Hz, sung +// (v10.1: ALT_SUNG / ALT_THREE left with THE HUMANS act — the raw takes +// they pointed at are exactly the "skips aesthetivox, feels too raw" +// vocals the revision removes.) + +// ── THE DOTS, FROM EVERYWHERE ───────────────────────────────────────── +// v10.1: @jeffrey — "in the opening we should have lots of the 'dot dot +// dot' … from all the videos … samples and aesthetivoxed … to blend in +// with the phone tones". Every harvested post that says "dot" is here, +// grouped BY POST so one video can speak a whole morse S in its own voice +// — nine posts, twenty-two takes — and the aesthetivox bank answers with +// the same syllable resynthesized to a pitch. That pairing is how a human +// dot blends into a machine's: the raw take carries the room, the sung +// take carries the chord, and both ride the signal layer's echo and its +// dial-tone pan positions. +const ALT_DOTS = [ + ["alt-70551-dot1", "alt-70551-dot2"], + ["alt-70555-dot1", "alt-70555-dot2", "alt-70555-dot3"], + ["alt-71018-dot1", "alt-71018-dot2", "alt-71018-dot3"], + ["alt-71195-dot1", "alt-71195-dot2", "alt-71195-dot3"], + ["alt-71244-dot1", "alt-71244-dot2", "alt-71244-dot3"], + ["alt-71437-dot1", "alt-71437-dot2"], + ["alt-71441-dot1", "alt-71441-dot2", "alt-71441-dot3"], + ["alt-71448-dot"], + ["alt-71560-dot1", "alt-71560-dot2"], +]; + +// The aesthetivoxed dots — bin/sing.mjs's WORLD renders, one syllable at +// a real pitch per performer — keyed the way choirFor keys the choir: +// keep the bank names whose pitch class sits in the current triad. +const DOT_PITCH = { + "dot-j-b2": 47, "dot-j-d3": 50, "dot-j-e3": 52, "dot-j-fs3": 54, + "dot-j-g3": 55, "dot-j-a3": 57, + "dot-a-fs3": 54, "dot-a-g3": 55, "dot-a-a3": 57, "dot-a-b3": 59, + "dot-a-d4": 62, "dot-a-e4": 64, + "dot-c-b3": 59, "dot-c-cs4": 61, "dot-c-d4": 62, "dot-c-e4": 64, + "dot-c-fs4": 66, "dot-c-g4": 67, "dot-c-a4": 69, +}; +function dotsFor(deg) { + const pcs = new Set(triad(deg).map((m) => ((m % 12) + 12) % 12)); + const ok = Object.keys(DOT_PITCH) + .filter((n) => has(n) && pcs.has(DOT_PITCH[n] % 12)) + .sort((a, b) => DOT_PITCH[a] - DOT_PITCH[b]); + return ok.length ? ok : ["dot-b3"]; +} + +// One video says ONE dot, held long. @jeffrey heard the first pass +// (three-dot morse figures, two or three a bar, DTMF shadows) and called +// it: "too chaotic … wayyyy less of them … all extended a lot … more in +// the background" — then heard the granular stretch and called that too: +// "more smoooooth … lessss choppy in their slowness". So the long dots +// are now REAL held notes: bin/longdots.sh runs each video's take through +// the WORLD chain (sing.py duration control — the vowel sustains, the +// consonant stays a consonant) at its own measured pitch snapped into +// B minor, and this function simply plays that render with a slow wiggle. +// The granular stretch survives only as the fallback for an unbuilt bank. +// Per-video pitch of the altdot-*-long renders (longdots.sh's snap), so +// callers can deepen register-aware: `deep` drops any take above B3 by an +// octave — same pitch class, key-safe, twice the length, half the light. +const ALT_DOT_MIDI = [59, 47, 55, 47, 61, 59, 62, 66, 61]; +function dotDrift(t, vid, { + gain = 0.16, pan = 0, dur = 1.6, stretch = 4.5, dly = 0.5, dark = 0.45, semis = 0, deep = false, +} = {}) { + const takes = ALT_DOTS[((vid % ALT_DOTS.length) + ALT_DOTS.length) % ALT_DOTS.length].filter(has); + if (!takes.length) return; + const long = `altdot-${takes[0].split("-")[1]}-long`; + const vi = ((vid % ALT_DOTS.length) + ALT_DOTS.length) % ALT_DOTS.length; + const dsemis = semis + (deep && ALT_DOT_MIDI[vi] >= 59 ? -12 : 0); + if (has(long)) { + sung(long, t, { + gain: gain * 1.15 * vel(0.12), pan, semis: dsemis, side: 0.85, dark, dly, + wig: 6, wigHz: 0.5 + 0.3 * (vid % 3), wigPhase: vid * 1.9, wigDrift: (vid % 2 ? -4 : 4), wigIn: 0.8, + }); + return; + } + let pick = takes[0]; + for (const nm of takes) if (BANK[nm].length > BANK[pick].length) pick = nm; + stretched(pick, t, { gain: gain * vel(0.12), pan, semis, stretch, dur, side: 0.85, dark, dly }); +} + +// …and the aesthetivoxed kind: one performer's dot held at a chord tone. +// The 0.2 s staccato sung/dot-* bank chops when slowed, so longdots.sh +// renders voxdot-* — the same takes through the WORLD chain, held 1.7 s — +// and this picks whichever sits in the current triad. @jeffrey: "lets +// start with just camille and alex on dots … no jeffrey voice" — so the +// drift is Camille and Alex only, everywhere. Stretch fallback only for +// an unbuilt bank. +const VOXDOT_PITCH = { + "voxdot-j-b2": 47, "voxdot-j-fs3": 54, "voxdot-a-a3": 57, + "voxdot-a-d4": 62, "voxdot-c-b3": 59, "voxdot-c-fs4": 66, +}; +function dotDriftVox(t, bar, { + gain = 0.14, pan = 0, dur = 1.4, stretch = 3.8, dly = 0.45, dark = 0.35, semis = 0, +} = {}) { + const pcs = new Set(triad(degAt(bar)).map((m) => ((m % 12) + 12) % 12)); + const long = Object.keys(VOXDOT_PITCH) + .filter((n) => has(n) && pcs.has(VOXDOT_PITCH[n] % 12)) + .filter((n) => !n.startsWith("voxdot-j")) + .flatMap((n) => (n.startsWith("voxdot-c") ? [n, n] : [n])); // "more camille dots" + if (long.length) { + const nm = long[Math.abs(bar * 7 + 3) % long.length]; + sung(nm, t, { + gain: gain * 1.15 * vel(0.12), pan, semis, side: 0.8, dark, dly, + wig: 5, wigHz: 0.45, wigPhase: bar * 1.3, wigDrift: (bar % 2 ? -3 : 3), wigIn: 0.8, + }); + return; + } + const bank = dotsFor(degAt(bar)); + const nm = bank[Math.min(bank.length - 1, bank.length >> 1)]; + stretched(nm, t, { gain: gain * vel(0.12), pan, stretch, dur, side: 0.8, dark, dly }); +} + +// v10.1's other ask: @jeffrey — "remove 'i wanna hide away' and 'i wanna +// run real fast' until the end". The loop keeps looping, but before act +// VII its two WORDED lines are withheld: every early statement is +// morse-only — the dash lines, the dot lines — with dot figures standing +// exactly where the words will land. Act VII was always titled "the whole +// message the record has been withholding"; now the record actually +// withholds it, and the first time anyone says what they wanna do is the +// moment the harmony comes home at bar 76. +const wordsIn = (bar) => bar >= S.whole[0]; let seed = 20220120; // the cult post date const rnd = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296); @@ -1009,7 +1240,7 @@ const vel = (spread = 0.20) => 1 - rnd() * spread; function wigDepth(bar) { const s = sectionAt(bar); return { three: 4, message: 10, secret: 7, reply: 16, spread: 22, - whole: 14, recognise: 9, humans: 6, carrieroff: 5, carrier: 0 }[s] ?? 10; + whole: 14, recognise: 9, carrieroff: 5, carrier: 0 }[s] ?? 10; } // One syllable, three people, one pitch, 28 ms apart — and now each of them @@ -1052,14 +1283,25 @@ function hook(bar, { full = true } = {}) { const t = at(bar); const G = full ? 1.0 : 0.70; dashStack(t + 0.00, "fs4", G, bar); - sung("iwanna-a-sung", t + 1.50 + jit(4), { gain: 0.82 * G, pan: -0.18, side: 0.50, dly: 0.20 }); + sung(has("iwannaslow-a") ? "iwannaslow-a" : "iwanna-a-sung", t + 1.42 + jit(4), { gain: 0.70 * G, pan: -0.18, side: 0.50, dly: 0.24, atk: 0.05 }); dashStack(t + 2.00, "d4", G * 0.95, bar); - sung("iwanna-b-sung", t + 3.50 + jit(4), { gain: 0.82 * G, pan: 0.18, side: 0.50, dly: 0.20 }); + sung(has("iwannaslow-b") ? "iwannaslow-b" : "iwanna-b-sung", t + 3.42 + jit(4), { gain: 0.70 * G, pan: 0.18, side: 0.50, dly: 0.24, atk: 0.05 }); // The long-"real" take — run · reeeeeaaaal (2.0 s) · fast. At 2.80 s it // rings on under the dot answers at +6.00, which is the point. - sungSub("runrealfast-long-hi", t + 4.00 + jit(4), { gain: 0.96 * G, pan: 0.00, side: 0.45, dly: 0.26 }, { amount: 0.30 }); - sung("dot-b3", t + 6.00 + jit(3), { gain: 0.90 * G, pan: -0.45, side: 0.75, dly: 0.38 }); - sung("dot-fs3", t + 6.50 + jit(3), { gain: 0.90 * G, pan: 0.45, side: 0.75, dly: 0.38 }); + // Withheld before act VII: one aesthetivoxed dot stretched across the + // slot instead — the length of the line with none of its words. + if (wordsIn(bar)) { + // same diction fix as the chorus: syllabic take first, melisma under + sungSub("runrealfast-hi", t + 4.00 + jit(4), { gain: 1.26 * G, pan: 0.00, side: 0.35, dly: 0.10 }, { amount: 0.30 }); + sung("runrealfast-long-hi", t + 4.80 + jit(4), { gain: 0.50 * G, pan: -0.10, side: 0.60, dly: 0.35 }); + } + else { + dotDriftVox(t + 4.00 + jit(20), bar, { gain: 0.30 * G, pan: 0.10, dur: 1.9, stretch: 4.2, dly: 0.35, dark: 0.30 }); + bop(t + 5.50 + jit(4), hz(triad(degAt(bar))[0] + 12), + { gain: 0.20 * G, pan: -0.28, side: 0.7, dly: 0.40 }); + } + sung("dot-b3", t + 6.00 + jit(3), { gain: 0.76 * G, pan: -0.45, side: 0.75, dly: 0.38, atk: 0.025 }); + sung("dot-fs3", t + 6.50 + jit(3), { gain: 0.76 * G, pan: 0.45, side: 0.75, dly: 0.38, atk: 0.025 }); if (full) sung("dot-d4", t + 7.00, { gain: 0.34 * G, pan: 0.0, side: 0.60, dly: 0.55 }); } @@ -1090,29 +1332,55 @@ function chorus(bar, { const both = lead === "both", lo = lead === "lo"; // ── line 1 · "run real fast" ────────────────────────────────────────── - if (!gone.has(1)) { + // (withheld before act VII: one stranger's dot, stretched across the + // slot where the words will land — background, not statement) + if (!gone.has(1) && !wordsIn(bar)) { + dotDrift(t + 0.30 + jit(20), bar >> 3, { + gain: 0.26 * G, pan: -0.30, dur: 1.5, stretch: 4.0, dly: 0.40, dark: 0.35, + }); + } else if (!gone.has(1)) { if (fast) { for (const [k, oct] of [[0.00, "hi"], [1.00, "lo"]]) sung(`runrealfast-fast-${oct}`, t + k + jit(3), - { gain: (oct === "hi" ? 0.92 : 0.70) * G, pan: oct === "hi" ? -0.14 : 0.14, side: 0.5, dly: 0.22 }); + { gain: (oct === "hi" ? 1.25 : 0.95) * G, pan: oct === "hi" ? -0.12 : 0.12, side: 0.42, dly: 0.12 }); } else if (lo) { - sung("runrealfast-long-lo", t + jit(4), { gain: 0.94 * G, pan: 0.00, side: 0.45, dly: 0.22 }); + sung("runrealfast-long-lo", t + jit(4), { gain: 1.30 * G, pan: 0.00, side: 0.40, dly: 0.12 }); } else { - sungSub("runrealfast-long-hi", t + jit(4), { gain: 0.96 * G, pan: both ? -0.10 : 0.00, side: 0.5, dly: 0.24 }, { amount: 0.32 }); - if (both) sung("runrealfast-long-lo", t + 0.022 + jit(4), { gain: 0.58 * G, pan: 0.16, side: 0.45, dly: 0.20 }); + // v10.1 fourth pass. @jeffrey, three listens running: "still can't + // hear the run real fast words". The level was never the problem — + // the LONG take is a melisma ("run reeeeaaal faaast", 2.3 s of held + // vowel, barely a consonant in it), so every boost was boosting a + // drone. The syllabic take SAYS the line now, dry and center, and + // the melisma enters under it a beat later as the vowel it always + // was. + sungSub("runrealfast-hi", t + jit(4), { gain: 1.30 * G, pan: 0.00, side: 0.35, dly: 0.10 }, { amount: 0.30 }); + sung("runrealfast-long-hi", t + 0.80 + jit(4), { gain: 0.55 * G, pan: both ? -0.14 : -0.08, side: 0.60, dly: 0.30 }); + if (both) sung("runrealfast-long-lo", t + 0.84 + jit(4), { gain: 0.44 * G, pan: 0.16, side: 0.55, dly: 0.28 }); } } // ── line 2 · "i wanna hide a — waaaay" ──────────────────────────────── - if (!gone.has(2)) { - sung(lo ? "iwannahide-lo" : "iwannahide-hi", t + 2.00 + jit(4), { gain: 0.86 * G, pan: -0.14, side: 0.5, dly: 0.20 }); - if (both) sung("iwannahide-lo", t + 2.022 + jit(4), { gain: 0.52 * G, pan: 0.18, side: 0.45, dly: 0.20 }); - // the held vowel the line was written for: tube bus, deep pump - held(lo ? "away-lo" : "away-hi", t + 3.00 + jit(3), { - gain: 0.86 * G, pan: -0.06, side: 0.62, dly: 0.26, + // (withheld before act VII: one aesthetivoxed dot, stretched where the + // held vowel would have been, and a bop takes the vowel's peak) + if (!gone.has(2) && !wordsIn(bar)) { + dotDriftVox(t + 2.20 + jit(20), bar, { gain: 0.22 * G, pan: 0.24, dur: 1.4, stretch: 3.8, dly: 0.40 }); + bop(t + 3.00 + jit(4), hz(triad(degAt(bar))[1] + 12), + { gain: 0.22 * G, pan: -0.20, side: 0.7, dly: 0.40 }); + } else if (!gone.has(2)) { + // v10.1 third pass. The granular stretch smeared the words ("feels + // broken … kept the 'i wanna', lost the hide away") — so no stretcher + // anywhere near this line. The hideaway take (no pickup: F#4→G4→A4, + // its own 1.3 s held tail) sings CLEAN with a resampled sub floor, + // and the "much longerrrrr" comes from real vowels relaying under it: + // away-hi picks the vowel up mid-ring, away-lo hands it down the + // octave, so the waaaay carries four-plus seconds into the dash line + // without a single grain. + sungSub("hideaway-hi", t + 2.00 + jit(4), { gain: 0.94 * G, pan: -0.10, side: 0.5, dly: 0.24 }, { amount: 0.30 }); + held("away-hi", t + 3.45 + jit(3), { + gain: 0.80 * G, pan: -0.06, side: 0.62, dly: 0.26, wig: wigDepth(bar) * 0.8, wigHz: 4.9, wigPhase: 1.1, wigDrift: 0.3 * wigDepth(bar), wigIn: 0.55, }); - if (both) held("away-lo", t + 3.026 + jit(3), { + if (both) held("away-lo", t + 4.20 + jit(3), { gain: 0.52 * G, pan: 0.22, side: 0.62, dly: 0.26, wig: wigDepth(bar) * 0.9, wigHz: 6.3, wigPhase: 3.4, wigDrift: -0.35 * wigDepth(bar), wigIn: 0.48, }); @@ -1120,7 +1388,14 @@ function chorus(bar, { // ── line 3 · "i wanna dash" ─────────────────────────────────────────── if (!gone.has(3)) { - sung("iwanna-c-sung", t + 4.00 + jit(4), { gain: 0.84 * G, pan: 0.18, side: 0.5, dly: 0.20 }); + // v10.1: "no 'i wanna' until after 0:40" — the first statement (bar 24 + // lands at 0:32 shipped) keeps only its dash; the pickup starts + // singing from the first hook at 0:41 on. And the pickup itself is + // the slowed take: "could be slowed a bit more, like faded … its + // sorta rushed" (longdots.sh renders iwannaslow-* 1.5x with a soft + // attack; faded = a shade lower, a shade wetter). + if (at(bar) >= 54) + sung(has("iwannaslow-c") ? "iwannaslow-c" : "iwanna-c-sung", t + 3.92 + jit(4), { gain: 0.72 * G, pan: 0.18, side: 0.5, dly: 0.24, atk: 0.05 }); dashStack(t + 4.50, "d4", G * 0.98, bar); } else { // dropped: the bar becomes one long dash — v3's chorus4 move @@ -1129,30 +1404,38 @@ function chorus(bar, { // ── line 4 · "dot dot dash" ─────────────────────────────────────────── if (!gone.has(4)) { - sung("dot-c-b3", t + 6.00 + jit(3), { gain: 0.90 * G, pan: -0.45, side: 0.75, dly: 0.36 }); - sung("dot-j-g3", t + 6.50 + jit(3), { gain: 0.86 * G, pan: 0.45, side: 0.75, dly: 0.36 }); + // "the dot dot at 32 is too abrupt" — the staccato dots enter on a + // 30 ms cosine instead of a cliff, a shade lower + sung("dot-c-b3", t + 6.00 + jit(3), { gain: 0.74 * G, pan: -0.45, side: 0.75, dly: 0.36, atk: 0.03 }); + sung("dot-j-g3", t + 6.50 + jit(3), { gain: 0.70 * G, pan: 0.45, side: 0.75, dly: 0.36, atk: 0.03 }); dashStack(t + 7.00, "b3", G * 0.92, bar); } // ── the answer figure — a different one each statement ──────────────── if (answer === "dots") { - for (const [k, nm, pan] of [[1.30, "dot-c-b3", 0.38], [1.65, "dot-j-g3", -0.38]]) - sung(nm, t + k + jit(4), { gain: 0.30 * G, pan, side: 0.8, dly: 0.5 }); + sung("dot-c-b3", t + 1.30 + jit(4), { gain: 0.30 * G, pan: 0.38, side: 0.8, dly: 0.5, atk: 0.02 }); + sung(has("voxdot-c-b3") ? "voxdot-c-b3" : "dot-c-d4", t + 1.65 + jit(4), + { gain: 0.26 * G, pan: -0.38, side: 0.8, dly: 0.5, atk: 0.03 }); } else if (answer === "sos") { for (const [k, nm] of [[0, "dot-b3"], [1, "dot-fs3"], [2, "dot-d4"]]) sung(nm, t + 1.20 + k * 0.25 + jit(3), { gain: 0.26 * G, pan: k === 1 ? 0 : k ? 0.4 : -0.4, side: 0.85, dly: 0.55 }); } - // a tag in the last bar: the double-time line as punctuation - if (tag === "fast") - for (const [k, oct] of [[0.00, "hi"], [0.50, "lo"]]) - sung(`runrealfast-fast-${oct}`, t + 7.0 + k + jit(3), { - gain: (oct === "hi" ? 0.44 : 0.34) * G, pan: oct === "hi" ? 0.3 : -0.3, - side: 0.7, dly: 0.45, - }); + // a tag in the last bar: the double-time line as punctuation — or, while + // the words are withheld, one quiet drifting dot closing the statement + if (tag === "fast") { + if (wordsIn(bar)) + for (const [k, oct] of [[0.00, "hi"], [0.50, "lo"]]) + sung(`runrealfast-fast-${oct}`, t + 7.0 + k + jit(3), { + gain: (oct === "hi" ? 0.54 : 0.42) * G, pan: oct === "hi" ? 0.3 : -0.3, + side: 0.7, dly: 0.45, + }); + else + dotDrift(t + 7.0 + jit(20), bar >> 2, { gain: 0.18 * G, pan: 0.30, dur: 0.95, stretch: 3.0, dly: 0.45, dark: 0.40 }); + } - if (choirUnder) choir(bar, 0.30 * G); + if (choirUnder) choir(bar, 0.18 * G); } // ── THE SOS FIGURE ──────────────────────────────────────────────────── @@ -1168,21 +1451,151 @@ function sos(bar, g = 1) { sung(n, t + 6.5 + k * BEAT + jit(3), { gain: 0.64 * g, pan: p, side: 0.78, dly: 0.42 }); } -// The choir: sung "cult" held four seconds at three chord tones, 45 ms -// apart — which is itself a crossover, so it gets a small slow drift. +// The choir: sung "cult" held at three chord tones, 45 ms apart — which +// is itself a crossover, so it gets a small slow drift. v10.1: prefers +// the cultlong-* renders (5 s + 900 ms release, longdots.sh) so the chord +// decays under whatever comes next instead of stopping at four seconds — +// @jeffrey: "much longer decay … so they don't cut off when the perc and +// kicks come back". +const cultTake = (nm) => (has(`cultlong-${nm}`) ? `cultlong-${nm}` : `cult-${nm}`); function choir(bar, g) { const t = at(bar); const picks = choirFor(degAt(bar)); const gains = [0.46, 0.38, 0.30], pans = [0.0, -0.50, 0.50], sides = [0.40, 0.85, 0.85]; const w = Math.min(7, wigDepth(bar) * 0.4); - picks.forEach((nm, i) => sung(`cult-${nm}`, t + i * 0.045, { + picks.forEach((nm, i) => sung(cultTake(nm), t + i * 0.045, { gain: g * (gains[i] ?? 0.28), pan: pans[i] ?? 0, side: sides[i] ?? 0.7, dark: 0.32, wig: w, wigHz: 0.7 + 0.25 * i, wigPhase: i * 2.3, wigDrift: (i % 2 ? -1 : 1) * w * 0.6, wigIn: 1.2, })); + // v10.1: "downpitched / lower octaves … maybe get chordal" — the slow + // harmonies grow a floor: the root take an octave below itself, dark + // and centered, so the chord stands on something. + sung(cultTake(picks[0]), t + 0.09, { + gain: g * 0.55, semis: -12, dark: 0.52, side: 0.28, pan: 0, + wig: w * 0.5, wigHz: 0.5, wigIn: 1.6, evVoice: "sub", + }); +} + +// v10.1: @jeffrey — "flange the harmonies more around 1:15 … have them +// sit much deeper in the mix". The act-IV cult stacks: each voice gets a +// twin seven milliseconds behind itself carrying a slow deep wiggle the +// original doesn't share — two copies of one take drifting through each +// other IS a tape flange — and the whole stack drops ~4 dB, darker and +// wetter, so the sweep happens BEHIND the record instead of in front. +function secretChoir(bar, beat, gains, phase = 0) { + const picks = choirFor(degAt(bar)); + const pans = [0, -0.46, 0.46], sides = [0.40, 0.85, 0.85]; + picks.forEach((nm, i) => { + const o = { + gain: (gains[i] ?? 0.30) * 0.62, pan: pans[i] ?? 0, + side: sides[i] ?? 0.7, dark: 0.45, dly: 0.42, + }; + sung(cultTake(nm), at(bar, beat) + i * 0.045, { + ...o, wig: 5, wigHz: 0.6 + 0.2 * i, wigPhase: phase + i * 2.3, wigDrift: (i % 2 ? -3 : 3), wigIn: 1.4, + }); + sung(cultTake(nm), at(bar, beat) + i * 0.045 + 0.007, { + ...o, gain: o.gain * 0.85, + wig: 11, wigHz: 0.33, wigPhase: phase + 2.4 + i * 1.7, wigDrift: (i % 2 ? 9 : -9), wigIn: 0.8, + }); + }); + sung(cultTake(picks[0]), at(bar, beat) + 0.09, { + gain: (gains[0] ?? 0.5) * 0.38, semis: -12, dark: 0.55, side: 0.28, pan: 0, dly: 0.42, + evVoice: "sub", + }); +} + +// v10.1: @jeffrey — "can the distant blips and such change in their +// melody and counterpoint". The bops stop repeating one chord tone and +// walk a counterline: a per-bar degree chosen against the bass root in +// loose contrary motion, rotated a step every eight bars, so the phone's +// little answers form a tune of their own across the record. +const BLIP_LINE = [2, 4, 6, 4, 2, 0, 5, 3]; +const blipMidi = (bar, k = 0) => + 71 + sd(degAt(bar) + ((BLIP_LINE[(bar + k * 3) % 8] + (bar >> 3)) % 7)); + +// …and the phone knows the song: the four-line chorus melody as single +// sine beeps on the signal bus — "dance the 'original cult melody' with +// the phone, as a decoration. thatd be cute". Two bars, up an octave, +// swung wide across the field. +const PHONE_TUNE = [ + [0.00, 67], [0.50, 66], [1.00, 62], // run real fast + [2.00, 62], [2.50, 64], [3.00, 66], // i wanna hide + [4.00, 67], [4.75, 69], // a — waaay + [6.00, 59], [6.50, 55], [7.00, 59], // dot · dot · dash +]; +function phoneTune(bar, g = 0.26) { + PHONE_TUNE.forEach(([k, m], i) => + beep(at(bar) + k * BEAT + jit(6), hz(m + 12), 0, 0.11, + { gain: g * vel(0.2), pan: i % 2 ? 0.4 : -0.4, side: 0.85, dly: 0.45 })); +} + +// v10.1: @jeffrey heard the reply's deep-wiggled dash line at 1:42 and +// called it "an awesome almost 'indian' sounding vocal — any way we can +// examine that technique / stretch it". What he heard was the crossover +// wiggle at depth 16 bending a held dash: vibrato wide enough to read as +// ornament — a gamaka, near enough. raga() leans in: the LONG dash take +// at a chord tone, wiggle twice as deep and slower, with a drift that +// slides it toward the next scale tone and back — the bend made melody. +const DASHLONG_PITCH = { b3: 59, d4: 62, e4: 64, fs4: 66 }; +function raga(bar, beat, g = 0.42) { + const pcs = new Set(triad(degAt(bar)).map((m) => ((m % 12) + 12) % 12)); + const ok = Object.keys(DASHLONG_PITCH) + .filter((n) => has(`dashlong-camille-${n}`) && pcs.has(DASHLONG_PITCH[n] % 12)); + const nm = ok.length ? ok[bar % ok.length] : "b3"; + const w = Math.max(24, wigDepth(bar) * 1.8); + held(`dashlong-camille-${nm}`, at(bar, beat) + jit(8), { + gain: g, pan: bar % 2 ? 0.24 : -0.24, side: 0.7, dly: 0.30, + wig: w, wigHz: 3.1, wigPhase: bar * 1.7, wigDrift: (bar % 2 ? -1 : 1) * w * 1.4, wigIn: 0.35, + }); +} + +// v10.1: @jeffrey stopped at 1:37 — the reply statement's line-4 dots +// colliding with the stranger's sung F#4, a voxdot, and jeffrey's low +// dot-j-g3 — "whats the jeffrey vocal? can we try extending that +// crazyness for like 10 seconds or so?" So the collision becomes a +// passage: dotCloud() keeps the cluster alive — held stranger dots, +// camille/alex voxdots, and jeffrey's g3 answering every ~0.7–0.9 s, +// each wiggling at its own rate. Ten seconds of the chord being said +// as people. +function dotCloud(t0, dur = 10) { + const picks = [ + ["altdot-71448-long", 0.26], ["voxdot-c-fs4", 0.22], ["dot-j-g3", 0.30], + ["altdot-71437-long", 0.24], ["voxdot-a-d4", 0.22], ["dot-c-b3", 0.26], + ["altdot-70551-long", 0.22], ["voxdot-c-b3", 0.22], ["dot-j-g3", 0.26], + ["altdot-71441-long", 0.22], ["voxdot-a-a3", 0.20], ["dot-c-b3", 0.24], + ]; + let u = 0; + for (let k = 0; u < dur; k++) { + const [nm, g] = picks[k % picks.length]; + if (has(nm)) sung(nm, t0 + u + jit(30), { + gain: g * vel(0.2), pan: ((k * 5) % 7 - 3) / 4, side: 0.8, dark: 0.25, dly: 0.38, + wig: 8 + (k % 3) * 4, wigHz: 0.4 + (k % 4) * 0.9, wigPhase: k * 1.7, + wigDrift: (k % 2 ? -6 : 6), wigIn: 0.5, + }); + u += 0.7 + ((k * 37) % 5) * 0.05; + } +} + +// v10.1: @jeffrey — "i want more camille dots … dots should be more +// extended usually … and i think some arpeggiated dot dot dot dot dot +// dot would be fun, like full scaled at times". Camille's sung dot bank +// happens to hold the entire B natural minor scale from B3 to A4 — +// seven takes, one per degree — so dotArp() literally runs the scale as +// syllables, up or down, lightly swung, softened attacks. +const CAM_SCALE = ["b3", "cs4", "d4", "e4", "fs4", "g4", "a4"]; +function dotArp(t, { count = 5, up = true, gap = 0.16, gain = 0.34, pan = 0.2, dly = 0.34 } = {}) { + for (let k = 0; k < count; k++) { + const nm = `dot-c-${CAM_SCALE[up ? k % 7 : 6 - (k % 7)]}`; + if (!has(nm)) continue; + sung(nm, t + k * gap + jit(4) + (k % 2 ? 0.012 : 0), { + gain: gain * (1 - 0.05 * k) * vel(0.15), pan: pan * (k % 2 ? -1 : 1), + side: 0.7, dly, atk: 0.012, + }); + } } // ── score ───────────────────────────────────────────────────────────── -console.log(`→ scoring ${BARS} bars @ ${BPM} BPM · B minor · ten acts · ${(BARS * BAR).toFixed(1)}s`); +console.log(`→ scoring ${BARS} bars @ ${BPM} BPM · B minor · ${Object.keys(S).length} acts · ${(BARS * BAR).toFixed(1)}s`); for (let bar = 0; bar < BARS; bar++) { const t = at(bar); @@ -1231,9 +1644,36 @@ for (let bar = 0; bar < BARS; bar++) { for (let s = 0; s < 4; s++) shot("tambo", t + (s + 0.5) * BEAT + jit(9), { gain: 0.055 * vel(0.4), pan: s & 1 ? 0.42 : -0.42, side: 0.7, dur: 0.09 }); + // ---- v10.1: "i want more perc play" — ghosts, blocks, a turnaround --- + // The kit stops just keeping time and starts commenting on it: ghost + // snares off the beat, a woodblock conversation through the dense acts, + // and a climbing three-hit fill into every eighth bar. + if (kickOn(bar)) { + if (D || eight >= 4) { + snare(t + 1.25 * BEAT + jit(6), 0.22 * vel(0.3)); + if (bar % 2) snare(t + 2.75 * BEAT + jit(6), 0.16 * vel(0.3)); + } + if (D) + for (const s of [0.75, 1.5, 3.25]) + if ((bar + s * 4) % 3 < 2) + shot("block", t + s * BEAT + jit(8), { gain: 0.10 * vel(0.4), pan: s > 2 ? 0.5 : -0.34, side: 0.7, dur: 0.09 }); + if (eight === 7) { + for (let k = 0; k < 3; k++) + snare(t + (3 + k / 3) * BEAT + jit(4), (0.30 + 0.14 * k) * vel(0.2)); + shot("hatO", t + 3.9 * BEAT, { gain: 0.16, pan: 0.2, side: 0.6, dur: 0.3 }); + } + } + + // ---- v10.1: the re-entries announce themselves — a reversed kick + // rising into the downbeat, then two bars of wub under the groove. + if (bar === S.reply[0] || bar === S.whole[0]) + revKick(t, 0.9, bar === S.whole[0] ? 0.55 : 0.48); + if ((inS(bar, "reply") && bar - S.reply[0] < 2) || bar === S.whole[0] - 2 || bar === S.whole[0] - 1) + wub(t, bassRoot(deg), 1, 0.26, 4); // whole: the wub BUILDS INTO the words, then clears out for them + // ---- bass ------------------------------------------------------------- if (kickOn(bar) || inS(bar, "secret")) { - const g = introBar(bar) ? 0.52 : inS(bar, "secret") ? 0.44 : inS(bar, "recognise") ? 0.72 : 0.86; + const g = introBar(bar) ? 0.52 : inS(bar, "secret") ? 0.62 : inS(bar, "recognise") ? 0.72 : 0.86; for (let b = 0; b < 4; b++) { const fifth = four === 3 && b === 3; bass(t + (b + 0.5) * BEAT + jit(3), root + (fifth ? 7 : 0), 0.26, g, fifth ? root : null); @@ -1265,10 +1705,32 @@ for (let bar = 0; bar < BARS; bar++) { if (bar % 4 === 3) tap(at(bar, 3.5), { gain: 0.30, pan: 0.3, side: 0.7, dly: 0.3 }); } - // ══ ACT II · THE MACHINE ALONE ═══════════════════════════════════════ - // v10: no words, no voices — sixteen bars of kick, ticking and dial. - // The channel dials C-U-L-T over the groove (act I's idea, relocated), - // and the ear gets nothing human to hold until "run real fast" at 24. + // ══ ACT II · THE MACHINE ALONE — AND A DOT DRIFTING THROUGH ══════════ + // v10.1 second pass. The first cut of this opening was a morse crowd — + // two or three three-dot figures a bar, from every video at once — and + // @jeffrey called it: "too chaotic … wayyyy less of them … all extended + // a lot … more in the background". So now: sixteen bars of kick, ticking + // and dial, with ONE dot at a time drifting behind them — each of the + // nine harvested posts gets exactly one appearance across the opening, + // its longest take stretched to a two-second sigh, dark, wide, deep in + // the dial's echo. You still hear everyone before you hear anyone; you + // just hear them the way you hear weather. The words stay withheld + // until act VII. + if (bar === 8 && has("phone-pickup-a")) + // the first gesture on the shipped record: a hand picking up a real + // receiver, right under the first kick + shot("phone-pickup-a", at(8) + 0.02, { bus: "vox", gain: 0.20, pan: -0.12, side: 0.5, dark: 0.35, dur: 1.4, atk: 0.05, wig: 6, wigHz: 0.8, wigIn: 0.4 }); + if (bar === 12) phoneTune(bar, 0.20); // the phone hums the song before anyone sings it + if (introBar(bar) && bar % 2 === 0) { + const k = (bar - 8) / 2; // videos 0–3, one per two bars + dotDrift(at(bar, 2.0) + jit(30), k, { + gain: 0.09 + 0.007 * k, pan: k % 2 ? 0.40 : -0.40, + dur: BAR * 0.85, stretch: 4.5, dly: 0.52, dark: 0.62, deep: true, + }); + } + // one aesthetivoxed answer in the whole intro, under the bar-14 unison + if (bar === 13) + dotDriftVox(at(bar, 2.5) + jit(30), bar, { gain: 0.085, pan: 0.28, dur: BAR * 0.8, stretch: 4.0, dly: 0.5, dark: 0.55, semis: -12 }); if (introBar(bar)) { if (bar % 2 === 0) { const digit = CULT_DIAL[(bar / 2) % 4]; @@ -1278,7 +1740,7 @@ for (let bar = 0; bar < BARS; bar++) { // ticking: the machine is still on. Clicks on the "e" of every beat. for (let s = 0; s < 4; s++) if ((s + bar) % 2 === 0) click(t + (s + 0.25) * BEAT + jit(6), { gain: 0.70 * vel(0.4), pan: s & 1 ? 0.4 : -0.4, side: 0.7 }); - if (bar % 4 === 2) bop(at(bar, 3.5), hz(chord[0] + 12), { gain: 0.30, pan: 0.35, side: 0.7, dly: 0.35 }); + if (bar % 4 === 2) bop(at(bar, 3.5), hz(blipMidi(bar)), { gain: 0.30, pan: 0.35, side: 0.7, dly: 0.35 }); // A hand on a surface while the voices arrive: one slow slide per // introduction, panned to the performer who is about to speak. if (bar % 2 === 0) @@ -1292,9 +1754,22 @@ for (let bar = 0; bar < BARS; bar++) { cut0: 759, cut1: 2300, res0: 103, res1: 214, rough: 0.62 }); } if (sosBar(bar)) { - // v10: still nobody speaks. The machine beeps SOS into its own groove - // — the question the hook at bar 24 walks in and answers. The sung - // morse waits for the voices; the dots exist only as taps and beeps. + // v10.1 second pass: the SOS bars keep the drift going — videos 4–7, + // still one at a time, one per two bars, a shade closer than the intro + // ones so the opening leans forward without ever crowding. The ninth + // video speaks last, at bar 23, right before the first chorus: the + // final stranger arriving as the loop begins. The machine's beeped SOS + // at 20/22 stays the loudest signal in the act. + const k = bar - 16; + if (bar % 2 === 0) + dotDrift(at(bar, 1.5) + jit(30), 4 + k / 2, { + gain: 0.12, pan: (k / 2) % 2 ? -0.42 : 0.42, + dur: BAR * 0.9, stretch: 4.2, dly: 0.48, dark: 0.58, deep: true, + }); + if (bar === 19) + dotDriftVox(at(bar, 2.5) + jit(30), bar, { gain: 0.09, pan: -0.26, dur: BAR * 0.8, stretch: 4.0, dly: 0.5, dark: 0.55, semis: -12 }); + if (bar === 23) + dotDrift(at(bar, 2.0) + jit(30), 8, { gain: 0.13, pan: 0.0, dur: BAR * 0.9, stretch: 4.2, dly: 0.5, dark: 0.55, deep: true }); if (bar === 20 || bar === 22) beepSOS(at(bar, 1), { gain: 0.50, pan: bar === 20 ? 0.55 : -0.55, dly: 0.40 }); // taps mark where the dots will be, one bar ahead of the voices for (const s of [1.5, 3.5]) @@ -1318,9 +1793,10 @@ for (let bar = 0; bar < BARS; bar++) { // the phone keeps time under the hook: a tap on the "and" of 4 and a // click pair in the hole the hook leaves at bar 3. tap(at(bar, 3.5) + jit(6), { gain: 0.62 * vel(0.3), pan: 0.40, side: 0.7, dly: 0.26 }); + if (four === 3) dotArp(at(bar, 2.0) + jit(8), { count: 5, up: (bar >> 2) % 2 === 0, gain: 0.30, pan: 0.30 }); if (four === 3) { click(at(bar, 1.0), { gain: 0.50, pan: -0.42, side: 0.7, dly: 0.3 }); - bop(at(bar, 1.5), hz(chord[1] + 12), { gain: 0.26, pan: 0.42, side: 0.75, dly: 0.4 }); + bop(at(bar, 1.5), hz(blipMidi(bar, 1)), { gain: 0.26, pan: 0.42, side: 0.75, dly: 0.4 }); // the drag that hands the next hook its downbeat friction(at(bar, 2.4), 1.6, { shape: "drag", gain: 0.46, pan: 0.12, side: 0.22, cut0: 819, cut1: 2200, res0: 109, res1: 203, rough: 0.58 }); @@ -1333,24 +1809,17 @@ for (let bar = 0; bar < BARS; bar++) { // ══ ACT IV · THE SECRET ══════════════════════════════════════════════ // The turn. No kick. No beeps at all — the transmission stops, which is // the loudest thing in the record — except ONE click at the very top, - // which is a receiver being picked up. - if (bar === S.secret[0]) click(at(bar, 0) - 0.06, { gain: 0.40, pan: 0, side: 0.35 }); - if (bar === S.secret[0] || bar === S.secret[0] + 4) { - const picks = choirFor(degAt(bar)); - picks.forEach((nm, i) => sung(`cult-${nm}`, at(bar) + i * 0.045, { - gain: [0.62, 0.50, 0.40][i] ?? 0.32, pan: [0, -0.46, 0.46][i] ?? 0, - side: [0.40, 0.85, 0.85][i] ?? 0.7, dark: 0.30, dly: 0.26, - wig: 5, wigHz: 0.6 + 0.2 * i, wigPhase: i * 2.3, wigDrift: (i % 2 ? -3 : 3), wigIn: 1.4, - })); - } - if (bar === S.secret[0] + 2 || bar === S.secret[0] + 6) { - const picks = choirFor(degAt(bar)); - const gains = [0.52, 0.44, 0.36], pans = [0.0, -0.46, 0.46], sides = [0.40, 0.85, 0.85]; - picks.forEach((nm, i) => sung(`cult-${nm}`, at(bar, 2) + i * 0.045, { - gain: gains[i] ?? 0.30, pan: pans[i] ?? 0, side: sides[i] ?? 0.7, dark: 0.30, dly: 0.26, - wig: 5, wigHz: 0.6 + 0.2 * i, wigPhase: 1 + i * 2.3, wigDrift: (i % 2 ? 3 : -3), wigIn: 1.4, - })); + // which is a receiver being picked up. v10.1: it IS one now — a real + // handset lifted off a real cradle, with the synth click as its edge. + if (bar === S.secret[0]) { + click(at(bar, 0) - 0.06, { gain: 0.40, pan: 0, side: 0.35 }); + if (has("phone-pickup-b")) + shot("phone-pickup-b", at(bar, 0) - 0.10, { bus: "vox", gain: 0.17, pan: 0.10, side: 0.45, dark: 0.40, dur: 1.2, atk: 0.04, wig: 6, wigHz: 0.7, wigIn: 0.4 }); } + if (bar === S.secret[0] || bar === S.secret[0] + 4) + secretChoir(bar, 0, [0.62, 0.50, 0.40]); + if (bar === S.secret[0] + 2 || bar === S.secret[0] + 6) + secretChoir(bar, 2, [0.52, 0.44, 0.36], 1); // The beeps have stopped, so the only thing still moving in act IV is a // hand on a drumhead — the analog counterweight to a silent signal layer, // and the sound of three people still in the room. @@ -1372,8 +1841,20 @@ for (let bar = 0; bar < BARS; bar++) { if (inS(bar, "secret") && bar % 2 === 1) frictionPath(at(bar, 2.6), BAR * 0.34, { path: "scrub", shape: "skid", gain: 0.40, rough: 0.60, speed: 1.6, side: 0.30, dly: 0.10 }); - if (inS(bar, "secret") && bar % 2 === 1) material(bar, "cult-b3", 0.80); - if (inS(bar, "secret") && bar % 2 === 0) choir(bar, 0.34); + // v10.1: heavier low end under the chant — the bass keeps walking at a + // fuller gain (0.44 → 0.62 in the bass block's law) and each chord gets + // a deep guitar pluck: root two octaves down on the even bars, a chord + // tone answering on the odd, both ringing across the bar line. + if (inS(bar, "secret")) { + const gch = triad(degAt(bar)); + // "much much deeper and quieter": root three octaves down (~31 Hz + // fundamental, the fuzz carries it), answers one octave lower too + if (bar % 2 === 0) guitar(at(bar, 0.5) + jit(6), gch[0] - 36, BAR * 2.3, 0.34, -0.14); + else guitar(at(bar, 1.5) + jit(6), gch[(bar >> 1) % 3] - 24, BAR * 1.8, 0.24, 0.20); + bass(at(bar, 0) + jit(3), bassRoot(degAt(bar)) - 12, BAR * 0.92, 0.50); + } + if (inS(bar, "secret") && bar % 2 === 1) material(bar, "cult-b3", 0.62); + if (inS(bar, "secret") && bar % 2 === 0) choir(bar, 0.22); // ══ ACT V · THE REPLY ════════════════════════════════════════════════ // v9: the loop comes back from the turn an octave down — the same words, @@ -1390,8 +1871,11 @@ for (let bar = 0; bar < BARS; bar++) { sung(n, t + (2 + k) * BEAT + jit(4), { gain: 0.34, pan: p, side: 0.8, dly: 0.5 }); // …and the answer itself: SOS, beeped, in the hole at the end of every // eight bars. The machine has started talking back. - if (inS(bar, "reply") && eight === 7) + if (bar === S.reply[0] + 12) phoneTune(bar, 0.24); + if (inS(bar, "reply") && eight === 7) { beepSOS(at(bar, 1), { gain: 0.52, pan: 0.55, dly: 0.45 }); + dotArp(at(bar, 2.5) + jit(8), { count: 7, up: false, gap: 0.18, gain: 0.26, pan: -0.25 }); + } if (inS(bar, "reply")) { for (const s of [0.75, 2.75]) tap(t + s * BEAT + jit(6), { gain: 0.38 * vel(0.3), pan: s > 2 ? -0.4 : 0.4, side: 0.7, dly: 0.24 }); if (four === 1) click(at(bar, 3.75), { gain: 0.44, pan: 0.35, side: 0.7, dly: 0.35 }); @@ -1404,6 +1888,12 @@ for (let bar = 0; bar < BARS; bar++) { path: "spiralIn", rough: 0.66 }); } if (inS(bar, "reply") && bar % 4 === 2) material(bar, "dash-camille-fs4-hold", 0.62, { grain: 0.055 }); + // the 1:37 collision, extended: ten seconds of dot cloud off the back + // of the second reply statement + if (bar === S.reply[0] + 8) dotCloud(at(bar) + 5.5, 10); + // the 1:42 ornament, featured: twice in the reply, twice where it spreads + if (bar === S.reply[0] + 10 || bar === S.reply[0] + 14) raga(bar, 1.5, 0.44); + if (bar === S.spread[0] + 3 || bar === S.spread[0] + 7) raga(bar, 2.0, 0.38); // ══ ACT VI · IT SPREADS ══════════════════════════════════════════════ // The held dashes become the whole lead, at the widest wiggle in the @@ -1439,7 +1929,14 @@ for (let bar = 0; bar < BARS; bar++) { dtmf(u, digits[(bar * 3 + k) % digits.length], 0.055, { gain: 0.46 * vel(0.3), pan: k === 1 ? 0 : k ? 0.5 : -0.5, side: 0.85, dly: 0.40 }); } - if (four === 2) bop(at(bar, 3.25), hz(chord[2] + 12), { gain: 0.26, pan: -0.4, side: 0.8, dly: 0.45 }); + if (four === 2) bop(at(bar, 3.25), hz(blipMidi(bar, 2)), { gain: 0.26, pan: -0.4, side: 0.8, dly: 0.45 }); + if (four === 1) dotArp(at(bar, 1.5) + jit(8), { count: 6, up: bar % 8 < 4, gap: 0.15, gain: 0.24, pan: 0.35, dly: 0.42 }); + // v10.1: somebody in the switchboard is on a ROTARY phone — one real + // dial pass per half of the act, thrown to the edges like the cults + if (bar === S.spread[0] + 4 && has("phone-rotary-a")) + shot("phone-rotary-a", at(bar, 0.5), { bus: "vox", gain: 0.14, pan: -0.55, side: 0.85, dark: 0.45, dly: 0.45, atk: 0.08, wig: 9, wigHz: 0.35, wigIn: 0.6 }); + if (bar === S.spread[0] + 9 && has("phone-rotary-b")) + shot("phone-rotary-b", at(bar, 1.0), { bus: "vox", gain: 0.13, pan: 0.55, side: 0.85, dark: 0.45, dly: 0.45, atk: 0.08, wig: 9, wigHz: 0.30, wigPhase: 2.1, wigIn: 0.6 }); // it spreads: a skid every bar, alternating sides, never in the same // place twice — the friction equivalent of the wiggle coming apart. friction(at(bar, 2.5 + (bar % 3) * 0.25), 0.58, { shape: "skid", @@ -1449,6 +1946,34 @@ for (let bar = 0; bar < BARS; bar++) { friction(at(bar, 3.1), 0.95, { shape: "drag", gain: 0.30, pan: bar % 4 ? -0.2 : 0.2, side: 0.24, cut0: 900, cut1: 1900, res0: 119, res1: 189, rough: 0.55, synthetic: true }); } + // v10.1: @jeffrey — "around 1:57 the 'cult' should be extended alot, so + // its more angelic from alex". Twice in the act (bars 66 and 70, 1:56 + // and 2:04 shipped) the top chord-tone take stretches to eight airy + // seconds — bright, wide, mostly delay — with the take a third under it + // drifting behind: an angel hanging over the switchboard while the raw + // cults keep arriving beneath her. + if (bar === S.spread[0] + 2 || bar === S.spread[0] + 6) { + const picks = choirFor(degAt(bar)); + const hi = picks[picks.length - 1], mid = picks[Math.max(0, picks.length - 2)]; + if (has(`cultlong-${hi}`)) { + sung(`cultlong-${hi}`, at(bar, 1) + jit(10), { + gain: 0.36, pan: 0.30, side: 0.92, dark: 0.05, dly: 0.55, atk: 0.35, + wig: 7, wigHz: 0.4, wigPhase: 0.6, wigDrift: 4, wigIn: 1.2, + }); + sung(`cultlong-${mid}`, at(bar, 1.5) + jit(10), { + gain: 0.23, pan: -0.34, side: 0.92, dark: 0.12, dly: 0.55, atk: 0.45, + wig: 8, wigHz: 0.31, wigPhase: 2.8, wigDrift: -5, wigIn: 1.0, + }); + } else { + stretched(`cult-${hi}`, at(bar, 1) + jit(10), { + gain: 0.34, pan: 0.30, stretch: 2.3, dur: 8.0, side: 0.92, dark: 0.05, dly: 0.55, + }); + stretched(`cult-${mid}`, at(bar, 1.5) + jit(10), { + gain: 0.22, pan: -0.34, stretch: 2.6, dur: 8.0, side: 0.92, dark: 0.12, dly: 0.55, + }); + } + } + // …and the word itself spreading: one other person's "cult" per bar, // from a different post each time, thrown to the edges. Six voices from // six videos across 2022, none of them the take the record is built from. @@ -1468,6 +1993,9 @@ for (let bar = 0; bar < BARS; bar++) { // hook once more at 92 as the come-down. Statement two doubles line 1 at // half length — the lyric performing itself — which is the only place in // the record anything is said twice as fast. + // the full scale runs straight into the arrival: seven camille dots + // climbing B3→A4 across the last bar before the words land + if (bar === S.whole[0] - 1) dotArp(at(bar, 2.0), { count: 7, up: true, gap: 0.20, gain: 0.40, pan: 0.15, dly: 0.28 }); if (bar === S.whole[0]) chorus(bar, { lead: "both", g: 1.0, answer: "dots", choirUnder: true }); if (bar === S.whole[0] + 8) chorus(bar, { lead: "both", fast: true, g: 1.0, tag: "fast", choirUnder: true }); if (bar === S.whole[0] + 16) hook(bar, { full: true }); @@ -1489,6 +2017,7 @@ for (let bar = 0; bar < BARS; bar++) { // already gone — then the hook fragment, then people. if (bar === S.recognise[0]) chorus(bar, { lead: "hi", g: 0.62, drop: [2] }); if (bar === S.recognise[0] + 4) hook(bar, { full: false }); + if (bar === S.recognise[0] + 6) phoneTune(bar, 0.22); if (inS(bar, "recognise") && bar % 2 === 0) material(bar, "cult-b3", 0.50, { grain: 0.10, dly: 0.34 }); if (inS(bar, "recognise") && four === 3) friction(at(bar, 2.6), 1.4, { shape: "drag", side: 0.22, @@ -1497,52 +2026,48 @@ for (let bar = 0; bar < BARS; bar++) { if (inS(bar, "recognise") && four === 1) click(at(bar, 2.5), { gain: 0.44 * (1 - (bar - S.recognise[0]) / 9), pan: -0.4, side: 0.7, dly: 0.35 }); - // ══ ACT IX · THE HUMANS ══════════════════════════════════════════════ - // Drums out. The original recording, unprocessed — the reveal that all of - // this was made from eight seconds of three friends. No beeps: the signal - // layer has nothing to add to people. - if (bar === S.humans[0]) shot("hook-spoken", at(bar, 1), { gain: 0.56, pan: -0.10, bus: "vox", side: 0.6, dly: 0.22 }); - if (bar === S.humans[0] + 2) - stretched("three-of-us", at(bar, 1), { gain: 0.22, pan: 0.35, stretch: 1.8, dur: BAR * 1.6, side: 0.9, dly: 0.3 }); - if (bar === S.humans[0] + 4) shot("chant-full", at(bar, 1), { gain: 0.48, pan: 0, bus: "vox", side: 0.9, dark: 0.3, dly: 0.34 }); - // the reveal has a second half: somebody else, months later, SINGING it - if (bar === S.humans[0] + 5 && has(ALT_THREE)) - shot(ALT_THREE, at(bar, 2), { gain: 0.44, pan: 0.28, bus: "vox", side: 0.8, dark: 0.2, dly: 0.36 }); - if (bar === S.humans[0] + 6 && has(ALT_SUNG)) - shot(ALT_SUNG, at(bar, 1), { gain: 0.50, pan: -0.22, bus: "vox", side: 0.8, dark: 0.18, dly: 0.42 }); - - // ══ ACT X · CARRIER OFF ══════════════════════════════════════════════ - // the last gesture on the record: a hand leaving the surface, four bars - // long, under the drone that is walking out - if (bar === S.humans[0] + 6) + // ══ ACT IX · CARRIER OFF ═════════════════════════════════════════════ + // v10.1: THE HUMANS act is gone — the untreated recording with it — so + // the record steps straight from the peel-off into the walk-out. Every + // voice left on the record has been through the aesthetivox. + // The last gesture: a hand leaving the surface, four bars long, starting + // under the final drums and ending under the drone that is walking out. + if (bar === S.carrieroff[0] - 2) frictionPath(at(bar, 1.0), BAR * 3.2, { shape: "slide", gain: 0.34, pan: -0.16, side: 0.22, path: "edgeBack", rough: 0.42, rel: 0.30 }); - if (bar === S.carrieroff[0] + 2) shot("tagline", at(bar, 1), { gain: 0.50, pan: 0, bus: "vox", side: 0.8, dark: 0.35, dly: 0.40 }); if (inS(bar, "carrieroff")) { const k = bar - S.carrieroff[0]; if (k % 2 === 0 && k <= 6) dtmf(at(bar, 1), CULT_DIAL[(k / 2) % 4], 0.14, { gain: 0.44 * (1 - k / 10), pan: k % 4 ? 0.5 : -0.5, side: 0.85, dly: 0.42 }); + if (k === 2) phoneTune(bar, 0.16); // hums it once more on the way out if (k === 6) bop(at(bar, 3), hz(59), { gain: 0.24, pan: 0, side: 0.6, dly: 0.5 }); - // the last word on the record is somebody else's - if (k === 4 && has(ALT_CULTS[3][0])) - shot(ALT_CULTS[3][0], at(bar, 2), { gain: 0.34, pan: 0, bus: "vox", side: 0.7, dark: 0.22, dly: 0.55 }); + // the last voice on the record is a stranger's dot, stretched to vapor + if (k === 4) + dotDrift(at(bar, 2) + jit(30), 8, { gain: 0.16, pan: 0, dur: BAR * 0.9, stretch: 4.5, dly: 0.55, dark: 0.45 }); } - // ---- the drone, either side of the record --------------------------- - if ((inS(bar, "humans") || inS(bar, "carrieroff")) && bar % 2 === 0) - choir(bar, inS(bar, "carrieroff") ? 0.48 * (1 - (bar - S.carrieroff[0]) / 11) : 0.46); + // ---- the drone walking out ------------------------------------------ + if (inS(bar, "carrieroff") && bar % 2 === 0) + choir(bar, 0.48 * (1 - (bar - S.carrieroff[0]) / 11)); // one very soft noise wash at the act boundaries that need one — never a // riser, never resolving into anything. - if ([S.three[0], S.message[0], S.reply[0], S.whole[0], S.humans[0]].includes(bar)) + if ([S.three[0], S.message[0], S.reply[0], S.whole[0], S.carrieroff[0]].includes(bar)) shot("sweep", t - 0.9, { gain: 0.055, pan: 0, bus: "music", side: 0.9, dur: 2.2, dark: 0.35 }); } -// The last sound on the record: a hang-up click, after the music has already -// gone. The channel closes; the signal is still out there. +// The last sound on the record: the hang-up, after the music has already +// gone — and v10.1 makes it a REAL one, a handset meeting a cradle, with +// the two synth clicks the record has always ended on riding its edge. +// Then, very far away, one second of busy signal: the far end is still +// trying. The channel closes; the signal is still out there. +if (has("phone-hangup-a")) + shot("phone-hangup-a", at(BARS) + 0.50, { bus: "vox", gain: 0.26, pan: 0, side: 0.35, dark: 0.20, atk: 0.012, wig: 5, wigHz: 0.9, wigIn: 0.3 }); click(at(BARS) + 0.55, { gain: 0.34, pan: 0, side: 0.30 }); click(at(BARS) + 0.62, { gain: 0.20, pan: 0.2, side: 0.30 }); +if (has("phone-busy-us")) + shot("phone-busy-us", at(BARS) + 1.35, { bus: "vox", gain: 0.06, pan: 0.30, side: 0.85, dark: 0.55, dly: 0.55, atk: 0.15, wig: 10, wigHz: 0.25, wigIn: 0.8 }); if (missing.size) console.warn(` ! missing samples: ${[...missing].join(", ")}`); console.log(` ${kicks.length} kicks · ${snares.length} snares`); @@ -1705,7 +2230,7 @@ const sideOut = new Float32Array(N); bar < S.spread[0] ? 0.52 : bar < S.whole[0] ? 0.86 : bar < S.recognise[0] ? 0.46 : - bar < S.humans[0] ? 0.60 : 0.90; + bar < S.carrieroff[0] ? 0.60 : 0.90; send += 0.00004 * (target - send); sideOut[i] = lp * send; } diff --git a/pop/cult/bin/transcript.mjs b/pop/cult/bin/transcript.mjs new file mode 100644 index 0000000000..69ffe3620d --- /dev/null +++ b/pop/cult/bin/transcript.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +// transcript.mjs — a word-level transcript of the shipped record. +// +// @jeffrey: "make me a 'sample transcript' so i can just see timestamps of +// every word said in the song, so we can make specific edits". The events +// receipt already knows every sample the score placed; this maps sample +// names back to the words they say and prints them in SHIPPED time (the +// cut starts 15.95 s into the full render), one line per utterance, with +// who is speaking and which act it lands in. Non-verbal voices (kicks, +// beeps, skids, friction) are left out — this is the libretto, not the +// score. +// +// node pop/cult/bin/transcript.mjs # prints + writes .transcript.txt + +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const OUT = resolve(dirname(fileURLToPath(import.meta.url)), "../out"); +const TRIM = 15.95; + +const receipt = JSON.parse(readFileSync(join(OUT, "cult-remix-v10.events.json"), "utf8")); +const events = receipt.events ?? receipt; + +// Acts in shipped bars (bar 8 of the full render is shipped 0:00). +const ACTS = [ + [8, "II THREE VOICES"], [24, "III THE MESSAGE"], [40, "IV THE SECRET"], + [48, "V THE REPLY"], [64, "VI IT SPREADS"], [76, "VII THE WHOLE MESSAGE"], + [96, "VIII RECOGNITION"], [104, "IX CARRIER OFF"], +]; +const actAt = (bar) => { let a = ACTS[0][1]; for (const [b, n] of ACTS) if (bar >= b) a = n; return a; }; + +function words(name) { + if (/^runrealfast-fast/.test(name)) return "run-real-fast (double-time)"; + if (/^runrealfast/.test(name)) return "run real fast"; + if (/^runitfast/.test(name)) return "run it fast"; + if (/^hideaway/.test(name)) return "hide away"; + if (/^iwannahide/.test(name)) return "i wanna hide"; + if (/^iwanna/.test(name)) return "i wanna"; + if (/^away/.test(name)) return "a-waaay"; + if (/^dotdotdash/.test(name)) return "dot dot dash"; + if (/^(dot|voxdot)-/.test(name)) return "dot"; + if (/^altdot-(\d+)-long$/.test(name)) return `dot (held, post ${name.split("-")[1]}…)`; + if (/^alt-(\d+)-dot/.test(name)) return `dot (post ${name.split("-")[1]}…)`; + if (/^alt-(\d+)-cult/.test(name)) return `cult (post ${name.split("-")[1]}…)`; + if (/^alt-(\d+)-threeofus/.test(name)) return `the three of us (post ${name.split("-")[1]}…)`; + if (/^(cult|cultlong)-/.test(name)) return "cult"; + if (/^(dash|dashlong|bassdash|sos-dash)/.test(name)) return "dash"; + if (/^eleven-dotdot/.test(name)) return "dot dot"; + if (/^eleven-dash/.test(name)) return "dash"; + if (name === "hook-spoken") return "[spoken hook]"; + if (name === "chant-full") return "[full chant]"; + if (name === "three-of-us") return "the three of us"; + if (name === "tagline") return "[tagline]"; + if (name === "hide-away") return "hide away"; + return null; +} + +const mmss = (s) => `${Math.floor(s / 60)}:${(s % 60).toFixed(1).padStart(4, "0")}`; + +const lines = []; +for (const e of events) { + if (typeof e.sample !== "string") continue; + const w = words(e.sample); + if (!w) continue; + const t = e.t - TRIM; + if (t < -0.5) continue; + const bar = e.t / 2; + lines.push({ + t, text: `${mmss(Math.max(0, t))} ${w}${e.who ? ` · ${e.who}` : ""}` + + ` · ${e.sample} · g${e.gain} · ${actAt(bar)} · bar ${bar.toFixed(1)}`, + }); +} +lines.sort((a, b) => a.t - b.t); + +const text = [ + "whistlegraph cult --- remix (v10.1) — word transcript, shipped time", + `${lines.length} utterances · cut starts ${TRIM}s into the full render`, + "", + ...lines.map((l) => l.text), +].join("\n"); + +writeFileSync(join(OUT, "cult-remix-v10.transcript.txt"), text + "\n"); +console.log(text); +console.log(`\n✓ ${join(OUT, "cult-remix-v10.transcript.txt")}`); diff --git a/pop/cult/c/.gitignore b/pop/cult/c/.gitignore new file mode 100644 index 0000000000..af6efe1af2 --- /dev/null +++ b/pop/cult/c/.gitignore @@ -0,0 +1,2 @@ +cultremix +out/ diff --git a/pop/cult/c/README.md b/pop/cult/c/README.md new file mode 100644 index 0000000000..dd8143ee51 --- /dev/null +++ b/pop/cult/c/README.md @@ -0,0 +1,55 @@ +# cult remix — C engine + +`cultremix.c` is the whistlegraph cult remix renderer (`pop/cult/bin/render10.mjs`, +v10.1) ported to the fleet-standard single-file C engine, in the shape of +`pop/boombaboom/c`, `pop/hellsine/c` and `pop/hopehop/c`. + +``` +bash pop/cult/c/build.sh # cc -O2 -o cultremix cultremix.c -lm +pop/cult/c/cultremix # → pop/cult/c/out/cult-remix-c.wav +``` + +## Parity vs `node pop/cult/bin/render10.mjs` + +Measured against a fresh Node render of the same source +(md5 `2e2ac6bab8a5f8cc3c17a2136a9dcf9b`), same machine, 2026-08-16: + +| metric | Node | C | +| --- | --- | --- | +| duration | 10,905,600 frames · 227.200 s | identical, frame-exact | +| integrated loudness (ffmpeg loudnorm) | −17.40 LUFS | −17.40 LUFS | +| true peak | −0.72 dBTP | −0.72 dBTP | +| sample peak | −0.724 dBFS | −0.724 dBFS | +| pre-master peak / linear trim | 1.895557 / 0.485 | 1.895557 / 0.485 | +| 1 s window RMS Δ @ 20/60/95/155/210 s | — | 0.000 dB at all five | +| max per-sample difference | — | 1.34 × 10⁻⁷ (residual −165 dBFS) | +| render wall time | ~17.3 s | ~3.9 s (~4.5×) | + +Every voice is exact — kick/revKick/wub, bass, sines, DTMF/bops/clicks/taps, +woodTap, friction and frictionPath, shot with pitch wiggle, granular stretch, +subharmonic doubling, choir/secretChoir/raga/phoneTune, tube DC block, dub +delay, both ducks, the side return and the single linear trim. No voice is +approximate: both generators are reproduced bit-for-bit (score LCG seed +20220120 stepping `seed*1664525+1013904223` mod 2³² then /2³²; noise +xorshift32 seed 987654321 divided by 2³²−1), and every `jit()/vel()/nrnd()` +draw happens in the Node score's evaluation order — jit-before-vel pairs are +explicitly sequenced because C leaves argument order unspecified. The +remaining 10⁻⁷ residual is float32 accumulation-order noise. + +## What differs from the Node renderer + +- **No events JSON receipt.** `render10.mjs` remains the receipt generator + for the video/score pipeline; this engine renders audio only. +- **No `--stems`, `MUTE=`, `ONLY=` debug lanes.** +- **No mp3 decode.** The perc/sweep demos load from the ffmpeg caches the + Node renderer leaves in `pop/cult/out/.cache-*.wav`; a missing cache is + built by shelling out to ffmpeg with the same command. +- Output is `c/out/cult-remix-c.wav` (float32 stereo 48 k, same format as + the Node writer), leaving `out/cult-remix-v10-full.wav` to the Node lane. +- Memory: ~850 MB peak (fifteen N-length float busses plus envelopes, same + layout as the Node render). + +The port tracks the working-tree v10.1 source (iwannaslow takes, withheld +words, choir sub floors, blip counterline, phone tune, raga ornament, phone +gestures, 112 bars / 9 acts). If `render10.mjs` changes, re-port before +trusting parity. diff --git a/pop/cult/c/build.sh b/pop/cult/c/build.sh new file mode 100644 index 0000000000..80661327cd --- /dev/null +++ b/pop/cult/c/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# build.sh — compile the cult remix C engine (same flags as the sibling lanes) +set -euo pipefail +cd "$(dirname "$0")" +cc -O2 -o cultremix cultremix.c -lm +echo "built ./cultremix" diff --git a/pop/cult/phone/manifest.json b/pop/cult/phone/manifest.json new file mode 100644 index 0000000000..d8002dc309 --- /dev/null +++ b/pop/cult/phone/manifest.json @@ -0,0 +1,17 @@ +{ + "note": "Real telephone gestures for the signal layer — the sounds the record's synthesized clicks and DTMF have been impersonating. All freesound.org, all CC0 (public domain), so they may sit in a release master. bin/phones.sh rebuilds phone/*.wav from the vault freesound cache (~/aesthetic-computer-vault/personal/pop/freesound-cache); attributions are ledgered there in _attributions.json.", + "samples": { + "phone-pickup-a": { "id": 531058, "by": "HaleyReeseCalhoun", "title": "Sneaky Phone Pick Up - HRC.m4a" }, + "phone-pickup-b": { "id": 634525, "by": "DexD73", "title": "House Phone Pick Up and Put Down" }, + "phone-pickup-hangup": { "id": 699159, "by": "Mr.Epic777", "title": "Telephone Pick Up And Hang Up" }, + "phone-hangup-a": { "id": 406240, "by": "_stubb", "title": "telephone_hang up_near.wav" }, + "phone-hangup-b": { "id": 467200, "by": "ThunderQuads", "title": "Phone Hangup.wav" }, + "phone-hangup-c": { "id": 121758, "by": "ondrosik", "title": "hang-up6.wav" }, + "phone-hangup-d": { "id": 240681, "by": "mariiao2", "title": "hang up.mp3" }, + "phone-rotary-a": { "id": 556484, "by": "cookies+policy", "title": "Rotary Dial 1" }, + "phone-rotary-b": { "id": 556483, "by": "cookies+policy", "title": "Rotary Dial 2" }, + "phone-busy-us": { "id": 178820, "by": "Felfa", "title": "busy signal_us.mp3" }, + "phone-busy-eu": { "id": 178821, "by": "Felfa", "title": "busy signal_europe.mp3" }, + "phone-offhook": { "id": 455409, "by": "AnthonyRamirez", "title": "Phone Off-Hook Tone.wav" } + } +} diff --git a/pop/out/pop-library.json b/pop/out/pop-library.json index 33dd9e508e..46725ad60c 100644 --- a/pop/out/pop-library.json +++ b/pop/out/pop-library.json @@ -1,7 +1,58 @@ { - "generated": "2026-08-16T16:54:35.890Z", - "count": 79, + "generated": "2026-08-27T16:25:28.284Z", + "count": 82, "tracks": [ + { + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-release.mp3", + "title": "femrag-plusplus-release", + "artist": "Aesthetic Dot Computer", + "lane": "maytrax", + "backend": "C engine", + "status": "RELEASED", + "updated": "2026-08-27T03:45:14.324Z", + "revisions": 24, + "bytes": 5869309, + "durationSec": null, + "bpm": 144, + "key": "A major", + "releaseDate": "2026-08-27", + "art": null, + "media": [], + "links": { + "spotify": "https://open.spotify.com/track/2v7LcVM38kpiIYd0RWQJzm", + "apple": null, + "youtube": null, + "distrokid": null + } + }, + { + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus.mp3", + "title": "femrag-plusplus", + "artist": "Aesthetic Dot Computer", + "lane": "maytrax", + "backend": "C engine", + "status": "RELEASED", + "updated": "2026-08-27T03:45:00.808Z", + "revisions": 24, + "bytes": 5869558, + "durationSec": null, + "bpm": 144, + "key": "A major", + "releaseDate": "2026-08-27", + "art": null, + "media": [ + { + "kind": "video", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-canvas.mp4" + } + ], + "links": { + "spotify": "https://open.spotify.com/track/2v7LcVM38kpiIYd0RWQJzm", + "apple": null, + "youtube": null, + "distrokid": null + } + }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/marimbaba.mp3", "title": "marimbaba", @@ -32,7 +83,7 @@ "lane": "wattajetta", "backend": "C engine", "status": "RELEASED", - "updated": "2026-08-09T19:58:01.016Z", + "updated": "2026-08-16T14:48:05.000Z", "revisions": 12, "bytes": 3769570, "durationSec": null, @@ -49,36 +100,28 @@ } }, { - "path": "/Users/jas/aesthetic-computer/pop/cult/out/cult-remix.mp3", - "title": "cult-remix", + "path": "/Users/jas/aesthetic-computer/pop/loner/out/lonerclub.mp3", + "title": "lonerclub", "artist": "Aesthetic Dot Computer", - "lane": "cult", + "lane": "loner", "backend": "C engine", "status": "MASTERING", - "updated": "2026-08-16T16:53:30.000Z", - "revisions": 7, - "bytes": 8453204, + "updated": "2026-08-26T17:52:24.000Z", + "revisions": 67, + "bytes": 7796522, "durationSec": null, - "bpm": 120, - "key": "B minor", + "bpm": 122, + "key": "A# minor", "releaseDate": null, "art": null, "media": [ { "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/cult/out/cult-remix-v4-extended-score.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/cult/out/cult-remix-v5-extended-score.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/cult/out/cult-remix-v6-extended-score.mp4" + "path": "/Users/jas/aesthetic-computer/pop/loner/out/lonerclub-v4pid-review-score-light.mp4" }, { "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/cult/out/cult-remix-v7-extended-score.mp4" + "path": "/Users/jas/aesthetic-computer/pop/loner/out/lonerclub-v4pid-review-score.mp4" } ], "links": { @@ -88,6 +131,29 @@ "distrokid": null } }, + { + "path": "/Users/jas/aesthetic-computer/pop/cult/out/cult-remix.mp3", + "title": "cult-remix", + "artist": "Aesthetic Dot Computer", + "lane": "cult", + "backend": "C engine", + "status": "MASTERING", + "updated": "2026-08-16T16:53:30.000Z", + "revisions": 12, + "bytes": 8453204, + "durationSec": null, + "bpm": 120, + "key": "B minor", + "releaseDate": null, + "art": null, + "media": [], + "links": { + "spotify": null, + "apple": null, + "youtube": null, + "distrokid": null + } + }, { "path": "/Users/jas/aesthetic-computer/pop/jungle/out/solafiya.mp3", "title": "solafiya", @@ -95,7 +161,7 @@ "lane": "jungle", "backend": "JS DSP", "status": "MASTERING", - "updated": "2026-07-30T05:10:20.517Z", + "updated": "2026-08-16T14:45:40.000Z", "revisions": 5, "bytes": 2131546, "durationSec": null, @@ -112,18 +178,18 @@ } }, { - "path": "/Users/jas/aesthetic-computer/pop/season/out/season-remix.mp3", - "title": "season-remix", + "path": "/Users/jas/aesthetic-computer/pop/loner/out/loner-remix.mp3", + "title": "loner-remix", "artist": "Aesthetic Dot Computer", - "lane": "season", - "backend": "JS DSP", + "lane": "loner", + "backend": "C engine", "status": "RENDER", - "updated": "2026-08-16T16:53:41.000Z", - "revisions": 1, - "bytes": 7047638, + "updated": "2026-08-24T11:23:57.000Z", + "revisions": 67, + "bytes": 6370959, "durationSec": null, "bpm": 122, - "key": null, + "key": "A# minor", "releaseDate": null, "art": null, "media": [], @@ -135,18 +201,18 @@ } }, { - "path": "/Users/jas/aesthetic-computer/pop/loner/out/loner-remix.mp3", - "title": "loner-remix", + "path": "/Users/jas/aesthetic-computer/pop/season/out/season-remix.mp3", + "title": "season-remix", "artist": "Aesthetic Dot Computer", - "lane": "loner", + "lane": "season", "backend": "JS DSP", "status": "RENDER", - "updated": "2026-08-16T16:53:40.000Z", - "revisions": 0, - "bytes": 6850897, + "updated": "2026-08-16T16:53:41.000Z", + "revisions": 1, + "bytes": 7047638, "durationSec": null, - "bpm": 80, - "key": "A# minor", + "bpm": 122, + "key": null, "releaseDate": null, "art": null, "media": [], @@ -162,10 +228,10 @@ "title": "factory-remix", "artist": "Aesthetic Dot Computer", "lane": "factory", - "backend": "JS DSP", + "backend": "C engine", "status": "RENDER", "updated": "2026-08-16T16:53:36.000Z", - "revisions": 0, + "revisions": 2, "bytes": 9517800, "durationSec": null, "bpm": 90, @@ -187,7 +253,7 @@ "lane": "teknull", "backend": "C engine", "status": "WIP", - "updated": "2026-07-30T02:43:24.307Z", + "updated": "2026-08-16T14:47:37.000Z", "revisions": 6, "bytes": 4801965, "durationSec": null, @@ -210,7 +276,7 @@ "lane": "teknull", "backend": "C engine", "status": "WIP", - "updated": "2026-07-30T02:42:23.935Z", + "updated": "2026-08-16T14:47:26.000Z", "revisions": 6, "bytes": 2776461, "durationSec": null, @@ -226,6 +292,24 @@ "distrokid": null } }, + { + "path": "/Users/jas/aesthetic-computer/pop/cult/out/whistlecultspatial.mp3", + "title": "whistlecultspatial", + "artist": "Aesthetic Dot Computer", + "lane": "cult", + "backend": "C engine", + "status": "UNRELEASED", + "updated": "2026-08-16T20:12:00.000Z", + "revisions": 12, + "bytes": 9090285, + "durationSec": null, + "bpm": null, + "key": null, + "releaseDate": null, + "art": null, + "media": [], + "links": null + }, { "path": "/Users/jas/aesthetic-computer/pop/wattajetta/out/wattajetta.mp3", "title": "wattajetta", @@ -281,66 +365,33 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus.mp3", - "title": "femrag-plusplus", + "path": "/Users/jas/aesthetic-computer/pop/wattajetta/out/watjetsto720.mp3", + "title": "watjetsto720", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "wattajetta", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-08-13T23:04:24.718Z", - "revisions": 23, - "bytes": 5869543, + "updated": "2026-08-16T14:47:56.000Z", + "revisions": 12, + "bytes": 13261936, "durationSec": null, "bpm": null, "key": null, "releaseDate": null, "art": null, - "media": [ - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-beat-glow.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-beat-lights.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-beat-vign.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-reel-final.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-reel-lit.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-reel.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-yt-lit.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-yt.mp4" - } - ], + "media": [], "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/wattajetta/out/watjetsto720.mp3", - "title": "watjetsto720", + "path": "/Users/jas/aesthetic-computer/pop/wattajetta/out/taksmukkeklokkewattajetta.mp3", + "title": "taksmukkeklokkewattajetta", "artist": "Aesthetic Dot Computer", "lane": "wattajetta", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-08-13T22:05:56.035Z", + "updated": "2026-08-16T14:47:53.000Z", "revisions": 12, - "bytes": 13261936, + "bytes": 6900691, "durationSec": null, "bpm": null, "key": null, @@ -350,15 +401,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezen2.mp3", - "title": "cutezen2", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/peaktek.mp3", + "title": "peaktek", "artist": "Aesthetic Dot Computer", - "lane": "cutezen", - "backend": "JS DSP", + "lane": "minitek", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-08-13T20:13:41.383Z", - "revisions": 3, - "bytes": 3320349, + "updated": "2026-08-16T14:47:16.000Z", + "revisions": 2, + "bytes": 4730022, "durationSec": null, "bpm": null, "key": null, @@ -368,66 +419,33 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag.mp3", - "title": "femrag", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/rumbletek.mp3", + "title": "rumbletek", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:47.814Z", - "revisions": 23, - "bytes": 4802021, + "updated": "2026-08-16T14:47:16.000Z", + "revisions": 2, + "bytes": 4792424, "durationSec": null, "bpm": null, "key": null, "releaseDate": null, "art": null, - "media": [ - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-beat-glow.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-beat-lights.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-beat-vign.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-reel-final.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-reel-lit.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-reel.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-yt-lit.mp4" - }, - { - "kind": "video", - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-shakeout-yt.mp4" - } - ], + "media": [], "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/microtrax.mp3", - "title": "microtrax", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/minitek.mp3", + "title": "minitek", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:44.418Z", - "revisions": 23, - "bytes": 4802024, + "updated": "2026-08-16T14:47:14.000Z", + "revisions": 2, + "bytes": 4922022, "durationSec": null, "bpm": null, "key": null, @@ -437,15 +455,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/pianotrax.mp3", - "title": "pianotrax", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/brokentek.mp3", + "title": "brokentek", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:42.098Z", - "revisions": 23, - "bytes": 4578344, + "updated": "2026-08-16T14:47:11.000Z", + "revisions": 2, + "bytes": 4656104, "durationSec": null, "bpm": null, "key": null, @@ -455,15 +473,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/lumitrax.mp3", - "title": "lumitrax", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/hypnotek.mp3", + "title": "hypnotek", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:36.380Z", - "revisions": 23, - "bytes": 4876903, + "updated": "2026-08-16T14:47:10.000Z", + "revisions": 2, + "bytes": 4990183, "durationSec": null, "bpm": null, "key": null, @@ -473,15 +491,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/cavetrax.mp3", - "title": "cavetrax", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/dubtek.mp3", + "title": "dubtek", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:29.707Z", - "revisions": 23, - "bytes": 5122663, + "updated": "2026-08-16T14:47:09.000Z", + "revisions": 2, + "bytes": 4852901, "durationSec": null, "bpm": null, "key": null, @@ -491,15 +509,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/hum/out/hum.mp3", - "title": "hum", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/glitchtek.mp3", + "title": "glitchtek", "artist": "Aesthetic Dot Computer", - "lane": "hum", - "backend": "JS DSP", + "lane": "minitek", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:25.682Z", + "updated": "2026-08-16T14:47:06.000Z", "revisions": 2, - "bytes": 623085, + "bytes": 4669544, "durationSec": null, "bpm": null, "key": null, @@ -509,15 +527,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/dusktrax.mp3", - "title": "dusktrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/tidetrax.mp3", + "title": "tidetrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:22.176Z", - "revisions": 23, - "bytes": 4935463, + "updated": "2026-08-16T14:47:05.000Z", + "revisions": 24, + "bytes": 4890343, "durationSec": null, "bpm": null, "key": null, @@ -527,15 +545,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/hippyhayzard/out/hippyhayzard.mp3", - "title": "hippyhayzard", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/deeptek.mp3", + "title": "deeptek", "artist": "Aesthetic Dot Computer", - "lane": "hippyhayzard", - "backend": "JS DSP", + "lane": "minitek", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:21.725Z", - "revisions": 1, - "bytes": 1123005, + "updated": "2026-08-16T14:47:05.000Z", + "revisions": 2, + "bytes": 4785702, "durationSec": null, "bpm": null, "key": null, @@ -545,15 +563,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/frosttrax.mp3", - "title": "frosttrax", + "path": "/Users/jas/aesthetic-computer/pop/minitek/out/acidtek.mp3", + "title": "acidtek", "artist": "Aesthetic Dot Computer", - "lane": "maytrax", + "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:14.399Z", - "revisions": 23, - "bytes": 4791464, + "updated": "2026-08-16T14:47:04.000Z", + "revisions": 2, + "bytes": 4856742, "durationSec": null, "bpm": null, "key": null, @@ -563,15 +581,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/glasstrax.mp3", - "title": "glasstrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/mosstrax.mp3", + "title": "mosstrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:08.122Z", - "revisions": 23, - "bytes": 4578344, + "updated": "2026-08-16T14:47:01.000Z", + "revisions": 24, + "bytes": 4894183, "durationSec": null, "bpm": null, "key": null, @@ -581,15 +599,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/hopehop/out/hopehop.mp3", - "title": "hopehop", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/sinetrax.mp3", + "title": "sinetrax", "artist": "Aesthetic Dot Computer", - "lane": "hopehop", + "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:05.742Z", - "revisions": 2, - "bytes": 2827821, + "updated": "2026-08-16T14:46:58.000Z", + "revisions": 24, + "bytes": 5522023, "durationSec": null, "bpm": null, "key": null, @@ -599,15 +617,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/mosstrax.mp3", - "title": "mosstrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/pianotrax.mp3", + "title": "pianotrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:11:00.576Z", - "revisions": 23, - "bytes": 4894183, + "updated": "2026-08-16T14:46:54.000Z", + "revisions": 24, + "bytes": 4578344, "durationSec": null, "bpm": null, "key": null, @@ -617,15 +635,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/embertrax.mp3", - "title": "embertrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/maytrax.mp3", + "title": "maytrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:51.845Z", - "revisions": 23, - "bytes": 4904744, + "updated": "2026-08-16T14:46:53.000Z", + "revisions": 24, + "bytes": 5718822, "durationSec": null, "bpm": null, "key": null, @@ -635,15 +653,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/tidetrax.mp3", - "title": "tidetrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/cavetrax.mp3", + "title": "cavetrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:42.961Z", - "revisions": 23, - "bytes": 4890343, + "updated": "2026-08-16T14:46:49.000Z", + "revisions": 24, + "bytes": 5122663, "durationSec": null, "bpm": null, "key": null, @@ -653,33 +671,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/dewtrax.mp3", - "title": "dewtrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/microtrax.mp3", + "title": "microtrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:34.696Z", - "revisions": 23, - "bytes": 5042022, - "durationSec": null, - "bpm": null, - "key": null, - "releaseDate": null, - "art": null, - "media": [], - "links": null - }, - { - "path": "/Users/jas/aesthetic-computer/pop/gecs/out/gecs.mp3", - "title": "gecs", - "artist": "Aesthetic Dot Computer", - "lane": "gecs", - "backend": "JS DSP", - "status": "UNRELEASED", - "updated": "2026-07-30T05:10:32.511Z", - "revisions": 1, - "bytes": 2202045, + "updated": "2026-08-16T14:46:49.000Z", + "revisions": 24, + "bytes": 4802024, "durationSec": null, "bpm": null, "key": null, @@ -689,15 +689,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/sinetrax.mp3", - "title": "sinetrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/glasstrax.mp3", + "title": "glasstrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:27.065Z", - "revisions": 23, - "bytes": 5522023, + "updated": "2026-08-16T14:46:46.000Z", + "revisions": 24, + "bytes": 4578344, "durationSec": null, "bpm": null, "key": null, @@ -707,15 +707,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/maytrax.mp3", - "title": "maytrax", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/frosttrax.mp3", + "title": "frosttrax", "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:19.928Z", - "revisions": 23, - "bytes": 5718822, + "updated": "2026-08-16T14:46:44.000Z", + "revisions": 24, + "bytes": 4791464, "durationSec": null, "bpm": null, "key": null, @@ -725,15 +725,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/jungle/out/solfia.mp3", - "title": "solfia", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/lumitrax.mp3", + "title": "lumitrax", "artist": "Aesthetic Dot Computer", - "lane": "jungle", - "backend": "JS DSP", + "lane": "maytrax", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:13.713Z", - "revisions": 5, - "bytes": 1651529, + "updated": "2026-08-16T14:46:42.000Z", + "revisions": 24, + "bytes": 4876903, "durationSec": null, "bpm": null, "key": null, @@ -743,33 +743,38 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/jungle/out/rodando.mp3", - "title": "rodando", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag.mp3", + "title": "femrag", "artist": "Aesthetic Dot Computer", - "lane": "jungle", - "backend": "JS DSP", + "lane": "maytrax", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:09.167Z", - "revisions": 5, - "bytes": 1582077, + "updated": "2026-08-16T14:46:40.000Z", + "revisions": 24, + "bytes": 4802021, "durationSec": null, "bpm": null, "key": null, "releaseDate": null, "art": null, - "media": [], + "media": [ + { + "kind": "video", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/femrag-plusplus-canvas.mp4" + } + ], "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/rumbletek.mp3", - "title": "rumbletek", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/dewtrax.mp3", + "title": "dewtrax", "artist": "Aesthetic Dot Computer", - "lane": "minitek", + "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:07.536Z", - "revisions": 2, - "bytes": 4792424, + "updated": "2026-08-16T14:46:39.000Z", + "revisions": 24, + "bytes": 5042022, "durationSec": null, "bpm": null, "key": null, @@ -779,15 +784,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/jungle/out/raggasol.mp3", - "title": "raggasol", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/embertrax.mp3", + "title": "embertrax", "artist": "Aesthetic Dot Computer", - "lane": "jungle", - "backend": "JS DSP", + "lane": "maytrax", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:10:04.082Z", - "revisions": 5, - "bytes": 1655707, + "updated": "2026-08-16T14:46:39.000Z", + "revisions": 24, + "bytes": 4904744, "durationSec": null, "bpm": null, "key": null, @@ -797,15 +802,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/jungle/out/jungleton.mp3", - "title": "jungleton", + "path": "/Users/jas/aesthetic-computer/pop/marimba/out/flutterbappavox.mp3", + "title": "flutterbappavox", "artist": "Aesthetic Dot Computer", - "lane": "jungle", - "backend": "JS DSP", + "lane": "marimba", + "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:59.518Z", - "revisions": 5, - "bytes": 1694013, + "updated": "2026-08-16T14:46:32.000Z", + "revisions": 26, + "bytes": 3970666, "durationSec": null, "bpm": null, "key": null, @@ -815,15 +820,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/glitchtek.mp3", - "title": "glitchtek", + "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/dusktrax.mp3", + "title": "dusktrax", "artist": "Aesthetic Dot Computer", - "lane": "minitek", + "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:57.404Z", - "revisions": 2, - "bytes": 4669544, + "updated": "2026-08-16T14:46:30.000Z", + "revisions": 24, + "bytes": 4935463, "durationSec": null, "bpm": null, "key": null, @@ -833,15 +838,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/brokentek.mp3", - "title": "brokentek", + "path": "/Users/jas/aesthetic-computer/pop/marimba/out/flutterbap.mp3", + "title": "flutterbap", "artist": "Aesthetic Dot Computer", - "lane": "minitek", + "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:50.931Z", - "revisions": 2, - "bytes": 4656104, + "updated": "2026-08-16T14:46:21.000Z", + "revisions": 26, + "bytes": 1496349, "durationSec": null, "bpm": null, "key": null, @@ -851,15 +856,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/deeptek.mp3", - "title": "deeptek", + "path": "/Users/jas/aesthetic-computer/pop/jungle/out/solfia.mp3", + "title": "solfia", "artist": "Aesthetic Dot Computer", - "lane": "minitek", - "backend": "C engine", + "lane": "jungle", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:45.275Z", - "revisions": 2, - "bytes": 4785702, + "updated": "2026-08-16T14:45:46.000Z", + "revisions": 5, + "bytes": 1651529, "durationSec": null, "bpm": null, "key": null, @@ -869,15 +874,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/peaktek.mp3", - "title": "peaktek", + "path": "/Users/jas/aesthetic-computer/pop/jungle/out/raggasol.mp3", + "title": "raggasol", "artist": "Aesthetic Dot Computer", - "lane": "minitek", - "backend": "C engine", + "lane": "jungle", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:39.679Z", - "revisions": 2, - "bytes": 4730022, + "updated": "2026-08-16T14:45:42.000Z", + "revisions": 5, + "bytes": 1655707, "durationSec": null, "bpm": null, "key": null, @@ -887,15 +892,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/dubtek.mp3", - "title": "dubtek", + "path": "/Users/jas/aesthetic-computer/pop/gecs/out/gecs.mp3", + "title": "gecs", "artist": "Aesthetic Dot Computer", - "lane": "minitek", - "backend": "C engine", + "lane": "gecs", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:34.249Z", - "revisions": 2, - "bytes": 4852901, + "updated": "2026-08-16T14:45:41.000Z", + "revisions": 1, + "bytes": 2202045, "durationSec": null, "bpm": null, "key": null, @@ -905,15 +910,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezenith.mp3", - "title": "cutezenith", + "path": "/Users/jas/aesthetic-computer/pop/jungle/out/jungleton.mp3", + "title": "jungleton", "artist": "Aesthetic Dot Computer", - "lane": "cutezen", + "lane": "jungle", "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:33.849Z", - "revisions": 3, - "bytes": 2999997, + "updated": "2026-08-16T14:45:41.000Z", + "revisions": 5, + "bytes": 1694013, "durationSec": null, "bpm": null, "key": null, @@ -923,15 +928,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezip.mp3", - "title": "cutezip", + "path": "/Users/jas/aesthetic-computer/pop/jungle/out/rodando.mp3", + "title": "rodando", "artist": "Aesthetic Dot Computer", - "lane": "cutezen", + "lane": "jungle", "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:32.233Z", - "revisions": 3, - "bytes": 2455053, + "updated": "2026-08-16T14:45:40.000Z", + "revisions": 5, + "bytes": 1582077, "durationSec": null, "bpm": null, "key": null, @@ -941,15 +946,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/acidtek.mp3", - "title": "acidtek", + "path": "/Users/jas/aesthetic-computer/pop/hopehop/out/hopehop.mp3", + "title": "hopehop", "artist": "Aesthetic Dot Computer", - "lane": "minitek", + "lane": "hopehop", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:27.897Z", + "updated": "2026-08-16T14:45:37.000Z", "revisions": 2, - "bytes": 4856742, + "bytes": 2827821, "durationSec": null, "bpm": null, "key": null, @@ -965,7 +970,7 @@ "lane": "cutezen", "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:23.321Z", + "updated": "2026-08-16T14:45:35.000Z", "revisions": 3, "bytes": 3168333, "durationSec": null, @@ -977,15 +982,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/hypnotek.mp3", - "title": "hypnotek", + "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezenith.mp3", + "title": "cutezenith", "artist": "Aesthetic Dot Computer", - "lane": "minitek", - "backend": "C engine", + "lane": "cutezen", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:21.853Z", - "revisions": 2, - "bytes": 4990183, + "updated": "2026-08-16T14:45:35.000Z", + "revisions": 3, + "bytes": 2999997, "durationSec": null, "bpm": null, "key": null, @@ -995,15 +1000,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/minitek/out/minitek.mp3", - "title": "minitek", + "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezip.mp3", + "title": "cutezip", "artist": "Aesthetic Dot Computer", - "lane": "minitek", - "backend": "C engine", + "lane": "cutezen", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T05:09:16.360Z", - "revisions": 2, - "bytes": 4922022, + "updated": "2026-08-16T14:45:35.000Z", + "revisions": 3, + "bytes": 2455053, "durationSec": null, "bpm": null, "key": null, @@ -1013,15 +1018,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/wattajetta/out/taksmukkeklokkewattajetta.mp3", - "title": "taksmukkeklokkewattajetta", + "path": "/Users/jas/aesthetic-computer/pop/hum/out/hum.mp3", + "title": "hum", "artist": "Aesthetic Dot Computer", - "lane": "wattajetta", - "backend": "C engine", + "lane": "hum", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-30T03:08:32.260Z", - "revisions": 12, - "bytes": 6900691, + "updated": "2026-08-16T14:45:35.000Z", + "revisions": 2, + "bytes": 623085, "durationSec": null, "bpm": null, "key": null, @@ -1031,15 +1036,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/marimba/out/flutterbappavox.mp3", - "title": "flutterbappavox", + "path": "/Users/jas/aesthetic-computer/pop/hippyhayzard/out/hippyhayzard.mp3", + "title": "hippyhayzard", "artist": "Aesthetic Dot Computer", - "lane": "marimba", - "backend": "C engine", + "lane": "hippyhayzard", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-28T18:32:42.000Z", - "revisions": 26, - "bytes": 3970666, + "updated": "2026-08-16T14:45:33.000Z", + "revisions": 1, + "bytes": 1123005, "durationSec": null, "bpm": null, "key": null, @@ -1049,15 +1054,15 @@ "links": null }, { - "path": "/Users/jas/aesthetic-computer/pop/marimba/out/flutterbap.mp3", - "title": "flutterbap", + "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezen2.mp3", + "title": "cutezen2", "artist": "Aesthetic Dot Computer", - "lane": "marimba", - "backend": "C engine", + "lane": "cutezen", + "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-27T19:37:03.000Z", - "revisions": 26, - "bytes": 1496349, + "updated": "2026-08-16T14:45:29.000Z", + "revisions": 3, + "bytes": 3320349, "durationSec": null, "bpm": null, "key": null, @@ -1073,7 +1078,7 @@ "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", - "updated": "2026-07-27T19:26:35.000Z", + "updated": "2026-08-16T14:44:52.000Z", "revisions": 2, "bytes": 5549371, "durationSec": null, @@ -1091,7 +1096,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:27:51.000Z", + "updated": "2026-06-14T18:27:51.142Z", "revisions": 1, "bytes": 6007785, "durationSec": null, @@ -1109,7 +1114,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:27:22.000Z", + "updated": "2026-06-14T18:27:22.963Z", "revisions": 1, "bytes": 6149870, "durationSec": null, @@ -1127,7 +1132,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:26:29.000Z", + "updated": "2026-06-14T18:26:29.118Z", "revisions": 1, "bytes": 5745707, "durationSec": null, @@ -1145,7 +1150,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:25:51.000Z", + "updated": "2026-06-14T18:25:51.840Z", "revisions": 1, "bytes": 5935787, "durationSec": null, @@ -1163,7 +1168,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:25:17.000Z", + "updated": "2026-06-14T18:25:17.208Z", "revisions": 1, "bytes": 4322027, "durationSec": null, @@ -1181,7 +1186,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:24:38.000Z", + "updated": "2026-06-14T18:24:38.891Z", "revisions": 1, "bytes": 4486185, "durationSec": null, @@ -1199,7 +1204,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:23:35.000Z", + "updated": "2026-06-14T18:23:35.482Z", "revisions": 1, "bytes": 2699626, "durationSec": null, @@ -1217,7 +1222,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:22:59.000Z", + "updated": "2026-06-14T18:22:59.975Z", "revisions": 1, "bytes": 3627948, "durationSec": null, @@ -1235,7 +1240,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:22:33.000Z", + "updated": "2026-06-14T18:22:33.162Z", "revisions": 1, "bytes": 4008106, "durationSec": null, @@ -1253,7 +1258,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:22:31.000Z", + "updated": "2026-06-14T18:22:31.309Z", "revisions": 1, "bytes": 4581225, "durationSec": null, @@ -1271,7 +1276,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:44.000Z", + "updated": "2026-06-14T18:19:44.749Z", "revisions": 1, "bytes": 3584746, "durationSec": null, @@ -1289,7 +1294,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:38.000Z", + "updated": "2026-06-14T18:19:38.581Z", "revisions": 1, "bytes": 5134185, "durationSec": null, @@ -1307,7 +1312,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:29.000Z", + "updated": "2026-06-14T18:19:29.708Z", "revisions": 1, "bytes": 2798504, "durationSec": null, @@ -1325,7 +1330,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:23.000Z", + "updated": "2026-06-14T18:19:23.301Z", "revisions": 1, "bytes": 3698025, "durationSec": null, @@ -1343,7 +1348,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:15.000Z", + "updated": "2026-06-14T18:19:15.276Z", "revisions": 1, "bytes": 4894244, "durationSec": null, @@ -1361,7 +1366,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:06.000Z", + "updated": "2026-06-14T18:19:06.674Z", "revisions": 1, "bytes": 2920426, "durationSec": null, @@ -1379,7 +1384,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:19:01.000Z", + "updated": "2026-06-14T18:19:01.450Z", "revisions": 1, "bytes": 2860909, "durationSec": null, @@ -1397,7 +1402,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:18:56.000Z", + "updated": "2026-06-14T18:18:56.327Z", "revisions": 1, "bytes": 4034025, "durationSec": null, @@ -1415,7 +1420,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:18:48.000Z", + "updated": "2026-06-14T18:18:48.486Z", "revisions": 1, "bytes": 6246825, "durationSec": null, @@ -1433,7 +1438,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:18:35.000Z", + "updated": "2026-06-14T18:18:35.244Z", "revisions": 1, "bytes": 3790226, "durationSec": null, @@ -1451,7 +1456,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:18:27.000Z", + "updated": "2026-06-14T18:18:27.505Z", "revisions": 1, "bytes": 3198827, "durationSec": null, @@ -1469,7 +1474,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:18:21.000Z", + "updated": "2026-06-14T18:18:21.041Z", "revisions": 1, "bytes": 4064748, "durationSec": null, @@ -1487,7 +1492,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:18:12.000Z", + "updated": "2026-06-14T18:18:12.293Z", "revisions": 1, "bytes": 5942504, "durationSec": null, @@ -1505,7 +1510,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:17:59.000Z", + "updated": "2026-06-14T18:17:59.222Z", "revisions": 1, "bytes": 3200747, "durationSec": null, @@ -1523,7 +1528,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T18:17:53.000Z", + "updated": "2026-06-14T18:17:53.070Z", "revisions": 1, "bytes": 6562691, "durationSec": null, @@ -1541,7 +1546,7 @@ "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", - "updated": "2026-06-14T17:30:00.000Z", + "updated": "2026-06-14T17:30:00.499Z", "revisions": 1, "bytes": 3120113, "durationSec": null,