Ollin

Composition

This page covers deciding what to play. Synthesis shapes the sound a note makes. The types here are the other half, and they decide which notes there are and when they sound. Add import OllinAudio beside import Ollin to reach them.

Every type here is a plain value. It has no clock, no engine, and no randomness of its own beyond a seed you give it. You read each type by step number, and one small counter turns musical time into step numbers. Because of that split, the same pattern runs from the sketch clock now and from a drum machine later, without changing a line.

import Ollin
import OllinAudio

final class Loop: Sketch {
    let synth = Synth(.pluck)
    let rhythm = Rhythm(5, in: 16)          // five strikes, evenly spread
    let scale = Scale(.minorPentatonic, root: "A3")
    let tempo: Tempo = 120
    var counter = StepCounter(perBeat: 4)

    override func draw() {
        background(.black)
        for step in counter.steps(upTo: tempo.beats(at: time)) {
            if rhythm[step] { synth.play(scale[step % 5], for: tempo.seconds(of: .eighth)) }
        }
    }
}

Contents

  • StepCounter - musical time in, step numbers out
  • Tempo and note lengths - seconds into beats, and a note's length back into seconds
  • Rhythm - a cycle of strikes, spread as evenly as the numbers allow
  • StepSequencer - a bar of steps with a velocity, a chance, and a ratchet each, and swing on the bar
  • Scale - whole numbers in, notes in key out
  • Chord - notes meant to sound together
  • Progression - a cycle of chords, built out of a key
  • Arpeggio - a chord played one note at a time
  • Arpeggiator - the notes held right now, played one at a time against the beat
  • MarkovChain - new elements that continue a sequence the way it was shown
  • Tuning - pitches described by ratios rather than by semitones
  • BeatFollower - the beat in what the sketch hears, so it can play along
  • Note - a pitch with its loudness and length attached
  • Putting it together
  • What is not here yet

StepCounter

You give it the current position of the music, in beats. It returns the steps that have gone by since the last call.

var counter = StepCounter(perBeat: 4)      // four steps to a beat

for step in counter.steps(upTo: beats) { … }

It returns a range rather than one step. Most frames pass no step at all, and the range is empty. A sixteenth at 120 BPM lasts seven and a half frames at 60 fps. A slow frame or a fast rate can pass several steps, and each one still has to be played. The first call always includes step 0, so a pattern starts on the downbeat.

The sketch decides where beats comes from. That is deliberate, and it is why the counter works with any clock. Use tempo.beats(at: time), with a Tempo, to run from the sketch clock, or clock.beats from a TempoClock to run from a drum machine's clock. A number you advance yourself works too. Nothing in this tier but the tempo knows what a second is.

There are two behaviors to know about:

  • Time going backwards, when a loop comes round or after a seek, makes the counter report the step it landed on. A repeating figure therefore starts again and carries on from there.
  • Time jumping a long way, after a stall or a window dragged onto another display, makes the counter skip ahead. The counter does not empty the whole pattern into one frame. maxCatchUp sets how far it catches up, sixteen steps by default.

reset(to:) moves the counter without reporting the steps in between.


Tempo and note lengths

A Tempo is beats per minute, with how many beats make a bar. It is the one value on this page that knows what a second is, so it does the two conversions everything else leaves out:

let tempo: Tempo = 104                  // or Tempo(104, beatsPerBar: 3)

tempo.beats(at: time)                   // seconds in, beats out: what a StepCounter wants
tempo.seconds(of: .eighth)              // a note length in, seconds out: what `play(for:)` wants
tempo.seconds(beats: 1.5)               // any count of beats
tempo.seconds(bars: 8)                  // eight bars, for a `loopDuration`
tempo.secondsPerBeat                    // 0.577 at 104
tempo.secondsPerBar                     // four of those at the default
tempo.beatsPerMinute                    // 104, as it was set
tempo.bars(at: time)                    // where the music is, in bars

A NoteLength is a length in beats, where a beat is a quarter note. The names are the ones on a stave, and two properties derive the rest:

LengthBeats
.whole4
.half2
.quarter1
.eighth0.5
.sixteenth0.25
.thirtySecond0.125
.quarter.dotted1.5, the dot after a note
.eighth.tripleta third, three in the time of two

A plain number stands in for either type, so tempo: 120 and length: 0.5 read as they always did, and NoteLength(beats: 1.1) holds a length no name covers. length.seconds(at: tempo) is the same conversion from the other side, and .quarter * 3 or .quarter + .eighth build a longer one.

Both are parameters. @Param(60...160) var tempo: Tempo = 104 is a slider in beats per minute. A MIDI knob or an OSC address binds to it the way it binds to a number. The beats per bar stay what the declaration gave them. @Param var length: NoteLength = .eighth is a menu of the named lengths.


Rhythm

A Rhythm is a cycle of steps, and each step is struck or silent. The quick way to make one is a count of strikes over a count of steps:

let rhythm = Rhythm(3, in: 8)       // x..x..x.
rhythm[step]                        // true on a strike; wraps, so any step works

The strikes are spread as evenly as whole steps allow, using Bjorklund's algorithm. When the counts divide, the result is the obvious even spacing. When they do not, the result is a rhythm that is already played in some musical tradition. Rhythm(3, in: 8) is the Cuban tresillo, Rhythm(5, in: 8) is the cinquillo, and Rhythm(4, in: 9) is the Turkish aksak. The named ones are available on the type:

.tresillo .cinquillo .bellPattern .bossaNova .samba .aksak .ruchenitza .yorkSamai .nawakhat .agsagSamai .fandango

Seven rows showing 2, 3, 4, 5, 7, 9, and 11 strikes spread over sixteen steps, with the gaps between strikes listed beside each row, and below them the tresillo, cinquillo, and bell pattern drawn as the shape between their strikes on a circle

You can also write a rhythm out as text, which is the better form when you already know the pattern:

let clave: Rhythm = "x..x..x...x.x..."

x, 1, and * are strikes. ., -, _, 0, and a space are rests. Rhythm(pattern:) is the form that returns nil rather than complaining, so use it for text that came from somewhere else.

To read one:

rhythm[step]whether that step is struck. Wraps in both directions
lengthhow many steps before it repeats
onsetCounthow many are struck
onsetsthe struck step numbers
intervalsthe gaps between strikes, counting the wrap
rotated(by:)the same cycle starting later
inverted()strikes and rests exchanged
descriptionwritten out, x and .

intervals is the compact way to compare two rhythms. The tresillo is [3, 3, 2] whichever step it starts on, so two rhythms that share their gaps but start in different places are close relatives. Several of the named ones are rotations. For example, .bellPattern is Rhythm(7, in: 12) started at its third strike.


StepSequencer

A StepSequencer is a bar of steps, each with its own note, loudness, and chance of playing, read out against the beat. It is the drum machine's grid.

var drums: StepSequencer = "36 . . 36 . . 36 . 38 . . 36 . 38 . ."
drums.swing = 0.58

override func draw() {
    let now = tempo.beats(at: time)
    let ahead = tempo.beats(at: time + deltaTime)
    synth.play(drums.events(upTo: ahead), tempo: tempo, from: now)
}

Write a bar out with one token per step: a note name (C3), a MIDI number (36), or . for a rest. init(pattern:) returns nil for a token it cannot read, and the literal form leaves an empty bar and says so once. StepSequencer(.tresillo, pitch: 36) puts one note on every strike of a rhythm, and StepSequencer(count: 16) is an empty bar to fill in.

Each step is a Step with a pitch (nil for a rest, which isRest reads), a velocity, a probability, and a ratchet. The subscript reads and writes them and wraps, so drums[17] is the second step of a sixteen-step bar:

drums[7] = StepSequencer.Step(42, velocity: 0.6, ratchet: 3)   // three strikes in the time of one
drums[13] = StepSequencer.Step(42, probability: 0.5)           // plays on half the bars

The settings on the bar as a whole:

SettingWhat it does
ratehow long one step lasts, a NoteLength. .sixteenth by default
swinghow far the second step of each pair leans late, 0...1. 0.5 is straight, and two thirds is the shuffle. The first step of a pair never moves
gatehow much of a step a note sounds for, as a fraction. Half is short and separate, and one runs the notes together
seedwhich bars the steps left to chance play on. The same seed plays the same bars the same way
maxCatchUpthe most steps to hand out in one frame, as on the StepCounter inside

events(upTo:) hands back a ScheduledNote for every note that falls before the beat you give it. A ratchet's strikes come back as separate notes on one step, and a step left to chance comes back or not. The chance is a pure function of the step and the seed. A bar replays the same way, and the sketch's own randomness is untouched.

Ask for the notes ahead of time. Hand events(upTo:) where the music will be at the end of the frame, and hand Synth.play(_:tempo:from:) where it is now. A note whose beat is still ahead then waits for it, to the sample. A swung offbeat lands on its own moment rather than on the frame that asked. Leave from: out and every note plays with its frame, which is where every other call on this page lands. That is fine for a straight pattern and not for a swung one. The counter reaches a swung step at its straight place, so a note played with its frame loses the lean.

reset(to:) moves the sequencer without playing the steps in between. nextStep is where it has got to, so nextStep % length is the playhead. Changing rate mid-bar carries on from where the music is.

ScheduledNote

What a sequencer or an arpeggiator hands back. It is a Note (pitch, velocity, and a length in beats), the beat it lands on, and the step that asked for it. Synth.play(_:tempo:from:) turns the beat into a wait. play(_:tempo:) on the note alone plays it at once.


Scale

A Scale is a set of pitches and a root they are measured from. It turns whole numbers into notes of that key, which is what keeps generated music sounding musical. You can pick a number any way you like, and the scale keeps it in key.

let scale = Scale(.minorPentatonic, root: "A3")
scale[0]       // the root
scale[5]       // an octave up, five notes along
scale[-1]      // the note below the root

Degrees run in both directions and past the ends of the scale, so a wandering number never leaves the key, however far it goes.

The named modes are: .major .dorian .phrygian .lydian .mixolydian .minor .locrian .harmonicMinor .melodicMinor .majorPentatonic .minorPentatonic .blues .wholeTone .octatonic .chromatic .hirajoshi .inSen .iwato. Scale(intervals:root:) takes a mode of your own, as semitones above the root.

Sometimes a pitch comes from something that is not music, like a mouse position or a measurement. Then snap(_:) moves it to the nearest note of the scale:

synth.play(scale.snap(Pitch(40 + mouseY / 12)))

degree(nearest:) answers the same question the other way round, returning the degree nearest a pitch. pitches(_:from:) returns a run of pitches.

chord(on:noteCount:spacing:) builds a chord out of the scale itself, by taking every other note:

scale.chord(on: 0)          // a triad on the root
scale.chord(on: 1)          // a triad on the second degree
scale.chord(on: 0, noteCount: 4) // four notes, so a seventh

On a major scale the first of those is a major chord and the second is a minor chord, from the same call. That is the point of building a chord out of a key. The quality follows from where you started rather than from a choice you made, so it changes when the key changes.

Left: a ladder of pentatonic scale rungs over a faint semitone grid, with a wandering numbered sequence of dots landing only on rungs. Right: seven triads built on the degrees of C major, each three stacked marks two rungs apart, colored by what fell out: major on I, IV, and V, minor on ii, iii, and vi, diminished on the seventh

Scale.Mode is a ParamOption, so a mode can be a parameter:

@Param var mode: Scale.Mode = .minorPentatonic

Chord

A Chord is a set of notes meant to sound together. It is written as a root and a shape of intervals stacked on that root.

let chord = Chord("C4", .minorSeventh)
synth.play(chord: chord.pitches, for: 2)

The qualities are: .major .minor .diminished .augmented .sus2 .sus4 .fifth .sixth .minorSixth .dominantSeventh .majorSeventh .minorSeventh .minorMajorSeventh .halfDiminishedSeventh .diminishedSeventh .addNine .ninth .majorNinth .minorNinth .eleventh .thirteenth. Chord.Quality is a ParamOption too.

