synth
The sound of FURROW, computed rather than recorded.
synth is a small offline DSP kit: it fills a track — two mono f32
buffers, one per channel — with tones, noise and events, and hands back the
interleaved PCM that asset.create("soundClip", …) bakes into a clip. Nothing
in it touches an entity, the mixer, or the network, so a score built on it is
the same score on every machine and cannot fail for a credit or a connection.
The rule everything is shaped by
A bed that loops must be seamless end to end. Loop points are accepted by the clip format and read by no playback surface, so the only loop this engine plays is a whole clip repeated — and a whole clip repeated clicks once a lap unless its last sample runs into its first.
Three mechanisms hold that:
snap(track, hz)returns the nearest frequency that fits a whole number of cycles into the track. Every periodic generator runs its frequencies and its LFO rates through it, so a tone arrives back at its starting phase.noiseBuffilters its ring circularly — the one-pole filters are primed with a full lap before anything is kept, so the filter state at sample 0 is the state at sample n.hit,burstandglidewrite with wraparound, so an event near the end of a loop spills into its head instead of being cut off. Each also opens on a short ramp rather than a step, because a strike landing on sample 0 is exactly the discontinuity the loop point would carry.
seam(track) is how you check: it returns the amplitude step across the loop
point beside the mean step between adjacent samples. A ratio near 1 means
the loop point is indistinguishable from every other sample boundary in the
clip. bake_score.luau prints it for every bed it writes.
Shape of a bake
local synth = require("furrow.synth")
local tr = synth.newTrack(8) -- 8 seconds, stereo, 44100
synth.addSine(tr, { freq = 55, amp = 0.2, amRate = 0.25, amDepth = 0.5 })
synth.addNoise(tr, { seed = 7, lo = 140, hi = 2400, amp = 0.3, pan = -0.8,
gustRate = 0.125, gustDepth = 0.7 })
synth.hit(tr, { at = 0.25, freq = 118, endFreq = 31, decay = 0.6 })
synth.normalize(tr, 0.7)
print(select(2, synth.seam(tr))) -- want this near 1
asset.create("soundClip", "bed", { pcm = synth.interleave(tr),
sampleRate = synth.RATE, channels = 2 })
Call fadeEdges on a one-shot so it starts and ends at exact zero. Never
call it on a looping bed — that is what would put the click in.
softclip is the only dynamics processing available anywhere: the engine
exposes no compressor, limiter, EQ or reverb to Luau, so anything that has to
survive a full-mass climax is glued here, in the samples, before it is encoded.
Reference
| call | what it does |
|---|---|
newTrack(dur, rate?) | a silent stereo track, dur seconds long |
snap(track, hz) | the nearest frequency that wraps in this track |
pan(p) | equal-power gains for a pan in −1…1 |
rng(seed) | a deterministic 0…1 generator |
addSine(track, o) | a sine partial, optionally tremoloed |
addSaw(track, o) | a lowpassed sawtooth |
noiseBuf(track, seed, lo?, hi?) | a circularly band-passed noise ring, RMS 1 |
addNoise(track, o) | mix a ring in, gusting on up to two LFOs |
hit(track, o) | a percussive strike, pitch falling under a decay |
burst(track, o) | a noise swell with attack and release |
glide(track, o) | an enveloped tone that rises or falls between two pitches |
peak / normalize / softclip | levels |
fadeEdges(track, head, tail) | zero the ends of a one-shot |
seam(track) | (edge, ratio) — whether a bed actually loops |
interleave(track) | the buffer asset.create and audio.encodePcm read |
Scoped to this part · feeds back into the world's score.