songcode

ode, a musical coding language

Songcode’s own coding language is the key to its flexibility

technologyunder the hood4 min read

Every Songcode track is written in ode. We designed this language for one job: optimized for writing by language models like Claude and ChatGPT, readable and easily editable by humans, and playable in your browser on Songcode.

Currently we’re on ode 1.

It borrows what musicians already read

People have been writing music as code for a while now; the live coding scene is one of the things that inspired Songcode. But no one has written a language optimized for modern AI models to write. So we did.

Drum parts are written in a step grid, the language of drum machines. One character is one step. x is a hit, . is a rest, o is a soft ghost hit and a digit is a roll.

stem drums: live {
  kick  [x... ..x. x... ....]
  snare [.... x... .... x..o]
  hat   [x.xx x.x. xxxx x.22]
}

Harmony is a chord chart, written the way a typical lead sheet would write it. One line near the top of the track holds the chords and all the parts follow it.

chords [Fm9 | % | Dbmaj7 | Eb6]

stem bass: fmbass {
  rhythm [x.x. ..x. x... ..xx]
  tones  [root]
  octave 1
}

The bass has a rhythm and one instruction for pitch: play the root of whichever chord the chart is on. No note name appears in the stem. Change Dbmaj7 to Bbm7 in the chart and the bass moves with it.

Melodies are note names with lengths. c5/4 is C in octave 5 for a quarter note, r/4 is a rest, and a note with no length takes the length of the note before it.

stem lead: sawlead {
  line [c5/4 eb5/8 f5 g5/4. f5/8 | eb5/2 r/4 c5]
}

A song in three parts

Form, arrangement, and mix sit alongside the instruments in ode and are written to guide stems. Stems adjusted in our web DAW also write back changes to the code.

form intro 8, main 16, break 8, main 16

arrange {
  sections [intro main break main]
  drums    [.     x    .     x]
  bass     [.     x    .     x]
  lead     [x     .    x     x]
}

mix {
  drums 0db
  bass  -4db
  lead  -9db, pan: 10L
  duck bass under drums, depth: 60%, recover: 120ms
}

Each fact is written once. A level lives in the mix table and nowhere else, and dragging a fader in the stems view rewrites its row. A section named twice in the form is the same music played again, so a chorus is written one time.

The rows of the mix table are the track’s resting mix. A block under them changes it for part of the track.

form intro 8, main 16, break 8, main 16

mix {
  drums 0db
  bass  -4db
  lead  -9db

  in break {
    lead ramp(-15db, -9db)
  }
  last 4bars into end {
    master fade out
  }
}

The lead rises through the break, and the whole track fades out over its last four bars.

Every value says what it is

A number in ode carries its unit: 420hz, 8ms, -6db, 30%, 16bars. A bare 400 on a filter could be hertz, a share of a range or a count of bars, and neither a person nor a model should have to remember which.

A value that moves is written as where it starts and where it ends.

stem pad: ensemble {
  rhythm  [x]
  tones   [all]
  lowpass sweep(600hz, 2.5khz, every: 8bars)
  in break {
    lowpass ramp(500hz, 6khz)
  }
}

The sweep goes up and back once every eight bars. The ramp sits inside in break, so it runs across the break, however long the break is, and nobody has to work out a duration.

It is strict

ode has one grammar, and anything outside it is an error. An unknown word, sound or unit stops the track and the message names the line, so nothing is skipped in silence.

Every count is checked before you hear anything: steps in a bar, pitches against hits, note lengths against the meter, bars in a section, columns in the arrange table. A bar of melody that is an eighth note short is reported with its line, its bar and both totals.

We made the language strict because of how models write music. In our tests they were reliable with note names, chord symbols and dynamics, and weaker at keeping count across a long bar. So ode puts the counting where a checker can do it, and it lets a part follow the chord chart so the model does not have to hold a key in its head.

It describes a song and runs nothing

ode has no loops, no conditions and no functions of your own. A track is a description, so opening one from a stranger runs none of their code.

Chance is in the language, and it repeats. chance 88% drops some hits, and the track’s seed decides which, so the same hits are missing on every play and in every export.

From a loop to a written-out piece

The same grammar covers an eight bar loop and music that never repeats. A score block writes a few bars of every part together, so bar 5 of the violin sits above bar 5 of the cello.

score a bars 5..8 {
  violin [5: b4/4 d5/8 c5 b4 a4 | g4/4 e5 d5 | c5/8 b4 a4/4 f#4 | g4/2.]
  cello  [5: g2/2. | e3/2 b2/4 | c3/4 d3 d2 | g2/2.]
}

Every row in a block holds the same number of bars, and the bar numbers are checked against what is written. A part that drifts by a bar is caught in the block where it happens.

A melody that comes back gets a name. let names a line above the stems, and any line can quote it, as written or changed.

key d minor
let call = line [d4/4 f4/8 a4 g4/4 f4/8 e4 | d4/2 a3/2]

stem lead: sawlead {
  line [call | call(up 2) | call(inverted) | call(first 1bar) a4/2 d4/2]
}

That line is eight bars written from two: the tune, the tune a third higher in the key, the tune upside down, and its first bar with a new ending.

What the 1 means

A track that starts with ode 1 keeps playing the way it does today. If the language ever changes in a way that would alter how an existing track sounds, the new behaviour gets a new number and tracks marked 1 keep the old one.

How to read a track goes through one loop a stem at a time, and the whole language is in the docs.

Describe a song.
Get it in code.

open songcode