Sequencer
a drum machine's grid: three lanes of sixteen steps with a swing slider, the hat's offbeats left to chance, two ratchets, and an arpeggiator climbing the chord of the bar under them. Click a cell to turn it on or off (StepSequencer, Arpeggiator, play(_:tempo:from:))
- Run
swift run --package-path Examples Example-Audio-Sequencer- Imports
OllinAudio- Source
- Examples/Audio/Sequencer/Sketch.swift
import Ollin
import OllinAudio
/// A drum machine's grid, and an arpeggiator running over it.
///
/// Three lanes of a `StepSequencer`, sixteen steps each: a kick, a snare, and
/// a hat. Every step holds its own loudness, a chance of playing, and a
/// ratchet, and the whole bar swings. Click a cell to turn it on or off. Under
/// the drums an `Arpeggiator` climbs the chord of the bar, one note a step,
/// and the chord changes every bar from a `Progression`.
///
/// Every note is asked for before its time. The sketch reads the patterns up
/// to where the music will be at the end of the frame, and the synth waits
/// the rest, so a swung offbeat or a ratchet's strikes land on their own
/// samples rather than on the frame that asked. Nothing here has a clock of
/// its own: the beat comes from the sketch clock through one `Tempo`.
@main
final class Sequencer: Sketch {
@Param(60 ... 160, icon: "metronome", group: "Time") var tempo: Tempo = 112
@Param(0.5 ... 0.75, icon: "arrow.right.to.line", group: "Time") var swing = 0.58
@Param(0 ... 1, icon: "die.face.3", group: "Drums") var chance = 0.6
@Param(icon: "arrow.up.arrow.down", group: "Arpeggio") var figure: Arpeggio.Pattern = .upDown
@Param(1 ... 3, icon: "square.stack", group: "Arpeggio") var octaves = 2
@Param(icon: "music.note", group: "Arpeggio") var rate: NoteLength = .sixteenth
/// One bar, in steps. Sixteen sixteenths.
let steps = 16
/// A drum is a struck body; a snare and a hat are bursts of noise, each
/// through its own band.
let kick = Synth(.drum, polyphony: 4)
let snare = Synth(Voice(
waveform: .noise,
envelope: Envelope(attack: 0.001, decay: 0.12, sustain: 0, release: 0.05),
filter: Voice.Filter(mode: .bandpass, cutoff: 1800, resonance: 0.4),
gain: 0.7
), polyphony: 4)
let hat = Synth(Voice(
waveform: .noise,
envelope: Envelope(attack: 0.001, decay: 0.04, sustain: 0, release: 0.03),
filter: Voice.Filter(mode: .highpass, cutoff: 7000, resonance: 0.3),
gain: 0.45
), polyphony: 8)
let keys = Synth(.pluck, polyphony: 12)
/// The three lanes, written out. The hat is filled in by hand in `setup`
/// so its offbeats can carry a chance and two steps a ratchet.
var lanes: [StepSequencer] = [
"40 . . . 40 . . . 40 . . 40 . . 40 .",
". . . . 62 . . . . . . . 62 . . 62",
StepSequencer(count: 16),
]
let names = ["kick", "snare", "hat"]
let colors = [Color(hex: 0xF2B134), Color(hex: 0xE56B6F), Color(hex: 0x67C9D9)]
var arp = Arpeggiator(.upDown, octaves: 2)
let homeKey = Scale(.minor, root: "A2")
lazy var changes = Progression("i VI III VII", in: homeKey)
var bar = -1
/// Where each strike lands on the picture, and on which beat.
var flashes: [(lane: Int, step: Int, beat: Double)] = []
var played: [(pitch: Pitch, beat: Double)] = []
override func setup() {
noStroke()
kick.gain = 0.9
keys.gain = 0.4
keys.reverb = Reverb(.room, mix: 0.2)
lanes[0].gate = 0.6
lanes[1].gate = 0.5
lanes[2].gate = 0.25
for step in 0..<steps {
let offbeat = step % 2 == 1
lanes[2][step] = StepSequencer.Step(Pitch(96), velocity: offbeat ? 0.45 : 0.8,
probability: offbeat ? chance : 1)
}
lanes[2][7] = StepSequencer.Step(Pitch(96), velocity: 0.7, ratchet: 3)
lanes[2][15] = StepSequencer.Step(Pitch(96), velocity: 0.7, ratchet: 4)
}
override func draw() {
background(Color(white: 0.05))
// Where the music is now, and where it will be when this frame ends.
// The patterns are read up to the second, and every note is played
// from the first, so each lands on its own sample.
let now = tempo.beats(at: time)
let ahead = tempo.beats(at: time + deltaTime)
for index in lanes.indices { lanes[index].swing = swing }
for step in stride(from: 1, to: steps, by: 2) where lanes[2][step].ratchet == 1 {
lanes[2][step].probability = chance
}
arp.pattern = figure
arp.octaves = octaves
arp.rate = rate
arp.swing = swing
// A new bar is a new chord. The held notes change and the figure
// keeps its place in the ladder.
let thisBar = Int((now / 4).rounded(.down))
if thisBar != bar {
bar = thisBar
arp.notes = changes.pitches(at: bar)
}
for (index, synth) in [kick, snare, hat].enumerated() {
let notes = lanes[index].events(upTo: ahead)
synth.play(notes, tempo: tempo, from: now)
for note in notes { flashes.append((index, note.step % steps, note.beat)) }
}
let figureNotes = arp.events(upTo: ahead)
keys.play(figureNotes, tempo: tempo, from: now)
for note in figureNotes { played.append((note.pitch, note.beat)) }
drawGrid(now: now)
drawLadder(now: now)
drawCaption("\(tempo) · swing \(Int((swing * 100).rounded())) · \(figure.optionLabel) · \(rate)", edge: .top)
drawCaption("Click a cell to turn it on or off. The hat's offbeats play by chance; two of its steps are ratchets.")
}
// MARK: The grid
private var gridLeft: Double { Double(width) * 0.08 }
private var gridTop: Double { Double(height) * 0.14 }
private var cellWidth: Double { Double(width) * 0.84 / Double(steps) }
private var cellHeight: Double { Double(height) * 0.1 }
private func drawGrid(now: Double) {
let seconds = tempo.secondsPerBeat
flashes.removeAll { (now - $0.beat) * seconds > 0.4 }
for (lane, sequencer) in lanes.enumerated() {
let top = gridTop + Double(lane) * (cellHeight + 12 * scale)
let color = colors[lane]
for step in 0..<steps {
let entry = sequencer[step]
let x = gridLeft + Double(step) * cellWidth
let inset = 3 * scale
if entry.isRest {
fill(Color(white: step % 4 == 0 ? 0.16 : 0.11))
drawRect(x + inset, top + inset, cellWidth - 2 * inset, cellHeight - 2 * inset, cornerRadius: 4 * scale)
continue
}
// A struck step: as tall as its loudness, as solid as its
// chance, and split into as many bars as its ratchet.
let height = cellHeight * (0.35 + 0.65 * entry.velocity)
let bars = entry.ratchet
let gap = 2 * scale
let barWidth = (cellWidth - 2 * inset - Double(bars - 1) * gap) / Double(bars)
fill(color.withAlpha(0.3 + 0.7 * entry.probability))
for bar in 0..<bars {
drawRect(x + inset + Double(bar) * (barWidth + gap), top + cellHeight - height,
barWidth, height - inset, cornerRadius: 3 * scale)
}
}
fill(Color(white: 0.6))
textFont(BitmapFont.builtIn)
textSize(16 * scale)
textAlign(.right)
drawText(names[lane], gridLeft - 14 * scale, top + cellHeight * 0.62)
}
// The strikes as they land, brightest on their own beat.
blendMode(.add)
for flash in flashes {
let age = max(0, (now - flash.beat) * seconds) / 0.4
guard age <= 1 else { continue }
let top = gridTop + Double(flash.lane) * (cellHeight + 12 * scale)
let x = gridLeft + Double(flash.step) * cellWidth
fill(Color.white.withAlpha((1 - age) * 0.35))
drawRect(x, top, cellWidth, cellHeight, cornerRadius: 4 * scale)
}
blendMode(.normal)
// The playhead sweeps evenly. The swing is in when the notes sound,
// not in where the bar is.
let position = now * 4 - Double(Int((now * 4 / Double(steps)).rounded(.down)) * steps)
let x = gridLeft + position * cellWidth
stroke(Color(white: 0.85))
strokeWeight(1.5 * scale)
drawLine(x, gridTop - 10 * scale, x, gridTop + 3 * cellHeight + 2 * 12 * scale + 10 * scale)
noStroke()
}
// MARK: The ladder
private func drawLadder(now: Double) {
let seconds = tempo.secondsPerBeat
played.removeAll { (now - $0.beat) * seconds > 2.5 }
let top = gridTop + 3 * cellHeight + 2 * 12 * scale + 50 * scale
let bottom = Double(height) * 0.86
let order = arp.figure.order
guard !order.isEmpty else { return }
let low = (order.min()?.midi ?? 48) - 2
let high = (order.max()?.midi ?? 72) + 2
func y(_ pitch: Pitch) -> Double {
bottom - (pitch.midi - low) / max(1, high - low) * (bottom - top)
}
// The figure as the arpeggiator will play it, one column a step.
let columns = Double(order.count)
let columnWidth = Double(width) * 0.84 / columns
stroke(Color(white: 0.18))
strokeWeight(1 * scale)
for pitch in Set(order) {
drawLine(gridLeft, y(pitch), gridLeft + Double(width) * 0.84, y(pitch))
}
noStroke()
for (index, pitch) in order.enumerated() {
let x = gridLeft + (Double(index) + 0.5) * columnWidth
let isNext = arp.next == pitch && index == order.firstIndex(of: pitch)
fill(isNext ? Color.white : Color(hex: 0xB59CF2).withAlpha(0.7))
drawCircle(x, y(pitch), (isNext ? 7 : 5) * scale)
}
// What was just played, drifting left as it ages.
blendMode(.add)
for note in played {
let age = (now - note.beat) * seconds / 2.5
guard age >= 0, age <= 1 else { continue }
let x = gridLeft + Double(width) * 0.84 * (1 - age)
fill(Color(hex: 0xB59CF2).withAlpha((1 - age) * 0.9))
drawCircle(x, y(note.pitch), (3 + (1 - age) * 6) * scale)
}
blendMode(.normal)
fill(Color(white: 0.6))
textFont(BitmapFont.builtIn)
textSize(16 * scale)
textAlign(.right)
drawText("keys", gridLeft - 14 * scale, (top + bottom) / 2)
textAlign(.left)
let chord = changes.pitches(at: bar).map { "\($0)" }.joined(separator: " ")
drawText(chord, gridLeft, bottom + 26 * scale)
}
// MARK: The mouse
override func mousePressed() {
let column = Int(((mouse.x - gridLeft) / cellWidth).rounded(.down))
guard column >= 0, column < steps else { return }
for lane in lanes.indices {
let top = gridTop + Double(lane) * (cellHeight + 12 * scale)
guard mouse.y >= top, mouse.y < top + cellHeight else { continue }
let pitches: [Pitch] = [40, 62, 96]
if lanes[lane][column].isRest {
lanes[lane][column] = StepSequencer.Step(pitches[lane], velocity: 0.8)
} else {
lanes[lane][column] = .rest
}
}
}
}