Skip to content

5. Song Structure

So far every cycle sounds the same. This tutorial shows how to make music that changes over time.

ctx — the cycle context

Every song(ctx) call receives a context object:

PropertyTypeMeaning
ctx.cyclenumberCurrent cycle (0-indexed, increments each cycle)
ctx.beatsPerCyclenumberCurrent beats per cycle setting
ctx.rand()functionSeeded random (reproducible). rand(min, max) for a float range, rand.int(min, max) for an integer (inclusive)

Use ctx.cycle to make things change:

function* song(ctx) {
const synth = vasynth({ wave: 'sawtooth', release: 0.3 })
const key = scales(4, 'C4:minor')
// Different melody on even vs odd cycles
const melody = ctx.cycle % 2 === 0
? seq(4, '0,2,4,3')
: seq(4, '4,3,2,0')
yield cast(synth, key, melody, ctx)
}

Conditional parts

Add a hi-hat only after cycle 4, and a second melody after cycle 8:

function* song(ctx) {
const lead = vasynth({ wave: 'triangle', release: 0.3 })
const hh = hat()
const key = scales(4, 'D4:minor')
yield cast(lead, key, seq(4, '0,2,4,3,2,0,_,_'), ctx)
if (ctx.cycle >= 2) {
yield cast(hh, 'drums', seq(4, 'x.x.x.x.'), ctx)
}
if (ctx.cycle >= 4) {
const pad = vasynth({ wave: 'sawtooth', cutoff: 600, attack: 0.6, gain: 0.25 })
yield cast(pad, key, seq(4, '{0,2,4},~,~,~'), ctx)
}
}

Four-bar phrase structure

Group cycles into 4-bar phrases using modulo:

const phase = ctx.cycle % 4 // 0, 1, 2, 3, 0, 1, 2, 3, ...
// Variation on bar 4 (phase 3)
const melody = phase < 3
? seq(4, '0,2,4,3,2,4,0,_')
: seq(4, '4,3,2,0,_,_,_,_') // fill / turnaround
yield cast(lead, prog, melody, ctx)

Meta events — tempo and time signature

Yield meta events alongside patterns to change global settings mid-song.

The ctl.* and fx.* helpers build these for you (raw objects work too — see Meta Events).

Tempo changes

yield ctl.tempo(140) // change to 140 BPM immediately
yield ctl.tempo(120, { once: true }) // only on cycle 0, stays until changed again

Beats per cycle

yield ctl.bpc(8) // switch to 8 beats per cycle
yield ctl.bpc(3) // 3/4 waltz feel

Master FX from meta events

yield fx.reverb(0.4)
yield fx.delay(0.375, 0.4)
yield fx.gain(0.8)

Conditional tempo ramp

function* song(ctx) {
const bpm = 100 + ctx.cycle * 2 // speeds up by 2 BPM each cycle
yield ctl.tempo(Math.min(bpm, 160)) // cap at 160
const synth = vasynth()
const key = scales(4, 'C4:major')
yield cast(synth, key, seq(4, '0,2,4,2'), ctx)
}

arrange() — named sections

arrange() lets you define a song as named sections that play in order. Each section runs for a set number of cycles.

function* song(ctx) {
yield* arrange(ctx, [
{
name: 'intro',
cycles: 4,
patterns: (c) => [
cast(lead, key, seq(4, '0,2,4,2'), c),
],
},
{
name: 'verse',
cycles: 8,
patterns: (c) => [
cast(lead, prog, seq(4, '0,2,4,3,2,0,_,_'), c),
cast(bass, prog, seq(4, '0,_,4,_'), c),
cast(kk, 'drums', seq(4, 'x...x.x.'), c),
],
},
{
name: 'chorus',
cycles: 8,
crossfade: 1,
patterns: (c) => [
cast(lead, prog2, seq(4, '4,3,2,0,4,3,2,_'), c),
cast(pad, prog2, seq(4, '{0,2,4},~,~,~'), c),
cast(bass, prog2, seq(4, '0,_,4,_'), c),
cast(kk, 'drums', seq(4, 'x.x.x.x.'), c),
cast(sn, 'drums', seq(4, '..x...x.'), c),
],
},
])
}

The patterns field is a function receiving the current IncantoContext — use it to access cycle, rand(), etc.

crossfade: N blends the previous section over N cycles for smooth transitions.

Full arrange example — verse/chorus song

function* song(ctx) {
const lead = vasynth({ wave: 'triangle', cutoff: 3500, release: 0.3, gain: 0.55 })
const pad = vasynth({ wave: 'sawtooth', cutoff: 600, attack: 0.5, release: 1.5, gain: 0.2 })
const bass = vasynth({ wave: 'square', cutoff: 350, release: 0.15, gain: 0.6 })
const kk = kick()
const sn = snare()
const hh = hat({ decay: 0.04 })
const key = scales(4, 'A4:minor')
const bKey = key.transpose(-24)
const prog = chords(8, key, 'I,VII,VI,VII')
const bProg = prog.transpose(-24)
yield* arrange(ctx, [
{
name: 'intro',
cycles: 4,
patterns: (c) => [
cast(pad, prog, seq(4, '{0,2,4},~,~,~'), c),
],
},
{
name: 'verse',
cycles: 8,
crossfade: 1,
patterns: (c) => [
cast(lead, prog, seq(4, '0,2,4,3,2,0,_,_'), c),
cast(bass, bProg, seq(4, '0,_,_,4,0,_,2,_'), c),
cast(kk, 'drums', seq(4, 'x...x.x.....x...'), c),
cast(sn, 'drums', seq(4, '....x.......x...'), c),
cast(hh, 'drums', seq(4, 'x.x.x.x.x.x.x.x.'), c),
],
},
{
name: 'chorus',
cycles: 8,
crossfade: 2,
patterns: (c) => [
cast(lead, prog, seq(4, '4,3,2,4,7,4,2,0'), c),
cast(pad, prog, seq(8, '{0,2,4},~,~,~,{0,2,4},~,~,~'), c),
cast(bass, bProg, seq(4, '0,_,_,4,0,_,2,_'), c),
cast(kk, 'drums', seq(4, 'x.x.x.x.x.x.x.x.'), c),
cast(sn, 'drums', seq(4, '..x...x.'), c),
cast(hh, 'drums', seq(2, 'x.x.'), c),
],
},
{
name: 'outro',
cycles: 4,
crossfade: 2,
patterns: (c) => [
cast(pad, prog, seq(4, '{0,2,4},~,~,~'), c),
],
},
])
}

Ending a song

arrange() ends the song after all sections complete. For manual control, yield ctl.eos():

if (ctx.cycle >= 16) yield ctl.eos()

Next

6. Generative Music → — modifiers, Markov chains, L-systems, and sidechain routing.