Learning Goals
3 minBy the end of this lesson you can:
- Store a beat pattern as a list of booleans and toggle values with a mouse click.
- Use
clock.schedule_intervalto step through the pattern at a fixed tempo. - Play the correct sound for each active step and highlight the current beat on screen.
Warm-Up · Lists of Booleans
5 minA beat pattern is just a list of True/False values. Predict the output:
pattern = [True, False, True, True, False, False, True, False] active = [i for i, on in enumerate(pattern) if on] print(active)
Show the answer
Output
[0, 2, 3, 6]
Only the indices where the value is True are kept. In the drum machine these are the beats that will make a sound.
New Concept · Steppers with clock.schedule_interval
12 minA drum machine has a step — a counter that advances on every beat. Think of it like a metronome: tick, tick, tick — and each tick moves to the next slot in the pattern.
The step counter
step = 0 STEPS = 8 def advance(): global step step = (step + 1) % STEPS # wrap back to 0 after the last step clock.schedule_interval(advance, 0.25) # 4 beats per second = 240 BPM
Using % STEPS ensures the counter wraps back to 0 automatically — no if needed.
Playing the current beat
def advance(): global step step = (step + 1) % STEPS if pattern[step]: sounds.kick.play()
One line check: if the current slot is True, play the sound. No sound otherwise.
Toggling a pad
def on_mouse_down(pos, button): for i, rect in enumerate(pad_rects): if rect.collidepoint(pos): pattern[i] = not pattern[i] # flip True ↔ False
not pattern[i] flips a True to False and vice versa — a one-liner toggle.
Worked Example · 8-Step Drum Machine
12 minWei Jie builds a single-row, 8-step drum machine. You will need sounds/kick.wav in your project. Save as drum_machine.py:
# drum_machine.py — part 1: layout import pgzrun WIDTH = 520 HEIGHT = 200 TITLE = "Drum Machine" STEPS = 8 PAD_W = 50 PAD_H = 50 PAD_GAP = 10 MARGIN_X = 30 MARGIN_Y = 70 step = 0 pattern = [False] * STEPS pad_rects = [ Rect((MARGIN_X + i * (PAD_W + PAD_GAP), MARGIN_Y), (PAD_W, PAD_H)) for i in range(STEPS) ]
# drum_machine.py — part 2: logic and drawing def advance(): global step step = (step + 1) % STEPS if pattern[step]: sounds.kick.play() def on_mouse_down(pos, button): for i, rect in enumerate(pad_rects): if rect.collidepoint(pos): pattern[i] = not pattern[i] def draw(): screen.fill("black") screen.draw.text("DRUM MACHINE", topleft=(MARGIN_X, 16), fontsize=22, color="white") for i, rect in enumerate(pad_rects): if i == step: colour = "yellow" # current beat highlight elif pattern[i]: colour = "tomato" # active pad else: colour = "dimgrey" # inactive pad screen.draw.filled_rect(rect, colour) screen.draw.rect(rect, "white") clock.schedule_interval(advance, 0.25) pgzrun.go()
Try It Yourself
13 minChange STEPS = 16 and adjust PAD_W = 22 and PAD_GAP = 6 so all 16 pads fit in the 520-pixel window. Update MARGIN_X = 20. Nothing else changes.
Hint
STEPS = 16 PAD_W = 22 PAD_GAP = 6 MARGIN_X = 20 # total width = 16 * (22 + 6) - 6 + 2*20 = 16*28 - 6 + 40 = 482 — fits!
Create a second list hat_pattern = [False] * STEPS and a second row of pads 70 pixels below the first. In advance(), if hat_pattern[step] is True, play sounds.hihat.play(). Use a different colour (e.g. "dodgerblue") for the hi-hat row.
Hint
hat_rects = [ Rect((MARGIN_X + i * (PAD_W + PAD_GAP), MARGIN_Y + PAD_H + 20), (PAD_W, PAD_H)) for i in range(STEPS) ]
Mini-Challenge · Tempo Slider
8 minRight now the tempo is fixed at 0.25 seconds per step. Combine today's clock skills with the keyboard input you learnt in PZ-14: pressing Up speeds the drum machine up; pressing Down slows it down. You will need to unschedule the old interval and schedule a new one with the updated tempo.
It works if…
pressing Up makes the beat noticeably faster; Down makes it slower
Show one possible solution
tempo = 0.25 # seconds per step def on_key_down(key): global tempo if key == keys.UP: tempo = max(0.05, round(tempo - 0.05, 2)) elif key == keys.DOWN: tempo = min(1.0, round(tempo + 0.05, 2)) clock.unschedule(advance) clock.schedule_interval(advance, tempo)
Call clock.unschedule(advance) before rescheduling, otherwise you end up with two overlapping intervals and a very noisy drum machine.
Recap
3 minA drum machine is a list of booleans and a step counter. clock.schedule_intervaladvances the step on every beat. If the current slot is True, play the sound. Clicking a pad flips its boolean with not pattern[i]. The draw loop colours each pad based on its state: active, current, or inactive.
Vocabulary Card
- clock.schedule_interval(fn, s)
- Calls
fnevery s seconds, forever. Used here as the drum machine's metronome. - clock.unschedule(fn)
- Cancels a previously scheduled repeating call. Required before changing tempo.
- step counter
- An integer that advances one slot per beat and wraps back to 0 using
% STEPS. - toggle
- Flipping a boolean with
not value:TruebecomesFalseand vice versa.
Homework
4 minAdd a Clear All button to your drum machine — a rectangle at the bottom. Clicking it sets every slot in every pattern back to False. Also display the current tempo (in steps/sec, not seconds/step) at the top of the screen. Save as drum_machine_v2.py and bring a screenshot to the next class.
Sample · drum_machine_v2.py
# drum_machine_v2.py — clear button + tempo display # (add to your existing drum_machine.py) clear_btn = Rect((MARGIN_X, MARGIN_Y + PAD_H + 20), (100, 30)) def on_mouse_down(pos, button): global pattern for i, rect in enumerate(pad_rects): if rect.collidepoint(pos): pattern[i] = not pattern[i] if clear_btn.collidepoint(pos): pattern = [False] * STEPS def draw(): screen.fill("black") bps = round(1.0 / tempo, 1) screen.draw.text(f"DRUM MACHINE {bps} steps/sec", topleft=(MARGIN_X, 16), fontsize=20, color="white") # ... pad drawing unchanged ... screen.draw.filled_rect(clear_btn, "slategrey") screen.draw.text("Clear All", center=clear_btn.center, fontsize=16, color="white")
Steps-per-second is simply 1 / tempo. The clear button resets the whole list in one assignment.