soundsynth
Every clip in *First Light: Luteway* is synthesised here, on the CPU, from raw interleaved f32 PCM, and baked into a `.soundClip`. Nothing calls a generation service, so the game's sound is part of its source: it cannot be muted by a credit balance, a rate limit or a network erro…
soundsynth — the sound of the Luteway, made from arithmetic
Every clip in First Light: Luteway is synthesised here, on the CPU, from raw
interleaved f32 PCM, and baked into a .soundClip. Nothing calls a generation
service, so the game's sound is part of its source: it cannot be muted by a
credit balance, a rate limit or a network error, and it rebuilds identically on
any machine.
The bank
| key | clip | what it is |
|---|---|---|
bed | lw_bed | 10 s of pre-dawn weather — wind through a breathing filter, three surf washes, the weight of the water under it, and a thin cold hiss on top |
scoreLow | lw_score_low | 12 s pad in D — the layer that is there from the title card on |
scoreMid | lw_score_mid | 8 s plucked figure, twelve beats at 90 bpm — arrives as the sky turns |
scoreHigh | lw_score_high | 6 s of bells and glass — only the first light brings it |
hop | lw_hop | air, then a small body thump; pitched by how hard the hop was |
land_rock | lw_land_rock | a dry, close thud with grit in it |
land_ice | lw_land_ice | a bright ring over a hard tick |
land_lute | lw_land_lute | a squelch: a resonance closing as the clay takes the feet |
slip | lw_slip | a skid across ice, cutting up as it goes |
cling | lw_cling | the feet taking hold of the wall |
shake | lw_shake | seven wet slaps as the lute is flung off |
bray | lw_bray | the rockhopper's call — a rasped pulse train through two formants |
wave | lw_wave | the sea taking the beach |
won | lw_won | first light: a rising chord with a shimmer over it |
light | lw_light | the light line arriving — a swell with no attack |
tick / confirm / back | lw_ui_* | the interface answering |
Why the loops are PCM and the one-shots are Opus
The bed and the three score layers are looped by the Audio component's own
looping field, so their seam is the end of the file meeting its start. That
means the file has to be seamless END TO END:
- tonal material is written at frequencies that complete a whole number of cycles in the loop, so it joins itself exactly;
- noise material is generated longer than the loop and its tail is equal-power cross-faded back over its head;
- a note or a bell whose tail runs past the end is written wrapped, so the tail arrives back at the head.
They are codec = "pcm" because an Opus clip loses its first few milliseconds
on decode, which a loop hears as a tick at every seam. One-shots have no seam,
so they are Opus and small.
The three score loops are 12 s, 8 s and 6 s. They are tempo-locked but not the same length, so they phase against each other and the repeat is hard to hear over a two-minute climb.
Using it
local SND = require("luteway.sound.soundsynth")
SND.bake() -- make whatever is missing (idempotent)
SND.bake({ only = { "bed" } }) -- one clip; a long bake splits across calls
SND.bake({ force = true }) -- re-author in place, keeping each guid
local clips = SND.resolve() -- key -> AssetRef, for the clips that exist
SND.GAIN.hop -- the level that clip sits at in the mix
Every payload is proved with audio.info before it becomes an asset — a clip
of zero frames, zero duration or the wrong channel count never reaches a
.soundClip, because that is the failure that otherwise ships silently.
SND.report holds what the proof read back.
Scoped to this part · feeds back into the world's score.