Sequencer

Showcase panel 21: silent until clicked: a Synth note (x sets the pitch), a Pluck, and a bar of Kick, Snare and HiHat in a Sequencer with its playhead and a Reverb on the master, with master.spectrum() and master.level() drawn live; needs kiln-gl[sound].

Showcase panel 21: silent until clicked: a Synth note (x sets the pitch), a Pluck, and a bar of Kick, Snare and HiHat in a Sequencer with its playhead and a Reverb on the master, with master.spectrum() and master.level() drawn live; needs kiln-gl[sound].
Try it live: run uv run python examples/showcase.py, then click panel 21, or see the showcase.

A drum machine plays in steps: a bar cut into 16 little slots, each one a hit or a rest. kiln.Sequencer is that machine. You give each instrument a row of steps, press start(), and it plays them in time, while draw() gets on with the pictures.

Why not just call note_on in update() when the frame count says so? Because frames are not a clock you can hear by. At 60 fps a frame lasts 16.7 ms, and a note started by a frame lands anywhere in it: a hat line played that way wobbles audibly. The sequencer works a little ahead of the music instead (a tenth of a second), and hands each note to the audio thread with the exact sample it starts on. Every note lands on the grid, to the sample, however busy the frame.

As in the Sound tutorial, the examples on this page are recorded: wait(seconds) lets that much music go by. You need the sound extra: pip install "kiln-gl[sound]".

A first beat

Two synths dressed up as drums: a kick (a sine whose pitch falls fast) and a hat (a burst of noise through a high-pass filter). A pattern is a string, one character per step: x is a hit and . a rest. With 16 steps to a bar (four to a beat), "x..." is a kick on every beat:

import kiln

kick = kiln.Synth()
kick.osc1_wave, kick.osc2_level = "sine", 0.0
kick.pitch_env_amount, kick.filter_decay, kick.filter_sustain = 30, 0.04, 0
kick.decay, kick.sustain, kick.volume = 0.3, 0, 1.0

hat = kiln.Synth()
hat.osc1_level, hat.osc2_level, hat.noise_level = 0, 0, 1
hat.filter_type, hat.cutoff = "hp", 8000
hat.decay, hat.sustain, hat.release, hat.volume = 0.04, 0, 0.03, 0.35

seq = kiln.Sequencer(bpm=122)
seq.pattern(kick, "x...x...x...x...")
seq.pattern(hat, "..x...x...x...x.")

with record():
    seq.start()
    wait(2.0)           # one bar at 122 beats per minute
    seq.stop()
    wait(0.3)

waveform of the sound

Spaces and | in a pattern are ignored, so "x... x... | x... x..." reads in beats. Each instrument’s steps loop on their own length: a 3-step pattern against a 16-step one is a polyrhythm.

Accents and swing

A capital X is an accent, a louder hit. And swing pushes every second step of a beat late, by a share of a step (the beat itself never moves): 0.2 is a light shuffle, about 0.33 a triplet feel. Swing is what makes a hat line groove instead of tick:

seq = kiln.Sequencer(bpm=122, swing=0.25)
seq.pattern(kick, "x...x...x...x...")
seq.pattern(hat, "xxXx xxXx xxXx xxXX")

with record():
    seq.start()
    wait(2.0)
    seq.stop()
    wait(0.3)

waveform of the sound

bpm and swing can change while it plays; the change applies from the next step.

Notes

notes gives an instrument a line of pitches, one per step: a note name ("A1", "C#4", "Eb3"; A4 is 440 Hz), a MIDI number (45), None for a rest, or "-" to hold the note before it for one more step. Each note is let go after half of its last step; gate= sets that share, from short and plucky to fully legato (1.0):

bass = kiln.Synth(polyphony=1)
bass.cutoff, bass.resonance, bass.sub_level = 600, 0.5, 0.6
bass.filter_env_amount, bass.filter_decay, bass.filter_sustain = 2.5, 0.12, 0

seq = kiln.Sequencer(bpm=122)
seq.pattern(kick, "x...x...x...x...")
seq.notes(bass, [None, None, "A1", "A1", None, "A2", "A1", None,
                 None, None, "A1", "C2", "-", "-", "G1", None], gate=0.6)

with record():
    seq.start()
    wait(2.0)
    seq.stop()
    wait(0.3)

waveform of the sound

For pitched instruments, pattern plays every hit at one note: note="A1" sets it.

Chords

chords is notes for a progression. A step is a list of notes, None for a rest, or "-" to hold the chord before it. All the notes of a chord start on the same sample, and let go together:

pad = kiln.Synth(polyphony=8)
pad.osc2_level, pad.detune, pad.cutoff = 0.8, 8, 1800
pad.attack, pad.sustain, pad.release, pad.volume = 0.05, 0.8, 0.5, 0.35

