Symbolic Cycle (work in progress)

Write chords, keys, drums and seeded generators as code in the Code Sequencer, and build songs from the layers of other songs.

Work in progress. Symbolic Cycle is changing quickly. This page describes what the Code Sequencer does today, September 2026. Sections marked Planned describe work that has not shipped. Every example you can paste into the sequencer is checked by an automated test, so it plays as written.

Symbolic Cycle is a small composition language for musicians who code. You write musical structure: chords, keys, scale steps, Roman numerals, drum parts, and seeded generators that draft progressions, melodies and basslines. The Code Sequencer turns it into MIDI notes, one track per part, for an instrument to play. It is built on Strudel's pattern engine, so Strudel's mini-notation and pattern functions work as you know them. It adds musical values that stay abstract until they sound: a chord can wait for its key, its root or its voicing.

It covers MIDI only. Audio, samples and synthesis belong to the instrument the notes are sent to. The musical ideas come from the Symbolic catalog of concepts and notations.

Try it

  1. Open the Code Sequencer.
  2. Switch the title bar from Strudel to Symbolic. The preset menu then lists the Symbolic Cycle examples, each with a line on what to try.
  3. Paste an example from this page, or pick a preset, and press Preview (Cmd/Ctrl+Enter). The page's built-in audition voice plays it without a connected instrument.

While it plays, keep editing: a valid edit takes over at the next cycle, and the music keeps its place instead of starting again. Cmd/Ctrl+Enter applies an edit straight away, and Cmd/Ctrl+. stops. Click an underlined name or value to edit it with a control in the palette beside the code.

The editor highlights the words that are sounding, and the MIDI roll under it shows one track per part. The title bar's ⋯ menu holds Reference (every function, with an example to insert), Export MIDI (those tracks as a MIDI file), Full screen and the code size.

The code runs in your browser tab as JavaScript. Only run code you trust.

What a cycle is

Time is counted in cycles. At the sequencer's tempo one cycle lasts four beats, so it works like a bar: at 120 BPM a cycle is two seconds. The Cycles setting next to the tempo chooses how many cycles one pass prepares.

Strings in double quotes are mini-notation. Each string divides a cycle into steps:

WriteMeaning
"c4 e4 g4"three steps in one cycle
"c4 ~ e4 ~"~ is a rest
"c4 [e4 g4]"[ ] fits several steps into one
"<c4 a3 f3 g3>"< > plays one entry per cycle
"c4*4 e4"*4 plays a step four times in its own place
"c4!2 e4"!2 repeats a step as two steps: the same as "c4 c4 e4"
"c4@3 e4"@ gives a step more length: here three quarters and one quarter
"kick(3,8)"a Euclidean rhythm: 3 hits spread over 8 steps
"kick ~, hh*8"a comma layers two rhythms in one string
"<C:major G:major>/8"/8 stretches the pattern: each entry lasts 8 cycles

Symbolic Cycle adds one rule: : joins the members of one value, so 0:4:7 is one chord, one event and one word you can click.

// One cycle is one bar. "<c2 a1 f1 g1>" changes the bass note every cycle,
// and .struct() gives it a rhythm. The drums layer three rhythms with commas.
export const kit = drums("kick ~ [~ kick] ~, ~ snare ~ snare, closedHat*8")
export const bass = note("<c2 a1 f1 g1>").struct("x ~ x x")

Each export const is a named part, a layer, and gets its own MIDI track. Songs are modules explains layers in full.

Pitch domains

The same chord can be written in several ways. Each spelling fixes some things and leaves the rest to context.

SpellingExampleWhat it fixesWhat comes from contextCatalog concept
Scale stepsn("0 2 4")steps in a scalethe key and its octaveKey
Roman numeralsroman("ii7 V7 I")degree and qualitythe key (C major if none)Functional harmony
Chord symbolschord("Dm7 G7 C")root and qualitythe register (roots from C3 to B3)Chord
Pitch-class setspcs("0:3:7")which pitch classesthe root (C4 if none)Pitch-class set
Point setspoints("0:7:15")a shape with its octavesthe root (C4 if none)Point set
Note sets and notesnoteset("d3:f3:a3"), note("d3")every pitchnothingNote set
Drumsdrums("kick snare")the drum soundnothing

