On this page
Sound syntax reference
Native sound files use ashfox-model 1 followed by sound name { … }.
Properties are literal name = value; records and lists. There are no samples,
imports, expressions or runtime evaluation. Unknown and duplicate keys fail.
The current contract hard-cuts the former layer grammar.
Root contract
Retain the native literal grammar (field = value;, records, lists, strings,
finite numbers and comments). No expressions or implicit symbol evaluation.
References such as voice = "whistle" are exact local IDs. All keys below are
required unless explicitly marked optional; unknown and duplicate keys fail.
All IDs follow [a-z][a-z0-9_]{0,31} and are unique within their owner. Root ID
comes from the declaration and follows this rule too.
| Root field | Contract |
|---|---|
duration |
Raw synthesis seconds, 0.05–30; finite, not inferred from events |
sampleRate |
Exactly 48000 |
seed |
Integer 1–4294967295 |
voices |
1–16 definitions, each referenced by at least one step |
sequences |
1–32 sequence definitions |
variants |
1–8 { id, seed } records; existing seed integer range |
playback |
{ kind = "oneshot"; } or { kind = "loop"; start; end; crossfade; } |
output |
{ gainDb, peakDb }; fixed gain -48..24 dB and ceiling -12..-1 dBFS |
A voice is { id, source, gain, highpass, lowpass }. Gain is linear amplitude
0..2; highpass 10..10000 Hz, lowpass 20..16000 Hz. These three controls accept
curves. A voice owns no timeline, random seed, attack, release or duration.
Duration belongs to each scheduled instance. Source-specific amplitudes such
as breath are synthesis controls; voice gain is the one articulation envelope.
1. Common parameter curves
A control is either a numeric constant or the exact record below:
pitch = {
domain = "log";
interpolation = "smooth";
points = [
{ at = 0; value = 2800; },
{ at = 0.35; value = 5400; },
{ at = 1; value = 3200; }
];
};
domain is linear or log; interpolation is linear or smooth.
There are 2–16 points, with strictly increasing at, exactly starting at 0
and ending at 1. at is normalized instance time, not root time or seconds.
Each curve interval must occupy at least 96 frames at the shortest possible
instance duration. Validate this after duration-range analysis. Duplicate or
collapsed knots fail; never sort, merge, or invent endpoint knots.
For an N-frame instance, evaluate u=i/(N-1), i=0..N-1. For segment endpoints a,b, v=(u-a.at)/(b.at-a.at); interpolation weight is v or vv(3-2*v). Interpolate values directly in linear domain, or interpolate their logarithms and exponentiate in log domain. Log values must be strictly positive. Smooth has zero slope at each knot and no overshoot; it is not a spline with hidden tangents. Values retain their parameter's bounds for the entire segment.
| Control | Accepted representation and range |
|---|---|
Voice gain |
Scalar/curve, 0..2; only linear domain (zero is meaningful) |
Voice highpass, lowpass |
Scalar/curve, ranges above; either domain |
FM pitch |
Scalar/curve, 40..3000 Hz; either domain |
FM index |
Scalar/curve, 0..4; linear domain |
Vocal pitch |
Scalar/curve, 50..1200 Hz; either domain |
Chirp pitch |
Scalar/curve, 500..8000 Hz; either domain |
Chirp brightness |
Scalar/curve, 0..1; linear domain |
| Resonator modal frequency/decay | Constants only; see resonance section |
| All other source fields | Constants with current ranges; curves rejected |
FM keeps ratio, vibratoHz, vibratoCents; vocal keeps formants,
bandwidths, breath, jitter, roughness; chirp keeps trillHz,
trillCents, trillDepth, breath, jitterCents. Noise has only kind.
These are complete discriminated source key sets together with controls in
the table and kind. sweepSeconds, two-element pitch, and contour are rejected.
FM/vocal/chirp source implementations consume the same evaluated pitch control.
Filtering remains highpass then lowpass with the existing one-pole recurrences; recalculate their stable coefficients at each sample from evaluated cutoff Hz. Validate max(highpass) < min(lowpass) over the full curves. This conservative rule deliberately rejects overlapping cutoff ranges even if their pointwise trajectories never cross; it avoids accepting a relation only some instances satisfy. The diagnostic explains how to separate the ranges.
attack and release are rejected. Authors express attack, sustain and release as
voice gain curves. No hidden fade is applied. One-shots must begin and end with
voice gain zero for every event, enforced on curve endpoints; a scalar zero
voice is valid structurally but a silent final product fails. Loop events may
use nonzero endpoints only when that endpoint lies outside or exactly on the
raw loop boundaries; an onset or termination strictly inside the loop region
still requires zero gain. That permits continuous wind while preventing abrupt
internal cuts. At least 96-frame knot separation bounds envelope transitions.
Resonator decay does not exempt an event from the envelope endpoint rule.
2. Reuse, repetition and sequences
Each sequence is { id, start, repeat, steps }:
start: nonnegative seconds from raw sound start.repeat:{ count, period }; count integer 1..64. For count=1 period must equal 0; otherwise period is positive seconds, at least one output frame.steps: 1..32{ id, voice, at, duration, gain, pitchCents, vary }records.atis a nonnegative seconds offset from each iteration's start. Duration is 0.02..30 seconds; gain is 0..2, pitchCents is -1200..1200.vary: the explicit variation record below. No optional silent defaults.
For sequence iteration k and step s, the nominal onset is
sequence.start + k*period + s.at. Each step starts its own oscillator,
filters, modulation phases and envelope. Overlap is permitted and summed,
including across repetitions. A sequence has no automatic duration, tail
extension, nested repetition, voice recursion, or tempo. period measures
start-to-start spacing, not silence after the previous voice. Silent padding
comes from root duration. Instances must fit; there is no truncation.
Each instance identity is (sequence.id, k, step.id, voice.id). Sort mix order
lexically by sequence ID, numerically by k, then lexically by step ID. Source
list reordering never changes sample summation or random assignments. Renaming
an ID intentionally changes stochastic identity. Editing a voice updates all
references. Repetition count changes do not alter existing instances.
3. Controlled random variation
Each step supplies exactly these four range records, with numeric endpoints:
vary = {
timing = [-0.008, 0.008];
pitchCents = [-25, 25];
gain = [0.92, 1.08];
duration = [0.96, 1.04];
};
The no-variation value is { timing = [0,0]; pitchCents = [0,0]; gain = [1,1]; duration = [1,1]; }. Endpoints are inclusive support bounds,
min<=max; degenerate ranges do not draw RNG. Uniform sampling uses U in [0,1),
min + (max-min)*U. No Gaussian tails, correlated humanize switch, random
knot positions, probability of omission, or arbitrary field-path modulation.
| Dimension | Range limits | Operation |
|---|---|---|
timing |
-0.5..0.5 seconds | Add to nominal onset; independent per instance, not accumulated interval drift |
pitchCents |
-1200..1200 | Add to step pitchCents, multiply source pitch by 2^(cents/1200) |
gain |
0..2 | Multiply step gain then voice gain; combined gain <=4 |
duration |
0.25..4 | Multiply step duration before frame conversion; stretches control time but not pitch |
Pitch shift applies to FM/vocal/chirp fundamental or all resonator modes; it does not move vocal formants, filters or modulation rates. Noise steps must use pitchCents=0 and vary.pitchCents=[0,0]. Random timing is relative to the nominal onset: it may reorder or overlap events, but never reorder their sum. Per-instance shifts are separate from within-note trill/jitter.
Validation proves the entire Cartesian support of these ranges is legal, not merely the chosen seeds: earliest onset >=0, latest end <=raw duration, minimum duration >=0.02, curve resolution, envelope boundary rule, finite gain, and shifted pitch within that source's documented frequency range. No clamp, reroll or silent skip. A failed variant fails the whole sound build. The endpoint rule uses the worst-case onset/end relative to loop boundaries. Zero-gain random draws can yield silence; such a selected variant fails the existing silent-output gate instead of being silently replaced.
The sound ID, root seed, variant ID/seed, sequence ID, iteration, step ID and voice ID determine each event's stochastic identity. Timing, pitch, gain, duration, noise, breath, jitter, trill phase and excitation use independent random streams. Changing one range does not consume another control's stream; disabling breath or jitter does not perturb other controls. Event pitch variation owns fixed detuning, while chirp jitter shapes drift within the note.
Reordering records and appending repetitions preserve existing event randomness. Changing a variant ID or seed affects that variant only. Renaming a sequence, step or voice intentionally changes its random identity. Adding events can change the final mixed waveform even though existing events retain their own samples. Use the same CLI sound-contract fingerprint and source to reproduce PCM16 bytes; different OGG encoder versions are not guaranteed to produce identical bytes.
4. Modal resonator source
source = {
kind = "resonator";
excitation = { kind = "impulse"; };
modes = [
{ id = "body"; hz = 640; decay = 0.45; gain = 1; },
{ id = "edge"; hz = 1730; decay = 0.22; gain = 0.35; }
];
};
modes: 1..16 unique ID records, hz 40..16000, decay 0.01..10 seconds,
gain 0..1 with at least one positive gain. Decay is T60: seconds until a
free mode's amplitude falls to 0.001 of its starting envelope. All values
are constants. Per-event pitch variation moves hz; duration variation scales
the event/envelope but never rewrites this physical decay time.
Excitation is exactly { kind = "impulse"; } or
{ kind = "noise"; duration = 0.006; }, with noise duration 0.002..0.05
seconds and no longer than the shortest event. Excitation is shared across
modes, not independently randomized per mode. No external audio excitation.
Impulse x[0]=1 and x[n>0]=0. A noise burst is seeded uniform [-1,1), multiplied
by sin(pi*n/(M-1))^2 and divided by sqrt(sum(window^2)); do not normalize each
random realization. M is quantized excitation duration, at least 96 frames.
For a mode: theta=2pihz/48000, r=10^(-3/(decay48000)). Use the impulse
response h[n]=r^nsin((n+1)*theta), n>=0, implemented by the recurrence
y[n]=2*r*cos(theta)*y[n-1]-r*r*y[n-2]+sin(theta)*x[n], initial state zero.
It fixes the gain convention without a hidden Q parameter or per-mode peak
normalizer. Sum modes in ID order with weights gain/sum(all mode gains); compute that
denominator in the same ID order. Equal or nearby mode frequencies are allowed
(no hidden merge or spacing constraint); beating is authored behavior.
The modal sum passes through voice highpass, lowpass, voice gain and event gain.
Mode frequencies and decays describe the sound; material realism still needs listening.
Frequency shift must keep every mode within 40..16000 Hz. Modal tails end at event duration and obey the explicit gain envelope; never auto-extend the root. The resonator is linear and has no nonlinear collision, feedback, frequency modulation or animated mode parameters.
5. Loop region, bake and delivery
A loop source explicitly chooses the raw interval and overlap seconds:
playback = { kind = "loop"; start = 0.4; end = 4.4; crossfade = 0.08; };
The raw sound is rendered including any warm-up before start. Samples after
end are not delivered. The interval is [S,E), where frame conversion below
applies, L=E-S and X=crossfade frames. Require 0<=S<E<=rawFrames,
X>=96, X<=min(48000,floor(L/4)) and delivered L-X>=2400 frames. Zero-crossfade
and metadata-only looping are rejected in this scope. Crossfade is fixed
smooth equal-gain (unity-sum), with no curve selection or equal-power default.
This avoids gain inflation for correlated head/tail material; uncorrelated
noise can have a dip; listen to repeated cycles to assess the result.
Bake exactly once before fixed output gain, where raw PCM is x:
- Output begins with x[S+X .. E-X), length L-2X.
- Append X samples: for k=0..X-1, u=k/(X-1), w=uu(3-2*u), append (1-w)x[E-X+k] + wx[S+k].
- Result has L-X frames, starts at raw S+X, and wraps from raw S+X-1 to raw S+X. No duplicated head, extra silent pad, sample averaging at the seam, post-bake fade-in/out, or padding back to the requested raw length.
Example: the 4-second raw interval above produces a 3.92-second loop (188160
frames), not 4 seconds. The authored duration remains raw render duration;
CLI reports both raw and delivered duration. The loop's playback origin shift
is explicit and deterministic. Intro/outro playback around a loop is out of
scope: use separate assets; do not silently preserve cropped audio.
Apply output.gainDb once to the delivered buffer, after any crossfade:
sample *= 10^(gainDb/20). rmsDb is rejected. There is no automatic RMS
normalization, limiter, clipping or variant-dependent gain correction. Those
would erase an authored gain variation, especially for a single event. RMS
and DC are measurements only; warn-level loudness advice never mutates PCM.
Compilation fails if prequantized RMS is below 1e-8, a sample is nonfinite, or
peak exceeds 10^(peakDb/20). PCM16 rounding happens once; a decoded peak above
the ceiling or an all-zero result also fails. The diagnostic reports measured
peak and suggests a lower fixed gain. Review that suggestion and edit the source;
the compiler never retries with a different gain. Leave headroom for rounding.
Receipt peak, RMS and DC describe decoded PCM16. Seam delta and maximum adjacent sample delta help inspect a loop boundary. First and last samples need not be equal: they are consecutive samples across the wrap, not a duplicated sample. Crossfade reduces discontinuity but cannot guarantee an inaudible edit for every input. Choose levels and assess seams by listening as well as measurement.
Every sound variant receipt and game runtime entry carries playback intent:
{ kind: 'oneshot' } or { kind: 'loop', startFrame: 0, endFrame: frames }.
These bounds describe baked output; raw crop and crossfade remain in source and
inspection. WAV is fmt+data PCM16 with no embedded loop instruction. When using
a standalone WAV, enable full-buffer looping in the consumer yourself.
| Delivery path | Behavior |
|---|---|
| CLI WAV export / build | Exact baked WAV; inspection and build receipt expose playback intent |
| Audio study / landing | Full-buffer loop playback; playback stops on navigation, selection, downloads and comparisons |
game_assets WAV |
Playback metadata includes validated frame bounds; configure runtime looping from the manifest |
| OGG export or game OGG pack | Loop assets are rejected because exact loop playback is not supported through this codec |
| Minecraft pack | Loop assets are rejected because sound-event delivery does not carry loop intent |
| One-shot OGG/Minecraft | Existing behavior retained with the new source contract |
Resource limits
A source is at most 256 KiB, 12000 literal tokens and 16 nesting levels. At most 1024 curve knots, 256 expanded events per variant, 32 simultaneous events and 30 raw seconds are accepted. Across all variants, event work is limited to 24 million frames and weighted DSP work to 192 million frames. Variation bounds count toward limits even when the sampled event is shorter.
A workspace may contain up to 32 sound sources and 240 seconds of raw audio across all variants. Its combined event and weighted-work limits are the same as a single sound: 24,000,000 event frames and 192,000,000 weighted frames. All sources are checked before synthesis. FM uses 8× synthesis and a fixed 20 kHz lowpass before downsampling; its work weight is 128.
Complete example
This pattern combines curves, repetition, bounded variation, resonance and a
baked loop. Save as bell_pattern.ashfox; it delivers 96960 frames (2.02 s).
ashfox-model 1
sound bell_pattern {
duration = 2.4;
sampleRate = 48000;
seed = 7349;
voices = [{
id = "bell";
source = {
kind = "resonator";
excitation = { kind = "impulse"; };
modes = [
{ id = "body"; hz = 640; decay = 0.35; gain = 1; },
{ id = "edge"; hz = 1730; decay = 0.18; gain = 0.35; }
];
};
gain = {
domain = "linear";
interpolation = "smooth";
points = [
{ at = 0; value = 0; },
{ at = 0.01; value = 0.8; },
{ at = 0.7; value = 0.5; },
{ at = 1; value = 0; }
];
};
highpass = 80;
lowpass = {
domain = "log";
interpolation = "smooth";
points = [{ at = 0; value = 8000; }, { at = 1; value = 3000; }];
};
}];
sequences = [{
id = "phrase";
start = 0.1;
repeat = { count = 4; period = 0.5; };
steps = [{
id = "strike";
voice = "bell";
at = 0;
duration = 0.4;
gain = 1;
pitchCents = 0;
vary = {
timing = [-0.005, 0.005];
pitchCents = [-20, 20];
gain = [0.9, 1.1];
duration = [0.95, 1.05];
};
}];
}];
variants = [{ id = "base"; seed = 42; }, { id = "alternate"; seed = 43; }];
playback = { kind = "loop"; start = 0; end = 2.1; crossfade = 0.08; };
output = { gainDb = -6; peakDb = -3; };
}
npx --no-install ashfox build bell_pattern.ashfox --json
npx --no-install ashfox export bell_pattern.ashfox --variant base --output bell.wav
See sound creation and listening for review and delivery.