PROCITY/pipeline/gen_audio.py
m3ultra 1568c9b62b Lane E R38 (v8 wave 1): the asset wave — trellis2_mlx swap verified 1.92x, six sounds, four grounds, and the thin-structure answer
A. PIPELINE FIX: gen_props.py trellis_mac -> trellis2_mlx, contract read from its
   manifest.json. The two operators are NOT drop-in on output — trellis2_mlx writes
   BOTH candidate.glb (raw) and candidate_pbr.glb (baked), so the old
   sorted(rglob('*.glb'))[0] would have silently shipped untextured meshes. Added
   _pick_glb() (result.json first) + the bg_remove_local cutout stage, fail-soft.
   MEASURED: median 128.0s vs trellis_mac's 245.4s p50 = 1.92x. 5/5 first attempt,
   0 retries against the 4-5% budget. One 489s tail outlier, no error — named.

B. AUDIO (1.1) — six PACK entries, +0.86MB, ZERO draws/tris, all originalby
   construction: dog-bark-fence, mower-distant, radio-through-window, sprinkler,
   roller-door, magpie-carol. Seeded via crc32(name); beds fold seamlessly
   (wrap-jump <=0.18x internal step). Verified by spectrogram + band-energy, and
   one defect found by LOOKING: the sprinkler's spray masked its own ticks and read
   as a hose — rebalanced and re-rendered. Distance is spent in the SPECTRUM, not
   the level, because encode()'s loudnorm undoes a quiet render.

C. GROUNDS (1.2) — gen_skins.py had NO ground template; added one with its own
   outdoor style prefix (the house STYLE says "warm fluorescent", an interior cue).
   4 new 512^2 square tiles. Review's 5.7s is 9.7s here — same per-pixel, mine are
   1.78x the pixels. Found: gravel/reddust are selected by nothing partly because
   their `use` values name no slot ground.js:177 has.

D. MESHES — 3 shipped (magpie 894 tris, paling_fence 1121, water_tank 500), all
   GLB-law clean. 2 FAILED and were DELETED: hills_hoist + tv_aerial. Thin wire
   returns as thousands of disconnected fragments, so collapse floors at 250k/89k
   AND a remesh coarse enough for 600 tris erases the pole. Lane B's boxMats
   primitives cost +0 draws and are strictly better. The magpie's geometry passes
   but TRELLIS crushed its white nape/wing to black — traced to the operator's bake
   (the cutout and our decimation are both exonerated); flagged, not hidden.

E. REUSE — all 9 idle assets re-measured; the review's "one line each" holds for
   only 5. furniture.js's extractGLB keeps only the FIRST material, so park_bench/
   coffee_table/box_crate render in material #1 and bench_wood (0 materials) falls
   to grey. Nothing was regenerated that already existed.

The 3 GLBs are staged, NOT published: a depot push is outward-facing and had no
in-session authorization. Held at _r38_results.PENDING_PUBLISH.json; publish +
rename + rebuild lands them. Gate green: validate_manifest 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 18:30:33 +10:00