Here are the four chords C, A minor, F and G in five spellings. Only the layer in the default export plays. Put another name there and you hear the same notes: C3 E3 G3, A3 C4 E4, F3 A3 C4 and G3 B3 D4.

const numerals = roman("<I vi IV V>").key("C:major")
const symbols = chord("<C Am F G>")
const shapes = pcs("<0:4:7 0:3:7 0:4:7 0:4:7>").root("<c3 a3 f3 g3>")
const spread = points("<0:4:7 0:3:7 0:4:7 0:4:7>").root("<c3 a3 f3 g3>")
const fixed = noteset("<c3:e3:g3 a3:c4:e4 f3:a3:c4 g3:b3:d4>")

export default { numerals }

They differ in what you can change afterwards:

  • Scale steps are zero-based, as in Strudel: in a seven-note scale 7 is the next octave and -1 is the step below the tonic. A b or # suffix moves a step by a semitone, so "2b" in C major is E flat. Step 0 sounds in octave 3 unless the key names one, as in "E4:minor". Without a key, n is a MIDI note number.
  • Roman numerals use upper case for a major third and lower case for a minor third, b and # to move the root (bVII), and qualities such as 7, maj7, o, ø, sus4 and add9. V7~V is a secondary dominant, the V7 of V. The root sits in the octave that starts at the key's tonic.
  • Chord symbols are Strudel's own chord control, voiced in close position. Since / means "stretch" in mini-notation, a slash chord is written with ~: C~E is C over E.
  • Pitch-class sets are rootless and octave-free: 0:4:7 is a major triad on whatever root it gets, and t and e spell 10 and 11. .mirror() turns a set upside down, so a major triad becomes a minor one.
  • Point sets keep their octave spacing, so one shape is one voicing: 0:7:16 is an open major chord.
  • Note sets and note() are absolute. Keys and roots do not move them; .transpose() and .octave() do.

Chords in every spelling can be revoiced as they sound: .voicing("open") takes the styles close, open, drop2, drop3, drop24, spread and root, and .inversion(1) raises the lowest note by an octave.

In the editor, every set word (pcs, points and noteset) has a dotted underline. Click one, or put the caret inside it, to open the structure editor in the palette: toggle members, transpose or mirror the set, try voicings (inversions, drop 2, open and more for point and note sets; rotations and the mirror for pitch-class sets), and see the same value written as note names, binary, an interval vector, a prime form and a chord or scale name. Each click rewrites the word in place and is one undo step.

Keys and context

.key() and .scale() set the same thing: the key that scale steps and Roman numerals are read in. A key is written Tonic:type, with an optional octave: E:minor, F#4:dorian, Bb:mixolydian, C:harmonic_minor. Keys are patterns too, so modulation is one value.

// Nothing here names a key except the last line: E minor for four
// cycles, then G minor. Chords and bass follow; drums never move.
export default stack(
  roman("<i III VI VII>").voicing().velocity(0.5).s("keys"),
  n("<0 2 5 6>").octave(-2).s("bass"),
  drums("kick ~ snare ~, closedHat*8"),
).key("<E:minor G:minor>/4")

"<D:major@24 E:major@8>" holds D major for 24 cycles and E major for 8, the usual way to lift a last chorus by a tone.

Context is inner-wins. A part that sets its own key keeps it, and an outer .key() fills in only the parts that have none:

const fixed = n("0 2 4").key("C:major")
const follows = n("0 2 4")

// fixed plays C E G in every cycle. follows plays E G B, then G Bb D.
export default stack(fixed.s("fixed"), follows.s("follows")).key("<E:minor G:minor>")

The other context methods work the same way:

