Built-in project · Arduino Uno R3

ADSR synth on an Arduino Uno

A one-voice synth on an Uno: eight keys and four knobs for attack, decay, sustain and release. The piezo plays the note and an LED draws the envelope. The whole build, an Arduino Uno and 15 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 45 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $35. 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 45 minutes
  • Board: Arduino Uno R3
  • Parts: 17

How it works

A one-voice synth on an Uno: eight keys and four knobs for attack, decay, sustain and release. The piezo plays the note and an LED draws the envelope.

Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Piezo speaker, Resistor, 220 Ω, LED, green, Potentiometer. pinMode sets KEY_PIN[k] as an input with its pull-up on. digitalRead reads whether KEY_PIN[k] is HIGH or LOW. analogRead measures the voltage on A0 as a number. analogWrite sends a PWM signal out on LED_PIN. Serial.begin opens the serial port at 9600 baud.

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
Pushbutton (8) pin 1aArduino Uno R3 pin 12
Piezo speaker pin 1Arduino Uno R3 pin 10
Pushbutton (7) pin 1aArduino Uno R3 pin 9
Pushbutton (6) pin 1aArduino Uno R3 pin 8
Pushbutton (5) pin 1aArduino Uno R3 pin 7
Resistor, 220 Ω pin 1Arduino 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 (1) pin 3Arduino Uno R3 pin 5V
Potentiometer (2) pin 3Arduino Uno R3 pin 5V
Potentiometer (3) pin 3Arduino Uno R3 pin 5V
Potentiometer (4) pin 3Arduino Uno R3 pin 5V
LED, green pin CArduino Uno R3 pin GND
Potentiometer (1) pin 1Arduino Uno R3 pin GND
Potentiometer (2) pin 1Arduino Uno R3 pin GND
Potentiometer (3) pin 1Arduino Uno R3 pin GND
Potentiometer (4) pin 1Arduino Uno R3 pin GND
Potentiometer (1) pin 2Arduino Uno R3 pin A0
Potentiometer (2) pin 2Arduino Uno R3 pin A1
Potentiometer (3) pin 2Arduino Uno R3 pin A2
Potentiometer (4) pin 2Arduino Uno R3 pin A3
LED, green pin AResistor, 220 Ω 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
// ADSR synth: eight keys, four knobs, a piezo and an LED that draws the envelope.
//
// Wiring:
//   keys      pins 2, 3, 4, 5, 7, 8, 9, 12, each to GND (C D E F G A B C)
//   piezo     pin 10 -> piezo -> GND
//   envelope  pin 6 -> 220 ohms -> LED -> GND
//   knobs     four potentiometers, wipers to A0 A1 A2 A3, ends to 5 V and GND:
//             attack, decay, sustain, release
//
// When a key goes down, the sound does not jump to full volume and stop dead
// when you let go. It follows an envelope with four parts:
//
//   attack   how long it takes to rise from silence to the peak
//   decay    how long it takes to fall from the peak to the sustain level
//   sustain  the level it holds while the key stays down
//   release  how long it takes to fade out after the key comes up
//
// A piezo can only play at one volume, so the LED shows the envelope as
// brightness (PWM on pin 6) and the piezo plays the note while the envelope is
// above a small threshold. Turn sustain all the way down and every note dies
// away by itself, like a plucked string. Turn it up and the note holds, like an
// organ. Turn release up and the note rings on after you let go.
//
// The envelope is a little state machine. Every pass through loop() adds or
// subtracts a small step from the level, scaled by how many milliseconds have
// passed since the last pass, so the times stay right however fast loop() runs.
//
// Things to try. Attack at the minimum and sustain at zero: a pluck. Attack at
// the maximum: a pad that swells in. Press a second key while the first is still
// held and watch the envelope restart.

const uint8_t KEYS = 8;
const uint8_t KEY_PIN[KEYS] = {2, 3, 4, 5, 7, 8, 9, 12};
const uint16_t NOTE_HZ[KEYS] = {262, 294, 330, 349, 392, 440, 494, 523};
const char *const NOTE_NAME[KEYS] = {"C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"};
const uint8_t PIEZO_PIN = 10;
const uint8_t LED_PIN = 6;

// the note stops sounding once the envelope falls below this (of 1000)
const float AUDIBLE = 40;

enum Stage : uint8_t { IDLE, ATTACK, DECAY, SUSTAIN, RELEASE };
const char *const STAGE_NAME[] = {"idle", "attack", "decay", "sustain", "release"};

Stage stage = IDLE;
float level = 0;  // 0 to 1000
int8_t held = -1;
uint32_t lastMs = 0;
bool sounding = false;

// the four settings, read from the knobs
float attackMs, decayMs, sustainLevel, releaseMs;

