The SCgf Binary Format¶
This document specifies the SCgf version 2 binary layout that nanosynth emits (via SynthDef.compile(), compile_synthdefs(), and nanosynth compile), plus the machine-readable UGen spec that accompanies it. Together they are the shared, language-neutral contract for anyone writing a loader, validator, or an independent SynthDef compiler in another language.
The goal is that the frontend algorithm (multichannel expansion, rate inference, constant folding) is reimplemented natively per language, while the data -- the UGen metadata table and this byte layout -- is shared, so the two cannot be transcribed by hand and drift apart. The golden SCgf fixtures under tests/fixtures/scgf/ freeze proven-correct output and serve as the byte-level cross-implementation contract.
Conventions¶
-
All multi-byte integers and floats are big-endian.
-
pstringis a Pascal-style string: a singleuint8length followed by that many ASCII bytes (no null terminator). Names are therefore limited to 255 bytes. -
Type shorthand below:
u8,u16,u32are unsigned big-endian integers;f32is an IEEE-754 big-endian float.
File layout¶
"SCgf" 4 bytes, ASCII magic
u32 version always 2
u16 synthdef_count
synthdef_count x SynthDef
SynthDef¶
pstring name
Constants
Parameters
UGens
u16 variant_count nanosynth always emits 0
The name is the identifier used with /s_new; it is independent of any filename. With --anonymous (or for an unnamed SynthDef) it is the 32-character lowercase MD5 hash of the compiled graph.
Constants¶
u32 constant_count
constant_count x f32 the constant pool
UGen inputs that are literal numbers reference this pool by index (see below).
Parameters¶
Two back-to-back tables: the flat vector of initial control values, then the named index into it.
u32 value_count sum of all control widths
value_count x f32 initial parameter values
u32 name_count
name_count x (pstring name, u32 index)
Each named parameter maps a name to its starting offset (index) in the control-value vector. Multi-channel parameters occupy several consecutive slots.
UGens¶
u32 ugen_count
ugen_count x UGen in topologically sorted order
UGen¶
pstring class_name e.g. "SinOsc", "BinaryOpUGen"
u8 calculation_rate see the calculation_rates enum
u32 input_count
u32 output_count
u16 special_index operator selector, else 0
input_count x InputSpec
output_count x u8 per-output calculation rate
special_index selects the operator for BinaryOpUGen / UnaryOpUGen (indexing the binary_operators / unary_operators enum) and is 0 for ordinary UGens.
InputSpec¶
Each input is a pair of u32. The first value distinguishes the two cases:
constant input: u32 = 0xFFFFFFFF, u32 = index into the constant pool
ugen input: u32 = source ugen index, u32 = output index on that ugen
That is the complete format. The backend that writes it is about 150 lines; the complexity of a SynthDef compiler lives entirely in the frontend that produces the sorted UGen list, the constant pool, and the control tables.
The UGen spec¶
spec/nanosynth-ugens.json is the language-neutral UGen metadata table, generated from the code by scripts/generate_ugen_spec.py and kept in sync by tests/test_ugen_spec.py. Regenerate it with:
python scripts/generate_ugen_spec.py
Top-level fields¶
-
spec_version-- schema version of this file. Bumped only when the spec's shape changes, so it is independent of the nanosynth release version. -
supercollider_version-- the SuperCollider release whose UGens and format these match. -
scgf_version-- the binary format version documented above (2). -
enums-- the wire-level integer tables:calculation_rates(token tou8),binary_operatorsandunary_operators(name tospecial_index), anddone_actions(name to value). -
operator_ugens-- the two operator UGens (see below), listed separately because theirspecial_indexis not zero. -
ugens-- the metadata table, one entry per instantiable UGen.
UGen entry¶
{
"name": "SinOsc",
"rates": ["ar", "kr"],
"flags": { "pure": true, "output": false, "width_first": false, "has_done_flag": false },
"parameters": [
{ "name": "frequency", "default": 440.0, "unexpanded": false },
{ "name": "phase", "default": 0.0, "unexpanded": false }
]
}
-
name-- theclass_namewritten into the SCgf UGen record. -
rates-- the calculation rates the UGen can be instantiated at, as tokens keyed inenums.calculation_rates. -
outputs-- the output arity. Either{ "kind": "fixed", "count": N }(most UGens;Nis 1 by default, 0 for output UGens likeOut, or a fixed width like 2 forPan2) or{ "kind": "variable", "default": N }for multichannel UGens whose output count is chosen at construction time (e.g.In), withNthe default. -
flags:-
pure-- side-effect-free, so it may be constant-folded or eliminated as dead code when its outputs are unused. -
output-- writes to a bus and produces no outputs (e.g.Out); never dead-code-eliminated. -
width_first-- must be ordered ahead of its width-first peers during the topological sort (e.g.LocalBuf, FFT-chain setup). -
has_done_flag-- exposes a done action / done flag.
-
-
parameters-- ordered input parameters.unexpandedmarks a variadic tail (a parameter that consumes all remaining channels rather than expanding, e.g. an array argument).defaultis one of:-
a number -- the numeric default;
-
null-- an explicitNonedefault; -
"required"-- no default, the caller must supply a value; -
"computed"-- derived from other parameters at construction time; -
"inf"/"-inf"/"nan"-- the corresponding non-finite float.
-
Operator UGens¶
Arithmetic operators do not each get their own UGen class. Instead, two UGens carry an operator selector in their special_index field:
{
"name": "BinaryOpUGen",
"inputs": ["left", "right"],
"output_count": 1,
"special_index_enum": "binary_operators",
"flags": { "pure": true }
}
-
name-- theclass_namein the SCgf record (BinaryOpUGenorUnaryOpUGen). -
inputs-- the fixed positional inputs (left/rightfor binary,sourcefor unary). -
output_count-- always 1. -
special_index_enum-- names the enum table (binary_operatorsorunary_operators) whose value the record'sspecial_indexholds. This is the one case wherespecial_indexis meaningful for a non-operator decoder: aclass_nameofBinaryOpUGenwithspecial_index2 is a multiplication.
Unlike the entries in ugens, operator UGens have no rate constructors -- their calculation rate is inferred from the inputs (the maximum of the input rates) -- so they carry no rates list. They are always pure.
See Deploying Precompiled SynthDefs for how to load the compiled binaries from any host language.