MethodWhat it setsChained twice
.key(name), .scale(name)the key of scale steps and Roman numeralsinner-wins
.root(note)the root of pitch-class and point sets; where chord symbols and Roman numerals start their octaveinner-wins
.voicing(style), .inversion(steps)how chords are voicedinner-wins
.phrase(cycles)the phrase length a generator reads from chords (see Seeds and phrases)inner-wins
.transpose(amount)semitones, or interval names such as "P5" or "-M2"adds up
.octave(octaves)moves realized notes by that many octavesthe last one wins

A key does not move notes, note sets, pitch-class sets or point sets: they already say which pitches they are. .transpose() moves everything except drums.

Drums and automatic channels

drums() plays General MIDI percussion by name: kick (or bd), snare (sd), clap (cp), rim (rs), closedHat (hh), pedalHat (ph), openHat (oh), lowTom, midTom, highTom, crash, ride, tambourine, cowbell and shaker, or a MIDI note number from 0 to 127. kick:closedHat strikes both at once. Keys, transposition and octave shifts never move drums.

You do not have to number MIDI channels. Drums play on channel 10. Every other part takes the next free channel in the order its notes first appear in the code, skipping 10 and any channel you set with .midichan(). A generated part's notes appear where a string it plays is written, such as a seed pattern or a rhythm. Parts whose notes have no written position, such as a generator with a numeric seed and the default rhythm, come after the others, in alphabetical order. Use .midichan() to pin a part's channel.

// No channel numbers except lead's. kit plays on channel 10, pad takes 1,
// bass takes 2, and lead keeps the 5 it asks for.
export const kit = drums("kick ~ [~ kick] ~, ~ snare ~ [snare clap], closedHat*8")
export const pad = pcs("<0:3:7:t 0:4:7:e>").root("<a3 f3>").velocity(0.4)
export const bass = note("<a1 f1>").struct("x ~ x x")
export const lead = n("<[4 ~ 2 ~] [0 ~ 2 4]>").scale("A4:minor").midichan(5)

The roll's legend and Export MIDI show one track per part. More parts than free channels is an error that names the parts.

Seeds and phrases

Generators draw new material from a seed. A seed is a take: the same seed, with the same options and chords, always gives the same notes.

Generators work in phrases. A generated progression's phrase is its number of chords times the cycles per chord, 4 cycles by default. A melody or bassline over it uses the same phrase. The seed is read once per phrase, so a seed pattern is a sequence of takes:

// Each phrase is 4 cycles. seed: "<1 2>" plays take A, then take B,
// then take A again, note for note.
const prog = generateProgression({ scale: "F:major", chords: 4, cadence: "half", seed: "<1 2>" })
export const keys = prog.velocity(0.45)
export const lead = generateMelody({ over: prog, rhythm: "x ~ x x@2 x x@2", seed: "<5 8>", range: "f4:a5" })
  • "<1 1 1 2>" plays take 1 three times and take 2 once: an AAAB form.
  • A constant seed such as 7 repeats its phrase whenever the phrase's chords and rhythm repeat.
  • A rest in the seed pattern ("<1 ~>") leaves its phrase silent.
  • Each generator has its own seed, so changing the melody's seed never changes the chords or the bass.

This is not Strudel's seed. Strudel's own randomness (?, sometimes, rand) depends on time, so it does not repeat a take. It still works here, as it does in Strudel.

Generators

Three generators write material, and each returns an ordinary pattern: you can store it, transform it, stack it, and pass it to another generator as its harmony.

GeneratorWritesMain options
generateProgressiondiatonic chords, as pitch-class sets on rootsscale, histogram, chords (4), every (1 cycle), sevenths (0 to 1), cadence, seed
generateMelodyone note per rhythm step over chordsover, rhythm, range ("c4:g5"), stepSize, maxLeap, histogram, seed
generateBasslineone note per rhythm step under chordsover (required), rhythm, style ("root", "root-fifth", "octave", "arpeggio", "walking"), range ("e1:e3")

histogram("E:9 B:7 G:6") gives twelve pitch-class weights: the song's pitch colour. A progression steers its chords by it and takes its key from it unless scale names one. A melody multiplies it into its note choices.

