lingdev
↓ 0 downloadsLingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdevCode / Files (136)
tools/make_story_audio.py
#!/usr/bin/env python3
"""make_story_audio.py - builds the audio for the Moko's Picture Book demo.
python tools/make_story_audio.py (from the LingDev folder)
1. Synthesises kawaii sound effects (numpy) into assets/sfx/story/*.wav, 22.05 kHz mono:
boing jumps, the "pa-pa-pa" flutter, tongue slurp, nom / gulp, egg lay / throw / pop,
ground-pound thump, fruit chimes (one bell pitched at play time), sparkle, hurt "oof",
spring, block bonk, page turn and the page-clear fanfare.
2. Converts the chosen "<name>_Master.wav" tracks from ~/Music into assets/music/story/
(mono 22.05 kHz), with .beats files (tempo, key, onsets) for the tracks that have MIDI.
"""
import os, sys, math, subprocess
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import make_breaker_audio as mb # synthesis helpers: t_, env, sweep, noise, lowpass, bandpass, mix, at, reverb, bell, chip
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SFX = os.path.join(ROOT, "assets", "sfx", "story")
MUS = os.path.join(ROOT, "assets", "music", "story")
MUSIC_HOME = os.path.join(os.path.expanduser("~"), "Music")
SR = mb.SR
TRACKS = [ # (game name, wav, midi or None)
("title", "MoonToys/MoonToys_Master.wav", None),
("page1", "Bloop/Theme of the Marching Meadow_Master.wav", "Bloop/Theme of the Maching Meadow.mid"),
("page2", "Pelipo/Pelipo_Master.wav", None),
("page3", "MoonTime/MoonTime_Master.wav", "MoonTime/MoonTime.mid"),
("page4", "BirdEye/BirdEye_Master.wav", None),
]
def save(name, x, peak=0.85):
mb.SFX = SFX
mb.save(name, x, peak)
def squeak(f0, f1, d, vib=0.0):
t = mb.t_(d)
f = f0 * (f1 / f0) ** (t / d) * (1 + vib * np.sin(2 * np.pi * 18 * t))
return np.sin(2 * np.pi * np.cumsum(f) / SR)
def make_sfx():
os.makedirs(SFX, exist_ok=True)
t_, env, mix, at, reverb = mb.t_, mb.env, mb.mix, mb.at, mb.reverb
# jump: a springy rising boing
d = 0.32
save("jump", reverb(mix(squeak(260, 620, d, 0.05) * env(d, 0.003, 7), 0.25 * squeak(520, 1240, d) * env(d, 0.003, 12)), 0.15))
# flutter: three quick "pa" puffs (looped by the game while fluttering)
d = 0.36
out = np.zeros(int(SR * d))
for k in range(3):
p = 0.6 * mb.bandpass(mb.noise(0.07), 1600 + k * 200, 2.5) * env(0.07, 0.002, 40) + 0.4 * squeak(700, 900, 0.07) * env(0.07, 0.002, 45)
out = mix(out, at(p, k * 0.12, d))
save("flutter", out)
# tongue: a slurpy zip out
d = 0.22
save("tongue", reverb(0.7 * squeak(400, 1500, d, 0.1) * env(d, 0.002, 9) + 0.3 * mb.bandpass(mb.noise(d), 2500, 2) * env(d, 0.002, 14), 0.1))
# nom (fruit) and gulp (enemy)
d = 0.25
save("nom", reverb(mix(0.8 * squeak(500, 300, 0.12) * env(0.12, 0.002, 20), at(0.6 * squeak(700, 420, 0.1) * env(0.1, 0.002, 25), 0.11, d)), 0.15))
d = 0.4
save("gulp", reverb(0.9 * squeak(320, 110, d, 0.08) * env(d, 0.002, 6), 0.15))
# egg lay / throw / pop
d = 0.3
save("egg", reverb(mix(0.6 * squeak(900, 1400, 0.08) * env(0.08, 0.001, 30), at(0.5 * mb.bell(1567.98, 0.25, 0.4), 0.06, d)), 0.2))
d = 0.3
save("throw", reverb(0.6 * mb.bandpass(mb.noise(d), 1200, 1.2) * np.sin(np.linspace(0, np.pi, int(SR * d))) + 0.3 * squeak(500, 1100, d) * env(d, 0.01, 8), 0.15))
d = 0.5
out = 0.8 * mb.lowpass(mb.noise(0.08), 1500) * env(0.08, 0.0005, 40)
for k, semi in enumerate([0, 7, 12]):
out = mix(out, at(0.4 * mb.bell(1046.5 * 2 ** (semi / 12), 0.3, 0.5), 0.03 + k * 0.05, d))
save("pop", reverb(out, 0.25))
# ground pound: a spin whistle then a soft cushiony thump with stars
d = 0.7
spin = 0.35 * squeak(600, 1400, 0.22) * env(0.22, 0.005, 3)
thump = 0.9 * np.sin(2 * np.pi * np.cumsum(np.linspace(160, 50, int(SR * 0.3))) / SR) * env(0.3, 0.001, 12)
thump = mix(thump, 0.4 * mb.lowpass(mb.noise(0.2), 600) * env(0.2, 0.001, 18))
save("pound", reverb(mix(at(spin, 0, d), at(thump, 0.22, d)), 0.2))
# fruit chime (C6, pitched per fruit along a happy major scale) and the sparkle
save("chime", reverb(mb.bell(1046.5, 0.9, 0.6), 0.3))
d = 0.9
out = np.zeros(int(SR * d))
for k, semi in enumerate([0, 4, 7, 12, 16, 19]):
out = mix(out, at(0.35 * mb.bell(1567.98 * 2 ** (semi / 12), 0.4, 0.5), k * 0.05, d))
save("sparkle", reverb(out, 0.35))
# hurt: a sad little "oof" wobble
d = 0.5
save("hurt", reverb(0.8 * squeak(500, 200, d, 0.12) * env(d, 0.002, 4), 0.2))
# spring, block bonk, enemy poof
d = 0.45
save("spring", reverb(0.8 * squeak(200, 900, d, 0.25) * env(d, 0.002, 4), 0.15))
d = 0.2
save("bonk", reverb(mix(0.7 * mb.chip(523.25, d, 0.25), 0.5 * mb.lowpass(mb.noise(0.04), 900) * env(0.04, 0.001, 50)), 0.15))
d = 0.4
save("poof", reverb(0.7 * mb.bandpass(mb.noise(d), 900, 0.8) * env(d, 0.002, 9) + 0.3 * squeak(700, 300, d) * env(d, 0.002, 10), 0.25))
# page turn (paper) and menu blip
d = 0.5
save("page", reverb(0.6 * mb.bandpass(mb.noise(d), 3000, 0.9) * np.sin(np.linspace(0, np.pi, int(SR * d))) ** 2, 0.2))
save("blip", reverb(0.5 * mb.chip(1760, 0.05, 0.5), 0.15))
# heart (health back) and page clear fanfare
d = 0.8
out = np.zeros(int(SR * d))
for k, semi in enumerate([0, 4, 7, 12]):
out = mix(out, at(0.45 * mb.chip(880 * 2 ** (semi / 12), 0.12, 0.5), k * 0.07, d))
save("heart", reverb(out, 0.25))
d = 2.6
out = np.zeros(int(SR * d))
melody = [(0, 0.0), (4, 0.15), (7, 0.3), (12, 0.45), (9, 0.75), (12, 0.9), (16, 1.05), (19, 1.2)]
for semi, st in melody:
out = mix(out, at(0.4 * mb.chip(659.25 * 2 ** (semi / 12), 0.14, 0.25), st, d), at(0.35 * mb.bell(1318.5 * 2 ** (semi / 12), 0.8, 0.6), st, d))
save("clear", reverb(out, 0.35, 1.2))
def make_music():
os.makedirs(MUS, exist_ok=True)
for name, wav, mid in TRACKS:
src = os.path.join(MUSIC_HOME, wav)
dst = os.path.join(MUS, name + ".wav")
if not os.path.exists(dst):
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", src, "-ac", "1", "-ar", str(SR), "-sample_fmt", "s16", dst], check=True)
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", dst], capture_output=True, text=True)
length = float(out.stdout.strip() or 0)
if mid:
text, bpm, key = mb.beats_for(os.path.join(MUSIC_HOME, mid), length)
with open(os.path.join(MUS, name + ".beats"), "w") as f:
f.write(text)
print("music", name, "%.0fs" % length, "bpm %.0f" % bpm, "key", key[1], key[2])
else:
print("music", name, "%.0fs" % length, "(no MIDI: the game bobs at a steady tempo)")
with open(os.path.join(MUS, "CREDITS.txt"), "w", encoding="utf-8") as f:
f.write("Moko's Picture Book music: original tracks by Ling Lin (taellinglin), from the author's own masters.\n\n")
for name, wav, mid in TRACKS:
f.write("%-6s %s\n" % (name, os.path.splitext(os.path.basename(wav))[0].replace("_Master", "")))
if __name__ == "__main__":
what = sys.argv[1:] or ["sfx", "music"]
if "sfx" in what:
make_sfx()
if "music" in what:
make_music()