灵符lingfu

lingdev

↓ 0 downloads

LingDev: 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).

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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()

← Back to index