// The generative loop: a pitch colour, four chords drawn from it (E minor),
// then a melody and a bass over the chords. Two seeds alternate per phrase.
export const colour = histogram("E:9 B:7 G:6 D:4 A:3 F#:3 C:2")
const prog = generateProgression({ histogram: colour, chords: 4, seed: "<3 5>" })

export const keys = prog.voicing("open").velocity(0.45)
export const lead = generateMelody({ over: prog, rhythm: "x@3 x x@2 x@2", seed: "<9 26>", range: "e4:e5" })
export const bass = generateBassline({ over: prog, rhythm: "x ~ ~ x ~ ~ x ~", style: "root-fifth" })
export const kit = drums("kick ~ [~ kick] ~, ~ snare ~ snare, closedHat*8")

colour is exported but is not a pattern, so it is a value, not a layer: it does not play, but it is part of what the song exports.

histogramOf(part) learns the weights from a part you wrote: how long each pitch class sounds in its first 8 cycles. A counter-melody given those weights favours the notes the tune dwells on and avoids the ones it leaves out, so it keeps to the tune's mode without being told it. Here it never plays E, the one note of D Dorian that the tune leaves out:

const tune = n("<[0 ~ 2 4 7@2 6 4] [5@2 4 3 2@3 ~]>").scale("D4:dorian")
const chords = pcs("<0:3:7:t 0:4:7:t>").root("<d3 g3>").phrase(2)

export const melody = tune
export const counter = generateMelody({ over: chords, histogram: histogramOf(tune), rhythm: "~ x x ~ x ~ x x", seed: 23, range: "a3:a4", repeatPenalty: 0.1 }).velocity(0.6)

Where a generator's key comes from:

  • a progression uses its scale option, else the key its histogram suggests, else C major;
  • a melody uses its own scale option, else the key of its over chords, else the key its histogram suggests, else the chord tones;
  • a bassline uses its own scale option, else the key of its over chords;
  • an outer .key() does not re-key generated material, and generators read their over chords on their own. Give written over chords their own key, as in roman("<i VI>").key("E:minor");
  • .transpose() moves generated material, and on a progression it moves the chords' key too, so lines generated over it stay in key.

Options are checked as you type. A misspelt option names the closest one and lists them all:

generateMelody: unknown option "rythm"; did you mean "rhythm"? The options are over, rhythm, seed, phrase, scale (or key), histogram (or weights), range, chordToneWeight, stepSize, maxLeap, density, temperature, repeatPenalty, startNear and endOnChordTone.

Reference, in the ⋯ menu, lists every option of each generator.

Render

When you like a take, render it. After the code has been prepared (press Preview), put the caret inside a generator call. Render is in the header of the generator's card (see Entity cards). It comments out the statement that holds the call and writes the same statement below it with the generated values:

// const prog = generateProgression({ scale: "E:minor", chords: 2, seed: "<1 2>" })
const prog = pcs("<0:3:7 0:3:6 0:3:7 0:4:7>").root("<e3 f#3 e3 c3>").key("E:minor").phrase(2)
export const keys = prog.velocity(0.5)
  • A progression renders as pitch-class sets on roots, with its key and phrase, so each chord can be clicked and revoiced, and melodies still generated over it do not change. A melody or bassline renders as notes, with the key it was generated in.
  • A render is plain notes. Inside a .modulation() it moves by the interval between the tonics, so a move that keeps the mode plays as before, but a mode change (C major to C minor) no longer maps it note by note.
  • A progression also offers Render as objects, the same chords as data: pcs([[0, 3, 7], [0, 3, 6], …]).root(["e3", "f#3", …]).key({ tonic: "E", mode: "minor" }) (see Two forms for every value).
  • A render covers whole phrases and at least two loops of the seed pattern, so a two-seed loop renders both takes. It covers at most 256 cycles.
  • Render is refused when the result would not fit the preset's size limit. A call has no render when a rhythm step crosses a cycle boundary or the material needs more than 48 steps per cycle.
  • There is no Unfreeze yet. The commented-out call stays in the code, so deleting the rendered line and uncommenting the call brings it back.