A Chord is a root, a quality, and an inversion, and Chord.quality(_:) reads a quality back out of a written symbol. inverted(_:) moves notes from the bottom of the chord to the top. That is how one chord moves to the next without every part leaping. Inverting a chord all the way round brings it back an octave up. A negative inversion drops notes from the top to the bottom instead, which is how a close voicing is opened out. spread(over:) lays the chord across several octaves, because a chord packed inside one octave sounds crowded rather than rich.

Progression

A Progression is a cycle of chords, written as scale degrees rather than as chord names.

let changes = Progression("I vi IV V", in: Scale(.major, root: "C3"))

for step in counter.steps(upTo: time * 2) {
    synth.play(chord: changes.pitches(at: step), for: 1.8)
}

It uses degrees rather than names because degrees survive a change of key. A progression written as chord symbols instead holds them in writtenChords, since those chords carry their own qualities and the degrees go unused. I vi IV V is the same progression in every key. Written that way, the quality of each chord comes from the scale instead of being spelled out. So the same four numerals come out major in a major key and minor in a minor key, with nothing changed.

Two rows of four chord stacks. The top row, in C major, reads C major, A minor, F major, G major; the bottom row, the same numerals in C minor, reads C minor, G sharp major, F minor, G minor. Each stack shows the three notes the progression hands back, at their own pitches

The text is Roman numerals I to VII, separated by any other characters. Upper and lower case are both accepted and treated the same, because the key decides major or minor. Anything unreadable is skipped.

MemberWhat it gives
pitches(at:) / [step]the notes of the chord at a step, lowest first
degree(at:)which scale degree that chord is built on
root(at:)its root, for a bass line underneath
chord(at:)the Chord itself, for a progression written as symbols. Nil for one written as degrees
counthow many chords before it repeats
noteCount / spacingnoteCount is three or four, and a spacing of two degrees is the usual stack of thirds
transposed(by:) / rotated(by:)the same progression elsewhere, or starting elsewhere

The cycle wraps, so a step number can climb forever.

The named progressions each take a key to build in. They are .pop (I V vi IV), .fifties (I vi IV V), .twoFiveOne, .blues over twelve bars, .andalusian, and .circleOfFifths.

Wandering

let changes = Progression("I vi IV V ii V", in: key).wandering(32)

wandering(_:) builds a longer progression that only makes the moves the original already makes. The result sounds like the same music, but it is a different cycle. Underneath, it is a MarkovChain over the degrees. The chain is learned round the loop, so the last chord returning to the first counts as a move.

It is seeded per call rather than from the sketch's own randomness. So you can ask again for a progression you liked, and get the same one back. Adding one cannot shift anything else you were drawing at random.

Chord symbols

let chord: Chord = "F#m7"
let changes = Progression(symbols: "Dm7 G7 Cmaj7 Cmaj7")

Symbols are the other way to write changes down, and the one to use when the chords do not all come from one key. A symbol starts with a root like C, Bb, or F#, with an optional octave as in C3. An optional quality comes next, such as m7, maj7, dim7, sus4, m7b5, or add9. Each of those has several spellings, and any of them is accepted. An optional bass note comes last, after a slash, as in C/G, which inverts the chord until that note is lowest.

Chord(symbol:) returns nil for anything that is not a chord, so a typo is something you find out about. The literal form lands on C major instead and says so once, the same way a pitch name literal does.

The one ambiguous part of the notation is a trailing number. The 7 of Cmaj7 is part of a quality, and the 3 of Cm3 is an octave. The parser tries the whole suffix as a quality first. Only if that is not a quality does it read a trailing number as an octave.

The trade between the two forms is that degrees survive a change of key and symbols do not.

Examples/Audio/ChordSymbols draws the symbol form, and Examples/Audio/Changes draws the degree form. In the first, you type a chart into a text parameter. The example draws a card for each token, and it spells out the chord that is sounding.



Arpeggio

