Skip to content

3. Melody & Harmony

Real music has multiple simultaneous parts. In Nimbus, each yield in a cycle plays in parallel — just add more lines.

Multiple voices

function* song(ctx) {
const lead = vasynth({ wave: 'triangle', cutoff: 3000, release: 0.3 })
const pad = vasynth({ wave: 'sawtooth', cutoff: 600, attack: 0.5, release: 1.5, gain: 0.3 })
const key = scales(4, 'D4:minor')
yield cast(lead, key, seq(4, '0,2,4,3,2,0,_,_'), ctx)
yield cast(pad, key, seq(4, '{0,2,4},~,~,~'), ctx)
}

Each cast is an independent voice. They share the same scale, so their notes always fit together.


chords — chord progressions

chords(beats, scale, progression) cycles through Roman-numeral chords, with each chord changing the note pool for every cast that uses it.

const key = scales(4, 'C4:major')
const prog = chords(8, key, 'I,V,VI,IV') // classic I–V–vi–IV

Pass prog to cast instead of key:

yield cast(lead, prog, seq(4, '0,2,4,2'), ctx)

Now the melody automatically fits the chord changes.

Common progressions

chords(8, key, 'I,V,VI,IV') // pop (C G Am F)
chords(8, key, 'I,IV,V,I') // blues / gospel
chords(8, key, 'II,V,I,I') // jazz II–V–I
chords(4, key, 'I,VII,VI,VII') // rock (I–bVII–bVI–bVII)
chords(8, key, 'Imaj7,IVmaj7,IIm7,V7') // smooth jazz

Chord quality suffixes

You can override the diatonic quality:

chords(8, key, 'Imaj7,IVmaj7,V7,I') // jazz major sound
chords(8, key, 'Im7,IVm7,bVIImaj7,Im7') // minor funk
chords(4, key, 'Idim7,IIdim7,IIIdim7') // fully diminished — tense!

Holding chords longer

Use ~ to extend a chord across extra beats:

chords(8, key, 'I~~,V~~,VI,IV') // I for 3 beats, V for 3 beats, then VI and IV one beat each

Melody over chords

The key insight: when you cast a melody against a ChordPattern, every scale degree resolves against the current chord — not against the root key. Degree 0 is always the chord root, 2 is the chord third, 4 is the fifth.

function* song(ctx) {
const lead = vasynth({ wave: 'triangle', cutoff: 4000, release: 0.35, gain: 0.55 })
const chd = vasynth({ wave: 'sawtooth', cutoff: 500, attack: 0.4, release: 1.2, gain: 0.25 })
const bass = vasynth({ wave: 'square', cutoff: 400, attack: 0.01, release: 0.2, gain: 0.6 })
const key = scales(4, 'D4:minor')
const bKey = key.transpose(-24) // two octaves lower for bass
const prog = chords(8, key, 'I,VII,VI,VII')
yield cast(lead, prog, seq(4, '0,2,4,2,3,2,0,_'), ctx)
yield cast(chd, prog, seq(8, '{0,2,4},~,~,~,{0,2,4},~,~,~'), ctx)
yield cast(bass, prog, seq(4, '0,_,_,_,0,_,4,_'), ctx)
}

Bass lines

A bass line typically plays the chord root two octaves below. Use .transpose() on the chord pattern or scale:

const bProg = prog.transpose(-24) // two octaves down
yield cast(bass, bProg, seq(4, '0,_,0,4,_,0,_,_'), ctx)

Walking bass

Move between chord tones and passing tones:

// Arpeggiate the chord root–third–fifth–third
yield cast(bass, prog, seq(4, '0,2,4,2'), ctx)
// Simpler two-note pattern
yield cast(bass, prog, seq(4, '0,_,4,_'), ctx)
// Funky: syncopated with a push
yield cast(bass, prog, seq(4, '0,_,_,4,0,_,2,_'), ctx)

Octave displacement

Add 7 to a degree to jump an octave up (for 7-note scales):

// Jump to the upper octave on the last note
yield cast(lead, prog, seq(4, '0,2,4,7'), ctx)
// Wide-range arpeggio
yield cast(lead, prog, seq(4, '0,2,4,7,4,2,0,_'), ctx)

Negative degrees go below the root: -7 = one octave down.


Complete song: melodic pop beat

function* song(ctx) {
const lead = vasynth({ wave: 'triangle', cutoff: 3500, attack: 0.005, release: 0.3, gain: 0.55 })
const pad = vasynth({ wave: 'sawtooth', cutoff: 700, attack: 0.6, release: 1.5, gain: 0.2 })
const bass = vasynth({ wave: 'square', cutoff: 350, attack: 0.01, release: 0.15, gain: 0.65 })
const kk = kick()
const sn = snare()
const hh = hat({ decay: 0.04 })
const key = scales(4, 'F4:minor')
const prog = chords(8, key, 'I,VI,III,VII')
const bProg = prog.transpose(-24)
yield cast(lead, prog, seq(4, '0,2,4,3,2,4,0,_'), ctx)
yield cast(pad, prog, seq(8, '{0,2,4},~,~,~,{0,2,4},~,~,~'), ctx)
yield cast(bass, bProg, seq(4, '0,_,_,4,0,_,2,_'), ctx)
yield cast(kk, 'drums', seq(4, 'x...x.x.....x...'), ctx)
yield cast(sn, 'drums', seq(4, '....x.......x...'), ctx)
yield cast(hh, 'drums', seq(4, 'x.x.x.x.x.x.x.x.'), ctx)
}

Next

4. Sound Design → — shape timbre with synth parameters, FX, and LFO.