Skip to content

MIDI Notation

Ask an AI to write MIDI directly and it has to emit raw note data: pitch numbers, start times, and durations in abstract beats. That is error-prone and unmusical — nothing in it reads like music, so nothing about it plays to a language model's strengths.

So Producer Pal doesn't ask for raw MIDI. It gives the AI a text notation to compose in, and translates that notation into real notes in your Live Set. The notation is the single largest investment in the project, and it's the main reason results feel musical rather than mechanical.

There are three notations to choose from, and one expression language — transforms — that reshapes notes in any of them.

Choosing a notation

NotationBest forStrengthsTradeoffs
bar|beatThe default. Capable models.Most expressive: velocity ranges, probability, bar copying, note deletionStateful syntax; bars and beats shift meaning in odd meters
MIDI JSONCoding agents; exact dataHighest fidelity: exact velocities and tuplets; trivial to generate and parseVerbose — every note spelled out in full; no delete syntax
StarkSmall and local models; drums; chordsLiteral and round-trippable; chord symbols; event-based drum linesVelocity is lossy (three dynamics); no probability or velocity randomness

If you're using Claude, GPT, Gemini, or another frontier model through the chat UI or an MCP client, stay on bar|beat. It's the default, it's the most expressive, and capable models handle it well.

Reach for MIDI JSON when a coding agent is driving Producer Pal, or when you need exact velocities and tuplet ratios preserved — it's the most literal of the three. Reach for Stark when you're running a small or local model, or when you're working mostly in drums and chord progressions.

Notation and small-model mode are independent

Small model mode trims the tool schemas and skills; the notation setting changes how notes are encoded. They're separate switches, and any combination works. A common pairing for local models is Stark plus small-model mode.

Setting the notation

The notation is a global device setting, not a per-conversation or per-tool one. Whatever you pick applies to the built-in Chat UI, external MCP clients, and the REST API alike. Set it in any of these places:

  • Max for Live device — the Notation control on the Setup tab
  • Chat UI — Settings → Tools → Advanced → Notation
  • REST APIPOST /config with {"notation": "stark"}
  • Command linenpx producer-pal --notation stark
  • Coding agents — the Agent Skill sets it itself with node ppal.mjs --set-config '{"notation":"midi-json"}'

Switching takes effect in a new conversation

The AI is taught its notation once, at the start of a conversation. If you switch mid-chat, it can still read your existing notes in the new notation, but it will keep writing the old one until you start a new conversation.

The notation also rewrites the clip tools' own parameter descriptions, so the AI is told what to send in the notation that's actually active. Each section below ends with the notes parameter text the AI receives under that notation. Small model mode shortens these further and hides some parameters outright, so the text shown is the standard-mode wording.

bar|beat

The default. A compact, human-readable notation where time is counted the way musicians count it.

v100 n/4 C3 E3 G3 1|1

That's a C major triad, velocity 100, quarter notes, at bar 1 beat 1. The pieces are:

  • Pitches are names — C3 (middle C, MIDI 60), F#4, Bb2.
  • Positions are bar|beat, both 1-indexed — 1|1 is the first beat, 2|3 is bar 2 beat 3. Sub-beat positions can be decimals (2|3.5) or note-value offsets (1|1+n/12).
  • Durations are note values — n/4 is a quarter, n/8 an eighth, n/12 an eighth triplet.
  • Velocity is v100, or a range like v90-110 for humanized randomness.
  • Probability is p0.5 — the note plays half the time.

v, n, and p are stateful prefixes: once set, they apply to every note that follows until changed. That's what keeps it compact — you state a velocity once, not on every note.

v90-110 n/4 C1 1|1,3 n/8 D1 1|2,4   // humanized kick and snare
n/12 C3 1|1x3                        // three eighth-note triplets

Bars can be copied rather than retyped — @2=1 copies bar 1 to bar 2, @2-8=1 fans it across bars 2 through 8, and @3-10=1-2 tiles a two-bar pattern across bars 3 through 10.

Strengths. The most expressive of the three. It's the only notation with velocity ranges, note probability, bar copying, and a delete sentinel (v0 removes a note at a position). Positions read the way a musician thinks about time, which is exactly the translation an LLM is good at.