An Arpeggio is a chord played one note at a time, in a set order.

let arp = Arpeggio(Chord("A3", .minorSeventh), .upDown, octaves: 2)
synth.play(arp[step], for: 0.1)

Like a rhythm, you read it by step number, and it wraps. Reading it at the step, rather than counting strikes, gives each strike the note the figure holds at that step. A rhythm that strikes only some steps skips the notes in between, instead of walking through them one by one.

The patterns are: .up .down .upDown .downUp .asPlayed .converge .diverge .random.

.upDown and .downUp do not play either end note twice in a row, so the turn sounds like a turn and not a stutter. .asPlayed is the only pattern that keeps the notes in the order they were given. Every other pattern sorts the notes into a ladder, so .asPlayed is the only way to hear a voicing you arranged by hand. .random is seeded and is a pure function of the step. The same seed always gives the same sequence, and adding one cannot shift anything else the sketch does at random.

order returns one full pass of the pattern, so a sketch can draw the figure it is about to play. notes is the pool of notes, and spreadPitches is that pool spread over the octaves the arpeggio covers, lowest first.


Arpeggiator

An Arpeggiator is an Arpeggio that follows the notes as they change: whatever is held right now, played one note a step, against the beat. playing is the figure's notes at this moment, which is what is held, or the last chord while it is latched, and empty when it is silent.

var arp = Arpeggiator(.upDown, octaves: 2, rate: .sixteenth)

override func draw() {
    arp.notes = midi.heldNotes.map { Pitch(Double($0.note)) }
    let now = tempo.beats(at: time)
    synth.play(arp.events(upTo: tempo.beats(at: time + deltaTime)), tempo: tempo, from: now)
}

Set notes to the keys a controller reports each frame, or hold(_:) and release(_:) one at a time. The pattern, octaves, and seed are the arpeggio's own. rate, gate, and swing are the sequencer's, with the same meanings. figure is the Arpeggio it is playing right now, so a sketch can draw the ladder, and next is the note the next step will play.

Three rules decide what happens when the notes change:

  • A new chord after silence starts from its first note.
  • A note added or taken away while the others are held keeps the figure's place. The note joins the ladder where it belongs on the next pass, and nothing restarts.
  • latches keeps the last chord playing after every key goes up, until a new key starts a new chord. Off, silence follows the last release. Either way the count goes on underneath, so the next note lands on the grid.

events(upTo:) hands back ScheduledNotes the same way the sequencer does. It wants the same look-ahead: the notes up to the end of the frame, played from now.


MarkovChain

A MarkovChain learns from a sequence what tends to follow what. Then, when you ask it for elements, it gives you new ones with the same habits. It lives in the core, beside Tempo, so it needs no import OllinAudio, and it picks colors or lengths as readily as notes.

var melody = MarkovChain<Int>(seed: 4)
melody.learn([0, 2, 4, 2, 0, -3, 0, 4], loops: true)
melody.start(at: 0)

let degree = melody.next() ?? 0
synth.play(scale[degree])

The chain shows its work: vocabulary is every element it has seen, in the order it first saw them, context the run it will choose the next one from, and continuations(after:) what may follow a context with how likely each is, sorted from most likely down, since what it learned is a picture as much as a sound. It works over any hashable type, so degrees, Pitches, chord qualities, and step counts all fit. There is a one-line form too. MarkovChain(learning:order:seed:loops:) builds the chain and learns the sequence in one step, which is what the example at the end of this page uses.

order is how far back it looks. At order 1, each element is chosen from what followed the one before it. At order 2, it looks at the last two elements, which tracks the source more closely and invents less. When it has never seen the current context, it falls back to a shorter one. If that fails too, it falls back to how often each element appeared at all, so it always has an answer.

loops: true says the sequence repeats, so what follows the last element is the first. Use it for a repeating figure. Without it, the chain can walk off the end of what it was shown and then has to fall back.

What it knows can also be declared instead of shown. learn(_:after:times:) records that a context was followed by an element, a number of times. So a table of counts becomes a chain with no sequence behind it, whether you counted it off a picture or wrote it by hand. Each call counts as that many occurrences in a learned sequence would. The counts also land under every shorter tail of the context, so an unseen context backs off to them:

var paints = MarkovChain<String>(order: 2, seed: 3)
paints.learn("yellow", after: ["white", "black"], times: 24)
paints.learn("white", after: ["red", "black"], times: 17)

The Hiroshi Kawano recreations are built that way. Design declares three tables counted cell by cell off two of his 1964 designs and walks a grid with them. ArtificialMondrian declares two small chains of its own: one for the gaps between lines, and one for whether a line runs on through a crossing.

Learning the same phrase twice counts it twice, which is how you make one phrase more likely than another. continuations reads the learned counts back as probabilities, most likely first, so a sketch can draw the chain as well as play it:

for (degree, probability) in melody.continuations {
    drawRect(x, y, probability * 200, 12)
}

The walk is seeded and keeps its own random generator. reset() forgets where it is, keeps what it learned, and rewinds the randomness, so the same walk comes out again.

Tuning

A Tuning describes pitches by frequency ratios rather than by semitones.

let tuning = Tuning.just.rooted(at: "C3")
synth.play(tuning[degree])

Scale divides the octave into twelve semitones, because almost all the music a sketch is likely to make uses those twelve semitones. A Tuning does not assume that division. It has the same shape as a Scale, so a sketch that indexes degrees and snaps stray pitches works the same way with either type.

The reason to use one is that equal temperament is a compromise. It makes every key equally usable by making every interval except the octave slightly out of tune. A drone piece that never changes key gives up nothing by being tuned in whole number ratios. It gains intervals that lock together instead of beating.

MemberWhat it gives
tuning[degree] / pitch(_:)the pitch of a degree, running past both ends into the periods above and below
frequency(_:)the same in Hz
snap(_:)the nearest pitch in the tuning, for something decided by a mouse or a sensor
centshow far each degree sits above the root, so you can see what a tuning is doing
ratios / period / degreeCountwhat it is made of
rooted(at:)the same tuning somewhere else
NamedWhat it is
.equalTemperamenttwelve equal steps: the ordinary keyboard
.justfive limit just intonation, the major scale in whole number ratios
.pythagoreanstacked fifths: a pure fifth and a noticeably wide third
.quarterTonestwenty four equal steps
.nineteen / .thirtyOneequal divisions with sweeter thirds than twelve
.bohlenPiercethirteen equal steps up to three times the frequency, rather than an octave

Tuning.equal(_:period:root:) builds any equal division, and Tuning(ratios:) takes ratios of your own, folded into one period and deduplicated.

Bohlen-Pierce has no octave in it at all. Every listener is used to a doubled frequency, so a tuning without one sounds wrong at first. After you listen for a while, it stops sounding wrong and simply sounds strange. It works because odd harmonics still line up, so it suits sounds that have only odd harmonics. The blown tube is one such sound.

Examples/Audio/Tunings plays one triad through all seven tunings. A ladder places every degree by its cents against the equal-tempered grid, so you can see what moved as well as hear it.


BeatFollower

A BeatFollower follows the beat in whatever the sketch is listening to, so the sketch can play along.

let mic = AudioInput()
lazy var room = BeatFollower(mic)
var counter = StepCounter(perBeat: 2)

override func draw() {
    room.advance(to: time)
    for step in counter.steps(upTo: room.beats) {
        synth.play(scale[step % 5], for: 0.2)      // in time with the room
    }
}
MemberWhat it gives
advance(to:)reads what has been heard since the last call. Call it once a frame, handing it the sketch's clock
beatsthe current position of the music, in beats. Hand this to a StepCounter
tempobeats per minute, or zero until it has an estimate
isFollowingwhether it has heard enough to be worth following
steadiness0...1. One means machine-steady timing. A low number means a human player with uneven timing, or a detector that is unsure
rhythm(steps:perBeat:)the room's own pattern, as a Rhythm a sketch can play
reset()forget everything, for when the music changes

