Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

- `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.

Expand Down
143 changes: 137 additions & 6 deletions tests/test_soundtrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,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
Expand All @@ -72,6 +75,135 @@ 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_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.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")]
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, 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
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)


@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):
"""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)

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)


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):
"""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)
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):
"""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."""
Expand Down Expand Up @@ -160,16 +292,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.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):
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)
Expand Down
Loading
Loading