Built-in project · Arduino Uno R3

Drum machine on an Arduino Uno

An 8-step drum machine on an Uno: buttons toggle steps for a kick, a snare and a hi-hat, a knob sets the tempo, and a pixel strip shows the pattern and the beat. The whole build, an Arduino Uno and 13 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 40 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $37. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Sound
  • Lights
  • Controllers
  • Reading a button
  • Choosing and reading a resistor
  • Difficulty: Intermediate
  • Time: About 40 minutes
  • Board: Arduino Uno R3
  • Parts: 15

How it works

An 8-step drum machine on an Uno: buttons toggle steps for a kick, a snare and a hi-hat, a knob sets the tempo, and a pixel strip shows the pattern and the beat.

Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Piezo speaker, Resistor, 330 Ω, Potentiometer, Addressable RGB LEDs. pinMode sets STEP_PIN[i] as an input with its pull-up on. digitalRead reads whether pin is HIGH or LOW. analogRead measures the voltage on A1 as a number. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Pushbutton (1) pin 2aArduino Uno R3 pin GND
Pushbutton (2) pin 2aArduino Uno R3 pin GND
Pushbutton (3) pin 2aArduino Uno R3 pin GND
Pushbutton (4) pin 2aArduino Uno R3 pin GND
Pushbutton (5) pin 2aArduino Uno R3 pin GND
Pushbutton (6) pin 2aArduino Uno R3 pin GND
Pushbutton (7) pin 2aArduino Uno R3 pin GND
Pushbutton (8) pin 2aArduino Uno R3 pin GND
Piezo speaker pin 2Arduino Uno R3 pin GND
Potentiometer pin 1Arduino Uno R3 pin GND
Pushbutton (9) pin 2aArduino Uno R3 pin GND
Pushbutton (9) pin 1aArduino Uno R3 pin 12
Resistor, 330 Ω pin 1Arduino Uno R3 pin 11
Piezo speaker pin 1Arduino Uno R3 pin 10
Pushbutton (8) pin 1aArduino Uno R3 pin 9
Pushbutton (7) pin 1aArduino Uno R3 pin 8
Pushbutton (6) pin 1aArduino Uno R3 pin 7
Pushbutton (5) pin 1aArduino Uno R3 pin 6
Pushbutton (4) pin 1aArduino Uno R3 pin 5
Pushbutton (3) pin 1aArduino Uno R3 pin 4
Pushbutton (2) pin 1aArduino Uno R3 pin 3
Pushbutton (1) pin 1aArduino Uno R3 pin 2
Potentiometer pin 3Arduino Uno R3 pin 5V
Addressable RGB LEDs pin VDDArduino Uno R3 pin 5V
Addressable RGB LEDs pin VSSArduino Uno R3 pin GND
Potentiometer pin 2Arduino Uno R3 pin A0
Addressable RGB LEDs pin DINResistor, 330 Ω pin 2

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Drum machine: eight steps, three drums, one piezo.
//
// Wiring:
//   step buttons  pins 2 to 9, each to GND (steps 1 to 8)
//   drum button   pin 12 to GND: picks which drum the step buttons edit
//   tempo knob    potentiometer wiper -> A0, ends to 5 V and GND
//   piezo         pin 10 -> piezo -> GND
//   pixels        8 WS2812Bs, DIN <- 330 ohms <- pin 11
//
// A drum machine is a loop of steps. Each step is one eighth note: if its
// button is on for a drum, that drum plays when the beat gets there. The
// pixels show the drum you are editing, and a white dot runs along them with
// the beat.
//
// Steady tempo. The sketch never waits with delay(). It keeps the time the next
// step is due and adds one step length to it each time, so a step that runs a
// little late does not push the rest of the song late with it. The knob
// changes the step length, and the new tempo starts on the very next step.
//
// One piezo can only make one pitch at a time, so each drum is a pitch that
// moves while it sounds, the way the real ones do:
//   kick   a low note that falls in steps (210 Hz down to 58 Hz)
//   snare  a burst of random pitches, which is what noise is
//   hat    a very short burst of random high pitches
// If two drums land on one step, the kick wins, then the snare.
//
// Things to try. Turn the knob. Press the drum button, then add steps. Change
// the starting pattern in KICK, SNARE and HAT: each bit is one step.

