From 23390143794cc4cf9c02410184bd32907bbe8d1c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 27 Sep 2026 22:42:34 -0400 Subject: [PATCH 1/3] Shape drum and impact peaks at the source so the limiter stops pumping the mix The master's -2 dBTP limiter cut 13% of 10 ms windows by more than 1 dB and 4% by more than 3 dB. Every kick-plus-clap backbeat went 4.3-6.5 dB past its ceiling and the hit and drop 6.4-7.6 dB, so the whole mix (pad, arp, bass, cues) dipped about 5 dB on beats 2 and 4 and rippled 2-3 dB for most of a second after each impact. The finding's 16.2 dB worst cut is the 30 ms fade-in, not the limiter; the limiter's own worst was 7.55 dB, at the drop. - soft_clip: a 4x-oversampled tanh soft clip on the dry drum bus, with the peaks found through the master's 30 Hz high-pass; bit-exact below the knee - kick and clap at 2/3 on backbeats (the limiter held them ~3.6 dB down), kicks at 0.75 under the plink ripple - the hit and drop take their room smoothly: they and the music dip to 0.55/0.5 and recover over 0.6 s, the drums over 1.5 s, and their boom takes the downbeat kick; the land plays 2 dB down - the master is unchanged: -14 LUFS, true peak <= -2 dBTP, every sweep exactly SWEEP_UNDER under the bed, deterministic, no rng draw moved Tests bound the limiter on the returned audio (no 10 ms window cut past 3 dB, at most 1% past 1 dB; no cue starting in a >1 dB dip), check the clip's identity below the knee and impact_gain's shape, and tighten the mastered sweep margin to 6 +- 0.3 LU now that the swell is no longer limited. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++- tests/test_soundtrack.py | 107 ++++++++++++++++++++++++++++++++++++-- tools/soundtrack.py | 109 +++++++++++++++++++++++++++++++++------ 3 files changed, 200 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2e91acf..c6a4ce8 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,15 @@ These hold for every input, and `bun run test` plus CI check them (`tests/`, `.g | Rebuilding `video.json` from `data/` is a no-op | round-trip | CI | | Easings map 0→0 and 1→1 and (except the intended `outBack` overshoot) stay in [0, 1] and never decrease; progress and envelopes stay in [0, 1]; the seeded PRNG is deterministic and in [0, 1); number formatting round-trips | property-based (fast-check) | `util.test.js` | | A fresh page and a page that played the film up to *t* draw identical pixels, in both layouts | determinism | `tools/determinism.mjs`, CI | -| Panning keeps power; synthesis is bit-identical across runs; the master is −14 ± 0.5 LUFS with true peak ≤ −1 dBTP and clean edges | property-based (Hypothesis), mastering | `test_soundtrack.py` | +| Panning keeps power; synthesis is bit-identical across runs; the master is −14 ± 0.5 LUFS with true peak ≤ −2 dBTP and clean edges | property-based (Hypothesis), mastering | `test_soundtrack.py` | +| The limiter only catches stray peaks: no 10 ms window cut by more than 3 dB and at most 1% by more than 1 dB, and no cue starts in a window cut by more than 1 dB; every swoosh sits 6 ± 0.3 LU under the music after the master | bounds, measured on the returned audio | `test_soundtrack.py` | +| The drum soft clip returns its input bit for bit below the knee; an impact's room is exactly its depth where it lands, only recovers after the last one, and is exactly 1 elsewhere | property-based (Hypothesis) | `test_soundtrack.py` | ## How it's made - `site/` — a deterministic HTML/canvas timeline. `window.renderAt(t)` draws the frame at time `t`; nothing depends on wall-clock time or `Math.random`. - `tools/render.mjs` — Playwright drives headless Chromium frame by frame. `--stream out.mp4` pipes PNG screenshots straight into parallel ffmpeg encoders and joins the segments, so no frames touch the disk. -- `tools/soundtrack.py` — the score is synthesized with numpy/scipy at 120 BPM (D minor; Bb → C → F under the wordmark), mastered to −14 LUFS integrated with a 4× oversampled true-peak limiter at −2 dBTP. Sound effects come from `events.json`, which the page exports from the same timeline, so each click and soft mallet note lands on the frame that causes it; gainer notes rise in pitch with the $800 step, and the family curve brightens a held chord as the gain rises. +- `tools/soundtrack.py` — the score is synthesized with numpy/scipy at 120 BPM (D minor; Bb → C → F under the wordmark), mastered to −14 LUFS integrated with a 4× oversampled true-peak limiter at −2 dBTP. The loud transients are shaped where they are made, so the limiter only shaves strays: the drum bus is soft-clipped (4× oversampled, found through the master's high-pass) with kick and clap eased back on the backbeats, and the hit and drop smoothly dip themselves and the music (and the drums for longer) instead of rippling the whole mix with the limiter. Sound effects come from `events.json`, which the page exports from the same timeline, so each click and soft mallet note lands on the frame that causes it; gainer notes rise in pitch with the $800 step, and the family curve brightens a held chord as the gain rises. - `tools/determinism.mjs` — proves a fresh page and a page that played the film up to *t* render identical pixels at 11 timestamps, in both layouts. Streaming workers start mid-film, so this is what makes parallel rendering safe. - `tools/adapt_outputs.py` → `tools/build_video_data.py` — turn the raw PolicyEngine outputs into `data/video.json`, the only data the page reads. If any input is missing the page paints a striped MOCK DATA banner on every frame. diff --git a/tests/test_soundtrack.py b/tests/test_soundtrack.py index 5736b78..3f7cb82 100644 --- a/tests/test_soundtrack.py +++ b/tests/test_soundtrack.py @@ -7,6 +7,7 @@ import pytest from hypothesis import given, settings from hypothesis import strategies as st +from scipy.signal import resample_poly import soundtrack as sd from conftest import ROOT @@ -72,6 +73,103 @@ def test_mastering_hits_platform_targets(score): assert (np.abs(mix[:, :edge]) <= ramp).all() and (np.abs(mix[:, -edge:]) <= ramp[::-1]).all() +def _limiter_cut(mix, stems): + """Measured on the returned audio: the master's gain against the mix it was given (the + stems before it, through its 30 Hz high-pass) in 10 ms windows, clear of the 30 ms edge + fades, in dB under the loudness normalization. That is the largest window gain: every + window gets it, and the limiter only takes away. Returns window start times and cuts.""" + pre = sd.hp(stems["bed_pre"] + stems["sweeps_pre"], 30) + w = int(0.010 * sd.SR) + n = sd.N // w + edge = int(0.03 * sd.SR) // w # the fades span three windows at each end + + def rms(x): + return np.sqrt((x[:, : n * w].reshape(2, n, w) ** 2).mean(axis=(0, 2)))[edge: n - edge] + gain = 20 * np.log10(rms(mix) / rms(pre)) + return np.arange(edge, n - edge) * w / sd.SR, gain.max() - gain + + +def test_the_limiter_only_catches_stray_peaks(score): + """The -2 dBTP limiter is a safety net. When a kick and clap landing together went 4-6.5 dB + past it, and the hit and drop 6-7.5 dB, it cut the whole mix with them: 13% of windows by + more than 1 dB and 4% by more than 3 dB, so the pad and arp pumped on every backbeat. + Those peaks are now shaped where they are made, and the limiter shaves strays.""" + _, mix, _, stems = score + _, cut = _limiter_cut(mix, stems) + assert cut.max() <= 3.0 and (cut > 1.0).mean() <= 0.01, (cut.max(), (cut > 1.0).mean()) + + +def test_no_cue_lands_in_a_limiter_dip(score): + """The cues that carry the story keep the level they were written at: none starts in a + window the limiter cut by more than 1 dB. The third statistic used to land on a backbeat + and lose 6 dB, where its two siblings lost under 2.""" + events, mix, _, stems = score + at, cut = _limiter_cut(mix, stems) + cues = [e for e in events if e["type"] in ("lock", "ping", "stat", "plink", "decile")] + assert len(cues) > 20 + for e in cues: + first = (at > e["t"] - 0.01) & (at < e["t"] + 0.02) # the windows its first 20 ms fall in + assert cut[first].max() <= 1.0, (e, cut[first].max()) + + +@settings(max_examples=40) +@given(st.integers(0, 2**32 - 1), st.floats(0.05, 0.99), st.floats(40, 4000)) +def test_soft_clip_leaves_what_stays_under_the_knee_alone(seed, level, lo): + """Wherever nothing reaches the knee, as the master will see it (through its high-pass, at + 4x), soft_clip returns its input bit for bit.""" + x = sd.bp(np.random.default_rng(seed).standard_normal((2, sd.SR // 10)), lo, min(20000, 4 * lo)) + x *= level * sd.DRUM_KNEE / np.abs(resample_poly(sd.hp(x, 30), 4, 1, axis=-1)).max() + assert np.array_equal(sd.soft_clip(x, sd.DRUM_CEIL, sd.DRUM_KNEE), x) + + +@pytest.mark.parametrize("parts", [ + [("kick", 0.6)], # a kick on its own + [("kick", 0.6 * sd.BACKBEAT), ("clap", 0.4 * sd.BACKBEAT)], # backbeats, 8.5-14 s + [("kick", 0.6 * sd.BACKBEAT), ("clap", 0.55 * sd.BACKBEAT)], # backbeats, 14-26 s +]) +def test_the_drums_come_through_soft_clip_near_its_ceiling(parts): + """The score's own drum hits, at the gains it plays them, cross the knee and come out with + their peak (as the master sees it) pulled back to within 1.5 dB of DRUM_CEIL. It is not + exact: the excess comes off the dry signal, which the master's high-pass then reshapes, so a + lone kick lands about 1.1 dB past the ceiling.""" + state = sd.rng.bit_generator.state # the score draws from the same stream + sounds = {"kick": sd.kick(), "clap": sd.clap()} + sd.rng.bit_generator.state = state + x = np.zeros((2, sd.SR // 2)) + for name, g in parts: + sd.place(x, sounds[name], 0.05, g) + + def peak(y): + return np.abs(resample_poly(sd.hp(y, 30), 4, 1, axis=-1)).max() + y = sd.soft_clip(x, sd.DRUM_CEIL, sd.DRUM_KNEE) + assert peak(x) > sd.DRUM_CEIL and peak(y) < peak(x) + assert peak(y) <= sd.DRUM_CEIL * 10 ** (1.5 / 20), 20 * np.log10(peak(y) / sd.DRUM_CEIL) + + +@settings(max_examples=60) +@given(st.lists(st.tuples(st.sampled_from(["hit", "drop", "plink"]), st.floats(-2.0, 32.0)), max_size=4), + st.sampled_from([sd.IMPACT_RELEASE, sd.DRUM_RELEASE, 0.05])) +def test_impacts_take_their_room_and_give_it_back(cues, release): + """impact_gain: all ones without an impact; exactly IMPACT[kind] where one lands (or lower, + where another's room overlaps); never under the deepest IMPACT or over 1; untouched before + the first one's 3 ms ramp; only recovering after the last one lands; and back to exactly 1 + ten release times after it.""" + events = [{"type": kind, "t": t} for kind, t in cues] + g = sd.impact_gain(events, release) + assert g.shape == (sd.N,) and (g <= 1).all() and (g >= min(sd.IMPACT.values())).all() + at = sorted((int(round(t * sd.SR)), kind) for kind, t in cues if kind in sd.IMPACT) + if not at: + assert (g == 1).all() + return + for i, kind in at: + if 0 <= i < sd.N: + assert g[i] <= sd.IMPACT[kind] + first, last = at[0][0] - int(0.003 * sd.SR), at[-1][0] + assert (g[: max(0, first)] == 1).all() + assert (np.diff(g[min(sd.N, max(0, last)):]) >= 0).all() + assert (g[min(sd.N, max(0, last + int(10 * release * sd.SR))):] == 1).all() + + def test_heavy_cues_land_on_downbeats(score): """At 120 BPM in 4/4 a bar is 2 s, so the hit, the drop and the logo sit on even seconds (a hit on beat 4 made the bar sound like 3/4); the riser and swell end where they resolve.""" @@ -160,16 +258,15 @@ def test_mastered_stems_are_the_score(score): def test_sweeps_sit_under_the_music_in_the_mastered_score(score): """The same margin through the master's shared gain envelope, bounded on both sides so a - muted or halved sweep fails as surely as a loud one. Where the limiter works inside a - sweep's window it trims the margin: the swell shares its window with a kick and clap at - 25.5 s and lands near 4.6 LU; every other sweep stays within half an LU of 6.""" + muted or halved sweep fails as surely as a loud one. Only limiting inside a sweep's window + could move it, and with the drums shaped at the source there is none worth the name: the + swell, which shares its window with a kick and clap at 25.5 s, used to land near 4.6 LU.""" events, _, report, stems = score kinds = [e["type"] for e in events if e["type"] in SWEEPS] for r, kind in zip(report, kinds): i, w = r["window_start"], r["window_len"] under = _loudness(stems["bed"], i, w) - _loudness(stems["sweeps"], i, w) - floor = 4.3 if kind == "swell" else sd.SWEEP_UNDER - 0.5 - assert floor <= under <= sd.SWEEP_UNDER + 0.3, (r["t"], kind, under) + assert abs(under - sd.SWEEP_UNDER) <= 0.3, (r["t"], kind, under) @settings(max_examples=15, deadline=None) diff --git a/tools/soundtrack.py b/tools/soundtrack.py index af3635f..fdacd92 100644 --- a/tools/soundtrack.py +++ b/tools/soundtrack.py @@ -25,7 +25,7 @@ from pathlib import Path import numpy as np -from scipy.signal import butter, fftconvolve, lfilter, sosfilt +from scipy.signal import butter, fftconvolve, lfilter, resample_poly, sosfilt SR = 48000 DUR = 30.0 @@ -91,6 +91,23 @@ def tri(freq, n): return 2 * np.abs(2 * ph - 1) - 1 +def soft_clip(x, ceil, knee): + """x with its peaks past knee rounded off toward ceil: a tanh shoulder that leaves the knee + at slope 1. The peaks are found as the master will see them, through its 30 Hz high-pass + (which lifts a kick's first trough about 1.6 dB past its dry peak) and at 4x, so intersample + peaks count and the curve's new harmonics are filtered off on the way back down instead of + folding into the audio. Only the excess is taken off x, so away from the peaks x is + untouched, bit for bit. That excess comes off the dry signal, which the master's high-pass + then reshapes, so a peak lands near ceil rather than under it: a lone kick about 1 dB past.""" + up = resample_poly(hp(x, 30), 4, 1, axis=-1) + a = np.abs(up) + over = a > knee + w = ceil - knee + cut = np.zeros_like(up) + cut[over] = up[over] - np.sign(up[over]) * (knee + w * np.tanh((a[over] - knee) / w)) + return x - resample_poly(cut, 1, 4, axis=-1) + + def place(buf, sig, t, gain=1.0, pan=0.0): """Mix a mono signal into the stereo buffer at time t with equal-power pan.""" i = int(round(t * SR)) @@ -195,6 +212,53 @@ def level_sweeps(bed, sweeps, report=None): return out +# ------------------------------------------------------------------ headroom +# The master normalizes to -14 LUFS (about +6.5 dB for this score) and then limits at +# -2 dBTP, so its ceiling sits near 0.37 on the mix before it. A kick and clap landing together +# went 4.3-6.5 dB past that on every backbeat, and the hit and drop 6.4-7.6 dB: the limiter +# caught them, and the whole mix (pad, arp, bass, cues) dipped with it, by over 1 dB for about +# 200 ms after each clap and most of a second after each impact. Those peaks are now shaped +# where they are made, at the levels the limiter used to let through, so the balance heard +# before stays and the limiter is left with stray peaks. +# the drum bus (into the mix at 0.92) soft-clips 3 dB under the master's ceiling (a kick comes +# through about 1 dB past that: see soft_clip), leaving room for the music under it; the curve +# spans the top 3 dB +DRUM_CEIL = 0.28 +DRUM_KNEE = DRUM_CEIL / np.sqrt(2) +BACKBEAT = 2 / 3 # kick and clap together: the limiter held their first 100 ms about 3.6 dB down +# The room an impact takes: as it lands, it and the music drop to IMPACT[kind] and come back +# over IMPACT_RELEASE, much as the limiter made them do (over 1 dB down for 0.66-0.75 s), but +# smoothly, where the limiter re-triggered on every cycle of the boom and rippled the whole mix +# 2-3.3 dB. The drums come back over DRUM_RELEASE instead: it is their hits under the ringing +# boom that would still peak. The cues riding over it keep their level. +IMPACT = {"hit": 0.55, "drop": 0.5} +IMPACT_RELEASE = 0.6 +DRUM_RELEASE = 1.5 +# the land's thump and ping went 1.9 dB past the ceiling on their own, and the limiter held +# them 2 dB down for the 100 ms they last: so does this +LAND = 0.8 + + +def impact_gain(events, release): + """The gain the impacts in events leave the rest of the score: down to IMPACT[kind] over the + 3 ms before each one lands (the master's lookahead, so no gain steps under the attack), then + back toward 1 with time constant release, ending after ten of them (a step under 1e-4); + where two overlap, the lower one.""" + g = np.ones(N) + ramp = int(0.003 * SR) + for e in events: + if e["type"] not in IMPACT: + continue + i = int(round(e["t"] * SR)) + depth = 1 - IMPACT[e["type"]] + env = 1 - depth * np.concatenate([np.linspace(0, 1, ramp, endpoint=False), + exp_decay(int(10 * release * SR), release)]) + a, b = max(0, i - ramp), min(N, i - ramp + len(env)) + if b > a: + g[a:b] = np.minimum(g[a:b], env[a - (i - ramp): b - (i - ramp)]) + return g + + # ------------------------------------------------------------------ harmony # D minor, four chords per 8-second cycle: Dm - Bb - F - C (i - VI - III - VII) CHORDS = [ @@ -299,6 +363,7 @@ def build(events, report=None, stems=None): mus = np.zeros((2, N)) # music bus (gets ducked by the kick) drm = np.zeros((2, N)) # drums sfx = np.zeros((2, N)) # effects + imp = np.zeros((2, N)) # the hit and drop, which take their room below sweeps = [] # noise sweeps, levelled against the finished bed (see level_sweeps) def sweep_buf(): @@ -368,6 +433,7 @@ def section(t): # drums kick_s, hat_c, hat_o, clap_s = kick(), hat(), hat(True), clap() + impacts = [e["t"] for e in events if e["type"] in IMPACT] for k in range(int(DUR / BEAT)): t = k * BEAT sec = section(t) @@ -385,11 +451,16 @@ def section(t): continue # drop out under the riser if sec == 4 and t < 24 and beat_in_bar in (1, 3): continue # half-time under the decile chart - place(drm, kick_s, t, 0.6) - duck_at(duck, t, 0.35) ripple = 15.0 <= t < 17.0 - if beat_in_bar in (1, 3) and not ripple: - place(drm, clap_s, t, 0.55 if sec > 2 else 0.4) + backbeat = beat_in_bar in (1, 3) and not ripple + # the kick steps back under the clap, and under the ripple of plinks, which already + # rests the clap and open hat and thins the arp; where a hit or drop lands, its boom + # is the kick + if not any(abs(t - ti) < 0.03 for ti in impacts): + place(drm, kick_s, t, 0.6 * (BACKBEAT if backbeat else 0.75 if ripple else 1.0)) + duck_at(duck, t, 0.35) + if backbeat: + place(drm, clap_s, t, (0.55 if sec > 2 else 0.4) * BACKBEAT) if not ripple: place(drm, hat_o, t + BEAT / 2, 0.35, 0.2) for s in (1, 3): @@ -426,9 +497,9 @@ def section(t): elif kind == "land": n = int(0.5 * SR) thump = np.tanh(2.5 * np.sin(2 * np.pi * 110 * t_axis(n)) * exp_decay(n, 0.08)) * 0.6 - place(sfx, thump, t, 0.9) + place(sfx, thump, t, 0.9 * LAND) k = int(0.04 * SR) - place(sfx, np.sin(2 * np.pi * 1320 * t_axis(k)) * exp_decay(k, 0.008) * 0.5, t, 1.0) + place(sfx, np.sin(2 * np.pi * 1320 * t_axis(k)) * exp_decay(k, 0.008) * 0.5, t, LAND) elif kind == "key": n = int(0.03 * SR) lo, hi_ = (2500, 6000) if e["i"] % 2 == 0 else (4000, 9000) @@ -475,16 +546,16 @@ def section(t): elif kind == "hit": n = int(2.5 * SR) boom = np.sin(2 * np.pi * np.cumsum(38 + 60 * exp_decay(n, 0.05)) / SR) * exp_decay(n, 0.5) - place(sfx, boom * 0.5, t, 1.0) + place(imp, boom * 0.5, t, 1.0) for m in CHORDS[2]: - place(sfx, pad_voice(m + 12, n) * exp_decay(n, 0.7) * 0.5, t, 0.5) - place(sfx, hp(rng.standard_normal(n), 5000) * exp_decay(n, 0.6) * 0.25, t, 1.0) + place(imp, pad_voice(m + 12, n) * exp_decay(n, 0.7) * 0.5, t, 0.5) + place(imp, hp(rng.standard_normal(n), 5000) * exp_decay(n, 0.6) * 0.25, t, 1.0) elif kind == "drop": n = int(3.0 * SR) boom = np.sin(2 * np.pi * np.cumsum(30 + 90 * exp_decay(n, 0.08)) / SR) * exp_decay(n, 0.9) - place(sfx, np.tanh(2 * boom) * 0.35, t, 1.0) - place(sfx, hp(rng.standard_normal(n), 4000) * exp_decay(n, 0.9) * 0.22, t, 1.0, -0.3) - place(sfx, hp(rng.standard_normal(n), 4000) * exp_decay(n, 0.9) * 0.22, t, 1.0, 0.3) + place(imp, np.tanh(2 * boom) * 0.35, t, 1.0) + place(imp, hp(rng.standard_normal(n), 4000) * exp_decay(n, 0.9) * 0.22, t, 1.0, -0.3) + place(imp, hp(rng.standard_normal(n), 4000) * exp_decay(n, 0.9) * 0.22, t, 1.0, 0.3) elif kind == "plink": nlit, step = e["n"], e.get("step", 1) # G5 -> D6 -> A6: one child's worth, two, three or more; fits both Dm and C @@ -512,8 +583,13 @@ def section(t): v = pad_voice(m, n) * exp_decay(n, 1.0) place(sfx, lp(v, 3000), t, 0.16, rng.uniform(-0.5, 0.5)) - # sidechain duck on the music bus - mus *= duck[None, :] + # an impact takes its room: it, the music and the drums step back as it lands (the cues + # riding over it don't) + room = impact_gain(events, IMPACT_RELEASE) + sfx += imp * room[None, :] + drm *= impact_gain(events, DRUM_RELEASE)[None, :] + # sidechain duck on the music bus: the kick's, or the impact's room where that is deeper + mus *= np.minimum(duck, room)[None, :] # the outro: music fades under the logo resolve fade = np.ones(N) a, b = int(26.0 * SR), int(29.6 * SR) @@ -527,6 +603,9 @@ def section(t): tail[b2:] = 0 sfx *= tail[None, :] + # a clap on a kick's swing is rounded off here, where the limiter used to take the whole mix + # down for it + drm = soft_clip(drm, DRUM_CEIL, DRUM_KNEE) bed = verb(mus, 0.28) + verb(drm, 0.08) + verb(sfx, 0.32) air = level_sweeps(bed, sweeps, report) mix = bed + air From fabb63668e63ae2f6c8823ae55802bf30ee535ab Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 28 Sep 2026 08:35:04 -0400 Subject: [PATCH 2/3] Address review: hats go round the drum clip; pin the tests; correct the numbers Review of the first commit by an Opus 5.5 and a GPT-6 Astra reviewer: - The open hat's right channel reached the clip's knee (a -41 dB change, one channel only). The hats now go round the clip; only kick and clap are shaped. Limiter, balance and cue numbers are unchanged. - impact_gain's test now compares against a piecewise reference written out independently (integer sample offsets), so a missing 3 ms ramp, a wrong depth or an early cut-off fails; it also bounds the steepest step. - A new test puts one loud burst in quiet noise and checks soft_clip leaves every sample outside the resampling filter's reach untouched. - The drum-clip test draws its kick and clap from a fixed-seed generator swapped in under try/finally, and says so, instead of claiming the score's own hits. - Comments and README say what was measured: the whole mix, not the drum bus alone, went 4.3-6.5 dB past the ceiling; the land went 1.3 dB past, not 1.9; the score's kick is lifted 1.6 dB by the high-pass; the music stays over 1 dB down for 0.85-0.9 s after an impact and the drums for 2.1-2.3 s; true peak is <= -2 dBTP as the master's 4x meter reads it (about -1.8 at 16x, as before); the cue bound covers the felt-mallet cues; the master's stale "glue + soft clip" comment is corrected (its code is unchanged). Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +-- tests/test_soundtrack.py | 92 +++++++++++++++++++++++++++------------- tools/soundtrack.py | 59 ++++++++++++++------------ 3 files changed, 99 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index c6a4ce8..a1b1d72 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ These hold for every input, and `bun run test` plus CI check them (`tests/`, `.g | Rebuilding `video.json` from `data/` is a no-op | round-trip | CI | | Easings map 0→0 and 1→1 and (except the intended `outBack` overshoot) stay in [0, 1] and never decrease; progress and envelopes stay in [0, 1]; the seeded PRNG is deterministic and in [0, 1); number formatting round-trips | property-based (fast-check) | `util.test.js` | | A fresh page and a page that played the film up to *t* draw identical pixels, in both layouts | determinism | `tools/determinism.mjs`, CI | -| Panning keeps power; synthesis is bit-identical across runs; the master is −14 ± 0.5 LUFS with true peak ≤ −2 dBTP and clean edges | property-based (Hypothesis), mastering | `test_soundtrack.py` | -| The limiter only catches stray peaks: no 10 ms window cut by more than 3 dB and at most 1% by more than 1 dB, and no cue starts in a window cut by more than 1 dB; every swoosh sits 6 ± 0.3 LU under the music after the master | bounds, measured on the returned audio | `test_soundtrack.py` | -| The drum soft clip returns its input bit for bit below the knee; an impact's room is exactly its depth where it lands, only recovers after the last one, and is exactly 1 elsewhere | property-based (Hypothesis) | `test_soundtrack.py` | +| Panning keeps power; synthesis is bit-identical across runs; the master is −14 ± 0.5 LUFS with true peak ≤ −2 dBTP as its 4× meter reads it (about −1.8 at 16×) and clean edges | property-based (Hypothesis), mastering | `test_soundtrack.py` | +| The limiter only catches stray peaks: no 10 ms window cut by more than 3 dB and at most 1% by more than 1 dB, and no felt-mallet cue (lock, ping, statistic, plink, decile) starts in a window cut by more than 1 dB; every swoosh sits 6 ± 0.3 LU under the music after the master | bounds, measured on the returned audio | `test_soundtrack.py` | +| The drum soft clip returns a signal that never reaches its knee bit for bit, and touches nothing further than the resampling filter's reach from a peak; an impact's room equals an independent piecewise reference (3 ms ramp, exact depth where it lands, exponential return, exactly 1 after ten release times) | property-based (Hypothesis), differential | `test_soundtrack.py` | ## How it's made diff --git a/tests/test_soundtrack.py b/tests/test_soundtrack.py index 3f7cb82..ab97627 100644 --- a/tests/test_soundtrack.py +++ b/tests/test_soundtrack.py @@ -63,7 +63,9 @@ def test_mastering_hits_platform_targets(score): _, mix, _, _ = score lufs = pyln.Meter(sd.SR).integrated_loudness(mix.T) true_peak = 20 * np.log10(np.abs(resample_poly(mix, 4, 1, axis=1)).max()) - # the limiter's ceiling is -2 dBTP; 1e-3 dB allows for the 4x resampling estimate + # the limiter's ceiling is -2 dBTP as its own 4x meter reads it (1e-3 dB for this estimate's + # rounding); read at 16x the score peaks about 0.16 dB higher, as it did before the drums + # were shaped at the source assert abs(lufs + 14) < 0.5 and true_peak <= -2.0 + 1e-3 # clean edges: the 30 ms fades start and end at exactly zero, and no sample inside them # exceeds the limiter ceiling (-2 dBTP) times the fade ramp @@ -100,9 +102,11 @@ def test_the_limiter_only_catches_stray_peaks(score): def test_no_cue_lands_in_a_limiter_dip(score): - """The cues that carry the story keep the level they were written at: none starts in a - window the limiter cut by more than 1 dB. The third statistic used to land on a backbeat - and lose 6 dB, where its two siblings lost under 2.""" + """The felt-mallet cues that carry the story (the lock, pings, statistics, plinks and decile + notes) keep the level they were written at: none starts in a window the limiter cut by more + than 1 dB. The third statistic used to land on a backbeat and lose 6 dB, where its two + siblings lost under 2. (The typing clicks are not held to this: one at 7.03 s, on a kick, + still loses about 1.1 dB, as it did before.)""" events, mix, _, stems = score at, cut = _limiter_cut(mix, stems) cues = [e for e in events if e["type"] in ("lock", "ping", "stat", "plink", "decile")] @@ -112,7 +116,7 @@ def test_no_cue_lands_in_a_limiter_dip(score): assert cut[first].max() <= 1.0, (e, cut[first].max()) -@settings(max_examples=40) +@settings(max_examples=40, deadline=None) @given(st.integers(0, 2**32 - 1), st.floats(0.05, 0.99), st.floats(40, 4000)) def test_soft_clip_leaves_what_stays_under_the_knee_alone(seed, level, lo): """Wherever nothing reaches the knee, as the master will see it (through its high-pass, at @@ -122,19 +126,40 @@ def test_soft_clip_leaves_what_stays_under_the_knee_alone(seed, level, lo): assert np.array_equal(sd.soft_clip(x, sd.DRUM_CEIL, sd.DRUM_KNEE), x) +@settings(max_examples=25, deadline=None) +@given(st.integers(0, 2**32 - 1), st.floats(1.5, 8.0), st.floats(0.05, 0.3)) +def test_soft_clip_only_touches_its_peaks(seed, over, at): + """A single loud 10 ms burst in quiet noise: the burst is clipped, and every sample more + than the resampling filter's reach (10 samples) before it, or after the high-passed burst + has fallen back under the knee, comes back bit for bit.""" + g = np.random.default_rng(seed) + n, i, w = sd.SR // 2, int(at * sd.SR), int(0.01 * sd.SR) + x = sd.bp(g.standard_normal((2, n)), 200, 8000) * 0.01 + x[:, i: i + w] += sd.bp(g.standard_normal((2, w)), 900, 5000) + x[:, i: i + w] *= over * sd.DRUM_CEIL / np.abs(x[:, i: i + w]).max() + y = sd.soft_clip(x, sd.DRUM_CEIL, sd.DRUM_KNEE) + loud = np.flatnonzero((np.abs(resample_poly(sd.hp(x, 30), 4, 1, axis=-1)) > sd.DRUM_KNEE).any(0)) // 4 + assert not np.array_equal(y[:, i: i + w], x[:, i: i + w]) + assert np.array_equal(y[:, : loud[0] - 10], x[:, : loud[0] - 10]) + assert np.array_equal(y[:, loud[-1] + 11:], x[:, loud[-1] + 11:]) + + @pytest.mark.parametrize("parts", [ [("kick", 0.6)], # a kick on its own [("kick", 0.6 * sd.BACKBEAT), ("clap", 0.4 * sd.BACKBEAT)], # backbeats, 8.5-14 s [("kick", 0.6 * sd.BACKBEAT), ("clap", 0.55 * sd.BACKBEAT)], # backbeats, 14-26 s ]) def test_the_drums_come_through_soft_clip_near_its_ceiling(parts): - """The score's own drum hits, at the gains it plays them, cross the knee and come out with - their peak (as the master sees it) pulled back to within 1.5 dB of DRUM_CEIL. It is not - exact: the excess comes off the dry signal, which the master's high-pass then reshapes, so a - lone kick lands about 1.1 dB past the ceiling.""" - state = sd.rng.bit_generator.state # the score draws from the same stream - sounds = {"kick": sd.kick(), "clap": sd.clap()} - sd.rng.bit_generator.state = state + """A kick and clap synthesized as the score's are (their noise from a fixed seed, not the + score's own draws), at the gains it plays them, cross the knee and come out with their peak + (as the master sees it) pulled back to within 1.5 dB of DRUM_CEIL. It is not exact: the + excess comes off the dry signal, which the master's high-pass then reshapes, so a lone kick + lands about 1.1 dB past the ceiling.""" + score_rng, sd.rng = sd.rng, np.random.default_rng(7) # the score draws from the module's stream + try: + sounds = {"kick": sd.kick(), "clap": sd.clap()} + finally: + sd.rng = score_rng x = np.zeros((2, sd.SR // 2)) for name, g in parts: sd.place(x, sounds[name], 0.05, g) @@ -146,28 +171,37 @@ def peak(y): assert peak(y) <= sd.DRUM_CEIL * 10 ** (1.5 / 20), 20 * np.log10(peak(y) / sd.DRUM_CEIL) -@settings(max_examples=60) +def _room(t, depth, release): + """One impact's room, written out piece by piece from its definition, as a reference: 1 up + to 3 ms before it lands, a straight line down to 1 - depth where it lands, then back + exponentially, and 1 again from ten release times on.""" + k = np.arange(sd.N) - int(round(t * sd.SR)) # samples from the landing, kept integer so + ramp = int(0.003 * sd.SR) # every boundary falls on the same sample + g = np.ones(sd.N) + down = (k >= -ramp) & (k < 0) + g[down] = 1 - depth * (k[down] + ramp) / ramp + back = (k >= 0) & (k < int(10 * release * sd.SR)) + g[back] = 1 - depth * np.exp(-k[back] / sd.SR / release) + return g + + +@settings(max_examples=60, deadline=None) @given(st.lists(st.tuples(st.sampled_from(["hit", "drop", "plink"]), st.floats(-2.0, 32.0)), max_size=4), st.sampled_from([sd.IMPACT_RELEASE, sd.DRUM_RELEASE, 0.05])) def test_impacts_take_their_room_and_give_it_back(cues, release): - """impact_gain: all ones without an impact; exactly IMPACT[kind] where one lands (or lower, - where another's room overlaps); never under the deepest IMPACT or over 1; untouched before - the first one's 3 ms ramp; only recovering after the last one lands; and back to exactly 1 - ten release times after it.""" + """Differential: impact_gain equals the pointwise minimum of each impact's room written out + independently (_room), so its 3 ms ramp, its exact depth where each impact lands, its + recovery and its return to exactly 1 are all pinned; other cues leave it all ones. And its + steepest step is the ramp's, so it never jumps under an attack.""" events = [{"type": kind, "t": t} for kind, t in cues] g = sd.impact_gain(events, release) - assert g.shape == (sd.N,) and (g <= 1).all() and (g >= min(sd.IMPACT.values())).all() - at = sorted((int(round(t * sd.SR)), kind) for kind, t in cues if kind in sd.IMPACT) - if not at: - assert (g == 1).all() - return - for i, kind in at: - if 0 <= i < sd.N: - assert g[i] <= sd.IMPACT[kind] - first, last = at[0][0] - int(0.003 * sd.SR), at[-1][0] - assert (g[: max(0, first)] == 1).all() - assert (np.diff(g[min(sd.N, max(0, last)):]) >= 0).all() - assert (g[min(sd.N, max(0, last + int(10 * release * sd.SR))):] == 1).all() + ref = np.ones(sd.N) + for kind, t in cues: + if kind in sd.IMPACT: + ref = np.minimum(ref, _room(t, 1 - sd.IMPACT[kind], release)) + assert np.abs(g - ref).max() < 1e-9 + steepest = max(1 - v for v in sd.IMPACT.values()) / int(0.003 * sd.SR) + assert np.abs(np.diff(g)).max() <= steepest + 1e-9 def test_heavy_cues_land_on_downbeats(score): diff --git a/tools/soundtrack.py b/tools/soundtrack.py index fdacd92..fce6eb2 100644 --- a/tools/soundtrack.py +++ b/tools/soundtrack.py @@ -94,11 +94,13 @@ def tri(freq, n): def soft_clip(x, ceil, knee): """x with its peaks past knee rounded off toward ceil: a tanh shoulder that leaves the knee at slope 1. The peaks are found as the master will see them, through its 30 Hz high-pass - (which lifts a kick's first trough about 1.6 dB past its dry peak) and at 4x, so intersample - peaks count and the curve's new harmonics are filtered off on the way back down instead of - folding into the audio. Only the excess is taken off x, so away from the peaks x is - untouched, bit for bit. That excess comes off the dry signal, which the master's high-pass - then reshapes, so a peak lands near ceil rather than under it: a lone kick about 1 dB past.""" + (which lifts the score's kick 1.6 dB past its dry peak) and at 4x, so intersample + peaks count and most of the curve's new harmonics are filtered off on the way back down, not + folding into the audio (4x reduces aliasing; it does not remove it). Only the excess is + taken off x: a signal that never reaches the knee comes back bit for bit, and so does every + sample further from a peak than the resampling filter reaches (10 samples). The excess comes + off the dry signal, which the master's high-pass then reshapes, so a peak lands near ceil + rather than under it: a lone kick about 1 dB past.""" up = resample_poly(hp(x, 30), 4, 1, axis=-1) a = np.abs(up) over = a > knee @@ -214,12 +216,13 @@ def level_sweeps(bed, sweeps, report=None): # ------------------------------------------------------------------ headroom # The master normalizes to -14 LUFS (about +6.5 dB for this score) and then limits at -# -2 dBTP, so its ceiling sits near 0.37 on the mix before it. A kick and clap landing together -# went 4.3-6.5 dB past that on every backbeat, and the hit and drop 6.4-7.6 dB: the limiter -# caught them, and the whole mix (pad, arp, bass, cues) dipped with it, by over 1 dB for about -# 200 ms after each clap and most of a second after each impact. Those peaks are now shaped -# where they are made, at the levels the limiter used to let through, so the balance heard -# before stays and the limiter is left with stray peaks. +# -2 dBTP, so its ceiling sits near 0.37 on the mix before it. Where a kick and clap landed +# together the mix went 4.3-6.5 dB past that, on every backbeat, and 6.4-7.6 dB where the hit +# and drop landed on a kick: the limiter caught them, and the whole mix (pad, arp, bass, cues) +# dipped with it, by over 1 dB for about 200 ms after each clap and for 0.66-0.75 s, in +# bursts, after each impact. Those peaks are now shaped where they are made, at the levels the +# limiter used to let through, so the balance heard before stays and the limiter is left with +# stray peaks. # the drum bus (into the mix at 0.92) soft-clips 3 dB under the master's ceiling (a kick comes # through about 1 dB past that: see soft_clip), leaving room for the music under it; the curve # spans the top 3 dB @@ -227,15 +230,15 @@ def level_sweeps(bed, sweeps, report=None): DRUM_KNEE = DRUM_CEIL / np.sqrt(2) BACKBEAT = 2 / 3 # kick and clap together: the limiter held their first 100 ms about 3.6 dB down # The room an impact takes: as it lands, it and the music drop to IMPACT[kind] and come back -# over IMPACT_RELEASE, much as the limiter made them do (over 1 dB down for 0.66-0.75 s), but -# smoothly, where the limiter re-triggered on every cycle of the boom and rippled the whole mix -# 2-3.3 dB. The drums come back over DRUM_RELEASE instead: it is their hits under the ringing -# boom that would still peak. The cues riding over it keep their level. +# with time constant IMPACT_RELEASE (over 1 dB down for 0.85-0.9 s), smoothly, where the +# limiter re-triggered on every cycle of the boom and rippled the whole mix 2-3.3 dB. The drums +# come back with DRUM_RELEASE instead (over 1 dB down for 2.1-2.3 s): it is their hits under +# the ringing boom that would still peak. The cues riding over it keep their level. IMPACT = {"hit": 0.55, "drop": 0.5} IMPACT_RELEASE = 0.6 DRUM_RELEASE = 1.5 -# the land's thump and ping went 1.9 dB past the ceiling on their own, and the limiter held -# them 2 dB down for the 100 ms they last: so does this +# the land's thump and ping went 1.3 dB past the ceiling on their own, and the limiter cut up +# to 2 dB there (over 1 dB for about 100 ms); they now play 1.9 dB down instead LAND = 0.8 @@ -361,7 +364,8 @@ def build(events, report=None, stems=None): master ("bed_pre", "sweeps_pre") and after it ("bed", "sweeps"). The master is a filter and one gain envelope shared by both, so the mastered stems sum to the score.""" mus = np.zeros((2, N)) # music bus (gets ducked by the kick) - drm = np.zeros((2, N)) # drums + drm = np.zeros((2, N)) # kick and clap (soft-clipped below) + hats = np.zeros((2, N)) # hats, which the clip leaves alone sfx = np.zeros((2, N)) # effects imp = np.zeros((2, N)) # the hit and drop, which take their room below sweeps = [] # noise sweeps, levelled against the finished bed (see level_sweeps) @@ -442,7 +446,7 @@ def section(t): if beat_in_bar in (0, 2): place(drm, kick_s, t, 0.5) duck_at(duck, t, 0.45) - place(drm, hat_c, t + BEAT / 2, 0.22, 0.25) + place(hats, hat_c, t + BEAT / 2, 0.22, 0.25) if beat_in_bar == 0 and 7.9 < t < 8.1: for s16 in range(4): place(drm, clap_s, t + s16 * BEAT / 4, 0.12 + 0.08 * s16) @@ -462,13 +466,13 @@ def section(t): if backbeat: place(drm, clap_s, t, (0.55 if sec > 2 else 0.4) * BACKBEAT) if not ripple: - place(drm, hat_o, t + BEAT / 2, 0.35, 0.2) + place(hats, hat_o, t + BEAT / 2, 0.35, 0.2) for s in (1, 3): - place(drm, hat_c, t + s * BEAT / 4, 0.25, -0.2) + place(hats, hat_c, t + s * BEAT / 4, 0.25, -0.2) # intro hats: quiet ticking sixteenths like a clock under the quote for k in range(int(4.0 / (BEAT / 4))): t = k * BEAT / 4 - place(drm, hat_c, t, 0.16 + 0.08 * (k % 4 == 0), 0.3 if k % 2 else -0.3) + place(hats, hat_c, t, 0.16 + 0.08 * (k % 4 == 0), 0.3 if k % 2 else -0.3) # ---------------------------------------------------------- effects for e in events: @@ -587,7 +591,9 @@ def section(t): # riding over it don't) room = impact_gain(events, IMPACT_RELEASE) sfx += imp * room[None, :] - drm *= impact_gain(events, DRUM_RELEASE)[None, :] + drums_room = impact_gain(events, DRUM_RELEASE)[None, :] + drm *= drums_room + hats *= drums_room # sidechain duck on the music bus: the kick's, or the impact's room where that is deeper mus *= np.minimum(duck, room)[None, :] # the outro: music fades under the logo resolve @@ -597,6 +603,7 @@ def section(t): fade[b:] = 0 mus *= fade[None, :] drm *= fade[None, :] + hats *= fade[None, :] tail = np.ones(N) a2, b2 = int(28.0 * SR), int(29.8 * SR) tail[a2:b2] = np.linspace(1, 0, b2 - a2) ** 2 @@ -604,12 +611,12 @@ def section(t): sfx *= tail[None, :] # a clap on a kick's swing is rounded off here, where the limiter used to take the whole mix - # down for it - drm = soft_clip(drm, DRUM_CEIL, DRUM_KNEE) + # down for it; the hats, panned and never the problem, go round the clip + drm = soft_clip(drm, DRUM_CEIL, DRUM_KNEE) + hats bed = verb(mus, 0.28) + verb(drm, 0.08) + verb(sfx, 0.32) air = level_sweeps(bed, sweeps, report) mix = bed + air - # gentle master: glue + soft clip, then normalize loudness to ~-14 LUFS (RMS proxy) + # the master: normalize to -14 LUFS integrated (pyloudnorm), then a true-peak limiter at -2 dBTP mix = hp(mix, 30) env = np.ones(N) # the master's gain envelope, tracked for the stems import pyloudnorm as pyln From ae3a45fb968e6eab365050a6b7b7e7c745101a30 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 28 Sep 2026 09:10:33 -0400 Subject: [PATCH 3/3] Name the cue test for the felt-mallet cues it checks; fix two stale figures in test docstrings Co-Authored-By: Claude Opus 5.5 --- tests/test_soundtrack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_soundtrack.py b/tests/test_soundtrack.py index ab97627..6f06fa7 100644 --- a/tests/test_soundtrack.py +++ b/tests/test_soundtrack.py @@ -101,12 +101,12 @@ def test_the_limiter_only_catches_stray_peaks(score): assert cut.max() <= 3.0 and (cut > 1.0).mean() <= 0.01, (cut.max(), (cut > 1.0).mean()) -def test_no_cue_lands_in_a_limiter_dip(score): +def test_no_felt_mallet_cue_lands_in_a_limiter_dip(score): """The felt-mallet cues that carry the story (the lock, pings, statistics, plinks and decile notes) keep the level they were written at: none starts in a window the limiter cut by more than 1 dB. The third statistic used to land on a backbeat and lose 6 dB, where its two siblings lost under 2. (The typing clicks are not held to this: one at 7.03 s, on a kick, - still loses about 1.1 dB, as it did before.)""" + still loses about 1.2 dB, as it did before.)""" events, mix, _, stems = score at, cut = _limiter_cut(mix, stems) cues = [e for e in events if e["type"] in ("lock", "ping", "stat", "plink", "decile")] @@ -294,7 +294,7 @@ def test_sweeps_sit_under_the_music_in_the_mastered_score(score): """The same margin through the master's shared gain envelope, bounded on both sides so a muted or halved sweep fails as surely as a loud one. Only limiting inside a sweep's window could move it, and with the drums shaped at the source there is none worth the name: the - swell, which shares its window with a kick and clap at 25.5 s, used to land near 4.6 LU.""" + swell, which shares its window with a kick and clap at 25.5 s, used to land near 4.8 LU.""" events, _, report, stems = score kinds = [e["type"] for e in events if e["type"] in SWEEPS] for r, kind in zip(report, kinds):