seq = kiln.Sequencer(bpm=122)
seq.chords(pad, [["A3", "C4", "E4"], "-", "-", "-",
                 ["F3", "A3", "C4"], "-", "-", "-"], gate=0.9)

with record():
    seq.start()
    wait(2.0)
    seq.stop()
    wait(0.5)

waveform of the sound

A chord that ends rings out its release while the next one starts, so the instrument needs a voice for each: give it a polyphony of at least twice the biggest chord (the default 8 does for triads and four-note chords). With fewer voices the new chord takes the ones still releasing, and chords warns you.

Automation

Some changes take bars, not steps: a filter that opens slowly over a whole build-up. ramp(instrument, control, start, end, bars) moves a control in a straight line from the next bar line, and lands on end exactly on the bar line bars later. Here the bass filter opens over two bars:

seq = kiln.Sequencer(bpm=122)
seq.pattern(kick, "x...x...x...x...")
seq.notes(bass, ["A1", "A1", "A2", "A1"] * 4, gate=0.5)
seq.ramp(bass, "cutoff", 150, 3000, bars=2)

with record(view="spectrogram"):
    seq.start()
    wait(4.0)
    seq.stop()
    wait(0.3)

spectrogram of the sound

In the picture, the bass’s harmonics climb higher with every note as the filter opens over the two bars.

Moves on a bar

A build-up belongs on a particular bar, and at_bar puts it there. Bars count from 0 at start(), and the ramp starts on that bar’s first sample. You can ask for it at any time, in setup() or bars before, because the ramp is handed to the audio thread like a note, ahead of time; it does not matter which frame happens to land near the bar, or whether one is late. Here the filter stays shut for the first bar and opens through the second:

bass.cutoff = 150
seq = kiln.Sequencer(bpm=160)
seq.pattern(kick, "x...x...x...x...")
seq.notes(bass, ["A1", "A1", "A2", "A1"] * 4, gate=0.5)
seq.ramp(bass, "cutoff", 150, 3000, bars=1, at_bar=1)

with record():
    seq.start()
    wait(3.0)
    seq.stop()
    wait(0.3)

waveform of the sound

Several ramps on one control queue up in time order: a later one takes over from its own first bar, and the earlier one runs until then. So a whole arrangement of moves can be written down before start(), one line each.

Drawing on the beat

beat, bar and phase tell draw() where the music is: beat counts beats since start() (2.5 is halfway through the third), bar counts bars of four beats, and phase goes from 0 on each beat to just below 1 before the next. So 1 - phase is a pulse that jumps on every beat and falls away through it.

Live, these follow the sound as it reaches your speaker: the sequencer takes the device’s output delay off, so a shape drawn on the beat lands with the kick you hear.

class Pulse(kiln.App):
    def setup(self):
        self.seq = kiln.Sequencer(bpm=122)
        self.seq.pattern(kick, "x...x...x...x...")
        self.seq.pattern(hat, "..x...x...x...xX")
        self.seq.start()

    def draw(self):
        kiln.background(18, 16, 24)
        pulse = (1 - self.seq.phase) ** 3        # 1 on the beat, then fading
        kiln.fill(255, 107, 82)
        kiln.circle(240, 135, 40 + 50 * pulse)
        step = int(self.seq.beat * 4) % 16       # the 16th note being heard
        kiln.fill(255, 255, 255, 120)
        kiln.rect(48 + step * 24, 240, 20, 12)


record_visual(Pulse(), frames=150)

How it keeps time

A Sequencer plays inside a kiln sketch, or with offline.render. Live, Kiln’s frame loop asks the sequencer once a frame for every step due in the next 0.1 s, and those notes go to the audio thread with their sample time. A frame can be late by up to a tenth of a second and nothing moves. When rendering offline, as on this page, it is asked before every buffer instead. In a plain script, the REPL or a notebook, nothing asks it: it would play a tenth of a second and stop, so start() warns. Once started, it plays until stop() or until the sketch ends, even if you keep no reference to it, and it keeps time while the window is minimised.

If the main thread stalls for longer than that (a slow load, a long computation in update()), the steps whose time has passed are skipped, not played late in a burst; the music carries on in time from the next step. Changes you make (a new pattern, bpm, stop()) apply to the steps not yet scheduled, about 0.1 s ahead.

If one sequencer fails while scheduling its steps, Kiln warns, stops that sequencer (releasing its notes) and the others play on.

Try this

  • Give the hat "x.x.x.x.x.x.x.xX" and slide swing from 0 to 0.4 with the mouse.
  • Put an arpeggio in notes with 6 steps against the 16-step kick: it shifts against the bar every time round.
  • Draw one ring per kick that grows through the beat: radius 100 + 200 * phase.
  • examples/18_sequencer.py puts it all together: four instruments, a filter opening over eight bars, and a picture that pulses with them.