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.
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.
Parts list
17 parts, plus the jumper wires. They retail for about $35; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 8 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 4 × PotentiometerIn Halloween Prop Kit, Starter Projects Kit.
Kits that can build it
These 9 starter kits have every part in the circuit. Each kit's page lists what else it can build.
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 pin | Connects to |
|---|---|
| Pushbutton (1) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (2) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (3) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (4) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (5) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (6) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (7) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (8) pin 2a | Arduino Uno R3 pin GND |
| Piezo speaker pin 2 | Arduino Uno R3 pin GND |
| Pushbutton (8) pin 1a | Arduino Uno R3 pin 12 |
| Piezo speaker pin 1 | Arduino Uno R3 pin 10 |
| Pushbutton (7) pin 1a | Arduino Uno R3 pin 9 |
| Pushbutton (6) pin 1a | Arduino Uno R3 pin 8 |
| Pushbutton (5) pin 1a | Arduino Uno R3 pin 7 |
| Resistor, 220 Ω pin 1 | Arduino Uno R3 pin 6 |
| Pushbutton (4) pin 1a | Arduino Uno R3 pin 5 |
| Pushbutton (3) pin 1a | Arduino Uno R3 pin 4 |
| Pushbutton (2) pin 1a | Arduino Uno R3 pin 3 |
| Pushbutton (1) pin 1a | Arduino Uno R3 pin 2 |
| Potentiometer (1) pin 3 | Arduino Uno R3 pin 5V |
| Potentiometer (2) pin 3 | Arduino Uno R3 pin 5V |
| Potentiometer (3) pin 3 | Arduino Uno R3 pin 5V |
| Potentiometer (4) pin 3 | Arduino Uno R3 pin 5V |
| LED, green pin C | Arduino Uno R3 pin GND |
| Potentiometer (1) pin 1 | Arduino Uno R3 pin GND |
| Potentiometer (2) pin 1 | Arduino Uno R3 pin GND |
| Potentiometer (3) pin 1 | Arduino Uno R3 pin GND |
| Potentiometer (4) pin 1 | Arduino Uno R3 pin GND |
| Potentiometer (1) pin 2 | Arduino Uno R3 pin A0 |
| Potentiometer (2) pin 2 | Arduino Uno R3 pin A1 |
| Potentiometer (3) pin 2 | Arduino Uno R3 pin A2 |
| Potentiometer (4) pin 2 | Arduino Uno R3 pin A3 |
| LED, green pin A | Resistor, 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.
// 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 editorTroubleshooting
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.