rng(seed: number) → Rng
| arg | type | description |
|---|
| seed | number | |
rand(r: Rng) → number
Uniform in [0, 1).
bipolar(r: Rng) → number
Uniform in [-1, 1).
range(r: Rng, lo: number, hi: number) → number
Uniform in [lo, hi).
| arg | type | description |
|---|
| r | Rng | |
| lo | number | |
| hi | number | |
clamp(v: number, lo: number, hi: number) → number
| arg | type | description |
|---|
| v | number | |
| lo | number | |
| hi | number | |
lerp(a: number, b: number, t: number) → number
| arg | type | description |
|---|
| a | number | |
| b | number | |
| t | number | |
snap(freq: number, seconds: number) → number
The nearest frequency that completes a WHOLE number of cycles in `seconds`.
Snapping every partial of a looped drone is what makes the loop join itself:
an unsnapped 49.3 Hz tone is mid-cycle at the seam and clicks once a lap.
| arg | type | description |
|---|
| freq | number | |
| seconds | number | |
track(frames: number) → buffer
A "track" is a Luau `buffer` of interleaved f32 — the shape `audio.encodePcm`
reads directly. Everything below works on mono tracks; `Synth.interleave`
makes the stereo pair at the end when a clip wants width.
| arg | type | description |
|---|
| frames | number | |
get(b: buffer, i: number) → number
| arg | type | description |
|---|
| b | buffer | |
| i | number | |
set(b: buffer, i: number, v: number) → void
| arg | type | description |
|---|
| b | buffer | |
| i | number | |
| v | number | |
add(b: buffer, i: number, v: number) → void
| arg | type | description |
|---|
| b | buffer | |
| i | number | |
| v | number | |
frames(b: buffer) → number
| arg | type | description |
|---|
| b | buffer | |
mix(dst: buffer, src: buffer, gain: number) → void
Add `src` into `dst` scaled by `gain`, both mono and the same length.
| arg | type | description |
|---|
| dst | buffer | |
| src | buffer | |
| gain | number | |
stamp(dst: buffer, src: buffer, atFrame: number, gain: number) → void
Stamp a short event into a loop at `atFrame`, WRAPPING past the end. An event
that starts 40 ms before the seam finishes 40 ms after the start of the same
buffer, which is exactly what the ear hears on the second lap.
| arg | type | description |
|---|
| dst | buffer | |
| src | buffer | |
| atFrame | number | |
| gain | number | |
noise(frames: number, r: Rng) → buffer
White noise, deterministic from the rng.
| arg | type | description |
|---|
| frames | number | |
| r | Rng | |
pink(frames: number, r: Rng) → buffer
Pink noise (Paul Kellet's economy filter): the spectrum wind and sea actually
have. White noise under a lowpass reads as hiss; pink reads as air.
| arg | type | description |
|---|
| frames | number | |
| r | Rng | |
sine(dst: buffer, sampleRate: number, freq: number, amp: number, phase: number, am: Curve?) → void
| arg | type | description |
|---|
| dst | buffer | |
| sampleRate | number | |
| freq | number | |
| amp | number | |
| phase | number | |
| am | Curve? | |
saw(dst: buffer, sampleRate: number, freq: number, amp: number, partials: number, loopSeconds: number?, am: Curve?) → void
A band-limited-ish saw built from `partials` sines, each snapped to the loop.
Warmer than a raw ramp and periodic by construction.
| arg | type | description |
|---|
| dst | buffer | |
| sampleRate | number | |
| freq | number | |
| amp | number | |
| partials | number | |
| loopSeconds | number? | |
| am | Curve? | |
svf(src: buffer, opts: SvfOpts) → buffer
| arg | type | description |
|---|
| src | buffer | |
| opts | SvfOpts | |
dcTrim(src: buffer, sampleRate: number, cutoff: number, circular: boolean?) → buffer
One-pole highpass — the DC and rumble trim every noise layer wants.
| arg | type | description |
|---|
| src | buffer | |
| sampleRate | number | |
| cutoff | number | |
| circular | boolean? | |
ar(frames: number, sampleRate: number, attack: number, decay: number, curve: number?) → buffer
Attack/decay envelope over a whole track, `curve` bending the decay
(1 = linear, 3 = sharp percussive).
| arg | type | description |
|---|
| frames | number | |
| sampleRate | number | |
| attack | number | |
| decay | number | |
| curve | number? | |
applyEnv(dst: buffer, env: buffer) → void
Multiply a track by an envelope of the same length, in place.
| arg | type | description |
|---|
| dst | buffer | |
| env | buffer | |
edges(dst: buffer, sampleRate: number, headSeconds: number, tailSeconds: number) → void
Fade the head and tail of a ONE-SHOT to zero, so a clip that is cut mid-cycle
neither clicks on start nor on stop. Never call this on a loop: it is exactly
the discontinuity a loop must not have.
| arg | type | description |
|---|
| dst | buffer | |
| sampleRate | number | |
| headSeconds | number | |
| tailSeconds | number | |
leadIn(src: buffer, sampleRate: number, seconds: number) → buffer
Prepend silence. Opus loses roughly the first 5 ms of a clip on decode, so a
one-shot whose whole character is its attack gets a lead-in that absorbs the
loss instead of losing the transient.
| arg | type | description |
|---|
| src | buffer | |
| sampleRate | number | |
| seconds | number | |
normalize(dst: buffer, peak: number) → number
Scale so the loudest sample sits at `peak`. Returns the gain applied, which a
recipe can report when it wants to know how much headroom it had.
| arg | type | description |
|---|
| dst | buffer | |
| peak | number | |
saturate(dst: buffer, drive: number) → void
tanh-ish saturation: glues a stack of layers together and guarantees nothing
leaves the buffer outside [-1, 1] whatever the layers summed to.
| arg | type | description |
|---|
| dst | buffer | |
| drive | number | |
interleave(left: buffer, right: buffer) → buffer
Interleave two mono tracks into one stereo buffer.
| arg | type | description |
|---|
| left | buffer | |
| right | buffer | |
widen(mono: buffer, sampleRate: number, delaySeconds: number, gain: number) → buffer
A cheap Haas/comb widener: the right channel is the left, delayed and dimmed,
with the delay taken circularly so a loop stays a loop in both channels.
| arg | type | description |
|---|
| mono | buffer | |
| sampleRate | number | |
| delaySeconds | number | |
| gain | number | |
sweep(dst: buffer, sampleRate: number, f0: number, f1: number, amp: number, shape: number?) → void
A sine whose frequency glides from `f0` to `f1` across the track, phase
integrated per sample so the glide has no discontinuity in it. `shape` bends
the sweep (1 linear, 2 slow-then-fast, 0.5 fast-then-slow).
| arg | type | description |
|---|
| dst | buffer | |
| sampleRate | number | |
| f0 | number | |
| f1 | number | |
| amp | number | |
| shape | number? | |
bell(dst: buffer, sampleRate: number, freq: number, amp: number, decay: number, ratios: { number }?) → void
A struck-bell voice: a fundamental plus the inharmonic partials that make
metal and glass read as metal and glass rather than as an organ.
| arg | type | description |
|---|
| dst | buffer | |
| sampleRate | number | |
| freq | number | |
| amp | number | |
| decay | number | |
| ratios | { number }? | |
pcmClip(pcm: buffer, sampleRate: number, channels: number, desc: string) → Baked
| arg | type | description |
|---|
| pcm | buffer | |
| sampleRate | number | |
| channels | number | |
| desc | string | |
loopClip(pcm: buffer, sampleRate: number, channels: number, desc: string) → Baked
| arg | type | description |
|---|
| pcm | buffer | |
| sampleRate | number | |
| channels | number | |
| desc | string | |
opusClip(pcm: buffer, sampleRate: number, channels: number, desc: string) → Baked
A one-shot long enough to be worth compressing. Opus eats roughly the first
5 ms on decode, so the clip gets a 12 ms silent lead-in and keeps its attack.
| arg | type | description |
|---|
| pcm | buffer | |
| sampleRate | number | |
| channels | number | |
| desc | string | |
monoOf(ref: any) → void
One cue in this game is generated rather than computed: the orchestral body
of the relighting, which no amount of additive synthesis is going to be. A
generated take arrives quiet, long, and with dead air on both ends, so it is
brought in through here — decoded, trimmed to where it actually sounds,
levelled and faded — and lands in the clips folder under a name of ours. The
relight recipe then layers it under the strike, and falls back to pure
synthesis whenever it is absent, so a failed generation costs the moment its
body and never its sound.
One channel of a clip's decoded samples, as a mono track.
trimSilence(src: buffer, sampleRate: number, floor: number, padSeconds: number) → buffer
Trim a track to the span where it is actually above `floor`, with a little
room either side. A generated take is mostly its own silence otherwise.
| arg | type | description |
|---|
| src | buffer | |
| sampleRate | number | |
| floor | number | |
| padSeconds | number | |
adopt(sourceRef: any, name: string, peak: number) → void
Bring a clip in as `game.sound.clips.<name>`, levelled and topped and tailed.
| arg | type | description |
|---|
| sourceRef | any | |
| name | string | |
| peak | number | |
bed_room( ) → void
Twenty seconds of the lamp room at the top of Lucina: the sea working the
basalt a hundred feet down, wind finding the gaps in the storm glazing, the
clock in the watch room, and the timber deciding what to do about the cold.
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
droneCommon(out: buffer, SR: number, DUR: number, r: Synth.Rng) → void
Two drones on the same root. `music_night` is what the room sounds like
while it is still dark; `music_dawn` is the same chord with the light in it.
The Sound component crossfades one into the other as `Core.state.dawn`
climbs, so the score warms without ever changing key.
| arg | type | description |
|---|
| out | buffer | |
| SR | number | |
| DUR | number | |
| r | Synth.Rng | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
cutoff(t: number) → void
| arg | type | description |
|---|
| t | number | |
writeReadme(name: string, ref: any, info: any, codec: string) → void
Run the named recipes and write each as a `.soundClip` under
`game/sound/clips`. Call it from an `execute` — a create made from a
component callback is filed in the ephemeral `/runtime` store instead of on
disk, and these clips have to be committed.
`force` re-bakes clips that already exist; without it a name already on disk
is skipped, so a bake can be resumed a few clips at a time (each `execute`
has a deadline, and the bed alone is several million samples).
The README beside one clip: what it is, and what it actually holds.
| arg | type | description |
|---|
| name | string | |
| ref | any | |
| info | any | |
| codec | string | |
bake(names: { string }, force: boolean?) → void
| arg | type | description |
|---|
| names | { string } | |
| force | boolean? | |