Log inGet started
World

Blockbuster 767

Branchmain
Branchesmain
Commit8e9b87fcce

README.md

audio.Synth

The bake-time DSP kit that computes every synthesised sound in The Eighth Ketyl — the bed, the ambience, and twenty one-shots — as raw f32 PCM in Luau.

Pure CPU. No service, no network, no credit, and nothing here runs per frame: the clips are baked once by the scripts in this folder and played back as assets.

local Synth = require("audio.Synth")

local t = Synth.track(0.26, 44100, 1)                     -- 260 ms, mono
Synth.voice(t, { t0 = 0, dur = 0.16, wave = "sine",
                 freq = 620, freqEnd = 2500, decay = 0.032 })   -- a drip
Synth.normalize(t, 0.62)
asset.create("soundClip", "drip", { pcm = Synth.pcm(t), sampleRate = 44100, channels = 1 })

The one idea worth knowing

A bed that loops without a click is a bed whose every sustained partial completes a whole number of cycles over the loop, and whose struck events wrap their tails round the end into the start.

  • Synth.snap(freq, loopSeconds) gives the nearest frequency that ends the loop at the phase it started.
  • wrap = true on a voice folds its decay past the end back onto the beginning.
  • Synth.seamGap(t) reports how far apart the first and last frames sit — measured on the bed at 3.8% of the largest step that occurs inside it, which is to say the loop point is quieter than the music.

That is why this slice needs no loop points, which the engine stores and never reads, and why the looping clips are stored as pcm rather than Opus, which loses the head of a clip on decode.

The primitive

Synth.voice(track, v) is an oscillator or noise through an envelope and an optional swept state-variable filter, and every clip in the game is built from it. wave is sine / saw / square / tri / pulse / noise; freq and freqEnd glide; fmRatio and fmDepth give a bell or a brass strike; filter is lp / hp / bp with cutoff sweeping to cutoffEnd at q; attack, hold and decay shape it; pan places it and wrap loops it.

Whole-track passes

normalize · gain · saturate · fadeEdges · seamless · lowpass · highpass · echo · crush · ring · pitch

seamless(t, seconds) turns any track — generated audio included, which never arrives seamless — into one that loops, by folding its tail over its head with an equal-power crossfade.

fromPcm(pcm, sampleRate, channels) wraps what audio.decode hands back so the passes above can work on recorded or generated audio: that is how the two speakers get their treatment and how the recorded room becomes a loop.

The bakes in this folder

scriptwhat it makescosts
bake_sfx.luau20 one-shots under audio/clips/nothing
bake_bed.luaubed_music and amb_stillnothing
bake_room.luauamb_room, the recorded room2 credits, once
voice_gen.luaubuys each spoken line once2 credits a line
voice_bake.luautreats each line and files it under audio/voice/nothing

All five are idempotent — a clip that exists is not bought again.