Songs are modules

A song is a JavaScript module. Its layers are named exports, and its default export, when it has one, is the mix. That makes songs composable: one song can take its drums from a second song and its chords from a third.

A song file describes musical structure: the layers, the values they are built from (sets, keys, histograms, rhythms, seeds) and how they are arranged. Playing it is one use. Analysis and mixing tools are planned to read the same exports (see Planned).

Layers and what plays

  • export const name = pattern is a layer. Its name is its MIDI track.
  • A label, name: pattern, is Strudel's way of naming a part. It still works, and in a file that uses import or export it is a layer and an export too.
  • Exports that are not patterns, such as a histogram or a helper function, are values. They do not play.
  • Without export default, every layer plays, in the order it is written. A label that starts with _ stays muted, as in Strudel.
  • With export default, only the default plays:
    • an object plays each of its patterns as a layer named by its key;
    • a pattern plays as one part, and a list plays each of its patterns;
    • anything else is an error.
const colour = histogram("E:9 B:7 G:6 D:4")
export const keys = generateProgression({ histogram: colour, seed: 3 }).voicing("open")
export const lead = generateMelody({ over: keys, seed: "<9 26>" })
bass: generateBassline({ over: keys, style: "root-fifth" }) // a label is a layer too

// The mix. lead is exported, so other songs can import it, but it does not play here.
export default { keys, bass }

A file with no import or export works as it always has, as in Strudel: its labels play, or, when it has none, its last expression.

Importing from other songs

A song imports layers and values from another song by its id: "./<id>.js". Today the sequencer can import its built-in examples. Importing your own saved songs is not available yet.

import { kit, perc } from "./symbolic-drums-channels.js"
import { keys, lead } from "./symbolic-generative-loop.js"

// Drums from one example, harmony and lead from another, the lead an octave up.
export default { keys, lead: lead.transpose(12), kit, perc }

Imported layers are ordinary patterns in the new song. Here a new counter-melody plays over another song's chords, in the pitch colour of that song's lead:

import { keys, lead } from "./symbolic-generative-loop.js"

export const counter = generateMelody({ over: keys, histogram: histogramOf(lead), rhythm: "~ x ~ x", seed: 4, range: "e3:e4" })
export default { keys, counter }

Values are imported the same way. The Learned colour example exports the pitch colour it learns from its tune, and this song draws new chords and a new line from it:

import { colour } from "./symbolic-learned-colour.js"

const prog = generateProgression({ histogram: colour, chords: 4, seed: 2 })
export const keys = prog.voicing("open").velocity(0.45)
export const lead = generateMelody({ over: prog, histogram: colour, rhythm: "x ~ x x", seed: "<1 2>" })
  • An imported layer sounds as it does in its own song. It keeps any key it sets itself; a layer without one follows the importing song's .key(), like a local layer.
  • The importing song's methods apply to it: .transpose(), .velocity(), .fast() and every other.
  • Drums stay drums, on channel 10.
  • While an imported layer plays, its name in the import line lights up.
  • import groove from "./<id>.js" gives the song's default export. In the sequencer, a song without one still has a default for importers: an object of the layers that play. import * as groove from "./<id>.js" gives every export.
  • A Strudel example can be imported too, and it plays as it does in Strudel.

The Symbolic Cycle examples you can import:

PresetId
Pitch-class setssymbolic-sets
Voicing shapessymbolic-points
Rootless voicingssymbolic-voicings
Note sets in rhythmsymbolic-notesets
Set transformationssymbolic-transforms
Drums and channelssymbolic-drums-channels
Generative loopsymbolic-generative-loop
Learned coloursymbolic-learned-colour
Roman numerals and keyssymbolic-roman-keys
Seed forms (AAAB)symbolic-seed-forms
Set arrangementsymbolic-arrangement
Distilled groovesymbolic-distilled-groove