#include <Adafruit_NeoPixel.h>

const uint8_t STEPS = 8;
const uint8_t STEP_PIN[STEPS] = {2, 3, 4, 5, 6, 7, 8, 9};
const uint8_t PIEZO_PIN = 10;
const uint8_t PIXEL_PIN = 11;
const uint8_t DRUM_PIN = 12;

enum Drum : uint8_t { KICK, SNARE, HAT, DRUMS };
const char *const DRUM_NAME[DRUMS] = {"kick", "snare", "hat"};

// bit 0 is step 1. Kick on 1 and 5, snare on 3 and 7, hat on the offbeats.
uint8_t pattern[DRUMS] = {0b00010001, 0b01000100, 0b10101010};

Adafruit_NeoPixel pixels(STEPS, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

uint8_t editing = KICK;
uint8_t step = 0;      // the step to play next
uint8_t playhead = 0;  // the step that played last
uint32_t nextStep = 0;
uint16_t bpm = 120;

// the drum that is sounding now, when it started, and the noise slot it is in
int8_t voice = -1;
uint32_t voiceStart = 0;
uint8_t slot = 255;
uint16_t lastHz = 0;
uint16_t noiseHz = 0;

// button edges: the last reading of each button and when it last changed
bool wasDown[STEPS + 1];
uint32_t changedAt[STEPS + 1];

static void startVoice(int8_t drum)
{
  voice = drum;
  voiceStart = millis();
  slot = 255;
}

static void stopVoice()
{
  voice = -1;
  lastHz = 0;
  noTone(PIEZO_PIN);
}

// A new random pitch every `slotMs`: that is what makes it sound like noise.
static uint16_t noise(uint32_t age, uint8_t slotMs, uint16_t low, uint16_t spread)
{
  uint8_t now = age / slotMs;
  if (now != slot) {
    slot = now;
    noiseHz = low + random(0, spread);
  }
  return noiseHz;
}

// The kick is a few notes in a row, each lower than the last. Each one has to
// last longer than half a wave of its own pitch, or the pin never gets to
// swing: tone() starts the timer over every time it is called.
const uint16_t KICK_HZ[] = {210, 150, 110, 80, 58};
const uint8_t KICK_MS[] = {14, 18, 24, 30, 40};
const uint8_t KICK_STAGES = sizeof(KICK_HZ) / sizeof(KICK_HZ[0]);

static void sing(uint16_t hz)
{
  // only on a change: calling tone() every pass would restart the wave
  if (hz != lastHz) {
    lastHz = hz;
    tone(PIEZO_PIN, hz);
  }
}

// Called every pass through loop(): moves the pitch of the drum that is
// sounding, and switches it off when its time is up.
static void updateVoice()
{
  if (voice < 0) {
    return;
  }
  uint32_t age = millis() - voiceStart;
  if (voice == KICK) {
    uint32_t until = 0;
    for (uint8_t s = 0; s < KICK_STAGES; s++) {
      until += KICK_MS[s];
      if (age < until) {
        sing(KICK_HZ[s]);
        return;
      }
    }
    stopVoice();
  } else if (voice == SNARE) {
    if (age >= 90) {
      stopVoice();
      return;
    }
    sing(noise(age, 6, 180, 1400));
  } else {
    if (age >= 30) {
      stopVoice();
      return;
    }
    sing(noise(age, 3, 3000, 2000));
  }
}

static void drawPixels()
{
  static const uint8_t COLOR[DRUMS][3] = {{200, 20, 20}, {220, 140, 0}, {0, 150, 190}};
  for (uint8_t i = 0; i < STEPS; i++) {
    if (i == playhead) {
      pixels.setPixelColor(i, 255, 255, 255);
    } else if (pattern[editing] & (1 << i)) {
      pixels.setPixelColor(i, COLOR[editing][0] / 3, COLOR[editing][1] / 3, COLOR[editing][2] / 3);
    } else {
      pixels.setPixelColor(i, 0, 0, 0);
    }
  }
  pixels.show();
}

static void playStep()
{
  playhead = step;
  int8_t hit = -1;
  for (uint8_t d = 0; d < DRUMS; d++) {
    if (pattern[d] & (1 << step)) {
      hit = d;
      break;
    }
  }
  Serial.print("Step ");
  Serial.print(step + 1);
  Serial.print(" ");
  Serial.print(hit < 0 ? "-" : DRUM_NAME[hit]);
  Serial.print("  ");
  Serial.print(bpm);
  Serial.println(" BPM");
  if (hit >= 0) {
    startVoice(hit);
  }
  drawPixels();
}

// True once for each press of button `index` (0 to 7 are the steps, 8 is the
// drum button), with 20 ms of debounce.
static bool pressed(uint8_t index, uint8_t pin)
{
  bool down = digitalRead(pin) == LOW;
  uint32_t now = millis();
  if (down != wasDown[index] && now - changedAt[index] > 20) {
    wasDown[index] = down;
    changedAt[index] = now;
    return down;
  }
  return false;
}

void setup()
{
  Serial.begin(9600);
  for (uint8_t i = 0; i < STEPS; i++) {
    pinMode(STEP_PIN[i], INPUT_PULLUP);
  }
  pinMode(DRUM_PIN, INPUT_PULLUP);
  pinMode(PIEZO_PIN, OUTPUT);
  pixels.begin();
  pixels.setBrightness(120);
  randomSeed(analogRead(A1));
  Serial.println("Drum machine ready: 8 steps, kick snare hat");
  nextStep = millis() + 250;
}

void loop()
{
  uint32_t now = millis();

  // the knob: 60 to 180 beats per minute
  bpm = 60 + (uint16_t)((analogRead(A0) * 120UL) / 1023);

  if (now >= nextStep) {
    // One step is an eighth note: half a beat.
    nextStep += 30000UL / bpm;
    playStep();
    step = (step + 1) % STEPS;
  }

  updateVoice();

  for (uint8_t i = 0; i < STEPS; i++) {
    if (pressed(i, STEP_PIN[i])) {
      pattern[editing] ^= (1 << i);
      Serial.print(DRUM_NAME[editing]);
      Serial.print(" step ");
      Serial.print(i + 1);
      Serial.println((pattern[editing] & (1 << i)) ? " on" : " off");
      drawPixels();
    }
  }
  if (pressed(STEPS, DRUM_PIN)) {
    editing = (editing + 1) % DRUMS;
    Serial.print("Editing ");
    Serial.println(DRUM_NAME[editing]);
    drawPixels();
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup()
{
  Serial.begin(9600);
  for (uint8_t i = 0; i < STEPS; i++) {
    pinMode(STEP_PIN[i], INPUT_PULLUP);
  }
  pinMode(DRUM_PIN, INPUT_PULLUP);
  pinMode(PIEZO_PIN, OUTPUT);
  pixels.begin();
  pixels.setBrightness(120);
  randomSeed(analogRead(A1));
  Serial.println("Drum machine ready: 8 steps, kick snare hat");
  nextStep = millis() + 250;
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop()
{
  uint32_t now = millis();

  // the knob: 60 to 180 beats per minute
  bpm = 60 + (uint16_t)((analogRead(A0) * 120UL) / 1023);

  if (now >= nextStep) {
    // One step is an eighth note: half a beat.
    nextStep += 30000UL / bpm;
    playStep();
    step = (step + 1) % STEPS;
  }

  updateVoice();

  for (uint8_t i = 0; i < STEPS; i++) {
    if (pressed(i, STEP_PIN[i])) {
      pattern[editing] ^= (1 << i);
      Serial.print(DRUM_NAME[editing]);
      Serial.print(" step ");
      Serial.print(i + 1);
      Serial.println((pattern[editing] & (1 << i)) ? " on" : " off");
      drawPixels();
    }
  }
  if (pressed(STEPS, DRUM_PIN)) {
    editing = (editing + 1) % DRUMS;
  // ... 5 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
    A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.
  • Sound is very quiet or the pin seems overloaded. Piezo speaker
    Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The reading jumps around or only covers part of the range. Potentiometer
    Wire the two outer legs to 5 V and ground and the middle leg (the wiper) to an analog pin. Using the wrong two legs gives a dead or partial range.
  • Turning the knob does nothing, or the value is stuck at 0 or 1023. Potentiometer
    Check that the wiper goes to an analog input (A0 to A5 on an Uno), not a digital pin. Make sure both ends are connected, since a floating end gives noise.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.