The Sloppy app manifest (v1) and sloppy check
A Sloppy app is a Synth App for the Sloppy stack: a creator (a person or the AI builder) ships a
manifest + one Faust voice (+ an optional composition canvas); the host (Studio) supplies the
blocks (piano roll, step grid, pads, keys, automation lane, knob tiles, navigation). Design and
rationale: more-fire refactor/sloppy-stack.md (§2 the manifest, §2.3 the Faust contract, §2.8 the
harness, §3 lowering). This page is the SDK-side reference.
The bundle
<id>/
sloppy.json the manifest (schema: sdk/manifest/sloppy-app.schema.json, `"sloppy": 1`)
dsp/main.dsp the Faust voice (the Sloppy Faust contract below)
demo/preset.json param values by id ($defs/preset) -> the slot's init params
demo/pattern.json the demonstration pattern ($defs/pattern): 16th steps, keys, lengths, velocities, slide flags
canvas/canvas.json optional composition canvas ($defs/canvas): rows of xy | lane | pads | knobs | steps | phrases
samples/*.wav optional, each with a licence in assets[] (not built yet: see "Deferred")
README.md, CHANGELOG.mdEvery file but sloppy.json, README.md, CHANGELOG.md, LICENSE and demo/* is listed in
assets with its sha256 (content-addressed). Three worked examples live in sdk/examples/sloppy/:
sub-808 (mono-legato, FL slides, a DRILL canvas), dusty-kit (16 one-shots, poly steps, per-key
params, a choke group), mono-lead (mono-legato, scale-aware keys, a wobble FX, a remix).
Validation: strict authoring, tolerant loading
sloppy validate <dir> (and the first stage of check) validates sloppy.json against the schema
with unknown keys in known objects as errors, so a misspelled field never silently becomes
"optional metadata", then applies the rules the schema cannot say: unique param ids, ids ≠ 50/51
(reserved: Next Glide / Slide To), labels ≤ 8 characters, devices.* / layout.sound / fx /
stepLocks referring to existing params, roll needing a pitched app or labelled keys, every asset
present with its sha256, no file outside assets, and requires naming what the manifest uses
(poly-steps, slides, choke, per-key-params, canvas:composition, each block). A host
loading a published package validates with strict: false (unknown optional fields are ignored) and
refuses only on requires it lacks, by name. Studio's capabilities: steps roll keys auto slides poly-steps per-key-params choke canvas:composition (STUDIO_CAPABILITIES; mirrored in more-fire's
:wire SloppyManifest.kt).
The validator (sdk/sloppy/src/schema.ts) implements the JSON Schema 2020-12 subset the schema
uses; the test fails if the schema ever uses a keyword it does not know.
sloppy check <dir>: the gate
og sloppy check sdk/examples/sloppy/sub-808 (or: sloppy check …; --fast for iteration; --json; --out <dir>)| Stage | What | Fails when |
|---|---|---|
| schema | the validation above | any problem |
| faust | the pinned faust 2.79.3 (-lang c -single -ftz 1 -json), the source scan of the Faust contract (no ffunction/fconstant/fvariable, only standard libraries, no [midi:on], no OSC, no soundfile() yet), the licence audit (GPL/AGPL code in the output fails) |
compile error, forbidden primitive, copyleft |
| crosscheck | the manifest against the compiled descriptor: every control exists; gate is a button; the pitch control's range covers the keys (Hz: the pitch domain, or 20 Hz..8 kHz for melodic apps; midi: the domain; key-index: 0..n-1); glideTime is milliseconds and allows 0; each param's [min,max] inside the DSP's, units agree with [unit:], defaults in range; a lite target must lower with nothing dropped |
any mismatch |
| build | gen/sloppy_app.h (the manifest's og_param_info table in plain units + 50 Next Glide when the app slides, roles, voice policy, key domain, choke groups, the demo pattern) + gen/main_faust.h (og faust) + og/faust/fog_sloppy.h → og build → build/fw/build/<id>.wasm (≤ 16 MiB) |
compile/link error |
| verify | og verify --profile generic,kick,fire: boots, audio on pad presses, fuzz, deadline, levels, state round trip, params |
any checked row false |
| render | 2 bars of the demo pattern (or, with demo: null, the default key on every beat) at the demo's tempo, at the default preset: audible (peak > −60 dBFS) and no clipping (no sample at or above −0.01 dBFS); in --fast too |
silent or clipping |
| sound | headless on the firmware (HeadlessDevice, the og render path): every key of the pitch domain (or three octaves around the default key) is audible (> −60 dBFS) and, for pitched apps, in tune within ±2 cents (normalised autocorrelation + parabolic interpolation, measured 150–400 ms into the note); velocity monotonic in RMS (30/70/110/127); a slide (param 50 = 250 ms, then 51 = key + 7 at the same frame) lands within ±2 cents of the target, is linear in semitones at the midpoint, and shows no gate edge (no > 6 dB dip); one-shots ignore the release; no stuck notes: 2000 seeded gestures (notes, params, slides, pads), params back to default, everything released, then silence (< −60 dBFS 3 s later) or a still-falling tail |
any check |
| matrix | the portability matrix (below): every target the creator chose, the same phrase | a cell that ran and failed |
| lower | the lossy-lowering report for og and lite (below) |
reported only (sloppy lower --strict exits 1 on a drop) |
The report is build/sloppy-report.json (every stage with ms, errors, warnings; the package
sha256; built: { faust, og_abi, sloppy_tools }; the checked record; the sound report; the
lowering rows). The badge is one line that names what was tested, never just "ok":
sloppy check ✓ aran.sub-808@1.0.0 · sha256 ee317d2706b5 (20.7 KiB) · faust 2.79.3 · og-abi 0.1 · sloppy-tools 0.1.0 · og verify 3/3 profiles · keys 37/37 in tune (max 0.0¢) · slide lands (0¢) · velocity monotonic · no stuck notes (2000 gestures, tail -240 dBFS) · lowers to lite--fast (iteration in the builder) stops after the build and a 2 s smoke render of the demo
pattern. The AI builder (roadmap E5) calls exactly this command and feeds errors back to the model.
sloppy check is also an og-check suite (scripts/og-check --only sloppy; in the full tier, and in
fast when sdk/sloppy, sdk/manifest, sdk/examples/sloppy, og/faust/*.h or the Faust CLI change):
one row per bundle under sdk/examples/sloppy/, the badge as the metric, every report under the suite's
artifacts. It needs faust 2.79.3 (the build box has it; GitHub's runners skip it, like showcase).
One format, four readers
The schema is the superset every reader of a manifest shares; never fork it per host:
| Reader | Reads |
|---|---|
Studio (more-fire :wire SloppyManifestWire, :core ManifestSynthApp) |
everything but plugin; tolerant loading, refuses on requires only |
the web slot (og-slot/slot-host.mjs) and the native slot (slot_runner.h) |
nothing of the manifest itself: they run the firmware sloppy check built; the param table is in the firmware (og_param_info) and cached in the project (SynthAppSlotWire.paramInfoCache) |
the plugin shell (plugin/, tools/gen_params.py) |
engine.* (the Faust roles, params with stable ids, slide.live), devices.*, layout.*, plugin |
the hub (planned, registry kind sloppy-app) |
identity, tags, targets, requires, assets, the report |
Two fields exist for the plugin target: engine.slide.live (a parameter of its own, id + label +
range in ms: the legato glide applied when LIVE notes overlap without a parked Next Glide; the sequenced
slide never uses it; Studio's live pads do not use it yet, sloppy lower reports it substituted) and
plugin (id reverse-DNS, name, vendor, url, auv2 four-character codes; stable once
published, like param ids; only when targets is omitted or names plugin). demo may be null.
Two more top-level fields (founder, 2026-10-29): kind instrument (default: keys in, audio out) |
effect (audio in -> audio out, an insert in a track's chain; readers refuse any other value; the slots
have no audio-in yet, so sloppy check validates an effect's manifest and DSP and then stops with a
clear error, and Studio refuses to mount one as a track instrument), and targets: the targets the
creator chose to build for, plugin | kick | phone, omitted = all; sloppy check runs only those
columns of its matrix (plus always the web render). budget.cpu (default 0.5) is the render's CPU
time as a fraction of real time a target may spend.
The portability matrix (sloppy check, founder 2026-10-29)
sloppy check is the portability gate: one column per target, one row per gate, printed after the
stages and in report.stages.matrix:
| web | native-slot | plugin-shell | |
|---|---|---|---|
| what | Faust → wasm, the og-slot render (HeadlessDevice); what every target shares, always run |
og-run (wasmtime) on the same wasm; the KICK (targets: kick); needs runtime-native/build/og-run, else SKIP |
Faust → C++ in plugin/ (targets: plugin); macOS only; the E4 spike builds ONE app, so it runs only for that DSP (plugin/build/sloppy-render-host + the .clap), else SKIP |
| compiles | the wasm built | loads in wasmtime | the .clap renders |
| param-ids | og_param_info ids == the manifest's (+ 50) |
the same, from og-run's stats | pinned by gen_params.py / clap-validator (SKIP) |
| render | 2 bars of the demo at its tempo as MIDI (slides as og 50 before the note; no demo: the default key on every beat), audible, no clipping | same phrase, same gate | same phrase through the render host |
| parity | the reference (the first target that rendered) | plugin/tools/parity/compare.py vs the reference: RMS within 0.1 dB, onsets within 1 sample |
same |
| cpu | render CPU time / real time ≤ budget.cpu (default 0.5) |
og-run's realtime_factor ≤ budget |
host wall time / real time ≤ budget |
A cell that cannot run is · SKIP with its reason (once, as a footnote); the check exits non-zero only
on a cell that ran and failed. The WAVs and compare.py reports are under build/matrix/.
The firmware a manifest becomes (og/faust/fog_sloppy.h)
One og ABI 0.1 instrument around N instances of the Faust voice, driven like 808-slide's external
mode: MIDI notes are the keys (pitch unit hz | midi | key-index), velocity is written to the
velocity zone once per hit before the gate (a DSP must not smooth it), OG_PARAM_CHANGE on a
manifest id writes that zone sample-accurately on every voice, 50/51 implement the FL slide
(glide_time before the pitch target, no gate edge). Voice modes: mono-legato (one voice; glide
0 over a sounding note = legato),
poly(same key → free → oldest),one-shot(the gate is a one-sample trigger, the release is ignored, choke groups cut the other members at the same frame). A voice that has gone silent with its gate low is parked (not computed) until its next note, so a 16-voice kit costs only what sounds. Pads play the keys; PLAY runs the demo pattern (slides included) at the host tempo; state = the params by id under the manifest's state schema (unknown ids dropped, missing ids default). No display: the host owns the UI.
The Sloppy Faust contract (short)
One voice, 1–2 outputs, no inputs, declare options "[midi:off]", standard libraries only, no
foreign functions. Controls the manifest binds: gate (a button), the pitch control ([unit:Hz]
or a MIDI key or a key index), the velocity control (0..1, not smoothed), glide_time ([unit:ms],
allows 0) for slides; the manifest's params are the other sliders/nentries (units via [unit:],
[scale:log|exp] is the host's curve hint). A one-shot DSP owns its envelope and must not rely on
the gate's length (hold the trigger for its attack: see dusty-kit/dsp/main.dsp). The slide idiom
(linear in semitones, landing exactly glide_time later) is in sub-808/dsp/main.dsp.
sloppy lower <dir> --target og|lite
The lossy-lowering report: each manifest part is mapped (an exact twin), substituted (a stand-in
exists: the roll becomes the step grid of the focused key, poly steps become one track per key, Free
lanes become stepped locks) or dropped (lost: a wasm canvas or a declared soundfile on this
toolchain, a > 64-step demo on lite). A report, not a gate (--strict exits 1 on a drop). The three
examples lower to lite with nothing dropped ((b) substitutes poly steps).
Deferred (sloppy-tools 0.1)
engine.slide.livein the og firmware (Studio / Lite live pads): declared and reported, the firmware's table does not carry it yet (sequenced slides through 50/51 work everywhere).engine.soundfile: schema-valid and reported, not built (Faustsoundfile()needs the og asset binding in the wrapper).dusty-kitships a synthesised kit for now.perKeyparams: the firmware applies a param to every voice; the manifest marks the intent and the host edits all keys (the Drum Rack'spad:<i>:<param>storage is the next step).Param-id reuse / unit-meaning checks against the previously published version (needs the hub).
The hub registry kind
sloppy-app(source bundle in, firmware built by the hub).