Skip to content

Soundef file reference - .soundef v0

Every .soundef file 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/on booleans, 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

KeyTypeRequiredDescription
namestring (1+ chars)yesHuman name. Used as default filename and bake prefix (<name>_baked_<seed>.wav).
versioninteger enum [0]yes0 for v0. Any other value fails with a clear error.
layersobject map name → LayeryesAt least one layer. Key is the lane name shown in the timeline. Order is display order.
effectsobjectnoForge-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.

FieldTypeDefaultDescription
sourcesstring[] (1+ items)-Required. Files for this layer. See Sources.
selectionobject{ mode: random }How to choose from sources when the layer fires. See Selection.
probabilitynumber 0..1 or { randomRange: [number, number] } or bare number string1Chance 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).
offsetscalar or randomRange in ms0msDelay from trigger to layer start. See Value forms.
gainscalar or randomRange in dB0dBLevel. Negative = quieter, positive = louder. 0dB = unity.
pitchscalar or randomRange in st0stPitch 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.

FormExampleWhat it means
Scalar string with unitgain: "-2dB" · offset: "17ms" · pitch: "-2st"Fixed value.
Bare number (implicit unit)gain: -2 · offset: 17 · pitch: -2Same 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 belowUniform 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

UnitSuffixExamplesNotes
decibelsdB-2dB, 0dB, 1.5dBFor gain, limiter.threshold/ceiling. Coerced to linear gain 10^(dB/20) at playback. Case-insensitive (db ok).
millisecondsms0ms, 17ms, 120ms, -5msFor offset, delay.time. Can be negative in spec; playback clamps as needed.
semitonesst-2st, 0st, 7st, -12st12st = 1 octave. For pitch. Playback uses 2^(st/12).
secondss1.5s, 0.3sFor reverb.decay. Case-insensitive (S ok).
unitless(none)0.7, 0.05For 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 error x-soundef-sameUnit.
  • A range's unit must match the field: gain expects dB, offset expects ms, pitch expects st.
  • Whitespace is trimmed before checking, and bare numbers emit implicit-unit warnings (they still play).

Sources

RuleWhat it meansExample
Hard sounds/-only, bare portablePath is relative to the project sounds/ folder. Never an absolute path.swords/body_01.wavMyProject/sounds/swords/body_01.wav (your project folder)
sounds/ prefix alias okNormalized 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 globsGlob 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 warnsAny 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 arrayEven 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
FieldTypeDefaultDescription
moderandom or sequencerandomrandom = uniform pick; sequence = round-robin in listed order.
avoidRepeatinteger >= 00Only 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.

PathTypeDescription
effects.reverb.amountnumber 0..1 or { randomRange: [number, number] } or bare number stringReverb wet gain, unitless. 0 = dry, 1 = max. Scalar = fixed; randomRange = per-trigger randomized via an isolated seed+"#reverb" stream.
effects.reverb.decaystring "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.dampingnumber 0..1 or { randomRange: [number, number] }High-freq damping, 0 = bright, 1 = dark. Via seed+"#reverb".
effects.delay.timestring "Nms" or bare number (ms) or { randomRange: ["Nms","Nms"] }Delay time in ms, e.g. "250ms". Via seed+"#delay".
effects.delay.feedbacknumber 0..1 or { randomRange: [number, number] }Feedback amount, 0..0.95 clamped. Via seed+"#delay".
effects.delay.mixnumber 0..1 or { randomRange: [number, number] }Wet mix, 0 = dry, 1 = full wet. Via seed+"#delay".
effects.limiter.thresholdstring "NdB" or bare number (dB) or { randomRange: ["NdB","NdB"] }Bus limiter threshold in dB, e.g. "-6dB". Via seed+"#limiter".
effects.limiter.ceilingstring "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.

  • seed is string | number (e.g. 42, "tavern-hit").
  • Each layer gets an isolated RNG seeded with seed + "#" + layerName (mulberry32(xmur3(...))). Dragging one layer's offset never changes another layer's probability/choice - and renaming a layer re-rolls that layer.
  • effects.reverb.* with randomRange uses seed + "#reverb" (shared stream for amount→decay→damping order, isolated from layers).
  • effects.delay.* uses seed + "#delay" and effects.limiter.* uses seed + "#limiter".
  • The resolved plan keeps skipped layers 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.

CodeSeverityWhen
yaml-parse-errorerrorMalformed YAML.
empty-documenterrorEmpty file.
missing-requirederrorMissing name, version, or layers.
unknown-versionerrorversion not 0.
unknown-propertyerrorUnknown top-level or layer key (except inside effects).
path-outside-soundserrorsources has /absolute or / prefix.
invalid-sourceerrorsources has .., **, dir glob (*/), or \.
x-soundef-sameUniterrorrandomRange ends have different units (e.g. [-2dB, 5ms]).
unit-mismatcherrorRange unit doesn't match field (gain got ms).
invalid-uniterrorScalar string isn't -2dB / 17ms / -2st / 1.5s etc.
no-sourceserrorLayer has no sources after glob expansion.
implicit-unitwarningBare number used where a unit suffix is canonical (gain: 11dB). Still plays.
unsupported-formatwarningExtension outside wav/ogg/oga/opus/mp3/flac/aif/aiff/m4a/aac/webm. Still parses; decode may fail.
unresolved-sourcewarningGlob matched 0 files (or no availableFiles given).
avoidRepeat-too-largewarningavoidRepeat >= sources.length (clamped).
forge-onlywarningUnknown effects.* key - Forge-only, ignored live, baked only. Alias forge-ignored.
bake-clippedwarningRender 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.56

Canonical - 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.2

With 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: 460ms

Kitchen 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 7st

See also

Soundef — declarative SFX behavior