A typed PCFG over patch terms
The genome is a term in quiver's combinator algebra, generated by a probabilistic context-free grammar whose non-terminals are signal kinds.
The representation decision
The genome is a tree: a term in quiver's Layer-1 combinator algebra. It is not a raw patch graph and not a parameter vector, and the patch graph is the compilation target.
Everything follows from this. One representation covers what usually needs three:
| Level of evolution | In the term grammar |
|---|---|
| Node settings | Leaf parameter sites — an f64 or usize draw at each module node |
| Connectivity | Interior structure — chains, parallel branches, modulation attachments |
| Node set | Which productions fire — the module choice sites |
A parameter-vector genome cannot change topology. A raw-graph genome can, but most of its mutations produce invalid graphs, so it needs a repair step, which is a second, undocumented grammar. A typed term needs neither: every sampled term compiles to a valid, sound-making patch, because the type system constrains which productions can fire where.
The sorts are quiver's signal kinds: Audio, V/Oct, Gate, CV. Auracle's
PatchTree splits into AudioNode and ModNode, and the split is enforced by
Rust's own type system: an ill-sorted term is not rejected at runtime; it
cannot be constructed.
The grammar as a probabilistic program
PatchGrammarPrior is a fugue program. Every node at tree path emits real
probabilistic choices at path-keyed addresses:
| Site | Address | Distribution |
|---|---|---|
| source-vs-processor | <p>#leaf | , forced at max depth |
| source kind | <p>#src | , 6 kinds |
| processor kind | <p>#op | , 20 kinds |
| modulation kind | <p>/m#mod | , 9 kinds |
| CV-processor kind | <p>/m#modop | Uniform over ModOp::ALL |
| CV-combiner kind | <p>/m#pairop | Uniform over PairOp::ALL |
| discrete params | <p>#wave, #oct, #color, #fkind, #table, #dmode | Uniform categoricals |
| continuous params | <p>#cut, #res, #det, … |
The amplitude envelope is fixed at amp#attack … amp#release.
The three categorical orders (7 sources, 20 processors, 9 modulation kinds) are the persisted wire format, because the codec writes the chosen index into the trace. They are append-only.
Parsimony is the prior, not a penalty
Deeper terms pay more prior mass by construction: each additional level of
recursion multiplies in another #leaf Bernoulli that came out "processor",
and each processor node draws its own parameters. There is no size penalty term
anywhere.
Ad-hoc parsimony penalties are the norm in genetic programming and a persistent source of trouble: they need tuning, they interact badly with fitness scaling, and they leave the target distribution unwritten. Here the target is written down: , and is exactly the parsimony pressure.
Modulation is a recursive sort
A modulation input does not take "an LFO". It takes a modulation term, which can itself be built from modulation terms:
- Six leaves: LFO, envelope, random (sample-and-hold), envelope follower, Euclidean, step sequencer.
Opwraps one modulation term: quantize, slew, rectify, hold.Paircombines two.
The #mod order is None, Lfo, Env, Rand, Follow, Euclid, Op, Pair, Steps.
The step sequencer is a leaf that sits after the two branches, because the
order is append-only wire format and it arrived last. So "is this kind a leaf"
is a predicate (mod_kind_is_leaf), not an index range: the range it replaced
(kind < 6) would have switched the new leaf off at the depth bound along with
the branches, and a term forced to bottom out could never have drawn it.
The step sequencer's values are latent
Steps carries eleven continuous sites: #srate, #slen, #sslew, and one
per step, #s0 … #s7. Each step value is its own
draw and plays as ;
#slen decides how many of the eight play (2 to 8, seven equal bins of the
knob). This is the Mutable Instruments Marbles design, turned into a genome:
- One site, one step. An MH proposal that moves
#s3re-voices step four and nothing else, so evolution edits a pattern the way a hand does. - Hidden, not deleted. The steps past
#slenstay in the trace. A proposal that shortens the pattern and a later one that lengthens it give back the steps that were hidden instead of inventing new ones.
The module behind it is Auracle's own (auracle_grammar::steps::StepsCv)
rather than quiver's StepSequencer, whose values are internal state with no
ports: every one of the eleven sites is a port driven by a live knob, so a bar
drag in the rack is an atomic write, not a recompile. Its clock is free-running
(#srate is steps per second), and every audition hears it
that way; tempo sync is a live-instrument concern and is not in the genome.
With the dock's sync on, the live engine snaps each sequencer's rate to the
nearest division of the tempo in octaves (straight, triplet or dotted, a
quarter-step per beat up to eight) and drives every voice's clock from one
transport through a sync port the term never sees (<key>#~sync). The
transport restarts on the first key down or on MIDI start, and it counts steps,
not bars, so a five-step pattern keeps its polymeter against a four-beat arp.
So s&h rand → quantize → slew is a legal modulation term, and the rack draws
the whole chain. Subterms live at <p>/m/0 and <p>/m/1, the same child
convention the audio tree uses; it is unambiguous because every modulation key
sits below a /m.
Its parsimony pressure is max_mod_depth, and the renormalizations that
enforce it live in mod_weights_at: at maximum depth only leaves remain
available, and below a processor the "no modulation" option is removed so a
slot that must be filled is filled.
A modulation slot hangs off every module with somewhere to send it. The
exceptions are the ones without: Noise, whose only site is a colour switch,
and Mix / RingMod, whose two inputs are both audio and whose single knob is
the blend. Having two audio children is not itself an exception: the four
dynamics productions take two subterms and carry a slot as well.
The palette
Forty-three modules: 7 sources, 20 processors, 16 modulators.
sources Vco Supersaw NoiseGenerator Wavetable KarplusStrong FormantOsc
Silence
processors Mix Filter Fold Delay Chorus Reverb Distortion Bitcrush
Phaser RingMod Flanger Tremolo Vibrato Eq Granular Shift
Comp Duck Gate Vocoder
modulators Lfo Adsr SampleAndHold SlewLimiter EnvelopeFollower …
StepsCv (Auracle's own: a step sequencer whose values are ports)
Six processors are binary:
| Production | Second input | |
|---|---|---|
Mix, RingMod | Audio | Merges two chains into one |
Comp, Duck, Gate, Vocoder | Control | Real sidechaining, in a typed tree |
A compressor's sidechain is not an audio input, and the type system makes wiring it as one impossible.
What is not in the grammar
Feedback. Terms are acyclic: there are no feedback combinator productions. Modules with internal feedback (delay, chorus, reverb) are fine, and there are plenty of them.
This is a v1 constraint rather than a principle. A tamed feedback production, with a mandatory attenuator and limiter in the loop path, is the intended v2 grammar extension. Until then cycles are unrepresentable, which is why cable dragging in the UI does not offer them.
Strict validation as an oracle
Grammar output is compiled with quiver's ValidationMode::Strict in the test
suite. Because the grammar is typed, a SignalMismatch is by construction a
bug in our grammar, so Strict doubles as a property-test oracle: sample
terms, compile all of them, and any error fails the test with quiver's
actionable message.
Patches are wired in Warn mode, though, with an allowlist test pinning the
warning classes. Strict rejects two warning-class pairings the compiler
deliberately uses, the clearest being a constant bipolar Offset feeding a
unipolar knob. The allowlist test is what keeps "we know about these two" from
quietly becoming "we ignore all warnings".
Where this comes from
The design mirrors fugue-evo's ArithmeticGrammarPrior, with quiver signal
sorts in place of arithmetic types. That is deliberate: Auracle's genome gets
subtree mutation, subtree-swap crossover, reversible-jump MH and tempered SMC
from fugue-evo unchanged, because they operate on traces and this genome's
trace encoding is faithful.