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:
| Property | Type | Meaning |
|---|---|---|
ctx.cycle | number | Current cycle (0-indexed, increments each cycle) |
ctx.beatsPerCycle | number | Current beats per cycle setting |
ctx.rand() | function | Seeded random number 0–1 (reproducible) |
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.
Tempo changes
yield { type: 'tempo', bpm: 140 } // change to 140 BPM immediatelyyield { type: 'tempo', bpm: 120, once: true } // only on cycle 0, stays until changed againBeats per cycle
yield { type: 'bpc', bpc: 8 } // switch to 8 beats per cycleyield { type: 'bpc', bpc: 3 } // 3/4 waltz feelMaster FX from meta events
yield { type: 'fx_reverb', mix: 0.4 }yield { type: 'fx_delay', time: 0.375, feedback: 0.4 }yield { type: 'fx_gain', gain: 0.8 }Conditional tempo ramp
function* song(ctx) { const bpm = 100 + ctx.cycle * 2 // speeds up by 2 BPM each cycle yield { type: 'tempo', bpm: 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 { type: 'eos' }:
if (ctx.cycle >= 16) yield { type: 'eos' }Next
6. Generative Music → — modifiers, Markov chains, L-systems, and sidechain routing.