981 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""PROCITY audio pack — 100% procedural synthesis, on-device, $0 (round-11 audio round).
TOOL CHOICE (Fable asked E to pick + document): **pure procedural synthesis in numpy**, not a
neural model. MODELBEAST has no audio operator, and for THIS pack synthesis wins on every axis:
* $0, no 215 GB model download, runs in the mflux venv (numpy only).
* Deterministic + seeded (citySeed/shopId flavour is house law) — same seed, same sound.
* Perfect **seamless loops** (we crossfade the tail into the head) — neural gens don't loop.
* **Parody-safe by construction**: every sample is an original oscillator/noise render, so there
is no way to imitate a specific real recording. Instrumental originals only.
* Precise budget control (≤25 MB): we choose length + Opus bitrate per asset.
Pipeline: synth (numpy float32) -> WAV (stdlib wave, int16) -> ffmpeg -> OGG/Opus (+ M4A/AAC).
Loudness: long beds/music -> ffmpeg loudnorm (EBU R128, target -16 LUFS integrated); short SFX ->
peak-normalized in-synth (loudnorm is unreliable under ~3 s).
<mflux-venv-python> pipeline/gen_audio.py --dry-run # list the pack + budget, no render
<mflux-venv-python> pipeline/gen_audio.py # render everything -> web/assets/audio/
<mflux-venv-python> pipeline/gen_audio.py --only music # just the music beds
<mflux-venv-python> pipeline/gen_audio.py --wav-only # keep .genaudio/*.wav, skip ffmpeg (debug)
Run with the numpy-having interpreter: ~/Documents/MODELBEAST/venvs/mflux/bin/python
House audio law: the game runs silent-and-happy with zero assets — none of this blocks anyone.
"""
import os, sys, math, wave, struct, subprocess, hashlib, json, zlib
import numpy as np
SR = 44100
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RAW = os.path.join(ROOT, "pipeline", ".genaudio") # WAV scratch (git-ignored)
OUT = os.path.join(ROOT, "web", "assets", "audio") # shipped OGG/M4A
os.makedirs(RAW, exist_ok=True)
os.makedirs(OUT, exist_ok=True)
FF = "ffmpeg"
# ─────────────────────────────────────────────────────────────────────────────
# DSP toolkit (pure numpy)
# ─────────────────────────────────────────────────────────────────────────────
def seed_of(name):
return zlib.crc32(name.encode()) & 0xFFFFFFFF
def rng(name):
return np.random.default_rng(seed_of(name))
def tarr(dur):
return np.arange(int(dur * SR)) / SR
def sine(f, dur, phase=0.0):
return np.sin(2 * np.pi * f * tarr(dur) + phase)
def _phase(f, dur):
# allow f to be scalar or array (for vibrato/glide)
n = int(dur * SR)
if np.isscalar(f):
f = np.full(n, f, dtype=np.float64)
else:
f = np.resize(f, n)
return np.cumsum(2 * np.pi * f / SR)
def osc(f, dur, kind="sine"):
ph = _phase(f, dur)
if kind == "sine":
return np.sin(ph)
if kind == "tri":
return 2 / np.pi * np.arcsin(np.sin(ph))
if kind == "saw":
return 2 * ((ph / (2 * np.pi)) % 1.0) - 1
if kind == "square":
return np.sign(np.sin(ph))
if kind == "pulse25":
return np.where((ph / (2 * np.pi)) % 1.0 < 0.25, 1.0, -1.0)
raise ValueError(kind)
def noise(dur, r=None, kind="white"):
n = int(dur * SR)
r = r or np.random.default_rng(0)
x = r.standard_normal(n)
if kind == "pink": # ~ -3 dB/oct via cheap one-pole cascade
b = [0.0, 0.0, 0.0]
out = np.empty(n)
for i in range(n):
b[0] = 0.99765 * b[0] + x[i] * 0.0990460
b[1] = 0.96300 * b[1] + x[i] * 0.2965164
b[2] = 0.57000 * b[2] + x[i] * 1.0526913
out[i] = b[0] + b[1] + b[2] + x[i] * 0.1848
return out / 4
return x
def adsr(dur, a=0.01, d=0.1, s=0.7, r=0.1, sl=None):
n = int(dur * SR)
sl = (dur - a - d - r) if sl is None else sl
sl = max(sl, 0.0)
segs = [(a, 0, 1), (d, 1, s), (sl, s, s), (r, s, 0)]
env = np.concatenate([np.linspace(x0, x1, max(int(t * SR), 1)) for t, x0, x1 in segs])
return np.resize(env, n)
def lp1(x, cutoff):
# one-pole low-pass
a = math.exp(-2 * math.pi * cutoff / SR)
y = np.empty_like(x)
acc = 0.0
for i in range(len(x)):
acc = (1 - a) * x[i] + a * acc
y[i] = acc
return y
def hp1(x, cutoff):
return x - lp1(x, cutoff)
def biquad_lp(x, cutoff, q=0.707):
# RBJ low-pass biquad
w0 = 2 * math.pi * cutoff / SR
alpha = math.sin(w0) / (2 * q)
cw = math.cos(w0)
b0 = (1 - cw) / 2; b1 = 1 - cw; b2 = (1 - cw) / 2
a0 = 1 + alpha; a1 = -2 * cw; a2 = 1 - alpha
b0, b1, b2, a1, a2 = (b0/a0, b1/a0, b2/a0, a1/a0, a2/a0)
y = np.empty_like(x); x1 = x2 = y1 = y2 = 0.0
for i in range(len(x)):
y0 = b0*x[i] + b1*x1 + b2*x2 - a1*y1 - a2*y2
x2, x1 = x1, x[i]; y2, y1 = y1, y0
y[i] = y0
return y
def bandpass(x, lo, hi):
return lp1(hp1(x, lo), hi)
def softclip(x, drive=1.0):
return np.tanh(x * drive)
def fade(x, fin=0.01, fout=0.01):
n = len(x); ai = int(fin * SR); ao = int(fout * SR)
if ai: x[:ai] *= np.linspace(0, 1, ai)
if ao: x[-ao:] *= np.linspace(1, 0, ao)
return x
def peaknorm(x, target_db=-3.0):
p = np.max(np.abs(x)) or 1.0
return x * (10 ** (target_db / 20) / p)
def seamless(x, xf=0.75):
"""Return a loop of length len(x)-xf*SR that plays end->start seamlessly, by crossfading
the tail (xf s) over the head. Input should be rendered xf longer than the wanted loop."""
k = int(xf * SR)
if k <= 0 or len(x) <= k:
return x
body = x[:-k].copy()
tail = x[-k:]
w = np.linspace(0, 1, k)
if body.ndim == 2: # stereo: broadcast window across channels
w = w[:, None]
body[:k] = body[:k] * w + tail * (1 - w) # head = head*rise + tail*fall
return body
def stereo(l, r=None):
if r is None:
r = l
n = max(len(l), len(r))
out = np.zeros((n, 2), dtype=np.float64)
out[:len(l), 0] = l; out[:len(r), 1] = r
return out
def pan(x, p): # p in [-1,1]
p = (p + 1) / 2
return stereo(x * math.cos(p * math.pi / 2), x * math.sin(p * math.pi / 2))
def mix(*layers):
n = max(len(a) for a in layers)
ch = max((a.ndim == 2 and a.shape[1] or 1) for a in layers)
out = np.zeros((n, ch) if ch == 2 else n, dtype=np.float64)
for a in layers:
if ch == 2 and a.ndim == 1:
a = stereo(a)
if ch == 2:
out[:len(a)] += a
else:
out[:len(a)] += a
return out
def schroeder_reverb(x, mix_amt=0.2, decay=0.5):
if x.ndim == 2:
return np.stack([schroeder_reverb(x[:, c], mix_amt, decay) for c in range(2)], axis=1)
out = np.zeros(len(x) + SR)
out[:len(x)] = x
for dl, g in [(1116, 0.805), (1188, 0.827), (1277, 0.783), (1356, 0.764)]:
buf = np.zeros(len(out))
gg = g * decay
for i in range(dl, len(out)):
buf[i] = out[i] + gg * buf[i - dl]
out = out + buf * 0.25
wet = out[:len(x)]
wet = wet / (np.max(np.abs(wet)) or 1.0)
return (1 - mix_amt) * x + mix_amt * wet
# ─────────────────────────────────────────────────────────────────────────────
# instruments + drums (for music beds)
# ─────────────────────────────────────────────────────────────────────────────
def midi_hz(m):
return 440.0 * 2 ** ((m - 69) / 12)
def synth_note(midi, dur, kind="saw", a=0.005, d=0.08, s=0.6, rel=0.1,
cutoff=2500, detune=0.0, vib=0.0):
f = midi_hz(midi)
if vib:
f = f * (1 + vib * 0.006 * np.sin(2 * np.pi * 5 * tarr(dur)))
x = osc(f, dur, kind)
if detune:
x = 0.5 * x + 0.5 * osc(midi_hz(midi) * (1 + detune), dur, kind)
x = x * adsr(dur, a, d, s, rel)
if cutoff < SR / 2:
x = biquad_lp(x, cutoff, 0.8)
return x
def epiano(midi, dur):
f = midi_hz(midi)
x = (np.sin(2*np.pi*f*tarr(dur)) + 0.5*np.sin(2*np.pi*2*f*tarr(dur)*1.001)
+ 0.25*np.sin(2*np.pi*3*f*tarr(dur)))
return x * adsr(dur, 0.005, 0.5, 0.25, 0.3)
def pad(midi, dur, detune=0.008):
x = (osc(midi_hz(midi), dur, "saw") + osc(midi_hz(midi)*(1+detune), dur, "saw")
+ osc(midi_hz(midi)*(1-detune), dur, "saw")) / 3
x = biquad_lp(x, 1800, 0.7)
return x * adsr(dur, 0.4, 0.2, 0.8, 0.5)
def organ(midi, dur):
# drawbar-organ-ish: stacked harmonic sines (16'/8'/5⅓'/4'/2') + a gentle Leslie tremolo and a
# soft percussive attack. Warm and mellow — the RSL covers-band voice.
f = midi_hz(midi); t = tarr(dur)
drawbars = [(0.5, 1), (1.0, 2), (0.55, 3), (0.4, 4), (0.22, 6)]
x = sum(a * np.sin(2 * np.pi * f * mult * t) for a, mult in drawbars) / 2.6
trem = 1 + 0.06 * np.sin(2 * np.pi * 5.2 * t) # gentle Leslie tremolo
return x * trem * adsr(dur, 0.012, 0.15, 0.82, 0.12)
def kick(dur=0.28):
n = int(dur * SR)
f = np.linspace(120, 45, n)
x = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-np.linspace(0, 9, n))
click = noise(0.005) * np.exp(-np.linspace(0, 40, int(0.005*SR)))
return mix(x, np.concatenate([click, np.zeros(n - len(click))]))
def snare(dur=0.2, r=None):
n = int(dur * SR)
body = np.sin(2*np.pi*190*tarr(dur)) * np.exp(-np.linspace(0, 14, n))
nz = noise(dur, r) * np.exp(-np.linspace(0, 16, n))
return 0.5*body + 0.8*bandpass(nz, 1200, 6000)
def hat(dur=0.05, r=None, open_=False):
n = int(dur * SR)
nz = bandpass(noise(dur, r), 7000, 16000)
env = np.exp(-np.linspace(0, 3 if open_ else 30, n))
return nz * env
# ─────────────────────────────────────────────────────────────────────────────
# sequencer for music beds
# ─────────────────────────────────────────────────────────────────────────────
def render_line(notes, bpm, instr, gap=0.0):
"""notes: list of (midi_or_None, beats). instr: fn(midi,dur)->array. Returns concatenated."""
spb = 60.0 / bpm
parts = []
for midi, beats in notes:
dur = beats * spb
if midi is None:
parts.append(np.zeros(int(dur * SR)))
else:
x = instr(midi, max(dur - gap, 0.05))
x = np.concatenate([x, np.zeros(int(dur * SR) - len(x))]) if len(x) < int(dur*SR) else x[:int(dur*SR)]
parts.append(x)
return np.concatenate(parts)
def render_drums(pattern, bpm, bars, r):
"""pattern: dict 'k'/'s'/'h' -> list of 16th-step indices (0..15) for one bar."""
spb = 60.0 / bpm
step = spb / 4
barlen = int(16 * step * SR)
out = np.zeros(barlen * bars)
voices = {"k": lambda: kick(), "s": lambda: snare(r=r), "h": lambda: hat(r=r),
"o": lambda: hat(r=r, open_=True)}
for b in range(bars):
for v, steps in pattern.items():
for s in steps:
pos = int((b * 16 + s) * step * SR)
smp = voices[v]()
end = min(pos + len(smp), len(out))
out[pos:end] += smp[:end - pos]
return out
# ─────────────────────────────────────────────────────────────────────────────
# AMBIENCE generators (loopable ~40s)
# ─────────────────────────────────────────────────────────────────────────────
AMB_LEN = 40.0
XF = 1.0
def amb_street_day(name):
r = rng(name)
dur = AMB_LEN + XF
wind = lp1(noise(dur, r, "pink"), 600) * 0.6
# distant traffic: slow-swelling band-passed noise
traf = bandpass(noise(dur, r), 120, 900)
swell = 0.5 + 0.5 * np.sin(2 * np.pi * 0.05 * tarr(dur) + r.random() * 6)
traf = traf * swell * 0.35
# sparse bird chirps
birds = np.zeros(int(dur * SR))
for _ in range(int(dur * 0.7)):
at = r.uniform(0, dur - 0.4)
f0 = r.uniform(2200, 4200)
cd = r.uniform(0.06, 0.16)
chirp = np.sin(2*np.pi*np.cumsum(np.linspace(f0, f0*r.uniform(0.8,1.4), int(cd*SR)))/SR)
chirp *= np.hanning(len(chirp)) * r.uniform(0.05, 0.12)
p = int(at * SR); birds[p:p+len(chirp)] += chirp
body = mix(pan(wind, -0.2), pan(traf, 0.1), pan(birds, r.uniform(-0.5, 0.5)))
return seamless(body, XF)
def amb_street_night(name):
r = rng(name)
dur = AMB_LEN + XF
hum = lp1(noise(dur, r, "pink"), 200) * 0.4 + osc(60, dur, "sine") * 0.02
# crickets: rhythmic AM high tone
base = osc(4500, dur, "sine")
trill = (np.sin(2*np.pi*22*tarr(dur)) > 0.3).astype(float)
gate = (0.5 + 0.5*np.sin(2*np.pi*0.7*tarr(dur))) > 0.55
crickets = base * trill * gate * 0.04
crickets = bandpass(crickets, 3500, 6000)
body = mix(pan(hum, 0), pan(crickets, 0.3), pan(bandpass(noise(dur, r), 200, 500)*0.1, -0.3))
return seamless(body, XF)
def amb_rain(name):
r = rng(name)
dur = AMB_LEN + XF
base = bandpass(noise(dur, r), 800, 9000) * 0.5 # hiss
low = lp1(noise(dur, r), 400) * 0.25 # rumble
drops = np.zeros(int(dur * SR))
for _ in range(int(dur * 12)):
at = r.uniform(0, dur - 0.05); f = r.uniform(1500, 5000)
d = np.sin(2*np.pi*f*tarr(0.02)) * np.exp(-np.linspace(0, 30, int(0.02*SR))) * r.uniform(0.05, 0.15)
p = int(at*SR); drops[p:p+len(d)] += d
body = mix(pan(base, 0), pan(low, 0), pan(drops, r.uniform(-0.4, 0.4)))
return seamless(body, XF)
def _roomtone(name, hum_f, hiss_lo, hiss_hi, hiss_g, extra=None):
r = rng(name)
dur = AMB_LEN + XF
hum = (osc(hum_f, dur, "sine")*0.5 + osc(hum_f*2, dur, "sine")*0.2) * 0.06
hiss = bandpass(noise(dur, r), hiss_lo, hiss_hi) * hiss_g
layers = [pan(hum, 0), pan(hiss, 0)]
if extra is not None:
layers.append(pan(extra(r, dur), 0.2))
return seamless(mix(*layers), XF)
def amb_roomtone_retail(name):
return _roomtone(name, 100, 200, 2000, 0.05)
def amb_roomtone_milkbar(name):
# fridge compressor buzz
return _roomtone(name, 120, 300, 1500, 0.05,
extra=lambda r, d: softclip(osc(120, d, "tri")*0.4, 2)*0.05)
def amb_roomtone_video(name):
# faint CRT flyback whine (~15.7 kHz) + hum
return _roomtone(name, 60, 400, 3000, 0.03,
extra=lambda r, d: osc(15734, d, "sine")*0.012)
def _walla_chan(r, dur):
base = bandpass(noise(dur, r), 300, 2600)
env = np.full(int(dur * SR), 0.5) # crowd "waves" = sum of slow random LFOs
for _ in range(4):
env = env + 0.22 * np.sin(2 * np.pi * r.uniform(0.1, 0.5) * tarr(dur) + r.random() * 6)
walla = base * np.clip(env, 0.1, 1.2) * 0.5
walla = 0.6 * walla + 0.4 * biquad_lp(hp1(walla, 400), 1600, 3.0) # vocal-ish formant colour
for _ in range(int(dur * 0.8)): # sparse laughs/calls
at = r.uniform(0, dur - 0.3)
blip = bandpass(noise(r.uniform(0.1, 0.3), r), 500, 2500)
blip *= np.hanning(len(blip)) * r.uniform(0.1, 0.25)
p = int(at * SR); walla[p:p+len(blip)] += blip
return walla
def amb_crowd_walla(name):
# pub crowd murmur — decorrelated L/R for width, seamless loop (gig interior layer)
r = rng(name); dur = 20.0 + XF
body = stereo(_walla_chan(r, dur), _walla_chan(r, dur))
return seamless(peaknorm(body, -6), XF)
# ─────────────────────────────────────────────────────────────────────────────
# SFX generators (short, dry, peak-normalized)
# ─────────────────────────────────────────────────────────────────────────────
def _bell(midi, dur, partials, decay):
f = midi_hz(midi)
x = np.zeros(int(dur * SR))
for mult, g in partials:
x[:] += g * np.sin(2*np.pi*f*mult*tarr(dur))
return x * np.exp(-np.linspace(0, decay, len(x)))
def sfx_doorbell(name):
ding = _bell(76, 0.9, [(1, 1), (2.76, 0.4), (5.4, 0.15)], 5) # E5
dong = _bell(72, 1.1, [(1, 1), (2.76, 0.4), (5.4, 0.15)], 5) # C5
x = np.concatenate([ding[:int(0.45*SR)], dong])
return peaknorm(fade(x, 0.002, 0.05), -3)
def sfx_door_open(name):
r = rng(name)
latch = noise(0.03, r) * np.exp(-np.linspace(0, 30, int(0.03*SR)))
latch = bandpass(latch, 800, 5000)
creak = bandpass(noise(0.4, r), 300, 1200)
creak *= (0.3 + 0.7*np.abs(np.sin(2*np.pi*7*tarr(0.4)))) * np.linspace(1, 0, int(0.4*SR)) * 0.4
x = np.concatenate([latch, np.zeros(int(0.05*SR)), creak])
return peaknorm(fade(x, 0.002, 0.03), -4)
def sfx_till(name):
# pleasant buy-confirm: two-note bell arpeggio + tiny mechanical "cha"
r = rng(name)
n1 = _bell(72, 0.5, [(1, 1), (2.0, 0.3), (3.01, 0.2)], 6)
n2 = _bell(79, 0.7, [(1, 1), (2.0, 0.3), (3.01, 0.2)], 6)
cha = bandpass(noise(0.04, r), 2000, 8000) * np.exp(-np.linspace(0, 25, int(0.04*SR))) * 0.5
x = mix(np.concatenate([cha, np.zeros(int(0.6*SR))]),
np.concatenate([n1[:int(0.12*SR)], n2]))
return peaknorm(fade(x, 0.002, 0.05), -3)
def sfx_riffle(name):
# record-bin dig: a run of soft filtered noise "flips"
r = rng(name)
out = np.zeros(int(0.9 * SR))
tpos = 0.02
while tpos < 0.85:
flip = bandpass(noise(0.05, r), 500, 4000) * np.exp(-np.linspace(0, 22, int(0.05*SR)))
flip *= r.uniform(0.4, 1.0)
p = int(tpos * SR); out[p:p+len(flip)] += flip
tpos += r.uniform(0.05, 0.11)
return peaknorm(fade(out, 0.002, 0.03), -5)
def sfx_toast(name):
x = np.concatenate([synth_note(84, 0.06, "sine", 0.002, 0.02, 0.3, 0.03, cutoff=SR//2),
synth_note(88, 0.12, "sine", 0.002, 0.03, 0.3, 0.06, cutoff=SR//2)])
return peaknorm(fade(x, 0.002, 0.03), -6)
def sfx_tram_bell(name):
x = _bell(84, 1.2, [(1, 1), (2.4, 0.5), (3.9, 0.25), (5.2, 0.12)], 4)
d2 = np.concatenate([np.zeros(int(0.18*SR)),
_bell(84, 1.0, [(1, 1), (2.4, 0.5), (3.9, 0.25)], 4)])
d2 = np.concatenate([d2, np.zeros(len(x))])[:len(x)] # zero-pad/trim to x
return peaknorm(fade(x + 0.5*d2, 0.002, 0.08), -3)
def sfx_tram_rumble(name):
# loopable continuous low rumble (~3 s) — smooth (no discrete clacks that would collide at
# the loop fold); the wheel-clatter texture is a modulated mid-band noise instead.
r = rng(name); dur = 3.0 + XF
low = lp1(noise(dur, r), 120) * 0.6 + osc(45, dur, "sine") * 0.2
clatter = bandpass(noise(dur, r), 200, 1400)
lfo = 0.4 + 0.6 * (0.5 + 0.5 * np.sin(2 * np.pi * 6 * tarr(dur))) # 6 Hz wheel wobble
body = mix(low, clatter * lfo * 0.18)
return seamless(peaknorm(body, -6), XF)
def sfx_applause(name):
# applause + cheer sting (one-shot): a swell of ~400 filtered-noise claps over a cheer bed
r = rng(name); dur = 2.6
out = np.zeros(int(dur * SR))
for _ in range(400):
at = min(r.uniform(0, dur) * r.uniform(0.5, 1.0), dur - 0.02) # bias toward a swell
clap = bandpass(noise(0.015, r), 900, 5000) * np.exp(-np.linspace(0, 40, int(0.015*SR)))
p = int(at * SR); out[p:p+len(clap)] += clap * r.uniform(0.2, 0.6)
cheer = bandpass(noise(dur, r), 400, 2000)
swell = np.concatenate([np.linspace(0, 1, int(0.4*SR)),
np.ones(max(int(dur*SR) - int(1.2*SR), 0)),
np.linspace(1, 0, int(0.8*SR))])
cheer = cheer * np.resize(swell, len(cheer)) * 0.3
return peaknorm(fade(mix(out, cheer), 0.005, 0.15), -3)
# ─────────────────────────────────────────────────────────────────────────────
# ROUND 38 (v8 wave 1.1) — THE INHABITATION LAYER: someone else's life, off-screen.
#
# Six point sources for Lane B's seeded anchors. Same PACK discipline as everything above:
# deterministic through crc32(name), seamless where it loops, 🟢 original by construction (every
# sample is an oscillator or a noise render — there is no recording to imitate and nothing to
# clear). Zero draws, zero tris: the only item in the epoch that cannot touch the render budget.
#
# NOTE FOR LANE B, measured not assumed: `playSfx` is mono with a gain multiplier, so DISTANCE
# works and DIRECTION does not (R38 binding correction 1). These are all authored mono-compatible
# — the loops carry a little stereo width for the bed case, but nothing here depends on panning
# to read correctly. The "distance" in `mower-distant` is baked into the SPECTRUM (the top two
# octaves are gone, the way air actually filters a mower two streets over), not into the level,
# because `encode()` runs loudnorm over every `ambience` asset and would undo a quiet render.
# Ship the quietness as the manifest `gain`.
# ─────────────────────────────────────────────────────────────────────────────
def _bark(r, f0, dur):
"""One bark: a pitch-dropping glottal buzz through two vocal-tract formants, with a noise
burst on the attack. f0 ~150-210 Hz is a medium suburban dog."""
n = int(dur * SR)
f = np.linspace(f0 * 1.45, f0 * 0.82, n)
src = osc(f, dur, "saw")
nz = noise(dur, r) * np.exp(-np.linspace(0, 26, n)) * 0.5
x = src * 0.8 + nz
x = 0.55 * bandpass(x, 300, 1200) + 0.35 * bandpass(x, 900, 2600) + 0.2 * lp1(x, 300)
env = np.exp(-np.linspace(0, 7, n)) * (1 - np.exp(-np.linspace(0, 60, n)))
return x * env
def sfx_dog_bark_fence(name):
"""The dog going off behind a paling fence. A burst of 6-10 barks at an irregular rate — a dog
that has decided about you, not a single alert woof. The fence and the yard are the whole
character: a paling slap-back at ~28 ms and a lid on everything above 2.2 kHz."""
r = rng(name)
dur = 3.2
n = int(dur * SR)
out = np.zeros(n)
f0 = r.uniform(150, 210) # this dog's voice — fixed for the whole burst
t = 0.05
while t < dur - 0.35:
b = _bark(r, f0 * r.uniform(0.92, 1.10), r.uniform(0.13, 0.20))
p = int(t * SR); e = min(p + len(b), n)
out[p:e] += b[:e - p] * r.uniform(0.7, 1.0)
t += r.uniform(0.22, 0.46)
out = lp1(out, 2200) # timber + distance take the top
out = out + np.concatenate([np.zeros(int(0.028 * SR)), out])[:n] * 0.25 # paling slap-back
out = schroeder_reverb(out, 0.16, 0.45) # the yard it is standing in
return peaknorm(fade(out, 0.003, 0.08), -4)
def _warble(r, dur, f0, f1):
"""One fluted magpie note: a glide f0->f1 under a fast quaver (the syrinx tremolo), with the
2nd and 3rd harmonics that make it read as flute rather than whistle."""
n = int(dur * SR)
f = np.linspace(f0, f1, n) * (1 + 0.06 * np.sin(2 * np.pi * r.uniform(28, 52) * np.arange(n) / SR))
x = osc(f, dur, "sine") + 0.35 * osc(f * 2, dur, "sine") + 0.12 * osc(f * 3, dur, "sine")
return x * np.hanning(n)
def sfx_magpie_carol(name):
"""The CAROL — the warble, not the clack. A magpie has two sides to its syrinx and sings with
both at once, which is why the carol sounds like two birds in one throat: every note here is
doubled by a quieter voice roughly a fifth up. Open-air reverb, no roof."""
r = rng(name)
dur = 2.6
n = int(dur * SR)
out = np.zeros(n)
t = 0.06
while t < dur - 0.30:
d = r.uniform(0.10, 0.26)
f0 = r.uniform(900, 2400)
f1 = f0 * r.uniform(0.62, 1.55)
note = _warble(r, d, f0, f1) + 0.35 * _warble(r, d, f0 * 1.48, f1 * 1.42) # 2nd voice
p = int(t * SR); e = min(p + len(note), n)
out[p:e] += note[:e - p] * r.uniform(0.5, 1.0)
t += d + r.uniform(0.01, 0.09)
out = bandpass(out, 500, 7000)
out = schroeder_reverb(out, 0.22, 0.55)
return peaknorm(fade(out, 0.005, 0.10), -5)
def sfx_roller_door(name):
"""A corrugated roller door run up: a dense clatter of the curtain over the guides that speeds
up and slows down, the sheet-metal drum rumbling under it, the thin panel modes ringing only
while it rattles (amplitude-followed, so the ring can never sound detached), then the stop."""
r = rng(name)
dur = 2.8
n = int(dur * SR)
out = np.zeros(n)
t, tend = 0.04, dur - 0.35
while t < tend:
c = bandpass(noise(0.02, r), 1200, 7000) * np.exp(-np.linspace(0, 45, int(0.02 * SR)))
p = int(t * SR); e = min(p + len(c), n)
out[p:e] += c[:e - p] * r.uniform(0.3, 1.0)
rate = 0.020 + 0.030 * abs(math.cos(math.pi * min(t / tend, 1.0))) # slow-fast-slow
t += rate * r.uniform(0.7, 1.3)
rum = bandpass(noise(dur, r), 90, 420) * 0.6
rum *= np.clip(np.sin(np.pi * np.linspace(0, 1, n)) * 1.4, 0, 1)
follow = lp1(np.abs(out), 60)
follow = follow / (np.max(follow) or 1.0)
ring = (osc(196, dur, "sine") * 0.5 + osc(263, dur, "sine") * 0.3
+ osc(431, dur, "sine") * 0.2) * follow
clunk = np.zeros(n)
cp = int((dur - 0.30) * SR)
kl = _bell(43, 0.55, [(1, 1), (2.1, 0.5), (3.4, 0.25)], 9)
clunk[cp:cp + len(kl)] += kl[:n - cp] * 0.9
body = softclip(mix(out * 0.9, rum * 0.5, ring * 0.6, clunk) * 1.2, 1.2)
return peaknorm(fade(body, 0.003, 0.06), -3)
MOWER_LEN = 12.0
def amb_mower_distant(name):
"""A four-stroke mower two streets over. The firing pulse train wanders as it hits thick grass;
the whole thing is low-passed at 900 Hz because that is what a couple of hundred metres of air
and two rows of houses actually do to it. Every LFO is an exact harmonic of 1/MOWER_LEN and the
firing rate is rounded to a whole number of cycles per loop, so the loop genuinely folds and
the crossfade has almost nothing left to hide."""
r = rng(name)
dur = MOWER_LEN + XF
t = tarr(dur)
cyc = round(r.uniform(48, 58) * MOWER_LEN) # whole cycles per loop => phase folds
f = (cyc / MOWER_LEN) * (1 + 0.020 * np.sin(2 * np.pi * (3 / MOWER_LEN) * t)
+ 0.012 * np.sin(2 * np.pi * (1 / MOWER_LEN) * t + 1.1))
eng = softclip((osc(f, dur, "saw") * 0.6 + osc(f * 2, dur, "square") * 0.25
+ osc(f * 3, dur, "saw") * 0.12) * 1.4, 1.6)
blade = bandpass(noise(dur, r), 700, 3000) * 0.25 # the cut, hissing off the blade
whine = osc(f * 7.5, dur, "sine") * 0.05
body = lp1(mix(eng * 0.5, blade, whine), 900) # <- the distance, spent on spectrum
body = body * (0.55 + 0.45 * (0.5 + 0.5 * np.sin(2 * np.pi * (1 / MOWER_LEN) * t
+ r.random() * 6))) # pushed away, back
body = mix(pan(body, r.uniform(-0.4, 0.4)), pan(lp1(noise(dur, r, "pink"), 300) * 0.08, 0))
return seamless(peaknorm(body, -8), XF)
SPRINK_LEN = 8.0
def amb_sprinkler(name):
"""An impulse sprinkler on a lawn: the spray hiss sweeping as the head turns, and the arm
ticking its way across the arc then swinging back silently. The tick is the whole sound —
a hard metal transient with a short descending body under it."""
r = rng(name)
dur = SPRINK_LEN + XF
n = int(dur * SR)
sweep = 0.35 + 0.65 * (0.5 + 0.5 * np.sin(2 * np.pi * (1 / SPRINK_LEN) * tarr(dur) - np.pi / 2))
# spray sits UNDER the arm: at 0.5 the hiss masked the ticks in the render (verified on the
# spectrogram) and it read as a hose, not a sprinkler. The tick is the sprinkler.
out = bandpass(noise(dur, r), 1800, 9000) * sweep * 0.28
tk = int(0.012 * SR)
t = 0.0
while t < dur:
if (t % SPRINK_LEN) / SPRINK_LEN < 0.62: # ticking arc
k = (bandpass(noise(0.012, r), 2500, 11000) * np.exp(-np.linspace(0, 55, tk))
+ osc(np.linspace(1400, 900, tk), 0.012, "sine") * np.exp(-np.linspace(0, 40, tk)) * 0.4)
p = int(t * SR); e = min(p + tk, n)
out[p:e] += k[:e - p] * r.uniform(0.55, 1.0)
t += r.uniform(0.085, 0.115)
else: # the swing back: no ticks
t += 0.02
return seamless(peaknorm(pan(lp1(out, 6000), r.uniform(-0.3, 0.3)), -8), XF)
def amb_radio_through_window(name):
"""A radio through a window: an ORIGINAL four-bar turnaround (parody-safe by construction, like
every music bed in this pack) played through a small tinny speaker, then through glass. The
band-limit is the point — 380-2600 Hz is a windowsill radio, and the 1.5 kHz lid on top of that
is the window. You are not meant to be able to name the song; you are meant to know someone
is home."""
r = rng(name); bpm = 112; bars = 4
spb = 60 / bpm
cell = int(spb * SR)
prog = [50, 57, 55, 52] # D A G E — original voicing
bass = render_line(sum([[(root - 12, 1.0)] * 4 for root in prog], []), bpm,
lambda m, d: synth_note(m, d, "tri", 0.006, 0.10, 0.6, 0.06, cutoff=700))
chords = []
for root in prog:
for _ in range(4):
ch = mix(epiano(root + 12, spb * 0.85), epiano(root + 16, spb * 0.85),
epiano(root + 19, spb * 0.85)) * 0.5
chords.append(np.concatenate([ch, np.zeros(max(cell - len(ch), 0))])[:cell])
chords = np.concatenate(chords)
drums = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 2, 4, 6, 8, 10, 12, 14]}, bpm, bars, r)
body = mix(bass * 0.8, chords * 0.7, drums * 0.5)
body = softclip(bandpass(body, 380, 2600) * 1.15, 1.3) # the little speaker, driven
hiss = bandpass(noise(len(body) / SR, r), 1200, 5000) * 0.02 # AM tuning hiss
body = lp1(mix(body, hiss), 1500) # the glass
return loop_music(peaknorm(body, -10), 0.16)
def _footstep(name, bright, res_f):
r = rng(name)
thump = osc(np.linspace(res_f*1.6, res_f, int(0.05*SR)), 0.05, "sine")
thump *= np.exp(-np.linspace(0, 18, int(0.05*SR)))
click = bandpass(noise(0.04, r), 1500 if bright else 700, 9000 if bright else 4000)
click *= np.exp(-np.linspace(0, 30, int(0.04*SR))) * (0.7 if bright else 0.4)
x = mix(thump, click) * r.uniform(0.8, 1.0)
return peaknorm(fade(x, 0.001, 0.02), -6)
# ─────────────────────────────────────────────────────────────────────────────
# MUSIC beds (instrumental, loopable, seeded, parody-safe originals)
# ─────────────────────────────────────────────────────────────────────────────
def loop_music(body, revmix=0.12, tail=1.2):
"""Seamless music loop: render the reverb ring-out PAST the loop end into `tail` seconds of
silence, then crossfade that genuine ring-out over the head. Result: when the loop restarts,
the decay from the 'previous' pass bleeds into the start — no click. (The broken way is to
append a head-copy, which makes tail==head and the crossfade a no-op.)"""
if body.ndim == 1:
body = stereo(body)
T = int(tail * SR)
wet = schroeder_reverb(np.concatenate([body, np.zeros((T, 2))]), revmix)
return seamless(wet, tail)
def music_record_shop(name):
r = rng(name); bpm = 96; bars = 8
spb = 60/bpm; barlen = int(4*spb*SR)
# Am7 - D7 - Gmaj7 - Cmaj7 kind of groove (original voicing), root midis:
prog = [45, 50, 43, 48] * 2 # A, D, G, C (one per bar)
third = [+3, +4, +4, +4]; seventh = [+10, +10, +11, +11]
bass = render_line(sum([[(root, 0.75), (None, 0.25), (root+7, 0.5), (root+12, 0.5),
(root, 1.0), (root+7, 1.0)] for root in prog], []),
bpm, lambda m, d: synth_note(m-12, d, "saw", 0.005, 0.1, 0.5, 0.05, cutoff=700))
chords = []
for i, root in enumerate(prog):
for _ in range(4): # comp on each beat
ch = mix(epiano(root+12, spb*0.9), epiano(root+12+third[i % 4], spb*0.9),
epiano(root+12+seventh[i % 4], spb*0.9))
chords.append(ch * 0.5)
chords = np.concatenate([np.concatenate([c, np.zeros(max(int(spb*SR)-len(c),0))])[:int(spb*SR)] for c in chords])
drums = render_drums({"k": [0, 6, 8], "s": [4, 12], "h": [0, 2, 4, 6, 8, 10, 12, 14]}, bpm, bars, r)
body = mix(pan(bass, 0)*0.9, pan(chords, -0.15)*0.7, pan(drums, 0.1)*0.6)
return loop_music(body, 0.12)
def music_milkbar(name):
r = rng(name); bpm = 108; bars = 8
spb = 60/bpm
prog = [48, 55, 53, 50] # C G F D-ish bright pop (original)
bass = render_line(sum([[(root-12, 1.0), (root-12, 1.0), (root-5, 1.0), (root-12, 1.0)] for root in prog]*2, []),
bpm, lambda m, d: synth_note(m, d, "tri", 0.005, 0.1, 0.6, 0.05, cutoff=900))
lead = render_line([(r.choice([root, root+4, root+7, root+12]), 0.5) for root in prog for _ in range(8)],
bpm, lambda m, d: synth_note(m+12, d, "square", 0.005, 0.05, 0.4, 0.05, cutoff=3000))
drums = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 2, 4, 6, 8, 10, 12, 14], "o": [14]}, bpm, bars, r)
body = mix(pan(bass, 0)*0.9, pan(lead, 0.2)*0.35, pan(drums, 0)*0.55)
return loop_music(body, 0.1)
def music_video_synth(name):
r = rng(name); bpm = 84; bars = 8
spb = 60/bpm
prog = [45, 45, 50, 43] # moody synthwave (original)
pads = np.concatenate([mix(pad(root, 4*spb), pad(root+7, 4*spb), pad(root+10, 4*spb))[:int(4*spb*SR)]
for root in prog]*2)
arp_notes = []
for root in prog*2:
for s in [0, 7, 12, 7]:
arp_notes.append((root+12+s, 0.5))
arp = render_line(arp_notes, bpm, lambda m, d: synth_note(m, d, "pulse25", 0.002, 0.05, 0.3, 0.05, cutoff=2600))
bass = render_line(sum([[(root-12, 2.0), (root-12, 2.0)] for root in prog]*2, []),
bpm, lambda m, d: synth_note(m, d, "saw", 0.01, 0.1, 0.7, 0.1, cutoff=500))
drums = render_drums({"k": [0, 8], "s": [8], "h": [2, 6, 10, 14]}, bpm, bars, r)
body = mix(pan(pads, 0)*0.5, pan(arp, 0.3)*0.3, pan(bass, 0)*0.8, pan(drums, 0)*0.4)
return loop_music(body, 0.2)
def music_arcade(name):
r = rng(name); bpm = 132; bars = 8
spb = 60/bpm
prog = [48, 48, 53, 55] # upbeat chiptune (original)
lead_scale = [0, 2, 4, 7, 9, 12]
lead = render_line([(root+12+r.choice(lead_scale), 0.25) for root in prog for _ in range(16)],
bpm, lambda m, d: synth_note(m, d, "square", 0.001, 0.02, 0.5, 0.02, cutoff=SR//2))
bass = render_line(sum([[(root-12, 0.5), (root, 0.5)]*4 for root in prog]*2, []),
bpm, lambda m, d: synth_note(m, d, "pulse25", 0.002, 0.03, 0.5, 0.02, cutoff=SR//2))
hats = render_drums({"h": list(range(0, 16, 2)), "k": [0, 4, 8, 12], "s": [4, 12]}, bpm, bars, r)
body = mix(pan(lead, 0.1)*0.4, pan(bass, -0.1)*0.5, pan(hats, 0)*0.4)
return loop_music(body, 0.03)
def powerchord(root, dur, drive=3.5):
x = osc(midi_hz(root), dur, "saw") + osc(midi_hz(root+7), dur, "saw") + osc(midi_hz(root+12), dur, "saw")
x = softclip(x * 0.4, drive) # overdriven pub-rock crunch
x = biquad_lp(x, 3200, 0.9)
return x * adsr(dur, 0.003, 0.05, 0.7, 0.04)
def grungechord(root, dur, drive=5.0):
# sludgier, down-tuned cousin of powerchord: root+5th+octave, a square sub-octave for weight,
# heavier fuzz and a darker filter. `drive` swings the quiet-loud grunge dynamic.
x = (osc(midi_hz(root), dur, "saw") + osc(midi_hz(root+7), dur, "saw")
+ osc(midi_hz(root+12), dur, "saw") + 0.6 * osc(midi_hz(root-12), dur, "square"))
x = softclip(x * 0.5, drive) # thick fuzz
x = biquad_lp(x, 2400, 0.9) # darker than pub-rock's 3200
return x * adsr(dur, 0.004, 0.06, 0.75, 0.06)
def music_pubrock(name):
# the gig live bed — 90s Aussie pub-rock: driving power chords, punchy bass, rock kit. Louder
# + rawer than the shop beds. Original I-bVII-IV-I riff in E.
r = rng(name); bpm = 144; bars = 8
prog = [40, 38, 45, 40] * 2 # E D A E, ×2 bars = 8 bars
guitar = render_line([(root, 0.5) for root in prog for _ in range(8)], # palm-mute 8ths
bpm, lambda m, d: powerchord(m, d), gap=0.02)
bass = render_line([(root - 12, 0.5) for root in prog for _ in range(8)],
bpm, lambda m, d: synth_note(m, d, "saw", 0.003, 0.05, 0.6, 0.03, cutoff=650))
drums = render_drums({"k": [0, 3, 6, 8, 11, 14], "s": [4, 12],
"h": [0, 2, 4, 6, 8, 10, 12, 14], "o": [15]}, bpm, bars, r)
body = mix(pan(guitar, -0.12)*0.5, pan(bass, 0.1)*0.7, pan(drums, 0)*0.7)
return loop_music(body, 0.06)
def music_grunge(name):
# band-room gig bed — 90s grunge: down-tuned sludge riff with a quiet-loud dynamic (clean-ish
# verse → full-fuzz chorus) and a loose, heavy kit. Original i-bVII-bVI riff, low + dark. Slower
# and murkier than the pub-rock bed so the two venues never sound the same.
r = rng(name); bpm = 112
prog = [35, 35, 42, 40] # 4 bars (B B E D-ish), played verse then chorus
verse = render_line([(root, 0.5) for root in prog for _ in range(8)], # palm-mute 8ths, cleanish
bpm, lambda m, d: grungechord(m, d, 2.4) * 0.32, gap=0.03)
chorus = render_line([(root, 0.5) for root in prog for _ in range(8)], # full sludge
bpm, lambda m, d: grungechord(m, d, 5.5) * 0.55, gap=0.0)
guitar = np.concatenate([verse, chorus])
bass = render_line([(root - 12, 0.5) for root in prog for _ in range(8)] * 2,
bpm, lambda m, d: synth_note(m, d, "saw", 0.004, 0.06, 0.7, 0.04, cutoff=520))
vdr = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 4, 8, 12]}, bpm, 4, r) # sparse verse
cdr = render_drums({"k": [0, 3, 6, 8, 11, 14], "s": [4, 12],
"h": [0, 2, 4, 6, 8, 10, 12, 14], "o": [7, 15]}, bpm, 4, r) # driving chorus
drums = np.concatenate([vdr, cdr])
body = mix(pan(guitar, -0.1)*0.72, pan(bass, 0.08)*0.62, pan(drums, 0)*0.62) # headroom: dense chorus
return loop_music(body, 0.10)
def music_covers(name):
# RSL gig bed — a covers band at the returned-servicemen's club: mellow, organ-led easy-listening.
# Warm I-vi-IV-V turnaround (original voicing), walking bass, soft kit, a sparse organ answer.
# Cleaner and warmer than the pub/band-room beds — the crowd-cap stress room reads relaxed.
r = rng(name); bpm = 100; bars = 8
spb = 60 / bpm
prog = [48, 45, 53, 55] # C Am F G7 (roots), one per bar, ×2 = 8 bars
chords = {48: [0, 4, 7], 45: [0, 3, 7], 53: [0, 4, 7], 55: [0, 4, 7, 10]} # G7 on the turnaround
org_bars = []
for root in prog * 2: # sustained organ triad per bar
ch = mix(*[organ(root + 12 + iv, 4 * spb) for iv in chords[root]]) * 0.5
cell = int(4 * spb * SR)
org_bars.append(np.concatenate([ch, np.zeros(max(cell - len(ch), 0))])[:cell])
organ_part = np.concatenate(org_bars)
walk = sum([[(root - 12, 1.0), (root - 5, 1.0), (root, 1.0), (root - 3, 1.0)] for root in prog * 2], [])
bass = render_line(walk, bpm, lambda m, d: synth_note(m, d, "tri", 0.006, 0.12, 0.6, 0.06, cutoff=800))
drums = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 2, 4, 6, 8, 10, 12, 14]}, bpm, bars, r) * 0.5
lead_notes = sum([[(root + 12, 1.5), (None, 0.5), (root + 19, 1.0), (None, 1.0)] for root in prog * 2], [])
lead = render_line(lead_notes, bpm, # gentle organ-ish answer
lambda m, d: synth_note(m, d, "tri", 0.02, 0.12, 0.5, 0.2, cutoff=1600, vib=0.8))
body = mix(pan(organ_part, -0.1)*0.7, pan(bass, 0.05)*0.8, pan(drums, 0)*0.5, pan(lead, 0.25)*0.3)
return loop_music(body, 0.18) # warmer reverb than the rock beds
# ─────────────────────────────────────────────────────────────────────────────
# the pack: name -> (category, generator, {loop, gain, note})
# ─────────────────────────────────────────────────────────────────────────────
PACK = {
# ambience (loop, quiet beds)
"ambience-street-day": ("ambience", amb_street_day, dict(loop=True, gain=0.5)),
"ambience-street-night": ("ambience", amb_street_night, dict(loop=True, gain=0.5)),
"ambience-rain": ("ambience", amb_rain, dict(loop=True, gain=0.45, note="layer over street beds")),
"roomtone-retail": ("ambience", amb_roomtone_retail, dict(loop=True, gain=0.35)),
"roomtone-milkbar": ("ambience", amb_roomtone_milkbar, dict(loop=True, gain=0.35)),
"roomtone-video": ("ambience", amb_roomtone_video, dict(loop=True, gain=0.35)),
# sfx
"sfx-doorbell": ("sfx", sfx_doorbell, dict(loop=False, gain=0.7)),
"sfx-door-open": ("sfx", sfx_door_open, dict(loop=False, gain=0.6)),
"sfx-till": ("sfx", sfx_till, dict(loop=False, gain=0.7)),
"sfx-riffle": ("sfx", sfx_riffle, dict(loop=False, gain=0.6)),
"sfx-toast": ("sfx", sfx_toast, dict(loop=False, gain=0.5)),
"sfx-tram-bell": ("sfx", sfx_tram_bell, dict(loop=False, gain=0.7)),
"sfx-tram-rumble":("sfx", sfx_tram_rumble,dict(loop=True, gain=0.5)),
# footsteps (2 surfaces x 3 variants)
"step-pavement-1": ("sfx", lambda n: _footstep(n, True, 90), dict(loop=False, gain=0.4)),
"step-pavement-2": ("sfx", lambda n: _footstep(n, True, 95), dict(loop=False, gain=0.4)),
"step-pavement-3": ("sfx", lambda n: _footstep(n, True, 85), dict(loop=False, gain=0.4)),
"step-timber-1": ("sfx", lambda n: _footstep(n, False, 70), dict(loop=False, gain=0.4)),
"step-timber-2": ("sfx", lambda n: _footstep(n, False, 75), dict(loop=False, gain=0.4)),
"step-timber-3": ("sfx", lambda n: _footstep(n, False, 65), dict(loop=False, gain=0.4)),
# music beds (loop, instrumental originals)
"music-record-shop": ("music", music_record_shop, dict(loop=True, gain=0.55, note="record shop")),
"music-milkbar": ("music", music_milkbar, dict(loop=True, gain=0.5, note="milk bar radio")),
"music-video-synth": ("music", music_video_synth, dict(loop=True, gain=0.5, note="video store")),
"music-arcade": ("music", music_arcade, dict(loop=True, gain=0.5, note="arcade")),
# ── gig layer (?gigs=1 consumers: F plays these when a gig is on) — the manifest key IS the
# gigKey, ALWAYS `gig-<genreKey>` (R13 debt #1: one key, zero mapping). One bed per venue kind. ──
"music-gig-pubrock": ("music", music_pubrock, dict(loop=True, gain=0.6, note="gig live bed — pub-rock (pub); F plays via room.audio gigKey")),
"music-gig-grunge": ("music", music_grunge, dict(loop=True, gain=0.6, note="gig live bed — grunge (band_room)")),
"music-gig-covers": ("music", music_covers, dict(loop=True, gain=0.5, note="gig live bed — covers (RSL); mellower, organ-led")),
"ambience-crowd-walla":("ambience", amb_crowd_walla, dict(loop=True, gain=0.4, note="gig crowd layer (interior)")),
"sfx-applause": ("sfx", sfx_applause, dict(loop=False, gain=0.7, note="between-sets cheer")),
# ── R38 / v8 wave 1.1 — THE INHABITATION LAYER (six point sources at seeded anchors; Lane B
# owns the mixer layers + falloff). Zero draws, zero tris. `life` flag in the manifest. ──
"ambience-mower-distant": ("ambience", amb_mower_distant, dict(loop=True, gain=0.30, note="a mower two streets over")),
"ambience-radio-through-window":("ambience", amb_radio_through_window,dict(loop=True, gain=0.25, note="someone's radio through a window")),
"ambience-sprinkler": ("ambience", amb_sprinkler, dict(loop=True, gain=0.35, note="impulse sprinkler on a lawn")),
"sfx-dog-bark-fence": ("sfx", sfx_dog_bark_fence, dict(loop=False, gain=0.55, note="the dog going off behind a paling fence")),
"sfx-roller-door": ("sfx", sfx_roller_door, dict(loop=False, gain=0.50, note="a roller door run up")),
"sfx-magpie-carol": ("sfx", sfx_magpie_carol, dict(loop=False, gain=0.45, note="the carol — the warble, not the clack")),
}
def write_wav(path, x):
if x.ndim == 1:
x = stereo(x)
peak = np.max(np.abs(x))
if peak > 0.95: # limiter: never clip (loudnorm sets final loudness)
x = x * (0.95 / peak)
x = np.clip(x, -1.0, 1.0)
pcm = (x * 32767).astype("<i2")
with wave.open(path, "wb") as w:
w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR)
w.writeframes(pcm.tobytes())
def encode(wav, base, category):
"""WAV -> OGG/Opus (+ M4A/AAC). Long beds/music get loudnorm; SFX are pre-peak-normalized."""
ogg = os.path.join(OUT, base + ".ogg")
m4a = os.path.join(OUT, base + ".m4a")
af = ["-af", "loudnorm=I=-16:TP=-1.5:LRA=11"] if category in ("ambience", "music") else []
br = "96k" if category == "music" else ("80k" if category == "ambience" else "64k")
subprocess.run([FF, "-y", "-i", wav, *af, "-c:a", "libopus", "-b:a", br, "-ar", "48000", ogg],
check=True, capture_output=True)
subprocess.run([FF, "-y", "-i", wav, *af, "-c:a", "aac", "-b:a", "128k", "-ar", "44100", m4a],
check=True, capture_output=True)
return os.path.getsize(ogg), os.path.getsize(m4a)
if __name__ == "__main__":
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
wav_only = "--wav-only" in sys.argv
todo = {k: v for k, v in PACK.items() if not only or only in k or only == v[0]}
if "--dry-run" in sys.argv:
print(f"{len(todo)} audio assets (procedural synth, $0):")
by = {}
for k, (cat, _, meta) in sorted(todo.items()):
by.setdefault(cat, []).append(k)
for cat, ks in by.items():
print(f" [{cat}] {len(ks)}: {', '.join(ks)}")
sys.exit(0)
print(f"rendering {len(todo)} assets @ {SR}Hz (numpy synth -> ffmpeg opus+aac)")
total_ogg = total_m4a = 0
for k, (cat, gen, meta) in todo.items():
wav = os.path.join(RAW, k + ".wav")
x = gen(k)
write_wav(wav, x)
dur = (len(x) / SR)
if wav_only:
print(f" [{cat}] {k} {dur:.1f}s (wav only)")
continue
og, m4 = encode(wav, k, cat)
total_ogg += og; total_m4a += m4
print(f" [{cat}] {k:22s} {dur:5.1f}s ogg={og//1024:>4}KB m4a={m4//1024:>4}KB")
if not wav_only:
print(f"\ntotal shipped: ogg {total_ogg/1e6:.2f}MB + m4a {total_m4a/1e6:.2f}MB "
f"= {(total_ogg+total_m4a)/1e6:.2f}MB (budget 25MB)")