Mistakes are reported on the line that makes them:

  • an unknown id: Cannot find song "./symbolic-generative-lop.js"; close ids: symbolic-generative-loop;
  • a name the song does not export: the message lists what it does export;
  • a name that is both imported and declared: "kit" is imported and declared;
  • export * from "./<id>.js": name the layers instead, as in export { kit, bass } from "./<id>.js".

The language import

The functions of the language (drums, pcs, generateMelody and the rest) need no import in the sequencer. A file without an import line is compiled as if it imported every language name it uses. Written out, that line is:

import { drums, generateMelody, generateProgression } from "@codex-music/module-code-sequencer/symbolic-cycle"
const prog = generateProgression({ scale: "D:dorian", seed: 2 })
export const keys = prog.velocity(0.5)
export const lead = generateMelody({ over: prog, seed: "<1 2>" })
export const kit = drums("kick ~ snare ~")

The sequencer checks an import line when there is one: a name that is not part of the language is an error ("prog" is not part of Symbolic Cycle), and an import from @strudel/core tells you to switch the language to Strudel. In the sequencer, the short specifier "symbolic-cycle" works too. Methods such as .velocity() need no import.

The module has a function that writes this line, withLanguageImports. The editor has no button for it yet.

Running a song in plain JavaScript (preview)

With its import line, a song file is a real JavaScript module. The runtime entry exports the language's functions and songPattern, which gives the pattern the sequencer would play: the same rules for what plays, with keys, voicings and track names applied.

// run.ts, next to my-song.js (a song file with its import line)
import { songPattern } from "@codex-music/module-code-sequencer/symbolic-cycle"
import * as song from "./my-song.js"

const pattern = songPattern(song)
for (const hap of pattern.queryArc(0, 1)) {
  console.log(hap.whole.begin.valueOf(), hap.value.note) // position in cycles, MIDI note
}

This is a preview. The package is not published yet, and today Node needs a TypeScript loader and a resolver hook for one of Strudel's dependencies. The module's tests use both: they import song files with Node's own import() and check that songPattern plays exactly what the sequencer plays. MIDI channels are numbered when the sequencer prepares its timeline, so a pattern played this way has no automatic channels.

Arrangements and modulation

arrange puts sections in order, and it names them. Write a plan of section names with their lengths in cycles, and the patterns it names:

// intro for 2 cycles, then verse for 2. .modulation() lifts the second pass
// a tone: every part moves from the key it is written in, even the lead,
// which is in B minor. Keys and lead land in E major; drums never move.
const intro = roman("<I IV>").velocity(0.5).s("keys")
const verse = stack(intro, n("<[2 ~ 2 4] [5@3 4]>").octave(1).key("B:minor").s("lead"), drums("kick ~ snare ~").s("kit"))
export default arrange("<intro@2 verse@2>", { intro, verse }).key("D:major").modulation("<D:major@4 E:major@4>")
  • Three forms, one sound. Strudel's arrange([2, intro], [2, verse]) works as before, and so does data: arrange({ sections: [{ name: "intro", cycles: 2, play: intro }, …] }). The roll labels each section above the notes, the editor tints the one playing, and the arrange card switches a call between the three forms.
  • Copies restart, lengths continue. Each section starts its pattern afresh on the first pass. verse!2 is two sections that each restart; verse@2 is one longer section that continues. ~@4 is four cycles of silence.
  • Arrangements compose. song.section("verse") keeps the verses, song.map(…) gives sections new patterns, song.times(2) repeats the plan, and an arrangement or an imported song can be a section of another: a DJ set is arrange("<a@32 b@32>", { a: groove, b: loop }).
  • .key() fills in, .modulation() moves. An outer .key() only keys parts that have none. .modulation() moves every tonal part from its own key: numerals and scale steps are read again in the new key, generated lines move by degree, and notes and chord symbols move by the interval between the tonics (they need a key, from .key() or { from: "C:major" }). "<0@24 2@8>" moves by semitones instead.

