# 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.

Canonical: https://maru.audio/docs/symbolic-cycle
Documentation index: https://maru.audio/llms.txt

> **Work in progress.** Symbolic Cycle is changing quickly. This page describes
> what the [Code Sequencer](https://maru.audio/modules/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](https://maru.audio/modules/code-sequencer) turns it into MIDI notes, one track per part, for an instrument
to play. It runs on Maru's own pattern engine. Its mini-notation and
pattern functions (`stack`, `fast`, `arrange`…) follow the live-coding
tradition of [TidalCycles](https://tidalcycles.org/), so they read 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](https://maru.audio/symbolic)
catalog of concepts and notations.

## Try it

1. Open the [Code Sequencer](https://maru.audio/modules/code-sequencer).
2. The preset menu in the title bar 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:

| Write | Meaning |
| --- | --- |
| `"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.

```js
// 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](#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.

| Spelling | Example | What it fixes | What comes from context | Catalog concept |
| --- | --- | --- | --- | --- |
| Scale steps | `n("0 2 4")` | steps in a scale | the key and its octave | [Key](https://maru.audio/symbolic/key) |
| Roman numerals | `roman("ii7 V7 I")` | degree and quality | the key (C major if none) | [Functional harmony](https://maru.audio/symbolic/functional-harmony) |
| Chord symbols | `chord("Dm7 G7 C")` | root and quality | the register (roots from C3 to B3) | [Chord](https://maru.audio/symbolic/chord) |
| Pitch-class sets | `pcs("0:3:7")` | which pitch classes | the root (C4 if none) | [Pitch-class set](https://maru.audio/symbolic/pc-set) |
| Point sets | `points("0:7:15")` | a shape with its octaves | the root (C4 if none) | [Point set](https://maru.audio/symbolic/point-set) |
| Note sets and notes | `noteset("d3:f3:a3")`, `note("d3")` | every pitch | nothing | [Note set](https://maru.audio/symbolic/note-set) |
| Drums | `drums("kick snare")` | the drum sound | nothing | |

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.

```js
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: 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 the `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.

```js
// 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:

```js
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:

| Method | What it sets | Chained twice |
| --- | --- | --- |
| `.key(name)`, `.scale(name)` | the key of scale steps and Roman numerals | inner-wins |
| `.root(note)` | the root of pitch-class and point sets; where chord symbols and Roman numerals start their octave | inner-wins |
| `.voicing(style)`, `.inversion(steps)` | how chords are voiced | inner-wins |
| `.phrase(cycles)` | the phrase length a generator reads from chords (see [Seeds and phrases](#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 octaves | the 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.

```js
// 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:

```js
// 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.

Pattern randomness (`?`, `|`, `sometimes`, `degradeBy`) is seeded too: the
same cycles always make the same choices, and `.seed(n)` picks another set.
It is separate from the generators' seeds.

## 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.

| Generator | Writes | Main options |
| --- | --- | --- |
| `generateProgression` | diatonic chords, as pitch-class sets on roots | `scale`, `histogram`, `chords` (4), `every` (1 cycle), `sevenths` (0 to 1), `cadence`, `seed` |
| `generateMelody` | one note per rhythm step over chords | `over`, `rhythm`, `range` (`"c4:g5"`), `stepSize`, `maxLeap`, `histogram`, `seed` |
| `generateBassline` | one note per rhythm step under chords | `over` (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.

```js
// 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:

```js
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:

```text
generateMelody: unknown option "rythm"; did you mean "rhythm"? The options are over, rhythm, seed, phrase, scale (or key), histogram (or weights), inferScale, histogramMode, preserveVelocity, 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](#entity-cards)). It
comments out the statement that holds the call and writes the same statement
below it with the generated values:

```js
// 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](#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](#planned)).

### Layers and what plays

- `export const name = pattern` is a layer. Its name is its MIDI track.
- A label, `name: pattern`, is the live-coding 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.
- **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.

```js
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: 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.

```js
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:

```js
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:

```js
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.

The Symbolic Cycle examples you can import:

| Preset | Id |
| --- | --- |
| Two-channel duet | `channel-duet` |
| Chord loop | `chord-loop` |
| Bassline | `bassline` |
| Euclidean kit | `euclid-kit` |
| Song form | `song-form` |
| Groove with breakdown | `groove-build` |
| Drifting ambient | `drifting-ambient` |
| Sustained note | `sustained-note` |
| Full-length density check | `density-check` |
| Pitch-class sets | `symbolic-sets` |
| Voicing shapes | `symbolic-points` |
| Rootless voicings | `symbolic-voicings` |
| Note sets in rhythm | `symbolic-notesets` |
| Set transformations | `symbolic-transforms` |
| Drums and channels | `symbolic-drums-channels` |
| Generative loop | `symbolic-generative-loop` |
| Learned colour | `symbolic-learned-colour` |
| Rhythm names and references | `symbolic-rhythm-references` |
| ABC tune over a PLR chain | `symbolic-abc-plr` |
| Roman numerals and keys | `symbolic-roman-keys` |
| Seed forms (AAAB) | `symbolic-seed-forms` |
| Set arrangement | `symbolic-arrangement` |
| Ragtime 121 placements | `symbolic-ragtime-121` |
| Modal rhythm roles | `symbolic-modal-rhythm-roles` |
| 121 seed variants | `symbolic-ragtime-variants` |
| Distilled groove | `symbolic-distilled-groove` |

The two 121 studies use the [Code Sequencer](https://maru.audio/modules/code-sequencer)'s digital **Analyze** lane. After
Preview, open Analyze above the roll to inspect the selected melody's attacks
and the four 2/4 onset-mask placements. Switch the readout between beats,
seconds and sixteenth steps, or show the accompaniment as inferred chord names
and anchored points. Brackets mark exact repeats of a voiced chord cycle. In
**121 seed variants**, edit `level` (0–4) and `seed`
then Preview again; the Roman-numeral harmony stays fixed while the selected
mask count changes. This count is a pattern constraint, not a listener rating
of syncopation.

**Modal rhythm roles** gives the lead a fixed attack/hold/rest grid and the bass
absolute beat positions. Edit `tonic = 'F'` to `tonic = 'G'` to move the tonal
center of its named harmony, lead, bass and melody tracks together. Edit `mode`
to change the scale context; the Roman chord qualities are still the numerals
written in the source. `chordRegister` sets the chords' voicing window without
changing the tonic. The symbolic library's `convertRhythmNotation` converts
between duration, position and grid spellings while preserving exact timing
when the target grid can represent it.

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:

```js
import { drums, generateMelody, generateProgression } from "@maruaudio/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 of another pattern language says to write Symbolic Cycle instead. 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.

```ts
// run.ts, next to my-song.js (a song file with its import line)
import { songPattern } from "@maruaudio/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. The module's tests use one: 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:

```js
// 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.** The positional `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:

```js
// 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" })
```

| Value | String form | Object 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](#entity-cards) 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](#render)).

## Using the Symbolic library from your song

[Symbolic](https://maru.audio/symbolic) is the library behind this language. It knows concepts
and notations, not time: it has no cycles, tempo or MIDI channels. Symbolic
Cycle is one program built on it. Because the values are plain data, you can
move between the two with ordinary JavaScript:

1. **Take a value out.** A set is an array of semitones, a rhythm is a list of
   hits, and `key()`, `keySequence()` and `histogram()` give values whose
   `JSON.stringify(v)` is their [object form](#two-forms-for-every-value).
2. **Transform it** with any JavaScript: `map`, `sort`, `filter`, your own
   functions.
3. **Give it back.** Options, and constructors such as `pcs()`, take the
   object form as well as the string form, so the result plays like anything
   you typed.

```js
// A pitch-class set is plain data: semitones above a root. Rotate the
// C major triad so each of its notes becomes 0 in turn, then give the
// arrays back to pcs() in its object form, one set per cycle.
const triad = [0, 4, 7]
const rotate = (set, from) => set.map((step) => (step - set[from] + 12) % 12).sort((a, b) => a - b)
const shapes = triad.map((_, from) => rotate(triad, from))
export const turns = pcs(shapes).root("c3")
```

`shapes` is `[[0, 4, 7], [0, 3, 8], [0, 5, 9]]`, so `turns` plays C major,
A flat major over C and F major over C: three chords that share the note C.

### Values by address: `sym`, `defineConcept` and `Symbolic`

Every value on the [Symbolic catalog](https://maru.audio/symbolic) pages has an address with
three parts, `concept/notation/value`, the same as its page:
`/symbolic/pc-set/binary/100010010000` is the C major triad written in binary.
`sym()` plays any address, a name for one, or a library value. Write an
address with slashes in single quotes, because in a double-quoted string `/`
divides time. Inside mini-notation use the colon form,
`concept:notation:value`, or a name.

- `defineConcept("home", 'pc-set/numbers/0-4-7')` names a value for this song.
  Built-in names come from the catalog: `tristan`, `mystic`, `petrushka`,
  `major-chord`, `whole-tone-scale` and others.
- `Symbolic` is the library itself, inside the song. `Symbolic('pc-set/numbers/0-4-7')`
  gives the typed value (here a pitch-class set with a `pitchClasses` list);
  change it in JavaScript and give it to `sym()` or `pcs()`.

```js
// A name, a binary set, a chord symbol and the Tristan chord, one per cycle,
// then the same triad mirrored in plain JavaScript.
defineConcept("home", 'pc-set/numbers/0-4-7')
const triad = Symbolic('pc-set/binary/100010010000')
const mirrored = triad.pitchClasses.map((pc) => (12 - pc) % 12)
export const pad = sym("<home chord:symbol:Am7 tristan>").root("c4").s("pad")
export const echo = pcs([mirrored]).root("c3").s("echo")
```

`sym()` also plays chord and numeral spellings that mini-notation cannot
hold, because `/`, `|` and parentheses are operators there: a polychord
`sym('chord/symbol/D|C')`, `sym('chord/symbol/C(b5)')`, a Harte label
`sym('chord/harte/C:maj7/3')`, figured numerals `sym('functional-harmony/roman/ii-V65-I64')`
and Nashville numbers `sym('functional-harmony/nashville/1 4 5/7 6m')`. Slash
basses and inversions sound exactly. In `chord()` and `roman()` write the
slash as `~` (`C~E`, `IV~5`); `roman("<V65 I64>")` reads inversion figures, and
`chord("N.C.")` is a rest.

`sym()` also reads scores and pad layouts. A tune in ABC or Humdrum
`**kern` (`melody/abc/…`, `score/kern/…`) plays at its own durations, one bar
of its meter per cycle; a score plays each voice as its own part on its own
channel, and its chord lane (ABC `"Am"`) as chords. `chord-sequence/plr/G:P`
walks a neo-Riemannian chain from a triad (P, L and R flip one note), one
chord per cycle. A pad position (`grid-position/launchpad/11 13 21`, also
`cells` and `chess`) sounds on the layout `.layout()` names, such as
`wicki-hayden`, `harmonic-table`, `janko` or `tonnetz`, with cell (0, 0) on
`.root()`. Without `.layout()` it uses `aura` and **Problems** says so.

```js
// A 3/4 tune in ABC over G and G minor, and a G triad on Wicki-Hayden pads.
export const lead = sym('melody/abc/X:1 M:3/4 L:1/4 K:G B A G|_B2 D|]').s("lead")
export const pad = sym('chord-sequence/plr/G:P').root("c3").s("pad")
export const pads = sym('grid-position/launchpad/11 13 21').layout("wicki-hayden").root("g3").s("pads")
```

An address can also be written as a `symbolic:` URI,
`symbolic:pc-set/binary/100010010000`: the part after `symbolic:` is the
page's path after `/symbolic/`, so every value names the page that shows it.
`Symbolic.transcribe(value, { to, from, context })` moves a value into another
concept or notation and lets the library choose the conversion: it reads the
value, finds the bridge path, fills missing context with a default and lists
each assumption and loss under **Problems**. `Symbolic.canTranscribe(from, to)`
says whether a pair converts, and `Symbolic.describeBridge(from, to)` how: two-way,
one-way with what it drops, partial, or deliberately not at all, as for a
rhythm's 0/1 string and a pitch-class set's. `sym()` plays the result.

```js
// The C major triad by URI, and ii-V-I transcribed into chord symbols in G major.
export const pad = sym('symbolic:pc-set/binary/100010010000').root("c4").s("pad")
const changes = Symbolic.transcribe('ii-V-I', { from: 'functional-harmony/roman', to: 'chord-sequence/symbols', context: { key: 'G major' } })
export const keys = sym(changes).root("c3").s("keys")
```

When a value lacks something the notes need, such as a pitch-class set's
root or a chord's octave, the sequencer uses the part's `.root()` or `.key()`
and otherwise a default, and lists what it assumed under **Problems** with an
underline in the code. Every concept and how it plays is in the
[language reference](https://github.com/iplanwebsites/maru-workspace/blob/main/modules/code-sequencer/docs/symbolic-cycle-reference.md#references-and-named-concepts).

### Named rhythms

`rhythm()` writes a rhythm by name or in a rhythm notation, and `.rhythm()`
puts a pattern's notes on it. Drum lanes take the same words.

```js
// Tresillo and son clave from Toussaint's catalog of timelines, a 3-3-2
// written as inter-onset intervals, and a rotated Euclidean rhythm.
export const kit = drums({ kick: "tresillo", rim: "son-clave", closedHat: "E(5,8,2)" })
export const bass = n("0 0 3 5").scale("A2:minor").rhythm("ioi:3-3-2").s("bass")
```

The notations are binary (`10010010`), box (`x..x..x.`), inter-onset
intervals (`ioi:3-3-2`), Euclidean (`E(3,8,2)`) and onset positions
(`pos:0-3-6:8`); names include tresillo, cinquillo, habanera, both claves,
bossa-nova, gahu and bembé.

## 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.
The other pattern 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.

| Value | Card |
| --- | --- |
| Seed | a slot per phrase, each with a dice button, and buttons to add a phrase or randomize them all |
| Note range | a two-thumb slider over a keyboard strip |
| Histogram | twelve 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 |
| Rhythm | a 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, choices | a 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 `over` | one line saying where it comes from, until you set it |
| Key or scale | twelve tonic buttons and a mode menu |
| Key sequence | its segments, each a key and a length in cycles |
| Note, interval | semitone and octave buttons with a note field, or semitones with the interval name |
| A name that refers to another value | what the name is and where it is defined, with a button to go there |
| Set words | the 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](#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 pattern code or
MIDI notes as the most abstract Symbolic Cycle that plays them exactly.

## Good to know

- **Familiar pattern functions.** `stack`, `arrange`, `fast`, `slow`, `every`,
  `off`, `struct`, Euclidean rhythms and the rest work as in TidalCycles, on
  Maru's own engine. Two names differ: `.voicing()` takes a style
  name, and `.octave(octaves)` is a relative shift. Only MIDI-relevant
  functions exist: there are no sound or effect controls.
- **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.