Tradeoffs. The statefulness is a footgun — a n/8 in the wrong place retimes everything after it. And bars and beats are meter-relative: a "beat" is a quarter in 4/4 but an eighth in 6/8, while note values like n/4 mean the same duration in every meter. That distinction is precise but it's one more thing for a model to get right.

No chord symbols. bar|beat deliberately has none, and won't get them — C7 is already a valid pitch (note C, octave 7), so a chord symbol would be lexically ambiguous with a note name. Simultaneous notes at one position (1|1 C3 E3 G3) express any chord fully, with per-note velocity and duration. Stark gets chord symbols because its chords: line header declares the context up front.

Tool parameters under bar|beat (standard mode)
ToolParameterDescription
ppal-create-clipnotesMIDI in bar|beat notation: v0-127 n<dur> [p0-1] note(s) bar|beat(s) - MIDI clips only
ppal-update-clipnotesMIDI notes in bar|beat notation: v0-127 n<dur> [p0-1] note(s) bar|beat(s) - MIDI clips only. MERGES into existing notes (overwrites at same pitch+start - restate a note to edit it in place). To delete/move existing notes or replace a region use preTransforms; don't rewrite the whole clip

MIDI JSON

Notes as a compact array of objects. The most literal of the three, and the easiest for a program to generate or parse.

[{p:60,t:0,d:4,v:100},{p:62,t:1,d:1,v:90,vd:10,c:0.75}]

The keys are p pitch (0-127, C3 = 60), t start and d duration in musical beats, v velocity (1-127), plus optional vd velocity deviation and c probability (chance). Defaults are omitted, so most notes are just four keys.

Times are absolute musical beats: t:0 is the clip start, t:4 is beat 5, and chords share a t. Tuplets can be written as exact fractions — d:2/3 is a triplet quarter — and read back as fractions rather than a lossy 0.3333.

The parser is deliberately tolerant, so whatever an LLM emits still parses: keys may be quoted or bare, short (p/t/d/v) or long (pitch/start/…).

Strengths. Exactness. Velocities, deviations, and tuplet ratios all survive a round trip precisely. Coding agents generate and parse JSON natively rather than composing a text notation by hand, which is why the Agent Skill selects it.

Tradeoffs. It's the most verbose per note — there's no statefulness, no bar copying, no repeats, so a busy clip costs the most tokens. And it has no delete syntax; to remove or edit existing notes, use preTransforms.

Tool parameters under MIDI JSON (standard mode)
ToolParameterDescription
ppal-create-clipnotesMIDI notes as a JSON array string, e.g. [{p:60,t:0,d:4,v:100}] (see Skills) - MIDI clips only
ppal-update-clipnotesMIDI notes as a JSON array string, e.g. [{p:60,t:0,d:4,v:100}] (p pitch, t start & d duration in beats, v velocity; see Skills) - MIDI clips only. MERGES (overwrites at same pitch+start; restate to edit in place). No v0-delete; delete/move existing notes via preTransforms - don't rewrite the clip

Stark

A literal, round-trippable notation built for small models. One line per part, written as type: content.

kick: X z X z
snare: z X z X
hihat /8: X X X X X X X X

That's a one-bar 4/4 backbeat: kick on 1 and 3, snare on 2 and 4, closed hi-hat on every eighth. X is a normal hit, x soft, ^ an accent, and z a rest.

Every section is event-based — whitespace between tokens is just a separator, with no rhythmic meaning. Each token advances time by its own duration, which defaults to /4 (a quarter note) — except chords: lines, which default to /1 (a whole note) — and can be set per line (hihat /8:) or glued to a single token (X/8). So a pattern counts by the subdivision you're already thinking in: a 4/4 bar of quarters is four tokens, of eighths is eight. *N repeats a token — hihat /16: X*16 is a one-bar 16th roll.

Pitched lines work the same way, and chords can be written as symbols:

