The Field modular system & creating nodes
Field is Ongenet's modular node-graph instrument and effect — a patchable "grid" where you wire small components together to build synths, effects and modulation from
scratch. It ships as both an instrument (TypeId "field", appears in the Instruments list) and an
effect (appears in the add-effect menu under Field), sharing one engine.
This guide explains how Field works and how to add your own node type.
1. How Field runs
Field is one shared engine surfaced two ways:
FieldInstrument— anIInstrument. Its output is added into the mix (polyphonic).FieldEffect— anIAudioEffect,IContextualEffect,IMidiAwareEffect,ISourceTrackEffect. It reads the track's incoming audio (an Audio In node) and processes it in place.
Both host an editable FieldGraph and run an immutable
CompiledGraph snapshot produced by the
FieldGraphCompiler. Editing the graph recompiles off
the audio path and swaps the snapshot in via a volatile reference, so it is safe to edit while playing.
Signals
Most wires are a mono block of float — audio and control/modulation share the same representation
(exactly like Grid), so any output can patch into any input. The FieldSignalKind (Audio / Cv / Note) drives
wire colour. Stereo is explicit: use a Pan node (mono → L/R) or the Voice Sum node's L/R.
Asset ports. A fourth kind, Asset, carries a resource (a loaded soundfont, a wavetable) rather than
a per-sample buffer. Asset wires allocate no audio buffers and never run on the audio thread: the compiler
resolves them once by pushing the provider's asset into the consumer
(IFieldAssetProvider.GetAsset → IFieldAssetConsumer.SetAsset). This is how the SoundFont node feeds
the Sampler node. Add new port kinds here as new resource types appear.
Modulation of any knob
Every FloatParameter on a node automatically gains a hidden CV modulation inlet. Patch any generator
(LFO, Drift, Random, Macro, an envelope, or another node's output) into it and the node reads
base + Σ(CV) × range per sample. Generators modulating generators is just CV → CV — no special case.
Polyphony (instrument)
The compiler partitions the graph into a per-voice region (nodes reachable from note sources —
oscillators, envelopes, per-voice filters) and a global region (post-mix FX). Per-voice nodes run once
per active voice; global nodes run once and sum their per-voice inputs — that summation is the
voice-collecting boundary (the Audio Out and Voice Sum nodes are always global). Voices are
allocated/stolen by FieldVoiceManager and freed a few
blocks after they fall silent.
Feedback loops are allowed: the compiler drops the back-edge from the processing order, so it reads the previous block's buffer (a one-block delay).
2. The node library
Built-in node types are registered in
FieldNodeCatalog, grouped by category
(FieldNodeCategories): I/O, Oscillators, Envelopes, Filters, Modulators, Shapers, Dynamics, Delay & Space,
Sampler, Math, Logic, Modules. Almost every node wraps a primitive from
Ongenet.Core/Audio/Dsp.
Module wrappers. Every built-in instrument and effect — and every discovered CLAP/LV2/VST/AU plugin — is
also available as a single module node (module.inst.<id> / module.fx.<id>), registered at startup by
FieldModuleNodes. So you can drop a whole Reverb, a
whole Padda, or a plugin into a graph and modulate its parameters.
Built-in patches. FieldBuiltInPatches
reconstructs every built-in instrument and effect as a Field graph — the tractable ones from primitive nodes,
the irreducible cores (grain scheduler, SFZ mapping, freeverb, pitch engines, Stuttero) via their faithful
module node. Pick them from the Preset dropdown in the Field editor.
3. The editor UI
- Middle-drag to pan, scroll to zoom (about the cursor).
- Left-drag a node to move it; click to select (its parameters show in the right inspector).
- Drag the bottom-right handle to resize a node (widen it, or enlarge its visualization area).
- Drag from a port to another port to connect; drop a wire off an input to disconnect it.
- Right-click empty space to add a node (categorised menu); right-click a node to delete/disconnect.
- Delete/Backspace removes the selected node.
- Pop out opens the full editor in its own resizable window (shares the same live graph).
On-graph visualizations. Nodes that expose a visual (FieldNode.HasVisual) render a live GPU
visualization directly on the node body: the Scope node shows the 3D waveform-trail oscilloscope, and the
Wavetable Osc node shows the morphing 3D table. These are real Engine3DVisualHost views positioned over
the canvas by the editor (FieldNodeVisuals maps a node to its IEngine3DVisualization), tracking pan/zoom
and node resize. Where no GPU is available they degrade to a placeholder, like the 3D Scope effect.
Resource nodes. The SoundFont node loads an .sfz/.sf2 and outputs it on an asset port; the
Sampler node consumes that asset and plays it (its own polyphony). The Wavetable Osc node is a
self-contained wavetable source: pick a built-in table (Basic/Harmonics/Random) or Load sample… to slice
an audio file into a table — both rebuild the table (and its 3D view) live. Load buttons appear in the
inspector when a resource node is selected.
4. Writing a new node
Subclass FieldNode. Declare ports and parameters in the
constructor, then call Build() (which appends the modulation inlets). Do per-voice DSP in ProcessBlock,
indexing per-voice state by ctx.Voice.
public sealed class TremoloCvNode : FieldNode
{
public const string Type = "example.tremolo";
public override string TypeId => Type;
public override string DisplayName => "Tremolo CV";
public override string Category => FieldNodeCategories.Modulators;
public double Rate { get; set; } = 5.0;
private Lfo[] _lfo = System.Array.Empty<Lfo>();
public TremoloCvNode()
{
AddOutput("out", "Out", FieldSignalKind.Cv);
AddParam(new FloatParameter("Rate", 0.1, 20, () => Rate, v => Rate = v, "0.0", "Hz"));
Build(); // appends a modulation inlet for "Rate"
}
public override void Prepare(AudioFormat format, int maxBlock, int voiceCount)
{
base.Prepare(format, maxBlock, voiceCount);
_lfo = new Lfo[VoiceCount];
for (var i = 0; i < VoiceCount; i++) _lfo[i] = new Lfo();
}
public override void ResetVoice(int voice) { /* reset per-voice state if needed */ }
public override void ProcessBlock(FieldRenderContext ctx)
{
var lfo = _lfo[ctx.Voice];
var outBuf = ctx.Output(0);
for (var i = 0; i < ctx.Frames; i++)
{
lfo.SetRate(ModValue(ctx, 0, Rate, i), Format.SampleRate); // reads Rate + any modulation
outBuf[i] = (float)(lfo.Next() * 0.5 + 0.5);
}
}
}
Then register it in FieldNodeCatalog.BuiltIns():
yield return new FieldNodeInfo(TremoloCvNode.Type, "Tremolo CV",
FieldNodeCategories.Modulators, () => new TremoloCvNode());
Rules of thumb:
- Allocate in
Prepare, never inProcessBlock— the audio thread must not allocate. Per-voice state is an array sizedVoiceCount; index it byctx.Voice(global nodes only ever use slot 0). - Read parameters with
ModValue(ctx, paramIndex, baseValue, sample)so knobs pick up modulation. - Set
ForceGlobal => truefor nodes that must run once and sum voices (reverbs, master utilities). - Set
IsNoteSource => truefor nodes that originate per-voice note data. - Extra state that isn't a parameter (e.g. a loaded sample, a curve) is persisted by also implementing
IProjectStatefulComponent/ISampleHost; the graph serializer captures it.
5. Persistence
The whole graph (nodes, parameters, connections, groups, canvas view, and any embedded samples) is written by
FieldGraphSerializer as the Field component's
IProjectStatefulComponent blob, so Field instruments/effects save and reload inside .ongen projects and
.ongenpreset presets like any other component.