static void readKnobs()
{
  attackMs = 5 + analogRead(A0) * (595.0f / 1023);        // 5 to 600 ms
  decayMs = 20 + analogRead(A1) * (580.0f / 1023);        // 20 to 600 ms
  sustainLevel = analogRead(A2) * (1000.0f / 1023);       // 0 to 1000
  releaseMs = 20 + analogRead(A3) * (1480.0f / 1023);     // 20 to 1500 ms
}

static void enter(Stage next)
{
  stage = next;
  Serial.print(STAGE_NAME[next]);
  Serial.print(" ");
  Serial.println((int)level);
}

// The key that is down, or -1. The highest key wins.
static int8_t keyRaw()
{
  for (int8_t k = KEYS - 1; k >= 0; k--) {
    if (digitalRead(KEY_PIN[k]) == LOW) {
      return k;
    }
  }
  return -1;
}

// A switch bounces: for a few milliseconds after a press it flickers open and
// shut. A reading only counts once it has held still for DEBOUNCE_MS, or one
// press would restart the envelope several times.
const uint8_t DEBOUNCE_MS = 15;
int8_t candidate = -1;
uint32_t candidateSince = 0;
int8_t stableKey = -1;

static int8_t keyDown()
{
  int8_t raw = keyRaw();
  uint32_t now = millis();
  if (raw != candidate) {
    candidate = raw;
    candidateSince = now;
  } else if (now - candidateSince >= DEBOUNCE_MS) {
    stableKey = raw;
  }
  return stableKey;
}

static void stepEnvelope(float dt)
{
  switch (stage) {
    case ATTACK:
      level += 1000.0f * dt / attackMs;
      if (level >= 1000) {
        level = 1000;
        enter(DECAY);
      }
      break;
    case DECAY:
      level -= (1000.0f - sustainLevel) * dt / decayMs;
      if (level <= sustainLevel) {
        level = sustainLevel;
        enter(SUSTAIN);
      }
      break;
    case SUSTAIN:
      level = sustainLevel;
      break;
    case RELEASE:
      level -= 1000.0f * dt / releaseMs;
      if (level <= 0) {
        level = 0;
        enter(IDLE);
      }
      break;
    default:
      break;
  }
}

void setup()
{
  Serial.begin(9600);
  for (uint8_t k = 0; k < KEYS; k++) {
    pinMode(KEY_PIN[k], INPUT_PULLUP);
  }
  pinMode(PIEZO_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  readKnobs();
  lastMs = millis();
  Serial.println("ADSR synth ready");
}

void loop()
{
  uint32_t now = millis();
  float dt = (float)(now - lastMs);
  lastMs = now;

  readKnobs();

  int8_t key = keyDown();
  if (key != held) {
    if (key >= 0) {
      // a new key: the envelope starts again from where it is
      Serial.print("Note ");
      Serial.print(NOTE_NAME[key]);
      Serial.print(" ");
      Serial.print(NOTE_HZ[key]);
      Serial.println(" Hz");
      held = key;
      enter(ATTACK);
      tone(PIEZO_PIN, NOTE_HZ[key]);
      sounding = true;
    } else {
      held = -1;
      enter(RELEASE);
    }
  }

  stepEnvelope(dt);

  // the LED draws the envelope; the piezo plays while it is high enough to hear
  analogWrite(LED_PIN, (int)(level * 255.0f / 1000.0f));
  if (sounding && level < AUDIBLE && held < 0) {
    noTone(PIEZO_PIN);
    sounding = false;
  } else if (sounding && level < AUDIBLE && stage == DECAY) {
    // decayed away to nothing while the key is still down: a pluck
    noTone(PIEZO_PIN);
    sounding = false;
  } else if (!sounding && held >= 0 && level >= AUDIBLE) {
    tone(PIEZO_PIN, NOTE_HZ[held]);
    sounding = true;
  }
}

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 k = 0; k < KEYS; k++) {
    pinMode(KEY_PIN[k], INPUT_PULLUP);
  }
  pinMode(PIEZO_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  readKnobs();
  lastMs = millis();
  Serial.println("ADSR synth ready");
}

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();
  float dt = (float)(now - lastMs);
  lastMs = now;

  readKnobs();

  int8_t key = keyDown();
  if (key != held) {
    if (key >= 0) {
      // a new key: the envelope starts again from where it is
      Serial.print("Note ");
      Serial.print(NOTE_NAME[key]);
      Serial.print(" ");
      Serial.print(NOTE_HZ[key]);
      Serial.println(" Hz");
      held = key;
      enter(ATTACK);
      tone(PIEZO_PIN, NOTE_HZ[key]);
      sounding = true;
    } else {
      held = -1;
      enter(RELEASE);
    }
  }

  stepEnvelope(dt);
  // ... 15 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 LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.
  • A blue or white LED stays dark on 3.3 V. LED
    Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.

Change it and keep it

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