What this is. A compact, JSON‑based loop format for vibe‑coding musical ideas with large language models (LLMs), quick human edits, and reliable MIDI playback on the OP‑XY. One file = one loop. You can regenerate, tweak, and audition rapidly without wrestling DAW projects or opaque binary formats.
What you can do with it.
- Generate with an LLM: Keys are stable, field names are explicit, and defaults are predictable—so models can create and modify patterns safely.
- Edit by hand or UI: The structure maps cleanly to a step grid, plus lanes for CC and LFO modulation.
- Play it back: A lightweight runtime reads the JSON and schedules notes/automation to the OP‑XY over MIDI.
End goals.
- Fast idea‑to‑sound loop for composition and prototyping.
- Deterministic playback with minimal hidden state—easy to diff, version, and share.
- Musical context aware: Optional key/mode for degree‑based notes and Roman‑numeral chords.
- Device‑friendly: Keep OP‑XY specifics (fixed CC mapping, engines) out of the data where possible.
Scope. This specification covers a single looping pattern per document. Scenes/arrangements and file‑level time‑signature changes are intentionally out of scope. The OP‑XY owns its internal meter; this format doesn’t try to change it.
A valid opxyloop‑1.0 document is a JSON object with these keys:
| Key | Required | Description |
|---|---|---|
| version | Yes | Must equal "opxyloop-1.0" (schema identifier). |
| meta | Yes | Global timing and optional tonal context (see §2). |
| deviceProfile | No | Device defaults for MIDI port and drum note mappings (see §3). |
| tracks | Yes | Array of track objects (see §4). |
Defines the global timebase for the loop and, optionally, its tonal context for degree/chord resolution.
| Key | Type | Required | Description |
|---|---|---|---|
| tempo | number | Yes | Beats per minute used by the playback engine to drive MIDI clock. |
| ppq | integer | Yes | Pulses per quarter note (e.g., 96, 480) for internal tick math. |
| stepsPerBar | integer | Yes | Step grid per bar (e.g., 16 for a 1/16th grid). |
| swing | number 0–1 | No | Swing amount applied to every second grid subdivision. |
| key | string | No | Root key for resolving degrees/chord numerals (e.g., "C", "F#"). |
| mode | string | No | Scale mode: "major", "minor", or modal names ("ionian", "dorian", etc.). |
If
key/modeare omitted, use absolute pitches or absolute chord symbols instead of relative numerals.
Provides per‑project settings specific to the connected OP‑XY device.
| Key | Type | Required | Description |
|---|---|---|---|
| portName | string | No | MIDI port name to send messages to (e.g., "OP-XY"). |
| drumMap | object | No | Friendly drum names mapped to MIDI note numbers (e.g., "kick": 36). |
{
"portName": "OP-XY",
"drumMap": { "kick": 36, "snare": 38, "ch": 42, "oh": 46, "clap": 39 }
}Each track loops independently over its pattern length and targets a single MIDI channel/engine.
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique identifier (e.g., "t-drum"). |
| name | string | Yes | Human‑readable name (e.g., "Drums"). |
| type | string | Yes | OP‑XY synth engine (e.g., "sampler", "multiSampler", "axis", "dissolve"). |
| midiChannel | integer 0–15 | Yes | MIDI channel for this track. |
| role | string | No | High‑level musical role label (annotation only; no playback effect). Recommended values: "drums", "bass", "pad", "pluck", "lead", "keys", "piano", "guitar", "fx", "vox". |
| pattern | object | Yes | Loop definition (see §5). |
| ccLanes | array | No | Continuous‑controller automation (see §6). |
| lfos | array | No | Runtime LFO modulators (see §7). |
Note: There is no
programChangeormutedfield in this schema; omit a track to silence it.
roleis pure metadata for editors/generators (e.g., grouping, color, suggested register/density).- Players/readers must not change MIDI/audio behavior based on
role. - The
"drums"role covers both drum kit and auxiliary percussion. - The underlying structure remains
tracks → pattern → steps → events.
Defines the looping note content for a track.
| Key | Type | Required | Description |
|---|---|---|---|
| lengthBars | integer ≥ 1 | Yes | Number of bars in the loop before it restarts. |
| steps | array of Step | Yes | Sparse array of steps; omit silent indices. |
Describes notes (or rests) beginning at a grid position.
| Key | Type | Required | Description |
|---|---|---|---|
| idx | integer ≥ 0 | Yes | Absolute step index: bar * stepsPerBar + step (zero‑based). |
| events | array of NoteEvent | No | One or more note events at this step; empty/omitted = rest. |
| tuplet | "triplet" | "quintuplet" | "septuplet" | No | Non‑standard subdivision across the step. |
| mute | boolean | No | If true, ignore all events at this step. |
A NoteEvent can represent a single pitch/degree or an entire chord. All common timing/dynamics fields apply in either case.
| Field | Type / range | Required | Description |
|---|---|---|---|
| pitch | integer 0–127 | Yes* | Absolute MIDI note number. Use for fixed‑pitch tracks (drums or specific notes). |
| degree | integer 1–7 | Yes* | Scale degree relative to the track’s key/scale; typically used with octaveOffset. |
| octaveOffset | integer | No | Octave shift applied with degree to place the note in register. |
| chord | string | Yes* | Shorthand for a chord (relative or absolute), e.g., Imaj7, V7, vi7, IVadd9, Cmaj7, Am7, G7(b9), Vsus2, Imaj7/3. When provided, this single NoteEvent requests the full chord. |
| lengthSteps | integer ≥ 1 | Yes | Number of grid steps to hold the note/chord. |
| velocity | integer 1–127 | Yes | Velocity for the note or for all chord tones (unless velocities provided). |
| prob | number 0–1 | No | Probability of playing (applies to the whole event or chord). |
| gate | number 0–1 | No | Percentage of nominal duration to hold (staccato/legato). |
| microshiftMs | integer | No | Shift in milliseconds relative to the step boundary (humanize). |
| ratchet | integer ≥ 2 | No | Evenly spaced retriggers within the step; probability applies to the group. |
| meta | object | No | Freeform metadata for UI/articulation hints. |
* A NoteEvent must include one of: pitch, degree, or chord. When degree is used, include octaveOffset. When chord is used, timing/dynamics fields apply to the chord as a whole.
These hints control how a chord is voiced/realized when chord is present.
| Field | Type | Description |
|---|---|---|
| invert | integer ≥ 0 | Inversion index (0=root position, 1=first inversion, …). |
| register | [string, string] |
Clamp realized pitches to a range (e.g., ["C3","B4"]). |
| voicing | string | Voicing hint, e.g., "close", "open-4", "spread-5". |
| omit | array of strings | Omit chord degrees by symbol: "3", "5", "7", etc. |
| velocities | array of ints | Per‑tone velocities (low→high). |
| rollMs | integer | Arpeggiated “strum” delay between adjacent tones (ms). |
- Relative (uses
meta.key/mode):I,ii,iii,IV,V,vi,vii°with optional quality:maj,min,dim,aug,7,maj7,min7,ø7,sus2,sus4,add9,9,11,13. Examples:Imaj7,Vsus2,ii7,V7(b9),IVadd9. - Absolute:
Cmaj7,Am7,Fadd9,G7(b9). - Slash bass:
Imaj7/3,G7/B. - Defaults: Unspecified qualities resolve diatonically for the given mode.
Implementation notes for chord expansion
- Expand a chord NoteEvent into multiple per‑pitch events internally, sharing timing and probability unless overridden.
- Tools lacking chord support may pre‑expand chords into ordinary NoteEvents.
- If no inversion/voicing hints are provided, use close position within a practical register (default
["C3","B4"]).
Author all drum parts in one track using compact pattern strings. The playback engine reads drumKit directly and schedules hits at runtime—no pre‑conversion to steps → events is required. Placed here because it maps cleanly to the Step/NoteEvent timing model. Tools that don’t implement drumKit can ignore it, or optionally convert it into steps themselves if needed for editing workflows.
Location: Optional field on a drum track object (typically type: "sampler").
Effect: Schedules hits at specific Step.idx values based on per‑bar pattern strings.
Within a drum track, add a drumKit object with these fields:
- patterns (array of DrumPatternSpec) — required.
- repeatBars (integer ≥ 1) — optional. Repeat all listed patterns forward from each
barfor this many bars; clamped to the track’spattern.lengthBars. Default:1. - lengthSteps (integer ≥ 1) — optional. Default note length for emitted hits. Default:
1.
DrumPatternSpec (elements of drumKit.patterns):
- bar (integer ≥ 1) — the starting bar where this pattern applies.
- key (string) — must be a key in
deviceProfile.drumMap(e.g.,"kick","snare","clap","ch","oh"). - pattern (string) — exactly
meta.stepsPerBarcharacters; allowed chars:x(hit),.or-(rest). No whitespace. - vel (integer 1–127, optional) — velocity for all hits produced by this spec; if omitted, use a reasonable default (e.g., 100).
- lengthSteps (integer ≥ 1, optional) — per‑spec override; falls back to
drumKit.lengthStepsif not set.
First‑class helper.
drumKitis valid at playback time. Implementations MAY convert it intopattern.steps[].events[]internally for editing or export, but this is not required for playback.
Let S = meta.stepsPerBar, B = pattern.lengthBars on the same track.
For each DrumPatternSpec {bar = b0, key = k, pattern = P, vel, lengthSteps}:
- Resolve
pitchasdeviceProfile.drumMap[k](required). - For each bar offset
tin[0 .. repeatBars-1]whileb = b0 + t ≤ B:- For each index
sin[0 .. S-1]:- If
P[s] == 'x', schedule a hit at absoluteStep.idx = (b-1)*S + swith:pitch: resolved fromdrumMap.velocity: fromvel(or default if omitted).lengthSteps: from spec’slengthSteps→ elsedrumKit.lengthSteps→ else1.
- If
- For each index
If multiple specs produce hits at the same idx (e.g., kick and clap together), schedule both hits at that step. No hits are scheduled for ./- positions.
- The track MUST define
pattern.lengthBars. - Each
patternstring MUST be exactlymeta.stepsPerBarchars. keyMUST exist indeviceProfile.drumMap.- Default velocity if unspecified MAY be
100. - Default
lengthStepsif unspecified is1.
Authoring (compact, single track)
{
"version": "opxyloop-1.0",
"meta": { "tempo": 112, "ppq": 96, "stepsPerBar": 16, "swing": 0.10 },
"deviceProfile": {
"portName": "OP-XY",
"drumMap": { "kick": 36, "snare": 38, "clap": 39, "ch": 42, "oh": 46 }
},
"tracks": [
{
"id": "t-drums",
"name": "Drums (one-track)",
"type": "sampler",
"midiChannel": 0,
"role": "drums",
"drumKit": {
"patterns": [
{ "bar": 1, "key": "kick", "pattern": "x...x...x...x...", "vel": 120 },
{ "bar": 1, "key": "clap", "pattern": "..x...x...x...x.", "vel": 102 },
{ "bar": 1, "key": "ch", "pattern": ".x.x.x.x.x.x.x.x", "vel": 80 },
{ "bar": 1, "key": "oh", "pattern": "................", "vel": 95 }
],
"repeatBars": 8,
"lengthSteps": 1
},
"pattern": { "lengthBars": 8, "steps": [] }
}
]
}Use ccLanes to schedule CC automation over time (e.g., filter cutoff, resonance, sends). Destination names must match the OP‑XY’s fixed CC mapping known by the runtime.
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Lane identifier (e.g., "cutoff-sweep"). |
| dest | string | integer | Yes | Destination CC: cc:<number> or name:<identifier> resolved by the runtime. |
| channel | integer 0–15 | No | Overrides the track’s channel for this lane. |
| mode | "points" | "hold" | "ramp" | Yes | Interpolation/emit behavior. |
| points | array of CCPoint | Yes | Defines the curve; points are sorted by time. |
| range | [number, number] |
No | Clamp CC values (default [0,127]). |
CCPoint (time/value pair):
{ "t": { "ticks": number } | { "bar": number, "step": number }, "v": 0–127, "curve": "linear"|"exp"|"log"|"s-curve" }.
Runtime‑generated periodic modulation (distinct from explicit CC points).
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | LFO identifier (e.g., "cutoff-wobble"). |
| dest | string | integer | Yes | Destination controller (same formats as CC lanes). |
| channel | integer 0–15 | No | Defaults to the track’s channel. |
| depth | integer 0–127 | Yes | Peak‑to‑peak amplitude. |
| rate | object | Yes | `{"sync": "1/4" |
| phase | number 0–1 | No | Initial phase offset. |
| offset | integer 0–127 | No | Baseline value around which the LFO oscillates. |
| shape | "sine" | "triangle" | "saw" | "ramp" | "square" | "samplehold" | Yes | Waveform. |
| fadeMs | integer ≥ 0 | No | Fade‑in time (ms). |
| on | array of windows | No | Active windows: { "from": TickRef, "to": TickRef }. |
| stereoSpread | number 0–1 | No | Phase offset for L/R automation when applicable. |
- Engine enumeration: Track
typemust match an OP‑XY engine ("sampler","multiSampler","axis","dissolve", etc.). - Sparse steps: Omit silent indices in
steps; the array is intentionally sparse. - Probability & ratchets: If both are present, probability applies to the entire ratcheted group.
- Tonal context: When using degrees or relative chord numerals, set
meta.keyandmeta.modeso the runtime can resolve to MIDI pitches. Absolute pitches/symbols require no tonal context. - Fixed CC mapping: Destination names (e.g.,
name:cutoff) are resolved by the runtime against the OP‑XY’s fixed CC map; do not embed accMapin JSON. - No time‑signature changes: This schema doesn’t encode time‑signature changes; the OP‑XY handles its own meter.
{
"version": "opxyloop-1.0",
"meta": { "tempo": 120, "ppq": 480, "stepsPerBar": 16 },
"deviceProfile": {
"portName": "OP-XY",
"drumMap": { "kick": 36, "snare": 38, "ch": 42, "oh": 46 }
},
"tracks": [
{
"id": "t-drum",
"name": "Drums",
"type": "sampler",
"midiChannel": 9,
"role": "drums",
"pattern": {
"lengthBars": 1,
"steps": [
{ "idx": 0, "events": [{ "pitch": 36, "lengthSteps": 2, "velocity": 112 }] },
{ "idx": 4, "events": [{ "pitch": 38, "lengthSteps": 2, "velocity": 104 }] },
{ "idx": 8, "events": [{ "pitch": 36, "lengthSteps": 2, "velocity": 112, "ratchet": 2 }] },
{ "idx": 12, "events": [{ "pitch": 38, "lengthSteps": 2, "velocity": 104, "prob": 0.8 }] }
]
}
}
]
}{
"version": "opxyloop-1.0",
"meta": { "tempo": 100, "ppq": 480, "stepsPerBar": 16, "key": "C", "mode": "dorian" },
"tracks": [
{
"id": "t-bass",
"name": "Bass",
"type": "axis",
"midiChannel": 1,
"role": "bass",
"pattern": {
"lengthBars": 2,
"steps": [
{ "idx": 0, "events": [{ "degree": 1, "octaveOffset": -2, "lengthSteps": 4, "velocity": 108 }] },
{ "idx": 4, "events": [{ "degree": 5, "octaveOffset": -2, "lengthSteps": 4, "velocity": 108 }] },
{ "idx": 8, "events": [{ "degree": 6, "octaveOffset": -2, "lengthSteps": 4, "velocity": 108 }] },
{ "idx": 12, "events": [{ "degree": 4, "octaveOffset": -2, "lengthSteps": 4, "velocity": 108 }] }
]
}
}
]
}{
"version": "opxyloop-1.0",
"meta": { "tempo": 96, "ppq": 480, "stepsPerBar": 16, "swing": 0.08, "key": "C", "mode": "ionian" },
"deviceProfile": { "portName": "OP-XY" },
"tracks": [
{
"id": "t-chords",
"name": "Pad",
"type": "dissolve",
"midiChannel": 0,
"role": "pad",
"pattern": {
"lengthBars": 2,
"steps": [
{ "idx": 0, "events": [{ "chord": "Imaj7", "lengthSteps": 8, "velocity": 96, "invert": 1, "register": ["C3","B4"] }] },
{ "idx": 8, "events": [{ "chord": "V7", "lengthSteps": 8, "velocity": 96, "rollMs": 12 }] }
]
},
"ccLanes": [
{
"id": "cutoff-sweep",
"dest": "name:cutoff",
"mode": "ramp",
"points": [
{ "t": { "bar": 0, "step": 0 }, "v": 40 },
{ "t": { "bar": 1, "step": 15 }, "v": 100 }
]
}
],
"lfos": [
{
"id": "pad-wobble",
"dest": "name:resonance",
"depth": 18,
"rate": { "sync": "1/8T" },
"shape": "triangle",
"offset": 64
}
]
}
]
}See §5.1.2 for schema and semantics. Here’s a minimal one‑bar example that the playback engine reads directly (no pre‑conversion of drumKit to steps is required):
{
"version": "opxyloop-1.0",
"meta": { "tempo": 120, "ppq": 480, "stepsPerBar": 16 },
"deviceProfile": { "portName": "OP-XY", "drumMap": { "kick": 36, "snare": 38 } },
"tracks": [
{
"id": "t-drums",
"name": "Kit",
"type": "sampler",
"midiChannel": 9,
"role": "drums",
"drumKit": {
"patterns": [
{ "bar": 1, "key": "kick", "pattern": "x...x...x...x...", "vel": 112 },
{ "bar": 1, "key": "snare", "pattern": "....x.......x...", "vel": 104 }
],
"repeatBars": 1,
"lengthSteps": 1
},
"pattern": { "lengthBars": 1, "steps": [] }
}
]
}{
"version": "opxyloop-1.0",
"meta": { "tempo": 112, "ppq": 96, "stepsPerBar": 16, "swing": 0.10 },
"deviceProfile": {
"portName": "OP-XY",
"drumMap": { "kick": 36, "snare": 38, "clap": 39, "ch": 42, "oh": 46 }
},
"tracks": [
{
"id": "t-drums",
"name": "Kit + Aux Perc",
"type": "sampler",
"midiChannel": 0,
"role": "drums",
"pattern": {
"lengthBars": 4,
"steps": [
{ "idx": 0, "events": [{ "pitch": 36, "velocity": 120, "lengthSteps": 1 }] },
{ "idx": 6, "events": [{ "pitch": 39, "velocity": 102, "lengthSteps": 1 }] },
{ "idx": 2, "events": [{ "pitch": 42, "velocity": 80, "lengthSteps": 1 }] }
]
}
},
{
"id": "t-bass",
"name": "Sub Bass",
"type": "axis",
"midiChannel": 1,
"role": "bass",
"pattern": {
"lengthBars": 4,
"steps": [
{ "idx": 0, "events": [{ "degree": 1, "octaveOffset": -2, "velocity": 110, "lengthSteps": 4 }] },
{ "idx": 8, "events": [{ "degree": 5, "octaveOffset": -2, "velocity": 112, "lengthSteps": 4 }] }
]
}
},
{
"id": "t-pluck",
"name": "Glimmer Pluck",
"type": "multiSampler",
"midiChannel": 3,
"role": "pluck",
"pattern": {
"lengthBars": 4,
"steps": [
{ "idx": 1, "events": [{ "degree": 6, "octaveOffset": 1, "velocity": 105, "lengthSteps": 2 }] },
{ "idx": 5, "events": [{ "degree": 7, "octaveOffset": 1, "velocity": 103, "lengthSteps": 2 }] }
]
}
}
]
}versionis exactly"opxyloop-1.0".metaincludestempo,ppq,stepsPerBar(and optionalswing,key,mode).- Each
trackdefinesid,name,type,midiChannel, and apattern. - Each
Stepprovidesidxand optionalevents/tuplet/mute. - Each
NoteEventhas one ofpitch|degree|chord, pluslengthStepsandvelocity. - If
degreeis used, includeoctaveOffset; ifchordis used, optional hints may guide voicing/inversion/strum. - Optional
ccLanes/lfosfollow their schemas and reference valid destinations. - If a track includes
drumKit:patterns[].keymust exist indeviceProfile.drumMap; eachpatterns[].patternlength equalsmeta.stepsPerBar; scheduling respectspattern.lengthBarsand the runtime semantics in §5.1.2. - If a track includes
role: treat it as annotation only (no playback effect). Recommended values include"drums","bass","pad","pluck","lead","keys","piano","guitar","fx","vox";"drums"covers both kit and auxiliary percussion.
This spec is intended to be small, explicit, and implementation‑friendly. If you need scenes or arrangements, store each as its own opxyloop-1.0 document and sequence them in your application logic.