Two forms for every value

Every value has a string form, the short mini-notation you live-code with, and an object form, plain data that a tool or a script can write. Every option takes either, and both play the same notes:

// The same kind of song written as data: a key, a seed per phrase, a range,
// a rhythm and drum lanes as objects. Quoted words inside are constants.
const prog = generateProgression({ scale: { tonic: "E", mode: "minor" }, seed: { phrases: [3, 5] } })
const rhythm = { steps: 4, hits: [{ at: 0, length: 1 }, { at: 2, length: 2 }] }
export const keys = prog.velocity(0.45)
export const lead = generateMelody({ over: prog, range: { low: "e4", high: "b5" }, rhythm, seed: 9 })
export const kit = drums({ kick: "x ~ [~ x] ~", snare: "~ x ~ x", closedHat: "x*8" })
ValueString formObject form
seed"<3 5 ~ 8>"{ phrases: [3, 5, null, 8] }
note range"e4:e5"{ low: "e4", high: "e5" }
histogram"E:9 B:7 G:6"{ E: 9, B: 7, G: 6 }
rhythm"x@3 x x@2 x@2"{ steps: 8, hits: [{ at: 0, length: 3 }, …] }
key"E3:minor"{ tonic: "E", mode: "minor", octave: 3 }
keys over time"<F3:major@24 E:major@8>"[{ key: "F3:major", cycles: 24 }, { key: "E:major", cycles: 8 }]
sets"<0:4:7 ~ 0:3:7>"[[0, 4, 7], null, [0, 3, 7]]
Roman numerals, chords"<I V vi IV>", "<Em G>"{ chords: [{ roman: "I", cycles: 1 }, …] }, [{ chord: "Em", cycles: 1 }, …]
drums"kick ~ snare ~"{ kick: "x ~ ~ ~", snare: "~ ~ x ~" }
  • Values print themselves. key("E:minor"), keySequence("<…>"), histogram(…) and histogramOf(…) give values whose String(v) is the string form and whose JSON.stringify(v) is the object form.
  • Some strings have no object form, such as a seed with brackets or a set held for two cycles, and the object form does not keep spelling such as ! or _. An object also has no source positions, so its notes do not light up word by word.
  • The card switches forms. A card row with two forms has Write as object or Write as string in its ⋯ menu, which rewrites the value without changing what plays, or says why it cannot. A generated progression also offers Render as objects (see Render).

Entity cards

Put the caret in a Symbolic Cycle call (a generator, histogram(), a pitch or drum constructor such as pcs() or drums(), or a method such as .key(), .octave() or .velocity()), or click its name or one of its values, and its card opens in the palette beside the code, at the parameter you clicked. The code keeps its width while the palette opens and closes. Strudel's other functions, such as .struct(), .s() or stack(), are edited in the code. A set word still opens the structure editor. With nothing selected, the palette says what to click.

  • The call card. The header names the function, with Render for a generator, an ⓘ button that shows what the call and each parameter do, and a ⋯ menu. The parameters follow in order: the ones you wrote, each with its control, then the rest under More options with their defaults; setting one writes it into the code. Nested calls, such as .velocity() after a generator, get a row of buttons to switch between them. The row you clicked is scrolled into view and highlighted.
  • Chords. A card for chords (a generated progression, pcs(), chord() or roman()) names them (Em Am D G), and its + voicing, + inversion, + transpose and + key buttons add that method to the chain and open its card.
  • Keys and modulation. When a .modulation() on the same chain overrides a .key(), the key card says so and offers to edit the modulation. A key sequence such as "<D:major@24 E:major@8>" opens a card of segments, each a key and a length in cycles.
  • Your spelling stays. A change rewrites only that value, in the way you wrote it: quotes, note case, flats or sharps, the order of histogram entries.
  • Names are followed. For histogram: colour, the card edits const colour = histogram(…) where it is written and says on which line. For over: prog, it says what prog is, with a button that moves the editor to its definition.
  • One change per gesture. A whole slider drag, or a typed value once you press Enter or leave the field, is written as one change. Cmd/Ctrl+Enter in a field writes it before applying.
  • Size limit. A change that would take the code past 3,072 bytes is refused, and the card says why.
  • The dotted underline marks every name and value a card can edit.