There are three things to know about it:

  • A tempo outside the range is folded into it. The common failure is hearing every eighth note as a beat. That reports twice the tempo for the same music. The follower halves or doubles the tempo until it lands in 60...160, which stops that.
  • A missed beat costs nothing. The tempo is the middle value of the recent gaps rather than their average. One missed beat doubles one gap, which moves an average but does not move a middle value.
  • It hears note onsets, not the beat a drummer would tap. It follows a steady loop well, and it follows rubato playing badly. steadiness tells you how much to trust it.

BeatEngine is the same mechanism with no audio input. hearBeat(at:) says a beat was heard at a time in seconds, memory is how far back it weighs what it has heard, and musical time comes out. That makes it testable, and it is there for a sketch that decides for itself when a beat happened.

Examples/Audio/PlayAlong runs a BeatFollower on the microphone. Once isFollowing comes on, it plays a note on every beat the follower reports. A generated pulse stands in until the microphone is allowed, so the whole mechanism is visible before any permission is granted.



Note

A Note is a pitch with its loudness and length attached, for when all three were decided together.

let note = Note(scale[3], velocity: 0.9, length: .eighth)   // half a beat
synth.play(note, tempo: 120)

Its length is a NoteLength, in beats, because nothing in this tier knows how fast the music is going. The Tempo is supplied when the note is played, and note.seconds(at: tempo) is the number the synth is handed.

Most sketches never need it, because the composition types deal in pitches and step numbers, and synth.play(pitch, velocity:for:) takes the loudness and length directly.


Putting it together

The pieces are meant to be combined. A rhythm decides when, a scale decides which notes, a chain or an arpeggio decides what comes next, and the counter joins them to time:

let bass = Synth(.bass)
let chords = Synth(.pluck)

let pulse = Rhythm(3, in: 16)
let figure = Rhythm(5, in: 16)
let key = Scale(.minorPentatonic, root: "A2")
var melody = MarkovChain(learning: [0, 2, 4, 2, 0, -3], seed: 4, loops: true)
let tempo: Tempo = 104
var counter = StepCounter(perBeat: 4)

override func draw() {
    for step in counter.steps(upTo: tempo.beats(at: time)) {
        if pulse[step] {
            bass.play(key[melody.next() ?? 0], for: 0.34)
        }
        if figure[step] {
            let arp = Arpeggio(Chord(key[0].transposed(by: 12), .minorSeventh), .upDown, octaves: 2)
            chords.play(key.snap(arp[step]), velocity: 0.55, for: 0.22)
        }
    }
}

Examples/Audio/Generative draws that sketch as three Euclidean rings on one step count, and its parameters change what is played while it runs.


What is not here yet

This section lists what is missing, so you can plan around it rather than go looking:

  • No arrangement. A StepSequencer holds a bar and an Arpeggiator holds a figure, and both read the beat you hand them. Nothing holds a whole piece with sections and plays it back. The arrangement is the sketch's own code, and that is deliberate. A piece with a clock of its own would stop this tier from running on somebody else's clock.
  • A Scale is still twelve-tone. Scale(intervals:) takes whole semitones. Anything else is a Tuning, which is a separate type rather than a setting on a scale. Chords are not built out of a tuning.
  • Following a beat is not following a bar. A BeatFollower knows where the beat is but not where the downbeat is. So a pattern locks to the pulse rather than to the phrase.

Credits

The Euclidean rhythms come from Bjorklund's algorithm for spacing pulses in a spallation neutron source. Godfried Toussaint connected that algorithm to musical timelines, and the named rhythms and the pattern-and-gap notation are from his paper. The implementation here is written from the description of the construction, not translated from anyone's code. The tunings are historical. Five limit just intonation and the Pythagorean stack of fifths are older than notation. Bohlen-Pierce is named for Heinz Bohlen, Kees van Prooijen, and John R. Pierce, who each arrived at it separately. Full credit is in ATTRIBUTION.md.


Synthesis covers the sound a note makes. MIDI files covers a piece read from or written to disk. MIDI covers running on an external clock.

↑↓ move ↩ open