Old Tam speaks (Kokoro); CC0 birdsong, crickets and owls; no plants through floors; menu and marker fixes

This commit is contained in:
Paul Bailey
2026-11-06 00:03:59 +00:00
parent 0550a4daa0
commit 787d8d1d65
70 changed files with 495 additions and 87 deletions

View File

@@ -6,6 +6,7 @@ third-party audio), so they're ours to release under CC0 too.
python3 tools/audio/make_audio.py # everything
python3 tools/audio/make_audio.py music # or: music | ambience | sfx | voices
python3 tools/audio/make_audio.py --kenney ~/Downloads/kenney # also re-copy the Kenney files
python3 tools/audio/make_audio.py birds # or crickets: one recorded loop, from tools/audio/sources
Writes .ogg files under assets/audio/ (via ffmpeg). Deterministic: the same
seeds give the same files. Pure Python (no numpy), so it takes a minute or two.
@@ -341,69 +342,6 @@ def track_boss():
# --- ambience ---------------------------------------------------------------------
def amb_birds():
sr, secs = 22050, 32
L = sr * secs
rng = random.Random(61)
l, r = [0.0] * L, [0.0] * L
add_noise(l, sr, 0, secs, 0.06, lp=0.03)
add_noise(r, sr, 0, secs, 0.06, lp=0.03, rng=random.Random(5))
t = 0.4
while t < secs - 1.0:
pan = rng.random()
base = rng.uniform(2400, 4200)
kind = rng.randrange(3)
n = rng.randint(2, 6)
tt = t
for k in range(n):
d = rng.uniform(0.05, 0.11)
f0 = base * rng.uniform(0.9, 1.15)
f1 = f0 * (rng.uniform(0.7, 0.85) if kind == 0 else rng.uniform(1.15, 1.35) if kind == 1 else 1.0)
nn = int(d * sr)
ph = 0.0
a = rng.uniform(0.15, 0.3)
for j in range(nn):
q = j / nn
ph += TAU * (f0 + (f1 - f0) * q) / sr
e = sin(math.pi * q) ** 2
s = sin(ph) * e * a
i = (int(tt * sr) + j) % L
l[i] += s * (1 - pan)
r[i] += s * pan
tt += d + rng.uniform(0.02, 0.09)
t += rng.uniform(0.6, 2.6)
return sr, (l, r)
def amb_crickets():
sr, secs = 22050, 30
L = sr * secs
rng = random.Random(71)
l, r = [0.0] * L, [0.0] * L
add_noise(l, sr, 0, secs, 0.04, lp=0.02)
add_noise(r, sr, 0, secs, 0.04, lp=0.02, rng=random.Random(9))
for c in range(4):
pan = rng.random()
f = rng.uniform(3800, 4700)
period = rng.uniform(0.5, 0.9)
pulses = rng.randint(3, 5)
amp = rng.uniform(0.06, 0.14)
t = rng.random() * period
while t < secs:
for p in range(pulses):
nn = int(0.018 * sr)
i0 = int((t + p * 0.032) * sr)
for j in range(nn):
e = sin(math.pi * j / nn)
s = sin(TAU * f * j / sr) * e * amp
l[(i0 + j) % L] += s * (1 - pan)
r[(i0 + j) % L] += s * pan
t += period * rng.uniform(0.95, 1.05)
if rng.random() < 0.08:
t += rng.uniform(1.0, 3.0) # (they pause now and then)
return sr, (l, r)
def amb_fire():
sr, secs = 22050, 6
L = sr * secs
@@ -700,9 +638,80 @@ def make_sfx():
def make_ambience():
d = os.path.join(OUT, "ambience")
print("ambience ->", os.path.relpath(d, ROOT))
for name, fn in (("birds", amb_birds), ("crickets", amb_crickets), ("fire", amb_fire)):
for name, fn in (("fire", amb_fire),):
sr, ch = fn()
_encode(os.path.join(d, name + ".ogg"), list(ch), sr, 3)
for name in RECORDED:
make_recorded(name)
SOURCES = os.path.join(ROOT, "tools", "audio", "sources")
def sources(prefix):
"""The CC0 recordings a loop is made from (credited in assets/audio/CREDITS.md)."""
if not os.path.isdir(SOURCES):
return []
return sorted(os.path.join(SOURCES, f) for f in os.listdir(SOURCES) if f.startswith(prefix + "_"))
## The field-recorded loops: name -> (tone filter, which stretch of each source).
## Birds: the shrillest top notes pulled back a little (a dawn chorus is bright).
## Crickets: the rumble under them (wind, a distant road) taken out.
RECORDED = {
"birds": ("highshelf=f=3800:g=-5,lowpass=f=8500", None),
"crickets": ("highpass=f=300,highshelf=f=6000:g=-3", None),
}
def _decode(src, sr, af=None):
cmd = ["ffmpeg", "-v", "error", "-i", src] + (["-af", af] if af else []) + \
["-f", "s16le", "-ac", "2", "-ar", str(sr), "-"]
raw = subprocess.run(cmd, capture_output=True, check=True).stdout
n = len(raw) // 4
left = [struct.unpack_from("<h", raw, i * 4)[0] / 32768.0 for i in range(n)]
right = [struct.unpack_from("<h", raw, i * 4 + 2)[0] / 32768.0 for i in range(n)]
# Each to the same loudness (RMS), so the loop doesn't swell and fade.
rms = math.sqrt(sum(x * x for x in left + right) / (2 * n)) or 1.0
return [x * 0.06 / rms for x in left], [x * 0.06 / rms for x in right]
def make_recorded(name, srcs=None):
"""A field-recorded ambience loop (assets/audio/ambience/<name>.ogg) from
CC0 recordings (tools/audio/sources/<name>_*, or the files given): each
evened out to the same loudness, chained one into the next and the last
back into the first with 3 s equal-power crossfades, into one seamless
loop. (Synthesised birdsong sounded like beeping, and crickets like a
smoke alarm.)"""
tone, _ = RECORDED[name]
srcs = srcs or sources(name)
if not srcs:
sys.exit("no recordings for %s in %s" % (name, os.path.relpath(SOURCES, ROOT)))
sr, xf = 44100, 3.0
k = int(xf * sr)
clips = [_decode(s, sr, tone) for s in srcs]
out_l, out_r = [], []
for c, (left, right) in enumerate(clips):
if c == 0:
out_l, out_r = list(left), list(right)
continue
# The end of what we have fades out over the start of the next clip.
base = len(out_l) - k
for i in range(k):
a = math.sin(0.5 * math.pi * i / k)
b = math.cos(0.5 * math.pi * i / k)
out_l[base + i] = out_l[base + i] * b + left[i] * a
out_r[base + i] = out_r[base + i] * b + right[i] * a
out_l += left[k:]
out_r += right[k:]
# And the end back into the beginning, so it loops.
L = len(out_l) - k
for i in range(k):
a = math.sin(0.5 * math.pi * i / k)
b = math.cos(0.5 * math.pi * i / k)
out_l[i] = out_l[i] * a + out_l[L + i] * b
out_r[i] = out_r[i] * a + out_r[L + i] * b
_encode(os.path.join(OUT, "ambience", name + ".ogg"), [out_l[:L], out_r[:L]], sr, 4)
def make_music():
@@ -773,6 +782,9 @@ if __name__ == "__main__":
copy_kenney(os.path.expanduser(args[i + 1]))
del args[i:i + 2]
what = args[0] if args else "all"
if what in RECORDED: # one recorded loop: birds | crickets
make_recorded(what)
sys.exit(0)
if what in ("all", "sfx"):
make_sfx()
if what in ("all", "sfx", "voices"):

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.