ValueCard
Seeda slot per phrase, each with a dice button, and buttons to add a phrase or randomize them all
Note rangea two-thumb slider over a keyboard strip
Histogramtwelve bars over a keyboard: drag a bar, or sweep across several, to raise or lower a pitch class. The card names the key the weights read as, or draws the call's own key
Rhythma step grid: click a step to start or clear a note, drag to set its length, and choose how many steps a cycle has
Numbers, switches, choicesa one-line slider with its value, an on/off switch, segmented buttons
A value the generator computes, such as a histogram or scale taken from overone line saying where it comes from, until you set it
Key or scaletwelve tonic buttons and a mode menu
Key sequenceits segments, each a key and a length in cycles
Note, intervalsemitone and octave buttons with a note field, or semitones with the interval name
A name that refers to another valuewhat the name is and where it is defined, with a button to go there
Set wordsthe structure editor

Some values are shown as written, with the reason: patterns such as .root("<c4 a3>"), drum words, other mini-notation, and histogramOf(…), which is learned when the song plays.

Planned

None of the following is built beyond what Entity cards describes.

Harmony selectors (planned, MOD-146)

Changing key, reaching minor or a mode, or modulating should not need typing .key("<F3:major@24 E:major@8>").

  • A key card: today's card has twelve tonic buttons and a mode menu; the planned one adds a key wheel and a Major, Minor and Modes switch, with the relative, parallel, dominant and subdominant keys one click away.
  • A modulation timeline for a patterned key: today's card lists the segments, each a key and a length in cycles; the timeline adds dragging a boundary and splitting or merging segments.
  • A Roman progression card for roman("…"): the chords as chips, a palette of numerals that fit the key, and a preview of each chord. The key is an input of the card.
  • A chord-symbol card, and a generator scale option that can itself be a pattern, so a generated part can modulate.

Key finding (planned, MOD-147)

The histogram card already names the key its weights read as, and generateProgression already takes its key from a histogram. The plan is one shared key-finding implementation for both, with a choice of key profiles and of modes, and:

  • keyOf(colour), which returns a key name for .key() or a generator's scale option;
  • .key(histogram("E:9 G:6 B:7")), which keys a part from twelve weights in one call;
  • a ranked list of candidate keys in the histogram card, where you pick the one to write.

describeSong (planned, MOD-148)

describeSong(song, { cycles }) would return a plain, JSON-ready description of a song: its layers, each with a role (tonal or drums) and events that keep their musical meaning (set, root, key, scale degree, voicing, the generator and options that made them), its key timeline, its sections, histograms and tempo hints. Analysis views and a DJ-style mixer would read that description, and a player could play it without the Symbolic Cycle runtime.

Further out

The roadmap also lists notation conversion in the editor (for example pcs to points with the root folded in), more notations (intervals, set classes, guitar and grid positions), more generators (rhythm, harmonizing a melody, motif development, voice leading) and distillation: rewriting Strudel code or MIDI notes as the most abstract Symbolic Cycle that plays them exactly.

Good to know

  • It is Strudel underneath. stack, arrange, fast, slow, every, off, struct, Euclidean rhythms and the rest work as in Strudel. Two names differ: .voicing() takes a style name instead of reading Strudel's voicing dictionaries, and .octave(octaves) is a relative shift.
  • Limits. A song's source is at most 3,072 bytes (Code size in the ⋯ menu) and a pass is at most 256 cycles. Imports let a song use more material than fits in one file.
  • Shortcuts. Cmd/Ctrl+Enter plays, or applies an edit while playing, at the next cycle; Cmd/Ctrl+. stops. In the editor, Tab indents; Escape then Tab moves focus on.
  • Presets keep working. Saved Symbolic Cycle presets, including ones written with labels, play as before.