Toy source format 0.1.0

This specification governs the portable .toy.json document: what it contains, which references must resolve, which bounds every host enforces, and which parts are still draft. It i

This specification governs the portable *.toy.json document: what it contains, which references must resolve, which bounds every host enforces, and which parts are still draft. It is the write model shared by the browser editor, the agent authoring path, the native Player and independent exports.

Status: draft local authoring subset; schema and validator implemented on browser, native and WASM, including the v0.0.5 cable types, parameter sockets, node data, buses and node-face widgets. Video and sensing nodes validate everywhere but execute only in the browser. The authoritative structure is toy.schema.json. TypeScript types are generated from it, never maintained by hand. The schema $id is a proposed stable identifier; validation loads the bundled schema and never fetches that URL.

Document shape#

A Toy is UTF-8 JSON with format: "tune-toys" and an exact schemaVersion. It is source, not an installed binary or a published release manifest.

Top-level fieldHolds
id, revision, titleDurable Toy identity, authoring revision and presentation title
entrypoint, entryFrameThe definition that runs and the Frame that opens; entry Frame ownership must match
definitionsKeyed reusable implementations: nodes, connections, public parameters, inputs, outputs and optional loose widgets
framesKeyed explicit playable surfaces, each owned by one definition
assetsDeclared media with relative assets/ path, SHA-256, size, media type and license
styles, themeOptional portable appearance (see Appearance)
tuningOptional Toy-wide note tuning for instruments and Tuners (see Tuning)
Namespaced extensionsOptional non-executable metadata, preserved within JSON bounds and reserved-key rules
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FAFAF7', 'primaryTextColor': '#1C1C1B', 'primaryBorderColor': '#1C1C1B', 'lineColor': '#64645F', 'fontFamily': 'DM Sans, Inter, system-ui, sans-serif'}}}%%
flowchart LR
  TOY[("Toy")] --> DEF["Definition<br/>inputs · outputs · parameters"]
  DEF --> NODE["Node<br/>primitive or group instance"]
  DEF --> CONN["Connection<br/>{node, port} → {node, port}"]
  DEF --> PARAM["Public parameter<br/>targets node parameters"]
  TOY --> FRAME["Frame<br/>owned by one definition"]
  FRAME --> WIDGET["Widget<br/>binds a public parameter"]
  FRAME --> FOLDER["Layer folder"]
  FRAME -.->|"nested Frame names a group node"| NODE
  TOY --> ASSET["Asset declaration"]

  classDef source fill:#F0442E,stroke:#1C1C1B,color:#FAFAF7
  classDef shared fill:#DCEBDD,stroke:#1C1C1B,color:#1C1C1B
  classDef host fill:#DED8F0,stroke:#1C1C1B,color:#1C1C1B
  class TOY source
  class DEF,NODE,CONN,PARAM shared
  class FRAME,WIDGET,FOLDER,ASSET host

A definition may own up to 512 loose interface components in widgets, using the same Widget union and local bindings as Frame.widgets. Their bounds.x/y are absolute Patch coordinates within ±1,000,000; dimensions retain the shared widget bounds. Frame members use local coordinates inside the Frame. Folder membership and responsive Frame constraints apply to explicit Frame members. Loose components are authoring views and do not join a published Frame by proximity.

Identity and composition#

Keys are stable lowercase IDs; names and labels are presentation. A node is either a pinned draft primitive or a reference to a local group definition. A group node's key identifies its instance; the definition ID identifies reusable source. Runtime addresses derive from instance paths and are never substituted for source IDs.