melody: C Eb G' z/2 A/8!
chords: C Am F G7
chords: Cm7 [Eb G C']

' and , shift the octave, ! accents, ? softens, and [...] stacks notes into an explicit voicing. Chord symbols (Cm7, G7/B, Fmaj9) get realized into concrete pitches. Drums use the 16 General MIDI names (kick, snare, hihat, clap, …); an absolute pitch name works as a header too (C3:) for pads outside that set.

Strengths. Literal and predictable — no key, no scale, no snapping, and accidentals spelled explicitly. Reading a clip back re-emits Stark rather than falling back to another notation, so the AI sees what it wrote. The event-based drum lines and the chord symbols are the two things small models handle noticeably more easily than they handle bar|beat.

Tradeoffs. Velocity is the lossy axis. On read-back it snaps to three dynamics — soft, normal, accent — and is re-randomized within each range, so reach for bar|beat or MIDI JSON when exact velocities matter. Stark also has no note probability and no velocity deviation at all. Chord symbols are input-only sugar: the serializer never emits a chords: line, so a clip you wrote with symbols reads back as literal notes.

Tool parameters under Stark (standard mode)
ToolParameterDescription
ppal-create-clipnotesMIDI notes in stark notation (literal, round-trippable type: content; event-based drum hits, see Skills) - MIDI clips only
ppal-update-clipnotesMIDI notes in stark notation, a literal per-line type: content format with event-based drum hits (see Skills) - MIDI clips only. MERGES (overwrites at same pitch+start; restate to edit in place). Delete/move existing notes via preTransforms - don't rewrite the clip

Transforms

Notation says what the notes are. Transforms say how to reshape them — a small expression language for changing notes and audio properties with math. Pass a transforms string to Create Clip, Update Clip, or Duplicate.

Each line is [selector:] parameter operator expression:

timing = swing(0.05)                  // swing the whole clip
C1-C2: velocity += 30                 // accent the bass notes
1|1-4|4.75: velocity = ramp(40, 127)  // crescendo over 4 bars
where(note.velocity < 40): delete     // drop the quiet notes
pitch += clipseq(0, 5, 7)             // per-copy transposition
  • Selectors narrow what a line touches — a pitch (C3) or pitch range (C3-C5), a time range (1|1-2|4, or 3|* for all of bar 3), a value test (where(note.velocity > 100)), or a combination. No selector means every note. Each selector applies only to its own line.
  • Parameters are velocity, pitch, timing, duration, probability, and deviation for MIDI, plus gain and pitchShift for audio clips.
  • Operators are +=, -=, *=, /=, and =.
  • Expressions are arithmetic over the current values, with LFO waveforms (sin, cos, tri, saw, square), ramp and curve for interpolation, rand and choose for randomness, seq to cycle through values, scale functions like snap (to the Live Set's scale), timing functions like legato, and general math functions.
  • Note-count operations change how many notes exist: ratchet(4) rolls a note into four, repeat(n/8, 3) echoes it, split cuts at explicit positions, and merge() fuses same-pitch notes into one sustained note.
  • Context variables let expressions read where they are: note.index, note.count, next.pitch, and clip metadata like clip.index and clip.duration.

A transform string broadcasts across every clip or copy in a batch, so one string can vary many clips at once — clip.index arithmetic or clipseq(...) gives each one a different result. That's what makes Duplicate able to generate a set of variations in a single call, and it pairs naturally with take lanes.

preTransforms is the same language, applied to a clip's existing notes before any new notes merge in. It's the general editing path — and for MIDI JSON and Stark, which have no delete syntax of their own, it's how you remove or rewrite notes.

Transforms are the same in every notation

Transforms work identically whichever notation is active — they operate on notes after they've been parsed, so nothing about them changes. Their syntax, though, is bar|beat-flavored: selectors use bar|beat positions and pitch names regardless. Even on MIDI JSON or Stark, you still write 1|1-2|1: delete and C1: velocity += 30.

The instructions the AI actually gets

Producer Pal teaches the AI its notation at the start of every conversation, by returning a skill set from the Connect tool. The full text of those skills — including the notation guide for whichever notation is active — isn't reproduced here; see the features page for where to read it: the Chat UI's Skills tab previews the assembled document for any notation and model size, and the source is browsable on GitHub. You can customize or replace them, including swapping in your own notation guide.

Released under the GPL-3.0 License.