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 field | Holds |
|---|---|
id, revision, title | Durable Toy identity, authoring revision and presentation title |
entrypoint, entryFrame | The definition that runs and the Frame that opens; entry Frame ownership must match |
definitions | Keyed reusable implementations: nodes, connections, public parameters, inputs, outputs and optional loose widgets |
frames | Keyed explicit playable surfaces, each owned by one definition |
assets | Declared media with relative assets/ path, SHA-256, size, media type and license |
styles, theme | Optional portable appearance (see Appearance) |
tuning | Optional Toy-wide note tuning for instruments and Tuners (see Tuning) |
| Namespaced extensions | Optional 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 hostA 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}.$inis the enclosing definition's input source and$outits output destination; neither can be used in reverse. - Every port is a bundle of lanes of one signal type:
audio(1–32channels),video(1–8streams),control(1–32lanes) ormidi(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-onlydatatype is described with the data cable. Manifest ports may also besignal(Math and Compare, resolved to audio or control per cable; never a definition boundary,PORT_TYPE) orany(bus.send/bus.receiveonly). 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-lanecontrolcable 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
datathe manifest admits per key: float tables (std.wavetabletable, 1–8 frames of 2048 samples in −1…1;ctl.curvepoints, 2–32(x, y, bend)groups with x and y in 0…1 and bend in −1…1, from the manifest'sminItemsand per-positionfields,NODE_DATAotherwise) and short text (for example the logicallabelofmidi.inorvideo.camera, theassetorsourceofmedia.player, thetemplateofvideo.text). The whole object stays within 64 KiB. Data is source, hashed into releases and edited through authoring; large media stays inassets. Avideo.imageormedia.playerassetmust name a declared asset of the image, or video/audio, family (ASSET_REFERENCE). - An
itemsdata key contains an ordered list of records with unique durableids ([a-z][a-z0-9_-]{0,31}), and optional manifest-declared bounded name, colour or icon fields. A manifestrepeatblock adds each item's ports and numeric parameters as<id>.<member>; parameter sockets use@<id>.<member>.std.mixerusesdata.channels(0–32 items);std.eq@0.3.0usesdata.bands(1–32). Numeric DSP values remain invalues, 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.sendandbus.receivenodes carry abusname 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 kind | Binding and rules |
|---|---|
knob, slider | One public parameter |
toggle, momentary, trigger | One public parameter with optional in-domain distinct onValue/offValue; only trigger admits pulseMs (10–2000, default 80) |
xy | Two distinct public parameter IDs |
filter, envelope | Graphical roles must resolve to matching parameters of one real processor, including nested aliases |
face | face 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) |
text | Inert |
icon | Schema-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 |
piano | node: a durable path to a midi.keyboard node, and a 2–37 key range; otherwise FRAME_PERFORMANCE |
screen | mode 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) |
paint | source: the video output of a video.paint node of the definition; optional color, width, opacity (WIDGET_SOURCE_KIND) |
curve | node: a std.wavetable node of the definition; tableFrame 0–7 (WIDGET_SOURCE_KIND) |
switcher | node: a video.switch; parameters binds program, preview and transition roles to distinct public controls targeting that node (WIDGET_SOURCE_KIND, FRAME_PARAMETER) |
readout | Exactly 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 Frame | Group node plus a Frame of that group's definition |
| Meters, presets, MIDI and transport fields | Admitted 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),hiddenandlocked. 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.
| Field | Holds |
|---|---|
preset | One of 12-tet, 19-edo, 22-edo, 24-edo, 31-edo, 53-edo, just, pythagorean, bohlen-pierce (tables in packages/node-catalog/tunings.json) |
cents | Instead of a preset: 1–128 custom degrees in cents, 0 first, strictly ascending, each below period and at most 9600 |
period | With cents: the repeat interval in cents, 1–9600 (1200 for an octave) |
root | Required. Tonic pitch class 0–11 (C = 0) of root-anchored tunings (just, Pythagorean, Bohlen–Pierce and custom); equal divisions of the octave ignore it |
reference | Required. 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#
| Bound | Limit |
|---|---|
| Source bytes | 1 MiB UTF-8 (readToy), then JSON parse, then validateToy |
| JSON traversal | Depth 64, 50,000 values; well-formed Unicode without NUL; lone surrogates reject |
| Structure | 64 definitions, 64 nodes and 256 connections and 256 ports per direction per definition, 64 Frames, 512 widgets per Frame, 64 assets |
| Semantic expansion | 256 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#
- packages/toy-format — schema, generated types, validator and fixtures.
- crates/compiler — generated structural checks and semantic admission for native and WASM.
- examples — gain, nested gain, Warm Echo and Little Sine all run through the shared Rust engine.
- tests/conformance — cross-host acceptance corpus.
Related#
- Frames and UI composition and appearance.
- Revisioned authoring operations are the only write path to this document.
- Decision 0001: one portable model.