Connections and ports:

  • Endpoints are {node, port}. $in is the enclosing definition's input source and $out its output destination; neither can be used in reverse.
  • Every port is a bundle of lanes of one signal type: audio (1–32 channels), video (1–8 streams), control (1–32 lanes) or midi (one stream of sixteen MIDI channels). Widths are fixed or "auto", with optional ordered labels. The shared engine executes audio, control and MIDI on every host; video runs only where a host has a video engine (the browser), and the host-only data type is described with the data cable. Manifest ports may also be signal (Math and Compare, resolved to audio or control per cable; never a definition boundary, PORT_TYPE) or any (bus.send/bus.receive only). See signal types and domains.
  • A connection needs matching types, compatible resolved lane counts and exactly one driver per destination. Mixers and conversions are explicit.
  • Feedback is rejected entirely in this draft. Delayed feedback needs a later compiler/profile contract.
  • A connection may end at a parameter socket, { node, port: "@name" }: exactly one one-lane control cable drives that parameter, its 0–1 value maps across the parameter's linear or logarithmic domain, and a public parameter aliasing the slot becomes read-only (no LFO or MIDI mapping may share it).
  • A primitive may carry bounded data the manifest admits per key: float tables (std.wavetable table, 1–8 frames of 2048 samples in −1…1; ctl.curve points, 2–32 (x, y, bend) groups with x and y in 0…1 and bend in −1…1, from the manifest's minItems and per-position fields, NODE_DATA otherwise) and short text (for example the logical label of midi.in or video.camera, the asset or source of media.player, the template of video.text). The whole object stays within 64 KiB. Data is source, hashed into releases and edited through authoring; large media stays in assets. A video.image or media.player asset must name a declared asset of the image, or video/audio, family (ASSET_REFERENCE).
  • An items data key contains an ordered list of records with unique durable ids ([a-z][a-z0-9_-]{0,31}), and optional manifest-declared bounded name, colour or icon fields. A manifest repeat block adds each item's ports and numeric parameters as <id>.<member>; parameter sockets use @<id>.<member>. std.mixer uses data.channels (0–32 items); std.eq@0.3.0 uses data.bands (1–32). Numeric DSP values remain in values, never in presentation fields. A node admits at most 288 values; a definition admits 512 public parameters. Reordering or renaming preserves all bindings and the prepared program. Removal atomically cleans up scoped values, cables, public targets and widgets; Undo restores them. See repeatable node items.
  • bus.send and bus.receive nodes carry a bus name and connect without a cable inside one definition: one Send per name, any number of Receives, type and width inherited from the Send's driver; an unnamed node is inert. The compiler lowers them to direct connections; a Receive feeding its own Send is a cycle.

Public parameters declare label, range, default, unit, linear or log scale, smoothing time and direct node-parameter targets. A group parameter targets the child's public parameter. The draft uses identity conversion and requires the public range to fit each target; nonlinear or multi-range mappings need explicit future operations. One public owner may target a node parameter. Node values are instance defaults, and enclosing public parameter defaults override their targets during preparation.

Frames#

A Frame belongs to a definition and contains explicit widgets. Geometry lays out only explicit members. A nested Frame names a group node plus a Frame of that group's definition and binds relatively to that instance.

Widget kindBinding and rules
knob, sliderOne public parameter
toggle, momentary, triggerOne public parameter with optional in-domain distinct onValue/offValue; only trigger admits pulseMs (10–2000, default 80)
xyTwo distinct public parameter IDs
filter, envelopeGraphical roles must resolve to matching parameters of one real processor, including nested aliases
faceface names a node face; parameters binds every role in FACE_BINDINGS to a distinct public control reaching that role's parameter of one node of the face's type, nested aliases included; node (that node) is required for data faces (FRAME_PARAMETER, WIDGET_SOURCE_KIND)
textInert
iconSchema-admitted bundled icon name, accessible label (empty for decorative art), bounds; optional appearance with none/currentColor/six-digit hex fill and stroke and a 0–4 stroke width
pianonode: a durable path to a midi.keyboard node, and a 2–37 key range; otherwise FRAME_PERFORMANCE
screenmode image or video with a declared asset (16 MiB), scene with a built-in scene, or live with source: {node, port} naming a video output of the Frame's definition and an optional 0-based stream for bundles and live-only tracking boolean for preview landmarks (FRAME_SOURCE, ASSET_REFERENCE)
paintsource: the video output of a video.paint node of the definition; optional color, width, opacity (WIDGET_SOURCE_KIND)
curvenode: a std.wavetable node of the definition; tableFrame 0–7 (WIDGET_SOURCE_KIND)
switchernode: a video.switch; parameters binds program, preview and transition roles to distinct public controls targeting that node (WIDGET_SOURCE_KIND, FRAME_PARAMETER)
readoutExactly one of a public parameter or a source control output with a lane; format number, percent, integer or boolean, precision, unit (FRAME_PARAMETER, WIDGET_SOURCE_KIND)
Nested FrameGroup node plus a Frame of that group's definition
Meters, presets, MIDI and transport fieldsAdmitted by the draft schema; executable semantics in creation workflows

Buttons, XY pads and graphical editors are numeric controls, not executable actions or a MIDI-note graph; see interaction semantics. Unknown icon names, external SVG and event handlers reject; see icons. face, paint, curve, switcher, readout and a live screen are faces of one node in the Frame's own definition; the validator, the editor palette and Design fields share their admission in faces.ts. A host without the service behind a face shows an unavailable state rather than a frozen picture (video).

Layers and folders:

  • Every widget may carry a layer name (1–128 characters), hidden and locked. Missing flags mean visible and unlocked. Names are independent of widget IDs, text content and control labels.
  • Hidden members are omitted from presentation at every nesting level without altering graph execution or bindings. Locks guard browser design edits; they are not an authorization boundary and do not disable live controls.
  • A Frame may hold up to 512 explicitly bound widgets, matching the 512 public-control budget so grouping a full Mixer or EQ can retain every control. Frames remain bounded to 8192 × 8192 authored units; the source-size and prepared resource limits still apply.

A Frame may hold up to 64 keyed layerFolders, each with name, sibling zIndex (0–1023) and optional parent, hidden, locked. A widget's optional folder names its parent. Folder and widget IDs are distinct within a Frame. Missing parents, cycles and depth beyond 16 fail admission on every host. Folders form contiguous sibling stacking contexts, hidden state propagates to descendants, and locks inherit without disabling playback. See layer behavior.

Responsive layout: optional Frame layout gives bounded viewport minimum and maximum sizes; widget constraints select start/end/center/stretch/proportional anchors and element limits. Authored bounds remain the reference and the shared renderer projects them. Omitted layout keeps fixed-canvas scaling. Reversed limits and unknown anchors reject. See responsive Frames.

Tuning#

An optional Toy-level tuning says how instruments map MIDI notes to pitch, so every instrument and every Tuner left on Toy tuning agree. It lives on the Toy, beside transport, rather than on a definition: a reusable instrument definition plays in whatever tuning the Toy that uses it declares, and nested groups never disagree.

FieldHolds
presetOne of 12-tet, 19-edo, 22-edo, 24-edo, 31-edo, 53-edo, just, pythagorean, bohlen-pierce (tables in packages/node-catalog/tunings.json)
centsInstead of a preset: 1–128 custom degrees in cents, 0 first, strictly ascending, each below period and at most 9600
periodWith cents: the repeat interval in cents, 1–9600 (1200 for an octave)
rootRequired. Tonic pitch class 0–11 (C = 0) of root-anchored tunings (just, Pythagorean, Bohlen–Pierce and custom); equal divisions of the octave ignore it
referenceRequired. A4 in Hz, 400–480

Exactly one of preset or cents + period is present, and the custom degree rules are semantic (TUNING); the schema bounds each field (SCHEMA). Absent means 12-TET at A4 = 440 Hz. The compiler copies the tuning into the Program and the engine resolves it once at preparation: MIDI note 60 is degree 0 (60 + root for root-anchored tunings), and degree note − 60 folds across periods. An equal division of the octave keeps A4 on the degree three of its own fifths above C; a root-anchored tuning puts its root at its 12-TET pitch from reference. 12-TET keeps reference · 2^((note − 69) / 12) in single precision, bit for bit. See std.poly and std.tuner.

Appearance#

Optional Toy and Frame theme values and element style overrides are portable presentation. A theme selects a bounded preset and/or overrides color, typography and radius tokens, plus the optional instrument-screen tokens screen, screenTrace and screenGlow (0–1); an explicit preset resets the base before its overrides. Element styles add bounded, geometry-independent visual properties. Colors are six-digit hex, fonts come from a local allowlist, and no arbitrary CSS or remote resource is admitted.

Toy-wide styles holds up to 64 named text or control definitions by durable ID, each with name, kind and bounded values. A widget styleRef must reference an existing compatible definition: text styles address text, control styles address knobs, sliders and meters. Resolved appearance merges theme, named values, then local element values. Shared values exclude layer depth, layout, content and bindings. Rename preserves identity; authoring deletion materializes effective appearance before detaching. See appearance resolution.

Bounds and diagnostics#

BoundLimit
Source bytes1 MiB UTF-8 (readToy), then JSON parse, then validateToy
JSON traversalDepth 64, 50,000 values; well-formed Unicode without NUL; lone surrogates reject
Structure64 definitions, 64 nodes and 256 connections and 256 ports per direction per definition, 64 Frames, 512 widgets per Frame, 64 assets
Semantic expansion256 expanded node instances per definition and 16 definition levels; repeated empty groups count

These are validator limits, not measured audio budgets. All definitions and Frames are checked, including unused ones. Native and WASM compilation derive structural checks from the same schema at Cargo build time and repeat semantic admission before lowering; the build generator supports the schema's closed JSON Schema 2020-12 keyword and ASCII-pattern subset and fails on anything else. Native JSON parsing runs a streaming Rust preflight first. A shared corpus compares browser, native and WASM acceptance without claiming general JSON Schema certification.

Diagnostics return stable category codes and source paths for schema, references, domains, ownership, cycles, expansion and Frame/asset declarations. Validation never mutates input or fills defaults. Unknown core fields and versions fail visibly. Unknown namespaced extension values are preserved but are never executable authority.

The maintained catalog keeps std.gain, std.lowpass, std.delay and std.oscillator at 0.1.0 fixed-stereo and 0.2.0 bundle contracts. std.crossfade@0.2.0 blends two sources; std.mixer@0.2.0 sums durable channel items. std.eq@0.3.0 uses durable band items. Other admitted types and versions come from the node catalog; engine admission remains a separate bounded profile.

Host requirements are never authored. The compiler derives requires on the prepared program (and toyRequirements() on source) from the node manifest and the root buses, so a Toy cannot claim or omit a service its graph does not use. Assets: the validator checks declaration shape, path uniqueness and traversal only. The development media/publication service verifies library-backed bytes, pinned versions, declared rights and closure bounds; full decoder and archive admission is a managed-ingestion gate.

Evolution beyond the subset#

Definition presets, parameter MIDI CC and LFO settings, Toy transport defaults, preview-asset references and Frame meters are admitted by the draft schema; their executable semantics are in creation workflows and media library. External dependencies and release locks, sized port families, MIDI 2.0, nonlinear target mappings, explicit side-effect roots and broader target profiles are future versioned contracts. Do not hide essential behavior in extensions to bypass validation. Proposed release fields live in releases.

The source version, DSP semantic version, engine build, target profile, state version and package release version are distinct. Before format v1: add migration fixtures, exact canonicalization and hash vectors, supported reader/writer rules and compatibility tests. Until then, 0.x draft revisions may change with explicit example and tool updates. Legacy .bitdsp is not automatically compatible.

Implementation#

Edit this page on GitHub

esc

↑ ↓ move↩ open⌘↩ new tabesc close