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.
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 = trueon 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
| script | what it makes | costs |
|---|---|---|
bake_sfx.luau | 20 one-shots under audio/clips/ | nothing |
bake_bed.luau | bed_music and amb_still | nothing |
bake_room.luau | amb_room, the recorded room | 2 credits, once |
voice_gen.luau | buys each spoken line once | 2 credits a line |
voice_bake.luau | treats each line and files it under audio/voice/ | nothing |
All five are idempotent — a clip that exists is not bought again.
Scoped to this part · feeds back into the world's score.