Appearance
Soundef file reference - .soundef v0
Every
.soundeffile is YAML 1.2 Core Schema. Any JSON document is valid. Canonical schema: /schemas/soundef.v0.json. This page mirrors it.
At a glance
yaml
name: sword_hit # string, required
version: 0 # integer, must be 0
layers: # at least one layer, ordered
body:
sources:
- swords/body_01.wav
- swords/body_02.wav
selection:
mode: random
avoidRepeat: 2
gain:
randomRange: [-2dB, 0dB]
pitch:
randomRange: [-2st, 2st]
metal:
sources:
- swords/metal_01.wav
probability: 0.7
offset:
randomRange: [5ms, 25ms]
gain:
randomRange: [-4dB, -1dB]
sparkle:
sources:
- swords/sparkle.wav
probability: 0.2
offset:
randomRange: [40ms, 90ms]
gain:
randomRange: [-8dB, -4dB]
effects: # optional, Forge-only (baked includes it, live may skip; users may implement it)
reverb:
amount: 0.2 # or { randomRange: [0.05, 0.35] }JSON is valid too - same keys, same validation:
json
{
"name": "sword_hit",
"version": 0,
"layers": {
"body": { "sources": ["swords/body_01.wav"], "gain": { "randomRange": ["-2dB", "0dB"] } }
}
}File type
- YAML 1.2 Core Schema only. No
yes/onbooleans, no custom tags, no anchors-as-logic. - Any JSON document is valid YAML 1.2. JSON tools work.
- Comments and key order are preserved by the editor so your formatting survives saves.
Top-level keys
| Key | Type | Required | Description |
|---|---|---|---|
name | string (1+ chars) | yes | Human name. Used as default filename and bake prefix (<name>_baked_<seed>.wav). |
version | integer enum [0] | yes | 0 for v0. Any other value fails with a clear error. |
layers | object map name → Layer | yes | At least one layer. Key is the lane name shown in the timeline. Order is display order. |
effects | object | no | Forge-only, optional and ignorable live - baked render includes it; users may implement it. See Effects. Omit if you don't need them. |
No other top-level keys are allowed.
Layers - layers.<name>
Each layer is one lane on the timeline. Values describe how to pick and place one file when the sound triggers.
| Field | Type | Default | Description |
|---|---|---|---|
sources | string[] (1+ items) | - | Required. Files for this layer. See Sources. |
selection | object | { mode: random } | How to choose from sources when the layer fires. See Selection. |
probability | number 0..1 or { randomRange: [number, number] } or bare number string | 1 | Chance the layer plays at all. Roll is per-layer and per-trigger. With randomRange, the effective probability is itself randomized each trigger (e.g. {randomRange:[0.5,0.9]} → lerp then roll). |
offset | scalar or randomRange in ms | 0ms | Delay from trigger to layer start. See Value forms. |
gain | scalar or randomRange in dB | 0dB | Level. Negative = quieter, positive = louder. 0dB = unity. |
pitch | scalar or randomRange in st | 0st | Pitch shift in semitones. 12st = one octave. Source duration scales as duration / 2^(pitch/12). |
No other layer keys are allowed.
Value forms
offset, gain, pitch each accept three written forms that mean the same thing - pick whichever reads best. The parser is permissive; the examples use block style.
| Form | Example | What it means |
|---|---|---|
| Scalar string with unit | gain: "-2dB" · offset: "17ms" · pitch: "-2st" | Fixed value. |
| Bare number (implicit unit) | gain: -2 · offset: 17 · pitch: -2 | Same as above - warning implicit-unit is emitted, canonical is with unit. Keeps old files playing instead of failing. |
| Random range (strings) | gain: { randomRange: ["-2dB", "0dB"] } or block form below | Uniform lerp(min, max, rng) in the unit domain. |
Block style (same as inline, just more readable for ranges):
yaml
gain:
randomRange: [-2dB, 0dB]
offset:
randomRange: [5ms, 25ms]
pitch:
randomRange: [-2st, 2st]Inline style also valid:
yaml
gain: { randomRange: [-2dB, 0dB] }probability and unitless effects (reverb.amount, reverb.damping, delay.feedback/mix) use the same randomRange idea but unitless numbers:
yaml
probability: 0.7
probability: { randomRange: [0.5, 0.9] } # randomize the chance itself per trigger
effects:
reverb:
amount: 0.2
# or
amount: { randomRange: [0.05, 0.35] }
decay: "1.5s" # or { randomRange: ["1.2s", "2.0s"] }
damping: 0.3 # or { randomRange: [0.2, 0.5] }
delay:
time: "250ms" # or { randomRange: ["180ms", "320ms"] }
feedback: 0.35
mix: 0.25
limiter:
threshold: "-6dB"
ceiling: "-0.1dB"Units
| Unit | Suffix | Examples | Notes |
|---|---|---|---|
| decibels | dB | -2dB, 0dB, 1.5dB | For gain, limiter.threshold/ceiling. Coerced to linear gain 10^(dB/20) at playback. Case-insensitive (db ok). |
| milliseconds | ms | 0ms, 17ms, 120ms, -5ms | For offset, delay.time. Can be negative in spec; playback clamps as needed. |
| semitones | st | -2st, 0st, 7st, -12st | 12st = 1 octave. For pitch. Playback uses 2^(st/12). |
| seconds | s | 1.5s, 0.3s | For reverb.decay. Case-insensitive (S ok). |
| unitless | (none) | 0.7, 0.05 | For probability, reverb.amount/damping, delay.feedback/mix. No suffix. |
Rules:
- Ranges must have the same unit on both ends:
[-2dB, 0dB]ok,[-2dB, 5ms]is errorx-soundef-sameUnit. - A range's unit must match the field:
gainexpectsdB,offsetexpectsms,pitchexpectsst. - Whitespace is trimmed before checking, and bare numbers emit
implicit-unitwarnings (they still play).
Sources
| Rule | What it means | Example |
|---|---|---|
Hard sounds/-only, bare portable | Path is relative to the project sounds/ folder. Never an absolute path. | swords/body_01.wav → MyProject/sounds/swords/body_01.wav (your project folder) |
sounds/ prefix alias ok | Normalized to bare. | sounds/swords/body_01.wav also valid, same file |
No .., no /absolute, no \ | Escaping the project root is rejected. | ../other/file.wav → error; /home/me/file.wav → error; swords\body.wav → error |
No **, no directory globs | Glob only in the basename (last segment). | swords/body_*.wav ok; swords/*/*.wav rejected; **/*.wav rejected |
* and ? basename globs | * = any run, ? = single char, in the filename only. | impact/footstep_wood_00*.ogg matches footstep_wood_001.ogg etc. |
| Extension-agnostic in schema, but Studio warns | Any extension passes schema, but Studio warns on unsupported-format if outside supported set. Supported first-class: wav, ogg, oga, opus, mp3, flac, aif, aiff; best-effort: m4a, aac, webm. Bake skips undecoded layers with a warning. | alert.wma → warning |
| Always an array | Even one file is a one-item list. | sources: ["swords/body_01.wav"] not sources: "swords/body_01.wav" |
Diagnostics for bad paths surface as squiggles in the editor and as errors in parse(): path-outside-sounds (absolute), invalid-source (.., **, dir-glob, \).
Selection
Controls which file from sources is chosen each trigger.
yaml
selection:
mode: random # or sequence
avoidRepeat: 2| Field | Type | Default | Description |
|---|---|---|---|
mode | random or sequence | random | random = uniform pick; sequence = round-robin in listed order. |
avoidRepeat | integer >= 0 | 0 | Only for random. Don't repeat the last N picks (retry draws from RNG, bounded). Clamped to sources.length - 1; warns avoidRepeat-too-large if larger and no glob is present. |
avoidRepeat retries consume RNG so determinism accounts for them.
Effects
effects is Forge-only - optional and ignorable live. A live runtime may skip it; a baked render includes it. Users may implement it in their own runtime. No wrapper key like forge: - effects itself signals Forge custom. See forge-only in Diagnostics.
| Path | Type | Description |
|---|---|---|
effects.reverb.amount | number 0..1 or { randomRange: [number, number] } or bare number string | Reverb wet gain, unitless. 0 = dry, 1 = max. Scalar = fixed; randomRange = per-trigger randomized via an isolated seed+"#reverb" stream. |
effects.reverb.decay | string "Ns" (s seconds, e.g. "1.5s") or bare number (seconds) or { randomRange: ["Ns","Ns"] } | Reverb tail length in seconds, 0.1..10. 1.5s = default. randomRange lerped via seed+"#reverb". |
effects.reverb.damping | number 0..1 or { randomRange: [number, number] } | High-freq damping, 0 = bright, 1 = dark. Via seed+"#reverb". |
effects.delay.time | string "Nms" or bare number (ms) or { randomRange: ["Nms","Nms"] } | Delay time in ms, e.g. "250ms". Via seed+"#delay". |
effects.delay.feedback | number 0..1 or { randomRange: [number, number] } | Feedback amount, 0..0.95 clamped. Via seed+"#delay". |
effects.delay.mix | number 0..1 or { randomRange: [number, number] } | Wet mix, 0 = dry, 1 = full wet. Via seed+"#delay". |
effects.limiter.threshold | string "NdB" or bare number (dB) or { randomRange: ["NdB","NdB"] } | Bus limiter threshold in dB, e.g. "-6dB". Via seed+"#limiter". |
effects.limiter.ceiling | string "NdB" or bare number (dB) or { randomRange: ["NdB","NdB"] } | Output ceiling in dB, e.g. "-0.1dB" (default mirrors bake hard limiter). Via seed+"#limiter". |
Unknown keys inside effects are allowed (additionalProperties: true) and surface as forge-only warnings (alias forge-ignored), not errors - ignored live, baked only.
Render details: reverb adds a tail (decay seconds, damping controls brightness), delay adds an echo (time, feedback, mix), limiter controls loudness (threshold, ceiling). Chain order is layers sum → delay → reverb → limiter → output in both live and baked render. effects is optional - Forge uses it for baked render, live may skip (users may implement it). All effect tails extend the baked duration (capped 10s).
Determinism
definition + seed → resolved playback plan. Same .soundef and same seed always produce the same plan; different seed gives a controlled variation.
seedisstring | number(e.g.42,"tavern-hit").- Each layer gets an isolated RNG seeded with
seed + "#" + layerName(mulberry32(xmur3(...))). Dragging one layer'soffsetnever changes another layer's probability/choice - and renaming a layer re-rolls that layer. effects.reverb.*withrandomRangeusesseed + "#reverb"(shared stream foramount→decay→dampingorder, isolated from layers).effects.delay.*usesseed + "#delay"andeffects.limiter.*usesseed + "#limiter".- The resolved plan keeps
skippedlayers marked{ skipped: true, probabilityRoll }so the timeline can show ghosts (dashed) while playback/bake skips them.
Diagnostics
A validator should return diagnostics with { severity: "error" | "warning", message, code?, range? } - Forge maps them to editor squiggles. error blocks the plan; warning still plays with a note. See Integration.
| Code | Severity | When |
|---|---|---|
yaml-parse-error | error | Malformed YAML. |
empty-document | error | Empty file. |
missing-required | error | Missing name, version, or layers. |
unknown-version | error | version not 0. |
unknown-property | error | Unknown top-level or layer key (except inside effects). |
path-outside-sounds | error | sources has /absolute or / prefix. |
invalid-source | error | sources has .., **, dir glob (*/), or \. |
x-soundef-sameUnit | error | randomRange ends have different units (e.g. [-2dB, 5ms]). |
unit-mismatch | error | Range unit doesn't match field (gain got ms). |
invalid-unit | error | Scalar string isn't -2dB / 17ms / -2st / 1.5s etc. |
no-sources | error | Layer has no sources after glob expansion. |
implicit-unit | warning | Bare number used where a unit suffix is canonical (gain: 1 → 1dB). Still plays. |
unsupported-format | warning | Extension outside wav/ogg/oga/opus/mp3/flac/aif/aiff/m4a/aac/webm. Still parses; decode may fail. |
unresolved-source | warning | Glob matched 0 files (or no availableFiles given). |
avoidRepeat-too-large | warning | avoidRepeat >= sources.length (clamped). |
forge-only | warning | Unknown effects.* key - Forge-only, ignored live, baked only. Alias forge-ignored. |
bake-clipped | warning | Render peaked over 0dB; baked file has a hard limiter at -0.1dB + red clip dot. |
Examples
All examples validate against /schemas/soundef.v0.json. Minimal shows the smallest valid file; canonical is sword_hit.
Minimal
yaml
name: minimal
version: 0
layers:
hit:
sources:
- hit.wav
probability: 0.56Canonical - sword_hit (layers + selection + gain/pitch/offset + reverb)
This is the canonical sword_hit:
yaml
name: sword_hit
version: 0
layers:
body:
sources:
- swords/body_01.wav
- swords/body_02.wav
- swords/body_03.wav
selection:
mode: random
avoidRepeat: 2
gain:
randomRange: [-2dB, 0dB]
pitch:
randomRange: [-2st, 2st]
metal:
sources:
- swords/metal_01.wav
- swords/metal_02.wav
selection:
mode: random
avoidRepeat: 1
probability: 0.7
offset:
randomRange: [5ms, 25ms]
gain:
randomRange: [-4dB, -1dB]
pitch:
randomRange: [-1st, 1st]
sparkle:
sources:
- swords/sparkle.wav
probability: 0.2
offset:
randomRange: [40ms, 90ms]
gain:
randomRange: [-8dB, -4dB]
effects:
reverb:
amount: 0.2With randomized reverb (same shape, randomRange form):
yaml
effects:
reverb:
amount: { randomRange: [0.05, 0.35] }
decay: { randomRange: ["1.0s", "2.2s"] }
damping: { randomRange: [0.1, 0.5] }Full effects (reverb + simple delay + limiter):
yaml
effects:
reverb:
amount: 0.2
decay: "1.5s"
damping: 0.25
delay:
time: "250ms"
feedback: 0.3
mix: 0.2
limiter:
threshold: "-6dB"
ceiling: "-0.1dB"Glob + randomized probability
yaml
name: footstep_suite
version: 0
layers:
body:
sources:
- impact/footstep_wood_00*.ogg
selection:
mode: random
avoidRepeat: 1
gain: 1 # bare number → warning implicit-unit, plays as 1dB
pitch:
randomRange: [-1st, 1st]
carpet:
sources:
- impact/footstep_carpet_00*.ogg
probability: 0.6
gain:
randomRange: [-4dB, -1dB]
offset: 460msKitchen sink - all field types
yaml
name: impact_heavy
version: 0
layers:
bell:
sources:
- impact/impactBell_heavy_00*.ogg
- impact/footstep_carpet_003.ogg
selection:
mode: sequence
gain: 14dB
pitch:
randomRange: [-1st, 1st]
offset:
randomRange: [0ms, 5ms]
generic:
sources:
- impact/impactGeneric_light_00*.ogg
probability: 0.8
gain:
randomRange: [-4dB, -1dB]
offset:
randomRange: [8ms, 20ms]
glass:
sources:
- impact/impactGlass_heavy_00*.ogg
probability: 1
gain:
randomRange: [-6dB, -2dB]
pitch:
randomRange: [-12st, -10st]
offset:
randomRange: [15ms, 35ms]
effects:
reverb:
amount: 0.18
decay: "1.8s"
damping: 0.3
delay:
time: "220ms"
feedback: 0.25
mix: 0.2
limiter:
threshold: "-6dB"
ceiling: "-0.1dB"Invalid - path and unit errors (do not validate)
yaml
name: bad
version: 0
layers:
body:
sources:
- /home/user/sounds/body.wav # error path-outside-sounds
- "swords/body_01.wav"
gain:
randomRange: [-2dB, 5ms] # error x-soundef-sameUnit
extra:
sources:
- "foley/sfx.wav"
gain:
randomRange: [-20dB, -5dB]
pitch: 7 # warning implicit-unit - plays as 7stSee also
- Getting Started - open a project, edit → preview loop, bake and share.
- FAQ - offset vs delay, mixer, paths, auto-save.
- Integration - turn a
.soundef+seedinto aPlaybackPlanand map it to audio; the schema is at /schemas/soundef.v0.json. - Examples - 11 soundefs that cover every field.